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.
16 from __future__ import generators ### KEEP FOR COMPATIBILITY FIXERS
18 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
34 from TestCommon import *
35 from TestCommon import __all__
37 # Some tests which verify that SCons has been packaged properly need to
38 # look for specific version file names. Replicating the version number
39 # here provides some independent verification that what we packaged
40 # conforms to what we expect.
42 default_version = '1.3.0'
44 copyright_years = '2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010'
46 # In the checked-in source, the value of SConsVersion in the following
47 # line must remain "__ VERSION __" (without the spaces) so the built
48 # version in build/QMTest/TestSCons.py contains the actual version
49 # string of the packages that have been built.
50 SConsVersion = '__VERSION__'
51 if SConsVersion == '__' + 'VERSION' + '__':
52 SConsVersion = default_version
54 __all__.extend([ 'TestSCons',
75 except AttributeError:
76 # Windows doesn't have a uname() function. We could use something like
77 # sys.platform as a fallback, but that's not really a "machine," so
78 # just leave it as None.
82 machine = machine_map.get(machine, machine)
84 python = python_executable
85 _python_ = '"' + python_executable + '"'
96 """Test whether -lfrtbegin is required. This can probably be done in
97 a more reliable way, but using popen3 is relatively efficient."""
107 stderr = popen2.popen3(cmd)[2]
111 p = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE)
114 for l in stderr.readlines():
116 if len(list) > 3 and list[:2] == ['gcc', 'version']:
117 if list[2][:3] in ('4.1','4.2','4.3'):
118 libs = ['gfortranbegin']
120 if list[2][:2] in ('3.', '4.'):
121 libs = ['frtbegin'] + libs
126 if sys.platform == 'cygwin':
127 # On Cygwin, os.path.normcase() lies, so just report back the
128 # fact that the underlying Win32 OS is case-insensitive.
129 def case_sensitive_suffixes(s1, s2):
132 def case_sensitive_suffixes(s1, s2):
133 return (os.path.normcase(s1) != os.path.normcase(s2))
136 if sys.platform == 'win32':
137 fortran_lib = gccFortranLibs()
138 elif sys.platform == 'cygwin':
139 fortran_lib = gccFortranLibs()
140 elif sys.platform.find('irix') != -1:
141 fortran_lib = ['ftn']
143 fortran_lib = gccFortranLibs()
147 file_expr = r"""File "[^"]*", line \d+, in .+
150 # re.escape escapes too much.
152 for c in ['.', '[', ']', '(', ')', '*', '+', '?']: # Not an exhaustive list.
153 str = str.replace(c, '\\' + c)
160 except AttributeError:
161 # Pre-1.6 Python has no sys.version_info
162 version_string = sys.version.split()[0]
163 version_ints = list(map(int, version_string.split('.')))
164 sys.version_info = tuple(version_ints + ['final', 0])
166 def python_version_string():
167 return sys.version.split()[0]
169 def python_minor_version_string():
170 return sys.version[:3]
172 def unsupported_python_version(version=sys.version_info):
173 return version < (1, 5, 2)
175 def deprecated_python_version(version=sys.version_info):
176 return version < (2, 4, 0)
178 if deprecated_python_version():
180 scons: warning: Support for pre-2.4 Python (%s) is deprecated.
181 If this will cause hardship, contact dev@scons.tigris.org.
184 deprecated_python_expr = re_escape(msg % python_version_string()) + file_expr
187 deprecated_python_expr = ""
191 class TestSCons(TestCommon):
192 """Class for testing SCons.
194 This provides a common place for initializing SCons tests,
195 eliminating the need to begin every test with the same repeated
199 scons_version = SConsVersion
201 def __init__(self, **kw):
202 """Initialize an SCons testing object.
204 If they're not overridden by keyword arguments, this
205 initializes the object with the following default values:
207 program = 'scons' if it exists,
209 interpreter = 'python'
213 The workdir value means that, by default, a temporary workspace
214 directory is created for a TestSCons environment. In addition,
215 this method changes directory (chdir) to the workspace directory,
216 so an explicit "chdir = '.'" on all of the run() method calls
219 self.orig_cwd = os.getcwd()
221 script_dir = os.environ['SCONS_SCRIPT_DIR']
226 if 'program' not in kw:
227 kw['program'] = os.environ.get('SCONS')
228 if not kw['program']:
229 if os.path.exists('scons'):
230 kw['program'] = 'scons'
232 kw['program'] = 'scons.py'
233 elif not os.path.isabs(kw['program']):
234 kw['program'] = os.path.join(self.orig_cwd, kw['program'])
235 if 'interpreter' not in kw and not os.environ.get('SCONS_EXEC'):
236 kw['interpreter'] = [python, '-tt']
237 if 'match' not in kw:
238 kw['match'] = match_exact
239 if 'workdir' not in kw:
242 # Term causing test failures due to bogus readline init
243 # control character output on FC8
244 # TERM can cause test failures due to control chars in prompts etc.
245 os.environ['TERM'] = 'dumb'
247 self.ignore_python_version=kw.get('ignore_python_version',1)
248 if kw.get('ignore_python_version',-1) != -1:
249 del kw['ignore_python_version']
251 if self.ignore_python_version and deprecated_python_version():
252 sconsflags = os.environ.get('SCONSFLAGS')
254 sconsflags = [sconsflags]
257 sconsflags = sconsflags + ['--warn=no-python-version']
258 os.environ['SCONSFLAGS'] = ' '.join(sconsflags)
260 TestCommon.__init__(self, **kw)
263 if SCons.Node.FS.default_fs is None:
264 SCons.Node.FS.default_fs = SCons.Node.FS.FS()
266 def Environment(self, ENV=None, *args, **kw):
268 Return a construction Environment that optionally overrides
269 the default external environment with the specified ENV.
271 import SCons.Environment
276 return SCons.Environment.Environment(*args, **kw)
277 except (SCons.Errors.UserError, SCons.Errors.InternalError):
280 def detect(self, var, prog=None, ENV=None, norm=None):
282 Detect a program named 'prog' by first checking the construction
283 variable named 'var' and finally searching the path used by
284 SCons. If either method fails to detect the program, then false
285 is returned, otherwise the full path to prog is returned. If
286 prog is None, then the value of the environment variable will be
289 env = self.Environment(ENV)
290 v = env.subst('$'+var)
297 result = env.WhereIs(prog)
298 if norm and os.sep != '/':
299 result = result.replace(os.sep, '/')
302 def detect_tool(self, tool, prog=None, ENV=None):
304 Given a tool (i.e., tool specification that would be passed
305 to the "tools=" parameter of Environment()) and a program that
306 corresponds to that tool, return true if and only if we can find
307 that tool using Environment.Detect().
309 By default, prog is set to the value passed into the tools parameter.
314 env = self.Environment(ENV, tools=[tool])
317 return env.Detect([prog])
319 def where_is(self, prog, path=None):
321 Given a program, search for it in the specified external PATH,
322 or in the actual external PATH is none is specified.
324 import SCons.Environment
325 env = SCons.Environment.Environment()
327 path = os.environ['PATH']
328 return env.WhereIs(prog, path)
330 def wrap_stdout(self, build_str = "", read_str = "", error = 0, cleaning = 0):
331 """Wraps standard output string(s) in the normal
332 "Reading ... done" and "Building ... done" strings
334 cap,lc = [ ('Build','build'),
335 ('Clean','clean') ][cleaning]
337 term = "scons: %sing terminated because of errors.\n" % lc
339 term = "scons: done %sing targets.\n" % lc
340 return "scons: Reading SConscript files ...\n" + \
342 "scons: done reading SConscript files.\n" + \
343 "scons: %sing targets ...\n" % cap + \
347 def run(self, *args, **kw):
349 Add the --warn=no-python-version option to SCONSFLAGS every
350 command so test scripts don't have to filter out Python version
351 deprecation warnings.
352 Same for --warn=no-visual-c-missing.
354 save_sconsflags = os.environ.get('SCONSFLAGS')
356 sconsflags = [save_sconsflags]
359 if self.ignore_python_version and deprecated_python_version():
360 sconsflags = sconsflags + ['--warn=no-python-version']
361 # Provide a way to suppress or provide alternate flags for
362 # TestSCons purposes by setting TESTSCONS_SCONSFLAGS.
363 # (The intended use case is to set it to null when running
364 # timing tests of earlier versions of SCons which don't
365 # support the --warn=no-visual-c-missing warning.)
366 sconsflags = sconsflags + [os.environ.get('TESTSCONS_SCONSFLAGS',
367 '--warn=no-visual-c-missing')]
368 os.environ['SCONSFLAGS'] = ' '.join(sconsflags)
370 result = TestCommon.run(self, *args, **kw)
372 sconsflags = save_sconsflags
375 def up_to_date(self, options = None, arguments = None, read_str = "", **kw):
377 for arg in arguments.split():
378 s = s + "scons: `%s' is up to date.\n" % arg
380 arguments = options + " " + arguments
381 kw['arguments'] = arguments
382 stdout = self.wrap_stdout(read_str = read_str, build_str = s)
383 # Append '.*' so that timing output that comes after the
384 # up-to-date output is okay.
385 kw['stdout'] = re.escape(stdout) + '.*'
386 kw['match'] = self.match_re_dotall
389 def not_up_to_date(self, options = None, arguments = None, **kw):
390 """Asserts that none of the targets listed in arguments is
391 up to date, but does not make any assumptions on other targets.
392 This function is most useful in conjunction with the -n option.
395 for arg in arguments.split():
396 s = s + "(?!scons: `%s' is up to date.)" % re.escape(arg)
398 arguments = options + " " + arguments
399 s = '('+s+'[^\n]*\n)*'
400 kw['arguments'] = arguments
401 stdout = re.escape(self.wrap_stdout(build_str='ARGUMENTSGOHERE'))
402 kw['stdout'] = stdout.replace('ARGUMENTSGOHERE', s)
403 kw['match'] = self.match_re_dotall
406 def option_not_yet_implemented(self, option, arguments=None, **kw):
408 Verifies expected behavior for options that are not yet implemented:
409 a warning message, and exit status 1.
411 msg = "Warning: the %s option is not yet implemented\n" % option
414 # If it's a long option and the argument string begins with '=',
415 # it's of the form --foo=bar and needs no separating space.
416 if option[:2] == '--' and arguments[0] == '=':
417 kw['arguments'] = option + arguments
419 kw['arguments'] = option + ' ' + arguments
421 #return self.run(**kw)
422 return self.run(**kw)
424 def diff_substr(self, expect, actual, prelen=20, postlen=40):
426 for x, y in zip(expect, actual):
428 return "Actual did not match expect at char %d:\n" \
431 % (i, repr(expect[i-prelen:i+postlen]),
432 repr(actual[i-prelen:i+postlen]))
434 return "Actual matched the expected output???"
436 def python_file_line(self, file, line):
438 Returns a Python error line for output comparisons.
440 The exec of the traceback line gives us the correct format for
441 this version of Python. Before 2.5, this yielded:
443 File "<string>", line 1, ?
445 Python 2.5 changed this to:
447 File "<string>", line 1, <module>
449 We stick the requested file name and line number in the right
450 places, abstracting out the version difference.
452 exec 'import traceback; x = traceback.format_stack()[-1]'
454 x = x.replace('<string>', file)
455 x = x.replace('line 1,', 'line %s,' % line)
458 def normalize_pdf(self, s):
459 s = re.sub(r'/(Creation|Mod)Date \(D:[^)]*\)',
460 r'/\1Date (D:XXXX)', s)
461 s = re.sub(r'/ID \[<[0-9a-fA-F]*> <[0-9a-fA-F]*>\]',
462 r'/ID [<XXXX> <XXXX>]', s)
463 s = re.sub(r'/(BaseFont|FontName) /[A-Z]{6}',
465 s = re.sub(r'/Length \d+ *\n/Filter /FlateDecode\n',
466 r'/Length XXXX\n/Filter /FlateDecode\n', s)
474 begin_marker = '/FlateDecode\n>>\nstream\n'
475 end_marker = 'endstream\nendobj'
478 b = s.find(begin_marker, 0)
480 b = b + len(begin_marker)
481 e = s.find(end_marker, b)
482 encoded.append((b, e))
483 b = s.find(begin_marker, e + len(end_marker))
489 d = zlib.decompress(s[b:e])
490 d = re.sub(r'%%CreationDate: [^\n]*\n',
491 r'%%CreationDate: 1970 Jan 01 00:00:00\n', d)
492 d = re.sub(r'%DVIPSSource: TeX output \d\d\d\d\.\d\d\.\d\d:\d\d\d\d',
493 r'%DVIPSSource: TeX output 1970.01.01:0000', d)
494 d = re.sub(r'/(BaseFont|FontName) /[A-Z]{6}',
503 def paths(self,patterns):
507 result.extend(sorted(glob.glob(p)))
511 def java_ENV(self, version=None):
513 Initialize with a default external environment that uses a local
514 Java SDK in preference to whatever's found in the default PATH.
517 return self._java_env[version]['ENV']
518 except AttributeError:
523 import SCons.Environment
524 env = SCons.Environment.Environment()
525 self._java_env[version] = env
530 '/usr/java/jdk%s*/bin' % version,
531 '/usr/lib/jvm/*-%s*/bin' % version,
532 '/usr/local/j2sdk%s*/bin' % version,
534 java_path = self.paths(patterns) + [env['ENV']['PATH']]
537 '/usr/java/latest/bin',
538 '/usr/lib/jvm/*/bin',
539 '/usr/local/j2sdk*/bin',
541 java_path = self.paths(patterns) + [env['ENV']['PATH']]
543 env['ENV']['PATH'] = os.pathsep.join(java_path)
546 def java_where_includes(self,version=None):
548 Return java include paths compiling java jni code
554 frame = '/System/Library/Frameworks/JavaVM.framework/Headers/jni.h'
556 frame = '/System/Library/Frameworks/JavaVM.framework/Versions/%s*/Headers/jni.h'%version
557 jni_dirs = ['/usr/lib/jvm/java-*-sun-%s*/include/jni.h'%version,
558 '/usr/java/jdk%s*/include/jni.h'%version,
561 dirs = self.paths(jni_dirs)
564 d=os.path.dirname(self.paths(jni_dirs)[0])
567 if sys.platform == 'win32':
568 result.append(os.path.join(d,'win32'))
569 elif sys.platform == 'linux2':
570 result.append(os.path.join(d,'linux'))
574 def java_where_java_home(self,version=None):
575 if sys.platform[:6] == 'darwin':
577 home = '/System/Library/Frameworks/JavaVM.framework/Home'
579 home = '/System/Library/Frameworks/JavaVM.framework/Versions/%s/Home' % version
581 jar = self.java_where_jar(version)
582 home = os.path.normpath('%s/..'%jar)
583 if os.path.isdir(home):
585 print("Could not determine JAVA_HOME: %s is not a directory" % home)
588 def java_where_jar(self, version=None):
589 ENV = self.java_ENV(version)
590 if self.detect_tool('jar', ENV=ENV):
591 where_jar = self.detect('JAR', 'jar', ENV=ENV)
593 where_jar = self.where_is('jar', ENV['PATH'])
595 self.skip_test("Could not find Java jar, skipping test(s).\n")
598 def java_where_java(self, version=None):
600 Return a path to the java executable.
602 ENV = self.java_ENV(version)
603 where_java = self.where_is('java', ENV['PATH'])
605 self.skip_test("Could not find Java java, skipping test(s).\n")
608 def java_where_javac(self, version=None):
610 Return a path to the javac compiler.
612 ENV = self.java_ENV(version)
613 if self.detect_tool('javac'):
614 where_javac = self.detect('JAVAC', 'javac', ENV=ENV)
616 where_javac = self.where_is('javac', ENV['PATH'])
618 self.skip_test("Could not find Java javac, skipping test(s).\n")
619 self.run(program = where_javac,
620 arguments = '-version',
624 if self.stderr().find('javac %s' % version) == -1:
625 fmt = "Could not find javac for Java version %s, skipping test(s).\n"
626 self.skip_test(fmt % version)
628 m = re.search(r'javac (\d\.\d)', self.stderr())
633 return where_javac, version
635 def java_where_javah(self, version=None):
636 ENV = self.java_ENV(version)
637 if self.detect_tool('javah'):
638 where_javah = self.detect('JAVAH', 'javah', ENV=ENV)
640 where_javah = self.where_is('javah', ENV['PATH'])
642 self.skip_test("Could not find Java javah, skipping test(s).\n")
645 def java_where_rmic(self, version=None):
646 ENV = self.java_ENV(version)
647 if self.detect_tool('rmic'):
648 where_rmic = self.detect('RMIC', 'rmic', ENV=ENV)
650 where_rmic = self.where_is('rmic', ENV['PATH'])
652 self.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n")
655 def Qt_dummy_installation(self, dir='qt'):
656 # create a dummy qt installation
658 self.subdir( dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'] )
660 self.write([dir, 'bin', 'mymoc.py'], """\
664 # -w and -z are fake options used in test/QT/QTFLAGS.py
665 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:wz', [])
669 for opt, arg in cmd_opts:
670 if opt == '-o': output = open(arg, 'wb')
671 elif opt == '-i': impl = 1
672 else: opt_string = opt_string + ' ' + opt
673 output.write("/* mymoc.py%s */\\n" % opt_string)
675 contents = open(a, 'rb').read()
676 a = a.replace('\\\\', '\\\\\\\\')
677 subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
679 contents = re.sub( r'#include.*', '', contents )
680 output.write(contents.replace('Q_OBJECT', subst))
685 self.write([dir, 'bin', 'myuic.py'], """\
694 for arg in sys.argv[1:]:
696 output = open(arg, 'wb')
705 elif arg[0:1] == "-":
706 opt_string = opt_string + ' ' + arg
710 source = open(arg, 'rb')
712 output.write("/* myuic.py%s */\\n" % opt_string)
714 output.write( '#include "' + impl + '"\\n' )
715 includes = re.findall('<include.*?>(.*?)</include>', source.read())
716 for incFile in includes:
717 # this is valid for ui.h files, at least
718 if os.path.exists(incFile):
719 output.write('#include "' + incFile + '"\\n')
721 output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
726 self.write([dir, 'include', 'my_qobject.h'], r"""
728 void my_qt_symbol(const char *arg);
731 self.write([dir, 'lib', 'my_qobject.cpp'], r"""
732 #include "../include/my_qobject.h"
734 void my_qt_symbol(const char *arg) {
735 fputs( arg, stdout );
739 self.write([dir, 'lib', 'SConstruct'], r"""
742 if sys.platform == 'win32':
743 env.StaticLibrary( 'myqt', 'my_qobject.cpp' )
745 env.SharedLibrary( 'myqt', 'my_qobject.cpp' )
748 self.run(chdir = self.workpath(dir, 'lib'),
751 match = self.match_re_dotall)
753 self.QT = self.workpath(dir)
755 self.QT_MOC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'mymoc.py'))
756 self.QT_UIC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'myuic.py'))
757 self.QT_LIB_DIR = self.workpath(dir, 'lib')
759 def Qt_create_SConstruct(self, place):
760 if isinstance(place, list):
761 place = test.workpath(*place)
762 self.write(place, """\
763 if ARGUMENTS.get('noqtdir', 0): QTDIR=None
765 env = Environment(QTDIR = QTDIR,
769 tools=['default','qt'])
771 if ARGUMENTS.get('variant_dir', 0):
772 if ARGUMENTS.get('chdir', 0):
776 dup=int(ARGUMENTS.get('dup', 1))
778 builddir = 'build_dup0'
782 VariantDir(builddir, '.', duplicate=dup)
784 sconscript = Dir(builddir).File('SConscript')
786 sconscript = File('SConscript')
788 SConscript( sconscript )
789 """ % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
792 NCR = 0 # non-cached rebuild
793 CR = 1 # cached rebuild (up to date)
794 NCF = 2 # non-cached build failure
795 CF = 3 # cached build failure
797 if sys.platform == 'win32':
798 Configure_lib = 'msvcrt'
802 # to use cygwin compilers on cmd.exe -> uncomment following line
805 def checkLogAndStdout(self, checks, results, cached,
806 logfile, sconf_dir, sconstruct,
807 doCheckLog=1, doCheckStdout=1):
810 def __init__(self, p):
813 def matchPart(log, logfile, lastEnd, NoMatch=NoMatch):
814 m = re.match(log, logfile[lastEnd:])
816 raise NoMatch, lastEnd
817 return m.end() + lastEnd
819 #print len(os.linesep)
822 for i in range(len(ls)):
826 nols = nols + "[^" + ls[i] + "])"
831 logfile = self.read(self.workpath(logfile))
833 logfile.find( "scons: warning: The stored build "
834 "information has an unexpected class." ) >= 0):
836 sconf_dir = sconf_dir
837 sconstruct = sconstruct
839 log = r'file\ \S*%s\,line \d+:' % re.escape(sconstruct) + ls
840 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
841 log = "\t" + re.escape("Configure(confdir = %s)" % sconf_dir) + ls
842 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
845 for check,result,cache_desc in zip(checks, results, cached):
846 log = re.escape("scons: Configure: " + check) + ls
847 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
850 for bld_desc in cache_desc: # each TryXXX
851 for ext, flag in bld_desc: # each file in TryBuild
852 file = os.path.join(sconf_dir,"conftest_%d%s" % (cnt, ext))
855 if ext in ['.c', '.cpp']:
856 log=log + re.escape(file + " <-") + ls
857 log=log + r"( \|" + nols + "*" + ls + ")+?"
859 log=log + "(" + nols + "*" + ls +")*?"
864 re.escape("scons: Configure: \"%s\" is up to date."
866 log=log+re.escape("scons: Configure: The original builder "
868 log=log+r"( \|.*"+ls+")+"
870 # non-cached rebuild failure
871 log=log + "(" + nols + "*" + ls + ")*?"
874 # cached rebuild failure
876 re.escape("scons: Configure: Building \"%s\" failed "
877 "in a previous run and all its sources are"
878 " up to date." % file) + ls
879 log=log+re.escape("scons: Configure: The original builder "
881 log=log+r"( \|.*"+ls+")+"
884 result = "(cached) " + result
885 rdstr = rdstr + re.escape(check) + re.escape(result) + "\n"
886 log=log + re.escape("scons: Configure: " + result) + ls + ls
887 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
889 if doCheckLog: lastEnd = matchPart(ls, logfile, lastEnd)
890 if doCheckLog and lastEnd != len(logfile):
891 raise NoMatch, lastEnd
894 print "Cannot match log file against log regexp."
896 print "------------------------------------------------------"
897 print logfile[m.pos:]
898 print "------------------------------------------------------"
900 print "------------------------------------------------------"
902 print "------------------------------------------------------"
906 exp_stdout = self.wrap_stdout(".*", rdstr)
907 if not self.match_re_dotall(self.stdout(), exp_stdout):
908 print "Unexpected stdout: "
909 print "-----------------------------------------------------"
910 print repr(self.stdout())
911 print "-----------------------------------------------------"
912 print repr(exp_stdout)
913 print "-----------------------------------------------------"
916 def get_python_version(self):
918 Returns the Python version (just so everyone doesn't have to
919 hand-code slicing the right number of characters).
921 # see also sys.prefix documentation
922 return python_minor_version_string()
924 def get_platform_python_info(self):
926 Returns a path to a Python executable suitable for testing on
927 this platform and its associated include path, library path,
930 python = self.where_is('python')
932 self.skip_test('Can not find installed "python", skipping test.\n')
934 self.run(program = python, stdin = """\
937 py_ver = 'python%d.%d' % sys.version_info[:2]
938 except AttributeError:
939 py_ver = 'python' + sys.version[:3]
940 print os.path.join(sys.prefix, 'include', py_ver)
941 print os.path.join(sys.prefix, 'lib', py_ver, 'config')
945 return [python] + self.stdout().strip().split('\n')
947 def start(self, *args, **kw):
949 Starts SCons in the test environment.
951 This method exists to tell Test{Cmd,Common} that we're going to
952 use standard input without forcing every .start() call in the
953 individual tests to do so explicitly.
955 if 'stdin' not in kw:
957 return TestCommon.start(self, *args, **kw)
959 def wait_for(self, fname, timeout=10.0, popen=None):
961 Waits for the specified file name to exist.
964 while not os.path.exists(fname):
965 if timeout and waited >= timeout:
966 sys.stderr.write('timed out waiting for %s to exist\n' % fname)
973 waited = waited + 1.0
975 def get_alt_cpp_suffix(self):
977 Many CXX tests have this same logic.
978 They all needed to determine if the current os supports
979 files with .C and .c as different files or not
980 in which case they are instructed to use .cpp instead of .C
982 if not case_sensitive_suffixes('.c','.C'):
983 alt_cpp_suffix = '.cpp'
985 alt_cpp_suffix = '.C'
986 return alt_cpp_suffix
990 def __init__(self, name, units, expression, convert=None):
992 convert = lambda x: x
995 self.expression = re.compile(expression)
996 self.convert = convert
999 Stat('memory-initial', 'kbytes',
1000 r'Memory before reading SConscript files:\s+(\d+)',
1001 convert=lambda s: int(s) / 1024),
1002 Stat('memory-prebuild', 'kbytes',
1003 r'Memory before building targets:\s+(\d+)',
1004 convert=lambda s: int(s) / 1024),
1005 Stat('memory-final', 'kbytes',
1006 r'Memory after building targets:\s+(\d+)',
1007 convert=lambda s: int(s) / 1024),
1009 Stat('time-sconscript', 'seconds',
1010 r'Total SConscript file execution time:\s+([\d.]+) seconds'),
1011 Stat('time-scons', 'seconds',
1012 r'Total SCons execution time:\s+([\d.]+) seconds'),
1013 Stat('time-commands', 'seconds',
1014 r'Total command execution time:\s+([\d.]+) seconds'),
1015 Stat('time-total', 'seconds',
1016 r'Total build time:\s+([\d.]+) seconds'),
1020 class TimeSCons(TestSCons):
1021 """Class for timing SCons."""
1022 def __init__(self, *args, **kw):
1024 In addition to normal TestSCons.TestSCons intialization,
1025 this enables verbose mode (which causes the command lines to
1026 be displayed in the output) and copies the contents of the
1027 directory containing the executing script to the temporary
1030 self.variables = kw.get('variables')
1031 if self.variables is not None:
1032 for variable, value in self.variables.items():
1033 value = os.environ.get(variable, value)
1038 value = float(value)
1041 self.variables[variable] = value
1044 self.calibrate = os.environ.get('TIMESCONS_CALIBRATE', '0') != '0'
1046 if 'verbose' not in kw and not self.calibrate:
1047 kw['verbose'] = True
1050 #TestSCons.__init__(self, *args, **kw)
1051 TestSCons.__init__(self, *args, **kw)
1053 # TODO(sgk): better way to get the script dir than sys.argv[0]
1054 test_dir = os.path.dirname(sys.argv[0])
1055 test_name = os.path.basename(test_dir)
1057 if not os.path.isabs(test_dir):
1058 test_dir = os.path.join(self.orig_cwd, test_dir)
1059 self.copy_timing_configuration(test_dir, self.workpath())
1061 def main(self, *args, **kw):
1063 The main entry point for standard execution of timings.
1065 This method run SCons three times:
1067 Once with the --help option, to have it exit after just reading
1070 Once as a full build of all targets.
1072 Once again as a (presumably) null or up-to-date build of
1075 The elapsed time to execute each build is printed after
1078 if 'options' not in kw and self.variables:
1080 for variable, value in self.variables.items():
1081 options.append('%s=%s' % (variable, value))
1082 kw['options'] = ' '.join(options)
1085 #self.calibration(*args, **kw)
1086 self.calibration(*args, **kw)
1090 #self.startup(*args, **kw)
1091 #self.full(*args, **kw)
1092 #self.null(*args, **kw)
1093 self.startup(*args, **kw)
1094 self.full(*args, **kw)
1095 self.null(*args, **kw)
1097 def trace(self, graph, name, value, units, sort=None):
1098 fmt = "TRACE: graph=%s name=%s value=%s units=%s"
1099 line = fmt % (graph, name, value, units)
1100 if sort is not None:
1101 line = line + (' sort=%s' % sort)
1103 sys.stdout.write(line)
1106 def report_traces(self, trace, stats):
1107 self.trace('TimeSCons-elapsed',
1109 self.elapsed_time(),
1112 for name, args in stats.items():
1114 #self.trace(name, trace, *args)
1115 self.trace(name, trace, **args)
1119 fp = open('/proc/loadavg')
1120 except EnvironmentError:
1123 avg1, avg5, avg15 = fp.readline().split(" ")[:3]
1125 self.trace('load-average', 'average1', avg1, 'processes')
1126 self.trace('load-average', 'average5', avg5, 'processes')
1127 self.trace('load-average', 'average15', avg15, 'processes')
1129 def collect_stats(self, input):
1131 for stat in StatList:
1132 m = stat.expression.search(input)
1134 value = stat.convert(m.group(1))
1135 # The dict keys match the keyword= arguments
1136 # of the trace() method above so they can be
1137 # applied directly to that call.
1138 result[stat.name] = {'value':value, 'units':stat.units}
1141 def startup(self, *args, **kw):
1143 Runs scons with the --help option.
1145 This serves as a way to isolate just the amount of startup time
1146 spent reading up the configuration, since --help exits before any
1147 "real work" is done.
1149 kw['options'] = kw.get('options', '') + ' --help'
1150 # Ignore the exit status. If the --help run dies, we just
1151 # won't report any statistics for it, but we can still execute
1152 # the full and null builds.
1155 #self.run(*args, **kw)
1156 self.run(*args, **kw)
1157 sys.stdout.write(self.stdout())
1158 stats = self.collect_stats(self.stdout())
1159 # Delete the time-commands, since no commands are ever
1160 # executed on the help run and it is (or should be) always 0.0.
1161 del stats['time-commands']
1162 self.report_traces('startup', stats)
1164 def full(self, *args, **kw):
1166 Runs a full build of SCons.
1169 #self.run(*args, **kw)
1170 self.run(*args, **kw)
1171 sys.stdout.write(self.stdout())
1172 stats = self.collect_stats(self.stdout())
1173 self.report_traces('full', stats)
1175 #self.trace('full-memory', 'initial', **stats['memory-initial'])
1176 #self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
1177 #self.trace('full-memory', 'final', **stats['memory-final'])
1178 self.trace('full-memory', 'initial', **stats['memory-initial'])
1179 self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
1180 self.trace('full-memory', 'final', **stats['memory-final'])
1182 def calibration(self, *args, **kw):
1184 Runs a full build of SCons, but only reports calibration
1185 information (the variable(s) that were set for this configuration,
1186 and the elapsed time to run.
1189 #self.run(*args, **kw)
1190 self.run(*args, **kw)
1192 for variable, value in self.variables.items():
1193 sys.stdout.write('VARIABLE: %s=%s\n' % (variable, value))
1194 sys.stdout.write('ELAPSED: %s\n' % self.elapsed_time())
1196 def null(self, *args, **kw):
1198 Runs an up-to-date null build of SCons.
1200 # TODO(sgk): allow the caller to specify the target (argument)
1201 # that must be up-to-date.
1203 #self.up_to_date(arguments='.', **kw)
1205 kw['arguments'] = '.'
1206 self.up_to_date(**kw)
1207 sys.stdout.write(self.stdout())
1208 stats = self.collect_stats(self.stdout())
1209 # time-commands should always be 0.0 on a null build, because
1210 # no commands should be executed. Remove it from the stats
1211 # so we don't trace it, but only if it *is* 0 so that we'll
1212 # get some indication if a supposedly-null build actually does
1214 if float(stats['time-commands']['value']) == 0.0:
1215 del stats['time-commands']
1216 self.report_traces('null', stats)
1218 #self.trace('null-memory', 'initial', **stats['memory-initial'])
1219 #self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
1220 #self.trace('null-memory', 'final', **stats['memory-final'])
1221 self.trace('null-memory', 'initial', **stats['memory-initial'])
1222 self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
1223 self.trace('null-memory', 'final', **stats['memory-final'])
1225 def elapsed_time(self):
1227 Returns the elapsed time of the most recent command execution.
1229 return self.endTime - self.startTime
1231 def run(self, *args, **kw):
1233 Runs a single build command, capturing output in the specified file.
1235 Because this class is about timing SCons, we record the start
1236 and end times of the elapsed execution, and also add the
1237 --debug=memory and --debug=time options to have SCons report
1238 its own memory and timing statistics.
1240 kw['options'] = kw.get('options', '') + ' --debug=memory --debug=time'
1241 self.startTime = time.time()
1244 #result = TestSCons.run(self, *args, **kw)
1245 result = TestSCons.run(self, *args, **kw)
1247 self.endTime = time.time()
1250 def copy_timing_configuration(self, source_dir, dest_dir):
1252 Copies the timing configuration from the specified source_dir (the
1253 directory in which the controlling script lives) to the specified
1254 dest_dir (a temporary working directory).
1256 This ignores all files and directories that begin with the string
1257 'TimeSCons-', and all '.svn' subdirectories.
1259 for root, dirs, files in os.walk(source_dir):
1263 #dirs = [ d for d in dirs if not d.startswith('TimeSCons-') ]
1264 #files = [ f for f in files if not f.startswith('TimeSCons-') ]
1265 not_timescons_entries = lambda s: not s.startswith('TimeSCons-')
1266 dirs = list(filter(not_timescons_entries, dirs))
1267 files = list(filter(not_timescons_entries, files))
1268 for dirname in dirs:
1269 source = os.path.join(root, dirname)
1270 destination = source.replace(source_dir, dest_dir)
1271 os.mkdir(destination)
1272 if sys.platform != 'win32':
1273 shutil.copystat(source, destination)
1274 for filename in files:
1275 source = os.path.join(root, filename)
1276 destination = source.replace(source_dir, dest_dir)
1277 shutil.copy2(source, destination)
1280 # In some environments, $AR will generate a warning message to stderr
1281 # if the library doesn't previously exist and is being created. One
1282 # way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
1283 # but this is difficult to do in a platform-/implementation-specific
1284 # method. Instead, we will use the following as a stderr match for
1285 # tests that use AR so that we will view zero or more "ar: creating
1286 # <file>" messages to be successful executions of the test (see
1287 # test/AR.py for sample usage).
1289 noisy_ar=r'(ar: creating( archive)? \S+\n?)*'
1293 # indent-tabs-mode:nil
1295 # vim: set expandtab tabstop=4 shiftwidth=4: