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