@@ -9,14 +9,18 @@ const {
9
9
PromiseResolve,
10
10
ReflectApply,
11
11
SafeMap,
12
- PromiseRace,
12
+ SafePromiseRace,
13
+ Symbol,
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 ;
@@ -44,20 +48,19 @@ const testOnlyFlag = !isTestRunner && getOptionValue('--test-only');
44
48
// TODO(cjihrig): Use uv_available_parallelism() once it lands.
45
49
const rootConcurrency = isTestRunner ? cpus ( ) . length : 1 ;
46
50
51
+ const kShouldAbort = Symbol ( 'kShouldAbort' ) ;
47
52
48
- function testTimeout ( promise , timeout ) {
53
+
54
+ function stopTest ( timeout , signal ) {
49
55
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
- ] ) ;
56
+ return once ( signal , 'abort' ) ;
57
+ }
58
+ return setTimeout ( timeout , null , { ref : false , signal } ) . then ( ( ) => {
59
+ throw new ERR_TEST_FAILURE (
60
+ `test timed out after ${ timeout } ms` ,
61
+ kTestTimeoutFailure
62
+ ) ;
63
+ } ) ;
61
64
}
62
65
63
66
class TestContext {
@@ -67,6 +70,10 @@ class TestContext {
67
70
this . #test = test ;
68
71
}
69
72
73
+ get signal ( ) {
74
+ return this . #test. signal ;
75
+ }
76
+
70
77
diagnostic ( message ) {
71
78
this . #test. diagnostic ( message ) ;
72
79
}
@@ -92,11 +99,14 @@ class TestContext {
92
99
}
93
100
94
101
class Test extends AsyncResource {
102
+ #abortController;
103
+ #outerSignal;
104
+
95
105
constructor ( options ) {
96
106
super ( 'Test' ) ;
97
107
98
108
let { fn, name, parent, skip } = options ;
99
- const { concurrency, only, timeout, todo } = options ;
109
+ const { concurrency, only, timeout, todo, signal } = options ;
100
110
101
111
if ( typeof fn !== 'function' ) {
102
112
fn = noop ;
@@ -149,6 +159,14 @@ class Test extends AsyncResource {
149
159
fn = noop ;
150
160
}
151
161
162
+ this . #abortController = new AbortController ( ) ;
163
+ this . #outerSignal = signal ;
164
+ this . signal = this . #abortController. signal ;
165
+
166
+ validateAbortSignal ( signal , 'options.signal' ) ;
167
+ this . #outerSignal?. addEventListener ( 'abort' , this . #abortHandler) ;
168
+
169
+
152
170
this . fn = fn ;
153
171
this . name = name ;
154
172
this . parent = parent ;
@@ -242,7 +260,8 @@ class Test extends AsyncResource {
242
260
243
261
// If this test has already ended, attach this test to the root test so
244
262
// that the error can be properly reported.
245
- if ( this . finished ) {
263
+ const preventAddingSubtests = this . finished || this . buildPhaseFinished ;
264
+ if ( preventAddingSubtests ) {
246
265
while ( parent . parent !== null ) {
247
266
parent = parent . parent ;
248
267
}
@@ -254,7 +273,7 @@ class Test extends AsyncResource {
254
273
parent . waitingOn = test . testNumber ;
255
274
}
256
275
257
- if ( this . finished ) {
276
+ if ( preventAddingSubtests ) {
258
277
test . startTime = test . startTime || hrtime ( ) ;
259
278
test . fail (
260
279
new ERR_TEST_FAILURE (
@@ -268,18 +287,23 @@ class Test extends AsyncResource {
268
287
return test ;
269
288
}
270
289
271
- cancel ( ) {
290
+ #abortHandler = ( ) => {
291
+ this . cancel ( this . #outerSignal?. reason || new AbortError ( 'The test was aborted' ) ) ;
292
+ } ;
293
+
294
+ cancel ( error ) {
272
295
if ( this . endTime !== null ) {
273
296
return ;
274
297
}
275
298
276
- this . fail (
299
+ this . fail ( error ||
277
300
new ERR_TEST_FAILURE (
278
301
'test did not finish before its parent and was cancelled' ,
279
302
kCancelledByParent
280
303
)
281
304
) ;
282
305
this . cancelled = true ;
306
+ this . #abortController. abort ( ) ;
283
307
}
284
308
285
309
fail ( err ) {
@@ -330,6 +354,16 @@ class Test extends AsyncResource {
330
354
return this . run ( ) ;
331
355
}
332
356
357
+ [ kShouldAbort ] ( ) {
358
+ if ( this . signal . aborted ) {
359
+ return true ;
360
+ }
361
+ if ( this . #outerSignal?. aborted ) {
362
+ this . cancel ( this . #outerSignal. reason || new AbortError ( 'The test was aborted' ) ) ;
363
+ return true ;
364
+ }
365
+ }
366
+
333
367
getRunArgs ( ) {
334
368
const ctx = new TestContext ( this ) ;
335
369
return { ctx, args : [ ctx ] } ;
@@ -339,7 +373,13 @@ class Test extends AsyncResource {
339
373
this . parent . activeSubtests ++ ;
340
374
this . startTime = hrtime ( ) ;
341
375
376
+ if ( this [ kShouldAbort ] ( ) ) {
377
+ this . postRun ( ) ;
378
+ return ;
379
+ }
380
+
342
381
try {
382
+ const stopPromise = stopTest ( this . timeout , this . signal ) ;
343
383
const { args, ctx } = this . getRunArgs ( ) ;
344
384
ArrayPrototypeUnshift ( args , this . fn , ctx ) ; // Note that if it's not OK to mutate args, we need to first clone it.
345
385
@@ -355,13 +395,19 @@ class Test extends AsyncResource {
355
395
'passed a callback but also returned a Promise' ,
356
396
kCallbackAndPromisePresent
357
397
) ) ;
358
- await testTimeout ( ret , this . timeout ) ;
398
+ await SafePromiseRace ( [ ret , stopPromise ] ) ;
359
399
} else {
360
- await testTimeout ( promise , this . timeout ) ;
400
+ await SafePromiseRace ( [ PromiseResolve ( promise ) , stopPromise ] ) ;
361
401
}
362
402
} else {
363
403
// This test is synchronous or using Promises.
364
- await testTimeout ( ReflectApply ( this . runInAsyncScope , this , args ) , this . timeout ) ;
404
+ const promise = ReflectApply ( this . runInAsyncScope , this , args ) ;
405
+ await SafePromiseRace ( [ PromiseResolve ( promise ) , stopPromise ] ) ;
406
+ }
407
+
408
+ if ( this [ kShouldAbort ] ( ) ) {
409
+ this . postRun ( ) ;
410
+ return ;
365
411
}
366
412
367
413
this . pass ( ) ;
@@ -410,6 +456,8 @@ class Test extends AsyncResource {
410
456
this . fail ( new ERR_TEST_FAILURE ( msg , kSubtestsFailed ) ) ;
411
457
}
412
458
459
+ this . #outerSignal?. removeEventListener ( 'abort' , this . #abortHandler) ;
460
+
413
461
if ( this . parent !== null ) {
414
462
this . parent . activeSubtests -- ;
415
463
this . parent . addReadySubtest ( this ) ;
@@ -477,20 +525,21 @@ class Test extends AsyncResource {
477
525
class ItTest extends Test {
478
526
constructor ( opt ) { super ( opt ) ; } // eslint-disable-line no-useless-constructor
479
527
getRunArgs ( ) {
480
- return { ctx : { } , args : [ ] } ;
528
+ return { ctx : { signal : this . signal } , args : [ ] } ;
481
529
}
482
530
}
483
531
class Suite extends Test {
484
532
constructor ( options ) {
485
533
super ( options ) ;
486
534
487
535
try {
488
- this . buildSuite = this . runInAsyncScope ( this . fn ) ;
536
+ const context = { signal : this . signal } ;
537
+ this . buildSuite = this . runInAsyncScope ( this . fn , context , [ context ] ) ;
489
538
} catch ( err ) {
490
539
this . fail ( new ERR_TEST_FAILURE ( err , kTestCodeFailure ) ) ;
491
540
}
492
541
this . fn = ( ) => { } ;
493
- this . finished = true ; // Forbid adding subtests to this suite
542
+ this . buildPhaseFinished = true ;
494
543
}
495
544
496
545
start ( ) {
@@ -505,11 +554,21 @@ class Suite extends Test {
505
554
}
506
555
this . parent . activeSubtests ++ ;
507
556
this . startTime = hrtime ( ) ;
557
+
558
+ if ( this [ kShouldAbort ] ( ) ) {
559
+ this . subtests = [ ] ;
560
+ this . postRun ( ) ;
561
+ return ;
562
+ }
563
+
564
+ const stopPromise = stopTest ( this . timeout , this . signal ) ;
508
565
const subtests = this . skipped || this . error ? [ ] : this . subtests ;
509
- await testTimeout ( ArrayPrototypeReduce ( subtests , async ( prev , subtest ) => {
566
+ const promise = ArrayPrototypeReduce ( subtests , async ( prev , subtest ) => {
510
567
await prev ;
511
568
await subtest . run ( ) ;
512
- } , PromiseResolve ( ) ) , this . timeout ) ;
569
+ } , PromiseResolve ( ) ) ;
570
+
571
+ await SafePromiseRace ( [ promise , stopPromise ] ) ;
513
572
this . pass ( ) ;
514
573
this . postRun ( ) ;
515
574
}
0 commit comments