@@ -22,6 +22,8 @@ const {
2222 SafePromiseAll,
2323 SafePromiseAllReturnVoid,
2424 SafePromiseAllSettledReturnVoid,
25+ SafePromisePrototypeFinally,
26+ SafePromiseRace,
2527 SafeSet,
2628 StringPrototypeIndexOf,
2729 StringPrototypeSlice,
@@ -147,6 +149,7 @@ function getRunArgs(path, { forceExit,
147149 testNamePatterns,
148150 testSkipPatterns,
149151 only,
152+ bail,
150153 argv : suppliedArgs ,
151154 execArgv,
152155 rerunFailuresFilePath,
@@ -185,6 +188,9 @@ function getRunArgs(path, { forceExit,
185188 if ( only === true ) {
186189 ArrayPrototypePush ( runArgs , '--test-only' ) ;
187190 }
191+ if ( bail === true ) {
192+ ArrayPrototypePush ( runArgs , '--test-bail' ) ;
193+ }
188194 if ( timeout != null ) {
189195 ArrayPrototypePush ( runArgs , `--test-timeout=${ timeout } ` ) ;
190196 }
@@ -271,9 +277,13 @@ class FileTest extends Test {
271277 this . reporter [ kEmitMessage ] ( item . type , item . data ) ;
272278 }
273279 #accumulateReportItem( item ) {
274- if ( item . type !== 'test:pass' && item . type !== 'test:fail' ) {
280+ if ( item . type !== 'test:pass' && item . type !== 'test:fail' && item . type !== 'test:bail' ) {
275281 return ;
276282 }
283+ // If a test failure occurred and bail is enabled, emit a bail event after reporting the failure
284+ if ( item . type === 'test:bail' && this . root . harness ?. bail && ! this . root . harness . bailedOut ) {
285+ this . root . harness . bailedOut = true ;
286+ }
277287 this . #reportedChildren++ ;
278288 if ( item . data . nesting === 0 && item . type === 'test:fail' ) {
279289 this . failedSubtests = true ;
@@ -604,6 +614,7 @@ function run(options = kEmptyObject) {
604614 } = options ;
605615 const {
606616 concurrency,
617+ bail,
607618 timeout,
608619 signal,
609620 files,
@@ -747,7 +758,9 @@ function run(options = kEmptyObject) {
747758 functionCoverage : functionCoverage ,
748759 cwd,
749760 globalSetupPath,
761+ bail,
750762 } ;
763+
751764 const root = createTestTree ( rootTestOptions , globalOptions ) ;
752765 let testFiles = files ?? createTestFileList ( globPatterns , cwd ) ;
753766 const { isTestRunner } = globalOptions ;
@@ -756,10 +769,18 @@ function run(options = kEmptyObject) {
756769 testFiles = ArrayPrototypeFilter ( testFiles , ( _ , index ) => index % shard . total === shard . index - 1 ) ;
757770 }
758771
772+ if ( bail ) {
773+ validateBoolean ( bail , 'options.bail' ) ;
774+ if ( watch ) {
775+ throw new ERR_INVALID_ARG_VALUE ( 'options.bail' , watch , 'bail not supported with watch mode' ) ;
776+ }
777+ }
778+
759779 let teardown ;
760780 let postRun ;
761781 let filesWatcher ;
762782 let runFiles ;
783+
763784 const opts = {
764785 __proto__ : null ,
765786 root,
@@ -770,6 +791,7 @@ function run(options = kEmptyObject) {
770791 hasFiles : files != null ,
771792 globPatterns,
772793 only,
794+ bail,
773795 forceExit,
774796 cwd,
775797 isolation,
@@ -792,15 +814,53 @@ function run(options = kEmptyObject) {
792814 teardown = ( ) => root . harness . teardown ( ) ;
793815 }
794816
795- runFiles = ( ) => {
796- root . harness . bootstrapPromise = null ;
797- root . harness . buildPromise = null ;
798- return SafePromiseAllSettledReturnVoid ( testFiles , ( path ) => {
799- const subtest = runTestFile ( path , filesWatcher , opts ) ;
800- filesWatcher ?. runningSubtests . set ( path , subtest ) ;
801- return subtest ;
802- } ) ;
803- } ;
817+ if ( bail ) {
818+ runFiles = async ( ) => {
819+ root . harness . bootstrapPromise = null ;
820+ root . harness . buildPromise = null ;
821+
822+ const running = new SafeSet ( ) ;
823+ let index = 0 ;
824+
825+ const shouldBail = ( ) => bail && root . harness . bailedOut ;
826+
827+ const enqueueNext = ( ) => {
828+ if ( index < testFiles . length && ! shouldBail ( ) ) {
829+ const path = testFiles [ index ++ ] ;
830+ const subtest = runTestFile ( path , filesWatcher , opts ) ;
831+ filesWatcher ?. runningSubtests . set ( path , subtest ) ;
832+ running . add ( subtest ) ;
833+ SafePromisePrototypeFinally ( subtest , ( ) => running . delete ( subtest ) ) ;
834+ }
835+ } ;
836+
837+ // Fill initial pool up to root test concurrency
838+ // We use root test concurrency here because concurrency logic is handled at test level.
839+ while ( running . size < root . concurrency && index < testFiles . length && ! shouldBail ( ) ) {
840+ enqueueNext ( ) ;
841+ }
842+
843+ // As each test completes, enqueue the next one
844+ while ( running . size > 0 ) {
845+ await SafePromiseRace ( [ ...running ] ) ;
846+
847+ // Refill pool after completion(s)
848+ while ( running . size < root . concurrency && index < testFiles . length && ! shouldBail ( ) ) {
849+ enqueueNext ( ) ;
850+ }
851+ }
852+ } ;
853+ } else {
854+ runFiles = ( ) => {
855+ root . harness . bootstrapPromise = null ;
856+ root . harness . buildPromise = null ;
857+ return SafePromiseAllSettledReturnVoid ( testFiles , ( path ) => {
858+ const subtest = runTestFile ( path , filesWatcher , opts ) ;
859+ filesWatcher ?. runningSubtests . set ( path , subtest ) ;
860+ return subtest ;
861+ } ) ;
862+ } ;
863+ }
804864 } else if ( isolation === 'none' ) {
805865 if ( watch ) {
806866 const absoluteTestFiles = ArrayPrototypeMap ( testFiles , ( file ) => ( isAbsolute ( file ) ? file : resolve ( cwd , file ) ) ) ;
0 commit comments