Alias

#!python# Build one or more test runners.program=env.Program('test','TestMain.cpp')# Depend on the runner to ensure that it's built before running it.test_alias=Alias('test',[program],program[0].path)# Simply required. Without it, 'test' is never considered out of date.AlwaysBuild(test_alias)

Note that program[0].path might give issues when running on OS'es that do not explicitly search for executables in the current directory (Unix-like OS'es where you explicitly need to add '.' as a search path). In that case, you can use the following:

#!python# Build one or more test runners.program=env.Program('test','TestMain.cpp')# Depend on the runner to ensure that it's built before running it - Note: using abspath.test_alias=Alias('test',[program],program[0].abspath)# Simply required. Without it, 'test' is never considered out of date.AlwaysBuild(test_alias)

<span style="display:none">This doesn't work if your unit test program depends on a certain shared library that resides on the same folder as the unit test program since the environment variable LD_LIBRARY_PATH needs to be edited.</span>

Alias with Command

If you want your unit test being invoked only on demand, the following work for me (java unit test)

Command

Another idea is inspired on the boost build V2 system, that will create a file stamp if the unittest has run succesful. If it ran succesfull (exit code 0) and there is nothing changed, there is no need to run the unit test again.

Note by Dov Grobgeld 2005-12-18

I modified the method mentioned above in order to be able to use it in a SConscript file without needing to defining runUnitTest in each SConscript file. Here is what I did:

In the SConstruct file:

#!pythondefbuilder_unit_test(target,source,env):app=str(source[0].abspath)ifos.spawnl(os.P_WAIT,app,app)==0:open(str(target[0]),'w').write("PASSED\n")else:return1# Create a builder for testsbld=Builder(action=builder_unit_test)env.Append(BUILDERS={'Test':bld})

The test may then be declared in each of the library SConscript files by doing:

Now I am going to build a builder for automatically running valgrind/purify as well.

Note by Matt Doar 2007-01-24

I modified Dov's work to support comparing the results of running a test to a file containing the expected results. I also added an option to regenerate the expected results file. This is working nicely for us in practice.

In the SConstruct file, first add the boilerplate for the new regenerate option:

#!python# Add some command line options to SCons to support different build types.# Example of using an option: scons regenerate=1 ...command_line_options=Options()command_line_options.AddOptions(('regenerate','Set to 1 to regenerate the expected results of unit tests',0),)# The default build environment, used for all programsenv=Environment(options=command_line_options,)# Generate the "scons --help" text for the optionsHelp(command_line_options.GenerateHelpText(env))# Used in UnitTestenv['REGENERATE']=0ifstr(ARGUMENTS.get('regenerate',0))=='1':env['REGENERATE']=1

#!pythonimportosdefrun(cmd,env):"""Run a Unix command and return the exit code."""res=os.system(cmd)if(os.WIFEXITED(res)):code=os.WEXITSTATUS(res)returncode# Assumes that if a process doesn't call exit, it was successfulreturn0defunit_test_emitter(target,source,env):base,ext=os.path.splitext(source[1].abspath)source.append(base+'.expected')return(target,source)defUnitTest(target,source,env):'''Run some app with an inputfile and compare the output with a .expected file containing the expected results.'''app=str(source[0].abspath)inputfile=str(source[1].abspath)base,ext=os.path.splitext(inputfile)expected=base+'.expected'# Output can come on both stdout and stderrcmd=app+' '+inputfile+' 2>&1 | diff '+expected+' -'ifenv['REGENERATE']==1:print"Regenerating expected results file: "+expectedcmd=app+' '+inputfile+' &> '+expectedres=run(cmd,env)# If the test passed, create the target file so the test won't be run againifres==0:open(str(target[0]),'w').write("PASSED\n")returnres# Create a builder for running unit testsbld=Builder(action=Action(UnitTest,varlist=['REGENERATE']),emitter=unit_test_emitter)env.Append(BUILDERS={'UnitTest':bld})# NOTE: Only apply changes to env above hereExport('env')

Using the new Builder in an SConscript file:

#!pythonImport('env')# removed the code to build myapp ...# Note: this test will look for a file named inputfile1.expected so you may have# to touch that file to bootstrap the creation of the test.mytest1=env.UnitTest("tests/test1.passed",[myapp,'inputfile1.txt'])Alias("mytest1",mytest1)

First generate the expected results file with "scons regenerate=1 mytest1". Then run the unit test with "scons mytest1".

Unit Test integration with an scons Tool

Section added by Chris Foster, 23-7-2007

I wanted to integrate unit testing into the aqsis scons build system, in a way which made it as easy as possible to add tests from our Sconstruct files. I ended up writing an scons Tool (see listing below) to encapsulate adding the appropriate things to an environment, building on Dov's work above.

The nice thing about this is that you can very cleanly create add a test environment which includes the tool, and add any libraries which you need to link with to that test building environment. Here's what a section of a main SConstruct file might look like, when using boost.test for testing:

#!python# make an initial construction environmentenv=Environment()Export('env')# Set up the test environment. We copy the environment so that we can add the# extra libraries needed without messing up the environment for production# builds.## Here we use boost.test as the unit testing framework.testEnv=env.Copy()testEnv.Tool('unittest',toolpath=['build_tools'],UTEST_MAIN_SRC=File('build_tools/boostautotestmain.cpp'),LIBS=['boost_unit_test_framework'])Export('testEnv')# grab stuff from sub-directories.env.SConscript(dirs=['onelib'])

In some sub-directory, onelib, you can then add tests quite easily, as follows:

#!python#-------------------------------------------------------------------------------# Unit testsImport('testEnv')testEnv=testEnv.Copy()testEnv.AppendUnique(LIBPATH=[env.Dir('.')],LIBS=['one'])testEnv.PrependENVPath('LD_LIBRARY_PATH',env.Dir('.').abspath)# We can add single file unit tests very easily.testEnv.addUnitTest('two_test.cpp')# also, multiple files can be compiled into a single test suite.libone_test_sources=Split(''' one_test.cpp two_test.cpp ''')testEnv.addUnitTest('libone_test_all',libone_test_sources)# all the tests added above are automatically added to the 'test' alias.

Because the tool automatically adds Aliases, it's easy to run a particular test,

$ scons two_test

or the whole set of tests:

$ scons test

Here's the code for the tool:

#!pythonimportosdefunitTestAction(target,source,env):''' Action for a 'UnitTest' builder object. Runs the supplied executable, reporting failure to scons via the test exit status. When the test succeeds, the file target.passed is created to indicate that the test was successful and doesn't need running again unless dependencies change. '''app=str(source[0].abspath)ifos.spawnle(os.P_WAIT,app,env['ENV'])==0:open(str(target[0]),'w').write("PASSED\n")else:return1defunitTestActionString(target,source,env):''' Return output string which will be seen when running unit tests. '''return'Running tests in '+str(source[0])defaddUnitTest(env,target=None,source=None,*args,**kwargs):''' Add a unit test Parameters: target - If the target parameter is present, it is the name of the test executable source - list of source files to create the test executable. any additional parameters are passed along directly to env.Program(). Returns: The scons node for the unit test. Any additional files listed in the env['UTEST_MAIN_SRC'] build variable are also included in the source list. All tests added with addUnitTest can be run with the test alias: "scons test" Any test can be run in isolation from other tests, using the name of the test executable provided in the target parameter: "scons target" '''ifsourceisNone:source=targettarget=Nonesource=[source,env['UTEST_MAIN_SRC']]program=env.Program(target,source,*args,**kwargs)utest=env.UnitTest(program)# add alias to run all unit tests.env.Alias('test',utest)# make an alias to run the test in isolation from the rest of the tests.env.Alias(str(program[0]),utest)returnutest#-------------------------------------------------------------------------------# Functions used to initialize the unit test tool.defgenerate(env,UTEST_MAIN_SRC=[],LIBS=[]):env['BUILDERS']['UnitTest']=env.Builder(action=env.Action(unitTestAction,unitTestActionString),suffix='.passed')env['UTEST_MAIN_SRC']=UTEST_MAIN_SRCenv.AppendUnique(LIBS=LIBS)# The following is a bit of a nasty hack to add a wrapper function for the# UnitTest builder, see http://www.scons.org/wiki/WrapperFunctionsfromSCons.Script.SConscriptimportSConsEnvironmentSConsEnvironment.addUnitTest=addUnitTestdefexists(env):return1

Since I am quite new to scons, I wasn't able to figure out how exactly to put my extensions into a separate file to be sourced by scons automatically, and I hope someone can supply that knowledge.

This also does not support all CxxTest functionality. I only built in what I required, but the result is neat and simple.

Without further ado, here is the code from my SConstruct:

#!pythonfromSCons.Script.SConscriptimportSConsEnvironmentenv=Environment()# required for the cxxbuilder.# If you use the normal header files, just use .h here.env['TEST_SUFFIX']='.t.h'# ----------------------------------# cxx test builder# ----------------------------------CxxTestCpp_bld=Builder(action="./cxxtestgen.py --error-printer -o $TARGET $SOURCE",suffix=".cpp",src_suffix='$TEST_SUFFIX')env['BUILDERS']['CxxTestCpp']=CxxTestCpp_bld# ----------------------------------# UnitTest function - a wrapper around# the Program call that adds the result# of the build to the tests-to-run target.# ----------------------------------defUnitTest(environ,target,source=[],**kwargs):test=environ.Program(target,source=source,**kwargs)environ.AlwaysBuild('check')environ.Alias('check',test,test[0].abspath)returntestSConsEnvironment.UnitTest=UnitTest# ----------------------------------# A wrapper that supplies the multipart# build functionality CxxTest requires.# ----------------------------------defCxxTest(environ,target,source=[],**kwargs):if(source==[]):source=Split(target+environ['TEST_SUFFIX'])sources=Split(source)sources[0]=environ.CxxTestCpp(sources[0])returnenviron.UnitTest(target,source=sources,**kwargs)SConsEnvironment.CxxTest=CxxTest

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['TEST_SUFFIX'],