Add overlooked .aeignore file.
[scons.git] / runtest.py
1 #!/usr/bin/env python
2 #
3 # __COPYRIGHT__
4 #
5 # runtest.py - wrapper script for running SCons tests
6 #
7 # This script mainly exists to set PYTHONPATH to the right list of
8 # directories to test the SCons modules.
9 #
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).
12 #
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.
19 #
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.
24 #
25 # Options:
26 #
27 #       -a              Run all tests; does a virtual 'find' for
28 #                       all SCons tests under the current directory.
29 #
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
34 #                       config file.
35 #
36 #       -d              Debug.  Runs the script under the Python
37 #                       debugger (pdb.py) so you don't have to
38 #                       muck with PYTHONPATH yourself.
39 #
40 #       -f file         Only execute the tests listed in the specified
41 #                       file.
42 #
43 #       -h              Print the help and exit.
44 #
45 #       -o file         Print test results to the specified file.
46 #                       The --aegis and --xml options specify the
47 #                       output format.
48 #
49 #       -P Python       Use the specified Python interpreter.
50 #
51 #       -p package      Test against the specified package.
52 #
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.
56 #
57 #       -q              Quiet.  By default, runtest.py prints the
58 #                       command line it will execute before
59 #                       executing it.  This suppresses that print.
60 #
61 #       -X              The scons "script" is an executable; don't
62 #                       feed it to Python.
63 #
64 #       -x scons        The scons script to use for tests.
65 #
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.
70 #
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.)
76 #
77
78 import getopt
79 import glob
80 import os
81 import os.path
82 import popen2
83 import re
84 import stat
85 import string
86 import sys
87
88 all = 0
89 debug = ''
90 format = None
91 tests = []
92 printcommand = 1
93 package = None
94 print_passed_summary = None
95 scons = None
96 scons_exec = None
97 outputfile = None
98 testlistfile = None
99 version = ''
100
101 if os.name == 'java':
102     python = os.path.join(sys.prefix, 'jython')
103 else:
104     python = sys.executable
105
106 cwd = os.getcwd()
107
108 if sys.platform == 'win32' or os.name == 'java':
109     lib_dir = os.path.join(sys.exec_prefix, "Lib")
110 else:
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])
114
115 helpstr = """\
116 Usage: runtest.py [OPTIONS] [TEST ...]
117 Options:
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:
127                                 deb           Debian
128                                 local-tar-gz  .tar.gz standalone package
129                                 local-zip     .zip standalone package
130                                 rpm           Red Hat
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.
141 """
142
143 opts, args = getopt.getopt(sys.argv[1:], "adf:ho:P:p:qv:Xx:",
144                             ['all', 'aegis',
145                              'debug', 'file=', 'help', 'output=',
146                              'package=', 'passed', 'python=', 'quiet',
147                              'version=', 'exec=',
148                              'xml'])
149
150 for o, a in opts:
151     if o == '-a' or o == '--all':
152         all = 1
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)
158         testlistfile = a
159     elif o == '-h' or o == '--help':
160         print helpstr
161         sys.exit(0)
162     elif o == '-o' or o == '--output':
163         if a != '-' and not os.path.isabs(a):
164             a = os.path.join(cwd, a)
165         outputfile = a
166     elif o == '-p' or o == '--package':
167         package = a
168     elif o == '--passed':
169         print_passed_summary = 1
170     elif o == '-P' or o == '--python':
171         python = a
172     elif o == '-q' or o == '--quiet':
173         printcommand = 0
174     elif o == '-v' or o == '--version':
175         version = a
176     elif o == '-X':
177         scons_exec = 1
178     elif o == '-x' or o == '--exec':
179         scons = a
180     elif o in ['--aegis', '--xml']:
181         format = o
182
183 def whereis(file):
184     for dir in string.split(os.environ['PATH'], os.pathsep):
185         f = os.path.join(dir, file)
186         if os.path.isfile(f):
187             try:
188                 st = os.stat(f)
189             except OSError:
190                 continue
191             if stat.S_IMODE(st[stat.ST_MODE]) & 0111:
192                 return f
193     return None
194
195 aegis = whereis('aegis')
196
197 sp = []
198
199 if 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)
204 else:
205     spe = []
206
207 sp.append(cwd)
208
209 class Base:
210     def __init__(self, path, spe=None):
211         self.path = path
212         self.abspath = os.path.abspath(path)
213         if spe:
214             for dir in spe:
215                 f = os.path.join(dir, path)
216                 if os.path.isfile(f):
217                     self.abspath = f
218                     break
219         self.status = None
220
221 class SystemExecutor(Base):
222     def execute(self):
223         s = os.system(self.command)
224         if s >= 256:
225             s = s / 256
226         self.status = s
227         if s < 0 or s > 2:
228             sys.stdout.write("Unexpected exit status %d\n" % s)
229
230 try:
231     popen2.Popen3
232 except AttributeError:
233     class PopenExecutor(Base):
234         def execute(self):
235             (tochild, fromchild, childerr) = os.popen3(self.command)
236             tochild.close()
237             self.stdout = fromchild.read()
238             self.stderr = childerr.read()
239             fromchild.close()
240             self.status = childerr.close()
241             if not self.status:
242                 self.status = 0
243 else:
244     class PopenExecutor(Base):
245         def execute(self):
246             p = popen2.Popen3(self.command, 1)
247             p.tochild.close()
248             self.stdout = p.fromchild.read()
249             self.stderr = p.childerr.read()
250             self.status = p.wait()
251
252 class Aegis(SystemExecutor):
253     def header(self, f):
254         f.write('test_result = [\n')
255     def write(self, f):
256         f.write('    { file_name = "%s";\n' % self.path)
257         f.write('      exit_status = %d; },\n' % self.status)
258     def footer(self, f):
259         f.write('];\n')
260
261 class XML(PopenExecutor):
262     def header(self, f):
263         f.write('  <results>\n')
264     def write(self, f):
265         f.write('    <test>\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')
272     def footer(self, f):
273         f.write('  </results>\n')
274
275 format_class = {
276     None        : SystemExecutor,
277     '--aegis'   : Aegis,
278     '--xml'     : XML,
279 }
280 Test = format_class[format]
281
282 if args:
283     if spe:
284         for a in args:
285             if os.path.isabs(a):
286                 for g in glob.glob(a):
287                     tests.append(Test(g))
288             else:
289                 for dir in spe:
290                     x = os.path.join(dir, a)
291                     globs = glob.glob(x)
292                     if globs:
293                         for g in globs:
294                             tests.append(Test(g))
295                         break
296     else:
297         for a in args:
298             for g in glob.glob(a):
299                 tests.append(Test(g))
300 elif all:
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'
304     # subdirectory.
305     #
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
311     # things correctly.)
312     tdict = {}
313
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):
318                 tdict[t] = Test(t)
319     os.path.walk('src', find_Tests_py, 0)
320
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):
325                 tdict[t] = Test(t)
326     os.path.walk('test', find_py, 0)
327
328     if aegis:
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)
337             if a[0] == "test":
338                 if a[1] == "remove":
339                     del tdict[a[-1]]
340                 elif not tdict.has_key(a[-1]):
341                     tdict[a[-1]] = Test(a[-1], spe)
342
343     keys = tdict.keys()
344     keys.sort()
345     tests = map(tdict.get, keys)
346 elif testlistfile:
347     tests = map(Test, map(lambda x: x[:-1], open(testlistfile, 'r').readlines()))
348 else:
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.
353 """)
354
355
356 if package:
357
358     dir = {
359         'deb'          : 'usr',
360         'local-tar-gz' : None,
361         'local-zip'    : None,
362         'rpm'          : 'usr',
363         'src-tar-gz'   : '',
364         'src-zip'      : '',
365         'tar-gz'       : '',
366         'zip'          : '',
367     }
368
369     # The hard-coded "python2.1" here is the library directory
370     # name on Debian systems, not an executable, so it's all right.
371     lib = {
372         'deb'        : os.path.join('python2.1', 'site-packages')
373     }
374
375     if not dir.has_key(package):
376         sys.stderr.write("Unknown package '%s'\n" % package)
377         sys.exit(2)
378
379     test_dir = os.path.join(cwd, 'build', 'test-%s' % package)
380
381     if dir[package] is None:
382         scons_script_dir = test_dir
383         globs = glob.glob(os.path.join(test_dir, 'scons-local-*'))
384         if not globs:
385             sys.stderr.write("No `scons-local-*' dir in `%s'\n" % test_dir)
386             sys.exit(2)
387         scons_lib_dir = None
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
393     else:
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
398
399 else:
400     sd = None
401     ld = None
402
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).
406     #
407     #if spe:
408     #    if not scons:
409     #        for dir in spe:
410     #            d = os.path.join(dir, 'src', 'script')
411     #            f = os.path.join(d, 'scons.py')
412     #            if os.path.isfile(f):
413     #                sd = d
414     #                scons = f
415     #    spe = map(lambda x: os.path.join(x, 'src', 'engine'), spe)
416     #    ld = string.join(spe, os.pathsep)
417
418     scons_script_dir = sd or os.path.join(cwd, 'src', 'script')
419
420     scons_lib_dir = ld or os.path.join(cwd, 'src', 'engine')
421
422     pythonpath_dir = scons_lib_dir
423
424 if scons:
425     # Let the version of SCons that the -x option pointed to find
426     # its own modules.
427     os.environ['SCONS'] = scons
428 elif scons_lib_dir:
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
433
434 if scons_exec:
435     os.environ['SCONS_EXEC'] = '1'
436
437 os.environ['SCONS_SCRIPT_DIR'] = scons_script_dir
438 os.environ['SCONS_CWD'] = cwd
439
440 os.environ['SCONS_VERSION'] = version
441
442 old_pythonpath = os.environ.get('PYTHONPATH')
443
444 pythonpaths = [ pythonpath_dir ]
445 for p in sp:
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)
449
450 if old_pythonpath:
451     os.environ['PYTHONPATH'] = os.environ['PYTHONPATH'] + \
452                                os.pathsep + \
453                                old_pythonpath
454
455 os.chdir(scons_script_dir)
456
457 class Unbuffered:
458     def __init__(self, file):
459         self.file = file
460     def write(self, arg):
461         self.file.write(arg)
462         self.file.flush()
463     def __getattr__(self, attr):
464         return getattr(self.file, attr)
465
466 sys.stdout = Unbuffered(sys.stdout)
467
468 _ws = re.compile('\s')
469
470 def escape(s):
471     if _ws.search(s):
472         s = '"' + s + '"'
473     return s
474
475 for t in tests:
476     t.command = string.join(map(escape, [python, debug, t.abspath]), " ")
477     if printcommand:
478         sys.stdout.write(t.command + "\n")
479     t.execute()
480
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)
484
485 if len(tests) != 1:
486     if passed and print_passed_summary:
487         if len(passed) == 1:
488             sys.stdout.write("\nPassed the following test:\n")
489         else:
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")
493     if fail:
494         if len(fail) == 1:
495             sys.stdout.write("\nFailed the following test:\n")
496         else:
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")
500     if no_result:
501         if len(no_result) == 1:
502             sys.stdout.write("\nNO RESULT from the following test:\n")
503         else:
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")
507
508 if outputfile:
509     if outputfile == '-':
510         f = sys.stdout
511     else:
512         f = open(outputfile, 'w')
513     tests[0].header(f)
514     #f.write("test_result = [\n")
515     for t in tests:
516         t.write(f)
517     tests[0].footer(f)
518     #f.write("];\n")
519     if outputfile != '-':
520         f.close()
521
522 if format == '--aegis':
523     sys.exit(0)
524 elif len(fail):
525     sys.exit(1)
526 elif len(no_result):
527     sys.exit(2)
528 else:
529     sys.exit(0)