#!/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, then executes the targeted test runner using the safest path for that project: - `dotnet exec ` for standard library-style test assemblies - `dotnet run --project -- ...` for ASP.NET host tests that rely on `Microsoft.AspNetCore.Mvc.Testing` loader semantics .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 Build project references as part of the scoped `dotnet build`. Use this for WebApplicationFactory-style tests and other hosts that need transitive runtime assemblies copied into the output folder. .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, [switch]$BuildProjectReferences, [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 $usesMvcTesting = $false 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() } } $packageReferences = @($projectXml.SelectNodes('/Project/ItemGroup/PackageReference')) foreach ($packageReference in $packageReferences) { $packageId = $packageReference.GetAttribute('Include') if ([string]::IsNullOrWhiteSpace($packageId)) { $packageId = $packageReference.GetAttribute('Update') } if ([string]::Equals($packageId, 'Microsoft.AspNetCore.Mvc.Testing', [System.StringComparison]::OrdinalIgnoreCase)) { $usesMvcTesting = $true break } } if (-not $assemblyName) { $assemblyName = [System.IO.Path]::GetFileNameWithoutExtension($ProjectPath) } return [pscustomobject]@{ AssemblyName = $assemblyName TargetFramework = $targetFramework TargetFrameworks = $targetFrameworks UsesMvcTesting = $usesMvcTesting } } 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) { $shouldBuildProjectReferences = $BuildProjectReferences.IsPresent -or $metadata.UsesMvcTesting $buildProjectReferencesValue = if ($shouldBuildProjectReferences) { '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 = if ($metadata.UsesMvcTesting) { $arguments = @( 'run', '--no-build', '--project', $projectPath, '-c', $Configuration ) if ($resolvedFramework) { $arguments += @('-f', $resolvedFramework) } $arguments += '--' $arguments } else { @( '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 } if ($metadata.UsesMvcTesting) { Write-Host "Running targeted xUnit project runner for $projectPath" } else { Write-Host "Running targeted xUnit assembly $assemblyPath" } & dotnet @runnerArguments exit $LASTEXITCODE