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):
518 result.extend(sorted(glob.glob(p)))
522 def java_ENV(self, version=None):
524 Initialize with a default external environment that uses a local
525 Java SDK in preference to whatever's found in the default PATH.
528 return self._java_env[version]['ENV']
529 except AttributeError:
534 import SCons.Environment
535 env = SCons.Environment.Environment()
536 self._java_env[version] = env
541 '/usr/java/jdk%s*/bin' % version,
542 '/usr/lib/jvm/*-%s*/bin' % version,
543 '/usr/local/j2sdk%s*/bin' % version,
545 java_path = self.paths(patterns) + [env['ENV']['PATH']]
548 '/usr/java/latest/bin',
549 '/usr/lib/jvm/*/bin',
550 '/usr/local/j2sdk*/bin',
552 java_path = self.paths(patterns) + [env['ENV']['PATH']]
554 env['ENV']['PATH'] = os.pathsep.join(java_path)
557 def java_where_includes(self,version=None):
559 Return java include paths compiling java jni code
565 frame = '/System/Library/Frameworks/JavaVM.framework/Headers/jni.h'
567 frame = '/System/Library/Frameworks/JavaVM.framework/Versions/%s*/Headers/jni.h'%version
568 jni_dirs = ['/usr/lib/jvm/java-*-sun-%s*/include/jni.h'%version,
569 '/usr/java/jdk%s*/include/jni.h'%version,
572 dirs = self.paths(jni_dirs)
575 d=os.path.dirname(self.paths(jni_dirs)[0])
578 if sys.platform == 'win32':
579 result.append(os.path.join(d,'win32'))
580 elif sys.platform == 'linux2':
581 result.append(os.path.join(d,'linux'))
585 def java_where_java_home(self,version=None):
586 if sys.platform[:6] == 'darwin':
588 home = '/System/Library/Frameworks/JavaVM.framework/Home'
590 home = '/System/Library/Frameworks/JavaVM.framework/Versions/%s/Home' % version
592 jar = self.java_where_jar(version)
593 home = os.path.normpath('%s/..'%jar)
594 if os.path.isdir(home):
596 print("Could not determine JAVA_HOME: %s is not a directory" % home)
599 def java_where_jar(self, version=None):
600 ENV = self.java_ENV(version)
601 if self.detect_tool('jar', ENV=ENV):
602 where_jar = self.detect('JAR', 'jar', ENV=ENV)
604 where_jar = self.where_is('jar', ENV['PATH'])
606 self.skip_test("Could not find Java jar, skipping test(s).\n")
609 def java_where_java(self, version=None):
611 Return a path to the java executable.
613 ENV = self.java_ENV(version)
614 where_java = self.where_is('java', ENV['PATH'])
616 self.skip_test("Could not find Java java, skipping test(s).\n")
619 def java_where_javac(self, version=None):
621 Return a path to the javac compiler.
623 ENV = self.java_ENV(version)
624 if self.detect_tool('javac'):
625 where_javac = self.detect('JAVAC', 'javac', ENV=ENV)
627 where_javac = self.where_is('javac', ENV['PATH'])
629 self.skip_test("Could not find Java javac, skipping test(s).\n")
630 self.run(program = where_javac,
631 arguments = '-version',
635 if self.stderr().find('javac %s' % version) == -1:
636 fmt = "Could not find javac for Java version %s, skipping test(s).\n"
637 self.skip_test(fmt % version)
639 m = re.search(r'javac (\d\.\d)', self.stderr())
644 return where_javac, version
646 def java_where_javah(self, version=None):
647 ENV = self.java_ENV(version)
648 if self.detect_tool('javah'):
649 where_javah = self.detect('JAVAH', 'javah', ENV=ENV)
651 where_javah = self.where_is('javah', ENV['PATH'])
653 self.skip_test("Could not find Java javah, skipping test(s).\n")
656 def java_where_rmic(self, version=None):
657 ENV = self.java_ENV(version)
658 if self.detect_tool('rmic'):
659 where_rmic = self.detect('RMIC', 'rmic', ENV=ENV)
661 where_rmic = self.where_is('rmic', ENV['PATH'])
663 self.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n")
666 def Qt_dummy_installation(self, dir='qt'):
667 # create a dummy qt installation
669 self.subdir( dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'] )
671 self.write([dir, 'bin', 'mymoc.py'], """\
675 # -w and -z are fake options used in test/QT/QTFLAGS.py
676 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:wz', [])
680 for opt, arg in cmd_opts:
681 if opt == '-o': output = open(arg, 'wb')
682 elif opt == '-i': impl = 1
683 else: opt_string = opt_string + ' ' + opt
684 output.write("/* mymoc.py%s */\\n" % opt_string)
686 contents = open(a, 'rb').read()
687 a = a.replace('\\\\', '\\\\\\\\')
688 subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
690 contents = re.sub( r'#include.*', '', contents )
691 output.write(contents.replace('Q_OBJECT', subst))
696 self.write([dir, 'bin', 'myuic.py'], """\
705 for arg in sys.argv[1:]:
707 output = open(arg, 'wb')
716 elif arg[0:1] == "-":
717 opt_string = opt_string + ' ' + arg
721 source = open(arg, 'rb')
723 output.write("/* myuic.py%s */\\n" % opt_string)
725 output.write( '#include "' + impl + '"\\n' )
726 includes = re.findall('<include.*?>(.*?)</include>', source.read())
727 for incFile in includes:
728 # this is valid for ui.h files, at least
729 if os.path.exists(incFile):
730 output.write('#include "' + incFile + '"\\n')
732 output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
737 self.write([dir, 'include', 'my_qobject.h'], r"""
739 void my_qt_symbol(const char *arg);
742 self.write([dir, 'lib', 'my_qobject.cpp'], r"""
743 #include "../include/my_qobject.h"
745 void my_qt_symbol(const char *arg) {
746 fputs( arg, stdout );
750 self.write([dir, 'lib', 'SConstruct'], r"""
753 if sys.platform == 'win32':
754 env.StaticLibrary( 'myqt', 'my_qobject.cpp' )
756 env.SharedLibrary( 'myqt', 'my_qobject.cpp' )
759 self.run(chdir = self.workpath(dir, 'lib'),
762 match = self.match_re_dotall)
764 self.QT = self.workpath(dir)
766 self.QT_MOC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'mymoc.py'))
767 self.QT_UIC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'myuic.py'))
768 self.QT_LIB_DIR = self.workpath(dir, 'lib')
770 def Qt_create_SConstruct(self, place):
771 if isinstance(place, list):
772 place = test.workpath(*place)
773 self.write(place, """\
774 if ARGUMENTS.get('noqtdir', 0): QTDIR=None
776 env = Environment(QTDIR = QTDIR,
780 tools=['default','qt'])
782 if ARGUMENTS.get('variant_dir', 0):
783 if ARGUMENTS.get('chdir', 0):
787 dup=int(ARGUMENTS.get('dup', 1))
789 builddir = 'build_dup0'
793 VariantDir(builddir, '.', duplicate=dup)
795 sconscript = Dir(builddir).File('SConscript')
797 sconscript = File('SConscript')
799 SConscript( sconscript )
800 """ % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
803 NCR = 0 # non-cached rebuild
804 CR = 1 # cached rebuild (up to date)
805 NCF = 2 # non-cached build failure
806 CF = 3 # cached build failure
808 if sys.platform == 'win32':
809 Configure_lib = 'msvcrt'
813 # to use cygwin compilers on cmd.exe -> uncomment following line
816 def checkLogAndStdout(self, checks, results, cached,
817 logfile, sconf_dir, sconstruct,
818 doCheckLog=1, doCheckStdout=1):
821 def __init__(self, p):
824 def matchPart(log, logfile, lastEnd, NoMatch=NoMatch):
825 m = re.match(log, logfile[lastEnd:])
827 raise NoMatch, lastEnd
828 return m.end() + lastEnd
830 #print len(os.linesep)
833 for i in range(len(ls)):
837 nols = nols + "[^" + ls[i] + "])"
842 logfile = self.read(self.workpath(logfile))
844 logfile.find( "scons: warning: The stored build "
845 "information has an unexpected class." ) >= 0):
847 sconf_dir = sconf_dir
848 sconstruct = sconstruct
850 log = r'file\ \S*%s\,line \d+:' % re.escape(sconstruct) + ls
851 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
852 log = "\t" + re.escape("Configure(confdir = %s)" % sconf_dir) + ls
853 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
856 for check,result,cache_desc in zip(checks, results, cached):
857 log = re.escape("scons: Configure: " + check) + ls
858 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
861 for bld_desc in cache_desc: # each TryXXX
862 for ext, flag in bld_desc: # each file in TryBuild
863 file = os.path.join(sconf_dir,"conftest_%d%s" % (cnt, ext))
866 if ext in ['.c', '.cpp']:
867 log=log + re.escape(file + " <-") + ls
868 log=log + r"( \|" + nols + "*" + ls + ")+?"
870 log=log + "(" + nols + "*" + ls +")*?"
875 re.escape("scons: Configure: \"%s\" is up to date."
877 log=log+re.escape("scons: Configure: The original builder "
879 log=log+r"( \|.*"+ls+")+"
881 # non-cached rebuild failure
882 log=log + "(" + nols + "*" + ls + ")*?"
885 # cached rebuild failure
887 re.escape("scons: Configure: Building \"%s\" failed "
888 "in a previous run and all its sources are"
889 " up to date." % file) + ls
890 log=log+re.escape("scons: Configure: The original builder "
892 log=log+r"( \|.*"+ls+")+"
895 result = "(cached) " + result
896 rdstr = rdstr + re.escape(check) + re.escape(result) + "\n"
897 log=log + re.escape("scons: Configure: " + result) + ls + ls
898 if doCheckLog: lastEnd = matchPart(log, logfile, lastEnd)
900 if doCheckLog: lastEnd = matchPart(ls, logfile, lastEnd)
901 if doCheckLog and lastEnd != len(logfile):
902 raise NoMatch, lastEnd
905 print "Cannot match log file against log regexp."
907 print "------------------------------------------------------"
908 print logfile[m.pos:]
909 print "------------------------------------------------------"
911 print "------------------------------------------------------"
913 print "------------------------------------------------------"
917 exp_stdout = self.wrap_stdout(".*", rdstr)
918 if not self.match_re_dotall(self.stdout(), exp_stdout):
919 print "Unexpected stdout: "
920 print "-----------------------------------------------------"
921 print repr(self.stdout())
922 print "-----------------------------------------------------"
923 print repr(exp_stdout)
924 print "-----------------------------------------------------"
927 def get_python_version(self):
929 Returns the Python version (just so everyone doesn't have to
930 hand-code slicing the right number of characters).
932 # see also sys.prefix documentation
933 return python_minor_version_string()
935 def get_platform_python_info(self):
937 Returns a path to a Python executable suitable for testing on
938 this platform and its associated include path, library path,
941 python = self.where_is('python')
943 self.skip_test('Can not find installed "python", skipping test.\n')
945 self.run(program = python, stdin = """\
948 py_ver = 'python%d.%d' % sys.version_info[:2]
949 except AttributeError:
950 py_ver = 'python' + sys.version[:3]
951 print os.path.join(sys.prefix, 'include', py_ver)
952 print os.path.join(sys.prefix, 'lib', py_ver, 'config')
956 return [python] + self.stdout().strip().split('\n')
958 def start(self, *args, **kw):
960 Starts SCons in the test environment.
962 This method exists to tell Test{Cmd,Common} that we're going to
963 use standard input without forcing every .start() call in the
964 individual tests to do so explicitly.
966 if 'stdin' not in kw:
968 return TestCommon.start(self, *args, **kw)
970 def wait_for(self, fname, timeout=10.0, popen=None):
972 Waits for the specified file name to exist.
975 while not os.path.exists(fname):
976 if timeout and waited >= timeout:
977 sys.stderr.write('timed out waiting for %s to exist\n' % fname)
984 waited = waited + 1.0
986 def get_alt_cpp_suffix(self):
988 Many CXX tests have this same logic.
989 They all needed to determine if the current os supports
990 files with .C and .c as different files or not
991 in which case they are instructed to use .cpp instead of .C
993 if not case_sensitive_suffixes('.c','.C'):
994 alt_cpp_suffix = '.cpp'
996 alt_cpp_suffix = '.C'
997 return alt_cpp_suffix
1001 def __init__(self, name, units, expression, convert=None):
1003 convert = lambda x: x
1006 self.expression = re.compile(expression)
1007 self.convert = convert
1010 Stat('memory-initial', 'kbytes',
1011 r'Memory before reading SConscript files:\s+(\d+)',
1012 convert=lambda s: int(s) / 1024),
1013 Stat('memory-prebuild', 'kbytes',
1014 r'Memory before building targets:\s+(\d+)',
1015 convert=lambda s: int(s) / 1024),
1016 Stat('memory-final', 'kbytes',
1017 r'Memory after building targets:\s+(\d+)',
1018 convert=lambda s: int(s) / 1024),
1020 Stat('time-sconscript', 'seconds',
1021 r'Total SConscript file execution time:\s+([\d.]+) seconds'),
1022 Stat('time-scons', 'seconds',
1023 r'Total SCons execution time:\s+([\d.]+) seconds'),
1024 Stat('time-commands', 'seconds',
1025 r'Total command execution time:\s+([\d.]+) seconds'),
1026 Stat('time-total', 'seconds',
1027 r'Total build time:\s+([\d.]+) seconds'),
1031 class TimeSCons(TestSCons):
1032 """Class for timing SCons."""
1033 def __init__(self, *args, **kw):
1035 In addition to normal TestSCons.TestSCons intialization,
1036 this enables verbose mode (which causes the command lines to
1037 be displayed in the output) and copies the contents of the
1038 directory containing the executing script to the temporary
1041 self.variables = kw.get('variables')
1042 if self.variables is not None:
1043 for variable, value in self.variables.items():
1044 value = os.environ.get(variable, value)
1049 value = float(value)
1052 self.variables[variable] = value
1055 self.calibrate = os.environ.get('TIMESCONS_CALIBRATE', '0') != '0'
1057 if 'verbose' not in kw and not self.calibrate:
1058 kw['verbose'] = True
1061 #TestSCons.__init__(self, *args, **kw)
1062 TestSCons.__init__(self, *args, **kw)
1064 # TODO(sgk): better way to get the script dir than sys.argv[0]
1065 test_dir = os.path.dirname(sys.argv[0])
1066 test_name = os.path.basename(test_dir)
1068 if not os.path.isabs(test_dir):
1069 test_dir = os.path.join(self.orig_cwd, test_dir)
1070 self.copy_timing_configuration(test_dir, self.workpath())
1072 def main(self, *args, **kw):
1074 The main entry point for standard execution of timings.
1076 This method run SCons three times:
1078 Once with the --help option, to have it exit after just reading
1081 Once as a full build of all targets.
1083 Once again as a (presumably) null or up-to-date build of
1086 The elapsed time to execute each build is printed after
1089 if 'options' not in kw and self.variables:
1091 for variable, value in self.variables.items():
1092 options.append('%s=%s' % (variable, value))
1093 kw['options'] = ' '.join(options)
1096 #self.calibration(*args, **kw)
1097 self.calibration(*args, **kw)
1101 #self.startup(*args, **kw)
1102 #self.full(*args, **kw)
1103 #self.null(*args, **kw)
1104 self.startup(*args, **kw)
1105 self.full(*args, **kw)
1106 self.null(*args, **kw)
1108 def trace(self, graph, name, value, units, sort=None):
1109 fmt = "TRACE: graph=%s name=%s value=%s units=%s"
1110 line = fmt % (graph, name, value, units)
1111 if sort is not None:
1112 line = line + (' sort=%s' % sort)
1114 sys.stdout.write(line)
1117 def report_traces(self, trace, stats):
1118 self.trace('TimeSCons-elapsed',
1120 self.elapsed_time(),
1123 for name, args in stats.items():
1125 #self.trace(name, trace, *args)
1126 self.trace(name, trace, **args)
1130 fp = open('/proc/loadavg')
1131 except EnvironmentError:
1134 avg1, avg5, avg15 = fp.readline().split(" ")[:3]
1136 self.trace('load-average', 'average1', avg1, 'processes')
1137 self.trace('load-average', 'average5', avg5, 'processes')
1138 self.trace('load-average', 'average15', avg15, 'processes')
1140 def collect_stats(self, input):
1142 for stat in StatList:
1143 m = stat.expression.search(input)
1145 value = stat.convert(m.group(1))
1146 # The dict keys match the keyword= arguments
1147 # of the trace() method above so they can be
1148 # applied directly to that call.
1149 result[stat.name] = {'value':value, 'units':stat.units}
1152 def startup(self, *args, **kw):
1154 Runs scons with the --help option.
1156 This serves as a way to isolate just the amount of startup time
1157 spent reading up the configuration, since --help exits before any
1158 "real work" is done.
1160 kw['options'] = kw.get('options', '') + ' --help'
1161 # Ignore the exit status. If the --help run dies, we just
1162 # won't report any statistics for it, but we can still execute
1163 # the full and null builds.
1166 #self.run(*args, **kw)
1167 self.run(*args, **kw)
1168 sys.stdout.write(self.stdout())
1169 stats = self.collect_stats(self.stdout())
1170 # Delete the time-commands, since no commands are ever
1171 # executed on the help run and it is (or should be) always 0.0.
1172 del stats['time-commands']
1173 self.report_traces('startup', stats)
1175 def full(self, *args, **kw):
1177 Runs a full build of SCons.
1180 #self.run(*args, **kw)
1181 self.run(*args, **kw)
1182 sys.stdout.write(self.stdout())
1183 stats = self.collect_stats(self.stdout())
1184 self.report_traces('full', stats)
1186 #self.trace('full-memory', 'initial', **stats['memory-initial'])
1187 #self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
1188 #self.trace('full-memory', 'final', **stats['memory-final'])
1189 self.trace('full-memory', 'initial', **stats['memory-initial'])
1190 self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
1191 self.trace('full-memory', 'final', **stats['memory-final'])
1193 def calibration(self, *args, **kw):
1195 Runs a full build of SCons, but only reports calibration
1196 information (the variable(s) that were set for this configuration,
1197 and the elapsed time to run.
1200 #self.run(*args, **kw)
1201 self.run(*args, **kw)
1203 for variable, value in self.variables.items():
1204 sys.stdout.write('VARIABLE: %s=%s\n' % (variable, value))
1205 sys.stdout.write('ELAPSED: %s\n' % self.elapsed_time())
1207 def null(self, *args, **kw):
1209 Runs an up-to-date null build of SCons.
1211 # TODO(sgk): allow the caller to specify the target (argument)
1212 # that must be up-to-date.
1214 #self.up_to_date(arguments='.', **kw)
1216 kw['arguments'] = '.'
1217 self.up_to_date(**kw)
1218 sys.stdout.write(self.stdout())
1219 stats = self.collect_stats(self.stdout())
1220 # time-commands should always be 0.0 on a null build, because
1221 # no commands should be executed. Remove it from the stats
1222 # so we don't trace it, but only if it *is* 0 so that we'll
1223 # get some indication if a supposedly-null build actually does
1225 if float(stats['time-commands']['value']) == 0.0:
1226 del stats['time-commands']
1227 self.report_traces('null', stats)
1229 #self.trace('null-memory', 'initial', **stats['memory-initial'])
1230 #self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
1231 #self.trace('null-memory', 'final', **stats['memory-final'])
1232 self.trace('null-memory', 'initial', **stats['memory-initial'])
1233 self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
1234 self.trace('null-memory', 'final', **stats['memory-final'])
1236 def elapsed_time(self):
1238 Returns the elapsed time of the most recent command execution.
1240 return self.endTime - self.startTime
1242 def run(self, *args, **kw):
1244 Runs a single build command, capturing output in the specified file.
1246 Because this class is about timing SCons, we record the start
1247 and end times of the elapsed execution, and also add the
1248 --debug=memory and --debug=time options to have SCons report
1249 its own memory and timing statistics.
1251 kw['options'] = kw.get('options', '') + ' --debug=memory --debug=time'
1252 self.startTime = time.time()
1255 #result = TestSCons.run(self, *args, **kw)
1256 result = TestSCons.run(self, *args, **kw)
1258 self.endTime = time.time()
1261 def copy_timing_configuration(self, source_dir, dest_dir):
1263 Copies the timing configuration from the specified source_dir (the
1264 directory in which the controlling script lives) to the specified
1265 dest_dir (a temporary working directory).
1267 This ignores all files and directories that begin with the string
1268 'TimeSCons-', and all '.svn' subdirectories.
1270 for root, dirs, files in os.walk(source_dir):
1274 #dirs = [ d for d in dirs if not d.startswith('TimeSCons-') ]
1275 #files = [ f for f in files if not f.startswith('TimeSCons-') ]
1276 not_timescons_entries = lambda s: not s.startswith('TimeSCons-')
1277 dirs = list(filter(not_timescons_entries, dirs))
1278 files = list(filter(not_timescons_entries, files))
1279 for dirname in dirs:
1280 source = os.path.join(root, dirname)
1281 destination = source.replace(source_dir, dest_dir)
1282 os.mkdir(destination)
1283 if sys.platform != 'win32':
1284 shutil.copystat(source, destination)
1285 for filename in files:
1286 source = os.path.join(root, filename)
1287 destination = source.replace(source_dir, dest_dir)
1288 shutil.copy2(source, destination)
1291 # In some environments, $AR will generate a warning message to stderr
1292 # if the library doesn't previously exist and is being created. One
1293 # way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
1294 # but this is difficult to do in a platform-/implementation-specific
1295 # method. Instead, we will use the following as a stderr match for
1296 # tests that use AR so that we will view zero or more "ar: creating
1297 # <file>" messages to be successful executions of the test (see
1298 # test/AR.py for sample usage).
1300 noisy_ar=r'(ar: creating( archive)? \S+\n?)*'
1304 # indent-tabs-mode:nil
1306 # vim: set expandtab tabstop=4 shiftwidth=4: