I have written a template for a C program implementing a simple test framework.
If the program is run with the first parameter "test", all subsequent parameters will be interpreted as a sequence of test cases (without the test_
prefix) to be executed. If no other parameters are specified after the first parameter "test", all functions named test_*
will be executed in the sequence specified in the TEST_CASES
macro.
If one of the test cases fails, the program will report this and stop. In no test case fails, All test cases completed successfully
will be logged.
Normally I would split the program into a header, general function definitions and test case files, but for SE, I thought it would be easier post the amalgamated code.
What do you think?
/*template.c - program template with a basic test framework*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*To add a new test case for function foo(), add a line "X(foo) \" to the
TEST_CASES macro and then define the concrete test case function as -
result_t *test_foo(result_t *result){
// Code to test foo() goes here. If the test is successful, set
// result->rc = SUCCESS. Messages should be added to result->string using
// the cat_message function
}
*/
#define TEST_CASES \
X(init_string) \
X(cat_message) \
X(parse_tc) \
X(get_all_tc)
/*Return codes*/
typedef enum {
SUCCESS,
ERR_MALLOC,
ERR_REALLOC,
ERR_GENERAL
} rc_t;
/*String object : array of char with allocated size*/
typedef struct {
size_t size;
char *str;
} string_t;
/*Test results object : string object with return code*/
typedef struct {
string_t *string;
rc_t rc;
} result_t;
/*tc_func_t : test case function signature */
typedef result_t *(*tc_func_t)(result_t *result);
/*Test case execution sequence*/
typedef struct {
int num_tc; /* number in test cases in the tc_funcs array */
tc_func_t *tc_funcs; /*pointer to an array of test case functions*/
} tc_seq_t;
/*report_func_t : report results function signature*/
typedef rc_t (*report_func_t)(result_t *result);
/*Function prototypes*/
/*Initialise string object */
string_t *init_string(size_t size);
/*Concatenate a message to the string object*/
string_t *cat_message(const char *message, string_t *string);
/*Count number of test cases in TEST_CASES*/
int count_tc(void);
/*Create a test case set from the command line parameters*/
tc_seq_t *parse_tc(int argc, char **argv);
/*Create a test case set with all test cases specified in the TEST_CASES macro*/
tc_seq_t *get_all_tc(void);
/*Run all test cases in the test case sequence and report results*/
result_t *run_tests(tc_seq_t *tc_seq, report_func_t report_func,
result_t *result);
/*Write results to STDOUT*/
rc_t print_stdout(result_t *result);
/*Test case prototypes are generated by the X macro expansion*/
#define X(name) result_t *test_ ## name(result_t *result);
TEST_CASES
#undef X
string_t *init_string(size_t size){
string_t *string = malloc(sizeof *string);
/*If an error occurs, return a NULL pointer*/
if (string == NULL) return NULL;
string->str = malloc(size);
if (string->str == NULL){
free(string);
return NULL;
}
string->size = size;
*string->str = '\0';
return string;
}
string_t *cat_message(const char *message, string_t *string){
/*If the caller does supply the correct parameters, return the string object
* unchanged*/
if (message == NULL || string == NULL || string->str == NULL) return string;
while (strlen(message) >= string->size - strlen(string->str)){
/*If the memory allocated to the string object is insufficient, keep
* doubling the allocation until it is enough to concancenate the message*/
string->str = realloc(string->str, string->size * 2);
if (string->str == NULL) return string;
string->size *= 2;
}
memcpy(string->str+strlen(string->str), message, strlen(message)+1);
return string;
}
int count_tc(void){
int num_tc = 0;
#define X(name) num_tc++;
TEST_CASES
#undef X
return num_tc;
}
tc_seq_t *parse_tc(int argc, char **argv){
/*If the first parameter is not "test", return a NULL pointer*/
if (argc == 1 || strcmp(argv[1], "test")) return NULL;
/*if the first and only parameter is "test", return all test cases */
if (argc == 2) return get_all_tc();
/*return test cases specified in the parameters after "test"*/
tc_seq_t *tc_set = malloc(sizeof *tc_set);
tc_set->tc_funcs = malloc((argc - 2) * sizeof *tc_set->tc_funcs);
/*If an error occurs, return a NULL pointer*/
if (tc_set->tc_funcs == NULL) {
free(tc_set);
return NULL;
}
int tc_ind = 0;
/*Fill the array testcase->tc_funcs[] with pointers to test functions*/
for (int i = 2; i < argc; i++){
#define X(name) if (!strcmp(argv[i], #name)) \
{tc_set->tc_funcs[tc_ind] = test_ ## name; tc_ind++; break;}
TEST_CASES
#undef X
}
tc_set->num_tc = argc - 2;
return tc_set;
}
tc_seq_t *get_all_tc(void){
tc_seq_t *tc_set = malloc(sizeof *tc_set);
if (tc_set == NULL) return NULL;
tc_set->tc_funcs = malloc(count_tc() * sizeof *tc_set->tc_funcs);
/*If an error occurs, return a NULL pointer*/
if (tc_set->tc_funcs == NULL) {
free(tc_set);
return NULL;
}
tc_set->num_tc = count_tc();
/*Fill the array testcase->tc_funcs[] with pointers to test functions*/
int tc = -1;
#define X(name) tc++; tc_set->tc_funcs[tc] = test_ ## name;
TEST_CASES
#undef X
return tc_set;
}
result_t *run_tests(tc_seq_t *tc_seq, report_func_t report_func,
result_t *result){
for (int tc_ind = 0; tc_ind < tc_seq->num_tc; tc_ind++) {
result = tc_seq->tc_funcs[tc_ind](result);
if (result->rc != SUCCESS) break;
}
if (result->rc == SUCCESS) cat_message(
"All test cases completed successfully\n", result->string);
free(tc_seq->tc_funcs);
free(tc_seq);
report_func(result);
return result;
}
rc_t print_stdout(result_t *result){
if (result == NULL) return ERR_GENERAL;
if (result->string == NULL) return ERR_GENERAL;
if (result->string->str == NULL) return ERR_GENERAL;
printf("%s", result->string->str);
printf("Return code: %d\n", result->rc);
return SUCCESS;
}
int main(int argc, char **argv){
string_t *string = init_string(1024);
if (string == NULL) return ERR_MALLOC;
string = cat_message("Start log\n", string);
if (string == NULL) return ERR_REALLOC;
result_t *result = malloc(sizeof *result);
if (result == NULL) {
free(string->str);
free(string);
return ERR_GENERAL;
}
result->string = string;
result->rc = SUCCESS;
tc_seq_t *tc_seq = parse_tc(argc, argv);
if (tc_seq != NULL) result = run_tests(tc_seq, print_stdout, result);
free(string->str);
free(string);
free(result);
}
result_t *test_init_string(result_t *result){
string_t *string = init_string(10);
if (string == NULL){
result->rc = ERR_MALLOC;
result->string = cat_message("init_string failed\n", result->string);
return result;
}
if (string->size == 10 && !strcmp(string->str, "")){
result->rc = SUCCESS;
} else {
result->rc = ERR_GENERAL;
result->string = cat_message("init_string failed\n", result->string);
}
free(string->str);
free(string);
return result;
}
result_t *test_cat_message(result_t *result){
string_t *string = init_string(10);
if (string == NULL){
result->rc = ERR_MALLOC;
return result;
}
string = cat_message("foo", string);
if (string == NULL){
result->rc = ERR_REALLOC;
result->string = cat_message("cat_message failed\n", result->string);
return result;
}
string = cat_message("bar", string);
if (string == NULL){
result->string = cat_message("cat_message failed\n", result->string);
result->rc = ERR_REALLOC;
return result;
}
if (string->size == 10 && !strcmp(string->str, "foobar")){
result->rc = SUCCESS;
} else {
result->string = cat_message("cat_message failed\n", result->string);
result->rc = ERR_GENERAL;
}
free(string->str);
free(string);
return result;
}
result_t *test_parse_tc(result_t *result){
char *argv[] = {"template", "test", "parce_tc", "get_all_tc"};
tc_seq_t *tc = parse_tc(4, argv);
if (tc == NULL){
result->string = cat_message("parse_tc failed\n", result->string);
result->rc = ERR_GENERAL;
return result;
}
if (tc->num_tc == 2) {
result->rc = SUCCESS;
} else {
result->rc = ERR_GENERAL;
result->string = cat_message("parse_tc failed\n", result->string);
}
free(tc->tc_funcs);
free(tc);
return result;
}
result_t *test_get_all_tc(result_t *result){
tc_seq_t *tc = get_all_tc();
if (tc == NULL){
result->rc = ERR_GENERAL;
result->string = cat_message("get_all_tc failed\n", result->string);
return result;
}
if (tc->num_tc == count_tc()) {
result->rc = SUCCESS;
} else {
result->rc = ERR_GENERAL;
result->string = cat_message("get_all_tc failed\n", result->string);
}
free(tc->tc_funcs);
free(tc);
return result;
}
<!-- -->
at each file break. This makes it easy to copy+paste into a file, but still leaves it clear where the boundaries are. \$\endgroup\$