fix(#4012): the events reporter has to survive SIGKILL
The remote run proved the instrumentation did not work. Timing landed — "chunk 1/1 was killed after 2006ms" — but the in-flight-file naming produced nothing and fell through to the pre-existing generic message. The feature I wrote to diagnose a kill was itself destroyed by the kill. Root cause, confirmed rather than assumed. The reporter yielded strings, which node pipes into the --test-reporter-destination WriteStream. That stream BUFFERS. execFileSync's timeout sends SIGKILL, which is uncatchable and gives nothing a chance to flush, so the events sat in a buffer that died with the child. The parent's own timer reported correctly because it lives in the parent — which is exactly why half the feature looked fine. The reporter now writes each event with fs.appendFileSync, unbuffered and durable at the moment it happens, to a path passed through GSD_RUN_TESTS_EVENTS_FILE. Env vars do not count toward the Windows 32,767-char argv ceiling, so moving the path out of argv also REDUCES FIXED_OVERHEAD; the accounting moved with it rather than being left stale. The destination is now a fixed devNull sink that stays empty by design. Silence was the reason this was invisible for a whole run. Failing to read the events file now says so explicitly, and distinguishes a file that could not be read at all from one that exists but is empty — the generic fallback firing quietly is what let a broken feature look like a working one. A write is unbuffered but not atomic, so a kill can still interleave a partial line; the reader tolerates exactly one unparsable trailing line and reports the complete ones before it. The failing T1 was left red and untouched rather than weakened to pass. Three new unit tests cover the reader directly, with no subprocess, so the parsing half is verifiable without a full runner pass: missing file, existing-but-empty file, and a truncated final line. Also adds ndjson-reporter.cjs to GSD_SCRIPTS_LIB_FILES in bin/install.js — scripts/lib/ ships, and omitting it meant the file would install everywhere and orphan on uninstall. That single omission caused 4 of the 7 remote failures. Verification runs on the remote runner. Refs #4012
This commit is contained in:
@@ -2228,3 +2228,68 @@ describe('chunk packing weights measured cost (#2456)', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── analyzeChunkEvents (#3889 durability fix) ──────────────────────────────
|
||||
//
|
||||
// scripts/lib/ndjson-reporter.cjs now writes durably (fs.appendFileSync to a
|
||||
// GSD_RUN_TESTS_EVENTS_FILE path) instead of yielding strings for Node to
|
||||
// pipe through a buffered --test-reporter-destination WriteStream that a
|
||||
// SIGKILL can wipe out before it flushes. These tests exercise the READER
|
||||
// side (analyzeChunkEvents) directly against a hand-written events file, so
|
||||
// they pin the reader's contract independently of whether the writer side
|
||||
// managed to flush anything in a given subprocess run.
|
||||
const { analyzeChunkEvents } = require('../scripts/run-tests.cjs');
|
||||
|
||||
describe('analyzeChunkEvents (#3889)', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempDir('gsd-3889-events-');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('a missing events file is reported as an explicit read error, not silently as "no events"', () => {
|
||||
const missingPath = path.join(tmpDir, 'does-not-exist.ndjson');
|
||||
const result = analyzeChunkEvents(missingPath);
|
||||
assert.strictEqual(result.readError, true, 'a missing file must set readError=true');
|
||||
assert.strictEqual(result.sawAnyEvent, false);
|
||||
assert.deepStrictEqual(result.files, []);
|
||||
});
|
||||
|
||||
test('an existing-but-empty events file is distinguished from a missing one (readError=false)', () => {
|
||||
const emptyPath = path.join(tmpDir, 'empty.ndjson');
|
||||
fs.writeFileSync(emptyPath, '', 'utf8');
|
||||
const result = analyzeChunkEvents(emptyPath);
|
||||
assert.strictEqual(result.readError, false, 'an existing empty file must NOT be reported as a read error');
|
||||
assert.strictEqual(result.sawAnyEvent, false);
|
||||
assert.deepStrictEqual(result.files, []);
|
||||
});
|
||||
|
||||
test('a truncated final line does not crash the reader and earlier complete lines are still reported', () => {
|
||||
const truncatedPath = path.join(tmpDir, 'truncated.ndjson');
|
||||
const complete = [
|
||||
JSON.stringify({ type: 'test:start', file: 'a.test.cjs', name: 'first', nesting: 0, testNumber: 1, ts: 1000 }),
|
||||
JSON.stringify({ type: 'test:pass', file: 'a.test.cjs', name: 'first', nesting: 0, testNumber: 1, ts: 1010 }),
|
||||
JSON.stringify({ type: 'test:start', file: 'b.test.cjs', name: 'hangs', nesting: 0, testNumber: 1, ts: 1020 }),
|
||||
].join('\n');
|
||||
// Simulate a SIGKILL mid-appendFileSync: the trailing line is cut off
|
||||
// partway through a JSON object, exactly as an unbuffered but non-atomic
|
||||
// write can be interrupted.
|
||||
const truncatedTrailer = '\n{"type":"test:start","file":"c.test.cjs","name":"cut off mid-writ';
|
||||
fs.writeFileSync(truncatedPath, complete + truncatedTrailer, 'utf8');
|
||||
|
||||
const result = analyzeChunkEvents(truncatedPath);
|
||||
assert.strictEqual(result.readError, false, 'a readable-but-truncated file must not be a read error');
|
||||
// The complete test:start (b.test.cjs) with no matching pass/fail is
|
||||
// still identified as in-flight despite the unparsable trailing line.
|
||||
assert.deepStrictEqual(result.files, ['b.test.cjs']);
|
||||
// a.test.cjs completed (start+pass), so it must NOT show as in-flight.
|
||||
assert.ok(!result.files.includes('a.test.cjs'));
|
||||
// c.test.cjs never parsed (truncated line), so it cannot appear either.
|
||||
assert.ok(!result.files.includes('c.test.cjs'));
|
||||
assert.strictEqual(result.sawAnyEvent, true, 'the complete lines before the truncation must still count as events');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user