From f2e4457f97461056f7a7f3a3249d97d109ee788a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Sat, 17 Oct 2026 17:03:09 +0300 Subject: [PATCH] ci: a hung test module fails inside its pass under a session timeout (#447) Each test pass runs with a 25-minute --timeout, about four times the slowest measured module, and a failed main pass also writes its test listing, so the rows still in flight are among those listed but not recorded. --- .github/workflows/ci.yml | 14 +++++++++++--- docs/engineering/quality-gates.md | 7 +++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c3ee033..454cef24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -301,14 +301,22 @@ jobs: # reason. They run after a failed first pass (not after a failed build), so the report sees # every pass, and they leave the HTML report upload to the first pass, whose artifact names # (assembly, OS, framework) they would repeat. + # + # Every pass ends a hung module inside its step under a session --timeout; the listing below, + # written only when the main pass fails, narrows the rows in flight (quality-gates.md). - name: Test - run: dotnet test --configuration Release --no-build --max-parallel-test-modules ${{ matrix.test-modules }} -- --report-trx --results-directory "${{ github.workspace }}/test-results/main" --treenode-filter "/*/*/*/*[(Category!=ServiceElection)&(Category!=NativeAot)]" --minimum-expected-tests 1 + id: test + run: dotnet test --configuration Release --no-build --max-parallel-test-modules ${{ matrix.test-modules }} -- --report-trx --results-directory "${{ github.workspace }}/test-results/main" --treenode-filter "/*/*/*/*[(Category!=ServiceElection)&(Category!=NativeAot)]" --minimum-expected-tests 1 --timeout 25m env: TUNIT_ARTIFACT_RETENTION_DAYS: 7 + - name: List tests (main, after a failed pass) + if: ${{ !cancelled() && steps.test.outcome == 'failure' }} + run: dotnet test --configuration Release --no-build --list-tests -- --treenode-filter "/*/*/*/*[(Category!=ServiceElection)&(Category!=NativeAot)]" > "${{ github.workspace }}/test-results/main/listed-tests.txt" + - name: Test (service elections) if: ${{ !cancelled() && steps.build.outcome == 'success' }} - run: dotnet test --project "${{ github.workspace }}/tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj" --configuration Release --no-build --max-parallel-test-modules 1 -- --report-trx --results-directory "${{ github.workspace }}/test-results/service-elections" --treenode-filter "/*/*/*/*[Category=ServiceElection]" --minimum-expected-tests 1 + run: dotnet test --project "${{ github.workspace }}/tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj" --configuration Release --no-build --max-parallel-test-modules 1 -- --report-trx --results-directory "${{ github.workspace }}/test-results/service-elections" --treenode-filter "/*/*/*/*[Category=ServiceElection]" --minimum-expected-tests 1 --timeout 25m env: TUNIT_ARTIFACT_RETENTION_DAYS: 7 TUNIT_DISABLE_ARTIFACT_UPLOAD: true @@ -318,7 +326,7 @@ jobs: # host's native toolchain and runs the host's own runtime identifier. - name: Test (native AOT) if: ${{ !cancelled() && steps.build.outcome == 'success' }} - run: dotnet test --project "${{ github.workspace }}/tests/OpenCode.Sdk.Tools.Tests/OpenCode.Sdk.Tools.Tests.csproj" --configuration Release --no-build --max-parallel-test-modules 1 -- --report-trx --results-directory "${{ github.workspace }}/test-results/native-aot" --treenode-filter "/*/*/*/*[Category=NativeAot]" --minimum-expected-tests 1 + run: dotnet test --project "${{ github.workspace }}/tests/OpenCode.Sdk.Tools.Tests/OpenCode.Sdk.Tools.Tests.csproj" --configuration Release --no-build --max-parallel-test-modules 1 -- --report-trx --results-directory "${{ github.workspace }}/test-results/native-aot" --treenode-filter "/*/*/*/*[Category=NativeAot]" --minimum-expected-tests 1 --timeout 25m env: TUNIT_ARTIFACT_RETENTION_DAYS: 7 TUNIT_DISABLE_ARTIFACT_UPLOAD: true diff --git a/docs/engineering/quality-gates.md b/docs/engineering/quality-gates.md index ec100c9f..335290bd 100644 --- a/docs/engineering/quality-gates.md +++ b/docs/engineering/quality-gates.md @@ -230,6 +230,13 @@ no unexpected or duplicate modules. The roster is deliberately explicit rather t cannot shrink together with the coverage it guards — so adding or removing a test project or a test target framework updates it in the same change. +Every CI test pass runs under a 25-minute session `--timeout`, about four times the slowest +measured module (`OpenCode.Sdk.Tests` on net472 on Windows, under six minutes), so a hung module +fails inside its step instead of at the job timeout. An expired session exits with code 7 and keeps +the TRX of the rows that finished, but it does not name the rows still in flight. When the main +pass fails, the job also writes that pass's test listing to `test-results/main/listed-tests.txt`, +and the rows in flight are among those listed but not recorded. + ## Clone gate The detector ignores using directives; a baseline entry that matches no clone fails the gate like a new clone does.