Merged revisions 1826-1882 via svnmerge from
[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 os.path
21 import string
22 import sys
23
24 from TestCommon import *
25 from TestCommon import __all__
26
27 # Some tests which verify that SCons has been packaged properly need to
28 # look for specific version file names.  Replicating the version number
29 # here provides independent verification that what we packaged conforms
30 # to what we expect.  (If we derived the version number from the same
31 # data driving the build we might miss errors if the logic breaks.)
32
33 SConsVersion = '0.96.96'
34
35 __all__.extend([ 'TestSCons',
36                  'python',
37                  '_exe',
38                  '_obj',
39                  '_shobj',
40                  'lib_',
41                  '_lib',
42                  'dll_',
43                  '_dll'
44                ])
45
46 python = python_executable
47 _python_ = '"' + python_executable + '"'
48 _exe = exe_suffix
49 _obj = obj_suffix
50 _shobj = shobj_suffix
51 _lib = lib_suffix
52 lib_ = lib_prefix
53 _dll = dll_suffix
54 dll_ = dll_prefix
55
56 def gccFortranLibs():
57     """Test whether -lfrtbegin is required.  This can probably be done in
58     a more reliable way, but using popen3 is relatively efficient."""
59
60     libs = ['g2c']
61
62     try:
63         import popen2
64         stderr = popen2.popen3('gcc -v')[2]
65     except OSError:
66         return libs
67
68     for l in stderr.readlines():
69         list = string.split(l)
70         if len(list) > 3 and list[:2] == ['gcc', 'version']:
71             if list[2][:2] == '3.':
72                 libs = ['frtbegin'] + libs
73                 break
74     return libs
75
76
77 if sys.platform == 'cygwin':
78     # On Cygwin, os.path.normcase() lies, so just report back the
79     # fact that the underlying Win32 OS is case-insensitive.
80     def case_sensitive_suffixes(s1, s2):
81         return 0
82 else:
83     def case_sensitive_suffixes(s1, s2):
84         return (os.path.normcase(s1) != os.path.normcase(s2))
85
86
87 if sys.platform == 'win32':
88     fortran_lib = gccFortranLibs()
89 elif sys.platform == 'cygwin':
90     fortran_lib = gccFortranLibs()
91 elif string.find(sys.platform, 'irix') != -1:
92     fortran_lib = ['ftn']
93 else:
94     fortran_lib = gccFortranLibs()
95
96
97
98 file_expr = r"""File "[^"]*", line \d+, in .+
99 """
100
101 # re.escape escapes too much.
102 def re_escape(str):
103     for c in ['.', '[', ']', '(', ')', '*', '+', '?']:  # Not an exhaustive list.
104         str = string.replace(str, c, '\\' + c)
105     return str
106
107
108
109 class TestSCons(TestCommon):
110     """Class for testing SCons.
111
112     This provides a common place for initializing SCons tests,
113     eliminating the need to begin every test with the same repeated
114     initializations.
115     """
116
117     scons_version = SConsVersion
118
119     def __init__(self, **kw):
120         """Initialize an SCons testing object.
121
122         If they're not overridden by keyword arguments, this
123         initializes the object with the following default values:
124
125                 program = 'scons' if it exists,
126                           else 'scons.py'
127                 interpreter = 'python'
128                 match = match_exact
129                 workdir = ''
130
131         The workdir value means that, by default, a temporary workspace
132         directory is created for a TestSCons environment.  In addition,
133         this method changes directory (chdir) to the workspace directory,
134         so an explicit "chdir = '.'" on all of the run() method calls
135         is not necessary.
136         """
137         self.orig_cwd = os.getcwd()
138         try:
139             script_dir = os.environ['SCONS_SCRIPT_DIR']
140         except KeyError:
141             pass
142         else:
143             os.chdir(script_dir)
144         if not kw.has_key('program'):
145             kw['program'] = os.environ.get('SCONS')
146             if not kw['program']:
147                 if os.path.exists('scons'):
148                     kw['program'] = 'scons'
149                 else:
150                     kw['program'] = 'scons.py'
151         if not kw.has_key('interpreter') and not os.environ.get('SCONS_EXEC'):
152             kw['interpreter'] = [python, '-tt']
153         if not kw.has_key('match'):
154             kw['match'] = match_exact
155         if not kw.has_key('workdir'):
156             kw['workdir'] = ''
157         apply(TestCommon.__init__, [self], kw)
158
159     def Environment(self, ENV=None, *args, **kw):
160         """
161         Return a construction Environment that optionally overrides
162         the default external environment with the specified ENV.
163         """
164         import SCons.Environment
165         import SCons.Errors
166         if not ENV is None:
167             kw['ENV'] = ENV
168         try:
169             return apply(SCons.Environment.Environment, args, kw)
170         except (SCons.Errors.UserError, SCons.Errors.InternalError):
171             return None
172
173     def detect(self, var, prog=None, ENV=None):
174         """
175         Detect a program named 'prog' by first checking the construction
176         variable named 'var' and finally searching the path used by
177         SCons. If either method fails to detect the program, then false
178         is returned, otherwise the full path to prog is returned. If
179         prog is None, then the value of the environment variable will be
180         used as prog.
181         """
182         env = self.Environment(ENV)
183         v = env.subst('$'+var)
184         if not v:
185             return None
186         if prog is None:
187             prog = v
188         if v != prog:
189             return None
190         return env.WhereIs(prog)
191
192     def detect_tool(self, tool, prog=None, ENV=None):
193         """
194         Given a tool (i.e., tool specification that would be passed
195         to the "tools=" parameter of Environment()) and a program that
196         corresponds to that tool, return true if and only if we can find
197         that tool using Environment.Detect().
198
199         By default, prog is set to the value passed into the tools parameter.
200         """
201
202         if not prog:
203             prog = tool
204         env = self.Environment(ENV, tools=[tool])
205         if env is None:
206             return None
207         return env.Detect([prog])
208
209     def where_is(self, prog, path=None):
210         """
211         Given a program, search for it in the specified external PATH,
212         or in the actual external PATH is none is specified.
213         """
214         import SCons.Environment
215         env = SCons.Environment.Environment()
216         if path is None:
217             path = os.environ['PATH']
218         return env.WhereIs(prog, path)
219
220     def wrap_stdout(self, build_str = "", read_str = "", error = 0, cleaning = 0):
221         """Wraps standard output string(s) in the normal
222         "Reading ... done" and "Building ... done" strings
223         """
224         cap,lc = [ ('Build','build'),
225                    ('Clean','clean') ][cleaning]
226         if error:
227             term = "scons: %sing terminated because of errors.\n" % lc
228         else:
229             term = "scons: done %sing targets.\n" % lc
230         return "scons: Reading SConscript files ...\n" + \
231                read_str + \
232                "scons: done reading SConscript files.\n" + \
233                "scons: %sing targets ...\n" % cap + \
234                build_str + \
235                term
236
237     def up_to_date(self, options = None, arguments = None, read_str = "", **kw):
238         s = ""
239         for arg in string.split(arguments):
240             s = s + "scons: `%s' is up to date.\n" % arg
241             if options:
242                 arguments = options + " " + arguments
243         kw['arguments'] = arguments
244         kw['stdout'] = self.wrap_stdout(read_str = read_str, build_str = s)
245         kw['match'] = self.match_exact
246         apply(self.run, [], kw)
247
248     def not_up_to_date(self, options = None, arguments = None, **kw):
249         """Asserts that none of the targets listed in arguments is
250         up to date, but does not make any assumptions on other targets.
251         This function is most useful in conjunction with the -n option.
252         """
253         s = ""
254         for  arg in string.split(arguments):
255             s = s + "(?!scons: `%s' is up to date.)" % arg
256             if options:
257                 arguments = options + " " + arguments
258         kw['arguments'] = arguments
259         kw['stdout'] = self.wrap_stdout(build_str="("+s+"[^\n]*\n)*")
260         kw['stdout'] = string.replace(kw['stdout'],'\n','\\n')
261         kw['stdout'] = string.replace(kw['stdout'],'.','\\.')
262         kw['match'] = self.match_re_dotall
263         apply(self.run, [], kw)
264
265     def skip_test(self, message="Skipping test.\n"):
266         """Skips a test.
267
268         Proper test-skipping behavior is dependent on whether we're being
269         executed as part of development of a change under Aegis.
270
271         Technically, skipping a test is a NO RESULT, but Aegis will
272         treat that as a test failure and prevent the change from going
273         to the next step.  We don't want to force anyone using Aegis
274         to have to install absolutely every tool used by the tests,
275         so we actually report to Aegis that a skipped test has PASSED
276         so that the workflow isn't held up.
277         """
278         if message:
279             sys.stdout.write(message)
280             sys.stdout.flush()
281         devdir = os.popen("aesub '$dd' 2>/dev/null", "r").read()[:-1]
282         intdir = os.popen("aesub '$intd' 2>/dev/null", "r").read()[:-1]
283         if devdir and self._cwd[:len(devdir)] == devdir or \
284            intdir and self._cwd[:len(intdir)] == intdir:
285             # We're under the development directory for this change,
286             # so this is an Aegis invocation; pass the test (exit 0).
287             self.pass_test()
288         else:
289             # skip=1 means skip this function when showing where this
290             # result came from.  They only care about the line where the
291             # script called test.skip_test(), not the line number where
292             # we call test.no_result().
293             self.no_result(skip=1)
294
295     def diff_substr(self, expect, actual):
296         i = 0
297         for x, y in zip(expect, actual):
298             if x != y:
299                 return "Actual did not match expect at char %d:\n" \
300                        "    Expect:  %s\n" \
301                        "    Actual:  %s\n" \
302                        % (i, repr(expect[i-20:i+40]), repr(actual[i-20:i+40]))
303             i = i + 1
304         return "Actual matched the expected output???"
305
306     def python_file_line(self, file, line):
307         """
308         Returns a Python error line for output comparisons.
309
310         The exec of the traceback line gives us the correct format for
311         this version of Python.  Before 2.5, this yielded:
312
313             File "<string>", line 1, ?
314
315         Python 2.5 changed this to:
316
317             File "<string>", line 1, <module>
318
319         We stick the requested file name and line number in the right
320         places, abstracting out the version difference.
321         """
322         exec 'import traceback; x = traceback.format_stack()[-1]'
323         x = string.lstrip(x)
324         x = string.replace(x, '<string>', file)
325         x = string.replace(x, 'line 1,', 'line %s,' % line)
326         return x
327
328     def java_ENV(self):
329         """
330         Return a default external environment that uses a local Java SDK
331         in preference to whatever's found in the default PATH.
332         """
333         import SCons.Environment
334         env = SCons.Environment.Environment()
335         java_path = [
336             '/usr/local/j2sdk1.4.2/bin',
337             '/usr/local/j2sdk1.4.1/bin',
338             '/usr/local/j2sdk1.3.1/bin',
339             '/usr/local/j2sdk1.3.0/bin',
340             '/usr/local/j2sdk1.2.2/bin',
341             '/usr/local/j2sdk1.2/bin',
342             '/usr/local/j2sdk1.1.8/bin',
343             '/usr/local/j2sdk1.1.7/bin',
344             '/usr/local/j2sdk1.1.6/bin',
345             '/usr/local/j2sdk1.1.5/bin',
346             '/usr/local/j2sdk1.1.4/bin',
347             '/usr/local/j2sdk1.1.3/bin',
348             '/usr/local/j2sdk1.1.2/bin',
349             '/usr/local/j2sdk1.1.1/bin',
350             env['ENV']['PATH'],
351         ]
352         env['ENV']['PATH'] = string.join(java_path, os.pathsep)
353         return env['ENV']
354
355     def Qt_dummy_installation(self, dir='qt'):
356         # create a dummy qt installation
357
358         self.subdir( dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'] )
359
360         self.write([dir, 'bin', 'mymoc.py'], """\
361 import getopt
362 import sys
363 import string
364 import re
365 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:', [])
366 output = None
367 impl = 0
368 opt_string = ''
369 for opt, arg in cmd_opts:
370     if opt == '-o': output = open(arg, 'wb')
371     elif opt == '-i': impl = 1
372     else: opt_string = opt_string + ' ' + opt
373 for a in args:
374     contents = open(a, 'rb').read()
375     subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
376     if impl:
377         contents = re.sub( r'#include.*', '', contents )
378     output.write(string.replace(contents, 'Q_OBJECT', subst))
379 output.close()
380 sys.exit(0)
381 """)
382
383         self.write([dir, 'bin', 'myuic.py'], """\
384 import os.path
385 import re
386 import sys
387 import string
388 output_arg = 0
389 impl_arg = 0
390 impl = None
391 source = None
392 for arg in sys.argv[1:]:
393     if output_arg:
394         output = open(arg, 'wb')
395         output_arg = 0
396     elif impl_arg:
397         impl = arg
398         impl_arg = 0
399     elif arg == "-o":
400         output_arg = 1
401     elif arg == "-impl":
402         impl_arg = 1
403     else:
404         if source:
405             sys.exit(1)
406         source = open(arg, 'rb')
407         sourceFile = arg
408 if impl:
409     output.write( '#include "' + impl + '"\\n' )
410     includes = re.findall('<include.*?>(.*?)</include>', source.read())
411     for incFile in includes:
412         # this is valid for ui.h files, at least
413         if os.path.exists(incFile):
414             output.write('#include "' + incFile + '"\\n')
415 else:
416     output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
417 output.close()
418 sys.exit(0)
419 """ )
420
421         self.write([dir, 'include', 'my_qobject.h'], r"""
422 #define Q_OBJECT ;
423 void my_qt_symbol(const char *arg);
424 """)
425
426         self.write([dir, 'lib', 'my_qobject.cpp'], r"""
427 #include "../include/my_qobject.h"
428 #include <stdio.h>
429 void my_qt_symbol(const char *arg) {
430   printf( arg );
431 }
432 """)
433
434         self.write([dir, 'lib', 'SConstruct'], r"""
435 env = Environment()
436 env.SharedLibrary( 'myqt', 'my_qobject.cpp' )
437 """)
438
439         self.run(chdir = self.workpath(dir, 'lib'),
440                  arguments = '.',
441                  stderr = noisy_ar,
442                  match = self.match_re_dotall)
443
444         self.QT = self.workpath(dir)
445         self.QT_LIB = 'myqt'
446         self.QT_MOC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'mymoc.py'))
447         self.QT_UIC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'myuic.py'))
448         self.QT_LIB_DIR = self.workpath(dir, 'lib')
449
450     def Qt_create_SConstruct(self, place):
451         if type(place) is type([]):
452             place = apply(test.workpath, place)
453         self.write(place, """\
454 if ARGUMENTS.get('noqtdir', 0): QTDIR=None
455 else: QTDIR=r'%s'
456 env = Environment(QTDIR = QTDIR,
457                   QT_LIB = r'%s',
458                   QT_MOC = r'%s',
459                   QT_UIC = r'%s',
460                   tools=['default','qt'])
461 dup = 1
462 if ARGUMENTS.get('build_dir', 0):
463     if ARGUMENTS.get('chdir', 0):
464         SConscriptChdir(1)
465     else:
466         SConscriptChdir(0)
467     dup=int(ARGUMENTS.get('dup', 1))
468     if dup == 0:
469         builddir = 'build_dup0'
470         env['QT_DEBUG'] = 1
471     else:
472         builddir = 'build'
473     BuildDir(builddir, '.', duplicate=dup)
474     print builddir, dup
475     sconscript = Dir(builddir).File('SConscript')
476 else:
477     sconscript = File('SConscript')
478 Export("env dup")
479 SConscript( sconscript )
480 """ % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
481
482     def msvs_versions(self):
483         if not hasattr(self, '_msvs_versions'):
484
485             # Determine the SCons version and the versions of the MSVS
486             # environments installed on the test machine.
487             #
488             # We do this by executing SCons with an SConstruct file
489             # (piped on stdin) that spits out Python assignments that
490             # we can just exec().  We construct the SCons.__"version"__
491             # string in the input here so that the SCons build itself
492             # doesn't fill it in when packaging SCons.
493             input = """\
494 import SCons
495 print "self._scons_version =", repr(SCons.__%s__)
496 env = Environment();
497 print "self._msvs_versions =", str(env['MSVS']['VERSIONS'])
498 """ % 'version'
499         
500             self.run(arguments = '-n -q -Q -f -', stdin = input)
501             exec(self.stdout())
502
503         return self._msvs_versions
504
505     def vcproj_sys_path(self, fname):
506         """
507         """
508         orig = 'sys.path = [ join(sys'
509
510         enginepath = repr(os.path.join(self._cwd, '..', 'engine'))
511         replace = 'sys.path = [ %s, join(sys' % enginepath
512
513         contents = self.read(fname)
514         contents = string.replace(contents, orig, replace)
515         self.write(fname, contents)
516
517     def msvs_substitute(self, input, msvs_ver,
518                         subdir=None, sconscript=None,
519                         python=sys.executable,
520                         project_guid=None):
521         if not hasattr(self, '_msvs_versions'):
522             self.msvs_versions()
523
524         if subdir:
525             workpath = self.workpath(subdir)
526         else:
527             workpath = self.workpath()
528
529         if sconscript is None:
530             sconscript = self.workpath('SConstruct')
531
532         if project_guid is None:
533             project_guid = "{E5466E26-0003-F18B-8F8A-BCD76C86388D}"
534
535         if os.environ.has_key('SCONS_LIB_DIR'):
536             exec_script_main = "from os.path import join; import sys; sys.path = [ r'%s' ] + sys.path; import SCons.Script; SCons.Script.main()" % os.environ['SCONS_LIB_DIR']
537         else:
538             exec_script_main = "from os.path import join; import sys; sys.path = [ join(sys.prefix, 'Lib', 'site-packages', 'scons-%s'), join(sys.prefix, 'scons-%s'), join(sys.prefix, 'Lib', 'site-packages', 'scons'), join(sys.prefix, 'scons') ] + sys.path; import SCons.Script; SCons.Script.main()" % (self._scons_version, self._scons_version)
539         exec_script_main_xml = string.replace(exec_script_main, "'", "&apos;")
540
541         result = string.replace(input, r'<WORKPATH>', workpath)
542         result = string.replace(result, r'<PYTHON>', python)
543         result = string.replace(result, r'<SCONSCRIPT>', sconscript)
544         result = string.replace(result, r'<SCONS_SCRIPT_MAIN>', exec_script_main)
545         result = string.replace(result, r'<SCONS_SCRIPT_MAIN_XML>', exec_script_main_xml)
546         result = string.replace(result, r'<PROJECT_GUID>', project_guid)
547         return result
548
549     def get_msvs_executable(self, version):
550         """Returns a full path to the executable (MSDEV or devenv)
551         for the specified version of Visual Studio.
552         """
553         common_msdev98_bin_msdev_com = ['Common', 'MSDev98', 'Bin', 'MSDEV.COM']
554         common7_ide_devenv_com       = ['Common7', 'IDE', 'devenv.com']
555         common7_ide_vcexpress_exe    = ['Common7', 'IDE', 'VCExpress.exe']
556         sub_paths = {
557             '6.0' : [
558                 common_msdev98_bin_msdev_com,
559             ],
560             '7.0' : [
561                 common7_ide_devenv_com,
562             ],
563             '7.1' : [
564                 common7_ide_devenv_com,
565             ],
566             '8.0' : [
567                 common7_ide_devenv_com,
568                 common7_ide_vcexpress_exe,
569             ],
570         }
571         from SCons.Tool.msvs import get_msvs_install_dirs
572         vs_path = get_msvs_install_dirs(version)['VSINSTALLDIR']
573         for sp in sub_paths[version]:
574             p = apply(os.path.join, [vs_path] + sp)
575             if os.path.exists(p):
576                 return p
577         return apply(os.path.join, [vs_path] + sub_paths[version][0])
578
579 # In some environments, $AR will generate a warning message to stderr
580 # if the library doesn't previously exist and is being created.  One
581 # way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
582 # but this is difficult to do in a platform-/implementation-specific
583 # method.  Instead, we will use the following as a stderr match for
584 # tests that use AR so that we will view zero or more "ar: creating
585 # <file>" messages to be successful executions of the test (see
586 # test/AR.py for sample usage).
587
588 noisy_ar=r'(ar: creating( archive)? \S+\n?)*'