#!/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 ` 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) { $assemblyNameNode = $group.SelectSingleNode('AssemblyName') if (-not $assemblyName -and $assemblyNameNode -and -not [string]::IsNullOrWhiteSpace($assemblyNameNode.InnerText)) { $assemblyName = $assemblyNameNode.InnerText.Trim() } $targetFrameworkNode = $group.SelectSingleNode('TargetFramework') if (-not $targetFramework -and $targetFrameworkNode -and -not [string]::IsNullOrWhiteSpace($targetFrameworkNode.InnerText)) { $targetFramework = $targetFrameworkNode.InnerText.Trim() } $targetFrameworksNode = $group.SelectSingleNode('TargetFrameworks') if (-not $targetFrameworks -and $targetFrameworksNode -and -not [string]::IsNullOrWhiteSpace($targetFrameworksNode.InnerText)) { $targetFrameworks = $targetFrameworksNode.InnerText.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, [Parameter(Mandatory)] [string]$ProjectPath ) 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 = ([bool]$MethodFilters) -or ([bool]$ClassFilters) -or ([bool]$NamespaceFilters) -or ([bool]$TraitFilters) 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.' } } function Expand-FilterValues { param( [string[]]$Values ) $expanded = New-Object System.Collections.Generic.List[string] foreach ($value in $Values) { if ([string]::IsNullOrWhiteSpace($value)) { continue } foreach ($segment in $value.Split(',', [System.StringSplitOptions]::RemoveEmptyEntries)) { $trimmed = $segment.Trim() if (-not [string]::IsNullOrWhiteSpace($trimmed)) { $expanded.Add($trimmed) } } } return @($expanded) } $repoRoot = Resolve-RepoRoot $projectPath = Resolve-ProjectPath -InputPath $Project -RepoRoot $repoRoot $metadata = Get-ProjectMetadata -ProjectPath $projectPath $resolvedFramework = Resolve-Framework -Metadata $metadata -RequestedFramework $Framework -ProjectPath $projectPath $Method = Expand-FilterValues -Values $Method $Class = Expand-FilterValues -Values $Class $Namespace = Expand-FilterValues -Values $Namespace $Trait = Expand-FilterValues -Values $Trait 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) { $buildProjectReferencesValue = if ($BuildProjectReferences) { 'true' } else { 'false' } $buildArguments = @( 'build', $projectPath, '--no-restore', '--nologo', '--disable-build-servers', '-v', 'minimal', '-c', $Configuration, ('-p:BuildProjectReferences=' + $buildProjectReferencesValue), '/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