From 4d8c8379e1d4c7860a1c1d29b7302b394c920844 Mon Sep 17 00:00:00 2001 From: dirkbaechle Date: Fri, 3 Aug 2012 23:01:53 +0200 Subject: - basic merge with source from the external scons-test-framework --- QMTest/TestCmd.py | 124 +++++++++++++++++++++++++++++++++++--- QMTest/TestSCons.py | 170 +++++++++++++++++++++++++++++++++------------------- runtest.py | 142 ++++++++++++++++++++++++++++++++++++++----- 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: -- cgit v0.12