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