@@ -8,15 +8,19 @@ const {
8
8
Number,
9
9
PromiseResolve,
10
10
ReflectApply,
11
+ SafeArrayIterator,
11
12
SafeMap,
12
13
PromiseRace,
13
14
} = primordials ;
14
15
const { AsyncResource } = require ( 'async_hooks' ) ;
16
+ const { once } = require ( 'events' ) ;
17
+ const { AbortController } = require ( 'internal/abort_controller' ) ;
15
18
const {
16
19
codes : {
17
20
ERR_TEST_FAILURE ,
18
21
} ,
19
22
kIsNodeError,
23
+ AbortError,
20
24
} = require ( 'internal/errors' ) ;
21
25
const { getOptionValue } = require ( 'internal/options' ) ;
22
26
const { TapStream } = require ( 'internal/test_runner/tap_stream' ) ;
@@ -26,7 +30,7 @@ const {
26
30
kEmptyObject,
27
31
} = require ( 'internal/util' ) ;
28
32
const { isPromise } = require ( 'internal/util/types' ) ;
29
- const { isUint32 } = require ( 'internal/validators' ) ;
33
+ const { isUint32, validateAbortSignal } = require ( 'internal/validators' ) ;
30
34
const { setTimeout } = require ( 'timers/promises' ) ;
31
35
const { cpus } = require ( 'os' ) ;
32
36
const { bigint : hrtime } = process . hrtime ;
@@ -45,19 +49,16 @@ const testOnlyFlag = !isTestRunner && getOptionValue('--test-only');
45
49
const rootConcurrency = isTestRunner ? cpus ( ) . length : 1 ;
46
50
47
51
48
- function testTimeout ( promise , timeout ) {
52
+ function stopTest ( timeout , signal ) {
49
53
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
+ } ) ;
61
62
}
62
63
63
64
class TestContext {
@@ -67,6 +68,10 @@ class TestContext {
67
68
this . #test = test ;
68
69
}
69
70
71
+ get signal ( ) {
72
+ return this . #test. signal ;
73
+ }
74
+
70
75
diagnostic ( message ) {
71
76
this . #test. diagnostic ( message ) ;
72
77
}
@@ -92,11 +97,14 @@ class TestContext {
92
97
}
93
98
94
99
class Test extends AsyncResource {
100
+ #abortController;
101
+ #outerSignal;
102
+
95
103
constructor ( options ) {
96
104
super ( 'Test' ) ;
97
105
98
106
let { fn, name, parent, skip } = options ;
99
- const { concurrency, only, timeout, todo } = options ;
107
+ const { concurrency, only, timeout, todo, signal } = options ;
100
108
101
109
if ( typeof fn !== 'function' ) {
102
110
fn = noop ;
@@ -149,6 +157,14 @@ class Test extends AsyncResource {
149
157
fn = noop ;
150
158
}
151
159
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
+
152
168
this . fn = fn ;
153
169
this . name = name ;
154
170
this . parent = parent ;
@@ -268,18 +284,23 @@ class Test extends AsyncResource {
268
284
return test ;
269
285
}
270
286
271
- cancel ( ) {
287
+ #abortHandler = ( ) => {
288
+ this . cancel ( this . #outerSignal?. reason || new AbortError ( 'The test was aborted' ) ) ;
289
+ } ;
290
+
291
+ cancel ( error ) {
272
292
if ( this . endTime !== null ) {
273
293
return ;
274
294
}
275
295
276
- this . fail (
296
+ this . fail ( error ||
277
297
new ERR_TEST_FAILURE (
278
298
'test did not finish before its parent and was cancelled' ,
279
299
kCancelledByParent
280
300
)
281
301
) ;
282
302
this . cancelled = true ;
303
+ this . #abortController. abort ( ) ;
283
304
}
284
305
285
306
fail ( err ) {
@@ -329,6 +350,15 @@ class Test extends AsyncResource {
329
350
330
351
return this . run ( ) ;
331
352
}
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
+ }
332
362
333
363
getRunArgs ( ) {
334
364
const ctx = new TestContext ( this ) ;
@@ -339,9 +369,15 @@ class Test extends AsyncResource {
339
369
this . parent . activeSubtests ++ ;
340
370
this . startTime = hrtime ( ) ;
341
371
372
+ if ( this . #shouldAbort( ) ) {
373
+ this . postRun ( ) ;
374
+ return ;
375
+ }
376
+
342
377
try {
378
+ const stopPromise = stopTest ( this . timeout , this . signal ) ;
343
379
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 ) ;
345
381
346
382
if ( this . fn . length === args . length - 1 ) {
347
383
// This test is using legacy Node.js error first callbacks.
@@ -355,13 +391,19 @@ class Test extends AsyncResource {
355
391
'passed a callback but also returned a Promise' ,
356
392
kCallbackAndPromisePresent
357
393
) ) ;
358
- await testTimeout ( ret , this . timeout ) ;
394
+ await PromiseRace ( SafeArrayIterator ( [ ret , stopPromise ] ) ) ;
359
395
} else {
360
- await testTimeout ( promise , this . timeout ) ;
396
+ await PromiseRace ( SafeArrayIterator ( [ promise , stopPromise ] ) ) ;
361
397
}
362
398
} else {
363
399
// 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 ;
365
407
}
366
408
367
409
this . pass ( ) ;
@@ -409,6 +451,8 @@ class Test extends AsyncResource {
409
451
this . fail ( new ERR_TEST_FAILURE ( msg , kSubtestsFailed ) ) ;
410
452
}
411
453
454
+ this . #outerSignal?. removeEventListener ( 'abort' , this . #abortHandler) ;
455
+
412
456
if ( this . parent !== null ) {
413
457
this . parent . activeSubtests -- ;
414
458
this . parent . addReadySubtest ( this ) ;
0 commit comments