Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@
<PackageReference Include="Microsoft.DotNet.Build.Tasks.Templating" Version="$(MicrosoftDotNetBuildTasksTemplatingPackageVersion)" AllowExplicitReference="true" PrivateAssets="All" IsImplicitlyDefined="true" />
</ItemGroup>

<ItemGroup Condition=" '$(IsTestingPlatformApplication)' == 'true'
and '$([MSBuild]::GetTargetFrameworkIdentifier($(TargetFramework)))' == '.NETCoreApp' ">
<PackageReference Include="Microsoft.Testing.Extensions.AffectedTests" />
<PackageReference Include="Microsoft.Testing.Extensions.AffectedTests.Storage.AzureDevOps" />
</ItemGroup>
Comment on lines +75 to +79

<PropertyGroup Condition=" '$(AffectedTestsCoreOnly)' == 'true'
and '$(TargetFrameworkIdentifier)' != '.NETCoreApp' ">
<IsTestProject>false</IsTestProject>
<IsTestingPlatformApplication>false</IsTestingPlatformApplication>
</PropertyGroup>

<PropertyGroup Condition=" '$(AffectedTestsNonCoreOnly)' == 'true'
and '$(TargetFrameworkIdentifier)' == '.NETCoreApp' ">
<IsTestProject>false</IsTestProject>
<IsTestingPlatformApplication>false</IsTestingPlatformApplication>
</PropertyGroup>

<Target Name="_GenerateVersionSourceFileCache">
<PropertyGroup>
<_TemplateProperties>Version=$(Version)</_TemplateProperties>
Expand Down
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.4" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.1.0" />
<PackageVersion Include="Azure.Core" Version="1.62.0" />
<PackageVersion Include="Microsoft.Testing.Extensions.AffectedTests" Version="$(MicrosoftTestingExtensionsCodeCoverageVersion)" />
<PackageVersion Include="Microsoft.Testing.Extensions.AffectedTests.Storage.AzureDevOps" Version="$(MicrosoftTestingExtensionsCodeCoverageVersion)" />
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="$(MicrosoftTestingExtensionsCodeCoverageVersion)" />
<PackageVersion Include="Microsoft.TestPlatform.ObjectModel" Version="$(MicrosoftNETTestSdkVersion)" />
<PackageVersion Include="Microsoft.TestPlatform.TranslationLayer" Version="$(MicrosoftNETTestSdkVersion)" />
Expand Down
2 changes: 1 addition & 1 deletion azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ stages:

- template: /eng/pipelines/steps/test-windows-configuration-tests.yml
parameters:
# Enable only after the prerequisites in docs/affected-test-selection.md are satisfied.
# The package is wired, but collection remains disabled until its discovery child connects reliably.
enableAffectedTests: false
${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}:
affectedTestsMode: collect
Expand Down
87 changes: 37 additions & 50 deletions docs/affected-test-selection.md
Original file line number Diff line number Diff line change
@@ -1,68 +1,55 @@
# Affected-test selection rollout

This repository is prepared to adopt the experimental affected-test workflow from
This repository is wired for the experimental affected-test workflow from
[dotnet/sdk#55574](https://github.com/dotnet/sdk/pull/55574). The workflow is Microsoft.Testing.Platform-only and
builds on the composable filter-provider support from
[testfx#10235](https://github.com/microsoft/testfx/pull/10235).

The rollout is intentionally disabled. SDK `11.0.100-rc.1.26406.108` contains the affected-test commands but fails
`dotnet tool restore` on clean agents because it predates
[dotnet/sdk#55595](https://github.com/dotnet/sdk/pull/55595). The repository remains on the stable SDK until a fixed
daily is published. The affected-test extension package and its public local-filesystem storage contract are also not
available yet. Ordinary test commands therefore remain unchanged.

## Prepared layout

- `global.json` defines the repository-specific `test.affectedTests` change and instrumentation scopes.
- The trusted main-branch Windows Release test is the future `--collect-test-map` entry point.
- The Windows Release PR test is the future `--affected-tests` entry point.
- The shared Windows test call site passes `enableAffectedTests: false` and selects the mode from the source branch. The
inactive template branches restore the map through
Azure Pipelines `Cache@2` and set `DOTNET_CLI_ENABLE_AFFECTED_TESTS=1` only for the affected-test commands.
- `eng/validate-affected-tests.ps1` protects the disabled state and verifies that the public SDK gate and command names
do not drift.
The repository consumes `Microsoft.Testing.Extensions.AffectedTests` from the `test-tools` feed at the same version as
`Microsoft.Testing.Extensions.CodeCoverage`, plus the provider-specific
`Microsoft.Testing.Extensions.AffectedTests.Storage.AzureDevOps` package. The provider publishes versioned per-module
mapping shards to the `TestFx_AffectedTestsMaps` artifact produced by pipeline definition 209. Its builder hook is
registered by the repository's hand-authored MTP entry points.

Selection remains disabled at the shared pipeline call site. Package
`18.12.0-preview.26466.2` starts collection, but its internal `--list-tests` discovery child exits successfully before
connecting to the extension's discovery pipe, so the parent reports
`Test discovery child exited with code 0 before connecting.` and fails the run. Ordinary full-test CI remains active
until a package containing a working discovery handshake is available.

## CI layout

- `global.json` defines the repository-specific `test.affectedTests` change policy and selects Azure DevOps artifact
storage for the public `microsoft.testfx` pipeline.
- Once enabled, the trusted main-branch Windows Release test runs `--collect-test-map`.
- Once enabled, the Windows Release PR test runs `--affected-tests`.
- The package targets .NET 8 and later, so .NET Framework test modules continue to run in full before the affected-test
step. Collection, selection, and fallback commands are scoped to .NETCoreApp modules.
- The shared Windows test call site selects the mode from the source branch, supplies Azure DevOps build identity and
the scoped system access token, and sets `DOTNET_CLI_ENABLE_AFFECTED_TESTS=1` only for affected-test commands.
- `eng/validate-affected-tests.ps1` verifies the SDK gate, package reference, storage configuration, pipeline wiring,
command names, and fallbacks.

`DOTNET_CLI_TEST_AFFECTED_TESTS_MODE` is an SDK-to-extension authorization marker. Repository scripts and pipeline
definitions must not set it.

## Storage design

The map should use the extension's local-filesystem provider rooted at
`$(Pipeline.Workspace)\affected-test-map`. Azure Pipelines `Cache@2` transfers that directory between runs without
credentials:
The map uses the extension's Azure DevOps artifact provider:

- trusted main builds can restore the previous map and publish a new immutable cache entry;
- PR and fork-PR builds can read the target branch's cache scope but cannot write to it;
- the cache prefix includes its manual compatibility version, OS, architecture, and configuration;
- the unique build ID suffix lets every successful main collection publish a new map;
- prefix restore selects the newest compatible map.
- trusted main builds publish each module shard beneath the `TestFx_AffectedTestsMaps` artifact;
- PR builds query successful builds from definition 209 and select the newest artifact whose baseline commit is an
ancestor of the PR checkout;
- fork PRs receive no access token, so unavailable storage safely selects all tests rather than exposing credentials;
- mapping identity includes the repository, module, target/runtime, test metadata, policy, and storage compatibility
fields, so incompatible shards are rejected conservatively.

Azure Pipelines caches expire after seven days without activity. A cache miss is therefore an expected state, not a
test failure: the PR lane runs the unchanged full test command. The same fallback runs when the extension rejects a
missing, stale, or incompatible map, and scheduled or manual builds always keep full validation.
A missing, stale, incompatible, or inaccessible artifact is an expected state, not a partial selection: the extension
runs all tests. The pipeline's explicit full-test fallback remains for extension/process failures, while scheduled and
manual builds always keep full validation.

Selected-test runs do not publish their partial coverage as the repository coverage report. Collection and full
fallback runs still publish complete coverage.

Pipeline artifacts should contain only non-secret diagnostics or a mapping snapshot suitable for troubleshooting.
They are not the cross-run source of truth because artifact lookup and retention are tied to individual builds.

The `storage` property is deliberately absent from `test.affectedTests` until the extension package publishes the exact
local-filesystem provider schema. Adding an invented provider or path setting now would create configuration that
cannot be validated.

## Activation checklist

1. Update `global.json` to an SDK newer than `11.0.100-rc.1.26406.108` that contains dotnet/sdk#55595, then validate
`dotnet tool restore` on a clean agent.
2. Add the publicly available affected-test extension package through `Directory.Packages.props` and the test project
infrastructure, following the repository's normal dependency-flow and package-source policy.
3. Add `test.affectedTests.storage` using the package's published local-filesystem schema and point it at the Pipeline
Cache directory.
4. Update `affectedTestsCacheVersion` whenever the persisted map format or its compatibility dimensions change.
5. Set `enableAffectedTests` to `true` in the shared Windows test call site so main builds collect maps and PR builds use
them. Publish non-secret collection diagnostics as an Azure DevOps artifact.
6. After a compatible map exists, validate PR selection. Keep the full test command available as an explicit rollback by
setting `enableAffectedTests` back to `false`.
7. Validate a documentation-only change, a product change with a narrow affected set, a force-all change, a missing or
incompatible map, a fork PR without secrets, and a collection failure before making selection required.
The one-switch rollback remains setting `enableAffectedTests` to `false`, which keeps the package and dormant storage
configuration in place while restoring the ordinary full-test command.
55 changes: 26 additions & 29 deletions eng/pipelines/steps/test-windows-configuration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ parameters:
- disabled
- collect
- run
- name: affectedTestsCacheVersion
type: string
default: v1

steps:
# Because the build step is using -ci, restore is done in a local .packages directory.
Expand All @@ -36,43 +33,38 @@ steps:
# so report-azdo history queries no-op there; trusted branch builds exercise them end-to-end.
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

# These branches are intentionally disabled at their call sites. They can be enabled only after the affected-test
# extension package and its local-filesystem storage schema have flowed into this repository. The SDK owns
# These branches implement affected-test collection and selection. The SDK owns
# DOTNET_CLI_TEST_AFFECTED_TESTS_MODE; pipelines must not set it.
- ${{ if and(eq(parameters.enableAffectedTests, true), eq(parameters.affectedTestsMode, 'collect')) }}:
- task: Cache@2
displayName: Restore and publish affected-test map
inputs:
key: '"affected-tests" | "${{ parameters.affectedTestsCacheVersion }}" | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | "$(_BuildConfig)" | "$(Build.BuildId)"'
restoreKeys: |
"affected-tests" | "${{ parameters.affectedTestsCacheVersion }}" | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | "$(_BuildConfig)"
path: '$(Pipeline.Workspace)\affected-test-map'
cacheHitVar: AffectedTestsMapCacheRestored
- ${{ if and(eq(parameters.enableAffectedTests, true), ne(parameters.affectedTestsMode, 'disabled')) }}:
# The affected-tests extension targets .NET 8 and later. Keep the .NET Framework coverage by running those
# test modules normally, then let the collect/run/fallback steps below handle only .NETCoreApp modules.
- script: |
dotnet test --solution TestFx.slnx -c $(_BuildConfig) --no-build -bl:$(BUILD.SOURCESDIRECTORY)\artifacts\TestResults\$(_BuildConfig)\TestNonCore.binlog -p:UsingDotNetTest=true -p:TestingPlatformCaptureOutput=false -p:AffectedTestsNonCoreOnly=true --show-test-results failed
name: TestNonCore
displayName: Test .NET Framework modules
condition: and(succeeded(), eq(variables._BuildConfig, 'Release'), ne(variables._XmlDocsOnly, 'true'))
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

- ${{ if and(eq(parameters.enableAffectedTests, true), eq(parameters.affectedTestsMode, 'collect')) }}:
- script: |
echo ##vso[task.setvariable variable=PublishCoverageReport]true
dotnet test --solution TestFx.slnx -c $(_BuildConfig) --no-build -bl:$(BUILD.SOURCESDIRECTORY)\artifacts\TestResults\$(_BuildConfig)\TestStep.binlog -p:UsingDotNetTest=true -p:TestingPlatformCaptureOutput=false --show-test-results failed --collect-test-map
dotnet test --solution TestFx.slnx -c $(_BuildConfig) --no-build -bl:$(BUILD.SOURCESDIRECTORY)\artifacts\TestResults\$(_BuildConfig)\TestCollect.binlog -p:UsingDotNetTest=true -p:TestingPlatformCaptureOutput=false -p:AffectedTestsCoreOnly=true --show-test-results failed --collect-test-map
name: Test
displayName: Test and collect affected-test map
condition: and(succeeded(), eq(variables._BuildConfig, 'Release'), ne(variables._XmlDocsOnly, 'true'))
env:
DOTNET_CLI_ENABLE_AFFECTED_TESTS: 1
CTS_ACCESSTOKEN: $(System.AccessToken)
CTS_COLLECTIONURI: $(System.CollectionUri)
CTS_TEMPDIRECTORY: $(Agent.TempDirectory)
CTS_BUILDID: $(Build.BuildId)
BUILD_CONTAINERID: $(Build.ContainerId)
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

- ${{ if and(eq(parameters.enableAffectedTests, true), eq(parameters.affectedTestsMode, 'run')) }}:
- task: Cache@2
displayName: Restore affected-test map
inputs:
key: '"affected-tests" | "${{ parameters.affectedTestsCacheVersion }}" | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | "$(_BuildConfig)" | "$(Build.BuildId)"'
restoreKeys: |
"affected-tests" | "${{ parameters.affectedTestsCacheVersion }}" | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | "$(_BuildConfig)"
path: '$(Pipeline.Workspace)\affected-test-map'
cacheHitVar: AffectedTestsMapCacheRestored
condition: and(succeeded(), eq(variables._BuildConfig, 'Release'), ne(variables._XmlDocsOnly, 'true'), eq(variables['Build.Reason'], 'PullRequest'))

- pwsh: |
dotnet test --solution TestFx.slnx -c $(_BuildConfig) --no-build -bl:$(BUILD.SOURCESDIRECTORY)\artifacts\TestResults\$(_BuildConfig)\TestStep.binlog -p:UsingDotNetTest=true -p:TestingPlatformCaptureOutput=false --show-test-results failed --affected-tests
dotnet test --solution TestFx.slnx -c $(_BuildConfig) --no-build -bl:$(BUILD.SOURCESDIRECTORY)\artifacts\TestResults\$(_BuildConfig)\TestAffected.binlog -p:UsingDotNetTest=true -p:TestingPlatformCaptureOutput=false -p:AffectedTestsCoreOnly=true --show-test-results failed --affected-tests
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
Write-Host "##vso[task.setvariable variable=AffectedTestsSucceeded]true"
Expand All @@ -88,9 +80,14 @@ steps:
exit 0
name: TestAffected
displayName: Test affected changes
condition: and(succeeded(), eq(variables._BuildConfig, 'Release'), ne(variables._XmlDocsOnly, 'true'), 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'))
env:
DOTNET_CLI_ENABLE_AFFECTED_TESTS: 1
CTS_ACCESSTOKEN: $(System.AccessToken)
CTS_COLLECTIONURI: $(System.CollectionUri)
CTS_TEMPDIRECTORY: $(Agent.TempDirectory)
CTS_BUILDID: $(Build.BuildId)
BUILD_CONTAINERID: $(Build.ContainerId)
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

# Scheduled and manual runs keep their full-validation contract. PRs also fall back when the cache is absent or the
Expand All @@ -104,11 +101,11 @@ steps:
Remove-Item -Force
}

dotnet test --solution TestFx.slnx -c $(_BuildConfig) --no-build -bl:$(BUILD.SOURCESDIRECTORY)\artifacts\TestResults\$(_BuildConfig)\TestStep.binlog -p:UsingDotNetTest=true -p:TestingPlatformCaptureOutput=false --show-test-results failed
dotnet test --solution TestFx.slnx -c $(_BuildConfig) --no-build -bl:$(BUILD.SOURCESDIRECTORY)\artifacts\TestResults\$(_BuildConfig)\TestFallback.binlog -p:UsingDotNetTest=true -p:TestingPlatformCaptureOutput=false -p:AffectedTestsCoreOnly=true --show-test-results failed
exit $LASTEXITCODE
name: Test
displayName: Test (affected-test fallback)
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')))
condition: and(succeeded(), eq(variables._BuildConfig, 'Release'), ne(variables._XmlDocsOnly, 'true'), or(ne(variables['Build.Reason'], 'PullRequest'), ne(variables['AffectedTestsSucceeded'], 'true')))
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

Expand Down
Loading
Loading