Skip to content

Commit

Permalink
test_runner: recieve and pass AbortSignal
Browse files Browse the repository at this point in the history
  • Loading branch information
MoLow committed Jul 17, 2022
1 parent dabda03 commit 80f1a0a
Show file tree
Hide file tree
Showing 9 changed files with 586 additions and 81 deletions.
39 changes: 37 additions & 2 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ changes:
* `only` {boolean} If truthy, and the test context is configured to run
`only` tests, then this test will be run. Otherwise, the test is skipped.
**Default:** `false`.
* `signal` {AbortSignal} allows aborting an in-progress test
* `skip` {boolean|string} If truthy, the test is skipped. If a string is
provided, that string is displayed in the test results as the reason for
skipping the test. **Default:** `false`.
Expand Down Expand Up @@ -385,8 +386,9 @@ test('top level test', async (t) => {
does not have a name.
* `options` {Object} Configuration options for the suite.
supports the same options as `test([name][, options][, fn])`
* `fn` {Function} The function under suite.
a synchronous function declaring all subtests and subsuites.
* `fn` {Function|AsyncFunction} The function under suite
declaring all subtests and subsuites.
The first argument to this function is a [`SuiteContext`][] object.
**Default:** A no-op function.
* Returns: `undefined`.

Expand Down Expand Up @@ -483,6 +485,20 @@ test('top level test', (t) => {
});
```

### `context.signal`

<!-- YAML
added: REPLACEME
-->

this is an <AbortSignal> used to signal when the test has been aborted.

```js
test('top level test', async (t) => {
await fetch('some/uri', { signal: t.signal });
});
```

### `context.skip([message])`

<!-- YAML
Expand Down Expand Up @@ -573,9 +589,28 @@ test('top level test', async (t) => {
});
```

## Class: `SuiteContext`

<!-- YAML
added: REPLACEME
-->

An instance of `SuiteContext` is passed to each suite function in order to
interact with the test runner. However, the `SuiteContext` constructor is not
exposed as part of the API.

### `context.signal`

<!-- YAML
added: REPLACEME
-->

this is an <AbortSignal> used to signal when the test has been aborted.

[TAP]: https://testanything.org/
[`--test-only`]: cli.md#--test-only
[`--test`]: cli.md#--test
[`SuiteContext`]: #class-suitecontext
[`TestContext`]: #class-testcontext
[`test()`]: #testname-options-fn
[describe options]: #describename-options-fn
Expand Down
86 changes: 36 additions & 50 deletions lib/internal/main/test_runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@ const {
ArrayPrototypePush,
ArrayPrototypeSlice,
ArrayPrototypeSort,
Promise,
PromiseAll,
SafeArrayIterator,
SafePromiseAll,
SafeSet,
} = primordials;
const {
prepareMainThreadExecution,
} = require('internal/bootstrap/pre_execution');
const { spawn } = require('child_process');
const { readdirSync, statSync } = require('fs');
const { finished } = require('internal/streams/end-of-stream');
const console = require('internal/console/global');
const {
codes: {
Expand All @@ -30,6 +27,7 @@ const {
doesPathMatchFilter,
} = require('internal/test_runner/utils');
const { basename, join, resolve } = require('path');
const { once } = require('events');
const kFilterArgs = ['--test'];

prepareMainThreadExecution(false);
Expand Down Expand Up @@ -102,53 +100,41 @@ function filterExecArgv(arg) {
}

function runTestFile(path) {
return test(path, () => {
return new Promise((resolve, reject) => {
const args = ArrayPrototypeFilter(process.execArgv, filterExecArgv);
ArrayPrototypePush(args, path);

const child = spawn(process.execPath, args);
// TODO(cjihrig): Implement a TAP parser to read the child's stdout
// instead of just displaying it all if the child fails.
let stdout = '';
let stderr = '';
let err;

child.on('error', (error) => {
err = error;
});

child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');

child.stdout.on('data', (chunk) => {
stdout += chunk;
});

child.stderr.on('data', (chunk) => {
stderr += chunk;
});

child.once('exit', async (code, signal) => {
if (code !== 0 || signal !== null) {
if (!err) {
await PromiseAll(new SafeArrayIterator([finished(child.stderr), finished(child.stdout)]));
err = new ERR_TEST_FAILURE('test failed', kSubtestsFailed);
err.exitCode = code;
err.signal = signal;
err.stdout = stdout;
err.stderr = stderr;
// The stack will not be useful since the failures came from tests
// in a child process.
err.stack = undefined;
}

return reject(err);
}

resolve();
});
return test(path, async (t) => {
const args = ArrayPrototypeFilter(process.execArgv, filterExecArgv);
ArrayPrototypePush(args, path);

const child = spawn(process.execPath, args, { signal: t.signal });
// TODO(cjihrig): Implement a TAP parser to read the child's stdout
// instead of just displaying it all if the child fails.
let err;

child.on('error', (error) => {
err = error;
});

child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
const { 0: { code, signal }, 1: stdout, 2: stderr } = await SafePromiseAll([
once(child, 'exit', { signal: t.signal }),
child.stdout.toArray({ signal: t.signal }),
child.stderr.toArray({ signal: t.signal }),
]);

if (code !== 0 || signal !== null) {
if (!err) {
err = new ERR_TEST_FAILURE('test failed', kSubtestsFailed);
err.exitCode = code;
err.signal = signal;
err.stdout = stdout.join('');
err.stderr = stderr.join('');
// The stack will not be useful since the failures came from tests
// in a child process.
err.stack = undefined;
}

throw err;
}
});
}

Expand Down
Loading

0 comments on commit 80f1a0a

Please sign in to comment.