-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Implement a new --failing-and-slow-first command line argument to test runner. #24624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
be281b2
1008e6d
28e4ab8
a8544b2
78aa3fb
1fd0b64
b3cec56
06420cf
15cb203
846455f
ddcde4f
01df1ad
64b09c7
8f63c9d
3d85b01
d297ff1
797c5f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| # University of Illinois/NCSA Open Source License. Both these licenses can be | ||
| # found in the LICENSE file. | ||
|
|
||
| import json | ||
| import multiprocessing | ||
| import os | ||
| import sys | ||
|
|
@@ -19,7 +20,12 @@ | |
| seen_class = set() | ||
|
|
||
|
|
||
| def run_test(test): | ||
| def run_test(test, failfast_event): | ||
| # If failfast mode is in effect and any of the tests have failed, | ||
| # and then we should abort executing further tests immediately. | ||
| if failfast_event and failfast_event.is_set(): | ||
| return None | ||
|
|
||
| olddir = os.getcwd() | ||
| result = BufferedParallelTestResult() | ||
| temp_dir = tempfile.mkdtemp(prefix='emtest_') | ||
|
|
@@ -29,10 +35,16 @@ def run_test(test): | |
| seen_class.add(test.__class__) | ||
| test.__class__.setUpClass() | ||
| test(result) | ||
|
|
||
| # Alert all other multiprocess pool runners that they need to stop executing further tests. | ||
| if failfast_event is not None and result.test_result not in ['success', 'skipped']: | ||
| failfast_event.set() | ||
| except unittest.SkipTest as e: | ||
| result.addSkip(test, e) | ||
| except Exception as e: | ||
| result.addError(test, e) | ||
| if failfast_event is not None: | ||
| failfast_event.set() | ||
| # Before attempting to delete the tmp dir make sure the current | ||
| # working directory is not within it. | ||
| os.chdir(olddir) | ||
|
|
@@ -46,9 +58,11 @@ class ParallelTestSuite(unittest.BaseTestSuite): | |
| Creates worker threads, manages the task queue, and combines the results. | ||
| """ | ||
|
|
||
| def __init__(self, max_cores): | ||
| def __init__(self, max_cores, options): | ||
| super().__init__() | ||
| self.max_cores = max_cores | ||
| self.failfast = options.failfast | ||
| self.failing_and_slow_first = options.failing_and_slow_first | ||
|
|
||
| def addTest(self, test): | ||
| super().addTest(test) | ||
|
|
@@ -61,12 +75,42 @@ def run(self, result): | |
| # inherited by the child process, but can lead to hard-to-debug windows-only | ||
| # issues. | ||
| # multiprocessing.set_start_method('spawn') | ||
| tests = list(self.reversed_tests()) | ||
|
|
||
| # If we are running with --failing-and-slow-first, then the test list has been | ||
| # pre-sorted based on previous test run results. Otherwise run the tests in | ||
| # reverse alphabetical order. | ||
| tests = list(self if self.failing_and_slow_first else self.reversed_tests()) | ||
| use_cores = cap_max_workers_in_pool(min(self.max_cores, len(tests), num_cores())) | ||
| print('Using %s parallel test processes' % use_cores) | ||
| pool = multiprocessing.Pool(use_cores) | ||
| results = [pool.apply_async(run_test, (t,)) for t in tests] | ||
| results = [r.get() for r in results] | ||
| with multiprocessing.Manager() as manager: | ||
| pool = multiprocessing.Pool(use_cores) | ||
| failfast_event = manager.Event() if self.failfast else None | ||
| results = [pool.apply_async(run_test, (t, failfast_event)) for t in tests] | ||
| results = [r.get() for r in results] | ||
| results = [r for r in results if r is not None] | ||
|
|
||
| if self.failing_and_slow_first: | ||
| previous_test_run_results = common.load_previous_test_run_results() | ||
| for r in results: | ||
| # Save a test result record with the specific suite name (e.g. "core0.test_foo") | ||
| test_failed = r.test_result not in ['success', 'skipped'] | ||
|
|
||
| def update_test_results_to(test_name): | ||
| fail_frequency = previous_test_run_results[test_name]['fail_frequency'] if test_name in previous_test_run_results else int(test_failed) | ||
| # Apply exponential moving average with 50% weighting to merge previous fail frequency with new fail frequency | ||
| fail_frequency = (fail_frequency + int(test_failed)) / 2 | ||
| previous_test_run_results[test_name] = { | ||
| 'result': r.test_result, | ||
| 'duration': r.test_duration, | ||
| 'fail_frequency': fail_frequency, | ||
| } | ||
|
|
||
| update_test_results_to(r.test_name) | ||
| # Also save a test result record without suite name (e.g. just "test_foo"). This enables different suite runs to order tests | ||
| # for quick --failfast termination, in case a test fails in multiple suites | ||
| update_test_results_to(r.test_name.split(' ')[0]) | ||
|
|
||
| json.dump(previous_test_run_results, open(common.PREVIOUS_TEST_RUN_RESULTS_FILE, 'w'), indent=2) | ||
| pool.close() | ||
| pool.join() | ||
| return self.combine_results(result, results) | ||
|
|
@@ -104,6 +148,8 @@ class BufferedParallelTestResult: | |
| def __init__(self): | ||
| self.buffered_result = None | ||
| self.test_duration = 0 | ||
| self.test_result = 'errored' | ||
| self.test_name = '' | ||
|
|
||
| @property | ||
| def test(self): | ||
|
|
@@ -122,6 +168,7 @@ def updateResult(self, result): | |
| result.core_time += self.test_duration | ||
|
|
||
| def startTest(self, test): | ||
| self.test_name = str(test) | ||
| self.start_time = time.perf_counter() | ||
|
|
||
| def stopTest(self, test): | ||
|
|
@@ -132,26 +179,32 @@ def stopTest(self, test): | |
| def addSuccess(self, test): | ||
| print(test, '... ok (%.2fs)' % (self.calculateElapsed()), file=sys.stderr) | ||
| self.buffered_result = BufferedTestSuccess(test) | ||
| self.test_result = 'success' | ||
|
|
||
| def addExpectedFailure(self, test, err): | ||
| print(test, '... expected failure (%.2fs)' % (self.calculateElapsed()), file=sys.stderr) | ||
| self.buffered_result = BufferedTestExpectedFailure(test, err) | ||
| self.test_result = 'expected failure' | ||
|
|
||
| def addUnexpectedSuccess(self, test): | ||
| print(test, '... unexpected success (%.2fs)' % (self.calculateElapsed()), file=sys.stderr) | ||
| self.buffered_result = BufferedTestUnexpectedSuccess(test) | ||
| self.test_result = 'unexpected success' | ||
|
|
||
| def addSkip(self, test, reason): | ||
| print(test, "... skipped '%s'" % reason, file=sys.stderr) | ||
| self.buffered_result = BufferedTestSkip(test, reason) | ||
| self.test_result = 'skipped' | ||
|
|
||
| def addFailure(self, test, err): | ||
| print(test, '... FAIL', file=sys.stderr) | ||
| self.buffered_result = BufferedTestFailure(test, err) | ||
| self.test_result = 'failed' | ||
|
|
||
| def addError(self, test, err): | ||
| print(test, '... ERROR', file=sys.stderr) | ||
| self.buffered_result = BufferedTestError(test, err) | ||
| self.test_result = 'errored' | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It this needed? Isn't the existing
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Python doesn't have an API to ask the result of a test object, and it would have required some kind of an awkward isinstance() jungle to convert the test to a string, so I opted to writing simple looking code as a more preferable way.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought I posted this already, but can you try
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
|
||
|
|
||
| class BufferedTestBase: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens when we just this same file to write results from different test suites? I guess the names of the specific tests won't match so results from a different test suite will be mostly ignored?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JSON file has multiple entries for different test suites (
core1.test_atexitvscore2.test_atexit) but then also a global entry fortest_atexitwithout distinction of the test suite.The first accumulates results from specific suite, and the general one across all suites.
This way if a test fails in one suite, it gets bumped in fail frequency in other suites runs.