http://scons.tigris.org/issues/show_bug.cgi?id=2329
[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 from __future__ import generators  ### KEEP FOR COMPATIBILITY FIXERS
17
18 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
19
20 import os
21 import re
22 import shutil
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([l[i] for l in 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.3.0'
54
55 copyright_years = '2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010'
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 = l.split()
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 sys.platform.find('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 = str.replace(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 = sys.version.split()[0]
174     version_ints = list(map(int, version_string.split('.')))
175     sys.version_info = tuple(version_ints + ['final', 0])
176
177 def python_version_string():
178     return sys.version.split()[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 'program' not in kw:
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 'interpreter' not in kw and not os.environ.get('SCONS_EXEC'):
247             kw['interpreter'] = [python, '-tt']
248         if 'match' not in kw:
249             kw['match'] = match_exact
250         if 'workdir' not in kw:
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'] = ' '.join(sconsflags)
270
271         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 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 = result.replace(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'] = ' '.join(sconsflags)
380         try:
381             result = 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 arguments.split():
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         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 arguments.split():
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'] = stdout.replace('ARGUMENTSGOHERE', s)
414         kw['match'] = self.match_re_dotall
415         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 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 = x.lstrip()
465         x = x.replace('<string>', file)
466         x = x.replace('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 = s.find(begin_marker, 0)
490             while b != -1:
491                 b = b + len(begin_marker)
492                 e = s.find(end_marker, b)
493                 encoded.append((b, e))
494                 b = s.find(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 = ''.join(r)
511
512         return s
513
514     def paths(self,patterns):
515         import glob
516         result = []
517         for p in patterns:
518             result.extend(sorted(glob.glob(p)))
519         return result
520
521
522     def java_ENV(self, version=None):
523         """
524         Initialize with a default external environment that uses a local
525         Java SDK in preference to whatever's found in the default PATH.
526         """
527         try:
528             return self._java_env[version]['ENV']
529         except AttributeError:
530             self._java_env = {}
531         except KeyError:
532             pass
533
534         import SCons.Environment
535         env = SCons.Environment.Environment()
536         self._java_env[version] = env
537
538
539         if version:
540             patterns = [
541                 '/usr/java/jdk%s*/bin'    % version,
542                 '/usr/lib/jvm/*-%s*/bin' % version,
543                 '/usr/local/j2sdk%s*/bin' % version,
544             ]
545             java_path = self.paths(patterns) + [env['ENV']['PATH']]
546         else:
547             patterns = [
548                 '/usr/java/latest/bin',
549                 '/usr/lib/jvm/*/bin',
550                 '/usr/local/j2sdk*/bin',
551             ]
552             java_path = self.paths(patterns) + [env['ENV']['PATH']]
553
554         env['ENV']['PATH'] = os.pathsep.join(java_path)
555         return env['ENV']
556
557     def java_where_includes(self,version=None):
558         """
559         Return java include paths compiling java jni code
560         """
561         import glob
562         import sys
563         if not version:
564             version=''
565             frame = '/System/Library/Frameworks/JavaVM.framework/Headers/jni.h'
566         else:
567             frame = '/System/Library/Frameworks/JavaVM.framework/Versions/%s*/Headers/jni.h'%version
568         jni_dirs = ['/usr/lib/jvm/java-*-sun-%s*/include/jni.h'%version,
569                     '/usr/java/jdk%s*/include/jni.h'%version,
570                     frame,
571                     ]
572         dirs = self.paths(jni_dirs)
573         if not dirs:
574             return None
575         d=os.path.dirname(self.paths(jni_dirs)[0])
576         result=[d]
577
578         if sys.platform == 'win32':
579             result.append(os.path.join(d,'win32'))
580         elif sys.platform == 'linux2':
581             result.append(os.path.join(d,'linux'))
582         return result
583
584
585     def java_where_java_home(self,version=None):
586         if sys.platform[:6] == 'darwin':
587             if version is None:
588                 home = '/System/Library/Frameworks/JavaVM.framework/Home'
589             else:
590                 home = '/System/Library/Frameworks/JavaVM.framework/Versions/%s/Home' % version
591         else:
592             jar = self.java_where_jar(version)
593             home = os.path.normpath('%s/..'%jar)
594         if os.path.isdir(home):
595             return home
596         print("Could not determine JAVA_HOME: %s is not a directory" % home)
597         self.fail_test()
598
599     def java_where_jar(self, version=None):
600         ENV = self.java_ENV(version)
601         if self.detect_tool('jar', ENV=ENV):
602             where_jar = self.detect('JAR', 'jar', ENV=ENV)
603         else:
604             where_jar = self.where_is('jar', ENV['PATH'])
605         if not where_jar:
606             self.skip_test("Could not find Java jar, skipping test(s).\n")
607         return where_jar
608
609     def java_where_java(self, version=None):
610         """
611         Return a path to the java executable.
612         """
613         ENV = self.java_ENV(version)
614         where_java = self.where_is('java', ENV['PATH'])
615         if not where_java:
616             self.skip_test("Could not find Java java, skipping test(s).\n")
617         return where_java
618
619     def java_where_javac(self, version=None):
620         """
621         Return a path to the javac compiler.
622         """
623         ENV = self.java_ENV(version)
624         if self.detect_tool('javac'):
625             where_javac = self.detect('JAVAC', 'javac', ENV=ENV)
626         else:
627             where_javac = self.where_is('javac', ENV['PATH'])
628         if not where_javac:
629             self.skip_test("Could not find Java javac, skipping test(s).\n")
630         self.run(program = where_javac,
631                  arguments = '-version',
632                  stderr=None,
633                  status=None)
634         if version:
635             if self.stderr().find('javac %s' % version) == -1:
636                 fmt = "Could not find javac for Java version %s, skipping test(s).\n"
637                 self.skip_test(fmt % version)
638         else:
639             m = re.search(r'javac (\d\.\d)', self.stderr())
640             if m:
641                 version = m.group(1)
642             else:
643                 version = None
644         return where_javac, version
645
646     def java_where_javah(self, version=None):
647         ENV = self.java_ENV(version)
648         if self.detect_tool('javah'):
649             where_javah = self.detect('JAVAH', 'javah', ENV=ENV)
650         else:
651             where_javah = self.where_is('javah', ENV['PATH'])
652         if not where_javah:
653             self.skip_test("Could not find Java javah, skipping test(s).\n")
654         return where_javah
655
656     def java_where_rmic(self, version=None):
657         ENV = self.java_ENV(version)
658         if self.detect_tool('rmic'):
659             where_rmic = self.detect('RMIC', 'rmic', ENV=ENV)
660         else:
661             where_rmic = self.where_is('rmic', ENV['PATH'])
662         if not where_rmic:
663             self.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n")
664         return where_rmic
665
666     def Qt_dummy_installation(self, dir='qt'):
667         # create a dummy qt installation
668
669         self.subdir( dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'] )
670
671         self.write([dir, 'bin', 'mymoc.py'], """\
672 import getopt
673 import sys
674 import re
675 # -w and -z are fake options used in test/QT/QTFLAGS.py
676 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:wz', [])
677 output = None
678 impl = 0
679 opt_string = ''
680 for opt, arg in cmd_opts:
681     if opt == '-o': output = open(arg, 'wb')
682     elif opt == '-i': impl = 1
683     else: opt_string = opt_string + ' ' + opt
684 output.write("/* mymoc.py%s */\\n" % opt_string)
685 for a in args:
686     contents = open(a, 'rb').read()
687     a = a.replace('\\\\', '\\\\\\\\')
688     subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
689     if impl:
690         contents = re.sub( r'#include.*', '', contents )
691     output.write(contents.replace('Q_OBJECT', subst))
692 output.close()
693 sys.exit(0)
694 """)
695
696         self.write([dir, 'bin', 'myuic.py'], """\
697 import os.path
698 import re
699 import sys
700 output_arg = 0
701 impl_arg = 0
702 impl = None
703 source = None
704 opt_string = ''
705 for arg in sys.argv[1:]:
706     if output_arg:
707         output = open(arg, 'wb')
708         output_arg = 0
709     elif impl_arg:
710         impl = arg
711         impl_arg = 0
712     elif arg == "-o":
713         output_arg = 1
714     elif arg == "-impl":
715         impl_arg = 1
716     elif arg[0:1] == "-":
717         opt_string = opt_string + ' ' + arg
718     else:
719         if source:
720             sys.exit(1)
721         source = open(arg, 'rb')
722         sourceFile = arg
723 output.write("/* myuic.py%s */\\n" % opt_string)
724 if impl:
725     output.write( '#include "' + impl + '"\\n' )
726     includes = re.findall('<include.*?>(.*?)</include>', source.read())
727     for incFile in includes:
728         # this is valid for ui.h files, at least
729         if os.path.exists(incFile):
730             output.write('#include "' + incFile + '"\\n')
731 else:
732     output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
733 output.close()
734 sys.exit(0)
735 """ )
736
737         self.write([dir, 'include', 'my_qobject.h'], r"""
738 #define Q_OBJECT ;
739 void my_qt_symbol(const char *arg);
740 """)
741
742         self.write([dir, 'lib', 'my_qobject.cpp'], r"""
743 #include "../include/my_qobject.h"
744 #include <stdio.h>
745 void my_qt_symbol(const char *arg) {
746   fputs( arg, stdout );
747 }
748 """)
749
750         self.write([dir, 'lib', 'SConstruct'], r"""
751 env = Environment()
752 import sys
753 if sys.platform == 'win32':
754     env.StaticLibrary( 'myqt', 'my_qobject.cpp' )
755 else:
756     env.SharedLibrary( 'myqt', 'my_qobject.cpp' )
757 """)
758
759         self.run(chdir = self.workpath(dir, 'lib'),
760                  arguments = '.',
761                  stderr = noisy_ar,
762                  match = self.match_re_dotall)
763
764         self.QT = self.workpath(dir)
765         self.QT_LIB = 'myqt'
766         self.QT_MOC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'mymoc.py'))
767         self.QT_UIC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'myuic.py'))
768         self.QT_LIB_DIR = self.workpath(dir, 'lib')
769
770     def Qt_create_SConstruct(self, place):
771         if isinstance(place, list):
772             place = test.workpath(*place)
773         self.write(place, """\
774 if ARGUMENTS.get('noqtdir', 0): QTDIR=None
775 else: QTDIR=r'%s'
776 env = Environment(QTDIR = QTDIR,
777                   QT_LIB = r'%s',
778                   QT_MOC = r'%s',
779                   QT_UIC = r'%s',
780                   tools=['default','qt'])
781 dup = 1
782 if ARGUMENTS.get('variant_dir', 0):
783     if ARGUMENTS.get('chdir', 0):
784         SConscriptChdir(1)
785     else:
786         SConscriptChdir(0)
787     dup=int(ARGUMENTS.get('dup', 1))
788     if dup == 0:
789         builddir = 'build_dup0'
790         env['QT_DEBUG'] = 1
791     else:
792         builddir = 'build'
793     VariantDir(builddir, '.', duplicate=dup)
794     print builddir, dup
795     sconscript = Dir(builddir).File('SConscript')
796 else:
797     sconscript = File('SConscript')
798 Export("env dup")
799 SConscript( sconscript )
800 """ % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
801
802
803     NCR = 0 # non-cached rebuild
804     CR  = 1 # cached rebuild (up to date)
805     NCF = 2 # non-cached build failure
806     CF  = 3 # cached build failure
807
808     if sys.platform == 'win32':
809         Configure_lib = 'msvcrt'
810     else:
811         Configure_lib = 'm'
812
813     # to use cygwin compilers on cmd.exe -> uncomment following line
814     #Configure_lib = 'm'
815
816     def checkLogAndStdout(self, checks, results, cached,
817                           logfile, sconf_dir, sconstruct,
818                           doCheckLog=1, doCheckStdout=1):
819
820         class NoMatch:
821             def __init__(self, p):
822                 self.pos = p
823
824         def matchPart(log, logfile, lastEnd, NoMatch=NoMatch):
825             m = re.match(log, logfile[lastEnd:])
826             if not m:
827                 raise NoMatch, lastEnd
828             return m.end() + lastEnd
829         try:
830             #print len(os.linesep)
831             ls = os.linesep
832             nols = "("
833             for i in range(len(ls)):
834                 nols = nols + "("
835                 for j in range(i):
836                     nols = nols + ls[j]
837                 nols = nols + "[^" + ls[i] + "])"
838                 if i < len(ls)-1:
839                     nols = nols + "|"
840             nols = nols + ")"
841             lastEnd = 0
842             logfile = self.read(self.workpath(logfile))
843             if (doCheckLog and
844                 logfile.find( "scons: warning: The stored build "
845                              "information has an unexpected class." ) >= 0):
846                 self.fail_test()
847             sconf_dir = sconf_dir
848             sconstruct = sconstruct
849
850             log = r'file\ \S*%s\,line \d+:' % re.escape(sconstruct) + ls
851             if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
852             log = "\t" + re.escape("Configure(confdir = %s)" % sconf_dir) + ls
853             if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
854             rdstr = ""
855             cnt = 0
856             for check,result,cache_desc in zip(checks, results, cached):
857                 log   = re.escape("scons: Configure: " + check) + ls
858                 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
859                 log = ""
860                 result_cached = 1
861                 for bld_desc in cache_desc: # each TryXXX
862                     for ext, flag in bld_desc: # each file in TryBuild
863                         file = os.path.join(sconf_dir,"conftest_%d%s" % (cnt, ext))
864                         if flag == self.NCR:
865                             # rebuild will pass
866                             if ext in ['.c', '.cpp']:
867                                 log=log + re.escape(file + " <-") + ls
868                                 log=log + r"(  \|" + nols + "*" + ls + ")+?"
869                             else:
870                                 log=log + "(" + nols + "*" + ls +")*?"
871                             result_cached = 0
872                         if flag == self.CR:
873                             # up to date
874                             log=log + \
875                                  re.escape("scons: Configure: \"%s\" is up to date." 
876                                            % file) + ls
877                             log=log+re.escape("scons: Configure: The original builder "
878                                               "output was:") + ls
879                             log=log+r"(  \|.*"+ls+")+"
880                         if flag == self.NCF:
881                             # non-cached rebuild failure
882                             log=log + "(" + nols + "*" + ls + ")*?"
883                             result_cached = 0
884                         if flag == self.CF:
885                             # cached rebuild failure
886                             log=log + \
887                                  re.escape("scons: Configure: Building \"%s\" failed "
888                                            "in a previous run and all its sources are"
889                                            " up to date." % file) + ls
890                             log=log+re.escape("scons: Configure: The original builder "
891                                               "output was:") + ls
892                             log=log+r"(  \|.*"+ls+")+"
893                     cnt = cnt + 1
894                 if result_cached:
895                     result = "(cached) " + result
896                 rdstr = rdstr + re.escape(check) + re.escape(result) + "\n"
897                 log=log + re.escape("scons: Configure: " + result) + ls + ls
898                 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
899                 log = ""
900             if doCheckLog: lastEnd = matchPart(ls, logfile, lastEnd)
901             if doCheckLog and lastEnd != len(logfile):
902                 raise NoMatch, lastEnd
903             
904         except NoMatch, m:
905             print "Cannot match log file against log regexp."
906             print "log file: "
907             print "------------------------------------------------------"
908             print logfile[m.pos:]
909             print "------------------------------------------------------"
910             print "log regexp: "
911             print "------------------------------------------------------"
912             print log
913             print "------------------------------------------------------"
914             self.fail_test()
915
916         if doCheckStdout:
917             exp_stdout = self.wrap_stdout(".*", rdstr)
918             if not self.match_re_dotall(self.stdout(), exp_stdout):
919                 print "Unexpected stdout: "
920                 print "-----------------------------------------------------"
921                 print repr(self.stdout())
922                 print "-----------------------------------------------------"
923                 print repr(exp_stdout)
924                 print "-----------------------------------------------------"
925                 self.fail_test()
926
927     def get_python_version(self):
928         """
929         Returns the Python version (just so everyone doesn't have to
930         hand-code slicing the right number of characters).
931         """
932         # see also sys.prefix documentation
933         return python_minor_version_string()
934
935     def get_platform_python_info(self):
936         """
937         Returns a path to a Python executable suitable for testing on
938         this platform and its associated include path, library path,
939         and library name.
940         """
941         python = self.where_is('python')
942         if not python:
943             self.skip_test('Can not find installed "python", skipping test.\n')
944
945         self.run(program = python, stdin = """\
946 import os, sys
947 try:
948         py_ver = 'python%d.%d' % sys.version_info[:2]
949 except AttributeError:
950         py_ver = 'python' + sys.version[:3]
951 print os.path.join(sys.prefix, 'include', py_ver)
952 print os.path.join(sys.prefix, 'lib', py_ver, 'config')
953 print py_ver
954 """)
955
956         return [python] + self.stdout().strip().split('\n')
957
958     def start(self, *args, **kw):
959         """
960         Starts SCons in the test environment.
961
962         This method exists to tell Test{Cmd,Common} that we're going to
963         use standard input without forcing every .start() call in the
964         individual tests to do so explicitly.
965         """
966         if 'stdin' not in kw:
967             kw['stdin'] = True
968         return TestCommon.start(self, *args, **kw)
969
970     def wait_for(self, fname, timeout=10.0, popen=None):
971         """
972         Waits for the specified file name to exist.
973         """
974         waited = 0.0
975         while not os.path.exists(fname):
976             if timeout and waited >= timeout:
977                 sys.stderr.write('timed out waiting for %s to exist\n' % fname)
978                 if popen:
979                     popen.stdin.close()
980                     self.status = 1
981                     self.finish(popen)
982                 self.fail_test()
983             time.sleep(1.0)
984             waited = waited + 1.0
985
986     def get_alt_cpp_suffix(self):
987         """
988         Many CXX tests have this same logic.
989         They all needed to determine if the current os supports
990         files with .C and .c as different files or not
991         in which case they are instructed to use .cpp instead of .C
992         """
993         if not case_sensitive_suffixes('.c','.C'):
994             alt_cpp_suffix = '.cpp'
995         else:
996             alt_cpp_suffix = '.C'
997         return alt_cpp_suffix
998
999
1000 class Stat:
1001     def __init__(self, name, units, expression, convert=None):
1002         if convert is None:
1003             convert = lambda x: x
1004         self.name = name
1005         self.units = units
1006         self.expression = re.compile(expression)
1007         self.convert = convert
1008
1009 StatList = [
1010     Stat('memory-initial', 'kbytes',
1011          r'Memory before reading SConscript files:\s+(\d+)',
1012          convert=lambda s: int(s) / 1024),
1013     Stat('memory-prebuild', 'kbytes',
1014          r'Memory before building targets:\s+(\d+)',
1015          convert=lambda s: int(s) / 1024),
1016     Stat('memory-final', 'kbytes',
1017          r'Memory after building targets:\s+(\d+)',
1018          convert=lambda s: int(s) / 1024),
1019
1020     Stat('time-sconscript', 'seconds',
1021          r'Total SConscript file execution time:\s+([\d.]+) seconds'),
1022     Stat('time-scons', 'seconds',
1023          r'Total SCons execution time:\s+([\d.]+) seconds'),
1024     Stat('time-commands', 'seconds',
1025          r'Total command execution time:\s+([\d.]+) seconds'),
1026     Stat('time-total', 'seconds',
1027          r'Total build time:\s+([\d.]+) seconds'),
1028 ]
1029
1030
1031 class TimeSCons(TestSCons):
1032     """Class for timing SCons."""
1033     def __init__(self, *args, **kw):
1034         """
1035         In addition to normal TestSCons.TestSCons intialization,
1036         this enables verbose mode (which causes the command lines to
1037         be displayed in the output) and copies the contents of the
1038         directory containing the executing script to the temporary
1039         working directory.
1040         """
1041         self.variables = kw.get('variables')
1042         if self.variables is not None:
1043             for variable, value in self.variables.items():
1044                 value = os.environ.get(variable, value)
1045                 try:
1046                     value = int(value)
1047                 except ValueError:
1048                     try:
1049                         value = float(value)
1050                     except ValueError:
1051                         pass
1052                 self.variables[variable] = value
1053             del kw['variables']
1054
1055         self.calibrate = os.environ.get('TIMESCONS_CALIBRATE', '0') != '0'
1056
1057         if 'verbose' not in kw and not self.calibrate:
1058             kw['verbose'] = True
1059
1060         # TODO(1.5)
1061         #TestSCons.__init__(self, *args, **kw)
1062         TestSCons.__init__(self, *args, **kw)
1063
1064         # TODO(sgk):    better way to get the script dir than sys.argv[0]
1065         test_dir = os.path.dirname(sys.argv[0])
1066         test_name = os.path.basename(test_dir)
1067
1068         if not os.path.isabs(test_dir):
1069             test_dir = os.path.join(self.orig_cwd, test_dir)
1070         self.copy_timing_configuration(test_dir, self.workpath())
1071
1072     def main(self, *args, **kw):
1073         """
1074         The main entry point for standard execution of timings.
1075
1076         This method run SCons three times:
1077
1078           Once with the --help option, to have it exit after just reading
1079           the configuration.
1080
1081           Once as a full build of all targets.
1082
1083           Once again as a (presumably) null or up-to-date build of
1084           all targets.
1085
1086         The elapsed time to execute each build is printed after
1087         it has finished.
1088         """
1089         if 'options' not in kw and self.variables:
1090             options = []
1091             for variable, value in self.variables.items():
1092                 options.append('%s=%s' % (variable, value))
1093             kw['options'] = ' '.join(options)
1094         if self.calibrate:
1095             # TODO(1.5)
1096             #self.calibration(*args, **kw)
1097             self.calibration(*args, **kw)
1098         else:
1099             self.uptime()
1100             # TODO(1.5)
1101             #self.startup(*args, **kw)
1102             #self.full(*args, **kw)
1103             #self.null(*args, **kw)
1104             self.startup(*args, **kw)
1105             self.full(*args, **kw)
1106             self.null(*args, **kw)
1107
1108     def trace(self, graph, name, value, units, sort=None):
1109         fmt = "TRACE: graph=%s name=%s value=%s units=%s"
1110         line = fmt % (graph, name, value, units)
1111         if sort is not None:
1112           line = line + (' sort=%s' % sort)
1113         line = line + '\n'
1114         sys.stdout.write(line)
1115         sys.stdout.flush()
1116
1117     def report_traces(self, trace, stats):
1118         self.trace('TimeSCons-elapsed',
1119                    trace,
1120                    self.elapsed_time(),
1121                    "seconds",
1122                    sort=0)
1123         for name, args in stats.items():
1124             # TODO(1.5)
1125             #self.trace(name, trace, *args)
1126             self.trace(name, trace, **args)
1127
1128     def uptime(self):
1129         try:
1130             fp = open('/proc/loadavg')
1131         except EnvironmentError:
1132             pass
1133         else:
1134             avg1, avg5, avg15 = fp.readline().split(" ")[:3]
1135             fp.close()
1136             self.trace('load-average',  'average1', avg1, 'processes')
1137             self.trace('load-average',  'average5', avg5, 'processes')
1138             self.trace('load-average',  'average15', avg15, 'processes')
1139
1140     def collect_stats(self, input):
1141         result = {}
1142         for stat in StatList:
1143             m = stat.expression.search(input)
1144             if m:
1145                 value = stat.convert(m.group(1))
1146                 # The dict keys match the keyword= arguments
1147                 # of the trace() method above so they can be
1148                 # applied directly to that call.
1149                 result[stat.name] = {'value':value, 'units':stat.units}
1150         return result
1151
1152     def startup(self, *args, **kw):
1153         """
1154         Runs scons with the --help option.
1155
1156         This serves as a way to isolate just the amount of startup time
1157         spent reading up the configuration, since --help exits before any
1158         "real work" is done.
1159         """
1160         kw['options'] = kw.get('options', '') + ' --help'
1161         # Ignore the exit status.  If the --help run dies, we just
1162         # won't report any statistics for it, but we can still execute
1163         # the full and null builds.
1164         kw['status'] = None
1165         # TODO(1.5)
1166         #self.run(*args, **kw)
1167         self.run(*args, **kw)
1168         sys.stdout.write(self.stdout())
1169         stats = self.collect_stats(self.stdout())
1170         # Delete the time-commands, since no commands are ever
1171         # executed on the help run and it is (or should be) always 0.0.
1172         del stats['time-commands']
1173         self.report_traces('startup', stats)
1174
1175     def full(self, *args, **kw):
1176         """
1177         Runs a full build of SCons.
1178         """
1179         # TODO(1.5)
1180         #self.run(*args, **kw)
1181         self.run(*args, **kw)
1182         sys.stdout.write(self.stdout())
1183         stats = self.collect_stats(self.stdout())
1184         self.report_traces('full', stats)
1185         # TODO(1.5)
1186         #self.trace('full-memory', 'initial', **stats['memory-initial'])
1187         #self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
1188         #self.trace('full-memory', 'final', **stats['memory-final'])
1189         self.trace('full-memory', 'initial', **stats['memory-initial'])
1190         self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
1191         self.trace('full-memory', 'final', **stats['memory-final'])
1192
1193     def calibration(self, *args, **kw):
1194         """
1195         Runs a full build of SCons, but only reports calibration
1196         information (the variable(s) that were set for this configuration,
1197         and the elapsed time to run.
1198         """
1199         # TODO(1.5)
1200         #self.run(*args, **kw)
1201         self.run(*args, **kw)
1202         if self.variables:
1203             for variable, value in self.variables.items():
1204                 sys.stdout.write('VARIABLE: %s=%s\n' % (variable, value))
1205         sys.stdout.write('ELAPSED: %s\n' % self.elapsed_time())
1206
1207     def null(self, *args, **kw):
1208         """
1209         Runs an up-to-date null build of SCons.
1210         """
1211         # TODO(sgk):  allow the caller to specify the target (argument)
1212         # that must be up-to-date.
1213         # TODO(1.5)
1214         #self.up_to_date(arguments='.', **kw)
1215         kw = kw.copy()
1216         kw['arguments'] = '.'
1217         self.up_to_date(**kw)
1218         sys.stdout.write(self.stdout())
1219         stats = self.collect_stats(self.stdout())
1220         # time-commands should always be 0.0 on a null build, because
1221         # no commands should be executed.  Remove it from the stats
1222         # so we don't trace it, but only if it *is* 0 so that we'll
1223         # get some indication if a supposedly-null build actually does
1224         # build something.
1225         if float(stats['time-commands']['value']) == 0.0:
1226             del stats['time-commands']
1227         self.report_traces('null', stats)
1228         # TODO(1.5)
1229         #self.trace('null-memory', 'initial', **stats['memory-initial'])
1230         #self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
1231         #self.trace('null-memory', 'final', **stats['memory-final'])
1232         self.trace('null-memory', 'initial', **stats['memory-initial'])
1233         self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
1234         self.trace('null-memory', 'final', **stats['memory-final'])
1235
1236     def elapsed_time(self):
1237         """
1238         Returns the elapsed time of the most recent command execution.
1239         """
1240         return self.endTime - self.startTime
1241
1242     def run(self, *args, **kw):
1243         """
1244         Runs a single build command, capturing output in the specified file.
1245
1246         Because this class is about timing SCons, we record the start
1247         and end times of the elapsed execution, and also add the
1248         --debug=memory and --debug=time options to have SCons report
1249         its own memory and timing statistics.
1250         """
1251         kw['options'] = kw.get('options', '') + ' --debug=memory --debug=time'
1252         self.startTime = time.time()
1253         try:
1254             # TODO(1.5)
1255             #result = TestSCons.run(self, *args, **kw)
1256             result = TestSCons.run(self, *args, **kw)
1257         finally:
1258             self.endTime = time.time()
1259         return result
1260
1261     def copy_timing_configuration(self, source_dir, dest_dir):
1262         """
1263         Copies the timing configuration from the specified source_dir (the
1264         directory in which the controlling script lives) to the specified
1265         dest_dir (a temporary working directory).
1266
1267         This ignores all files and directories that begin with the string
1268         'TimeSCons-', and all '.svn' subdirectories.
1269         """
1270         for root, dirs, files in os.walk(source_dir):
1271             if '.svn' in dirs:
1272                 dirs.remove('.svn')
1273             # TODO(1.5)
1274             #dirs = [ d for d in dirs if not d.startswith('TimeSCons-') ]
1275             #files = [ f for f in files if not f.startswith('TimeSCons-') ]
1276             not_timescons_entries = lambda s: not s.startswith('TimeSCons-')
1277             dirs = list(filter(not_timescons_entries, dirs))
1278             files = list(filter(not_timescons_entries, files))
1279             for dirname in dirs:
1280                 source = os.path.join(root, dirname)
1281                 destination = source.replace(source_dir, dest_dir)
1282                 os.mkdir(destination)
1283                 if sys.platform != 'win32':
1284                     shutil.copystat(source, destination)
1285             for filename in files:
1286                 source = os.path.join(root, filename)
1287                 destination = source.replace(source_dir, dest_dir)
1288                 shutil.copy2(source, destination)
1289     
1290
1291 # In some environments, $AR will generate a warning message to stderr
1292 # if the library doesn't previously exist and is being created.  One
1293 # way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
1294 # but this is difficult to do in a platform-/implementation-specific
1295 # method.  Instead, we will use the following as a stderr match for
1296 # tests that use AR so that we will view zero or more "ar: creating
1297 # <file>" messages to be successful executions of the test (see
1298 # test/AR.py for sample usage).
1299
1300 noisy_ar=r'(ar: creating( archive)? \S+\n?)*'
1301
1302 # Local Variables:
1303 # tab-width:4
1304 # indent-tabs-mode:nil
1305 # End:
1306 # vim: set expandtab tabstop=4 shiftwidth=4: