#if !defined(DN_UT_H) #define DN_UT_H /* //////////////////////////////////////////////////////////////////////////////////////////////////// // // $$\ $$\ $$$$$$$$\ $$$$$$$$\ $$$$$$\ $$$$$$$$\ // $$ | $$ |\__$$ __|$$ _____|$$ __$$\\__$$ __| // $$ | $$ | $$ | $$ | $$ / \__| $$ | // $$ | $$ | $$ | $$$$$\ \$$$$$$\ $$ | // $$ | $$ | $$ | $$ __| \____$$\ $$ | // $$ | $$ | $$ | $$ | $$\ $$ | $$ | // \$$$$$$ | $$ | $$$$$$$$\ \$$$$$$ | $$ | // \______/ \__| \________| \______/ \__| // // dn_utest.h -- Extremely minimal unit testing framework // //////////////////////////////////////////////////////////////////////////////////////////////////// // // A super minimal testing framework, most of the logic here is the pretty // printing of test results. // // NOTE: Configuration ///////////////////////////////////////////////////////////////////////////// // // #define DN_UT_IMPLEMENTATION // Define this in one and only one C++ file to enable the implementation // code of the header file. This will also automatically enable the JSMN // implementation. // // #define DN_UT_RESULT_LPAD // Define this to a number to specify how much to pad the output of the test // result line before the test result is printed. // // #define DN_UT_RESULT_PAD_CHAR // Define this to a character to specify the default character to use for // padding. By default this is '.' // // #define DN_UT_SPACING // Define this to a number to specify the number of spaces between the group // declaration and the test output in the group. // // #define DN_UT_BAD_COLOR // Define this to a terminal color code to specify what color errors will be // presented as. // // #define DN_UT_GOOD_COLOR // Define this to a terminal color code to specify what color sucess will be // presented as. // //////////////////////////////////////////////////////////////////////////////////////////////////// */ // NOTE: Macros //////////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #if !defined(DN_UT_RESULT_LPAD) #define DN_UT_RESULT_LPAD 90 #endif #if !defined(DN_UT_RESULT_PAD_CHAR) #define DN_UT_RESULT_PAD_CHAR '.' #endif #if !defined(DN_UT_SPACING) #define DN_UT_SPACING 2 #endif #if !defined(DN_UT_BAD_COLOR) #define DN_UT_BAD_COLOR "\x1b[31m" #endif #if !defined(DN_UT_GOOD_COLOR) #define DN_UT_GOOD_COLOR "\x1b[32m" #endif #define DN_UT_COLOR_RESET "\x1b[0m" #define DN_UT_Test(test, fmt, ...) \ for (int dummy_ = (DN_UT_BeginF((test), fmt, ##__VA_ARGS__), 0); \ (void)dummy_, (test)->state == DN_UTState_TestBegun; \ DN_UT_End(test)) #define DN_UT_AssertF(test, expr, fmt, ...) \ DN_UT_AssertAtF((test), __FILE__, __LINE__, (expr), fmt, ##__VA_ARGS__) #define DN_UT_Assert(test, expr) \ DN_UT_AssertAt((test), __FILE__, __LINE__, (expr)) // TODO: Fix the logs. They print before the tests, we should accumulate logs // per test, then, dump them on test on. But to do this nicely without crappy // mem management we need to implement an arena. #define DN_UT_Log(test, fmt, ...) \ DN_UT_LogF(test, "%*s" fmt "\n", DN_UT_SPACING * 2, "", ##__VA_ARGS__) #define DN_UT_AssertAtF(test, file, line, expr, fmt, ...) \ do { \ if (!(expr)) { \ (test)->state = DN_UTState_TestFailed; \ DN_UT_LogInsideTestF(test, \ "%*sAssertion File: %s:%d\n" \ "%*sExpression: [" #expr \ "]\n" \ "%*sReason: " fmt "\n", \ DN_UT_SPACING * 2, \ "", \ file, \ line, \ DN_UT_SPACING * 2, \ "", \ DN_UT_SPACING * 2, \ "", \ ##__VA_ARGS__); \ } \ } while (0) #define DN_UT_AssertAt(test, file, line, expr) \ do { \ if (!(expr)) { \ (test)->state = DN_UTState_TestFailed; \ DN_UT_LogInsideTestF(test, \ "%*sAssertion File: %s:%d\n" \ "%*sExpression: [" #expr "]\n", \ DN_UT_SPACING * 2, \ "", \ file, \ line, \ DN_UT_SPACING * 2, \ ""); \ } \ } while (0) // NOTE: Header //////////////////////////////////////////////////////////////////////////////////// typedef enum DN_UTState { DN_UTState_Nil, DN_UTState_TestBegun, DN_UTState_TestFailed, } DN_UTState; typedef struct DN_UTStr8Link { char *data; size_t size; DN_UTStr8Link *next; DN_UTStr8Link *prev; } DN_UTStr8Link; typedef struct DN_UTCore { int num_tests_in_group; int num_tests_ok_in_group; DN_UTState state; char name[256]; size_t name_size; DN_UTStr8Link *curr_test_messages; DN_UTStr8Link *output; } DN_UTCore; void DN_UT_BeginFV(DN_UTCore *test, char const *fmt, va_list args); void DN_UT_BeginF(DN_UTCore *test, char const *fmt, ...); void DN_UT_End(DN_UTCore *test); void DN_UT_LogF(DN_UTCore *test, char const *fmt, ...); void DN_UT_LogInsideTestF(DN_UTCore *test, char const *fmt, ...); bool DN_UT_AllTestsPassed(DN_UTCore const *test); void DN_UT_PrintTests(DN_UTCore const *test); #endif // DN_UT_H // NOTE: Implementation //////////////////////////////////////////////////////////////////////////// #if defined(DN_UT_IMPLEMENTATION) DN_UTCore DN_UT_Init() { DN_UTCore result = {}; result.output = (DN_UTStr8Link *)calloc(1, sizeof(*result.output)); result.curr_test_messages = (DN_UTStr8Link *)calloc(1, sizeof(*result.curr_test_messages)); assert(result.output); assert(result.curr_test_messages); result.output->next = result.output->prev = result.output; result.curr_test_messages->next = result.curr_test_messages->prev = result.curr_test_messages; return result; } void DN_UT_Deinit(DN_UTCore *ut) { for (DN_UTStr8Link *it = ut->output->next; it != ut->output; it = ut->output->next) { it->next->prev = it->prev; it->prev->next = it->next; free(it); } free(ut->output); for (DN_UTStr8Link *it = ut->curr_test_messages->next; it != ut->curr_test_messages; it = ut->curr_test_messages->next) { it->next->prev = it->prev; it->prev->next = it->next; free(it); } free(ut->curr_test_messages); } void DN_UT_BeginFV(DN_UTCore *ut, char const *fmt, va_list args) { assert(ut->output && ut->curr_test_messages && "Test must be initialised by calling DN_UT_Init()"); assert(ut->state == DN_UTState_Nil && "Nesting a unit ut within another unit test is not allowed, ensure" "the first test has finished by calling DN_UT_End"); ut->num_tests_in_group++; ut->state = DN_UTState_TestBegun; ut->name_size = 0; { va_list args_copy; va_copy(args_copy, args); ut->name_size = vsnprintf(NULL, 0, fmt, args_copy); va_end(args_copy); } assert(ut->name_size < sizeof(ut->name)); vsnprintf(ut->name, sizeof(ut->name), fmt, args); } void DN_UT_BeginF(DN_UTCore *ut, char const *fmt, ...) { va_list args; va_start(args, fmt); DN_UT_BeginFV(ut, fmt, args); va_end(args); } static DN_UTStr8Link *DN_UT_AllocStr8LinkFV(char const *fmt, va_list args) { va_list args_copy; va_copy(args_copy, args); size_t size = vsnprintf(nullptr, 0, fmt, args_copy) + 1; va_end(args_copy); DN_UTStr8Link *result = (DN_UTStr8Link *)malloc(sizeof(*result) + size); if (result) { result->data = (char *)result + sizeof(*result); result->size = vsnprintf(result->data, size, fmt, args); } return result; } void DN_UT_End(DN_UTCore *ut) { assert(ut->state != DN_UTState_Nil && "Test was marked as ended but a ut was never commenced using DN_UT_Begin"); size_t pad_size = DN_UT_RESULT_LPAD - (DN_UT_SPACING + ut->name_size); if (pad_size < 0) pad_size = 0; char pad_buffer[DN_UT_RESULT_LPAD] = {}; memset(pad_buffer, DN_UT_RESULT_PAD_CHAR, pad_size); DN_UT_LogF(ut, "%*s%.*s%.*s", DN_UT_SPACING, "", (int)ut->name_size, ut->name, (int)pad_size, pad_buffer); if (ut->state == DN_UTState_TestFailed) { DN_UT_LogF(ut, DN_UT_BAD_COLOR " FAILED"); } else { DN_UT_LogF(ut, DN_UT_GOOD_COLOR " OK"); ut->num_tests_ok_in_group++; } DN_UT_LogF(ut, DN_UT_COLOR_RESET "\n"); ut->state = DN_UTState_Nil; // NOTE: Append any test messages (like assertions) into the main output buffer for (DN_UTStr8Link *it = ut->curr_test_messages->next; it != ut->curr_test_messages; it = ut->curr_test_messages->next) { // NOTE: Detach it->next->prev = it->prev; it->prev->next = it->next; // NOTE: Attach it->next = ut->output; it->prev = ut->output->prev; it->next->prev = it; it->prev->next = it; } } void DN_UT_LogF(DN_UTCore *ut, char const *fmt, ...) { assert(ut->output && ut->curr_test_messages && "UT was not initialised by calling UT_Init yet"); va_list args; va_start(args, fmt); DN_UTStr8Link *result = DN_UT_AllocStr8LinkFV(fmt, args); va_end(args); result->next = ut->output; result->prev = ut->output->prev; result->next->prev = result; result->prev->next = result; } void DN_UT_LogInsideTestF(DN_UTCore *ut, char const *fmt, ...) { assert(ut->state >= DN_UTState_TestBegun && ""); va_list args; va_start(args, fmt); DN_UTStr8Link *result = DN_UT_AllocStr8LinkFV(fmt, args); va_end(args); result->next = ut->curr_test_messages; result->prev = ut->curr_test_messages->prev; result->next->prev = result; result->prev->next = result; } bool DN_UT_AllTestsPassed(DN_UTCore const *ut) { bool result = ut->num_tests_ok_in_group == ut->num_tests_in_group; return result; } void DN_UT_PrintTests(DN_UTCore const *ut) { for (DN_UTStr8Link *it = ut->output->next; it != ut->output; it = it->next) fprintf(stdout, "%.*s", (int)it->size, it->data); bool all_clear = DN_UT_AllTestsPassed(ut); fprintf(stdout, "%s\n %02d/%02d tests passed -- %s\n\n" DN_UT_COLOR_RESET, all_clear ? DN_UT_GOOD_COLOR : DN_UT_BAD_COLOR, ut->num_tests_ok_in_group, ut->num_tests_in_group, all_clear ? "OK" : "FAILED"); } #endif // DN_UT_IMPLEMENTATION