Skip to content

Commit 33ec3ec

Browse files
authored
Add --buffer option to test runner. NFC (#25737)
This is standard option in the default python unintest runner. It now works for both the normal serial runner as well as the parallel runner. In the case of the parallel runner we not inherit from the standard `unittest.TestResult` so that we can inherit the the stdout/stderr buffering methods it implements. In the future we may want to enable this by default. This could especially useful the parallel runner where we have issue of interleaving the stdout and stderr of different tests is confusing.
1 parent e5128c0 commit 33ec3ec

File tree

2 files changed

+59
-39
lines changed

2 files changed

+59
-39
lines changed

test/parallel_testsuite.py

Lines changed: 57 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def cap_max_workers_in_pool(max_workers, is_browser):
4343
return max_workers
4444

4545

46-
def run_test(test, allowed_failures_counter, lock, progress_counter, num_tests):
46+
def run_test(test, allowed_failures_counter, lock, progress_counter, num_tests, buffer):
4747
# If we have exceeded the number of allowed failures during the test run,
4848
# abort executing further tests immediately.
4949
if allowed_failures_counter and allowed_failures_counter.value < 0:
@@ -54,8 +54,43 @@ def test_failed():
5454
with lock:
5555
allowed_failures_counter.value -= 1
5656

57+
start_time = time.perf_counter()
58+
59+
def compute_progress():
60+
if not lock:
61+
return ''
62+
with lock:
63+
val = f'[{int(progress_counter.value * 100 / num_tests)}%] '
64+
progress_counter.value += 1
65+
return with_color(CYAN, val)
66+
67+
def printResult(res):
68+
elapsed = time.perf_counter() - start_time
69+
progress = compute_progress()
70+
if res.test_result == 'success':
71+
msg = f'ok ({elapsed:.2f}s)'
72+
errlog(f'{progress}{res.test} ... {with_color(GREEN, msg)}')
73+
elif res.test_result == 'errored':
74+
msg = f'{res.test} ... ERROR'
75+
errlog(f'{progress}{with_color(RED, msg)}')
76+
elif res.test_result == 'failed':
77+
msg = f'{res.test} ... FAIL'
78+
errlog(f'{progress}{with_color(RED, msg)}')
79+
elif res.test_result == 'skipped':
80+
msg = f"skipped '{res.buffered_result.reason}'"
81+
errlog(f"{progress}{res.test} ... {with_color(CYAN, msg)}")
82+
elif res.test_result == 'unexpected success':
83+
msg = f'unexpected success ({elapsed:.2f}s)'
84+
errlog(f'{progress}{res.test} ... {with_color(RED, msg)}')
85+
elif res.test_result == 'expected failure':
86+
msg = f'expected failure ({elapsed:.2f}s)'
87+
errlog(f'{progress}{res.test} ... {with_color(RED, msg)}')
88+
else:
89+
assert False
90+
5791
olddir = os.getcwd()
58-
result = BufferedParallelTestResult(lock, progress_counter, num_tests)
92+
result = BufferedParallelTestResult()
93+
result.buffer = buffer
5994
temp_dir = tempfile.mkdtemp(prefix='emtest_')
6095
test.set_temp_dir(temp_dir)
6196
try:
@@ -72,6 +107,9 @@ def test_failed():
72107
except Exception as e:
73108
result.addError(test, e)
74109
test_failed()
110+
finally:
111+
printResult(result)
112+
75113
# Before attempting to delete the tmp dir make sure the current
76114
# working directory is not within it.
77115
os.chdir(olddir)
@@ -139,7 +177,7 @@ def run(self, result):
139177
allowed_failures_counter = manager.Value('i', self.max_failures)
140178
progress_counter = manager.Value('i', 0)
141179
lock = manager.Lock()
142-
results = pool.starmap(run_test, ((t, allowed_failures_counter, lock, progress_counter, len(tests)) for t in tests), chunksize=1)
180+
results = pool.starmap(run_test, ((t, allowed_failures_counter, lock, progress_counter, len(tests), result.buffer) for t in tests), chunksize=1)
143181
# Send a task to each worker to tear down the browser and server. This
144182
# relies on the implementation detail in the worker pool that all workers
145183
# are cycled through once.
@@ -233,21 +271,15 @@ def combine_results(self, result, buffered_results):
233271
return result
234272

235273

236-
class BufferedParallelTestResult:
274+
class BufferedParallelTestResult(unittest.TestResult):
237275
"""A picklable struct used to communicate test results across processes
238-
239-
Fulfills the interface for unittest.TestResult
240276
"""
241-
def __init__(self, lock, progress_counter, num_tests):
277+
def __init__(self):
278+
super().__init__()
242279
self.buffered_result = None
243280
self.test_duration = 0
244281
self.test_result = 'errored'
245282
self.test_name = ''
246-
self.lock = lock
247-
self.progress_counter = progress_counter
248-
self.num_tests = num_tests
249-
self.failures = []
250-
self.errors = []
251283

252284
@property
253285
def test(self):
@@ -261,9 +293,6 @@ def test_short_name(self):
261293
def addDuration(self, test, elapsed):
262294
self.test_duration = elapsed
263295

264-
def calculateElapsed(self):
265-
return time.perf_counter() - self.start_time
266-
267296
def updateResult(self, result):
268297
result.startTest(self.test)
269298
self.buffered_result.updateResult(result)
@@ -295,59 +324,49 @@ def log_test_run_for_visualization(self):
295324
prof.write(f',\n{{"pid":{dummy_test_task_counter},"op":"exit","time":{self.start_time + self.test_duration},"returncode":0}}')
296325

297326
def startTest(self, test):
327+
super().startTest(test)
298328
self.test_name = str(test)
299-
self.start_time = time.perf_counter()
300329

301330
def stopTest(self, test):
331+
super().stopTest(test)
302332
# TODO(sbc): figure out a way to display this duration information again when
303333
# these results get passed back to the TextTestRunner/TextTestResult.
304334
self.buffered_result.duration = self.test_duration
305-
306-
def compute_progress(self):
307-
if not self.lock:
308-
return ''
309-
with self.lock:
310-
val = f'[{int(self.progress_counter.value * 100 / self.num_tests)}%] '
311-
self.progress_counter.value += 1
312-
return with_color(CYAN, val)
335+
# Once we are done running the test and any stdout/stderr buffering has
336+
# being taking care or, we delete these fields which the parent class uses.
337+
# This is because they are not picklable (serializable).
338+
del self._original_stdout
339+
del self._original_stderr
313340

314341
def addSuccess(self, test):
315-
msg = f'ok ({self.calculateElapsed():.2f}s)'
316-
errlog(f'{self.compute_progress()}{test} ... {with_color(GREEN, msg)}')
342+
super().addSuccess(test)
317343
self.buffered_result = BufferedTestSuccess(test)
318344
self.test_result = 'success'
319345

320346
def addExpectedFailure(self, test, err):
321-
msg = f'expected failure ({self.calculateElapsed():.2f}s)'
322-
errlog(f'{self.compute_progress()}{test} ... {with_color(RED, msg)}')
347+
super().addExpectedFailure(test, err)
323348
self.buffered_result = BufferedTestExpectedFailure(test, err)
324349
self.test_result = 'expected failure'
325350

326351
def addUnexpectedSuccess(self, test):
327-
msg = f'unexpected success ({self.calculateElapsed():.2f}s)'
328-
errlog(f'{self.compute_progress()}{test} ... {with_color(RED, msg)}')
352+
super().addUnexpectedSuccess(test)
329353
self.buffered_result = BufferedTestUnexpectedSuccess(test)
330354
self.test_result = 'unexpected success'
331355

332356
def addSkip(self, test, reason):
333-
msg = f"skipped '{reason}'"
334-
errlog(f"{self.compute_progress()}{test} ... {with_color(CYAN, msg)}")
357+
super().addSkip(test, reason)
335358
self.buffered_result = BufferedTestSkip(test, reason)
336359
self.test_result = 'skipped'
337360

338361
def addFailure(self, test, err):
339-
msg = f'{test} ... FAIL'
340-
errlog(f'{self.compute_progress()}{with_color(RED, msg)}')
362+
super().addFailure(test, err)
341363
self.buffered_result = BufferedTestFailure(test, err)
342364
self.test_result = 'failed'
343-
self.failures += [test]
344365

345366
def addError(self, test, err):
346-
msg = f'{test} ... ERROR'
347-
errlog(f'{self.compute_progress()}{with_color(RED, msg)}')
367+
super().addError(test, err)
348368
self.buffered_result = BufferedTestError(test, err)
349369
self.test_result = 'errored'
350-
self.errors += [test]
351370

352371

353372
class BufferedTestBase:

test/runner.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ def run_tests(options, suites):
428428
failfast=options.failfast)
429429
print('Writing XML test output to ' + os.path.abspath(output.name))
430430
else:
431-
testRunner = unittest.TextTestRunner(verbosity=2, failfast=options.failfast)
431+
testRunner = unittest.TextTestRunner(verbosity=2, buffer=options.buffer, failfast=options.failfast)
432432

433433
total_core_time = 0
434434
run_start_time = time.perf_counter()
@@ -483,6 +483,7 @@ def parse_args():
483483
help='Use the default CI browser configuration.')
484484
parser.add_argument('tests', nargs='*')
485485
parser.add_argument('--failfast', action='store_true', help='If true, test run will abort on first failed test.')
486+
parser.add_argument('-b', '--buffer', action='store_true', help='Buffer stdout and stderr during tests')
486487
parser.add_argument('--max-failures', type=int, default=2**31 - 1, help='If specified, test run will abort after N failed tests.')
487488
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.')
488489
parser.add_argument('--start-at', metavar='NAME', help='Skip all tests up until <NAME>')

0 commit comments

Comments
 (0)