Commit 4d299831 authored by Victor Stinner's avatar Victor Stinner Committed by GitHub

bpo-36725: regrtest: add TestResult type (GH-12960)

* Add TestResult and MultiprocessResult types to ensure that results
  always have the same fields.
* runtest() now handles KeyboardInterrupt
* accumulate_result() and format_test_result() now takes a TestResult
* cleanup_test_droppings() is now called by runtest() and mark the
  test as ENV_CHANGED if the test leaks support.TESTFN file.
* runtest() now includes code "around" the test in the test timing
* Add print_warning() in test.libregrtest.utils to standardize how
  libregrtest logs warnings to ease parsing the test output.
* support.unload() is now called with abstest rather than test_name
* Rename 'test' variable/parameter to 'test_name'
* dash_R(): remove unused the_module parameter
* Remove unused imports
parent 9db03247
...@@ -105,26 +105,30 @@ class Regrtest: ...@@ -105,26 +105,30 @@ class Regrtest:
# used by --junit-xml # used by --junit-xml
self.testsuite_xml = None self.testsuite_xml = None
def accumulate_result(self, test, result): def accumulate_result(self, result):
ok, test_time, xml_data = result test_name = result.test_name
ok = result.result
if ok not in (CHILD_ERROR, INTERRUPTED): if ok not in (CHILD_ERROR, INTERRUPTED):
self.test_times.append((test_time, test)) self.test_times.append((result.test_time, test_name))
if ok == PASSED: if ok == PASSED:
self.good.append(test) self.good.append(test_name)
elif ok in (FAILED, CHILD_ERROR): elif ok in (FAILED, CHILD_ERROR):
self.bad.append(test) self.bad.append(test_name)
elif ok == ENV_CHANGED: elif ok == ENV_CHANGED:
self.environment_changed.append(test) self.environment_changed.append(test_name)
elif ok == SKIPPED: elif ok == SKIPPED:
self.skipped.append(test) self.skipped.append(test_name)
elif ok == RESOURCE_DENIED: elif ok == RESOURCE_DENIED:
self.skipped.append(test) self.skipped.append(test_name)
self.resource_denieds.append(test) self.resource_denieds.append(test_name)
elif ok == TEST_DID_NOT_RUN: elif ok == TEST_DID_NOT_RUN:
self.run_no_tests.append(test) self.run_no_tests.append(test_name)
elif ok != INTERRUPTED: elif ok != INTERRUPTED:
raise ValueError("invalid test result: %r" % ok) raise ValueError("invalid test result: %r" % ok)
xml_data = result.xml_data
if xml_data: if xml_data:
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
for e in xml_data: for e in xml_data:
...@@ -134,7 +138,7 @@ class Regrtest: ...@@ -134,7 +138,7 @@ class Regrtest:
print(xml_data, file=sys.__stderr__) print(xml_data, file=sys.__stderr__)
raise raise
def display_progress(self, test_index, test): def display_progress(self, test_index, text):
if self.ns.quiet: if self.ns.quiet:
return return
...@@ -143,7 +147,7 @@ class Regrtest: ...@@ -143,7 +147,7 @@ class Regrtest:
fails = len(self.bad) + len(self.environment_changed) fails = len(self.bad) + len(self.environment_changed)
if fails and not self.ns.pgo: if fails and not self.ns.pgo:
line = f"{line}/{fails}" line = f"{line}/{fails}"
line = f"[{line}] {test}" line = f"[{line}] {text}"
# add the system load prefix: "load avg: 1.80 " # add the system load prefix: "load avg: 1.80 "
if self.getloadavg: if self.getloadavg:
...@@ -275,13 +279,13 @@ class Regrtest: ...@@ -275,13 +279,13 @@ class Regrtest:
support.verbose = False support.verbose = False
support.set_match_tests(self.ns.match_tests) support.set_match_tests(self.ns.match_tests)
for test in self.selected: for test_name in self.selected:
abstest = get_abs_module(self.ns, test) abstest = get_abs_module(self.ns, test_name)
try: try:
suite = unittest.defaultTestLoader.loadTestsFromName(abstest) suite = unittest.defaultTestLoader.loadTestsFromName(abstest)
self._list_cases(suite) self._list_cases(suite)
except unittest.SkipTest: except unittest.SkipTest:
self.skipped.append(test) self.skipped.append(test_name)
if self.skipped: if self.skipped:
print(file=sys.stderr) print(file=sys.stderr)
...@@ -298,19 +302,19 @@ class Regrtest: ...@@ -298,19 +302,19 @@ class Regrtest:
print() print()
print("Re-running failed tests in verbose mode") print("Re-running failed tests in verbose mode")
self.rerun = self.bad[:] self.rerun = self.bad[:]
for test in self.rerun: for test_name in self.rerun:
print("Re-running test %r in verbose mode" % test, flush=True) print("Re-running test %r in verbose mode" % test_name, flush=True)
try: self.ns.verbose = True
self.ns.verbose = True ok = runtest(self.ns, test_name)
ok = runtest(self.ns, test)
except KeyboardInterrupt: if ok[0] in {PASSED, ENV_CHANGED, SKIPPED, RESOURCE_DENIED}:
self.interrupted = True self.bad.remove(test_name)
if ok.result == INTERRUPTED:
# print a newline separate from the ^C # print a newline separate from the ^C
print() print()
self.interrupted = True
break break
else:
if ok[0] in {PASSED, ENV_CHANGED, SKIPPED, RESOURCE_DENIED}:
self.bad.remove(test)
else: else:
if self.bad: if self.bad:
print(count(len(self.bad), 'test'), "failed again:") print(count(len(self.bad), 'test'), "failed again:")
...@@ -348,8 +352,8 @@ class Regrtest: ...@@ -348,8 +352,8 @@ class Regrtest:
self.test_times.sort(reverse=True) self.test_times.sort(reverse=True)
print() print()
print("10 slowest tests:") print("10 slowest tests:")
for time, test in self.test_times[:10]: for test_time, test in self.test_times[:10]:
print("- %s: %s" % (test, format_duration(time))) print("- %s: %s" % (test, format_duration(test_time)))
if self.bad: if self.bad:
print() print()
...@@ -387,10 +391,10 @@ class Regrtest: ...@@ -387,10 +391,10 @@ class Regrtest:
print("Run tests sequentially") print("Run tests sequentially")
previous_test = None previous_test = None
for test_index, test in enumerate(self.tests, 1): for test_index, test_name in enumerate(self.tests, 1):
start_time = time.monotonic() start_time = time.monotonic()
text = test text = test_name
if previous_test: if previous_test:
text = '%s -- %s' % (text, previous_test) text = '%s -- %s' % (text, previous_test)
self.display_progress(test_index, text) self.display_progress(test_index, text)
...@@ -398,22 +402,20 @@ class Regrtest: ...@@ -398,22 +402,20 @@ class Regrtest:
if self.tracer: if self.tracer:
# If we're tracing code coverage, then we don't exit with status # If we're tracing code coverage, then we don't exit with status
# if on a false return value from main. # if on a false return value from main.
cmd = ('result = runtest(self.ns, test); ' cmd = ('result = runtest(self.ns, test_name); '
'self.accumulate_result(test, result)') 'self.accumulate_result(result)')
ns = dict(locals()) ns = dict(locals())
self.tracer.runctx(cmd, globals=globals(), locals=ns) self.tracer.runctx(cmd, globals=globals(), locals=ns)
result = ns['result'] result = ns['result']
else: else:
try: result = runtest(self.ns, test_name)
result = runtest(self.ns, test) self.accumulate_result(result)
except KeyboardInterrupt:
self.interrupted = True if result.result == INTERRUPTED:
self.accumulate_result(test, (INTERRUPTED, None, None)) self.interrupted = True
break break
else:
self.accumulate_result(test, result) previous_test = format_test_result(result)
previous_test = format_test_result(test, result[0])
test_time = time.monotonic() - start_time test_time = time.monotonic() - start_time
if test_time >= PROGRESS_MIN_TIME: if test_time >= PROGRESS_MIN_TIME:
previous_test = "%s in %s" % (previous_test, format_duration(test_time)) previous_test = "%s in %s" % (previous_test, format_duration(test_time))
...@@ -441,8 +443,8 @@ class Regrtest: ...@@ -441,8 +443,8 @@ class Regrtest:
def _test_forever(self, tests): def _test_forever(self, tests):
while True: while True:
for test in tests: for test_name in tests:
yield test yield test_name
if self.bad: if self.bad:
return return
if self.ns.fail_env_changed and self.environment_changed: if self.ns.fail_env_changed and self.environment_changed:
......
import errno
import os import os
import re import re
import sys import sys
...@@ -18,7 +17,7 @@ except ImportError: ...@@ -18,7 +17,7 @@ except ImportError:
cls._abc_negative_cache, cls._abc_negative_cache_version) cls._abc_negative_cache, cls._abc_negative_cache_version)
def dash_R(ns, the_module, test_name, test_func): def dash_R(ns, test_name, test_func):
"""Run a test multiple times, looking for reference leaks. """Run a test multiple times, looking for reference leaks.
Returns: Returns:
......
This diff is collapsed.
import collections
import faulthandler import faulthandler
import json import json
import os import os
...@@ -5,13 +6,12 @@ import queue ...@@ -5,13 +6,12 @@ import queue
import sys import sys
import threading import threading
import time import time
import traceback
import types import types
from test import support from test import support
from test.libregrtest.runtest import ( from test.libregrtest.runtest import (
runtest, INTERRUPTED, CHILD_ERROR, PROGRESS_MIN_TIME, runtest, INTERRUPTED, CHILD_ERROR, PROGRESS_MIN_TIME,
format_test_result) format_test_result, TestResult)
from test.libregrtest.setup import setup_tests from test.libregrtest.setup import setup_tests
from test.libregrtest.utils import format_duration from test.libregrtest.utils import format_duration
...@@ -64,15 +64,9 @@ def run_tests_worker(worker_args): ...@@ -64,15 +64,9 @@ def run_tests_worker(worker_args):
setup_tests(ns) setup_tests(ns)
try: result = runtest(ns, testname)
result = runtest(ns, testname)
except KeyboardInterrupt:
result = INTERRUPTED, '', None
except BaseException as e:
traceback.print_exc()
result = CHILD_ERROR, str(e)
print() # Force a newline (just in case) print() # Force a newline (just in case)
print(json.dumps(result), flush=True) print(json.dumps(result), flush=True)
sys.exit(0) sys.exit(0)
...@@ -97,45 +91,51 @@ class MultiprocessIterator: ...@@ -97,45 +91,51 @@ class MultiprocessIterator:
return next(self.tests) return next(self.tests)
MultiprocessResult = collections.namedtuple('MultiprocessResult',
'result stdout stderr error_msg')
class MultiprocessThread(threading.Thread): class MultiprocessThread(threading.Thread):
def __init__(self, pending, output, ns): def __init__(self, pending, output, ns):
super().__init__() super().__init__()
self.pending = pending self.pending = pending
self.output = output self.output = output
self.ns = ns self.ns = ns
self.current_test = None self.current_test_name = None
self.start_time = None self.start_time = None
def _runtest(self): def _runtest(self):
try: try:
test = next(self.pending) test_name = next(self.pending)
except StopIteration: except StopIteration:
self.output.put((None, None, None, None)) self.output.put(None)
return True return True
try: try:
self.start_time = time.monotonic() self.start_time = time.monotonic()
self.current_test = test self.current_test_name = test_name
retcode, stdout, stderr = run_test_in_subprocess(test, self.ns) retcode, stdout, stderr = run_test_in_subprocess(test_name, self.ns)
finally: finally:
self.current_test = None self.current_test_name = None
if retcode != 0: if retcode != 0:
result = (CHILD_ERROR, "Exit code %s" % retcode, None) test_time = time.monotonic() - self.start_time
self.output.put((test, stdout.rstrip(), stderr.rstrip(), result = TestResult(test_name, CHILD_ERROR, test_time, None)
result)) err_msg = "Exit code %s" % retcode
mp_result = MultiprocessResult(result, stdout.rstrip(), stderr.rstrip(), err_msg)
self.output.put(mp_result)
return False return False
stdout, _, result = stdout.strip().rpartition("\n") stdout, _, result = stdout.strip().rpartition("\n")
if not result: if not result:
self.output.put((None, None, None, None)) self.output.put(None)
return True return True
# deserialize run_tests_worker() output
result = json.loads(result) result = json.loads(result)
assert len(result) == 3, f"Invalid result tuple: {result!r}" result = TestResult(*result)
self.output.put((test, stdout.rstrip(), stderr.rstrip(), mp_result = MultiprocessResult(result, stdout.rstrip(), stderr.rstrip(), None)
result)) self.output.put(mp_result)
return False return False
def run(self): def run(self):
...@@ -144,7 +144,7 @@ class MultiprocessThread(threading.Thread): ...@@ -144,7 +144,7 @@ class MultiprocessThread(threading.Thread):
while not stop: while not stop:
stop = self._runtest() stop = self._runtest()
except BaseException: except BaseException:
self.output.put((None, None, None, None)) self.output.put(None)
raise raise
...@@ -164,12 +164,12 @@ def run_tests_multiprocess(regrtest): ...@@ -164,12 +164,12 @@ def run_tests_multiprocess(regrtest):
def get_running(workers): def get_running(workers):
running = [] running = []
for worker in workers: for worker in workers:
current_test = worker.current_test current_test_name = worker.current_test_name
if not current_test: if not current_test_name:
continue continue
dt = time.monotonic() - worker.start_time dt = time.monotonic() - worker.start_time
if dt >= PROGRESS_MIN_TIME: if dt >= PROGRESS_MIN_TIME:
text = '%s (%s)' % (current_test, format_duration(dt)) text = '%s (%s)' % (current_test_name, format_duration(dt))
running.append(text) running.append(text)
return running return running
...@@ -182,40 +182,41 @@ def run_tests_multiprocess(regrtest): ...@@ -182,40 +182,41 @@ def run_tests_multiprocess(regrtest):
faulthandler.dump_traceback_later(test_timeout, exit=True) faulthandler.dump_traceback_later(test_timeout, exit=True)
try: try:
item = output.get(timeout=get_timeout) mp_result = output.get(timeout=get_timeout)
except queue.Empty: except queue.Empty:
running = get_running(workers) running = get_running(workers)
if running and not regrtest.ns.pgo: if running and not regrtest.ns.pgo:
print('running: %s' % ', '.join(running), flush=True) print('running: %s' % ', '.join(running), flush=True)
continue continue
test, stdout, stderr, result = item if mp_result is None:
if test is None:
finished += 1 finished += 1
continue continue
regrtest.accumulate_result(test, result) result = mp_result.result
regrtest.accumulate_result(result)
# Display progress # Display progress
ok, test_time, xml_data = result ok = result.result
text = format_test_result(test, ok)
text = format_test_result(result)
if (ok not in (CHILD_ERROR, INTERRUPTED) if (ok not in (CHILD_ERROR, INTERRUPTED)
and test_time >= PROGRESS_MIN_TIME and result.test_time >= PROGRESS_MIN_TIME
and not regrtest.ns.pgo): and not regrtest.ns.pgo):
text += ' (%s)' % format_duration(test_time) text += ' (%s)' % format_duration(result.test_time)
elif ok == CHILD_ERROR: elif ok == CHILD_ERROR:
text = '%s (%s)' % (text, test_time) text = '%s (%s)' % (text, mp_result.error_msg)
running = get_running(workers) running = get_running(workers)
if running and not regrtest.ns.pgo: if running and not regrtest.ns.pgo:
text += ' -- running: %s' % ', '.join(running) text += ' -- running: %s' % ', '.join(running)
regrtest.display_progress(test_index, text) regrtest.display_progress(test_index, text)
# Copy stdout and stderr from the child process # Copy stdout and stderr from the child process
if stdout: if mp_result.stdout:
print(stdout, flush=True) print(mp_result.stdout, flush=True)
if stderr and not regrtest.ns.pgo: if mp_result.stderr and not regrtest.ns.pgo:
print(stderr, file=sys.stderr, flush=True) print(mp_result.stderr, file=sys.stderr, flush=True)
if result[0] == INTERRUPTED: if result.result == INTERRUPTED:
raise KeyboardInterrupt raise KeyboardInterrupt
test_index += 1 test_index += 1
except KeyboardInterrupt: except KeyboardInterrupt:
...@@ -229,7 +230,7 @@ def run_tests_multiprocess(regrtest): ...@@ -229,7 +230,7 @@ def run_tests_multiprocess(regrtest):
# If tests are interrupted, wait until tests complete # If tests are interrupted, wait until tests complete
wait_start = time.monotonic() wait_start = time.monotonic()
while True: while True:
running = [worker.current_test for worker in workers] running = [worker.current_test_name for worker in workers]
running = list(filter(bool, running)) running = list(filter(bool, running))
if not running: if not running:
break break
......
...@@ -9,6 +9,7 @@ import sysconfig ...@@ -9,6 +9,7 @@ import sysconfig
import threading import threading
import warnings import warnings
from test import support from test import support
from test.libregrtest.utils import print_warning
try: try:
import _multiprocessing, multiprocessing.process import _multiprocessing, multiprocessing.process
except ImportError: except ImportError:
...@@ -283,8 +284,7 @@ class saved_test_environment: ...@@ -283,8 +284,7 @@ class saved_test_environment:
self.changed = True self.changed = True
restore(original) restore(original)
if not self.quiet and not self.pgo: if not self.quiet and not self.pgo:
print(f"Warning -- {name} was modified by {self.testname}", print_warning(f"{name} was modified by {self.testname}")
file=sys.stderr, flush=True)
print(f" Before: {original}\n After: {current} ", print(f" Before: {original}\n After: {current} ",
file=sys.stderr, flush=True) file=sys.stderr, flush=True)
return False return False
import os.path
import math import math
import os.path
import sys
import textwrap import textwrap
...@@ -54,3 +55,7 @@ def printlist(x, width=70, indent=4, file=None): ...@@ -54,3 +55,7 @@ def printlist(x, width=70, indent=4, file=None):
print(textwrap.fill(' '.join(str(elt) for elt in sorted(x)), width, print(textwrap.fill(' '.join(str(elt) for elt in sorted(x)), width,
initial_indent=blanks, subsequent_indent=blanks), initial_indent=blanks, subsequent_indent=blanks),
file=file) file=file)
def print_warning(msg):
print(f"Warning -- {msg}", file=sys.stderr, flush=True)
import subprocess
import sys
import os
import _winapi import _winapi
import msvcrt import msvcrt
import os
import subprocess
import uuid import uuid
from test import support from test import support
......
...@@ -26,8 +26,9 @@ ROOT_DIR = os.path.join(os.path.dirname(__file__), '..', '..') ...@@ -26,8 +26,9 @@ ROOT_DIR = os.path.join(os.path.dirname(__file__), '..', '..')
ROOT_DIR = os.path.abspath(os.path.normpath(ROOT_DIR)) ROOT_DIR = os.path.abspath(os.path.normpath(ROOT_DIR))
TEST_INTERRUPTED = textwrap.dedent(""" TEST_INTERRUPTED = textwrap.dedent("""
from signal import SIGINT, raise_signal from signal import SIGINT
try: try:
from signal import raise_signal
raise_signal(SIGINT) raise_signal(SIGINT)
except ImportError: except ImportError:
import os import os
...@@ -108,7 +109,7 @@ class ParseArgsTestCase(unittest.TestCase): ...@@ -108,7 +109,7 @@ class ParseArgsTestCase(unittest.TestCase):
self.assertTrue(ns.quiet) self.assertTrue(ns.quiet)
self.assertEqual(ns.verbose, 0) self.assertEqual(ns.verbose, 0)
def test_slow(self): def test_slowest(self):
for opt in '-o', '--slowest': for opt in '-o', '--slowest':
with self.subTest(opt=opt): with self.subTest(opt=opt):
ns = libregrtest._parse_args([opt]) ns = libregrtest._parse_args([opt])
...@@ -780,22 +781,23 @@ class ArgsTestCase(BaseTestCase): ...@@ -780,22 +781,23 @@ class ArgsTestCase(BaseTestCase):
% (self.TESTNAME_REGEX, len(tests))) % (self.TESTNAME_REGEX, len(tests)))
self.check_line(output, regex) self.check_line(output, regex)
def test_slow_interrupted(self): def test_slowest_interrupted(self):
# Issue #25373: test --slowest with an interrupted test # Issue #25373: test --slowest with an interrupted test
code = TEST_INTERRUPTED code = TEST_INTERRUPTED
test = self.create_test("sigint", code=code) test = self.create_test("sigint", code=code)
for multiprocessing in (False, True): for multiprocessing in (False, True):
if multiprocessing: with self.subTest(multiprocessing=multiprocessing):
args = ("--slowest", "-j2", test) if multiprocessing:
else: args = ("--slowest", "-j2", test)
args = ("--slowest", test) else:
output = self.run_tests(*args, exitcode=130) args = ("--slowest", test)
self.check_executed_tests(output, test, output = self.run_tests(*args, exitcode=130)
omitted=test, interrupted=True) self.check_executed_tests(output, test,
omitted=test, interrupted=True)
regex = ('10 slowest tests:\n')
self.check_line(output, regex) regex = ('10 slowest tests:\n')
self.check_line(output, regex)
def test_coverage(self): def test_coverage(self):
# test --coverage # test --coverage
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment