Skip to content

Commit 8f7927c

Browse files
committed
Parallelise outerloop quarantined test execution
Use the Arcade SDK capabilities to provide a custom test runner (see in eng/QuarantinedTestRunsheetBuilder) to intercept test execution to pretend to run tests. Instead, only query test assemblies whether those contain quarantined tests by running `dotnet test --list-tests`. Then inspect the output, and if a test project contains quarantined tests include the test project into a runsheet. The runsheet generation itself is a two step process. 1. The first step is performed by each project during RunTests target execution (in QuarantinedTestRunsheetBuilder) and it results in project-specific runsheets being generated. E.g., Aspire.Cli.Tests_net8.0_x64.linux.runsheet.json. 2. After all test projects have been inspected, all the individual runsheets are aggregated into a single runsheet, which is then used as a matrix by GHA workflow. This is happening in eng/AfterSolutionBuild.targets.
1 parent f9035bc commit 8f7927c

File tree

4 files changed

+330
-76
lines changed

4 files changed

+330
-76
lines changed

.github/workflows/tests-outerloop.yml

Lines changed: 42 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -12,88 +12,60 @@ concurrency:
1212

1313
jobs:
1414

15-
test:
16-
name: ${{ matrix.os.title }}
17-
runs-on: ${{ matrix.os.name }}
18-
strategy:
19-
fail-fast: false
20-
matrix:
21-
os:
22-
- name: ubuntu-latest
23-
title: Linux
24-
- name: windows-latest
25-
title: Windows
15+
generate_tests_matrix:
16+
name: Generate test runsheet
17+
runs-on: windows-latest
18+
if: ${{ github.repository_owner == 'dotnet' }}
19+
outputs:
20+
runsheet: ${{ steps.generate_tests_matrix.outputs.runsheet }}
2621
steps:
27-
- name: Setup vars (Linux)
28-
if: ${{ matrix.os.name == 'ubuntu-latest' }}
29-
run: |
30-
echo "DOTNET_SCRIPT=./dotnet.sh" >> $GITHUB_ENV
31-
echo "BUILD_SCRIPT=./build.sh" >> $GITHUB_ENV
22+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
3223

33-
- name: Setup vars (Windows)
34-
if: ${{ matrix.os.name == 'windows-latest' }}
24+
# We need to build the whole solution, so that we can interrogate each test project
25+
# and find out whether it contains any quarantined tests.
26+
- name: Build the solution
3527
run: |
36-
echo "DOTNET_SCRIPT=.\dotnet.cmd" >> $env:GITHUB_ENV
37-
echo "BUILD_SCRIPT=.\build.cmd" >> $env:GITHUB_ENV
28+
./build.cmd -restore -build -c Release -ci /p:CI=false /p:GeneratePackageOnBuild=false /p:InstallBrowsersForPlaywright=false /bl:./artifacts/log/Release/build.binlog
3829
39-
- name: Checkout code
40-
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
41-
42-
- name: Trust HTTPS development certificate (Linux)
43-
if: matrix.os.name == 'ubuntu-latest'
44-
run: ${{ env.DOTNET_SCRIPT }} dev-certs https --trust
45-
46-
- name: Run quarantined tests
47-
env:
48-
CI: false
30+
- name: Generate test runsheet
31+
id: generate_tests_matrix
4932
run: |
50-
${{ env.BUILD_SCRIPT }} -projects ${{ github.workspace }}/tests/Shared/SolutionTests.proj -ci -restore -build -test -c Release /p:RunQuarantinedTests=true /bl:${{ github.workspace }}/artifacts/log/Release/test-quarantined.binlog
33+
./build.cmd -test /p:TestRunnerName=QuarantinedTestRunsheetBuilder /p:RunQuarantinedTests=true -c Release -ci /p:CI=false /p:Restore=false /p:Build=false /bl:./artifacts/log/Release/runsheet.binlog
5134
52-
- name: Keep only relevant test logs
53-
if: always()
54-
shell: pwsh
55-
run: |
56-
# Define the directory to search for log files
57-
$logDirectory = "${{ github.workspace }}/artifacts/log/**/TestLogs"
35+
- name: Upload logs, and test results
36+
if: ${{ always() }}
37+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
38+
with:
39+
name: logs-runsheet
40+
path: |
41+
${{ github.workspace }}/artifacts/log/*/*.binlog
42+
${{ github.workspace }}/artifacts/log/*/TestLogs/**
43+
${{ github.workspace }}/artifacts/tmp/*/combined_runsheet.json
44+
retention-days: 5
5845

59-
# Define the text to search for in the log files
60-
$searchText = "No test matches the given testcase filter"
61-
$resultsFilePattern = "Results File: (.+)"
46+
run_tests:
47+
name: Test
48+
needs: generate_tests_matrix
49+
strategy:
50+
fail-fast: false
51+
matrix:
52+
tests: ${{ fromJson(needs.generate_tests_matrix.outputs.runsheet) }}
6253

63-
# Get all .log files in the specified directory and its subdirectories
64-
$logFiles = Get-ChildItem -Path $logDirectory -Filter *.log -Recurse
54+
runs-on: ${{ matrix.tests.os }} # Use the OS from the matrix
55+
if: ${{ github.repository_owner == 'dotnet' }}
6556

66-
foreach ($logFile in $logFiles) {
67-
# Read the content of the log file
68-
$content = Get-Content -Path $logFile.FullName
57+
steps:
58+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
6959

70-
# Check if the content contains the specified text
71-
if ($content -match $searchText) {
72-
# Remove the log file if it contains the specified text
73-
Remove-Item -Path $logFile.FullName -Force
74-
Write-Host "Removed file: $($logFile.FullName)"
75-
}
76-
else {
77-
# Extract paths from lines containing "Results File: <path>"
78-
foreach ($line in $content) {
79-
if ($line -match $resultsFilePattern) {
80-
$resultsFilePath = $matches[1]
81-
Write-Host "Found results file: $resultsFilePath"
82-
83-
# Copy the results file to the TestLogs folder
84-
$destinationPath = (Split-Path -Path $logFile.FullName -Parent)
85-
Copy-Item -Path $resultsFilePath -Destination $destinationPath -Force
86-
Write-Host "Copied $resultsFilePath to $destinationPath"
87-
}
88-
}
89-
}
90-
}
60+
- name: Test ${{ matrix.tests.project }}
61+
run: |
62+
${{ matrix.tests.command }}
9163
9264
- name: Process logs and post results
9365
if: always()
9466
shell: pwsh
9567
run: |
96-
$logDirectory = "${{ github.workspace }}/artifacts/log/**/TestLogs"
68+
$logDirectory = "${{ github.workspace }}/artifacts/TestResults"
9769
$trxFiles = Get-ChildItem -Path $logDirectory -Filter *.trx -Recurse
9870
9971
$testResults = @() # Initialize an array to store test results
@@ -157,19 +129,13 @@ jobs:
157129
$table | Out-File -FilePath $outputPath -Encoding utf8
158130
Write-Host "Test results saved to $outputPath"
159131
160-
# Windows-specific: Check for failed tests and set the exit code accordingly
161-
# This is a workaround for the issue with the `exit` command in PowerShell
162-
if ($failedTests -gt 0) {
163-
Write-Host "::error::Build failed. Check errors above."
164-
exit 1
165-
}
166-
167132
- name: Upload logs, and test results
168-
if: always()
133+
if: failure()
169134
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
170135
with:
171-
name: logs-${{ matrix.os.name }}
136+
name: logs-${{ matrix.tests.os }}-${{ matrix.tests.project }}
172137
path: |
173138
${{ github.workspace }}/artifacts/log/*/*.binlog
174139
${{ github.workspace }}/artifacts/log/*/TestLogs/**
140+
${{ github.workspace }}/artifacts/log/TestResults/*/*.trx
175141
retention-days: 5

eng/AfterSolutionBuild.targets

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<Project>
2+
3+
<!--
4+
In short, this file automates the creation of a combined JSON runsheet from individual test runsheets,
5+
making it easy to integrate quarantined test execution into CI workflows, especially GitHub Actions.
6+
-->
7+
8+
<!--
9+
This MSBuild targets file automates the generation of a combined test runsheet after building a solution,
10+
using the QuarantinedTestRunsheetBuilder (located in eng/QuarantinedTestRunsheetBuilder/QuarantinedTestRunsheetBuilder.targets).
11+
12+
Here's a high-level overview of its logic:
13+
14+
1. **Conditionally Triggered Target**:
15+
- The target _GenerateTestMatrix runs before the Test target, but only if the TestRunnerName property is set to QuarantinedTestRunsheetBuilder.
16+
17+
2. **Define Paths and Script Content**:
18+
- Defines a combined runsheet JSON file path (_CombinedRunsheetFile) in the artifacts temporary directory.
19+
- Defines a PowerShell script (_Command) that:
20+
- Finds all individual .runsheet.json files in the artifacts directory.
21+
- Reads and merges their JSON content into a single combined JSON array.
22+
- Writes the combined JSON to the combined runsheet file.
23+
- Checks if running in GitHub Actions environment:
24+
- If yes, outputs the combined JSON to GitHub Actions output (GITHUB_OUTPUT).
25+
- If no, prints the combined JSON to the console.
26+
27+
3. **Write and Execute the Script**:
28+
- Writes the defined PowerShell script content to a temporary script file (create-runsheet.ps1).
29+
- Executes this script using MSBuild's <Exec> task.
30+
31+
4. **Output Results**:
32+
- After execution, logs a message indicating the combined runsheet was created, along with the script's output.
33+
34+
A runsheet is a JSON file that describes what tests to be run.
35+
For example:
36+
37+
```json
38+
[
39+
{
40+
"project": "Aspire.Test",
41+
"os": "windows-latest",
42+
"command": "./eng/build.ps1 -restore -build -test -projects \"$(RelativeTestProjectPath)\" /bl:\"$(RelativeTestBinLog)\" -c $(Configuration) -ci /p:RunQuarantinedTests=true /p:CI=false"
43+
},
44+
{
45+
"project": "Aspire.Cli.Test",
46+
"os": "ubuntu-latest",
47+
"command": "./eng/build.sh -restore -build -test -projects \"$(RelativeTestProjectPath)\" /bl:\"$(RelativeTestBinLog)\" -c $(Configuration) -ci /p:RunQuarantinedTests=true /p:CI=false"
48+
}
49+
]
50+
```
51+
52+
-->
53+
<Target Name="_GenerateTestMatrix" BeforeTargets="Test" Condition=" '$(TestRunnerName)' == 'QuarantinedTestRunsheetBuilder' ">
54+
<PropertyGroup>
55+
<_CombinedRunsheetFile>$(ArtifactsTmpDir)/combined_runsheet.json</_CombinedRunsheetFile>
56+
<_Command>
57+
$combined = @()
58+
Get-ChildItem -Path '$(ArtifactsTmpDir)' -Filter '*.runsheet.json' |
59+
ForEach-Object {
60+
$content = Get-Content -Raw $_.FullName | ConvertFrom-Json
61+
if ($content -is [Array]) {
62+
$combined += $content
63+
}
64+
else {
65+
$combined += @($content)
66+
}
67+
}
68+
$jsonString = ($combined | ConvertTo-Json -Depth 10 -Compress)
69+
$jsonString | Set-Content '$(_CombinedRunsheetFile)';
70+
71+
# determine if the script is running in a GitHub Actions environment
72+
if ($env:CI -and $env:GITHUB_ACTIONS) {
73+
"runsheet=$jsonString" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
74+
}
75+
else {
76+
Write-Host "runsheet=$jsonString"
77+
}
78+
</_Command>
79+
<_Script>$([MSBuild]::NormalizePath($(ArtifactsTmpDir), 'create-runsheet.ps1'))</_Script>
80+
</PropertyGroup>
81+
82+
<WriteLinesToFile File="$(_Script)"
83+
Lines="$(_Command)"
84+
Overwrite="true" />
85+
<Exec Command="pwsh -File $(_Script)" ConsoleToMSBuild="true">
86+
<Output TaskParameter="ConsoleOutput" PropertyName="ScriptOutput" />
87+
</Exec>
88+
<Message Importance="high" Text="Combined runsheet created%0D%0A%0D%0A$(ScriptOutput)" />
89+
</Target>
90+
</Project>

0 commit comments

Comments
 (0)