CI green on a loaded shared machine: browser suites (now also glyph-ligatures and feature-fit, through the shared launch helper) in one dedicated shard started first at concurrency 2; timeouts scaled by load ÷ cores (tests/helpers/load.ts, max(1-, 5-min average), capped at 5x, logged) for browser tests, waits, launch/close, the other shards' default and the desktop boot check; one retry for a browser test that timed out while the factor was above 2, listed as 'retried under load' in the CI summary, never for an assertion failure; the six-type App Privacy walk no longer dumps every step (the --debug-dump check walks two types), wait timeouts print per-line times; play-edit-lock checks reads in flight instead of wall time; tests/load-retry.test.ts

This commit is contained in:
CRTkafa 2026-10-14 05:21:15 +08:00
parent 21fa91afd1
commit 918146d4a6
13 changed files with 261 additions and 96 deletions

View file

@ -6,8 +6,11 @@
// mapped through imports) + scrub
// The tests run as shards: N = CPU cores - 1 (max 8; STOREBOUND_CI_SHARDS=n overrides) parallel `bun test <files>`
// processes, balanced by the durations recorded after earlier runs (node_modules/.cache/storebound-ci/, smoothed by
// mergeTimings so one run on a loaded machine does not skew the split; falling back to file size). Browser suites
// (tools/ci-plan.ts HEAVY) each get a shard of their own and run their opted-in tests BROWSER_CONCURRENCY at a time.
// mergeTimings so one run on a loaded machine does not skew the split; falling back to file size). The browser suites
// (tools/ci-plan.ts HEAVY) run in one dedicated shard, started first, their opted-in tests BROWSER_CONCURRENCY at a
// time. The machine is shared: their timeouts scale with load ÷ cores (tests/helpers/load.ts, capped at 5x, the factor
// is logged), and a browser test that fails by timing out under load (factor > 2) runs once more; the summary lists
// those as "retried under load". An assertion failure is never retried.
// `bun test` covers the CLI and the desktop webview (apps/desktop/tests), the site overflow check and the
// no-hand-written-SVG guard; the desktop boot check (tools/desktop-boot-check.ts: the built window in headless
// Chromium, needs playwright's browser or Google Chrome) runs after the desktop build.
@ -18,6 +21,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { homedir, tmpdir } from 'node:os';
import { join } from 'node:path';
import { hostTarget, OUT_DIR, outName } from './build-sidecars';
import { loadFactor, loadNote, RETRY_MARK, scaled } from '../tests/helpers/load';
import { BROWSER_CONCURRENCY, changedFiles, HEAVY, junitTimes, listTests, mergeTimings, planShards, readTimings, relatedTests, ROOT, rustNeeded, writeTimings } from './ci-plan';
interface Task { name: string; cmd: string[]; cwd?: string; env?: Record<string, string>; after?: string[]; junit?: string }
@ -32,6 +36,8 @@ const tasks: Task[] = [];
const add = (t: Task) => tasks.push(t);
const tsc = (name: string, project: string) => add({ name, cmd: ['bunx', 'tsc', '--noEmit', '--incremental', '-p', project,
'--tsBuildInfoFile', join(cache, `${name.replace(/\W+/g, '-')}.tsbuildinfo`)] });
const retried: string[] = [];
console.log(`ci: ${loadNote(loadFactor())}`);
const junitDir = mkdtempSync(join(tmpdir(), 'sb-ci-'));
const node = Bun.which('node');
@ -48,8 +54,10 @@ function addTests(files: string[]) {
// the built-bundle check in apps/desktop/tests/webview.test.ts runs as "desktop bundle check" once the build is
// done; here it would read dist/ while vite rewrites it
// browser suites share the machine with the other steps: a browser start or close can pass bun's 5 s default
// browser tests that opt in (testScope().concurrent) run BROWSER_CONCURRENCY at a time within their file
const timeout = shard.some(f => HEAVY.includes(f)) ? ['--timeout=30000', `--max-concurrency=${BROWSER_CONCURRENCY}`] : [];
// (scaled by the load now); browser tests that opt in (testScope().concurrent) run BROWSER_CONCURRENCY at a time
// every other shard: bun's 5 s default (tests and hooks without their own timeout), scaled the same way
const f = loadFactor();
const timeout = shard.some(x => HEAVY.includes(x)) ? [`--timeout=${scaled(30_000, f)}`, `--max-concurrency=${BROWSER_CONCURRENCY}`] : [`--timeout=${scaled(5_000, f)}`];
add({ name: `tests ${i + 1}/${all.length}`, cmd: ['bun', 'test', ...timeout, '--reporter=junit', `--reporter-outfile=${junit}`, ...shard.map(f => `./${f}`)],
env: { STOREBOUND_SKIP_DIST_CHECK: '1' }, junit });
});
@ -133,6 +141,7 @@ async function pump(stream: ReadableStream<Uint8Array>, name: string, out: NodeJ
buf += dec.decode(value, { stream: true });
const lines = buf.split('\n');
buf = lines.pop()!;
for (const l of lines) if (l.includes(RETRY_MARK)) retried.push(`${name}: ${l.slice(l.indexOf(RETRY_MARK) + RETRY_MARK.length).trim()}`);
if (lines.length) out.write(lines.map(l => `${tag(name)} ${l}\n`).join(''));
}
if (buf && !(failed && failed !== name)) out.write(`${tag(name)} ${buf}\n`);
@ -186,5 +195,6 @@ for (const r of [...runs.values()].sort((a, b) => a.start - b.start)) {
console.log(` ${r.task.name.padEnd(width)} ${secs}s @${((r.start - t0) / 1000).toFixed(1).padStart(5)}s ${s}`);
}
for (const t of pending) console.log(` ${t.name.padEnd(width)} - not started`);
if (retried.length) console.log(`\nretried under load (${retried.length}; each timed out once while load ÷ cores was above 2, then ran again):\n${retried.map(r => ` ${r}`).join('\n')}`);
if (failed) { console.error(`\nci: ${failed} failed`); process.exit(1); }
console.log(`\n${quick ? 'ci:quick' : 'ci'}: all green`);