2 TestSCons.py: a testing framework for the SCons software construction
5 A TestSCons environment object is created via the usual invocation:
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.
17 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
28 except AttributeError:
31 for i in xrange(len(lists[0])):
32 result.append(tuple(map(lambda l, i=i: l[i], lists)))
36 from TestCommon import *
37 from TestCommon import __all__
39 # Some tests which verify that SCons has been packaged properly need to
40 # look for specific version file names. Replicating the version number
41 # here provides independent verification that what we packaged conforms
42 # to what we expect. (If we derived the version number from the same
43 # data driving the build we might miss errors if the logic breaks.)
47 __all__.extend([ 'TestSCons',
67 except AttributeError:
68 # Windows doesn't have a uname() function. We could use something like
69 # sys.platform as a fallback, but that's not really a "machine," so
70 # just leave it as None.
74 machine = machine_map.get(machine, machine)
76 python = python_executable
77 _python_ = '"' + python_executable + '"'
87 """Test whether -lfrtbegin is required. This can probably be done in
88 a more reliable way, but using popen3 is relatively efficient."""
94 stderr = popen2.popen3('gcc -v')[2]
98 for l in stderr.readlines():
99 list = string.split(l)
100 if len(list) > 3 and list[:2] == ['gcc', 'version']:
101 if list[2][:2] in ('3.', '4.'):
102 libs = ['frtbegin'] + libs
107 if sys.platform == 'cygwin':
108 # On Cygwin, os.path.normcase() lies, so just report back the
109 # fact that the underlying Win32 OS is case-insensitive.
110 def case_sensitive_suffixes(s1, s2):
113 def case_sensitive_suffixes(s1, s2):
114 return (os.path.normcase(s1) != os.path.normcase(s2))
117 if sys.platform == 'win32':
118 fortran_lib = gccFortranLibs()
119 elif sys.platform == 'cygwin':
120 fortran_lib = gccFortranLibs()
121 elif string.find(sys.platform, 'irix') != -1:
122 fortran_lib = ['ftn']
124 fortran_lib = gccFortranLibs()
128 file_expr = r"""File "[^"]*", line \d+, in .+
131 # re.escape escapes too much.
133 for c in ['.', '[', ']', '(', ')', '*', '+', '?']: # Not an exhaustive list.
134 str = string.replace(str, c, '\\' + c)
139 class TestSCons(TestCommon):
140 """Class for testing SCons.
142 This provides a common place for initializing SCons tests,
143 eliminating the need to begin every test with the same repeated
147 scons_version = SConsVersion
149 def __init__(self, **kw):
150 """Initialize an SCons testing object.
152 If they're not overridden by keyword arguments, this
153 initializes the object with the following default values:
155 program = 'scons' if it exists,
157 interpreter = 'python'
161 The workdir value means that, by default, a temporary workspace
162 directory is created for a TestSCons environment. In addition,
163 this method changes directory (chdir) to the workspace directory,
164 so an explicit "chdir = '.'" on all of the run() method calls
167 self.orig_cwd = os.getcwd()
169 script_dir = os.environ['SCONS_SCRIPT_DIR']
174 if not kw.has_key('program'):
175 kw['program'] = os.environ.get('SCONS')
176 if not kw['program']:
177 if os.path.exists('scons'):
178 kw['program'] = 'scons'
180 kw['program'] = 'scons.py'
181 if not kw.has_key('interpreter') and not os.environ.get('SCONS_EXEC'):
182 kw['interpreter'] = [python, '-tt']
183 if not kw.has_key('match'):
184 kw['match'] = match_exact
185 if not kw.has_key('workdir'):
187 apply(TestCommon.__init__, [self], kw)
189 def Environment(self, ENV=None, *args, **kw):
191 Return a construction Environment that optionally overrides
192 the default external environment with the specified ENV.
194 import SCons.Environment
199 return apply(SCons.Environment.Environment, args, kw)
200 except (SCons.Errors.UserError, SCons.Errors.InternalError):
203 def detect(self, var, prog=None, ENV=None):
205 Detect a program named 'prog' by first checking the construction
206 variable named 'var' and finally searching the path used by
207 SCons. If either method fails to detect the program, then false
208 is returned, otherwise the full path to prog is returned. If
209 prog is None, then the value of the environment variable will be
212 env = self.Environment(ENV)
213 v = env.subst('$'+var)
220 return env.WhereIs(prog)
222 def detect_tool(self, tool, prog=None, ENV=None):
224 Given a tool (i.e., tool specification that would be passed
225 to the "tools=" parameter of Environment()) and a program that
226 corresponds to that tool, return true if and only if we can find
227 that tool using Environment.Detect().
229 By default, prog is set to the value passed into the tools parameter.
234 env = self.Environment(ENV, tools=[tool])
237 return env.Detect([prog])
239 def where_is(self, prog, path=None):
241 Given a program, search for it in the specified external PATH,
242 or in the actual external PATH is none is specified.
244 import SCons.Environment
245 env = SCons.Environment.Environment()
247 path = os.environ['PATH']
248 return env.WhereIs(prog, path)
250 def wrap_stdout(self, build_str = "", read_str = "", error = 0, cleaning = 0):
251 """Wraps standard output string(s) in the normal
252 "Reading ... done" and "Building ... done" strings
254 cap,lc = [ ('Build','build'),
255 ('Clean','clean') ][cleaning]
257 term = "scons: %sing terminated because of errors.\n" % lc
259 term = "scons: done %sing targets.\n" % lc
260 return "scons: Reading SConscript files ...\n" + \
262 "scons: done reading SConscript files.\n" + \
263 "scons: %sing targets ...\n" % cap + \
267 def up_to_date(self, options = None, arguments = None, read_str = "", **kw):
269 for arg in string.split(arguments):
270 s = s + "scons: `%s' is up to date.\n" % arg
272 arguments = options + " " + arguments
273 kw['arguments'] = arguments
274 kw['stdout'] = self.wrap_stdout(read_str = read_str, build_str = s)
275 kw['match'] = self.match_exact
276 apply(self.run, [], kw)
278 def not_up_to_date(self, options = None, arguments = None, **kw):
279 """Asserts that none of the targets listed in arguments is
280 up to date, but does not make any assumptions on other targets.
281 This function is most useful in conjunction with the -n option.
284 for arg in string.split(arguments):
285 s = s + "(?!scons: `%s' is up to date.)" % arg
287 arguments = options + " " + arguments
288 kw['arguments'] = arguments
289 kw['stdout'] = self.wrap_stdout(build_str="("+s+"[^\n]*\n)*")
290 kw['stdout'] = string.replace(kw['stdout'],'\n','\\n')
291 kw['stdout'] = string.replace(kw['stdout'],'.','\\.')
292 kw['match'] = self.match_re_dotall
293 apply(self.run, [], kw)
295 def skip_test(self, message="Skipping test.\n"):
298 Proper test-skipping behavior is dependent on whether we're being
299 executed as part of development of a change under Aegis.
301 Technically, skipping a test is a NO RESULT, but Aegis will
302 treat that as a test failure and prevent the change from going
303 to the next step. We don't want to force anyone using Aegis
304 to have to install absolutely every tool used by the tests,
305 so we actually report to Aegis that a skipped test has PASSED
306 so that the workflow isn't held up.
309 sys.stdout.write(message)
311 devdir = os.popen("aesub '$dd' 2>/dev/null", "r").read()[:-1]
312 intdir = os.popen("aesub '$intd' 2>/dev/null", "r").read()[:-1]
313 if devdir and self._cwd[:len(devdir)] == devdir or \
314 intdir and self._cwd[:len(intdir)] == intdir:
315 # We're under the development directory for this change,
316 # so this is an Aegis invocation; pass the test (exit 0).
319 # skip=1 means skip this function when showing where this
320 # result came from. They only care about the line where the
321 # script called test.skip_test(), not the line number where
322 # we call test.no_result().
323 self.no_result(skip=1)
325 def diff_substr(self, expect, actual, prelen=20, postlen=40):
327 for x, y in zip(expect, actual):
329 return "Actual did not match expect at char %d:\n" \
332 % (i, repr(expect[i-prelen:i+postlen]),
333 repr(actual[i-prelen:i+postlen]))
335 return "Actual matched the expected output???"
337 def python_file_line(self, file, line):
339 Returns a Python error line for output comparisons.
341 The exec of the traceback line gives us the correct format for
342 this version of Python. Before 2.5, this yielded:
344 File "<string>", line 1, ?
346 Python 2.5 changed this to:
348 File "<string>", line 1, <module>
350 We stick the requested file name and line number in the right
351 places, abstracting out the version difference.
353 exec 'import traceback; x = traceback.format_stack()[-1]'
355 x = string.replace(x, '<string>', file)
356 x = string.replace(x, 'line 1,', 'line %s,' % line)
359 def normalize_pdf(self, s):
360 s = re.sub(r'/CreationDate \(D:[^)]*\)',
361 r'/CreationDate (D:XXXX)', s)
362 s = re.sub(r'/ID \[<[0-9a-fA-F]*> <[0-9a-fA-F]*>\]',
363 r'/ID [<XXXX> <XXXX>]', s)
368 Return a default external environment that uses a local Java SDK
369 in preference to whatever's found in the default PATH.
371 import SCons.Environment
372 env = SCons.Environment.Environment()
374 '/usr/local/j2sdk1.4.2/bin',
375 '/usr/local/j2sdk1.4.1/bin',
376 '/usr/local/j2sdk1.3.1/bin',
377 '/usr/local/j2sdk1.3.0/bin',
378 '/usr/local/j2sdk1.2.2/bin',
379 '/usr/local/j2sdk1.2/bin',
380 '/usr/local/j2sdk1.1.8/bin',
381 '/usr/local/j2sdk1.1.7/bin',
382 '/usr/local/j2sdk1.1.6/bin',
383 '/usr/local/j2sdk1.1.5/bin',
384 '/usr/local/j2sdk1.1.4/bin',
385 '/usr/local/j2sdk1.1.3/bin',
386 '/usr/local/j2sdk1.1.2/bin',
387 '/usr/local/j2sdk1.1.1/bin',
390 env['ENV']['PATH'] = string.join(java_path, os.pathsep)
393 def Qt_dummy_installation(self, dir='qt'):
394 # create a dummy qt installation
396 self.subdir( dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'] )
398 self.write([dir, 'bin', 'mymoc.py'], """\
403 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:', [])
407 for opt, arg in cmd_opts:
408 if opt == '-o': output = open(arg, 'wb')
409 elif opt == '-i': impl = 1
410 else: opt_string = opt_string + ' ' + opt
412 contents = open(a, 'rb').read()
413 a = string.replace(a, '\\\\', '\\\\\\\\')
414 subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
416 contents = re.sub( r'#include.*', '', contents )
417 output.write(string.replace(contents, 'Q_OBJECT', subst))
422 self.write([dir, 'bin', 'myuic.py'], """\
431 for arg in sys.argv[1:]:
433 output = open(arg, 'wb')
445 source = open(arg, 'rb')
448 output.write( '#include "' + impl + '"\\n' )
449 includes = re.findall('<include.*?>(.*?)</include>', source.read())
450 for incFile in includes:
451 # this is valid for ui.h files, at least
452 if os.path.exists(incFile):
453 output.write('#include "' + incFile + '"\\n')
455 output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
460 self.write([dir, 'include', 'my_qobject.h'], r"""
462 void my_qt_symbol(const char *arg);
465 self.write([dir, 'lib', 'my_qobject.cpp'], r"""
466 #include "../include/my_qobject.h"
468 void my_qt_symbol(const char *arg) {
473 self.write([dir, 'lib', 'SConstruct'], r"""
476 if sys.platform == 'win32':
477 env.StaticLibrary( 'myqt', 'my_qobject.cpp' )
479 env.SharedLibrary( 'myqt', 'my_qobject.cpp' )
482 self.run(chdir = self.workpath(dir, 'lib'),
485 match = self.match_re_dotall)
487 self.QT = self.workpath(dir)
489 self.QT_MOC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'mymoc.py'))
490 self.QT_UIC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'myuic.py'))
491 self.QT_LIB_DIR = self.workpath(dir, 'lib')
493 def Qt_create_SConstruct(self, place):
494 if type(place) is type([]):
495 place = apply(test.workpath, place)
496 self.write(place, """\
497 if ARGUMENTS.get('noqtdir', 0): QTDIR=None
499 env = Environment(QTDIR = QTDIR,
503 tools=['default','qt'])
505 if ARGUMENTS.get('build_dir', 0):
506 if ARGUMENTS.get('chdir', 0):
510 dup=int(ARGUMENTS.get('dup', 1))
512 builddir = 'build_dup0'
516 BuildDir(builddir, '.', duplicate=dup)
518 sconscript = Dir(builddir).File('SConscript')
520 sconscript = File('SConscript')
522 SConscript( sconscript )
523 """ % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
525 def msvs_versions(self):
526 if not hasattr(self, '_msvs_versions'):
528 # Determine the SCons version and the versions of the MSVS
529 # environments installed on the test machine.
531 # We do this by executing SCons with an SConstruct file
532 # (piped on stdin) that spits out Python assignments that
533 # we can just exec(). We construct the SCons.__"version"__
534 # string in the input here so that the SCons build itself
535 # doesn't fill it in when packaging SCons.
538 print "self._scons_version =", repr(SCons.__%s__)
540 print "self._msvs_versions =", str(env['MSVS']['VERSIONS'])
543 self.run(arguments = '-n -q -Q -f -', stdin = input)
546 return self._msvs_versions
548 def vcproj_sys_path(self, fname):
551 orig = 'sys.path = [ join(sys'
553 enginepath = repr(os.path.join(self._cwd, '..', 'engine'))
554 replace = 'sys.path = [ %s, join(sys' % enginepath
556 contents = self.read(fname)
557 contents = string.replace(contents, orig, replace)
558 self.write(fname, contents)
560 def msvs_substitute(self, input, msvs_ver,
561 subdir=None, sconscript=None,
562 python=sys.executable,
564 if not hasattr(self, '_msvs_versions'):
568 workpath = self.workpath(subdir)
570 workpath = self.workpath()
572 if sconscript is None:
573 sconscript = self.workpath('SConstruct')
575 if project_guid is None:
576 project_guid = "{E5466E26-0003-F18B-8F8A-BCD76C86388D}"
578 if os.environ.has_key('SCONS_LIB_DIR'):
579 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']
581 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)
582 exec_script_main_xml = string.replace(exec_script_main, "'", "'")
584 result = string.replace(input, r'<WORKPATH>', workpath)
585 result = string.replace(result, r'<PYTHON>', python)
586 result = string.replace(result, r'<SCONSCRIPT>', sconscript)
587 result = string.replace(result, r'<SCONS_SCRIPT_MAIN>', exec_script_main)
588 result = string.replace(result, r'<SCONS_SCRIPT_MAIN_XML>', exec_script_main_xml)
589 result = string.replace(result, r'<PROJECT_GUID>', project_guid)
592 def get_msvs_executable(self, version):
593 """Returns a full path to the executable (MSDEV or devenv)
594 for the specified version of Visual Studio.
596 common_msdev98_bin_msdev_com = ['Common', 'MSDev98', 'Bin', 'MSDEV.COM']
597 common7_ide_devenv_com = ['Common7', 'IDE', 'devenv.com']
598 common7_ide_vcexpress_exe = ['Common7', 'IDE', 'VCExpress.exe']
601 common_msdev98_bin_msdev_com,
604 common7_ide_devenv_com,
607 common7_ide_devenv_com,
610 common7_ide_devenv_com,
611 common7_ide_vcexpress_exe,
614 from SCons.Tool.msvs import get_msvs_install_dirs
615 vs_path = get_msvs_install_dirs(version)['VSINSTALLDIR']
616 for sp in sub_paths[version]:
617 p = apply(os.path.join, [vs_path] + sp)
618 if os.path.exists(p):
620 return apply(os.path.join, [vs_path] + sub_paths[version][0])
623 NCR = 0 # non-cached rebuild
624 CR = 1 # cached rebuild (up to date)
625 NCF = 2 # non-cached build failure
626 CF = 3 # cached build failure
628 if sys.platform == 'win32':
629 Configure_lib = 'msvcrt'
633 # to use cygwin compilers on cmd.exe -> uncomment following line
636 def checkLogAndStdout(self, checks, results, cached,
637 logfile, sconf_dir, sconstruct,
638 doCheckLog=1, doCheckStdout=1):
641 def __init__(self, p):
644 def matchPart(log, logfile, lastEnd):
645 m = re.match(log, logfile[lastEnd:])
647 raise NoMatch, lastEnd
648 return m.end() + lastEnd
650 #print len(os.linesep)
653 for i in range(len(ls)):
657 nols = nols + "[^" + ls[i] + "])"
662 logfile = self.read(self.workpath(logfile))
664 string.find( logfile, "scons: warning: The stored build "
665 "information has an unexpected class." ) >= 0):
667 sconf_dir = sconf_dir
668 sconstruct = sconstruct
670 log = r'file\ \S*%s\,line \d+:' % re.escape(sconstruct) + ls
671 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
672 log = "\t" + re.escape("Configure(confdir = %s)" % sconf_dir) + ls
673 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
676 for check,result,cache_desc in zip(checks, results, cached):
677 log = re.escape("scons: Configure: " + check) + ls
678 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
681 for bld_desc in cache_desc: # each TryXXX
682 for ext, flag in bld_desc: # each file in TryBuild
683 file = os.path.join(sconf_dir,"conftest_%d%s" % (cnt, ext))
686 if ext in ['.c', '.cpp']:
687 log=log + re.escape(file + " <-") + ls
688 log=log + r"( \|" + nols + "*" + ls + ")+?"
690 log=log + "(" + nols + "*" + ls +")*?"
695 re.escape("scons: Configure: \"%s\" is up to date."
697 log=log+re.escape("scons: Configure: The original builder "
699 log=log+r"( \|.*"+ls+")+"
701 # non-cached rebuild failure
702 log=log + "(" + nols + "*" + ls + ")*?"
705 # cached rebuild failure
707 re.escape("scons: Configure: Building \"%s\" failed "
708 "in a previous run and all its sources are"
709 " up to date." % file) + ls
710 log=log+re.escape("scons: Configure: The original builder "
712 log=log+r"( \|.*"+ls+")+"
715 result = "(cached) " + result
716 rdstr = rdstr + re.escape(check) + re.escape(result) + "\n"
717 log=log + re.escape("scons: Configure: " + result) + ls + ls
718 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
720 if doCheckLog: lastEnd = matchPart(ls, logfile, lastEnd)
721 if doCheckLog and lastEnd != len(logfile):
722 raise NoMatch, lastEnd
725 print "Cannot match log file against log regexp."
727 print "------------------------------------------------------"
728 print logfile[m.pos:]
729 print "------------------------------------------------------"
731 print "------------------------------------------------------"
733 print "------------------------------------------------------"
737 exp_stdout = self.wrap_stdout(".*", rdstr)
738 if not self.match_re_dotall(self.stdout(), exp_stdout):
739 print "Unexpected stdout: "
740 print "-----------------------------------------------------"
741 print repr(self.stdout())
742 print "-----------------------------------------------------"
743 print repr(exp_stdout)
744 print "-----------------------------------------------------"
747 # In some environments, $AR will generate a warning message to stderr
748 # if the library doesn't previously exist and is being created. One
749 # way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
750 # but this is difficult to do in a platform-/implementation-specific
751 # method. Instead, we will use the following as a stderr match for
752 # tests that use AR so that we will view zero or more "ar: creating
753 # <file>" messages to be successful executions of the test (see
754 # test/AR.py for sample usage).
756 noisy_ar=r'(ar: creating( archive)? \S+\n?)*'