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 crashed = new Set<string>();
|
||||
private currentFile: string | null = null;
|
||||
private inRecap = false;
|
||||
private testsRan: number | null = null;
|
||||
private filesRan: number | null = null;
|
||||
private sawSummary = false;
|
||||
|
|
@ -646,6 +647,16 @@ export class FreeRunReporter {
|
|||
const line = stripAnsiLine(rawLine).replace(/^::group::/, '');
|
||||
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);
|
||||
if (header) {
|
||||
const file = this.canonicalize(header[1]);
|
||||
|
|
@ -661,8 +672,12 @@ export class FreeRunReporter {
|
|||
const final = fail || retry ? null : CRASH_FINAL_RE.exec(line);
|
||||
if (fail) {
|
||||
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]}`;
|
||||
if (!this.failureKeys.has(key)) {
|
||||
if (!recapDuplicate && !this.failureKeys.has(key)) {
|
||||
this.failureKeys.add(key);
|
||||
this.failures.push({ file: this.currentFile, testName: fail[1] });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -505,6 +505,23 @@ describe('test-free-shards: GitHub Actions log-group attribution', () => {
|
|||
reporter.end();
|
||||
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', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue