2525from contextlib import contextmanager
2626from dataclasses import asdict , dataclass
2727from datetime import datetime
28+ from enum import Enum
2829from pathlib import Path
2930from typing import Dict , List , Optional , Tuple
3031
@@ -43,6 +44,14 @@ class TestConstants:
4344 FAILED_TEST_PATTERN = r"--- FAIL: kuttl/harness/([^\s]+)"
4445
4546
47+ class InitOutcome (Enum ):
48+ """Outcome of the initialization phase, i.e. what the retry logic should do next."""
49+
50+ RETRY = "retry" # There are failed tests to retry
51+ DONE = "done" # Nothing left to do, the run succeeded
52+ ABORTED = "aborted" # The run could not be completed (setup/infrastructure error)
53+
54+
4655@dataclass
4756class RuntimeHistory :
4857 """Tracks runtime history for a specific test."""
@@ -1047,15 +1056,15 @@ def create_test_summary(
10471056 self .test_summaries [test_name ] = summary
10481057 return summary
10491058
1050- def initialize_run_mode (self ) -> bool :
1059+ def initialize_run_mode (self ) -> InitOutcome :
10511060 """Initialize the test run based on mode (resume, rerun, or fresh)."""
10521061 # Check if we're resuming from a previous state
10531062 if self .failed_tests : # This will be populated if we loaded state
10541063 print ("Resuming from previous state..." )
10551064 print (f"Found { len (self .failed_tests )} failed tests to continue with:" )
10561065 for i , test in enumerate (self .failed_tests , 1 ):
10571066 print (f" { i } . { test } " )
1058- return True
1067+ return InitOutcome . RETRY
10591068
10601069 # Check if we're rerunning only failed tests
10611070 elif self .args .rerun_failed :
@@ -1066,7 +1075,7 @@ def initialize_run_mode(self) -> bool:
10661075 # Run initial test suite
10671076 return self ._run_initial_test_suite ()
10681077
1069- def _load_failed_tests_for_rerun (self ) -> bool :
1078+ def _load_failed_tests_for_rerun (self ) -> InitOutcome :
10701079 """Load failed tests for rerun mode."""
10711080 # Load failed tests from the specified state file
10721081 failed_tests = self .state_manager .load_failed_tests_from_state (
@@ -1075,17 +1084,17 @@ def _load_failed_tests_for_rerun(self) -> bool:
10751084
10761085 if not failed_tests :
10771086 print ("❌ No failed tests found to rerun" )
1078- return False
1087+ return InitOutcome . ABORTED
10791088
10801089 print (f"Found { len (failed_tests )} failed tests to rerun:" )
10811090 for i , test in enumerate (failed_tests , 1 ):
10821091 print (f" { i } . { test } " )
10831092
10841093 # Store failed tests for the retry process
10851094 self .failed_tests = failed_tests
1086- return True
1095+ return InitOutcome . RETRY
10871096
1088- def _run_initial_test_suite (self ) -> bool :
1097+ def _run_initial_test_suite (self ) -> InitOutcome :
10891098 """Run the initial test suite."""
10901099 print ("\n Step 1: Running initial full test suite..." )
10911100 initial_result = self .test_executor .run_single_test_suite (
@@ -1095,7 +1104,7 @@ def _run_initial_test_suite(self) -> bool:
10951104 if initial_result .success :
10961105 print (" All tests passed on initial run!" )
10971106 self .report_generator .generate_and_save_final_report (self , self .start_time )
1098- return False # No need to continue
1107+ return InitOutcome . DONE # No need to continue
10991108
11001109 # Parse failed tests
11011110 print ("\n Step 2: Parsing failed tests..." )
@@ -1104,15 +1113,15 @@ def _run_initial_test_suite(self) -> bool:
11041113 )
11051114
11061115 if not failed_tests :
1107- print (" No failed tests found in output but run-tests exited with code 1" )
1116+ print ("❌ No failed tests found in output but run-tests exited with code 1" )
11081117 print (
11091118 " This indicates an infrastructure or setup issue that prevents tests from running"
11101119 )
11111120 print (
11121121 " Check the log file for connection errors, missing dependencies, or cluster issues"
11131122 )
11141123 print (f" Log file: { initial_result .log_file } " )
1115- return False
1124+ return InitOutcome . ABORTED
11161125
11171126 print (f" Found { len (failed_tests )} failed tests:" )
11181127 for i , test in enumerate (failed_tests , 1 ):
@@ -1121,7 +1130,7 @@ def _run_initial_test_suite(self) -> bool:
11211130 # Store failed tests and initial result log for state persistence
11221131 self .failed_tests = failed_tests
11231132 self .initial_result_log = initial_result .log_file
1124- return True
1133+ return InitOutcome . RETRY
11251134
11261135 def execute_parallel_retries (self ) -> Dict [str , List [TestResult ]]:
11271136 """Execute parallel retry attempts for failed tests."""
@@ -1263,8 +1272,14 @@ def run(self) -> int:
12631272 print ("=" * 46 )
12641273
12651274 # Initialize based on run mode
1266- if not self .initialize_run_mode ():
1267- return 0 # Early exit (e.g., all tests passed initially)
1275+ outcome = self .initialize_run_mode ()
1276+ if outcome is InitOutcome .DONE :
1277+ return 0 # All tests passed initially
1278+ if outcome is InitOutcome .ABORTED :
1279+ # The test run never got to a point where retrying makes sense
1280+ # (e.g. release installation or test generation failed).
1281+ print ("\n ❌ Test run aborted, see above for details." )
1282+ return 1
12681283
12691284 # Execute test retries
12701285 if not self .execute_test_retries ():
0 commit comments