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