@@ -15,7 +15,6 @@ const {
1515 ArrayPrototypeSlice,
1616 ArrayPrototypeSome,
1717 ArrayPrototypeSort,
18- MathMax,
1918 ObjectAssign,
2019 PromisePrototypeThen,
2120 PromiseWithResolvers,
@@ -36,7 +35,6 @@ const {
3635
3736const { spawn } = require ( 'child_process' ) ;
3837const { finished } = require ( 'internal/streams/end-of-stream' ) ;
39- const { availableParallelism } = require ( 'os' ) ;
4038const { resolve, sep, isAbsolute } = require ( 'path' ) ;
4139const { DefaultDeserializer, DefaultSerializer } = require ( 'v8' ) ;
4240const { getOptionValue, getOptionsAsFlagsFromBinding } = require ( 'internal/options' ) ;
@@ -137,17 +135,22 @@ let kResistStopPropagation;
137135
138136// Worker ID pool management for concurrent test execution
139137class WorkerIdPool {
140- #nextId = 0 ;
141- #maxConcurrency;
142-
143- constructor ( maxConcurrency ) {
144- this . #maxConcurrency = maxConcurrency ;
145- }
138+ #acquiredIds = new SafeSet ( ) ;
146139
147140 acquire ( ) {
148- const id = ( this . #nextId++ % this . #maxConcurrency) + 1 ;
141+ let id = 1 ;
142+
143+ while ( this . #acquiredIds. has ( id ) ) {
144+ id ++ ;
145+ }
146+
147+ this . #acquiredIds. add ( id ) ;
149148 return id ;
150149 }
150+
151+ release ( id ) {
152+ this . #acquiredIds. delete ( id ) ;
153+ }
151154}
152155
153156function createTestFileList ( patterns , cwd ) {
@@ -519,94 +522,102 @@ function runTestFile(path, filesWatcher, opts) {
519522 debug ( 'Assigned worker ID %d to test file: %s' , workerId , path ) ;
520523 }
521524
522- if ( watchMode ) {
523- stdio . push ( 'ipc' ) ;
524- env . WATCH_REPORT_DEPENDENCIES = '1' ;
525- }
526- if ( opts . root . harness . shouldColorizeTestFiles ) {
527- env . FORCE_COLOR = '1' ;
528- }
529-
530- const child = spawn (
531- process . execPath , args ,
532- {
533- __proto__ : null ,
534- signal : t . signal ,
535- encoding : 'utf8' ,
536- env,
537- stdio,
538- cwd : opts . cwd ,
539- } ,
540- ) ;
541- if ( watchMode ) {
542- filesWatcher . runningProcesses . set ( path , child ) ;
543- filesWatcher . watcher . watchChildProcessModules ( child , path ) ;
544- }
545-
546- let err ;
525+ try {
526+ if ( watchMode ) {
527+ stdio . push ( 'ipc' ) ;
528+ env . WATCH_REPORT_DEPENDENCIES = '1' ;
529+ }
530+ if ( opts . root . harness . shouldColorizeTestFiles ) {
531+ env . FORCE_COLOR = '1' ;
532+ }
547533
548- child . on ( 'error' , ( error ) => {
549- err = error ;
550- } ) ;
534+ const child = spawn (
535+ process . execPath , args ,
536+ {
537+ __proto__ : null ,
538+ signal : t . signal ,
539+ encoding : 'utf8' ,
540+ env,
541+ stdio,
542+ cwd : opts . cwd ,
543+ } ,
544+ ) ;
545+ if ( watchMode ) {
546+ filesWatcher . runningProcesses . set ( path , child ) ;
547+ filesWatcher . watcher . watchChildProcessModules ( child , path ) ;
548+ }
551549
552- child . stdout . on ( 'data' , ( data ) => {
553- subtest . parseMessage ( data ) ;
554- } ) ;
550+ let err ;
555551
556- const rl = new Interface ( { __proto__ : null , input : child . stderr } ) ;
557- rl . on ( 'line' , ( line ) => {
558- if ( isInspectorMessage ( line ) ) {
559- process . stderr . write ( line + '\n' ) ;
560- return ;
561- }
552+ child . on ( 'error' , ( error ) => {
553+ err = error ;
554+ } ) ;
562555
563- // stderr cannot be treated as TAP, per the spec. However, we want to
564- // surface stderr lines to improve the DX. Inject each line into the
565- // test output as an unknown token as if it came from the TAP parser.
566- subtest . addToReport ( {
567- __proto__ : null ,
568- type : 'test:stderr' ,
569- data : { __proto__ : null , file : path , message : line + '\n' } ,
556+ child . stdout . on ( 'data' , ( data ) => {
557+ subtest . parseMessage ( data ) ;
570558 } ) ;
571- } ) ;
572559
573- const { 0 : { 0 : code , 1 : signal } } = await SafePromiseAll ( [
574- once ( child , 'exit' , { __proto__ : null , signal : t . signal } ) ,
575- finished ( child . stdout , { __proto__ : null , signal : t . signal } ) ,
576- ] ) ;
577-
578- // Close readline interface to prevent memory leak
579- rl . close ( ) ;
580-
581- if ( watchMode ) {
582- filesWatcher . runningProcesses . delete ( path ) ;
583- filesWatcher . runningSubtests . delete ( path ) ;
584- ( async ( ) => {
585- try {
586- await subTestEnded ;
587- } finally {
588- if ( filesWatcher . runningSubtests . size === 0 ) {
589- opts . root . reporter [ kEmitMessage ] ( 'test:watch:drained' ) ;
590- opts . root . postRun ( ) ;
591- }
560+ const rl = new Interface ( { __proto__ : null , input : child . stderr } ) ;
561+ rl . on ( 'line' , ( line ) => {
562+ if ( isInspectorMessage ( line ) ) {
563+ process . stderr . write ( line + '\n' ) ;
564+ return ;
592565 }
593- } ) ( ) ;
594- }
595566
596- if ( code !== 0 || signal !== null ) {
597- if ( ! err ) {
598- const failureType = subtest . failedSubtests ? kSubtestsFailed : kTestCodeFailure ;
599- err = ObjectAssign ( new ERR_TEST_FAILURE ( 'test failed' , failureType ) , {
567+ // stderr cannot be treated as TAP, per the spec. However, we want to
568+ // surface stderr lines to improve the DX. Inject each line into the
569+ // test output as an unknown token as if it came from the TAP parser.
570+ subtest . addToReport ( {
600571 __proto__ : null ,
601- exitCode : code ,
602- signal : signal ,
603- // The stack will not be useful since the failures came from tests
604- // in a child process.
605- stack : undefined ,
572+ type : 'test:stderr' ,
573+ data : { __proto__ : null , file : path , message : line + '\n' } ,
606574 } ) ;
575+ } ) ;
576+
577+ const { 0 : { 0 : code , 1 : signal } } = await SafePromiseAll ( [
578+ once ( child , 'exit' , { __proto__ : null , signal : t . signal } ) ,
579+ finished ( child . stdout , { __proto__ : null , signal : t . signal } ) ,
580+ ] ) ;
581+
582+ // Close readline interface to prevent memory leak
583+ rl . close ( ) ;
584+
585+ if ( watchMode ) {
586+ filesWatcher . runningProcesses . delete ( path ) ;
587+ filesWatcher . runningSubtests . delete ( path ) ;
588+ ( async ( ) => {
589+ try {
590+ await subTestEnded ;
591+ } finally {
592+ if ( filesWatcher . runningSubtests . size === 0 ) {
593+ opts . root . reporter [ kEmitMessage ] ( 'test:watch:drained' ) ;
594+ opts . root . postRun ( ) ;
595+ }
596+ }
597+ } ) ( ) ;
607598 }
608599
609- throw err ;
600+ if ( code !== 0 || signal !== null ) {
601+ if ( ! err ) {
602+ const failureType = subtest . failedSubtests ? kSubtestsFailed : kTestCodeFailure ;
603+ err = ObjectAssign ( new ERR_TEST_FAILURE ( 'test failed' , failureType ) , {
604+ __proto__ : null ,
605+ exitCode : code ,
606+ signal : signal ,
607+ // The stack will not be useful since the failures came from tests
608+ // in a child process.
609+ stack : undefined ,
610+ } ) ;
611+ }
612+
613+ throw err ;
614+ }
615+ } finally {
616+ // Every exit path must return the ID, including abort and spawn failure.
617+ if ( opts . workerIdPool && workerId !== undefined ) {
618+ opts . workerIdPool . release ( workerId ) ;
619+ debug ( 'Released worker ID %d from test file: %s' , workerId , path ) ;
620+ }
610621 }
611622 } ) ;
612623 const subTestEnded = subtest . start ( ) ;
@@ -992,23 +1003,10 @@ function run(options = kEmptyObject) {
9921003 let filesWatcher ;
9931004 let runFiles ;
9941005
995- // Create worker ID pool for concurrent test execution.
996- // Use concurrency from globalOptions which has been processed by parseCommandLine().
997- const effectiveConcurrency = globalOptions . concurrency ?? concurrency ;
998- let maxConcurrency = 1 ;
999- if ( effectiveConcurrency === true ) {
1000- maxConcurrency = MathMax ( availableParallelism ( ) - 1 , 1 ) ;
1001- } else if ( typeof effectiveConcurrency === 'number' ) {
1002- maxConcurrency = effectiveConcurrency ;
1003- }
1004- const workerIdPool = new WorkerIdPool ( maxConcurrency ) ;
1005- debug (
1006- 'Created worker ID pool with max concurrency: %d, ' +
1007- 'effectiveConcurrency: %s, testFiles: %d' ,
1008- maxConcurrency ,
1009- effectiveConcurrency ,
1010- testFiles . length ,
1011- ) ;
1006+ // The pool tracks the IDs actually in use, so they stay exclusive and never
1007+ // exceed the number of files running concurrently.
1008+ const workerIdPool = new WorkerIdPool ( ) ;
1009+ debug ( 'Created worker ID pool, testFiles: %d' , testFiles . length ) ;
10121010
10131011 const opts = {
10141012 __proto__ : null ,
0 commit comments