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
|
|
@ -36,12 +36,26 @@ test("fails when the child paints the marker and then exits non-zero", async ()
|
|||
const r = await runRenderSmoke({ ...opts, ...paintThenExit(1) });
|
||||
assert.equal(r.code, 1, `expected failure, got: ${r.message}`);
|
||||
assert.match(r.message, /code 1/);
|
||||
assert.match(r.message, /survival window/);
|
||||
assert.match(r.message, /it painted one frame, it did not run/);
|
||||
// The diagnostic must not read as a crash *before* first paint — that was a
|
||||
// different defect, and conflating them sends the reader to the wrong place.
|
||||
assert.doesNotMatch(r.message, /before rendering/);
|
||||
});
|
||||
|
||||
// `exit` races the child's last `data` event, so this stub can be observed
|
||||
// either way round on the same machine. Both orders must produce the same
|
||||
// verdict AND the same wording — a message phrased off observation order would
|
||||
// flip run to run, which is how a stable failure gets written off as flake.
|
||||
// Repeated because one pass proves nothing about a race.
|
||||
test("the verdict does not depend on whether exit beats the final output", async () => {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const r = await runRenderSmoke({ ...opts, ...paintThenExit(1) });
|
||||
assert.equal(r.code, 1, `run ${i}: expected failure, got: ${r.message}`);
|
||||
assert.match(r.message, /it painted one frame, it did not run/, `run ${i}`);
|
||||
assert.doesNotMatch(r.message, /before rendering/, `run ${i}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Exiting 0 right after painting is the same failure: the app did not run. It
|
||||
// is called out separately because `script(1)` under busybox reports 0 for a
|
||||
// crashed child, so the verdict cannot rest on the exit code.
|
||||
|
|
@ -159,10 +173,12 @@ test("a zero survival window falls back rather than passing instantly", async ()
|
|||
surviveMs: Number(""),
|
||||
});
|
||||
assert.equal(r.code, 1, `expected failure, got: ${r.message}`);
|
||||
// The window named in the message is the normalized one, not the 0 passed in.
|
||||
assert.match(
|
||||
r.message,
|
||||
new RegExp(`${DEFAULTS.surviveMs}ms survival window`),
|
||||
);
|
||||
assert.doesNotMatch(r.message, /\b0ms survival window/);
|
||||
});
|
||||
|
||||
test("outputTail drops a leading partial line but keeps short output whole", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue