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>
81 lines
2.5 KiB
JavaScript
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,
|
|
};
|