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)))
37 from TestCommon import *
38 from TestCommon import __all__
40 # Some tests which verify that SCons has been packaged properly need to
41 # look for specific version file names. Replicating the version number
42 # here provides some independent verification that what we packaged
43 # conforms to what we expect.
45 default_version = '1.2.0'
47 copyright_years = '2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009'
49 # In the checked-in source, the value of SConsVersion in the following
50 # line must remain "__ VERSION __" (without the spaces) so the built
51 # version in build/QMTest/TestSCons.py contains the actual version
52 # string of the packages that have been built.
53 SConsVersion = '__VERSION__'
54 if SConsVersion == '__' + 'VERSION' + '__':
55 SConsVersion = default_version
57 __all__.extend([ 'TestSCons',
78 except AttributeError:
79 # Windows doesn't have a uname() function. We could use something like
80 # sys.platform as a fallback, but that's not really a "machine," so
81 # just leave it as None.
85 machine = machine_map.get(machine, machine)
87 python = python_executable
88 _python_ = '"' + python_executable + '"'
99 """Test whether -lfrtbegin is required. This can probably be done in
100 a more reliable way, but using popen3 is relatively efficient."""
110 stderr = popen2.popen3(cmd)[2]
114 p = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE)
117 for l in stderr.readlines():
118 list = string.split(l)
119 if len(list) > 3 and list[:2] == ['gcc', 'version']:
120 if list[2][:3] in ('4.1','4.2','4.3'):
121 libs = ['gfortranbegin']
123 if list[2][:2] in ('3.', '4.'):
124 libs = ['frtbegin'] + libs
129 if sys.platform == 'cygwin':
130 # On Cygwin, os.path.normcase() lies, so just report back the
131 # fact that the underlying Win32 OS is case-insensitive.
132 def case_sensitive_suffixes(s1, s2):
135 def case_sensitive_suffixes(s1, s2):
136 return (os.path.normcase(s1) != os.path.normcase(s2))
139 if sys.platform == 'win32':
140 fortran_lib = gccFortranLibs()
141 elif sys.platform == 'cygwin':
142 fortran_lib = gccFortranLibs()
143 elif string.find(sys.platform, 'irix') != -1:
144 fortran_lib = ['ftn']
146 fortran_lib = gccFortranLibs()
150 file_expr = r"""File "[^"]*", line \d+, in .+
153 # re.escape escapes too much.
155 for c in ['.', '[', ']', '(', ')', '*', '+', '?']: # Not an exhaustive list.
156 str = string.replace(str, c, '\\' + c)
163 except AttributeError:
164 # Pre-1.6 Python has no sys.version_info
165 version_string = string.split(sys.version)[0]
166 version_ints = map(int, string.split(version_string, '.'))
167 sys.version_info = tuple(version_ints + ['final', 0])
169 def python_version_string():
170 return string.split(sys.version)[0]
172 def python_minor_version_string():
173 return sys.version[:3]
175 def unsupported_python_version(version=sys.version_info):
176 return version < (1, 5, 2)
178 def deprecated_python_version(version=sys.version_info):
179 return version < (2, 4, 0)
181 if deprecated_python_version():
183 scons: warning: Support for pre-2.4 Python (%s) is deprecated.
184 If this will cause hardship, contact dev@scons.tigris.org.
187 deprecated_python_expr = re_escape(msg % python_version_string()) + file_expr
190 deprecated_python_expr = ""
194 class TestSCons(TestCommon):
195 """Class for testing SCons.
197 This provides a common place for initializing SCons tests,
198 eliminating the need to begin every test with the same repeated
202 scons_version = SConsVersion
204 def __init__(self, **kw):
205 """Initialize an SCons testing object.
207 If they're not overridden by keyword arguments, this
208 initializes the object with the following default values:
210 program = 'scons' if it exists,
212 interpreter = 'python'
216 The workdir value means that, by default, a temporary workspace
217 directory is created for a TestSCons environment. In addition,
218 this method changes directory (chdir) to the workspace directory,
219 so an explicit "chdir = '.'" on all of the run() method calls
222 self.orig_cwd = os.getcwd()
224 script_dir = os.environ['SCONS_SCRIPT_DIR']
229 if not kw.has_key('program'):
230 kw['program'] = os.environ.get('SCONS')
231 if not kw['program']:
232 if os.path.exists('scons'):
233 kw['program'] = 'scons'
235 kw['program'] = 'scons.py'
236 if not kw.has_key('interpreter') and not os.environ.get('SCONS_EXEC'):
237 kw['interpreter'] = [python, '-tt']
238 if not kw.has_key('match'):
239 kw['match'] = match_exact
240 if not kw.has_key('workdir'):
243 # Term causing test failures due to bogus readline init
244 # control character output on FC8
245 # TERM can cause test failures due to control chars in prompts etc.
246 os.environ['TERM'] = 'dumb'
248 self.ignore_python_version=kw.get('ignore_python_version',1)
249 if kw.get('ignore_python_version',-1) != -1:
250 del kw['ignore_python_version']
252 if self.ignore_python_version and deprecated_python_version():
253 sconsflags = os.environ.get('SCONSFLAGS')
255 sconsflags = [sconsflags]
258 sconsflags = sconsflags + ['--warn=no-python-version']
259 os.environ['SCONSFLAGS'] = string.join(sconsflags)
261 apply(TestCommon.__init__, [self], kw)
264 if SCons.Node.FS.default_fs is None:
265 SCons.Node.FS.default_fs = SCons.Node.FS.FS()
267 def Environment(self, ENV=None, *args, **kw):
269 Return a construction Environment that optionally overrides
270 the default external environment with the specified ENV.
272 import SCons.Environment
277 return apply(SCons.Environment.Environment, args, kw)
278 except (SCons.Errors.UserError, SCons.Errors.InternalError):
281 def detect(self, var, prog=None, ENV=None, norm=None):
283 Detect a program named 'prog' by first checking the construction
284 variable named 'var' and finally searching the path used by
285 SCons. If either method fails to detect the program, then false
286 is returned, otherwise the full path to prog is returned. If
287 prog is None, then the value of the environment variable will be
290 env = self.Environment(ENV)
291 v = env.subst('$'+var)
298 result = env.WhereIs(prog)
299 if norm and os.sep != '/':
300 result = string.replace(result, os.sep, '/')
303 def detect_tool(self, tool, prog=None, ENV=None):
305 Given a tool (i.e., tool specification that would be passed
306 to the "tools=" parameter of Environment()) and a program that
307 corresponds to that tool, return true if and only if we can find
308 that tool using Environment.Detect().
310 By default, prog is set to the value passed into the tools parameter.
315 env = self.Environment(ENV, tools=[tool])
318 return env.Detect([prog])
320 def where_is(self, prog, path=None):
322 Given a program, search for it in the specified external PATH,
323 or in the actual external PATH is none is specified.
325 import SCons.Environment
326 env = SCons.Environment.Environment()
328 path = os.environ['PATH']
329 return env.WhereIs(prog, path)
331 def wrap_stdout(self, build_str = "", read_str = "", error = 0, cleaning = 0):
332 """Wraps standard output string(s) in the normal
333 "Reading ... done" and "Building ... done" strings
335 cap,lc = [ ('Build','build'),
336 ('Clean','clean') ][cleaning]
338 term = "scons: %sing terminated because of errors.\n" % lc
340 term = "scons: done %sing targets.\n" % lc
341 return "scons: Reading SConscript files ...\n" + \
343 "scons: done reading SConscript files.\n" + \
344 "scons: %sing targets ...\n" % cap + \
348 def run(self, *args, **kw):
350 Add the --warn=no-python-version option to SCONSFLAGS every
351 command so test scripts don't have to filter out Python version
352 deprecation warnings.
353 Same for --warn=no-visual-c-missing.
355 save_sconsflags = os.environ.get('SCONSFLAGS')
357 sconsflags = [save_sconsflags]
360 if self.ignore_python_version and deprecated_python_version():
361 sconsflags = sconsflags + ['--warn=no-python-version']
362 sconsflags = sconsflags + ['--warn=no-visual-c-missing']
363 os.environ['SCONSFLAGS'] = string.join(sconsflags)
365 result = apply(TestCommon.run, (self,)+args, kw)
367 sconsflags = save_sconsflags
370 def up_to_date(self, options = None, arguments = None, read_str = "", **kw):
372 for arg in string.split(arguments):
373 s = s + "scons: `%s' is up to date.\n" % arg
375 arguments = options + " " + arguments
376 kw['arguments'] = arguments
377 stdout = self.wrap_stdout(read_str = read_str, build_str = s)
378 # Append '.*' so that timing output that comes after the
379 # up-to-date output is okay.
380 kw['stdout'] = re.escape(stdout) + '.*'
381 kw['match'] = self.match_re_dotall
382 apply(self.run, [], kw)
384 def not_up_to_date(self, options = None, arguments = None, **kw):
385 """Asserts that none of the targets listed in arguments is
386 up to date, but does not make any assumptions on other targets.
387 This function is most useful in conjunction with the -n option.
390 for arg in string.split(arguments):
391 s = s + "(?!scons: `%s' is up to date.)" % re.escape(arg)
393 arguments = options + " " + arguments
394 s = '('+s+'[^\n]*\n)*'
395 kw['arguments'] = arguments
396 stdout = re.escape(self.wrap_stdout(build_str='ARGUMENTSGOHERE'))
397 kw['stdout'] = string.replace(stdout, 'ARGUMENTSGOHERE', s)
398 kw['match'] = self.match_re_dotall
399 apply(self.run, [], kw)
401 def diff_substr(self, expect, actual, prelen=20, postlen=40):
403 for x, y in zip(expect, actual):
405 return "Actual did not match expect at char %d:\n" \
408 % (i, repr(expect[i-prelen:i+postlen]),
409 repr(actual[i-prelen:i+postlen]))
411 return "Actual matched the expected output???"
413 def python_file_line(self, file, line):
415 Returns a Python error line for output comparisons.
417 The exec of the traceback line gives us the correct format for
418 this version of Python. Before 2.5, this yielded:
420 File "<string>", line 1, ?
422 Python 2.5 changed this to:
424 File "<string>", line 1, <module>
426 We stick the requested file name and line number in the right
427 places, abstracting out the version difference.
429 exec 'import traceback; x = traceback.format_stack()[-1]'
431 x = string.replace(x, '<string>', file)
432 x = string.replace(x, 'line 1,', 'line %s,' % line)
435 def normalize_pdf(self, s):
436 s = re.sub(r'/(Creation|Mod)Date \(D:[^)]*\)',
437 r'/\1Date (D:XXXX)', s)
438 s = re.sub(r'/ID \[<[0-9a-fA-F]*> <[0-9a-fA-F]*>\]',
439 r'/ID [<XXXX> <XXXX>]', s)
440 s = re.sub(r'/(BaseFont|FontName) /[A-Z]{6}',
442 s = re.sub(r'/Length \d+ *\n/Filter /FlateDecode\n',
443 r'/Length XXXX\n/Filter /FlateDecode\n', s)
451 begin_marker = '/FlateDecode\n>>\nstream\n'
452 end_marker = 'endstream\nendobj'
455 b = string.find(s, begin_marker, 0)
457 b = b + len(begin_marker)
458 e = string.find(s, end_marker, b)
459 encoded.append((b, e))
460 b = string.find(s, begin_marker, e + len(end_marker))
466 d = zlib.decompress(s[b:e])
467 d = re.sub(r'%%CreationDate: [^\n]*\n',
468 r'%%CreationDate: 1970 Jan 01 00:00:00\n', d)
469 d = re.sub(r'%DVIPSSource: TeX output \d\d\d\d\.\d\d\.\d\d:\d\d\d\d',
470 r'%DVIPSSource: TeX output 1970.01.01:0000', d)
471 d = re.sub(r'/(BaseFont|FontName) /[A-Z]{6}',
476 s = string.join(r, '')
480 def paths(self,patterns):
490 def java_ENV(self, version=None):
492 Initialize with a default external environment that uses a local
493 Java SDK in preference to whatever's found in the default PATH.
496 return self._java_env[version]['ENV']
497 except AttributeError:
502 import SCons.Environment
503 env = SCons.Environment.Environment()
504 self._java_env[version] = env
509 '/usr/java/jdk%s*/bin' % version,
510 '/usr/lib/jvm/*-%s*/bin' % version,
511 '/usr/local/j2sdk%s*/bin' % version,
513 java_path = self.paths(patterns) + [env['ENV']['PATH']]
516 '/usr/java/latest/bin',
517 '/usr/lib/jvm/*/bin',
518 '/usr/local/j2sdk*/bin',
520 java_path = self.paths(patterns) + [env['ENV']['PATH']]
522 env['ENV']['PATH'] = string.join(java_path, os.pathsep)
525 def java_where_includes(self,version=None):
527 Return java include paths compiling java jni code
533 frame = '/System/Library/Frameworks/JavaVM.framework/Headers/jni.h'
535 frame = '/System/Library/Frameworks/JavaVM.framework/Versions/%s*/Headers/jni.h'%version
536 jni_dirs = ['/usr/lib/jvm/java-*-sun-%s*/include/jni.h'%version,
537 '/usr/java/jdk%s*/include/jni.h'%version,
540 dirs = self.paths(jni_dirs)
543 d=os.path.dirname(self.paths(jni_dirs)[0])
546 if sys.platform == 'win32':
547 result.append(os.path.join(d,'win32'))
548 elif sys.platform == 'linux2':
549 result.append(os.path.join(d,'linux'))
553 def java_where_java_home(self,version=None):
554 if sys.platform[:6] == 'darwin':
556 home = '/System/Library/Frameworks/JavaVM.framework/Home'
558 home = '/System/Library/Frameworks/JavaVM.framework/Versions/%s/Home' % version
560 jar = self.java_where_jar(version)
561 home = os.path.normpath('%s/..'%jar)
562 if os.path.isdir(home):
564 print("Could not determine JAVA_HOME: %s is not a directory" % home)
567 def java_where_jar(self, version=None):
568 ENV = self.java_ENV(version)
569 if self.detect_tool('jar', ENV=ENV):
570 where_jar = self.detect('JAR', 'jar', ENV=ENV)
572 where_jar = self.where_is('jar', ENV['PATH'])
574 self.skip_test("Could not find Java jar, skipping test(s).\n")
577 def java_where_java(self, version=None):
579 Return a path to the java executable.
581 ENV = self.java_ENV(version)
582 where_java = self.where_is('java', ENV['PATH'])
584 self.skip_test("Could not find Java java, skipping test(s).\n")
587 def java_where_javac(self, version=None):
589 Return a path to the javac compiler.
591 ENV = self.java_ENV(version)
592 if self.detect_tool('javac'):
593 where_javac = self.detect('JAVAC', 'javac', ENV=ENV)
595 where_javac = self.where_is('javac', ENV['PATH'])
597 self.skip_test("Could not find Java javac, skipping test(s).\n")
598 self.run(program = where_javac,
599 arguments = '-version',
603 if string.find(self.stderr(), 'javac %s' % version) == -1:
604 fmt = "Could not find javac for Java version %s, skipping test(s).\n"
605 self.skip_test(fmt % version)
607 m = re.search(r'javac (\d\.\d)', self.stderr())
612 return where_javac, version
614 def java_where_javah(self, version=None):
615 ENV = self.java_ENV(version)
616 if self.detect_tool('javah'):
617 where_javah = self.detect('JAVAH', 'javah', ENV=ENV)
619 where_javah = self.where_is('javah', ENV['PATH'])
621 self.skip_test("Could not find Java javah, skipping test(s).\n")
624 def java_where_rmic(self, version=None):
625 ENV = self.java_ENV(version)
626 if self.detect_tool('rmic'):
627 where_rmic = self.detect('RMIC', 'rmic', ENV=ENV)
629 where_rmic = self.where_is('rmic', ENV['PATH'])
631 self.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n")
634 def Qt_dummy_installation(self, dir='qt'):
635 # create a dummy qt installation
637 self.subdir( dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'] )
639 self.write([dir, 'bin', 'mymoc.py'], """\
644 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:', [])
648 for opt, arg in cmd_opts:
649 if opt == '-o': output = open(arg, 'wb')
650 elif opt == '-i': impl = 1
651 else: opt_string = opt_string + ' ' + opt
653 contents = open(a, 'rb').read()
654 a = string.replace(a, '\\\\', '\\\\\\\\')
655 subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
657 contents = re.sub( r'#include.*', '', contents )
658 output.write(string.replace(contents, 'Q_OBJECT', subst))
663 self.write([dir, 'bin', 'myuic.py'], """\
672 for arg in sys.argv[1:]:
674 output = open(arg, 'wb')
686 source = open(arg, 'rb')
689 output.write( '#include "' + impl + '"\\n' )
690 includes = re.findall('<include.*?>(.*?)</include>', source.read())
691 for incFile in includes:
692 # this is valid for ui.h files, at least
693 if os.path.exists(incFile):
694 output.write('#include "' + incFile + '"\\n')
696 output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
701 self.write([dir, 'include', 'my_qobject.h'], r"""
703 void my_qt_symbol(const char *arg);
706 self.write([dir, 'lib', 'my_qobject.cpp'], r"""
707 #include "../include/my_qobject.h"
709 void my_qt_symbol(const char *arg) {
714 self.write([dir, 'lib', 'SConstruct'], r"""
717 if sys.platform == 'win32':
718 env.StaticLibrary( 'myqt', 'my_qobject.cpp' )
720 env.SharedLibrary( 'myqt', 'my_qobject.cpp' )
723 self.run(chdir = self.workpath(dir, 'lib'),
726 match = self.match_re_dotall)
728 self.QT = self.workpath(dir)
730 self.QT_MOC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'mymoc.py'))
731 self.QT_UIC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'myuic.py'))
732 self.QT_LIB_DIR = self.workpath(dir, 'lib')
734 def Qt_create_SConstruct(self, place):
735 if type(place) is type([]):
736 place = apply(test.workpath, place)
737 self.write(place, """\
738 if ARGUMENTS.get('noqtdir', 0): QTDIR=None
740 env = Environment(QTDIR = QTDIR,
744 tools=['default','qt'])
746 if ARGUMENTS.get('variant_dir', 0):
747 if ARGUMENTS.get('chdir', 0):
751 dup=int(ARGUMENTS.get('dup', 1))
753 builddir = 'build_dup0'
757 VariantDir(builddir, '.', duplicate=dup)
759 sconscript = Dir(builddir).File('SConscript')
761 sconscript = File('SConscript')
763 SConscript( sconscript )
764 """ % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
767 NCR = 0 # non-cached rebuild
768 CR = 1 # cached rebuild (up to date)
769 NCF = 2 # non-cached build failure
770 CF = 3 # cached build failure
772 if sys.platform == 'win32':
773 Configure_lib = 'msvcrt'
777 # to use cygwin compilers on cmd.exe -> uncomment following line
780 def checkLogAndStdout(self, checks, results, cached,
781 logfile, sconf_dir, sconstruct,
782 doCheckLog=1, doCheckStdout=1):
785 def __init__(self, p):
788 def matchPart(log, logfile, lastEnd, NoMatch=NoMatch):
789 m = re.match(log, logfile[lastEnd:])
791 raise NoMatch, lastEnd
792 return m.end() + lastEnd
794 #print len(os.linesep)
797 for i in range(len(ls)):
801 nols = nols + "[^" + ls[i] + "])"
806 logfile = self.read(self.workpath(logfile))
808 string.find( logfile, "scons: warning: The stored build "
809 "information has an unexpected class." ) >= 0):
811 sconf_dir = sconf_dir
812 sconstruct = sconstruct
814 log = r'file\ \S*%s\,line \d+:' % re.escape(sconstruct) + ls
815 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
816 log = "\t" + re.escape("Configure(confdir = %s)" % sconf_dir) + ls
817 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
820 for check,result,cache_desc in zip(checks, results, cached):
821 log = re.escape("scons: Configure: " + check) + ls
822 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
825 for bld_desc in cache_desc: # each TryXXX
826 for ext, flag in bld_desc: # each file in TryBuild
827 file = os.path.join(sconf_dir,"conftest_%d%s" % (cnt, ext))
830 if ext in ['.c', '.cpp']:
831 log=log + re.escape(file + " <-") + ls
832 log=log + r"( \|" + nols + "*" + ls + ")+?"
834 log=log + "(" + nols + "*" + ls +")*?"
839 re.escape("scons: Configure: \"%s\" is up to date."
841 log=log+re.escape("scons: Configure: The original builder "
843 log=log+r"( \|.*"+ls+")+"
845 # non-cached rebuild failure
846 log=log + "(" + nols + "*" + ls + ")*?"
849 # cached rebuild failure
851 re.escape("scons: Configure: Building \"%s\" failed "
852 "in a previous run and all its sources are"
853 " up to date." % file) + ls
854 log=log+re.escape("scons: Configure: The original builder "
856 log=log+r"( \|.*"+ls+")+"
859 result = "(cached) " + result
860 rdstr = rdstr + re.escape(check) + re.escape(result) + "\n"
861 log=log + re.escape("scons: Configure: " + result) + ls + ls
862 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
864 if doCheckLog: lastEnd = matchPart(ls, logfile, lastEnd)
865 if doCheckLog and lastEnd != len(logfile):
866 raise NoMatch, lastEnd
869 print "Cannot match log file against log regexp."
871 print "------------------------------------------------------"
872 print logfile[m.pos:]
873 print "------------------------------------------------------"
875 print "------------------------------------------------------"
877 print "------------------------------------------------------"
881 exp_stdout = self.wrap_stdout(".*", rdstr)
882 if not self.match_re_dotall(self.stdout(), exp_stdout):
883 print "Unexpected stdout: "
884 print "-----------------------------------------------------"
885 print repr(self.stdout())
886 print "-----------------------------------------------------"
887 print repr(exp_stdout)
888 print "-----------------------------------------------------"
891 def get_python_version(self):
893 Returns the Python version (just so everyone doesn't have to
894 hand-code slicing the right number of characters).
896 # see also sys.prefix documentation
897 return python_minor_version_string()
899 def get_platform_python_info(self):
901 Returns a path to a Python executable suitable for testing on
902 this platform and its associated include path, library path,
905 python = self.where_is('python')
907 self.skip_test('Can not find installed "python", skipping test.\n')
909 self.run(program = python, stdin = """\
912 py_ver = 'python%d.%d' % sys.version_info[:2]
913 except AttributeError:
914 py_ver = 'python' + sys.version[:3]
915 print os.path.join(sys.prefix, 'include', py_ver)
916 print os.path.join(sys.prefix, 'lib', py_ver, 'config')
920 return [python] + string.split(string.strip(self.stdout()), '\n')
922 def wait_for(self, fname, timeout=10.0, popen=None):
924 Waits for the specified file name to exist.
927 while not os.path.exists(fname):
928 if timeout and waited >= timeout:
929 sys.stderr.write('timed out waiting for %s to exist\n' % fname)
936 waited = waited + 1.0
938 def get_alt_cpp_suffix(self):
940 Many CXX tests have this same logic.
941 They all needed to determine if the current os supports
942 files with .C and .c as different files or not
943 in which case they are instructed to use .cpp instead of .C
945 if not case_sensitive_suffixes('.c','.C'):
946 alt_cpp_suffix = '.cpp'
948 alt_cpp_suffix = '.C'
949 return alt_cpp_suffix
953 def __init__(self, name, units, expression, important=False):
956 self.expression = re.compile(expression)
957 self.important = important
960 Graph('TimeSCons-elapsed', 'seconds',
961 r'TimeSCons elapsed time:\s+([\d.]+)',
964 Graph('memory-initial', 'bytes',
965 r'Memory before reading SConscript files:\s+(\d+)'),
966 Graph('memory-prebuild', 'bytes',
967 r'Memory before building targets:\s+(\d+)'),
968 Graph('memory-final', 'bytes',
969 r'Memory after building targets:\s+(\d+)'),
971 Graph('time-sconscript', 'seconds',
972 r'Total SConscript file execution time:\s+([\d.]+) seconds'),
973 Graph('time-scons', 'seconds',
974 r'Total SCons execution time:\s+([\d.]+) seconds'),
975 Graph('time-commands', 'seconds',
976 r'Total command execution time:\s+([\d.]+) seconds'),
977 Graph('time-total', 'seconds',
978 r'Total build time:\s+([\d.]+) seconds'),
982 class TimeSCons(TestSCons):
983 """Class for timing SCons."""
984 def __init__(self, *args, **kw):
986 In addition to normal TestSCons.TestSCons intialization,
987 this enables verbose mode (which causes the command lines to
988 be displayed in the output) and copies the contents of the
989 directory containing the executing script to the temporary
992 if not kw.has_key('verbose'):
995 #TestSCons.__init__(self, *args, **kw)
996 apply(TestSCons.__init__, (self,)+args, kw)
998 # TODO(sgk): better way to get the script dir than sys.argv[0]
999 test_dir = os.path.dirname(sys.argv[0])
1000 test_name = os.path.basename(test_dir)
1002 if not os.path.isabs(test_dir):
1003 test_dir = os.path.join(self.orig_cwd, test_dir)
1004 self.copy_timing_configuration(test_dir, self.workpath())
1006 def main(self, *args, **kw):
1008 The main entry point for standard execution of timings.
1010 This method run SCons three times:
1012 Once with the --help option, to have it exit after just reading
1015 Once as a full build of all targets.
1017 Once again as a (presumably) null or up-to-date build of
1020 The elapsed time to execute each build is printed after
1024 #self.help(*args, **kw)
1025 #self.full(*args, **kw)
1026 #self.null(*args, **kw)
1027 apply(self.help, args, kw)
1028 apply(self.full, args, kw)
1029 apply(self.null, args, kw)
1031 def trace(self, graph, name, value, units):
1032 fmt = "TRACE: graph=%s name=%s value=%s units=%s\n"
1033 sys.stdout.write(fmt % (graph, name, value, units))
1036 def report_traces(self, trace, input):
1037 self.trace('TimeSCons-elapsed', trace, self.elapsed_time(), "seconds")
1038 for graph in GraphList:
1039 m = graph.expression.search(input)
1041 self.trace(graph.name, trace, m.group(1), graph.units)
1043 def help(self, *args, **kw):
1045 Runs scons with the --help option.
1047 This serves as a way to isolate just the amount of time spent
1048 reading up the configuration, since --help exits before any
1049 "real work" is done.
1051 kw['options'] = kw.get('options', '') + ' --help'
1053 #self.run(*args, **kw)
1054 apply(self.run, args, kw)
1055 sys.stdout.write(self.stdout())
1056 self.report_traces('help', self.stdout())
1058 def full(self, *args, **kw):
1060 Runs a full build of SCons.
1063 #self.run(*args, **kw)
1064 apply(self.run, args, kw)
1065 sys.stdout.write(self.stdout())
1066 self.report_traces('full', self.stdout())
1068 def null(self, *args, **kw):
1070 Runs an up-to-date null build of SCons.
1072 # TODO(sgk): allow the caller to specify the target (argument)
1073 # that must be up-to-date.
1075 #self.up_to_date(arguments='.', **kw)
1077 kw['arguments'] = '.'
1078 apply(self.up_to_date, (), kw)
1079 sys.stdout.write(self.stdout())
1080 self.report_traces('null', self.stdout())
1082 def elapsed_time(self):
1084 Returns the elapsed time of the most recent command execution.
1086 return self.endTime - self.startTime
1088 def run(self, *args, **kw):
1090 Runs a single build command, capturing output in the specified file.
1092 Because this class is about timing SCons, we record the start
1093 and end times of the elapsed execution, and also add the
1094 --debug=memory and --debug=time options to have SCons report
1095 its own memory and timing statistics.
1097 kw['options'] = kw.get('options', '') + ' --debug=memory --debug=time'
1098 self.startTime = time.time()
1101 #result = TestSCons.run(self, *args, **kw)
1102 result = apply(TestSCons.run, (self,)+args, kw)
1104 self.endTime = time.time()
1107 def copy_timing_configuration(self, source_dir, dest_dir):
1109 Copies the timing configuration from the specified source_dir (the
1110 directory in which the controlling script lives) to the specified
1111 dest_dir (a temporary working directory).
1113 This ignores all files and directories that begin with the string
1114 'TimeSCons-', and all '.svn' subdirectories.
1116 for root, dirs, files in os.walk(source_dir):
1120 #dirs = [ d for d in dirs if not d.startswith('TimeSCons-') ]
1121 #files = [ f for f in files if not f.startswith('TimeSCons-') ]
1122 not_timescons_entries = lambda s: not s.startswith('TimeSCons-')
1123 dirs = filter(not_timescons_entries, dirs)
1124 files = filter(not_timescons_entries, files)
1125 for dirname in dirs:
1126 source = os.path.join(root, dirname)
1127 destination = source.replace(source_dir, dest_dir)
1128 os.mkdir(destination)
1129 if sys.platform != 'win32':
1130 shutil.copystat(source, destination)
1131 for filename in files:
1132 source = os.path.join(root, filename)
1133 destination = source.replace(source_dir, dest_dir)
1134 shutil.copy2(source, destination)
1137 # In some environments, $AR will generate a warning message to stderr
1138 # if the library doesn't previously exist and is being created. One
1139 # way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
1140 # but this is difficult to do in a platform-/implementation-specific
1141 # method. Instead, we will use the following as a stderr match for
1142 # tests that use AR so that we will view zero or more "ar: creating
1143 # <file>" messages to be successful executions of the test (see
1144 # test/AR.py for sample usage).
1146 noisy_ar=r'(ar: creating( archive)? \S+\n?)*'
1150 # indent-tabs-mode:nil
1152 # vim: set expandtab tabstop=4 shiftwidth=4: