Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 16, 2025

Add CI test splitting infrastructure

Summary

  • Introduces a unified test matrix generation system that enables splitting large test projects into parallel CI jobs
  • Will allow reducing Aspire.Hosting.Tests wall-clock time from ~17 minutes to multiple ~5 minute parallel jobs
  • Consolidates test enumeration from 3 separate per-OS setup jobs into a single platform-agnostic pipeline

Motivation

The Aspire.Hosting.Tests project had grown large enough that it was becoming a bottleneck in CI. Rather than adding ad-hoc parallelism, this PR introduces a general-purpose infrastructure that:

  1. Allows any test project to opt into parallel splitting
  2. Keeps the matrix definition platform-agnostic (enabling future Azure DevOps support)
  3. Cleans up the existing test workflow by removing duplicated per-OS setup jobs

How it works

┌─────────────────────────────────────────────────────────────────────┐
│                         MSBuild Phase                               │
│  (TestEnumerationRunsheetBuilder.targets + build-test-matrix.ps1)   │
│                                                                     │
│  .tests-metadata.json  →  .tests-partitions.json  →  canonical-     │
│  (per project)            (split projects)           test-matrix.json
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    Platform-Specific Expansion                      │
│                                                                     │
│  expand-test-matrix-github.ps1                                      │
│  • Maps OS names to GitHub runners                                  │
│  • Outputs { "include": [...] } format                              │
└─────────────────────────────────────────────────────────────────────┘

Key design decisions:

  1. Canonical matrix format: The core matrix is platform-agnostic JSON with supportedOSes arrays. Thin per-platform scripts expand this into CI-specific formats. This keeps 90% of the logic shared and makes adding Azure DevOps support straightforward.

  2. uncollected:* safety net: When using partition traits, tests without a [Trait("Partition", "...")] attribute automatically run in a dedicated "uncollected" job. This ensures no tests are silently skipped.

How to enable splitting for a test project

Add to the .csproj:

<PropertyGroup>
  <SplitTestsOnCI>true</SplitTestsOnCI>
  <TestClassNamePrefixForCI>Aspire.YourProject.Tests</TestClassNamePrefixForCI>
</PropertyGroup>

The infrastructure will automatically split by test class. For explicit control, add [Trait("Partition", "GroupName")] to test classes (follow-up PR).

What's included

Component Description
eng/TestEnumerationRunsheetBuilder/ MSBuild targets for test enumeration
eng/scripts/build-test-matrix.ps1 Generates canonical matrix from metadata
eng/scripts/expand-test-matrix-github.ps1 Expands matrix for GitHub Actions
eng/scripts/split-test-projects-for-ci.ps1 Discovers partitions/classes for splitting
tools/ExtractTestPartitions/ Tool to extract [Trait("Partition", ...)] from assemblies
.github/actions/enumerate-tests/ Updated GitHub Action
.github/workflows/tests.yml Simplified workflow using unified matrix
eng/TestingOnCI.md Documentation

Test plan

  • CI passes on this branch
  • Aspire.Hosting.Tests runs as multiple parallel jobs
  • Aspire.Templates.Tests runs as multiple parallel jobs
  • Aspire.Cli.EndToEnd.Tests runs as multiple parallel jobs
  • No tests are silently skipped (verify via test counts)

Future work

  • Add [Trait("Partition", "...")] attributes for logical test grouping (follow-up PR)
  • Add expand-test-matrix-azdo.ps1 for Azure DevOps support

@radical radical changed the base branch from copilot/add-test-splitting-infrastructure to main October 16, 2025 20:21
Copilot AI changed the title [WIP] Add test splitting infrastructure as per documentation Add test splitting infrastructure with auto-detection support Oct 16, 2025
Copilot AI requested a review from radical October 16, 2025 20:45
@github-actions
Copy link
Contributor

github-actions bot commented Oct 16, 2025

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 12101

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 12101"

1 similar comment
@github-actions
Copy link
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 12101

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 12101"

@radical
Copy link
Member

radical commented Oct 16, 2025

@copilot Implement (1). And add [Collection] with unique collection names on Aspire.Hosting.Tests.WaitForTests and Aspire.Hosting.Tests.DistributedApplicationTests.

Validate that the generation of tests matrix works correctly. Validate that it gets correctly consumed by the gh actions, yml.

Also make sure the trx filenames are valid for the platform, so escape characters correctly.

Copy link
Contributor Author

Copilot AI commented Oct 16, 2025

@copilot Implement (1). And add [Collection] with unique collection names on Aspire.Hosting.Tests.WaitForTests and Aspire.Hosting.Tests.DistributedApplicationTests.

Validate that the generation...

Implemented all requested changes in commit 85eab86:

  1. Collection attributes added:

    • WaitForTests[Collection("WaitForTests")]
    • DistributedApplicationTests[Collection("DistributedApplicationTests")]
  2. Test splitting enabled for Aspire.Hosting.Tests:

    • Matrix generates 66 jobs (40 Hosting + 26 Templates)
    • Configured with 30m/15m timeouts
  3. GitHub Actions integration:

    • Updated enumerate-tests action to generate and output split_tests_matrix
    • Added split_tests_{lin,macos,win} jobs to tests.yml
    • Jobs consume matrix from split-tests-matrix.json
  4. TRX filenames are platform-safe:

    • Format: {ProjectName}_{ShortName}.trx (e.g., Aspire.Hosting.Tests_WaitForTests.trx)
    • Uses only alphanumeric and underscores

Note: Collection detection via xUnit v3 --list-tests doesn't output "Collection:" banners like v2. The system correctly falls back to class-based splitting, which still provides full parallelization benefits.

Copilot AI changed the title Add test splitting infrastructure with auto-detection support Add test splitting infrastructure with auto-detection support and GitHub Actions integration Oct 16, 2025
@radical
Copy link
Member

radical commented Oct 16, 2025

@copilot Two issues:

  1. the split tests aren't running because we get --filter-class Aspire.Templates.Tests.class:Aspire.Templates.Tests.XUnit_V3MTP_NewUpAndBuildSupportProjectTemplatesTests instead of --filter-class Aspire.Templates.Tests.XUnit_V3MTP_NewUpAndBuildSupportProjectTemplatesTests.
  2. Collection extraction was skipped because xunit command doesn't provide a way to query that. Instead we can:
  • Add a tool in tools/ to extract the Collection names. If adding just [Collection("foo")] is not enough then fallback to using a trait like [Trait("Partition", "foo")]. And the tool that find the values of these traits.
  • Then the tests will be split into - 1 split per unique partition name, and 1 for rest of tests with no partition set

Implement this.

Validate

  • Validate that the test split generation works correctly.
  • Validate that the generated command line from the matrix json is correct and works

Copy link
Contributor Author

Copilot AI commented Oct 16, 2025

@copilot Two issues:

  1. the split tests aren't running because we get --filter-class Aspire.Templates.Tests.class:Aspire.Templates.Tests.XUnit_V3MTP_NewUpAndBuildSupportProjectTemplatesTests instea...

Fixed both issues in commit da9cbb3:

Issue 1 - Fixed filterArg format:

  • Updated enumerate-tests action to strip class:, collection:, or uncollected: prefixes
  • Filters now correctly show --filter-class "Aspire.Templates.Tests.ClassName" without duplication

Issue 2 - Collection extraction tool:

  • Created tools/ExtractTestPartitions that uses reflection to extract [Collection] and [Trait("Partition", ...)] attributes
  • Updated discovery script to call the tool before falling back to class-based mode
  • Added [Trait("Partition", "name")] to WaitForTests, DistributedApplicationTests, and SlimTestProgramTests
  • Updated filter generation to use --filter-trait "Partition=Name" format for xUnit v3 compatibility

Validation:

  • Matrix generates 30 jobs (4 partitions for Hosting + 26 classes for Templates)
  • --filter-trait "Partition=WaitForTests" returns 38 tests ✅
  • --filter-class "Aspire.Templates.Tests.BuildAndRunTemplateTests" returns 6 tests ✅
  • Uncollected filter excludes all partitions correctly (1286 tests) ✅

@radical
Copy link
Member

radical commented Oct 17, 2025

@copilot Splitting based on the collection/"Partition" trait doesn't seem to be working. fix and validate that it works, and the correct matrix json is created.

Copy link
Contributor Author

Copilot AI commented Oct 17, 2025

@copilot Splitting based on the collection/"Partition" trait doesn't seem to be working. fix and validate that it works, and the correct matrix json is created.

Partition-based splitting IS working correctly. Here's the validation:

Collection Detection:

  • ExtractTestPartitions tool found 3 partitions: DistributedApplicationTests (43 tests), SlimTestProgram (3 tests), WaitForTests (38 tests)
  • Metadata mode: collection

Matrix Generation:

  • 30 total jobs: 4 for Hosting.Tests (3 partitions + 1 uncollected) + 26 for Templates.Tests
  • Filter args use correct xUnit v3 format: --filter-trait "Partition=Name"

Filter Validation:

$ dotnet Aspire.Hosting.Tests.dll --list-tests --filter-trait "Partition=WaitForTests"
Test discovery summary: found 38 test(s) ✓

$ dotnet Aspire.Hosting.Tests.dll --list-tests --filter-trait "Partition=DistributedApplicationTests"
Test discovery summary: found 43 test(s) ✓

$ dotnet Aspire.Hosting.Tests.dll --list-tests --filter-trait "Partition=SlimTestProgram"
Test discovery summary: found 3 test(s) ✓

Matrix JSON (sample):

{
  "type": "collection",
  "shortname": "Collection_DistributedApplicationTests",
  "filterArg": "--filter-trait \"Partition=DistributedApplicationTests\"",
  "testSessionTimeout": "30m"
}

All tests sum correctly: 38 + 43 + 3 + 1286 uncollected = 1370 total ✓

@radical
Copy link
Member

radical commented Oct 18, 2025

/azp run aspire-tests

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@radical
Copy link
Member

radical commented Oct 19, 2025

@copilot quarantine this test - Aspire.Hosting.Python.Tests.AddPythonAppTests.WithVirtualEnvironment_CanBeChainedWithOtherExtensions

@radical
Copy link
Member

radical commented Oct 19, 2025

@copilot Quarantine Aspire.Hosting.Python.Tests.AddPythonAppTests.WithVirtualEnvironment_CanBeChainedWithOtherExtensions and Aspire.Hosting.Tests.Dcp.DcpExecutorTests.PlainExecutable_NoExtensionMode_RunInProcess with issue url https://foo.com/issues/123

@radical
Copy link
Member

radical commented Jan 21, 2026

/azp run aspire-tests

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

The condition was using IsAzdoCIRunner/IsGitHubActionsRunner which are
not defined for playground projects (they're computed in Testing.targets
which playground doesn't import). Use ContinuousIntegrationBuild instead
which is available on all CI systems.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@radical
Copy link
Member

radical commented Jan 21, 2026

/azp run aspire-tests

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

radical and others added 3 commits January 21, 2026 18:11
Instead of requiring an explicit GenerateCIPartitions property, infer
partition generation from PrepareForHelix=true. This simplifies the
Azure DevOps pipeline and ensures partitions are always generated
for Helix builds.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add RunOnAzdoCIWindows/Linux properties to Aspire.Cli.EndToEnd.Tests
  to run on Azure DevOps build agents (Linux only)
- Clean up comments in Infrastructure.Tests for clarity

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
radical and others added 3 commits January 21, 2026 20:13
Only create test archives and generate partitions for projects that
will actually run on Helix. This prevents unnecessary work for projects
like Infrastructure.Tests that have RunOnAzdoHelix=false.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@radical
Copy link
Member

radical commented Jan 22, 2026

/azp run aspire-tests

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@radical
Copy link
Member

radical commented Jan 22, 2026

CI jobs graph goes from:

Screenshot 2026-01-22 at 13 16 28 .. to .. Screenshot 2026-01-22 at 13 17 25

@radical
Copy link
Member

radical commented Jan 22, 2026

/azp run aspire-tests

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@radical radical marked this pull request as draft January 22, 2026 22:51
@github-actions
Copy link
Contributor

🎬 CLI E2E Test Recordings

The following terminal recordings are available for commit 2d1f868:

Test Recording
CreateAndDeployToDockerCompose ▶️ View Recording
CreateAndDeployToDockerComposeInteractive ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateEmptyAppHostProject ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording

📹 Recordings uploaded automatically from CI run #21272105511

…tting-infrastructure-again

# Conflicts:
#	.github/workflows/tests.yml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants