Skip to content

Commit 6a7d3c4

Browse files
committed
test_runner: bound the force-exit flush wait
Ensure --test-force-exit cannot hang if a reporter destination never signals completion, by capping the per-destination flush wait. The normal path still clears the timer as soon as the stream finishes, so a destination that is actively flushing is never cut short. Signed-off-by: Navaneeth Prabha <nvps742@gmail.com>
1 parent 75270c7 commit 6a7d3c4

1 file changed

Lines changed: 19 additions & 2 deletions

File tree

lib/internal/test_runner/test.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ const { relative } = require('path');
9494
const { availableParallelism } = require('os');
9595
const { innerOk } = require('internal/assert/utils');
9696
const { bigint: hrtime } = process.hrtime;
97+
// Upper bound (in milliseconds) on how long --test-force-exit waits for a
98+
// single reporter destination to flush before giving up, so a forced exit
99+
// can never hang on a destination that fails to signal completion.
100+
const kForceExitFlushTimeout = 5000;
97101
const kCallbackAndPromisePresent = 'callbackAndPromisePresent';
98102
const kCancelledByParent = 'cancelledByParent';
99103
const kAborted = 'testAborted';
@@ -1473,9 +1477,22 @@ class Test extends AsyncResource {
14731477
const { destination } = reporterScope.reporters[i];
14741478

14751479
ArrayPrototypePush(promises, new Promise((resolve) => {
1480+
// Force exit must never hang. If a destination never signals
1481+
// completion (for example one that already errored before this
1482+
// ran), fall back to a bounded timeout so the process.exit() below
1483+
// cannot be blocked indefinitely. In the normal path the completion
1484+
// callback fires first and clears this timer, so it never cuts off
1485+
// a destination that is still flushing. The timer is unref'd so it
1486+
// does not by itself keep the event loop alive.
1487+
const timer = setTimeout(resolve, kForceExitFlushTimeout);
1488+
timer.unref();
1489+
const done = () => {
1490+
clearTimeout(timer);
1491+
resolve();
1492+
};
14761493
destination.on('unpipe', () => {
14771494
if (!destination.closed && typeof destination.close === 'function') {
1478-
destination.close(resolve);
1495+
destination.close(done);
14791496
} else {
14801497
// The destination has no close() to await - for example a child
14811498
// process's stdout, which is a pipe to the test runner's parent
@@ -1489,7 +1506,7 @@ class Test extends AsyncResource {
14891506
typeof destination._handle.setBlocking === 'function') {
14901507
destination._handle.setBlocking(true);
14911508
}
1492-
destination.end(resolve);
1509+
destination.end(done);
14931510
}
14941511
});
14951512
}));

0 commit comments

Comments
 (0)