diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a74c22..5a2c556 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,9 @@ jobs: strategy: matrix: node: ['14', '16', '18'] + include: + - node: '14' + env: --experimental-abortcontroller --no-warnings steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -16,3 +19,5 @@ jobs: node-version: ${{ matrix.node }} - run: npm ci - run: npm test + env: + NODE_OPTIONS: ${{ matrix.env }} diff --git a/README.md b/README.md index a85ecd9..c67c1bd 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Minimal dependencies, with full test suite. Differences from the core implementation: - Doesn't hide its own stack frames. +- Requires `--experimental-abortcontroller` CLI flag to work on Node.js v14.x. ## Docs @@ -333,6 +334,7 @@ internally. - `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`. @@ -386,8 +388,9 @@ thus prevent the scheduled cancellation. 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`. @@ -455,6 +458,16 @@ have the `only` option set. Otherwise, all tests are run. If Node.js was not started with the [`--test-only`][] command-line option, this function is a no-op. +### `context.signal` + +* [`AbortSignal`][] Can be used to abort test subtasks when the test has been aborted. + +```js +test('top level test', async (t) => { + await fetch('some/uri', { signal: t.signal }); +}); +``` + ### `context.skip([message])` - `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. This function is used to create subtests under the current test. This function behaves in the same fashion as the top level [`test()`][] function. -[tap]: https://testanything.org/ -[`testcontext`]: #class-testcontext +## Class: `SuiteContext` + +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` + +* [`AbortSignal`][] Can be used to abort test subtasks when the test has been aborted. + +[`AbortSignal`]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal +[TAP]: https://testanything.org/ +[`SuiteContext`]: #class-suitecontext +[`TestContext`]: #class-testcontext [`test()`]: #testname-options-fn [describe options]: #describename-options-fn [it options]: #testname-options-fn diff --git a/lib/internal/abort_controller.js b/lib/internal/abort_controller.js new file mode 100644 index 0000000..007aa40 --- /dev/null +++ b/lib/internal/abort_controller.js @@ -0,0 +1,6 @@ +'use strict' + +module.exports = { + AbortController, + AbortSignal +} diff --git a/lib/internal/errors.js b/lib/internal/errors.js index b223d01..ac241f7 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -329,7 +329,16 @@ function hideInternalStackFrames (error) { }) } +class AbortError extends Error { + constructor (message = 'The operation was aborted', options = undefined) { + super(message, options) + this.code = 'ABORT_ERR' + this.name = 'AbortError' + } +} + module.exports = { + AbortError, codes, inspectWithNoCustomRetry, kIsNodeError diff --git a/lib/internal/main/test_runner.js b/lib/internal/main/test_runner.js index 3955d78..778ade4 100644 --- a/lib/internal/main/test_runner.js +++ b/lib/internal/main/test_runner.js @@ -1,15 +1,14 @@ -// https://github.com/nodejs/node/blob/e2225ba8e1c00995c0f8bd56e607ea7c5b463ab9/lib/internal/main/test_runner.js +// https://github.com/nodejs/node/blob/389b7e138e89a339fabe4ad628bf09cd9748f957/lib/internal/main/test_runner.js 'use strict' const { ArrayFrom, ArrayPrototypeFilter, ArrayPrototypeIncludes, + ArrayPrototypeJoin, ArrayPrototypePush, ArrayPrototypeSlice, ArrayPrototypeSort, - Promise, - PromiseAll, - SafeArrayIterator, + SafePromiseAll, SafeSet } = require('#internal/per_context/primordials') const { @@ -17,13 +16,12 @@ const { } = 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: { ERR_TEST_FAILURE } } = require('#internal/errors') +const { toArray } = require('#internal/streams/operators').promiseReturningOperators const { test } = require('#internal/test_runner/harness') const { kSubtestsFailed } = require('#internal/test_runner/test') const { @@ -31,6 +29,7 @@ const { doesPathMatchFilter } = require('#internal/test_runner/utils') const { basename, join, resolve } = require('path') +const { once } = require('events') const kFilterArgs = ['--test'] prepareMainThreadExecution(false) @@ -104,53 +103,39 @@ 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, encoding: 'utf8' }) + // 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 }) + + const { 0: { code, signal }, 1: stdout, 2: stderr } = await SafePromiseAll([ + once(child, 'exit', { signal: t.signal }), + toArray.call(child.stdout, { signal: t.signal }), + toArray.call(child.stderr, { 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 = ArrayPrototypeJoin(stdout, '') + err.stderr = ArrayPrototypeJoin(stderr, '') + // The stack will not be useful since the failures came from tests + // in a child process. + err.stack = undefined + } + + throw err + } }) } diff --git a/lib/internal/per_context/primordials.js b/lib/internal/per_context/primordials.js index 98a3f3b..60ed3fc 100644 --- a/lib/internal/per_context/primordials.js +++ b/lib/internal/per_context/primordials.js @@ -29,11 +29,13 @@ exports.ObjectPrototypeHasOwnProperty = (obj, property) => Object.prototype.hasO exports.ReflectApply = (target, self, args) => Reflect.apply(target, self, args) exports.Promise = Promise exports.PromiseAll = iterator => Promise.all(iterator) +exports.PromisePrototypeThen = (promise, thenFn, catchFn) => promise.then(thenFn, catchFn) exports.PromiseResolve = val => Promise.resolve(val) exports.PromiseRace = val => Promise.race(val) exports.SafeArrayIterator = class ArrayIterator {constructor (array) { this.array = array }[Symbol.iterator] () { return this.array.values() }} exports.SafeMap = Map -exports.SafePromiseAll = (array, mapFn) => Promise.all(array.map(mapFn)) +exports.SafePromiseAll = (array, mapFn) => Promise.all(mapFn ? array.map(mapFn) : array) +exports.SafePromiseRace = (array, mapFn) => Promise.race(mapFn ? array.map(mapFn) : array) exports.SafeSet = Set exports.SafeWeakMap = WeakMap exports.StringPrototypeMatch = (str, reg) => str.match(reg) diff --git a/lib/internal/streams/operators.js b/lib/internal/streams/operators.js new file mode 100644 index 0000000..a29662b --- /dev/null +++ b/lib/internal/streams/operators.js @@ -0,0 +1,24 @@ +const { + ArrayPrototypePush +} = require('#internal/per_context/primordials') +const { validateAbortSignal } = require('#internal/validators') +const { AbortError } = require('#internal/errors') + +async function toArray (options) { + if (options?.signal != null) { + validateAbortSignal(options.signal, 'options.signal') + } + + const result = [] + for await (const val of this) { + if (options?.signal?.aborted) { + throw new AbortError(undefined, { cause: options.signal.reason }) + } + ArrayPrototypePush(result, val) + } + return result +} + +module.exports.promiseReturningOperators = { + toArray +} diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index f3169a7..6a2c00f 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -1,4 +1,4 @@ -// https://github.com/nodejs/node/blob/e2225ba8e1c00995c0f8bd56e607ea7c5b463ab9/lib/internal/test_runner/test.js +// https://github.com/nodejs/node/blob/389b7e138e89a339fabe4ad628bf09cd9748f957/lib/internal/test_runner/test.js 'use strict' @@ -8,17 +8,23 @@ const { ArrayPrototypeUnshift, FunctionPrototype, Number, + PromisePrototypeThen, + PromiseResolve, ReflectApply, SafeMap, - PromiseRace, - SafePromiseAll + SafePromiseAll, + SafePromiseRace, + Symbol } = require('#internal/per_context/primordials') const { AsyncResource } = require('async_hooks') +const { once } = require('events') +const { AbortController } = require('#internal/abort_controller') const { codes: { ERR_TEST_FAILURE }, - kIsNodeError + kIsNodeError, + AbortError } = require('#internal/errors') const { getOptionValue } = require('#internal/options') const { TapStream } = require('#internal/test_runner/tap_stream') @@ -28,7 +34,7 @@ const { kEmptyObject } = require('#internal/util') const { isPromise } = require('#internal/util/types') -const { isUint32 } = require('#internal/validators') +const { isUint32, validateAbortSignal } = require('#internal/validators') const { setTimeout } = require('#timers/promises') const { cpus } = require('os') const { bigint: hrtime } = process.hrtime @@ -46,19 +52,18 @@ const testOnlyFlag = !isTestRunner && getOptionValue('--test-only') // TODO(cjihrig): Use uv_available_parallelism() once it lands. const rootConcurrency = isTestRunner ? cpus().length : 1 -function testTimeout (promise, timeout) { +const kShouldAbort = Symbol('kShouldAbort') + +function stopTest (timeout, signal) { if (timeout === kDefaultTimeout) { - return promise - } - return PromiseRace([ - promise, - setTimeout(timeout, null, { ref: false }).then(() => { - throw new ERR_TEST_FAILURE( - `test timed out after ${timeout}ms`, - kTestTimeoutFailure - ) - }) - ]) + return once(signal, 'abort') + } + return PromisePrototypeThen(setTimeout(timeout, null, { ref: false, signal }), () => { + throw new ERR_TEST_FAILURE( + `test timed out after ${timeout}ms`, + kTestTimeoutFailure + ) + }) } class TestContext { @@ -68,6 +73,10 @@ class TestContext { this.#test = test } + get signal () { + return this.#test.signal + } + diagnostic (message) { this.#test.diagnostic(message) } @@ -93,11 +102,14 @@ class TestContext { } class Test extends AsyncResource { + #abortController + #outerSignal + constructor (options) { super('Test') let { fn, name, parent, skip } = options - const { concurrency, only, timeout, todo } = options + const { concurrency, only, timeout, todo, signal } = options if (typeof fn !== 'function') { fn = noop @@ -151,6 +163,13 @@ class Test extends AsyncResource { fn = noop } + this.#abortController = new AbortController() + this.#outerSignal = signal + this.signal = this.#abortController.signal + + validateAbortSignal(signal, 'options.signal') + this.#outerSignal?.addEventListener('abort', this.#abortHandler) + this.fn = fn this.name = name this.parent = parent @@ -245,7 +264,8 @@ class Test extends AsyncResource { // If this test has already ended, attach this test to the root test so // that the error can be properly reported. - if (this.finished) { + const preventAddingSubtests = this.finished || this.buildPhaseFinished + if (preventAddingSubtests) { while (parent.parent !== null) { parent = parent.parent } @@ -257,7 +277,7 @@ class Test extends AsyncResource { parent.waitingOn = test.testNumber } - if (this.finished) { + if (preventAddingSubtests) { test.startTime = test.startTime || hrtime() test.fail( new ERR_TEST_FAILURE( @@ -271,18 +291,23 @@ class Test extends AsyncResource { return test } - cancel () { + #abortHandler = () => { + this.cancel(this.#outerSignal?.reason || new AbortError('The test was aborted')) + } + + cancel (error) { if (this.endTime !== null) { return } - this.fail( + this.fail(error || new ERR_TEST_FAILURE( 'test did not finish before its parent and was cancelled', kCancelledByParent ) ) this.cancelled = true + this.#abortController.abort() } fail (err) { @@ -333,6 +358,16 @@ class Test extends AsyncResource { return this.run() } + [kShouldAbort] () { + if (this.signal.aborted) { + return true + } + if (this.#outerSignal?.aborted) { + this.cancel(this.#outerSignal.reason || new AbortError('The test was aborted')) + return true + } + } + getRunArgs () { const ctx = new TestContext(this) return { ctx, args: [ctx] } @@ -342,7 +377,13 @@ class Test extends AsyncResource { this.parent.activeSubtests++ this.startTime = hrtime() + if (this[kShouldAbort]()) { + this.postRun() + return + } + try { + const stopPromise = stopTest(this.timeout, this.signal) const { args, ctx } = this.getRunArgs() ArrayPrototypeUnshift(args, this.fn, ctx) // Note that if it's not OK to mutate args, we need to first clone it. @@ -358,13 +399,19 @@ class Test extends AsyncResource { 'passed a callback but also returned a Promise', kCallbackAndPromisePresent )) - await testTimeout(ret, this.timeout) + await SafePromiseRace([ret, stopPromise]) } else { - await testTimeout(promise, this.timeout) + await SafePromiseRace([PromiseResolve(promise), stopPromise]) } } else { // This test is synchronous or using Promises. - await testTimeout(ReflectApply(this.runInAsyncScope, this, args), this.timeout) + const promise = ReflectApply(this.runInAsyncScope, this, args) + await SafePromiseRace([PromiseResolve(promise), stopPromise]) + } + + if (this[kShouldAbort]()) { + this.postRun() + return } this.pass() @@ -413,6 +460,8 @@ class Test extends AsyncResource { this.fail(new ERR_TEST_FAILURE(msg, kSubtestsFailed)) } + this.#outerSignal?.removeEventListener('abort', this.#abortHandler) + if (this.parent !== null) { this.parent.activeSubtests-- this.parent.addReadySubtest(this) @@ -480,7 +529,7 @@ class Test extends AsyncResource { class ItTest extends Test { constructor (opt) { super(opt) } // eslint-disable-line no-useless-constructor getRunArgs () { - return { ctx: {}, args: [] } + return { ctx: { signal: this.signal }, args: [] } } } class Suite extends Test { @@ -488,12 +537,13 @@ class Suite extends Test { super(options) try { - this.buildSuite = this.runInAsyncScope(this.fn) + const context = { signal: this.signal } + this.buildSuite = this.runInAsyncScope(this.fn, context, [context]) } catch (err) { this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)) } this.fn = () => {} - this.finished = true // Forbid adding subtests to this suite + this.buildPhaseFinished = true } start () { @@ -508,8 +558,18 @@ class Suite extends Test { } this.parent.activeSubtests++ this.startTime = hrtime() + + if (this[kShouldAbort]()) { + this.subtests = [] + this.postRun() + return + } + + const stopPromise = stopTest(this.timeout, this.signal) const subtests = this.skipped || this.error ? [] : this.subtests - await SafePromiseAll(subtests, (subtests) => subtests.start()) + const promise = SafePromiseAll(subtests, (subtests) => subtests.start()) + + await SafePromiseRace([promise, stopPromise]) this.pass() this.postRun() } diff --git a/lib/internal/validators.js b/lib/internal/validators.js index 453b865..bda3516 100644 --- a/lib/internal/validators.js +++ b/lib/internal/validators.js @@ -3,6 +3,16 @@ function isUint32 (value) { return value === (value >>> 0) } +const validateAbortSignal = (signal, name) => { + if (signal !== undefined && + (signal === null || + typeof signal !== 'object' || + !('aborted' in signal))) { + throw new TypeError(`Expected ${name} to be an AbortSignal, got ${signal}`) + } +} + module.exports = { - isUint32 + isUint32, + validateAbortSignal } diff --git a/lib/test.d.ts b/lib/test.d.ts index 35f22fa..8d07336 100644 --- a/lib/test.d.ts +++ b/lib/test.d.ts @@ -16,6 +16,17 @@ interface TestOptions { * Default: false. */ todo?: boolean | string + + /** + * A number of milliseconds the test will fail after. If unspecified, subtests inherit this value from their parent. + * Default: Infinity + */ + timeout?: number; + + /** + * Allows aborting an in-progress test + */ + signal?: AbortSignal; } type TestFn = (t: TestContext) => any | Promise @@ -27,14 +38,14 @@ export function test (name: string, fn: TestFn): void export function test (options: TestOptions, fn: TestFn): void export function test (fn: TestFn): void -type SuiteFn = () => void; +type SuiteFn = (t: SuiteContext) => void; export function describe (name: string, options: TestOptions, fn: SuiteFn): void export function describe (name: string, fn: SuiteFn): void export function describe (options: TestOptions, fn: SuiteFn): void export function describe (fn: SuiteFn): void -type ItFn = () => any | Promise +type ItFn = (t: ItContext) => any | Promise export function it (name: string, options: TestOptions, fn: ItFn): void export function it (name: string, fn: ItFn): void @@ -45,7 +56,7 @@ export function it (fn: ItFn): void * An instance of TestContext is passed to each test function in order to interact with the test runner. * However, the TestContext constructor is not exposed as part of the API. */ -declare class TestContext { + declare class TestContext { /** * This function is used to create subtests under the current test. This function behaves in the same fashion as the top level test() function. */ @@ -78,4 +89,34 @@ declare class TestContext { * @param message Optional TODO message to be displayed in TAP output. */ public todo (message?: string): void + + /** + * Can be used to abort test subtasks when the test has been aborted. + */ + public signal: AbortSignal +} + + +/** + * 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. + */ + declare class SuiteContext { + + /** + * Can be used to abort test subtasks when the test has been aborted. + */ + public signal: AbortSignal +} + +/** + * An instance of ItContext is passed to each suite function in order to interact with the test runner. + * However, the ItContext constructor is not exposed as part of the API. + */ + declare class ItContext { + + /** + * Can be used to abort test subtasks when the test has been aborted. + */ + public signal: AbortSignal } diff --git a/test/common/index.js b/test/common/index.js index 584d345..60532c5 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -102,6 +102,43 @@ function expectsError (validator, exact) { }, exact) } +if (typeof AbortSignal.timeout !== 'function') { + class AbortError extends Error { + constructor (message = 'The operation was aborted', options = undefined) { + super(message, options) + this.code = 23 + } + } + + AbortSignal.timeout = function timeout (delay) { + const ac = new AbortController() + setTimeout(() => ac.abort(new AbortError( + 'The operation was aborted due to timeout')), delay).unref() + return ac.signal + } +} + +if (process.version.startsWith('v14.') || process.version.startsWith('v16.')) { + AbortSignal.abort = () => { + const controller = new AbortController() + const error = new Error('This operation was aborted') + error.code = 20 + controller.abort(error) + return controller.signal + } + const nativeAbort = AbortController.prototype.abort + AbortController.prototype.abort = function abort (reason) { + if (arguments.length === 0) { + reason = new Error('This operation was aborted') + reason.code = 20 + } + if (process.version.startsWith('v14.')) { + this.signal.reason = reason + } + nativeAbort.call(this, reason) + } +} + module.exports = { expectsError } diff --git a/test/message/test_runner_abort.js b/test/message/test_runner_abort.js new file mode 100644 index 0000000..5e1c768 --- /dev/null +++ b/test/message/test_runner_abort.js @@ -0,0 +1,48 @@ +// https://github.com/nodejs/node/blob/389b7e138e89a339fabe4ad628bf09cd9748f957/test/message/test_runner_abort.js +// Flags: --no-warnings +'use strict' +require('../common') +const test = require('#node:test') + +test('promise timeout signal', { signal: AbortSignal.timeout(1) }, async (t) => { + await Promise.all([ + t.test('ok 1', async () => {}), + t.test('ok 2', () => {}), + t.test('ok 3', { signal: t.signal }, async () => {}), + t.test('ok 4', { signal: t.signal }, () => {}), + t.test('not ok 1', () => new Promise(() => {})), + t.test('not ok 2', (t, done) => {}), + t.test('not ok 3', { signal: t.signal }, () => new Promise(() => {})), + t.test('not ok 4', { signal: t.signal }, (t, done) => {}), + t.test('not ok 5', { signal: t.signal }, (t, done) => { + t.signal.addEventListener('abort', done) + }) + ]) +}) + +test('promise abort signal', { signal: AbortSignal.abort() }, async (t) => { + await t.test('should not appear', () => {}) +}) + +test('callback timeout signal', { signal: AbortSignal.timeout(1) }, (t, done) => { + t.test('ok 1', async () => {}) + t.test('ok 2', () => {}) + t.test('ok 3', { signal: t.signal }, async () => {}) + t.test('ok 4', { signal: t.signal }, () => {}) + t.test('not ok 1', () => new Promise(() => {})) + t.test('not ok 2', (t, done) => {}) + t.test('not ok 3', { signal: t.signal }, () => new Promise(() => {})) + t.test('not ok 4', { signal: t.signal }, (t, done) => {}) + t.test('not ok 5', { signal: t.signal }, (t, done) => { + t.signal.addEventListener('abort', done) + }) +}) + +test('callback abort signal', { signal: AbortSignal.abort() }, (t, done) => { + t.test('should not appear', done) +}) + +// AbortSignal.timeout(1) doesn't prevent process from closing +// thus we have to keep the process open to prevent cancelation +// of the entire test tree +setTimeout(() => {}, 1000) diff --git a/test/message/test_runner_abort.out b/test/message/test_runner_abort.out new file mode 100644 index 0000000..26f89a2 --- /dev/null +++ b/test/message/test_runner_abort.out @@ -0,0 +1,249 @@ +TAP version 13 +# Subtest: promise timeout signal + # Subtest: ok 1 + ok 1 - ok 1 + --- + duration_ms: * + ... + # Subtest: ok 2 + ok 2 - ok 2 + --- + duration_ms: * + ... + # Subtest: ok 3 + ok 3 - ok 3 + --- + duration_ms: * + ... + # Subtest: ok 4 + ok 4 - ok 4 + --- + duration_ms: * + ... + # Subtest: not ok 1 + not ok 5 - not ok 1 + --- + duration_ms: * + failureType: 'cancelledByParent' + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + ... + # Subtest: not ok 2 + not ok 6 - not ok 2 + --- + duration_ms: * + failureType: 'cancelledByParent' + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + ... + # Subtest: not ok 3 + not ok 7 - not ok 3 + --- + duration_ms: * + error: 'This operation was aborted' + code: 20 + stack: |- + * + * + * + * + * + * + * + * + * + * + ... + # Subtest: not ok 4 + not ok 8 - not ok 4 + --- + duration_ms: * + error: 'This operation was aborted' + code: 20 + stack: |- + * + * + * + * + * + * + * + * + * + * + ... + # Subtest: not ok 5 + not ok 9 - not ok 5 + --- + duration_ms: * + error: 'This operation was aborted' + code: 20 + stack: |- + * + * + * + * + * + * + * + * + * + * + ... + 1..9 +not ok 1 - promise timeout signal + --- + duration_ms: * + error: 'The operation was aborted due to timeout' + code: 23 + stack: |- + * + * + * + * + ... +# Subtest: promise abort signal +not ok 2 - promise abort signal + --- + duration_ms: * + error: 'This operation was aborted' + code: 20 + stack: |- + * + * + * + * + * + * + * + * + * + ... +# Subtest: callback timeout signal + # Subtest: ok 1 + ok 1 - ok 1 + --- + duration_ms: * + ... + # Subtest: ok 2 + ok 2 - ok 2 + --- + duration_ms: * + ... + # Subtest: ok 3 + ok 3 - ok 3 + --- + duration_ms: * + ... + # Subtest: ok 4 + ok 4 - ok 4 + --- + duration_ms: * + ... + # Subtest: not ok 1 + not ok 5 - not ok 1 + --- + duration_ms: * + failureType: 'cancelledByParent' + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + ... + # Subtest: not ok 2 + not ok 6 - not ok 2 + --- + duration_ms: * + failureType: 'cancelledByParent' + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + ... + # Subtest: not ok 3 + not ok 7 - not ok 3 + --- + duration_ms: * + error: 'This operation was aborted' + code: 20 + stack: |- + * + * + * + * + * + * + * + * + * + * + ... + # Subtest: not ok 4 + not ok 8 - not ok 4 + --- + duration_ms: * + error: 'This operation was aborted' + code: 20 + stack: |- + * + * + * + * + * + * + * + * + * + * + ... + # Subtest: not ok 5 + not ok 9 - not ok 5 + --- + duration_ms: * + error: 'This operation was aborted' + code: 20 + stack: |- + * + * + * + * + * + * + * + * + * + * + ... + 1..9 +not ok 3 - callback timeout signal + --- + duration_ms: * + error: 'The operation was aborted due to timeout' + code: 23 + stack: |- + * + * + * + * + ... +# Subtest: callback abort signal +not ok 4 - callback abort signal + --- + duration_ms: * + error: 'This operation was aborted' + code: 20 + stack: |- + * + * + * + * + * + * + * + * + * + ... +1..4 +# tests 4 +# pass 0 +# fail 0 +# cancelled 4 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/message/test_runner_abort_suite.js b/test/message/test_runner_abort_suite.js new file mode 100644 index 0000000..39c3066 --- /dev/null +++ b/test/message/test_runner_abort_suite.js @@ -0,0 +1,28 @@ +// https://github.com/nodejs/node/blob/389b7e138e89a339fabe4ad628bf09cd9748f957/test/message/test_runner_abort_suite.js +// Flags: --no-warnings +'use strict' +require('../common') +const { describe, it } = require('#node:test') + +describe('describe timeout signal', { signal: AbortSignal.timeout(1) }, (t) => { + it('ok 1', async () => {}) + it('ok 2', () => {}) + it('ok 3', { signal: t.signal }, async () => {}) + it('ok 4', { signal: t.signal }, () => {}) + it('not ok 1', () => new Promise(() => {})) + it('not ok 2', (done) => {}) + it('not ok 3', { signal: t.signal }, () => new Promise(() => {})) + it('not ok 4', { signal: t.signal }, (done) => {}) + it('not ok 5', { signal: t.signal }, function (done) { + this.signal.addEventListener('abort', done) + }) +}) + +describe('describe abort signal', { signal: AbortSignal.abort() }, () => { + it('should not appear', () => {}) +}) + +// AbortSignal.timeout(1) doesn't prevent process from closing +// thus we have to keep the process open to prevent cancelation +// of the entire test tree +setTimeout(() => {}, 1000) diff --git a/test/message/test_runner_abort_suite.out b/test/message/test_runner_abort_suite.out new file mode 100644 index 0000000..3866997 --- /dev/null +++ b/test/message/test_runner_abort_suite.out @@ -0,0 +1,99 @@ +TAP version 13 +# Subtest: describe timeout signal + # Subtest: ok 1 + ok 1 - ok 1 + --- + duration_ms: * + ... + # Subtest: ok 2 + ok 2 - ok 2 + --- + duration_ms: * + ... + # Subtest: ok 3 + ok 3 - ok 3 + --- + duration_ms: * + ... + # Subtest: ok 4 + ok 4 - ok 4 + --- + duration_ms: * + ... + # Subtest: not ok 1 + not ok 5 - not ok 1 + --- + duration_ms: * + failureType: 'cancelledByParent' + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + ... + # Subtest: not ok 2 + not ok 6 - not ok 2 + --- + duration_ms: * + failureType: 'cancelledByParent' + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + ... + # Subtest: not ok 3 + not ok 7 - not ok 3 + --- + duration_ms: * + failureType: 'cancelledByParent' + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + ... + # Subtest: not ok 4 + not ok 8 - not ok 4 + --- + duration_ms: * + failureType: 'cancelledByParent' + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + ... + # Subtest: not ok 5 + not ok 9 - not ok 5 + --- + duration_ms: * + failureType: 'cancelledByParent' + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + ... + 1..9 +not ok 1 - describe timeout signal + --- + duration_ms: * + error: 'The operation was aborted due to timeout' + code: 23 + stack: |- + * + * + * + * + ... +# Subtest: describe abort signal +not ok 2 - describe abort signal + --- + duration_ms: * + error: 'This operation was aborted' + code: 20 + stack: |- + * + * + * + * + * + * + * + * + * + ... +1..2 +# tests 2 +# pass 0 +# fail 0 +# cancelled 2 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/message/test_runner_desctibe_it.js b/test/message/test_runner_desctibe_it.js index 4e55647..51ad650 100644 --- a/test/message/test_runner_desctibe_it.js +++ b/test/message/test_runner_desctibe_it.js @@ -1,4 +1,4 @@ -// https://github.com/nodejs/node/blob/e2225ba8e1c00995c0f8bd56e607ea7c5b463ab9/test/message/test_runner_desctibe_it.js +// https://github.com/nodejs/node/blob/389b7e138e89a339fabe4ad628bf09cd9748f957/test/message/test_runner_desctibe_it.js // Flags: --no-warnings 'use strict' require('../common') @@ -225,15 +225,15 @@ it('callback fail', (done) => { }) it('sync t is this in test', function () { - assert.deepStrictEqual(this, {}) + assert.deepStrictEqual(this, { signal: this.signal }) }) it('async t is this in test', async function () { - assert.deepStrictEqual(this, {}) + assert.deepStrictEqual(this, { signal: this.signal }) }) it('callback t is this in test', function (done) { - assert.deepStrictEqual(this, {}) + assert.deepStrictEqual(this, { signal: this.signal }) done() }) diff --git a/test/message/test_runner_desctibe_it.out b/test/message/test_runner_desctibe_it.out index 9c6af00..177dbff 100644 --- a/test/message/test_runner_desctibe_it.out +++ b/test/message/test_runner_desctibe_it.out @@ -23,6 +23,7 @@ not ok 3 - sync fail todo # TODO * * * + * ... # Subtest: sync fail todo with message not ok 4 - sync fail todo with message # TODO this is a failing todo @@ -41,6 +42,7 @@ not ok 4 - sync fail todo with message # TODO this is a failing todo * * * + * ... # Subtest: sync skip pass ok 5 - sync skip pass # SKIP