node/lib/internal/test_runner/reporter/rerun.js
atlowChemi 74ccf384a9
test_runner: fix --test-rerun-failures swallowing failures on retry
Three independent bugs interacted to let a real failure-on-retry be
reported as a pass:

1. The runner's disambiguator stored the counter against the suffixed
   identifier (after mutation) instead of the base key, so the counter
   never advanced past 1 and every 3rd+ same-loc registration collided
   on :(1).
2. The reporter had the same off-by-one when writing the state file.
3. The reporter only bumped its counter on `test:pass`, so any failing
   test at a shared source location desynchronised the writer and
   runner counters - on retry, the surviving failing sibling would
   inherit a slot that in the previous attempt belonged to a different
   (passing) sibling. Node matched by that slot, replaced `this.fn`
   with a synthetic noop replay, and reported the failure as a pass.

Track the base identifier separately in the runner, bump the counter
against the base key in both the runner and the reporter, and bump the
reporter's counter on `test:fail` in addition to `test:pass`.

Fixes: https://github.com/nodejs/node/issues/63424
Signed-off-by: atlowChemi <chemi@atlow.co.il>
PR-URL: https://github.com/nodejs/node/pull/63431
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
2026-05-24 10:29:56 +03:00

81 lines
2.5 KiB
JavaScript

'use strict';
const {
ArrayPrototypeMap,
ArrayPrototypePush,
JSONStringify,
} = primordials;
const { relative } = require('path');
const { writeFileSync } = require('fs');
function reportReruns(previousRuns, globalOptions) {
return async function reporter(source) {
const obj = { __proto__: null };
const disambiguator = { __proto__: null };
let currentSuite = null;
const roots = [];
function getTestId(data) {
return `${relative(globalOptions.cwd, data.file)}:${data.line}:${data.column}`;
}
function startTest(data) {
const originalSuite = currentSuite;
currentSuite = { __proto__: null, data, parent: currentSuite, children: [] };
if (originalSuite?.children) {
ArrayPrototypePush(originalSuite.children, currentSuite);
}
if (!currentSuite.parent) {
ArrayPrototypePush(roots, currentSuite);
}
}
for await (const { type, data } of source) {
let currentTest;
if (type === 'test:start') {
startTest(data);
} else if (type === 'test:fail' || type === 'test:pass') {
if (!currentSuite) {
startTest({ __proto__: null, name: 'root', nesting: 0 });
}
if (currentSuite.data.name !== data.name || currentSuite.data.nesting !== data.nesting) {
startTest(data);
}
currentTest = currentSuite;
if (currentSuite?.data.nesting === data.nesting) {
currentSuite = currentSuite.parent;
}
}
if (type === 'test:pass' || type === 'test:fail') {
const baseIdentifier = getTestId(data);
let identifier = baseIdentifier;
if (disambiguator[baseIdentifier] !== undefined) {
identifier += `:(${disambiguator[baseIdentifier]})`;
disambiguator[baseIdentifier] += 1;
} else {
disambiguator[baseIdentifier] = 1;
}
if (type === 'test:pass') {
const children = ArrayPrototypeMap(currentTest.children, (child) => child.data);
obj[identifier] = {
__proto__: null,
name: data.name,
children,
passed_on_attempt: data.details.passed_on_attempt ?? data.details.attempt,
duration_ms: data.details.duration_ms,
};
}
}
}
ArrayPrototypePush(previousRuns, obj);
writeFileSync(globalOptions.rerunFailuresFilePath, JSONStringify(previousRuns, null, 2), 'utf8');
};
};
module.exports = {
__proto__: null,
reportReruns,
};