Files
git.stella-ops.org/scripts/test-targeted-xunit.ps1
master 333d894690 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>
2026-04-19 14:48:23 +03:00

255 lines
7.0 KiB
PowerShell

#!/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