Speed up adding children to the various Node lists (depends, ignore, sources, implicit).
[scons.git] / runtest.py
1 #!/usr/bin/env python
2 #
3 # runtest.py - wrapper script for running SCons tests
4 #
5 # This script mainly exists to set PYTHONPATH to the right list of
6 # directories to test the SCons modules.
7 #
8 # By default, it directly uses the modules in the local tree:
9 # ./src/ (source files we ship) and ./etc/ (other modules we don't).
10 #
11 # HOWEVER, now that SCons has Repository support, we don't have
12 # Aegis copy all of the files into the local tree.  So if you're
13 # using Aegis and want to run tests by hand using this script, you
14 # must "aecp ." the entire source tree into your local directory
15 # structure.  When you're done with your change, you can then
16 # "aecpu -unch ." to un-copy any files that you haven't changed.
17 #
18 # When any -p option is specified, this script assumes it's in a
19 # directory in which a build has been performed, and sets PYTHONPATH
20 # so that it *only* references the modules that have unpacked from
21 # the specified built package, to test whether the packages are good.
22 #
23 # Options:
24 #
25 #       -a              Run all tests; does a virtual 'find' for
26 #                       all SCons tests under the current directory.
27 #
28 #       -d              Debug.  Runs the script under the Python
29 #                       debugger (pdb.py) so you don't have to
30 #                       muck with PYTHONPATH yourself.
31 #
32 #       -h              Print the help and exit.
33 #
34 #       -o file         Print test results to the specified file
35 #                       in the format expected by aetest(5).  This
36 #                       is intended for use in the batch_test_command
37 #                       field in the Aegis project config file.
38 #
39 #       -P Python       Use the specified Python interpreter.
40 #
41 #       -p package      Test against the specified package.
42 #
43 #       -q              Quiet.  By default, runtest.py prints the
44 #                       command line it will execute before
45 #                       executing it.  This suppresses that print.
46 #
47 #       -X              The scons "script" is an executable; don't
48 #                       feed it to Python.
49 #
50 #       -x scons        The scons script to use for tests.
51 #
52 # (Note:  There used to be a -v option that specified the SCons
53 # version to be tested, when we were installing in a version-specific
54 # library directory.  If we ever resurrect that as the default, then
55 # you can find the appropriate code in the 0.04 version of this script,
56 # rather than reinventing that wheel.)
57 #
58
59 import getopt
60 import glob
61 import os
62 import os.path
63 import re
64 import stat
65 import string
66 import sys
67
68 all = 0
69 debug = ''
70 tests = []
71 printcmd = 1
72 package = None
73 scons = None
74 scons_exec = None
75 output = None
76 version = ''
77
78 if os.name == 'java':
79     python = os.path.join(sys.prefix, 'jython')
80 else:
81     python = sys.executable
82
83 cwd = os.getcwd()
84
85 if sys.platform == 'win32' or os.name == 'java':
86     lib_dir = os.path.join(sys.exec_prefix, "Lib")
87 else:
88     # The hard-coded "python" here is the directory name,
89     # not an executable, so it's all right.
90     lib_dir = os.path.join(sys.exec_prefix, "lib", "python" + sys.version[0:3])
91
92 helpstr = """\
93 Usage: runtest.py [OPTIONS] [TEST ...]
94 Options:
95   -a, --all                   Run all tests.
96   -d, --debug                 Run test scripts under the Python debugger.
97   -h, --help                  Print this message and exit.
98   -o FILE, --output FILE      Print test results to FILE (Aegis format).
99   -P Python                   Use the specified Python interpreter.
100   -p PACKAGE, --package PACKAGE
101                               Test against the specified PACKAGE:
102                                 deb           Debian
103                                 local-tar-gz  .tar.gz standalone package
104                                 local-zip     .zip standalone package
105                                 rpm           Red Hat
106                                 src-tar-gz    .tar.gz source package
107                                 src-zip       .zip source package
108                                 tar-gz        .tar.gz distribution
109                                 zip           .zip distribution
110   -q, --quiet                 Don't print the test being executed.
111   -v version                  Specify the SCons version.
112   -X                          Test script is executable, don't feed to Python.
113   -x SCRIPT, --exec SCRIPT    Test SCRIPT.
114 """
115
116 opts, args = getopt.getopt(sys.argv[1:], "adho:P:p:qv:Xx:",
117                             ['all', 'debug', 'help', 'output=',
118                              'package=', 'python=', 'quiet',
119                              'version=', 'exec='])
120
121 for o, a in opts:
122     if o == '-a' or o == '--all':
123         all = 1
124     elif o == '-d' or o == '--debug':
125         debug = os.path.join(lib_dir, "pdb.py")
126     elif o == '-h' or o == '--help':
127         print helpstr
128         sys.exit(0)
129     elif o == '-o' or o == '--output':
130         if not os.path.isabs(a):
131             a = os.path.join(cwd, a)
132         output = a
133     elif o == '-P' or o == '--python':
134         python = a
135     elif o == '-p' or o == '--package':
136         package = a
137     elif o == '-q' or o == '--quiet':
138         printcmd = 0
139     elif o == '-v' or o == '--version':
140         version = a
141     elif o == '-X':
142         scons_exec = 1
143     elif o == '-x' or o == '--exec':
144         scons = a
145
146 def whereis(file):
147     for dir in string.split(os.environ['PATH'], os.pathsep):
148         f = os.path.join(dir, file)
149         if os.path.isfile(f):
150             try:
151                 st = os.stat(f)
152             except OSError:
153                 continue
154             if stat.S_IMODE(st[stat.ST_MODE]) & 0111:
155                 return f
156     return None
157
158 aegis = whereis('aegis')
159
160 sp = []
161
162 if aegis:
163     paths = os.popen("aesub '$sp' 2>/dev/null", "r").read()[:-1]
164     sp.extend(string.split(paths, os.pathsep))
165     spe = os.popen("aesub '$spe' 2>/dev/null", "r").read()[:-1]
166     spe = string.split(spe, os.pathsep)
167 else:
168     spe = []
169
170 sp.append(cwd)
171
172 class Test:
173     def __init__(self, path, spe=None):
174         self.path = path
175         self.abspath = os.path.abspath(path)
176         if spe:
177             for dir in spe:
178                 f = os.path.join(dir, path)
179                 if os.path.isfile(f):
180                     self.abspath = f
181                     break
182         self.status = None
183
184 if args:
185     if spe:
186         for a in args:
187             if os.path.isabs(a):
188                 for g in glob.glob(a):
189                     tests.append(Test(g))
190             else:
191                 for dir in spe:
192                     x = os.path.join(dir, a)
193                     globs = glob.glob(x)
194                     if globs:
195                         for g in globs:
196                             tests.append(Test(g))
197                         break
198     else:
199         for a in args:
200             for g in glob.glob(a):
201                 tests.append(Test(g))
202 elif all:
203     tdict = {}
204
205     def find_Test_py(arg, dirname, names, tdict=tdict):
206         for n in filter(lambda n: n[-8:] == "Tests.py", names):
207             t = os.path.join(dirname, n)
208             if not tdict.has_key(t):
209                 tdict[t] = Test(t)
210     os.path.walk('src', find_Test_py, 0)
211
212     def find_py(arg, dirname, names, tdict=tdict):
213         for n in filter(lambda n: n[-3:] == ".py", names):
214             t = os.path.join(dirname, n)
215             if not tdict.has_key(t):
216                 tdict[t] = Test(t)
217     os.path.walk('test', find_py, 0)
218
219     if aegis:
220         cmd = "aegis -list -unf pf 2>/dev/null"
221         for line in os.popen(cmd, "r").readlines():
222             a = string.split(line)
223             if a[0] == "test" and not tdict.has_key(a[-1]):
224                 tdict[a[-1]] = Test(a[-1], spe)
225         cmd = "aegis -list -unf cf 2>/dev/null"
226         for line in os.popen(cmd, "r").readlines():
227             a = string.split(line)
228             if a[0] == "test":
229                 if a[1] == "remove":
230                     del tdict[a[-1]]
231                 elif not tdict.has_key(a[-1]):
232                     tdict[a[-1]] = Test(a[-1], spe)
233
234     keys = tdict.keys()
235     keys.sort()
236     tests = map(tdict.get, keys)
237
238 if package:
239
240     dir = {
241         'deb'          : 'usr',
242         'local-tar-gz' : None,
243         'local-zip'    : None,
244         'rpm'          : 'usr',
245         'src-tar-gz'   : '',
246         'src-zip'      : '',
247         'tar-gz'       : '',
248         'zip'          : '',
249     }
250
251     # The hard-coded "python2.1" here is the library directory
252     # name on Debian systems, not an executable, so it's all right.
253     lib = {
254         'deb'        : os.path.join('python2.1', 'site-packages')
255     }
256
257     if not dir.has_key(package):
258         sys.stderr.write("Unknown package '%s'\n" % package)
259         sys.exit(2)
260
261     test_dir = os.path.join(cwd, 'build', 'test-%s' % package)
262
263     if dir[package] is None:
264         scons_script_dir = test_dir
265         globs = glob.glob(os.path.join(test_dir, 'scons-local-*'))
266         if not globs:
267             sys.stderr.write("No `scons-local-*' dir in `%s'\n" % test_dir)
268             sys.exit(2)
269         scons_lib_dir = None
270         pythonpath_dir = globs[len(globs)-1]
271     elif sys.platform == 'win32':
272         scons_script_dir = os.path.join(test_dir, dir[package], 'Scripts')
273         scons_lib_dir = os.path.join(test_dir, dir[package])
274         pythonpath_dir = scons_lib_dir
275     else:
276         scons_script_dir = os.path.join(test_dir, dir[package], 'bin')
277         l = lib.get(package, 'scons')
278         scons_lib_dir = os.path.join(test_dir, dir[package], 'lib', l)
279         pythonpath_dir = scons_lib_dir
280
281 else:
282     sd = None
283     ld = None
284
285     # XXX:  Logic like the following will be necessary once
286     # we fix runtest.py to run tests within an Aegis change
287     # without symlinks back to the baseline(s).
288     #
289     #if spe:
290     #    if not scons:
291     #        for dir in spe:
292     #            d = os.path.join(dir, 'src', 'script')
293     #            f = os.path.join(d, 'scons.py')
294     #            if os.path.isfile(f):
295     #                sd = d
296     #                scons = f
297     #    spe = map(lambda x: os.path.join(x, 'src', 'engine'), spe)
298     #    ld = string.join(spe, os.pathsep)
299
300     scons_script_dir = sd or os.path.join(cwd, 'src', 'script')
301
302     scons_lib_dir = ld or os.path.join(cwd, 'src', 'engine')
303
304     pythonpath_dir = scons_lib_dir
305
306 if scons:
307     # Let the version of SCons that the -x option pointed to find
308     # its own modules.
309     os.environ['SCONS'] = scons
310 elif scons_lib_dir:
311     # Because SCons is really aggressive about finding its modules,
312     # it sometimes finds SCons modules elsewhere on the system.
313     # This forces SCons to use the modules that are being tested.
314     os.environ['SCONS_LIB_DIR'] = scons_lib_dir
315
316 if scons_exec:
317     os.environ['SCONS_EXEC'] = '1'
318
319 os.environ['SCONS_SCRIPT_DIR'] = scons_script_dir
320 os.environ['SCONS_CWD'] = cwd
321
322 os.environ['SCONS_VERSION'] = version
323
324 old_pythonpath = os.environ.get('PYTHONPATH')
325
326 pythonpaths = [ pythonpath_dir ]
327 for p in sp:
328     pythonpaths.append(os.path.join(p, 'build', 'etc'))
329     pythonpaths.append(os.path.join(p, 'etc'))
330 os.environ['PYTHONPATH'] = string.join(pythonpaths, os.pathsep)
331
332 if old_pythonpath:
333     os.environ['PYTHONPATH'] = os.environ['PYTHONPATH'] + \
334                                os.pathsep + \
335                                old_pythonpath
336
337 os.chdir(scons_script_dir)
338
339 class Unbuffered:
340     def __init__(self, file):
341         self.file = file
342     def write(self, arg):
343         self.file.write(arg)
344         self.file.flush()
345     def __getattr__(self, attr):
346         return getattr(self.file, attr)
347
348 sys.stdout = Unbuffered(sys.stdout)
349
350 for t in tests:
351     cmd = string.join([python, debug, t.abspath], " ")
352     if printcmd:
353         sys.stdout.write(cmd + "\n")
354     s = os.system(cmd)
355     if s >= 256:
356         s = s / 256
357     t.status = s
358     if s < 0 or s > 2:
359         sys.stdout.write("Unexpected exit status %d\n" % s)
360
361 fail = filter(lambda t: t.status == 1, tests)
362 no_result = filter(lambda t: t.status == 2, tests)
363
364 if len(tests) != 1:
365     if fail:
366         if len(fail) == 1:
367             sys.stdout.write("\nFailed the following test:\n")
368         else:
369             sys.stdout.write("\nFailed the following %d tests:\n" % len(fail))
370         paths = map(lambda x: x.path, fail)
371         sys.stdout.write("\t" + string.join(paths, "\n\t") + "\n")
372     if no_result:
373         if len(no_result) == 1:
374             sys.stdout.write("\nNO RESULT from the following test:\n")
375         else:
376             sys.stdout.write("\nNO RESULT from the following %d tests:\n" % len(no_result))
377         paths = map(lambda x: x.path, no_result)
378         sys.stdout.write("\t" + string.join(paths, "\n\t") + "\n")
379
380 if output:
381     f = open(output, 'w')
382     f.write("test_result = [\n")
383     for t in tests:
384         f.write('    { file_name = "%s";\n' % t.path)
385         f.write('      exit_status = %d; },\n' % t.status)
386     f.write("];\n")
387     f.close()
388     sys.exit(0)
389 else:
390     if len(fail):
391         sys.exit(1)
392     elif len(no_result):
393         sys.exit(2)
394     else:
395         sys.exit(0)