@@ -8,15 +8,19 @@ const {
88 Number,
99 PromiseResolve,
1010 ReflectApply,
11+ SafeArrayIterator,
1112 SafeMap,
1213 PromiseRace,
1314} = primordials ;
1415const { AsyncResource } = require ( 'async_hooks' ) ;
16+ const { once } = require ( 'events' ) ;
17+ const { AbortController } = require ( 'internal/abort_controller' ) ;
1518const {
1619 codes : {
1720 ERR_TEST_FAILURE ,
1821 } ,
1922 kIsNodeError,
23+ AbortError,
2024} = require ( 'internal/errors' ) ;
2125const { getOptionValue } = require ( 'internal/options' ) ;
2226const { TapStream } = require ( 'internal/test_runner/tap_stream' ) ;
@@ -26,7 +30,7 @@ const {
2630 kEmptyObject,
2731} = require ( 'internal/util' ) ;
2832const { isPromise } = require ( 'internal/util/types' ) ;
29- const { isUint32 } = require ( 'internal/validators' ) ;
33+ const { isUint32, validateAbortSignal } = require ( 'internal/validators' ) ;
3034const { setTimeout } = require ( 'timers/promises' ) ;
3135const { cpus } = require ( 'os' ) ;
3236const { bigint : hrtime } = process . hrtime ;
@@ -45,19 +49,16 @@ const testOnlyFlag = !isTestRunner && getOptionValue('--test-only');
4549const rootConcurrency = isTestRunner ? cpus ( ) . length : 1 ;
4650
4751
48- function testTimeout ( promise , timeout ) {
52+ function stopTest ( timeout , signal ) {
4953 if ( timeout === kDefaultTimeout ) {
50- return promise ;
51- }
52- return PromiseRace ( [
53- promise ,
54- setTimeout ( timeout , null , { ref : false } ) . then ( ( ) => {
55- throw new ERR_TEST_FAILURE (
56- `test timed out after ${ timeout } ms` ,
57- kTestTimeoutFailure
58- ) ;
59- } ) ,
60- ] ) ;
54+ return once ( signal , 'abort' ) ;
55+ }
56+ return setTimeout ( timeout , null , { ref : false , signal } ) . then ( ( ) => {
57+ throw new ERR_TEST_FAILURE (
58+ `test timed out after ${ timeout } ms` ,
59+ kTestTimeoutFailure
60+ ) ;
61+ } ) ;
6162}
6263
6364class TestContext {
@@ -67,6 +68,10 @@ class TestContext {
6768 this . #test = test ;
6869 }
6970
71+ get signal ( ) {
72+ return this . #test. signal ;
73+ }
74+
7075 diagnostic ( message ) {
7176 this . #test. diagnostic ( message ) ;
7277 }
@@ -92,11 +97,14 @@ class TestContext {
9297}
9398
9499class Test extends AsyncResource {
100+ #abortController;
101+ #outerSignal;
102+
95103 constructor ( options ) {
96104 super ( 'Test' ) ;
97105
98106 let { fn, name, parent, skip } = options ;
99- const { concurrency, only, timeout, todo } = options ;
107+ const { concurrency, only, timeout, todo, signal } = options ;
100108
101109 if ( typeof fn !== 'function' ) {
102110 fn = noop ;
@@ -149,6 +157,14 @@ class Test extends AsyncResource {
149157 fn = noop ;
150158 }
151159
160+ this . #abortController = new AbortController ( ) ;
161+ this . #outerSignal = signal ;
162+ this . signal = this . #abortController. signal ;
163+
164+ validateAbortSignal ( signal , 'options.signal' ) ;
165+ this . #outerSignal?. addEventListener ( 'abort' , this . #abortHandler) ;
166+
167+
152168 this . fn = fn ;
153169 this . name = name ;
154170 this . parent = parent ;
@@ -268,18 +284,23 @@ class Test extends AsyncResource {
268284 return test ;
269285 }
270286
271- cancel ( ) {
287+ #abortHandler = ( ) => {
288+ this . cancel ( this . #outerSignal?. reason || new AbortError ( 'The test was aborted' ) ) ;
289+ } ;
290+
291+ cancel ( error ) {
272292 if ( this . endTime !== null ) {
273293 return ;
274294 }
275295
276- this . fail (
296+ this . fail ( error ||
277297 new ERR_TEST_FAILURE (
278298 'test did not finish before its parent and was cancelled' ,
279299 kCancelledByParent
280300 )
281301 ) ;
282302 this . cancelled = true ;
303+ this . #abortController. abort ( ) ;
283304 }
284305
285306 fail ( err ) {
@@ -329,6 +350,15 @@ class Test extends AsyncResource {
329350
330351 return this . run ( ) ;
331352 }
353+ #shouldAbort( ) {
354+ if ( this . signal . aborted ) {
355+ return true ;
356+ }
357+ if ( this . #outerSignal?. aborted ) {
358+ this . cancel ( this . #outerSignal. reason || new AbortError ( 'The test was aborted' ) ) ;
359+ return true ;
360+ }
361+ }
332362
333363 getRunArgs ( ) {
334364 const ctx = new TestContext ( this ) ;
@@ -339,9 +369,15 @@ class Test extends AsyncResource {
339369 this . parent . activeSubtests ++ ;
340370 this . startTime = hrtime ( ) ;
341371
372+ if ( this . #shouldAbort( ) ) {
373+ this . postRun ( ) ;
374+ return ;
375+ }
376+
342377 try {
378+ const stopPromise = stopTest ( this . timeout , this . signal ) ;
343379 const { args, ctx } = this . getRunArgs ( ) ;
344- ArrayPrototypeUnshift ( args , this . fn , ctx ) ; // Note that if it's not OK to mutate args, we need to first clone it.
380+ ArrayPrototypeUnshift ( args , this . fn , ctx ) ;
345381
346382 if ( this . fn . length === args . length - 1 ) {
347383 // This test is using legacy Node.js error first callbacks.
@@ -355,13 +391,19 @@ class Test extends AsyncResource {
355391 'passed a callback but also returned a Promise' ,
356392 kCallbackAndPromisePresent
357393 ) ) ;
358- await testTimeout ( ret , this . timeout ) ;
394+ await PromiseRace ( SafeArrayIterator ( [ ret , stopPromise ] ) ) ;
359395 } else {
360- await testTimeout ( promise , this . timeout ) ;
396+ await PromiseRace ( SafeArrayIterator ( [ promise , stopPromise ] ) ) ;
361397 }
362398 } else {
363399 // This test is synchronous or using Promises.
364- await testTimeout ( ReflectApply ( this . runInAsyncScope , this , args ) , this . timeout ) ;
400+ const promise = ReflectApply ( this . runInAsyncScope , this , args ) ;
401+ await PromiseRace ( new SafeArrayIterator ( [ promise , stopPromise ] ) ) ;
402+ }
403+
404+ if ( this . #shouldAbort( ) ) {
405+ this . postRun ( ) ;
406+ return ;
365407 }
366408
367409 this . pass ( ) ;
@@ -409,6 +451,8 @@ class Test extends AsyncResource {
409451 this . fail ( new ERR_TEST_FAILURE ( msg , kSubtestsFailed ) ) ;
410452 }
411453
454+ this . #outerSignal?. removeEventListener ( 'abort' , this . #abortHandler) ;
455+
412456 if ( this . parent !== null ) {
413457 this . parent . activeSubtests -- ;
414458 this . parent . addReadySubtest ( this ) ;
0 commit comments