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 = ''; 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; }