summaryrefslogtreecommitdiffstats
path: root/runtest.py
diff options
context:
space:
mode:
Diffstat (limited to 'runtest.py')
-rw-r--r--runtest.py426
1 files changed, 238 insertions, 188 deletions
diff --git a/runtest.py b/runtest.py
index 58950dd..9780f1c 100644
--- a/runtest.py
+++ b/runtest.py
@@ -10,13 +10,6 @@
# By default, it directly uses the modules in the local tree:
# ./src/ (source files we ship) and ./QMTest/ (other modules we don't).
#
-# HOWEVER, now that SCons has Repository support, we don't have
-# Aegis copy all of the files into the local tree. So if you're
-# using Aegis and want to run tests by hand using this script, you
-# must "aecp ." the entire source tree into your local directory
-# structure. When you're done with your change, you can then
-# "aecpu -unch ." to un-copy any files that you haven't changed.
-#
# When any -p option is specified, this script assumes it's in a
# directory in which a build has been performed, and sets PYTHONPATH
# so that it *only* references the modules that have unpacked from
@@ -28,28 +21,31 @@
#
# -a Run all tests; does a virtual 'find' for
# all SCons tests under the current directory.
-#
-# --aegis Print test results to an output file (specified
-# by the -o option) in the format expected by
-# aetest(5). This is intended for use in the
-# batch_test_command field in the Aegis project
-# config file.
+# You can also specify a list of subdirectories
+# (not available with the "--qmtest" option!). Then,
+# only the given folders are searched for test files.
#
# -d Debug. Runs the script under the Python
# debugger (pdb.py) so you don't have to
# muck with PYTHONPATH yourself.
#
+# -e Starts the script in external mode, for
+# testing separate Tools and packages.
+#
# -f file Only execute the tests listed in the specified
# file.
#
# -h Print the help and exit.
#
+# -k Suppress printing of count and percent progress for
+# the single tests.
+#
# -l List available tests and exit.
#
# -n No execute, just print command lines.
#
# -o file Print test results to the specified file.
-# The --aegis and --xml options specify the
+# The --xml option specifies the
# output format.
#
# -P Python Use the specified Python interpreter.
@@ -64,9 +60,11 @@
# command line it will execute before
# executing it. This suppresses that print.
#
-# --sp The Aegis search path.
-#
-# --spe The Aegis executable search path.
+# -s Short progress. Prints only the command line
+# and a percentage value, based on the total and
+# current number of tests.
+# All stdout and stderr messages get suppressed (this
+# does only work with subprocess though)!
#
# -t Print the execution time of each test.
#
@@ -94,36 +92,12 @@ import stat
import sys
import time
-try:
- sorted
-except NameError:
- # Pre-2.4 Python has no sorted() function.
- #
- # The pre-2.4 Python list.sort() method does not support
- # list.sort(key=) nor list.sort(reverse=) keyword arguments, so
- # we must implement the functionality of those keyword arguments
- # by hand instead of passing them to list.sort().
- def sorted(iterable, cmp=None, key=None, reverse=0):
- if key is not None:
- result = [(key(x), x) for x in iterable]
- else:
- result = iterable[:]
- if cmp is None:
- # Pre-2.3 Python does not support list.sort(None).
- result.sort()
- else:
- result.sort(cmp)
- if key is not None:
- result = [t1 for t0,t1 in result]
- if reverse:
- result.reverse()
- return result
-
cwd = os.getcwd()
all = 0
baseline = 0
builddir = os.path.join(cwd, 'build')
+external = 0
debug = ''
execute_tests = 1
format = None
@@ -139,23 +113,32 @@ testlistfile = None
version = ''
print_times = None
python = None
-sp = None
-spe = None
+sp = []
+print_progress = 1
+suppress_stdout = False
+suppress_stderr = False
+allow_pipe_files = True
helpstr = """\
Usage: runtest.py [OPTIONS] [TEST ...]
Options:
-3 Warn about Python 3.x incompatibilities.
-a, --all Run all tests.
- --aegis Print results in Aegis format.
-b BASE, --baseline BASE Run test scripts against baseline BASE.
--builddir DIR Directory in which packages were built.
-d, --debug Run test scripts under the Python debugger.
+ -e, --external Run the script in external mode (for testing separate Tools)
-f FILE, --file FILE Run tests in specified FILE.
-h, --help Print this message and exit.
+ -k, --no-progress Suppress count and percent progress messages.
-l, --list List available tests and exit.
-n, --no-exec No execute, just print command lines.
--noqmtest Execute tests directly, not using QMTest.
+ --nopipefiles Doesn't use the "file pipe" workaround for subprocess.Popen()
+ for starting tests. WARNING: Only use this when too much file
+ traffic is giving you trouble AND you can be sure that none of
+ your tests create output that exceed 65K chars! You might
+ run into some deadlocks else.
-o FILE, --output FILE Print test results to FILE.
-P Python Use the specified Python interpreter.
-p PACKAGE, --package PACKAGE
@@ -171,8 +154,9 @@ Options:
--passed Summarize which tests passed.
--qmtest Run using the QMTest harness.
-q, --quiet Don't print the test being executed.
- --sp PATH The Aegis search path.
- --spe PATH The Aegis executable search path.
+ -s, --short-progress Short progress, prints only the command line
+ and a percentage value, based on the total and
+ current number of tests.
-t, --time Print test execution time.
-v version Specify the SCons version.
--verbose=LEVEL Set verbose level: 1 = print executed commands,
@@ -188,12 +172,12 @@ Environment Variables:
TESTCMD_VERBOSE: turn on verbosity in TestCommand
"""
-opts, args = getopt.getopt(sys.argv[1:], "3ab:df:hlno:P:p:qv:Xx:t",
- ['all', 'aegis', 'baseline=', 'builddir=',
- 'debug', 'file=', 'help',
- 'list', 'no-exec', 'noqmtest', 'output=',
+opts, args = getopt.getopt(sys.argv[1:], "3ab:def:hklno:P:p:qsv:Xx:t",
+ ['all', 'baseline=', 'builddir=',
+ 'debug', 'external', 'file=', 'help', 'no-progress',
+ 'list', 'no-exec', 'noqmtest', 'nopipefiles', 'output=',
'package=', 'passed', 'python=', 'qmtest',
- 'quiet', 'sp=', 'spe=', 'time',
+ 'quiet', 'short-progress', 'time',
'version=', 'exec=',
'verbose=', 'xml'])
@@ -214,6 +198,8 @@ for o, a in opts:
if os.path.exists(pdb):
debug = pdb
break
+ elif o in ['-e', '--external']:
+ external = 1
elif o in ['-f', '--file']:
if not os.path.isabs(a):
a = os.path.join(cwd, a)
@@ -221,12 +207,16 @@ for o, a in opts:
elif o in ['-h', '--help']:
print helpstr
sys.exit(0)
+ elif o in ['-k', '--no-progress']:
+ print_progress = 0
elif o in ['-l', '--list']:
list_only = 1
elif o in ['-n', '--no-exec']:
execute_tests = None
elif o in ['--noqmtest']:
qmtest = None
+ elif o in ['--nopipefiles']:
+ allow_pipe_files = False
elif o in ['-o', '--output']:
if a != '-' and not os.path.isabs(a):
a = os.path.join(cwd, a)
@@ -245,10 +235,12 @@ for o, a in opts:
qmtest = 'qmtest'
elif o in ['-q', '--quiet']:
printcommand = 0
- elif o in ['--sp']:
- sp = a.split(os.pathsep)
- elif o in ['--spe']:
- spe = a.split(os.pathsep)
+ suppress_stdout = True
+ suppress_stderr = True
+ elif o in ['-s', '--short-progress']:
+ print_progress = 1
+ suppress_stdout = True
+ suppress_stderr = True
elif o in ['-t', '--time']:
print_times = 1
elif o in ['--verbose']:
@@ -259,7 +251,7 @@ for o, a in opts:
scons_exec = 1
elif o in ['-x', '--exec']:
scons = a
- elif o in ['--aegis', '--xml']:
+ elif o in ['--xml']:
format = o
if not args and not all and not testlistfile:
@@ -303,39 +295,6 @@ try:
qmtest
except NameError:
qmtest = None
- # Old code for using QMTest by default if it's installed.
- # We now default to not using QMTest unless explicitly asked for.
- #for q in ['qmtest', 'qmtest.py']:
- # path = whereis(q)
- # if path:
- # # The name was found on $PATH; just execute the found name so
- # # we don't have to worry about paths containing white space.
- # qmtest = q
- # break
- #if not qmtest:
- # msg = ('Warning: found neither qmtest nor qmtest.py on $PATH;\n' +
- # '\tassuming --noqmtest option.\n')
- # sys.stderr.write(msg)
- # sys.stderr.flush()
-
-aegis = whereis('aegis')
-
-if format == '--aegis' and aegis:
- change = os.popen("aesub '$c' 2>/dev/null", "r").read()
- if change:
- if sp is None:
- paths = os.popen("aesub '$sp' 2>/dev/null", "r").read()[:-1]
- sp = paths.split(os.pathsep)
- if spe is None:
- spe = os.popen("aesub '$spe' 2>/dev/null", "r").read()[:-1]
- spe = spe.split(os.pathsep)
- else:
- aegis = None
-
-if sp is None:
- sp = []
-if spe is None:
- spe = []
sp.append(builddir)
sp.append(cwd)
@@ -349,6 +308,105 @@ def escape(s):
s = s.replace('\\', '\\\\')
return s
+# Try to use subprocess instead of the more low-level
+# spawn command...
+use_subprocess = True
+try:
+ import subprocess
+except:
+ use_subprocess = False
+
+if use_subprocess:
+ if not suppress_stdout and not suppress_stderr:
+ # Without any output suppressed, we let the subprocess
+ # write its stuff freely to stdout/stderr.
+ def spawn_it(command_args):
+ p = subprocess.Popen(' '.join(command_args),
+ shell=True)
+ return (None, None, p.wait())
+ else:
+ # Else, we catch the output of both pipes...
+ if allow_pipe_files:
+ # The subprocess.Popen() suffers from a well-known
+ # problem. Data for stdout/stderr is read into a
+ # memory buffer of fixed size, 65K which is not very much.
+ # When it fills up, it simply stops letting the child process
+ # write to it. The child will then sit and patiently wait to
+ # be able to write the rest of its output. Hang!
+ # In order to work around this, we follow a suggestion
+ # by Anders Pearson in
+ # http://http://thraxil.org/users/anders/posts/2008/03/13/Subprocess-Hanging-PIPE-is-your-enemy/
+ # and pass temp file objects to Popen() instead of the ubiquitous
+ # subprocess.PIPE.
+ def spawn_it(command_args):
+ # Create temporary files
+ import tempfile
+ tmp_stdout = tempfile.TemporaryFile(mode='w+t')
+ tmp_stderr = tempfile.TemporaryFile(mode='w+t')
+ # Start subprocess...
+ p = subprocess.Popen(' '.join(command_args),
+ stdout=tmp_stdout,
+ stderr=tmp_stderr,
+ shell=True)
+ # ... and wait for it to finish.
+ ret = p.wait()
+
+ try:
+ # Rewind to start of files
+ tmp_stdout.seek(0)
+ tmp_stderr.seek(0)
+ # Read output
+ spawned_stdout = tmp_stdout.read()
+ spawned_stderr = tmp_stderr.read()
+ finally:
+ # Remove temp files by closing them
+ tmp_stdout.close()
+ tmp_stderr.close()
+
+ # Return values
+ return (spawned_stderr, spawned_stdout, ret)
+
+ else:
+ # We get here only if the user gave the '--nopipefiles'
+ # option, meaning the "temp file" approach for
+ # subprocess.communicate() above shouldn't be used.
+ # He hopefully knows what he's doing, but again we have a
+ # potential deadlock situation in the following code:
+ # If the subprocess writes a lot of data to its stderr,
+ # the pipe will fill up (nobody's reading it yet) and the
+ # subprocess will wait for someone to read it.
+ # But the parent process is trying to read from stdin
+ # (but the subprocess isn't writing anything there).
+ # Hence a deadlock.
+ # Be dragons here! Better don't use this!
+ def spawn_it(command_args):
+ p = subprocess.Popen(' '.join(command_args),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ shell=True)
+ spawned_stdout = p.stdout.read()
+ spawned_stderr = p.stderr.read()
+ return (spawned_stderr, spawned_stdout, p.wait())
+else:
+ has_subprocess = False
+ # Set up lowest-common-denominator spawning of a process on both Windows
+ # and non-Windows systems that works all the way back to Python 1.5.2.
+ try:
+ os.spawnv
+ except AttributeError:
+ def spawn_it(command_args):
+ pid = os.fork()
+ if pid == 0:
+ os.execv(command_args[0], command_args)
+ else:
+ pid, status = os.waitpid(pid, 0)
+ return (None, None, status >> 8)
+ else:
+ def spawn_it(command_args):
+ command = command_args[0]
+ command_args = [escape(c) for c in command_args]
+ return (None, None, os.spawnv(os.P_WAIT, command, command_args))
+
class Base(object):
def __init__(self, path, spe=None):
self.path = path
@@ -363,15 +421,12 @@ class Base(object):
class SystemExecutor(Base):
def execute(self):
- command = self.command_args[0]
- command_args = [escape(arg) for arg in self.command_args]
- s = self.status = os.spawnv(os.P_WAIT, command, command_args)
+ self.stderr, self.stdout, s = spawn_it(self.command_args)
+ self.status = s
if s < 0 or s > 2:
sys.stdout.write("Unexpected exit status %d\n" % s)
-try:
- import subprocess
-except ImportError:
+if not use_subprocess:
import popen2
try:
popen2.Popen3
@@ -399,23 +454,43 @@ except ImportError:
self.status = self.status >> 8
else:
class PopenExecutor(Base):
- def execute(self):
- p = subprocess.Popen(self.command_str,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- shell=True)
- self.stdout = p.stdout.read()
- self.stderr = p.stderr.read()
- self.status = p.wait()
-
-class Aegis(SystemExecutor):
- def header(self, f):
- f.write('test_result = [\n')
- def write(self, f):
- f.write(' { file_name = "%s";\n' % self.path)
- f.write(' exit_status = %d; },\n' % self.status)
- def footer(self, f):
- f.write('];\n')
+ # For an explanation of the following 'if ... else'
+ # and the 'allow_pipe_files' option, please check out the
+ # use_subprocess path in the definition of spawn_it() above.
+ if allow_pipe_files:
+ def execute(self):
+ # Create temporary files
+ import tempfile
+ tmp_stdout = tempfile.TemporaryFile(mode='w+t')
+ tmp_stderr = tempfile.TemporaryFile(mode='w+t')
+ # Start subprocess...
+ p = subprocess.Popen(self.command_str,
+ stdout=tmp_stdout,
+ stderr=tmp_stderr,
+ shell=True)
+ # ... and wait for it to finish.
+ self.status = p.wait()
+
+ try:
+ # Rewind to start of files
+ tmp_stdout.seek(0)
+ tmp_stderr.seek(0)
+ # Read output
+ self.stdout = tmp_stdout.read()
+ self.stderr = tmp_stderr.read()
+ finally:
+ # Remove temp files by closing them
+ tmp_stdout.close()
+ tmp_stderr.close()
+ else:
+ def execute(self):
+ p = subprocess.Popen(self.command_str,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ shell=True)
+ self.stdout = p.stdout.read()
+ self.stderr = p.stderr.read()
+ self.status = p.wait()
class XML(PopenExecutor):
def header(self, f):
@@ -435,7 +510,6 @@ class XML(PopenExecutor):
format_class = {
None : SystemExecutor,
- '--aegis' : Aegis,
'--xml' : XML,
}
@@ -490,21 +564,6 @@ else:
sd = None
ld = None
- # XXX: Logic like the following will be necessary once
- # we fix runtest.py to run tests within an Aegis change
- # without symlinks back to the baseline(s).
- #
- #if spe:
- # if not scons:
- # for dir in spe:
- # d = os.path.join(dir, 'src', 'script')
- # f = os.path.join(d, 'scons.py')
- # if os.path.isfile(f):
- # sd = d
- # scons = f
- # spe = map(lambda x: os.path.join(x, 'src', 'engine'), spe)
- # ld = os.pathsep.join(spe)
-
if not baseline or baseline == '.':
base = cwd
elif baseline == '-':
@@ -541,9 +600,12 @@ else:
scons_runtest_dir = base
- scons_script_dir = sd or os.path.join(base, 'src', 'script')
-
- scons_lib_dir = ld or os.path.join(base, 'src', 'engine')
+ if not external:
+ scons_script_dir = sd or os.path.join(base, 'src', 'script')
+ scons_lib_dir = ld or os.path.join(base, 'src', 'engine')
+ else:
+ scons_script_dir = sd or ''
+ scons_lib_dir = ld or ''
pythonpath_dir = scons_lib_dir
@@ -560,6 +622,9 @@ elif scons_lib_dir:
if scons_exec:
os.environ['SCONS_EXEC'] = '1'
+if external:
+ os.environ['SCONS_EXTERNAL_TEST'] = '1'
+
os.environ['SCONS_RUNTEST_DIR'] = scons_runtest_dir
os.environ['SCONS_SCRIPT_DIR'] = scons_script_dir
os.environ['SCONS_CWD'] = cwd
@@ -576,13 +641,12 @@ old_pythonpath = os.environ.get('PYTHONPATH')
pythonpaths = [ pythonpath_dir ]
for dir in sp:
- if format == '--aegis':
- q = os.path.join(dir, 'build', 'QMTest')
- else:
- q = os.path.join(dir, 'QMTest')
+ q = os.path.join(dir, 'QMTest')
pythonpaths.append(q)
-os.environ['SCONS_SOURCE_PATH_EXECUTABLE'] = os.pathsep.join(spe)
+# Add path of the QMTest folder to PYTHONPATH
+scriptpath = os.path.dirname(os.path.realpath(__file__))
+pythonpaths.append(os.path.join(scriptpath, 'QMTest'))
os.environ['PYTHONPATH'] = os.pathsep.join(pythonpaths)
@@ -599,6 +663,9 @@ tests = []
def find_Tests_py(directory):
result = []
for dirpath, dirnames, filenames in os.walk(directory):
+ # Skip folders containing a sconstest.skip file
+ if 'sconstest.skip' in filenames:
+ continue
for fname in filenames:
if fname.endswith("Tests.py"):
result.append(os.path.join(dirpath, fname))
@@ -607,6 +674,9 @@ def find_Tests_py(directory):
def find_py(directory):
result = []
for dirpath, dirnames, filenames in os.walk(directory):
+ # Skip folders containing a sconstest.skip file
+ if 'sconstest.skip' in filenames:
+ continue
try:
exclude_fp = open(os.path.join(dirpath, ".exclude_tests"))
except EnvironmentError:
@@ -620,28 +690,16 @@ def find_py(directory):
return sorted(result)
if args:
- if spe:
- for a in args:
- if os.path.isabs(a):
- tests.extend(glob.glob(a))
+ for a in args:
+ for path in glob.glob(a):
+ if os.path.isdir(path):
+ if path[:3] == 'src':
+ tests.extend(find_Tests_py(path))
+
+ elif path[:4] == 'test':
+ tests.extend(find_py(path))
else:
- for dir in spe:
- x = os.path.join(dir, a)
- globs = glob.glob(x)
- if globs:
- tests.extend(globs)
- break
- else:
- for a in args:
- for path in glob.glob(a):
- if os.path.isdir(path):
- if path[:3] == 'src':
- tests.extend(find_Tests_py(path))
-
- elif path[:4] == 'test':
- tests.extend(find_py(path))
- else:
- tests.append(path)
+ tests.append(path)
elif testlistfile:
tests = open(testlistfile, 'r').readlines()
tests = [x for x in tests if x[0] != '#']
@@ -661,20 +719,6 @@ elif all and not qmtest:
# things correctly.)
tests.extend(find_Tests_py('src'))
tests.extend(find_py('test'))
- if format == '--aegis' and aegis:
- cmd = "aegis -list -unf pf 2>/dev/null"
- for line in os.popen(cmd, "r").readlines():
- a = line.split()
- if a[0] == "test" and a[-1] not in tests:
- tests.append(Test(a[-1], spe))
- cmd = "aegis -list -unf cf 2>/dev/null"
- for line in os.popen(cmd, "r").readlines():
- a = line.split()
- if a[0] == "test":
- if a[1] == "remove":
- tests.remove(a[-1])
- elif a[-1] not in tests:
- tests.append(Test(a[-1], spe))
tests.sort()
if qmtest:
@@ -690,12 +734,6 @@ if qmtest:
qmtest_args = [ qmtest, ]
- if format == '--aegis':
- dir = builddir
- if not os.path.isdir(dir):
- dir = cwd
- qmtest_args.extend(['-D', dir])
-
qmtest_args.extend([
'run',
'--output %s' % qmr_file,
@@ -715,10 +753,7 @@ if qmtest:
rs = '--result-stream="%s(filename=%s)"' % (rsclass, qof)
qmtest_args.append(rs)
- if format == '--aegis':
- tests = [x.replace(cwd+os.sep, '') for x in tests]
- else:
- os.environ['SCONS'] = os.path.join(cwd, 'src', 'script', 'scons.py')
+ os.environ['SCONS'] = os.path.join(cwd, 'src', 'script', 'scons.py')
cmd = ' '.join(qmtest_args + tests)
if printcommand:
@@ -740,7 +775,7 @@ if qmtest:
#except OSError:
# pass
-tests = list(map(Test, tests))
+tests = [Test(t) for t in tests]
class Unbuffered(object):
def __init__(self, file):
@@ -783,7 +818,8 @@ else:
print_time_func = lambda fmt, time: None
total_start_time = time_func()
-for t in tests:
+total_num_tests = len(tests)
+for idx,t in enumerate(tests):
command_args = ['-tt']
if python3incompatibilities:
command_args.append('-3')
@@ -793,10 +829,26 @@ for t in tests:
t.command_args = [python] + command_args
t.command_str = " ".join([escape(python)] + command_args)
if printcommand:
- sys.stdout.write(t.command_str + "\n")
+ if print_progress:
+ sys.stdout.write("%d/%d (%.2f%s) %s\n" % (idx+1, total_num_tests,
+ float(idx+1)*100.0/float(total_num_tests),
+ '%',
+ t.command_str))
+ else:
+ sys.stdout.write(t.command_str + "\n")
+ head, tail = os.path.split(t.abspath)
+ if head:
+ os.environ['PYTHON_SCRIPT_DIR'] = head
+ else:
+ os.environ['PYTHON_SCRIPT_DIR'] = ''
test_start_time = time_func()
if execute_tests:
t.execute()
+ if not suppress_stdout and t.stdout:
+ print t.stdout
+ if not suppress_stderr and t.stderr:
+ print t.stderr
+
t.test_time = time_func() - test_start_time
print_time_func("Test execution time: %.1f seconds\n", t.test_time)
if len(tests) > 0:
@@ -844,9 +896,7 @@ if outputfile:
if outputfile != '-':
f.close()
-if format == '--aegis':
- sys.exit(0)
-elif len(fail):
+if len(fail):
sys.exit(1)
elif len(no_result):
sys.exit(2)