mirror of
https://github.com/boostorg/build.git
synced 2026-01-31 08:02:14 +00:00
904 lines
32 KiB
Python
904 lines
32 KiB
Python
# Copyright 2002-2005 Vladimir Prus.
|
|
# Copyright 2002-2003 Dave Abrahams.
|
|
# Copyright 2006 Rene Rivera.
|
|
# Distributed under the Boost Software License, Version 1.0.
|
|
# (See accompanying file LICENSE_1_0.txt or copy at
|
|
# http://www.boost.org/LICENSE_1_0.txt)
|
|
|
|
import TestCmd
|
|
import copy
|
|
import fnmatch
|
|
import glob
|
|
import math
|
|
import os
|
|
import re
|
|
import shutil
|
|
import string
|
|
import StringIO
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import traceback
|
|
import tree
|
|
import types
|
|
|
|
|
|
annotations = []
|
|
|
|
|
|
def print_annotation(name, value):
|
|
"""Writes some named bits of information about test run.
|
|
"""
|
|
print name + " {{{"
|
|
print value
|
|
print "}}}"
|
|
|
|
|
|
def flush_annotations():
|
|
global annotations
|
|
for ann in annotations:
|
|
print_annotation(ann[0], ann[1])
|
|
annotations = []
|
|
|
|
|
|
defer_annotations = 0
|
|
|
|
|
|
def set_defer_annotations(n):
|
|
global defer_annotations
|
|
defer_annotations = n
|
|
|
|
|
|
def annotation(name, value):
|
|
"""Records an annotation about the test run.
|
|
"""
|
|
annotations.append((name, value))
|
|
if not defer_annotations:
|
|
flush_annotations()
|
|
|
|
|
|
def get_toolset():
|
|
toolset = None;
|
|
for arg in sys.argv[1:]:
|
|
if not arg.startswith('-'):
|
|
toolset = arg
|
|
return toolset or 'gcc'
|
|
|
|
|
|
# Detect the host OS.
|
|
windows = False
|
|
if os.environ.get('OS','').lower().startswith('windows') or \
|
|
os.__dict__.has_key('uname') and \
|
|
os.uname()[0].lower().startswith('cygwin'):
|
|
windows = True
|
|
|
|
|
|
suffixes = {}
|
|
|
|
|
|
# Prepare the map of suffixes
|
|
def prepare_suffix_map(toolset):
|
|
global windows
|
|
global suffixes
|
|
suffixes = {'.exe': '', '.dll': '.so', '.lib': '.a', '.obj': '.o'}
|
|
suffixes['.implib'] = '.no_implib_files_on_this_platform'
|
|
if windows:
|
|
suffixes = {}
|
|
if toolset in ["gcc"]:
|
|
suffixes['.lib'] = '.a' # static libs have '.a' suffix with mingw...
|
|
suffixes['.obj'] = '.o'
|
|
suffixes['.implib'] = '.lib'
|
|
if os.__dict__.has_key('uname') and os.uname()[0] == 'Darwin':
|
|
suffixes['.dll'] = '.dylib'
|
|
|
|
|
|
def re_remove(sequence, regex):
|
|
me = re.compile(regex)
|
|
result = filter( lambda x: me.match(x), sequence )
|
|
if 0 == len(result):
|
|
raise ValueError()
|
|
for r in result:
|
|
sequence.remove(r)
|
|
|
|
|
|
def glob_remove(sequence, pattern):
|
|
result = fnmatch.filter(sequence,pattern)
|
|
if 0 == len(result):
|
|
raise ValueError()
|
|
for r in result:
|
|
sequence.remove(r)
|
|
|
|
|
|
# Configuration stating whether Boost Build is expected to automatically prepend
|
|
# prefixes to built library targets.
|
|
lib_prefix = True
|
|
dll_prefix = True
|
|
if windows:
|
|
dll_prefix = False
|
|
|
|
|
|
#
|
|
# FIXME: this is copy-pasted from TestSCons.py
|
|
# Should be moved to TestCmd.py?
|
|
#
|
|
if os.name == 'posix':
|
|
def _failed(self, status=0):
|
|
if self.status is None:
|
|
return None
|
|
if os.WIFSIGNALED(status):
|
|
return None
|
|
return _status(self) != status
|
|
def _status(self):
|
|
if os.WIFEXITED(self.status):
|
|
return os.WEXITSTATUS(self.status)
|
|
else:
|
|
return -1
|
|
elif os.name == 'nt':
|
|
def _failed(self, status=0):
|
|
return not self.status is None and self.status != status
|
|
def _status(self):
|
|
return self.status
|
|
|
|
|
|
class Tester(TestCmd.TestCmd):
|
|
"""Main tester class for Boost Build.
|
|
|
|
Optional arguments:
|
|
|
|
`arguments` - Arguments passed to the run executable.
|
|
`executable` - Name of the executable to invoke.
|
|
`match` - Function to use for compating actual and
|
|
expected file contents.
|
|
`boost_build_path` - Boost build path to be passed to the run
|
|
executable.
|
|
`translate_suffixes` - Whether to update suffixes on the the file
|
|
names passed from the test script so they
|
|
match those actually created by the current
|
|
toolset. For example, static library files
|
|
are specified by using the .lib suffix but
|
|
when the 'gcc' toolset is used it actually
|
|
creates them using the .a suffix.
|
|
`pass_toolset` - Whether the test system should pass the
|
|
specified toolset to the run executable.
|
|
`use_test_config` - Whether the test system should tell the run
|
|
executable to read in the test_config.jam
|
|
configuration file.
|
|
`ignore_toolset_requirements` - Whether the test system should tell the run
|
|
executable to ignore toolset requirements.
|
|
`workdir` - indicates an absolute directory where the
|
|
test will be run from.
|
|
|
|
Optional arguments inherited from the base class:
|
|
|
|
`description` - Test description string displayed in case of
|
|
a failed test.
|
|
`subdir' - List of subdirectories to automatically
|
|
create under the working directory. Each
|
|
subdirectory needs to be specified
|
|
separately parent coming before its child.
|
|
`verbose` - Flag that may be used to enable more verbose
|
|
test system output. Note that it does not
|
|
also enable more verbose build system
|
|
output like the --verbose command line
|
|
option does.
|
|
"""
|
|
def __init__(self, arguments="", executable="bjam",
|
|
match=TestCmd.match_exact, boost_build_path=None,
|
|
translate_suffixes=True, pass_toolset=True, use_test_config=True,
|
|
ignore_toolset_requirements=True, workdir="", **keywords):
|
|
|
|
self.original_workdir = os.getcwd()
|
|
if workdir != '' and not os.path.isabs(workdir):
|
|
raise "Parameter workdir <"+workdir+"> must point to an absolute directory: "
|
|
|
|
self.last_build_time_start = 0
|
|
self.last_build_time_finish = 0
|
|
self.translate_suffixes = translate_suffixes
|
|
self.use_test_config = use_test_config
|
|
|
|
self.toolset = get_toolset()
|
|
self.pass_toolset = pass_toolset
|
|
self.ignore_toolset_requirements = ignore_toolset_requirements
|
|
|
|
prepare_suffix_map(pass_toolset and self.toolset or 'gcc')
|
|
|
|
if not '--default-bjam' in sys.argv:
|
|
jam_build_dir = ""
|
|
if os.name == 'nt':
|
|
jam_build_dir = "bin.ntx86"
|
|
elif os.name == 'posix' and os.__dict__.has_key('uname'):
|
|
if os.uname()[0].lower().startswith('cygwin'):
|
|
jam_build_dir = "bin.cygwinx86"
|
|
if 'TMP' in os.environ and os.environ['TMP'].find('~') != -1:
|
|
print 'Setting $TMP to /tmp to get around problem with short path names'
|
|
os.environ['TMP'] = '/tmp'
|
|
elif os.uname()[0] == 'Linux':
|
|
cpu = os.uname()[4]
|
|
if re.match("i.86", cpu):
|
|
jam_build_dir = "bin.linuxx86";
|
|
else:
|
|
jam_build_dir = "bin.linux" + os.uname()[4]
|
|
elif os.uname()[0] == 'SunOS':
|
|
jam_build_dir = "bin.solaris"
|
|
elif os.uname()[0] == 'Darwin':
|
|
if os.uname()[4] == 'i386':
|
|
jam_build_dir = "bin.macosxx86"
|
|
else:
|
|
jam_build_dir = "bin.macosxppc"
|
|
elif os.uname()[0] == "AIX":
|
|
jam_build_dir = "bin.aix"
|
|
elif os.uname()[0] == "IRIX64":
|
|
jam_build_dir = "bin.irix"
|
|
elif os.uname()[0] == "FreeBSD":
|
|
jam_build_dir = "bin.freebsd"
|
|
elif os.uname()[0] == "OSF1":
|
|
jam_build_dir = "bin.osf"
|
|
else:
|
|
raise "Don't know directory where Jam is built for this system: " + os.name + "/" + os.uname()[0]
|
|
else:
|
|
raise "Don't know directory where Jam is built for this system: " + os.name
|
|
|
|
# Find where jam_src is located. Try for the debug version if it's
|
|
# lying around.
|
|
dirs = [os.path.join('../../../jam/src', jam_build_dir + '.debug'),
|
|
os.path.join('../../../jam/src', jam_build_dir),
|
|
os.path.join('../../jam_src', jam_build_dir + '.debug'),
|
|
os.path.join('../../jam_src', jam_build_dir),
|
|
os.path.join('../jam_src', jam_build_dir + '.debug'),
|
|
os.path.join('../jam_src', jam_build_dir)]
|
|
for d in dirs:
|
|
if os.path.exists(d):
|
|
jam_build_dir = d
|
|
break
|
|
else:
|
|
print "Cannot find built Boost.Jam"
|
|
os.exit(1)
|
|
|
|
verbosity = ['-d0', '--quiet']
|
|
if '--verbose' in sys.argv:
|
|
keywords['verbose'] = True
|
|
verbosity = ['-d+2']
|
|
|
|
if boost_build_path is None:
|
|
boost_build_path = self.original_workdir
|
|
|
|
program_list = []
|
|
|
|
if '--default-bjam' in sys.argv:
|
|
program_list.append(executable)
|
|
inpath_bjam = True
|
|
else:
|
|
program_list.append(os.path.join(jam_build_dir, executable))
|
|
inpath_bjam = None
|
|
program_list.append('-sBOOST_BUILD_PATH="' + boost_build_path + '"')
|
|
if verbosity:
|
|
program_list += verbosity
|
|
if arguments:
|
|
program_list += arguments.split(" ")
|
|
|
|
TestCmd.TestCmd.__init__(
|
|
self
|
|
, program=program_list
|
|
, match=match
|
|
, workdir=workdir
|
|
, inpath=inpath_bjam
|
|
, **keywords)
|
|
|
|
os.chdir(self.workdir)
|
|
|
|
def cleanup(self):
|
|
try:
|
|
TestCmd.TestCmd.cleanup(self)
|
|
os.chdir(self.original_workdir)
|
|
except AttributeError:
|
|
# When this is called during TestCmd.TestCmd.__del__ we can have
|
|
# both 'TestCmd' and 'os' unavailable in our scope. Do nothing in
|
|
# this case.
|
|
pass
|
|
|
|
#
|
|
# Methods that change the working directory's content.
|
|
#
|
|
def set_tree(self, tree_location):
|
|
# It is not possible to remove the current directory.
|
|
d = os.getcwd()
|
|
os.chdir(os.path.dirname(self.workdir))
|
|
shutil.rmtree(self.workdir, ignore_errors=False)
|
|
|
|
if not os.path.isabs(tree_location):
|
|
tree_location = os.path.join(self.original_workdir, tree_location)
|
|
shutil.copytree(tree_location, self.workdir)
|
|
|
|
os.chdir(d)
|
|
|
|
def make_writable(unused, dir, entries):
|
|
for e in entries:
|
|
name = os.path.join(dir, e)
|
|
os.chmod(name, os.stat(name)[0] | 0222)
|
|
|
|
os.path.walk(".", make_writable, None)
|
|
|
|
def write(self, file, content):
|
|
self.wait_for_time_change_since_last_build()
|
|
nfile = self.native_file_name(file)
|
|
try:
|
|
os.makedirs(os.path.dirname(nfile))
|
|
except Exception, e:
|
|
pass
|
|
open(nfile, "wb").write(content)
|
|
|
|
def rename(self, old, new):
|
|
try:
|
|
os.makedirs(os.path.dirname(new))
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
os.remove(new)
|
|
except:
|
|
pass
|
|
|
|
os.rename(old, new)
|
|
self.touch(new);
|
|
|
|
def copy(self, src, dst):
|
|
self.wait_for_time_change_since_last_build()
|
|
try:
|
|
self.write(dst, self.read(src, 1))
|
|
except:
|
|
self.fail_test(1)
|
|
|
|
def copy_preserving_timestamp(self, src, dst):
|
|
src_name = self.native_file_name(src)
|
|
dst_name = self.native_file_name(dst)
|
|
stats = os.stat(src_name)
|
|
self.write(dst, self.read(src, 1))
|
|
os.utime(dst_name, (stats.st_atime, stats.st_mtime))
|
|
|
|
def touch(self, names):
|
|
self.wait_for_time_change_since_last_build()
|
|
for name in self.adjust_names(names):
|
|
os.utime(self.native_file_name(name), None)
|
|
|
|
def rm(self, names):
|
|
self.wait_for_time_change_since_last_build()
|
|
if not type(names) == types.ListType:
|
|
names = [names]
|
|
|
|
# Avoid attempts to remove the current directory.
|
|
os.chdir(self.original_workdir)
|
|
for name in names:
|
|
n = self.native_file_name(name)
|
|
n = glob.glob(n)
|
|
if n: n = n[0]
|
|
if not n:
|
|
n = self.glob_file(string.replace(name, "$toolset", self.toolset+"*"))
|
|
if n:
|
|
if os.path.isdir(n):
|
|
shutil.rmtree(n, ignore_errors=False)
|
|
else:
|
|
os.unlink(n)
|
|
|
|
# Create working dir root again in case we removed it.
|
|
if not os.path.exists(self.workdir):
|
|
os.mkdir(self.workdir)
|
|
os.chdir(self.workdir)
|
|
|
|
def expand_toolset(self, name):
|
|
"""Expands $toolset in the given file to tested toolset.
|
|
"""
|
|
content = self.read(name)
|
|
content = string.replace(content, "$toolset", self.toolset)
|
|
self.write(name, content)
|
|
|
|
def dump_stdio(self):
|
|
annotation("STDOUT", self.stdout())
|
|
annotation("STDERR", self.stderr())
|
|
|
|
#
|
|
# FIXME: Large portion copied from TestSCons.py, should be moved?
|
|
#
|
|
def run_build_system(self, extra_args="", subdir="", stdout=None, stderr="",
|
|
status=0, match=None, pass_toolset=None, use_test_config=None,
|
|
ignore_toolset_requirements=None, expected_duration=None, **kw):
|
|
|
|
self.last_build_time_start = time.time()
|
|
|
|
try:
|
|
if os.path.isabs(subdir):
|
|
if stderr:
|
|
print "You must pass a relative directory to subdir <"+subdir+">."
|
|
status = 1
|
|
return
|
|
|
|
self.previous_tree = tree.build_tree(self.workdir)
|
|
|
|
if match is None:
|
|
match = self.match
|
|
|
|
if pass_toolset is None:
|
|
pass_toolset = self.pass_toolset
|
|
|
|
if use_test_config is None:
|
|
use_test_config = self.use_test_config
|
|
|
|
if ignore_toolset_requirements is None:
|
|
ignore_toolset_requirements = self.ignore_toolset_requirements
|
|
|
|
try:
|
|
kw['program'] = []
|
|
kw['program'] += self.program
|
|
if extra_args:
|
|
kw['program'] += extra_args.split(" ")
|
|
if pass_toolset:
|
|
kw['program'].append("toolset=" + self.toolset)
|
|
if use_test_config:
|
|
kw['program'].append('--test-config="%s"'
|
|
% os.path.join(self.original_workdir, "test-config.jam"))
|
|
if ignore_toolset_requirements:
|
|
kw['program'].append("--ignore-toolset-requirements")
|
|
kw['chdir'] = subdir
|
|
apply(TestCmd.TestCmd.run, [self], kw)
|
|
except:
|
|
self.dump_stdio()
|
|
raise
|
|
finally:
|
|
self.last_build_time_finish = time.time()
|
|
|
|
if status != None and _failed(self, status):
|
|
expect = ''
|
|
if status != 0:
|
|
expect = " (expected %d)" % status
|
|
|
|
annotation("failure", '"%s" returned %d%s'
|
|
% (kw['program'], _status(self), expect))
|
|
|
|
annotation("reason", "error returned by bjam")
|
|
self.fail_test(1)
|
|
|
|
if not stdout is None and not match(self.stdout(), stdout):
|
|
annotation("failure", "Unexpected stdout")
|
|
annotation("Expected STDOUT", stdout)
|
|
annotation("Actual STDOUT", self.stdout())
|
|
stderr = self.stderr()
|
|
if stderr:
|
|
annotation("STDERR", stderr)
|
|
self.maybe_do_diff(self.stdout(), stdout)
|
|
self.fail_test(1, dump_stdio=False)
|
|
|
|
# Intel tends to produce some messages to stderr which make tests fail.
|
|
intel_workaround = re.compile("^xi(link|lib): executing.*\n", re.M)
|
|
actual_stderr = re.sub(intel_workaround, "", self.stderr())
|
|
|
|
if not stderr is None and not match(actual_stderr, stderr):
|
|
annotation("failure", "Unexpected stderr")
|
|
annotation("Expected STDERR", stderr)
|
|
annotation("Actual STDERR", self.stderr())
|
|
annotation("STDOUT", self.stdout())
|
|
self.maybe_do_diff(actual_stderr, stderr)
|
|
self.fail_test(1, dump_stdio=False)
|
|
|
|
if not expected_duration is None:
|
|
actual_duration = self.last_build_time_finish - self.last_build_time_start
|
|
if ( actual_duration > expected_duration ):
|
|
print "Test run lasted %f seconds while it was expected to " \
|
|
"finish in under %f seconds." % (actual_duration,
|
|
expected_duration)
|
|
self.fail_test(1, dump_stdio=False)
|
|
|
|
self.tree = tree.build_tree(self.workdir)
|
|
self.difference = tree.trees_difference(self.previous_tree, self.tree)
|
|
self.difference.ignore_directories()
|
|
self.unexpected_difference = copy.deepcopy(self.difference)
|
|
|
|
def glob_file(self, name):
|
|
result = None
|
|
if hasattr(self,'difference'):
|
|
for f in self.difference.added_files+self.difference.modified_files+self.difference.touched_files:
|
|
if fnmatch.fnmatch(f,name):
|
|
result = self.native_file_name(f)
|
|
break
|
|
if not result:
|
|
result = glob.glob(self.native_file_name(name))
|
|
if result:
|
|
result = result[0]
|
|
return result
|
|
|
|
def read(self, name, binary=False):
|
|
try:
|
|
if self.toolset:
|
|
name = string.replace(name, "$toolset", self.toolset+"*")
|
|
name = self.glob_file(name)
|
|
openMode = "r"
|
|
if ( binary ):
|
|
openMode += "b"
|
|
else:
|
|
openMode += "U"
|
|
return open(name, openMode).read()
|
|
except:
|
|
annotation("failure", "Could not open '%s'" % name)
|
|
self.fail_test(1)
|
|
return ''
|
|
|
|
def read_and_strip(self, name):
|
|
lines = open(self.glob_file(name), "rb").readlines()
|
|
result = string.join(map(string.rstrip, lines), "\n")
|
|
if lines and lines[-1][-1] == '\n':
|
|
return result + '\n'
|
|
else:
|
|
return result
|
|
|
|
def fail_test(self, condition, dump_stdio=True, *args):
|
|
if not condition:
|
|
return
|
|
|
|
if hasattr(self, 'difference'):
|
|
f = StringIO.StringIO()
|
|
self.difference.pprint(f)
|
|
annotation("changes caused by the last build command", f.getvalue())
|
|
|
|
if dump_stdio:
|
|
self.dump_stdio()
|
|
|
|
if '--preserve' in sys.argv:
|
|
print
|
|
print "*** Copying the state of working dir into 'failed_test' ***"
|
|
print
|
|
path = os.path.join(self.original_workdir, "failed_test")
|
|
if os.path.isdir(path):
|
|
shutil.rmtree(path, ignore_errors=False)
|
|
elif os.path.exists(path):
|
|
raise "Path " + path + " already exists and is not a directory";
|
|
shutil.copytree(self.workdir, path)
|
|
|
|
at = TestCmd.caller(traceback.extract_stack(), 0)
|
|
annotation("stacktrace", at)
|
|
sys.exit(1)
|
|
|
|
# A number of methods below check expectations with actual difference
|
|
# between directory trees before and after a build. All the 'expect*'
|
|
# methods require exact names to be passed. All the 'ignore*' methods allow
|
|
# wildcards.
|
|
|
|
# All names can be lists, which are taken to be directory components.
|
|
def expect_addition(self, names):
|
|
for name in self.adjust_names(names):
|
|
try:
|
|
glob_remove(self.unexpected_difference.added_files,name)
|
|
except:
|
|
print "File %s not added as expected" % name
|
|
self.fail_test(1)
|
|
|
|
def ignore_addition(self, wildcard):
|
|
self.ignore_elements(self.unexpected_difference.added_files, wildcard)
|
|
|
|
def expect_removal(self, names):
|
|
for name in self.adjust_names(names):
|
|
try:
|
|
glob_remove(self.unexpected_difference.removed_files,name)
|
|
except:
|
|
print "File %s not removed as expected" % name
|
|
self.fail_test(1)
|
|
|
|
def ignore_removal(self, wildcard):
|
|
self.ignore_elements(self.unexpected_difference.removed_files, wildcard)
|
|
|
|
def expect_modification(self, names):
|
|
for name in self.adjust_names(names):
|
|
try:
|
|
glob_remove(self.unexpected_difference.modified_files,name)
|
|
except:
|
|
print "File %s not modified as expected" % name
|
|
self.fail_test(1)
|
|
|
|
def ignore_modification(self, wildcard):
|
|
self.ignore_elements(self.unexpected_difference.modified_files, wildcard)
|
|
|
|
def expect_touch(self, names):
|
|
d = self.unexpected_difference
|
|
for name in self.adjust_names(names):
|
|
# We need to check both touched and modified files. The reason is
|
|
# that:
|
|
# (1) Windows binaries such as obj, exe or dll files have slight
|
|
# differences even with identical inputs due to Windows PE
|
|
# format headers containing an internal timestamp.
|
|
# (2) Intel's compiler for Linux has the same behaviour.
|
|
filesets = [d.modified_files, d.touched_files]
|
|
|
|
while filesets:
|
|
try:
|
|
glob_remove(filesets[-1],name)
|
|
break
|
|
except ValueError:
|
|
filesets.pop()
|
|
|
|
if not filesets:
|
|
annotation("failure", "File %s not touched as expected" % name)
|
|
self.fail_test(1)
|
|
|
|
def ignore_touch(self, wildcard):
|
|
self.ignore_elements(self.unexpected_difference.touched_files, wildcard)
|
|
|
|
def ignore(self, wildcard):
|
|
self.ignore_elements(self.unexpected_difference.added_files, wildcard)
|
|
self.ignore_elements(self.unexpected_difference.removed_files, wildcard)
|
|
self.ignore_elements(self.unexpected_difference.modified_files, wildcard)
|
|
self.ignore_elements(self.unexpected_difference.touched_files, wildcard)
|
|
|
|
def expect_nothing(self, names):
|
|
for name in self.adjust_names(names):
|
|
if name in self.difference.added_files:
|
|
annotation("failure",
|
|
"File %s added, but no action was expected" % name)
|
|
self.fail_test(1)
|
|
if name in self.difference.removed_files:
|
|
annotation("failure",
|
|
"File %s removed, but no action was expected" % name)
|
|
self.fail_test(1)
|
|
pass
|
|
if name in self.difference.modified_files:
|
|
annotation("failure",
|
|
"File %s modified, but no action was expected" % name)
|
|
self.fail_test(1)
|
|
if name in self.difference.touched_files:
|
|
annotation("failure",
|
|
"File %s touched, but no action was expected" % name)
|
|
self.fail_test(1)
|
|
|
|
def expect_nothing_more(self):
|
|
# Not totally sure about this change, but I don't see a good
|
|
# alternative.
|
|
if windows:
|
|
self.ignore('*.ilk') # MSVC incremental linking files.
|
|
self.ignore('*.pdb') # MSVC program database files.
|
|
self.ignore('*.rsp') # Response files.
|
|
self.ignore('*.tds') # Borland debug symbols.
|
|
self.ignore('*.manifest') # MSVC DLL manifests.
|
|
|
|
# Debug builds of bjam built with gcc produce this profiling data.
|
|
self.ignore('gmon.out')
|
|
self.ignore('*/gmon.out')
|
|
|
|
if not self.unexpected_difference.empty():
|
|
print 'FAILED'
|
|
print '------- The following changes were unexpected -------'
|
|
self.unexpected_difference.pprint()
|
|
self.fail_test(1)
|
|
|
|
def __expect_line(self, content, expected, expected_to_exist):
|
|
expected = expected.strip()
|
|
lines = content.splitlines()
|
|
found = False
|
|
for line in lines:
|
|
line = line.strip()
|
|
if fnmatch.fnmatch(line, expected):
|
|
found = True
|
|
break
|
|
|
|
if expected_to_exist and not found:
|
|
annotation( "failure",
|
|
"Did not find expected line:\n%s\nin output:\n%s" %
|
|
(expected, content))
|
|
self.fail_test(1)
|
|
if not expected_to_exist and found:
|
|
annotation( "failure",
|
|
"Found an unexpected line:\n%s\nin output:\n%s" %
|
|
(expected, content))
|
|
self.fail_test(1)
|
|
|
|
def expect_output_line(self, line, expected_to_exist=True):
|
|
self.__expect_line(self.stdout(), line, expected_to_exist)
|
|
|
|
def expect_content_line(self, name, line, expected_to_exist=True):
|
|
content = self.__read_file(name)
|
|
self.__expect_line(content, line, expected_to_exist)
|
|
|
|
def __read_file(self, name, exact=False):
|
|
name = self.adjust_names(name)[0]
|
|
result = ""
|
|
try:
|
|
if exact:
|
|
result = self.read(name)
|
|
else:
|
|
result = string.replace(self.read_and_strip(name), "\\", "/")
|
|
except (IOError, IndexError):
|
|
print "Note: could not open file", name
|
|
self.fail_test(1)
|
|
return result
|
|
|
|
def expect_content(self, name, content, exact=False):
|
|
actual = self.__read_file(name, exact)
|
|
content = string.replace(content, "$toolset", self.toolset+"*")
|
|
|
|
matched = False
|
|
if exact:
|
|
matched = fnmatch.fnmatch(actual,content)
|
|
else:
|
|
def sorted_(x):
|
|
x.sort()
|
|
return x
|
|
actual_ = map(lambda x: sorted_(x.split()),actual.splitlines())
|
|
content_ = map(lambda x: sorted_(x.split()),content.splitlines())
|
|
if len(actual_) == len(content_):
|
|
matched = map(
|
|
lambda x,y: map(lambda n,p: fnmatch.fnmatch(n,p),x,y),
|
|
actual_, content_ )
|
|
matched = reduce(
|
|
lambda x,y: x and reduce(
|
|
lambda a,b: a and b,
|
|
y ),
|
|
matched )
|
|
|
|
if not matched:
|
|
print "Expected:\n"
|
|
print content
|
|
print "Got:\n"
|
|
print actual
|
|
self.fail_test(1)
|
|
|
|
def maybe_do_diff(self, actual, expected):
|
|
if os.environ.has_key("DO_DIFF") and os.environ["DO_DIFF"] != '':
|
|
e = tempfile.mktemp("expected")
|
|
a = tempfile.mktemp("actual")
|
|
open(e, "w").write(expected)
|
|
open(a, "w").write(actual)
|
|
print "DIFFERENCE"
|
|
if os.system("diff -u " + e + " " + a):
|
|
print "Unable to compute difference: diff -u %s %s" % (e,a)
|
|
os.unlink(e)
|
|
os.unlink(a)
|
|
else:
|
|
print "Set environmental variable 'DO_DIFF' to examine difference."
|
|
|
|
# Helpers.
|
|
def mul(self, *arguments):
|
|
if len(arguments) == 0:
|
|
return None
|
|
|
|
here = arguments[0]
|
|
if type(here) == type(''):
|
|
here = [here]
|
|
|
|
if len(arguments) > 1:
|
|
there = apply(self.mul, arguments[1:])
|
|
result = []
|
|
for i in here:
|
|
for j in there:
|
|
result.append(i + j)
|
|
return result
|
|
|
|
return here
|
|
|
|
# Internal methods.
|
|
def ignore_elements(self, list, wildcard):
|
|
"""Removes in-place, element of 'list' that match the given wildcard.
|
|
"""
|
|
list[:] = filter(lambda x, w=wildcard: not fnmatch.fnmatch(x, w), list)
|
|
|
|
def adjust_lib_name(self, name):
|
|
global lib_prefix
|
|
result = name
|
|
|
|
pos = string.rfind(name, ".")
|
|
if pos != -1:
|
|
suffix = name[pos:]
|
|
if suffix == ".lib":
|
|
(head, tail) = os.path.split(name)
|
|
if lib_prefix:
|
|
tail = "lib" + tail
|
|
result = os.path.join(head, tail)
|
|
elif suffix == ".dll":
|
|
(head, tail) = os.path.split(name)
|
|
if dll_prefix:
|
|
tail = "lib" + tail
|
|
result = os.path.join(head, tail)
|
|
# If we want to use this name in a Jamfile, we better convert \ to /, as
|
|
# otherwise we'd have to quote \.
|
|
result = string.replace(result, "\\", "/")
|
|
return result
|
|
|
|
def adjust_suffix(self, name):
|
|
if not self.translate_suffixes:
|
|
return name
|
|
|
|
pos = string.rfind(name, ".")
|
|
if pos != -1:
|
|
suffix = name[pos:]
|
|
name = name[:pos]
|
|
|
|
if suffixes.has_key(suffix):
|
|
suffix = suffixes[suffix]
|
|
else:
|
|
suffix = ''
|
|
|
|
return name + suffix
|
|
|
|
# Acceps either a string or a list of strings and returns a list of strings.
|
|
# Adjusts suffixes on all names.
|
|
def adjust_names(self, names):
|
|
if type(names) == types.StringType:
|
|
names = [names]
|
|
r = map(self.adjust_lib_name, names)
|
|
r = map(self.adjust_suffix, r)
|
|
r = map(lambda x, t=self.toolset: string.replace(x, "$toolset", t+"*"), r)
|
|
return r
|
|
|
|
def native_file_name(self, name):
|
|
name = self.adjust_names(name)[0]
|
|
elements = string.split(name, "/")
|
|
return os.path.normpath(apply(os.path.join, [self.workdir]+elements))
|
|
|
|
# Wait while time is no longer equal to the time last "run_build_system"
|
|
# call finished. Used to avoid subsequent builds treating existing files as
|
|
# 'current'.
|
|
def wait_for_time_change_since_last_build(self):
|
|
while 1:
|
|
# In fact, I'm not sure why "+ 2" as opposed to "+ 1" is needed but
|
|
# empirically, "+ 1" sometimes causes 'touch' and other functions
|
|
# not to bump the file time enough for a rebuild to happen.
|
|
if math.floor(time.time()) < math.floor(self.last_build_time_finish) + 2:
|
|
time.sleep(0.1)
|
|
else:
|
|
break
|
|
|
|
|
|
class List:
|
|
|
|
def __init__(self, s=""):
|
|
elements = []
|
|
if isinstance(s, type("")):
|
|
# Have to handle espaced spaces correctly.
|
|
s = string.replace(s, "\ ", '\001')
|
|
elements = string.split(s)
|
|
else:
|
|
elements = s;
|
|
|
|
self.l = []
|
|
for e in elements:
|
|
self.l.append(string.replace(e, '\001', ' '))
|
|
|
|
def __len__(self):
|
|
return len(self.l)
|
|
|
|
def __getitem__(self, key):
|
|
return self.l[key]
|
|
|
|
def __setitem__(self, key, value):
|
|
self.l[key] = value
|
|
|
|
def __delitem__(self, key):
|
|
del self.l[key]
|
|
|
|
def __str__(self):
|
|
return str(self.l)
|
|
|
|
def __repr__(self):
|
|
return ( self.__module__ + '.List('
|
|
+ repr(string.join(self.l, ' '))
|
|
+ ')')
|
|
|
|
def __mul__(self, other):
|
|
result = List()
|
|
if not isinstance(other, List):
|
|
other = List(other)
|
|
for f in self:
|
|
for s in other:
|
|
result.l.append(f + s)
|
|
return result
|
|
|
|
def __rmul__(self, other):
|
|
if not isinstance(other, List):
|
|
other = List(other)
|
|
return List.__mul__(other, self)
|
|
|
|
def __add__(self, other):
|
|
result = List()
|
|
result.l = self.l[:] + other.l[:]
|
|
return result
|
|
|
|
# Quickie tests. Should use doctest instead.
|
|
if __name__ == '__main__':
|
|
assert str(List("foo bar") * "/baz") == "['foo/baz', 'bar/baz']"
|
|
assert repr("foo/" * List("bar baz")) == "__main__.List('foo/bar foo/baz')"
|
|
print 'tests passed'
|