mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
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:
1 parent
a913e5466e
commit
ed6ae54ef6
19 files changed
+1039
-104
No files matched your search
@@ -0,0 +1,119 @@
|
||||
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,
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user