Skip to content

Commit 86679ee

Browse files
MoLowaduh95
authored andcommitted
feat: recieve and pass AbortSignal
PR-URL: nodejs/node#43554 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> (cherry picked from commit 389b7e138e89a339fabe4ad628bf09cd9748f957)
1 parent 751ffc6 commit 86679ee

17 files changed

+669
-91
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@ jobs:
99
strategy:
1010
matrix:
1111
node: ['14', '16', '18']
12+
include:
13+
- node: '14'
14+
env: --experimental-abortcontroller --no-warnings
1215
steps:
1316
- uses: actions/checkout@v3
1417
- uses: actions/setup-node@v3
1518
with:
1619
node-version: ${{ matrix.node }}
1720
- run: npm ci
1821
- run: npm test
22+
env:
23+
NODE_OPTIONS: ${{ matrix.env }}

README.md

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Minimal dependencies, with full test suite.
1111
Differences from the core implementation:
1212

1313
- Doesn't hide its own stack frames.
14+
- Requires `--experimental-abortcontroller` CLI flag to work on Node.js v14.x.
1415

1516
## Docs
1617

@@ -333,6 +334,7 @@ internally.
333334
- `only` {boolean} If truthy, and the test context is configured to run
334335
`only` tests, then this test will be run. Otherwise, the test is skipped.
335336
**Default:** `false`.
337+
* `signal` {AbortSignal} Allows aborting an in-progress test
336338
- `skip` {boolean|string} If truthy, the test is skipped. If a string is
337339
provided, that string is displayed in the test results as the reason for
338340
skipping the test. **Default:** `false`.
@@ -386,8 +388,9 @@ thus prevent the scheduled cancellation.
386388
does not have a name.
387389
* `options` {Object} Configuration options for the suite.
388390
supports the same options as `test([name][, options][, fn])`
389-
* `fn` {Function} The function under suite.
390-
a synchronous function declaring all subtests and subsuites.
391+
* `fn` {Function|AsyncFunction} The function under suite
392+
declaring all subtests and subsuites.
393+
The first argument to this function is a [`SuiteContext`][] object.
391394
**Default:** A no-op function.
392395
* Returns: `undefined`.
393396

@@ -455,6 +458,16 @@ have the `only` option set. Otherwise, all tests are run. If Node.js was not
455458
started with the [`--test-only`][] command-line option, this function is a
456459
no-op.
457460

461+
### `context.signal`
462+
463+
* [`AbortSignal`][] Can be used to abort test subtasks when the test has been aborted.
464+
465+
```js
466+
test('top level test', async (t) => {
467+
await fetch('some/uri', { signal: t.signal });
468+
});
469+
```
470+
458471
### `context.skip([message])`
459472

460473
- `message` {string} Optional skip message to be displayed in TAP output.
@@ -503,8 +516,20 @@ execution of the test function. This function does not return a value.
503516
This function is used to create subtests under the current test. This function
504517
behaves in the same fashion as the top level [`test()`][] function.
505518

506-
[tap]: https://testanything.org/
507-
[`testcontext`]: #class-testcontext
519+
## Class: `SuiteContext`
520+
521+
An instance of `SuiteContext` is passed to each suite function in order to
522+
interact with the test runner. However, the `SuiteContext` constructor is not
523+
exposed as part of the API.
524+
525+
### `context.signal`
526+
527+
* [`AbortSignal`][] Can be used to abort test subtasks when the test has been aborted.
528+
529+
[`AbortSignal`]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
530+
[TAP]: https://testanything.org/
531+
[`SuiteContext`]: #class-suitecontext
532+
[`TestContext`]: #class-testcontext
508533
[`test()`]: #testname-options-fn
509534
[describe options]: #describename-options-fn
510535
[it options]: #testname-options-fn

lib/internal/abort_controller.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict'
2+
3+
module.exports = {
4+
AbortController,
5+
AbortSignal
6+
}

lib/internal/errors.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,16 @@ function hideInternalStackFrames (error) {
329329
})
330330
}
331331

332+
class AbortError extends Error {
333+
constructor (message = 'The operation was aborted', options = undefined) {
334+
super(message, options)
335+
this.code = 'ABORT_ERR'
336+
this.name = 'AbortError'
337+
}
338+
}
339+
332340
module.exports = {
341+
AbortError,
333342
codes,
334343
inspectWithNoCustomRetry,
335344
kIsNodeError

lib/internal/main/test_runner.js

Lines changed: 37 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,35 @@
1-
// https://github.com/nodejs/node/blob/e2225ba8e1c00995c0f8bd56e607ea7c5b463ab9/lib/internal/main/test_runner.js
1+
// https://github.com/nodejs/node/blob/389b7e138e89a339fabe4ad628bf09cd9748f957/lib/internal/main/test_runner.js
22
'use strict'
33
const {
44
ArrayFrom,
55
ArrayPrototypeFilter,
66
ArrayPrototypeIncludes,
7+
ArrayPrototypeJoin,
78
ArrayPrototypePush,
89
ArrayPrototypeSlice,
910
ArrayPrototypeSort,
10-
Promise,
11-
PromiseAll,
12-
SafeArrayIterator,
11+
SafePromiseAll,
1312
SafeSet
1413
} = require('#internal/per_context/primordials')
1514
const {
1615
prepareMainThreadExecution
1716
} = require('#internal/bootstrap/pre_execution')
1817
const { spawn } = require('child_process')
1918
const { readdirSync, statSync } = require('fs')
20-
const { finished } = require('#internal/streams/end-of-stream')
21-
const console = require('#internal/console/global')
2219
const {
2320
codes: {
2421
ERR_TEST_FAILURE
2522
}
2623
} = require('#internal/errors')
24+
const { toArray } = require('#internal/streams/operators').promiseReturningOperators
2725
const { test } = require('#internal/test_runner/harness')
2826
const { kSubtestsFailed } = require('#internal/test_runner/test')
2927
const {
3028
isSupportedFileType,
3129
doesPathMatchFilter
3230
} = require('#internal/test_runner/utils')
3331
const { basename, join, resolve } = require('path')
32+
const { once } = require('events')
3433
const kFilterArgs = ['--test']
3534

3635
prepareMainThreadExecution(false)
@@ -104,53 +103,39 @@ function filterExecArgv (arg) {
104103
}
105104

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

lib/internal/per_context/primordials.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ exports.ObjectPrototypeHasOwnProperty = (obj, property) => Object.prototype.hasO
2929
exports.ReflectApply = (target, self, args) => Reflect.apply(target, self, args)
3030
exports.Promise = Promise
3131
exports.PromiseAll = iterator => Promise.all(iterator)
32+
exports.PromisePrototypeThen = (promise, thenFn, catchFn) => promise.then(thenFn, catchFn)
3233
exports.PromiseResolve = val => Promise.resolve(val)
3334
exports.PromiseRace = val => Promise.race(val)
3435
exports.SafeArrayIterator = class ArrayIterator {constructor (array) { this.array = array }[Symbol.iterator] () { return this.array.values() }}
3536
exports.SafeMap = Map
36-
exports.SafePromiseAll = (array, mapFn) => Promise.all(array.map(mapFn))
37+
exports.SafePromiseAll = (array, mapFn) => Promise.all(mapFn ? array.map(mapFn) : array)
38+
exports.SafePromiseRace = (array, mapFn) => Promise.race(mapFn ? array.map(mapFn) : array)
3739
exports.SafeSet = Set
3840
exports.SafeWeakMap = WeakMap
3941
exports.StringPrototypeMatch = (str, reg) => str.match(reg)

lib/internal/streams/operators.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const {
2+
ArrayPrototypePush
3+
} = require('#internal/per_context/primordials')
4+
const { validateAbortSignal } = require('#internal/validators')
5+
const { AbortError } = require('#internal/errors')
6+
7+
async function toArray (options) {
8+
if (options?.signal != null) {
9+
validateAbortSignal(options.signal, 'options.signal')
10+
}
11+
12+
const result = []
13+
for await (const val of this) {
14+
if (options?.signal?.aborted) {
15+
throw new AbortError(undefined, { cause: options.signal.reason })
16+
}
17+
ArrayPrototypePush(result, val)
18+
}
19+
return result
20+
}
21+
22+
module.exports.promiseReturningOperators = {
23+
toArray
24+
}

0 commit comments

Comments
 (0)