Skip to content

Commit b30afcd

Browse files
MoLowtargos
authored andcommitted
test_runner: recieve and pass AbortSignal
PR-URL: #43554 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent fde5e05 commit b30afcd

File tree

9 files changed

+586
-81
lines changed

9 files changed

+586
-81
lines changed

doc/api/test.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ changes:
337337
* `only` {boolean} If truthy, and the test context is configured to run
338338
`only` tests, then this test will be run. Otherwise, the test is skipped.
339339
**Default:** `false`.
340+
* `signal` {AbortSignal} Allows aborting an in-progress test
340341
* `skip` {boolean|string} If truthy, the test is skipped. If a string is
341342
provided, that string is displayed in the test results as the reason for
342343
skipping the test. **Default:** `false`.
@@ -390,8 +391,9 @@ thus prevent the scheduled cancellation.
390391
does not have a name.
391392
* `options` {Object} Configuration options for the suite.
392393
supports the same options as `test([name][, options][, fn])`
393-
* `fn` {Function} The function under suite.
394-
a synchronous function declaring all subtests and subsuites.
394+
* `fn` {Function|AsyncFunction} The function under suite
395+
declaring all subtests and subsuites.
396+
The first argument to this function is a [`SuiteContext`][] object.
395397
**Default:** A no-op function.
396398
* Returns: `undefined`.
397399

@@ -488,6 +490,20 @@ test('top level test', (t) => {
488490
});
489491
```
490492

493+
### `context.signal`
494+
495+
<!-- YAML
496+
added: REPLACEME
497+
-->
498+
499+
* <AbortSignal> Can be used to abort test subtasks when the test has been aborted.
500+
501+
```js
502+
test('top level test', async (t) => {
503+
await fetch('some/uri', { signal: t.signal });
504+
});
505+
```
506+
491507
### `context.skip([message])`
492508

493509
<!-- YAML
@@ -578,9 +594,28 @@ test('top level test', async (t) => {
578594
});
579595
```
580596

597+
## Class: `SuiteContext`
598+
599+
<!-- YAML
600+
added: REPLACEME
601+
-->
602+
603+
An instance of `SuiteContext` is passed to each suite function in order to
604+
interact with the test runner. However, the `SuiteContext` constructor is not
605+
exposed as part of the API.
606+
607+
### `context.signal`
608+
609+
<!-- YAML
610+
added: REPLACEME
611+
-->
612+
613+
* <AbortSignal> Can be used to abort test subtasks when the test has been aborted.
614+
581615
[TAP]: https://testanything.org/
582616
[`--test-only`]: cli.md#--test-only
583617
[`--test`]: cli.md#--test
618+
[`SuiteContext`]: #class-suitecontext
584619
[`TestContext`]: #class-testcontext
585620
[`test()`]: #testname-options-fn
586621
[describe options]: #describename-options-fn

lib/internal/main/test_runner.js

Lines changed: 35 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@ const {
33
ArrayFrom,
44
ArrayPrototypeFilter,
55
ArrayPrototypeIncludes,
6+
ArrayPrototypeJoin,
67
ArrayPrototypePush,
78
ArrayPrototypeSlice,
89
ArrayPrototypeSort,
9-
Promise,
10-
PromiseAll,
11-
SafeArrayIterator,
10+
SafePromiseAll,
1211
SafeSet,
1312
} = primordials;
1413
const {
1514
prepareMainThreadExecution,
1615
} = require('internal/bootstrap/pre_execution');
1716
const { spawn } = require('child_process');
1817
const { readdirSync, statSync } = require('fs');
19-
const { finished } = require('internal/streams/end-of-stream');
2018
const console = require('internal/console/global');
2119
const {
2220
codes: {
@@ -30,6 +28,7 @@ const {
3028
doesPathMatchFilter,
3129
} = require('internal/test_runner/utils');
3230
const { basename, join, resolve } = require('path');
31+
const { once } = require('events');
3332
const kFilterArgs = ['--test'];
3433

3534
prepareMainThreadExecution(false);
@@ -102,53 +101,39 @@ function filterExecArgv(arg) {
102101
}
103102

104103
function runTestFile(path) {
105-
return test(path, () => {
106-
return new Promise((resolve, reject) => {
107-
const args = ArrayPrototypeFilter(process.execArgv, filterExecArgv);
108-
ArrayPrototypePush(args, path);
109-
110-
const child = spawn(process.execPath, args);
111-
// TODO(cjihrig): Implement a TAP parser to read the child's stdout
112-
// instead of just displaying it all if the child fails.
113-
let stdout = '';
114-
let stderr = '';
115-
let err;
116-
117-
child.on('error', (error) => {
118-
err = error;
119-
});
120-
121-
child.stdout.setEncoding('utf8');
122-
child.stderr.setEncoding('utf8');
123-
124-
child.stdout.on('data', (chunk) => {
125-
stdout += chunk;
126-
});
127-
128-
child.stderr.on('data', (chunk) => {
129-
stderr += chunk;
130-
});
131-
132-
child.once('exit', async (code, signal) => {
133-
if (code !== 0 || signal !== null) {
134-
if (!err) {
135-
await PromiseAll(new SafeArrayIterator([finished(child.stderr), finished(child.stdout)]));
136-
err = new ERR_TEST_FAILURE('test failed', kSubtestsFailed);
137-
err.exitCode = code;
138-
err.signal = signal;
139-
err.stdout = stdout;
140-
err.stderr = stderr;
141-
// The stack will not be useful since the failures came from tests
142-
// in a child process.
143-
err.stack = undefined;
144-
}
145-
146-
return reject(err);
147-
}
148-
149-
resolve();
150-
});
104+
return test(path, async (t) => {
105+
const args = ArrayPrototypeFilter(process.execArgv, filterExecArgv);
106+
ArrayPrototypePush(args, path);
107+
108+
const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8' });
109+
// TODO(cjihrig): Implement a TAP parser to read the child's stdout
110+
// instead of just displaying it all if the child fails.
111+
let err;
112+
113+
child.on('error', (error) => {
114+
err = error;
151115
});
116+
117+
const { 0: { code, signal }, 1: stdout, 2: stderr } = await SafePromiseAll([
118+
once(child, 'exit', { signal: t.signal }),
119+
child.stdout.toArray({ signal: t.signal }),
120+
child.stderr.toArray({ signal: t.signal }),
121+
]);
122+
123+
if (code !== 0 || signal !== null) {
124+
if (!err) {
125+
err = new ERR_TEST_FAILURE('test failed', kSubtestsFailed);
126+
err.exitCode = code;
127+
err.signal = signal;
128+
err.stdout = ArrayPrototypeJoin(stdout, '');
129+
err.stderr = ArrayPrototypeJoin(stderr, '');
130+
// The stack will not be useful since the failures came from tests
131+
// in a child process.
132+
err.stack = undefined;
133+
}
134+
135+
throw err;
136+
}
152137
});
153138
}
154139

0 commit comments

Comments
 (0)