@@ -14,7 +14,6 @@ const {
1414 ArrayPrototypeSlice,
1515 ArrayPrototypeSome,
1616 ArrayPrototypeSort,
17- MathMax,
1817 ObjectAssign,
1918 PromisePrototypeThen,
2019 PromiseWithResolvers,
@@ -37,7 +36,6 @@ const {
3736const { spawn } = require ( 'child_process' ) ;
3837const { statSync } = require ( 'fs' ) ;
3938const { finished } = require ( 'internal/streams/end-of-stream' ) ;
40- const { availableParallelism } = require ( 'os' ) ;
4139const { resolve, sep, isAbsolute } = require ( 'path' ) ;
4240const { DefaultDeserializer, DefaultSerializer } = require ( 'v8' ) ;
4341const { getOptionValue, getOptionsAsFlagsFromBinding } = require ( 'internal/options' ) ;
@@ -138,17 +136,22 @@ let kResistStopPropagation;
138136
139137// Worker ID pool management for concurrent test execution
140138class WorkerIdPool {
141- #nextId = 0 ;
142- #maxConcurrency;
143-
144- constructor ( maxConcurrency ) {
145- this . #maxConcurrency = maxConcurrency ;
146- }
139+ #acquiredIds = new SafeSet ( ) ;
147140
148141 acquire ( ) {
149- const id = ( this . #nextId++ % this . #maxConcurrency) + 1 ;
142+ let id = 1 ;
143+
144+ while ( this . #acquiredIds. has ( id ) ) {
145+ id ++ ;
146+ }
147+
148+ this . #acquiredIds. add ( id ) ;
150149 return id ;
151150 }
151+
152+ release ( id ) {
153+ this . #acquiredIds. delete ( id ) ;
154+ }
152155}
153156
154157function createTestFileList ( patterns , cwd ) {
@@ -537,94 +540,102 @@ function runTestFile(path, filesWatcher, opts) {
537540 debug ( 'Assigned worker ID %d to test file: %s' , workerId , path ) ;
538541 }
539542
540- if ( watchMode ) {
541- stdio . push ( 'ipc' ) ;
542- env . WATCH_REPORT_DEPENDENCIES = '1' ;
543- }
544- if ( opts . root . harness . shouldColorizeTestFiles ) {
545- env . FORCE_COLOR = '1' ;
546- }
547-
548- const child = spawn (
549- process . execPath , args ,
550- {
551- __proto__ : null ,
552- signal : t . signal ,
553- encoding : 'utf8' ,
554- env,
555- stdio,
556- cwd : opts . cwd ,
557- } ,
558- ) ;
559- if ( watchMode ) {
560- filesWatcher . runningProcesses . set ( path , child ) ;
561- filesWatcher . watcher . watchChildProcessModules ( child , path ) ;
562- }
563-
564- let err ;
543+ try {
544+ if ( watchMode ) {
545+ stdio . push ( 'ipc' ) ;
546+ env . WATCH_REPORT_DEPENDENCIES = '1' ;
547+ }
548+ if ( opts . root . harness . shouldColorizeTestFiles ) {
549+ env . FORCE_COLOR = '1' ;
550+ }
565551
566- child . on ( 'error' , ( error ) => {
567- err = error ;
568- } ) ;
552+ const child = spawn (
553+ process . execPath , args ,
554+ {
555+ __proto__ : null ,
556+ signal : t . signal ,
557+ encoding : 'utf8' ,
558+ env,
559+ stdio,
560+ cwd : opts . cwd ,
561+ } ,
562+ ) ;
563+ if ( watchMode ) {
564+ filesWatcher . runningProcesses . set ( path , child ) ;
565+ filesWatcher . watcher . watchChildProcessModules ( child , path ) ;
566+ }
569567
570- child . stdout . on ( 'data' , ( data ) => {
571- subtest . parseMessage ( data ) ;
572- } ) ;
568+ let err ;
573569
574- const rl = new Interface ( { __proto__ : null , input : child . stderr } ) ;
575- rl . on ( 'line' , ( line ) => {
576- if ( isInspectorMessage ( line ) ) {
577- process . stderr . write ( line + '\n' ) ;
578- return ;
579- }
570+ child . on ( 'error' , ( error ) => {
571+ err = error ;
572+ } ) ;
580573
581- // stderr cannot be treated as TAP, per the spec. However, we want to
582- // surface stderr lines to improve the DX. Inject each line into the
583- // test output as an unknown token as if it came from the TAP parser.
584- subtest . addToReport ( {
585- __proto__ : null ,
586- type : 'test:stderr' ,
587- data : { __proto__ : null , file : path , message : line + '\n' } ,
574+ child . stdout . on ( 'data' , ( data ) => {
575+ subtest . parseMessage ( data ) ;
588576 } ) ;
589- } ) ;
590577
591- const { 0 : { 0 : code , 1 : signal } } = await SafePromiseAll ( [
592- once ( child , 'exit' , { __proto__ : null , signal : t . signal } ) ,
593- finished ( child . stdout , { __proto__ : null , signal : t . signal } ) ,
594- ] ) ;
595-
596- // Close readline interface to prevent memory leak
597- rl . close ( ) ;
598-
599- if ( watchMode ) {
600- filesWatcher . runningProcesses . delete ( path ) ;
601- filesWatcher . runningSubtests . delete ( path ) ;
602- ( async ( ) => {
603- try {
604- await subTestEnded ;
605- } finally {
606- if ( filesWatcher . runningSubtests . size === 0 ) {
607- opts . root . reporter [ kEmitMessage ] ( 'test:watch:drained' ) ;
608- opts . root . postRun ( ) ;
609- }
578+ const rl = new Interface ( { __proto__ : null , input : child . stderr } ) ;
579+ rl . on ( 'line' , ( line ) => {
580+ if ( isInspectorMessage ( line ) ) {
581+ process . stderr . write ( line + '\n' ) ;
582+ return ;
610583 }
611- } ) ( ) ;
612- }
613584
614- if ( code !== 0 || signal !== null ) {
615- if ( ! err ) {
616- const failureType = subtest . failedSubtests ? kSubtestsFailed : kTestCodeFailure ;
617- err = ObjectAssign ( new ERR_TEST_FAILURE ( 'test failed' , failureType ) , {
585+ // stderr cannot be treated as TAP, per the spec. However, we want to
586+ // surface stderr lines to improve the DX. Inject each line into the
587+ // test output as an unknown token as if it came from the TAP parser.
588+ subtest . addToReport ( {
618589 __proto__ : null ,
619- exitCode : code ,
620- signal : signal ,
621- // The stack will not be useful since the failures came from tests
622- // in a child process.
623- stack : undefined ,
590+ type : 'test:stderr' ,
591+ data : { __proto__ : null , file : path , message : line + '\n' } ,
624592 } ) ;
593+ } ) ;
594+
595+ const { 0 : { 0 : code , 1 : signal } } = await SafePromiseAll ( [
596+ once ( child , 'exit' , { __proto__ : null , signal : t . signal } ) ,
597+ finished ( child . stdout , { __proto__ : null , signal : t . signal } ) ,
598+ ] ) ;
599+
600+ // Close readline interface to prevent memory leak
601+ rl . close ( ) ;
602+
603+ if ( watchMode ) {
604+ filesWatcher . runningProcesses . delete ( path ) ;
605+ filesWatcher . runningSubtests . delete ( path ) ;
606+ ( async ( ) => {
607+ try {
608+ await subTestEnded ;
609+ } finally {
610+ if ( filesWatcher . runningSubtests . size === 0 ) {
611+ opts . root . reporter [ kEmitMessage ] ( 'test:watch:drained' ) ;
612+ opts . root . postRun ( ) ;
613+ }
614+ }
615+ } ) ( ) ;
625616 }
626617
627- throw err ;
618+ if ( code !== 0 || signal !== null ) {
619+ if ( ! err ) {
620+ const failureType = subtest . failedSubtests ? kSubtestsFailed : kTestCodeFailure ;
621+ err = ObjectAssign ( new ERR_TEST_FAILURE ( 'test failed' , failureType ) , {
622+ __proto__ : null ,
623+ exitCode : code ,
624+ signal : signal ,
625+ // The stack will not be useful since the failures came from tests
626+ // in a child process.
627+ stack : undefined ,
628+ } ) ;
629+ }
630+
631+ throw err ;
632+ }
633+ } finally {
634+ // Every exit path must return the ID, including abort and spawn failure.
635+ if ( opts . workerIdPool && workerId !== undefined ) {
636+ opts . workerIdPool . release ( workerId ) ;
637+ debug ( 'Released worker ID %d from test file: %s' , workerId , path ) ;
638+ }
628639 }
629640 } ) ;
630641 const subTestEnded = subtest . start ( ) ;
@@ -1010,23 +1021,10 @@ function run(options = kEmptyObject) {
10101021 let filesWatcher ;
10111022 let runFiles ;
10121023
1013- // Create worker ID pool for concurrent test execution.
1014- // Use concurrency from globalOptions which has been processed by parseCommandLine().
1015- const effectiveConcurrency = globalOptions . concurrency ?? concurrency ;
1016- let maxConcurrency = 1 ;
1017- if ( effectiveConcurrency === true ) {
1018- maxConcurrency = MathMax ( availableParallelism ( ) - 1 , 1 ) ;
1019- } else if ( typeof effectiveConcurrency === 'number' ) {
1020- maxConcurrency = effectiveConcurrency ;
1021- }
1022- const workerIdPool = new WorkerIdPool ( maxConcurrency ) ;
1023- debug (
1024- 'Created worker ID pool with max concurrency: %d, ' +
1025- 'effectiveConcurrency: %s, testFiles: %d' ,
1026- maxConcurrency ,
1027- effectiveConcurrency ,
1028- testFiles . length ,
1029- ) ;
1024+ // The pool tracks the IDs actually in use, so they stay exclusive and never
1025+ // exceed the number of files running concurrently.
1026+ const workerIdPool = new WorkerIdPool ( ) ;
1027+ debug ( 'Created worker ID pool, testFiles: %d' , testFiles . length ) ;
10301028
10311029 const opts = {
10321030 __proto__ : null ,
0 commit comments