5 # runtest.py - wrapper script for running SCons tests
7 # This script mainly exists to set PYTHONPATH to the right list of
8 # directories to test the SCons modules.
10 # By default, it directly uses the modules in the local tree:
11 # ./src/ (source files we ship) and ./QMTest/ (other modules we don't).
13 # HOWEVER, now that SCons has Repository support, we don't have
14 # Aegis copy all of the files into the local tree. So if you're
15 # using Aegis and want to run tests by hand using this script, you
16 # must "aecp ." the entire source tree into your local directory
17 # structure. When you're done with your change, you can then
18 # "aecpu -unch ." to un-copy any files that you haven't changed.
20 # When any -p option is specified, this script assumes it's in a
21 # directory in which a build has been performed, and sets PYTHONPATH
22 # so that it *only* references the modules that have unpacked from
23 # the specified built package, to test whether the packages are good.
27 # -a Run all tests; does a virtual 'find' for
28 # all SCons tests under the current directory.
30 # --aegis Print test results to an output file (specified
31 # by the -o option) in the format expected by
32 # aetest(5). This is intended for use in the
33 # batch_test_command field in the Aegis project
36 # -d Debug. Runs the script under the Python
37 # debugger (pdb.py) so you don't have to
38 # muck with PYTHONPATH yourself.
40 # -f file Only execute the tests listed in the specified
43 # -h Print the help and exit.
45 # -l List available tests and exit.
47 # -n No execute, just print command lines.
49 # -o file Print test results to the specified file.
50 # The --aegis and --xml options specify the
53 # -P Python Use the specified Python interpreter.
55 # -p package Test against the specified package.
57 # --passed In the final summary, also report which tests
58 # passed. The default is to only report tests
59 # which failed or returned NO RESULT.
61 # -q Quiet. By default, runtest.py prints the
62 # command line it will execute before
63 # executing it. This suppresses that print.
65 # --sp The Aegis search path.
67 # --spe The Aegis executable search path.
69 # -t Print the execution time of each test.
71 # -X The scons "script" is an executable; don't
74 # -x scons The scons script to use for tests.
76 # --xml Print test results to an output file (specified
77 # by the -o option) in an SCons-specific XML format.
78 # This is (will be) used for reporting results back
79 # to a central SCons test monitoring infrastructure.
81 # (Note: There used to be a -v option that specified the SCons
82 # version to be tested, when we were installing in a version-specific
83 # library directory. If we ever resurrect that as the default, then
84 # you can find the appropriate code in the 0.04 version of this script,
85 # rather than reinventing that wheel.)
87 from __future__ import generators ### KEEP FOR COMPATIBILITY FIXERS
97 if not hasattr(os, 'WEXITSTATUS'):
98 os.WEXITSTATUS = lambda x: x
104 builddir = os.path.join(cwd, 'build')
111 print_passed_summary = None
123 Usage: runtest.py [OPTIONS] [TEST ...]
125 -a, --all Run all tests.
126 --aegis Print results in Aegis format.
127 -b BASE, --baseline BASE Run test scripts against baseline BASE.
128 --builddir DIR Directory in which packages were built.
129 -d, --debug Run test scripts under the Python debugger.
130 -f FILE, --file FILE Run tests in specified FILE.
131 -h, --help Print this message and exit.
132 -l, --list List available tests and exit.
133 -n, --no-exec No execute, just print command lines.
134 --noqmtest Execute tests directly, not using QMTest.
135 -o FILE, --output FILE Print test results to FILE.
136 -P Python Use the specified Python interpreter.
137 -p PACKAGE, --package PACKAGE
138 Test against the specified PACKAGE:
140 local-tar-gz .tar.gz standalone package
141 local-zip .zip standalone package
143 src-tar-gz .tar.gz source package
144 src-zip .zip source package
145 tar-gz .tar.gz distribution
146 zip .zip distribution
147 --passed Summarize which tests passed.
148 --qmtest Run using the QMTest harness.
149 -q, --quiet Don't print the test being executed.
150 --sp PATH The Aegis search path.
151 --spe PATH The Aegis executable search path.
152 -t, --time Print test execution time.
153 -v version Specify the SCons version.
154 --verbose=LEVEL Set verbose level: 1 = print executed commands,
155 2 = print commands and non-zero output,
156 3 = print commands and all output.
157 -X Test script is executable, don't feed to Python.
158 -x SCRIPT, --exec SCRIPT Test SCRIPT.
159 --xml Print results in SCons XML format.
162 opts, args = getopt.getopt(sys.argv[1:], "ab:df:hlno:P:p:qv:Xx:t",
163 ['all', 'aegis', 'baseline=', 'builddir=',
164 'debug', 'file=', 'help',
165 'list', 'no-exec', 'noqmtest', 'output=',
166 'package=', 'passed', 'python=', 'qmtest',
167 'quiet', 'sp=', 'spe=', 'time',
172 if o in ['-a', '--all']:
174 elif o in ['-b', '--baseline']:
176 elif o in ['--builddir']:
178 if not os.path.isabs(builddir):
179 builddir = os.path.normpath(os.path.join(cwd, builddir))
180 elif o in ['-d', '--debug']:
182 pdb = os.path.join(dir, 'pdb.py')
183 if os.path.exists(pdb):
186 elif o in ['-f', '--file']:
187 if not os.path.isabs(a):
188 a = os.path.join(cwd, a)
190 elif o in ['-h', '--help']:
193 elif o in ['-l', '--list']:
195 elif o in ['-n', '--no-exec']:
197 elif o in ['--noqmtest']:
199 elif o in ['-o', '--output']:
200 if a != '-' and not os.path.isabs(a):
201 a = os.path.join(cwd, a)
203 elif o in ['-p', '--package']:
205 elif o in ['--passed']:
206 print_passed_summary = 1
207 elif o in ['-P', '--python']:
209 elif o in ['--qmtest']:
210 if sys.platform == 'win32':
211 # typically in c:/PythonXX/Scripts
215 elif o in ['-q', '--quiet']:
218 sp = a.split(os.pathsep)
220 spe = a.split(os.pathsep)
221 elif o in ['-t', '--time']:
223 elif o in ['--verbose']:
224 os.environ['TESTCMD_VERBOSE'] = a
225 elif o in ['-v', '--version']:
229 elif o in ['-x', '--exec']:
231 elif o in ['--aegis', '--xml']:
234 if not args and not all and not testlistfile:
235 sys.stderr.write("""\
236 runtest.py: No tests were specified.
237 List one or more tests on the command line, use the
238 -f option to specify a file containing a list of tests,
239 or use the -a option to find and run all tests.
244 if sys.platform in ('win32', 'cygwin'):
247 pathext = [''] + os.environ['PATHEXT'].split(os.pathsep)
248 for dir in os.environ['PATH'].split(os.pathsep):
249 f = os.path.join(dir, file)
252 if os.path.isfile(fext):
259 for dir in os.environ['PATH'].split(os.pathsep):
260 f = os.path.join(dir, file)
261 if os.path.isfile(f):
266 if stat.S_IMODE(st[stat.ST_MODE]) & 0111:
270 # See if --qmtest or --noqmtest specified
275 # Old code for using QMTest by default if it's installed.
276 # We now default to not using QMTest unless explicitly asked for.
277 #for q in ['qmtest', 'qmtest.py']:
280 # # The name was found on $PATH; just execute the found name so
281 # # we don't have to worry about paths containing white space.
285 # msg = ('Warning: found neither qmtest nor qmtest.py on $PATH;\n' +
286 # '\tassuming --noqmtest option.\n')
287 # sys.stderr.write(msg)
290 aegis = whereis('aegis')
292 if format == '--aegis' and aegis:
293 change = os.popen("aesub '$c' 2>/dev/null", "r").read()
296 paths = os.popen("aesub '$sp' 2>/dev/null", "r").read()[:-1]
297 sp = paths.split(os.pathsep)
299 spe = os.popen("aesub '$spe' 2>/dev/null", "r").read()[:-1]
300 spe = spe.split(os.pathsep)
313 _ws = re.compile('\s')
318 s = s.replace('\\', '\\\\')
321 # Set up lowest-common-denominator spawning of a process on both Windows
322 # and non-Windows systems that works all the way back to Python 1.5.2.
325 except AttributeError:
326 def spawn_it(command_args):
329 os.execv(command_args[0], command_args)
331 pid, status = os.waitpid(pid, 0)
334 def spawn_it(command_args):
335 command = command_args[0]
336 command_args = list(map(escape, command_args))
337 return os.spawnv(os.P_WAIT, command, command_args)
340 def __init__(self, path, spe=None):
342 self.abspath = os.path.abspath(path)
345 f = os.path.join(dir, path)
346 if os.path.isfile(f):
351 class SystemExecutor(Base):
353 s = spawn_it(self.command_args)
356 sys.stdout.write("Unexpected exit status %d\n" % s)
364 except AttributeError:
365 class PopenExecutor(Base):
367 (tochild, fromchild, childerr) = os.popen3(self.command_str)
369 self.stderr = childerr.read()
370 self.stdout = fromchild.read()
372 self.status = childerr.close()
376 self.status = self.status >> 8
378 class PopenExecutor(Base):
380 p = popen2.Popen3(self.command_str, 1)
382 self.stdout = p.fromchild.read()
383 self.stderr = p.childerr.read()
384 self.status = p.wait()
385 self.status = self.status >> 8
387 class PopenExecutor(Base):
389 p = subprocess.Popen(self.command_str,
390 stdout=subprocess.PIPE,
391 stderr=subprocess.PIPE,
393 self.stdout = p.stdout.read()
394 self.stderr = p.stderr.read()
395 self.status = p.wait()
397 class Aegis(SystemExecutor):
399 f.write('test_result = [\n')
401 f.write(' { file_name = "%s";\n' % self.path)
402 f.write(' exit_status = %d; },\n' % self.status)
406 class XML(PopenExecutor):
408 f.write(' <results>\n')
411 f.write(' <file_name>%s</file_name>\n' % self.path)
412 f.write(' <command_line>%s</command_line>\n' % self.command_str)
413 f.write(' <exit_status>%s</exit_status>\n' % self.status)
414 f.write(' <stdout>%s</stdout>\n' % self.stdout)
415 f.write(' <stderr>%s</stderr>\n' % self.stderr)
416 f.write(' <time>%.1f</time>\n' % self.test_time)
417 f.write(' </test>\n')
419 f.write(' <time>%.1f</time>\n' % self.total_time)
420 f.write(' </results>\n')
423 None : SystemExecutor,
428 Test = format_class[format]
434 'local-tar-gz' : None,
443 # The hard-coded "python2.1" here is the library directory
444 # name on Debian systems, not an executable, so it's all right.
446 'deb' : os.path.join('python2.1', 'site-packages')
449 if package not in dir:
450 sys.stderr.write("Unknown package '%s'\n" % package)
453 test_dir = os.path.join(builddir, 'test-%s' % package)
455 if dir[package] is None:
456 scons_script_dir = test_dir
457 globs = glob.glob(os.path.join(test_dir, 'scons-local-*'))
459 sys.stderr.write("No `scons-local-*' dir in `%s'\n" % test_dir)
462 pythonpath_dir = globs[len(globs)-1]
463 elif sys.platform == 'win32':
464 scons_script_dir = os.path.join(test_dir, dir[package], 'Scripts')
465 scons_lib_dir = os.path.join(test_dir, dir[package])
466 pythonpath_dir = scons_lib_dir
468 scons_script_dir = os.path.join(test_dir, dir[package], 'bin')
469 l = lib.get(package, 'scons')
470 scons_lib_dir = os.path.join(test_dir, dir[package], 'lib', l)
471 pythonpath_dir = scons_lib_dir
473 scons_runtest_dir = builddir
479 # XXX: Logic like the following will be necessary once
480 # we fix runtest.py to run tests within an Aegis change
481 # without symlinks back to the baseline(s).
486 # d = os.path.join(dir, 'src', 'script')
487 # f = os.path.join(d, 'scons.py')
488 # if os.path.isfile(f):
491 # spe = map(lambda x: os.path.join(x, 'src', 'engine'), spe)
492 # ld = os.pathsep.join(spe)
494 if not baseline or baseline == '.':
496 elif baseline == '-':
497 # Tentative code for fetching information directly from the
498 # QMTest context file.
501 #import qm.test.context
503 #context = qm.test.context.Context()
504 #context.Read('context')
507 svn_info = os.popen("svn info 2>&1", "r").read()
508 match = re.search('URL: (.*)', svn_info)
512 sys.stderr.write('runtest.py: could not find a URL:\n')
513 sys.stderr.write(svn_info)
516 base = tempfile.mkdtemp(prefix='runtest-tmp-')
518 command = 'cd %s && svn co -q %s' % (base, url)
520 base = os.path.join(base, os.path.split(url)[1])
528 scons_runtest_dir = base
530 scons_script_dir = sd or os.path.join(base, 'src', 'script')
532 scons_lib_dir = ld or os.path.join(base, 'src', 'engine')
534 pythonpath_dir = scons_lib_dir
537 # Let the version of SCons that the -x option pointed to find
539 os.environ['SCONS'] = scons
541 # Because SCons is really aggressive about finding its modules,
542 # it sometimes finds SCons modules elsewhere on the system.
543 # This forces SCons to use the modules that are being tested.
544 os.environ['SCONS_LIB_DIR'] = scons_lib_dir
547 os.environ['SCONS_EXEC'] = '1'
549 os.environ['SCONS_RUNTEST_DIR'] = scons_runtest_dir
550 os.environ['SCONS_SCRIPT_DIR'] = scons_script_dir
551 os.environ['SCONS_CWD'] = cwd
553 os.environ['SCONS_VERSION'] = version
555 old_pythonpath = os.environ.get('PYTHONPATH')
557 # FIXME: the following is necessary to pull in half of the testing
558 # harness from $srcdir/etc. Those modules should be transfered
559 # to QMTest/ once we completely cut over to using that as
560 # the harness, in which case this manipulation of PYTHONPATH
561 # should be able to go away.
562 pythonpaths = [ pythonpath_dir ]
565 if format == '--aegis':
566 q = os.path.join(dir, 'build', 'QMTest')
568 q = os.path.join(dir, 'QMTest')
569 pythonpaths.append(q)
571 os.environ['SCONS_SOURCE_PATH_EXECUTABLE'] = os.pathsep.join(spe)
573 os.environ['PYTHONPATH'] = os.pathsep.join(pythonpaths)
576 os.environ['PYTHONPATH'] = os.environ['PYTHONPATH'] + \
582 def find_Tests_py(tdict, dirname, names):
583 for n in [n for n in names if n[-8:] == "Tests.py"]:
584 tdict[os.path.join(dirname, n)] = 1
586 def find_py(tdict, dirname, names):
587 tests = [n for n in names if n[-3:] == ".py"]
589 excludes = open(os.path.join(dirname,".exclude_tests")).readlines()
590 except (OSError, IOError):
593 for exclude in excludes:
594 exclude = exclude.split('#' , 1)[0]
595 exclude = exclude.strip()
596 if not exclude: continue
597 tests = [n for n in tests if n != exclude]
599 tdict[os.path.join(dirname, n)] = 1
605 tests.extend(glob.glob(a))
608 x = os.path.join(dir, a)
615 for path in glob.glob(a):
616 if os.path.isdir(path):
618 if path[:3] == 'src':
619 os.path.walk(path, find_Tests_py, tdict)
620 elif path[:4] == 'test':
621 os.path.walk(path, find_py, tdict)
628 tests = open(testlistfile, 'r').readlines()
629 tests = [x for x in tests if x[0] != '#']
630 tests = [x[:-1] for x in tests]
631 elif all and not qmtest:
632 # Find all of the SCons functional tests in the local directory
633 # tree. This is anything under the 'src' subdirectory that ends
634 # with 'Tests.py', or any Python script (*.py) under the 'test'
637 # Note that there are some tests under 'src' that *begin* with
638 # 'test_', but they're packaging and installation tests, not
639 # functional tests, so we don't execute them by default. (They can
640 # still be executed by hand, though, and are routinely executed
641 # by the Aegis packaging build to make sure that we're building
644 os.path.walk('src', find_Tests_py, tdict)
645 os.path.walk('test', find_py, tdict)
646 if format == '--aegis' and aegis:
647 cmd = "aegis -list -unf pf 2>/dev/null"
648 for line in os.popen(cmd, "r").readlines():
650 if a[0] == "test" and a[-1] not in tdict:
651 tdict[a[-1]] = Test(a[-1], spe)
652 cmd = "aegis -list -unf cf 2>/dev/null"
653 for line in os.popen(cmd, "r").readlines():
658 elif a[-1] not in tdict:
659 tdict[a[-1]] = Test(a[-1], spe)
666 aegis_result_stream = 'scons_tdb.AegisBaselineStream'
667 qmr_file = 'baseline.qmr'
669 aegis_result_stream = 'scons_tdb.AegisChangeStream'
670 qmr_file = 'results.qmr'
673 aegis_result_stream = aegis_result_stream + "(print_time='1')"
675 qmtest_args = [ qmtest, ]
677 if format == '--aegis':
679 if not os.path.isdir(dir):
681 qmtest_args.extend(['-D', dir])
685 '--output %s' % qmr_file,
687 '--result-stream="%s"' % aegis_result_stream,
691 qmtest_args.append('--context python="%s"' % python)
694 if format == '--xml':
695 rsclass = 'scons_tdb.SConsXMLResultStream'
697 rsclass = 'scons_tdb.AegisBatchStream'
698 qof = "r'" + outputfile + "'"
699 rs = '--result-stream="%s(filename=%s)"' % (rsclass, qof)
700 qmtest_args.append(rs)
702 if format == '--aegis':
703 tests = [x.replace(cwd+os.sep, '') for x in tests]
705 os.environ['SCONS'] = os.path.join(cwd, 'src', 'script', 'scons.py')
707 cmd = ' '.join(qmtest_args + tests)
709 sys.stdout.write(cmd + '\n')
713 status = os.WEXITSTATUS(os.system(cmd))
717 # os.chdir(scons_script_dir)
721 tests = list(map(Test, tests))
724 def __init__(self, file):
726 def write(self, arg):
729 def __getattr__(self, attr):
730 return getattr(self.file, attr)
732 sys.stdout = Unbuffered(sys.stdout)
733 sys.stderr = Unbuffered(sys.stderr)
737 sys.stdout.write(t.path + "\n")
742 if os.name == 'java':
743 python = os.path.join(sys.prefix, 'jython')
745 python = sys.executable
747 # time.clock() is the suggested interface for doing benchmarking timings,
748 # but time.time() does a better job on Linux systems, so let that be
749 # the non-Windows default.
751 if sys.platform == 'win32':
752 time_func = time.clock
754 time_func = time.time
757 print_time_func = lambda fmt, time: sys.stdout.write(fmt % time)
759 print_time_func = lambda fmt, time: None
761 total_start_time = time_func()
763 command_args = ['-tt']
765 command_args.append(debug)
766 command_args.append(t.path)
767 t.command_args = [python] + command_args
768 t.command_str = " ".join([escape(python)] + command_args)
770 sys.stdout.write(t.command_str + "\n")
771 test_start_time = time_func()
774 t.test_time = time_func() - test_start_time
775 print_time_func("Test execution time: %.1f seconds\n", t.test_time)
777 tests[0].total_time = time_func() - total_start_time
778 print_time_func("Total execution time for all tests: %.1f seconds\n", tests[0].total_time)
780 passed = [t for t in tests if t.status == 0]
781 fail = [t for t in tests if t.status == 1]
782 no_result = [t for t in tests if t.status == 2]
784 if len(tests) != 1 and execute_tests:
785 if passed and print_passed_summary:
787 sys.stdout.write("\nPassed the following test:\n")
789 sys.stdout.write("\nPassed the following %d tests:\n" % len(passed))
790 paths = [x.path for x in passed]
791 sys.stdout.write("\t" + "\n\t".join(paths) + "\n")
794 sys.stdout.write("\nFailed the following test:\n")
796 sys.stdout.write("\nFailed the following %d tests:\n" % len(fail))
797 paths = [x.path for x in fail]
798 sys.stdout.write("\t" + "\n\t".join(paths) + "\n")
800 if len(no_result) == 1:
801 sys.stdout.write("\nNO RESULT from the following test:\n")
803 sys.stdout.write("\nNO RESULT from the following %d tests:\n" % len(no_result))
804 paths = [x.path for x in no_result]
805 sys.stdout.write("\t" + "\n\t".join(paths) + "\n")
808 if outputfile == '-':
811 f = open(outputfile, 'w')
813 #f.write("test_result = [\n")
818 if outputfile != '-':
821 if format == '--aegis':
832 # indent-tabs-mode:nil
834 # vim: set expandtab tabstop=4 shiftwidth=4: