30
30
import sys
31
31
import unittest
32
32
import time
33
+ from functools import cmp_to_key
33
34
34
35
# Setup
35
36
@@ -270,8 +271,84 @@ def error_on_legacy_suite_names(args):
270
271
utils .exit_with_error ('`%s` test suite has been replaced with `%s`' , a , new )
271
272
272
273
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
275
352
276
353
loader = unittest .TestLoader ()
277
354
error_on_legacy_suite_names (args )
@@ -291,20 +368,22 @@ def load_test_suites(args, modules, start_at, repeat):
291
368
if names_in_module :
292
369
loaded_tests = loader .loadTestsFromNames (sorted (names_in_module ), m )
293
370
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 )))
295
374
for test in tests :
296
375
if not found_start :
297
376
# Skip over tests until we find the start
298
- if test .id ().endswith (start_at ):
377
+ if test .id ().endswith (options . start_at ):
299
378
found_start = True
300
379
else :
301
380
continue
302
- for _x in range (repeat ):
381
+ for _x in range (options . repeat ):
303
382
total_tests += 1
304
383
suite .addTest (test )
305
384
suites .append ((m .__name__ , suite ))
306
385
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 } ' )
308
387
if total_tests == 1 or parallel_testsuite .num_cores () == 1 :
309
388
# TODO: perhaps leave it at 2 if it was 2 before?
310
389
common .EMTEST_SAVE_DIR = 1
@@ -318,13 +397,13 @@ def flattened_tests(loaded_tests):
318
397
return tests
319
398
320
399
321
- def suite_for_module (module , tests ):
400
+ def suite_for_module (module , tests , options ):
322
401
suite_supported = module .__name__ in ('test_core' , 'test_other' , 'test_posixtest' )
323
402
if not common .EMTEST_SAVE_DIR and not shared .DEBUG :
324
403
has_multiple_tests = len (tests ) > 1
325
404
has_multiple_cores = parallel_testsuite .num_cores () > 1
326
405
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 )
328
407
return unittest .TestSuite ()
329
408
330
409
@@ -398,6 +477,7 @@ def parse_args():
398
477
help = 'Use the default CI browser configuration.' )
399
478
parser .add_argument ('tests' , nargs = '*' )
400
479
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.' )
401
481
parser .add_argument ('--start-at' , metavar = 'NAME' , help = 'Skip all tests up until <NAME>' )
402
482
parser .add_argument ('--continue' , dest = '_continue' , action = 'store_true' ,
403
483
help = 'Resume from the last run test.'
@@ -496,7 +576,7 @@ def prepend_default(arg):
496
576
if os .path .exists (common .LAST_TEST ):
497
577
options .start_at = utils .read_file (common .LAST_TEST ).strip ()
498
578
499
- suites , unmatched_tests = load_test_suites (tests , modules , options . start_at , options . repeat )
579
+ suites , unmatched_tests = load_test_suites (tests , modules , options )
500
580
if unmatched_tests :
501
581
print ('ERROR: could not find the following tests: ' + ' ' .join (unmatched_tests ))
502
582
return 1
0 commit comments