Stop the strict acceptance from waiting for itself

A scenario whose body ran past about 19 s idled until 45 s after its
start, and twice one never exited and hit the runner's 300 s timeout
after it had passed: something of Chrome outlived Browser.close and kept
its stdout and stderr open.

- scripts/qa/browser.mjs closes Chrome within 5 s, kills its process
  group and closes its pipes; the harness then finishes and exits with
  the scenario's code. QA_EXIT_DIAGNOSTICS=1 keeps the old ending and
  prints the open handles and Chrome's process tree 5 s after finish.
- Every strict page starts with its clock at the 18:30 UTC nearest to
  the real time, so Navara's sun is up over the test cities whenever the
  acceptance runs; a scenario can ask for its own instant or the real
  clock (Wildlife does, for its freshness fixtures).
- The runner records a scenario that passed but did not exit as hung,
  kills the whole process group on a timeout, runs a failed scenario
  once more and reports one that passes then as flaky, and appends each
  run to --history=FILE; scripts/report-acceptance-flake.mjs reads it.
This commit is contained in:
German Khokhlov committed 2026-11-10 14:10:46 +04:00
1 parent a913e5466e
commit ed6ae54ef6
19 files changed
+1039 -104

No files matched your search

+73
View File
@@ -0,0 +1,73 @@
import { processTree, readProcesses } from './process.mjs';
/** Bound shutdown even when Chrome leaves descendants holding its pipes. */
export async function closeBrowser(browser, { deadlineMs = 5000 } = {}) {
const start = performance.now();
let child;
let closed = false;
let killed = false;
let timer;
try {
child = browser.process();
} catch {}
try {
closed = await Promise.race([
Promise.resolve()
.then(() => browser.close())
.then(
() => true,
() => false,
),
new Promise((resolve) => {
timer = setTimeout(() => resolve(false), deadlineMs);
}),
]);
} catch {}
clearTimeout(timer);
if (process.env.QA_EXIT_DIAGNOSTICS !== '1') {
try {
if (Number.isInteger(child?.pid) && child.pid > 0) {
// The group can outlive its leader, so probe the group itself.
process.kill(-child.pid, 0);
process.kill(-child.pid, 'SIGKILL');
killed = true;
}
} catch {}
for (const name of ['stdout', 'stderr']) {
try {
child?.[name]?.destroy();
} catch {}
}
}
return { closed, killed, ms: performance.now() - start };
}
export async function exitAfterFlush(code = process.exitCode ?? 0) {
process.exitCode = code;
if (process.env.QA_EXIT_DIAGNOSTICS === '1') return;
await Promise.all(
[process.stdout, process.stderr].map(
(stream) =>
new Promise((resolve) => {
if (stream.destroyed) return resolve();
stream.write('', resolve);
}),
),
);
process.exit(code);
}
export function diagnoseExit(pids) {
if (process.env.QA_EXIT_DIAGNOSTICS !== '1') return;
setTimeout(() => {
console.error('QA EXIT resources', process.getActiveResourcesInfo());
const processes = readProcesses();
console.error('QA EXIT Chrome: pid ppid pgid elapsed command');
for (const pid of pids) {
const rows = processTree(pid, processes);
console.error(`Chrome ${pid}:`);
for (const row of rows) console.error(row.line);
if (!rows.length) console.error('(no remaining processes)');
}
}, 5000).unref();
}