From 8637b8889cb125bd944665743160b5cb17f75275 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 08:23:08 -0600 Subject: [PATCH 01/22] Update mcr.microsoft.com/dotnet/sdk:10.0.401 Docker digest to 35d4030 (575) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 7fbd7f50..e06f26ac 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # Refer to https://hub.docker.com/_/microsoft-dotnet-sdk for available versions -FROM mcr.microsoft.com/dotnet/sdk:10.0.401@sha256:2fa828c68761b1b8c23d7662dc134421b9d3b59fe1425fdbc80804e390cdb24d +FROM mcr.microsoft.com/dotnet/sdk:10.0.401@sha256:35d40304542c8689331f8cab17c65926cdf48fe711e289321d71924b230a7d29 # Installing mono makes `dotnet test` work without errors even for net472. # But installing it takes a long time, so it's excluded by default. From d59c238d5246caa4d87230d6835451a843007c5a Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 22 Sep 2026 08:41:08 -0600 Subject: [PATCH 02/22] Avoid re-installing .NET SDK or runtimes --- tools/Install-DotNetSdk.ps1 | 200 +++++++++++++++++++++++++----------- 1 file changed, 141 insertions(+), 59 deletions(-) diff --git a/tools/Install-DotNetSdk.ps1 b/tools/Install-DotNetSdk.ps1 index 2621dda1..f4fcdaae 100644 --- a/tools/Install-DotNetSdk.ps1 +++ b/tools/Install-DotNetSdk.ps1 @@ -198,6 +198,66 @@ Function Install-DotNet($Version, $Architecture, [ValidateSet('Sdk','Runtime','W } } +Function Test-DotNetRuntimeInstalled( + $DotNetRoot, + [ValidateSet('Microsoft.NETCore.App','Microsoft.WindowsDesktop.App','Microsoft.AspNetCore.App')] + [string]$Runtime, + [Version]$Version +) { + if (!$DotNetRoot) { + return $false + } + + $runtimeRoot = Join-Path $DotNetRoot "shared\$Runtime" + if (!(Test-Path -LiteralPath $runtimeRoot)) { + return $false + } + + foreach ($runtimeDirectory in Get-ChildItem -LiteralPath $runtimeRoot -Directory) { + $installedVersion = $null + if ([Version]::TryParse($runtimeDirectory.Name, [ref]$installedVersion) -and + $Version.Major -eq $installedVersion.Major -and + $Version.Minor -eq $installedVersion.Minor -and + (($Version.Build -eq -1) -or ($Version.Build -eq $installedVersion.Build)) -and + (($Version.Revision -eq -1) -or ($Version.Revision -eq $installedVersion.Revision))) { + return $true + } + } + + return $false +} + +Function Test-DotNetSdkInstalled($DotNetRoot, $Version, $Channel) { + if (!$DotNetRoot) { + return $false + } + + $sdkRoot = Join-Path $DotNetRoot 'sdk' + if (!(Test-Path -LiteralPath $sdkRoot)) { + return $false + } + + if ($Version) { + return Test-Path -LiteralPath (Join-Path $sdkRoot $Version) -PathType Container + } + + $channelVersion = $null + if (![Version]::TryParse($Channel, [ref]$channelVersion)) { + return $false + } + + foreach ($sdkDirectory in Get-ChildItem -LiteralPath $sdkRoot -Directory) { + $installedVersion = $null + if ([Version]::TryParse($sdkDirectory.Name, [ref]$installedVersion) -and + $channelVersion.Major -eq $installedVersion.Major -and + $channelVersion.Minor -eq $installedVersion.Minor) { + return $true + } + } + + return $false +} + $switches = @() $envVars = @{ # For locally installed dotnet, skip first time experience which takes a long time @@ -208,53 +268,67 @@ if ($InstallLocality -eq 'machine') { if ($IsMacOS -or $IsLinux) { $DotNetInstallDir = '/usr/share/dotnet' } else { + $DotNetInstallDir = Join-Path $(if ($env:ProgramW6432) { $env:ProgramW6432 } else { $env:ProgramFiles }) 'dotnet' + $DotNetX86InstallDir = Join-Path ${env:ProgramFiles(x86)} 'dotnet' $restartRequired = $false $sdks |% { if ($_.Version) { $version = $_.Version } else { $version = $_.Channel } - if ($PSCmdlet.ShouldProcess(".NET SDK $version ($arch)", "Install")) { + if (!(Test-DotNetSdkInstalled -DotNetRoot $DotNetInstallDir -Version $_.Version -Channel $_.Channel) -and + $PSCmdlet.ShouldProcess(".NET SDK $version ($arch)", "Install")) { Install-DotNet -Version $version -Architecture $arch $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } - if ($IncludeX86) { - Install-DotNet -Version $version -Architecture x86 - $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) - } + if ($IncludeX86 -and + !(Test-DotNetSdkInstalled -DotNetRoot $DotNetX86InstallDir -Version $_.Version -Channel $_.Channel) -and + $PSCmdlet.ShouldProcess(".NET SDK $version (x86)", "Install")) { + Install-DotNet -Version $version -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) } } - $runtimeVersions | Sort-Object | Get-Unique |% { - if ($PSCmdlet.ShouldProcess(".NET runtime $_", "Install")) { + $runtimeVersions | Sort-Object -Unique |% { + if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.NETCore.App -Version $_) -and + $PSCmdlet.ShouldProcess(".NET runtime $_ ($arch)", "Install")) { Install-DotNet -Version $_ -sku Runtime -Architecture $arch $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } - if ($IncludeX86) { - Install-DotNet -Version $_ -sku Runtime -Architecture x86 - $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) - } + if ($IncludeX86 -and + !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.NETCore.App -Version $_) -and + $PSCmdlet.ShouldProcess(".NET runtime $_ (x86)", "Install")) { + Install-DotNet -Version $_ -sku Runtime -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) } } - $windowsDesktopRuntimeVersions | Sort-Object | Get-Unique |% { - if ($PSCmdlet.ShouldProcess(".NET Windows Desktop $_", "Install")) { + $windowsDesktopRuntimeVersions | Sort-Object -Unique |% { + if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.WindowsDesktop.App -Version $_) -and + $PSCmdlet.ShouldProcess(".NET Windows Desktop $_ ($arch)", "Install")) { Install-DotNet -Version $_ -sku WindowsDesktop -Architecture $arch $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } - if ($IncludeX86) { - Install-DotNet -Version $_ -sku WindowsDesktop -Architecture x86 - $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) - } + if ($IncludeX86 -and + !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.WindowsDesktop.App -Version $_) -and + $PSCmdlet.ShouldProcess(".NET Windows Desktop $_ (x86)", "Install")) { + Install-DotNet -Version $_ -sku WindowsDesktop -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) } } - $aspnetRuntimeVersions | Sort-Object | Get-Unique |% { - if ($PSCmdlet.ShouldProcess("ASP.NET Core $_", "Install")) { + $aspnetRuntimeVersions | Sort-Object -Unique |% { + if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.AspNetCore.App -Version $_) -and + $PSCmdlet.ShouldProcess("ASP.NET Core $_ ($arch)", "Install")) { Install-DotNet -Version $_ -sku AspNetCore -Architecture $arch $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } - if ($IncludeX86) { - Install-DotNet -Version $_ -sku AspNetCore -Architecture x86 - $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) - } + if ($IncludeX86 -and + !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.AspNetCore.App -Version $_) -and + $PSCmdlet.ShouldProcess("ASP.NET Core $_ (x86)", "Install")) { + Install-DotNet -Version $_ -sku AspNetCore -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) } } if ($restartRequired) { @@ -317,19 +391,21 @@ $global:LASTEXITCODE = 0 $sdks |% { if ($_.Version) { $parameters = '-Version', $_.Version } else { $parameters = '-Channel', $_.Channel } - if ($PSCmdlet.ShouldProcess(".NET SDK $_ ($arch)", "Install")) { - $anythingInstalled = $true - Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture $arch -InstallDir $DotNetInstallDir $switches" + if (!(Test-DotNetSdkInstalled -DotNetRoot $DotNetInstallDir -Version $_.Version -Channel $_.Channel)) { + if ($PSCmdlet.ShouldProcess(".NET SDK $_ ($arch)", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture $arch -InstallDir $DotNetInstallDir $switches" - if ($LASTEXITCODE -ne 0) { - Write-Error ".NET SDK installation failure: $LASTEXITCODE" - exit $LASTEXITCODE + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture $arch -InstallDir $DotNetInstallDir $switches -DryRun" } - } else { - Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture $arch -InstallDir $DotNetInstallDir $switches -DryRun" } - if ($IncludeX86) { + if ($IncludeX86 -and !(Test-DotNetSdkInstalled -DotNetRoot $DotNetX86InstallDir -Version $_.Version -Channel $_.Channel)) { if ($PSCmdlet.ShouldProcess(".NET x86 SDK $_", "Install")) { $anythingInstalled = $true Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture x86 -InstallDir $DotNetX86InstallDir $switches" @@ -347,19 +423,21 @@ $sdks |% { $dotnetRuntimeSwitches = $switches + '-Runtime','dotnet' $runtimeVersions | Sort-Object -Unique |% { - if ($PSCmdlet.ShouldProcess(".NET $Arch runtime $_", "Install")) { - $anythingInstalled = $true - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches" + if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.NETCore.App -Version $_)) { + if ($PSCmdlet.ShouldProcess(".NET $Arch runtime $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches" - if ($LASTEXITCODE -ne 0) { - Write-Error ".NET SDK installation failure: $LASTEXITCODE" - exit $LASTEXITCODE + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches -DryRun" } - } else { - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches -DryRun" } - if ($IncludeX86) { + if ($IncludeX86 -and !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.NETCore.App -Version $_)) { if ($PSCmdlet.ShouldProcess(".NET x86 runtime $_", "Install")) { $anythingInstalled = $true Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $dotnetRuntimeSwitches" @@ -377,19 +455,21 @@ $runtimeVersions | Sort-Object -Unique |% { $windowsDesktopRuntimeSwitches = $switches + '-Runtime','windowsdesktop' $windowsDesktopRuntimeVersions | Sort-Object -Unique |% { - if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop $arch runtime $_", "Install")) { - $anythingInstalled = $true - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches" + if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.WindowsDesktop.App -Version $_)) { + if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop $arch runtime $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches" - if ($LASTEXITCODE -ne 0) { - Write-Error ".NET SDK installation failure: $LASTEXITCODE" - exit $LASTEXITCODE + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches -DryRun" } - } else { - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches -DryRun" } - if ($IncludeX86) { + if ($IncludeX86 -and !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.WindowsDesktop.App -Version $_)) { if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop x86 runtime $_", "Install")) { $anythingInstalled = $true Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $windowsDesktopRuntimeSwitches" @@ -407,19 +487,21 @@ $windowsDesktopRuntimeVersions | Sort-Object -Unique |% { $aspnetRuntimeSwitches = $switches + '-Runtime','aspnetcore' $aspnetRuntimeVersions | Sort-Object -Unique |% { - if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core $arch runtime $_", "Install")) { - $anythingInstalled = $true - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches" + if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.AspNetCore.App -Version $_)) { + if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core $arch runtime $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches" - if ($LASTEXITCODE -ne 0) { - Write-Error ".NET SDK installation failure: $LASTEXITCODE" - exit $LASTEXITCODE + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches -DryRun" } - } else { - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches -DryRun" } - if ($IncludeX86) { + if ($IncludeX86 -and !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.AspNetCore.App -Version $_)) { if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core x86 runtime $_", "Install")) { $anythingInstalled = $true Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $aspnetRuntimeSwitches" From 13d25ee93ad38e0f8c965a44f5c76545717e2d50 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 09:00:59 -0600 Subject: [PATCH 03/22] Update dependency docfx to v2.81.0 (577) --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index aed56a8d..160aded9 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -24,7 +24,7 @@ "rollForward": false }, "docfx": { - "version": "2.80.1", + "version": "2.81.0", "commands": [ "docfx" ], From 95dd30b4ca796f74be3833140f136f496648a7c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:48:35 -0600 Subject: [PATCH 04/22] Update becheran/mlc action to v1.2.2 (578) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64a106f5..e210b36c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,6 +76,6 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: πŸ”— Markup Link Checker (mlc) - uses: becheran/mlc@7ec24825cefe0c9c8c6bac48430e1f69e3ec356e # v1.2.0 + uses: becheran/mlc@b0d0d353e04fcbcfd1d86716103d78a61e5b25dd # v1.2.2 with: args: --do-not-warn-for-redirect-to https://learn.microsoft.com*,https://dotnet.microsoft.com/*,https://dev.azure.com/*,https://app.codecov.io/* -p docfx -i https://www.npmjs.com/package/*,https://get.dot.net/ From d4778325fe9cd707986b9a9eb3565107c66225a2 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 30 Sep 2026 11:50:55 -0600 Subject: [PATCH 05/22] Make docs workflow recoverable via re-run or manual dispatch Re-running a failed docs deployment failed because the prior attempt's github-pages artifact remained, making deploy-pages find two artifacts. Use a per-attempt artifact name, and allow workflow_dispatch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/docs.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c0492a90..13dfd6d5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,6 +4,7 @@ on: push: branches: - main + workflow_dispatch: # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. @@ -37,7 +38,12 @@ jobs: uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: docfx/_site + # Artifacts from prior attempts remain visible on re-runs, so make the name unique per attempt + # to avoid deploy-pages failing due to multiple artifacts with the same name. + name: github-pages-${{ github.run_attempt }} - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 + with: + artifact_name: github-pages-${{ github.run_attempt }} From 57fc2b2ee08529340f110ddb5d4ef98a97a8f167 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 1 Oct 2026 10:29:38 -0600 Subject: [PATCH 06/22] Pin Ubuntu workflows to 26.04 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/libtemplate-update.yml | 2 +- azure-pipelines/build.yml | 2 +- azure-pipelines/libtemplate-update.yml | 2 +- azure-pipelines/release.yml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e210b36c..db4b2e0f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: fail-fast: false matrix: os: - - ubuntu-24.04 + - ubuntu-26.04 - macOS-15 - windows-2025 @@ -72,7 +72,7 @@ jobs: docs: name: πŸ“ƒ Docs - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: πŸ”— Markup Link Checker (mlc) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 71adf9e7..74a62566 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -16,7 +16,7 @@ on: jobs: # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. copilot-setup-steps: - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 # Set the permissions to the lowest permissions possible needed for your steps. # Copilot will be given its own token for its operations. permissions: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 13dfd6d5..9616f694 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/.github/workflows/libtemplate-update.yml b/.github/workflows/libtemplate-update.yml index af1a9eef..3de947a1 100644 --- a/.github/workflows/libtemplate-update.yml +++ b/.github/workflows/libtemplate-update.yml @@ -12,7 +12,7 @@ on: jobs: merge: - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 permissions: contents: write pull-requests: write diff --git a/azure-pipelines/build.yml b/azure-pipelines/build.yml index 7d808ede..3f154027 100644 --- a/azure-pipelines/build.yml +++ b/azure-pipelines/build.yml @@ -28,7 +28,7 @@ jobs: - job: Linux pool: - vmImage: ubuntu-24.04 + vmImage: ubuntu-26.04 steps: - checkout: self fetchDepth: 0 # avoid shallow clone so nbgv can do its work. diff --git a/azure-pipelines/libtemplate-update.yml b/azure-pipelines/libtemplate-update.yml index fdfa1df5..84ab4cae 100644 --- a/azure-pipelines/libtemplate-update.yml +++ b/azure-pipelines/libtemplate-update.yml @@ -22,7 +22,7 @@ stages: jobs: - job: merge pool: - vmImage: ubuntu-latest + vmImage: ubuntu-26.04 steps: - checkout: self fetchDepth: 0 diff --git a/azure-pipelines/release.yml b/azure-pipelines/release.yml index 4c9db860..a737d039 100644 --- a/azure-pipelines/release.yml +++ b/azure-pipelines/release.yml @@ -15,7 +15,7 @@ variables: jobs: - job: release pool: - vmImage: ubuntu-latest + vmImage: ubuntu-26.04 steps: - checkout: none - powershell: | From 3f29bf73558e39312648235bd19a8f5e38b870f3 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 29 Sep 2026 20:58:54 -0600 Subject: [PATCH 07/22] Migrate from xUnit to TUnit --- .../template-release-notes.md | 24 +++++++++++ .github/renovate.json | 5 +++ .github/workflows/build.yml | 4 +- AGENTS.md | 32 ++++++--------- CONTRIBUTING.md | 16 ++++++++ Directory.Build.props | 17 ++++++++ Directory.Packages.props | 4 +- azure-pipelines/dotnet.yml | 8 +++- global.json | 3 ++ init.ps1 | 3 +- src/Directory.Build.props | 1 + src/dirs.proj | 5 +++ test/AOT.props | 38 +++++++++++++++++ test/Directory.Build.props | 7 ++++ test/Directory.Build.targets | 41 +++++++++++++++++-- test/Library.Tests/CalculatorTests.cs | 7 +--- test/Library.Tests/Library.Tests.csproj | 6 +-- test/Library.Tests/xunit.runner.json | 4 -- test/dirs.proj | 21 ++++++++++ tools/Get-NativeAOTTestProjects.ps1 | 25 +++++++++++ tools/dirs.proj | 6 +++ tools/dotnet-test-cloud.ps1 | 34 ++++++++++++++- 22 files changed, 268 insertions(+), 43 deletions(-) create mode 100644 src/dirs.proj create mode 100644 test/AOT.props delete mode 100644 test/Library.Tests/xunit.runner.json create mode 100644 test/dirs.proj create mode 100644 tools/Get-NativeAOTTestProjects.ps1 create mode 100644 tools/dirs.proj diff --git a/.agents/skills/update-library-template/template-release-notes.md b/.agents/skills/update-library-template/template-release-notes.md index 975da315..8bd62058 100644 --- a/.agents/skills/update-library-template/template-release-notes.md +++ b/.agents/skills/update-library-template/template-release-notes.md @@ -18,3 +18,27 @@ dotnet solution EXISTING.sln migrate This will create an EXISTING.slnx file. `git add` that file, then `git rm` the old `.sln` file. Sometimes a repo will reference the sln filename in a script or doc somewhere. Search the repo for such references and update them to the slnx file. + +## Migrating from xUnit to TUnit + +The template now uses TUnit with Microsoft.Testing.Platform instead of the xUnit test runner. +When merging this update into an existing repository, migrate all test projects and repo-specific test automation. +Follow the [TUnit xUnit migration guide](https://tunit.dev/docs/migration/xunit), including its automated migration code fixes. + +* Replace xUnit runner/framework package references with `TUnit.Engine` and set `Exe` in each test project. + Remove obsolete runner dependencies such as `xunit.runner.visualstudio`, `Microsoft.NET.Test.Sdk`, and `coverlet.collector`, along with `xunit.runner.json`. + Reconcile central package versions with the template's `Directory.Packages.props`. +* Existing xUnit assertions can remain: the template retains `xunit.v3.assert`, using `xunit.v3.assert.aot` for .NET 9 and later. + Keep `Xunit` imports for assertions, but use TUnit for test attributes and lifecycle APIs. +* Convert `[Fact]` and `[Theory]` to `[Test]`, and `[InlineData]` to `[Arguments]`. + Migrate member/class data sources, traits, fixtures, collection behavior, setup/teardown, and test output according to the migration guide. + Review tests that depend on xUnit's execution ordering or parallelization rules; do not assume those rules carry over. +* Merge the `global.json` Microsoft.Testing.Platform runner setting, test directory build files, traversal projects, and updated PowerShell/YAML automation. + Keep every test project in the repository's solution for managed test runs. + The traversals discover `.csproj` files under `src` and `test`; adjust discovery if your projects live elsewhere or use another project language. +* Update custom test commands and filters. + VSTest `--filter` expressions do not work with this runner; pass TUnit filters after `--`, for example `-- --treenode-filter "/*/*/ClassName/MethodName"`. + Replace any xUnit-specific CI result or coverage handling with the template's Microsoft.Testing.Platform integration. +* Validate managed tests and coverage, then run `dotnet publish tools\dirs.proj -c Release` and `.\tools\dotnet-test-cloud.ps1 -Configuration Release -IncludeNativeAOT`. + Test projects targeting .NET 8 or later are eligible for NativeAOT publication by default. + If a project cannot support NativeAOT, set `false` in its project file; it will still run as managed tests. diff --git a/.github/renovate.json b/.github/renovate.json index 79dba1e5..786187da 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -11,6 +11,11 @@ "matchPackageNames": ["xunit*"], "groupName": "xunit" }, + { + "matchPackageNames": ["tunit*"], + "groupName": "tunit", + "schedule": ["on the first day of the month"] + }, { "matchPackageNames": ["Microsoft.Testing.Extensions.*"], "groupName": "Microsoft Testing Platform" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db4b2e0f..bfe01efd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,9 +45,9 @@ jobs: run: tools/variables/_define.ps1 shell: pwsh - name: πŸ›  build - run: dotnet build -t:build,pack --no-restore -c ${{ env.BUILDCONFIGURATION }} -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"${{ runner.temp }}/_artifacts/build_logs/build.binlog" + run: dotnet build tools/dirs.proj -t:build,pack,publish --no-restore -c ${{ env.BUILDCONFIGURATION }} -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"${{ runner.temp }}/_artifacts/build_logs/build.binlog" - name: πŸ§ͺ test - run: tools/dotnet-test-cloud.ps1 -Configuration ${{ env.BUILDCONFIGURATION }} -Agent ${{ runner.os }} + run: tools/dotnet-test-cloud.ps1 -Configuration ${{ env.BUILDCONFIGURATION }} -Agent ${{ runner.os }} -IncludeNativeAOT shell: pwsh - name: πŸ’…πŸ» Verify formatted code run: dotnet format --verify-no-changes --no-restore diff --git a/AGENTS.md b/AGENTS.md index 0187843a..49f47191 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,11 +13,10 @@ ## Testing -**IMPORTANT**: This repository uses Microsoft.Testing.Platform (MTP v2) with xunit v3. Traditional `--filter` syntax does NOT work. Use the options below instead. +**IMPORTANT**: This repository uses TUnit with Microsoft.Testing.Platform (MTP v2). Traditional `--filter` syntax does NOT work. Use the options below instead. * There should generally be one test project (under the `test` directory) per shipping project (under the `src` directory). Test projects are named after the project being tested with a `.Tests` suffix. -* Tests use xunit v3 with Microsoft.Testing.Platform (MTP v2). Traditional VSTest `--filter` syntax does NOT work. -* Some tests are known to be unstable. When running tests, you should skip the unstable ones by using `-- --filter-not-trait "FailsInCloudTest=true"`. +* Tests use TUnit with Microsoft.Testing.Platform (MTP v2), while retaining xUnit assertions. Traditional VSTest `--filter` syntax does NOT work. ### Running Tests @@ -33,47 +32,40 @@ dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Rele **Run a single test method**: ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-method ClassName.MethodName +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --treenode-filter "/*/*/ClassName/MethodName" ``` **Run all tests in a test class**: ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-class ClassName +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --treenode-filter "/*/*/ClassName/*" ``` **Run tests with wildcard matching** (supports wildcards at beginning and/or end): ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-method "*Pattern*" +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --treenode-filter "/*/*/*/*Pattern*" ``` -**Run tests with a specific trait** (equivalent to category filtering): +**Run tests with a specific property**: ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-trait "TraitName=value" -``` - -**Exclude tests with a specific trait** (skip unstable tests): -```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-not-trait "TestCategory=FailsInCloudTest" +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --treenode-filter "/*/*/*/*[PropertyName=value]" ``` **Run tests for a specific framework only**: ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release --framework net9.0 +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release --framework net8.0 ``` **List all available tests without running them**: ```bash cd test/Library.Tests -dotnet run --no-build -c Release --framework net9.0 -- --list-tests +dotnet run --no-build -c Release --framework net8.0 -- --list-tests ``` -**Key points about test filtering with MTP v2 / xunit v3**: +**Key points about test filtering with TUnit / MTP v2**: - Options after `--` are passed to the test runner, not to `dotnet test` -- Use `--filter-method`, `--filter-class`, `--filter-namespace` for simple filtering -- Use `--filter-trait` and `--filter-not-trait` for trait-based filtering (replaces `--filter "TestCategory=..."`) +- Use `--treenode-filter` to select tests by assembly, namespace, class, method, or property - Traditional VSTest `--filter` expressions do NOT work -- Wildcards `*` are supported at the beginning and/or end of filter values -- Multiple simple filters of the same type use OR logic, different types combine with AND +- Wildcards `*` are supported in tree node segments - See `--help` for query filter language for advanced scenarios ## Coding style diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c566c0b..bf43aad4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,6 +45,22 @@ You can use `dotnet test` to build and/or test the repo. There may be tests that are known to be unstable or have special requirements. These can be avoided by running tests using the [dotnet-test-cloud.ps1](tools/dotnet-test-cloud.ps1) script *after* running `dotnet build`. +To build both managed and NativeAOT tests, run `dotnet publish tools/dirs.proj -c Release`. +Then run `./tools/dotnet-test-cloud.ps1 -Configuration Release -IncludeNativeAOT`. +The traversal projects discover projects under `src` and `test`, and publish each eligible test framework targeting .NET 8 or later. +Keep test projects in the solution as well: managed test runs still use the solution, while NativeAOT runs use the traversal's evaluated executable paths. +Test projects can opt out of NativeAOT publishing with `false`. +One restore includes all test target frameworks, runtime identifiers, and NativeAOT compiler dependencies. +Managed builds are RID-neutral by default; NativeAOT builds use the SDK's runtime-specific output directories. +For a specified RID, managed execution and native publishing can share the same build outputs. +Test builds keep dynamic code, startup hooks, and event tracing enabled for managed code coverage. +The `ConfigureNativeAOTTestFeatures` target disables those features only in the native compiler's publish-time inputs, without rewriting the managed runtime configuration. +It preserves all other runtime feature options, including invariant globalization, so native compilation and linking use consistent settings. +For an existing RID-specific build, `dotnet publish test/Library.Tests/Library.Tests.csproj -f net8.0 -r -p:NativeAOT=true --no-build` publishes native tests from the managed build. +Use the same configuration, framework, and RID for the preceding build and the publish. +Test builds use invariant globalization and retain only English satellite resources; RID-specific builds are self-contained. +Root `Directory.Build.props` supplies project-reference defaults for both traversal and SDK projects that remove the `_IsPublishing` global property for managed dependencies, avoiding duplicate project instances that write to the same outputs during parallel publishing. + ## Releases Use `nbgv tag` to create a tag for a particular commit that you mean to release. diff --git a/Directory.Build.props b/Directory.Build.props index a3d5fb8e..1a890354 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -48,11 +48,28 @@ 16.9 + + win + linux + osx + + x64 + arm64 + + $(RidOsPrefix)-$(RidOsArchitecture) + $(DefaultRuntimeIdentifier) + + + + + _IsPublishing + _IsPublishing + diff --git a/Directory.Packages.props b/Directory.Packages.props index 6f2124c1..11e38433 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,7 +16,9 @@ - + + + diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml index 9a572afe..d399b31b 100644 --- a/azure-pipelines/dotnet.yml +++ b/azure-pipelines/dotnet.yml @@ -3,10 +3,14 @@ parameters: steps: -- script: dotnet build -t:build,pack --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" +- script: dotnet build tools/dirs.proj -t:build,pack --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" displayName: πŸ›  dotnet build -- powershell: tools/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults +- script: dotnet publish tools/dirs.proj --no-restore -c $(BuildConfiguration) + displayName: πŸ›  publish NativeAOT tests + condition: and(succeeded(), ${{ parameters.RunTests }}) + +- powershell: tools/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults -IncludeNativeAOT displayName: πŸ§ͺ dotnet test condition: and(succeeded(), ${{ parameters.RunTests }}) diff --git a/global.json b/global.json index 8c833e41..b925aaba 100644 --- a/global.json +++ b/global.json @@ -6,5 +6,8 @@ }, "test": { "runner": "Microsoft.Testing.Platform" + }, + "msbuild-sdks": { + "Microsoft.Build.Traversal": "4.1.82" } } diff --git a/init.ps1 b/init.ps1 index b4004e79..3469e0b7 100755 --- a/init.ps1 +++ b/init.ps1 @@ -90,10 +90,11 @@ try { if (!$NoRestore -and $PSCmdlet.ShouldProcess("NuGet packages", "Restore")) { Write-Host "Restoring NuGet packages" -ForegroundColor $HeaderColor - dotnet restore @RestoreArguments + dotnet restore "$PSScriptRoot/tools/dirs.proj" @RestoreArguments if ($lastexitcode -ne 0) { throw "Failure while restoring packages." } + } if (!$NoToolRestore -and $PSCmdlet.ShouldProcess("dotnet tool", "restore")) { diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 9ba7818d..b01ac125 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -9,4 +9,5 @@ + diff --git a/src/dirs.proj b/src/dirs.proj new file mode 100644 index 00000000..b6fa81f9 --- /dev/null +++ b/src/dirs.proj @@ -0,0 +1,5 @@ + + + + + diff --git a/test/AOT.props b/test/AOT.props new file mode 100644 index 00000000..9a252b65 --- /dev/null +++ b/test/AOT.props @@ -0,0 +1,38 @@ + + + true + $(AvailableRuntimeIdentifiers) + + + + true + true + true + true + en + + + true + + + + %(ProjectReference.GlobalPropertiesToRemove);RuntimeIdentifier;NativeAOT + + + + + + false + false + false + + + + + false + + + + diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 2b5b542d..035e949c 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -3,9 +3,16 @@ + true + true false true true + false + + + + $(DefaultRuntimeIdentifier) diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets index 3758bb8c..95b0658b 100644 --- a/test/Directory.Build.targets +++ b/test/Directory.Build.targets @@ -1,14 +1,47 @@ + + + + $(NoWarn);IL2104;IL3053 + + - - - - + + + + <_TestFramework Include="$(TargetFrameworks)" /> + + + + + + + + + + + + <_NativeExecutableExtension Condition="$([MSBuild]::IsOsPlatform('Windows'))">.exe + + + <_NativeAOTTestExecutable Include="$(MSBuildProjectFullPath)"> + $(MSBuildProjectName) + $(TargetFramework) + $([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '$(PublishDir)', '$(AssemblyName)$(_NativeExecutableExtension)')) + + + diff --git a/test/Library.Tests/CalculatorTests.cs b/test/Library.Tests/CalculatorTests.cs index a35947bc..fb178818 100644 --- a/test/Library.Tests/CalculatorTests.cs +++ b/test/Library.Tests/CalculatorTests.cs @@ -2,15 +2,12 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Library; +using TUnit.Core; using Xunit; public class CalculatorTests { - public CalculatorTests() - { - } - - [Fact] + [Test] public void AddOrSubtract() { // This tests aggregation of code coverage across test runs. diff --git a/test/Library.Tests/Library.Tests.csproj b/test/Library.Tests/Library.Tests.csproj index e9e12e43..29e319fd 100644 --- a/test/Library.Tests/Library.Tests.csproj +++ b/test/Library.Tests/Library.Tests.csproj @@ -1,18 +1,18 @@ - net8.0 $(TargetFrameworks);net472 Exe - - + + + diff --git a/test/Library.Tests/xunit.runner.json b/test/Library.Tests/xunit.runner.json deleted file mode 100644 index 8465a454..00000000 --- a/test/Library.Tests/xunit.runner.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", - "shadowCopy": false -} diff --git a/test/dirs.proj b/test/dirs.proj new file mode 100644 index 00000000..835c4a89 --- /dev/null +++ b/test/dirs.proj @@ -0,0 +1,21 @@ + + + + + + + + + + + + <_NativeAOTTest Update="@(_NativeAOTTest)" UndefineProperties="" /> + + + + + + + diff --git a/tools/Get-NativeAOTTestProjects.ps1 b/tools/Get-NativeAOTTestProjects.ps1 new file mode 100644 index 00000000..27a39ec3 --- /dev/null +++ b/tools/Get-NativeAOTTestProjects.ps1 @@ -0,0 +1,25 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Gets NativeAOT test executable paths evaluated by the test traversal project. +.PARAMETER Configuration + The build configuration used to evaluate output paths. +#> +[CmdletBinding()] +Param( + [string]$Configuration='Debug' +) + +$result = & dotnet msbuild "$PSScriptRoot/../test/dirs.proj" ` + -nologo ` + -getTargetResult:GetNativeAOTTests ` + "-property:Configuration=$Configuration" +if ($LASTEXITCODE -ne 0) { + throw 'Failed to discover NativeAOT tests through MSBuild.' +} + +$tests = ($result | Out-String | ConvertFrom-Json).TargetResults.GetNativeAOTTests.Items +foreach ($test in $tests) { + $test +} diff --git a/tools/dirs.proj b/tools/dirs.proj new file mode 100644 index 00000000..7b266e21 --- /dev/null +++ b/tools/dirs.proj @@ -0,0 +1,6 @@ + + + + + + diff --git a/tools/dotnet-test-cloud.ps1 b/tools/dotnet-test-cloud.ps1 index 3d8d0870..1a000768 100644 --- a/tools/dotnet-test-cloud.ps1 +++ b/tools/dotnet-test-cloud.ps1 @@ -5,6 +5,8 @@ Runs tests as they are run in cloud test runs. .PARAMETER Configuration The configuration within which to run tests +.PARAMETER IncludeNativeAOT + Runs the NativeAOT-compiled tests and fails if the expected image is missing. .PARAMETER Agent The name of the agent. This is used in preparing test run titles. .PARAMETER PublishResults @@ -19,6 +21,7 @@ [CmdletBinding()] Param( [string]$Configuration='Debug', + [switch]$IncludeNativeAOT, [string]$Agent='Local', [switch]$PublishResults, [switch]$x86, @@ -50,6 +53,9 @@ if ($x86) { $testBinLog = Join-Path $ArtifactStagingFolder (Join-Path build_logs test.binlog) $testLogs = Join-Path $ArtifactStagingFolder test_logs +if (Test-Path -LiteralPath $testLogs) { + Remove-Item -LiteralPath $testLogs -Recurse -Force +} $globalJson = Get-Content $PSScriptRoot/../global.json | ConvertFrom-Json $isMTP = $globalJson.test.runner -eq 'Microsoft.Testing.Platform' @@ -95,12 +101,38 @@ if ($isMTP) { -c $Configuration ` -bl:"$testBinLog" ` -- ` - --filter-not-trait 'TestCategory=FailsInCloudTest' ` @mtpArgs ` @dumpSwitches ` @extraArgs if ($LASTEXITCODE -ne 0) { $failedTests += 1 } + if ($IncludeNativeAOT) { + $nativeAotTests = @(& "$PSScriptRoot/Get-NativeAOTTestProjects.ps1" -Configuration $Configuration) + foreach ($nativeAotTest in $nativeAotTests) { + $testExecutable = $nativeAotTest.ExecutablePath + if (-not (Test-Path -LiteralPath $testExecutable -PathType Leaf)) { + Write-Error "Expected NativeAOT TUnit test executable '$testExecutable' was not found." + $failedTests += 1 + continue + } + + $nativeAotArgs = @( + ,'--diagnostic' + ,'--diagnostic-output-directory',$testLogs + ,'--diagnostic-verbosity','Information' + ,'--results-directory',$testLogs + ,'--report-trx' + ,'--report-trx-filename',"$($nativeAotTest.ProjectName)_$($nativeAotTest.TargetFramework)_NativeAOT_{arch}.trx" + ) + if ($IsWindows) { + $nativeAotArgs += $dumpSwitches + } + Write-Host "Running NativeAOT TUnit tests from '$testExecutable'." -ForegroundColor Cyan + & $testExecutable @nativeAotArgs @extraArgs + if ($LASTEXITCODE -ne 0) { $failedTests += 1 } + } + } + $trxFiles = Get-ChildItem -Recurse -Path $testLogs\*.trx } else { $testDiagLog = Join-Path $ArtifactStagingFolder (Join-Path test_logs diag.log) From 302ccbeab00342f34c7e0299e39f7b22df2f1c07 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 1 Oct 2026 10:16:35 -0600 Subject: [PATCH 08/22] Build and test NativeAOT-safe libraries --- .../template-release-notes.md | 9 +++++++++ CONTRIBUTING.md | 3 +++ Expand-Template.ps1 | 6 ++++++ Library.slnx | 1 + README.md | 1 + src/Library/Library.csproj | 1 + test/AOT.props | 4 +++- .../AotCompatibilityTest.csproj | 19 +++++++++++++++++++ test/AotCompatibilityTest/Program.cs | 4 ++++ test/dirs.proj | 2 ++ 10 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 test/AotCompatibilityTest/AotCompatibilityTest.csproj create mode 100644 test/AotCompatibilityTest/Program.cs diff --git a/.agents/skills/update-library-template/template-release-notes.md b/.agents/skills/update-library-template/template-release-notes.md index 8bd62058..7ce3f9c7 100644 --- a/.agents/skills/update-library-template/template-release-notes.md +++ b/.agents/skills/update-library-template/template-release-notes.md @@ -42,3 +42,12 @@ Follow the [TUnit xUnit migration guide](https://tunit.dev/docs/migration/xunit) * Validate managed tests and coverage, then run `dotnet publish tools\dirs.proj -c Release` and `.\tools\dotnet-test-cloud.ps1 -Configuration Release -IncludeNativeAOT`. Test projects targeting .NET 8 or later are eligible for NativeAOT publication by default. If a project cannot support NativeAOT, set `false` in its project file; it will still run as managed tests. + +## NativeAOT compatibility validation + +Shipping projects now set `IsAotCompatible` for target frameworks compatible with .NET 8. +Apply the same conditional property to each shipping project that is intended to support NativeAOT, then resolve all trim and AOT analyzer warnings rather than suppressing them broadly. + +The new `test/AotCompatibilityTest` project catches NativeAOT compiler warnings that library analyzers may not report. +Keep it in the solution, add every NativeAOT-compatible shipping assembly as a `TrimmerRootAssembly`, and retain its publishable entry in `test/dirs.proj`. +CI validates this project through the existing traversal publish on Windows, Linux, and macOS; it is intentionally marked `IsTestProject=false` because publishing successfully is the test. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf43aad4..5bda7fc9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,6 +59,9 @@ It preserves all other runtime feature options, including invariant globalizatio For an existing RID-specific build, `dotnet publish test/Library.Tests/Library.Tests.csproj -f net8.0 -r -p:NativeAOT=true --no-build` publishes native tests from the managed build. Use the same configuration, framework, and RID for the preceding build and the publish. Test builds use invariant globalization and retain only English satellite resources; RID-specific builds are self-contained. +Shipping libraries targeting .NET 8 or later opt into NativeAOT compatibility analysis with `IsAotCompatible`. +The `test/AotCompatibilityTest` project complements those analyzers by rooting the shipping assembly and passing it through the NativeAOT compiler during every traversal publish. +Add each shipping assembly that must be validated as a `TrimmerRootAssembly`, and keep this project publishable in `test/dirs.proj`. Root `Directory.Build.props` supplies project-reference defaults for both traversal and SDK projects that remove the `_IsPublishing` global property for managed dependencies, avoiding duplicate project instances that write to the same outputs during parallel publishing. ## Releases diff --git a/Expand-Template.ps1 b/Expand-Template.ps1 index eb525325..37e96b57 100755 --- a/Expand-Template.ps1 +++ b/Expand-Template.ps1 @@ -140,6 +140,12 @@ try { 'Library'=$LibraryName 'COMPANY-PLACEHOLDER'=$Author } + Replace-Placeholders -Path "test/AotCompatibilityTest/AotCompatibilityTest.csproj" -Replacements @{ + 'Library'=$LibraryName + } + Replace-Placeholders -Path "test/AotCompatibilityTest/Program.cs" -Replacements @{ + 'COMPANY-PLACEHOLDER'=$Author + } Replace-Placeholders -Path "src/AssemblyInfo.cs" -Replacements @{ 'COMPANY-PLACEHOLDER'=$Author } diff --git a/Library.slnx b/Library.slnx index dc016d9e..4d7254f1 100644 --- a/Library.slnx +++ b/Library.slnx @@ -22,5 +22,6 @@ + diff --git a/README.md b/README.md index 3f8c9764..1f6ce981 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ * Auto-versioning (via [Nerdbank.GitVersioning](https://github.com/dotnet/nerdbank.gitversioning)) * Builds with a "pinned" .NET SDK to ensure reproducible builds across machines and across time. * Automatically pack the library and publish it as an artifact, and even push it to some NuGet feed for consumption. +* Build NativeAOT-safe libraries with thorough compatibility testing * Testing * Testing on .NET Framework, multiple .NET versions * Testing on Windows, Linux and OSX diff --git a/src/Library/Library.csproj b/src/Library/Library.csproj index fa79c05e..e4cd98aa 100644 --- a/src/Library/Library.csproj +++ b/src/Library/Library.csproj @@ -1,6 +1,7 @@ net8.0;netstandard2.0 + true diff --git a/test/AOT.props b/test/AOT.props index 9a252b65..623ff24d 100644 --- a/test/AOT.props +++ b/test/AOT.props @@ -3,11 +3,13 @@ true $(AvailableRuntimeIdentifiers) - + true true true + + true en diff --git a/test/AotCompatibilityTest/AotCompatibilityTest.csproj b/test/AotCompatibilityTest/AotCompatibilityTest.csproj new file mode 100644 index 00000000..f8a37360 --- /dev/null +++ b/test/AotCompatibilityTest/AotCompatibilityTest.csproj @@ -0,0 +1,19 @@ + + + net10.0 + Exe + false + $(DefaultRuntimeIdentifier) + true + + + + + + + + + + + + diff --git a/test/AotCompatibilityTest/Program.cs b/test/AotCompatibilityTest/Program.cs new file mode 100644 index 00000000..1467eff9 --- /dev/null +++ b/test/AotCompatibilityTest/Program.cs @@ -0,0 +1,4 @@ +// Copyright (c) COMPANY-PLACEHOLDER. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +Console.WriteLine("This project validates NativeAOT compatibility when it is published; the executable is not run."); diff --git a/test/dirs.proj b/test/dirs.proj index 835c4a89..6f6def8c 100644 --- a/test/dirs.proj +++ b/test/dirs.proj @@ -1,6 +1,8 @@ + + From a3c19c065e59510f19cec90f62cb75eef346a933 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 1 Oct 2026 18:42:20 -0600 Subject: [PATCH 09/22] Add opt-out instructions for the NativeAOT support --- .../template-release-notes.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.agents/skills/update-library-template/template-release-notes.md b/.agents/skills/update-library-template/template-release-notes.md index 7ce3f9c7..1a731e63 100644 --- a/.agents/skills/update-library-template/template-release-notes.md +++ b/.agents/skills/update-library-template/template-release-notes.md @@ -51,3 +51,31 @@ Apply the same conditional property to each shipping project that is intended to The new `test/AotCompatibilityTest` project catches NativeAOT compiler warnings that library analyzers may not report. Keep it in the solution, add every NativeAOT-compatible shipping assembly as a `TrimmerRootAssembly`, and retain its publishable entry in `test/dirs.proj`. CI validates this project through the existing traversal publish on Windows, Linux, and macOS; it is intentionally marked `IsTestProject=false` because publishing successfully is the test. + +## NativeAOT opt-out and AI merge guidance + +Not every repo wants to take on NativeAOT readiness or NativeAOT-published test validation. +If a receiving repo does not intend to support NativeAOT, opt out explicitly instead of inheriting the template's defaults. + +For test projects that should remain managed-only: + +* Set `false` in the project file or in a repo-level `Directory.Build.props`. +* Leave the managed test project in the solution so usual IL-based testing continues to work. +* Keep `dotnet test` and the managed coverage flow unchanged; only the NativeAOT publish step is disabled. + +For shipping libraries that do not want NativeAOT compatibility analysis: + +* Remove `IsAotCompatible` from the project, or set it only for specific target frameworks that truly support NativeAOT. +* Do not add new AOT compatibility warnings to the build if the library is not intended to be AOT-compatible. +* If the repo does not target .NET 8 or later, the property should be omitted entirely rather than applied unconditionally. + +For the compatibility project itself: + +* Remove the `test/AotCompatibilityTest` project from the solution if NativeAOT validation is out of scope. +* Remove its entry from `test/dirs.proj` so the traversal publish no longer publishes it. +* Keep the project only when the repo intends to validate shipping assemblies under NativeAOT in CI. + +When merging this template into a downstream repo, the AI assistant should ask whether NativeAOT is a relevant target for that repo before applying these changes if `IsAotCompatible` is not already set by any of the projects. +If the answer is no, it should apply the opt-out steps above instead of blindly keeping the NativeAOT defaults. +This is especially important for repos that are not shipping a NativeAOT-ready library or that cannot support the extra publish-time validation in CI. +If the repo operator is unsure about NativeAOT support, you might offer to test the repo for NativeAOT readiness by attempting a NativeAOT publish of the relevant projects and observing whether any compatibility warnings or errors are produced. From 5e9fca2e2173de7753112ea8447df56f06f17d87 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 1 Oct 2026 19:11:22 -0600 Subject: [PATCH 10/22] Improve release notes further --- .../skills/update-library-template/template-release-notes.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.agents/skills/update-library-template/template-release-notes.md b/.agents/skills/update-library-template/template-release-notes.md index 1a731e63..0ef41c2c 100644 --- a/.agents/skills/update-library-template/template-release-notes.md +++ b/.agents/skills/update-library-template/template-release-notes.md @@ -78,4 +78,7 @@ For the compatibility project itself: When merging this template into a downstream repo, the AI assistant should ask whether NativeAOT is a relevant target for that repo before applying these changes if `IsAotCompatible` is not already set by any of the projects. If the answer is no, it should apply the opt-out steps above instead of blindly keeping the NativeAOT defaults. This is especially important for repos that are not shipping a NativeAOT-ready library or that cannot support the extra publish-time validation in CI. -If the repo operator is unsure about NativeAOT support, you might offer to test the repo for NativeAOT readiness by attempting a NativeAOT publish of the relevant projects and observing whether any compatibility warnings or errors are produced. +If the repo owner is unsure about NativeAOT support, you might offer to test the repo for NativeAOT readiness by attempting a NativeAOT publish of the relevant projects and observing whether any compatibility warnings or errors are produced. + +In the event the user elects for NativeAOT targeting, make efforts to get tests to work under NativeAOT as well so the shipping code can confidently advertise its NativeAOT compatibility. +If aspects of the library or tests make successful builds, publish or test runs problematic, discuss this with the repo owner before making significant changes or giving up on NativeAOT support. From 3eebddd0f5a3f5cd314c97a2acbed43000cc29e3 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 1 Oct 2026 19:16:07 -0600 Subject: [PATCH 11/22] Build and publish in one step in Azure Pipelines --- azure-pipelines/dotnet.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml index d399b31b..91568abb 100644 --- a/azure-pipelines/dotnet.yml +++ b/azure-pipelines/dotnet.yml @@ -3,12 +3,13 @@ parameters: steps: -- script: dotnet build tools/dirs.proj -t:build,pack --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" - displayName: πŸ›  dotnet build +- ${{ if parameters.RunTests }}: + - script: dotnet build tools/dirs.proj -t:build,pack,publish --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" + displayName: πŸ›  dotnet build -- script: dotnet publish tools/dirs.proj --no-restore -c $(BuildConfiguration) - displayName: πŸ›  publish NativeAOT tests - condition: and(succeeded(), ${{ parameters.RunTests }}) +- ${{ if not(parameters.RunTests) }}: + - script: dotnet build tools/dirs.proj -t:build,pack --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" + displayName: πŸ›  dotnet build - powershell: tools/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults -IncludeNativeAOT displayName: πŸ§ͺ dotnet test From 8b0c0d02d2edcc103739fc044ff5d396af7c24fc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:45:45 -0600 Subject: [PATCH 12/22] Update mcr.microsoft.com/dotnet/sdk:10.0.401 Docker digest to 83e0db9 (582) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index e06f26ac..79c20cd8 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # Refer to https://hub.docker.com/_/microsoft-dotnet-sdk for available versions -FROM mcr.microsoft.com/dotnet/sdk:10.0.401@sha256:35d40304542c8689331f8cab17c65926cdf48fe711e289321d71924b230a7d29 +FROM mcr.microsoft.com/dotnet/sdk:10.0.401@sha256:83e0db97c45d2e39b80123fe42940a23c423405a17f80b608a4b8768033d6392 # Installing mono makes `dotnet test` work without errors even for net472. # But installing it takes a long time, so it's excluded by default. From 84e1db7701eb938e8d4e30fdbc6a331bdc9726cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:30:51 +0000 Subject: [PATCH 13/22] Update mcr.microsoft.com/dotnet/sdk:10.0.401 Docker digest to e70cdb7 (583) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 79c20cd8..d2602ed4 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # Refer to https://hub.docker.com/_/microsoft-dotnet-sdk for available versions -FROM mcr.microsoft.com/dotnet/sdk:10.0.401@sha256:83e0db97c45d2e39b80123fe42940a23c423405a17f80b608a4b8768033d6392 +FROM mcr.microsoft.com/dotnet/sdk:10.0.401@sha256:e70cdb7f80b0348f5cb85f19a8f670fca061f033d57eed12fa003d58b0e06317 # Installing mono makes `dotnet test` work without errors even for net472. # But installing it takes a long time, so it's excluded by default. From 84911e43f17e1de8b4f469e2bbe7addb6545bad3 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sat, 3 Oct 2026 09:41:33 -0600 Subject: [PATCH 14/22] Clarify API xml doc requirements --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 49f47191..397458e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,4 +72,4 @@ dotnet run --no-build -c Release --framework net8.0 -- --list-tests * Honor StyleCop rules and fix any reported build warnings *after* getting tests to pass. * In C# files, use namespace *statements* instead of namespace *blocks* for all new files. -* Add API doc comments to all new public and internal members. +* Add API doc comments to all new public and internal members in shipping code under `src`. Tests and samples do not require XML API documentation; do not add XML docs to test or sample members solely to satisfy this rule. From 580ec8a253aab34763f6a80fa82ae48563028782 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sun, 4 Oct 2026 12:16:08 -0600 Subject: [PATCH 15/22] Fix version.json schema URL --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 39add986..e75607ee 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", "version": "0.1-beta", "publicReleaseRefSpec": [ "^refs/heads/main$", From d9061ce383ef71884bbfcaecaa87cc340acdd8ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Oct 2026 20:54:32 +0000 Subject: [PATCH 16/22] Initial plan From 6b8242d5ce32d4e20c9cae4b66f7340eeeb3dd85 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 5 Oct 2026 15:36:17 -0600 Subject: [PATCH 17/22] chore: re-trigger CI after Copilot merge From b8820efd059e75aa05db9b3c205544f7399530a8 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:47:25 -0600 Subject: [PATCH 18/22] fix: exclude Mac tests from traversal build on non-macOS agents test/dirs.proj was pulling Xunit.StaFact.Tests.Mac into tools/dirs.proj on Linux/Windows. Keep Mac coverage on the dedicated macOS workflow step. --- test/dirs.proj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/dirs.proj b/test/dirs.proj index 250f1016..db402d51 100644 --- a/test/dirs.proj +++ b/test/dirs.proj @@ -1,7 +1,7 @@ - - + + From 18f7adad53b8532bd22c8455956006aa8a9eb1fe Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:57:45 -0600 Subject: [PATCH 19/22] fix: restore valid version.json publicReleaseRefSpec escapes Merge conflict resolution dropped backslashes in the v\\d+\\.\\d+ regex, producing invalid JSON and breaking nbgv inherit/walk. --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 7840de6c..53efd6e6 100644 --- a/version.json +++ b/version.json @@ -3,6 +3,6 @@ "version": "4.1-beta", "publicReleaseRefSpec": [ "^refs/heads/main$", - "^refs/heads/v\d+\.\d+$" + "^refs/heads/v\\d+\\.\\d+$" ] } From 4e091f96d3e56e17052903e352c0dd46a9e72aaf Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:06:28 -0600 Subject: [PATCH 20/22] fix: restore repo-specific content overwritten by Library.Template merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prefer the template only where the repo’s prior content was equivalent. Restore xunit (or classic VSTest) AGENTS.md guidance where tests did not migrate to TUnit, drop unused TUnit CPM package versions, and reinstate cloud-test filters and bespoke test runner behavior that the template overwrite removed. --- AGENTS.md | 36 ++++++++++++++++++++++-------------- Directory.Packages.props | 2 -- tools/dotnet-test-cloud.ps1 | 4 +++- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 397458e5..f06294ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,10 +13,11 @@ ## Testing -**IMPORTANT**: This repository uses TUnit with Microsoft.Testing.Platform (MTP v2). Traditional `--filter` syntax does NOT work. Use the options below instead. +**IMPORTANT**: This repository uses Microsoft.Testing.Platform (MTP v2) with xunit v3. Traditional `--filter` syntax does NOT work. Use the options below instead. * There should generally be one test project (under the `test` directory) per shipping project (under the `src` directory). Test projects are named after the project being tested with a `.Tests` suffix. -* Tests use TUnit with Microsoft.Testing.Platform (MTP v2), while retaining xUnit assertions. Traditional VSTest `--filter` syntax does NOT work. +* Tests use xunit v3 with Microsoft.Testing.Platform (MTP v2). Traditional VSTest `--filter` syntax does NOT work. +* Some tests are known to be unstable. When running tests, you should skip the unstable ones by using `-- --filter-not-trait "FailsInCloudTest=true"`. ### Running Tests @@ -27,49 +28,56 @@ dotnet test --no-build -c Release **Run tests for a specific test project**: ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release +dotnet test --project test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj --no-build -c Release ``` **Run a single test method**: ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --treenode-filter "/*/*/ClassName/MethodName" +dotnet test --project test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj --no-build -c Release -- --filter-method ClassName.MethodName ``` **Run all tests in a test class**: ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --treenode-filter "/*/*/ClassName/*" +dotnet test --project test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj --no-build -c Release -- --filter-class ClassName ``` **Run tests with wildcard matching** (supports wildcards at beginning and/or end): ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --treenode-filter "/*/*/*/*Pattern*" +dotnet test --project test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj --no-build -c Release -- --filter-method "*Pattern*" ``` -**Run tests with a specific property**: +**Run tests with a specific trait** (equivalent to category filtering): ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --treenode-filter "/*/*/*/*[PropertyName=value]" +dotnet test --project test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj --no-build -c Release -- --filter-trait "TraitName=value" +``` + +**Exclude tests with a specific trait** (skip unstable tests): +```bash +dotnet test --project test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj --no-build -c Release -- --filter-not-trait "TestCategory=FailsInCloudTest" ``` **Run tests for a specific framework only**: ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release --framework net8.0 +dotnet test --project test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj --no-build -c Release --framework net8.0 ``` **List all available tests without running them**: ```bash -cd test/Library.Tests +cd test/Xunit.StaFact.Tests dotnet run --no-build -c Release --framework net8.0 -- --list-tests ``` -**Key points about test filtering with TUnit / MTP v2**: +**Key points about test filtering with MTP v2 / xunit v3**: - Options after `--` are passed to the test runner, not to `dotnet test` -- Use `--treenode-filter` to select tests by assembly, namespace, class, method, or property +- Use `--filter-method`, `--filter-class`, `--filter-namespace` for simple filtering +- Use `--filter-trait` and `--filter-not-trait` for trait-based filtering (replaces `--filter "TestCategory=..."`) - Traditional VSTest `--filter` expressions do NOT work -- Wildcards `*` are supported in tree node segments +- Wildcards `*` are supported at the beginning and/or end of filter values +- Multiple simple filters of the same type use OR logic, different types combine with AND - See `--help` for query filter language for advanced scenarios ## Coding style * Honor StyleCop rules and fix any reported build warnings *after* getting tests to pass. * In C# files, use namespace *statements* instead of namespace *blocks* for all new files. -* Add API doc comments to all new public and internal members in shipping code under `src`. Tests and samples do not require XML API documentation; do not add XML docs to test or sample members solely to satisfy this rule. +* Add API doc comments to all new public and internal members. diff --git a/Directory.Packages.props b/Directory.Packages.props index c8858dfc..61eb67eb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,9 +25,7 @@ - - diff --git a/tools/dotnet-test-cloud.ps1 b/tools/dotnet-test-cloud.ps1 index 1a000768..363eaa39 100644 --- a/tools/dotnet-test-cloud.ps1 +++ b/tools/dotnet-test-cloud.ps1 @@ -101,6 +101,8 @@ if ($isMTP) { -c $Configuration ` -bl:"$testBinLog" ` -- ` + --filter-not-trait 'TestCategory=FailsInCloudTest' ` + --filter-not-trait 'TestCategory=FailureExpected' ` @mtpArgs ` @dumpSwitches ` @extraArgs @@ -147,7 +149,7 @@ if ($isMTP) { & $dotnet test $RepoRoot ` --no-build ` -c $Configuration ` - --filter "TestCategory!=FailsInCloudTest" ` + --filter "TestCategory!=FailsInCloudTest & TestCategory!=FailureExpected" ` --blame-hang-timeout 60s ` --blame-crash ` -bl:"$testBinLog" ` From ccabde72f6b4cfd597ab191909281945e1a76660 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:17:28 -0600 Subject: [PATCH 21/22] fix: build net8.0-windows test TFMs only on Windows Linux/macOS CI was failing with exit 150 when MTP tried to launch the net8.0-windows test host ("You must install or update .NET"). --- test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj b/test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj index 51f87864..e219f36a 100644 --- a/test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj +++ b/test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj @@ -1,7 +1,7 @@ ο»Ώ - net8.0;net8.0-windows - $(TargetFrameworks);net8.0-windows10.0.17763.0;net472 + net8.0 + net8.0;net8.0-windows;net8.0-windows10.0.17763.0;net472 Exe From 05c2761e0cc26d1b0917d7f03e36e9327c660862 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 5 Oct 2026 20:06:16 -0600 Subject: [PATCH 22/22] AOT.props: don't overwrite a test project's InvariantGlobalization/SatelliteResourceLanguages (#585) test/AOT.props is imported from test/Directory.Build.targets, which evaluates after the project body. Unconditional assignments therefore clobbered a project's explicit opt-out (e.g. InvariantGlobalization=false for culture-dependent tests). --- test/AOT.props | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/AOT.props b/test/AOT.props index 623ff24d..41b29eda 100644 --- a/test/AOT.props +++ b/test/AOT.props @@ -10,8 +10,9 @@ true - true - en + + true + en true