d66bd931b7301fe19d9906262799fe5e225a1528
[scons.git] / QMTest / TestSCons.py
1 """
2 TestSCons.py:  a testing framework for the SCons software construction
3 tool.
4
5 A TestSCons environment object is created via the usual invocation:
6
7     test = TestSCons()
8
9 TestScons is a subclass of TestCommon, which is in turn is a subclass
10 of TestCmd), and hence has available all of the methods and attributes
11 from those classes, as well as any overridden or additional methods or
12 attributes defined in this subclass.
13 """
14
15 # __COPYRIGHT__
16
17 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
18
19 import os
20 import re
21 import shutil
22 import string
23 import sys
24 import time
25
26 import __builtin__
27 try:
28     __builtin__.zip
29 except AttributeError:
30     def zip(*lists):
31         result = []
32         for i in xrange(len(lists[0])):
33             result.append(tuple(map(lambda l, i=i: l[i], lists)))
34         return result
35     __builtin__.zip = zip
36
37 from TestCommon import *
38 from TestCommon import __all__
39
40 # Some tests which verify that SCons has been packaged properly need to
41 # look for specific version file names.  Replicating the version number
42 # here provides some independent verification that what we packaged
43 # conforms to what we expect.
44
45 default_version = '1.2.0'
46
47 copyright_years = '2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009'
48
49 # In the checked-in source, the value of SConsVersion in the following
50 # line must remain "__ VERSION __" (without the spaces) so the built
51 # version in build/QMTest/TestSCons.py contains the actual version
52 # string of the packages that have been built.
53 SConsVersion = '__VERSION__'
54 if SConsVersion == '__' + 'VERSION' + '__':
55     SConsVersion = default_version
56
57 __all__.extend([ 'TestSCons',
58                  'machine',
59                  'python',
60                  '_exe',
61                  '_obj',
62                  '_shobj',
63                  'shobj_',
64                  'lib_',
65                  '_lib',
66                  'dll_',
67                  '_dll'
68                ])
69
70 machine_map = {
71     'i686'  : 'i386',
72     'i586'  : 'i386',
73     'i486'  : 'i386',
74 }
75
76 try:
77     uname = os.uname
78 except AttributeError:
79     # Windows doesn't have a uname() function.  We could use something like
80     # sys.platform as a fallback, but that's not really a "machine," so
81     # just leave it as None.
82     machine = None
83 else:
84     machine = uname()[4]
85     machine = machine_map.get(machine, machine)
86
87 python = python_executable
88 _python_ = '"' + python_executable + '"'
89 _exe = exe_suffix
90 _obj = obj_suffix
91 _shobj = shobj_suffix
92 shobj_ = shobj_prefix
93 _lib = lib_suffix
94 lib_ = lib_prefix
95 _dll = dll_suffix
96 dll_ = dll_prefix
97
98 def gccFortranLibs():
99     """Test whether -lfrtbegin is required.  This can probably be done in
100     a more reliable way, but using popen3 is relatively efficient."""
101
102     libs = ['g2c']
103     cmd = 'gcc -v'
104
105     try:
106         import subprocess
107     except ImportError:
108         try:
109             import popen2
110             stderr = popen2.popen3(cmd)[2]
111         except OSError:
112             return libs
113     else:
114         p = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE)
115         stderr = p.stderr
116
117     for l in stderr.readlines():
118         list = string.split(l)
119         if len(list) > 3 and list[:2] == ['gcc', 'version']:
120             if list[2][:3] in ('4.1','4.2','4.3'):
121                 libs = ['gfortranbegin']
122                 break
123             if list[2][:2] in ('3.', '4.'):
124                 libs = ['frtbegin'] + libs
125                 break
126     return libs
127
128
129 if sys.platform == 'cygwin':
130     # On Cygwin, os.path.normcase() lies, so just report back the
131     # fact that the underlying Win32 OS is case-insensitive.
132     def case_sensitive_suffixes(s1, s2):
133         return 0
134 else:
135     def case_sensitive_suffixes(s1, s2):
136         return (os.path.normcase(s1) != os.path.normcase(s2))
137
138
139 if sys.platform == 'win32':
140     fortran_lib = gccFortranLibs()
141 elif sys.platform == 'cygwin':
142     fortran_lib = gccFortranLibs()
143 elif string.find(sys.platform, 'irix') != -1:
144     fortran_lib = ['ftn']
145 else:
146     fortran_lib = gccFortranLibs()
147
148
149
150 file_expr = r"""File "[^"]*", line \d+, in .+
151 """
152
153 # re.escape escapes too much.
154 def re_escape(str):
155     for c in ['.', '[', ']', '(', ')', '*', '+', '?']:  # Not an exhaustive list.
156         str = string.replace(str, c, '\\' + c)
157     return str
158
159
160
161 try:
162     sys.version_info
163 except AttributeError:
164     # Pre-1.6 Python has no sys.version_info
165     version_string = string.split(sys.version)[0]
166     version_ints = map(int, string.split(version_string, '.'))
167     sys.version_info = tuple(version_ints + ['final', 0])
168
169 def python_version_string():
170     return string.split(sys.version)[0]
171
172 def python_minor_version_string():
173     return sys.version[:3]
174
175 def unsupported_python_version(version=sys.version_info):
176     return version < (1, 5, 2)
177
178 def deprecated_python_version(version=sys.version_info):
179     return version < (2, 4, 0)
180
181 if deprecated_python_version():
182     msg = r"""
183 scons: warning: Support for pre-2.4 Python (%s) is deprecated.
184     If this will cause hardship, contact dev@scons.tigris.org.
185 """
186
187     deprecated_python_expr = re_escape(msg % python_version_string()) + file_expr
188     del msg
189 else:
190     deprecated_python_expr = ""
191
192
193
194 class TestSCons(TestCommon):
195     """Class for testing SCons.
196
197     This provides a common place for initializing SCons tests,
198     eliminating the need to begin every test with the same repeated
199     initializations.
200     """
201
202     scons_version = SConsVersion
203
204     def __init__(self, **kw):
205         """Initialize an SCons testing object.
206
207         If they're not overridden by keyword arguments, this
208         initializes the object with the following default values:
209
210                 program = 'scons' if it exists,
211                           else 'scons.py'
212                 interpreter = 'python'
213                 match = match_exact
214                 workdir = ''
215
216         The workdir value means that, by default, a temporary workspace
217         directory is created for a TestSCons environment.  In addition,
218         this method changes directory (chdir) to the workspace directory,
219         so an explicit "chdir = '.'" on all of the run() method calls
220         is not necessary.
221         """
222         self.orig_cwd = os.getcwd()
223         try:
224             script_dir = os.environ['SCONS_SCRIPT_DIR']
225         except KeyError:
226             pass
227         else:
228             os.chdir(script_dir)
229         if not kw.has_key('program'):
230             kw['program'] = os.environ.get('SCONS')
231             if not kw['program']:
232                 if os.path.exists('scons'):
233                     kw['program'] = 'scons'
234                 else:
235                     kw['program'] = 'scons.py'
236         if not kw.has_key('interpreter') and not os.environ.get('SCONS_EXEC'):
237             kw['interpreter'] = [python, '-tt']
238         if not kw.has_key('match'):
239             kw['match'] = match_exact
240         if not kw.has_key('workdir'):
241             kw['workdir'] = ''
242
243         # Term causing test failures due to bogus readline init
244         # control character output on FC8
245         # TERM can cause test failures due to control chars in prompts etc.
246         os.environ['TERM'] = 'dumb'
247         
248         self.ignore_python_version=kw.get('ignore_python_version',1)
249         if kw.get('ignore_python_version',-1) != -1:
250             del kw['ignore_python_version']
251
252         if self.ignore_python_version and deprecated_python_version():
253             sconsflags = os.environ.get('SCONSFLAGS')
254             if sconsflags:
255                 sconsflags = [sconsflags]
256             else:
257                 sconsflags = []
258             sconsflags = sconsflags + ['--warn=no-python-version']
259             os.environ['SCONSFLAGS'] = string.join(sconsflags)
260
261         apply(TestCommon.__init__, [self], kw)
262
263         import SCons.Node.FS
264         if SCons.Node.FS.default_fs is None:
265             SCons.Node.FS.default_fs = SCons.Node.FS.FS()
266
267     def Environment(self, ENV=None, *args, **kw):
268         """
269         Return a construction Environment that optionally overrides
270         the default external environment with the specified ENV.
271         """
272         import SCons.Environment
273         import SCons.Errors
274         if not ENV is None:
275             kw['ENV'] = ENV
276         try:
277             return apply(SCons.Environment.Environment, args, kw)
278         except (SCons.Errors.UserError, SCons.Errors.InternalError):
279             return None
280
281     def detect(self, var, prog=None, ENV=None, norm=None):
282         """
283         Detect a program named 'prog' by first checking the construction
284         variable named 'var' and finally searching the path used by
285         SCons. If either method fails to detect the program, then false
286         is returned, otherwise the full path to prog is returned. If
287         prog is None, then the value of the environment variable will be
288         used as prog.
289         """
290         env = self.Environment(ENV)
291         v = env.subst('$'+var)
292         if not v:
293             return None
294         if prog is None:
295             prog = v
296         if v != prog:
297             return None
298         result = env.WhereIs(prog)
299         if norm and os.sep != '/':
300             result = string.replace(result, os.sep, '/')
301         return result
302
303     def detect_tool(self, tool, prog=None, ENV=None):
304         """
305         Given a tool (i.e., tool specification that would be passed
306         to the "tools=" parameter of Environment()) and a program that
307         corresponds to that tool, return true if and only if we can find
308         that tool using Environment.Detect().
309
310         By default, prog is set to the value passed into the tools parameter.
311         """
312
313         if not prog:
314             prog = tool
315         env = self.Environment(ENV, tools=[tool])
316         if env is None:
317             return None
318         return env.Detect([prog])
319
320     def where_is(self, prog, path=None):
321         """
322         Given a program, search for it in the specified external PATH,
323         or in the actual external PATH is none is specified.
324         """
325         import SCons.Environment
326         env = SCons.Environment.Environment()
327         if path is None:
328             path = os.environ['PATH']
329         return env.WhereIs(prog, path)
330
331     def wrap_stdout(self, build_str = "", read_str = "", error = 0, cleaning = 0):
332         """Wraps standard output string(s) in the normal
333         "Reading ... done" and "Building ... done" strings
334         """
335         cap,lc = [ ('Build','build'),
336                    ('Clean','clean') ][cleaning]
337         if error:
338             term = "scons: %sing terminated because of errors.\n" % lc
339         else:
340             term = "scons: done %sing targets.\n" % lc
341         return "scons: Reading SConscript files ...\n" + \
342                read_str + \
343                "scons: done reading SConscript files.\n" + \
344                "scons: %sing targets ...\n" % cap + \
345                build_str + \
346                term
347
348     def run(self, *args, **kw):
349         """
350         Add the --warn=no-python-version option to SCONSFLAGS every
351         command so test scripts don't have to filter out Python version
352         deprecation warnings.
353         """
354         save_sconsflags = os.environ.get('SCONSFLAGS')
355         if self.ignore_python_version and deprecated_python_version():
356             if save_sconsflags:
357                 sconsflags = [save_sconsflags]
358             else:
359                 sconsflags = []
360             sconsflags = sconsflags + ['--warn=no-python-version']
361             os.environ['SCONSFLAGS'] = string.join(sconsflags)
362         try:
363             result = apply(TestCommon.run, (self,)+args, kw)
364         finally:
365             sconsflags = save_sconsflags
366         return result
367
368     def up_to_date(self, options = None, arguments = None, read_str = "", **kw):
369         s = ""
370         for arg in string.split(arguments):
371             s = s + "scons: `%s' is up to date.\n" % arg
372             if options:
373                 arguments = options + " " + arguments
374         kw['arguments'] = arguments
375         stdout = self.wrap_stdout(read_str = read_str, build_str = s)
376         # Append '.*' so that timing output that comes after the
377         # up-to-date output is okay.
378         kw['stdout'] = re.escape(stdout) + '.*'
379         kw['match'] = self.match_re_dotall
380         apply(self.run, [], kw)
381
382     def not_up_to_date(self, options = None, arguments = None, **kw):
383         """Asserts that none of the targets listed in arguments is
384         up to date, but does not make any assumptions on other targets.
385         This function is most useful in conjunction with the -n option.
386         """
387         s = ""
388         for arg in string.split(arguments):
389             s = s + "(?!scons: `%s' is up to date.)" % re.escape(arg)
390             if options:
391                 arguments = options + " " + arguments
392         s = '('+s+'[^\n]*\n)*'
393         kw['arguments'] = arguments
394         stdout = re.escape(self.wrap_stdout(build_str='ARGUMENTSGOHERE'))
395         kw['stdout'] = string.replace(stdout, 'ARGUMENTSGOHERE', s)
396         kw['match'] = self.match_re_dotall
397         apply(self.run, [], kw)
398
399     def diff_substr(self, expect, actual, prelen=20, postlen=40):
400         i = 0
401         for x, y in zip(expect, actual):
402             if x != y:
403                 return "Actual did not match expect at char %d:\n" \
404                        "    Expect:  %s\n" \
405                        "    Actual:  %s\n" \
406                        % (i, repr(expect[i-prelen:i+postlen]),
407                              repr(actual[i-prelen:i+postlen]))
408             i = i + 1
409         return "Actual matched the expected output???"
410
411     def python_file_line(self, file, line):
412         """
413         Returns a Python error line for output comparisons.
414
415         The exec of the traceback line gives us the correct format for
416         this version of Python.  Before 2.5, this yielded:
417
418             File "<string>", line 1, ?
419
420         Python 2.5 changed this to:
421
422             File "<string>", line 1, <module>
423
424         We stick the requested file name and line number in the right
425         places, abstracting out the version difference.
426         """
427         exec 'import traceback; x = traceback.format_stack()[-1]'
428         x = string.lstrip(x)
429         x = string.replace(x, '<string>', file)
430         x = string.replace(x, 'line 1,', 'line %s,' % line)
431         return x
432
433     def normalize_pdf(self, s):
434         s = re.sub(r'/(Creation|Mod)Date \(D:[^)]*\)',
435                    r'/\1Date (D:XXXX)', s)
436         s = re.sub(r'/ID \[<[0-9a-fA-F]*> <[0-9a-fA-F]*>\]',
437                    r'/ID [<XXXX> <XXXX>]', s)
438         s = re.sub(r'/(BaseFont|FontName) /[A-Z]{6}',
439                    r'/\1 /XXXXXX', s)
440         s = re.sub(r'/Length \d+ *\n/Filter /FlateDecode\n',
441                    r'/Length XXXX\n/Filter /FlateDecode\n', s)
442
443
444         try:
445             import zlib
446         except ImportError:
447             pass
448         else:
449             begin_marker = '/FlateDecode\n>>\nstream\n'
450             end_marker = 'endstream\nendobj'
451
452             encoded = []
453             b = string.find(s, begin_marker, 0)
454             while b != -1:
455                 b = b + len(begin_marker)
456                 e = string.find(s, end_marker, b)
457                 encoded.append((b, e))
458                 b = string.find(s, begin_marker, e + len(end_marker))
459
460             x = 0
461             r = []
462             for b, e in encoded:
463                 r.append(s[x:b])
464                 d = zlib.decompress(s[b:e])
465                 d = re.sub(r'%%CreationDate: [^\n]*\n',
466                            r'%%CreationDate: 1970 Jan 01 00:00:00\n', d)
467                 d = re.sub(r'%DVIPSSource:  TeX output \d\d\d\d\.\d\d\.\d\d:\d\d\d\d',
468                            r'%DVIPSSource:  TeX output 1970.01.01:0000', d)
469                 d = re.sub(r'/(BaseFont|FontName) /[A-Z]{6}',
470                            r'/\1 /XXXXXX', d)
471                 r.append(d)
472                 x = e
473             r.append(s[x:])
474             s = string.join(r, '')
475
476         return s
477
478     def paths(self,patterns):
479         import glob
480         result = []
481         for p in patterns:
482             paths = glob.glob(p)
483             paths.sort()
484             result.extend(paths)
485         return result
486
487
488     def java_ENV(self, version=None):
489         """
490         Initialize with a default external environment that uses a local
491         Java SDK in preference to whatever's found in the default PATH.
492         """
493         try:
494             return self._java_env[version]['ENV']
495         except AttributeError:
496             self._java_env = {}
497         except KeyError:
498             pass
499
500         import SCons.Environment
501         env = SCons.Environment.Environment()
502         self._java_env[version] = env
503
504
505         if version:
506             patterns = [
507                 '/usr/java/jdk%s*/bin'    % version,
508                 '/usr/lib/jvm/*-%s*/bin' % version,
509                 '/usr/local/j2sdk%s*/bin' % version,
510             ]
511             java_path = self.paths(patterns) + [env['ENV']['PATH']]
512         else:
513             patterns = [
514                 '/usr/java/latest/bin',
515                 '/usr/lib/jvm/*/bin',
516                 '/usr/local/j2sdk*/bin',
517             ]
518             java_path = self.paths(patterns) + [env['ENV']['PATH']]
519
520         env['ENV']['PATH'] = string.join(java_path, os.pathsep)
521         return env['ENV']
522
523     def java_where_includes(self,version=None):
524         """
525         Return java include paths compiling java jni code
526         """
527         import glob
528         import sys
529         if not version:
530             version=''
531             frame = '/System/Library/Frameworks/JavaVM.framework/Headers/jni.h'
532         else:
533             frame = '/System/Library/Frameworks/JavaVM.framework/Versions/%s*/Headers/jni.h'%version
534         jni_dirs = ['/usr/lib/jvm/java-*-sun-%s*/include/jni.h'%version,
535                     '/usr/java/jdk%s*/include/jni.h'%version,
536                     frame,
537                     ]
538         dirs = self.paths(jni_dirs)
539         if not dirs:
540             return None
541         d=os.path.dirname(self.paths(jni_dirs)[0])
542         result=[d]
543
544         if sys.platform == 'win32':
545             result.append(os.path.join(d,'win32'))
546         elif sys.platform == 'linux2':
547             result.append(os.path.join(d,'linux'))
548         return result
549
550
551     def java_where_java_home(self,version=None):
552         if sys.platform[:6] == 'darwin':
553             if version is None:
554                 home = '/System/Library/Frameworks/JavaVM.framework/Home'
555             else:
556                 home = '/System/Library/Frameworks/JavaVM.framework/Versions/%s/Home' % version
557         else:
558             jar = self.java_where_jar(version)
559             home = os.path.normpath('%s/..'%jar)
560         if os.path.isdir(home):
561             return home
562         print("Could not determine JAVA_HOME: %s is not a directory" % home)
563         self.fail_test()
564
565     def java_where_jar(self, version=None):
566         ENV = self.java_ENV(version)
567         if self.detect_tool('jar', ENV=ENV):
568             where_jar = self.detect('JAR', 'jar', ENV=ENV)
569         else:
570             where_jar = self.where_is('jar', ENV['PATH'])
571         if not where_jar:
572             self.skip_test("Could not find Java jar, skipping test(s).\n")
573         return where_jar
574
575     def java_where_java(self, version=None):
576         """
577         Return a path to the java executable.
578         """
579         ENV = self.java_ENV(version)
580         where_java = self.where_is('java', ENV['PATH'])
581         if not where_java:
582             self.skip_test("Could not find Java java, skipping test(s).\n")
583         return where_java
584
585     def java_where_javac(self, version=None):
586         """
587         Return a path to the javac compiler.
588         """
589         ENV = self.java_ENV(version)
590         if self.detect_tool('javac'):
591             where_javac = self.detect('JAVAC', 'javac', ENV=ENV)
592         else:
593             where_javac = self.where_is('javac', ENV['PATH'])
594         if not where_javac:
595             self.skip_test("Could not find Java javac, skipping test(s).\n")
596         self.run(program = where_javac,
597                  arguments = '-version',
598                  stderr=None,
599                  status=None)
600         if version:
601             if string.find(self.stderr(), 'javac %s' % version) == -1:
602                 fmt = "Could not find javac for Java version %s, skipping test(s).\n"
603                 self.skip_test(fmt % version)
604         else:
605             m = re.search(r'javac (\d\.\d)', self.stderr())
606             if m:
607                 version = m.group(1)
608             else:
609                 version = None
610         return where_javac, version
611
612     def java_where_javah(self, version=None):
613         ENV = self.java_ENV(version)
614         if self.detect_tool('javah'):
615             where_javah = self.detect('JAVAH', 'javah', ENV=ENV)
616         else:
617             where_javah = self.where_is('javah', ENV['PATH'])
618         if not where_javah:
619             self.skip_test("Could not find Java javah, skipping test(s).\n")
620         return where_javah
621
622     def java_where_rmic(self, version=None):
623         ENV = self.java_ENV(version)
624         if self.detect_tool('rmic'):
625             where_rmic = self.detect('RMIC', 'rmic', ENV=ENV)
626         else:
627             where_rmic = self.where_is('rmic', ENV['PATH'])
628         if not where_rmic:
629             self.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n")
630         return where_rmic
631
632     def Qt_dummy_installation(self, dir='qt'):
633         # create a dummy qt installation
634
635         self.subdir( dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'] )
636
637         self.write([dir, 'bin', 'mymoc.py'], """\
638 import getopt
639 import sys
640 import string
641 import re
642 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:', [])
643 output = None
644 impl = 0
645 opt_string = ''
646 for opt, arg in cmd_opts:
647     if opt == '-o': output = open(arg, 'wb')
648     elif opt == '-i': impl = 1
649     else: opt_string = opt_string + ' ' + opt
650 for a in args:
651     contents = open(a, 'rb').read()
652     a = string.replace(a, '\\\\', '\\\\\\\\')
653     subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
654     if impl:
655         contents = re.sub( r'#include.*', '', contents )
656     output.write(string.replace(contents, 'Q_OBJECT', subst))
657 output.close()
658 sys.exit(0)
659 """)
660
661         self.write([dir, 'bin', 'myuic.py'], """\
662 import os.path
663 import re
664 import sys
665 import string
666 output_arg = 0
667 impl_arg = 0
668 impl = None
669 source = None
670 for arg in sys.argv[1:]:
671     if output_arg:
672         output = open(arg, 'wb')
673         output_arg = 0
674     elif impl_arg:
675         impl = arg
676         impl_arg = 0
677     elif arg == "-o":
678         output_arg = 1
679     elif arg == "-impl":
680         impl_arg = 1
681     else:
682         if source:
683             sys.exit(1)
684         source = open(arg, 'rb')
685         sourceFile = arg
686 if impl:
687     output.write( '#include "' + impl + '"\\n' )
688     includes = re.findall('<include.*?>(.*?)</include>', source.read())
689     for incFile in includes:
690         # this is valid for ui.h files, at least
691         if os.path.exists(incFile):
692             output.write('#include "' + incFile + '"\\n')
693 else:
694     output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
695 output.close()
696 sys.exit(0)
697 """ )
698
699         self.write([dir, 'include', 'my_qobject.h'], r"""
700 #define Q_OBJECT ;
701 void my_qt_symbol(const char *arg);
702 """)
703
704         self.write([dir, 'lib', 'my_qobject.cpp'], r"""
705 #include "../include/my_qobject.h"
706 #include <stdio.h>
707 void my_qt_symbol(const char *arg) {
708   printf( arg );
709 }
710 """)
711
712         self.write([dir, 'lib', 'SConstruct'], r"""
713 env = Environment()
714 import sys
715 if sys.platform == 'win32':
716     env.StaticLibrary( 'myqt', 'my_qobject.cpp' )
717 else:
718     env.SharedLibrary( 'myqt', 'my_qobject.cpp' )
719 """)
720
721         self.run(chdir = self.workpath(dir, 'lib'),
722                  arguments = '.',
723                  stderr = noisy_ar,
724                  match = self.match_re_dotall)
725
726         self.QT = self.workpath(dir)
727         self.QT_LIB = 'myqt'
728         self.QT_MOC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'mymoc.py'))
729         self.QT_UIC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'myuic.py'))
730         self.QT_LIB_DIR = self.workpath(dir, 'lib')
731
732     def Qt_create_SConstruct(self, place):
733         if type(place) is type([]):
734             place = apply(test.workpath, place)
735         self.write(place, """\
736 if ARGUMENTS.get('noqtdir', 0): QTDIR=None
737 else: QTDIR=r'%s'
738 env = Environment(QTDIR = QTDIR,
739                   QT_LIB = r'%s',
740                   QT_MOC = r'%s',
741                   QT_UIC = r'%s',
742                   tools=['default','qt'])
743 dup = 1
744 if ARGUMENTS.get('variant_dir', 0):
745     if ARGUMENTS.get('chdir', 0):
746         SConscriptChdir(1)
747     else:
748         SConscriptChdir(0)
749     dup=int(ARGUMENTS.get('dup', 1))
750     if dup == 0:
751         builddir = 'build_dup0'
752         env['QT_DEBUG'] = 1
753     else:
754         builddir = 'build'
755     VariantDir(builddir, '.', duplicate=dup)
756     print builddir, dup
757     sconscript = Dir(builddir).File('SConscript')
758 else:
759     sconscript = File('SConscript')
760 Export("env dup")
761 SConscript( sconscript )
762 """ % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
763
764
765     NCR = 0 # non-cached rebuild
766     CR  = 1 # cached rebuild (up to date)
767     NCF = 2 # non-cached build failure
768     CF  = 3 # cached build failure
769
770     if sys.platform == 'win32':
771         Configure_lib = 'msvcrt'
772     else:
773         Configure_lib = 'm'
774
775     # to use cygwin compilers on cmd.exe -> uncomment following line
776     #Configure_lib = 'm'
777
778     def checkLogAndStdout(self, checks, results, cached,
779                           logfile, sconf_dir, sconstruct,
780                           doCheckLog=1, doCheckStdout=1):
781
782         class NoMatch:
783             def __init__(self, p):
784                 self.pos = p
785
786         def matchPart(log, logfile, lastEnd, NoMatch=NoMatch):
787             m = re.match(log, logfile[lastEnd:])
788             if not m:
789                 raise NoMatch, lastEnd
790             return m.end() + lastEnd
791         try:
792             #print len(os.linesep)
793             ls = os.linesep
794             nols = "("
795             for i in range(len(ls)):
796                 nols = nols + "("
797                 for j in range(i):
798                     nols = nols + ls[j]
799                 nols = nols + "[^" + ls[i] + "])"
800                 if i < len(ls)-1:
801                     nols = nols + "|"
802             nols = nols + ")"
803             lastEnd = 0
804             logfile = self.read(self.workpath(logfile))
805             if (doCheckLog and
806                 string.find( logfile, "scons: warning: The stored build "
807                              "information has an unexpected class." ) >= 0):
808                 self.fail_test()
809             sconf_dir = sconf_dir
810             sconstruct = sconstruct
811
812             log = r'file\ \S*%s\,line \d+:' % re.escape(sconstruct) + ls
813             if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
814             log = "\t" + re.escape("Configure(confdir = %s)" % sconf_dir) + ls
815             if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
816             rdstr = ""
817             cnt = 0
818             for check,result,cache_desc in zip(checks, results, cached):
819                 log   = re.escape("scons: Configure: " + check) + ls
820                 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
821                 log = ""
822                 result_cached = 1
823                 for bld_desc in cache_desc: # each TryXXX
824                     for ext, flag in bld_desc: # each file in TryBuild
825                         file = os.path.join(sconf_dir,"conftest_%d%s" % (cnt, ext))
826                         if flag == self.NCR:
827                             # rebuild will pass
828                             if ext in ['.c', '.cpp']:
829                                 log=log + re.escape(file + " <-") + ls
830                                 log=log + r"(  \|" + nols + "*" + ls + ")+?"
831                             else:
832                                 log=log + "(" + nols + "*" + ls +")*?"
833                             result_cached = 0
834                         if flag == self.CR:
835                             # up to date
836                             log=log + \
837                                  re.escape("scons: Configure: \"%s\" is up to date." 
838                                            % file) + ls
839                             log=log+re.escape("scons: Configure: The original builder "
840                                               "output was:") + ls
841                             log=log+r"(  \|.*"+ls+")+"
842                         if flag == self.NCF:
843                             # non-cached rebuild failure
844                             log=log + "(" + nols + "*" + ls + ")*?"
845                             result_cached = 0
846                         if flag == self.CF:
847                             # cached rebuild failure
848                             log=log + \
849                                  re.escape("scons: Configure: Building \"%s\" failed "
850                                            "in a previous run and all its sources are"
851                                            " up to date." % file) + ls
852                             log=log+re.escape("scons: Configure: The original builder "
853                                               "output was:") + ls
854                             log=log+r"(  \|.*"+ls+")+"
855                     cnt = cnt + 1
856                 if result_cached:
857                     result = "(cached) " + result
858                 rdstr = rdstr + re.escape(check) + re.escape(result) + "\n"
859                 log=log + re.escape("scons: Configure: " + result) + ls + ls
860                 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
861                 log = ""
862             if doCheckLog: lastEnd = matchPart(ls, logfile, lastEnd)
863             if doCheckLog and lastEnd != len(logfile):
864                 raise NoMatch, lastEnd
865             
866         except NoMatch, m:
867             print "Cannot match log file against log regexp."
868             print "log file: "
869             print "------------------------------------------------------"
870             print logfile[m.pos:]
871             print "------------------------------------------------------"
872             print "log regexp: "
873             print "------------------------------------------------------"
874             print log
875             print "------------------------------------------------------"
876             self.fail_test()
877
878         if doCheckStdout:
879             exp_stdout = self.wrap_stdout(".*", rdstr)
880             if not self.match_re_dotall(self.stdout(), exp_stdout):
881                 print "Unexpected stdout: "
882                 print "-----------------------------------------------------"
883                 print repr(self.stdout())
884                 print "-----------------------------------------------------"
885                 print repr(exp_stdout)
886                 print "-----------------------------------------------------"
887                 self.fail_test()
888
889     def get_python_version(self):
890         """
891         Returns the Python version (just so everyone doesn't have to
892         hand-code slicing the right number of characters).
893         """
894         # see also sys.prefix documentation
895         return python_minor_version_string()
896
897     def get_platform_python_info(self):
898         """
899         Returns a path to a Python executable suitable for testing on
900         this platform and its associated include path, library path,
901         and library name.
902         """
903         python = self.where_is('python')
904         if not python:
905             self.skip_test('Can not find installed "python", skipping test.\n')
906
907         self.run(program = python, stdin = """\
908 import os, sys
909 try:
910         py_ver = 'python%d.%d' % sys.version_info[:2]
911 except AttributeError:
912         py_ver = 'python' + sys.version[:3]
913 print os.path.join(sys.prefix, 'include', py_ver)
914 print os.path.join(sys.prefix, 'lib', py_ver, 'config')
915 print py_ver
916 """)
917
918         return [python] + string.split(string.strip(self.stdout()), '\n')
919
920     def wait_for(self, fname, timeout=10.0, popen=None):
921         """
922         Waits for the specified file name to exist.
923         """
924         waited = 0.0
925         while not os.path.exists(fname):
926             if timeout and waited >= timeout:
927                 sys.stderr.write('timed out waiting for %s to exist\n' % fname)
928                 if popen:
929                     popen.stdin.close()
930                     self.status = 1
931                     self.finish(popen)
932                 self.fail_test()
933             time.sleep(1.0)
934             waited = waited + 1.0
935
936     def get_alt_cpp_suffix(self):
937         """
938         Many CXX tests have this same logic.
939         They all needed to determine if the current os supports
940         files with .C and .c as different files or not
941         in which case they are instructed to use .cpp instead of .C
942         """
943         if not case_sensitive_suffixes('.c','.C'):
944             alt_cpp_suffix = '.cpp'
945         else:
946             alt_cpp_suffix = '.C'
947         return alt_cpp_suffix
948
949
950 class TimeSCons(TestSCons):
951     """Class for timing SCons."""
952     def __init__(self, *args, **kw):
953         """
954         In addition to normal TestSCons.TestSCons intialization,
955         this enables verbose mode (which causes the command lines to
956         be displayed in the output) and copies the contents of the
957         directory containing the executing script to the temporary
958         working directory.
959         """
960         if not kw.has_key('verbose'):
961             kw['verbose'] = True
962         # TODO(1.5)
963         #TestSCons.__init__(self, *args, **kw)
964         apply(TestSCons.__init__, (self,)+args, kw)
965
966         # TODO(sgk):    better way to get the script dir than sys.argv[0]
967         test_dir = os.path.dirname(sys.argv[0])
968         test_name = os.path.basename(test_dir)
969
970         if not os.path.isabs(test_dir):
971             test_dir = os.path.join(self.orig_cwd, test_dir)
972         self.copy_timing_configuration(test_dir, self.workpath())
973
974     def main(self, *args, **kw):
975         """
976         The main entry point for standard execution of timings.
977
978         This method run SCons three times:
979
980           Once with the --help option, to have it exit after just reading
981           the configuration.
982
983           Once as a full build of all targets.
984
985           Once again as a (presumably) null or up-to-date build of
986           all targets.
987
988         The elapsed time to execute each build is printed after
989         it has finished.
990         """
991         # TODO(1.5)
992         #self.help(*args, **kw)
993         #self.full(*args, **kw)
994         #self.null(*args, **kw)
995         apply(self.help, args, kw)
996         apply(self.full, args, kw)
997         apply(self.null, args, kw)
998
999     def help(self, *args, **kw):
1000         """
1001         Runs scons with the --help option.
1002
1003         This serves as a way to isolate just the amount of time spent
1004         reading up the configuration, since --help exits before any
1005         "real work" is done.
1006         """
1007         kw['options'] = kw.get('options', '') + ' --help'
1008         # TODO(1.5)
1009         #self.run(*args, **kw)
1010         apply(self.run, args, kw)
1011         sys.stdout.write(self.stdout())
1012         print "RESULT", self.elapsed_time()
1013
1014     def full(self, *args, **kw):
1015         """
1016         Runs a full build of SCons.
1017         """
1018         # TODO(1.5)
1019         #self.run(*args, **kw)
1020         apply(self.run, args, kw)
1021         sys.stdout.write(self.stdout())
1022         print "RESULT", self.elapsed_time()
1023
1024     def null(self, *args, **kw):
1025         """
1026         Runs an up-to-date null build of SCons.
1027         """
1028         # TODO(sgk):  allow the caller to specify the target (argument)
1029         # that must be up-to-date.
1030         # TODO(1.5)
1031         #self.up_to_date(arguments='.', **kw)
1032         kw = kw.copy()
1033         kw['arguments'] = '.'
1034         apply(self.up_to_date, (), kw)
1035         sys.stdout.write(self.stdout())
1036         print "RESULT", self.elapsed_time()
1037
1038     def elapsed_time(self):
1039         """
1040         Returns the elapsed time of the most recent command execution.
1041         """
1042         return self.endTime - self.startTime
1043
1044     def run(self, *args, **kw):
1045         """
1046         Runs a single build command, capturing output in the specified file.
1047
1048         Because this class is about timing SCons, we record the start
1049         and end times of the elapsed execution, and also add the
1050         --debug=memory and --debug=time options to have SCons report
1051         its own memory and timing statistics.
1052         """
1053         kw['options'] = kw.get('options', '') + ' --debug=memory --debug=time'
1054         self.startTime = time.time()
1055         try:
1056             # TODO(1.5)
1057             #result = TestSCons.run(self, *args, **kw)
1058             result = apply(TestSCons.run, (self,)+args, kw)
1059         finally:
1060             self.endTime = time.time()
1061         return result
1062
1063     def copy_timing_configuration(self, source_dir, dest_dir):
1064         """
1065         Copies the timing configuration from the specified source_dir (the
1066         directory in which the controlling script lives) to the specified
1067         dest_dir (a temporary working directory).
1068
1069         This ignores all files and directories that begin with the string
1070         'TimeSCons-', and all '.svn' subdirectories.
1071         """
1072         for root, dirs, files in os.walk(source_dir):
1073             if '.svn' in dirs:
1074                 dirs.remove('.svn')
1075             # TODO(1.5)
1076             #dirs = [ d for d in dirs if not d.startswith('TimeSCons-') ]
1077             #files = [ f for f in files if not f.startswith('TimeSCons-') ]
1078             not_timescons_entries = lambda s: not s.startswith('TimeSCons-')
1079             dirs = filter(not_timescons_entries, dirs)
1080             files = filter(not_timescons_entries, files)
1081             for dirname in dirs:
1082                 source = os.path.join(root, dirname)
1083                 destination = source.replace(source_dir, dest_dir)
1084                 os.mkdir(destination)
1085                 if sys.platform != 'win32':
1086                     shutil.copystat(source, destination)
1087             for filename in files:
1088                 source = os.path.join(root, filename)
1089                 destination = source.replace(source_dir, dest_dir)
1090                 shutil.copy2(source, destination)
1091     
1092
1093 # In some environments, $AR will generate a warning message to stderr
1094 # if the library doesn't previously exist and is being created.  One
1095 # way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
1096 # but this is difficult to do in a platform-/implementation-specific
1097 # method.  Instead, we will use the following as a stderr match for
1098 # tests that use AR so that we will view zero or more "ar: creating
1099 # <file>" messages to be successful executions of the test (see
1100 # test/AR.py for sample usage).
1101
1102 noisy_ar=r'(ar: creating( archive)? \S+\n?)*'
1103
1104 # Local Variables:
1105 # tab-width:4
1106 # indent-tabs-mode:nil
1107 # End:
1108 # vim: set expandtab tabstop=4 shiftwidth=4: