Description

The builder for running unit tests created with the cxxtest framework.

I'm pleased to say that this builder has now been included in the new and improved version of CxxTest that is currently in svn. So, yeah, we got kind of official. Yepee. The script here is not updated every time I fix a bug, mainly because I forget. So, the most up-to-date version can always be found here: http://cxxtest.tigris.org/source/browse/cxxtest/branches/v3/build_tools/SCons/.

So, please, go get it while it's hot :).

Installation

Get the code here: cxxtest.py.

Copy this code into a cxxtest.py somewhere in your toolpath (usually to #/site_scons/site_tools/cxxtest.py).

Usage

In short, it can be used in the same way the Program() builder can be used. You can even specify other flags on call etc.

Example

   1 # vanilla include (the 'cxxtest' would be 'CxxTest' if the file was called CxxTest.py!)
   2 env = Environment(tools = ['default', 'cxxtest'])
   3 # if you have to specify some options: (others can be found in the comment block of the generate function in the code.
   4 env = Environment(tools = ['default', ('cxxtest', {'CXXTEST_DIR':'#'})])
   5 # or you can use this syntax:
   6 env = Environment(tools = ['default','cxxtest'], CXXTEST='#/path/to/cxxtestgen.py', CXXTEST_DIR='')
   7 
   8 env.CxxTest('test_quaternion', source = 'Quaternion.t.h')      # the quaternion test
   9 env.CxxTest('test_utility', ['utility.t.h', '../utility.cpp']) # utility functions test
  10 env.CxxTest('test_timer', ['Timer.t.h'])                       # timer class test

Authors

The 1st version was written by Gašper Ažman, and then completely rewritten again to produce the code as it largely is today.

Diego Nieto Cid kindly provided the windows interpreter finding fixes. Many thanks!

Some fixes and additions (and code cleanup) were initiated and implemented by J. Darby Mitchell. It is because of him that I started working on getting this builder to try to work out of the box.

The current maintainer is Gašper Ažman (gasper dot azman at gmail dot com).

The code

   1 # coding=UTF-8
   2 #
   3 # == Preamble ==
   4 #
   5 # CxxTest builder by Gasper Azman
   6 #
   7 # Contributors:
   8 # J. Darby Mitchell (2008-08-22)
   9 #
  10 # please send bugreports/praise/comments/criticism to
  11 # gasper.azman at gmail.com or the cxxtest mailing list.
  12 #
  13 # This file is maintained as a part of the CxxTest test suite.
  14 #
  15 # == About ==
  16 #
  17 # This builder correctly tracks dependencies and supports just about every
  18 # configuration option for CxxTest that I can think of. It automatically
  19 # defines a target "check" (configurable), so all tests can be run with a
  20 #  % scons check
  21 # This will first compile and then run the tests.
  22 #
  23 # The default configuration assumes that cxxtest is installed in the base
  24 # source directory (where SConstruct is), that the cxxtestgen.py is under
  25 # cxxtest/cxxtestgen.py and headers are in cxxtest/cxxtest/. The header
  26 # include path is automatically added to CPPPATH. It, however, can also
  27 # recognise that cxxtest is installed system-wide (based on redhat's RPM).
  28 #
  29 # For a list of environment variables and their defaults, see the generate()
  30 # function.
  31 #
  32 # This should be in a file called cxxtest.py somewhere in the scons toolpath.
  33 # (default: #/site_scons/site_tools/)
  34 #
  35 # == Usage: ==
  36 #
  37 # This builder has a variety of different possible usages, so bear with me.
  38 #
  39 # env.CxxTest('target')
  40 # The simplest of them all, it models the Program call. This sees if target.t.h
  41 # is around and passes it through the cxxtestgen.py and compiles it. Might only
  42 # work on unix though, because target can't have a suffix right now.
  43 #
  44 # env.CxxTest(['target.t.h'])
  45 # This compiles target.t.h as in the previous example, but now sees that it is a
  46 # source file. It need not have the same suffix as the env['CXXTEST_SUFFIX']
  47 # variable dictates. The only file provided is taken as the test source file.
  48 #
  49 # env.CxxTest(['test1.t.h','test1_lib.cpp','test1_lib2.cpp','test2.t.h',...])
  50 # You may also specify multiple source files. In this case, the 1st file that
  51 # ends with CXXTEST_SUFFIX (default: .t.h) will be taken as the default test
  52 # file. All others will be run with the --part switch and linked in. All files
  53 # *not* having the right suffix will be passed to the Program call verbatim.
  54 #
  55 # In the last two cases, you may also specify the desired name of the test as
  56 # the 1st argument to the function. This will result in the end executable
  57 # called that. Normal Program builder rules apply.
  58 #
  59 # == Changelog ==
  60 # 2008-09-28: Added the option that #, as understood in pathnames in the rest of
  61 #    SCons, is now also understood here. Minor bugfixes all around.
  62 # 2008-08-30: CXXTEST_RUNNER became CXXTEST_PYTHON, and CXXTEST_PRINTER became
  63 #    CXXTEST_RUNNER because it now governs the argument of the --runner
  64 #    commandline option.
  65 # 2008-08-29: Gašper Ažman added the --root and --part functionality.
  66 #    Documentation above.
  67 # 2008-08-28: Gašper Ažman added CXXTEST_SKIP_ERRORS that makes all tests run no
  68 #    matter whether they fail or succeed. Fixed the issue where CPPPATH would
  69 #    vanish. Fixed bug #40
  70 # 2008-08-25: Gasper Azman and Darby Mitchell (MIT) refactored the tool to
  71 #    set defaults that don't override previously set environment variables, and
  72 #    automatically search for the cxxtestgen Python script in the path and
  73 #    project if it isn't specified by the user in the environment or kwargs.
  74 # 2008-08-23: Gasper Azman removed Perl support, since SCons is based on Python
  75 #    and CxxTest has plans to drop the cxxtestgen Perl script in the near future.
  76 # 2008-03-29:
  77 #    Fixed a bug introduced in the last version, where the script would fail if
  78 #    CXXFLAGS was never set.
  79 # 2008-03-23:
  80 #    Added CXXTEST_CXXFLAGS_REMOVE
  81 #    Bugfix: if only a single test was specified, scons check would do nothing
  82 #    Bugfix: test would not compile if '#' was not a part of path. Now added
  83 #    automatically.
  84 # 2008-06-22:
  85 #    Bugfix: on windows, the script would not work because windows does not
  86 #    support running scripts without an explicit interpreter invocation
  87 #    (./script.py).
  88 #    Patch kindly provided by Diego Nieto Cid (dnietoc at gmail dot com)
  89 # 2008-06-25:
  90 #    Bugfix: Diego also provided a patch to his added functionality, so that
  91 #    now not all paths are imported, but just the path required to run the
  92 #    interpreter.
  93 
  94 from SCons.Script import *
  95 from SCons.Builder import Builder
  96 from os import path
  97 
  98 # A warning class to notify users of problems
  99 class ToolCxxTestWarning(SCons.Warnings.Warning):
 100     pass
 101 
 102 SCons.Warnings.enableWarningClass(ToolCxxTestWarning)
 103 
 104 def UnitTest(env, target, source = [], **kwargs):
 105     """
 106     Prepares the Program call arguments, calls Program and adds the result to
 107     the check target.
 108     """
 109 
 110     cxxflags = ""
 111     if (type(env["CXXFLAGS"]) == "str"):
 112         cxxflags = env["CXXFLAGS"]
 113     cxxflags = kwargs.get("CXXFLAGS", cxxflags)
 114     for item in env['CXXTEST_CXXFLAGS_REMOVE']:
 115         cxxflags = cxxflags.replace(item, "")
 116     kwargs["CXXFLAGS"] = cxxflags;
 117     test = env.Program(target, source = source, **kwargs)
 118     if (env['CXXTEST_SKIP_ERRORS']):
 119         runner = env.Action(test[0].abspath, exitstatfunc=lambda x:0)
 120     else:
 121         runner = env.Action(test[0].abspath)
 122     env.Alias(env['CXXTEST_TARGET'], test, runner)
 123     env.AlwaysBuild(env['CXXTEST_TARGET'])
 124     return test
 125 
 126 def multiget(dictlist, key, default = None):
 127     """
 128     Takes a list of dictionaries as its 1st argument. Checks if the key exists
 129     in each one and returns the 1st one it finds. If the key is found in no
 130     dictionaries, the default is returned.
 131     """
 132 
 133     for dict in dictlist:
 134         if dict.has_key(key):
 135             return dict[key]
 136     else:
 137         return default
 138 
 139 def isValidScriptPath(cxxtestgen):
 140     """check keyword arg or environment variable locating cxxtestgen.py script"""
 141 
 142     if(cxxtestgen and cxxtestgen.endswith('.py') and path.exists(cxxtestgen) ):
 143         return True
 144     else:
 145         SCons.Warnings.warn(ToolCxxTestWarning,
 146                             "Invalid CXXTEST environment variable specified!")
 147         return False
 148 
 149 
 150 def findCxxTestGen(env):
 151     """locate the cxxtestgen script by checking environment, path and project"""
 152 
 153     # check the SCons environment...
 154     # Then, check the OS environment...
 155     cxxtest = (env.get('CXXTEST', None) or
 156                os.environ.get('CXXTEST', None)
 157               )
 158 
 159     if cxxtest:
 160         try:
 161             #try getting the absolute path of the file first. Required to expand '#'
 162             cxxtest = env.File(cxxtest).abspath;
 163         except TypeError:
 164             try:
 165                 #maybe only the directory was specified?
 166                 cxxtest = env.File(cxxtest+'/cxxtestgen.py').abspath;
 167             except TypeError:
 168                 pass
 169 
 170         # If the user specified the location in the environment, make sure it was correct
 171         if isValidScriptPath(cxxtest):
 172            return cxxtest
 173 
 174     # No valid environment variable found, so...
 175     # Next, check the path...
 176     # Finally, check the project
 177     cxxtest = (env.WhereIs('cxxtestgen.py') or
 178                env.WhereIs('cxxtestgen.py', path=Dir(path.join('#', 'cxxtest') ).abspath)
 179               )
 180 
 181     if cxxtest:
 182         return cxxtest
 183     else:
 184         # If we weren't able to locate the cxxtestgen.py script, complain...
 185         SCons.Warnings.warn(
 186                 ToolCxxTestWarning,
 187                 "Unable to locate cxxtestgen in environment, path or project!")
 188         return None
 189 
 190 
 191 def generate(env, **kwargs):
 192     """
 193     Accepted keyword arguments:
 194     CXXTEST         - the path to the cxxtestgen.py
 195                         Default: searches SCons environment, OS environment,
 196                         path and project in that order.
 197     CXXTEST_RUNNER  - the runner to use.  Default: ErrorPrinter
 198     CXXTEST_OPTS    - other options to pass to cxxtest.  Default: ''
 199     CXXTEST_SUFFIX  - the suffix of the test files.  Default: '.t.h'
 200     CXXTEST_TARGET  - the target to append the tests to.  Default: check
 201     CXXTEST_CXXFLAGS_REMOVE - the flags that cxxtests can't compile with,
 202                               or give lots of warnings. Will be stripped.
 203                               Default: -pedantic -Weffc++
 204     CXXTEST_PYTHON  - the path to the python binary.
 205                         Default: searches path for python
 206     CXXTEST_SKIP_ERRORS - set to True to continue running the next test if one
 207                           test fails. Default: False
 208     ... and all others that Program() accepts, like CPPPATH etc.
 209     """
 210 
 211     print "Loading CxxTest tool..."
 212 
 213     #If the user specified the path to CXXTEST, make sure it is correct
 214     #otherwise, search for and set the default toolpath.
 215     if (not kwargs.has_key('CXXTEST') or not isValidScriptPath(kwargs['CXXTEST']) ):
 216         env["CXXTEST"] = findCxxTestGen(env)
 217 
 218     #
 219     # Expected behavior: keyword arguments override environment variables;
 220     # environment variables override default settings.
 221     #
 222     env.SetDefault( CXXTEST_RUNNER  = 'ErrorPrinter'        )
 223     env.SetDefault( CXXTEST_OPTS    = ''                    )
 224     env.SetDefault( CXXTEST_SUFFIX  = '.t.h'                )
 225     env.SetDefault( CXXTEST_TARGET  = 'check'               )
 226     env.SetDefault( CXXTEST_CPPPATH = ['#']                 )
 227     env.SetDefault( CXXTEST_PYTHON  = env.WhereIs('python') )
 228     env.SetDefault( CXXTEST_CXXFLAGS_REMOVE = ['-pedantic','-Weffc++'] )
 229     env.SetDefault( CXXTEST_SKIP_ERRORS = False             )
 230 
 231     #Here's where keyword arguments are applied
 232     apply(env.Replace, (), kwargs)
 233 
 234     cxxtest = env['CXXTEST']
 235     if(cxxtest):
 236         # Check to see if there is a 'cxxtest' subdirectory in the location where
 237         # the script was found.  If so, assume that is the header directory, and
 238         # therefore the script directory should be included in the CPPPATH
 239         if(path.exists(path.join(path.dirname(cxxtest), 'cxxtest') ) ):
 240            # for some reason, setting PATH here doesn't work for me (Gašper)
 241            env.AppendUnique(CXXTEST_CPPPATH = [path.dirname(cxxtest)] )
 242 
 243         #
 244         # Create the Builder (only if we have a valid cxxtestgen!)
 245         #
 246         cxxtest_builder = Builder(
 247             action =
 248             [["$CXXTEST_PYTHON",cxxtest,"--runner=$CXXTEST_RUNNER","$CXXTEST_OPTS","-o","$TARGET","$SOURCE"]],
 249             suffix = ".cpp",
 250             src_suffix = '$CXXTEST_SUFFIX'
 251             )
 252 
 253     def CxxTest(env, target, source = [], **kwargs):
 254         """Usage:
 255         The function is modelled to be called as the Program() call is:
 256         env.CxxTest('target_name') will build the test from the source
 257             target_name + env['CXXTEST_SUFFIX'],
 258         env.CxxTest('target_name', source = 'test_src.t.h') will build the test
 259             from test_src.t.h source,
 260         env.CxxTest('target_name, source = ['test_src.t.h', other_srcs]
 261             builds the test from source[0] and links in other files mentioned in
 262             sources,
 263         You may also add additional arguments to the function. In that case, they
 264         will be passed to the actual Program builder call unmodified. Convenient
 265         for passing different CPPPATHs and the sort. This function also appends
 266         CXXTEST_CPPPATH to CPPPATH. It does not clutter the environment's CPPPATH.
 267         """
 268         if (source == []):
 269             source = Split(target + multiget([kwargs, env], 'CXXTEST_SUFFIX', None))
 270         sources = Flatten(Split(source))
 271         headers = []
 272         linkins = []
 273         for s in sources:
 274             if s.endswith(multiget([kwargs, env], 'CXXTEST_SUFFIX', None)):
 275                 headers.append(s)
 276             else:
 277                 linkins.append(s)
 278 
 279         deps = []
 280         if len(headers) == 0:
 281             # old functionality - the 1st source specified is the test
 282             deps.append(env.CxxTestCpp(linkins.pop(0), **kwargs))
 283         else:
 284             deps.append(env.CxxTestCpp(headers.pop(0), **kwargs))
 285             deps.extend(
 286                 [env.CxxTestCpp(header, CXXTEST_RUNNER = 'none', **kwargs)
 287                     for header in headers]
 288                 )
 289         deps.extend(linkins)
 290         kwargs['CPPPATH'] = list(set(
 291             Split(kwargs.get('CPPPATH', [])) +
 292             Split(env.get(   'CPPPATH', [])) +
 293             Split(kwargs.get('CXXTEST_CPPPATH', [])) +
 294             Split(env.get(   'CXXTEST_CPPPATH', []))
 295             ))
 296 
 297         return UnitTest(env, target, source = deps, **kwargs)
 298 
 299     env.Append( BUILDERS = { "CxxTest" : CxxTest, "CxxTestCpp" : cxxtest_builder } )
 300 
 301 def exists(env):
 302     return path.exists(env['CXXTEST'])

CxxTestBuilder (last edited 2008-09-28 15:14:41 by GasperAzman)