mirror of https://github.com/garrytan/gstack.git
fix(test-runner): bun's headerless failure recap can't invent a phantom failing file
Round-3 CI showed the remaining half of the recap bug: bun prints 'N tests failed:' then re-prints every (fail) line with NO file headers, so they attributed to the stale currentFile — an innocent file (test/uninstall.test.ts) was charged with another file's 5 failures. The recap marker now ends attribution (currentFile=null, chunk closed) and recap re-prints of already-recorded test names dedupe; a recap-only failure the main run never attributed still records, unattributed, as belt and braces.
This commit is contained in:
parent
aca5061274
commit
8169b30a80
|
|
@ -587,6 +587,7 @@ export class FreeRunReporter {
|
||||||
private readonly failures: FreeRunFailure[] = [];
|
private readonly failures: FreeRunFailure[] = [];
|
||||||
private readonly crashed = new Set<string>();
|
private readonly crashed = new Set<string>();
|
||||||
private currentFile: string | null = null;
|
private currentFile: string | null = null;
|
||||||
|
private inRecap = false;
|
||||||
private testsRan: number | null = null;
|
private testsRan: number | null = null;
|
||||||
private filesRan: number | null = null;
|
private filesRan: number | null = null;
|
||||||
private sawSummary = false;
|
private sawSummary = false;
|
||||||
|
|
@ -646,6 +647,16 @@ export class FreeRunReporter {
|
||||||
const line = stripAnsiLine(rawLine).replace(/^::group::/, '');
|
const line = stripAnsiLine(rawLine).replace(/^::group::/, '');
|
||||||
let visible = false;
|
let visible = false;
|
||||||
|
|
||||||
|
// Bun's terminal recap ("N tests failed:") re-prints every (fail) line
|
||||||
|
// WITHOUT re-printing file headers. Attributing those to the stale
|
||||||
|
// currentFile invented a phantom failing file on the first Linux run
|
||||||
|
// (5 real failures reported as 10 across 2 files, one innocent).
|
||||||
|
if (/^\d+ tests? failed:$/.test(line)) {
|
||||||
|
this.inRecap = true;
|
||||||
|
if (this.currentFile) this.progressFor(this.currentFile).ended = true;
|
||||||
|
this.currentFile = null;
|
||||||
|
}
|
||||||
|
|
||||||
const header = FILE_HEADER_RE.exec(line);
|
const header = FILE_HEADER_RE.exec(line);
|
||||||
if (header) {
|
if (header) {
|
||||||
const file = this.canonicalize(header[1]);
|
const file = this.canonicalize(header[1]);
|
||||||
|
|
@ -661,8 +672,12 @@ export class FreeRunReporter {
|
||||||
const final = fail || retry ? null : CRASH_FINAL_RE.exec(line);
|
const final = fail || retry ? null : CRASH_FINAL_RE.exec(line);
|
||||||
if (fail) {
|
if (fail) {
|
||||||
visible = true;
|
visible = true;
|
||||||
|
// In the recap, a (fail) line only records a failure the main run
|
||||||
|
// somehow never attributed (belt and braces); known names dedupe.
|
||||||
|
const recapDuplicate = this.inRecap
|
||||||
|
&& this.failures.some((f) => f.testName === fail[1]);
|
||||||
const key = `${this.currentFile ?? ''}\u0000${fail[1]}`;
|
const key = `${this.currentFile ?? ''}\u0000${fail[1]}`;
|
||||||
if (!this.failureKeys.has(key)) {
|
if (!recapDuplicate && !this.failureKeys.has(key)) {
|
||||||
this.failureKeys.add(key);
|
this.failureKeys.add(key);
|
||||||
this.failures.push({ file: this.currentFile, testName: fail[1] });
|
this.failures.push({ file: this.currentFile, testName: fail[1] });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -505,6 +505,23 @@ describe('test-free-shards: GitHub Actions log-group attribution', () => {
|
||||||
reporter.end();
|
reporter.end();
|
||||||
expect(reporter.report().failures).toEqual([{ file: 'test/b.test.ts', testName: 'planted' }]);
|
expect(reporter.report().failures).toEqual([{ file: 'test/b.test.ts', testName: 'planted' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('headerless recap re-prints do not invent a phantom failing file', () => {
|
||||||
|
// Round-3 CI shape: bun's recap prints "N tests failed:" then the (fail)
|
||||||
|
// lines with NO file headers — the stale currentFile (an innocent file)
|
||||||
|
// was charged with the previous file's failures.
|
||||||
|
const reporter = new FreeRunReporter(['test/a.test.ts', 'test/b.test.ts']);
|
||||||
|
reporter.write('::group::test/a.test.ts:\n', 'stderr');
|
||||||
|
reporter.write(`${failLine('planted')}\n`, 'stderr');
|
||||||
|
reporter.write('::endgroup::\n', 'stderr');
|
||||||
|
reporter.write('::group::test/b.test.ts:\n', 'stderr');
|
||||||
|
reporter.write('(pass-ish output, no failures here)\n', 'stderr');
|
||||||
|
reporter.write('2 tests failed:\n', 'stderr');
|
||||||
|
reporter.write(`${failLine('planted')}\n`, 'stderr');
|
||||||
|
reporter.write(`${failLine('planted')}\n`, 'stderr');
|
||||||
|
reporter.end();
|
||||||
|
expect(reporter.report().failures).toEqual([{ file: 'test/a.test.ts', testName: 'planted' }]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('test-free-shards: curated-list census pins', () => {
|
describe('test-free-shards: curated-list census pins', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue