The test harness provides a system for scripting regression tests on C functions and source modules containing interacting sets of functions. It consists of two parts:
Unit tests are defined within test scripts. These are plain text files which contain a sequence of function test definitions interspersed with comment lines used to describe tests and their expected results. Blank lines may be added for improved readability. Each test definition is a line containing the name of the function to be executed, a list of parameter values and an optional comment at the end of the line, which is typically used to say whether this test is expected to succeed or fail and to show the expected return value, if any.
This is a short example script:
---------------------------------------------------------------- # re01.txt - clean re_error() test. # ======== - error with simple text description # reporterror re01 # OK re_error "error text 1" # "re01: error text 1" expected # # No errors expected # ----------------------------------------------------------------
The first test runs the reporterror() function with the string "re01" as the only parameter. The second test runs the re_error() function with "error text 1" as its only parameter.
The output from running a test script is intended to be an easily readable file, with outputs from each function test call, including error reports, immediately following the line that defined the test.
Regression testing is easily managed by a shell script that can:
Every line in a test script written to an output file via stdout, preceded by a line number. Comment lines may appear anywhere and have a hash, #, as the first character. Comments and blank lines are copied to the output but otherwise ignored.
Each non-comment line defines a function call. This takes the form:
function param1 param2 .... # comment documenting expected results
The function name must start at the first character in the line and parameters are separated by one or more whitespace characters. There may be up to 10 parameters on each line. Quoted parameters may contain whitespace. Unquoted parameters must not contain whitespace.
If a parameter contains whitespace it must be 'quoted' by enclosing it in single or double quotes ( 'param text' or "param text" ). Quoted parameters must be terminated with the same single or double quote mark that they started with, but may contain the other type of quote, so "String val '%s'" and 'Char val "%c"' will both be parsed correctly, but "stringvalue' will include all following parameters until either another double quote or the end of the parameter list is found.
The parameter list may be followed by a comment, starting with a hash, which is a good way of documenting the expected results from a function call.
When the text has been parsed into function name plus a list of parameter strings, a parallel list of integer numeric values is set up. Each parameter is first tested to see if it is a boolean value. If its first character is T,t or 1 its numeric value is set to 1 (TRUE) and if its first character is F,f or 0 its numeric value to 0 (FALSE).
If neither comparison matches, the parameter is used as input to atoi() and the result is used as its numeric value.
The calladapter() function, which will be described later, is now called to execute the required function test. It is passed the function name and both string and integer versions of the parameter list, runs the test and returns the test results by calling one or more of the following functions, which each write an output line to stdout immediately after the script line that defined the test:
Function called | Output |
void setboolreply(int n); | Returned: true|false |
void setcharreply(char c); | Returned: 'c' |
void setdoublereply(double d); | Returned: n.nnnnnn |
void seterror(char *e); | Error: error text |
void setfloatreply(float f); | Returned: n.nnnnnn |
void setintreply(int r); | Returned: nnnn |
void setstringreply(char *s); | Returned: "string value" |
The number and type of results shown is determined by the way in which you customise calladapter() - see below.
The number of calls to seterror() are counted and output when the test script has been completely processed.
Note:
This process will normally only require changes to the custom.c source file. Make a copy of custom.c and custom.h before you start, so you still have the original files available building further test harneses
custom.c provides the interface between the test harness functionality in runtest.c and the C functions being tested. It contains two functions:
Tests are driven by code in runtest.c, which uses command line arguments and options to set run conditions and specify which test scripts will be run. Runtest interprets each test script, using the calladapter() function in this module to exercise the functions being tested, and writes the results of each call to stdout immediately after the script line that defined the test.
This is a code-specific call adapter which must be modified to call the set of functions to be tested.
Entry and exit is reported if debug level is 2 or more. custom.c is an example implementation that contains the code to run one example function, whose prototype is:
int example(char *s int i);
It is used to show a basic setup which will compile and run successfully. The basic if..then..else chain of function calls should be preserved, but any or all if-actions can be converted into code blocks and/or may call additional privately defined functions.
The functions used to return results, report errors, etc, are defined in runtest.c. They are seterror() and the setxxxxreply() functions.
The final item in the condition chain should not be modified. It is there to trap function names in test scripts that don't match any of the functions being tested.
When calladapter() is called by code in runtest.c:
debug | (normally zero) can be set to 1 to showcalladapter() entry and exit conditions by adding a single -d option to the command line. |
fn | must contain the name of the function being tested. |
s[][] | is an array containing the call parameters as strings. Unused parameters have a NULL pointer, so can cause crashes. |
n[] | is an array containing the call parameters as int values with those that weren't numeric set to zero. |
It is the customiser's responsibility to map the contents of the s[][] and n[] arrays to the parameter list of each function being tested. Any additional code that may be needed to provide the correct mapping can be added as required, e.g. converting s[1] to a char or n[2] to a double.
On exit calladapter() should return 0 if the overall call didn't encounter any fatal errors and a non-zero value if an error was encountered that was serious enough to abandon the run part-way through the test script.
The text in showhelp() is displayed if the test harness is run with the '-?' option. It should say that this is a customised test harness, describe what functions or modules are being tested and identify the (set of) test scripts it uses, e.g. that their names are all of the form data/mytest99.txt
This is the example function called by calladapter() as supplied. As you can see it will write its first parameter to stdout and return the second parameter.
This process will normally only require changes to the custom.c source file. Make a copy of custom.c and custom.h and adapt the Makefile to suit. Finger trouble aside, if you don't change any function prototypes or headers a new test harness should build and run pretty much straight away.
Here is the Makefile used to compile a demo test program called custom using the files as they stand.
NOTE: compiling the test harness requires my environ library, which you can download from https://www.libelle-systems.com and should be put in /usr/local/lib.
LIB = -L/usr/local/lib INC = -I/usr/local/include EXE = /usr/local/bin MAN = /usr/local/man/man1 BIN = custom MANP = README.html CFLAGS = $(INC) -c HEADERS = custom.h ../testargparser.h ../testre.h all: $(BIN) testargparser.o testre.o clean: rm *.o $(BIN) custom: custom.o runtest.o cc custom.o runtest.o -o custom custom.o: custom.c cc custom.c $(CFLAGS) testargparser.o: testargparser.c ../argparser.h cc testargparser.c $(CFLAGS) runtest.o: runtest.c runtest.h cc runtest.c $(CFLAGS) testre.o: testre.c ../reporterror.o ../reporterror.h cc testre.c $(CFLAGS)