Skip to content

Commit f96a879

Browse files
committed
test_runner: address flaky retry review nits
Reflect reviewer feedback on the flaky retry change: add JSDoc block comments for IntelliSense, wrap long conditionals and calls across multiple lines, clarify comment wording, and simplify the report directive merge since spreading undefined is a no-op. Alphabetize the diagnostics filter list and de-mangle the TAP and JUnit reporter calls. No behavior change. Signed-off-by: sangwook <rewq5991@gmail.com>
1 parent 442ac3c commit f96a879

5 files changed

Lines changed: 132 additions & 62 deletions

File tree

lib/internal/test_runner/harness.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,8 @@ function runInParentContext(Factory) {
412412
const overrides = {
413413
__proto__: null,
414414
[keyword]: keyword === 'flaky' && typeof options?.[keyword] === 'number' ?
415-
options[keyword] : true,
415+
options[keyword] :
416+
true,
416417
loc: getCallerLocation(),
417418
};
418419

lib/internal/test_runner/reporter/junit.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,19 @@ module.exports = async function* junitReporter(source) {
133133
}
134134
if (event.data.retryCount > 0) {
135135
ArrayPrototypePush(currentTest.children, {
136-
__proto__: null, nesting: event.data.nesting + 1, tag: 'properties',
136+
__proto__: null,
137+
nesting: event.data.nesting + 1,
138+
tag: 'properties',
137139
attrs: { __proto__: null },
138140
children: [{
139-
__proto__: null, nesting: event.data.nesting + 2, tag: 'property',
140-
attrs: { __proto__: null, name: 'flaky', value: event.data.retryCount },
141+
__proto__: null,
142+
nesting: event.data.nesting + 2,
143+
tag: 'property',
144+
attrs: {
145+
__proto__: null,
146+
name: 'flaky',
147+
value: event.data.retryCount,
148+
},
141149
}],
142150
});
143151
}

lib/internal/test_runner/reporter/tap.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,30 @@ async function * tapReporter(source) {
3434
for await (const { type, data } of source) {
3535
switch (type) {
3636
case 'test:fail': {
37-
yield reportTest(data.nesting, data.testNumber, 'not ok', data.name,
38-
data.skip, data.todo, data.expectFailure, data.retryCount);
37+
yield reportTest(
38+
data.nesting,
39+
data.testNumber,
40+
'not ok',
41+
data.name,
42+
data.skip,
43+
data.todo,
44+
data.expectFailure,
45+
data.retryCount,
46+
);
3947
const location = data.file ? `${data.file}:${data.line}:${data.column}` : null;
4048
yield reportDetails(data.nesting, data.details, location);
4149
break;
4250
} case 'test:pass':
43-
yield reportTest(data.nesting, data.testNumber, 'ok', data.name,
44-
data.skip, data.todo, data.expectFailure, data.retryCount);
51+
yield reportTest(
52+
data.nesting,
53+
data.testNumber,
54+
'ok',
55+
data.name,
56+
data.skip,
57+
data.todo,
58+
data.expectFailure,
59+
data.retryCount,
60+
);
4561
yield reportDetails(data.nesting, data.details, null);
4662
break;
4763
case 'test:plan':

lib/internal/test_runner/runner.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,15 @@ const kFilterArgValues = [
123123
'--experimental-config-file',
124124
];
125125
const kDiagnosticsFilterArgs = [
126-
'tests', 'suites', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'flaky', 'duration_ms',
126+
'cancelled',
127+
'duration_ms',
128+
'fail',
129+
'flaky',
130+
'pass',
131+
'skipped',
132+
'suites',
133+
'tests',
134+
'todo',
127135
];
128136

129137
const kCanceledTests = new SafeSet()
@@ -321,9 +329,11 @@ class FileTest extends Test {
321329
// Only an exhausted flaky timeout is upgraded to a failure, mirroring
322330
// the child-side rule; other cancellations stay cancelled.
323331
cancelled: kCanceledTests.has(item.data.details?.error?.failureType) &&
324-
!(item.data.retryCount > 0 &&
325-
item.data.details?.error?.failureType === kTestTimeoutFailure),
326-
// retryCount is present (even 0) only for flaky-marked tests, so its
332+
!(
333+
item.data.retryCount > 0 &&
334+
item.data.details?.error?.failureType === kTestTimeoutFailure
335+
),
336+
// retryCount is present (even when 0) only for flaky-tagged tests, so its
327337
// presence is what marks this reconstructed test as flaky for the parent
328338
// counter across the process-isolation IPC boundary.
329339
isFlaky: item.data.retryCount !== undefined,

lib/internal/test_runner/test.js

Lines changed: 85 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ class TestContext {
335335
const assert = { __proto__: null };
336336
// Each assertion counts against an active `t.plan()`. A flaky test re-arms
337337
// `test.plan` on every retry, so it must be read live (deferred to call
338-
// time). A non-flaky test counts against the plan captured once here, at
338+
// time). A non-flaky test counts against the plan captured here once, at
339339
// first `.assert` access: this preserves the historical behavior that a
340340
// `t.plan()` called *after* the first assertion is not retroactively
341341
// counted (see test-runner/flaky/assert-before-plan). The flaky-vs-captured
@@ -753,11 +753,12 @@ class Test extends AsyncResource {
753753
this.plan = null;
754754
this.expectedAssertions = plan;
755755
this.cancelled = false;
756-
if (flaky === undefined || flaky === null) {
757-
// Inherit flaky only from a suite parent: a suite propagates it to its
756+
if (flaky == null) {
757+
// Inherit flaky only from a suite parent: a suite propagates to its
758758
// test-cases, but a test must not propagate it to its own subtests.
759759
this.flakyRetries = this.parent?.reportedType === 'suite' ?
760-
(this.parent.flakyRetries ?? 0) : 0;
760+
(this.parent.flakyRetries ?? 0) :
761+
0;
761762
} else if (typeof flaky === 'boolean') {
762763
this.flakyRetries = flaky ? kDefaultFlakyRetries : 0;
763764
} else if (typeof flaky === 'number') {
@@ -1322,12 +1323,17 @@ class Test extends AsyncResource {
13221323
this.report = noop;
13231324
queueMicrotask(() => this.postRun());
13241325
}
1325-
// Reset per-attempt state before retrying a flaky test. Anything that would
1326-
// otherwise leak between attempts is cleared here so the retry starts from a
1327-
// clean slate. `before`/`after` (suite-once) and the accumulated parent hooks
1328-
// are intentionally left untouched. `baseDiagnosticsCount` is the diagnostics
1329-
// length captured before the first attempt, so diagnostics emitted during
1330-
// construction survive while a previous attempt's are discarded.
1326+
/**
1327+
* Reset per-attempt state before retrying a flaky test. Anything that would
1328+
* otherwise leak between attempts is cleared here so the retry starts from a
1329+
* clean slate. `before`/`after` (suite-once) and the accumulated parent hooks
1330+
* are intentionally left untouched. `baseDiagnosticsCount` is the diagnostics
1331+
* length captured before the first attempt, so diagnostics emitted during
1332+
* construction survive while a previous attempt's are discarded.
1333+
* @param {TestContext} ctx
1334+
* @param {number} baseDiagnosticsCount
1335+
* @param {{ beforeEach: number, ownAfterEachCount: number }} baseHookCounts
1336+
*/
13311337
#resetForRetry(ctx, baseDiagnosticsCount, baseHookCounts) {
13321338
this.error = null;
13331339
this.passed = false;
@@ -1339,11 +1345,11 @@ class Test extends AsyncResource {
13391345
// Reset assertion bookkeeping; re-arm the plan if one was requested via the
13401346
// `plan` option so that `t.plan(N)` does not accumulate across attempts.
13411347
this.plan = null;
1342-
if (this.expectedAssertions) {
1348+
if (this.expectedAssertions != null) {
13431349
ctx.plan(this.expectedAssertions);
13441350
}
1345-
// Drop subtests created by the previous attempt so they are not double
1346-
// counted or double reported. outputSubtestCount must be reset too:
1351+
// Drop subtests created by the previous attempt so they are not double-
1352+
// counted or double-reported. `outputSubtestCount` must be reset too:
13471353
// otherwise it accumulates across attempts while `subtests` holds only the
13481354
// final attempt's children, inflating the plan count and - when the final
13491355
// attempt creates no subtest - leaving outputSubtestCount > 0 with an empty
@@ -1364,8 +1370,11 @@ class Test extends AsyncResource {
13641370
// The re-executing body re-registers its per-attempt hooks; before/after
13651371
// are kept (runOnce, and an earlier attempt's teardown must survive).
13661372
ArrayPrototypeSplice(this.hooks.beforeEach, baseHookCounts.beforeEach);
1367-
ArrayPrototypeSplice(this.hooks.afterEach, baseHookCounts.ownAfterEachCount,
1368-
this.hooks.ownAfterEachCount - baseHookCounts.ownAfterEachCount);
1373+
ArrayPrototypeSplice(
1374+
this.hooks.afterEach,
1375+
baseHookCounts.ownAfterEachCount,
1376+
(this.hooks.ownAfterEachCount - baseHookCounts.ownAfterEachCount),
1377+
);
13691378
this.hooks.ownAfterEachCount = baseHookCounts.ownAfterEachCount;
13701379
// Abort the previous attempt's controller so any signal-bound work it
13711380
// started (e.g. a timer awaiting t.signal) is torn down instead of leaking.
@@ -1377,13 +1386,20 @@ class Test extends AsyncResource {
13771386
this.abortController = new AbortController();
13781387
this.signal = this.abortController.signal;
13791388
}
1380-
// The retry-eligibility terms shared by the catch-path `willRetry` gate and
1381-
// the loop's `shouldRetry` decision. Kept in one place so a future stop
1382-
// condition cannot be added to one site and forgotten at the other.
1389+
/**
1390+
* The retry-eligibility terms shared by the catch-path `willRetry` gate and
1391+
* the loop's `shouldRetry` decision. Kept in one place so a future stop
1392+
* condition cannot be added to one site and forgotten at the other.
1393+
* @param {number} attempt
1394+
* @param {number} maxAttempts
1395+
* @returns {boolean}
1396+
*/
13831397
#retriesRemaining(attempt, maxAttempts) {
1384-
return attempt < maxAttempts &&
1398+
return (
1399+
attempt < maxAttempts &&
13851400
!this.expectFailure &&
1386-
!(this.signal.aborted || this.outerSignal?.aborted);
1401+
!(this.signal.aborted || this.outerSignal?.aborted)
1402+
);
13871403
}
13881404

13891405
async run() {
@@ -1436,7 +1452,11 @@ class Test extends AsyncResource {
14361452
let afterEach = makeAfterEach();
14371453
let stopPromise;
14381454
const basePublishEnd = () => testChannel.end.publish(channelContext);
1439-
const basePublishError = (err) => testChannel.error.publish({ __proto__: null, ...channelContext, error: err });
1455+
const basePublishError = (err) => testChannel.error.publish({
1456+
__proto__: null,
1457+
...channelContext,
1458+
error: err,
1459+
});
14401460
let publishEnd = basePublishEnd;
14411461
let publishError = basePublishError;
14421462

@@ -1447,11 +1467,13 @@ class Test extends AsyncResource {
14471467
const maxAttempts = this.flakyRetries + 1;
14481468
const baseDiagnosticsCount = this.diagnostics.length;
14491469
// Baseline for #resetForRetry to truncate body-registered hooks back to.
1450-
const baseHookCounts = this.isFlaky ? {
1451-
__proto__: null,
1452-
beforeEach: this.hooks.beforeEach.length,
1453-
ownAfterEachCount: this.hooks.ownAfterEachCount,
1454-
} : null;
1470+
const baseHookCounts = this.isFlaky ?
1471+
{
1472+
__proto__: null,
1473+
beforeEach: this.hooks.beforeEach.length,
1474+
ownAfterEachCount: this.hooks.ownAfterEachCount,
1475+
} :
1476+
null;
14551477

14561478
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
14571479
if (attempt > 1) {
@@ -1481,16 +1503,15 @@ class Test extends AsyncResource {
14811503
// the test runs within both the runStores context (for AsyncLocalStorage/bindStore)
14821504
// AND the AsyncResource scope. It's critical that runStores wraps the function,
14831505
// not the runInAsyncScope call itself, to maintain AsyncLocalStorage bindings.
1484-
let testFn = this.fn;
1485-
if (channelContext !== null && testChannel.start.hasSubscribers) {
1486-
testFn = (...fnArgs) => testChannel.start.runStores(channelContext, () => {
1506+
const testFn = (channelContext !== null && testChannel.start.hasSubscribers) ?
1507+
(...fnArgs) => testChannel.start.runStores(channelContext, () => {
14871508
// Rebind from the base each attempt; binding on top of the
14881509
// previous binding would pin the first attempt's async context.
14891510
publishEnd = AsyncResource.bind(basePublishEnd);
14901511
publishError = AsyncResource.bind(basePublishError);
14911512
return ReflectApply(this.fn, this, fnArgs);
1492-
});
1493-
}
1513+
}) :
1514+
this.fn;
14941515

14951516
ArrayPrototypeUnshift(runArgs, testFn, ctx);
14961517

@@ -1541,20 +1562,26 @@ class Test extends AsyncResource {
15411562
await afterEach();
15421563
// Keep `after` for the final attempt: a failed subtest is about to
15431564
// trigger a retry, which must not run against torn-down state.
1544-
const subtestRetryPending =
1565+
const subtestRetryPending = (
15451566
this.#retriesRemaining(attempt, maxAttempts) &&
1546-
ArrayPrototypeSome(this.subtests,
1547-
(subtest) => !subtest.passed && !subtest.isTodo);
1567+
ArrayPrototypeSome(
1568+
this.subtests,
1569+
(subtest) => !subtest.passed && !subtest.isTodo,
1570+
)
1571+
);
15481572
if (!subtestRetryPending) {
15491573
await after();
15501574
}
15511575
} catch (err) {
15521576
attemptError = err;
15531577
if (isTestFailureError(err)) {
1554-
if (err.failureType === kTestTimeoutFailure &&
1555-
(!this.isFlaky || this.expectFailure)) {
1556-
// expectFailure never retries, so its timeout keeps the non-flaky
1557-
// cancellation semantics instead of becoming an expected failure.
1578+
if (
1579+
err.failureType === kTestTimeoutFailure &&
1580+
(!this.isFlaky || this.expectFailure)
1581+
) {
1582+
// expectFailure tests are intentionally not retried, so an
1583+
// expectFailure timeout flows through #cancel to preserve the
1584+
// existing non-flaky cancellation semantics.
15581585
this.#cancel(err);
15591586
} else {
15601587
// A flaky timeout must not flow through #cancel: #cancel aborts the
@@ -1593,19 +1620,26 @@ class Test extends AsyncResource {
15931620
// the parent later, in postRun) is detected here so the parent still
15941621
// retries. An external abort and an expectFailure verdict stop retrying
15951622
// immediately.
1596-
const subtestsFailed = this.isFlaky &&
1597-
ArrayPrototypeSome(this.subtests, (subtest) => !subtest.passed && !subtest.isTodo);
1623+
const subtestsFailed = (
1624+
this.isFlaky &&
1625+
ArrayPrototypeSome(this.subtests, (subtest) => !subtest.passed && !subtest.isTodo)
1626+
);
15981627
if (this.passed && !subtestsFailed) {
15991628
break;
16001629
}
1601-
const shouldRetry = this.#retriesRemaining(attempt, maxAttempts) &&
1602-
(!this.passed || subtestsFailed);
1630+
const shouldRetry = (
1631+
this.#retriesRemaining(attempt, maxAttempts) &&
1632+
(!this.passed || subtestsFailed)
1633+
);
16031634
if (!shouldRetry) {
16041635
// Run any final-attempt duties deferred toward a retry that is now
16051636
// called off (e.g. the outer signal aborted during afterEach).
16061637
try { await after(); } catch { /* test is already failing */ }
1607-
if (willRetry && attemptError !== undefined &&
1608-
channelContext !== null && testChannel.error.hasSubscribers) {
1638+
if (
1639+
willRetry && attemptError !== undefined &&
1640+
channelContext !== null &&
1641+
testChannel.error.hasSubscribers
1642+
) {
16091643
publishError(attemptError);
16101644
}
16111645
break;
@@ -1828,9 +1862,7 @@ class Test extends AsyncResource {
18281862

18291863
if (this.isFlaky) {
18301864
const flaky = this.reporter.getFlaky(this.retryCount);
1831-
directive = directive === undefined ?
1832-
flaky :
1833-
{ __proto__: null, ...directive, ...flaky };
1865+
directive = { __proto__: null, ...directive, ...flaky };
18341866
}
18351867

18361868
if (this.reportedType) {
@@ -1849,8 +1881,10 @@ class Test extends AsyncResource {
18491881
return { __proto__: null, details, directive };
18501882
}
18511883

1852-
// Counters must not see a subtree that a flaky ancestor may still discard:
1853-
// park the count on that ancestor until it settles (report()) or retries.
1884+
/**
1885+
* Counters must not see a subtree that a flaky ancestor may still discard:
1886+
* park the count on that ancestor until it settles (report()) or retries.
1887+
*/
18541888
#countCompleted() {
18551889
let ancestor = this.parent;
18561890
while (ancestor !== null) {
@@ -1878,7 +1912,8 @@ class Test extends AsyncResource {
18781912
// outputSubtestCount was non-zero; fall back to this test's own nesting
18791913
// rather than dereferencing subtests[0] (which would crash the run).
18801914
const subtestNesting = this.subtests.length ?
1881-
this.subtests[0].nesting : this.nesting + 1;
1915+
this.subtests[0].nesting :
1916+
this.nesting + 1;
18821917
this.reporter.plan(subtestNesting, this.loc, this.outputSubtestCount);
18831918
} else {
18841919
this.reportStarted();

0 commit comments

Comments
 (0)