Files
git.stella-ops.org/scripts/test-targeted-xunit.ps1
master ad77711ac2 feat(services): pre-session batch across workflow / notifier / notify / cli / libs / misc
Bundled pre-session work from multiple sprints and background refactors:
- src/Workflow: new workflow renderer work (ElkSharp-related)
- src/Notifier: SPRINT_20260420_013 retire orphan digest scheduler path
- src/Notify: SPRINT_20260422_001 notify compat leftovers (non-DEPRECATE)
- src/__Libraries: shared library updates (audit emission surfaces + misc)
- src/Cli: crypto commands + db group + tests
- src/AirGap, src/BinaryIndex, src/AdvisoryAI, src/Replay, src/Findings:
  small module updates
- scripts/test-targeted-xunit.ps1: xunit test runner tweaks

File-level granularity preserved for blame.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:06:20 +03:00

330 lines
9.7 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, then executes the targeted test runner
using the safest path for that project:
- `dotnet exec <test-dll>` for standard library-style test assemblies
- `dotnet run --project <test-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