This is a simple test harness for C. Test cases can be linked easily by other modules with function pointers. This harness runs all registered cases or ones selected in the command line and reports results to a file.
Also includes examples on how to use getopt_long() and qsort().
#getopt_long #qsort #test-framework #unit-testing #testing #c
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <stdlib.h>
#include <getopt.h>
/* ------------------------ GLOBAL VARIABLES ------------------------------- */
char *g_resultFile = NULL;
/* ------------------------- LOCAL CONSTANTS ------------------------------- */
/** Length for test case ID */
#define LEN_TC_ID 6
/** Max length for TC description */
#define LEN_TC_NAME 40
/** Max number of test cases */
#define NUM_TC 50
/** Defines that a test case should not be run */
#define TC_NORUN 0
/** Defines that a test case should be run */
#define TC_RUN 1
/** ID value for empty test case slots */
#define TC_NOT_SET INT_MAX
#define DEFAULT_RESULT_FILE "results.txt"
/* ------------------------- LOCAL TYPES ----------------------------------- */
/** Test case structure for holding information about the test case and
results. */
typedef struct {
/** Test case number. */
int num;
/** Test case ID. This is used as the identifier when selecting cases. */
char id[LEN_TC_ID];
/** Test case name. */
char name[LEN_TC_NAME];
/** Pointer to test function. */
void (*fp) (const int, int *);
/** Flag defining whether to run case or not. */
unsigned int run;
/** Test case result. */
int result;
} TestCase_t;
/** Table to hold all the test cases - declared static because we have to
edit this file when we add new cases anyways, so increasing this table
can be done at the same time. */
static TestCase_t test_cases[NUM_TC];
/* ----------------------- LOCAL VARIABLES --------------------------------- */
/** Short versions for command-line options */
static const char short_options [] = "t:r:lh";
/** Long versions for command-line options */
static const struct option
long_options [] = {
{ "test-case", required_argument, NULL, 't' },
{ "result-file", required_argument, NULL, 'r' },
{ "list-cases", no_argument, NULL, 'l' },
{ "help", no_argument, NULL, 'h' },
{ 0, 0, 0, 0 }
};
/* ------------------------- LOCAL PROTOTYPES ------------------------------ */
static void reportResult(FILE * filePtr, const TestCase_t test_case);
static void init(void);
static int compTC(const void *a, const void *b);
static TestCase_t *findTcById(const char *id);
static void printTCs(void);
static void usage(char **argv);
static void setGlobalString(char **dest, char *src);
/* ------------------------- LOCAL FUNCTIONS ------------------------------- */
/** Initialises the tc structure.
*
* A call to each module's test case init function should be added to this
* function.
*
*/
static void init(void)
{
int i;
/* Init tc structures */
for (i = 0; i < NUM_TC; i++) {
test_cases[i].num = TC_NOT_SET;
memset(test_cases[i].id, '\0', LEN_TC_ID);
memset(test_cases[i].name, '\0', LEN_TC_NAME);
test_cases[i].fp = NULL;
test_cases[i].run = TC_NORUN;
test_cases[i].result = -1;
}
/* Include call to test init function for each module here */
/* Sort the test cases -> it will be easier to find test cases by
id after this and */
qsort(test_cases, NUM_TC, sizeof(TestCase_t), compTC);
}
/** Prints test result to file.
*
* @param[in] filePtr Pointer to output file.
* @param[in] test_case Test case to report.
*
*/
static void reportResult(FILE * filePtr, const TestCase_t test_case)
{
if (filePtr != NULL) {
fprintf(filePtr, "\n");
fprintf(filePtr, "TC number: %d\n", test_case.num);
fprintf(filePtr, "TC ID: %s\n", test_case.id);
fprintf(filePtr, "TC Name: %s\n", test_case.name);
fprintf(filePtr, "TC Result: %d", test_case.result);
if (test_case.result == 0) {
fprintf(filePtr, "\n\n");
} else {
fprintf(filePtr, "<------\n\n");
}
} else {
printf("Result file not open!\n");
}
}
/** Qsort comparison function for test case id's.
*
* @param[in] a First argument to compare.
* @param[in] b Second argument to compare.
*
* @return Negative if b > a, positive if a > b, 0 if a == b.
*/
static int compTC(const void *a, const void *b)
{
const TestCase_t *sa = (const TestCase_t *)a;
const TestCase_t *sb = (const TestCase_t *)b;
return sa->num - sb->num;
}
/** Finds a test case from the test case set by the ID.
*
* @param id ID of test case to find.
*
* @return Pointer to test case or NULL if not found.
*
* @todo Use binary search instead of brute-force.
*/
static TestCase_t *findTcById(const char *id)
{
int i;
for (i = 0; i < NUM_TC; i++) {
if (test_cases[i].num == TC_NOT_SET) {
/* Cases with id == TC_NOT_SET are at the end
of the list after the qsort so we now we
have gone through all the test cases by now */
break;
}
if (!strncmp(test_cases[i].id, id, LEN_TC_ID - 1)) {
return &(test_cases[i]);
}
}
return NULL;
}
/** Prints usage (command-line options) information
*
* @param argv Command-line arguments.
*/
static void usage(char **argv)
{
printf("Usage: %s [options]\n\n"
"Options:\n"
"-t, --test-case \n"
"-r, --result-file Name of file to print results to\n"
" (default: %s)\n"
"-l, --list-cases List all test cases (can be used in conjunction\n"
" with -t, i.e. cases will be printed and cases run)\n"
"-h, --help This help\n",
argv[0], DEFAULT_RESULT_FILE);
}
/** For debugging purposes atm.
*/
static void printTCs(void)
{
int i;
for (i = 0; i < NUM_TC; i++) {
if (test_cases[i].num == TC_NOT_SET) {
/* Do not print case slots that are not defined */
break;
}
printf("Num: %d, ID: %s, Name: %s, Run: %u, Result: %d\n",
test_cases[i].num, test_cases[i].id, test_cases[i].name,
test_cases[i].run, test_cases[i].result);
}
}
/* ------------------------- EXPORTED FUNCTIONS ---------------------------- */
/** Registers a test case to the test case list.
*
* Any test case the user wants to run must be registered using this function.
*
* @param[in] name Name of test case (only used for reporting purposes).
* @param[in] id ID of test case (used to identify test case).
* @param[in] num Test case number (used to sort cases, this defines the
' order the cases are run in).
* @param[in] fp Pointer to test function.
*/
void addTestCase(const char *name, const char *id, const int num,
const void *fp)
{
static int lastTC = 0;
if (lastTC >= NUM_TC) {
printf("Max %d test slots allowed!\n", NUM_TC);
} else {
strncpy(test_cases[lastTC].id, id, LEN_TC_ID);
test_cases[lastTC].id[LEN_TC_ID - 1] = '\0';
strncpy(test_cases[lastTC].name, name, LEN_TC_NAME);
test_cases[lastTC].name[LEN_TC_NAME - 1] = '\0';
test_cases[lastTC].num = num;
test_cases[lastTC].fp = fp;
lastTC++;
}
}
/** Sets dest to src if dest string is NULL, otherwise leaves it to original value.
*
* @param dest Destination string to set.
* @param src Source string to copy from.
*
*/
static void setGlobalString(char **dest, char *src)
{
if (*dest == NULL) {
*dest = (char*)malloc(strlen(src));
if (*dest == NULL) {
fprintf(stderr, "Failed to allocate memory for global string.\n");
exit(0);
}
strcpy(*dest, src);
}
}
/** Main function
*/
int main(int argc, char *argv[])
{
int i;
int res = 0;
FILE *resultFilePtr;
unsigned int run_all = 0;
TestCase_t *test_case;
int retVal;
unsigned int printCases = 0;
if (argc < 2) {
usage(argv);
exit(0);
}
init();
for(;;) {
int index;
int c;
c = getopt_long (argc, argv,
short_options, long_options,
&index);
if (c == -1) {
break;
}
switch (c) {
case 0:
break;
case 't':
test_case = findTcById(optarg);
if (test_case != NULL) {
test_case->run = TC_RUN;
} else {
fprintf(stderr, "Unknown test case %s.\n", optarg);
}
break;
case 'r':
g_resultFile = malloc(strlen(optarg));
strcpy(g_resultFile, optarg);
break;
case 'l':
printCases = 1;
break;
case 'h':
default:
usage(argv);
exit(0);
}
}
/* Open result file */
setGlobalString(&g_resultFile, DEFAULT_RESULT_FILE);
resultFilePtr = fopen(g_resultFile, "w");
if (resultFilePtr == NULL) {
fprintf(stderr, "Unable to open result file %s!\n", g_resultFile);
return 0;
}
if (printCases) {
printf("Full list of test cases:\n");
printTCs();
}
/* Run designated tests */
for (i = 0; i < NUM_TC; i++) {
if (test_cases[i].num == TC_NOT_SET) {
break;
}
if ((run_all == 1)
|| (test_cases[i].run == TC_RUN
&& test_cases[i].fp != NULL)) {
fprintf(stderr, "Running case %s : %s\n",
test_cases[i].id, test_cases[i].name);
(*test_cases[i].fp) (test_cases[i].num, &res);
test_cases[i].result = res;
reportResult(resultFilePtr, test_cases[i]);
res = 0;
}
}
/* Close result file */
if ((retVal = fclose(resultFilePtr)) != 0) {
printf("Error closing results file!\n");
}
return 0;
}