@@ -22,6 +22,8 @@ const {
2222 SafePromiseAll,
2323 SafePromiseAllReturnVoid,
2424 SafePromiseAllSettledReturnVoid,
25+ SafePromisePrototypeFinally,
26+ SafePromiseRace,
2527 SafeSet,
2628 StringPrototypeIndexOf,
2729 StringPrototypeSlice,
@@ -254,6 +256,7 @@ class FileTest extends Test {
254256 if ( item . data . details ?. error ) {
255257 item . data . details . error = deserializeError ( item . data . details . error ) ;
256258 }
259+
257260 if ( item . type === 'test:pass' || item . type === 'test:fail' ) {
258261 item . data . testNumber = isTopLevel ? ( this . root . harness . counters . topLevel + 1 ) : item . data . testNumber ;
259262 countCompletedTest ( {
@@ -604,6 +607,7 @@ function run(options = kEmptyObject) {
604607 } = options ;
605608 const {
606609 concurrency,
610+ bail,
607611 timeout,
608612 signal,
609613 files,
@@ -747,6 +751,7 @@ function run(options = kEmptyObject) {
747751 functionCoverage : functionCoverage ,
748752 cwd,
749753 globalSetupPath,
754+ bail,
750755 } ;
751756 const root = createTestTree ( rootTestOptions , globalOptions ) ;
752757 let testFiles = files ?? createTestFileList ( globPatterns , cwd ) ;
@@ -756,10 +761,33 @@ function run(options = kEmptyObject) {
756761 testFiles = ArrayPrototypeFilter ( testFiles , ( _ , index ) => index % shard . total === shard . index - 1 ) ;
757762 }
758763
764+ if ( bail ) {
765+ validateBoolean ( bail , 'options.bail' ) ;
766+ if ( watch ) {
767+ throw new ERR_INVALID_ARG_VALUE ( 'options.bail' , watch , 'bail not supported with watch mode' ) ;
768+ }
769+ }
770+
759771 let teardown ;
760772 let postRun ;
761773 let filesWatcher ;
762774 let runFiles ;
775+
776+ if ( bail ) {
777+ root . reporter . on ( 'test:fail' , ( item ) => {
778+ if ( root . harness . bail && ! root . harness . bailedOut ) {
779+ root . harness . bailedOut = true ;
780+ queueMicrotask ( ( ) => {
781+ root . reporter [ kEmitMessage ] ( 'test:bail' , {
782+ __proto__ : null ,
783+ file : item . name ,
784+ test : item . data ,
785+ } ) ;
786+ } ) ;
787+ }
788+ } ) ;
789+ }
790+
763791 const opts = {
764792 __proto__ : null ,
765793 root,
@@ -770,6 +798,7 @@ function run(options = kEmptyObject) {
770798 hasFiles : files != null ,
771799 globPatterns,
772800 only,
801+ bail,
773802 forceExit,
774803 cwd,
775804 isolation,
@@ -792,15 +821,53 @@ function run(options = kEmptyObject) {
792821 teardown = ( ) => root . harness . teardown ( ) ;
793822 }
794823
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- } ;
824+ if ( bail ) {
825+ runFiles = async ( ) => {
826+ root . harness . bootstrapPromise = null ;
827+ root . harness . buildPromise = null ;
828+
829+ const running = new SafeSet ( ) ;
830+ let index = 0 ;
831+
832+ const shouldBail = ( ) => bail && root . harness . bailedOut ;
833+
834+ const enqueueNext = ( ) => {
835+ if ( index < testFiles . length && ! shouldBail ( ) ) {
836+ const path = testFiles [ index ++ ] ;
837+ const subtest = runTestFile ( path , filesWatcher , opts ) ;
838+ filesWatcher ?. runningSubtests . set ( path , subtest ) ;
839+ running . add ( subtest ) ;
840+ SafePromisePrototypeFinally ( subtest , ( ) => running . delete ( subtest ) ) ;
841+ }
842+ } ;
843+
844+ // Fill initial pool up to root test concurrency
845+ // We use root test concurrency here because concurrency logic is handled at test level.
846+ while ( running . size < root . concurrency && index < testFiles . length && ! shouldBail ( ) ) {
847+ enqueueNext ( ) ;
848+ }
849+
850+ // As each test completes, enqueue the next one
851+ while ( running . size > 0 ) {
852+ await SafePromiseRace ( [ ...running ] ) ;
853+
854+ // Refill pool after completion(s)
855+ while ( running . size < root . concurrency && index < testFiles . length && ! shouldBail ( ) ) {
856+ enqueueNext ( ) ;
857+ }
858+ }
859+ } ;
860+ } else {
861+ runFiles = ( ) => {
862+ root . harness . bootstrapPromise = null ;
863+ root . harness . buildPromise = null ;
864+ return SafePromiseAllSettledReturnVoid ( testFiles , ( path ) => {
865+ const subtest = runTestFile ( path , filesWatcher , opts ) ;
866+ filesWatcher ?. runningSubtests . set ( path , subtest ) ;
867+ return subtest ;
868+ } ) ;
869+ } ;
870+ }
804871 } else if ( isolation === 'none' ) {
805872 if ( watch ) {
806873 const absoluteTestFiles = ArrayPrototypeMap ( testFiles , ( file ) => ( isAbsolute ( file ) ? file : resolve ( cwd , file ) ) ) ;
@@ -813,17 +880,21 @@ function run(options = kEmptyObject) {
813880 return subtest ;
814881 } ;
815882 } else {
883+
816884 runFiles = async ( ) => {
817- const { promise, resolve : finishBootstrap } = PromiseWithResolvers ( ) ;
885+ // Ensure global bootstrap is completed before running files, then allow
886+ // subtests to start immediately so bail can stop further file imports.
887+ if ( root . harness . bootstrapPromise ) {
888+ await root . harness . bootstrapPromise ;
889+ root . harness . bootstrapPromise = null ;
890+ }
891+ root . harness . buildPromise = null ;
818892
819893 await root . runInAsyncScope ( async ( ) => {
820894 const parentURL = pathToFileURL ( cwd + sep ) . href ;
821895 const cascadedLoader = esmLoader . getOrInitializeCascadedLoader ( ) ;
822896 let topLevelTestCount = 0 ;
823-
824- root . harness . bootstrapPromise = root . harness . bootstrapPromise ?
825- SafePromiseAllReturnVoid ( [ root . harness . bootstrapPromise , promise ] ) :
826- promise ;
897+ let failedCount = root . harness . counters . failed ;
827898
828899 // We need to setup the user modules in the test runner if we are running with
829900 // --test-isolation=none and --test in order to avoid loading the user modules
@@ -841,7 +912,12 @@ function run(options = kEmptyObject) {
841912 }
842913
843914 for ( let i = 0 ; i < testFiles . length ; ++ i ) {
915+ if ( root . harness . bail && root . harness . bailedOut ) {
916+ break ;
917+ }
918+
844919 const testFile = testFiles [ i ] ;
920+ const testFilePath = resolve ( cwd , testFile ) ;
845921 const fileURL = pathToFileURL ( resolve ( cwd , testFile ) ) ;
846922 const parent = i === 0 ? undefined : parentURL ;
847923 let threw = false ;
@@ -872,18 +948,26 @@ function run(options = kEmptyObject) {
872948 }
873949
874950 topLevelTestCount = root . subtests . length ;
951+
952+ // Ensure any subtest chains spawned by this file are finished.
953+ if ( root . subtestsPromise ?. promise ) {
954+ await root . subtestsPromise . promise ;
955+ }
875956 }
876957 } ) ;
877958
878959 debug ( 'beginning test execution' ) ;
879960 root . entryFile = null ;
880- finishBootstrap ( ) ;
961+ if ( root . harness . bail && root . harness . bailedOut ) {
962+ return ;
963+ }
881964 return root . processPendingSubtests ( ) ;
882965 } ;
883966 }
884967 }
885968
886969 const runChain = async ( ) => {
970+
887971 if ( root . harness ?. bootstrapPromise ) {
888972 await root . harness . bootstrapPromise ;
889973 }
0 commit comments