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