Repository navigation
ci: loop flaky tests on their runners with timing traces #613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
4e197bd
ci: loop flaky tests on their runners with timing traces
klNuno 67abc86
ci(flake-loop): build the alone E2E file list without a stale exit code
klNuno 01ef587
ci(flake-loop): let a running loop finish when a new head arrives
klNuno 990ea7d
ci(flake-loop): name the busiest processes when Chromium stops answer…
klNuno f334fba
ci(flake-loop): check Chrome's devtools frames through a relay and ke…
klNuno 9a007c1
ci(flake-loop): loop with #623 applied, reverted before merge
klNuno 3f7178d
ci(flake-loop): loop macOS arm64 browser files two at once beside four
klNuno a1e5985
ci(flake-loop): count the devtools messages Chrome sent against those…
klNuno e50f0b0
Revert "ci(flake-loop): loop with #623 applied, reverted before merge"
klNuno a2a3134
ci(flake-loop): merge main, then log where a stalled UI request waited
klNuno 2cce06d
ci(flake-loop): loop the macOS browser files with the temp folder on …
klNuno f552199
ci(flake-loop): loop the macOS browser files with Spotlight on and off
klNuno b0872c3
ci(flake-loop): loop cli.test.ts with the pinned Bun and canary
klNuno 5149d55
ci(flake-loop): loop only cli.test.ts on macOS until #623 is reverted
klNuno f081c68
Merge remote-tracking branch 'origin/main' into ci/flake-loop
klNuno File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| name: flake-loop | ||
|
|
||
| # Runs flaky tests many times on the runners where they fail, with timing | ||
| # traces, to find a cause and to prove a fix (docs/ci.md#checks). A pull | ||
| # request runs it once it carries the `flake-loop` label. | ||
| on: | ||
| pull_request: | ||
| types: [opened, synchronize, reopened, labeled] | ||
| workflow_dispatch: | ||
| inputs: | ||
| runs: | ||
| description: Runs per job. | ||
| type: number | ||
| default: 10 | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: flake-loop-${{ github.event.pull_request.number || github.ref }} | ||
| # A loop already running keeps its data; a newer head waits for it. | ||
| cancel-in-progress: false | ||
|
|
||
| env: | ||
| RUNS: ${{ inputs.runs || 10 }} | ||
|
|
||
| jobs: | ||
| browser: | ||
| name: Core browser files (${{ matrix.name }}, ${{ matrix.parallel }} at once, Spotlight ${{ matrix.spotlight }}) | ||
| # Parked while the cli loop runs alone after #635: set the FLAKE_LOOP_ALL variable to 1 to run it. | ||
| if: vars.FLAKE_LOOP_ALL == '1' | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| # Chromium stopped answering at a load of 25 to 53, with Spotlight's mdworker_shared and mds | ||
| # the busiest processes, temp folder on disk or in RAM alike (run 39197082818). The same loop | ||
| # with indexing off. | ||
| - { os: macos-15, name: macOS arm64, parallel: 4, spotlight: on } | ||
| - { os: macos-15, name: macOS arm64, parallel: 4, spotlight: off } | ||
| runs-on: ${{ matrix.os }} | ||
| timeout-minutes: 75 | ||
| env: | ||
| BOITE_CDP_TRACE: '1' | ||
| FLAKE_LOOP_TITLE: Core browser files, ${{ matrix.name }}, ${{ matrix.parallel }} at once, Spotlight ${{ matrix.spotlight }} | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | ||
| with: | ||
| persist-credentials: false | ||
| - uses: ./.github/actions/setup | ||
| - name: Spotlight off | ||
| if: matrix.spotlight == 'off' | ||
| run: | | ||
| sudo mdutil -a -i off | ||
| mdutil -a -s | ||
| - run: | | ||
| mkdir -p flake-loop | ||
| # Disk throughput and load each second, to line a stall up with the disk. | ||
| (while true; do echo "$(date -u +%H:%M:%S) $(iostat -d -c 2 -w 1 disk0 | tail -1) $(sysctl -n vm.loadavg)"; done > flake-loop/iostat.txt 2>&1 &) | ||
| # The five busiest processes every 5 s, to see what indexes or scans while Chromium stalls. | ||
| (while true; do echo "== $(date -u +%H:%M:%S)"; ps -Aro pcpu,rss,comm | head -6; sleep 5; done > flake-loop/top.txt 2>&1 &) | ||
| bun scripts/ci/flake-loop.ts --runs "$RUNS" --parallel ${{ matrix.parallel }} --cwd packages/core --out flake-loop test/browser-sharing.test.ts test/browser.test.ts test/browser-sleep.test.ts test/browser-profile-lifecycle.test.ts | ||
| - uses: ./.github/actions/upload-artifact | ||
| if: always() | ||
| with: | ||
| name: flake-loop-browser-${{ matrix.os }}-${{ matrix.parallel }}-spotlight-${{ matrix.spotlight }} | ||
| path: flake-loop/ | ||
|
|
||
| cli: | ||
| name: cli.test.ts (${{ matrix.name }}, Bun ${{ matrix.bun }}) | ||
| if: github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'flake-loop') | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| # The 1002 closes on macOS, 200 runs four at a time: 5 with #623, 11 (as 1006) on Bun canary. | ||
| # The same loop once #623 is reverted (#635). | ||
| include: | ||
| - { os: macos-15, name: macOS arm64, bun: pinned } | ||
| runs-on: ${{ matrix.os }} | ||
| timeout-minutes: 45 | ||
| env: | ||
| FLAKE_LOOP_TITLE: cli.test.ts, ${{ matrix.name }}, Bun ${{ matrix.bun }} | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | ||
| with: | ||
| persist-credentials: false | ||
| - uses: ./.github/actions/setup | ||
| # Four copies at once load the runner as the core suite's four workers do. | ||
| - shell: bash | ||
| run: bun scripts/ci/flake-loop.ts --runs "$((RUNS * 20))" --parallel 4 --cwd packages/core --out flake-loop test/cli.test.ts test/cli.test.ts test/cli.test.ts test/cli.test.ts | ||
| - uses: ./.github/actions/upload-artifact | ||
| if: always() | ||
| with: | ||
| name: flake-loop-cli-${{ matrix.os }}-${{ matrix.bun }} | ||
| path: flake-loop/ | ||
|
|
||
| e2e: | ||
| name: E2E ${{ matrix.mode }} | ||
| # Parked while the cli loop runs alone after #635: set the FLAKE_LOOP_ALL variable to 1 to run it. | ||
| if: vars.FLAKE_LOOP_ALL == '1' | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| mode: [alone, shard 1, shard 2, shard 3] | ||
| runs-on: windows-2025 | ||
| timeout-minutes: 90 | ||
| env: | ||
| BOITE_E2E_PREBUILT_UI: '1' | ||
| BOITE_E2E_FAKE_UI: tests/e2e/.artifacts/fake-ui | ||
| BOITE_E2E_TRACE: '1' | ||
| FLAKE_LOOP_TITLE: E2E ${{ matrix.mode }} | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | ||
| with: | ||
| persist-credentials: false | ||
| - uses: ./.github/actions/setup | ||
| - run: bun run build:ui | ||
| - run: bun tests/e2e/lib/warm.ts --build-only $env:BOITE_E2E_FAKE_UI | ||
| # Same temp drive as the E2E job in ci.yml. | ||
| - name: Prepare end-to-end tests | ||
| env: | ||
| TEMP: ${{ runner.temp }} | ||
| TMP: ${{ runner.temp }} | ||
| run: bun tests/e2e/lib/warm.ts --prebuilt $env:BOITE_E2E_FAKE_UI | ||
| - name: Loop | ||
| env: | ||
| TEMP: ${{ runner.temp }} | ||
| TMP: ${{ runner.temp }} | ||
| MODE: ${{ matrix.mode }} | ||
| run: | | ||
| if ($env:MODE -eq 'alone') { | ||
| $files = @('tests/e2e/coordination-bridge-live.test.ts', 'tests/e2e/machines.test.ts'); $runs = [int]$env:RUNS * 2; $together = @() | ||
| } else { | ||
| $shard = $env:MODE.Split(' ')[1] | ||
| $files = @(bun scripts/ci/e2e-shards.ts $shard 3) | ||
| if ($LASTEXITCODE -ne 0 -or $files.Count -eq 0) { throw "No E2E files for $env:MODE" } | ||
| $runs = [math]::Max(1, [int]([int]$env:RUNS / 2)); $together = @('--together') | ||
| } | ||
| New-Item -ItemType Directory -Force flake-loop/devtools | Out-Null | ||
| $env:BOITE_E2E_TRACE_DIR = (Resolve-Path flake-loop/devtools).Path | ||
| # A page stall lines up with these: is the disk under Chrome's profile slow then, and is Defender scanning it? | ||
| Get-MpComputerStatus | Select-Object RealTimeProtectionEnabled, OnAccessProtectionEnabled, BehaviorMonitorEnabled | Format-List | Out-File flake-loop/defender.txt | ||
| (Get-MpPreference).ExclusionPath | Out-File -Append flake-loop/defender.txt | ||
| Get-Content flake-loop/defender.txt | ||
| $perf = Start-Process typeperf -ArgumentList @('\PhysicalDisk(*)\Avg. Disk sec/Transfer', '\PhysicalDisk(*)\Current Disk Queue Length', '\Processor(_Total)\% Processor Time', '\Process(MsMpEng)\% Processor Time', '-si', '1', '-f', 'CSV', '-o', (Join-Path $PWD 'flake-loop/perf.csv'), '-y') -PassThru -WindowStyle Hidden | ||
| bun scripts/ci/flake-loop.ts --runs $runs --out flake-loop @together @files | ||
| $code = $LASTEXITCODE | ||
| Stop-Process -Id $perf.Id -ErrorAction SilentlyContinue | ||
| exit $code | ||
| - uses: ./.github/actions/upload-artifact | ||
| if: always() | ||
| with: | ||
| name: flake-loop-e2e-${{ matrix.mode == 'alone' && 'alone' || format('shard{0}', strategy.job-index) }} | ||
| path: flake-loop/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { expect, test } from 'bun:test'; | ||
| import { failureBlock, results, summary } from './flake-loop.ts'; | ||
|
|
||
| const output = [ | ||
| '(pass) first [12.50ms]', | ||
| 'error: the client is closed: the socket closed (1002)', | ||
| ' at call (client.ts:203:29)', | ||
| '(fail) group > second [1.20s]', | ||
| '(pass) third', | ||
| ].join('\n'); | ||
|
|
||
| const junit = `<testsuites> | ||
| <testsuite name="a.test.ts"> | ||
| <testcase name="first" classname="" time="0.0125" file="a.test.ts" line="1" assertions="1" /> | ||
| <testsuite name="group"> | ||
| <testcase name="second "quoted"" classname="group" time="1.2" file="a.test.ts" line="5" assertions="0"> | ||
| <failure type="AssertionError" /> | ||
| </testcase> | ||
| </testsuite> | ||
| <testcase name="filtered" classname="" time="0" file="a.test.ts" line="9" assertions="0"> | ||
| <skipped /> | ||
| </testcase> | ||
| </testsuite> | ||
| </testsuites>`; | ||
|
|
||
| test('results reads each test the report names, its outcome and its duration, skipped ones left out', () => { | ||
| expect(results(junit)).toEqual([ | ||
| { file: 'a.test.ts', name: 'first', passed: true, ms: 12.5 }, | ||
| { file: 'a.test.ts', name: 'group > second "quoted"', passed: false, ms: 1200 }, | ||
| ]); | ||
| }); | ||
|
|
||
| test('a failure block runs from the end of the test before to its fail line', () => { | ||
| expect(failureBlock(output, 'group > second')).toBe('error: the client is closed: the socket closed (1002)\n at call (client.ts:203:29)\n(fail) group > second [1.20s]'); | ||
| expect(failureBlock(output, 'missing')).toBe(''); | ||
| }); | ||
|
|
||
| test('the summary puts the most failed test first and counts every failure', () => { | ||
| const table = summary(new Map([ | ||
| ['a.test.ts: steady', { pass: 3, fail: 0, ms: [10, 20, 30] }], | ||
| ['a.test.ts: flaky | piped', { pass: 1, fail: 2, ms: [5, 50, 500] }], | ||
| ]), 3); | ||
| expect(table.split('\n')[2]).toBe('| a.test.ts: flaky \\| piped | 2 | 1 | 50 | 500 | 500 |'); | ||
| expect(table).toEndWith('3 runs, 2 failures'); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: beboite/boite
Length of output: 5986
Reject the timed-out command when tracing fails.
When
BOITE_CDP_TRACE=1and a non-Browser.getVersioncommand times out, the code removes the command from#waitingand waits for#trace(). If#trace()throws fromBun.spawnSync()or the unread-byte callback, the.then()rejection handler does not run. The original command promise remains pending, and the diagnostic rejection is unhandled. Catch trace failures and reject with the original timeout error.🤖 Prompt for AI Agents