22from concurrent .futures import Future
33from contextlib import contextmanager
44from crystal .app_preferences import app_prefs
5- from crystal .tests .runner .shared import available_modules_str , normalize_test_names
5+ from crystal .tests .runner .shared import MAX_INTERRUPTED_TEST_COUNT_TO_REPORT , available_modules_str , normalize_test_names
66from crystal .tests .util .downloads import delay_between_downloads_minimized
77from crystal .tests .util .runner import run_test
88from crystal .tests .util .subtests import SubtestFailed
3030
3131
3232@bg_affinity
33- def run_tests (raw_test_names : list [str ], * , interactive : bool = False ) -> bool :
33+ def run_tests (raw_test_names : list [str ], * , interactive : bool = False , maxfail : int | None = None ) -> bool :
3434 """
3535 Runs automated UI tests, printing a summary report,
3636 and returning whether the run was OK.
@@ -54,10 +54,10 @@ def run_tests(raw_test_names: list[str], *, interactive: bool = False) -> bool:
5454 else :
5555 test_names = [] # ignored
5656
57- return _run_tests (test_names , interactive = interactive )
57+ return _run_tests (test_names , interactive = interactive , maxfail = maxfail )
5858
5959
60- def _run_tests (test_names : list [str ], * , interactive : bool = False ) -> bool :
60+ def _run_tests (test_names : list [str ], * , interactive : bool = False , maxfail : int | None = None ) -> bool :
6161 from crystal .tests .index import TEST_FUNCS
6262
6363 # Ensure ancestor caller did already call set_tests_are_running()
@@ -83,6 +83,7 @@ def _run_tests(test_names: list[str], *, interactive: bool = False) -> bool:
8383 test_func_by_name [test_name ] = test_func
8484
8585 # Interactive mode: read test names from stdin one at a time
86+ fail_count = 0 # for --maxfail tracking
8687 try :
8788 while True :
8889 # Print prompt
@@ -152,6 +153,15 @@ def _run_tests(test_names: list[str], *, interactive: bool = False) -> bool:
152153 except NoForegroundThreadError :
153154 # Fatal error; abort
154155 break
156+
157+ # Check if --maxfail threshold has been reached
158+ if maxfail is not None :
159+ last_result = result_for_test_func_id .get (test_func_id )
160+ if (last_result is not None and
161+ not isinstance (last_result , (SkipTest , _TestInterrupted ))):
162+ fail_count += 1
163+ if fail_count >= maxfail :
164+ break
155165 except KeyboardInterrupt :
156166 # Proceed to print a summary section, and exit the process
157167 pass
@@ -166,9 +176,16 @@ def _run_tests(test_names: list[str], *, interactive: bool = False) -> bool:
166176 if test_name not in test_names and test_func .__module__ not in test_names :
167177 continue
168178 test_funcs_to_run .append (test_func )
169-
179+
180+ def mark_remaining_tests_as_interrupted () -> None :
181+ for remaining_test_func in test_funcs_to_run [test_func_index :]:
182+ remaining_test_func_id = (remaining_test_func .__module__ , remaining_test_func .__name__ )
183+ if remaining_test_func_id not in result_for_test_func_id :
184+ result_for_test_func_id [remaining_test_func_id ] = _TestInterrupted ()
185+
170186 num_test_funcs_to_run = len (test_funcs_to_run ) # cache
171187
188+ fail_count = 0 # for --maxfail tracking
172189 try :
173190 for (test_func_index , test_func ) in enumerate (test_funcs_to_run ):
174191 test_func_id = (test_func .__module__ , test_func .__name__ )
@@ -196,12 +213,18 @@ def _run_tests(test_names: list[str], *, interactive: bool = False) -> bool:
196213 except NoForegroundThreadError :
197214 # Fatal error; abort
198215 break
216+
217+ # Check if --maxfail threshold has been reached
218+ if maxfail is not None :
219+ last_result = result_for_test_func_id .get (test_func_id )
220+ if (last_result is not None and
221+ not isinstance (last_result , (SkipTest , _TestInterrupted ))):
222+ fail_count += 1
223+ if fail_count >= maxfail :
224+ mark_remaining_tests_as_interrupted ()
225+ break
199226 except KeyboardInterrupt :
200- # Mark all remaining tests as interrupted
201- for remaining_test_func in test_funcs_to_run [test_func_index :]:
202- remaining_test_func_id = (remaining_test_func .__module__ , remaining_test_func .__name__ )
203- if remaining_test_func_id not in result_for_test_func_id :
204- result_for_test_func_id [remaining_test_func_id ] = _TestInterrupted ()
227+ mark_remaining_tests_as_interrupted ()
205228
206229 # Proceed to print a summary section, and exit the process
207230 pass
@@ -314,7 +337,10 @@ def _run_tests(test_names: list[str], *, interactive: bool = False) -> bool:
314337 if len (interrupted_test_names ) != 0 :
315338 print ()
316339 print ('Rerun interrupted tests with:' )
317- print (f'$ crystal test { " " .join (interrupted_test_names )} ' )
340+ if len (interrupted_test_names ) < MAX_INTERRUPTED_TEST_COUNT_TO_REPORT :
341+ print (f'$ crystal test { " " .join (interrupted_test_names )} ' )
342+ else :
343+ print (f'$ crystal test <{ len (interrupted_test_names )} tests>' )
318344
319345 # Play bell sound in terminal
320346 print ('\a ' , end = '' , flush = True )
0 commit comments