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 ./etc/ (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 # -o file Print test results to the specified file.
46 # The --aegis and --xml options specify the
49 # -P Python Use the specified Python interpreter.
51 # -p package Test against the specified package.
53 # --passed In the final summary, also report which tests
54 # passed. The default is to only report tests
55 # which failed or returned NO RESULT.
57 # -q Quiet. By default, runtest.py prints the
58 # command line it will execute before
59 # executing it. This suppresses that print.
61 # -X The scons "script" is an executable; don't
64 # -x scons The scons script to use for tests.
66 # --xml Print test results to an output file (specified
67 # by the -o option) in an SCons-specific XML format.
68 # This is (will be) used for reporting results back
69 # to a central SCons test monitoring infrastructure.
71 # (Note: There used to be a -v option that specified the SCons
72 # version to be tested, when we were installing in a version-specific
73 # library directory. If we ever resurrect that as the default, then
74 # you can find the appropriate code in the 0.04 version of this script,
75 # rather than reinventing that wheel.)
94 print_passed_summary = None
101 if os.name == 'java':
102 python = os.path.join(sys.prefix, 'jython')
104 python = sys.executable
108 if sys.platform == 'win32' or os.name == 'java':
109 lib_dir = os.path.join(sys.exec_prefix, "Lib")
111 # The hard-coded "python" here is the directory name,
112 # not an executable, so it's all right.
113 lib_dir = os.path.join(sys.exec_prefix, "lib", "python" + sys.version[0:3])
116 Usage: runtest.py [OPTIONS] [TEST ...]
118 -a, --all Run all tests.
119 --aegis Print results in Aegis format.
120 -d, --debug Run test scripts under the Python debugger.
121 -f FILE, --file FILE Run tests in specified FILE.
122 -h, --help Print this message and exit.
123 -o FILE, --output FILE Print test results to FILE.
124 -P Python Use the specified Python interpreter.
125 -p PACKAGE, --package PACKAGE
126 Test against the specified PACKAGE:
128 local-tar-gz .tar.gz standalone package
129 local-zip .zip standalone package
131 src-tar-gz .tar.gz source package
132 src-zip .zip source package
133 tar-gz .tar.gz distribution
134 zip .zip distribution
135 --passed Summarize which tests passed.
136 -q, --quiet Don't print the test being executed.
137 -v version Specify the SCons version.
138 -X Test script is executable, don't feed to Python.
139 -x SCRIPT, --exec SCRIPT Test SCRIPT.
140 --xml Print results in SCons XML format.
143 opts, args = getopt.getopt(sys.argv[1:], "adf:ho:P:p:qv:Xx:",
145 'debug', 'file=', 'help', 'output=',
146 'package=', 'passed', 'python=', 'quiet',
151 if o == '-a' or o == '--all':
153 elif o == '-d' or o == '--debug':
154 debug = os.path.join(lib_dir, "pdb.py")
155 elif o == '-f' or o == '--file':
156 if not os.path.isabs(a):
157 a = os.path.join(cwd, a)
159 elif o == '-h' or o == '--help':
162 elif o == '-o' or o == '--output':
163 if a != '-' and not os.path.isabs(a):
164 a = os.path.join(cwd, a)
166 elif o == '-p' or o == '--package':
168 elif o == '--passed':
169 print_passed_summary = 1
170 elif o == '-P' or o == '--python':
172 elif o == '-q' or o == '--quiet':
174 elif o == '-v' or o == '--version':
178 elif o == '-x' or o == '--exec':
180 elif o in ['--aegis', '--xml']:
184 for dir in string.split(os.environ['PATH'], os.pathsep):
185 f = os.path.join(dir, file)
186 if os.path.isfile(f):
191 if stat.S_IMODE(st[stat.ST_MODE]) & 0111:
195 aegis = whereis('aegis')
200 paths = os.popen("aesub '$sp' 2>/dev/null", "r").read()[:-1]
201 sp.extend(string.split(paths, os.pathsep))
202 spe = os.popen("aesub '$spe' 2>/dev/null", "r").read()[:-1]
203 spe = string.split(spe, os.pathsep)
210 def __init__(self, path, spe=None):
212 self.abspath = os.path.abspath(path)
215 f = os.path.join(dir, path)
216 if os.path.isfile(f):
221 class SystemExecutor(Base):
223 s = os.system(self.command)
228 sys.stdout.write("Unexpected exit status %d\n" % s)
232 except AttributeError:
233 class PopenExecutor(Base):
235 (tochild, fromchild, childerr) = os.popen3(self.command)
237 self.stdout = fromchild.read()
238 self.stderr = childerr.read()
240 self.status = childerr.close()
244 class PopenExecutor(Base):
246 p = popen2.Popen3(self.command, 1)
248 self.stdout = p.fromchild.read()
249 self.stderr = p.childerr.read()
250 self.status = p.wait()
252 class Aegis(SystemExecutor):
254 f.write('test_result = [\n')
256 f.write(' { file_name = "%s";\n' % self.path)
257 f.write(' exit_status = %d; },\n' % self.status)
261 class XML(PopenExecutor):
263 f.write(' <results>\n')
266 f.write(' <file_name>%s</file_name>\n' % self.path)
267 f.write(' <command_line>%s</command_line>\n' % self.command)
268 f.write(' <exit_status>%s</exit_status>\n' % self.status)
269 f.write(' <stdout>%s</stdout>\n' % self.stdout)
270 f.write(' <stderr>%s</stderr>\n' % self.stderr)
271 f.write(' </test>\n')
273 f.write(' </results>\n')
276 None : SystemExecutor,
280 Test = format_class[format]
286 for g in glob.glob(a):
287 tests.append(Test(g))
290 x = os.path.join(dir, a)
294 tests.append(Test(g))
298 for g in glob.glob(a):
299 tests.append(Test(g))
301 # Find all of the SCons functional tests in the local directory
302 # tree. This is anything under the 'src' subdirectory that ends
303 # with 'Tests.py', or any Python script (*.py) under the 'test'
306 # Note that there are some tests under 'src' that *begin* with
307 # 'test_', but they're packaging and installation tests, not
308 # functional tests, so we don't execute them by default. (They can
309 # still be executed by hand, though, and are routinely executed
310 # by the Aegis packaging build to make sure that we're building
314 def find_Tests_py(arg, dirname, names, tdict=tdict):
315 for n in filter(lambda n: n[-8:] == "Tests.py", names):
316 t = os.path.join(dirname, n)
317 if not tdict.has_key(t):
319 os.path.walk('src', find_Tests_py, 0)
321 def find_py(arg, dirname, names, tdict=tdict):
322 for n in filter(lambda n: n[-3:] == ".py", names):
323 t = os.path.join(dirname, n)
324 if not tdict.has_key(t):
326 os.path.walk('test', find_py, 0)
329 cmd = "aegis -list -unf pf 2>/dev/null"
330 for line in os.popen(cmd, "r").readlines():
331 a = string.split(line)
332 if a[0] == "test" and not tdict.has_key(a[-1]):
333 tdict[a[-1]] = Test(a[-1], spe)
334 cmd = "aegis -list -unf cf 2>/dev/null"
335 for line in os.popen(cmd, "r").readlines():
336 a = string.split(line)
340 elif not tdict.has_key(a[-1]):
341 tdict[a[-1]] = Test(a[-1], spe)
345 tests = map(tdict.get, keys)
347 tests = map(Test, map(lambda x: x[:-1], open(testlistfile, 'r').readlines()))
349 sys.stderr.write("""\
350 runtest.py: No tests were specified on the command line.
351 List one or more tests, or use the -a option
352 to find and run all tests.
360 'local-tar-gz' : None,
369 # The hard-coded "python2.1" here is the library directory
370 # name on Debian systems, not an executable, so it's all right.
372 'deb' : os.path.join('python2.1', 'site-packages')
375 if not dir.has_key(package):
376 sys.stderr.write("Unknown package '%s'\n" % package)
379 test_dir = os.path.join(cwd, 'build', 'test-%s' % package)
381 if dir[package] is None:
382 scons_script_dir = test_dir
383 globs = glob.glob(os.path.join(test_dir, 'scons-local-*'))
385 sys.stderr.write("No `scons-local-*' dir in `%s'\n" % test_dir)
388 pythonpath_dir = globs[len(globs)-1]
389 elif sys.platform == 'win32':
390 scons_script_dir = os.path.join(test_dir, dir[package], 'Scripts')
391 scons_lib_dir = os.path.join(test_dir, dir[package])
392 pythonpath_dir = scons_lib_dir
394 scons_script_dir = os.path.join(test_dir, dir[package], 'bin')
395 l = lib.get(package, 'scons')
396 scons_lib_dir = os.path.join(test_dir, dir[package], 'lib', l)
397 pythonpath_dir = scons_lib_dir
403 # XXX: Logic like the following will be necessary once
404 # we fix runtest.py to run tests within an Aegis change
405 # without symlinks back to the baseline(s).
410 # d = os.path.join(dir, 'src', 'script')
411 # f = os.path.join(d, 'scons.py')
412 # if os.path.isfile(f):
415 # spe = map(lambda x: os.path.join(x, 'src', 'engine'), spe)
416 # ld = string.join(spe, os.pathsep)
418 scons_script_dir = sd or os.path.join(cwd, 'src', 'script')
420 scons_lib_dir = ld or os.path.join(cwd, 'src', 'engine')
422 pythonpath_dir = scons_lib_dir
425 # Let the version of SCons that the -x option pointed to find
427 os.environ['SCONS'] = scons
429 # Because SCons is really aggressive about finding its modules,
430 # it sometimes finds SCons modules elsewhere on the system.
431 # This forces SCons to use the modules that are being tested.
432 os.environ['SCONS_LIB_DIR'] = scons_lib_dir
435 os.environ['SCONS_EXEC'] = '1'
437 os.environ['SCONS_SCRIPT_DIR'] = scons_script_dir
438 os.environ['SCONS_CWD'] = cwd
440 os.environ['SCONS_VERSION'] = version
442 old_pythonpath = os.environ.get('PYTHONPATH')
444 pythonpaths = [ pythonpath_dir ]
446 pythonpaths.append(os.path.join(p, 'build', 'etc'))
447 pythonpaths.append(os.path.join(p, 'etc'))
448 os.environ['PYTHONPATH'] = string.join(pythonpaths, os.pathsep)
451 os.environ['PYTHONPATH'] = os.environ['PYTHONPATH'] + \
455 os.chdir(scons_script_dir)
458 def __init__(self, file):
460 def write(self, arg):
463 def __getattr__(self, attr):
464 return getattr(self.file, attr)
466 sys.stdout = Unbuffered(sys.stdout)
468 _ws = re.compile('\s')
476 t.command = string.join(map(escape, [python, debug, t.abspath]), " ")
478 sys.stdout.write(t.command + "\n")
481 passed = filter(lambda t: t.status == 0, tests)
482 fail = filter(lambda t: t.status == 1, tests)
483 no_result = filter(lambda t: t.status == 2, tests)
486 if passed and print_passed_summary:
488 sys.stdout.write("\nPassed the following test:\n")
490 sys.stdout.write("\nPassed the following %d tests:\n" % len(passed))
491 paths = map(lambda x: x.path, passed)
492 sys.stdout.write("\t" + string.join(paths, "\n\t") + "\n")
495 sys.stdout.write("\nFailed the following test:\n")
497 sys.stdout.write("\nFailed the following %d tests:\n" % len(fail))
498 paths = map(lambda x: x.path, fail)
499 sys.stdout.write("\t" + string.join(paths, "\n\t") + "\n")
501 if len(no_result) == 1:
502 sys.stdout.write("\nNO RESULT from the following test:\n")
504 sys.stdout.write("\nNO RESULT from the following %d tests:\n" % len(no_result))
505 paths = map(lambda x: x.path, no_result)
506 sys.stdout.write("\t" + string.join(paths, "\n\t") + "\n")
509 if outputfile == '-':
512 f = open(outputfile, 'w')
514 #f.write("test_result = [\n")
519 if outputfile != '-':
522 if format == '--aegis':