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__"
29 except AttributeError:
32 for i in xrange(len(lists[0])):
33 result.append(tuple(map(lambda l, i=i: l[i], lists)))
45 from TestCommon import *
46 from TestCommon import __all__
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.
53 default_version = '1.2.0'
55 copyright_years = '2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009'
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
65 __all__.extend([ 'TestSCons',
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.
93 machine = machine_map.get(machine, machine)
95 python = python_executable
96 _python_ = '"' + python_executable + '"'
100 shobj_ = shobj_prefix
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."""
118 stderr = popen2.popen3(cmd)[2]
122 p = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE)
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']
131 if list[2][:2] in ('3.', '4.'):
132 libs = ['frtbegin'] + libs
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):
143 def case_sensitive_suffixes(s1, s2):
144 return (os.path.normcase(s1) != os.path.normcase(s2))
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']
154 fortran_lib = gccFortranLibs()
158 file_expr = r"""File "[^"]*", line \d+, in .+
161 # re.escape escapes too much.
163 for c in ['.', '[', ']', '(', ')', '*', '+', '?']: # Not an exhaustive list.
164 str = string.replace(str, c, '\\' + c)
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])
177 def python_version_string():
178 return string.split(sys.version)[0]
180 def python_minor_version_string():
181 return sys.version[:3]
183 def unsupported_python_version(version=sys.version_info):
184 return version < (1, 5, 2)
186 def deprecated_python_version(version=sys.version_info):
187 return version < (2, 4, 0)
189 if deprecated_python_version():
191 scons: warning: Support for pre-2.4 Python (%s) is deprecated.
192 If this will cause hardship, contact dev@scons.tigris.org.
195 deprecated_python_expr = re_escape(msg % python_version_string()) + file_expr
198 deprecated_python_expr = ""
202 class TestSCons(TestCommon):
203 """Class for testing SCons.
205 This provides a common place for initializing SCons tests,
206 eliminating the need to begin every test with the same repeated
210 scons_version = SConsVersion
212 def __init__(self, **kw):
213 """Initialize an SCons testing object.
215 If they're not overridden by keyword arguments, this
216 initializes the object with the following default values:
218 program = 'scons' if it exists,
220 interpreter = 'python'
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
230 self.orig_cwd = os.getcwd()
232 script_dir = os.environ['SCONS_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'
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'):
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'
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']
260 if self.ignore_python_version and deprecated_python_version():
261 sconsflags = os.environ.get('SCONSFLAGS')
263 sconsflags = [sconsflags]
266 sconsflags = sconsflags + ['--warn=no-python-version']
267 os.environ['SCONSFLAGS'] = string.join(sconsflags)
269 apply(TestCommon.__init__, [self], kw)
272 if SCons.Node.FS.default_fs is None:
273 SCons.Node.FS.default_fs = SCons.Node.FS.FS()
275 def Environment(self, ENV=None, *args, **kw):
277 Return a construction Environment that optionally overrides
278 the default external environment with the specified ENV.
280 import SCons.Environment
285 return apply(SCons.Environment.Environment, args, kw)
286 except (SCons.Errors.UserError, SCons.Errors.InternalError):
289 def detect(self, var, prog=None, ENV=None, norm=None):
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
298 env = self.Environment(ENV)
299 v = env.subst('$'+var)
306 result = env.WhereIs(prog)
307 if norm and os.sep != '/':
308 result = string.replace(result, os.sep, '/')
311 def detect_tool(self, tool, prog=None, ENV=None):
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().
318 By default, prog is set to the value passed into the tools parameter.
323 env = self.Environment(ENV, tools=[tool])
326 return env.Detect([prog])
328 def where_is(self, prog, path=None):
330 Given a program, search for it in the specified external PATH,
331 or in the actual external PATH is none is specified.
333 import SCons.Environment
334 env = SCons.Environment.Environment()
336 path = os.environ['PATH']
337 return env.WhereIs(prog, path)
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
343 cap,lc = [ ('Build','build'),
344 ('Clean','clean') ][cleaning]
346 term = "scons: %sing terminated because of errors.\n" % lc
348 term = "scons: done %sing targets.\n" % lc
349 return "scons: Reading SConscript files ...\n" + \
351 "scons: done reading SConscript files.\n" + \
352 "scons: %sing targets ...\n" % cap + \
356 def run(self, *args, **kw):
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.
363 save_sconsflags = os.environ.get('SCONSFLAGS')
365 sconsflags = [save_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)
379 result = apply(TestCommon.run, (self,)+args, kw)
381 sconsflags = save_sconsflags
384 def up_to_date(self, options = None, arguments = None, read_str = "", **kw):
386 for arg in string.split(arguments):
387 s = s + "scons: `%s' is up to date.\n" % arg
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)
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.
404 for arg in string.split(arguments):
405 s = s + "(?!scons: `%s' is up to date.)" % re.escape(arg)
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)
415 def option_not_yet_implemented(self, option, arguments=None, **kw):
417 Verifies expected behavior for options that are not yet implemented:
418 a warning message, and exit status 1.
420 msg = "Warning: the %s option is not yet implemented\n" % option
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
429 kw['arguments'] = option + ' ' + arguments
431 #return self.run(**kw)
432 return apply(self.run, (), kw)
434 def diff_substr(self, expect, actual, prelen=20, postlen=40):
436 for x, y in zip(expect, actual):
438 return "Actual did not match expect at char %d:\n" \
441 % (i, repr(expect[i-prelen:i+postlen]),
442 repr(actual[i-prelen:i+postlen]))
444 return "Actual matched the expected output???"
446 def python_file_line(self, file, line):
448 Returns a Python error line for output comparisons.
450 The exec of the traceback line gives us the correct format for
451 this version of Python. Before 2.5, this yielded:
453 File "<string>", line 1, ?
455 Python 2.5 changed this to:
457 File "<string>", line 1, <module>
459 We stick the requested file name and line number in the right
460 places, abstracting out the version difference.
462 exec 'import traceback; x = traceback.format_stack()[-1]'
464 x = string.replace(x, '<string>', file)
465 x = string.replace(x, 'line 1,', 'line %s,' % line)
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}',
475 s = re.sub(r'/Length \d+ *\n/Filter /FlateDecode\n',
476 r'/Length XXXX\n/Filter /FlateDecode\n', s)
484 begin_marker = '/FlateDecode\n>>\nstream\n'
485 end_marker = 'endstream\nendobj'
488 b = string.find(s, begin_marker, 0)
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))
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}',
509 s = string.join(r, '')
513 def paths(self,patterns):
523 def java_ENV(self, version=None):
525 Initialize with a default external environment that uses a local
526 Java SDK in preference to whatever's found in the default PATH.
529 return self._java_env[version]['ENV']
530 except AttributeError:
535 import SCons.Environment
536 env = SCons.Environment.Environment()
537 self._java_env[version] = env
542 '/usr/java/jdk%s*/bin' % version,
543 '/usr/lib/jvm/*-%s*/bin' % version,
544 '/usr/local/j2sdk%s*/bin' % version,
546 java_path = self.paths(patterns) + [env['ENV']['PATH']]
549 '/usr/java/latest/bin',
550 '/usr/lib/jvm/*/bin',
551 '/usr/local/j2sdk*/bin',
553 java_path = self.paths(patterns) + [env['ENV']['PATH']]
555 env['ENV']['PATH'] = string.join(java_path, os.pathsep)
558 def java_where_includes(self,version=None):
560 Return java include paths compiling java jni code
566 frame = '/System/Library/Frameworks/JavaVM.framework/Headers/jni.h'
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,
573 dirs = self.paths(jni_dirs)
576 d=os.path.dirname(self.paths(jni_dirs)[0])
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'))
586 def java_where_java_home(self,version=None):
587 if sys.platform[:6] == 'darwin':
589 home = '/System/Library/Frameworks/JavaVM.framework/Home'
591 home = '/System/Library/Frameworks/JavaVM.framework/Versions/%s/Home' % version
593 jar = self.java_where_jar(version)
594 home = os.path.normpath('%s/..'%jar)
595 if os.path.isdir(home):
597 print("Could not determine JAVA_HOME: %s is not a directory" % home)
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)
605 where_jar = self.where_is('jar', ENV['PATH'])
607 self.skip_test("Could not find Java jar, skipping test(s).\n")
610 def java_where_java(self, version=None):
612 Return a path to the java executable.
614 ENV = self.java_ENV(version)
615 where_java = self.where_is('java', ENV['PATH'])
617 self.skip_test("Could not find Java java, skipping test(s).\n")
620 def java_where_javac(self, version=None):
622 Return a path to the javac compiler.
624 ENV = self.java_ENV(version)
625 if self.detect_tool('javac'):
626 where_javac = self.detect('JAVAC', 'javac', ENV=ENV)
628 where_javac = self.where_is('javac', ENV['PATH'])
630 self.skip_test("Could not find Java javac, skipping test(s).\n")
631 self.run(program = where_javac,
632 arguments = '-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)
640 m = re.search(r'javac (\d\.\d)', self.stderr())
645 return where_javac, version
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)
652 where_javah = self.where_is('javah', ENV['PATH'])
654 self.skip_test("Could not find Java javah, skipping test(s).\n")
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)
662 where_rmic = self.where_is('rmic', ENV['PATH'])
664 self.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n")
667 def Qt_dummy_installation(self, dir='qt'):
668 # create a dummy qt installation
670 self.subdir( dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'] )
672 self.write([dir, 'bin', 'mymoc.py'], """\
677 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:', [])
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
686 contents = open(a, 'rb').read()
687 a = string.replace(a, '\\\\', '\\\\\\\\')
688 subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
690 contents = re.sub( r'#include.*', '', contents )
691 output.write(string.replace(contents, 'Q_OBJECT', subst))
696 self.write([dir, 'bin', 'myuic.py'], """\
705 for arg in sys.argv[1:]:
707 output = open(arg, 'wb')
719 source = open(arg, 'rb')
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')
729 output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
734 self.write([dir, 'include', 'my_qobject.h'], r"""
736 void my_qt_symbol(const char *arg);
739 self.write([dir, 'lib', 'my_qobject.cpp'], r"""
740 #include "../include/my_qobject.h"
742 void my_qt_symbol(const char *arg) {
747 self.write([dir, 'lib', 'SConstruct'], r"""
750 if sys.platform == 'win32':
751 env.StaticLibrary( 'myqt', 'my_qobject.cpp' )
753 env.SharedLibrary( 'myqt', 'my_qobject.cpp' )
756 self.run(chdir = self.workpath(dir, 'lib'),
759 match = self.match_re_dotall)
761 self.QT = self.workpath(dir)
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')
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
773 env = Environment(QTDIR = QTDIR,
777 tools=['default','qt'])
779 if ARGUMENTS.get('variant_dir', 0):
780 if ARGUMENTS.get('chdir', 0):
784 dup=int(ARGUMENTS.get('dup', 1))
786 builddir = 'build_dup0'
790 VariantDir(builddir, '.', duplicate=dup)
792 sconscript = Dir(builddir).File('SConscript')
794 sconscript = File('SConscript')
796 SConscript( sconscript )
797 """ % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
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
805 if sys.platform == 'win32':
806 Configure_lib = 'msvcrt'
810 # to use cygwin compilers on cmd.exe -> uncomment following line
813 def checkLogAndStdout(self, checks, results, cached,
814 logfile, sconf_dir, sconstruct,
815 doCheckLog=1, doCheckStdout=1):
818 def __init__(self, p):
821 def matchPart(log, logfile, lastEnd, NoMatch=NoMatch):
822 m = re.match(log, logfile[lastEnd:])
824 raise NoMatch, lastEnd
825 return m.end() + lastEnd
827 #print len(os.linesep)
830 for i in range(len(ls)):
834 nols = nols + "[^" + ls[i] + "])"
839 logfile = self.read(self.workpath(logfile))
841 string.find( logfile, "scons: warning: The stored build "
842 "information has an unexpected class." ) >= 0):
844 sconf_dir = sconf_dir
845 sconstruct = sconstruct
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)
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)
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))
863 if ext in ['.c', '.cpp']:
864 log=log + re.escape(file + " <-") + ls
865 log=log + r"( \|" + nols + "*" + ls + ")+?"
867 log=log + "(" + nols + "*" + ls +")*?"
872 re.escape("scons: Configure: \"%s\" is up to date."
874 log=log+re.escape("scons: Configure: The original builder "
876 log=log+r"( \|.*"+ls+")+"
878 # non-cached rebuild failure
879 log=log + "(" + nols + "*" + ls + ")*?"
882 # cached rebuild failure
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 "
889 log=log+r"( \|.*"+ls+")+"
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)
897 if doCheckLog: lastEnd = matchPart(ls, logfile, lastEnd)
898 if doCheckLog and lastEnd != len(logfile):
899 raise NoMatch, lastEnd
902 print "Cannot match log file against log regexp."
904 print "------------------------------------------------------"
905 print logfile[m.pos:]
906 print "------------------------------------------------------"
908 print "------------------------------------------------------"
910 print "------------------------------------------------------"
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 "-----------------------------------------------------"
924 def get_python_version(self):
926 Returns the Python version (just so everyone doesn't have to
927 hand-code slicing the right number of characters).
929 # see also sys.prefix documentation
930 return python_minor_version_string()
932 def get_platform_python_info(self):
934 Returns a path to a Python executable suitable for testing on
935 this platform and its associated include path, library path,
938 python = self.where_is('python')
940 self.skip_test('Can not find installed "python", skipping test.\n')
942 self.run(program = python, stdin = """\
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')
953 return [python] + string.split(string.strip(self.stdout()), '\n')
955 def wait_for(self, fname, timeout=10.0, popen=None):
957 Waits for the specified file name to exist.
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)
969 waited = waited + 1.0
971 def get_alt_cpp_suffix(self):
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
978 if not case_sensitive_suffixes('.c','.C'):
979 alt_cpp_suffix = '.cpp'
981 alt_cpp_suffix = '.C'
982 return alt_cpp_suffix
986 def __init__(self, name, units, expression, convert=None):
988 convert = lambda x: x
991 self.expression = re.compile(expression)
992 self.convert = convert
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),
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'),
1016 class TimeSCons(TestSCons):
1017 """Class for timing SCons."""
1018 def __init__(self, *args, **kw):
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
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)
1034 value = float(value)
1037 self.variables[variable] = value
1040 self.calibrate = os.environ.get('TIMESCONS_CALIBRATE', '0') != '0'
1042 if not kw.has_key('verbose') and not self.calibrate:
1043 kw['verbose'] = True
1046 #TestSCons.__init__(self, *args, **kw)
1047 apply(TestSCons.__init__, (self,)+args, kw)
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)
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())
1057 def main(self, *args, **kw):
1059 The main entry point for standard execution of timings.
1061 This method run SCons three times:
1063 Once with the --help option, to have it exit after just reading
1066 Once as a full build of all targets.
1068 Once again as a (presumably) null or up-to-date build of
1071 The elapsed time to execute each build is printed after
1074 if not kw.has_key('options') and self.variables:
1076 for variable, value in self.variables.items():
1077 options.append('%s=%s' % (variable, value))
1078 kw['options'] = ' '.join(options)
1081 #self.calibration(*args, **kw)
1082 apply(self.calibration, args, kw)
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)
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)
1098 sys.stdout.write(line)
1101 def report_traces(self, trace, stats):
1102 self.trace('TimeSCons-elapsed',
1104 self.elapsed_time(),
1107 for name, args in stats.items():
1109 #self.trace(name, trace, *args)
1110 apply(self.trace, (name, trace), args)
1112 def collect_stats(self, input):
1114 for stat in StatList:
1115 m = stat.expression.search(input)
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}
1124 def help(self, *args, **kw):
1126 Runs scons with the --help option.
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.
1132 kw['options'] = kw.get('options', '') + ' --help'
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)
1143 def full(self, *args, **kw):
1145 Runs a full build of SCons.
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)
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'])
1161 def calibration(self, *args, **kw):
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.
1168 #self.run(*args, **kw)
1169 apply(self.run, args, kw)
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())
1175 def null(self, *args, **kw):
1177 Runs an up-to-date null build of SCons.
1179 # TODO(sgk): allow the caller to specify the target (argument)
1180 # that must be up-to-date.
1182 #self.up_to_date(arguments='.', **kw)
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
1193 if float(stats['time-commands']['value']) == 0.0:
1194 del stats['time-commands']
1195 self.report_traces('null', stats)
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'])
1204 def elapsed_time(self):
1206 Returns the elapsed time of the most recent command execution.
1208 return self.endTime - self.startTime
1210 def run(self, *args, **kw):
1212 Runs a single build command, capturing output in the specified file.
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.
1219 kw['options'] = kw.get('options', '') + ' --debug=memory --debug=time'
1220 self.startTime = time.time()
1223 #result = TestSCons.run(self, *args, **kw)
1224 result = apply(TestSCons.run, (self,)+args, kw)
1226 self.endTime = time.time()
1229 def copy_timing_configuration(self, source_dir, dest_dir):
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).
1235 This ignores all files and directories that begin with the string
1236 'TimeSCons-', and all '.svn' subdirectories.
1238 for root, dirs, files in os.walk(source_dir):
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)
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).
1268 noisy_ar=r'(ar: creating( archive)? \S+\n?)*'
1272 # indent-tabs-mode:nil
1274 # vim: set expandtab tabstop=4 shiftwidth=4: