summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordirkbaechle <devnull@localhost>2012-08-03 21:01:53 (GMT)
committerdirkbaechle <devnull@localhost>2012-08-03 21:01:53 (GMT)
commit4d8c8379e1d4c7860a1c1d29b7302b394c920844 (patch)
tree30339b821646f25aa0b03076722b8effc821b7eb
parente5ed166538a2e12893346f9778cba8037f2dd847 (diff)
downloadSCons-4d8c8379e1d4c7860a1c1d29b7302b394c920844.zip
SCons-4d8c8379e1d4c7860a1c1d29b7302b394c920844.tar.gz
SCons-4d8c8379e1d4c7860a1c1d29b7302b394c920844.tar.bz2
- basic merge with source from the external scons-test-framework
-rw-r--r--QMTest/TestCmd.py124
-rw-r--r--QMTest/TestSCons.py170
-rw-r--r--runtest.py142
3 files changed, 353 insertions, 83 deletions
diff --git a/QMTest/TestCmd.py b/QMTest/TestCmd.py
index a27df27..8d382ce 100644
--- a/QMTest/TestCmd.py
+++ b/QMTest/TestCmd.py
@@ -897,6 +897,11 @@ class TestCmd(object):
combine = 0,
universal_newlines = 1,
timeout = None):
+ self.external = 0
+ try:
+ self.external = os.environ.get('SCONS_EXTERNAL_TEST', 0)
+ except KeyError:
+ pass
self._cwd = os.getcwd()
self.description_set(description)
self.program_set(program)
@@ -1009,13 +1014,19 @@ class TestCmd(object):
def command_args(self, program = None,
interpreter = None,
arguments = None):
- if program:
- if isinstance(program, str) and not os.path.isabs(program):
- program = os.path.join(self._cwd, program)
+ if not self.external:
+ if program:
+ if isinstance(program, str) and not os.path.isabs(program):
+ program = os.path.join(self._cwd, program)
+ else:
+ program = self.program
+ if not interpreter:
+ interpreter = self.interpreter
else:
- program = self.program
- if not interpreter:
- interpreter = self.interpreter
+ if not program:
+ program = self.program
+ if not interpreter:
+ interpreter = self.interpreter
if not isinstance(program, (list, tuple)):
program = [program]
cmd = list(program)
@@ -1189,8 +1200,9 @@ class TestCmd(object):
def program_set(self, program):
"""Set the executable program or script to be tested.
"""
- if program and not os.path.isabs(program):
- program = os.path.join(self._cwd, program)
+ if not self.external:
+ if program and not os.path.isabs(program):
+ program = os.path.join(self._cwd, program)
self.program = program
def read(self, file, mode = 'rb'):
@@ -1227,6 +1239,96 @@ class TestCmd(object):
self.timeout = timeout
self.timer = None
+ def parse_path(self, path, suppress_current=False):
+ """Return a list with the single path components of path.
+ """
+ head, tail = os.path.split(path)
+ result = []
+ if not tail:
+ if head == path:
+ return [head]
+ else:
+ result.append(tail)
+ head, tail = os.path.split(head)
+ while head and tail:
+ result.append(tail)
+ head, tail = os.path.split(head)
+ result.append(head or tail)
+ result.reverse()
+
+ return result
+
+ def dir_fixture(self, srcdir, dstdir=None):
+ """Copies the contents of the specified folder srcdir from
+ the directory of the called script, to the current
+ working directory.
+ The srcdir name may be a list, in which case the elements are
+ concatenated with the os.path.join() method. The dstdir is
+ assumed to be under the temporary working directory, it gets
+ created automatically, if it does not already exist.
+ """
+ if srcdir and self.script_srcdir and not os.path.isabs(srcdir):
+ spath = os.path.join(self.script_srcdir, srcdir)
+ else:
+ spath = srcdir
+ if dstdir:
+ dstdir = self.canonicalize(dstdir)
+ else:
+ dstdir = '.'
+
+ if dstdir != '.' and not os.path.exists(dstdir):
+ dstlist = self.parse_path(dstdir)
+ if len(dstlist) > 0 and dstlist[0] == ".":
+ dstlist = dstlist[1:]
+ for idx in range(len(dstlist)):
+ self.subdir(dstlist[:idx+1])
+
+ if dstdir and self.workdir:
+ dstdir = os.path.join(self.workdir, dstdir)
+
+ for entry in os.listdir(spath):
+ epath = os.path.join(spath, entry)
+ dpath = os.path.join(dstdir, entry)
+ if os.path.isdir(epath):
+ # Copy the subfolder
+ shutil.copytree(epath, dpath)
+ else:
+ shutil.copy(epath, dpath)
+
+ def file_fixture(self, srcfile, dstfile=None):
+ """Copies the file srcfile from the directory of
+ the called script, to the current working directory.
+ The dstfile is assumed to be under the temporary working
+ directory unless it is an absolute path name.
+ If dstfile is specified its target directory gets created
+ automatically, if it does not already exist.
+ """
+ srcpath, srctail = os.path.split(srcfile)
+ if srcpath:
+ if self.script_srcdir and not os.path.isabs(srcpath):
+ spath = os.path.join(self.script_srcdir, srcfile)
+ else:
+ spath = srcfile
+ else:
+ spath = os.path.join(self.script_srcdir, srcfile)
+ if not dstfile:
+ if srctail:
+ dpath = os.path.join(self.workdir, srctail)
+ else:
+ return
+ else:
+ dstpath, dsttail = os.path.split(dstfile)
+ if dstpath:
+ if not os.path.exists(os.path.join(self.workdir, dstpath)):
+ dstlist = self.parse_path(dstpath)
+ if len(dstlist) > 0 and dstlist[0] == ".":
+ dstlist = dstlist[1:]
+ for idx in range(len(dstlist)):
+ self.subdir(dstlist[:idx+1])
+
+ dpath = os.path.join(self.workdir, dstfile)
+ shutil.copy(spath, dpath)
+
def start(self, program = None,
interpreter = None,
arguments = None,
@@ -1304,6 +1406,12 @@ class TestCmd(object):
The specified program will have the original directory
prepended unless it is enclosed in a [list].
"""
+ if self.external:
+ if not program:
+ program = self.program
+ if not interpreter:
+ interpreter = self.interpreter
+
if chdir:
oldcwd = os.getcwd()
if not os.path.isabs(chdir):
diff --git a/QMTest/TestSCons.py b/QMTest/TestSCons.py
index 4f04a48..cd3836d 100644
--- a/QMTest/TestSCons.py
+++ b/QMTest/TestSCons.py
@@ -231,20 +231,36 @@ class TestSCons(TestCommon):
is not necessary.
"""
self.orig_cwd = os.getcwd()
+ self.external = 0
try:
- script_dir = os.environ['SCONS_SCRIPT_DIR']
+ self.external = os.environ.get('SCONS_EXTERNAL_TEST', 0)
except KeyError:
pass
+
+ if not self.external:
+ try:
+ script_dir = os.environ['SCONS_SCRIPT_DIR']
+ except KeyError:
+ pass
+ else:
+ os.chdir(script_dir)
else:
- os.chdir(script_dir)
+ try:
+ self.script_srcdir = os.environ['PYTHON_SCRIPT_DIR']
+ except KeyError:
+ pass
if 'program' not in kw:
kw['program'] = os.environ.get('SCONS')
if not kw['program']:
- if os.path.exists('scons'):
- kw['program'] = 'scons'
+ if not self.external:
+ if os.path.exists('scons'):
+ kw['program'] = 'scons'
+ else:
+ kw['program'] = 'scons.py'
else:
- kw['program'] = 'scons.py'
- elif not os.path.isabs(kw['program']):
+ kw['program'] = 'scons'
+ kw['interpreter'] = ''
+ elif not self.external and not os.path.isabs(kw['program']):
kw['program'] = os.path.join(self.orig_cwd, kw['program'])
if 'interpreter' not in kw and not os.environ.get('SCONS_EXEC'):
kw['interpreter'] = [python, '-tt']
@@ -264,23 +280,27 @@ class TestSCons(TestCommon):
TestCommon.__init__(self, **kw)
- import SCons.Node.FS
- if SCons.Node.FS.default_fs is None:
- SCons.Node.FS.default_fs = SCons.Node.FS.FS()
+ if not self.external:
+ import SCons.Node.FS
+ if SCons.Node.FS.default_fs is None:
+ SCons.Node.FS.default_fs = SCons.Node.FS.FS()
def Environment(self, ENV=None, *args, **kw):
"""
Return a construction Environment that optionally overrides
the default external environment with the specified ENV.
"""
- import SCons.Environment
- import SCons.Errors
- if not ENV is None:
- kw['ENV'] = ENV
- try:
- return SCons.Environment.Environment(*args, **kw)
- except (SCons.Errors.UserError, SCons.Errors.InternalError):
- return None
+ if not self.external:
+ import SCons.Environment
+ import SCons.Errors
+ if not ENV is None:
+ kw['ENV'] = ENV
+ try:
+ return SCons.Environment.Environment(*args, **kw)
+ except (SCons.Errors.UserError, SCons.Errors.InternalError):
+ return None
+
+ return None
def detect(self, var, prog=None, ENV=None, norm=None):
"""
@@ -292,17 +312,20 @@ class TestSCons(TestCommon):
used as prog.
"""
env = self.Environment(ENV)
- v = env.subst('$'+var)
- if not v:
- return None
- if prog is None:
- prog = v
- if v != prog:
- return None
- result = env.WhereIs(prog)
- if norm and os.sep != '/':
- result = result.replace(os.sep, '/')
- return result
+ if env:
+ v = env.subst('$'+var)
+ if not v:
+ return None
+ if prog is None:
+ prog = v
+ if v != prog:
+ return None
+ result = env.WhereIs(prog)
+ if norm and os.sep != '/':
+ result = result.replace(os.sep, '/')
+ return result
+
+ return self.where_is(prog)
def detect_tool(self, tool, prog=None, ENV=None):
"""
@@ -324,13 +347,35 @@ class TestSCons(TestCommon):
def where_is(self, prog, path=None):
"""
Given a program, search for it in the specified external PATH,
- or in the actual external PATH is none is specified.
+ or in the actual external PATH if none is specified.
"""
- import SCons.Environment
- env = SCons.Environment.Environment()
if path is None:
path = os.environ['PATH']
- return env.WhereIs(prog, path)
+ if isinstance(prog, str):
+ prog = [prog]
+ if self.external:
+ import stat
+ paths = path.split(os.pathsep)
+ for p in prog:
+ for d in paths:
+ f = os.path.join(d, p)
+ if os.path.isfile(f):
+ try:
+ st = os.stat(f)
+ except OSError:
+ # os.stat() raises OSError, not IOError if the file
+ # doesn't exist, so in this case we let IOError get
+ # raised so as to not mask possibly serious disk or
+ # network issues.
+ continue
+ if stat.S_IMODE(st[stat.ST_MODE]) & 0111:
+ return os.path.normpath(f)
+ else:
+ import SCons.Environment
+ env = SCons.Environment.Environment()
+ return env.WhereIs(prog, path)
+
+ return None
def wrap_stdout(self, build_str = "", read_str = "", error = 0, cleaning = 0):
"""Wraps standard output string(s) in the normal
@@ -616,36 +661,39 @@ class TestSCons(TestCommon):
Initialize with a default external environment that uses a local
Java SDK in preference to whatever's found in the default PATH.
"""
- try:
- return self._java_env[version]['ENV']
- except AttributeError:
- self._java_env = {}
- except KeyError:
- pass
-
- import SCons.Environment
- env = SCons.Environment.Environment()
- self._java_env[version] = env
-
-
- if version:
- patterns = [
- '/usr/java/jdk%s*/bin' % version,
- '/usr/lib/jvm/*-%s*/bin' % version,
- '/usr/local/j2sdk%s*/bin' % version,
- ]
- java_path = self.paths(patterns) + [env['ENV']['PATH']]
- else:
- patterns = [
- '/usr/java/latest/bin',
- '/usr/lib/jvm/*/bin',
- '/usr/local/j2sdk*/bin',
- ]
- java_path = self.paths(patterns) + [env['ENV']['PATH']]
-
- env['ENV']['PATH'] = os.pathsep.join(java_path)
- return env['ENV']
+ if not self.external:
+ try:
+ return self._java_env[version]['ENV']
+ except AttributeError:
+ self._java_env = {}
+ except KeyError:
+ pass
+
+ import SCons.Environment
+ env = SCons.Environment.Environment()
+ self._java_env[version] = env
+
+
+ if version:
+ patterns = [
+ '/usr/java/jdk%s*/bin' % version,
+ '/usr/lib/jvm/*-%s*/bin' % version,
+ '/usr/local/j2sdk%s*/bin' % version,
+ ]
+ java_path = self.paths(patterns) + [env['ENV']['PATH']]
+ else:
+ patterns = [
+ '/usr/java/latest/bin',
+ '/usr/lib/jvm/*/bin',
+ '/usr/local/j2sdk*/bin',
+ ]
+ java_path = self.paths(patterns) + [env['ENV']['PATH']]
+
+ env['ENV']['PATH'] = os.pathsep.join(java_path)
+ return env['ENV']
+ return None
+
def java_where_includes(self,version=None):
"""
Return java include paths compiling java jni code
diff --git a/runtest.py b/runtest.py
index 58950dd..281bfcc 100644
--- a/runtest.py
+++ b/runtest.py
@@ -28,6 +28,9 @@
#
# -a Run all tests; does a virtual 'find' for
# all SCons tests under the current directory.
+# You can also specify a list of subdirectories
+# (not available with the "--qmtest" option!). Then,
+# only the given folders are searched for test files.
#
# --aegis Print test results to an output file (specified
# by the -o option) in the format expected by
@@ -39,11 +42,17 @@
# 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.
#
+# -j Suppress printing of count and percent progress for
+# the single tests.
+#
# -l List available tests and exit.
#
# -n No execute, just print command lines.
@@ -64,6 +73,12 @@
# command line it will execute before
# executing it. This suppresses that print.
#
+# -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)!
+#
# --sp The Aegis search path.
#
# --spe The Aegis executable search path.
@@ -124,6 +139,7 @@ cwd = os.getcwd()
all = 0
baseline = 0
builddir = os.path.join(cwd, 'build')
+external = 0
debug = ''
execute_tests = 1
format = None
@@ -141,6 +157,9 @@ print_times = None
python = None
sp = None
spe = None
+print_progress = 1
+suppress_stdout = False
+suppress_stderr = False
helpstr = """\
Usage: runtest.py [OPTIONS] [TEST ...]
@@ -151,8 +170,10 @@ Options:
-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.
+ -j, --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.
@@ -188,12 +209,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",
+opts, args = getopt.getopt(sys.argv[1:], "3ab:def:hjlno:P:p:qsv:Xx:t",
['all', 'aegis', 'baseline=', 'builddir=',
- 'debug', 'file=', 'help',
+ 'debug', 'external', 'file=', 'help', 'no-progress',
'list', 'no-exec', 'noqmtest', 'output=',
'package=', 'passed', 'python=', 'qmtest',
- 'quiet', 'sp=', 'spe=', 'time',
+ 'quiet', 'short-progress', 'sp=', 'spe=', 'time',
'version=', 'exec=',
'verbose=', 'xml'])
@@ -214,6 +235,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,6 +244,8 @@ for o, a in opts:
elif o in ['-h', '--help']:
print helpstr
sys.exit(0)
+ elif o in ['-j', '--no-progress']:
+ print_progress = 0
elif o in ['-l', '--list']:
list_only = 1
elif o in ['-n', '--no-exec']:
@@ -245,6 +270,12 @@ for o, a in opts:
qmtest = 'qmtest'
elif o in ['-q', '--quiet']:
printcommand = 0
+ suppress_stdout = True
+ suppress_stderr = True
+ elif o in ['-s', '--short-progress']:
+ print_progress = 1
+ suppress_stdout = True
+ suppress_stderr = True
elif o in ['--sp']:
sp = a.split(os.pathsep)
elif o in ['--spe']:
@@ -349,6 +380,40 @@ def escape(s):
s = s.replace('\\', '\\\\')
return s
+# Try to use subprocess instead of the more low-level
+# spawn command...
+has_subprocess = True
+try:
+ import subprocess
+
+ 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())
+except:
+ 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 = list(map(escape, 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 +428,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 has_subprocess:
import popen2
try:
popen2.Popen3
@@ -541,9 +603,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 +625,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
@@ -582,6 +650,10 @@ for dir in sp:
q = os.path.join(dir, 'QMTest')
pythonpaths.append(q)
+# 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['SCONS_SOURCE_PATH_EXECUTABLE'] = os.pathsep.join(spe)
os.environ['PYTHONPATH'] = os.pathsep.join(pythonpaths)
@@ -599,6 +671,8 @@ tests = []
def find_Tests_py(directory):
result = []
for dirpath, dirnames, filenames in os.walk(directory):
+ if 'sconstest.skip' in filenames:
+ continue
for fname in filenames:
if fname.endswith("Tests.py"):
result.append(os.path.join(dirpath, fname))
@@ -607,6 +681,8 @@ def find_Tests_py(directory):
def find_py(directory):
result = []
for dirpath, dirnames, filenames in os.walk(directory):
+ if 'sconstest.skip' in filenames:
+ continue
try:
exclude_fp = open(os.path.join(dirpath, ".exclude_tests"))
except EnvironmentError:
@@ -619,6 +695,24 @@ def find_py(directory):
result.append(os.path.join(dirpath, fname))
return sorted(result)
+def find_sconstest_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:
+ excludes = []
+ else:
+ excludes = [ e.split('#', 1)[0].strip()
+ for e in exclude_fp.readlines() ]
+ for fname in filenames:
+ if fname.endswith(".py") and fname.startswith("sconstest-") and fname not in excludes:
+ result.append(os.path.join(dirpath, fname))
+ return sorted(result)
+
if args:
if spe:
for a in args:
@@ -640,6 +734,7 @@ if args:
elif path[:4] == 'test':
tests.extend(find_py(path))
+ tests.extend(find_sconstest_py(path))
else:
tests.append(path)
elif testlistfile:
@@ -661,6 +756,7 @@ elif all and not qmtest:
# things correctly.)
tests.extend(find_Tests_py('src'))
tests.extend(find_py('test'))
+ tests.extend(find_sconstest_py('test'))
if format == '--aegis' and aegis:
cmd = "aegis -list -unf pf 2>/dev/null"
for line in os.popen(cmd, "r").readlines():
@@ -783,7 +879,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 +890,27 @@ 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")
+ if external:
+ 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: