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>
330 lines
9.7 KiB
PowerShell
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
|