Skip to content

Commit 77cd236

Browse files
committed
Improve the SingleLineTestResult and integrate with parallel_testsuite
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 7dd9069 commit 77cd236

File tree

4 files changed

+93
-125
lines changed

4 files changed

+93
-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: 25 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')
@@ -118,43 +118,11 @@ def __init__(self, max_cores, options):
118118
self.max_cores = max_cores
119119
self.max_failures = options.max_failures
120120
self.failing_and_slow_first = options.failing_and_slow_first
121-
self.progress_counter = 0
122121

123122
def addTest(self, test):
124123
super().addTest(test)
125124
test.is_parallel = True
126125

127-
def printOneResult(self, res):
128-
self.progress_counter += 1
129-
progress = f'[{self.progress_counter}/{self.num_tests}] '
130-
131-
if res.test_result == 'success':
132-
msg = 'ok'
133-
color = GREEN
134-
elif res.test_result == 'errored':
135-
msg = 'ERROR'
136-
color = RED
137-
elif res.test_result == 'failed':
138-
msg = 'FAIL'
139-
color = RED
140-
elif res.test_result == 'skipped':
141-
reason = res.skipped[0][1]
142-
msg = f"skipped '{reason}'"
143-
color = CYAN
144-
elif res.test_result == 'unexpected success':
145-
msg = 'unexpected success'
146-
color = RED
147-
elif res.test_result == 'expected failure':
148-
color = RED
149-
msg = 'expected failure'
150-
else:
151-
assert False, f'unhandled test result {res.test_result}'
152-
153-
if res.test_result != 'skipped':
154-
msg += f' ({res.elapsed:.2f}s)'
155-
156-
errlog(f'{with_color(CYAN, progress)}{res.test} ... {with_color(color, msg)}')
157-
158126
def run(self, result):
159127
# The 'spawn' method is used on windows and it can be useful to set this on
160128
# all platforms when debugging multiprocessing issues. Without this we
@@ -163,6 +131,12 @@ def run(self, result):
163131
# issues.
164132
# multiprocessing.set_start_method('spawn')
165133

134+
# No need to worry about stdout/stderr buffering since are a not
135+
# actually running the test here, only setting the results.
136+
buffer = result.buffer
137+
result.buffer = False
138+
139+
result.core_time = 0
166140
tests = self.get_sorted_tests()
167141
self.num_tests = self.countTestCases()
168142
contains_browser_test = any(test.is_browser_test() for test in tests)
@@ -189,20 +163,20 @@ def run(self, result):
189163
lock = manager.Lock()
190164

191165
results = []
192-
args = ((t, allowed_failures_counter, lock, result.buffer) for t in tests)
166+
args = ((t, allowed_failures_counter, lock, buffer) for t in tests)
193167
for res in pool.imap_unordered(run_test, args, chunksize=1):
194168
# results may be be None if # of allowed errors was exceeded
195169
# and the harness aborted.
196170
if res:
197-
self.printOneResult(res)
171+
res.integrate_result(result)
198172
results.append(res)
199173

200174
# Send a task to each worker to tear down the browser and server. This
201175
# relies on the implementation detail in the worker pool that all workers
202176
# are cycled through once.
203177
num_tear_downs = sum([pool.apply(tear_down, ()) for i in range(use_cores)])
204178
# Assert the assumed behavior above hasn't changed.
205-
if num_tear_downs != use_cores:
179+
if num_tear_downs != use_cores and not buffer:
206180
errlog(f'Expected {use_cores} teardowns, got {num_tear_downs}')
207181

208182
if self.failing_and_slow_first:
@@ -228,7 +202,9 @@ def update_test_results_to(test_name):
228202

229203
json.dump(previous_test_run_results, open(common.PREVIOUS_TEST_RUN_RESULTS_FILE, 'w'), indent=2)
230204

231-
return self.combine_results(result, results)
205+
if EMTEST_VISUALIZE:
206+
self.visualize_results(results)
207+
return result
232208

233209
def get_sorted_tests(self):
234210
"""A list of this suite's tests, sorted with the @is_slow_test tests first.
@@ -247,45 +223,29 @@ def test_key(test):
247223

248224
return sorted(self, key=test_key, reverse=True)
249225

250-
def combine_results(self, result, buffered_results):
251-
errlog('')
252-
errlog('DONE: combining results on main thread')
253-
errlog('')
226+
def visualize_results(self, results):
227+
assert EMTEST_VISUALIZE
254228
# Sort the results back into alphabetical order. Running the tests in
255229
# parallel causes mis-orderings, this makes the results more readable.
256-
results = sorted(buffered_results, key=lambda res: str(res.test))
257-
result.core_time = 0
230+
results = sorted(results, key=lambda res: str(res.test))
258231

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

265-
# The next integrateResult loop will print a *lot* of lines really fast. This
266-
# will cause a Python exception being thrown when attempting to print to
267-
# stderr, if stderr is in nonblocking mode, like it is on Buildbot CI:
268-
# See https://github.com/buildbot/buildbot/issues/8659
269-
# To work around that problem, set stderr to blocking mode before printing.
270-
if not WINDOWS:
271-
os.set_blocking(sys.stderr.fileno(), True)
272-
273238
for r in results:
274-
# Integrate the test result to the global test result object
275-
r.integrateResult(result)
276239
r.log_test_run_for_visualization(flaky_tests)
277240

278241
# Generate the parallel test run visualization
279-
if EMTEST_VISUALIZE:
280-
emprofile.create_profiling_graph(utils.path_from_root('out/graph'))
281-
# Cleanup temp files that were used for the visualization
282-
emprofile.delete_profiler_logs()
283-
utils.delete_file(common.flaky_tests_log_filename)
284-
285-
return result
242+
emprofile.create_profiling_graph(utils.path_from_root('out/graph'))
243+
# Cleanup temp files that were used for the visualization
244+
emprofile.delete_profiler_logs()
245+
utils.delete_file(common.flaky_tests_log_filename)
286246

287247

288-
class BufferedParallelTestResult(unittest.TestResult):
248+
class BufferedParallelTestResult(BufferingMixin, unittest.TestResult):
289249
"""A picklable struct used to communicate test results across processes
290250
"""
291251
def __init__(self):
@@ -303,15 +263,12 @@ def test_short_name(self):
303263
def addDuration(self, test, elapsed):
304264
self.test_duration = elapsed
305265

306-
def integrateResult(self, overall_results):
266+
def integrate_result(self, overall_results):
307267
"""This method get called on the main thread once the buffered result
308-
is received. It add the buffered result to the overall result."""
268+
is received. It adds the buffered result to the overall result."""
309269
# The exception info objects that we are adding here have already
310270
# been turned into strings so make _exc_info_to_string into a no-op.
311271
overall_results._exc_info_to_string = lambda x, _y: x
312-
# No need to worry about stdout/stderr buffering since are a not
313-
# actually running the test here, only setting the results.
314-
overall_results.buffer = False
315272
overall_results.startTest(self.test)
316273
if self.test_result == 'success':
317274
overall_results.addSuccess(self.test)
@@ -331,7 +288,8 @@ def integrateResult(self, overall_results):
331288
overall_results.core_time += self.test_duration
332289

333290
def log_test_run_for_visualization(self, flaky_tests):
334-
if EMTEST_VISUALIZE and (self.test_result != 'skipped' or self.test_duration > 0.2):
291+
assert EMTEST_VISUALIZE
292+
if self.test_result != 'skipped' or self.test_duration > 0.2:
335293
test_result = self.test_result
336294
if test_result == 'success' and self.test_short_name() in flaky_tests:
337295
test_result = 'warnings'

test/runner.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -429,12 +429,12 @@ 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+
testRunner = ColorTextRunner(failfast=options.failfast)
438438

439439
total_core_time = 0
440440
run_start_time = time.perf_counter()
@@ -472,7 +472,9 @@ def parse_args():
472472
'test. Implies --cores=1. Defaults to true when running a single test')
473473
parser.add_argument('--no-clean', action='store_true',
474474
help='Do not clean the temporary directory before each test run')
475-
parser.add_argument('--verbose', '-v', action='store_true')
475+
parser.add_argument('--verbose', '-v', action='count', default=0,
476+
help="Show test stdout and stderr, and don't use the single-line test reporting. "
477+
'Specifying `-v` twice will enable test framework logging (i.e. EMTEST_VERBOSE)')
476478
# TODO: Replace with BooleanOptionalAction once we can depend on python3.9
477479
parser.add_argument('--ansi', action='store_true', default=None)
478480
parser.add_argument('--no-ansi', action='store_false', dest='ansi', default=None)
@@ -492,7 +494,6 @@ def parse_args():
492494
help='Use the default CI browser configuration.')
493495
parser.add_argument('tests', nargs='*')
494496
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')
496497
parser.add_argument('--max-failures', type=int, default=2**31 - 1, help='If specified, test run will abort after N failed tests.')
497498
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.')
498499
parser.add_argument('--start-at', metavar='NAME', help='Skip all tests up until <NAME>')
@@ -587,7 +588,7 @@ def set_env(name, option_value):
587588
set_env('EMTEST_SKIP_SLOW', options.skip_slow)
588589
set_env('EMTEST_ALL_ENGINES', options.all_engines)
589590
set_env('EMTEST_REBASELINE', options.rebaseline)
590-
set_env('EMTEST_VERBOSE', options.verbose)
591+
set_env('EMTEST_VERBOSE', options.verbose > 1)
591592
set_env('EMTEST_CORES', options.cores)
592593
set_env('EMTEST_FORCE64', options.force64)
593594

test/single_line_runner.py

Lines changed: 29 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import shutil
77
import unittest
88

9-
from color_runner import BufferingMixin, ColorTextRunner
9+
from color_runner import ColorTextResult, ColorTextRunner
1010

1111
from tools.colored_logger import CYAN, GREEN, RED, with_color
1212

@@ -20,65 +20,47 @@ def term_width():
2020
return shutil.get_terminal_size()[0]
2121

2222

23-
class SingleLineTestResult(BufferingMixin, unittest.TextTestResult):
23+
class SingleLineTestResult(ColorTextResult):
2424
"""Similar to the standard TextTestResult but uses ANSI escape codes
2525
for color output and reusing a single line on the terminal.
2626
"""
2727

28-
def __init__(self, *args, **kwargs):
29-
super().__init__(*args, **kwargs)
30-
self.progress_counter = 0
31-
32-
def writeStatusLine(self, line):
33-
clearline(self._original_stderr)
34-
self._original_stderr.write(line)
35-
self._original_stderr.flush()
36-
37-
def updateStatus(self, test, msg, color):
38-
progress = f'[{self.progress_counter}/{self.test_count}] '
39-
# Format the line so that it fix within the terminal width, unless it's less then min_len
28+
def writeStatus(self, test, msg, color, line_pos):
29+
# Becuse the message can include the skip reason (which can be very long somtimes), truncate it
30+
# to a reasonable length to avoid exceeding ling length.
31+
if len(msg) > 30:
32+
msg = msg[:30]
33+
# Format the line so that it fix within the terminal width, unless its less then min_len
4034
# in which case there is not much we can do, and we just overflow the line.
41-
min_len = len(progress) + len(msg) + 5
35+
min_len = line_pos + len(msg) + 5
4236
test_name = str(test)
4337
if term_width() > min_len:
4438
max_name = term_width() - min_len
4539
test_name = test_name[:max_name]
46-
line = f'{with_color(CYAN, progress)}{test_name} ... {with_color(color, msg)}'
47-
self.writeStatusLine(line)
40+
line = f'{test_name} ... {with_color(color, msg)}'
41+
self._original_stderr.write(line)
42+
43+
def _write_status(self, test, status):
44+
clearline(self._original_stderr)
45+
pos = self.writeProgressPrefix()
46+
# Add some color to the status message
47+
if status == 'ok':
48+
color = GREEN
49+
elif status.isupper():
50+
color = RED
51+
# Use a newline when a test fails, so you can see a list of failures while
52+
# the other tests are still running
53+
status += '\n'
54+
else:
55+
color = CYAN
56+
self.writeStatus(test, status, color, pos)
57+
self._original_stderr.flush()
4858

4959
def startTest(self, test):
5060
self.progress_counter += 1
51-
assert self.test_count > 0
52-
# Note: We explicitly do not use `super()` here but instead call `unittest.TestResult`. i.e.
53-
# we skip the superclass (since we don't want its specific behaviour) and instead call its
54-
# superclass.
61+
# We explictly don't call TextTestResult.startTest here since we delay all printing
62+
# of results until `_write_status`
5563
unittest.TestResult.startTest(self, test)
56-
if self.progress_counter == 1:
57-
self.updateStatus(test, '', GREEN)
58-
59-
def addSuccess(self, test):
60-
unittest.TestResult.addSuccess(self, test)
61-
self.updateStatus(test, 'ok', GREEN)
62-
63-
def addFailure(self, test, err):
64-
unittest.TestResult.addFailure(self, test, err)
65-
self.updateStatus(test, 'FAIL', RED)
66-
67-
def addError(self, test, err):
68-
unittest.TestResult.addError(self, test, err)
69-
self.updateStatus(test, 'ERROR', RED)
70-
71-
def addExpectedFailure(self, test, err):
72-
unittest.TestResult.addExpectedFailure(self, test, err)
73-
self.updateStatus(test, 'expected failure', RED)
74-
75-
def addUnexpectedSuccess(self, test, err):
76-
unittest.TestResult.addUnexpectedSuccess(self, test, err)
77-
self.updateStatus(test, 'UNEXPECTED SUCCESS', RED)
78-
79-
def addSkip(self, test, reason):
80-
unittest.TestResult.addSkip(self, test, reason)
81-
self.updateStatus(test, f"skipped '{reason}'", CYAN)
8264

8365
def printErrors(self):
8466
# All tests have been run at this point so print a final newline

0 commit comments

Comments
 (0)