fix(scripts): don't arm a survival timer after the verdict is settled (#2147)
Copilot review round 4. `exit` can be observed before the child's last output drains — `done()` already keeps reading until `close` for exactly that reason — so `onData` can run after the verdict is settled. Recording `markerAt` there is still right: the message is a thunk rendered at `finish()`, and a marker still sitting in the pipe when `exit` fired was painted all the same. Arming a survival timer is not. It could not change the result (`done` is a no-op once settled), but it holds the event loop open for the whole window, which under `node --test` is a suite lingering seconds for nothing. That implies something the comment did not say, and it had already bitten this PR: if `markerAt` can be set after the exit was observed, any wording derived from observation ORDER is unstable. The old message read "rendered at Nms and then exited Mms later", where M was `since() - markerAt` evaluated at finish — not the paint-to-exit gap at all, and incoherent whenever the marker surfaced during the drain. The test asserted on /survival window/, which the losing side of that race could drop. It could have flaked, and the flake would have looked like the bug this PR fixes. So the message now makes no ordering claim and reports both times as the observations they are, with `exitAt` captured in the exit handler rather than recomputed later. Assertions moved to the race-stable phrase, plus a test that runs the paint-then-die stub 12x asserting verdict AND wording are identical every time — one pass proves nothing about a race. The ordering could not be forced on darwin, so the guard is reasoned rather than reproduced, and the reply says so. Two attempts and what they actually showed: a 4MB payload with the marker last never gets there, because `process.exit()` discards pending stdout writes (the "before rendering" branch is correctly taken); marker on stderr with a graceful exitCode was observed first in all 30 runs, none lingering near the window. `done()` exists precisely because exit and close are separate events, so treating their order as guaranteed was worth removing anyway. Also (suppressed comment): the README's "Two more do the same" clause now lists four suites, not two, after this PR added render-smoke and pty. Signed-off-by: Cliff Hall <cliff@futurescale.com> Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
747f59892f
commit
4af38c719c
1 changed files with 41 additions and 5 deletions
|
|
@ -246,6 +246,15 @@ export function runRenderSmoke({
|
|||
// <marker>" — quoting an output tail that visibly contains the marker.
|
||||
// First paint has happened; its deadline no longer has a question to ask.
|
||||
clearTimeout(renderTimer);
|
||||
// `exit` can beat the last of the child's buffered output, and `done`
|
||||
// then keeps reading until `close` — so this can run *after* the verdict
|
||||
// is settled. Recording `markerAt` is still right (the deferred message
|
||||
// below reads it, and a marker that only surfaced in the drain was still
|
||||
// painted). Arming a timer is not: `done` is a no-op once settled, so it
|
||||
// could not change the result, but it holds the event loop open for the
|
||||
// whole survival window — which under `node --test` is a suite that
|
||||
// hangs on for two seconds per case for no reason.
|
||||
if (settled) return;
|
||||
// First paint is not the verdict — it only starts the clock on the
|
||||
// question this smoke actually asks.
|
||||
surviveTimer = setTimeout(() => {
|
||||
|
|
@ -267,14 +276,25 @@ export function runRenderSmoke({
|
|||
|
||||
child.on("exit", (code, signal) => {
|
||||
childExited = true;
|
||||
const exitAt = since();
|
||||
if (settled) return;
|
||||
const how = signal ? `signal ${signal}` : `code ${code}`;
|
||||
// Thunked: rendered at `finish()`, i.e. after `close`, so `markerAt` is
|
||||
// whatever the FULL output showed — a marker still sitting in the pipe
|
||||
// when `exit` fired is accounted for by the time this is read.
|
||||
//
|
||||
// Deliberately makes no claim about the order in which this process
|
||||
// *observed* the two. `exit` and the last `data` event race, so a
|
||||
// "N ms later" phrased off observation times would flip wording (and
|
||||
// sign) run to run on the same child. The times are reported as the
|
||||
// observations they are and the verdict does not rest on them.
|
||||
done(1, () =>
|
||||
markerAt === null
|
||||
? `child exited (${how}) before rendering "${marker}"\n${outputTail(output)}`
|
||||
: `child rendered "${marker}" at ${markerAt}ms and then exited (${how}) ` +
|
||||
`${since() - markerAt}ms later, inside the ${surviveMs}ms survival window — ` +
|
||||
`it painted one frame, it did not run\n${outputTail(output)}`,
|
||||
: `child rendered "${marker}" and then exited (${how}) inside the ` +
|
||||
`${surviveMs}ms survival window — it painted one frame, it did not run ` +
|
||||
`(first paint seen at ${markerAt}ms, exit at ${exitAt}ms)` +
|
||||
`\n${outputTail(output)}`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue