xlnt/third-party/cxxtest/build_tools/SCons/cxxtest.py

401 lines
17 KiB
Python
Raw Normal View History

# coding=UTF-8
#-------------------------------------------------------------------------
# CxxTest: A lightweight C++ unit testing library.
# Copyright (c) 2008 Sandia Corporation.
# This software is distributed under the LGPL License v3
# For more information, see the COPYING file in the top CxxTest directory.
# Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
# the U.S. Government retains certain rights in this software.
#-------------------------------------------------------------------------
#
# == Preamble ==
# Authors of this script are in the Authors file in the same directory as this
# script.
#
# Maintainer: Gašper Ažman <gasper.azman@gmail.com>
#
# This file is maintained as a part of the CxxTest test suite.
#
# == About ==
#
# This builder correctly tracks dependencies and supports just about every
# configuration option for CxxTest that I can think of. It automatically
# defines a target "check" (configurable), so all tests can be run with a
# % scons check
# This will first compile and then run the tests.
#
# The default configuration assumes that cxxtest is located at the base source
# directory (where SConstruct is), that the cxxtestgen is under
# cxxtest/bin/cxxtestgen and headers are in cxxtest/cxxtest/. The
# header include path is automatically added to CPPPATH. It, however, can also
# recognise that cxxtest is installed system-wide (based on redhat's RPM).
#
# For a list of environment variables and their defaults, see the generate()
# function.
#
# This should be in a file called cxxtest.py somewhere in the scons toolpath.
# (default: #/site_scons/site_tools/)
#
# == Usage: ==
#
# For configuration options, check the comment of the generate() function.
#
# This builder has a variety of different possible usages, so bear with me.
#
# env.CxxTest('target')
# The simplest of them all, it models the Program call. This sees if target.t.h
# is around and passes it through the cxxtestgen and compiles it. Might only
# work on unix though, because target can't have a suffix right now.
#
# env.CxxTest(['target.t.h'])
# This compiles target.t.h as in the previous example, but now sees that it is a
# source file. It need not have the same suffix as the env['CXXTEST_SUFFIX']
# variable dictates. The only file provided is taken as the test source file.
#
# env.CxxTest(['test1.t.h','test1_lib.cpp','test1_lib2.cpp','test2.t.h',...])
# You may also specify multiple source files. In this case, the 1st file that
# ends with CXXTEST_SUFFIX (default: .t.h) will be taken as the default test
# file. All others will be run with the --part switch and linked in. All files
# *not* having the right suffix will be passed to the Program call verbatim.
#
# In the last two cases, you may also specify the desired name of the test as
# the 1st argument to the function. This will result in the end executable
# called that. Normal Program builder rules apply.
#
from SCons.Script import *
from SCons.Builder import Builder
from SCons.Util import PrependPath, unique, uniquer
import os
# A warning class to notify users of problems
class ToolCxxTestWarning(SCons.Warnings.Warning):
pass
SCons.Warnings.enableWarningClass(ToolCxxTestWarning)
def accumulateEnvVar(dicts, name, default = []):
"""
Accumulates the values under key 'name' from the list of dictionaries dict.
The default value is appended to the end list if 'name' does not exist in
the dict.
"""
final = []
for d in dicts:
final += Split(d.get(name, default))
return final
def multiget(dictlist, key, default = None):
"""
Takes a list of dictionaries as its 1st argument. Checks if the key exists
in each one and returns the 1st one it finds. If the key is found in no
dictionaries, the default is returned.
"""
for dict in dictlist:
if dict.has_key(key):
return dict[key]
else:
return default
def envget(env, key, default=None):
"""Look in the env, then in os.environ. Otherwise same as multiget."""
return multiget([env, os.environ], key, default)
def prepend_ld_library_path(env, overrides, **kwargs):
"""Prepend LD_LIBRARY_PATH with LIBPATH to run successfully programs that
were linked against local shared libraries."""
# make it unique but preserve order ...
libpath = uniquer(Split(kwargs.get('CXXTEST_LIBPATH', [])) +
Split(env.get( 'CXXTEST_LIBPATH', [])))
if len(libpath) > 0:
libpath = env.arg2nodes(libpath, env.fs.Dir)
platform = env.get('PLATFORM','')
if platform == 'win32':
var = 'PATH'
else:
var = 'LD_LIBRARY_PATH'
eenv = overrides.get('ENV', env['ENV'].copy())
canonicalize = lambda p : p.abspath
eenv[var] = PrependPath(eenv.get(var,''), libpath, os.pathsep, 1, canonicalize)
overrides['ENV'] = eenv
return overrides
def UnitTest(env, target, source = [], **kwargs):
"""
Prepares the Program call arguments, calls Program and adds the result to
the check target.
"""
# get the c and cxx flags to process.
ccflags = Split( multiget([kwargs, env, os.environ], 'CCFLAGS' ))
cxxflags = Split( multiget([kwargs, env, os.environ], 'CXXFLAGS'))
# get the removal c and cxx flags
cxxremove = set( Split( multiget([kwargs, env, os.environ],'CXXTEST_CXXFLAGS_REMOVE')))
ccremove = set( Split( multiget([kwargs, env, os.environ],'CXXTEST_CCFLAGS_REMOVE' )))
# remove the required flags
ccflags = [item for item in ccflags if item not in ccremove]
cxxflags = [item for item in cxxflags if item not in cxxremove]
# fill the flags into kwargs
kwargs["CXXFLAGS"] = cxxflags
kwargs["CCFLAGS"] = ccflags
test = env.Program(target, source = source, **kwargs)
testCommand = multiget([kwargs, env, os.environ], 'CXXTEST_COMMAND')
if testCommand:
testCommand = testCommand.replace('%t', test[0].abspath)
else:
testCommand = test[0].abspath
if multiget([kwargs, env, os.environ], 'CXXTEST_SKIP_ERRORS', False):
runner = env.Action(testCommand, exitstatfunc=lambda x:0)
else:
runner = env.Action(testCommand)
overrides = prepend_ld_library_path(env, {}, **kwargs)
cxxtest_target = multiget([kwargs, env], 'CXXTEST_TARGET')
env.Alias(cxxtest_target, test, runner, **overrides)
env.AlwaysBuild(cxxtest_target)
return test
def isValidScriptPath(cxxtestgen):
"""check keyword arg or environment variable locating cxxtestgen script"""
if cxxtestgen and os.path.exists(cxxtestgen):
return True
else:
SCons.Warnings.warn(ToolCxxTestWarning,
"Invalid CXXTEST environment variable specified!")
return False
def defaultCxxTestGenLocation(env):
return os.path.join(
envget(env, 'CXXTEST_CXXTESTGEN_DEFAULT_LOCATION'),
envget(env, 'CXXTEST_CXXTESTGEN_SCRIPT_NAME')
)
def findCxxTestGen(env):
"""locate the cxxtestgen script by checking environment, path and project"""
# check the SCons environment...
# Then, check the OS environment...
cxxtest = envget(env, 'CXXTEST', None)
# check for common passing errors and provide diagnostics.
if isinstance(cxxtest, (list, tuple, dict)):
SCons.Warnings.warn(
ToolCxxTestWarning,
"The CXXTEST variable was specified as a list."
" This is not supported. Please pass a string."
)
if cxxtest:
try:
#try getting the absolute path of the file first.
# Required to expand '#'
cxxtest = env.File(cxxtest).abspath
except TypeError:
try:
#maybe only the directory was specified?
cxxtest = env.File(
os.path.join(cxxtest, defaultCxxTestGenLocation(env)
)).abspath
except TypeError:
pass
# If the user specified the location in the environment,
# make sure it was correct
if isValidScriptPath(cxxtest):
return os.path.realpath(cxxtest)
# No valid environment variable found, so...
# Next, check the path...
# Next, check the project
check_path = os.path.join(
envget(env, 'CXXTEST_INSTALL_DIR'),
envget(env, 'CXXTEST_CXXTESTGEN_DEFAULT_LOCATION'))
cxxtest = (env.WhereIs(envget(env, 'CXXTEST_CXXTESTGEN_SCRIPT_NAME')) or
env.WhereIs(envget(env, 'CXXTEST_CXXTESTGEN_SCRIPT_NAME'),
path=[Dir(check_path).abspath]))
if cxxtest:
return cxxtest
else:
# If we weren't able to locate the cxxtestgen script, complain...
SCons.Warnings.warn(
ToolCxxTestWarning,
"Unable to locate cxxtestgen in environment, path or"
" project!\n"
"Please set the CXXTEST variable to the path of the"
" cxxtestgen script"
)
return None
def findCxxTestHeaders(env):
searchfile = 'TestSuite.h'
cxxtestgen_pathlen = len(defaultCxxTestGenLocation(env))
default_path = Dir(envget(env,'CXXTEST_INSTALL_DIR')).abspath
os_cxxtestgen = os.path.realpath(File(env['CXXTEST']).abspath)
alt_path = os_cxxtestgen[:-cxxtestgen_pathlen]
searchpaths = [default_path, alt_path]
foundpaths = []
for p in searchpaths:
if os.path.exists(os.path.join(p, 'cxxtest', searchfile)):
foundpaths.append(p)
return foundpaths
def generate(env, **kwargs):
"""
Keyword arguments (all can be set via environment variables as well):
CXXTEST - the path to the cxxtestgen script.
Default: searches SCons environment, OS environment,
path and project in that order. Instead of setting this,
you can also set CXXTEST_INSTALL_DIR
CXXTEST_RUNNER - the runner to use. Default: ErrorPrinter
CXXTEST_OPTS - other options to pass to cxxtest. Default: ''
CXXTEST_SUFFIX - the suffix of the test files. Default: '.t.h'
CXXTEST_TARGET - the target to append the tests to. Default: check
CXXTEST_COMMAND - the command that will be executed to run the test,
%t will be replace with the test executable.
Can be used for example for MPI or valgrind tests.
Default: %t
CXXTEST_CXXFLAGS_REMOVE - the flags that cxxtests can't compile with,
or give lots of warnings. Will be stripped.
Default: -pedantic -Weffc++
CXXTEST_CCFLAGS_REMOVE - the same thing as CXXTEST_CXXFLAGS_REMOVE, just for
CCFLAGS. Default: same as CXXFLAGS.
CXXTEST_PYTHON - the path to the python binary.
Default: searches path for python
CXXTEST_SKIP_ERRORS - set to True to continue running the next test if one
test fails. Default: False
CXXTEST_CPPPATH - If you do not want to clutter your global CPPPATH with the
CxxTest header files and other stuff you only need for
your tests, this is the variable to set. Behaves as
CPPPATH does.
CXXTEST_LIBPATH - If your test is linked to shared libraries which are
outside of standard directories. This is used as LIBPATH
when compiling the test program and to modify
LD_LIBRARY_PATH (or PATH on win32) when running the
program.
CXXTEST_INSTALL_DIR - this is where you tell the builder where CxxTest is
installed. The install directory has cxxtest,
python, docs and other subdirectories.
... and all others that Program() accepts, like CPPPATH etc.
"""
print "Loading CxxTest tool..."
#
# Expected behaviour: keyword arguments override environment variables;
# environment variables override default settings.
#
env.SetDefault( CXXTEST_RUNNER = 'ErrorPrinter' )
env.SetDefault( CXXTEST_OPTS = '' )
env.SetDefault( CXXTEST_SUFFIX = '.t.h' )
env.SetDefault( CXXTEST_TARGET = 'check' )
env.SetDefault( CXXTEST_CPPPATH = ['#'] )
env.SetDefault( CXXTEST_PYTHON = env.WhereIs('python') )
env.SetDefault( CXXTEST_SKIP_ERRORS = False )
env.SetDefault( CXXTEST_CXXFLAGS_REMOVE =
['-pedantic','-Weffc++','-pedantic-errors'] )
env.SetDefault( CXXTEST_CCFLAGS_REMOVE =
['-pedantic','-Weffc++','-pedantic-errors'] )
env.SetDefault( CXXTEST_INSTALL_DIR = '#/cxxtest/' )
# this one's not for public use - it documents where the cxxtestgen script
# is located in the CxxTest tree normally.
env.SetDefault( CXXTEST_CXXTESTGEN_DEFAULT_LOCATION = 'bin' )
# the cxxtestgen script name.
env.SetDefault( CXXTEST_CXXTESTGEN_SCRIPT_NAME = 'cxxtestgen' )
#Here's where keyword arguments are applied
apply(env.Replace, (), kwargs)
#If the user specified the path to CXXTEST, make sure it is correct
#otherwise, search for and set the default toolpath.
if (not kwargs.has_key('CXXTEST') or not isValidScriptPath(kwargs['CXXTEST']) ):
env["CXXTEST"] = findCxxTestGen(env)
# find and add the CxxTest headers to the path.
env.AppendUnique( CXXTEST_CPPPATH = findCxxTestHeaders(env) )
cxxtest = env['CXXTEST']
if cxxtest:
#
# Create the Builder (only if we have a valid cxxtestgen!)
#
cxxtest_builder = Builder(
action =
[["$CXXTEST_PYTHON",cxxtest,"--runner=$CXXTEST_RUNNER",
"$CXXTEST_OPTS","$CXXTEST_ROOT_PART","-o","$TARGET","$SOURCE"]],
suffix = ".cpp",
src_suffix = '$CXXTEST_SUFFIX'
)
else:
cxxtest_builder = (lambda *a: sys.stderr.write("ERROR: CXXTESTGEN NOT FOUND!"))
def CxxTest(env, target, source = None, **kwargs):
"""Usage:
The function is modelled to be called as the Program() call is:
env.CxxTest('target_name') will build the test from the source
target_name + env['CXXTEST_SUFFIX'],
env.CxxTest('target_name', source = 'test_src.t.h') will build the test
from test_src.t.h source,
env.CxxTest('target_name, source = ['test_src.t.h', other_srcs]
builds the test from source[0] and links in other files mentioned in
sources,
You may also add additional arguments to the function. In that case, they
will be passed to the actual Program builder call unmodified. Convenient
for passing different CPPPATHs and the sort. This function also appends
CXXTEST_CPPPATH to CPPPATH. It does not clutter the environment's CPPPATH.
"""
if (source == None):
suffix = multiget([kwargs, env, os.environ], 'CXXTEST_SUFFIX', "")
source = [t + suffix for t in target]
sources = Flatten(Split(source))
headers = []
linkins = []
for l in sources:
# check whether this is a file object or a string path
try:
s = l.abspath
except AttributeError:
s = l
if s.endswith(multiget([kwargs, env, os.environ], 'CXXTEST_SUFFIX', None)):
headers.append(l)
else:
linkins.append(l)
deps = []
if len(headers) == 0:
if len(linkins) != 0:
# the 1st source specified is the test
deps.append(env.CxxTestCpp(linkins.pop(0), **kwargs))
else:
deps.append(env.CxxTestCpp(headers.pop(0), **kwargs))
deps.extend(
[env.CxxTestCpp(header, CXXTEST_RUNNER = 'none',
CXXTEST_ROOT_PART = '--part', **kwargs)
for header in headers]
)
deps.extend(linkins)
kwargs['CPPPATH'] = unique(
Split(kwargs.get('CPPPATH', [])) +
Split(env.get( 'CPPPATH', [])) +
Split(kwargs.get('CXXTEST_CPPPATH', [])) +
Split(env.get( 'CXXTEST_CPPPATH', []))
)
kwargs['LIBPATH'] = unique(
Split(kwargs.get('LIBPATH', [])) +
Split(env.get( 'LIBPATH', [])) +
Split(kwargs.get('CXXTEST_LIBPATH', [])) +
Split(env.get( 'CXXTEST_LIBPATH', []))
)
return UnitTest(env, target, source = deps, **kwargs)
env.Append( BUILDERS = { "CxxTest" : CxxTest, "CxxTestCpp" : cxxtest_builder } )
def exists(env):
return os.path.exists(env['CXXTEST'])