summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorAntoine Pitrou <solipsis@pitrou.net>2009-06-29 13:54:42 (GMT)
committerAntoine Pitrou <solipsis@pitrou.net>2009-06-29 13:54:42 (GMT)
commit88909540ffb2f25daee528f5bc2859d6fe253cbc (patch)
tree24d7f61505b252441e0f1aff8d4bff2a59600acc /Lib
parent6fbdfdabb616caa7bc815fb8bfae802302ac2fff (diff)
downloadcpython-88909540ffb2f25daee528f5bc2859d6fe253cbc.zip
cpython-88909540ffb2f25daee528f5bc2859d6fe253cbc.tar.gz
cpython-88909540ffb2f25daee528f5bc2859d6fe253cbc.tar.bz2
Merged revisions 73072 via svnmerge from
svn+ssh://pythondev@svn.python.org/python/trunk ........ r73072 | antoine.pitrou | 2009-05-31 16:20:14 +0200 (dim., 31 mai 2009) | 4 lines Issue #6152: New option '-j'/'--multiprocess' for regrtest allows running regression tests in parallel, shortening the total runtime. ........
Diffstat (limited to 'Lib')
-rwxr-xr-xLib/test/regrtest.py276
-rw-r--r--Lib/test/support.py58
-rw-r--r--Lib/test/test_parser.py1
3 files changed, 185 insertions, 150 deletions
diff --git a/Lib/test/regrtest.py b/Lib/test/regrtest.py
index e98a99a..157420f 100755
--- a/Lib/test/regrtest.py
+++ b/Lib/test/regrtest.py
@@ -28,13 +28,12 @@ Command line options:
-R: huntrleaks -- search for reference leaks (needs debug build, v. slow)
-M: memlimit -- run very large memory-consuming tests
-n: nowindows -- suppress error message boxes on Windows
+-j: multiprocess -- run several processes at once
If non-option arguments are present, they are names for tests to run,
unless -x is given, in which case they are names for tests not to run.
If no test names are given, all tests are run.
--v is incompatible with -g and does not compare test output files.
-
-r randomizes test execution order. You can use --randseed=int to provide a
int seed value for the randomizer; this is useful for reproducing troublesome
test orders.
@@ -132,6 +131,7 @@ option '-uall,-gui'.
"""
import getopt
+import json
import os
import random
import re
@@ -189,11 +189,11 @@ def usage(msg):
sys.exit(2)
-def main(tests=None, testdir=None, verbose=0, quiet=False, generate=False,
+def main(tests=None, testdir=None, verbose=0, quiet=False,
exclude=False, single=False, randomize=False, fromfile=None,
findleaks=False, use_resources=None, trace=False, coverdir='coverage',
runleaks=False, huntrleaks=False, verbose2=False, print_slow=False,
- random_seed=None):
+ random_seed=None, use_mp=None):
"""Execute a test suite.
This also parses command-line options and modifies its behavior
@@ -210,7 +210,7 @@ def main(tests=None, testdir=None, verbose=0, quiet=False, generate=False,
command-line will be used. If that's empty, too, then all *.py
files beginning with test_ will be used.
- The other default arguments (verbose, quiet, generate, exclude,
+ The other default arguments (verbose, quiet, exclude,
single, randomize, findleaks, use_resources, trace, coverdir,
print_slow, and random_seed) allow programmers calling main()
directly to set the values that would normally be set by flags
@@ -219,14 +219,14 @@ def main(tests=None, testdir=None, verbose=0, quiet=False, generate=False,
support.record_original_stdout(sys.stdout)
try:
- opts, args = getopt.getopt(sys.argv[1:], 'hvgqxsSrf:lu:t:TD:NLR:wM:n',
+ opts, args = getopt.getopt(sys.argv[1:], 'hvqxsSrf:lu:t:TD:NLR:wM:nj:',
['help', 'verbose', 'quiet', 'exclude',
'single', 'slow', 'random', 'fromfile',
'findleaks', 'use=', 'threshold=', 'trace',
'coverdir=', 'nocoverdir', 'runleaks',
'huntrleaks=', 'verbose2', 'memlimit=',
'debug', 'start=', 'nowindows',
- 'randseed=',
+ 'randseed=', 'multiprocess=', 'slaveargs=',
])
except getopt.error as msg:
usage(msg)
@@ -330,10 +330,24 @@ def main(tests=None, testdir=None, verbose=0, quiet=False, generate=False,
for m in [msvcrt.CRT_WARN, msvcrt.CRT_ERROR, msvcrt.CRT_ASSERT]:
msvcrt.CrtSetReportMode(m, msvcrt.CRTDBG_MODE_FILE)
msvcrt.CrtSetReportFile(m, msvcrt.CRTDBG_FILE_STDERR)
- if generate and verbose:
- usage("-g and -v don't go together!")
+ elif o in ('-j', '--multiprocess'):
+ use_mp = int(a)
+ elif o == '--slaveargs':
+ args, kwargs = json.loads(a)
+ try:
+ result = runtest(*args, **kwargs)
+ except BaseException as e:
+ result = -3, e.__class__.__name__
+ sys.stdout.flush()
+ print() # Force a newline (just in case)
+ print(json.dumps(result))
+ sys.exit(0)
if single and fromfile:
usage("-s and -f don't go together!")
+ if use_mp and trace:
+ usage(2, "-T and -j don't go together!")
+ if use_mp and findleaks:
+ usage(2, "-l and -j don't go together!")
good = []
bad = []
@@ -409,47 +423,117 @@ def main(tests=None, testdir=None, verbose=0, quiet=False, generate=False,
support.verbose = verbose # Tell tests to be moderately quiet
support.use_resources = use_resources
save_modules = sys.modules.keys()
- for test in tests:
- if not quiet:
- print(test)
- sys.stdout.flush()
- if trace:
- # If we're tracing code coverage, then we don't exit with status
- # if on a false return value from main.
- tracer.runctx('runtest(test, generate, verbose, quiet,'
- ' test_times, testdir)',
- globals=globals(), locals=vars())
+
+ def accumulate_result(test, result):
+ ok, test_time = result
+ test_times.append((test_time, test))
+ if ok > 0:
+ good.append(test)
+ elif ok == 0:
+ bad.append(test)
else:
+ skipped.append(test)
+ if ok == -2:
+ resource_denieds.append(test)
+
+ if use_mp:
+ from threading import Thread, Lock
+ from queue import Queue, Empty
+ from subprocess import Popen, PIPE, STDOUT
+ from collections import deque
+ # TextIOWrapper is not entirely thread-safe now,
+ # it can produce duplicate output when printing from several threads.
+ print_lock = Lock()
+ debug_output_pat = re.compile(r"\[\d+ refs\]$")
+ pending = deque()
+ output = Queue()
+ for test in tests:
+ args_tuple = (
+ (test, verbose, quiet, testdir),
+ dict(huntrleaks=huntrleaks, use_resources=use_resources,
+ debug=debug)
+ )
+ pending.append((test, args_tuple))
+ def work():
+ # A worker thread.
try:
- ok = runtest(test, generate, verbose, quiet, test_times,
- testdir, huntrleaks)
- except KeyboardInterrupt:
- # print a newline separate from the ^C
- print()
- break
- except:
+ while True:
+ try:
+ test, args_tuple = pending.popleft()
+ except IndexError:
+ output.put((None, None, None))
+ return
+ if not quiet:
+ with print_lock:
+ print(test)
+ sys.stdout.flush()
+ # -E is needed by some tests, e.g. test_import
+ popen = Popen([sys.executable, '-E', '-m', 'test.regrtest',
+ '--slaveargs', json.dumps(args_tuple)],
+ stdout=PIPE, stderr=STDOUT,
+ universal_newlines=True, close_fds=True)
+ out = popen.communicate()[0].strip()
+ out = debug_output_pat.sub("", out)
+ out, _, result = out.strip().rpartition("\n")
+ result = json.loads(result)
+ output.put((test, out.strip(), result))
+ except BaseException:
+ output.put((None, None, None))
raise
- if ok > 0:
- good.append(test)
- elif ok == 0:
- bad.append(test)
+ workers = [Thread(target=work) for i in range(use_mp)]
+ for worker in workers:
+ worker.start()
+ finished = 0
+ while finished < use_mp:
+ test, out, result = output.get()
+ if out:
+ with print_lock:
+ print(out)
+ sys.stdout.flush()
+ if test is None:
+ finished += 1
+ continue
+ if result[0] == -3:
+ assert result[1] == 'KeyboardInterrupt'
+ pending.clear()
+ raise KeyboardInterrupt # What else?
+ accumulate_result(test, result)
+ for worker in workers:
+ worker.join()
+ else:
+ for test in tests:
+ if not quiet:
+ print(test)
+ sys.stdout.flush()
+ if trace:
+ # If we're tracing code coverage, then we don't exit with status
+ # if on a false return value from main.
+ tracer.runctx('runtest(test, verbose, quiet, testdir)',
+ globals=globals(), locals=vars())
else:
- skipped.append(test)
- if ok == -2:
- resource_denieds.append(test)
- if 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
- 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."):
- support.unload(module)
+ try:
+ result = runtest(test, verbose, quiet,
+ testdir, huntrleaks, debug)
+ accumulate_result(test, result)
+ except KeyboardInterrupt:
+ # print a newline separate from the ^C
+ print()
+ break
+ except:
+ raise
+ if 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
+ 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."):
+ support.unload(module)
# The lists won't be sorted if running with -r
good.sort()
@@ -495,8 +579,8 @@ def main(tests=None, testdir=None, verbose=0, quiet=False, generate=False,
print("Re-running test %r in verbose mode" % test)
sys.stdout.flush()
try:
- support.verbose = True
- ok = runtest(test, generate, True, quiet, test_times, testdir,
+ verbose = True
+ ok = runtest(test, True, quiet, testdir,
huntrleaks, debug)
except KeyboardInterrupt:
# print a newline separate from the ^C
@@ -559,8 +643,8 @@ def findtests(testdir=None, stdtests=STDTESTS, nottests=NOTTESTS):
tests.sort()
return stdtests + tests
-def runtest(test, generate, verbose, quiet, test_times,
- testdir=None, huntrleaks=False, debug=False):
+def runtest(test, verbose, quiet,
+ testdir=None, huntrleaks=False, debug=False, use_resources=None):
"""Run a single test.
test -- the name of the test
@@ -579,29 +663,26 @@ def runtest(test, generate, verbose, quiet, test_times,
1 test passed
"""
+ support.verbose = verbose # Tell tests to be moderately quiet
+ if use_resources is not None:
+ support.use_resources = use_resources
try:
- return runtest_inner(test, generate, verbose, quiet, test_times,
- testdir, huntrleaks)
+ return runtest_inner(test, verbose, quiet,
+ testdir, huntrleaks, debug)
finally:
cleanup_test_droppings(test, verbose)
-def runtest_inner(test, generate, verbose, quiet, test_times,
+def runtest_inner(test, verbose, quiet,
testdir=None, huntrleaks=False, debug=False):
support.unload(test)
if not testdir:
testdir = findtestdir()
- if verbose:
- cfp = None
- else:
- cfp = io.StringIO() # XXX Should use io.StringIO()
+ test_time = 0.0
refleak = False # True if the test leaked references.
try:
save_stdout = sys.stdout
try:
- if cfp:
- sys.stdout = cfp
- print(test) # Output file starts with test name
if test.startswith('test.'):
abstest = test
else:
@@ -619,25 +700,24 @@ def runtest_inner(test, generate, verbose, quiet, test_times,
if huntrleaks:
refleak = dash_R(the_module, test, indirect_test, huntrleaks)
test_time = time.time() - start_time
- test_times.append((test_time, test))
finally:
sys.stdout = save_stdout
except support.ResourceDenied as msg:
if not quiet:
print(test, "skipped --", msg)
sys.stdout.flush()
- return -2
+ return -2, test_time
except unittest.SkipTest as msg:
if not quiet:
print(test, "skipped --", msg)
sys.stdout.flush()
- return -1
+ return -1, test_time
except KeyboardInterrupt:
raise
except support.TestFailed as msg:
print("test", test, "failed --", msg)
sys.stdout.flush()
- return 0
+ return 0, test_time
except:
type, value = sys.exc_info()[:2]
print("test", test, "crashed --", str(type) + ":", value)
@@ -645,21 +725,11 @@ def runtest_inner(test, generate, verbose, quiet, test_times,
if verbose or debug:
traceback.print_exc(file=sys.stdout)
sys.stdout.flush()
- return 0
+ return 0, test_time
else:
if refleak:
- return 0
- if not cfp:
- return 1
- output = cfp.getvalue()
- expected = test + "\n"
- if output == expected or huntrleaks:
- return 1
- print("test", test, "produced unexpected output:")
- sys.stdout.flush()
- reportdiff(expected, output)
- sys.stdout.flush()
- return 0
+ return 0, test_time
+ return 1, test_time
def cleanup_test_droppings(testname, verbose):
import shutil
@@ -734,6 +804,7 @@ def dash_R(the_module, test, indirect_test, huntrleaks):
repcount = nwarmup + ntracked
print("beginning", repcount, "repetitions", file=sys.stderr)
print(("1234567890"*(repcount//10 + 1))[:repcount], file=sys.stderr)
+ sys.stderr.flush()
dash_R_cleanup(fs, ps, pic, abcs)
for i in range(repcount):
rc = sys.gettotalrefcount()
@@ -747,9 +818,10 @@ def dash_R(the_module, test, indirect_test, huntrleaks):
if any(deltas):
msg = '%s leaked %s references, sum=%s' % (test, deltas, sum(deltas))
print(msg, file=sys.stderr)
- refrep = open(fname, "a")
- print(msg, file=refrep)
- refrep.close()
+ sys.stderr.flush()
+ with open(fname, "a") as refrep:
+ print(msg, file=refrep)
+ refrep.flush()
return True
return False
@@ -805,48 +877,6 @@ def warm_char_cache():
for i in range(256):
s[i:i+1]
-def reportdiff(expected, output):
- import difflib
- print("*" * 70)
- a = expected.splitlines(1)
- b = output.splitlines(1)
- sm = difflib.SequenceMatcher(a=a, b=b)
- tuples = sm.get_opcodes()
-
- def pair(x0, x1):
- # x0:x1 are 0-based slice indices; convert to 1-based line indices.
- x0 += 1
- if x0 >= x1:
- return "line " + str(x0)
- else:
- return "lines %d-%d" % (x0, x1)
-
- for op, a0, a1, b0, b1 in tuples:
- if op == 'equal':
- pass
-
- elif op == 'delete':
- print("***", pair(a0, a1), "of expected output missing:")
- for line in a[a0:a1]:
- print("-", line, end='')
-
- elif op == 'replace':
- print("*** mismatch between", pair(a0, a1), "of expected", \
- "output and", pair(b0, b1), "of actual output:")
- for line in difflib.ndiff(a[a0:a1], b[b0:b1]):
- print(line, end='')
-
- elif op == 'insert':
- print("***", pair(b0, b1), "of actual output doesn't appear", \
- "in expected output after line", str(a1)+":")
- for line in b[b0:b1]:
- print("+", line, end='')
-
- else:
- print("get_opcodes() returned bad tuple?!?!", (op, a0, a1, b0, b1))
-
- print("*" * 70)
-
def findtestdir():
if __name__ == '__main__':
file = sys.argv[0]
@@ -1217,6 +1247,6 @@ if __name__ == '__main__':
i -= 1
if os.path.abspath(os.path.normpath(sys.path[i])) == mydir:
del sys.path[i]
- if len(sys.path) == pathlen:
+ if '--slaveargs' not in sys.argv and len(sys.path) == pathlen:
print('Could not find %r in sys.path to remove it' % mydir)
main()
diff --git a/Lib/test/support.py b/Lib/test/support.py
index 337a33f..38ea107 100644
--- a/Lib/test/support.py
+++ b/Lib/test/support.py
@@ -336,34 +336,38 @@ if os.name == 'java':
else:
TESTFN = '@test'
- # Assuming sys.getfilesystemencoding()!=sys.getdefaultencoding()
- # TESTFN_UNICODE is a filename that can be encoded using the
- # file system encoding, but *not* with the default (ascii) encoding
- TESTFN_UNICODE = "@test-\xe0\xf2"
- TESTFN_ENCODING = sys.getfilesystemencoding()
- # TESTFN_UNICODE_UNENCODEABLE is a filename that should *not* be
- # able to be encoded by *either* the default or filesystem encoding.
- # This test really only makes sense on Windows NT platforms
- # which have special Unicode support in posixmodule.
- if (not hasattr(sys, "getwindowsversion") or
- sys.getwindowsversion()[3] < 2): # 0=win32s or 1=9x/ME
- TESTFN_UNICODE_UNENCODEABLE = None
+# Disambiguate TESTFN for parallel testing, while letting it remain a valid
+# module name.
+TESTFN = "{0}_{1}_tmp".format(TESTFN, os.getpid())
+
+# Assuming sys.getfilesystemencoding()!=sys.getdefaultencoding()
+# TESTFN_UNICODE is a filename that can be encoded using the
+# file system encoding, but *not* with the default (ascii) encoding
+TESTFN_UNICODE = TESTFN + "-\xe0\xf2"
+TESTFN_ENCODING = sys.getfilesystemencoding()
+# TESTFN_UNICODE_UNENCODEABLE is a filename that should *not* be
+# able to be encoded by *either* the default or filesystem encoding.
+# This test really only makes sense on Windows NT platforms
+# which have special Unicode support in posixmodule.
+if (not hasattr(sys, "getwindowsversion") or
+ sys.getwindowsversion()[3] < 2): # 0=win32s or 1=9x/ME
+ TESTFN_UNICODE_UNENCODEABLE = None
+else:
+ # Japanese characters (I think - from bug 846133)
+ TESTFN_UNICODE_UNENCODEABLE = TESTFN + "-\u5171\u6709\u3055\u308c\u308b"
+ try:
+ # XXX - Note - should be using TESTFN_ENCODING here - but for
+ # Windows, "mbcs" currently always operates as if in
+ # errors=ignore' mode - hence we get '?' characters rather than
+ # the exception. 'Latin1' operates as we expect - ie, fails.
+ # See [ 850997 ] mbcs encoding ignores errors
+ TESTFN_UNICODE_UNENCODEABLE.encode("Latin1")
+ except UnicodeEncodeError:
+ pass
else:
- # Japanese characters (I think - from bug 846133)
- TESTFN_UNICODE_UNENCODEABLE = "@test-\u5171\u6709\u3055\u308c\u308b"
- try:
- # XXX - Note - should be using TESTFN_ENCODING here - but for
- # Windows, "mbcs" currently always operates as if in
- # errors=ignore' mode - hence we get '?' characters rather than
- # the exception. 'Latin1' operates as we expect - ie, fails.
- # See [ 850997 ] mbcs encoding ignores errors
- TESTFN_UNICODE_UNENCODEABLE.encode("Latin1")
- except UnicodeEncodeError:
- pass
- else:
- print('WARNING: The filename %r CAN be encoded by the filesystem. '
- 'Unicode filename tests may not be effective'
- % TESTFN_UNICODE_UNENCODEABLE)
+ print('WARNING: The filename %r CAN be encoded by the filesystem. '
+ 'Unicode filename tests may not be effective'
+ % TESTFN_UNICODE_UNENCODEABLE)
# Make sure we can write to TESTFN, try in /tmp if we can't
fp = None
diff --git a/Lib/test/test_parser.py b/Lib/test/test_parser.py
index 65d1d35..340ae2f 100644
--- a/Lib/test/test_parser.py
+++ b/Lib/test/test_parser.py
@@ -496,6 +496,7 @@ class ParserStackLimitTestCase(unittest.TestCase):
e = self._nested_expression(100)
print("Expecting 's_push: parser stack overflow' in next line",
file=sys.stderr)
+ sys.stderr.flush()
self.assertRaises(MemoryError, parser.expr, e)
class STObjectTestCase(unittest.TestCase):