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__"
24 from TestCommon import *
25 from TestCommon import __all__
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.)
33 SConsVersion = '0.96.96'
35 __all__.extend([ 'TestSCons',
46 python = python_executable
47 _python_ = '"' + python_executable + '"'
57 """Test whether -lfrtbegin is required. This can probably be done in
58 a more reliable way, but using popen3 is relatively efficient."""
64 stderr = popen2.popen3('gcc -v')[2]
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
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):
83 def case_sensitive_suffixes(s1, s2):
84 return (os.path.normcase(s1) != os.path.normcase(s2))
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:
94 fortran_lib = gccFortranLibs()
98 file_expr = r"""File "[^"]*", line \d+, in .+
101 # re.escape escapes too much.
103 for c in ['.', '[', ']', '(', ')', '*', '+', '?']: # Not an exhaustive list.
104 str = string.replace(str, c, '\\' + c)
109 class TestSCons(TestCommon):
110 """Class for testing SCons.
112 This provides a common place for initializing SCons tests,
113 eliminating the need to begin every test with the same repeated
117 scons_version = SConsVersion
119 def __init__(self, **kw):
120 """Initialize an SCons testing object.
122 If they're not overridden by keyword arguments, this
123 initializes the object with the following default values:
125 program = 'scons' if it exists,
127 interpreter = 'python'
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
137 self.orig_cwd = os.getcwd()
139 script_dir = os.environ['SCONS_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'
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'):
157 apply(TestCommon.__init__, [self], kw)
159 def Environment(self, ENV=None, *args, **kw):
161 Return a construction Environment that optionally overrides
162 the default external environment with the specified ENV.
164 import SCons.Environment
169 return apply(SCons.Environment.Environment, args, kw)
170 except (SCons.Errors.UserError, SCons.Errors.InternalError):
173 def detect(self, var, prog=None, ENV=None):
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
182 env = self.Environment(ENV)
183 v = env.subst('$'+var)
190 return env.WhereIs(prog)
192 def detect_tool(self, tool, prog=None, ENV=None):
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().
199 By default, prog is set to the value passed into the tools parameter.
204 env = self.Environment(ENV, tools=[tool])
207 return env.Detect([prog])
209 def where_is(self, prog, path=None):
211 Given a program, search for it in the specified external PATH,
212 or in the actual external PATH is none is specified.
214 import SCons.Environment
215 env = SCons.Environment.Environment()
217 path = os.environ['PATH']
218 return env.WhereIs(prog, path)
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
224 cap,lc = [ ('Build','build'),
225 ('Clean','clean') ][cleaning]
227 term = "scons: %sing terminated because of errors.\n" % lc
229 term = "scons: done %sing targets.\n" % lc
230 return "scons: Reading SConscript files ...\n" + \
232 "scons: done reading SConscript files.\n" + \
233 "scons: %sing targets ...\n" % cap + \
237 def up_to_date(self, options = None, arguments = None, read_str = "", **kw):
239 for arg in string.split(arguments):
240 s = s + "scons: `%s' is up to date.\n" % arg
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)
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.
254 for arg in string.split(arguments):
255 s = s + "(?!scons: `%s' is up to date.)" % arg
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)
265 def skip_test(self, message="Skipping test.\n"):
268 Proper test-skipping behavior is dependent on whether we're being
269 executed as part of development of a change under Aegis.
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.
279 sys.stdout.write(message)
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).
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)
295 def diff_substr(self, expect, actual):
297 for x, y in zip(expect, actual):
299 return "Actual did not match expect at char %d:\n" \
302 % (i, repr(expect[i-20:i+40]), repr(actual[i-20:i+40]))
304 return "Actual matched the expected output???"
306 def python_file_line(self, file, line):
308 Returns a Python error line for output comparisons.
310 The exec of the traceback line gives us the correct format for
311 this version of Python. Before 2.5, this yielded:
313 File "<string>", line 1, ?
315 Python 2.5 changed this to:
317 File "<string>", line 1, <module>
319 We stick the requested file name and line number in the right
320 places, abstracting out the version difference.
322 exec 'import traceback; x = traceback.format_stack()[-1]'
324 x = string.replace(x, '<string>', file)
325 x = string.replace(x, 'line 1,', 'line %s,' % line)
330 Return a default external environment that uses a local Java SDK
331 in preference to whatever's found in the default PATH.
333 import SCons.Environment
334 env = SCons.Environment.Environment()
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',
352 env['ENV']['PATH'] = string.join(java_path, os.pathsep)
355 def Qt_dummy_installation(self, dir='qt'):
356 # create a dummy qt installation
358 self.subdir( dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'] )
360 self.write([dir, 'bin', 'mymoc.py'], """\
365 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:', [])
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
374 contents = open(a, 'rb').read()
375 subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
377 contents = re.sub( r'#include.*', '', contents )
378 output.write(string.replace(contents, 'Q_OBJECT', subst))
383 self.write([dir, 'bin', 'myuic.py'], """\
392 for arg in sys.argv[1:]:
394 output = open(arg, 'wb')
406 source = open(arg, 'rb')
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')
416 output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
421 self.write([dir, 'include', 'my_qobject.h'], r"""
423 void my_qt_symbol(const char *arg);
426 self.write([dir, 'lib', 'my_qobject.cpp'], r"""
427 #include "../include/my_qobject.h"
429 void my_qt_symbol(const char *arg) {
434 self.write([dir, 'lib', 'SConstruct'], r"""
436 env.SharedLibrary( 'myqt', 'my_qobject.cpp' )
439 self.run(chdir = self.workpath(dir, 'lib'),
442 match = self.match_re_dotall)
444 self.QT = self.workpath(dir)
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')
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
456 env = Environment(QTDIR = QTDIR,
460 tools=['default','qt'])
462 if ARGUMENTS.get('build_dir', 0):
463 if ARGUMENTS.get('chdir', 0):
467 dup=int(ARGUMENTS.get('dup', 1))
469 builddir = 'build_dup0'
473 BuildDir(builddir, '.', duplicate=dup)
475 sconscript = Dir(builddir).File('SConscript')
477 sconscript = File('SConscript')
479 SConscript( sconscript )
480 """ % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
482 def msvs_versions(self):
483 if not hasattr(self, '_msvs_versions'):
485 # Determine the SCons version and the versions of the MSVS
486 # environments installed on the test machine.
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.
495 print "self._scons_version =", repr(SCons.__%s__)
497 print "self._msvs_versions =", str(env['MSVS']['VERSIONS'])
500 self.run(arguments = '-n -q -Q -f -', stdin = input)
503 return self._msvs_versions
505 def vcproj_sys_path(self, fname):
508 orig = 'sys.path = [ join(sys'
510 enginepath = repr(os.path.join(self._cwd, '..', 'engine'))
511 replace = 'sys.path = [ %s, join(sys' % enginepath
513 contents = self.read(fname)
514 contents = string.replace(contents, orig, replace)
515 self.write(fname, contents)
517 def msvs_substitute(self, input, msvs_ver,
518 subdir=None, sconscript=None,
519 python=sys.executable,
521 if not hasattr(self, '_msvs_versions'):
525 workpath = self.workpath(subdir)
527 workpath = self.workpath()
529 if sconscript is None:
530 sconscript = self.workpath('SConstruct')
532 if project_guid is None:
533 project_guid = "{E5466E26-0003-F18B-8F8A-BCD76C86388D}"
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']
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, "'", "'")
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)
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.
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']
558 common_msdev98_bin_msdev_com,
561 common7_ide_devenv_com,
564 common7_ide_devenv_com,
567 common7_ide_devenv_com,
568 common7_ide_vcexpress_exe,
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):
577 return apply(os.path.join, [vs_path] + sub_paths[version][0])
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).
588 noisy_ar=r'(ar: creating( archive)? \S+\n?)*'