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
423 # If it's a long option and the argument string begins with '=',
424 # it's of the form --foo=bar and needs no separating space.
425 if option[:2] == '--' and arguments[0] == '=':
426 kw['arguments'] = option + arguments
428 kw['arguments'] = option + ' ' + arguments
430 #return self.run(**kw)
431 return apply(self.run, (), kw)
433 def diff_substr(self, expect, actual, prelen=20, postlen=40):
435 for x, y in zip(expect, actual):
437 return "Actual did not match expect at char %d:\n" \
440 % (i, repr(expect[i-prelen:i+postlen]),
441 repr(actual[i-prelen:i+postlen]))
443 return "Actual matched the expected output???"
445 def python_file_line(self, file, line):
447 Returns a Python error line for output comparisons.
449 The exec of the traceback line gives us the correct format for
450 this version of Python. Before 2.5, this yielded:
452 File "<string>", line 1, ?
454 Python 2.5 changed this to:
456 File "<string>", line 1, <module>
458 We stick the requested file name and line number in the right
459 places, abstracting out the version difference.
461 exec 'import traceback; x = traceback.format_stack()[-1]'
463 x = string.replace(x, '<string>', file)
464 x = string.replace(x, 'line 1,', 'line %s,' % line)
467 def normalize_pdf(self, s):
468 s = re.sub(r'/(Creation|Mod)Date \(D:[^)]*\)',
469 r'/\1Date (D:XXXX)', s)
470 s = re.sub(r'/ID \[<[0-9a-fA-F]*> <[0-9a-fA-F]*>\]',
471 r'/ID [<XXXX> <XXXX>]', s)
472 s = re.sub(r'/(BaseFont|FontName) /[A-Z]{6}',
474 s = re.sub(r'/Length \d+ *\n/Filter /FlateDecode\n',
475 r'/Length XXXX\n/Filter /FlateDecode\n', s)
483 begin_marker = '/FlateDecode\n>>\nstream\n'
484 end_marker = 'endstream\nendobj'
487 b = string.find(s, begin_marker, 0)
489 b = b + len(begin_marker)
490 e = string.find(s, end_marker, b)
491 encoded.append((b, e))
492 b = string.find(s, begin_marker, e + len(end_marker))
498 d = zlib.decompress(s[b:e])
499 d = re.sub(r'%%CreationDate: [^\n]*\n',
500 r'%%CreationDate: 1970 Jan 01 00:00:00\n', d)
501 d = re.sub(r'%DVIPSSource: TeX output \d\d\d\d\.\d\d\.\d\d:\d\d\d\d',
502 r'%DVIPSSource: TeX output 1970.01.01:0000', d)
503 d = re.sub(r'/(BaseFont|FontName) /[A-Z]{6}',
508 s = string.join(r, '')
512 def paths(self,patterns):
522 def java_ENV(self, version=None):
524 Initialize with a default external environment that uses a local
525 Java SDK in preference to whatever's found in the default PATH.
528 return self._java_env[version]['ENV']
529 except AttributeError:
534 import SCons.Environment
535 env = SCons.Environment.Environment()
536 self._java_env[version] = env
541 '/usr/java/jdk%s*/bin' % version,
542 '/usr/lib/jvm/*-%s*/bin' % version,
543 '/usr/local/j2sdk%s*/bin' % version,
545 java_path = self.paths(patterns) + [env['ENV']['PATH']]
548 '/usr/java/latest/bin',
549 '/usr/lib/jvm/*/bin',
550 '/usr/local/j2sdk*/bin',
552 java_path = self.paths(patterns) + [env['ENV']['PATH']]
554 env['ENV']['PATH'] = string.join(java_path, os.pathsep)
557 def java_where_includes(self,version=None):
559 Return java include paths compiling java jni code
565 frame = '/System/Library/Frameworks/JavaVM.framework/Headers/jni.h'
567 frame = '/System/Library/Frameworks/JavaVM.framework/Versions/%s*/Headers/jni.h'%version
568 jni_dirs = ['/usr/lib/jvm/java-*-sun-%s*/include/jni.h'%version,
569 '/usr/java/jdk%s*/include/jni.h'%version,
572 dirs = self.paths(jni_dirs)
575 d=os.path.dirname(self.paths(jni_dirs)[0])
578 if sys.platform == 'win32':
579 result.append(os.path.join(d,'win32'))
580 elif sys.platform == 'linux2':
581 result.append(os.path.join(d,'linux'))
585 def java_where_java_home(self,version=None):
586 if sys.platform[:6] == 'darwin':
588 home = '/System/Library/Frameworks/JavaVM.framework/Home'
590 home = '/System/Library/Frameworks/JavaVM.framework/Versions/%s/Home' % version
592 jar = self.java_where_jar(version)
593 home = os.path.normpath('%s/..'%jar)
594 if os.path.isdir(home):
596 print("Could not determine JAVA_HOME: %s is not a directory" % home)
599 def java_where_jar(self, version=None):
600 ENV = self.java_ENV(version)
601 if self.detect_tool('jar', ENV=ENV):
602 where_jar = self.detect('JAR', 'jar', ENV=ENV)
604 where_jar = self.where_is('jar', ENV['PATH'])
606 self.skip_test("Could not find Java jar, skipping test(s).\n")
609 def java_where_java(self, version=None):
611 Return a path to the java executable.
613 ENV = self.java_ENV(version)
614 where_java = self.where_is('java', ENV['PATH'])
616 self.skip_test("Could not find Java java, skipping test(s).\n")
619 def java_where_javac(self, version=None):
621 Return a path to the javac compiler.
623 ENV = self.java_ENV(version)
624 if self.detect_tool('javac'):
625 where_javac = self.detect('JAVAC', 'javac', ENV=ENV)
627 where_javac = self.where_is('javac', ENV['PATH'])
629 self.skip_test("Could not find Java javac, skipping test(s).\n")
630 self.run(program = where_javac,
631 arguments = '-version',
635 if string.find(self.stderr(), 'javac %s' % version) == -1:
636 fmt = "Could not find javac for Java version %s, skipping test(s).\n"
637 self.skip_test(fmt % version)
639 m = re.search(r'javac (\d\.\d)', self.stderr())
644 return where_javac, version
646 def java_where_javah(self, version=None):
647 ENV = self.java_ENV(version)
648 if self.detect_tool('javah'):
649 where_javah = self.detect('JAVAH', 'javah', ENV=ENV)
651 where_javah = self.where_is('javah', ENV['PATH'])
653 self.skip_test("Could not find Java javah, skipping test(s).\n")
656 def java_where_rmic(self, version=None):
657 ENV = self.java_ENV(version)
658 if self.detect_tool('rmic'):
659 where_rmic = self.detect('RMIC', 'rmic', ENV=ENV)
661 where_rmic = self.where_is('rmic', ENV['PATH'])
663 self.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n")
666 def Qt_dummy_installation(self, dir='qt'):
667 # create a dummy qt installation
669 self.subdir( dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'] )
671 self.write([dir, 'bin', 'mymoc.py'], """\
676 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:', [])
680 for opt, arg in cmd_opts:
681 if opt == '-o': output = open(arg, 'wb')
682 elif opt == '-i': impl = 1
683 else: opt_string = opt_string + ' ' + opt
685 contents = open(a, 'rb').read()
686 a = string.replace(a, '\\\\', '\\\\\\\\')
687 subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
689 contents = re.sub( r'#include.*', '', contents )
690 output.write(string.replace(contents, 'Q_OBJECT', subst))
695 self.write([dir, 'bin', 'myuic.py'], """\
704 for arg in sys.argv[1:]:
706 output = open(arg, 'wb')
718 source = open(arg, 'rb')
721 output.write( '#include "' + impl + '"\\n' )
722 includes = re.findall('<include.*?>(.*?)</include>', source.read())
723 for incFile in includes:
724 # this is valid for ui.h files, at least
725 if os.path.exists(incFile):
726 output.write('#include "' + incFile + '"\\n')
728 output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
733 self.write([dir, 'include', 'my_qobject.h'], r"""
735 void my_qt_symbol(const char *arg);
738 self.write([dir, 'lib', 'my_qobject.cpp'], r"""
739 #include "../include/my_qobject.h"
741 void my_qt_symbol(const char *arg) {
746 self.write([dir, 'lib', 'SConstruct'], r"""
749 if sys.platform == 'win32':
750 env.StaticLibrary( 'myqt', 'my_qobject.cpp' )
752 env.SharedLibrary( 'myqt', 'my_qobject.cpp' )
755 self.run(chdir = self.workpath(dir, 'lib'),
758 match = self.match_re_dotall)
760 self.QT = self.workpath(dir)
762 self.QT_MOC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'mymoc.py'))
763 self.QT_UIC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'myuic.py'))
764 self.QT_LIB_DIR = self.workpath(dir, 'lib')
766 def Qt_create_SConstruct(self, place):
767 if type(place) is type([]):
768 place = apply(test.workpath, place)
769 self.write(place, """\
770 if ARGUMENTS.get('noqtdir', 0): QTDIR=None
772 env = Environment(QTDIR = QTDIR,
776 tools=['default','qt'])
778 if ARGUMENTS.get('variant_dir', 0):
779 if ARGUMENTS.get('chdir', 0):
783 dup=int(ARGUMENTS.get('dup', 1))
785 builddir = 'build_dup0'
789 VariantDir(builddir, '.', duplicate=dup)
791 sconscript = Dir(builddir).File('SConscript')
793 sconscript = File('SConscript')
795 SConscript( sconscript )
796 """ % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
799 NCR = 0 # non-cached rebuild
800 CR = 1 # cached rebuild (up to date)
801 NCF = 2 # non-cached build failure
802 CF = 3 # cached build failure
804 if sys.platform == 'win32':
805 Configure_lib = 'msvcrt'
809 # to use cygwin compilers on cmd.exe -> uncomment following line
812 def checkLogAndStdout(self, checks, results, cached,
813 logfile, sconf_dir, sconstruct,
814 doCheckLog=1, doCheckStdout=1):
817 def __init__(self, p):
820 def matchPart(log, logfile, lastEnd, NoMatch=NoMatch):
821 m = re.match(log, logfile[lastEnd:])
823 raise NoMatch, lastEnd
824 return m.end() + lastEnd
826 #print len(os.linesep)
829 for i in range(len(ls)):
833 nols = nols + "[^" + ls[i] + "])"
838 logfile = self.read(self.workpath(logfile))
840 string.find( logfile, "scons: warning: The stored build "
841 "information has an unexpected class." ) >= 0):
843 sconf_dir = sconf_dir
844 sconstruct = sconstruct
846 log = r'file\ \S*%s\,line \d+:' % re.escape(sconstruct) + ls
847 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
848 log = "\t" + re.escape("Configure(confdir = %s)" % sconf_dir) + ls
849 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
852 for check,result,cache_desc in zip(checks, results, cached):
853 log = re.escape("scons: Configure: " + check) + ls
854 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
857 for bld_desc in cache_desc: # each TryXXX
858 for ext, flag in bld_desc: # each file in TryBuild
859 file = os.path.join(sconf_dir,"conftest_%d%s" % (cnt, ext))
862 if ext in ['.c', '.cpp']:
863 log=log + re.escape(file + " <-") + ls
864 log=log + r"( \|" + nols + "*" + ls + ")+?"
866 log=log + "(" + nols + "*" + ls +")*?"
871 re.escape("scons: Configure: \"%s\" is up to date."
873 log=log+re.escape("scons: Configure: The original builder "
875 log=log+r"( \|.*"+ls+")+"
877 # non-cached rebuild failure
878 log=log + "(" + nols + "*" + ls + ")*?"
881 # cached rebuild failure
883 re.escape("scons: Configure: Building \"%s\" failed "
884 "in a previous run and all its sources are"
885 " up to date." % file) + ls
886 log=log+re.escape("scons: Configure: The original builder "
888 log=log+r"( \|.*"+ls+")+"
891 result = "(cached) " + result
892 rdstr = rdstr + re.escape(check) + re.escape(result) + "\n"
893 log=log + re.escape("scons: Configure: " + result) + ls + ls
894 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
896 if doCheckLog: lastEnd = matchPart(ls, logfile, lastEnd)
897 if doCheckLog and lastEnd != len(logfile):
898 raise NoMatch, lastEnd
901 print "Cannot match log file against log regexp."
903 print "------------------------------------------------------"
904 print logfile[m.pos:]
905 print "------------------------------------------------------"
907 print "------------------------------------------------------"
909 print "------------------------------------------------------"
913 exp_stdout = self.wrap_stdout(".*", rdstr)
914 if not self.match_re_dotall(self.stdout(), exp_stdout):
915 print "Unexpected stdout: "
916 print "-----------------------------------------------------"
917 print repr(self.stdout())
918 print "-----------------------------------------------------"
919 print repr(exp_stdout)
920 print "-----------------------------------------------------"
923 def get_python_version(self):
925 Returns the Python version (just so everyone doesn't have to
926 hand-code slicing the right number of characters).
928 # see also sys.prefix documentation
929 return python_minor_version_string()
931 def get_platform_python_info(self):
933 Returns a path to a Python executable suitable for testing on
934 this platform and its associated include path, library path,
937 python = self.where_is('python')
939 self.skip_test('Can not find installed "python", skipping test.\n')
941 self.run(program = python, stdin = """\
944 py_ver = 'python%d.%d' % sys.version_info[:2]
945 except AttributeError:
946 py_ver = 'python' + sys.version[:3]
947 print os.path.join(sys.prefix, 'include', py_ver)
948 print os.path.join(sys.prefix, 'lib', py_ver, 'config')
952 return [python] + string.split(string.strip(self.stdout()), '\n')
954 def wait_for(self, fname, timeout=10.0, popen=None):
956 Waits for the specified file name to exist.
959 while not os.path.exists(fname):
960 if timeout and waited >= timeout:
961 sys.stderr.write('timed out waiting for %s to exist\n' % fname)
968 waited = waited + 1.0
970 def get_alt_cpp_suffix(self):
972 Many CXX tests have this same logic.
973 They all needed to determine if the current os supports
974 files with .C and .c as different files or not
975 in which case they are instructed to use .cpp instead of .C
977 if not case_sensitive_suffixes('.c','.C'):
978 alt_cpp_suffix = '.cpp'
980 alt_cpp_suffix = '.C'
981 return alt_cpp_suffix
985 def __init__(self, name, units, expression, convert=None):
987 convert = lambda x: x
990 self.expression = re.compile(expression)
991 self.convert = convert
994 Stat('memory-initial', 'kbytes',
995 r'Memory before reading SConscript files:\s+(\d+)',
996 convert=lambda s: int(s) / 1024),
997 Stat('memory-prebuild', 'kbytes',
998 r'Memory before building targets:\s+(\d+)',
999 convert=lambda s: int(s) / 1024),
1000 Stat('memory-final', 'kbytes',
1001 r'Memory after building targets:\s+(\d+)',
1002 convert=lambda s: int(s) / 1024),
1004 Stat('time-sconscript', 'seconds',
1005 r'Total SConscript file execution time:\s+([\d.]+) seconds'),
1006 Stat('time-scons', 'seconds',
1007 r'Total SCons execution time:\s+([\d.]+) seconds'),
1008 Stat('time-commands', 'seconds',
1009 r'Total command execution time:\s+([\d.]+) seconds'),
1010 Stat('time-total', 'seconds',
1011 r'Total build time:\s+([\d.]+) seconds'),
1015 class TimeSCons(TestSCons):
1016 """Class for timing SCons."""
1017 def __init__(self, *args, **kw):
1019 In addition to normal TestSCons.TestSCons intialization,
1020 this enables verbose mode (which causes the command lines to
1021 be displayed in the output) and copies the contents of the
1022 directory containing the executing script to the temporary
1025 self.variables = kw.get('variables')
1026 if self.variables is not None:
1027 for variable, value in self.variables.items():
1028 value = os.environ.get(variable, value)
1033 value = float(value)
1036 self.variables[variable] = value
1039 self.calibrate = os.environ.get('TIMESCONS_CALIBRATE', '0') != '0'
1041 if not kw.has_key('verbose') and not self.calibrate:
1042 kw['verbose'] = True
1045 #TestSCons.__init__(self, *args, **kw)
1046 apply(TestSCons.__init__, (self,)+args, kw)
1048 # TODO(sgk): better way to get the script dir than sys.argv[0]
1049 test_dir = os.path.dirname(sys.argv[0])
1050 test_name = os.path.basename(test_dir)
1052 if not os.path.isabs(test_dir):
1053 test_dir = os.path.join(self.orig_cwd, test_dir)
1054 self.copy_timing_configuration(test_dir, self.workpath())
1056 def main(self, *args, **kw):
1058 The main entry point for standard execution of timings.
1060 This method run SCons three times:
1062 Once with the --help option, to have it exit after just reading
1065 Once as a full build of all targets.
1067 Once again as a (presumably) null or up-to-date build of
1070 The elapsed time to execute each build is printed after
1073 if not kw.has_key('options') and self.variables:
1075 for variable, value in self.variables.items():
1076 options.append('%s=%s' % (variable, value))
1077 kw['options'] = ' '.join(options)
1080 #self.calibration(*args, **kw)
1081 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)
1114 fp = open('/proc/loadavg')
1115 except EnvironmentError:
1118 avg1, avg5, avg15 = fp.readline().split(" ")[:3]
1120 self.trace('load-average', 'average1', avg1, 'processes')
1121 self.trace('load-average', 'average5', avg5, 'processes')
1122 self.trace('load-average', 'average15', avg15, 'processes')
1124 def collect_stats(self, input):
1126 for stat in StatList:
1127 m = stat.expression.search(input)
1129 value = stat.convert(m.group(1))
1130 # The dict keys match the keyword= arguments
1131 # of the trace() method above so they can be
1132 # applied directly to that call.
1133 result[stat.name] = {'value':value, 'units':stat.units}
1136 def help(self, *args, **kw):
1138 Runs scons with the --help option.
1140 This serves as a way to isolate just the amount of time spent
1141 reading up the configuration, since --help exits before any
1142 "real work" is done.
1144 kw['options'] = kw.get('options', '') + ' --help'
1145 # Ignore the exit status. If the --help run dies, we just
1146 # won't report any statistics for it, but we can still execute
1147 # the full and null builds.
1150 #self.run(*args, **kw)
1151 apply(self.run, args, kw)
1152 sys.stdout.write(self.stdout())
1153 stats = self.collect_stats(self.stdout())
1154 # Delete the time-commands, since no commands are ever
1155 # executed on the help run and it is (or should be) always 0.0.
1156 del stats['time-commands']
1157 self.report_traces('help', stats)
1159 def full(self, *args, **kw):
1161 Runs a full build of SCons.
1164 #self.run(*args, **kw)
1165 apply(self.run, args, kw)
1166 sys.stdout.write(self.stdout())
1167 stats = self.collect_stats(self.stdout())
1168 self.report_traces('full', stats)
1170 #self.trace('full-memory', 'initial', **stats['memory-initial'])
1171 #self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
1172 #self.trace('full-memory', 'final', **stats['memory-final'])
1173 apply(self.trace, ('full-memory', 'initial'), stats['memory-initial'])
1174 apply(self.trace, ('full-memory', 'prebuild'), stats['memory-prebuild'])
1175 apply(self.trace, ('full-memory', 'final'), stats['memory-final'])
1177 def calibration(self, *args, **kw):
1179 Runs a full build of SCons, but only reports calibration
1180 information (the variable(s) that were set for this configuration,
1181 and the elapsed time to run.
1184 #self.run(*args, **kw)
1185 apply(self.run, args, kw)
1187 for variable, value in self.variables.items():
1188 sys.stdout.write('VARIABLE: %s=%s\n' % (variable, value))
1189 sys.stdout.write('ELAPSED: %s\n' % self.elapsed_time())
1191 def null(self, *args, **kw):
1193 Runs an up-to-date null build of SCons.
1195 # TODO(sgk): allow the caller to specify the target (argument)
1196 # that must be up-to-date.
1198 #self.up_to_date(arguments='.', **kw)
1200 kw['arguments'] = '.'
1201 apply(self.up_to_date, (), kw)
1202 sys.stdout.write(self.stdout())
1203 stats = self.collect_stats(self.stdout())
1204 # time-commands should always be 0.0 on a null build, because
1205 # no commands should be executed. Remove it from the stats
1206 # so we don't trace it, but only if it *is* 0 so that we'll
1207 # get some indication if a supposedly-null build actually does
1209 if float(stats['time-commands']['value']) == 0.0:
1210 del stats['time-commands']
1211 self.report_traces('null', stats)
1213 #self.trace('null-memory', 'initial', **stats['memory-initial'])
1214 #self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
1215 #self.trace('null-memory', 'final', **stats['memory-final'])
1216 apply(self.trace, ('null-memory', 'initial'), stats['memory-initial'])
1217 apply(self.trace, ('null-memory', 'prebuild'), stats['memory-prebuild'])
1218 apply(self.trace, ('null-memory', 'final'), stats['memory-final'])
1220 def elapsed_time(self):
1222 Returns the elapsed time of the most recent command execution.
1224 return self.endTime - self.startTime
1226 def run(self, *args, **kw):
1228 Runs a single build command, capturing output in the specified file.
1230 Because this class is about timing SCons, we record the start
1231 and end times of the elapsed execution, and also add the
1232 --debug=memory and --debug=time options to have SCons report
1233 its own memory and timing statistics.
1235 kw['options'] = kw.get('options', '') + ' --debug=memory --debug=time'
1236 self.startTime = time.time()
1239 #result = TestSCons.run(self, *args, **kw)
1240 result = apply(TestSCons.run, (self,)+args, kw)
1242 self.endTime = time.time()
1245 def copy_timing_configuration(self, source_dir, dest_dir):
1247 Copies the timing configuration from the specified source_dir (the
1248 directory in which the controlling script lives) to the specified
1249 dest_dir (a temporary working directory).
1251 This ignores all files and directories that begin with the string
1252 'TimeSCons-', and all '.svn' subdirectories.
1254 for root, dirs, files in os.walk(source_dir):
1258 #dirs = [ d for d in dirs if not d.startswith('TimeSCons-') ]
1259 #files = [ f for f in files if not f.startswith('TimeSCons-') ]
1260 not_timescons_entries = lambda s: not s.startswith('TimeSCons-')
1261 dirs = filter(not_timescons_entries, dirs)
1262 files = filter(not_timescons_entries, files)
1263 for dirname in dirs:
1264 source = os.path.join(root, dirname)
1265 destination = source.replace(source_dir, dest_dir)
1266 os.mkdir(destination)
1267 if sys.platform != 'win32':
1268 shutil.copystat(source, destination)
1269 for filename in files:
1270 source = os.path.join(root, filename)
1271 destination = source.replace(source_dir, dest_dir)
1272 shutil.copy2(source, destination)
1275 # In some environments, $AR will generate a warning message to stderr
1276 # if the library doesn't previously exist and is being created. One
1277 # way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
1278 # but this is difficult to do in a platform-/implementation-specific
1279 # method. Instead, we will use the following as a stderr match for
1280 # tests that use AR so that we will view zero or more "ar: creating
1281 # <file>" messages to be successful executions of the test (see
1282 # test/AR.py for sample usage).
1284 noisy_ar=r'(ar: creating( archive)? \S+\n?)*'
1288 # indent-tabs-mode:nil
1290 # vim: set expandtab tabstop=4 shiftwidth=4: