wip(tools): targeted xunit runner helper
Sprint SPRINT_20260419_028_Tools_targeted_xunit_runner_workflow (TEST-RUNNER-001 DOING — sprint remains active). - scripts/test-targeted-xunit.ps1: rebuild-and-invoke xUnit v3 in-process runner directly so targeted filters work under Microsoft Testing Platform (dotnet test --filter is ignored there). - Register sprint file in implplan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
254
scripts/test-targeted-xunit.ps1
Normal file
254
scripts/test-targeted-xunit.ps1
Normal file
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env pwsh
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Runs targeted xUnit v3 tests by executing the compiled test DLL directly.
|
||||
|
||||
.DESCRIPTION
|
||||
Some Stella Ops test projects run under Microsoft Testing Platform with the
|
||||
xUnit v3 in-process runner. In that mode, `dotnet test --filter` may be
|
||||
ignored, which makes targeted verification misleading. This helper rebuilds
|
||||
the requested test project if needed, resolves the output test assembly, and
|
||||
invokes `dotnet exec <test-dll>` with xUnit's native filter arguments.
|
||||
|
||||
.PARAMETER Project
|
||||
Path to the test `.csproj`.
|
||||
|
||||
.PARAMETER Method
|
||||
Fully qualified test method to run. May be specified more than once.
|
||||
|
||||
.PARAMETER Class
|
||||
Fully qualified test class to run. May be specified more than once.
|
||||
|
||||
.PARAMETER Namespace
|
||||
Namespace filter to run. May be specified more than once.
|
||||
|
||||
.PARAMETER Trait
|
||||
xUnit trait filter in `name=value` form. May be specified more than once.
|
||||
|
||||
.PARAMETER QueryFilter
|
||||
xUnit query filter string. Do not combine with `Method`, `Class`, `Namespace`,
|
||||
or `Trait`.
|
||||
|
||||
.PARAMETER Framework
|
||||
Target framework. Required when the project uses `TargetFrameworks`.
|
||||
|
||||
.PARAMETER Configuration
|
||||
Build configuration. Defaults to `Debug`.
|
||||
|
||||
.PARAMETER Reporter
|
||||
xUnit reporter to use. Defaults to `verbose`.
|
||||
|
||||
.PARAMETER SkipBuild
|
||||
Skip the project build and run the existing test DLL.
|
||||
|
||||
.PARAMETER BuildProjectReferences
|
||||
Whether to build project references. Defaults to `$false` to keep targeted
|
||||
verification fast and avoid broad graph rebuilds when outputs are already
|
||||
available.
|
||||
|
||||
.PARAMETER AdditionalRunnerArgument
|
||||
Additional raw xUnit runner arguments appended after the generated filters.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Project,
|
||||
|
||||
[string[]]$Method = @(),
|
||||
[string[]]$Class = @(),
|
||||
[string[]]$Namespace = @(),
|
||||
[string[]]$Trait = @(),
|
||||
[string]$QueryFilter,
|
||||
[string]$Framework,
|
||||
[string]$Configuration = 'Debug',
|
||||
[string]$Reporter = 'verbose',
|
||||
[switch]$SkipBuild,
|
||||
[bool]$BuildProjectReferences = $false,
|
||||
[string[]]$AdditionalRunnerArgument = @()
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Resolve-RepoRoot {
|
||||
return Split-Path -Parent $PSScriptRoot
|
||||
}
|
||||
|
||||
function Resolve-ProjectPath {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$InputPath,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$RepoRoot
|
||||
)
|
||||
|
||||
if ([System.IO.Path]::IsPathRooted($InputPath)) {
|
||||
return (Resolve-Path -LiteralPath $InputPath).ProviderPath
|
||||
}
|
||||
|
||||
return (Resolve-Path -LiteralPath (Join-Path $RepoRoot $InputPath)).ProviderPath
|
||||
}
|
||||
|
||||
function Get-ProjectMetadata {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ProjectPath
|
||||
)
|
||||
|
||||
[xml]$projectXml = Get-Content -LiteralPath $ProjectPath -Raw
|
||||
$propertyGroups = @($projectXml.Project.PropertyGroup)
|
||||
|
||||
$assemblyName = $null
|
||||
$targetFramework = $null
|
||||
$targetFrameworks = $null
|
||||
|
||||
foreach ($group in $propertyGroups) {
|
||||
if (-not $assemblyName -and $group.AssemblyName) {
|
||||
$assemblyName = $group.AssemblyName.Trim()
|
||||
}
|
||||
|
||||
if (-not $targetFramework -and $group.TargetFramework) {
|
||||
$targetFramework = $group.TargetFramework.Trim()
|
||||
}
|
||||
|
||||
if (-not $targetFrameworks -and $group.TargetFrameworks) {
|
||||
$targetFrameworks = $group.TargetFrameworks.Trim()
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $assemblyName) {
|
||||
$assemblyName = [System.IO.Path]::GetFileNameWithoutExtension($ProjectPath)
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
AssemblyName = $assemblyName
|
||||
TargetFramework = $targetFramework
|
||||
TargetFrameworks = $targetFrameworks
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-Framework {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
$Metadata,
|
||||
[string]$RequestedFramework
|
||||
)
|
||||
|
||||
if ($RequestedFramework) {
|
||||
return $RequestedFramework
|
||||
}
|
||||
|
||||
if ($Metadata.TargetFramework) {
|
||||
return $Metadata.TargetFramework
|
||||
}
|
||||
|
||||
if ($Metadata.TargetFrameworks) {
|
||||
$frameworks = @($Metadata.TargetFrameworks.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries))
|
||||
if ($frameworks.Count -eq 1) {
|
||||
return $frameworks[0].Trim()
|
||||
}
|
||||
|
||||
throw "Project declares multiple target frameworks ($($Metadata.TargetFrameworks)). Specify -Framework explicitly."
|
||||
}
|
||||
|
||||
throw "Could not determine a target framework from '$ProjectPath'. Specify -Framework explicitly."
|
||||
}
|
||||
|
||||
function Assert-FilterShape {
|
||||
param(
|
||||
[string[]]$MethodFilters,
|
||||
[string[]]$ClassFilters,
|
||||
[string[]]$NamespaceFilters,
|
||||
[string[]]$TraitFilters,
|
||||
[string]$Query
|
||||
)
|
||||
|
||||
$hasSimpleFilters = $MethodFilters.Count -gt 0 -or
|
||||
$ClassFilters.Count -gt 0 -or
|
||||
$NamespaceFilters.Count -gt 0 -or
|
||||
$TraitFilters.Count -gt 0
|
||||
|
||||
if (-not $hasSimpleFilters -and [string]::IsNullOrWhiteSpace($Query)) {
|
||||
throw 'Specify at least one filter via -Method, -Class, -Namespace, -Trait, or -QueryFilter.'
|
||||
}
|
||||
|
||||
if ($hasSimpleFilters -and -not [string]::IsNullOrWhiteSpace($Query)) {
|
||||
throw 'Do not mix -QueryFilter with -Method/-Class/-Namespace/-Trait.'
|
||||
}
|
||||
}
|
||||
|
||||
$repoRoot = Resolve-RepoRoot
|
||||
$projectPath = Resolve-ProjectPath -InputPath $Project -RepoRoot $repoRoot
|
||||
$metadata = Get-ProjectMetadata -ProjectPath $projectPath
|
||||
$resolvedFramework = Resolve-Framework -Metadata $metadata -RequestedFramework $Framework
|
||||
|
||||
Assert-FilterShape -MethodFilters $Method -ClassFilters $Class -NamespaceFilters $Namespace -TraitFilters $Trait -Query $QueryFilter
|
||||
|
||||
$projectDirectory = Split-Path -Parent $projectPath
|
||||
$assemblyPath = Join-Path $projectDirectory "bin\$Configuration\$resolvedFramework\$($metadata.AssemblyName).dll"
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
$buildArguments = @(
|
||||
'build',
|
||||
$projectPath,
|
||||
'--no-restore',
|
||||
'--nologo',
|
||||
'--disable-build-servers',
|
||||
'-v', 'minimal',
|
||||
'-c', $Configuration,
|
||||
'-p:BuildProjectReferences=' + ($BuildProjectReferences ? 'true' : 'false'),
|
||||
'/m:1'
|
||||
)
|
||||
|
||||
if ($resolvedFramework) {
|
||||
$buildArguments += @('-f', $resolvedFramework)
|
||||
}
|
||||
|
||||
Write-Host "Building $projectPath ($resolvedFramework, $Configuration)..."
|
||||
& dotnet @buildArguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $assemblyPath)) {
|
||||
throw "Compiled test assembly not found: $assemblyPath"
|
||||
}
|
||||
|
||||
$runnerArguments = @(
|
||||
'exec',
|
||||
$assemblyPath,
|
||||
'-noLogo'
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($Reporter)) {
|
||||
$runnerArguments += @('-reporter', $Reporter)
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($QueryFilter)) {
|
||||
$runnerArguments += @('-filter', $QueryFilter)
|
||||
}
|
||||
|
||||
foreach ($value in $Method) {
|
||||
$runnerArguments += @('-method', $value)
|
||||
}
|
||||
|
||||
foreach ($value in $Class) {
|
||||
$runnerArguments += @('-class', $value)
|
||||
}
|
||||
|
||||
foreach ($value in $Namespace) {
|
||||
$runnerArguments += @('-namespace', $value)
|
||||
}
|
||||
|
||||
foreach ($value in $Trait) {
|
||||
$runnerArguments += @('-trait', $value)
|
||||
}
|
||||
|
||||
foreach ($value in $AdditionalRunnerArgument) {
|
||||
$runnerArguments += $value
|
||||
}
|
||||
|
||||
Write-Host "Running targeted xUnit assembly $assemblyPath"
|
||||
& dotnet @runnerArguments
|
||||
exit $LASTEXITCODE
|
||||
Reference in New Issue
Block a user