summaryrefslogtreecommitdiff
path: root/testing/framework/TestSCons.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/framework/TestSCons.py')
-rw-r--r--testing/framework/TestSCons.py1701
1 files changed, 1701 insertions, 0 deletions
diff --git a/testing/framework/TestSCons.py b/testing/framework/TestSCons.py
new file mode 100644
index 0000000..e415291
--- /dev/null
+++ b/testing/framework/TestSCons.py
@@ -0,0 +1,1701 @@
+"""
+TestSCons.py: a testing framework for the SCons software construction
+tool.
+
+A TestSCons environment object is created via the usual invocation:
+
+ test = TestSCons()
+
+TestScons is a subclass of TestCommon, which in turn is a subclass
+of TestCmd), and hence has available all of the methods and attributes
+from those classes, as well as any overridden or additional methods or
+attributes defined in this subclass.
+"""
+
+# Copyright (c) 2001 - 2019 The SCons Foundation
+from __future__ import division, print_function
+
+__revision__ = "testing/framework/TestSCons.py 103260fce95bf5db1c35fb2371983087d85dd611 2019-07-13 18:25:30 bdbaddog"
+
+import os
+import re
+import shutil
+import sys
+import time
+import subprocess
+
+from TestCommon import *
+from TestCommon import __all__
+
+from TestCmd import Popen
+from TestCmd import PIPE
+
+# Some tests which verify that SCons has been packaged properly need to
+# look for specific version file names. Replicating the version number
+# here provides some independent verification that what we packaged
+# conforms to what we expect.
+
+default_version = '3.0.5'
+
+python_version_unsupported = (2, 6, 0)
+python_version_deprecated = (2, 7, 0)
+
+# In the checked-in source, the value of SConsVersion in the following
+# line must remain "__ VERSION __" (without the spaces) so the built
+# version in build/testing/framework/TestSCons.py contains the actual version
+# string of the packages that have been built.
+SConsVersion = '3.0.5'
+if SConsVersion == '__' + 'VERSION' + '__':
+ SConsVersion = default_version
+
+__all__.extend([
+ 'TestSCons',
+ 'machine',
+ 'python',
+ '_exe',
+ '_obj',
+ '_shobj',
+ 'shobj_',
+ 'lib_',
+ '_lib',
+ 'dll_',
+ '_dll'
+ ])
+
+machine_map = {
+ 'i686' : 'i386',
+ 'i586' : 'i386',
+ 'i486' : 'i386',
+}
+
+try:
+ uname = os.uname
+except AttributeError:
+ # Windows doesn't have a uname() function. We could use something like
+ # sys.platform as a fallback, but that's not really a "machine," so
+ # just leave it as None.
+ machine = None
+else:
+ machine = uname()[4]
+ machine = machine_map.get(machine, machine)
+
+_exe = exe_suffix
+_obj = obj_suffix
+_shobj = shobj_suffix
+shobj_ = shobj_prefix
+_lib = lib_suffix
+lib_ = lib_prefix
+_dll = dll_suffix
+dll_ = dll_prefix
+
+
+if sys.platform == 'cygwin':
+ # On Cygwin, os.path.normcase() lies, so just report back the
+ # fact that the underlying Win32 OS is case-insensitive.
+ def case_sensitive_suffixes(s1, s2):
+ return 0
+else:
+ def case_sensitive_suffixes(s1, s2):
+ return (os.path.normcase(s1) != os.path.normcase(s2))
+
+
+file_expr = r"""File "[^"]*", line \d+, in [^\n]+
+"""
+
+# re.escape escapes too much.
+def re_escape(str):
+ for c in '\\.[]()*+?': # Not an exhaustive list.
+ str = str.replace(c, '\\' + c)
+ return str
+
+#
+# Helper functions that we use as a replacement to the default re.match
+# when searching for special strings in stdout/stderr.
+#
+def search_re(out, l):
+ """ Search the regular expression 'l' in the output 'out'
+ and return the start index when successful.
+ """
+ m = re.search(l, out)
+ if m:
+ return m.start()
+
+ return None
+
+def search_re_in_list(out, l):
+ """ Search the regular expression 'l' in each line of
+ the given string list 'out' and return the line's index
+ when successful.
+ """
+ for idx, o in enumerate(out):
+ m = re.search(l, o)
+ if m:
+ return idx
+
+ return None
+
+#
+# Helpers for handling Python version numbers
+#
+def python_version_string():
+ return sys.version.split()[0]
+
+def python_minor_version_string():
+ return sys.version[:3]
+
+def unsupported_python_version(version=sys.version_info):
+ return version < python_version_unsupported
+
+def deprecated_python_version(version=sys.version_info):
+ return version < python_version_deprecated
+
+if deprecated_python_version():
+ msg = r"""
+scons: warning: Support for pre-2.7.0 Python version (%s) is deprecated.
+ If this will cause hardship, contact scons-dev@scons.org
+"""
+
+ deprecated_python_expr = re_escape(msg % python_version_string()) + file_expr
+ del msg
+else:
+ deprecated_python_expr = ""
+
+
+def initialize_sconsflags(ignore_python_version):
+ """
+ Add the --warn=no-python-version option to SCONSFLAGS for every
+ command so test scripts don't have to filter out Python version
+ deprecation warnings.
+ Same for --warn=no-visual-c-missing.
+ """
+ save_sconsflags = os.environ.get('SCONSFLAGS')
+ if save_sconsflags:
+ sconsflags = [save_sconsflags]
+ else:
+ sconsflags = []
+ if ignore_python_version and deprecated_python_version():
+ sconsflags.append('--warn=no-python-version')
+ # Provide a way to suppress or provide alternate flags for
+ # TestSCons purposes by setting TESTSCONS_SCONSFLAGS.
+ # (The intended use case is to set it to null when running
+ # timing tests of earlier versions of SCons which don't
+ # support the --warn=no-visual-c-missing warning.)
+ visual_c = os.environ.get('TESTSCONS_SCONSFLAGS',
+ '--warn=no-visual-c-missing')
+ if visual_c:
+ sconsflags.append(visual_c)
+ os.environ['SCONSFLAGS'] = ' '.join(sconsflags)
+ return save_sconsflags
+
+def restore_sconsflags(sconsflags):
+ if sconsflags is None:
+ del os.environ['SCONSFLAGS']
+ else:
+ os.environ['SCONSFLAGS'] = sconsflags
+
+
+class TestSCons(TestCommon):
+ """Class for testing SCons.
+
+ This provides a common place for initializing SCons tests,
+ eliminating the need to begin every test with the same repeated
+ initializations.
+ """
+
+ scons_version = SConsVersion
+ javac_is_gcj = False
+
+ def __init__(self, **kw):
+ """Initialize an SCons testing object.
+
+ If they're not overridden by keyword arguments, this
+ initializes the object with the following default values:
+
+ program = 'scons' if it exists,
+ else 'scons.py'
+ interpreter = 'python'
+ match = match_exact
+ workdir = ''
+
+ The workdir value means that, by default, a temporary workspace
+ directory is created for a TestSCons environment. In addition,
+ this method changes directory (chdir) to the workspace directory,
+ so an explicit "chdir = '.'" on all of the run() method calls
+ is not necessary.
+ """
+ self.orig_cwd = os.getcwd()
+ self.external = os.environ.get('SCONS_EXTERNAL_TEST', 0)
+
+ if not self.external:
+ try:
+ script_dir = os.environ['SCONS_SCRIPT_DIR']
+ except KeyError:
+ pass
+ else:
+ os.chdir(script_dir)
+ if 'program' not in kw:
+ kw['program'] = os.environ.get('SCONS')
+ if not kw['program']:
+ if not self.external:
+ if os.path.exists('scons'):
+ kw['program'] = 'scons'
+ else:
+ kw['program'] = 'scons.py'
+ else:
+ kw['program'] = 'scons'
+ kw['interpreter'] = ''
+ elif not self.external and not os.path.isabs(kw['program']):
+ kw['program'] = os.path.join(self.orig_cwd, kw['program'])
+ if 'interpreter' not in kw and not os.environ.get('SCONS_EXEC'):
+ kw['interpreter'] = [python,]
+ if sys.version_info[0] < 3:
+ kw['interpreter'].append('-tt')
+ if 'match' not in kw:
+ kw['match'] = match_exact
+ if 'workdir' not in kw:
+ kw['workdir'] = ''
+
+ # Term causing test failures due to bogus readline init
+ # control character output on FC8
+ # TERM can cause test failures due to control chars in prompts etc.
+ os.environ['TERM'] = 'dumb'
+
+ self.ignore_python_version = kw.get('ignore_python_version', 1)
+ if kw.get('ignore_python_version', -1) != -1:
+ del kw['ignore_python_version']
+
+ TestCommon.__init__(self, **kw)
+
+ if not self.external:
+ import SCons.Node.FS
+ if SCons.Node.FS.default_fs is None:
+ SCons.Node.FS.default_fs = SCons.Node.FS.FS()
+
+ try:
+ self.fixture_dirs = (os.environ['FIXTURE_DIRS']).split(os.pathsep)
+ except KeyError:
+ pass
+
+ def Environment(self, ENV=None, *args, **kw):
+ """
+ Return a construction Environment that optionally overrides
+ the default external environment with the specified ENV.
+ """
+ if not self.external:
+ import SCons.Environment
+ import SCons.Errors
+ if not ENV is None:
+ kw['ENV'] = ENV
+ try:
+ return SCons.Environment.Environment(*args, **kw)
+ except (SCons.Errors.UserError, SCons.Errors.InternalError):
+ return None
+
+ return None
+
+ def detect(self, var, prog=None, ENV=None, norm=None):
+ """
+ Return the detected path to a tool program.
+
+ Searches first the named construction variable, then
+ the SCons path.
+
+ Args:
+ var: name of construction variable to check for tool name.
+ prog: tool program to check for.
+ ENV: if present, kwargs to initialize an environment that
+ will be created to perform the lookup.
+ norm: if true, normalize any returned path looked up in
+ the environment to use UNIX-style path separators.
+
+ Returns: full path to the tool, or None.
+
+ """
+ env = self.Environment(ENV)
+ if env:
+ v = env.subst('$' + var)
+ if not v:
+ return None
+ if prog is None:
+ prog = v
+ if v != prog:
+ return None
+ result = env.WhereIs(prog)
+ if result and norm and os.sep != '/':
+ result = result.replace(os.sep, '/')
+ return result
+
+ return self.where_is(prog)
+
+ def detect_tool(self, tool, prog=None, ENV=None):
+ """
+ Given a tool (i.e., tool specification that would be passed
+ to the "tools=" parameter of Environment()) and a program that
+ corresponds to that tool, return true if and only if we can find
+ that tool using Environment.Detect().
+
+ By default, prog is set to the value passed into the tools parameter.
+ """
+
+ if not prog:
+ prog = tool
+ env = self.Environment(ENV, tools=[tool])
+ if env is None:
+ return None
+ return env.Detect([prog])
+
+ def where_is(self, prog, path=None, pathext=None):
+ """
+ Given a program, search for it in the specified external PATH,
+ or in the actual external PATH if none is specified.
+ """
+ if path is None:
+ path = os.environ['PATH']
+ if self.external:
+ if isinstance(prog, str):
+ prog = [prog]
+ for p in prog:
+ result = TestCmd.where_is(self, p, path, pathext)
+ if result:
+ return os.path.normpath(result)
+ else:
+ import SCons.Environment
+ env = SCons.Environment.Environment()
+ return env.WhereIs(prog, path, pathext)
+
+ return None
+
+ def wrap_stdout(self, build_str = "", read_str = "", error = 0, cleaning = 0):
+ """Wraps standard output string(s) in the normal
+ "Reading ... done" and "Building ... done" strings
+ """
+ cap,lc = [ ('Build','build'),
+ ('Clean','clean') ][cleaning]
+ if error:
+ term = "scons: %sing terminated because of errors.\n" % lc
+ else:
+ term = "scons: done %sing targets.\n" % lc
+ return "scons: Reading SConscript files ...\n" + \
+ read_str + \
+ "scons: done reading SConscript files.\n" + \
+ "scons: %sing targets ...\n" % cap + \
+ build_str + \
+ term
+
+ def run(self, *args, **kw):
+ """
+ Set up SCONSFLAGS for every command so test scripts don't need
+ to worry about unexpected warnings in their output.
+ """
+ sconsflags = initialize_sconsflags(self.ignore_python_version)
+ try:
+ TestCommon.run(self, *args, **kw)
+ finally:
+ restore_sconsflags(sconsflags)
+
+# Modifying the options should work and ought to be simpler, but this
+# class is used for more than just running 'scons' itself. If there's
+# an automated way of determining whether it's running 'scons' or
+# something else, this code should be resurected.
+# options = kw.get('options')
+# if options:
+# options = [options]
+# else:
+# options = []
+# if self.ignore_python_version and deprecated_python_version():
+# options.append('--warn=no-python-version')
+# # Provide a way to suppress or provide alternate flags for
+# # TestSCons purposes by setting TESTSCONS_SCONSFLAGS.
+# # (The intended use case is to set it to null when running
+# # timing tests of earlier versions of SCons which don't
+# # support the --warn=no-visual-c-missing warning.)
+# visual_c = os.environ.get('TESTSCONS_SCONSFLAGS',
+# '--warn=no-visual-c-missing')
+# if visual_c:
+# options.append(visual_c)
+# kw['options'] = ' '.join(options)
+# TestCommon.run(self, *args, **kw)
+
+ def up_to_date(self, arguments = '.', read_str = "", **kw):
+ """Asserts that all of the targets listed in arguments is
+ up to date, but does not make any assumptions on other targets.
+ This function is most useful in conjunction with the -n option.
+ """
+ s = ""
+ for arg in arguments.split():
+ s = s + "scons: `%s' is up to date.\n" % arg
+ kw['arguments'] = arguments
+ stdout = self.wrap_stdout(read_str = read_str, build_str = s)
+ # Append '.*' so that timing output that comes after the
+ # up-to-date output is okay.
+ kw['stdout'] = re.escape(stdout) + '.*'
+ kw['match'] = self.match_re_dotall
+ self.run(**kw)
+
+ def not_up_to_date(self, arguments = '.', **kw):
+ """Asserts that none of the targets listed in arguments is
+ up to date, but does not make any assumptions on other targets.
+ This function is most useful in conjunction with the -n option.
+ """
+ s = ""
+ for arg in arguments.split():
+ s = s + "(?!scons: `%s' is up to date.)" % re.escape(arg)
+ s = '('+s+'[^\n]*\n)*'
+ kw['arguments'] = arguments
+ stdout = re.escape(self.wrap_stdout(build_str='ARGUMENTSGOHERE'))
+ kw['stdout'] = stdout.replace('ARGUMENTSGOHERE', s)
+ kw['match'] = self.match_re_dotall
+ self.run(**kw)
+
+ def option_not_yet_implemented(self, option, arguments=None, **kw):
+ """
+ Verifies expected behavior for options that are not yet implemented:
+ a warning message, and exit status 1.
+ """
+ msg = "Warning: the %s option is not yet implemented\n" % option
+ kw['stderr'] = msg
+ if arguments:
+ # If it's a long option and the argument string begins with '=',
+ # it's of the form --foo=bar and needs no separating space.
+ if option[:2] == '--' and arguments[0] == '=':
+ kw['arguments'] = option + arguments
+ else:
+ kw['arguments'] = option + ' ' + arguments
+ return self.run(**kw)
+
+ def deprecated_wrap(self, msg):
+ """
+ Calculate the pattern that matches a deprecation warning.
+ """
+ return '\nscons: warning: ' + re_escape(msg) + '\n' + file_expr
+
+ def deprecated_fatal(self, warn, msg):
+ """
+ Determines if the warning has turned into a fatal error. If so,
+ passes the test, as any remaining runs are now moot.
+
+ This method expects a SConscript to be present that will causes
+ the warning. The method writes a SConstruct that calls the
+ SConsscript and looks to see what type of result occurs.
+
+ The pattern that matches the warning is returned.
+
+ TODO: Actually detect that it's now an error. We don't have any
+ cases yet, so there's no way to test it.
+ """
+ self.write('SConstruct', """if True:
+ WARN = ARGUMENTS.get('WARN')
+ if WARN: SetOption('warn', WARN)
+ SConscript('SConscript')
+ """)
+
+ def err_out():
+ # TODO calculate stderr for fatal error
+ return re_escape('put something here')
+
+ # no option, should get one of nothing, warning, or error
+ warning = self.deprecated_wrap(msg)
+ self.run(arguments = '.', stderr = None)
+ stderr = self.stderr()
+ if stderr:
+ # most common case done first
+ if match_re_dotall(stderr, warning):
+ # expected output
+ pass
+ elif match_re_dotall(stderr, err_out()):
+ # now a fatal error; skip the rest of the tests
+ self.pass_test()
+ else:
+ # test failed; have to do this by hand...
+ print(self.banner('STDOUT '))
+ print(self.stdout())
+ print(self.diff(warning, stderr, 'STDERR '))
+ self.fail_test()
+
+ return warning
+
+ def deprecated_warning(self, warn, msg):
+ """
+ Verifies the expected behavior occurs for deprecation warnings.
+ This method expects a SConscript to be present that will causes
+ the warning. The method writes a SConstruct and exercises various
+ combinations of command-line options and SetOption parameters to
+ validate that it performs correctly.
+
+ The pattern that matches the warning is returned.
+ """
+ warning = self.deprecated_fatal(warn, msg)
+
+ def RunPair(option, expected):
+ # run the same test with the option on the command line and
+ # then with the option passed via SetOption().
+ self.run(options = '--warn=' + option,
+ arguments = '.',
+ stderr = expected,
+ match = match_re_dotall)
+ self.run(options = 'WARN=' + option,
+ arguments = '.',
+ stderr = expected,
+ match = match_re_dotall)
+
+ # all warnings off, should get no output
+ RunPair('no-deprecated', '')
+
+ # warning enabled, should get expected output
+ RunPair(warn, warning)
+
+ # warning disabled, should get either nothing or mandatory message
+ expect = """()|(Can not disable mandataory warning: 'no-%s'\n\n%s)""" % (warn, warning)
+ RunPair('no-' + warn, expect)
+
+ return warning
+
+ def diff_substr(self, expect, actual, prelen=20, postlen=40):
+ i = 0
+ for x, y in zip(expect, actual):
+ if x != y:
+ return "Actual did not match expect at char %d:\n" \
+ " Expect: %s\n" \
+ " Actual: %s\n" \
+ % (i, repr(expect[i-prelen:i+postlen]),
+ repr(actual[i-prelen:i+postlen]))
+ i = i + 1
+ return "Actual matched the expected output???"
+
+ def python_file_line(self, file, line):
+ """
+ Returns a Python error line for output comparisons.
+
+ The exec of the traceback line gives us the correct format for
+ this version of Python.
+
+ File "<string>", line 1, <module>
+
+ We stick the requested file name and line number in the right
+ places, abstracting out the version difference.
+ """
+ # This routine used to use traceback to get the proper format
+ # that doesn't work well with py3. And the format of the
+ # traceback seems to be stable, so let's just format
+ # an appropriate string
+ #
+ #exec('import traceback; x = traceback.format_stack()[-1]')
+ # import traceback
+ # x = traceback.format_stack()
+ # x = # XXX: .lstrip()
+ # x = x.replace('<string>', file)
+ # x = x.replace('line 1,', 'line %s,' % line)
+ # x="\n".join(x)
+ x='File "%s", line %s, in <module>\n'%(file,line)
+ return x
+
+ def normalize_ps(self, s):
+ s = re.sub(r'(Creation|Mod)Date: .*',
+ r'\1Date XXXX', s)
+ s = re.sub(r'%DVIPSSource:\s+TeX output\s.*',
+ r'%DVIPSSource: TeX output XXXX', s)
+ s = re.sub(r'/(BaseFont|FontName) /[A-Z0-9]{6}',
+ r'/\1 /XXXXXX', s)
+ s = re.sub(r'BeginFont: [A-Z0-9]{6}',
+ r'BeginFont: XXXXXX', s)
+
+ return s
+
+ @staticmethod
+ def to_bytes_re_sub(pattern, repl, str, count=0, flags=0):
+ """
+ Wrapper around re.sub to change pattern and repl to bytes to work with
+ both python 2 & 3
+ """
+ pattern = to_bytes(pattern)
+ repl = to_bytes(repl)
+ return re.sub(pattern, repl, str, count, flags)
+
+ def normalize_pdf(self, s):
+ s = self.to_bytes_re_sub(r'/(Creation|Mod)Date \(D:[^)]*\)',
+ r'/\1Date (D:XXXX)', s)
+ s = self.to_bytes_re_sub(r'/ID \[<[0-9a-fA-F]*> <[0-9a-fA-F]*>\]',
+ r'/ID [<XXXX> <XXXX>]', s)
+ s = self.to_bytes_re_sub(r'/(BaseFont|FontName) /[A-Z]{6}',
+ r'/\1 /XXXXXX', s)
+ s = self.to_bytes_re_sub(r'/Length \d+ *\n/Filter /FlateDecode\n',
+ r'/Length XXXX\n/Filter /FlateDecode\n', s)
+
+ try:
+ import zlib
+ except ImportError:
+ pass
+ else:
+ begin_marker = to_bytes('/FlateDecode\n>>\nstream\n')
+ end_marker = to_bytes('endstream\nendobj')
+
+ encoded = []
+ b = s.find(begin_marker, 0)
+ while b != -1:
+ b = b + len(begin_marker)
+ e = s.find(end_marker, b)
+ encoded.append((b, e))
+ b = s.find(begin_marker, e + len(end_marker))
+
+ x = 0
+ r = []
+ for b, e in encoded:
+ r.append(s[x:b])
+ d = zlib.decompress(s[b:e])
+ d = self.to_bytes_re_sub(r'%%CreationDate: [^\n]*\n',
+ r'%%CreationDate: 1970 Jan 01 00:00:00\n', d)
+ d = self.to_bytes_re_sub(r'%DVIPSSource: TeX output \d\d\d\d\.\d\d\.\d\d:\d\d\d\d',
+ r'%DVIPSSource: TeX output 1970.01.01:0000', d)
+ d = self.to_bytes_re_sub(r'/(BaseFont|FontName) /[A-Z]{6}',
+ r'/\1 /XXXXXX', d)
+ r.append(d)
+ x = e
+ r.append(s[x:])
+ s = to_bytes('').join(r)
+
+ return s
+
+ def paths(self,patterns):
+ import glob
+ result = []
+ for p in patterns:
+ result.extend(sorted(glob.glob(p)))
+ return result
+
+ def unlink_sconsignfile(self,name='.sconsign.dblite'):
+ """
+ Delete sconsign file.
+ Note on python it seems to append .p3 to the file name so we take care of that
+ Parameters
+ ----------
+ name - expected name of sconsign file
+
+ Returns
+ -------
+ None
+ """
+ if sys.version_info[0] == 3:
+ name += '.p3'
+ self.unlink(name)
+
+ def java_ENV(self, version=None):
+ """
+ Initialize with a default external environment that uses a local
+ Java SDK in preference to whatever's found in the default PATH.
+ """
+ if not self.external:
+ try:
+ return self._java_env[version]['ENV']
+ except AttributeError:
+ self._java_env = {}
+ except KeyError:
+ pass
+
+ import SCons.Environment
+ env = SCons.Environment.Environment()
+ self._java_env[version] = env
+
+ if version:
+ if sys.platform == 'win32':
+ patterns = [
+ 'C:/Program Files*/Java/jdk%s*/bin'%version,
+ ]
+ else:
+ patterns = [
+ '/usr/java/jdk%s*/bin' % version,
+ '/usr/lib/jvm/*-%s*/bin' % version,
+ '/usr/local/j2sdk%s*/bin' % version,
+ ]
+ java_path = self.paths(patterns) + [env['ENV']['PATH']]
+ else:
+ if sys.platform == 'win32':
+ patterns = [
+ 'C:/Program Files*/Java/jdk*/bin',
+ ]
+ else:
+ patterns = [
+ '/usr/java/latest/bin',
+ '/usr/lib/jvm/*/bin',
+ '/usr/local/j2sdk*/bin',
+ ]
+ java_path = self.paths(patterns) + [env['ENV']['PATH']]
+
+ env['ENV']['PATH'] = os.pathsep.join(java_path)
+ return env['ENV']
+
+ return None
+
+ def java_where_includes(self,version=None):
+ """
+ Return java include paths compiling java jni code
+ """
+ import sys
+
+ result = []
+ if sys.platform[:6] == 'darwin':
+ java_home = self.java_where_java_home(version)
+ jni_path = os.path.join(java_home,'include','jni.h')
+ if os.path.exists(jni_path):
+ result.append(os.path.dirname(jni_path))
+
+ if not version:
+ version=''
+ jni_dirs = ['/System/Library/Frameworks/JavaVM.framework/Headers/jni.h',
+ '/usr/lib/jvm/default-java/include/jni.h',
+ '/usr/lib/jvm/java-*-oracle/include/jni.h']
+ else:
+ jni_dirs = ['/System/Library/Frameworks/JavaVM.framework/Versions/%s*/Headers/jni.h'%version]
+ jni_dirs.extend(['/usr/lib/jvm/java-*-sun-%s*/include/jni.h'%version,
+ '/usr/lib/jvm/java-%s*-openjdk*/include/jni.h'%version,
+ '/usr/java/jdk%s*/include/jni.h'%version])
+ dirs = self.paths(jni_dirs)
+ if not dirs:
+ return None
+ d=os.path.dirname(self.paths(jni_dirs)[0])
+ result.append(d)
+
+ if sys.platform == 'win32':
+ result.append(os.path.join(d,'win32'))
+ elif sys.platform.startswith('linux'):
+ result.append(os.path.join(d,'linux'))
+ return result
+
+ def java_where_java_home(self, version=None):
+ if sys.platform[:6] == 'darwin':
+ # osx 10.11, 10.12
+ home_tool = '/usr/libexec/java_home'
+ java_home = False
+ if os.path.exists(home_tool):
+ java_home = subprocess.check_output(home_tool).strip()
+ java_home = java_home.decode()
+
+ if version is None:
+ if java_home:
+ return java_home
+ else:
+ homes = ['/System/Library/Frameworks/JavaVM.framework/Home',
+ # osx 10.10
+ '/System/Library/Frameworks/JavaVM.framework/Versions/Current/Home']
+ for home in homes:
+ if os.path.exists(home):
+ return home
+
+ else:
+ if java_home.find('jdk%s'%version) != -1:
+ return java_home
+ else:
+ home = '/System/Library/Frameworks/JavaVM.framework/Versions/%s/Home' % version
+ if not os.path.exists(home):
+ # This works on OSX 10.10
+ home = '/System/Library/Frameworks/JavaVM.framework/Versions/Current/'
+ else:
+ jar = self.java_where_jar(version)
+ home = os.path.normpath('%s/..'%jar)
+ if os.path.isdir(home):
+ return home
+ print("Could not determine JAVA_HOME: %s is not a directory" % home)
+ self.fail_test()
+
+ def java_mac_check(self, where_java_bin, java_bin_name):
+ # on Mac there is a place holder java installed to start the java install process
+ # so we need to check the output in this case, more info here:
+ # http://anas.pk/2015/09/02/solution-no-java-runtime-present-mac-yosemite/
+ sp = subprocess.Popen([where_java_bin, "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdout, stderr = sp.communicate()
+ sp.wait()
+ if("No Java runtime" in str(stderr)):
+ self.skip_test("Could not find Java " + java_bin_name + ", skipping test(s).\n")
+
+ def java_where_jar(self, version=None):
+ ENV = self.java_ENV(version)
+ if self.detect_tool('jar', ENV=ENV):
+ where_jar = self.detect('JAR', 'jar', ENV=ENV)
+ else:
+ where_jar = self.where_is('jar', ENV['PATH'])
+ if not where_jar:
+ self.skip_test("Could not find Java jar, skipping test(s).\n")
+ elif sys.platform == "darwin":
+ self.java_mac_check(where_jar, 'jar')
+
+ return where_jar
+
+ def java_where_java(self, version=None):
+ """
+ Return a path to the java executable.
+ """
+ ENV = self.java_ENV(version)
+ where_java = self.where_is('java', ENV['PATH'])
+
+ if not where_java:
+ self.skip_test("Could not find Java java, skipping test(s).\n")
+ elif sys.platform == "darwin":
+ self.java_mac_check(where_java, 'java')
+
+ return where_java
+
+ def java_where_javac(self, version=None):
+ """
+ Return a path to the javac compiler.
+ """
+ ENV = self.java_ENV(version)
+ if self.detect_tool('javac'):
+ where_javac = self.detect('JAVAC', 'javac', ENV=ENV)
+ else:
+ where_javac = self.where_is('javac', ENV['PATH'])
+ if not where_javac:
+ self.skip_test("Could not find Java javac, skipping test(s).\n")
+ elif sys.platform == "darwin":
+ self.java_mac_check(where_javac, 'javac')
+
+ self.run(program = where_javac,
+ arguments = '-version',
+ stderr=None,
+ status=None)
+ if version:
+ if self.stderr().find('javac %s' % version) == -1:
+ fmt = "Could not find javac for Java version %s, skipping test(s).\n"
+ self.skip_test(fmt % version)
+ else:
+ m = re.search(r'javac (\d\.*\d)', self.stderr())
+ # Java 11 outputs this to stdout
+ if not m:
+ m = re.search(r'javac (\d\.*\d)', self.stdout())
+
+ if m:
+ version = m.group(1)
+ self.javac_is_gcj = False
+ elif self.stderr().find('gcj') != -1:
+ version='1.2'
+ self.javac_is_gcj = True
+ else:
+ version = None
+ self.javac_is_gcj = False
+ return where_javac, version
+
+ def java_where_javah(self, version=None):
+ ENV = self.java_ENV(version)
+ if self.detect_tool('javah'):
+ where_javah = self.detect('JAVAH', 'javah', ENV=ENV)
+ else:
+ where_javah = self.where_is('javah', ENV['PATH'])
+ if not where_javah:
+ self.skip_test("Could not find Java javah, skipping test(s).\n")
+ return where_javah
+
+ def java_where_rmic(self, version=None):
+ ENV = self.java_ENV(version)
+ if self.detect_tool('rmic'):
+ where_rmic = self.detect('RMIC', 'rmic', ENV=ENV)
+ else:
+ where_rmic = self.where_is('rmic', ENV['PATH'])
+ if not where_rmic:
+ self.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n")
+ return where_rmic
+
+
+ def java_get_class_files(self, dir):
+ result = []
+ for dirpath, dirnames, filenames in os.walk(dir):
+ for fname in filenames:
+ if fname.endswith('.class'):
+ result.append(os.path.join(dirpath, fname))
+ return sorted(result)
+
+
+ def Qt_dummy_installation(self, dir='qt'):
+ # create a dummy qt installation
+
+ self.subdir( dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'] )
+
+ self.write([dir, 'bin', 'mymoc.py'], """\
+import getopt
+import sys
+import re
+# -w and -z are fake options used in test/QT/QTFLAGS.py
+cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:wz', [])
+output = None
+impl = 0
+opt_string = ''
+for opt, arg in cmd_opts:
+ if opt == '-o': output = open(arg, 'w')
+ elif opt == '-i': impl = 1
+ else: opt_string = opt_string + ' ' + opt
+output.write("/* mymoc.py%s */\\n" % opt_string)
+for a in args:
+ with open(a, 'r') as f:
+ contents = f.read()
+ a = a.replace('\\\\', '\\\\\\\\')
+ subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
+ if impl:
+ contents = re.sub( r'#include.*', '', contents )
+ output.write(contents.replace('Q_OBJECT', subst))
+output.close()
+sys.exit(0)
+""")
+
+ self.write([dir, 'bin', 'myuic.py'], """\
+import os.path
+import re
+import sys
+output_arg = 0
+impl_arg = 0
+impl = None
+source = None
+opt_string = ''
+for arg in sys.argv[1:]:
+ if output_arg:
+ output = open(arg, 'w')
+ output_arg = 0
+ elif impl_arg:
+ impl = arg
+ impl_arg = 0
+ elif arg == "-o":
+ output_arg = 1
+ elif arg == "-impl":
+ impl_arg = 1
+ elif arg[0:1] == "-":
+ opt_string = opt_string + ' ' + arg
+ else:
+ if source:
+ sys.exit(1)
+ source = open(arg, 'r')
+ sourceFile = arg
+output.write("/* myuic.py%s */\\n" % opt_string)
+if impl:
+ output.write( '#include "' + impl + '"\\n' )
+ includes = re.findall('<include.*?>(.*?)</include>', source.read())
+ for incFile in includes:
+ # this is valid for ui.h files, at least
+ if os.path.exists(incFile):
+ output.write('#include "' + incFile + '"\\n')
+else:
+ output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
+output.close()
+sys.exit(0)
+""" )
+
+ self.write([dir, 'include', 'my_qobject.h'], r"""
+#define Q_OBJECT ;
+void my_qt_symbol(const char *arg);
+""")
+
+ self.write([dir, 'lib', 'my_qobject.cpp'], r"""
+#include "../include/my_qobject.h"
+#include <stdio.h>
+void my_qt_symbol(const char *arg) {
+ fputs( arg, stdout );
+}
+""")
+
+ self.write([dir, 'lib', 'SConstruct'], r"""
+env = Environment()
+import sys
+if sys.platform == 'win32':
+ env.StaticLibrary( 'myqt', 'my_qobject.cpp' )
+else:
+ env.SharedLibrary( 'myqt', 'my_qobject.cpp' )
+""")
+
+ self.run(chdir = self.workpath(dir, 'lib'),
+ arguments = '.',
+ stderr = noisy_ar,
+ match = self.match_re_dotall)
+
+ self.QT = self.workpath(dir)
+ self.QT_LIB = 'myqt'
+ self.QT_MOC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'mymoc.py'))
+ self.QT_UIC = '%s %s' % (_python_, self.workpath(dir, 'bin', 'myuic.py'))
+ self.QT_LIB_DIR = self.workpath(dir, 'lib')
+
+ def Qt_create_SConstruct(self, place):
+ if isinstance(place, list):
+ place = test.workpath(*place)
+ self.write(place, """\
+if ARGUMENTS.get('noqtdir', 0): QTDIR=None
+else: QTDIR=r'%s'
+env = Environment(QTDIR = QTDIR,
+ QT_LIB = r'%s',
+ QT_MOC = r'%s',
+ QT_UIC = r'%s',
+ tools=['default','qt'])
+dup = 1
+if ARGUMENTS.get('variant_dir', 0):
+ if ARGUMENTS.get('chdir', 0):
+ SConscriptChdir(1)
+ else:
+ SConscriptChdir(0)
+ dup=int(ARGUMENTS.get('dup', 1))
+ if dup == 0:
+ builddir = 'build_dup0'
+ env['QT_DEBUG'] = 1
+ else:
+ builddir = 'build'
+ VariantDir(builddir, '.', duplicate=dup)
+ print(builddir, dup)
+ sconscript = Dir(builddir).File('SConscript')
+else:
+ sconscript = File('SConscript')
+Export("env dup")
+SConscript( sconscript )
+""" % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
+
+
+ NCR = 0 # non-cached rebuild
+ CR = 1 # cached rebuild (up to date)
+ NCF = 2 # non-cached build failure
+ CF = 3 # cached build failure
+
+ if sys.platform == 'win32':
+ Configure_lib = 'msvcrt'
+ else:
+ Configure_lib = 'm'
+
+ # to use cygwin compilers on cmd.exe -> uncomment following line
+ #Configure_lib = 'm'
+
+ def coverage_run(self):
+ """ Check if the the tests are being run under coverage.
+ """
+ return 'COVERAGE_PROCESS_START' in os.environ or 'COVERAGE_FILE' in os.environ
+
+ def skip_if_not_msvc(self, check_platform=True):
+ """ Check whether we are on a Windows platform and skip the
+ test if not. This check can be omitted by setting
+ check_platform to False.
+ Then, for a win32 platform, additionally check
+ whether we have a MSVC toolchain installed
+ in the system, and skip the test if none can be
+ found (=MinGW is the only compiler available).
+ """
+ if check_platform:
+ if sys.platform != 'win32':
+ msg = "Skipping Visual C/C++ test on non-Windows platform '%s'\n" % sys.platform
+ self.skip_test(msg)
+ return
+
+ try:
+ import SCons.Tool.MSCommon as msc
+ if not msc.msvc_exists():
+ msg = "No MSVC toolchain found...skipping test\n"
+ self.skip_test(msg)
+ except:
+ pass
+
+ def checkLogAndStdout(self, checks, results, cached,
+ logfile, sconf_dir, sconstruct,
+ doCheckLog=True, doCheckStdout=True):
+ """
+ Used to verify the expected output from using Configure()
+ via the contents of one or both of stdout or config.log file.
+ The checks, results, cached parameters all are zipped together
+ for use in comparing results.
+
+ TODO: Perhaps a better API makes sense?
+
+ Parameters
+ ----------
+ checks : The Configure checks being run
+
+ results : The expected results for each check
+
+ cached : If the corresponding check is expected to be cached
+
+ logfile : Name of the config log
+
+ sconf_dir : Name of the sconf dir
+
+ sconstruct : SConstruct file name
+
+ doCheckLog : check specified log file, defaults to true
+
+ doCheckStdout : Check stdout, defaults to true
+
+ Returns
+ -------
+
+ """
+
+ class NoMatch(Exception):
+ def __init__(self, p):
+ self.pos = p
+
+ def matchPart(log, logfile, lastEnd, NoMatch=NoMatch):
+ """
+ Match part of the logfile
+ """
+ m = re.match(log, logfile[lastEnd:])
+ if not m:
+ raise NoMatch(lastEnd)
+ return m.end() + lastEnd
+
+ try:
+
+ # Build regexp for a character which is not
+ # a linesep, and in the case of CR/LF
+ # build it with both CR and CR/LF
+ # TODO: Not sure why this is a good idea. A static string
+ # could do the same since we only have two variations
+ # to do with?
+ # ls = os.linesep
+ # nols = "("
+ # for i in range(len(ls)):
+ # nols = nols + "("
+ # for j in range(i):
+ # nols = nols + ls[j]
+ # nols = nols + "[^" + ls[i] + "])"
+ # if i < len(ls)-1:
+ # nols = nols + "|"
+ # nols = nols + ")"
+ #
+ # Replaced above logic with \n as we're reading the file
+ # using non-binary read. Python will translate \r\n -> \n
+ # For us.
+ ls = '\n'
+ nols = '([^\n])'
+ lastEnd = 0
+
+ # Read the whole logfile
+ logfile = self.read(self.workpath(logfile), mode='r')
+
+ # Some debug code to keep around..
+ # sys.stderr.write("LOGFILE[%s]:%s"%(type(logfile),logfile))
+
+ if (doCheckLog and
+ logfile.find("scons: warning: The stored build information has an unexpected class.") >= 0):
+ self.fail_test()
+
+ sconf_dir = sconf_dir
+ sconstruct = sconstruct
+
+ log = r'file\ \S*%s\,line \d+:' % re.escape(sconstruct) + ls
+ if doCheckLog:
+ lastEnd = matchPart(log, logfile, lastEnd)
+
+ log = "\t" + re.escape("Configure(confdir = %s)" % sconf_dir) + ls
+ if doCheckLog:
+ lastEnd = matchPart(log, logfile, lastEnd)
+
+ rdstr = ""
+ cnt = 0
+ for check,result,cache_desc in zip(checks, results, cached):
+ log = re.escape("scons: Configure: " + check) + ls
+
+ if doCheckLog:
+ lastEnd = matchPart(log, logfile, lastEnd)
+
+ log = ""
+ result_cached = 1
+ for bld_desc in cache_desc: # each TryXXX
+ for ext, flag in bld_desc: # each file in TryBuild
+ file = os.path.join(sconf_dir,"conftest_%d%s" % (cnt, ext))
+ if flag == self.NCR:
+ # NCR = Non Cached Rebuild
+ # rebuild will pass
+ if ext in ['.c', '.cpp']:
+ log=log + re.escape(file + " <-") + ls
+ log=log + r"( \|" + nols + "*" + ls + ")+?"
+ else:
+ log=log + "(" + nols + "*" + ls +")*?"
+ result_cached = 0
+ if flag == self.CR:
+ # CR = cached rebuild (up to date)s
+ # up to date
+ log=log + \
+ re.escape("scons: Configure: \"%s\" is up to date."
+ % file) + ls
+ log=log+re.escape("scons: Configure: The original builder "
+ "output was:") + ls
+ log=log+r"( \|.*"+ls+")+"
+ if flag == self.NCF:
+ # non-cached rebuild failure
+ log=log + "(" + nols + "*" + ls + ")*?"
+ result_cached = 0
+ if flag == self.CF:
+ # cached rebuild failure
+ log=log + \
+ re.escape("scons: Configure: Building \"%s\" failed "
+ "in a previous run and all its sources are"
+ " up to date." % file) + ls
+ log=log+re.escape("scons: Configure: The original builder "
+ "output was:") + ls
+ log=log+r"( \|.*"+ls+")+"
+ cnt = cnt + 1
+ if result_cached:
+ result = "(cached) " + result
+ rdstr = rdstr + re.escape(check) + re.escape(result) + "\n"
+ log=log + re.escape("scons: Configure: " + result) + ls + ls
+
+ if doCheckLog:
+ lastEnd = matchPart(log, logfile, lastEnd)
+
+ log = ""
+ if doCheckLog: lastEnd = matchPart(ls, logfile, lastEnd)
+ if doCheckLog and lastEnd != len(logfile):
+ raise NoMatch(lastEnd)
+
+ except NoMatch as m:
+ print("Cannot match log file against log regexp.")
+ print("log file: ")
+ print("------------------------------------------------------")
+ print(logfile[m.pos:])
+ print("------------------------------------------------------")
+ print("log regexp: ")
+ print("------------------------------------------------------")
+ print(log)
+ print("------------------------------------------------------")
+ self.fail_test()
+
+ if doCheckStdout:
+ exp_stdout = self.wrap_stdout(".*", rdstr)
+ if not self.match_re_dotall(self.stdout(), exp_stdout):
+ print("Unexpected stdout: ")
+ print("-----------------------------------------------------")
+ print(repr(self.stdout()))
+ print("-----------------------------------------------------")
+ print(repr(exp_stdout))
+ print("-----------------------------------------------------")
+ self.fail_test()
+
+ def get_python_version(self):
+ """
+ Returns the Python version (just so everyone doesn't have to
+ hand-code slicing the right number of characters).
+ """
+ # see also sys.prefix documentation
+ return python_minor_version_string()
+
+ def get_platform_python_info(self, python_h_required=False):
+ """
+ Returns a path to a Python executable suitable for testing on
+ this platform and its associated include path, library path and
+ library name.
+
+ If the Python executable or Python header (if required)
+ is not found, the test is skipped.
+
+ Returns a tuple:
+ (path to python, include path, library path, library name)
+ """
+ python = os.environ.get('python_executable', self.where_is('python'))
+ if not python:
+ self.skip_test('Can not find installed "python", skipping test.\n')
+
+ # construct a program to run in the intended environment
+ # in order to fetch the characteristics of that Python.
+ # Windows Python doesn't store all the info in config vars.
+ if sys.platform == 'win32':
+ self.run(program=python, stdin="""\
+import sysconfig, sys, os.path
+py_ver = 'python%d%d' % sys.version_info[:2]
+# use distutils to help find include and lib path
+# TODO: PY3 fine to use sysconfig.get_config_var("INCLUDEPY")
+try:
+ import distutils.sysconfig
+ exec_prefix = distutils.sysconfig.EXEC_PREFIX
+ include = distutils.sysconfig.get_python_inc()
+ print(include)
+ lib_path = os.path.join(exec_prefix, 'libs')
+ if not os.path.exists(lib_path):
+ # check for virtualenv path.
+ # this might not build anything different than first try.
+ def venv_path():
+ if hasattr(sys, 'real_prefix'):
+ return sys.real_prefix
+ if hasattr(sys, 'base_prefix'):
+ return sys.base_prefix
+ lib_path = os.path.join(venv_path(), 'libs')
+ if not os.path.exists(lib_path):
+ # not clear this is useful: 'lib' does not contain linkable libs
+ lib_path = os.path.join(exec_prefix, 'lib')
+ print(lib_path)
+except:
+ include = os.path.join(sys.prefix, 'include', py_ver)
+ print(include)
+ lib_path = os.path.join(sys.prefix, 'lib', py_ver, 'config')
+ print(lib_path)
+print(py_ver)
+Python_h = os.path.join(include, "Python.h")
+if os.path.exists(Python_h):
+ print(Python_h)
+else:
+ print("False")
+""")
+ else:
+ self.run(program=python, stdin="""\
+import sys, sysconfig, os.path
+include = sysconfig.get_config_var("INCLUDEPY")
+print(include)
+print(sysconfig.get_config_var("LIBDIR"))
+py_library_ver = sysconfig.get_config_var("LDVERSION")
+if not py_library_ver:
+ py_library_ver = '%d.%d' % sys.version_info[:2]
+print("python"+py_library_ver)
+Python_h = os.path.join(include, "Python.h")
+if os.path.exists(Python_h):
+ print(Python_h)
+else:
+ print("False")
+""")
+ incpath, libpath, libname, python_h = self.stdout().strip().split('\n')
+ if python_h == "False" and python_h_required:
+ self.skip_test('Can not find required "Python.h", skipping test.\n')
+
+ return (python, incpath, libpath, libname)
+
+ def start(self, *args, **kw):
+ """
+ Starts SCons in the test environment.
+
+ This method exists to tell Test{Cmd,Common} that we're going to
+ use standard input without forcing every .start() call in the
+ individual tests to do so explicitly.
+ """
+ if 'stdin' not in kw:
+ kw['stdin'] = True
+ sconsflags = initialize_sconsflags(self.ignore_python_version)
+ try:
+ p = TestCommon.start(self, *args, **kw)
+ finally:
+ restore_sconsflags(sconsflags)
+ return p
+
+ def wait_for(self, fname, timeout=20.0, popen=None):
+ """
+ Waits for the specified file name to exist.
+ """
+ waited = 0.0
+ while not os.path.exists(fname):
+ if timeout and waited >= timeout:
+ sys.stderr.write('timed out waiting for %s to exist\n' % fname)
+ if popen:
+ popen.stdin.close()
+ popen.stdin = None
+ self.status = 1
+ self.finish(popen)
+ stdout = self.stdout()
+ if stdout:
+ sys.stdout.write(self.banner('STDOUT ') + '\n')
+ sys.stdout.write(stdout)
+ stderr = self.stderr()
+ if stderr:
+ sys.stderr.write(self.banner('STDERR ') + '\n')
+ sys.stderr.write(stderr)
+ self.fail_test()
+ time.sleep(1.0)
+ waited = waited + 1.0
+
+ def get_alt_cpp_suffix(self):
+ """
+ Many CXX tests have this same logic.
+ They all needed to determine if the current os supports
+ files with .C and .c as different files or not
+ in which case they are instructed to use .cpp instead of .C
+ """
+ if not case_sensitive_suffixes('.c','.C'):
+ alt_cpp_suffix = '.cpp'
+ else:
+ alt_cpp_suffix = '.C'
+ return alt_cpp_suffix
+
+ def platform_has_symlink(self):
+ if not hasattr(os, 'symlink') or sys.platform == 'win32':
+ return False
+ else:
+ return True
+
+
+class Stat:
+ def __init__(self, name, units, expression, convert=None):
+ if convert is None:
+ convert = lambda x: x
+ self.name = name
+ self.units = units
+ self.expression = re.compile(expression)
+ self.convert = convert
+
+StatList = [
+ Stat('memory-initial', 'kbytes',
+ r'Memory before reading SConscript files:\s+(\d+)',
+ convert=lambda s: int(s) // 1024),
+ Stat('memory-prebuild', 'kbytes',
+ r'Memory before building targets:\s+(\d+)',
+ convert=lambda s: int(s) // 1024),
+ Stat('memory-final', 'kbytes',
+ r'Memory after building targets:\s+(\d+)',
+ convert=lambda s: int(s) // 1024),
+
+ Stat('time-sconscript', 'seconds',
+ r'Total SConscript file execution time:\s+([\d.]+) seconds'),
+ Stat('time-scons', 'seconds',
+ r'Total SCons execution time:\s+([\d.]+) seconds'),
+ Stat('time-commands', 'seconds',
+ r'Total command execution time:\s+([\d.]+) seconds'),
+ Stat('time-total', 'seconds',
+ r'Total build time:\s+([\d.]+) seconds'),
+]
+
+
+class TimeSCons(TestSCons):
+ """Class for timing SCons."""
+ def __init__(self, *args, **kw):
+ """
+ In addition to normal TestSCons.TestSCons intialization,
+ this enables verbose mode (which causes the command lines to
+ be displayed in the output) and copies the contents of the
+ directory containing the executing script to the temporary
+ working directory.
+ """
+ self.variables = kw.get('variables')
+ default_calibrate_variables = []
+ if self.variables is not None:
+ for variable, value in self.variables.items():
+ value = os.environ.get(variable, value)
+ try:
+ value = int(value)
+ except ValueError:
+ try:
+ value = float(value)
+ except ValueError:
+ pass
+ else:
+ default_calibrate_variables.append(variable)
+ else:
+ default_calibrate_variables.append(variable)
+ self.variables[variable] = value
+ del kw['variables']
+ calibrate_keyword_arg = kw.get('calibrate')
+ if calibrate_keyword_arg is None:
+ self.calibrate_variables = default_calibrate_variables
+ else:
+ self.calibrate_variables = calibrate_keyword_arg
+ del kw['calibrate']
+
+ self.calibrate = os.environ.get('TIMESCONS_CALIBRATE', '0') != '0'
+
+ if 'verbose' not in kw and not self.calibrate:
+ kw['verbose'] = True
+
+ TestSCons.__init__(self, *args, **kw)
+
+ # TODO(sgk): better way to get the script dir than sys.argv[0]
+ self.test_dir = os.path.dirname(sys.argv[0])
+ test_name = os.path.basename(self.test_dir)
+
+ if not os.path.isabs(self.test_dir):
+ self.test_dir = os.path.join(self.orig_cwd, self.test_dir)
+ self.copy_timing_configuration(self.test_dir, self.workpath())
+
+ def main(self, *args, **kw):
+ """
+ The main entry point for standard execution of timings.
+
+ This method run SCons three times:
+
+ Once with the --help option, to have it exit after just reading
+ the configuration.
+
+ Once as a full build of all targets.
+
+ Once again as a (presumably) null or up-to-date build of
+ all targets.
+
+ The elapsed time to execute each build is printed after
+ it has finished.
+ """
+ if 'options' not in kw and self.variables:
+ options = []
+ for variable, value in self.variables.items():
+ options.append('%s=%s' % (variable, value))
+ kw['options'] = ' '.join(options)
+ if self.calibrate:
+ self.calibration(*args, **kw)
+ else:
+ self.uptime()
+ self.startup(*args, **kw)
+ self.full(*args, **kw)
+ self.null(*args, **kw)
+
+ def trace(self, graph, name, value, units, sort=None):
+ fmt = "TRACE: graph=%s name=%s value=%s units=%s"
+ line = fmt % (graph, name, value, units)
+ if sort is not None:
+ line = line + (' sort=%s' % sort)
+ line = line + '\n'
+ sys.stdout.write(line)
+ sys.stdout.flush()
+
+ def report_traces(self, trace, stats):
+ self.trace('TimeSCons-elapsed',
+ trace,
+ self.elapsed_time(),
+ "seconds",
+ sort=0)
+ for name, args in stats.items():
+ self.trace(name, trace, **args)
+
+ def uptime(self):
+ try:
+ fp = open('/proc/loadavg')
+ except EnvironmentError:
+ pass
+ else:
+ avg1, avg5, avg15 = fp.readline().split(" ")[:3]
+ fp.close()
+ self.trace('load-average', 'average1', avg1, 'processes')
+ self.trace('load-average', 'average5', avg5, 'processes')
+ self.trace('load-average', 'average15', avg15, 'processes')
+
+ def collect_stats(self, input):
+ result = {}
+ for stat in StatList:
+ m = stat.expression.search(input)
+ if m:
+ value = stat.convert(m.group(1))
+ # The dict keys match the keyword= arguments
+ # of the trace() method above so they can be
+ # applied directly to that call.
+ result[stat.name] = {'value':value, 'units':stat.units}
+ return result
+
+ def add_timing_options(self, kw, additional=None):
+ """
+ Add the necessary timings options to the kw['options'] value.
+ """
+ options = kw.get('options', '')
+ if additional is not None:
+ options += additional
+ kw['options'] = options + ' --debug=memory,time'
+
+ def startup(self, *args, **kw):
+ """
+ Runs scons with the --help option.
+
+ This serves as a way to isolate just the amount of startup time
+ spent reading up the configuration, since --help exits before any
+ "real work" is done.
+ """
+ self.add_timing_options(kw, ' --help')
+ # Ignore the exit status. If the --help run dies, we just
+ # won't report any statistics for it, but we can still execute
+ # the full and null builds.
+ kw['status'] = None
+ self.run(*args, **kw)
+ sys.stdout.write(self.stdout())
+ stats = self.collect_stats(self.stdout())
+ # Delete the time-commands, since no commands are ever
+ # executed on the help run and it is (or should be) always 0.0.
+ del stats['time-commands']
+ self.report_traces('startup', stats)
+
+ def full(self, *args, **kw):
+ """
+ Runs a full build of SCons.
+ """
+ self.add_timing_options(kw)
+ self.run(*args, **kw)
+ sys.stdout.write(self.stdout())
+ stats = self.collect_stats(self.stdout())
+ self.report_traces('full', stats)
+ self.trace('full-memory', 'initial', **stats['memory-initial'])
+ self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
+ self.trace('full-memory', 'final', **stats['memory-final'])
+
+ def calibration(self, *args, **kw):
+ """
+ Runs a full build of SCons, but only reports calibration
+ information (the variable(s) that were set for this configuration,
+ and the elapsed time to run.
+ """
+ self.add_timing_options(kw)
+ self.run(*args, **kw)
+ for variable in self.calibrate_variables:
+ value = self.variables[variable]
+ sys.stdout.write('VARIABLE: %s=%s\n' % (variable, value))
+ sys.stdout.write('ELAPSED: %s\n' % self.elapsed_time())
+
+ def null(self, *args, **kw):
+ """
+ Runs an up-to-date null build of SCons.
+ """
+ # TODO(sgk): allow the caller to specify the target (argument)
+ # that must be up-to-date.
+ self.add_timing_options(kw)
+ self.up_to_date(arguments='.', **kw)
+ sys.stdout.write(self.stdout())
+ stats = self.collect_stats(self.stdout())
+ # time-commands should always be 0.0 on a null build, because
+ # no commands should be executed. Remove it from the stats
+ # so we don't trace it, but only if it *is* 0 so that we'll
+ # get some indication if a supposedly-null build actually does
+ # build something.
+ if float(stats['time-commands']['value']) == 0.0:
+ del stats['time-commands']
+ self.report_traces('null', stats)
+ self.trace('null-memory', 'initial', **stats['memory-initial'])
+ self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
+ self.trace('null-memory', 'final', **stats['memory-final'])
+
+ def elapsed_time(self):
+ """
+ Returns the elapsed time of the most recent command execution.
+ """
+ return self.endTime - self.startTime
+
+ def run(self, *args, **kw):
+ """
+ Runs a single build command, capturing output in the specified file.
+
+ Because this class is about timing SCons, we record the start
+ and end times of the elapsed execution, and also add the
+ --debug=memory and --debug=time options to have SCons report
+ its own memory and timing statistics.
+ """
+ self.startTime = time.time()
+ try:
+ result = TestSCons.run(self, *args, **kw)
+ finally:
+ self.endTime = time.time()
+ return result
+
+ def copy_timing_configuration(self, source_dir, dest_dir):
+ """
+ Copies the timing configuration from the specified source_dir (the
+ directory in which the controlling script lives) to the specified
+ dest_dir (a temporary working directory).
+
+ This ignores all files and directories that begin with the string
+ 'TimeSCons-', and all '.svn' subdirectories.
+ """
+ for root, dirs, files in os.walk(source_dir):
+ if '.svn' in dirs:
+ dirs.remove('.svn')
+ dirs = [ d for d in dirs if not d.startswith('TimeSCons-') ]
+ files = [ f for f in files if not f.startswith('TimeSCons-') ]
+ for dirname in dirs:
+ source = os.path.join(root, dirname)
+ destination = source.replace(source_dir, dest_dir)
+ os.mkdir(destination)
+ if sys.platform != 'win32':
+ shutil.copystat(source, destination)
+ for filename in files:
+ source = os.path.join(root, filename)
+ destination = source.replace(source_dir, dest_dir)
+ shutil.copy2(source, destination)
+
+
+# In some environments, $AR will generate a warning message to stderr
+# if the library doesn't previously exist and is being created. One
+# way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
+# but this is difficult to do in a platform-/implementation-specific
+# method. Instead, we will use the following as a stderr match for
+# tests that use AR so that we will view zero or more "ar: creating
+# <file>" messages to be successful executions of the test (see
+# test/AR.py for sample usage).
+
+noisy_ar=r'(ar: creating( archive)? \S+\n?)*'
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4: