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__"
29 except AttributeError:
32 for i in xrange(len(lists[0])):
33 result.append(tuple([l[i] for l in 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.3.0'
55 copyright_years = '2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010'
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():
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 sys.platform.find('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 = str.replace(c, '\\' + c)
171 except AttributeError:
172 # Pre-1.6 Python has no sys.version_info
173 version_string = sys.version.split()[0]
174 version_ints = list(map(int, version_string.split('.')))
175 sys.version_info = tuple(version_ints + ['final', 0])
177 def python_version_string():
178 return sys.version.split()[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 'program' not in kw:
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 'interpreter' not in kw and not os.environ.get('SCONS_EXEC'):
247 kw['interpreter'] = [python, '-tt']
248 if 'match' not in kw:
249 kw['match'] = match_exact
250 if 'workdir' not in kw:
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'] = ' '.join(sconsflags)
271 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 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 = result.replace(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'] = ' '.join(sconsflags)
381 result = 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 arguments.split():
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
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 arguments.split():
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'] = stdout.replace('ARGUMENTSGOHERE', s)
414 kw['match'] = self.match_re_dotall
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 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 = x.replace('<string>', file)
466 x = x.replace('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 = s.find(begin_marker, 0)
491 b = b + len(begin_marker)
492 e = s.find(end_marker, b)
493 encoded.append((b, e))
494 b = s.find(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}',
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'] = os.pathsep.join(java_path)
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 self.stderr().find('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'], """\
677 # -w and -z are fake options used in test/QT/QTFLAGS.py
678 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:wz', [])
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
686 output.write("/* mymoc.py%s */\\n" % opt_string)
688 contents = open(a, 'rb').read()
689 a = a.replace('\\\\', '\\\\\\\\')
690 subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
692 contents = re.sub( r'#include.*', '', contents )
693 output.write(contents.replace('Q_OBJECT', subst))
698 self.write([dir, 'bin', 'myuic.py'], """\
707 for arg in sys.argv[1:]:
709 output = open(arg, 'wb')
718 elif arg[0:1] == "-":
719 opt_string = opt_string + ' ' + arg
723 source = open(arg, 'rb')
725 output.write("/* myuic.py%s */\\n" % opt_string)
727 output.write( '#include "' + impl + '"\\n' )
728 includes = re.findall('<include.*?>(.*?)</include>', source.read())
729 for incFile in includes:
730 # this is valid for ui.h files, at least
731 if os.path.exists(incFile):
732 output.write('#include "' + incFile + '"\\n')
734 output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
739 self.write([dir, 'include', 'my_qobject.h'], r"""
741 void my_qt_symbol(const char *arg);
744 self.write([dir, 'lib', 'my_qobject.cpp'], r"""
745 #include "../include/my_qobject.h"
747 void my_qt_symbol(const char *arg) {
748 fputs( arg, stdout );
752 self.write([dir, 'lib', 'SConstruct'], r"""
755 if sys.platform == 'win32':
756 env.StaticLibrary( 'myqt', 'my_qobject.cpp' )
758 env.SharedLibrary( 'myqt', 'my_qobject.cpp' )
761 self.run(chdir = self.workpath(dir, 'lib'),
764 match = self.match_re_dotall)
766 self.QT = self.workpath(dir)
768 self.QT_MOC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'mymoc.py'))
769 self.QT_UIC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'myuic.py'))
770 self.QT_LIB_DIR = self.workpath(dir, 'lib')
772 def Qt_create_SConstruct(self, place):
773 if type(place) is type([]):
774 place = test.workpath(*place)
775 self.write(place, """\
776 if ARGUMENTS.get('noqtdir', 0): QTDIR=None
778 env = Environment(QTDIR = QTDIR,
782 tools=['default','qt'])
784 if ARGUMENTS.get('variant_dir', 0):
785 if ARGUMENTS.get('chdir', 0):
789 dup=int(ARGUMENTS.get('dup', 1))
791 builddir = 'build_dup0'
795 VariantDir(builddir, '.', duplicate=dup)
797 sconscript = Dir(builddir).File('SConscript')
799 sconscript = File('SConscript')
801 SConscript( sconscript )
802 """ % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
805 NCR = 0 # non-cached rebuild
806 CR = 1 # cached rebuild (up to date)
807 NCF = 2 # non-cached build failure
808 CF = 3 # cached build failure
810 if sys.platform == 'win32':
811 Configure_lib = 'msvcrt'
815 # to use cygwin compilers on cmd.exe -> uncomment following line
818 def checkLogAndStdout(self, checks, results, cached,
819 logfile, sconf_dir, sconstruct,
820 doCheckLog=1, doCheckStdout=1):
823 def __init__(self, p):
826 def matchPart(log, logfile, lastEnd, NoMatch=NoMatch):
827 m = re.match(log, logfile[lastEnd:])
829 raise NoMatch, lastEnd
830 return m.end() + lastEnd
832 #print len(os.linesep)
835 for i in range(len(ls)):
839 nols = nols + "[^" + ls[i] + "])"
844 logfile = self.read(self.workpath(logfile))
846 logfile.find( "scons: warning: The stored build "
847 "information has an unexpected class." ) >= 0):
849 sconf_dir = sconf_dir
850 sconstruct = sconstruct
852 log = r'file\ \S*%s\,line \d+:' % re.escape(sconstruct) + ls
853 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
854 log = "\t" + re.escape("Configure(confdir = %s)" % sconf_dir) + ls
855 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
858 for check,result,cache_desc in zip(checks, results, cached):
859 log = re.escape("scons: Configure: " + check) + ls
860 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
863 for bld_desc in cache_desc: # each TryXXX
864 for ext, flag in bld_desc: # each file in TryBuild
865 file = os.path.join(sconf_dir,"conftest_%d%s" % (cnt, ext))
868 if ext in ['.c', '.cpp']:
869 log=log + re.escape(file + " <-") + ls
870 log=log + r"( \|" + nols + "*" + ls + ")+?"
872 log=log + "(" + nols + "*" + ls +")*?"
877 re.escape("scons: Configure: \"%s\" is up to date."
879 log=log+re.escape("scons: Configure: The original builder "
881 log=log+r"( \|.*"+ls+")+"
883 # non-cached rebuild failure
884 log=log + "(" + nols + "*" + ls + ")*?"
887 # cached rebuild failure
889 re.escape("scons: Configure: Building \"%s\" failed "
890 "in a previous run and all its sources are"
891 " up to date." % file) + ls
892 log=log+re.escape("scons: Configure: The original builder "
894 log=log+r"( \|.*"+ls+")+"
897 result = "(cached) " + result
898 rdstr = rdstr + re.escape(check) + re.escape(result) + "\n"
899 log=log + re.escape("scons: Configure: " + result) + ls + ls
900 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
902 if doCheckLog: lastEnd = matchPart(ls, logfile, lastEnd)
903 if doCheckLog and lastEnd != len(logfile):
904 raise NoMatch, lastEnd
907 print "Cannot match log file against log regexp."
909 print "------------------------------------------------------"
910 print logfile[m.pos:]
911 print "------------------------------------------------------"
913 print "------------------------------------------------------"
915 print "------------------------------------------------------"
919 exp_stdout = self.wrap_stdout(".*", rdstr)
920 if not self.match_re_dotall(self.stdout(), exp_stdout):
921 print "Unexpected stdout: "
922 print "-----------------------------------------------------"
923 print repr(self.stdout())
924 print "-----------------------------------------------------"
925 print repr(exp_stdout)
926 print "-----------------------------------------------------"
929 def get_python_version(self):
931 Returns the Python version (just so everyone doesn't have to
932 hand-code slicing the right number of characters).
934 # see also sys.prefix documentation
935 return python_minor_version_string()
937 def get_platform_python_info(self):
939 Returns a path to a Python executable suitable for testing on
940 this platform and its associated include path, library path,
943 python = self.where_is('python')
945 self.skip_test('Can not find installed "python", skipping test.\n')
947 self.run(program = python, stdin = """\
950 py_ver = 'python%d.%d' % sys.version_info[:2]
951 except AttributeError:
952 py_ver = 'python' + sys.version[:3]
953 print os.path.join(sys.prefix, 'include', py_ver)
954 print os.path.join(sys.prefix, 'lib', py_ver, 'config')
958 return [python] + self.stdout().strip().split('\n')
960 def start(self, *args, **kw):
962 Starts SCons in the test environment.
964 This method exists to tell Test{Cmd,Common} that we're going to
965 use standard input without forcing every .start() call in the
966 individual tests to do so explicitly.
968 if 'stdin' not in kw:
970 return TestCommon.start(self, *args, **kw)
972 def wait_for(self, fname, timeout=10.0, popen=None):
974 Waits for the specified file name to exist.
977 while not os.path.exists(fname):
978 if timeout and waited >= timeout:
979 sys.stderr.write('timed out waiting for %s to exist\n' % fname)
986 waited = waited + 1.0
988 def get_alt_cpp_suffix(self):
990 Many CXX tests have this same logic.
991 They all needed to determine if the current os supports
992 files with .C and .c as different files or not
993 in which case they are instructed to use .cpp instead of .C
995 if not case_sensitive_suffixes('.c','.C'):
996 alt_cpp_suffix = '.cpp'
998 alt_cpp_suffix = '.C'
999 return alt_cpp_suffix
1003 def __init__(self, name, units, expression, convert=None):
1005 convert = lambda x: x
1008 self.expression = re.compile(expression)
1009 self.convert = convert
1012 Stat('memory-initial', 'kbytes',
1013 r'Memory before reading SConscript files:\s+(\d+)',
1014 convert=lambda s: int(s) / 1024),
1015 Stat('memory-prebuild', 'kbytes',
1016 r'Memory before building targets:\s+(\d+)',
1017 convert=lambda s: int(s) / 1024),
1018 Stat('memory-final', 'kbytes',
1019 r'Memory after building targets:\s+(\d+)',
1020 convert=lambda s: int(s) / 1024),
1022 Stat('time-sconscript', 'seconds',
1023 r'Total SConscript file execution time:\s+([\d.]+) seconds'),
1024 Stat('time-scons', 'seconds',
1025 r'Total SCons execution time:\s+([\d.]+) seconds'),
1026 Stat('time-commands', 'seconds',
1027 r'Total command execution time:\s+([\d.]+) seconds'),
1028 Stat('time-total', 'seconds',
1029 r'Total build time:\s+([\d.]+) seconds'),
1033 class TimeSCons(TestSCons):
1034 """Class for timing SCons."""
1035 def __init__(self, *args, **kw):
1037 In addition to normal TestSCons.TestSCons intialization,
1038 this enables verbose mode (which causes the command lines to
1039 be displayed in the output) and copies the contents of the
1040 directory containing the executing script to the temporary
1043 self.variables = kw.get('variables')
1044 if self.variables is not None:
1045 for variable, value in self.variables.items():
1046 value = os.environ.get(variable, value)
1051 value = float(value)
1054 self.variables[variable] = value
1057 self.calibrate = os.environ.get('TIMESCONS_CALIBRATE', '0') != '0'
1059 if 'verbose' not in kw and not self.calibrate:
1060 kw['verbose'] = True
1063 #TestSCons.__init__(self, *args, **kw)
1064 TestSCons.__init__(self, *args, **kw)
1066 # TODO(sgk): better way to get the script dir than sys.argv[0]
1067 test_dir = os.path.dirname(sys.argv[0])
1068 test_name = os.path.basename(test_dir)
1070 if not os.path.isabs(test_dir):
1071 test_dir = os.path.join(self.orig_cwd, test_dir)
1072 self.copy_timing_configuration(test_dir, self.workpath())
1074 def main(self, *args, **kw):
1076 The main entry point for standard execution of timings.
1078 This method run SCons three times:
1080 Once with the --help option, to have it exit after just reading
1083 Once as a full build of all targets.
1085 Once again as a (presumably) null or up-to-date build of
1088 The elapsed time to execute each build is printed after
1091 if 'options' not in kw and self.variables:
1093 for variable, value in self.variables.items():
1094 options.append('%s=%s' % (variable, value))
1095 kw['options'] = ' '.join(options)
1098 #self.calibration(*args, **kw)
1099 self.calibration(*args, **kw)
1103 #self.startup(*args, **kw)
1104 #self.full(*args, **kw)
1105 #self.null(*args, **kw)
1106 self.startup(*args, **kw)
1107 self.full(*args, **kw)
1108 self.null(*args, **kw)
1110 def trace(self, graph, name, value, units, sort=None):
1111 fmt = "TRACE: graph=%s name=%s value=%s units=%s"
1112 line = fmt % (graph, name, value, units)
1113 if sort is not None:
1114 line = line + (' sort=%s' % sort)
1116 sys.stdout.write(line)
1119 def report_traces(self, trace, stats):
1120 self.trace('TimeSCons-elapsed',
1122 self.elapsed_time(),
1125 for name, args in stats.items():
1127 #self.trace(name, trace, *args)
1128 self.trace(name, trace, **args)
1132 fp = open('/proc/loadavg')
1133 except EnvironmentError:
1136 avg1, avg5, avg15 = fp.readline().split(" ")[:3]
1138 self.trace('load-average', 'average1', avg1, 'processes')
1139 self.trace('load-average', 'average5', avg5, 'processes')
1140 self.trace('load-average', 'average15', avg15, 'processes')
1142 def collect_stats(self, input):
1144 for stat in StatList:
1145 m = stat.expression.search(input)
1147 value = stat.convert(m.group(1))
1148 # The dict keys match the keyword= arguments
1149 # of the trace() method above so they can be
1150 # applied directly to that call.
1151 result[stat.name] = {'value':value, 'units':stat.units}
1154 def startup(self, *args, **kw):
1156 Runs scons with the --help option.
1158 This serves as a way to isolate just the amount of startup time
1159 spent reading up the configuration, since --help exits before any
1160 "real work" is done.
1162 kw['options'] = kw.get('options', '') + ' --help'
1163 # Ignore the exit status. If the --help run dies, we just
1164 # won't report any statistics for it, but we can still execute
1165 # the full and null builds.
1168 #self.run(*args, **kw)
1169 self.run(*args, **kw)
1170 sys.stdout.write(self.stdout())
1171 stats = self.collect_stats(self.stdout())
1172 # Delete the time-commands, since no commands are ever
1173 # executed on the help run and it is (or should be) always 0.0.
1174 del stats['time-commands']
1175 self.report_traces('startup', stats)
1177 def full(self, *args, **kw):
1179 Runs a full build of SCons.
1182 #self.run(*args, **kw)
1183 self.run(*args, **kw)
1184 sys.stdout.write(self.stdout())
1185 stats = self.collect_stats(self.stdout())
1186 self.report_traces('full', stats)
1188 #self.trace('full-memory', 'initial', **stats['memory-initial'])
1189 #self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
1190 #self.trace('full-memory', 'final', **stats['memory-final'])
1191 self.trace('full-memory', 'initial', **stats['memory-initial'])
1192 self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
1193 self.trace('full-memory', 'final', **stats['memory-final'])
1195 def calibration(self, *args, **kw):
1197 Runs a full build of SCons, but only reports calibration
1198 information (the variable(s) that were set for this configuration,
1199 and the elapsed time to run.
1202 #self.run(*args, **kw)
1203 self.run(*args, **kw)
1205 for variable, value in self.variables.items():
1206 sys.stdout.write('VARIABLE: %s=%s\n' % (variable, value))
1207 sys.stdout.write('ELAPSED: %s\n' % self.elapsed_time())
1209 def null(self, *args, **kw):
1211 Runs an up-to-date null build of SCons.
1213 # TODO(sgk): allow the caller to specify the target (argument)
1214 # that must be up-to-date.
1216 #self.up_to_date(arguments='.', **kw)
1218 kw['arguments'] = '.'
1219 self.up_to_date(**kw)
1220 sys.stdout.write(self.stdout())
1221 stats = self.collect_stats(self.stdout())
1222 # time-commands should always be 0.0 on a null build, because
1223 # no commands should be executed. Remove it from the stats
1224 # so we don't trace it, but only if it *is* 0 so that we'll
1225 # get some indication if a supposedly-null build actually does
1227 if float(stats['time-commands']['value']) == 0.0:
1228 del stats['time-commands']
1229 self.report_traces('null', stats)
1231 #self.trace('null-memory', 'initial', **stats['memory-initial'])
1232 #self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
1233 #self.trace('null-memory', 'final', **stats['memory-final'])
1234 self.trace('null-memory', 'initial', **stats['memory-initial'])
1235 self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
1236 self.trace('null-memory', 'final', **stats['memory-final'])
1238 def elapsed_time(self):
1240 Returns the elapsed time of the most recent command execution.
1242 return self.endTime - self.startTime
1244 def run(self, *args, **kw):
1246 Runs a single build command, capturing output in the specified file.
1248 Because this class is about timing SCons, we record the start
1249 and end times of the elapsed execution, and also add the
1250 --debug=memory and --debug=time options to have SCons report
1251 its own memory and timing statistics.
1253 kw['options'] = kw.get('options', '') + ' --debug=memory --debug=time'
1254 self.startTime = time.time()
1257 #result = TestSCons.run(self, *args, **kw)
1258 result = TestSCons.run(self, *args, **kw)
1260 self.endTime = time.time()
1263 def copy_timing_configuration(self, source_dir, dest_dir):
1265 Copies the timing configuration from the specified source_dir (the
1266 directory in which the controlling script lives) to the specified
1267 dest_dir (a temporary working directory).
1269 This ignores all files and directories that begin with the string
1270 'TimeSCons-', and all '.svn' subdirectories.
1272 for root, dirs, files in os.walk(source_dir):
1276 #dirs = [ d for d in dirs if not d.startswith('TimeSCons-') ]
1277 #files = [ f for f in files if not f.startswith('TimeSCons-') ]
1278 not_timescons_entries = lambda s: not s.startswith('TimeSCons-')
1279 dirs = list(filter(not_timescons_entries, dirs))
1280 files = list(filter(not_timescons_entries, files))
1281 for dirname in dirs:
1282 source = os.path.join(root, dirname)
1283 destination = source.replace(source_dir, dest_dir)
1284 os.mkdir(destination)
1285 if sys.platform != 'win32':
1286 shutil.copystat(source, destination)
1287 for filename in files:
1288 source = os.path.join(root, filename)
1289 destination = source.replace(source_dir, dest_dir)
1290 shutil.copy2(source, destination)
1293 # In some environments, $AR will generate a warning message to stderr
1294 # if the library doesn't previously exist and is being created. One
1295 # way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
1296 # but this is difficult to do in a platform-/implementation-specific
1297 # method. Instead, we will use the following as a stderr match for
1298 # tests that use AR so that we will view zero or more "ar: creating
1299 # <file>" messages to be successful executions of the test (see
1300 # test/AR.py for sample usage).
1302 noisy_ar=r'(ar: creating( archive)? \S+\n?)*'
1306 # indent-tabs-mode:nil
1308 # vim: set expandtab tabstop=4 shiftwidth=4: