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'])
