Have the TimeSCons class examine the run output for the reported memory
[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         Same for --warn=no-visual-c-missing.
354         """
355         save_sconsflags = os.environ.get('SCONSFLAGS')
356         if save_sconsflags:
357             sconsflags = [save_sconsflags]
358         else:
359             sconsflags = []
360         if self.ignore_python_version and deprecated_python_version():
361             sconsflags = sconsflags + ['--warn=no-python-version']
362         sconsflags = sconsflags + ['--warn=no-visual-c-missing']
363         os.environ['SCONSFLAGS'] = string.join(sconsflags)
364         try:
365             result = apply(TestCommon.run, (self,)+args, kw)
366         finally:
367             sconsflags = save_sconsflags
368         return result
369
370     def up_to_date(self, options = None, arguments = None, read_str = "", **kw):
371         s = ""
372         for arg in string.split(arguments):
373             s = s + "scons: `%s' is up to date.\n" % arg
374             if options:
375                 arguments = options + " " + arguments
376         kw['arguments'] = arguments
377         stdout = self.wrap_stdout(read_str = read_str, build_str = s)
378         # Append '.*' so that timing output that comes after the
379         # up-to-date output is okay.
380         kw['stdout'] = re.escape(stdout) + '.*'
381         kw['match'] = self.match_re_dotall
382         apply(self.run, [], kw)
383
384     def not_up_to_date(self, options = None, arguments = None, **kw):
385         """Asserts that none of the targets listed in arguments is
386         up to date, but does not make any assumptions on other targets.
387         This function is most useful in conjunction with the -n option.
388         """
389         s = ""
390         for arg in string.split(arguments):
391             s = s + "(?!scons: `%s' is up to date.)" % re.escape(arg)
392             if options:
393                 arguments = options + " " + arguments
394         s = '('+s+'[^\n]*\n)*'
395         kw['arguments'] = arguments
396         stdout = re.escape(self.wrap_stdout(build_str='ARGUMENTSGOHERE'))
397         kw['stdout'] = string.replace(stdout, 'ARGUMENTSGOHERE', s)
398         kw['match'] = self.match_re_dotall
399         apply(self.run, [], kw)
400
401     def diff_substr(self, expect, actual, prelen=20, postlen=40):
402         i = 0
403         for x, y in zip(expect, actual):
404             if x != y:
405                 return "Actual did not match expect at char %d:\n" \
406                        "    Expect:  %s\n" \
407                        "    Actual:  %s\n" \
408                        % (i, repr(expect[i-prelen:i+postlen]),
409                              repr(actual[i-prelen:i+postlen]))
410             i = i + 1
411         return "Actual matched the expected output???"
412
413     def python_file_line(self, file, line):
414         """
415         Returns a Python error line for output comparisons.
416
417         The exec of the traceback line gives us the correct format for
418         this version of Python.  Before 2.5, this yielded:
419
420             File "<string>", line 1, ?
421
422         Python 2.5 changed this to:
423
424             File "<string>", line 1, <module>
425
426         We stick the requested file name and line number in the right
427         places, abstracting out the version difference.
428         """
429         exec 'import traceback; x = traceback.format_stack()[-1]'
430         x = string.lstrip(x)
431         x = string.replace(x, '<string>', file)
432         x = string.replace(x, 'line 1,', 'line %s,' % line)
433         return x
434
435     def normalize_pdf(self, s):
436         s = re.sub(r'/(Creation|Mod)Date \(D:[^)]*\)',
437                    r'/\1Date (D:XXXX)', s)
438         s = re.sub(r'/ID \[<[0-9a-fA-F]*> <[0-9a-fA-F]*>\]',
439                    r'/ID [<XXXX> <XXXX>]', s)
440         s = re.sub(r'/(BaseFont|FontName) /[A-Z]{6}',
441                    r'/\1 /XXXXXX', s)
442         s = re.sub(r'/Length \d+ *\n/Filter /FlateDecode\n',
443                    r'/Length XXXX\n/Filter /FlateDecode\n', s)
444
445
446         try:
447             import zlib
448         except ImportError:
449             pass
450         else:
451             begin_marker = '/FlateDecode\n>>\nstream\n'
452             end_marker = 'endstream\nendobj'
453
454             encoded = []
455             b = string.find(s, begin_marker, 0)
456             while b != -1:
457                 b = b + len(begin_marker)
458                 e = string.find(s, end_marker, b)
459                 encoded.append((b, e))
460                 b = string.find(s, begin_marker, e + len(end_marker))
461
462             x = 0
463             r = []
464             for b, e in encoded:
465                 r.append(s[x:b])
466                 d = zlib.decompress(s[b:e])
467                 d = re.sub(r'%%CreationDate: [^\n]*\n',
468                            r'%%CreationDate: 1970 Jan 01 00:00:00\n', d)
469                 d = re.sub(r'%DVIPSSource:  TeX output \d\d\d\d\.\d\d\.\d\d:\d\d\d\d',
470                            r'%DVIPSSource:  TeX output 1970.01.01:0000', d)
471                 d = re.sub(r'/(BaseFont|FontName) /[A-Z]{6}',
472                            r'/\1 /XXXXXX', d)
473                 r.append(d)
474                 x = e
475             r.append(s[x:])
476             s = string.join(r, '')
477
478         return s
479
480     def paths(self,patterns):
481         import glob
482         result = []
483         for p in patterns:
484             paths = glob.glob(p)
485             paths.sort()
486             result.extend(paths)
487         return result
488
489
490     def java_ENV(self, version=None):
491         """
492         Initialize with a default external environment that uses a local
493         Java SDK in preference to whatever's found in the default PATH.
494         """
495         try:
496             return self._java_env[version]['ENV']
497         except AttributeError:
498             self._java_env = {}
499         except KeyError:
500             pass
501
502         import SCons.Environment
503         env = SCons.Environment.Environment()
504         self._java_env[version] = env
505
506
507         if version:
508             patterns = [
509                 '/usr/java/jdk%s*/bin'    % version,
510                 '/usr/lib/jvm/*-%s*/bin' % version,
511                 '/usr/local/j2sdk%s*/bin' % version,
512             ]
513             java_path = self.paths(patterns) + [env['ENV']['PATH']]
514         else:
515             patterns = [
516                 '/usr/java/latest/bin',
517                 '/usr/lib/jvm/*/bin',
518                 '/usr/local/j2sdk*/bin',
519             ]
520             java_path = self.paths(patterns) + [env['ENV']['PATH']]
521
522         env['ENV']['PATH'] = string.join(java_path, os.pathsep)
523         return env['ENV']
524
525     def java_where_includes(self,version=None):
526         """
527         Return java include paths compiling java jni code
528         """
529         import glob
530         import sys
531         if not version:
532             version=''
533             frame = '/System/Library/Frameworks/JavaVM.framework/Headers/jni.h'
534         else:
535             frame = '/System/Library/Frameworks/JavaVM.framework/Versions/%s*/Headers/jni.h'%version
536         jni_dirs = ['/usr/lib/jvm/java-*-sun-%s*/include/jni.h'%version,
537                     '/usr/java/jdk%s*/include/jni.h'%version,
538                     frame,
539                     ]
540         dirs = self.paths(jni_dirs)
541         if not dirs:
542             return None
543         d=os.path.dirname(self.paths(jni_dirs)[0])
544         result=[d]
545
546         if sys.platform == 'win32':
547             result.append(os.path.join(d,'win32'))
548         elif sys.platform == 'linux2':
549             result.append(os.path.join(d,'linux'))
550         return result
551
552
553     def java_where_java_home(self,version=None):
554         if sys.platform[:6] == 'darwin':
555             if version is None:
556                 home = '/System/Library/Frameworks/JavaVM.framework/Home'
557             else:
558                 home = '/System/Library/Frameworks/JavaVM.framework/Versions/%s/Home' % version
559         else:
560             jar = self.java_where_jar(version)
561             home = os.path.normpath('%s/..'%jar)
562         if os.path.isdir(home):
563             return home
564         print("Could not determine JAVA_HOME: %s is not a directory" % home)
565         self.fail_test()
566
567     def java_where_jar(self, version=None):
568         ENV = self.java_ENV(version)
569         if self.detect_tool('jar', ENV=ENV):
570             where_jar = self.detect('JAR', 'jar', ENV=ENV)
571         else:
572             where_jar = self.where_is('jar', ENV['PATH'])
573         if not where_jar:
574             self.skip_test("Could not find Java jar, skipping test(s).\n")
575         return where_jar
576
577     def java_where_java(self, version=None):
578         """
579         Return a path to the java executable.
580         """
581         ENV = self.java_ENV(version)
582         where_java = self.where_is('java', ENV['PATH'])
583         if not where_java:
584             self.skip_test("Could not find Java java, skipping test(s).\n")
585         return where_java
586
587     def java_where_javac(self, version=None):
588         """
589         Return a path to the javac compiler.
590         """
591         ENV = self.java_ENV(version)
592         if self.detect_tool('javac'):
593             where_javac = self.detect('JAVAC', 'javac', ENV=ENV)
594         else:
595             where_javac = self.where_is('javac', ENV['PATH'])
596         if not where_javac:
597             self.skip_test("Could not find Java javac, skipping test(s).\n")
598         self.run(program = where_javac,
599                  arguments = '-version',
600                  stderr=None,
601                  status=None)
602         if version:
603             if string.find(self.stderr(), 'javac %s' % version) == -1:
604                 fmt = "Could not find javac for Java version %s, skipping test(s).\n"
605                 self.skip_test(fmt % version)
606         else:
607             m = re.search(r'javac (\d\.\d)', self.stderr())
608             if m:
609                 version = m.group(1)
610             else:
611                 version = None
612         return where_javac, version
613
614     def java_where_javah(self, version=None):
615         ENV = self.java_ENV(version)
616         if self.detect_tool('javah'):
617             where_javah = self.detect('JAVAH', 'javah', ENV=ENV)
618         else:
619             where_javah = self.where_is('javah', ENV['PATH'])
620         if not where_javah:
621             self.skip_test("Could not find Java javah, skipping test(s).\n")
622         return where_javah
623
624     def java_where_rmic(self, version=None):
625         ENV = self.java_ENV(version)
626         if self.detect_tool('rmic'):
627             where_rmic = self.detect('RMIC', 'rmic', ENV=ENV)
628         else:
629             where_rmic = self.where_is('rmic', ENV['PATH'])
630         if not where_rmic:
631             self.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n")
632         return where_rmic
633
634     def Qt_dummy_installation(self, dir='qt'):
635         # create a dummy qt installation
636
637         self.subdir( dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'] )
638
639         self.write([dir, 'bin', 'mymoc.py'], """\
640 import getopt
641 import sys
642 import string
643 import re
644 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:', [])
645 output = None
646 impl = 0
647 opt_string = ''
648 for opt, arg in cmd_opts:
649     if opt == '-o': output = open(arg, 'wb')
650     elif opt == '-i': impl = 1
651     else: opt_string = opt_string + ' ' + opt
652 for a in args:
653     contents = open(a, 'rb').read()
654     a = string.replace(a, '\\\\', '\\\\\\\\')
655     subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
656     if impl:
657         contents = re.sub( r'#include.*', '', contents )
658     output.write(string.replace(contents, 'Q_OBJECT', subst))
659 output.close()
660 sys.exit(0)
661 """)
662
663         self.write([dir, 'bin', 'myuic.py'], """\
664 import os.path
665 import re
666 import sys
667 import string
668 output_arg = 0
669 impl_arg = 0
670 impl = None
671 source = None
672 for arg in sys.argv[1:]:
673     if output_arg:
674         output = open(arg, 'wb')
675         output_arg = 0
676     elif impl_arg:
677         impl = arg
678         impl_arg = 0
679     elif arg == "-o":
680         output_arg = 1
681     elif arg == "-impl":
682         impl_arg = 1
683     else:
684         if source:
685             sys.exit(1)
686         source = open(arg, 'rb')
687         sourceFile = arg
688 if impl:
689     output.write( '#include "' + impl + '"\\n' )
690     includes = re.findall('<include.*?>(.*?)</include>', source.read())
691     for incFile in includes:
692         # this is valid for ui.h files, at least
693         if os.path.exists(incFile):
694             output.write('#include "' + incFile + '"\\n')
695 else:
696     output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
697 output.close()
698 sys.exit(0)
699 """ )
700
701         self.write([dir, 'include', 'my_qobject.h'], r"""
702 #define Q_OBJECT ;
703 void my_qt_symbol(const char *arg);
704 """)
705
706         self.write([dir, 'lib', 'my_qobject.cpp'], r"""
707 #include "../include/my_qobject.h"
708 #include <stdio.h>
709 void my_qt_symbol(const char *arg) {
710   printf( arg );
711 }
712 """)
713
714         self.write([dir, 'lib', 'SConstruct'], r"""
715 env = Environment()
716 import sys
717 if sys.platform == 'win32':
718     env.StaticLibrary( 'myqt', 'my_qobject.cpp' )
719 else:
720     env.SharedLibrary( 'myqt', 'my_qobject.cpp' )
721 """)
722
723         self.run(chdir = self.workpath(dir, 'lib'),
724                  arguments = '.',
725                  stderr = noisy_ar,
726                  match = self.match_re_dotall)
727
728         self.QT = self.workpath(dir)
729         self.QT_LIB = 'myqt'
730         self.QT_MOC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'mymoc.py'))
731         self.QT_UIC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'myuic.py'))
732         self.QT_LIB_DIR = self.workpath(dir, 'lib')
733
734     def Qt_create_SConstruct(self, place):
735         if type(place) is type([]):
736             place = apply(test.workpath, place)
737         self.write(place, """\
738 if ARGUMENTS.get('noqtdir', 0): QTDIR=None
739 else: QTDIR=r'%s'
740 env = Environment(QTDIR = QTDIR,
741                   QT_LIB = r'%s',
742                   QT_MOC = r'%s',
743                   QT_UIC = r'%s',
744                   tools=['default','qt'])
745 dup = 1
746 if ARGUMENTS.get('variant_dir', 0):
747     if ARGUMENTS.get('chdir', 0):
748         SConscriptChdir(1)
749     else:
750         SConscriptChdir(0)
751     dup=int(ARGUMENTS.get('dup', 1))
752     if dup == 0:
753         builddir = 'build_dup0'
754         env['QT_DEBUG'] = 1
755     else:
756         builddir = 'build'
757     VariantDir(builddir, '.', duplicate=dup)
758     print builddir, dup
759     sconscript = Dir(builddir).File('SConscript')
760 else:
761     sconscript = File('SConscript')
762 Export("env dup")
763 SConscript( sconscript )
764 """ % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
765
766
767     NCR = 0 # non-cached rebuild
768     CR  = 1 # cached rebuild (up to date)
769     NCF = 2 # non-cached build failure
770     CF  = 3 # cached build failure
771
772     if sys.platform == 'win32':
773         Configure_lib = 'msvcrt'
774     else:
775         Configure_lib = 'm'
776
777     # to use cygwin compilers on cmd.exe -> uncomment following line
778     #Configure_lib = 'm'
779
780     def checkLogAndStdout(self, checks, results, cached,
781                           logfile, sconf_dir, sconstruct,
782                           doCheckLog=1, doCheckStdout=1):
783
784         class NoMatch:
785             def __init__(self, p):
786                 self.pos = p
787
788         def matchPart(log, logfile, lastEnd, NoMatch=NoMatch):
789             m = re.match(log, logfile[lastEnd:])
790             if not m:
791                 raise NoMatch, lastEnd
792             return m.end() + lastEnd
793         try:
794             #print len(os.linesep)
795             ls = os.linesep
796             nols = "("
797             for i in range(len(ls)):
798                 nols = nols + "("
799                 for j in range(i):
800                     nols = nols + ls[j]
801                 nols = nols + "[^" + ls[i] + "])"
802                 if i < len(ls)-1:
803                     nols = nols + "|"
804             nols = nols + ")"
805             lastEnd = 0
806             logfile = self.read(self.workpath(logfile))
807             if (doCheckLog and
808                 string.find( logfile, "scons: warning: The stored build "
809                              "information has an unexpected class." ) >= 0):
810                 self.fail_test()
811             sconf_dir = sconf_dir
812             sconstruct = sconstruct
813
814             log = r'file\ \S*%s\,line \d+:' % re.escape(sconstruct) + ls
815             if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
816             log = "\t" + re.escape("Configure(confdir = %s)" % sconf_dir) + ls
817             if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
818             rdstr = ""
819             cnt = 0
820             for check,result,cache_desc in zip(checks, results, cached):
821                 log   = re.escape("scons: Configure: " + check) + ls
822                 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
823                 log = ""
824                 result_cached = 1
825                 for bld_desc in cache_desc: # each TryXXX
826                     for ext, flag in bld_desc: # each file in TryBuild
827                         file = os.path.join(sconf_dir,"conftest_%d%s" % (cnt, ext))
828                         if flag == self.NCR:
829                             # rebuild will pass
830                             if ext in ['.c', '.cpp']:
831                                 log=log + re.escape(file + " <-") + ls
832                                 log=log + r"(  \|" + nols + "*" + ls + ")+?"
833                             else:
834                                 log=log + "(" + nols + "*" + ls +")*?"
835                             result_cached = 0
836                         if flag == self.CR:
837                             # up to date
838                             log=log + \
839                                  re.escape("scons: Configure: \"%s\" is up to date." 
840                                            % file) + ls
841                             log=log+re.escape("scons: Configure: The original builder "
842                                               "output was:") + ls
843                             log=log+r"(  \|.*"+ls+")+"
844                         if flag == self.NCF:
845                             # non-cached rebuild failure
846                             log=log + "(" + nols + "*" + ls + ")*?"
847                             result_cached = 0
848                         if flag == self.CF:
849                             # cached rebuild failure
850                             log=log + \
851                                  re.escape("scons: Configure: Building \"%s\" failed "
852                                            "in a previous run and all its sources are"
853                                            " up to date." % file) + ls
854                             log=log+re.escape("scons: Configure: The original builder "
855                                               "output was:") + ls
856                             log=log+r"(  \|.*"+ls+")+"
857                     cnt = cnt + 1
858                 if result_cached:
859                     result = "(cached) " + result
860                 rdstr = rdstr + re.escape(check) + re.escape(result) + "\n"
861                 log=log + re.escape("scons: Configure: " + result) + ls + ls
862                 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
863                 log = ""
864             if doCheckLog: lastEnd = matchPart(ls, logfile, lastEnd)
865             if doCheckLog and lastEnd != len(logfile):
866                 raise NoMatch, lastEnd
867             
868         except NoMatch, m:
869             print "Cannot match log file against log regexp."
870             print "log file: "
871             print "------------------------------------------------------"
872             print logfile[m.pos:]
873             print "------------------------------------------------------"
874             print "log regexp: "
875             print "------------------------------------------------------"
876             print log
877             print "------------------------------------------------------"
878             self.fail_test()
879
880         if doCheckStdout:
881             exp_stdout = self.wrap_stdout(".*", rdstr)
882             if not self.match_re_dotall(self.stdout(), exp_stdout):
883                 print "Unexpected stdout: "
884                 print "-----------------------------------------------------"
885                 print repr(self.stdout())
886                 print "-----------------------------------------------------"
887                 print repr(exp_stdout)
888                 print "-----------------------------------------------------"
889                 self.fail_test()
890
891     def get_python_version(self):
892         """
893         Returns the Python version (just so everyone doesn't have to
894         hand-code slicing the right number of characters).
895         """
896         # see also sys.prefix documentation
897         return python_minor_version_string()
898
899     def get_platform_python_info(self):
900         """
901         Returns a path to a Python executable suitable for testing on
902         this platform and its associated include path, library path,
903         and library name.
904         """
905         python = self.where_is('python')
906         if not python:
907             self.skip_test('Can not find installed "python", skipping test.\n')
908
909         self.run(program = python, stdin = """\
910 import os, sys
911 try:
912         py_ver = 'python%d.%d' % sys.version_info[:2]
913 except AttributeError:
914         py_ver = 'python' + sys.version[:3]
915 print os.path.join(sys.prefix, 'include', py_ver)
916 print os.path.join(sys.prefix, 'lib', py_ver, 'config')
917 print py_ver
918 """)
919
920         return [python] + string.split(string.strip(self.stdout()), '\n')
921
922     def wait_for(self, fname, timeout=10.0, popen=None):
923         """
924         Waits for the specified file name to exist.
925         """
926         waited = 0.0
927         while not os.path.exists(fname):
928             if timeout and waited >= timeout:
929                 sys.stderr.write('timed out waiting for %s to exist\n' % fname)
930                 if popen:
931                     popen.stdin.close()
932                     self.status = 1
933                     self.finish(popen)
934                 self.fail_test()
935             time.sleep(1.0)
936             waited = waited + 1.0
937
938     def get_alt_cpp_suffix(self):
939         """
940         Many CXX tests have this same logic.
941         They all needed to determine if the current os supports
942         files with .C and .c as different files or not
943         in which case they are instructed to use .cpp instead of .C
944         """
945         if not case_sensitive_suffixes('.c','.C'):
946             alt_cpp_suffix = '.cpp'
947         else:
948             alt_cpp_suffix = '.C'
949         return alt_cpp_suffix
950
951
952 class Graph:
953     def __init__(self, name, units, expression, important=False):
954         self.name = name
955         self.units = units
956         self.expression = re.compile(expression)
957         self.important = important
958
959 GraphList = [
960     Graph('TimeSCons-elapsed', 'seconds',
961           r'TimeSCons elapsed time:\s+([\d.]+)',
962           important=True),
963
964     Graph('memory-initial', 'bytes',
965           r'Memory before reading SConscript files:\s+(\d+)'),
966     Graph('memory-prebuild', 'bytes',
967           r'Memory before building targets:\s+(\d+)'),
968     Graph('memory-final', 'bytes',
969           r'Memory after building targets:\s+(\d+)'),
970
971     Graph('time-sconscript', 'seconds',
972           r'Total SConscript file execution time:\s+([\d.]+) seconds'),
973     Graph('time-scons', 'seconds',
974           r'Total SCons execution time:\s+([\d.]+) seconds'),
975     Graph('time-commands', 'seconds',
976           r'Total command execution time:\s+([\d.]+) seconds'),
977     Graph('time-total', 'seconds',
978           r'Total build time:\s+([\d.]+) seconds'),
979 ]
980
981
982 class TimeSCons(TestSCons):
983     """Class for timing SCons."""
984     def __init__(self, *args, **kw):
985         """
986         In addition to normal TestSCons.TestSCons intialization,
987         this enables verbose mode (which causes the command lines to
988         be displayed in the output) and copies the contents of the
989         directory containing the executing script to the temporary
990         working directory.
991         """
992         if not kw.has_key('verbose'):
993             kw['verbose'] = True
994         # TODO(1.5)
995         #TestSCons.__init__(self, *args, **kw)
996         apply(TestSCons.__init__, (self,)+args, kw)
997
998         # TODO(sgk):    better way to get the script dir than sys.argv[0]
999         test_dir = os.path.dirname(sys.argv[0])
1000         test_name = os.path.basename(test_dir)
1001
1002         if not os.path.isabs(test_dir):
1003             test_dir = os.path.join(self.orig_cwd, test_dir)
1004         self.copy_timing_configuration(test_dir, self.workpath())
1005
1006     def main(self, *args, **kw):
1007         """
1008         The main entry point for standard execution of timings.
1009
1010         This method run SCons three times:
1011
1012           Once with the --help option, to have it exit after just reading
1013           the configuration.
1014
1015           Once as a full build of all targets.
1016
1017           Once again as a (presumably) null or up-to-date build of
1018           all targets.
1019
1020         The elapsed time to execute each build is printed after
1021         it has finished.
1022         """
1023         # TODO(1.5)
1024         #self.help(*args, **kw)
1025         #self.full(*args, **kw)
1026         #self.null(*args, **kw)
1027         apply(self.help, args, kw)
1028         apply(self.full, args, kw)
1029         apply(self.null, args, kw)
1030
1031     def trace(self, graph, name, value, units):
1032         fmt = "TRACE: graph=%s name=%s value=%s units=%s\n"
1033         sys.stdout.write(fmt % (graph, name, value, units))
1034         sys.stdout.flush()
1035
1036     def report_traces(self, trace, input):
1037         self.trace('TimeSCons-elapsed', trace, self.elapsed_time(), "seconds")
1038         for graph in GraphList:
1039             m = graph.expression.search(input)
1040             if m:
1041                 self.trace(graph.name, trace, m.group(1), graph.units)
1042
1043     def help(self, *args, **kw):
1044         """
1045         Runs scons with the --help option.
1046
1047         This serves as a way to isolate just the amount of time spent
1048         reading up the configuration, since --help exits before any
1049         "real work" is done.
1050         """
1051         kw['options'] = kw.get('options', '') + ' --help'
1052         # TODO(1.5)
1053         #self.run(*args, **kw)
1054         apply(self.run, args, kw)
1055         sys.stdout.write(self.stdout())
1056         self.report_traces('help', self.stdout())
1057
1058     def full(self, *args, **kw):
1059         """
1060         Runs a full build of SCons.
1061         """
1062         # TODO(1.5)
1063         #self.run(*args, **kw)
1064         apply(self.run, args, kw)
1065         sys.stdout.write(self.stdout())
1066         self.report_traces('full', self.stdout())
1067
1068     def null(self, *args, **kw):
1069         """
1070         Runs an up-to-date null build of SCons.
1071         """
1072         # TODO(sgk):  allow the caller to specify the target (argument)
1073         # that must be up-to-date.
1074         # TODO(1.5)
1075         #self.up_to_date(arguments='.', **kw)
1076         kw = kw.copy()
1077         kw['arguments'] = '.'
1078         apply(self.up_to_date, (), kw)
1079         sys.stdout.write(self.stdout())
1080         self.report_traces('null', self.stdout())
1081
1082     def elapsed_time(self):
1083         """
1084         Returns the elapsed time of the most recent command execution.
1085         """
1086         return self.endTime - self.startTime
1087
1088     def run(self, *args, **kw):
1089         """
1090         Runs a single build command, capturing output in the specified file.
1091
1092         Because this class is about timing SCons, we record the start
1093         and end times of the elapsed execution, and also add the
1094         --debug=memory and --debug=time options to have SCons report
1095         its own memory and timing statistics.
1096         """
1097         kw['options'] = kw.get('options', '') + ' --debug=memory --debug=time'
1098         self.startTime = time.time()
1099         try:
1100             # TODO(1.5)
1101             #result = TestSCons.run(self, *args, **kw)
1102             result = apply(TestSCons.run, (self,)+args, kw)
1103         finally:
1104             self.endTime = time.time()
1105         return result
1106
1107     def copy_timing_configuration(self, source_dir, dest_dir):
1108         """
1109         Copies the timing configuration from the specified source_dir (the
1110         directory in which the controlling script lives) to the specified
1111         dest_dir (a temporary working directory).
1112
1113         This ignores all files and directories that begin with the string
1114         'TimeSCons-', and all '.svn' subdirectories.
1115         """
1116         for root, dirs, files in os.walk(source_dir):
1117             if '.svn' in dirs:
1118                 dirs.remove('.svn')
1119             # TODO(1.5)
1120             #dirs = [ d for d in dirs if not d.startswith('TimeSCons-') ]
1121             #files = [ f for f in files if not f.startswith('TimeSCons-') ]
1122             not_timescons_entries = lambda s: not s.startswith('TimeSCons-')
1123             dirs = filter(not_timescons_entries, dirs)
1124             files = filter(not_timescons_entries, files)
1125             for dirname in dirs:
1126                 source = os.path.join(root, dirname)
1127                 destination = source.replace(source_dir, dest_dir)
1128                 os.mkdir(destination)
1129                 if sys.platform != 'win32':
1130                     shutil.copystat(source, destination)
1131             for filename in files:
1132                 source = os.path.join(root, filename)
1133                 destination = source.replace(source_dir, dest_dir)
1134                 shutil.copy2(source, destination)
1135     
1136
1137 # In some environments, $AR will generate a warning message to stderr
1138 # if the library doesn't previously exist and is being created.  One
1139 # way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
1140 # but this is difficult to do in a platform-/implementation-specific
1141 # method.  Instead, we will use the following as a stderr match for
1142 # tests that use AR so that we will view zero or more "ar: creating
1143 # <file>" messages to be successful executions of the test (see
1144 # test/AR.py for sample usage).
1145
1146 noisy_ar=r'(ar: creating( archive)? \S+\n?)*'
1147
1148 # Local Variables:
1149 # tab-width:4
1150 # indent-tabs-mode:nil
1151 # End:
1152 # vim: set expandtab tabstop=4 shiftwidth=4: