3030import sys
3131import unittest
3232import time
33+ from functools import cmp_to_key
3334
3435# Setup
3536
@@ -270,8 +271,84 @@ def error_on_legacy_suite_names(args):
270271 utils .exit_with_error ('`%s` test suite has been replaced with `%s`' , a , new )
271272
272273
273- def load_test_suites (args , modules , start_at , repeat ):
274- found_start = not start_at
274+ # Creates a sorter object that sorts the test run order to find the best possible
275+ # order to run the tests in. Generally this is slowest-first to maximize
276+ # parallelization, but if running with fail-fast, then the tests with recent
277+ # known failure frequency are run first, followed by slowest first.
278+ def create_test_run_sorter (failfast ):
279+ previous_test_run_results = common .load_previous_test_run_results ()
280+
281+ def read_approx_fail_freq (test_name ):
282+ if test_name in previous_test_run_results and 'fail_frequency' in previous_test_run_results [test_name ]:
283+ # Quantize the float value to relatively fine-grained buckets for sorting.
284+ # This bucketization is needed to merge two competing sorting goals: we may
285+ # want to fail early (so tests with previous history of failures should sort first)
286+ # but we also want to run the slowest tests first.
287+ # We cannot sort for both goals at the same time, so have failure frequency
288+ # take priority over test runtime, and quantize the failures to distinct
289+ # frequencies, to be able to then sort by test runtime inside the same failure
290+ # frequency bucket.
291+ NUM_BUCKETS = 20
292+ return round (previous_test_run_results [test_name ]['fail_frequency' ] * NUM_BUCKETS ) / NUM_BUCKETS
293+ return 0
294+
295+ def sort_tests_failing_and_slowest_first_comparator (x , y ):
296+ x = str (x )
297+ y = str (y )
298+
299+ # Look at the number of times this test has failed, and order by failures count first
300+ # Only do this in --failfast, if we are looking to fail early. (otherwise sorting by last test run duration is more productive)
301+ if failfast :
302+ x_fail_freq = read_approx_fail_freq (x )
303+ y_fail_freq = read_approx_fail_freq (y )
304+ if x_fail_freq != y_fail_freq :
305+ return y_fail_freq - x_fail_freq
306+
307+ # Look at the number of times this test has failed overall in any other suite, and order by failures count first
308+ x_fail_freq = read_approx_fail_freq (x .split (' ' )[0 ])
309+ y_fail_freq = read_approx_fail_freq (y .split (' ' )[0 ])
310+ if x_fail_freq != y_fail_freq :
311+ return y_fail_freq - x_fail_freq
312+
313+ if x in previous_test_run_results :
314+ X = previous_test_run_results [x ]
315+
316+ # if test Y has not been run even once, run Y before X
317+ if y not in previous_test_run_results :
318+ return 1
319+ Y = previous_test_run_results [y ]
320+
321+ # If both X and Y have been run before, order the tests based on what the previous result was (failures first, skips very last)
322+ # N.b. it is important to sandwich all skipped tests between fails and successes. This is to maximize the chances that when
323+ # a failing test is detected, then the other cores will fail-fast as well. (successful tests are run slowest-first to help
324+ # scheduling)
325+ order_by_result = {'errored' : 0 , 'failed' : 1 , 'expected failure' : 2 , 'unexpected success' : 3 , 'skipped' : 4 , 'success' : 5 }
326+ x_result = order_by_result [X ['result' ]]
327+ y_result = order_by_result [Y ['result' ]]
328+ if x_result != y_result :
329+ return x_result - y_result
330+
331+ # Finally, order by test duration from last run
332+ if X ['duration' ] != Y ['duration' ]:
333+ if X ['result' ] == 'success' :
334+ # If both tests were successful tests, run the slower test first to improve parallelism
335+ return Y ['duration' ] - X ['duration' ]
336+ else :
337+ # If both tests were failing tests, run the quicker test first to improve --failfast detection time
338+ return X ['duration' ] - Y ['duration' ]
339+
340+ # if test X has not been run even once, but Y has, run X before Y
341+ if y in previous_test_run_results :
342+ return - 1
343+
344+ # Neither test have been run before, so run them in alphabetical order
345+ return (x > y ) - (x < y )
346+
347+ return sort_tests_failing_and_slowest_first_comparator
348+
349+
350+ def load_test_suites (args , modules , options ):
351+ found_start = not options .start_at
275352
276353 loader = unittest .TestLoader ()
277354 error_on_legacy_suite_names (args )
@@ -291,20 +368,22 @@ def load_test_suites(args, modules, start_at, repeat):
291368 if names_in_module :
292369 loaded_tests = loader .loadTestsFromNames (sorted (names_in_module ), m )
293370 tests = flattened_tests (loaded_tests )
294- suite = suite_for_module (m , tests )
371+ suite = suite_for_module (m , tests , options )
372+ if options .failing_and_slow_first :
373+ tests = sorted (tests , key = cmp_to_key (create_test_run_sorter (options .failfast )))
295374 for test in tests :
296375 if not found_start :
297376 # Skip over tests until we find the start
298- if test .id ().endswith (start_at ):
377+ if test .id ().endswith (options . start_at ):
299378 found_start = True
300379 else :
301380 continue
302- for _x in range (repeat ):
381+ for _x in range (options . repeat ):
303382 total_tests += 1
304383 suite .addTest (test )
305384 suites .append ((m .__name__ , suite ))
306385 if not found_start :
307- utils .exit_with_error (f'unable to find --start-at test: { start_at } ' )
386+ utils .exit_with_error (f'unable to find --start-at test: { options . start_at } ' )
308387 if total_tests == 1 or parallel_testsuite .num_cores () == 1 :
309388 # TODO: perhaps leave it at 2 if it was 2 before?
310389 common .EMTEST_SAVE_DIR = 1
@@ -318,13 +397,13 @@ def flattened_tests(loaded_tests):
318397 return tests
319398
320399
321- def suite_for_module (module , tests ):
400+ def suite_for_module (module , tests , options ):
322401 suite_supported = module .__name__ in ('test_core' , 'test_other' , 'test_posixtest' )
323402 if not common .EMTEST_SAVE_DIR and not shared .DEBUG :
324403 has_multiple_tests = len (tests ) > 1
325404 has_multiple_cores = parallel_testsuite .num_cores () > 1
326405 if suite_supported and has_multiple_tests and has_multiple_cores :
327- return parallel_testsuite .ParallelTestSuite (len (tests ))
406+ return parallel_testsuite .ParallelTestSuite (len (tests ), options )
328407 return unittest .TestSuite ()
329408
330409
@@ -398,6 +477,7 @@ def parse_args():
398477 help = 'Use the default CI browser configuration.' )
399478 parser .add_argument ('tests' , nargs = '*' )
400479 parser .add_argument ('--failfast' , action = 'store_true' )
480+ parser .add_argument ('--failing-and-slow-first' , action = 'store_true' , help = 'Run failing tests first, then sorted by slowest first. Combine with --failfast for fast fail-early CI runs.' )
401481 parser .add_argument ('--start-at' , metavar = 'NAME' , help = 'Skip all tests up until <NAME>' )
402482 parser .add_argument ('--continue' , dest = '_continue' , action = 'store_true' ,
403483 help = 'Resume from the last run test.'
@@ -496,7 +576,7 @@ def prepend_default(arg):
496576 if os .path .exists (common .LAST_TEST ):
497577 options .start_at = utils .read_file (common .LAST_TEST ).strip ()
498578
499- suites , unmatched_tests = load_test_suites (tests , modules , options . start_at , options . repeat )
579+ suites , unmatched_tests = load_test_suites (tests , modules , options )
500580 if unmatched_tests :
501581 print ('ERROR: could not find the following tests: ' + ' ' .join (unmatched_tests ))
502582 return 1
0 commit comments