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