diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 5cdba8649b..8c657c2a04 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -306,18 +306,18 @@ stages:
condition: and(succeeded(), not(and(in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI'), eq(variables['Build.SourceBranch'], 'refs/heads/main'))))
jobs:
- # Lightweight job that classifies the PR diff into two scopes (product vs samples)
- # and exposes them as output variables. Downstream jobs use them to skip work that
+ # Lightweight job that classifies the PR diff into product, samples, and XML-documentation-only
+ # scopes and exposes them as output variables. Downstream jobs use them to skip work that
# the diff does not touch (so e.g. a samples-only Dependabot PR does not spin up
# the full Windows/Linux/macOS product matrix, and a product-only PR does not run
# the samples build).
#
# Fail-safe behaviour:
# - Non-PR triggers (manual queue, scheduled, etc.) leave SYSTEM_PULLREQUEST_TARGETBRANCH
- # empty, so we emit "true" for both flags and run everything.
+ # empty, so we emit "true" for both change flags and "false" for XML-documentation-only.
# - If the diff is empty (e.g. force-push to the same SHA), we also run everything.
- # - Downstream conditions use `ne(..., 'false')` rather than `eq(..., 'true')`
- # so a missing/empty output (e.g. detect step failed) still runs the job.
+ # - Downstream conditions treat missing product/sample outputs as changed and a missing
+ # XML-documentation-only output as false, so classification uncertainty runs full validation.
- job: DetectChanges
displayName: Detect changed paths
pool:
@@ -340,6 +340,7 @@ stages:
echo "Not a PR build (SYSTEM_PULLREQUEST_TARGETBRANCH is empty); running all jobs."
hasProduct=true
hasSamples=true
+ xmlDocsOnly=false
else
echo "PR target branch: ${target}"
git fetch --no-tags --depth=200 origin "${target}" 2>/dev/null \
@@ -354,6 +355,7 @@ stages:
echo "No files changed in diff; running all jobs."
hasProduct=true
hasSamples=true
+ xmlDocsOnly=false
else
# A file is a "samples-affecting" change if it lives under samples/** or it is
# one of the eng/ scripts that drives the samples build (build-samples.ps1).
@@ -369,13 +371,32 @@ stages:
else
hasSamples=false
fi
+
+ # Never let a PR classify itself with code from its own branch. Extract the classifier from the
+ # trusted target branch and fail closed when it is missing, broken, or returns an unexpected value.
+ classifier="${AGENT_TEMPDIRECTORY}/classify-xml-doc-change.ps1"
+ xmlDocsOnly=false
+ if ! git show "origin/${target}:eng/classify-xml-doc-change.ps1" > "${classifier}"; then
+ echo "##vso[task.logissue type=warning]The trusted target branch does not contain the XML documentation classifier; using full validation."
+ elif ! pwsh -NoProfile -File "${classifier}" -SelfTest; then
+ echo "##vso[task.logissue type=warning]The trusted XML documentation classifier failed its self-tests; using full validation."
+ elif ! xmlDocsOnly="$(pwsh -NoProfile -File "${classifier}" -Base "${base}" -Head HEAD -Repository "${BUILD_SOURCESDIRECTORY}")"; then
+ echo "##vso[task.logissue type=warning]XML documentation change classification did not run; using full validation."
+ xmlDocsOnly=false
+ fi
+ if [[ "${xmlDocsOnly}" != "true" && "${xmlDocsOnly}" != "false" ]]; then
+ echo "##vso[task.logissue type=warning]XML documentation change classification returned an unexpected value; using full validation."
+ xmlDocsOnly=false
+ fi
fi
fi
echo "hasProductChanges=${hasProduct}"
echo "hasSamplesChanges=${hasSamples}"
+ echo "xmlDocsOnly=${xmlDocsOnly}"
echo "##vso[task.setvariable variable=hasProductChanges;isOutput=true]${hasProduct}"
echo "##vso[task.setvariable variable=hasSamplesChanges;isOutput=true]${hasSamples}"
+ echo "##vso[task.setvariable variable=xmlDocsOnly;isOutput=true]${xmlDocsOnly}"
name: detect
displayName: Classify changed files
@@ -395,15 +416,17 @@ stages:
jobs:
- job: Windows
dependsOn: DetectChanges
- # Release always runs so every public PR computes and publishes coverage. Debug remains skippable for
- # samples-only PRs, preserving the changed-path optimization where it does not affect coverage.
- condition: and(succeededOrFailed(), or(eq(variables._BuildConfig, 'Release'), ne(dependencies.DetectChanges.outputs['detect.hasProductChanges'], 'false')))
+ # Release normally runs full validation and coverage for every PR, but XML-documentation-only PRs use
+ # it only for compiler and package validation. Debug remains skippable for samples-only and XML-doc-only PRs.
+ condition: and(succeededOrFailed(), or(eq(variables._BuildConfig, 'Release'), and(ne(dependencies.DetectChanges.outputs['detect.xmlDocsOnly'], 'true'), ne(dependencies.DetectChanges.outputs['detect.hasProductChanges'], 'false'))))
timeoutInMinutes: 90
pool:
name: NetCore-Public
demands: ImageOverride -equals windows.vs2026preview.scout.amd64.open
variables:
- template: /eng/pipelines/variables/test-env-vars.yml
+ - name: _XmlDocsOnly
+ value: $[ dependencies.DetectChanges.outputs['detect.xmlDocsOnly'] ]
strategy:
matrix:
Release:
@@ -750,7 +773,7 @@ stages:
inputs:
PathtoPublish: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)'
ArtifactName: TestResults_Windows_$(_BuildConfig)_Attempt$(System.JobAttempt)
- condition: and(always(), eq(variables.HasTestResults, 'true'))
+ condition: and(always(), ne(variables._XmlDocsOnly, 'true'), eq(variables.HasTestResults, 'true'))
# The integration suite only creates artifacts/tmp/$(_BuildConfig)/testsuite once it runs, and
# CopyFiles@2 hard-fails when SourceFolder does not exist. A Test failure early enough to skip the
@@ -815,7 +838,7 @@ stages:
- job: WindowsAppModel
displayName: Windows application-model acceptance
dependsOn: DetectChanges
- condition: and(succeeded(), or(ne(dependencies.DetectChanges.outputs['detect.hasProductChanges'], 'false'), ne(dependencies.DetectChanges.outputs['detect.hasSamplesChanges'], 'false')))
+ condition: and(succeeded(), ne(dependencies.DetectChanges.outputs['detect.xmlDocsOnly'], 'true'), or(ne(dependencies.DetectChanges.outputs['detect.hasProductChanges'], 'false'), ne(dependencies.DetectChanges.outputs['detect.hasSamplesChanges'], 'false')))
timeoutInMinutes: 120
pool:
name: NetCore-Public
@@ -836,7 +859,7 @@ stages:
- job: Linux
dependsOn: DetectChanges
- condition: and(succeededOrFailed(), ne(dependencies.DetectChanges.outputs['detect.hasProductChanges'], 'false'))
+ condition: and(succeededOrFailed(), ne(dependencies.DetectChanges.outputs['detect.xmlDocsOnly'], 'true'), ne(dependencies.DetectChanges.outputs['detect.hasProductChanges'], 'false'))
timeoutInMinutes: 90
pool:
name: NetCore-Public
@@ -864,7 +887,7 @@ stages:
- job: MacOS
dependsOn: DetectChanges
- condition: and(succeededOrFailed(), ne(dependencies.DetectChanges.outputs['detect.hasProductChanges'], 'false'))
+ condition: and(succeededOrFailed(), ne(dependencies.DetectChanges.outputs['detect.xmlDocsOnly'], 'true'), ne(dependencies.DetectChanges.outputs['detect.hasProductChanges'], 'false'))
# macOS agents are historically slow and flaky, so this job is intentionally non-blocking for PRs.
# Rather than continueOnError (which yields "PartiallySucceeded" and is still blocked by Azure Repos
# build-validation policies), every macOS command appends $(_MacOSNonBlockingTrailer) so a failed
diff --git a/eng/classify-xml-doc-change.ps1 b/eng/classify-xml-doc-change.ps1
new file mode 100644
index 0000000000..1ffc1bc8fc
--- /dev/null
+++ b/eng/classify-xml-doc-change.ps1
@@ -0,0 +1,390 @@
+[CmdletBinding(DefaultParameterSetName = "GitDiff")]
+param(
+ [Parameter(Mandatory, ParameterSetName = "GitDiff")]
+ [string]$Base,
+
+ [Parameter(Mandatory, ParameterSetName = "GitDiff")]
+ [string]$Head,
+
+ [Parameter(ParameterSetName = "GitDiff")]
+ [string]$Repository,
+
+ [Parameter(Mandatory, ParameterSetName = "SelfTest")]
+ [switch]$SelfTest
+)
+
+$ErrorActionPreference = "Stop"
+$PSNativeCommandUseErrorActionPreference = $false
+
+$roslynAssemblies = @(
+ (Join-Path $PSHOME "Microsoft.CodeAnalysis.dll"),
+ (Join-Path $PSHOME "Microsoft.CodeAnalysis.CSharp.dll")
+)
+foreach ($assembly in $roslynAssemblies) {
+ if (-not (Test-Path -LiteralPath $assembly -PathType Leaf)) {
+ throw "The PowerShell Roslyn assembly '$assembly' is unavailable."
+ }
+}
+
+Add-Type -Path $roslynAssemblies
+
+function Get-CSharpSyntaxKind {
+ param([Microsoft.CodeAnalysis.SyntaxTrivia]$Trivia)
+
+ return [Microsoft.CodeAnalysis.CSharp.SyntaxKind]$Trivia.RawKind
+}
+
+function Get-ComparedTrivia {
+ param(
+ [Microsoft.CodeAnalysis.SyntaxNode]$Root,
+ [switch]$Documentation
+ )
+
+ $descendIntoChildren = [Func[Microsoft.CodeAnalysis.SyntaxNode, bool]] { $true }
+ $result = [System.Collections.Generic.List[string]]::new()
+
+ $tokenIndex = 0
+ foreach ($token in $Root.DescendantTokens($descendIntoChildren, $false)) {
+ foreach ($side in @("Leading", "Trailing")) {
+ $triviaList = if ($side -eq "Leading") { $token.LeadingTrivia } else { $token.TrailingTrivia }
+ foreach ($trivia in $triviaList) {
+ $kind = Get-CSharpSyntaxKind $trivia
+ $isDocumentation = $kind -in @(
+ [Microsoft.CodeAnalysis.CSharp.SyntaxKind]::SingleLineDocumentationCommentTrivia,
+ [Microsoft.CodeAnalysis.CSharp.SyntaxKind]::MultiLineDocumentationCommentTrivia
+ )
+
+ if ($Documentation -ne $isDocumentation) {
+ continue
+ }
+
+ if (-not $Documentation -and $kind -in @(
+ [Microsoft.CodeAnalysis.CSharp.SyntaxKind]::WhitespaceTrivia,
+ [Microsoft.CodeAnalysis.CSharp.SyntaxKind]::EndOfLineTrivia
+ )) {
+ continue
+ }
+
+ $text = $trivia.ToFullString()
+ $result.Add("${tokenIndex}:${side}:$($trivia.RawKind):$($text.Length):$text")
+ }
+ }
+
+ $tokenIndex++
+ }
+
+ return $result
+}
+
+function Test-SequenceEqual {
+ param(
+ [string[]]$Left,
+ [string[]]$Right
+ )
+
+ if ($Left.Count -ne $Right.Count) {
+ return $false
+ }
+
+ for ($index = 0; $index -lt $Left.Count; $index++) {
+ if ($Left[$index] -cne $Right[$index]) {
+ return $false
+ }
+ }
+
+ return $true
+}
+
+function Test-XmlDocOnlyTextChange {
+ param(
+ [string]$OldText,
+ [string]$NewText
+ )
+
+ if ($OldText -ceq $NewText) {
+ return $false
+ }
+
+ $parseOptions = [Microsoft.CodeAnalysis.CSharp.CSharpParseOptions]::Default.WithDocumentationMode(
+ [Microsoft.CodeAnalysis.DocumentationMode]::Diagnose)
+ $oldTree = [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($OldText, $parseOptions)
+ $newTree = [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($NewText, $parseOptions)
+
+ $cancellationToken = [System.Threading.CancellationToken]::None
+ $hasParseErrors =
+ @($oldTree.GetDiagnostics($cancellationToken) | Where-Object Severity -eq Error).Count -ne 0 -or
+ @($newTree.GetDiagnostics($cancellationToken) | Where-Object Severity -eq Error).Count -ne 0
+ if ($hasParseErrors) {
+ return $false
+ }
+
+ $oldRoot = $oldTree.GetRoot()
+ $newRoot = $newTree.GetRoot()
+ if (-not [Microsoft.CodeAnalysis.CSharp.SyntaxFactory]::AreEquivalent($oldRoot, $newRoot)) {
+ return $false
+ }
+
+ $oldNonDocumentationTrivia = @(Get-ComparedTrivia $oldRoot)
+ $newNonDocumentationTrivia = @(Get-ComparedTrivia $newRoot)
+ if (-not (Test-SequenceEqual $oldNonDocumentationTrivia $newNonDocumentationTrivia)) {
+ return $false
+ }
+
+ $oldDocumentation = @(Get-ComparedTrivia $oldRoot -Documentation)
+ $newDocumentation = @(Get-ComparedTrivia $newRoot -Documentation)
+ if (@($oldDocumentation + $newDocumentation | Where-Object { $_ -cmatch '<\s*include\b' }).Count -ne 0) {
+ return $false
+ }
+
+ return -not (Test-SequenceEqual $oldDocumentation $newDocumentation)
+}
+
+function Test-EligiblePath {
+ param([string]$Path)
+
+ return (
+ [System.IO.Path]::GetExtension($Path) -ceq ".cs" -and
+ -not $Path.StartsWith("samples/", [System.StringComparison]::Ordinal))
+}
+
+function Invoke-Git {
+ param(
+ [string]$Repository,
+ [string[]]$Arguments
+ )
+
+ $output = @(& git -C $Repository @Arguments 2>&1)
+ if ($LASTEXITCODE -ne 0) {
+ throw "git $($Arguments -join ' ') failed: $($output -join [Environment]::NewLine)"
+ }
+
+ return $output
+}
+
+function Get-GitFileText {
+ param(
+ [string]$Repository,
+ [string]$Revision,
+ [string]$Path
+ )
+
+ return (Invoke-Git $Repository @("show", "--no-textconv", "${Revision}:$Path")) -join "`n"
+}
+
+function Test-GitDiff {
+ param(
+ [string]$BaseRevision,
+ [string]$HeadRevision,
+ [string]$Repository
+ )
+
+ if ([string]::IsNullOrEmpty($Repository)) {
+ $repositoryOutput = @(Invoke-Git $PSScriptRoot @("rev-parse", "--show-toplevel"))
+ $Repository = $repositoryOutput[0]
+ }
+
+ $changes = @(Invoke-Git $Repository @(
+ "diff",
+ "--name-status",
+ "--no-renames",
+ "--diff-filter=ACDMRTUXB",
+ $BaseRevision,
+ $HeadRevision,
+ "--"
+ ))
+
+ if ($changes.Count -eq 0) {
+ return $false
+ }
+
+ foreach ($change in $changes) {
+ $status, $path = $change -split "`t", 2
+ if ($status -ne "M" -or -not (Test-EligiblePath $path)) {
+ return $false
+ }
+
+ $oldText = Get-GitFileText $Repository $BaseRevision $path
+ $newText = Get-GitFileText $Repository $HeadRevision $path
+ if (-not (Test-XmlDocOnlyTextChange $oldText $newText)) {
+ return $false
+ }
+ }
+
+ return $true
+}
+
+function Invoke-GitDiffSelfTest {
+ $repository = Join-Path ([System.IO.Path]::GetTempPath()) "testfx-xml-doc-classifier-$([System.Guid]::NewGuid().ToString('N'))"
+ [System.IO.Directory]::CreateDirectory($repository) | Out-Null
+
+ try {
+ $null = Invoke-Git $repository @("init", "--quiet")
+ $null = Invoke-Git $repository @("config", "user.name", "XML documentation classifier")
+ $null = Invoke-Git $repository @("config", "user.email", "classifier@example.invalid")
+
+ $firstPath = Join-Path $repository "First.cs"
+ $secondPath = Join-Path $repository "Second.cs"
+ [System.IO.File]::WriteAllText($firstPath, "/// old first`nclass First { }`n")
+ [System.IO.File]::WriteAllText($secondPath, "/// old second`nclass Second { int Value => 1; }`n")
+ $null = Invoke-Git $repository @("add", "--all")
+ $null = Invoke-Git $repository @("commit", "--quiet", "-m", "Base")
+ $baseRevision = @(Invoke-Git $repository @("rev-parse", "HEAD"))[0]
+
+ [System.IO.File]::WriteAllText($firstPath, "/// new first`nclass First { }`n")
+ [System.IO.File]::WriteAllText($secondPath, "/// new second`nclass Second { int Value => 1; }`n")
+ $null = Invoke-Git $repository @("add", "--all")
+ $null = Invoke-Git $repository @("commit", "--quiet", "-m", "Documentation")
+ $documentationRevision = @(Invoke-Git $repository @("rev-parse", "HEAD"))[0]
+ if (-not (Test-GitDiff $baseRevision $documentationRevision $repository)) {
+ throw "Git self-test expected a two-file documentation-only diff to classify as true."
+ }
+
+ [System.IO.File]::WriteAllText($firstPath, "/// newest first`nclass First { }`n")
+ [System.IO.File]::WriteAllText($secondPath, "/// new second`nclass Second { int Value => 2; }`n")
+ $null = Invoke-Git $repository @("add", "--all")
+ $null = Invoke-Git $repository @("commit", "--quiet", "-m", "Mixed")
+ $mixedRevision = @(Invoke-Git $repository @("rev-parse", "HEAD"))[0]
+ if (Test-GitDiff $documentationRevision $mixedRevision $repository) {
+ throw "Git self-test expected a mixed documentation and code diff to classify as false."
+ }
+
+ $addedPath = Join-Path $repository "Added.cs"
+ [System.IO.File]::WriteAllText($addedPath, "/// added`nclass Added { }`n")
+ $null = Invoke-Git $repository @("add", "--all")
+ $null = Invoke-Git $repository @("commit", "--quiet", "-m", "Added")
+ $addedRevision = @(Invoke-Git $repository @("rev-parse", "HEAD"))[0]
+ if (Test-GitDiff $mixedRevision $addedRevision $repository) {
+ throw "Git self-test expected an added file to classify as false."
+ }
+
+ [System.IO.File]::Delete($addedPath)
+ $null = Invoke-Git $repository @("add", "--all")
+ $null = Invoke-Git $repository @("commit", "--quiet", "-m", "Deleted")
+ $deletedRevision = @(Invoke-Git $repository @("rev-parse", "HEAD"))[0]
+ if (Test-GitDiff $addedRevision $deletedRevision $repository) {
+ throw "Git self-test expected a deleted file to classify as false."
+ }
+ }
+ finally {
+ Remove-Item -LiteralPath $repository -Recurse -Force
+ }
+}
+
+function Invoke-SelfTest {
+ $cases = @(
+ @{
+ Name = "XML documentation content"
+ Expected = $true
+ Old = "class C {`n /// old`n void M() { }`n}"
+ New = "class C {`n /// new`n void M() { }`n}"
+ },
+ @{
+ Name = "Added XML documentation line"
+ Expected = $true
+ Old = "class C {`n /// first`n void M() { }`n}"
+ New = "class C {`n /// first`n /// second`n void M() { }`n}"
+ },
+ @{
+ Name = "Removed XML documentation"
+ Expected = $true
+ Old = "class C {`n /// removed`n void M() { }`n}"
+ New = "class C {`n void M() { }`n}"
+ },
+ @{
+ Name = "Multiline XML documentation"
+ Expected = $true
+ Old = "class C {`n /** old */`n void M() { }`n}"
+ New = "class C {`n /** new */`n void M() { }`n}"
+ },
+ @{
+ Name = "XML documentation include"
+ Expected = $false
+ Old = "class C {`n /// `n void M() { }`n}"
+ New = "class C {`n /// `n void M() { }`n}"
+ },
+ @{
+ Name = "Ordinary comment"
+ Expected = $false
+ Old = "class C {`n // old`n void M() { }`n}"
+ New = "class C {`n // new`n void M() { }`n}"
+ },
+ @{
+ Name = "Executable code"
+ Expected = $false
+ Old = "class C { int P => 1; }"
+ New = "class C { int P => 2; }"
+ },
+ @{
+ Name = "XML-looking raw string content"
+ Expected = $false
+ Old = "class C { string P => """"""`n/// old`n""""""; }"
+ New = "class C { string P => """"""`n/// new`n""""""; }"
+ },
+ @{
+ Name = "Mixed XML and ordinary comments"
+ Expected = $false
+ Old = "class C {`n /// old`n // old`n void M() { }`n}"
+ New = "class C {`n /// new`n // new`n void M() { }`n}"
+ },
+ @{
+ Name = "Whitespace only"
+ Expected = $false
+ Old = "class C { void M() { } }"
+ New = "class C { void M() { } }"
+ },
+ @{
+ Name = "Identical files"
+ Expected = $false
+ Old = "class C { }"
+ New = "class C { }"
+ },
+ @{
+ Name = "Mixed XML documentation and preprocessor directive"
+ Expected = $false
+ Old = "#if DEBUG`n/// old`nclass C { }`n#endif"
+ New = "#if RELEASE`n/// new`nclass C { }`n#endif"
+ },
+ @{
+ Name = "Relocated preprocessor directive"
+ Expected = $false
+ Old = "#nullable disable`n/// old`nclass C { }`nclass D { }"
+ New = "/// new`nclass C { }`n#nullable disable`nclass D { }"
+ }
+ )
+
+ foreach ($case in $cases) {
+ $actual = Test-XmlDocOnlyTextChange $case.Old $case.New
+ if ($actual -ne $case.Expected) {
+ throw "Self-test '$($case.Name)' expected '$($case.Expected)' but got '$actual'."
+ }
+ }
+
+ foreach ($pathCase in @(
+ @{ Path = "src/Product.cs"; Expected = $true },
+ @{ Path = "samples/public/Sample.cs"; Expected = $false },
+ @{ Path = "samples/internal/Sample.cs"; Expected = $false },
+ @{ Path = "src/Product.vb"; Expected = $false }
+ )) {
+ $actual = Test-EligiblePath $pathCase.Path
+ if ($actual -ne $pathCase.Expected) {
+ throw "Path self-test '$($pathCase.Path)' expected '$($pathCase.Expected)' but got '$actual'."
+ }
+ }
+
+ Invoke-GitDiffSelfTest
+
+ Write-Output "XML documentation change classifier self-tests passed."
+}
+
+if ($SelfTest) {
+ Invoke-SelfTest
+ exit 0
+}
+
+try {
+ Write-Output ((Test-GitDiff $Base $Head $Repository).ToString().ToLowerInvariant())
+}
+catch {
+ [Console]::Error.WriteLine("##vso[task.logissue type=warning]XML documentation change classification failed; running full validation. $($_.Exception.Message)")
+ Write-Output "false"
+}
diff --git a/eng/pipelines/steps/test-windows-configuration-tests.yml b/eng/pipelines/steps/test-windows-configuration-tests.yml
index 611341cde9..7a818b8925 100644
--- a/eng/pipelines/steps/test-windows-configuration-tests.yml
+++ b/eng/pipelines/steps/test-windows-configuration-tests.yml
@@ -30,7 +30,7 @@ steps:
dotnet test -c $(_BuildConfig) --no-build -bl:$(BUILD.SOURCESDIRECTORY)\artifacts\TestResults\$(_BuildConfig)\TestStep.binlog -p:UsingDotNetTest=true -p:TestingPlatformCaptureOutput=false
name: Test
displayName: Test
- condition: and(succeeded(), eq(variables._BuildConfig, 'Release'))
+ condition: and(succeeded(), eq(variables._BuildConfig, 'Release'), ne(variables._XmlDocsOnly, 'true'))
env:
# Secret variables are not automatically exposed to scripts. Fork PR builds do not receive this token,
# so report-azdo history queries no-op there; trusted branch builds exercise them end-to-end.
@@ -48,14 +48,14 @@ steps:
"affected-tests" | "${{ parameters.affectedTestsCacheVersion }}" | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | "$(_BuildConfig)"
path: '$(Pipeline.Workspace)\affected-test-map'
cacheHitVar: AffectedTestsMapCacheRestored
- condition: and(succeeded(), eq(variables._BuildConfig, 'Release'))
+ condition: and(succeeded(), eq(variables._BuildConfig, 'Release'), ne(variables._XmlDocsOnly, 'true'))
- script: |
echo ##vso[task.setvariable variable=PublishCoverageReport]true
dotnet test -c $(_BuildConfig) --no-build -bl:$(BUILD.SOURCESDIRECTORY)\artifacts\TestResults\$(_BuildConfig)\TestStep.binlog -p:UsingDotNetTest=true -p:TestingPlatformCaptureOutput=false --collect-test-map
name: Test
displayName: Test and collect affected-test map
- condition: and(succeeded(), eq(variables._BuildConfig, 'Release'))
+ condition: and(succeeded(), eq(variables._BuildConfig, 'Release'), ne(variables._XmlDocsOnly, 'true'))
env:
DOTNET_CLI_ENABLE_AFFECTED_TESTS: 1
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
@@ -69,7 +69,7 @@ steps:
"affected-tests" | "${{ parameters.affectedTestsCacheVersion }}" | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | "$(_BuildConfig)"
path: '$(Pipeline.Workspace)\affected-test-map'
cacheHitVar: AffectedTestsMapCacheRestored
- condition: and(succeeded(), eq(variables._BuildConfig, 'Release'), eq(variables['Build.Reason'], 'PullRequest'))
+ condition: and(succeeded(), eq(variables._BuildConfig, 'Release'), ne(variables._XmlDocsOnly, 'true'), eq(variables['Build.Reason'], 'PullRequest'))
- pwsh: |
dotnet test -c $(_BuildConfig) --no-build -bl:$(BUILD.SOURCESDIRECTORY)\artifacts\TestResults\$(_BuildConfig)\TestStep.binlog -p:UsingDotNetTest=true -p:TestingPlatformCaptureOutput=false --affected-tests
@@ -88,7 +88,7 @@ steps:
exit 0
name: TestAffected
displayName: Test affected changes
- condition: and(succeeded(), eq(variables._BuildConfig, 'Release'), eq(variables['Build.Reason'], 'PullRequest'), ne(variables['AffectedTestsMapCacheRestored'], 'false'))
+ condition: and(succeeded(), eq(variables._BuildConfig, 'Release'), ne(variables._XmlDocsOnly, 'true'), eq(variables['Build.Reason'], 'PullRequest'), ne(variables['AffectedTestsMapCacheRestored'], 'false'))
env:
DOTNET_CLI_ENABLE_AFFECTED_TESTS: 1
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
@@ -108,7 +108,7 @@ steps:
exit $LASTEXITCODE
name: Test
displayName: Test (affected-test fallback)
- condition: and(succeeded(), eq(variables._BuildConfig, 'Release'), or(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['AffectedTestsMapCacheRestored'], 'false'), ne(variables['AffectedTestsSucceeded'], 'true')))
+ condition: and(succeeded(), eq(variables._BuildConfig, 'Release'), ne(variables._XmlDocsOnly, 'true'), or(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['AffectedTestsMapCacheRestored'], 'false'), ne(variables['AffectedTestsSucceeded'], 'true')))
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
@@ -134,7 +134,7 @@ steps:
/p:MSBuildCacheEnabled=false
exit $LASTEXITCODE
displayName: Prepare cached Debug test runtime
- condition: and(succeeded(), eq(variables._BuildConfig, 'Debug'), eq(variables['MSBuildCacheBuildSucceeded'], 'true'))
+ condition: and(succeeded(), eq(variables._BuildConfig, 'Debug'), ne(variables._XmlDocsOnly, 'true'), eq(variables['MSBuildCacheBuildSucceeded'], 'true'))
# The Debug leg exists to exercise DEBUG-conditional product code and tests. The Release integration suite
# already creates child test assets in both configurations, so repeating that suite in Debug is redundant.
@@ -142,7 +142,7 @@ steps:
- script: dotnet test --no-build --test-modules "artifacts/bin/*UnitTests*/$(_BuildConfig)/**/*UnitTests.exe" --results-directory $(BUILD.SOURCESDIRECTORY)\artifacts\TestResults\$(_BuildConfig) $(_DebugUnitTestReportArgs) --diagnostic --diagnostic-output-directory $(BUILD.SOURCESDIRECTORY)\artifacts\log\$(_BuildConfig) --diagnostic-verbosity trace
name: TestDebug
displayName: Test (unit tests only)
- condition: and(succeeded(), eq(variables._BuildConfig, 'Debug'))
+ condition: and(succeeded(), eq(variables._BuildConfig, 'Debug'), ne(variables._XmlDocsOnly, 'true'))
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)