[Python-checkins] [3.7] bpo-36719: sync regrtest with master branch (GH-12967)
Victor Stinner
webhook-mailer at python.org
Fri Apr 26 06:16:41 EDT 2019
https://github.com/python/cpython/commit/1069d38fa18f3a4f97c2e358bcb3b82cab1c051b
commit: 1069d38fa18f3a4f97c2e358bcb3b82cab1c051b
branch: 3.7
author: Victor Stinner <vstinner at redhat.com>
committer: GitHub <noreply at github.com>
date: 2019-04-26T12:16:30+02:00
summary:
[3.7] bpo-36719: sync regrtest with master branch (GH-12967)
* Clean up code which checked presence of os.{stat,lstat,chmod} (GH-11643)
(cherry picked from commit 8377cd4fcd0d51d86834c9b0518d29aac3b49e18)
* 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
(cherry picked from commit 4d29983185bc12ca685a1eb3873bacb8a7b67416)
* bpo-36725: Refactor regrtest multiprocessing code (GH-12961)
Rewrite run_tests_multiprocess() function as a new MultiprocessRunner
class with multiple methods to better report errors and stop
immediately when needed.
Changes:
* Worker processes are now killed immediately if tests are
interrupted or if a test does crash (CHILD_ERROR): worker
processes are killed.
* Rewrite how errors in a worker thread are reported to
the main thread. No longer ignore BaseException or parsing errors
silently.
* Remove 'finished' variable: use worker.is_alive() instead
* Always compute omitted tests. Add Regrtest.get_executed() method.
(cherry picked from commit 3cde440f20a9db75fb2c4e65e8e4d04a53216a2d)
* bpo-36719: regrtest always detect uncollectable objects (GH-12951)
regrtest now always detects uncollectable objects. Previously, the
check was only enabled by --findleaks. The check now also works with
-jN/--multiprocess N.
--findleaks becomes a deprecated alias to --fail-env-changed.
(cherry picked from commit 75120d2205af086140e5e4e2dc620eb19cdf9078)
* bpo-34060: Report system load when running test suite for Windows (GH-8357)
While Windows exposes the system processor queue length, the raw value
used for load calculations on Unix systems, it does not provide an API
to access the averaged value. Hence to calculate the load we must track
and average it ourselves. We can't use multiprocessing or a thread to
read it in the background while the tests run since using those would
conflict with test_multiprocessing and test_xxsubprocess.
Thus, we use Window's asynchronous IO API to run the tracker in the
background with it sampling at the correct rate. When we wish to access
the load we check to see if there's new data on the stream, if there is,
we update our load values.
(cherry picked from commit e16467af0bfcc9f399df251495ff2d2ad20a1669)
* bpo-36719: Fix regrtest re-run (GH-12964)
Properly handle a test which fail but then pass.
Add test_rerun_success() unit test.
(cherry picked from commit 837acc1957d86ca950433f5064fd06d09b57d23b)
* bpo-36719: regrtest closes explicitly WindowsLoadTracker (GH-12965)
Regrtest.finalize() now closes explicitly the WindowsLoadTracker
instance.
(cherry picked from commit 00db7c73af4f60df61e9df87cde7401c3ed9df69)
files:
A Lib/test/libregrtest/win_utils.py
A Misc/NEWS.d/next/Library/2019-01-21-13-56-55.bpo-35802.6633PE.rst
A Misc/NEWS.d/next/Tests/2019-04-26-04-12-29.bpo-36725.B8-ghi.rst
A Misc/NEWS.d/next/Tests/2019-04-26-09-02-49.bpo-36719.ys2uqH.rst
A Misc/NEWS.d/next/Windows/2018-07-20-13-09-19.bpo-34060.v-z87j.rst
M Lib/test/libregrtest/cmdline.py
M Lib/test/libregrtest/main.py
M Lib/test/libregrtest/refleak.py
M Lib/test/libregrtest/runtest.py
M Lib/test/libregrtest/runtest_mp.py
M Lib/test/libregrtest/save_env.py
M Lib/test/libregrtest/utils.py
M Lib/test/test_regrtest.py
diff --git a/Lib/test/libregrtest/cmdline.py b/Lib/test/libregrtest/cmdline.py
index 7cd85bf2803a..cb09ee0e03b3 100644
--- a/Lib/test/libregrtest/cmdline.py
+++ b/Lib/test/libregrtest/cmdline.py
@@ -226,8 +226,9 @@ def _create_parser():
'(instead of the Python stdlib test suite)')
group = parser.add_argument_group('Special runs')
- group.add_argument('-l', '--findleaks', action='store_true',
- help='if GC is available detect tests that leak memory')
+ group.add_argument('-l', '--findleaks', action='store_const', const=2,
+ default=1,
+ help='deprecated alias to --fail-env-changed')
group.add_argument('-L', '--runleaks', action='store_true',
help='run the leaks(1) command just before exit.' +
more_details)
@@ -309,7 +310,7 @@ def _parse_args(args, **kwargs):
# Defaults
ns = argparse.Namespace(testdir=None, verbose=0, quiet=False,
exclude=False, single=False, randomize=False, fromfile=None,
- findleaks=False, use_resources=None, trace=False, coverdir='coverage',
+ findleaks=1, use_resources=None, trace=False, coverdir='coverage',
runleaks=False, huntrleaks=False, verbose2=False, print_slow=False,
random_seed=None, use_mp=None, verbose3=False, forever=False,
header=False, failfast=False, match_tests=None, pgo=False)
@@ -330,12 +331,13 @@ def _parse_args(args, **kwargs):
parser.error("unrecognized arguments: %s" % arg)
sys.exit(1)
+ if ns.findleaks > 1:
+ # --findleaks implies --fail-env-changed
+ ns.fail_env_changed = True
if ns.single and ns.fromfile:
parser.error("-s and -f don't go together!")
if ns.use_mp is not None and ns.trace:
parser.error("-T and -j don't go together!")
- if ns.use_mp is not None and ns.findleaks:
- parser.error("-l and -j don't go together!")
if ns.failfast and not (ns.verbose or ns.verbose3):
parser.error("-G/--failfast needs either -v or -W")
if ns.pgo and (ns.verbose or ns.verbose2 or ns.verbose3):
diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py
index 32ac44029bc3..c19ea44db9b2 100644
--- a/Lib/test/libregrtest/main.py
+++ b/Lib/test/libregrtest/main.py
@@ -20,10 +20,6 @@
from test.libregrtest.setup import setup_tests
from test.libregrtest.utils import removepy, count, format_duration, printlist
from test import support
-try:
- import gc
-except ImportError:
- gc = None
# When tests are run from the Python build directory, it is best practice
@@ -79,8 +75,8 @@ def __init__(self):
self.skipped = []
self.resource_denieds = []
self.environment_changed = []
- self.rerun = []
self.run_no_tests = []
+ self.rerun = []
self.first_result = None
self.interrupted = False
@@ -90,9 +86,6 @@ def __init__(self):
# used by --coverage, trace.Trace instance
self.tracer = None
- # used by --findleaks, store for gc.garbage
- self.found_garbage = []
-
# used to display the progress bar "[ 3/100]"
self.start_time = time.monotonic()
self.test_count = ''
@@ -105,26 +98,43 @@ def __init__(self):
# used by --junit-xml
self.testsuite_xml = None
- def accumulate_result(self, test, result):
- ok, test_time, xml_data = result
- if ok not in (CHILD_ERROR, INTERRUPTED):
- self.test_times.append((test_time, test))
+ self.win_load_tracker = None
+
+ def get_executed(self):
+ return (set(self.good) | set(self.bad) | set(self.skipped)
+ | set(self.resource_denieds) | set(self.environment_changed)
+ | set(self.run_no_tests))
+
+ def accumulate_result(self, result, rerun=False):
+ test_name = result.test_name
+ ok = result.result
+
+ if ok not in (CHILD_ERROR, INTERRUPTED) and not rerun:
+ self.test_times.append((result.test_time, test_name))
+
if ok == PASSED:
- self.good.append(test)
+ self.good.append(test_name)
elif ok in (FAILED, CHILD_ERROR):
- self.bad.append(test)
+ if not rerun:
+ self.bad.append(test_name)
elif ok == ENV_CHANGED:
- self.environment_changed.append(test)
+ self.environment_changed.append(test_name)
elif ok == SKIPPED:
- self.skipped.append(test)
+ self.skipped.append(test_name)
elif ok == RESOURCE_DENIED:
- self.skipped.append(test)
- self.resource_denieds.append(test)
+ self.skipped.append(test_name)
+ self.resource_denieds.append(test_name)
elif ok == TEST_DID_NOT_RUN:
- self.run_no_tests.append(test)
- elif ok != INTERRUPTED:
+ self.run_no_tests.append(test_name)
+ elif ok == INTERRUPTED:
+ self.interrupted = True
+ else:
raise ValueError("invalid test result: %r" % ok)
+ if rerun and ok not in {FAILED, CHILD_ERROR, INTERRUPTED}:
+ self.bad.remove(test_name)
+
+ xml_data = result.xml_data
if xml_data:
import xml.etree.ElementTree as ET
for e in xml_data:
@@ -134,7 +144,7 @@ def accumulate_result(self, test, result):
print(xml_data, file=sys.__stderr__)
raise
- def display_progress(self, test_index, test):
+ def display_progress(self, test_index, text):
if self.ns.quiet:
return
@@ -143,12 +153,12 @@ def display_progress(self, test_index, test):
fails = len(self.bad) + len(self.environment_changed)
if fails and not self.ns.pgo:
line = f"{line}/{fails}"
- line = f"[{line}] {test}"
+ line = f"[{line}] {text}"
# add the system load prefix: "load avg: 1.80 "
- if hasattr(os, 'getloadavg'):
- load_avg_1min = os.getloadavg()[0]
- line = f"load avg: {load_avg_1min:.2f} {line}"
+ load_avg = self.getloadavg()
+ if load_avg is not None:
+ line = f"load avg: {load_avg:.2f} {line}"
# add the timestamp prefix: "0:01:05 "
test_time = time.monotonic() - self.start_time
@@ -164,22 +174,6 @@ def parse_args(self, kwargs):
"faulthandler.dump_traceback_later", file=sys.stderr)
ns.timeout = None
- if ns.threshold is not None and gc is None:
- print('No GC available, ignore --threshold.', file=sys.stderr)
- ns.threshold = None
-
- if ns.findleaks:
- if gc is not None:
- # Uncomment the line below to report garbage that is not
- # freeable by reference counting alone. By default only
- # garbage that is not collectable by the GC is reported.
- pass
- #gc.set_debug(gc.DEBUG_SAVEALL)
- else:
- print('No GC available, disabling --findleaks',
- file=sys.stderr)
- ns.findleaks = False
-
if ns.xmlpath:
support.junit_xml_list = self.testsuite_xml = []
@@ -275,13 +269,13 @@ def list_cases(self):
support.verbose = False
support.set_match_tests(self.ns.match_tests)
- for test in self.selected:
- abstest = get_abs_module(self.ns, test)
+ for test_name in self.selected:
+ abstest = get_abs_module(self.ns, test_name)
try:
suite = unittest.defaultTestLoader.loadTestsFromName(abstest)
self._list_cases(suite)
except unittest.SkipTest:
- self.skipped.append(test)
+ self.skipped.append(test_name)
if self.skipped:
print(file=sys.stderr)
@@ -298,23 +292,19 @@ def rerun_failed_tests(self):
print()
print("Re-running failed tests in verbose mode")
self.rerun = self.bad[:]
- for test in self.rerun:
- print("Re-running test %r in verbose mode" % test, flush=True)
- try:
- self.ns.verbose = True
- ok = runtest(self.ns, test)
- except KeyboardInterrupt:
- self.interrupted = True
- # print a newline separate from the ^C
- print()
+ for test_name in self.rerun:
+ print(f"Re-running {test_name} in verbose mode", flush=True)
+ self.ns.verbose = True
+ result = runtest(self.ns, test_name)
+
+ self.accumulate_result(result, rerun=True)
+
+ if result.result == INTERRUPTED:
break
- else:
- if ok[0] in {PASSED, ENV_CHANGED, SKIPPED, RESOURCE_DENIED}:
- self.bad.remove(test)
- else:
- if self.bad:
- print(count(len(self.bad), 'test'), "failed again:")
- printlist(self.bad)
+
+ if self.bad:
+ print(count(len(self.bad), 'test'), "failed again:")
+ printlist(self.bad)
self.display_result()
@@ -327,11 +317,11 @@ def display_result(self):
print("== Tests result: %s ==" % self.get_tests_result())
if self.interrupted:
- print()
- # print a newline after ^C
print("Test suite interrupted by signal SIGINT.")
- executed = set(self.good) | set(self.bad) | set(self.skipped)
- omitted = set(self.selected) - executed
+
+ omitted = set(self.selected) - self.get_executed()
+ if omitted:
+ print()
print(count(len(omitted), "test"), "omitted:")
printlist(omitted)
@@ -348,8 +338,8 @@ def display_result(self):
self.test_times.sort(reverse=True)
print()
print("10 slowest tests:")
- for time, test in self.test_times[:10]:
- print("- %s: %s" % (test, format_duration(time)))
+ for test_time, test in self.test_times[:10]:
+ print("- %s: %s" % (test, format_duration(test_time)))
if self.bad:
print()
@@ -387,10 +377,10 @@ def run_tests_sequential(self):
print("Run tests sequentially")
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()
- text = test
+ text = test_name
if previous_test:
text = '%s -- %s' % (text, previous_test)
self.display_progress(test_index, text)
@@ -398,22 +388,19 @@ def run_tests_sequential(self):
if self.tracer:
# If we're tracing code coverage, then we don't exit with status
# if on a false return value from main.
- cmd = ('result = runtest(self.ns, test); '
- 'self.accumulate_result(test, result)')
+ cmd = ('result = runtest(self.ns, test_name); '
+ 'self.accumulate_result(result)')
ns = dict(locals())
self.tracer.runctx(cmd, globals=globals(), locals=ns)
result = ns['result']
else:
- try:
- result = runtest(self.ns, test)
- except KeyboardInterrupt:
- self.interrupted = True
- self.accumulate_result(test, (INTERRUPTED, None, None))
- break
- else:
- self.accumulate_result(test, result)
-
- previous_test = format_test_result(test, result[0])
+ result = runtest(self.ns, test_name)
+ self.accumulate_result(result)
+
+ if result.result == INTERRUPTED:
+ break
+
+ previous_test = format_test_result(result)
test_time = time.monotonic() - start_time
if test_time >= PROGRESS_MIN_TIME:
previous_test = "%s in %s" % (previous_test, format_duration(test_time))
@@ -421,16 +408,6 @@ def run_tests_sequential(self):
# be quiet: say nothing if the test passed shortly
previous_test = None
- if self.ns.findleaks:
- gc.collect()
- if gc.garbage:
- print("Warning: test created", len(gc.garbage), end=' ')
- print("uncollectable object(s).")
- # move the uncollectable objects somewhere so we don't see
- # them again
- self.found_garbage.extend(gc.garbage)
- del gc.garbage[:]
-
# Unload the newly imported modules (best effort finalization)
for module in sys.modules.keys():
if module not in save_modules and module.startswith("test."):
@@ -441,8 +418,8 @@ def run_tests_sequential(self):
def _test_forever(self, tests):
while True:
- for test in tests:
- yield test
+ for test_name in tests:
+ yield test_name
if self.bad:
return
if self.ns.fail_env_changed and self.environment_changed:
@@ -515,6 +492,10 @@ def run_tests(self):
self.run_tests_sequential()
def finalize(self):
+ if self.win_load_tracker is not None:
+ self.win_load_tracker.close()
+ self.win_load_tracker = None
+
if self.next_single_filename:
if self.next_single_test:
with open(self.next_single_filename, 'w') as fp:
@@ -585,6 +566,15 @@ def main(self, tests=None, **kwargs):
with support.temp_cwd(test_cwd, quiet=True):
self._main(tests, kwargs)
+ def getloadavg(self):
+ if self.win_load_tracker is not None:
+ return self.win_load_tracker.getloadavg()
+
+ if hasattr(os, 'getloadavg'):
+ return os.getloadavg()[0]
+
+ return None
+
def _main(self, tests, kwargs):
if self.ns.huntrleaks:
warmup, repetitions, _ = self.ns.huntrleaks
@@ -616,6 +606,18 @@ def _main(self, tests, kwargs):
self.list_cases()
sys.exit(0)
+ # If we're on windows and this is the parent runner (not a worker),
+ # track the load average.
+ if sys.platform == 'win32' and (self.ns.worker_args is None):
+ from test.libregrtest.win_utils import WindowsLoadTracker
+
+ try:
+ self.win_load_tracker = WindowsLoadTracker()
+ except FileNotFoundError as error:
+ # Windows IoT Core and Windows Nano Server do not provide
+ # typeperf.exe for x64, x86 or ARM
+ print(f'Failed to create WindowsLoadTracker: {error}')
+
self.run_tests()
self.display_result()
diff --git a/Lib/test/libregrtest/refleak.py b/Lib/test/libregrtest/refleak.py
index 235d6bfd3af6..8d221232eb6c 100644
--- a/Lib/test/libregrtest/refleak.py
+++ b/Lib/test/libregrtest/refleak.py
@@ -1,4 +1,3 @@
-import errno
import os
import re
import sys
@@ -18,7 +17,7 @@ def _get_dump(cls):
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.
Returns:
diff --git a/Lib/test/libregrtest/runtest.py b/Lib/test/libregrtest/runtest.py
index 99486c72db3e..a9574929a4cd 100644
--- a/Lib/test/libregrtest/runtest.py
+++ b/Lib/test/libregrtest/runtest.py
@@ -1,4 +1,7 @@
+import collections
import faulthandler
+import functools
+import gc
import importlib
import io
import os
@@ -6,9 +9,11 @@
import time
import traceback
import unittest
+
from test import support
from test.libregrtest.refleak import dash_R, clear_caches
from test.libregrtest.save_env import saved_test_environment
+from test.libregrtest.utils import print_warning
# Test result constants.
@@ -55,9 +60,17 @@
NOTTESTS = set()
-def format_test_result(test_name, result):
- fmt = _FORMAT_TEST_RESULT.get(result, "%s")
- return fmt % test_name
+# used by --findleaks, store for gc.garbage
+FOUND_GARBAGE = []
+
+
+def format_test_result(result):
+ fmt = _FORMAT_TEST_RESULT.get(result.result, "%s")
+ return fmt % result.test_name
+
+
+def findtestdir(path=None):
+ return path or os.path.dirname(os.path.dirname(__file__)) or os.curdir
def findtests(testdir=None, stdtests=STDTESTS, nottests=NOTTESTS):
@@ -73,48 +86,34 @@ def findtests(testdir=None, stdtests=STDTESTS, nottests=NOTTESTS):
return stdtests + sorted(tests)
-def get_abs_module(ns, test):
- if test.startswith('test.') or ns.testdir:
- return test
+def get_abs_module(ns, test_name):
+ if test_name.startswith('test.') or ns.testdir:
+ return test_name
else:
- # Always import it from the test package
- return 'test.' + test
-
-
-def runtest(ns, test):
- """Run a single test.
+ # Import it from the test package
+ return 'test.' + test_name
- ns -- regrtest namespace of options
- test -- the name of the test
- Returns the tuple (result, test_time, xml_data), where result is one
- of the constants:
+TestResult = collections.namedtuple('TestResult',
+ 'test_name result test_time xml_data')
- INTERRUPTED KeyboardInterrupt when run under -j
- RESOURCE_DENIED test skipped because resource denied
- SKIPPED test skipped for some other reason
- ENV_CHANGED test failed because it changed the execution environment
- FAILED test failed
- PASSED test passed
- EMPTY_TEST_SUITE test ran no subtests.
-
- If ns.xmlpath is not None, xml_data is a list containing each
- generated testsuite element.
- """
+def _runtest(ns, test_name):
+ # Handle faulthandler timeout, capture stdout+stderr, XML serialization
+ # and measure time.
output_on_failure = ns.verbose3
use_timeout = (ns.timeout is not None)
if use_timeout:
faulthandler.dump_traceback_later(ns.timeout, exit=True)
+
+ start_time = time.perf_counter()
try:
support.set_match_tests(ns.match_tests)
- # reset the environment_altered flag to detect if a test altered
- # the environment
- support.environment_altered = False
support.junit_xml_list = xml_list = [] if ns.xmlpath else None
if ns.failfast:
support.failfast = True
+
if output_on_failure:
support.verbose = True
@@ -124,8 +123,9 @@ def runtest(ns, test):
try:
sys.stdout = stream
sys.stderr = stream
- result = runtest_inner(ns, test, display_failure=False)
- if result[0] != PASSED:
+ result = _runtest_inner(ns, test_name,
+ display_failure=False)
+ if result != PASSED:
output = stream.getvalue()
orig_stderr.write(output)
orig_stderr.flush()
@@ -133,98 +133,164 @@ def runtest(ns, test):
sys.stdout = orig_stdout
sys.stderr = orig_stderr
else:
- support.verbose = ns.verbose # Tell tests to be moderately quiet
- result = runtest_inner(ns, test, display_failure=not ns.verbose)
+ # Tell tests to be moderately quiet
+ support.verbose = ns.verbose
+
+ result = _runtest_inner(ns, test_name,
+ display_failure=not ns.verbose)
if xml_list:
import xml.etree.ElementTree as ET
xml_data = [ET.tostring(x).decode('us-ascii') for x in xml_list]
else:
xml_data = None
- return result + (xml_data,)
+
+ test_time = time.perf_counter() - start_time
+
+ return TestResult(test_name, result, test_time, xml_data)
finally:
if use_timeout:
faulthandler.cancel_dump_traceback_later()
- cleanup_test_droppings(test, ns.verbose)
support.junit_xml_list = None
-def post_test_cleanup():
+def runtest(ns, test_name):
+ """Run a single test.
+
+ ns -- regrtest namespace of options
+ test_name -- the name of the test
+
+ Returns the tuple (result, test_time, xml_data), where result is one
+ of the constants:
+
+ INTERRUPTED KeyboardInterrupt
+ RESOURCE_DENIED test skipped because resource denied
+ SKIPPED test skipped for some other reason
+ ENV_CHANGED test failed because it changed the execution environment
+ FAILED test failed
+ PASSED test passed
+ EMPTY_TEST_SUITE test ran no subtests.
+
+ If ns.xmlpath is not None, xml_data is a list containing each
+ generated testsuite element.
+ """
+ try:
+ return _runtest(ns, test_name)
+ except:
+ if not ns.pgo:
+ msg = traceback.format_exc()
+ print(f"test {test_name} crashed -- {msg}",
+ file=sys.stderr, flush=True)
+ return TestResult(test_name, FAILED, 0.0, None)
+
+
+def _test_module(the_module):
+ loader = unittest.TestLoader()
+ tests = loader.loadTestsFromModule(the_module)
+ for error in loader.errors:
+ print(error, file=sys.stderr)
+ if loader.errors:
+ raise Exception("errors while loading tests")
+ support.run_unittest(tests)
+
+
+def _runtest_inner2(ns, test_name):
+ # Load the test function, run the test function, handle huntrleaks
+ # and findleaks to detect leaks
+
+ abstest = get_abs_module(ns, test_name)
+
+ # remove the module from sys.module to reload it if it was already imported
+ support.unload(abstest)
+
+ the_module = importlib.import_module(abstest)
+
+ # If the test has a test_main, that will run the appropriate
+ # tests. If not, use normal unittest test loading.
+ test_runner = getattr(the_module, "test_main", None)
+ if test_runner is None:
+ test_runner = functools.partial(_test_module, the_module)
+
+ try:
+ if ns.huntrleaks:
+ # Return True if the test leaked references
+ refleak = dash_R(ns, test_name, test_runner)
+ else:
+ test_runner()
+ refleak = False
+ finally:
+ cleanup_test_droppings(test_name, ns.verbose)
+
+ support.gc_collect()
+
+ if gc.garbage:
+ support.environment_altered = True
+ print_warning(f"{test_name} created {len(gc.garbage)} "
+ f"uncollectable object(s).")
+
+ # move the uncollectable objects somewhere,
+ # so we don't see them again
+ FOUND_GARBAGE.extend(gc.garbage)
+ gc.garbage.clear()
+
support.reap_children()
+ return refleak
+
+
+def _runtest_inner(ns, test_name, display_failure=True):
+ # Detect environment changes, handle exceptions.
-def runtest_inner(ns, test, display_failure=True):
- support.unload(test)
+ # Reset the environment_altered flag to detect if a test altered
+ # the environment
+ support.environment_altered = False
+
+ if ns.pgo:
+ display_failure = False
- test_time = 0.0
- refleak = False # True if the test leaked references.
try:
- abstest = get_abs_module(ns, test)
clear_caches()
- with saved_test_environment(test, ns.verbose, ns.quiet, pgo=ns.pgo) as environment:
- start_time = time.perf_counter()
- the_module = importlib.import_module(abstest)
- # If the test has a test_main, that will run the appropriate
- # tests. If not, use normal unittest test loading.
- test_runner = getattr(the_module, "test_main", None)
- if test_runner is None:
- def test_runner():
- loader = unittest.TestLoader()
- tests = loader.loadTestsFromModule(the_module)
- for error in loader.errors:
- print(error, file=sys.stderr)
- if loader.errors:
- raise Exception("errors while loading tests")
- support.run_unittest(tests)
- if ns.huntrleaks:
- refleak = dash_R(ns, the_module, test, test_runner)
- else:
- test_runner()
- test_time = time.perf_counter() - start_time
- post_test_cleanup()
+
+ with saved_test_environment(test_name, ns.verbose, ns.quiet, pgo=ns.pgo) as environment:
+ refleak = _runtest_inner2(ns, test_name)
except support.ResourceDenied as msg:
if not ns.quiet and not ns.pgo:
- print(test, "skipped --", msg, flush=True)
- return RESOURCE_DENIED, test_time
+ print(f"{test_name} skipped -- {msg}", flush=True)
+ return RESOURCE_DENIED
except unittest.SkipTest as msg:
if not ns.quiet and not ns.pgo:
- print(test, "skipped --", msg, flush=True)
- return SKIPPED, test_time
- except KeyboardInterrupt:
- raise
- except support.TestFailed as msg:
- if not ns.pgo:
- if display_failure:
- print("test", test, "failed --", msg, file=sys.stderr,
- flush=True)
- else:
- print("test", test, "failed", file=sys.stderr, flush=True)
- return FAILED, test_time
+ print(f"{test_name} skipped -- {msg}", flush=True)
+ return SKIPPED
+ except support.TestFailed as exc:
+ msg = f"test {test_name} failed"
+ if display_failure:
+ msg = f"{msg} -- {exc}"
+ print(msg, file=sys.stderr, flush=True)
+ return FAILED
except support.TestDidNotRun:
- return TEST_DID_NOT_RUN, test_time
+ return TEST_DID_NOT_RUN
+ except KeyboardInterrupt:
+ print()
+ return INTERRUPTED
except:
- msg = traceback.format_exc()
if not ns.pgo:
- print("test", test, "crashed --", msg, file=sys.stderr,
- flush=True)
- return FAILED, test_time
- else:
- if refleak:
- return FAILED, test_time
- if environment.changed:
- return ENV_CHANGED, test_time
- return PASSED, test_time
+ msg = traceback.format_exc()
+ print(f"test {test_name} crashed -- {msg}",
+ file=sys.stderr, flush=True)
+ return FAILED
+ if refleak:
+ return FAILED
+ if environment.changed:
+ return ENV_CHANGED
+ return PASSED
-def cleanup_test_droppings(testname, verbose):
- import shutil
- import stat
- import gc
+def cleanup_test_droppings(test_name, verbose):
# First kill any dangling references to open files etc.
# This can also issue some ResourceWarnings which would otherwise get
# triggered during the following test run, and possibly produce failures.
- gc.collect()
+ support.gc_collect()
# Try to clean up junk commonly left behind. While tests shouldn't leave
# any files or directories behind, when a test fails that can be tedious
@@ -239,25 +305,23 @@ def cleanup_test_droppings(testname, verbose):
continue
if os.path.isdir(name):
+ import shutil
kind, nuker = "directory", shutil.rmtree
elif os.path.isfile(name):
kind, nuker = "file", os.unlink
else:
- raise SystemError("os.path says %r exists but is neither "
- "directory nor file" % name)
+ raise RuntimeError(f"os.path says {name!r} exists but is neither "
+ f"directory nor file")
if verbose:
- print("%r left behind %s %r" % (testname, kind, name))
+ print_warning("%r left behind %s %r" % (test_name, kind, name))
+ support.environment_altered = True
+
try:
- # if we have chmod, fix possible permissions problems
- # that might prevent cleanup
- if (hasattr(os, 'chmod')):
- os.chmod(name, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
+ import stat
+ # fix possible permissions problems that might prevent cleanup
+ os.chmod(name, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
nuker(name)
- except Exception as msg:
- print(("%r left behind %s %r and it couldn't be "
- "removed: %s" % (testname, kind, name, msg)), file=sys.stderr)
-
-
-def findtestdir(path=None):
- return path or os.path.dirname(os.path.dirname(__file__)) or os.curdir
+ except Exception as exc:
+ print_warning(f"{test_name} left behind {kind} {name!r} "
+ f"and it couldn't be removed: {exc}")
diff --git a/Lib/test/libregrtest/runtest_mp.py b/Lib/test/libregrtest/runtest_mp.py
index 6190574afdf8..dbab6954de86 100644
--- a/Lib/test/libregrtest/runtest_mp.py
+++ b/Lib/test/libregrtest/runtest_mp.py
@@ -1,7 +1,9 @@
+import collections
import faulthandler
import json
import os
import queue
+import subprocess
import sys
import threading
import time
@@ -11,7 +13,7 @@
from test.libregrtest.runtest import (
runtest, INTERRUPTED, CHILD_ERROR, PROGRESS_MIN_TIME,
- format_test_result)
+ format_test_result, TestResult)
from test.libregrtest.setup import setup_tests
from test.libregrtest.utils import format_duration
@@ -19,20 +21,12 @@
# Display the running tests if nothing happened last N seconds
PROGRESS_UPDATE = 30.0 # seconds
-# If interrupted, display the wait progress every N seconds
-WAIT_PROGRESS = 2.0 # seconds
+def must_stop(result):
+ return result.result in (INTERRUPTED, CHILD_ERROR)
-def run_test_in_subprocess(testname, ns):
- """Run the given test in a subprocess with --worker-args.
-
- ns is the option Namespace parsed from command-line arguments. regrtest
- is invoked in a subprocess with the --worker-args argument; when the
- subprocess exits, its return code, stdout and stderr are returned as a
- 3-tuple.
- """
- from subprocess import Popen, PIPE
+def run_test_in_subprocess(testname, ns):
ns_dict = vars(ns)
worker_args = (ns_dict, testname)
worker_args = json.dumps(worker_args)
@@ -47,15 +41,12 @@ def run_test_in_subprocess(testname, ns):
# Running the child from the same working directory as regrtest's original
# invocation ensures that TEMPDIR for the child is the same when
# sysconfig.is_python_build() is true. See issue 15300.
- popen = Popen(cmd,
- stdout=PIPE, stderr=PIPE,
- universal_newlines=True,
- close_fds=(os.name != 'nt'),
- cwd=support.SAVEDCWD)
- with popen:
- stdout, stderr = popen.communicate()
- retcode = popen.wait()
- return retcode, stdout, stderr
+ return subprocess.Popen(cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ universal_newlines=True,
+ close_fds=(os.name != 'nt'),
+ cwd=support.SAVEDCWD)
def run_tests_worker(worker_args):
@@ -64,14 +55,7 @@ def run_tests_worker(worker_args):
setup_tests(ns)
- try:
- result = runtest(ns, testname)
- except KeyboardInterrupt:
- result = INTERRUPTED, '', None
- except BaseException as e:
- traceback.print_exc()
- result = CHILD_ERROR, str(e)
-
+ result = runtest(ns, testname)
print() # Force a newline (just in case)
print(json.dumps(result), flush=True)
sys.exit(0)
@@ -83,7 +67,6 @@ class MultiprocessIterator:
"""A thread-safe iterator over tests for multiprocess mode."""
def __init__(self, tests):
- self.interrupted = False
self.lock = threading.Lock()
self.tests = tests
@@ -92,152 +75,213 @@ def __iter__(self):
def __next__(self):
with self.lock:
- if self.interrupted:
- raise StopIteration('tests interrupted')
return next(self.tests)
+MultiprocessResult = collections.namedtuple('MultiprocessResult',
+ 'result stdout stderr error_msg')
+
class MultiprocessThread(threading.Thread):
def __init__(self, pending, output, ns):
super().__init__()
self.pending = pending
self.output = output
self.ns = ns
- self.current_test = None
+ self.current_test_name = None
self.start_time = None
+ self._popen = None
- def _runtest(self):
- try:
- test = next(self.pending)
- except StopIteration:
- self.output.put((None, None, None, None))
- return True
+ def kill(self):
+ if not self.is_alive():
+ return
+ if self._popen is not None:
+ self._popen.kill()
+ def _runtest(self, test_name):
try:
self.start_time = time.monotonic()
- self.current_test = test
-
- retcode, stdout, stderr = run_test_in_subprocess(test, self.ns)
+ self.current_test_name = test_name
+
+ popen = run_test_in_subprocess(test_name, self.ns)
+ self._popen = popen
+ with popen:
+ try:
+ stdout, stderr = popen.communicate()
+ except:
+ popen.kill()
+ popen.wait()
+ raise
+
+ retcode = popen.wait()
finally:
- self.current_test = None
+ self.current_test_name = None
+ self._popen = None
- if retcode != 0:
- result = (CHILD_ERROR, "Exit code %s" % retcode, None)
- self.output.put((test, stdout.rstrip(), stderr.rstrip(),
- result))
- return False
-
- stdout, _, result = stdout.strip().rpartition("\n")
- if not result:
- self.output.put((None, None, None, None))
- return True
+ stdout = stdout.strip()
+ stderr = stderr.rstrip()
- result = json.loads(result)
- assert len(result) == 3, f"Invalid result tuple: {result!r}"
- self.output.put((test, stdout.rstrip(), stderr.rstrip(),
- result))
- return False
+ err_msg = None
+ if retcode != 0:
+ err_msg = "Exit code %s" % retcode
+ else:
+ stdout, _, result = stdout.rpartition("\n")
+ stdout = stdout.rstrip()
+ if not result:
+ err_msg = "Failed to parse worker stdout"
+ else:
+ try:
+ # deserialize run_tests_worker() output
+ result = json.loads(result)
+ result = TestResult(*result)
+ except Exception as exc:
+ err_msg = "Failed to parse worker JSON: %s" % exc
+
+ if err_msg is not None:
+ test_time = time.monotonic() - self.start_time
+ result = TestResult(test_name, CHILD_ERROR, test_time, None)
+
+ return MultiprocessResult(result, stdout, stderr, err_msg)
def run(self):
- try:
- stop = False
- while not stop:
- stop = self._runtest()
- except BaseException:
- self.output.put((None, None, None, None))
- raise
+ while True:
+ try:
+ try:
+ test_name = next(self.pending)
+ except StopIteration:
+ break
+ mp_result = self._runtest(test_name)
+ self.output.put((False, mp_result))
-def run_tests_multiprocess(regrtest):
- output = queue.Queue()
- pending = MultiprocessIterator(regrtest.tests)
- test_timeout = regrtest.ns.timeout
- use_timeout = (test_timeout is not None)
-
- workers = [MultiprocessThread(pending, output, regrtest.ns)
- for i in range(regrtest.ns.use_mp)]
- print("Run tests in parallel using %s child processes"
- % len(workers))
+ if must_stop(mp_result.result):
+ break
+ except BaseException:
+ self.output.put((True, traceback.format_exc()))
+ break
+
+
+def get_running(workers):
+ running = []
for worker in workers:
- worker.start()
-
- def get_running(workers):
- running = []
- for worker in workers:
- current_test = worker.current_test
- if not current_test:
- continue
- dt = time.monotonic() - worker.start_time
- if dt >= PROGRESS_MIN_TIME:
- text = '%s (%s)' % (current_test, format_duration(dt))
- running.append(text)
- return running
-
- finished = 0
- test_index = 1
- get_timeout = max(PROGRESS_UPDATE, PROGRESS_MIN_TIME)
- try:
- while finished < regrtest.ns.use_mp:
- if use_timeout:
- faulthandler.dump_traceback_later(test_timeout, exit=True)
+ current_test_name = worker.current_test_name
+ if not current_test_name:
+ continue
+ dt = time.monotonic() - worker.start_time
+ if dt >= PROGRESS_MIN_TIME:
+ text = '%s (%s)' % (current_test_name, format_duration(dt))
+ running.append(text)
+ return running
+
+
+class MultiprocessRunner:
+ def __init__(self, regrtest):
+ self.regrtest = regrtest
+ self.ns = regrtest.ns
+ self.output = queue.Queue()
+ self.pending = MultiprocessIterator(self.regrtest.tests)
+ if self.ns.timeout is not None:
+ self.test_timeout = self.ns.timeout * 1.5
+ else:
+ self.test_timeout = None
+ self.workers = None
+
+ def start_workers(self):
+ self.workers = [MultiprocessThread(self.pending, self.output, self.ns)
+ for _ in range(self.ns.use_mp)]
+ print("Run tests in parallel using %s child processes"
+ % len(self.workers))
+ for worker in self.workers:
+ worker.start()
+
+ def wait_workers(self):
+ for worker in self.workers:
+ worker.kill()
+ for worker in self.workers:
+ worker.join()
+
+ def _get_result(self):
+ if not any(worker.is_alive() for worker in self.workers):
+ # all worker threads are done: consume pending results
+ try:
+ return self.output.get(timeout=0)
+ except queue.Empty:
+ return None
+
+ while True:
+ if self.test_timeout is not None:
+ faulthandler.dump_traceback_later(self.test_timeout, exit=True)
+ # wait for a thread
+ timeout = max(PROGRESS_UPDATE, PROGRESS_MIN_TIME)
try:
- item = output.get(timeout=get_timeout)
+ return self.output.get(timeout=timeout)
except queue.Empty:
- running = get_running(workers)
- if running and not regrtest.ns.pgo:
- print('running: %s' % ', '.join(running), flush=True)
- continue
-
- test, stdout, stderr, result = item
- if test is None:
- finished += 1
- continue
- regrtest.accumulate_result(test, result)
-
- # Display progress
- ok, test_time, xml_data = result
- text = format_test_result(test, ok)
- if (ok not in (CHILD_ERROR, INTERRUPTED)
- and test_time >= PROGRESS_MIN_TIME
- and not regrtest.ns.pgo):
- text += ' (%s)' % format_duration(test_time)
- elif ok == CHILD_ERROR:
- text = '%s (%s)' % (text, test_time)
- running = get_running(workers)
- if running and not regrtest.ns.pgo:
- text += ' -- running: %s' % ', '.join(running)
- regrtest.display_progress(test_index, text)
-
- # Copy stdout and stderr from the child process
- if stdout:
- print(stdout, flush=True)
- if stderr and not regrtest.ns.pgo:
- print(stderr, file=sys.stderr, flush=True)
-
- if result[0] == INTERRUPTED:
- raise KeyboardInterrupt
- test_index += 1
- except KeyboardInterrupt:
- regrtest.interrupted = True
- pending.interrupted = True
- print()
- finally:
- if use_timeout:
- faulthandler.cancel_dump_traceback_later()
-
- # If tests are interrupted, wait until tests complete
- wait_start = time.monotonic()
- while True:
- running = [worker.current_test for worker in workers]
- running = list(filter(bool, running))
- if not running:
- break
-
- dt = time.monotonic() - wait_start
- line = "Waiting for %s (%s tests)" % (', '.join(running), len(running))
- if dt >= WAIT_PROGRESS:
- line = "%s since %.0f sec" % (line, dt)
- print(line, flush=True)
- for worker in workers:
- worker.join(WAIT_PROGRESS)
+ pass
+
+ # display progress
+ running = get_running(self.workers)
+ if running and not self.ns.pgo:
+ print('running: %s' % ', '.join(running), flush=True)
+
+ def display_result(self, mp_result):
+ result = mp_result.result
+
+ text = format_test_result(result)
+ if mp_result.error_msg is not None:
+ # CHILD_ERROR
+ text += ' (%s)' % mp_result.error_msg
+ elif (result.test_time >= PROGRESS_MIN_TIME and not self.ns.pgo):
+ text += ' (%s)' % format_duration(result.test_time)
+ running = get_running(self.workers)
+ if running and not self.ns.pgo:
+ text += ' -- running: %s' % ', '.join(running)
+ self.regrtest.display_progress(self.test_index, text)
+
+ def _process_result(self, item):
+ if item[0]:
+ # Thread got an exception
+ format_exc = item[1]
+ print(f"regrtest worker thread failed: {format_exc}",
+ file=sys.stderr, flush=True)
+ return True
+
+ self.test_index += 1
+ mp_result = item[1]
+ self.regrtest.accumulate_result(mp_result.result)
+ self.display_result(mp_result)
+
+ if mp_result.stdout:
+ print(mp_result.stdout, flush=True)
+ if mp_result.stderr and not self.ns.pgo:
+ print(mp_result.stderr, file=sys.stderr, flush=True)
+
+ if must_stop(mp_result.result):
+ return True
+
+ return False
+
+ def run_tests(self):
+ self.start_workers()
+
+ self.test_index = 0
+ try:
+ while True:
+ item = self._get_result()
+ if item is None:
+ break
+
+ stop = self._process_result(item)
+ if stop:
+ break
+ except KeyboardInterrupt:
+ print()
+ self.regrtest.interrupted = True
+ finally:
+ if self.test_timeout is not None:
+ faulthandler.cancel_dump_traceback_later()
+
+ self.wait_workers()
+
+
+def run_tests_multiprocess(regrtest):
+ MultiprocessRunner(regrtest).run_tests()
diff --git a/Lib/test/libregrtest/save_env.py b/Lib/test/libregrtest/save_env.py
index 45b365d45633..e133c3f1c765 100644
--- a/Lib/test/libregrtest/save_env.py
+++ b/Lib/test/libregrtest/save_env.py
@@ -8,6 +8,7 @@
import threading
import warnings
from test import support
+from test.libregrtest.utils import print_warning
try:
import _multiprocessing, multiprocessing.process
except ImportError:
@@ -276,8 +277,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
self.changed = True
restore(original)
if not self.quiet and not self.pgo:
- print(f"Warning -- {name} was modified by {self.testname}",
- file=sys.stderr, flush=True)
+ print_warning(f"{name} was modified by {self.testname}")
print(f" Before: {original}\n After: {current} ",
file=sys.stderr, flush=True)
return False
diff --git a/Lib/test/libregrtest/utils.py b/Lib/test/libregrtest/utils.py
index d36bf9196626..fb9971a64f66 100644
--- a/Lib/test/libregrtest/utils.py
+++ b/Lib/test/libregrtest/utils.py
@@ -1,5 +1,6 @@
-import os.path
import math
+import os.path
+import sys
import textwrap
@@ -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,
initial_indent=blanks, subsequent_indent=blanks),
file=file)
+
+
+def print_warning(msg):
+ print(f"Warning -- {msg}", file=sys.stderr, flush=True)
diff --git a/Lib/test/libregrtest/win_utils.py b/Lib/test/libregrtest/win_utils.py
new file mode 100644
index 000000000000..adfe278ba39b
--- /dev/null
+++ b/Lib/test/libregrtest/win_utils.py
@@ -0,0 +1,105 @@
+import _winapi
+import msvcrt
+import os
+import subprocess
+import uuid
+from test import support
+
+
+# Max size of asynchronous reads
+BUFSIZE = 8192
+# Exponential damping factor (see below)
+LOAD_FACTOR_1 = 0.9200444146293232478931553241
+# Seconds per measurement
+SAMPLING_INTERVAL = 5
+COUNTER_NAME = r'\System\Processor Queue Length'
+
+
+class WindowsLoadTracker():
+ """
+ This class asynchronously interacts with the `typeperf` command to read
+ the system load on Windows. Mulitprocessing and threads can't be used
+ here because they interfere with the test suite's cases for those
+ modules.
+ """
+
+ def __init__(self):
+ self.load = 0.0
+ self.start()
+
+ def start(self):
+ # Create a named pipe which allows for asynchronous IO in Windows
+ pipe_name = r'\\.\pipe\typeperf_output_' + str(uuid.uuid4())
+
+ open_mode = _winapi.PIPE_ACCESS_INBOUND
+ open_mode |= _winapi.FILE_FLAG_FIRST_PIPE_INSTANCE
+ open_mode |= _winapi.FILE_FLAG_OVERLAPPED
+
+ # This is the read end of the pipe, where we will be grabbing output
+ self.pipe = _winapi.CreateNamedPipe(
+ pipe_name, open_mode, _winapi.PIPE_WAIT,
+ 1, BUFSIZE, BUFSIZE, _winapi.NMPWAIT_WAIT_FOREVER, _winapi.NULL
+ )
+ # The write end of the pipe which is passed to the created process
+ pipe_write_end = _winapi.CreateFile(
+ pipe_name, _winapi.GENERIC_WRITE, 0, _winapi.NULL,
+ _winapi.OPEN_EXISTING, 0, _winapi.NULL
+ )
+ # Open up the handle as a python file object so we can pass it to
+ # subprocess
+ command_stdout = msvcrt.open_osfhandle(pipe_write_end, 0)
+
+ # Connect to the read end of the pipe in overlap/async mode
+ overlap = _winapi.ConnectNamedPipe(self.pipe, overlapped=True)
+ overlap.GetOverlappedResult(True)
+
+ # Spawn off the load monitor
+ command = ['typeperf', COUNTER_NAME, '-si', str(SAMPLING_INTERVAL)]
+ self.p = subprocess.Popen(command, stdout=command_stdout, cwd=support.SAVEDCWD)
+
+ # Close our copy of the write end of the pipe
+ os.close(command_stdout)
+
+ def close(self):
+ if self.p is None:
+ return
+ self.p.kill()
+ self.p.wait()
+ self.p = None
+
+ def __del__(self):
+ self.close()
+
+ def read_output(self):
+ import _winapi
+
+ overlapped, _ = _winapi.ReadFile(self.pipe, BUFSIZE, True)
+ bytes_read, res = overlapped.GetOverlappedResult(False)
+ if res != 0:
+ return
+
+ return overlapped.getbuffer().decode()
+
+ def getloadavg(self):
+ typeperf_output = self.read_output()
+ # Nothing to update, just return the current load
+ if not typeperf_output:
+ return self.load
+
+ # Process the backlog of load values
+ for line in typeperf_output.splitlines():
+ # typeperf outputs in a CSV format like this:
+ # "07/19/2018 01:32:26.605","3.000000"
+ toks = line.split(',')
+ # Ignore blank lines and the initial header
+ if line.strip() == '' or (COUNTER_NAME in line) or len(toks) != 2:
+ continue
+
+ load = float(toks[1].replace('"', ''))
+ # We use an exponentially weighted moving average, imitating the
+ # load calculation on Unix systems.
+ # https://en.wikipedia.org/wiki/Load_(computing)#Unix-style_load_calculation
+ new_load = self.load * LOAD_FACTOR_1 + load * (1.0 - LOAD_FACTOR_1)
+ self.load = new_load
+
+ return self.load
diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py
index a67458313add..4c6152153668 100644
--- a/Lib/test/test_regrtest.py
+++ b/Lib/test/test_regrtest.py
@@ -21,7 +21,7 @@
from test.libregrtest import utils
-Py_DEBUG = hasattr(sys, 'getobjects')
+Py_DEBUG = hasattr(sys, 'gettotalrefcount')
ROOT_DIR = os.path.join(os.path.dirname(__file__), '..', '..')
ROOT_DIR = os.path.abspath(os.path.normpath(ROOT_DIR))
@@ -109,7 +109,7 @@ def test_quiet(self):
self.assertTrue(ns.quiet)
self.assertEqual(ns.verbose, 0)
- def test_slow(self):
+ def test_slowest(self):
for opt in '-o', '--slowest':
with self.subTest(opt=opt):
ns = libregrtest._parse_args([opt])
@@ -255,9 +255,7 @@ def test_multiprocess(self):
self.checkError([opt], 'expected one argument')
self.checkError([opt, 'foo'], 'invalid int value')
self.checkError([opt, '2', '-T'], "don't go together")
- self.checkError([opt, '2', '-l'], "don't go together")
self.checkError([opt, '0', '-T'], "don't go together")
- self.checkError([opt, '0', '-l'], "don't go together")
def test_coverage(self):
for opt in '-T', '--coverage':
@@ -454,8 +452,8 @@ def list_regex(line_format, tests):
regex = list_regex('%s re-run test%s', rerun)
self.check_line(output, regex)
self.check_line(output, "Re-running failed tests in verbose mode")
- for name in rerun:
- regex = "Re-running test %r in verbose mode" % name
+ for test_name in rerun:
+ regex = f"Re-running {test_name} in verbose mode"
self.check_line(output, regex)
if no_test_ran:
@@ -487,7 +485,7 @@ def list_regex(line_format, tests):
result.append('SUCCESS')
result = ', '.join(result)
if rerun:
- self.check_line(output, 'Tests result: %s' % result)
+ self.check_line(output, 'Tests result: FAILURE')
result = 'FAILURE then %s' % result
self.check_line(output, 'Tests result: %s' % result)
@@ -781,22 +779,23 @@ def test_slowest(self):
% (self.TESTNAME_REGEX, len(tests)))
self.check_line(output, regex)
- def test_slow_interrupted(self):
+ def test_slowest_interrupted(self):
# Issue #25373: test --slowest with an interrupted test
code = TEST_INTERRUPTED
test = self.create_test("sigint", code=code)
for multiprocessing in (False, True):
- if multiprocessing:
- args = ("--slowest", "-j2", test)
- else:
- args = ("--slowest", test)
- output = self.run_tests(*args, exitcode=130)
- self.check_executed_tests(output, test,
- omitted=test, interrupted=True)
-
- regex = ('10 slowest tests:\n')
- self.check_line(output, regex)
+ with self.subTest(multiprocessing=multiprocessing):
+ if multiprocessing:
+ args = ("--slowest", "-j2", test)
+ else:
+ args = ("--slowest", test)
+ output = self.run_tests(*args, exitcode=130)
+ self.check_executed_tests(output, test,
+ omitted=test, interrupted=True)
+
+ regex = ('10 slowest tests:\n')
+ self.check_line(output, regex)
def test_coverage(self):
# test --coverage
@@ -915,13 +914,13 @@ def test_method2(self):
testname)
self.assertEqual(output.splitlines(), all_methods)
+ @support.cpython_only
def test_crashed(self):
# Any code which causes a crash
code = 'import faulthandler; faulthandler._sigsegv()'
crash_test = self.create_test(name="crash", code=code)
- ok_test = self.create_test(name="ok")
- tests = [crash_test, ok_test]
+ tests = [crash_test]
output = self.run_tests("-j2", *tests, exitcode=2)
self.check_executed_tests(output, tests, failed=crash_test,
randomize=True)
@@ -991,6 +990,7 @@ def test_env_changed(self):
fail_env_changed=True)
def test_rerun_fail(self):
+ # FAILURE then FAILURE
code = textwrap.dedent("""
import unittest
@@ -1005,6 +1005,26 @@ def test_bug(self):
self.check_executed_tests(output, [testname],
failed=testname, rerun=testname)
+ def test_rerun_success(self):
+ # FAILURE then SUCCESS
+ code = textwrap.dedent("""
+ import builtins
+ import unittest
+
+ class Tests(unittest.TestCase):
+ failed = False
+
+ def test_fail_once(self):
+ if not hasattr(builtins, '_test_failed'):
+ builtins._test_failed = True
+ self.fail("bug")
+ """)
+ testname = self.create_test(code=code)
+
+ output = self.run_tests("-w", testname, exitcode=0)
+ self.check_executed_tests(output, [testname],
+ rerun=testname)
+
def test_no_tests_ran(self):
code = textwrap.dedent("""
import unittest
@@ -1069,6 +1089,38 @@ def test_other_bug(self):
self.check_executed_tests(output, [testname, testname2],
no_test_ran=[testname])
+ @support.cpython_only
+ def test_findleaks(self):
+ code = textwrap.dedent(r"""
+ import _testcapi
+ import gc
+ import unittest
+
+ @_testcapi.with_tp_del
+ class Garbage:
+ def __tp_del__(self):
+ pass
+
+ class Tests(unittest.TestCase):
+ def test_garbage(self):
+ # create an uncollectable object
+ obj = Garbage()
+ obj.ref_cycle = obj
+ obj = None
+ """)
+ testname = self.create_test(code=code)
+
+ output = self.run_tests("--fail-env-changed", testname, exitcode=3)
+ self.check_executed_tests(output, [testname],
+ env_changed=[testname],
+ fail_env_changed=True)
+
+ # --findleaks is now basically an alias to --fail-env-changed
+ output = self.run_tests("--findleaks", testname, exitcode=3)
+ self.check_executed_tests(output, [testname],
+ env_changed=[testname],
+ fail_env_changed=True)
+
class TestUtils(unittest.TestCase):
def test_format_duration(self):
diff --git a/Misc/NEWS.d/next/Library/2019-01-21-13-56-55.bpo-35802.6633PE.rst b/Misc/NEWS.d/next/Library/2019-01-21-13-56-55.bpo-35802.6633PE.rst
new file mode 100644
index 000000000000..8b73d2bd5851
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2019-01-21-13-56-55.bpo-35802.6633PE.rst
@@ -0,0 +1,2 @@
+Clean up code which checked presence of ``os.stat`` / ``os.lstat`` /
+``os.chmod`` which are always present. Patch by Anthony Sottile.
diff --git a/Misc/NEWS.d/next/Tests/2019-04-26-04-12-29.bpo-36725.B8-ghi.rst b/Misc/NEWS.d/next/Tests/2019-04-26-04-12-29.bpo-36725.B8-ghi.rst
new file mode 100644
index 000000000000..b632c46d2b67
--- /dev/null
+++ b/Misc/NEWS.d/next/Tests/2019-04-26-04-12-29.bpo-36725.B8-ghi.rst
@@ -0,0 +1,3 @@
+When using mulitprocessing mode (-jN), regrtest now better reports errors if
+a worker process fails, and it exits immediately on a worker thread failure
+or when interrupted.
diff --git a/Misc/NEWS.d/next/Tests/2019-04-26-09-02-49.bpo-36719.ys2uqH.rst b/Misc/NEWS.d/next/Tests/2019-04-26-09-02-49.bpo-36719.ys2uqH.rst
new file mode 100644
index 000000000000..4b6ef76bc6d6
--- /dev/null
+++ b/Misc/NEWS.d/next/Tests/2019-04-26-09-02-49.bpo-36719.ys2uqH.rst
@@ -0,0 +1,4 @@
+regrtest now always detects uncollectable objects. Previously, the check was
+only enabled by ``--findleaks``. The check now also works with
+``-jN/--multiprocess N``. ``--findleaks`` becomes a deprecated alias to
+``--fail-env-changed``.
diff --git a/Misc/NEWS.d/next/Windows/2018-07-20-13-09-19.bpo-34060.v-z87j.rst b/Misc/NEWS.d/next/Windows/2018-07-20-13-09-19.bpo-34060.v-z87j.rst
new file mode 100644
index 000000000000..b77d805b7f2a
--- /dev/null
+++ b/Misc/NEWS.d/next/Windows/2018-07-20-13-09-19.bpo-34060.v-z87j.rst
@@ -0,0 +1,2 @@
+Report system load when running test suite on Windows. Patch by Ammar Askar.
+Based on prior work by Jeremy Kloth.
More information about the Python-checkins
mailing list