Skip to content

Commit aa8053e

Browse files
MoLowdanielleadams
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 80c2fa8 commit aa8053e

9 files changed

+586
-81
lines changed

doc/api/test.md

+37-2
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`.
@@ -385,8 +386,9 @@ test('top level test', async (t) => {
385386
does not have a name.
386387
* `options` {Object} Configuration options for the suite.
387388
supports the same options as `test([name][, options][, fn])`
388-
* `fn` {Function} The function under suite.
389-
a synchronous function declaring all subtests and subsuites.
389+
* `fn` {Function|AsyncFunction} The function under suite
390+
declaring all subtests and subsuites.
391+
The first argument to this function is a [`SuiteContext`][] object.
390392
**Default:** A no-op function.
391393
* Returns: `undefined`.
392394

@@ -483,6 +485,20 @@ test('top level test', (t) => {
483485
});
484486
```
485487

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

488504
<!-- YAML
@@ -573,9 +589,28 @@ test('top level test', async (t) => {
573589
});
574590
```
575591

592+
## Class: `SuiteContext`
593+
594+
<!-- YAML
595+
added: REPLACEME
596+
-->
597+
598+
An instance of `SuiteContext` is passed to each suite function in order to
599+
interact with the test runner. However, the `SuiteContext` constructor is not
600+
exposed as part of the API.
601+
602+
### `context.signal`
603+
604+
<!-- YAML
605+
added: REPLACEME
606+
-->
607+
608+
* <AbortSignal> Can be used to abort test subtasks when the test has been aborted.
609+
576610
[TAP]: https://testanything.org/
577611
[`--test-only`]: cli.md#--test-only
578612
[`--test`]: cli.md#--test
613+
[`SuiteContext`]: #class-suitecontext
579614
[`TestContext`]: #class-testcontext
580615
[`test()`]: #testname-options-fn
581616
[describe options]: #describename-options-fn

lib/internal/main/test_runner.js

+35-50
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)