ci speed: desktop boot check runs its cases in parallel (one browser, killed at the end; a page per case; condition waits instead of fixed sleeps: element, text or a quiet window of host calls and DOM); shots capture / render close the browser with a 3 s cap, then kill it; console engine waits injectable for tests (WAITS / engineWaits: poll rounds, a missing step ends once its loaded page stays quiet, team switch settle, field-miss grace), fillPage looks up each data type's dialog once per round and skips dialog sections when no dialog is open; browser tests run 4 at a time per file (testScope().concurrent with a scope per test, --max-concurrency in ci), 20 ms rounds; capture-web counts boots per test and runs its browser tests concurrently; console-session / -persist Chrome tests concurrent; recorded test timings smoothed (mergeTimings) so one loaded run does not skew the shard split

This commit is contained in:
CRTkafa 2026-10-13 02:20:03 +08:00
parent f2d7062bcb
commit 9ba8471002
16 changed files with 390 additions and 178 deletions

View file

@ -13,6 +13,8 @@
// - polling sleeps in the waits for log lines: event-driven now (resolved on each new line)
// Hook timeouts are bounded by the numbers below (each close is capped), never open-ended.
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AsyncLocalStorage } from 'node:async_hooks';
import { test } from 'bun:test';
/** Launching Chromium: well above a cold start on a loaded machine (seconds), far below "hung". */
export const LAUNCH_TIMEOUT_MS = 60_000;
@ -25,6 +27,16 @@ export const CLOSE_TIMEOUT_MS = 15_000;
export const AFTER_EACH_MS = 2 * CLOSE_TIMEOUT_MS + 5_000;
export const AFTER_ALL_MS = CLOSE_TIMEOUT_MS + 5_000;
/**
* The console engine's timings for tests on local fixture pages (production: a 1 s watch round, 200 ms poll rounds,
* a missing step waited out for the whole step timeout). A round of 20 ms; a poll ends as "not found" once the page
* is loaded and unchanged for 1.5 s (well above the one timer a fixture page has, asc-shell.html's 700 ms redirect),
* so a step that is not there no longer costs the whole navTimeoutMs, while a step that is there is still waited for
* as long as the machine needs. Set with engineWaits (cli/console/fill.ts) in beforeAll, restored in afterAll.
*/
export const TEST_TICK_MS = 20;
export const TEST_WAITS = { pollMs: 50, quietMs: 1500, settleMs: 50, missMs: 300 };
/** Waits for `p` (its error ignored) or `ms`, whichever is first: true when `p` settled in time. Never rejects. */
export async function capped(p: Promise<unknown>, ms: number): Promise<boolean> {
let t: ReturnType<typeof setTimeout> | undefined;
@ -88,11 +100,8 @@ export async function persistentBrowser(): Promise<{ label: string | null; skipW
: { label: null, skipWhy: 'neither Google Chrome nor Playwright\'s full Chromium is installed (the headless shell drops session cookies on relaunch); install Google Chrome or run `bunx playwright install chromium`' };
}
/**
* The contexts and engine runs of the current test. `newPage` opens a page in a fresh context (as browser.newPage
* does) and tracks it; `track` adds one opened elsewhere; `settle` (afterEach) closes them all and waits for the runs.
*/
export function testScope(browser: () => any) {
/** The contexts and engine runs of one test (see testScope). */
function plainScope(browser: () => any) {
const contexts = new Set<any>();
const runs = new Set<Promise<unknown>>();
return {
@ -113,6 +122,33 @@ export function testScope(browser: () => any) {
};
}
/**
* The contexts and engine runs of the current test. `newPage` opens a page in a fresh context (as browser.newPage
* does) and tracks it; `track` adds one opened elsewhere; `settle` (afterEach) closes them all and waits for the runs.
* `concurrent` declares a browser test that runs at the same time as the file's other concurrent tests
* (test.concurrent, at most --max-concurrency at once: tools/ci.ts passes BROWSER_CONCURRENCY from tools/ci-plan.ts): it gets a scope of
* its own (AsyncLocalStorage, so newPage / track / run inside it reach that scope), settled when it ends, inside its
* timeout. The tests share the one browser; each has its own contexts, pages and log.
*/
export function testScope(browser: () => any) {
type Scope = ReturnType<typeof plainScope>;
const shared = plainScope(browser);
const own = new AsyncLocalStorage<Scope>();
const cur = (): Scope => own.getStore() ?? shared;
return {
newPage: (opts?: Record<string, unknown>): Promise<any> => cur().newPage(opts),
track: <T>(ctx: T): T => cur().track(ctx),
run: <T>(p: Promise<T>): Promise<T> => cur().run(p),
settle: (): Promise<void> => shared.settle(),
concurrent(name: string, fn: () => Promise<void>, ms: number): void {
test.concurrent(name, () => {
const s = plainScope(browser);
return own.run(s, async () => { try { await fn(); } finally { await s.settle(); } });
}, ms + AFTER_EACH_MS);
},
};
}
/**
* The log lines of an engine run (`say`), with event-driven waits: `wait` resolves on the line that makes `pred`
* true (checked on every new line, no polling) and fails after `ms` with every line seen so far.