mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
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.
120 lines
3.4 KiB
JavaScript
120 lines
3.4 KiB
JavaScript
import { execFileSync, spawn } from 'node:child_process';
|
|
|
|
export function readProcesses() {
|
|
try {
|
|
return execFileSync('ps', ['-axo', 'pid=,ppid=,pgid=,etime=,command='], {
|
|
encoding: 'utf8',
|
|
timeout: 2000,
|
|
maxBuffer: 1 << 24,
|
|
})
|
|
.trim()
|
|
.split('\n')
|
|
.flatMap((line) => {
|
|
const match = line
|
|
.trim()
|
|
.match(/^(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/);
|
|
return match
|
|
? [
|
|
{
|
|
pid: Number(match[1]),
|
|
ppid: Number(match[2]),
|
|
pgid: Number(match[3]),
|
|
elapsed: match[4],
|
|
command: match[5],
|
|
line: line.trim(),
|
|
},
|
|
]
|
|
: [];
|
|
});
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/** Include the group even when its leader has already exited. */
|
|
export function processTree(pid, processes) {
|
|
const ids = new Set([pid]);
|
|
let changed;
|
|
do {
|
|
changed = false;
|
|
for (const row of processes) {
|
|
if (!ids.has(row.pid) && (ids.has(row.ppid) || row.pgid === pid)) {
|
|
ids.add(row.pid);
|
|
changed = true;
|
|
}
|
|
}
|
|
} while (changed);
|
|
return processes.filter((row) => ids.has(row.pid));
|
|
}
|
|
|
|
/** Sequential callers get captured output and a deadline for the whole tree. */
|
|
export function runScenario(command, args, { timeout, env }) {
|
|
return new Promise((resolve) => {
|
|
const child = spawn(command, args, {
|
|
env,
|
|
detached: true,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
const groups = new Set(child.pid ? [child.pid] : []);
|
|
const stdout = [];
|
|
const stderr = [];
|
|
let bytes = 0;
|
|
let error = null;
|
|
let done = false;
|
|
const rememberGroups = () => {
|
|
const processes = readProcesses();
|
|
for (const pid of [...groups]) {
|
|
const rows = processTree(pid, processes);
|
|
if (processes.length && !rows.length && pid !== child.pid)
|
|
groups.delete(pid);
|
|
for (const row of rows) groups.add(row.pgid);
|
|
}
|
|
};
|
|
const finish = (status, signal) => {
|
|
if (done) return;
|
|
done = true;
|
|
clearTimeout(deadline);
|
|
clearInterval(watch);
|
|
resolve({
|
|
status,
|
|
signal,
|
|
error,
|
|
stdout: Buffer.concat(stdout).toString(),
|
|
stderr: Buffer.concat(stderr).toString(),
|
|
});
|
|
};
|
|
const stop = (code, message) => {
|
|
if (done) return;
|
|
error = Object.assign(new Error(message), { code });
|
|
rememberGroups();
|
|
// Chrome detaches into another group; retain it before Node can exit.
|
|
for (const pid of [...groups].reverse()) {
|
|
try {
|
|
if (pid > 0) process.kill(-pid, 'SIGKILL');
|
|
} catch {}
|
|
}
|
|
child.stdout.destroy();
|
|
child.stderr.destroy();
|
|
child.unref();
|
|
finish(null, 'SIGKILL');
|
|
};
|
|
const capture = (chunks) => (chunk) => {
|
|
bytes += chunk.length;
|
|
if (bytes > 1 << 26) stop('ENOBUFS', 'Scenario output exceeded 64 MiB');
|
|
else chunks.push(chunk);
|
|
};
|
|
child.stdout.on('data', capture(stdout));
|
|
child.stderr.on('data', capture(stderr));
|
|
child.on('error', (failure) => {
|
|
error = failure;
|
|
});
|
|
child.on('close', finish);
|
|
// Keep detached descendants identifiable if Chrome's leader exits first.
|
|
const watch = setInterval(rememberGroups, 1000);
|
|
const deadline = setTimeout(
|
|
() => stop('ETIMEDOUT', `Scenario timed out after ${timeout} ms`),
|
|
timeout,
|
|
);
|
|
});
|
|
}
|