Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit a433759

Browse filesBrowse files
mcollinaaduh95
authored andcommitted
test_runner: show interrupted test on SIGINT
When the test runner process is killed with SIGINT (Ctrl+C), display which test was running at the time of interruption. This makes it easier to identify tests that hang or take too long. - Add `test:interrupted` event emitted when SIGINT is received - Add `interrupted()` method to TestsStream - Handle the event in both TAP and spec reporters - TAP outputs: `# Interrupted while running: <test>` - Spec outputs with yellow header and warning symbol - Use setImmediate to allow reporter stream to flush before exit With process isolation (default), shows the file path since the parent runner only knows about file-level tests. With --test-isolation=none, shows the actual test name. PR-URL: #61676 Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
1 parent b0a79b1 commit a433759
Copy full SHA for a433759

6 files changed

+104-4Lines changed: 104 additions & 4 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎doc/api/test.md‎

Copy file name to clipboardExpand all lines: doc/api/test.md
+26Lines changed: 26 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -3348,6 +3348,32 @@ This event is guaranteed to be emitted in the same order as the tests are
33483348
defined.
33493349
The corresponding execution ordered event is `'test:complete'`.
33503350

3351+
### Event: `'test:interrupted'`
3352+
3353+
<!-- YAML
3354+
added: REPLACEME
3355+
-->
3356+
3357+
* `data` {Object}
3358+
* `tests` {Array} An array of objects containing information about the
3359+
interrupted tests.
3360+
* `column` {number|undefined} The column number where the test is defined,
3361+
or `undefined` if the test was run through the REPL.
3362+
* `file` {string|undefined} The path of the test file,
3363+
`undefined` if test was run through the REPL.
3364+
* `line` {number|undefined} The line number where the test is defined, or
3365+
`undefined` if the test was run through the REPL.
3366+
* `name` {string} The test name.
3367+
* `nesting` {number} The nesting level of the test.
3368+
3369+
Emitted when the test runner is interrupted by a `SIGINT` signal (e.g., when
3370+
pressing <kbd>Ctrl</kbd>+<kbd>C</kbd>). The event contains information about
3371+
the tests that were running at the time of interruption.
3372+
3373+
When using process isolation (the default), the test name will be the file path
3374+
since the parent runner only knows about file-level tests. When using
3375+
`--test-isolation=none`, the actual test name is shown.
3376+
33513377
### Event: `'test:pass'`
33523378

33533379
* `data` {Object}
Collapse file

‎lib/internal/test_runner/harness.js‎

Copy file name to clipboardExpand all lines: lib/internal/test_runner/harness.js
+28-1Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const {
33
ArrayPrototypeForEach,
44
ArrayPrototypePush,
55
FunctionPrototypeBind,
6+
Promise,
67
PromiseResolve,
78
PromiseWithResolvers,
89
SafeMap,
@@ -32,7 +33,7 @@ const { PassThrough, compose } = require('stream');
3233
const { reportReruns } = require('internal/test_runner/reporter/rerun');
3334
const { queueMicrotask } = require('internal/process/task_queues');
3435
const { TIMEOUT_MAX } = require('internal/timers');
35-
const { clearInterval, setInterval } = require('timers');
36+
const { clearInterval, setImmediate, setInterval } = require('timers');
3637
const { bigint: hrtime } = process.hrtime;
3738
const testResources = new SafeMap();
3839
let globalRoot;
@@ -289,7 +290,33 @@ function setupProcessState(root, globalOptions) {
289290
}
290291
};
291292

293+
const findRunningTests = (test, running = []) => {
294+
if (test.startTime !== null && !test.finished) {
295+
for (let i = 0; i < test.subtests.length; i++) {
296+
findRunningTests(test.subtests[i], running);
297+
}
298+
// Only add leaf tests (innermost running tests)
299+
if (test.activeSubtests === 0 && test.name !== '<root>') {
300+
ArrayPrototypePush(running, {
301+
__proto__: null,
302+
name: test.name,
303+
nesting: test.nesting,
304+
file: test.loc?.file,
305+
line: test.loc?.line,
306+
column: test.loc?.column,
307+
});
308+
}
309+
}
310+
return running;
311+
};
312+
292313
const terminationHandler = async () => {
314+
const runningTests = findRunningTests(root);
315+
if (runningTests.length > 0) {
316+
root.reporter.interrupted(runningTests);
317+
// Allow the reporter stream to process the interrupted event
318+
await new Promise((resolve) => setImmediate(resolve));
319+
}
293320
await exitHandler(true);
294321
process.exit();
295322
};
Collapse file

‎lib/internal/test_runner/reporter/spec.js‎

Copy file name to clipboardExpand all lines: lib/internal/test_runner/reporter/spec.js
+23Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,31 @@ class SpecReporter extends Transform {
106106
break;
107107
case 'test:watch:restarted':
108108
return `\nRestarted at ${DatePrototypeToLocaleString(new Date())}\n`;
109+
case 'test:interrupted':
110+
return this.#formatInterruptedTests(data.tests);
109111
}
110112
}
113+
#formatInterruptedTests(tests) {
114+
if (tests.length === 0) {
115+
return '';
116+
}
117+
118+
const results = [
119+
`\n${colors.yellow}Interrupted while running:${colors.white}\n`,
120+
];
121+
122+
for (let i = 0; i < tests.length; i++) {
123+
const test = tests[i];
124+
let msg = `${indent(test.nesting)}${reporterUnicodeSymbolMap['warning:alert']}${test.name}`;
125+
if (test.file) {
126+
const relPath = relative(this.#cwd, test.file);
127+
msg += ` ${colors.gray}(${relPath}:${test.line}:${test.column})${colors.white}`;
128+
}
129+
ArrayPrototypePush(results, msg);
130+
}
131+
132+
return ArrayPrototypeJoin(results, '\n') + '\n';
133+
}
111134
_transform({ type, data }, encoding, callback) {
112135
callback(null, this.#handleEvent({ __proto__: null, type, data }));
113136
}
Collapse file

‎lib/internal/test_runner/reporter/tap.js‎

Copy file name to clipboardExpand all lines: lib/internal/test_runner/reporter/tap.js
+10Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ async function * tapReporter(source) {
6161
case 'test:coverage':
6262
yield getCoverageReport(indent(data.nesting), data.summary, '# ', '', true);
6363
break;
64+
case 'test:interrupted':
65+
for (let i = 0; i < data.tests.length; i++) {
66+
const test = data.tests[i];
67+
let msg = `Interrupted while running: ${test.name}`;
68+
if (test.file) {
69+
msg += ` at ${test.file}:${test.line}:${test.column}`;
70+
}
71+
yield `# ${tapEscape(msg)}\n`;
72+
}
73+
break;
6474
}
6575
}
6676
}
Collapse file

‎lib/internal/test_runner/tests_stream.js‎

Copy file name to clipboardExpand all lines: lib/internal/test_runner/tests_stream.js
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@ class TestsStream extends Readable {
149149
});
150150
}
151151

152+
interrupted(tests) {
153+
this[kEmitMessage]('test:interrupted', {
154+
__proto__: null,
155+
tests,
156+
});
157+
}
158+
152159
end() {
153160
this.#tryPush(null);
154161
}
Collapse file

‎test/parallel/test-runner-exit-code.js‎

Copy file name to clipboardExpand all lines: test/parallel/test-runner-exit-code.js
+10-3Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const { spawnSync, spawn } = require('child_process');
66
const { once } = require('events');
77
const { finished } = require('stream/promises');
88

9-
async function runAndKill(file) {
9+
async function runAndKill(file, expectedTestName) {
1010
if (common.isWindows) {
1111
common.printSkipMessage(`signals are not supported in windows, skipping ${file}`);
1212
return;
@@ -21,6 +21,9 @@ async function runAndKill(file) {
2121
const [code, signal] = await once(child, 'exit');
2222
await finished(child.stdout);
2323
assert(stdout.startsWith('TAP version 13\n'));
24+
// Verify interrupted test message
25+
assert(stdout.includes(`Interrupted while running: ${expectedTestName}`),
26+
`Expected output to contain interrupted test name`);
2427
assert.strictEqual(signal, null);
2528
assert.strictEqual(code, 1);
2629
}
@@ -67,6 +70,10 @@ if (process.argv[2] === 'child') {
6770
assert.strictEqual(child.status, 1);
6871
assert.strictEqual(child.signal, null);
6972

70-
runAndKill(fixtures.path('test-runner', 'never_ending_sync.js')).then(common.mustCall());
71-
runAndKill(fixtures.path('test-runner', 'never_ending_async.js')).then(common.mustCall());
73+
// With process isolation (default), the test name shown is the file path
74+
// because the parent runner only knows about file-level tests
75+
const neverEndingSync = fixtures.path('test-runner', 'never_ending_sync.js');
76+
const neverEndingAsync = fixtures.path('test-runner', 'never_ending_async.js');
77+
runAndKill(neverEndingSync, neverEndingSync).then(common.mustCall());
78+
runAndKill(neverEndingAsync, neverEndingAsync).then(common.mustCall());
7279
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.