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 elif not os.path.isabs(kw['program']):
245 kw['program'] = os.path.join(self.orig_cwd, kw['program'])
246 if not kw.has_key('interpreter') and not os.environ.get('SCONS_EXEC'):
247 kw['interpreter'] = [python, '-tt']
248 if not kw.has_key('match'):
249 kw['match'] = match_exact
250 if not kw.has_key('workdir'):
253 # Term causing test failures due to bogus readline init
254 # control character output on FC8
255 # TERM can cause test failures due to control chars in prompts etc.
256 os.environ['TERM'] = 'dumb'
258 self.ignore_python_version=kw.get('ignore_python_version',1)
259 if kw.get('ignore_python_version',-1) != -1:
260 del kw['ignore_python_version']
262 if self.ignore_python_version and deprecated_python_version():
263 sconsflags = os.environ.get('SCONSFLAGS')
265 sconsflags = [sconsflags]
268 sconsflags = sconsflags + ['--warn=no-python-version']
269 os.environ['SCONSFLAGS'] = string.join(sconsflags)
271 apply(TestCommon.__init__, [self], kw)
274 if SCons.Node.FS.default_fs is None:
275 SCons.Node.FS.default_fs = SCons.Node.FS.FS()
277 def Environment(self, ENV=None, *args, **kw):
279 Return a construction Environment that optionally overrides
280 the default external environment with the specified ENV.
282 import SCons.Environment
287 return apply(SCons.Environment.Environment, args, kw)
288 except (SCons.Errors.UserError, SCons.Errors.InternalError):
291 def detect(self, var, prog=None, ENV=None, norm=None):
293 Detect a program named 'prog' by first checking the construction
294 variable named 'var' and finally searching the path used by
295 SCons. If either method fails to detect the program, then false
296 is returned, otherwise the full path to prog is returned. If
297 prog is None, then the value of the environment variable will be
300 env = self.Environment(ENV)
301 v = env.subst('$'+var)
308 result = env.WhereIs(prog)
309 if norm and os.sep != '/':
310 result = string.replace(result, os.sep, '/')
313 def detect_tool(self, tool, prog=None, ENV=None):
315 Given a tool (i.e., tool specification that would be passed
316 to the "tools=" parameter of Environment()) and a program that
317 corresponds to that tool, return true if and only if we can find
318 that tool using Environment.Detect().
320 By default, prog is set to the value passed into the tools parameter.
325 env = self.Environment(ENV, tools=[tool])
328 return env.Detect([prog])
330 def where_is(self, prog, path=None):
332 Given a program, search for it in the specified external PATH,
333 or in the actual external PATH is none is specified.
335 import SCons.Environment
336 env = SCons.Environment.Environment()
338 path = os.environ['PATH']
339 return env.WhereIs(prog, path)
341 def wrap_stdout(self, build_str = "", read_str = "", error = 0, cleaning = 0):
342 """Wraps standard output string(s) in the normal
343 "Reading ... done" and "Building ... done" strings
345 cap,lc = [ ('Build','build'),
346 ('Clean','clean') ][cleaning]
348 term = "scons: %sing terminated because of errors.\n" % lc
350 term = "scons: done %sing targets.\n" % lc
351 return "scons: Reading SConscript files ...\n" + \
353 "scons: done reading SConscript files.\n" + \
354 "scons: %sing targets ...\n" % cap + \
358 def run(self, *args, **kw):
360 Add the --warn=no-python-version option to SCONSFLAGS every
361 command so test scripts don't have to filter out Python version
362 deprecation warnings.
363 Same for --warn=no-visual-c-missing.
365 save_sconsflags = os.environ.get('SCONSFLAGS')
367 sconsflags = [save_sconsflags]
370 if self.ignore_python_version and deprecated_python_version():
371 sconsflags = sconsflags + ['--warn=no-python-version']
372 # Provide a way to suppress or provide alternate flags for
373 # TestSCons purposes by setting TESTSCONS_SCONSFLAGS.
374 # (The intended use case is to set it to null when running
375 # timing tests of earlier versions of SCons which don't
376 # support the --warn=no-visual-c-missing warning.)
377 sconsflags = sconsflags + [os.environ.get('TESTSCONS_SCONSFLAGS',
378 '--warn=no-visual-c-missing')]
379 os.environ['SCONSFLAGS'] = string.join(sconsflags)
381 result = apply(TestCommon.run, (self,)+args, kw)
383 sconsflags = save_sconsflags
386 def up_to_date(self, options = None, arguments = None, read_str = "", **kw):
388 for arg in string.split(arguments):
389 s = s + "scons: `%s' is up to date.\n" % arg
391 arguments = options + " " + arguments
392 kw['arguments'] = arguments
393 stdout = self.wrap_stdout(read_str = read_str, build_str = s)
394 # Append '.*' so that timing output that comes after the
395 # up-to-date output is okay.
396 kw['stdout'] = re.escape(stdout) + '.*'
397 kw['match'] = self.match_re_dotall
398 apply(self.run, [], kw)
400 def not_up_to_date(self, options = None, arguments = None, **kw):
401 """Asserts that none of the targets listed in arguments is
402 up to date, but does not make any assumptions on other targets.
403 This function is most useful in conjunction with the -n option.
406 for arg in string.split(arguments):
407 s = s + "(?!scons: `%s' is up to date.)" % re.escape(arg)
409 arguments = options + " " + arguments
410 s = '('+s+'[^\n]*\n)*'
411 kw['arguments'] = arguments
412 stdout = re.escape(self.wrap_stdout(build_str='ARGUMENTSGOHERE'))
413 kw['stdout'] = string.replace(stdout, 'ARGUMENTSGOHERE', s)
414 kw['match'] = self.match_re_dotall
415 apply(self.run, [], kw)
417 def option_not_yet_implemented(self, option, arguments=None, **kw):
419 Verifies expected behavior for options that are not yet implemented:
420 a warning message, and exit status 1.
422 msg = "Warning: the %s option is not yet implemented\n" % option
425 # If it's a long option and the argument string begins with '=',
426 # it's of the form --foo=bar and needs no separating space.
427 if option[:2] == '--' and arguments[0] == '=':
428 kw['arguments'] = option + arguments
430 kw['arguments'] = option + ' ' + arguments
432 #return self.run(**kw)
433 return apply(self.run, (), kw)
435 def diff_substr(self, expect, actual, prelen=20, postlen=40):
437 for x, y in zip(expect, actual):
439 return "Actual did not match expect at char %d:\n" \
442 % (i, repr(expect[i-prelen:i+postlen]),
443 repr(actual[i-prelen:i+postlen]))
445 return "Actual matched the expected output???"
447 def python_file_line(self, file, line):
449 Returns a Python error line for output comparisons.
451 The exec of the traceback line gives us the correct format for
452 this version of Python. Before 2.5, this yielded:
454 File "<string>", line 1, ?
456 Python 2.5 changed this to:
458 File "<string>", line 1, <module>
460 We stick the requested file name and line number in the right
461 places, abstracting out the version difference.
463 exec 'import traceback; x = traceback.format_stack()[-1]'
465 x = string.replace(x, '<string>', file)
466 x = string.replace(x, 'line 1,', 'line %s,' % line)
469 def normalize_pdf(self, s):
470 s = re.sub(r'/(Creation|Mod)Date \(D:[^)]*\)',
471 r'/\1Date (D:XXXX)', s)
472 s = re.sub(r'/ID \[<[0-9a-fA-F]*> <[0-9a-fA-F]*>\]',
473 r'/ID [<XXXX> <XXXX>]', s)
474 s = re.sub(r'/(BaseFont|FontName) /[A-Z]{6}',
476 s = re.sub(r'/Length \d+ *\n/Filter /FlateDecode\n',
477 r'/Length XXXX\n/Filter /FlateDecode\n', s)
485 begin_marker = '/FlateDecode\n>>\nstream\n'
486 end_marker = 'endstream\nendobj'
489 b = string.find(s, begin_marker, 0)
491 b = b + len(begin_marker)
492 e = string.find(s, end_marker, b)
493 encoded.append((b, e))
494 b = string.find(s, begin_marker, e + len(end_marker))
500 d = zlib.decompress(s[b:e])
501 d = re.sub(r'%%CreationDate: [^\n]*\n',
502 r'%%CreationDate: 1970 Jan 01 00:00:00\n', d)
503 d = re.sub(r'%DVIPSSource: TeX output \d\d\d\d\.\d\d\.\d\d:\d\d\d\d',
504 r'%DVIPSSource: TeX output 1970.01.01:0000', d)
505 d = re.sub(r'/(BaseFont|FontName) /[A-Z]{6}',
510 s = string.join(r, '')
514 def paths(self,patterns):
524 def java_ENV(self, version=None):
526 Initialize with a default external environment that uses a local
527 Java SDK in preference to whatever's found in the default PATH.
530 return self._java_env[version]['ENV']
531 except AttributeError:
536 import SCons.Environment
537 env = SCons.Environment.Environment()
538 self._java_env[version] = env
543 '/usr/java/jdk%s*/bin' % version,
544 '/usr/lib/jvm/*-%s*/bin' % version,
545 '/usr/local/j2sdk%s*/bin' % version,
547 java_path = self.paths(patterns) + [env['ENV']['PATH']]
550 '/usr/java/latest/bin',
551 '/usr/lib/jvm/*/bin',
552 '/usr/local/j2sdk*/bin',
554 java_path = self.paths(patterns) + [env['ENV']['PATH']]
556 env['ENV']['PATH'] = string.join(java_path, os.pathsep)
559 def java_where_includes(self,version=None):
561 Return java include paths compiling java jni code
567 frame = '/System/Library/Frameworks/JavaVM.framework/Headers/jni.h'
569 frame = '/System/Library/Frameworks/JavaVM.framework/Versions/%s*/Headers/jni.h'%version
570 jni_dirs = ['/usr/lib/jvm/java-*-sun-%s*/include/jni.h'%version,
571 '/usr/java/jdk%s*/include/jni.h'%version,
574 dirs = self.paths(jni_dirs)
577 d=os.path.dirname(self.paths(jni_dirs)[0])
580 if sys.platform == 'win32':
581 result.append(os.path.join(d,'win32'))
582 elif sys.platform == 'linux2':
583 result.append(os.path.join(d,'linux'))
587 def java_where_java_home(self,version=None):
588 if sys.platform[:6] == 'darwin':
590 home = '/System/Library/Frameworks/JavaVM.framework/Home'
592 home = '/System/Library/Frameworks/JavaVM.framework/Versions/%s/Home' % version
594 jar = self.java_where_jar(version)
595 home = os.path.normpath('%s/..'%jar)
596 if os.path.isdir(home):
598 print("Could not determine JAVA_HOME: %s is not a directory" % home)
601 def java_where_jar(self, version=None):
602 ENV = self.java_ENV(version)
603 if self.detect_tool('jar', ENV=ENV):
604 where_jar = self.detect('JAR', 'jar', ENV=ENV)
606 where_jar = self.where_is('jar', ENV['PATH'])
608 self.skip_test("Could not find Java jar, skipping test(s).\n")
611 def java_where_java(self, version=None):
613 Return a path to the java executable.
615 ENV = self.java_ENV(version)
616 where_java = self.where_is('java', ENV['PATH'])
618 self.skip_test("Could not find Java java, skipping test(s).\n")
621 def java_where_javac(self, version=None):
623 Return a path to the javac compiler.
625 ENV = self.java_ENV(version)
626 if self.detect_tool('javac'):
627 where_javac = self.detect('JAVAC', 'javac', ENV=ENV)
629 where_javac = self.where_is('javac', ENV['PATH'])
631 self.skip_test("Could not find Java javac, skipping test(s).\n")
632 self.run(program = where_javac,
633 arguments = '-version',
637 if string.find(self.stderr(), 'javac %s' % version) == -1:
638 fmt = "Could not find javac for Java version %s, skipping test(s).\n"
639 self.skip_test(fmt % version)
641 m = re.search(r'javac (\d\.\d)', self.stderr())
646 return where_javac, version
648 def java_where_javah(self, version=None):
649 ENV = self.java_ENV(version)
650 if self.detect_tool('javah'):
651 where_javah = self.detect('JAVAH', 'javah', ENV=ENV)
653 where_javah = self.where_is('javah', ENV['PATH'])
655 self.skip_test("Could not find Java javah, skipping test(s).\n")
658 def java_where_rmic(self, version=None):
659 ENV = self.java_ENV(version)
660 if self.detect_tool('rmic'):
661 where_rmic = self.detect('RMIC', 'rmic', ENV=ENV)
663 where_rmic = self.where_is('rmic', ENV['PATH'])
665 self.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n")
668 def Qt_dummy_installation(self, dir='qt'):
669 # create a dummy qt installation
671 self.subdir( dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'] )
673 self.write([dir, 'bin', 'mymoc.py'], """\
678 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:', [])
682 for opt, arg in cmd_opts:
683 if opt == '-o': output = open(arg, 'wb')
684 elif opt == '-i': impl = 1
685 else: opt_string = opt_string + ' ' + opt
687 contents = open(a, 'rb').read()
688 a = string.replace(a, '\\\\', '\\\\\\\\')
689 subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
691 contents = re.sub( r'#include.*', '', contents )
692 output.write(string.replace(contents, 'Q_OBJECT', subst))
697 self.write([dir, 'bin', 'myuic.py'], """\
706 for arg in sys.argv[1:]:
708 output = open(arg, 'wb')
720 source = open(arg, 'rb')
723 output.write( '#include "' + impl + '"\\n' )
724 includes = re.findall('<include.*?>(.*?)</include>', source.read())
725 for incFile in includes:
726 # this is valid for ui.h files, at least
727 if os.path.exists(incFile):
728 output.write('#include "' + incFile + '"\\n')
730 output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
735 self.write([dir, 'include', 'my_qobject.h'], r"""
737 void my_qt_symbol(const char *arg);
740 self.write([dir, 'lib', 'my_qobject.cpp'], r"""
741 #include "../include/my_qobject.h"
743 void my_qt_symbol(const char *arg) {
748 self.write([dir, 'lib', 'SConstruct'], r"""
751 if sys.platform == 'win32':
752 env.StaticLibrary( 'myqt', 'my_qobject.cpp' )
754 env.SharedLibrary( 'myqt', 'my_qobject.cpp' )
757 self.run(chdir = self.workpath(dir, 'lib'),
760 match = self.match_re_dotall)
762 self.QT = self.workpath(dir)
764 self.QT_MOC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'mymoc.py'))
765 self.QT_UIC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'myuic.py'))
766 self.QT_LIB_DIR = self.workpath(dir, 'lib')
768 def Qt_create_SConstruct(self, place):
769 if type(place) is type([]):
770 place = apply(test.workpath, place)
771 self.write(place, """\
772 if ARGUMENTS.get('noqtdir', 0): QTDIR=None
774 env = Environment(QTDIR = QTDIR,
778 tools=['default','qt'])
780 if ARGUMENTS.get('variant_dir', 0):
781 if ARGUMENTS.get('chdir', 0):
785 dup=int(ARGUMENTS.get('dup', 1))
787 builddir = 'build_dup0'
791 VariantDir(builddir, '.', duplicate=dup)
793 sconscript = Dir(builddir).File('SConscript')
795 sconscript = File('SConscript')
797 SConscript( sconscript )
798 """ % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
801 NCR = 0 # non-cached rebuild
802 CR = 1 # cached rebuild (up to date)
803 NCF = 2 # non-cached build failure
804 CF = 3 # cached build failure
806 if sys.platform == 'win32':
807 Configure_lib = 'msvcrt'
811 # to use cygwin compilers on cmd.exe -> uncomment following line
814 def checkLogAndStdout(self, checks, results, cached,
815 logfile, sconf_dir, sconstruct,
816 doCheckLog=1, doCheckStdout=1):
819 def __init__(self, p):
822 def matchPart(log, logfile, lastEnd, NoMatch=NoMatch):
823 m = re.match(log, logfile[lastEnd:])
825 raise NoMatch, lastEnd
826 return m.end() + lastEnd
828 #print len(os.linesep)
831 for i in range(len(ls)):
835 nols = nols + "[^" + ls[i] + "])"
840 logfile = self.read(self.workpath(logfile))
842 string.find( logfile, "scons: warning: The stored build "
843 "information has an unexpected class." ) >= 0):
845 sconf_dir = sconf_dir
846 sconstruct = sconstruct
848 log = r'file\ \S*%s\,line \d+:' % re.escape(sconstruct) + ls
849 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
850 log = "\t" + re.escape("Configure(confdir = %s)" % sconf_dir) + ls
851 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
854 for check,result,cache_desc in zip(checks, results, cached):
855 log = re.escape("scons: Configure: " + check) + ls
856 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
859 for bld_desc in cache_desc: # each TryXXX
860 for ext, flag in bld_desc: # each file in TryBuild
861 file = os.path.join(sconf_dir,"conftest_%d%s" % (cnt, ext))
864 if ext in ['.c', '.cpp']:
865 log=log + re.escape(file + " <-") + ls
866 log=log + r"( \|" + nols + "*" + ls + ")+?"
868 log=log + "(" + nols + "*" + ls +")*?"
873 re.escape("scons: Configure: \"%s\" is up to date."
875 log=log+re.escape("scons: Configure: The original builder "
877 log=log+r"( \|.*"+ls+")+"
879 # non-cached rebuild failure
880 log=log + "(" + nols + "*" + ls + ")*?"
883 # cached rebuild failure
885 re.escape("scons: Configure: Building \"%s\" failed "
886 "in a previous run and all its sources are"
887 " up to date." % file) + ls
888 log=log+re.escape("scons: Configure: The original builder "
890 log=log+r"( \|.*"+ls+")+"
893 result = "(cached) " + result
894 rdstr = rdstr + re.escape(check) + re.escape(result) + "\n"
895 log=log + re.escape("scons: Configure: " + result) + ls + ls
896 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
898 if doCheckLog: lastEnd = matchPart(ls, logfile, lastEnd)
899 if doCheckLog and lastEnd != len(logfile):
900 raise NoMatch, lastEnd
903 print "Cannot match log file against log regexp."
905 print "------------------------------------------------------"
906 print logfile[m.pos:]
907 print "------------------------------------------------------"
909 print "------------------------------------------------------"
911 print "------------------------------------------------------"
915 exp_stdout = self.wrap_stdout(".*", rdstr)
916 if not self.match_re_dotall(self.stdout(), exp_stdout):
917 print "Unexpected stdout: "
918 print "-----------------------------------------------------"
919 print repr(self.stdout())
920 print "-----------------------------------------------------"
921 print repr(exp_stdout)
922 print "-----------------------------------------------------"
925 def get_python_version(self):
927 Returns the Python version (just so everyone doesn't have to
928 hand-code slicing the right number of characters).
930 # see also sys.prefix documentation
931 return python_minor_version_string()
933 def get_platform_python_info(self):
935 Returns a path to a Python executable suitable for testing on
936 this platform and its associated include path, library path,
939 python = self.where_is('python')
941 self.skip_test('Can not find installed "python", skipping test.\n')
943 self.run(program = python, stdin = """\
946 py_ver = 'python%d.%d' % sys.version_info[:2]
947 except AttributeError:
948 py_ver = 'python' + sys.version[:3]
949 print os.path.join(sys.prefix, 'include', py_ver)
950 print os.path.join(sys.prefix, 'lib', py_ver, 'config')
954 return [python] + string.split(string.strip(self.stdout()), '\n')
956 def wait_for(self, fname, timeout=10.0, popen=None):
958 Waits for the specified file name to exist.
961 while not os.path.exists(fname):
962 if timeout and waited >= timeout:
963 sys.stderr.write('timed out waiting for %s to exist\n' % fname)
970 waited = waited + 1.0
972 def get_alt_cpp_suffix(self):
974 Many CXX tests have this same logic.
975 They all needed to determine if the current os supports
976 files with .C and .c as different files or not
977 in which case they are instructed to use .cpp instead of .C
979 if not case_sensitive_suffixes('.c','.C'):
980 alt_cpp_suffix = '.cpp'
982 alt_cpp_suffix = '.C'
983 return alt_cpp_suffix
987 def __init__(self, name, units, expression, convert=None):
989 convert = lambda x: x
992 self.expression = re.compile(expression)
993 self.convert = convert
996 Stat('memory-initial', 'kbytes',
997 r'Memory before reading SConscript files:\s+(\d+)',
998 convert=lambda s: int(s) / 1024),
999 Stat('memory-prebuild', 'kbytes',
1000 r'Memory before building targets:\s+(\d+)',
1001 convert=lambda s: int(s) / 1024),
1002 Stat('memory-final', 'kbytes',
1003 r'Memory after building targets:\s+(\d+)',
1004 convert=lambda s: int(s) / 1024),
1006 Stat('time-sconscript', 'seconds',
1007 r'Total SConscript file execution time:\s+([\d.]+) seconds'),
1008 Stat('time-scons', 'seconds',
1009 r'Total SCons execution time:\s+([\d.]+) seconds'),
1010 Stat('time-commands', 'seconds',
1011 r'Total command execution time:\s+([\d.]+) seconds'),
1012 Stat('time-total', 'seconds',
1013 r'Total build time:\s+([\d.]+) seconds'),
1017 class TimeSCons(TestSCons):
1018 """Class for timing SCons."""
1019 def __init__(self, *args, **kw):
1021 In addition to normal TestSCons.TestSCons intialization,
1022 this enables verbose mode (which causes the command lines to
1023 be displayed in the output) and copies the contents of the
1024 directory containing the executing script to the temporary
1027 self.variables = kw.get('variables')
1028 if self.variables is not None:
1029 for variable, value in self.variables.items():
1030 value = os.environ.get(variable, value)
1035 value = float(value)
1038 self.variables[variable] = value
1041 self.calibrate = os.environ.get('TIMESCONS_CALIBRATE', '0') != '0'
1043 if not kw.has_key('verbose') and not self.calibrate:
1044 kw['verbose'] = True
1047 #TestSCons.__init__(self, *args, **kw)
1048 apply(TestSCons.__init__, (self,)+args, kw)
1050 # TODO(sgk): better way to get the script dir than sys.argv[0]
1051 test_dir = os.path.dirname(sys.argv[0])
1052 test_name = os.path.basename(test_dir)
1054 if not os.path.isabs(test_dir):
1055 test_dir = os.path.join(self.orig_cwd, test_dir)
1056 self.copy_timing_configuration(test_dir, self.workpath())
1058 def main(self, *args, **kw):
1060 The main entry point for standard execution of timings.
1062 This method run SCons three times:
1064 Once with the --help option, to have it exit after just reading
1067 Once as a full build of all targets.
1069 Once again as a (presumably) null or up-to-date build of
1072 The elapsed time to execute each build is printed after
1075 if not kw.has_key('options') and self.variables:
1077 for variable, value in self.variables.items():
1078 options.append('%s=%s' % (variable, value))
1079 kw['options'] = ' '.join(options)
1082 #self.calibration(*args, **kw)
1083 apply(self.calibration, args, kw)
1087 #self.help(*args, **kw)
1088 #self.full(*args, **kw)
1089 #self.null(*args, **kw)
1090 apply(self.help, args, kw)
1091 apply(self.full, args, kw)
1092 apply(self.null, args, kw)
1094 def trace(self, graph, name, value, units, sort=None):
1095 fmt = "TRACE: graph=%s name=%s value=%s units=%s"
1096 line = fmt % (graph, name, value, units)
1097 if sort is not None:
1098 line = line + (' sort=%s' % sort)
1100 sys.stdout.write(line)
1103 def report_traces(self, trace, stats):
1104 self.trace('TimeSCons-elapsed',
1106 self.elapsed_time(),
1109 for name, args in stats.items():
1111 #self.trace(name, trace, *args)
1112 apply(self.trace, (name, trace), args)
1116 fp = open('/proc/loadavg')
1117 except EnvironmentError:
1120 avg1, avg5, avg15 = fp.readline().split(" ")[:3]
1122 self.trace('load-average', 'average1', avg1, 'processes')
1123 self.trace('load-average', 'average5', avg5, 'processes')
1124 self.trace('load-average', 'average15', avg15, 'processes')
1126 def collect_stats(self, input):
1128 for stat in StatList:
1129 m = stat.expression.search(input)
1131 value = stat.convert(m.group(1))
1132 # The dict keys match the keyword= arguments
1133 # of the trace() method above so they can be
1134 # applied directly to that call.
1135 result[stat.name] = {'value':value, 'units':stat.units}
1138 def help(self, *args, **kw):
1140 Runs scons with the --help option.
1142 This serves as a way to isolate just the amount of time spent
1143 reading up the configuration, since --help exits before any
1144 "real work" is done.
1146 kw['options'] = kw.get('options', '') + ' --help'
1147 # Ignore the exit status. If the --help run dies, we just
1148 # won't report any statistics for it, but we can still execute
1149 # the full and null builds.
1152 #self.run(*args, **kw)
1153 apply(self.run, args, kw)
1154 sys.stdout.write(self.stdout())
1155 stats = self.collect_stats(self.stdout())
1156 # Delete the time-commands, since no commands are ever
1157 # executed on the help run and it is (or should be) always 0.0.
1158 del stats['time-commands']
1159 self.report_traces('help', stats)
1161 def full(self, *args, **kw):
1163 Runs a full build of SCons.
1166 #self.run(*args, **kw)
1167 apply(self.run, args, kw)
1168 sys.stdout.write(self.stdout())
1169 stats = self.collect_stats(self.stdout())
1170 self.report_traces('full', stats)
1172 #self.trace('full-memory', 'initial', **stats['memory-initial'])
1173 #self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
1174 #self.trace('full-memory', 'final', **stats['memory-final'])
1175 apply(self.trace, ('full-memory', 'initial'), stats['memory-initial'])
1176 apply(self.trace, ('full-memory', 'prebuild'), stats['memory-prebuild'])
1177 apply(self.trace, ('full-memory', 'final'), stats['memory-final'])
1179 def calibration(self, *args, **kw):
1181 Runs a full build of SCons, but only reports calibration
1182 information (the variable(s) that were set for this configuration,
1183 and the elapsed time to run.
1186 #self.run(*args, **kw)
1187 apply(self.run, args, kw)
1189 for variable, value in self.variables.items():
1190 sys.stdout.write('VARIABLE: %s=%s\n' % (variable, value))
1191 sys.stdout.write('ELAPSED: %s\n' % self.elapsed_time())
1193 def null(self, *args, **kw):
1195 Runs an up-to-date null build of SCons.
1197 # TODO(sgk): allow the caller to specify the target (argument)
1198 # that must be up-to-date.
1200 #self.up_to_date(arguments='.', **kw)
1202 kw['arguments'] = '.'
1203 apply(self.up_to_date, (), kw)
1204 sys.stdout.write(self.stdout())
1205 stats = self.collect_stats(self.stdout())
1206 # time-commands should always be 0.0 on a null build, because
1207 # no commands should be executed. Remove it from the stats
1208 # so we don't trace it, but only if it *is* 0 so that we'll
1209 # get some indication if a supposedly-null build actually does
1211 if float(stats['time-commands']['value']) == 0.0:
1212 del stats['time-commands']
1213 self.report_traces('null', stats)
1215 #self.trace('null-memory', 'initial', **stats['memory-initial'])
1216 #self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
1217 #self.trace('null-memory', 'final', **stats['memory-final'])
1218 apply(self.trace, ('null-memory', 'initial'), stats['memory-initial'])
1219 apply(self.trace, ('null-memory', 'prebuild'), stats['memory-prebuild'])
1220 apply(self.trace, ('null-memory', 'final'), stats['memory-final'])
1222 def elapsed_time(self):
1224 Returns the elapsed time of the most recent command execution.
1226 return self.endTime - self.startTime
1228 def run(self, *args, **kw):
1230 Runs a single build command, capturing output in the specified file.
1232 Because this class is about timing SCons, we record the start
1233 and end times of the elapsed execution, and also add the
1234 --debug=memory and --debug=time options to have SCons report
1235 its own memory and timing statistics.
1237 kw['options'] = kw.get('options', '') + ' --debug=memory --debug=time'
1238 self.startTime = time.time()
1241 #result = TestSCons.run(self, *args, **kw)
1242 result = apply(TestSCons.run, (self,)+args, kw)
1244 self.endTime = time.time()
1247 def copy_timing_configuration(self, source_dir, dest_dir):
1249 Copies the timing configuration from the specified source_dir (the
1250 directory in which the controlling script lives) to the specified
1251 dest_dir (a temporary working directory).
1253 This ignores all files and directories that begin with the string
1254 'TimeSCons-', and all '.svn' subdirectories.
1256 for root, dirs, files in os.walk(source_dir):
1260 #dirs = [ d for d in dirs if not d.startswith('TimeSCons-') ]
1261 #files = [ f for f in files if not f.startswith('TimeSCons-') ]
1262 not_timescons_entries = lambda s: not s.startswith('TimeSCons-')
1263 dirs = filter(not_timescons_entries, dirs)
1264 files = filter(not_timescons_entries, files)
1265 for dirname in dirs:
1266 source = os.path.join(root, dirname)
1267 destination = source.replace(source_dir, dest_dir)
1268 os.mkdir(destination)
1269 if sys.platform != 'win32':
1270 shutil.copystat(source, destination)
1271 for filename in files:
1272 source = os.path.join(root, filename)
1273 destination = source.replace(source_dir, dest_dir)
1274 shutil.copy2(source, destination)
1277 # In some environments, $AR will generate a warning message to stderr
1278 # if the library doesn't previously exist and is being created. One
1279 # way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
1280 # but this is difficult to do in a platform-/implementation-specific
1281 # method. Instead, we will use the following as a stderr match for
1282 # tests that use AR so that we will view zero or more "ar: creating
1283 # <file>" messages to be successful executions of the test (see
1284 # test/AR.py for sample usage).
1286 noisy_ar=r'(ar: creating( archive)? \S+\n?)*'
1290 # indent-tabs-mode:nil
1292 # vim: set expandtab tabstop=4 shiftwidth=4: