Skip to content

Commit a500942

Browse files
authored
Improve the SingleLineTestResult and integrate with parallel_testsuite (#25761)
Now the parallel test suite now longer needs its own reporting logic it can just share use the existing Runner/Result which now supports the color and single long output formats. By default we now buffer test output and use the single line test runner. Adding `-v` will disabled this and give multi-line non-buffered output. Adding another `-v` will enable logging in the test framework (same as setting EMTEST_VERBOSE)
1 parent 9bc36df commit a500942

File tree

4 files changed

+101
-125
lines changed

4 files changed

+101
-125
lines changed

test/color_runner.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,37 @@ def _restoreStdout(self):
3535
class ProgressMixin:
3636
test_count = 0
3737
progress_counter = 0
38+
fail_count = 0
39+
40+
def addError(self, test, err):
41+
self.fail_count += 1
42+
super().addError(test, err)
43+
44+
def addFailure(self, test, err):
45+
self.fail_count += 1
46+
super().addFailure(test, err)
47+
48+
def addUnexpectedSuccess(self, test):
49+
self.fail_count += 1
50+
super().addUnexpectedSuccess(test)
51+
52+
def writeProgressPrefix(self):
53+
if self.showAll:
54+
base_progress = f'[{self.progress_counter}/{self.test_count}'
55+
rtn = len(base_progress)
56+
progress = with_color(CYAN, base_progress)
57+
if self.fail_count:
58+
fail_str = f'-{self.fail_count}'
59+
progress += with_color(RED, fail_str)
60+
rtn += len(fail_str)
61+
progress += with_color(CYAN, '] ')
62+
self.stream.write(progress)
63+
return rtn + 2
64+
return 0
3865

3966
def startTest(self, test):
40-
assert self.test_count > 0
4167
self.progress_counter += 1
42-
if self.showAll:
43-
progress = f'[{self.progress_counter}/{self.test_count}] '
44-
self.stream.write(with_color(CYAN, progress))
68+
self.writeProgressPrefix()
4569
super().startTest(test)
4670

4771

@@ -62,6 +86,9 @@ class ColorTextRunner(unittest.TextTestRunner):
6286
"""Subclass of TextTestRunner that uses ColorTextResult"""
6387
resultclass = ColorTextResult # type: ignore
6488

89+
def __init__(self, *args, **kwargs):
90+
super().__init__(*args, verbosity=2, **kwargs)
91+
6592
def _makeResult(self):
6693
result = super()._makeResult()
6794
result.test_count = self.test_count

test/parallel_testsuite.py

Lines changed: 29 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313

1414
import browser_common
1515
import common
16+
from color_runner import BufferingMixin
1617
from common import errlog
1718

1819
from tools import emprofile, utils
19-
from tools.colored_logger import CYAN, GREEN, RED, with_color
2020
from tools.utils import WINDOWS
2121

2222
EMTEST_VISUALIZE = os.getenv('EMTEST_VISUALIZE')
@@ -66,6 +66,10 @@ def run_test(args):
6666
test.set_temp_dir(temp_dir)
6767
try:
6868
test(result)
69+
except KeyboardInterrupt:
70+
# In case of KeyboardInterrupt do not emit buffered stderr/stdout
71+
# as we unwind.
72+
result._mirrorOutput = False
6973
finally:
7074
result.elapsed = time.perf_counter() - start_time
7175

@@ -106,43 +110,11 @@ def __init__(self, max_cores, options):
106110
self.max_cores = max_cores
107111
self.max_failures = options.max_failures
108112
self.failing_and_slow_first = options.failing_and_slow_first
109-
self.progress_counter = 0
110113

111114
def addTest(self, test):
112115
super().addTest(test)
113116
test.is_parallel = True
114117

115-
def printOneResult(self, res):
116-
self.progress_counter += 1
117-
progress = f'[{self.progress_counter}/{self.num_tests}] '
118-
119-
if res.test_result == 'success':
120-
msg = 'ok'
121-
color = GREEN
122-
elif res.test_result == 'errored':
123-
msg = 'ERROR'
124-
color = RED
125-
elif res.test_result == 'failed':
126-
msg = 'FAIL'
127-
color = RED
128-
elif res.test_result == 'skipped':
129-
reason = res.skipped[0][1]
130-
msg = f"skipped '{reason}'"
131-
color = CYAN
132-
elif res.test_result == 'unexpected success':
133-
msg = 'unexpected success'
134-
color = RED
135-
elif res.test_result == 'expected failure':
136-
color = RED
137-
msg = 'expected failure'
138-
else:
139-
assert False, f'unhandled test result {res.test_result}'
140-
141-
if res.test_result != 'skipped':
142-
msg += f' ({res.elapsed:.2f}s)'
143-
144-
errlog(f'{with_color(CYAN, progress)}{res.test} ... {with_color(color, msg)}')
145-
146118
def run(self, result):
147119
# The 'spawn' method is used on windows and it can be useful to set this on
148120
# all platforms when debugging multiprocessing issues. Without this we
@@ -151,6 +123,12 @@ def run(self, result):
151123
# issues.
152124
# multiprocessing.set_start_method('spawn')
153125

126+
# No need to worry about stdout/stderr buffering since are a not
127+
# actually running the test here, only setting the results.
128+
buffer = result.buffer
129+
result.buffer = False
130+
131+
result.core_time = 0
154132
tests = self.get_sorted_tests()
155133
self.num_tests = self.countTestCases()
156134
contains_browser_test = any(test.is_browser_test() for test in tests)
@@ -176,23 +154,23 @@ def run(self, result):
176154
allowed_failures_counter = manager.Value('i', self.max_failures)
177155

178156
results = []
179-
args = ((t, allowed_failures_counter, result.buffer) for t in tests)
157+
args = ((t, allowed_failures_counter, buffer) for t in tests)
180158
for res in pool.imap_unordered(run_test, args, chunksize=1):
181159
# results may be be None if # of allowed errors was exceeded
182160
# and the harness aborted.
183161
if res:
184162
if res.test_result not in ['success', 'skipped'] and allowed_failures_counter is not None:
185163
# Signal existing multiprocess pool runners so that they can exit early if needed.
186164
allowed_failures_counter.value -= 1
187-
self.printOneResult(res)
165+
res.integrate_result(result)
188166
results.append(res)
189167

190168
# Send a task to each worker to tear down the browser and server. This
191169
# relies on the implementation detail in the worker pool that all workers
192170
# are cycled through once.
193171
num_tear_downs = sum([pool.apply(tear_down, ()) for i in range(use_cores)])
194172
# Assert the assumed behavior above hasn't changed.
195-
if num_tear_downs != use_cores:
173+
if num_tear_downs != use_cores and not buffer:
196174
errlog(f'Expected {use_cores} teardowns, got {num_tear_downs}')
197175

198176
if self.failing_and_slow_first:
@@ -218,7 +196,9 @@ def update_test_results_to(test_name):
218196

219197
json.dump(previous_test_run_results, open(common.PREVIOUS_TEST_RUN_RESULTS_FILE, 'w'), indent=2)
220198

221-
return self.combine_results(result, results)
199+
if EMTEST_VISUALIZE:
200+
self.visualize_results(results)
201+
return result
222202

223203
def get_sorted_tests(self):
224204
"""A list of this suite's tests, sorted with the @is_slow_test tests first.
@@ -237,45 +217,29 @@ def test_key(test):
237217

238218
return sorted(self, key=test_key, reverse=True)
239219

240-
def combine_results(self, result, buffered_results):
241-
errlog('')
242-
errlog('DONE: combining results on main thread')
243-
errlog('')
220+
def visualize_results(self, results):
221+
assert EMTEST_VISUALIZE
244222
# Sort the results back into alphabetical order. Running the tests in
245223
# parallel causes mis-orderings, this makes the results more readable.
246-
results = sorted(buffered_results, key=lambda res: str(res.test))
247-
result.core_time = 0
224+
results = sorted(results, key=lambda res: str(res.test))
248225

249226
# shared data structures are hard in the python multi-processing world, so
250227
# use a file to share the flaky test information across test processes.
251228
flaky_tests = open(common.flaky_tests_log_filename).read().split() if os.path.isfile(common.flaky_tests_log_filename) else []
252229
# Extract only the test short names
253230
flaky_tests = [x.split('.')[-1] for x in flaky_tests]
254231

255-
# The next integrateResult loop will print a *lot* of lines really fast. This
256-
# will cause a Python exception being thrown when attempting to print to
257-
# stderr, if stderr is in nonblocking mode, like it is on Buildbot CI:
258-
# See https://github.com/buildbot/buildbot/issues/8659
259-
# To work around that problem, set stderr to blocking mode before printing.
260-
if not WINDOWS:
261-
os.set_blocking(sys.stderr.fileno(), True)
262-
263232
for r in results:
264-
# Integrate the test result to the global test result object
265-
r.integrateResult(result)
266233
r.log_test_run_for_visualization(flaky_tests)
267234

268235
# Generate the parallel test run visualization
269-
if EMTEST_VISUALIZE:
270-
emprofile.create_profiling_graph(utils.path_from_root('out/graph'))
271-
# Cleanup temp files that were used for the visualization
272-
emprofile.delete_profiler_logs()
273-
utils.delete_file(common.flaky_tests_log_filename)
274-
275-
return result
236+
emprofile.create_profiling_graph(utils.path_from_root('out/graph'))
237+
# Cleanup temp files that were used for the visualization
238+
emprofile.delete_profiler_logs()
239+
utils.delete_file(common.flaky_tests_log_filename)
276240

277241

278-
class BufferedParallelTestResult(unittest.TestResult):
242+
class BufferedParallelTestResult(BufferingMixin, unittest.TestResult):
279243
"""A picklable struct used to communicate test results across processes
280244
"""
281245
def __init__(self):
@@ -293,15 +257,12 @@ def test_short_name(self):
293257
def addDuration(self, test, elapsed):
294258
self.test_duration = elapsed
295259

296-
def integrateResult(self, overall_results):
260+
def integrate_result(self, overall_results):
297261
"""This method get called on the main thread once the buffered result
298-
is received. It add the buffered result to the overall result."""
262+
is received. It adds the buffered result to the overall result."""
299263
# The exception info objects that we are adding here have already
300264
# been turned into strings so make _exc_info_to_string into a no-op.
301265
overall_results._exc_info_to_string = lambda x, _y: x
302-
# No need to worry about stdout/stderr buffering since are a not
303-
# actually running the test here, only setting the results.
304-
overall_results.buffer = False
305266
overall_results.startTest(self.test)
306267
if self.test_result == 'success':
307268
overall_results.addSuccess(self.test)
@@ -321,7 +282,8 @@ def integrateResult(self, overall_results):
321282
overall_results.core_time += self.test_duration
322283

323284
def log_test_run_for_visualization(self, flaky_tests):
324-
if EMTEST_VISUALIZE and (self.test_result != 'skipped' or self.test_duration > 0.2):
285+
assert EMTEST_VISUALIZE
286+
if self.test_result != 'skipped' or self.test_duration > 0.2:
325287
test_result = self.test_result
326288
if test_result == 'success' and self.test_short_name() in flaky_tests:
327289
test_result = 'warnings'

test/runner.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -429,12 +429,16 @@ def run_tests(options, suites):
429429
testRunner = xmlrunner.XMLTestRunner(output=output, verbosity=2,
430430
failfast=options.failfast)
431431
print('Writing XML test output to ' + os.path.abspath(output.name))
432-
elif options.buffer and options.ansi and not options.verbose:
433-
# When buffering is enabled and ansi color output is available use our nice single-line
432+
elif options.ansi and not options.verbose:
433+
# When not in verbose mode and ansi color output is available use our nice single-line
434434
# result display.
435-
testRunner = SingleLineTestRunner(verbosity=2, failfast=options.failfast)
435+
testRunner = SingleLineTestRunner(failfast=options.failfast)
436436
else:
437-
testRunner = ColorTextRunner(verbosity=2, failfast=options.failfast)
437+
if not options.ansi:
438+
print('using verbose test runner (ANSI not avilable)')
439+
else:
440+
print('using verbose test runner (verbose output requested)')
441+
testRunner = ColorTextRunner(failfast=options.failfast)
438442

439443
total_core_time = 0
440444
run_start_time = time.perf_counter()
@@ -472,7 +476,9 @@ def parse_args():
472476
'test. Implies --cores=1. Defaults to true when running a single test')
473477
parser.add_argument('--no-clean', action='store_true',
474478
help='Do not clean the temporary directory before each test run')
475-
parser.add_argument('--verbose', '-v', action='store_true')
479+
parser.add_argument('--verbose', '-v', action='count', default=0,
480+
help="Show test stdout and stderr, and don't use the single-line test reporting. "
481+
'Specifying `-v` twice will enable test framework logging (i.e. EMTEST_VERBOSE)')
476482
# TODO: Replace with BooleanOptionalAction once we can depend on python3.9
477483
parser.add_argument('--ansi', action='store_true', default=None)
478484
parser.add_argument('--no-ansi', action='store_false', dest='ansi', default=None)
@@ -492,7 +498,6 @@ def parse_args():
492498
help='Use the default CI browser configuration.')
493499
parser.add_argument('tests', nargs='*')
494500
parser.add_argument('--failfast', action='store_true', help='If true, test run will abort on first failed test.')
495-
parser.add_argument('-b', '--buffer', action='store_true', help='Buffer stdout and stderr during tests')
496501
parser.add_argument('--max-failures', type=int, default=2**31 - 1, help='If specified, test run will abort after N failed tests.')
497502
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.')
498503
parser.add_argument('--start-at', metavar='NAME', help='Skip all tests up until <NAME>')
@@ -587,7 +592,7 @@ def set_env(name, option_value):
587592
set_env('EMTEST_SKIP_SLOW', options.skip_slow)
588593
set_env('EMTEST_ALL_ENGINES', options.all_engines)
589594
set_env('EMTEST_REBASELINE', options.rebaseline)
590-
set_env('EMTEST_VERBOSE', options.verbose)
595+
set_env('EMTEST_VERBOSE', options.verbose > 1)
591596
set_env('EMTEST_CORES', options.cores)
592597
set_env('EMTEST_FORCE64', options.force64)
593598

0 commit comments

Comments
 (0)