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