@@ -22,6 +22,8 @@ const {
2222 SafePromiseAll,
2323 SafePromiseAllReturnVoid,
2424 SafePromiseAllSettledReturnVoid,
25+ SafePromisePrototypeFinally,
26+ SafePromiseRace,
2527 SafeSet,
2628 StringPrototypeIndexOf,
2729 StringPrototypeSlice,
@@ -254,6 +256,17 @@ class FileTest extends Test {
254256 if ( item . data . details ?. error ) {
255257 item . data . details . error = deserializeError ( item . data . details . error ) ;
256258 }
259+
260+ if ( item . type === 'test:fail' && this . root . harness . bail && ! this . root . harness . bailedOut ) {
261+ // Trigger bail if enabled and this is the first failure
262+ this . root . harness . bailedOut = true ;
263+ this . reporter [ kEmitMessage ] ( 'test:bail' , {
264+ __proto__ : null ,
265+ file : this . name ,
266+ test : item . data ,
267+ } ) ;
268+ }
269+
257270 if ( item . type === 'test:pass' || item . type === 'test:fail' ) {
258271 item . data . testNumber = isTopLevel ? ( this . root . harness . counters . topLevel + 1 ) : item . data . testNumber ;
259272 countCompletedTest ( {
@@ -604,6 +617,7 @@ function run(options = kEmptyObject) {
604617 } = options ;
605618 const {
606619 concurrency,
620+ bail,
607621 timeout,
608622 signal,
609623 files,
@@ -747,6 +761,7 @@ function run(options = kEmptyObject) {
747761 functionCoverage : functionCoverage ,
748762 cwd,
749763 globalSetupPath,
764+ bail,
750765 } ;
751766 const root = createTestTree ( rootTestOptions , globalOptions ) ;
752767 let testFiles = files ?? createTestFileList ( globPatterns , cwd ) ;
@@ -756,6 +771,16 @@ function run(options = kEmptyObject) {
756771 testFiles = ArrayPrototypeFilter ( testFiles , ( _ , index ) => index % shard . total === shard . index - 1 ) ;
757772 }
758773
774+ if ( bail ) {
775+ validateBoolean ( bail , 'options.bail' ) ;
776+ if ( watch ) {
777+ throw new ERR_INVALID_ARG_VALUE ( 'options.bail' , watch , 'bail not supported with watch mode' ) ;
778+ }
779+ if ( isolation === 'none' ) {
780+ throw new ERR_INVALID_ARG_VALUE ( 'options.bail' , isolation , 'bail not supported with \'none\' isolation' ) ;
781+ }
782+ }
783+
759784 let teardown ;
760785 let postRun ;
761786 let filesWatcher ;
@@ -770,6 +795,7 @@ function run(options = kEmptyObject) {
770795 hasFiles : files != null ,
771796 globPatterns,
772797 only,
798+ bail,
773799 forceExit,
774800 cwd,
775801 isolation,
@@ -792,15 +818,54 @@ function run(options = kEmptyObject) {
792818 teardown = ( ) => root . harness . teardown ( ) ;
793819 }
794820
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- } ;
821+ if ( bail ) {
822+ runFiles = async ( ) => {
823+ root . harness . bootstrapPromise = null ;
824+ root . harness . buildPromise = null ;
825+
826+ const running = new SafeSet ( ) ;
827+ let index = 0 ;
828+
829+ const shouldBail = ( ) => bail && root . harness . bailedOut ;
830+
831+ const enqueueNext = ( ) => {
832+ if ( index < testFiles . length && ! shouldBail ( ) ) {
833+ const path = testFiles [ index ++ ] ;
834+ const subtest = runTestFile ( path , filesWatcher , opts ) ;
835+ filesWatcher ?. runningSubtests . set ( path , subtest ) ;
836+ running . add ( subtest ) ;
837+ SafePromisePrototypeFinally ( subtest , ( ) => running . delete ( subtest ) ) ;
838+ }
839+ } ;
840+
841+ // Fill initial pool up to root test concurrency
842+ // We use root test concurrency here because concurrency logic is handled at test level.
843+ while ( running . size < root . concurrency && index < testFiles . length && ! shouldBail ( ) ) {
844+ enqueueNext ( ) ;
845+ }
846+
847+ // As each test completes, enqueue the next one
848+ while ( running . size > 0 ) {
849+ await SafePromiseRace ( [ ...running ] ) ;
850+
851+ // Refill pool after completion(s)
852+ while ( running . size < root . concurrency && index < testFiles . length && ! shouldBail ( ) ) {
853+ enqueueNext ( ) ;
854+ }
855+ }
856+ } ;
857+
858+ } else {
859+ runFiles = ( ) => {
860+ root . harness . bootstrapPromise = null ;
861+ root . harness . buildPromise = null ;
862+ return SafePromiseAllSettledReturnVoid ( testFiles , ( path ) => {
863+ const subtest = runTestFile ( path , filesWatcher , opts ) ;
864+ filesWatcher ?. runningSubtests . set ( path , subtest ) ;
865+ return subtest ;
866+ } ) ;
867+ } ;
868+ }
804869 } else if ( isolation === 'none' ) {
805870 if ( watch ) {
806871 const absoluteTestFiles = ArrayPrototypeMap ( testFiles , ( file ) => ( isAbsolute ( file ) ? file : resolve ( cwd , file ) ) ) ;
0 commit comments