@@ -87,6 +87,14 @@ lazy_static! {
8787/// source control systems as well.
8888const INDETERMINATE_EXIT_CODE : i32 = 125 ;
8989
90+ /// Similarly to `INDETERMINATE_EXIT_CODE`, this exit code is used officially by
91+ /// `git-bisect` and others to abort the process. It's also typically raised by
92+ /// the shell when the command is not found, so it's technically ambiguous
93+ /// whether the command existed or not. Nonetheless, it's intuitive for a
94+ /// failure to run a given command to abort the process altogether, so it
95+ /// shouldn't be too confusing in practice.
96+ const ABORT_EXIT_CODE : i32 = 127 ;
97+
9098/// How verbose of output to produce.
9199#[ derive( Clone , Copy , Debug , Ord , PartialOrd , Eq , PartialEq ) ]
92100enum Verbosity {
@@ -818,7 +826,7 @@ struct TestOutput {
818826}
819827
820828/// The possible results of attempting to run a test.
821- #[ derive( Debug ) ]
829+ #[ derive( Clone , Debug ) ]
822830enum TestStatus {
823831 /// Attempting to set up the working directory for the repository failed.
824832 CheckoutFailed ,
@@ -840,6 +848,9 @@ enum TestStatus {
840848 /// The test command indicated that the commit should be skipped for testing.
841849 Indeterminate { exit_code : i32 } ,
842850
851+ /// The test command indicated that the process should be aborted entirely.
852+ Abort { exit_code : i32 } ,
853+
843854 /// The test failed and returned the provided (non-zero) exit code.
844855 Failed {
845856 /// Whether or not the result was cached (indicating that we didn't
@@ -881,7 +892,7 @@ impl TestStatus {
881892 | TestStatus :: ReadCacheFailed ( _)
882893 | TestStatus :: TerminatedBySignal
883894 | TestStatus :: Indeterminate { .. } => icons:: EXCLAMATION ,
884- TestStatus :: Failed { .. } => icons:: CROSS ,
895+ TestStatus :: Failed { .. } | TestStatus :: Abort { .. } => icons:: CROSS ,
885896 TestStatus :: Passed { .. } => icons:: CHECKMARK ,
886897 }
887898 }
@@ -895,12 +906,18 @@ impl TestStatus {
895906 | TestStatus :: ReadCacheFailed ( _)
896907 | TestStatus :: TerminatedBySignal
897908 | TestStatus :: Indeterminate { .. } => * STYLE_SKIPPED ,
898- TestStatus :: Failed { .. } => * STYLE_FAILURE ,
909+ TestStatus :: Failed { .. } | TestStatus :: Abort { .. } => * STYLE_FAILURE ,
899910 TestStatus :: Passed { .. } => * STYLE_SUCCESS ,
900911 }
901912 }
902913}
903914
915+ #[ derive( Debug ) ]
916+ struct TestingAbortedError {
917+ commit_oid : NonZeroOid ,
918+ exit_code : i32 ,
919+ }
920+
904921#[ derive( Debug ) ]
905922struct SerializedNonZeroOid ( NonZeroOid ) ;
906923
@@ -969,6 +986,14 @@ fn make_test_status_description(
969986 . append ( commit. friendly_describe ( glyphs) ?)
970987 . build ( ) ,
971988
989+ TestStatus :: Abort { exit_code } => StyledStringBuilder :: new ( )
990+ . append_styled (
991+ format ! ( "Exit code indicated to abort testing (exit code {exit_code}): " ) ,
992+ * STYLE_FAILURE ,
993+ )
994+ . append ( commit. friendly_describe ( glyphs) ?)
995+ . build ( ) ,
996+
972997 TestStatus :: Failed {
973998 cached,
974999 interactive,
@@ -1085,7 +1110,8 @@ impl TestOutput {
10851110 | TestStatus :: TerminatedBySignal
10861111 | TestStatus :: AlreadyInProgress
10871112 | TestStatus :: ReadCacheFailed ( _)
1088- | TestStatus :: Indeterminate { .. } => false ,
1113+ | TestStatus :: Indeterminate { .. }
1114+ | TestStatus :: Abort { .. } => false ,
10891115 TestStatus :: Failed { interactive, .. } | TestStatus :: Passed { interactive, .. } => {
10901116 interactive
10911117 }
@@ -1190,6 +1216,7 @@ impl<'a> search::SearchGraph for SearchGraph<'a> {
11901216struct TestResults {
11911217 search_bounds : search:: Bounds < NonZeroOid > ,
11921218 test_outputs : IndexMap < NonZeroOid , TestOutput > ,
1219+ testing_aborted_error : Option < TestingAbortedError > ,
11931220}
11941221
11951222#[ instrument]
@@ -1264,7 +1291,11 @@ fn run_tests<'a>(
12641291 Some ( TestSearchStrategy :: Binary ) => Some ( search:: Strategy :: Binary ) ,
12651292 } ;
12661293
1267- let ( search, test_outputs_unordered) = {
1294+ let EventLoopOutput {
1295+ search,
1296+ test_outputs : test_outputs_unordered,
1297+ testing_aborted_error,
1298+ } = {
12681299 let ( effects, progress) =
12691300 effects. start_operation ( OperationType :: RunTests ( Arc :: new ( command. clone ( ) ) ) ) ;
12701301 progress. notify_progress ( 0 , commits. len ( ) ) ;
@@ -1376,8 +1407,12 @@ fn run_tests<'a>(
13761407 result_rx,
13771408 ) ?;
13781409
1379- debug ! ( "Waiting for workers" ) ;
13801410 work_queue. close ( ) ;
1411+ if test_results. testing_aborted_error . is_some ( ) {
1412+ return Ok ( test_results) ;
1413+ }
1414+
1415+ debug ! ( "Waiting for workers" ) ;
13811416 progress. notify_status ( OperationIcon :: InProgress , "Waiting for workers" ) ;
13821417 if search_strategy. is_none ( ) {
13831418 for ( worker_id, worker) in workers {
@@ -1406,7 +1441,7 @@ fn run_tests<'a>(
14061441 test_outputs_ordered. insert ( commit_oid, result) ;
14071442 }
14081443 None => {
1409- if search_strategy. is_none ( ) {
1444+ if search_strategy. is_none ( ) && testing_aborted_error . is_none ( ) {
14101445 warn ! ( ?commit_oid, "No result was returned for commit" ) ;
14111446 }
14121447 }
@@ -1428,18 +1463,26 @@ fn run_tests<'a>(
14281463 Some ( search_strategy) => search. search ( search_strategy) ?. bounds ,
14291464 } ,
14301465 test_outputs : test_outputs_ordered,
1466+ testing_aborted_error,
14311467 } ) )
14321468}
14331469
1470+ struct EventLoopOutput < ' a > {
1471+ search : search:: Search < SearchGraph < ' a > > ,
1472+ test_outputs : HashMap < NonZeroOid , TestOutput > ,
1473+ testing_aborted_error : Option < TestingAbortedError > ,
1474+ }
1475+
14341476fn event_loop (
14351477 commit_jobs : IndexMap < NonZeroOid , TestJob > ,
14361478 mut search : search:: Search < SearchGraph > ,
14371479 search_strategy : Option < search:: Strategy > ,
14381480 num_jobs : usize ,
14391481 work_queue : WorkQueue < TestJob > ,
14401482 result_rx : Receiver < JobResult < TestJob , TestOutput > > ,
1441- ) -> eyre:: Result < ( search :: Search < SearchGraph > , HashMap < NonZeroOid , TestOutput > ) > {
1483+ ) -> eyre:: Result < EventLoopOutput > {
14421484 let mut test_outputs = HashMap :: new ( ) ;
1485+ let mut testing_aborted_error = None ;
14431486 while let Ok ( message) = {
14441487 debug ! ( "Main thread waiting for new job result" ) ;
14451488 let result = if commit_jobs. is_empty ( ) {
@@ -1456,32 +1499,42 @@ fn event_loop(
14561499 commit_oid,
14571500 operation_type : _,
14581501 } = job;
1459- search. notify (
1460- commit_oid,
1461- match & test_output. test_status {
1462- TestStatus :: CheckoutFailed
1463- | TestStatus :: SpawnTestFailed ( _)
1464- | TestStatus :: TerminatedBySignal
1465- | TestStatus :: AlreadyInProgress
1466- | TestStatus :: ReadCacheFailed ( _)
1467- | TestStatus :: Indeterminate { .. } => search:: Status :: Indeterminate ,
1468-
1469- TestStatus :: Failed {
1470- cached : _,
1471- interactive : _,
1472- exit_code : _,
1473- } => search:: Status :: Failure ,
1474-
1475- TestStatus :: Passed {
1476- cached : _,
1477- interactive : _,
1478- fixed_tree_oid : _,
1479- } => search:: Status :: Success ,
1480- } ,
1481- ) ?;
1502+ let ( maybe_testing_aborted_error, search_status) = match & test_output. test_status {
1503+ TestStatus :: CheckoutFailed
1504+ | TestStatus :: SpawnTestFailed ( _)
1505+ | TestStatus :: TerminatedBySignal
1506+ | TestStatus :: AlreadyInProgress
1507+ | TestStatus :: ReadCacheFailed ( _)
1508+ | TestStatus :: Indeterminate { .. } => ( None , search:: Status :: Indeterminate ) ,
1509+
1510+ TestStatus :: Abort { exit_code } => (
1511+ Some ( TestingAbortedError {
1512+ commit_oid,
1513+ exit_code : * exit_code,
1514+ } ) ,
1515+ search:: Status :: Indeterminate ,
1516+ ) ,
1517+
1518+ TestStatus :: Failed {
1519+ cached : _,
1520+ interactive : _,
1521+ exit_code : _,
1522+ } => ( None , search:: Status :: Failure ) ,
1523+
1524+ TestStatus :: Passed {
1525+ cached : _,
1526+ interactive : _,
1527+ fixed_tree_oid : _,
1528+ } => ( None , search:: Status :: Success ) ,
1529+ } ;
1530+ search. notify ( commit_oid, search_status) ?;
14821531 test_outputs. insert ( commit_oid, test_output) ;
1532+ if let Some ( err) = maybe_testing_aborted_error {
1533+ testing_aborted_error = Some ( err) ;
1534+ break ;
1535+ }
14831536
1484- let should_exit = match search_strategy {
1537+ let search_completed = match search_strategy {
14851538 None => test_outputs. len ( ) == commit_jobs. len ( ) ,
14861539 Some ( search_strategy) => {
14871540 let solution = search. search ( search_strategy) ?;
@@ -1490,13 +1543,12 @@ fn event_loop(
14901543 . take ( num_jobs)
14911544 . map ( |commit_oid| commit_jobs[ & commit_oid] . clone ( ) )
14921545 . collect_vec ( ) ;
1493- let should_exit = next_to_search. is_empty ( ) ;
1546+ let search_completed = next_to_search. is_empty ( ) ;
14941547 work_queue. set ( next_to_search) ;
1495- should_exit
1548+ search_completed
14961549 }
14971550 } ;
1498- if should_exit {
1499- drop ( result_rx) ;
1551+ if search_completed {
15001552 break ;
15011553 }
15021554 }
@@ -1510,7 +1562,11 @@ fn event_loop(
15101562 }
15111563 }
15121564
1513- Ok ( ( search, test_outputs) )
1565+ Ok ( EventLoopOutput {
1566+ search,
1567+ test_outputs,
1568+ testing_aborted_error,
1569+ } )
15141570}
15151571
15161572#[ instrument]
@@ -1545,6 +1601,9 @@ fn print_summary(
15451601 | TestStatus :: TerminatedBySignal
15461602 | TestStatus :: Indeterminate { .. } => num_skipped += 1 ,
15471603
1604+ TestStatus :: Abort { .. } => {
1605+ num_failed += 1 ;
1606+ }
15481607 TestStatus :: Failed {
15491608 cached,
15501609 exit_code : _,
@@ -1682,6 +1741,23 @@ fn print_summary(
16821741 print_hint_suppression_notice ( effects, Hint :: CleanCachedTestResults ) ?;
16831742 }
16841743
1744+ if let Some ( testing_aborted_error) = & test_results. testing_aborted_error {
1745+ let TestingAbortedError {
1746+ commit_oid,
1747+ exit_code,
1748+ } = testing_aborted_error;
1749+ let commit = repo. find_commit_or_fail ( * commit_oid) ?;
1750+ writeln ! (
1751+ effects. get_output_stream( ) ,
1752+ "Aborted testing with exit code {} at commit: {}" ,
1753+ exit_code,
1754+ effects
1755+ . get_glyphs( )
1756+ . render( commit. friendly_describe( effects. get_glyphs( ) ) ?) ?
1757+ ) ?;
1758+ return Ok ( ExitCode ( 1 ) ) ;
1759+ }
1760+
16851761 if is_search {
16861762 Ok ( ExitCode ( 0 ) )
16871763 } else if num_failed > 0 || num_skipped > 0 {
@@ -1725,7 +1801,8 @@ fn apply_fixes(
17251801 | TestStatus :: AlreadyInProgress
17261802 | TestStatus :: ReadCacheFailed ( _)
17271803 | TestStatus :: Indeterminate { .. }
1728- | TestStatus :: Failed { .. } => None ,
1804+ | TestStatus :: Failed { .. }
1805+ | TestStatus :: Abort { .. } => None ,
17291806 } )
17301807 . collect ( ) ;
17311808
@@ -2064,7 +2141,9 @@ fn run_test(
20642141 | TestStatus :: ReadCacheFailed ( _)
20652142 | TestStatus :: Indeterminate { .. } => OperationIcon :: Warning ,
20662143
2067- TestStatus :: TerminatedBySignal | TestStatus :: Failed { .. } => OperationIcon :: Failure ,
2144+ TestStatus :: TerminatedBySignal
2145+ | TestStatus :: Failed { .. }
2146+ | TestStatus :: Abort { .. } => OperationIcon :: Failure ,
20682147
20692148 TestStatus :: Passed { .. } => OperationIcon :: Success ,
20702149 } ,
@@ -2155,6 +2234,13 @@ fn make_test_files(
21552234 TestStatus :: Indeterminate { exit_code }
21562235 }
21572236
2237+ Ok ( SerializedTestResult {
2238+ command : _,
2239+ exit_code,
2240+ fixed_tree_oid : _,
2241+ interactive : _,
2242+ } ) if exit_code == ABORT_EXIT_CODE => TestStatus :: Abort { exit_code } ,
2243+
21582244 Ok ( SerializedTestResult {
21592245 command : _,
21602246 exit_code,
@@ -2371,17 +2457,35 @@ fn test_commit(
23712457 let exit125 = effects
23722458 . get_glyphs ( )
23732459 . render ( StyledString :: styled ( "exit 125" , * STYLE_SKIPPED ) ) ?;
2460+ let exit127 = effects
2461+ . get_glyphs ( )
2462+ . render ( StyledString :: styled ( "exit 127" , * STYLE_FAILURE ) ) ?;
23742463
23752464 // NB: use `println` here instead of
23762465 // `writeln!(effects.get_output_stream(), ...)` because the effects are
23772466 // suppressed in interactive mode.
23782467 println ! (
23792468 "\
23802469 You are now at: {commit_desc}
2381- To mark this commit as {passed}, run: {exit0}
2382- To mark this commit as {failed}, run: {exit1}
2383- To mark this commit as {skipped}, run: {exit125}" ,
2470+ To mark this commit as {passed},run: {exit0}
2471+ To mark this commit as {failed}, run: {exit1}
2472+ To mark this commit as {skipped}, run: {exit125}
2473+ To abort testing entirely, run: {exit127}" ,
23842474 ) ;
2475+ match options. execution_strategy {
2476+ TestExecutionStrategy :: WorkingCopy => { }
2477+ TestExecutionStrategy :: Worktree => {
2478+ let warning = effects
2479+ . get_glyphs ( )
2480+ . render ( StyledString :: styled (
2481+ "Warning: You are in a worktree. Your changes will not be propagated between the worktree and the main repository." ,
2482+ * STYLE_SKIPPED
2483+ ) ) ?;
2484+ println ! ( "{warning}" ) ;
2485+ println ! ( "To save your changes, create a new branch or note the commit hash." ) ;
2486+ println ! ( "To incorporate the changes from the main repository, switch to the main repository's current commit or branch." ) ;
2487+ }
2488+ }
23852489 } else {
23862490 command
23872491 . stdin ( Stdio :: null ( ) )
@@ -2463,6 +2567,7 @@ To mark this commit as {skipped}, run: {exit125}",
24632567 }
24642568
24652569 exit_code @ INDETERMINATE_EXIT_CODE => TestStatus :: Indeterminate { exit_code } ,
2570+ exit_code @ ABORT_EXIT_CODE => TestStatus :: Abort { exit_code } ,
24662571
24672572 exit_code => TestStatus :: Failed {
24682573 cached : false ,
@@ -2486,6 +2591,7 @@ To mark this commit as {skipped}, run: {exit125}",
24862591 | TestStatus :: AlreadyInProgress
24872592 | TestStatus :: ReadCacheFailed ( _)
24882593 | TestStatus :: Failed { .. }
2594+ | TestStatus :: Abort { .. }
24892595 | TestStatus :: Indeterminate { .. } => None ,
24902596 } ,
24912597 interactive : options. interactive ,
0 commit comments