Skip to content

Commit b1fa59c

Browse files
MoLowaduh95
authored andcommitted
test_runner: preserve run duration when using test-rerun
Signed-off-by: Moshe Atlow <moshe@atlow.co.il> PR-URL: #63429 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Aviv Keller <me@aviv.sh>
1 parent 5a0b32d commit b1fa59c

4 files changed

Lines changed: 43 additions & 1 deletion

File tree

lib/internal/test_runner/reporter/rerun.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function reportReruns(previousRuns, globalOptions) {
6262
name: data.name,
6363
children,
6464
passed_on_attempt: data.details.passed_on_attempt ?? data.details.attempt,
65+
duration_ms: data.details.duration_ms,
6566
};
6667
}
6768
}

lib/internal/test_runner/test.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ const {
1010
ArrayPrototypeSplice,
1111
ArrayPrototypeUnshift,
1212
ArrayPrototypeUnshiftApply,
13+
BigInt,
1314
Error,
1415
FunctionPrototype,
1516
MathFloor,
1617
MathMax,
18+
MathRound,
1719
Number,
1820
NumberPrototypeToFixed,
1921
ObjectFreeze,
@@ -803,14 +805,25 @@ class Test extends AsyncResource {
803805
const previousAttempt = this.root.harness.previousRuns[this.attempt - 1]?.[testIdentifier];
804806
if (previousAttempt != null) {
805807
this.passedAttempt = previousAttempt.passed_on_attempt;
808+
if (previousAttempt.duration_ms !== undefined) {
809+
this.replayedDurationNs = BigInt(MathRound(previousAttempt.duration_ms * 1_000_000));
810+
}
806811
this.fn = () => {
812+
// Restore the original duration on the synthetic replay. Suites are
813+
// skipped here because Suite.run() unconditionally reassigns
814+
// startTime later; only non-suite tests benefit from setting it.
815+
if (this.reportedType !== 'suite') {
816+
this.startTime = hrtime();
817+
this.endTime = this.startTime + (this.replayedDurationNs ?? 0n);
818+
}
807819
for (let i = 0; i < (previousAttempt.children?.length ?? 0); i++) {
808820
const child = previousAttempt.children[i];
809821
const t = this.createSubtest(Test, child.name, { __proto__: null }, noop, {
810822
__proto__: null,
811823
loc: [child.line, child.column, child.file],
812824
}, noop);
813-
t.endTime = t.startTime = hrtime();
825+
t.startTime = hrtime();
826+
t.endTime = t.startTime + (t.replayedDurationNs ?? 0n);
814827
// For suites, Suite.run() starts the subtests via SafePromiseAll.
815828
// Starting them here as well would run them twice, re-invoking the
816829
// synthetic children-creator against a now-incremented disambiguator
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict';
2+
// A passing test with a measurable duration alongside a failing test. The
3+
// failing test forces the rerun feature to retry on a second invocation,
4+
// causing the passing test to be replayed via the synthetic noop stub.
5+
const { test } = require('node:test');
6+
7+
test('passing slow test', async () => {
8+
await new Promise((resolve) => setTimeout(resolve, 25));
9+
});
10+
11+
test('always failing', () => {
12+
throw new Error('boom');
13+
});

test/parallel/test-runner-test-rerun-failures.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ const getStateFile = async () => {
8989
res.forEach((entry) => {
9090
for (const item in entry) {
9191
delete entry[item].children;
92+
delete entry[item].duration_ms;
9293
}
9394
});
9495
return res;
@@ -152,6 +153,20 @@ test('test should pass on third rerun with `--test`', async () => {
152153
assert.deepStrictEqual(await getStateFile(), expectedStateFile);
153154
});
154155

156+
test('rerun preserves the original duration on the replayed pass', async () => {
157+
const durationFixture = fixtures.path('test-runner', 'rerun-duration.js');
158+
const args = ['--test-rerun-failures', stateFile, durationFixture];
159+
160+
await common.spawnPromisified(process.execPath, args);
161+
await common.spawnPromisified(process.execPath, args);
162+
163+
const raw = JSON.parse(await readFile(stateFile, 'utf8'));
164+
const passKey = Object.keys(raw[0]).find((k) => raw[0][k].name === 'passing slow test');
165+
assert.ok(passKey, 'expected the passing test to be recorded on attempt 0');
166+
assert.ok(raw[0][passKey].duration_ms > 0, 'expected a measurable duration on attempt 0');
167+
assert.strictEqual(raw[1][passKey].duration_ms, raw[0][passKey].duration_ms);
168+
});
169+
155170
test('using `run` api', async () => {
156171
let stream = run({ files: [fixture], rerunFailuresFilePath: stateFile });
157172
stream.on('test:pass', common.mustCall(19));

0 commit comments

Comments
 (0)