/* SPDX-License-Identifier: LGPL-2.1 */ /** * utest.h - header-only unit testing micro "framework". v1.0.0 * * Copyright (c) 2021-2023 Yaroslav de la Peña Smirnov */ #ifndef UTEST_H #define UTEST_H #include #include #include #include /* * TODO?: test runners that run in different processes in order to not crash the * whole test unit when a single test crashes. */ /** * Constants * ========= */ /** * Terminal escape codes * --------------------- * * TBLD: bold text. * TRED: red text. * TGRN: green text. * TBLU: blue text. * TRST: reset colors. * TDEL: delete current line. */ #ifndef NOCOLOR #define TBLD "\033[1m" #define TRED "\033[31m" #define TGRN "\033[32m" #define TBLU "\033[34m" #define TRST "\033[0m" #else #define TBLD "" #define TRED "" #define TGRN "" #define TBLU "" #define TRST "" #endif #define TDEL "\33[2K\r" /** * Typedefs * ======== */ typedef bool (*test_fn)(void); typedef void (*setup_fn)(void); /** * Internal variables * ================== */ static setup_fn tests_init = NULL; static setup_fn tests_destroy = NULL; /** * Functions and function-like macros * ================================== */ /** * FAIL_TEST - fail the test and print the @reason. * @reason: a variable list of arguments, where the first argument is the format * text. */ #define FAIL_TEST(reason...) \ printf(TDEL " [" TBLD TRED "×" TRST "]\t%s: " TBLD TRED "FAILED!\n" TRST, \ __this_name); \ printf("\t﹂%s:%d: ", __FILE__, __LINE__); \ printf(reason); \ printf("\n"); \ __ret = false; \ goto test_out /** * asserteq - assert @a and @b's equality; fail test if not equal. * @a: argument to compare to b. * @b: argument to compare to a. */ #define asserteq(a, b) \ ({ \ if ((a) != (b)) { \ FAIL_TEST("assertion " TBLD TBLU #a " == " #b TRST " failed"); \ } \ }) /** * assertneq - assert @a and @b's inequality; fail test if equal. * @a: argument to compare to b. * @b: argument to compare to a. */ #define assertneq(a, b) \ ({ \ if ((a) == (b)) { \ FAIL_TEST("assertion " TBLD TBLU #a " != " #b TRST " failed"); \ } \ }) /** * expect - fail the test on !@cond and print the reason. * @cond: condition to test. * @reason: a variable list of arguments, where the first argument is the format * text. */ #define expect(cond, reason...) \ ({ \ if (!(cond)) { \ FAIL_TEST(reason); \ } \ }) /** * TEST_BEGIN - declare test function @name. * @name: name of test function to declare. * * Should be followed by TEST_OUT and TEST_END. */ #define TEST_BEGIN(name) \ int name(void) \ { \ const char *__this_name = #name; \ bool __ret = true; \ printf(" [⏳]\t%s: " TBLU "running..." TRST, __this_name); \ fflush(stdout); /** * TEST_OUT - code that should be executed on test exit goes after this. * This declares a goto label, so that on test failure anything that needs to be * cleaned up is cleaned up. You need to use this even if there's nothing to be * cleaned up. */ #define TEST_OUT \ test_out:; /** * TEST_END - close the test scope. */ #define TEST_END \ if (__ret) { \ printf(TDEL " [" TBLD TGRN "✓" TRST "]\t%s: " TBLD TGRN \ "PASSED!\n" TRST, \ __this_name); \ } \ return __ret; \ } /** * __run_tests() - runs tests; for internal use. */ static inline int __run_tests(const char *const unit_name, ...) { va_list args; test_fn test; size_t passed = 0, total = 0, failed = 0; if (tests_init) { tests_init(); } printf("[***]\t" TBLD "running:" TRST TBLU " %s " TRST "tests\n", unit_name); va_start(args, unit_name); test = va_arg(args, test_fn); while (test) { test() ? passed++ : 0; total++; test = va_arg(args, test_fn); } va_end(args); failed = total - passed; printf("[***]\t" TBLD "finished:" TRST TBLU " %s: " TRST "tests: " TBLU "%lu" TRST ", passed: " TGRN "%lu" TRST ", failed: " TRED "%lu" TRST "\n", unit_name, total, passed, failed); if (tests_destroy) { tests_destroy(); } return failed; } /** * RUN_TEST_SUITE - runs the @tests given as arguments. * @init_fn: initialization function. * @destroy_fn: cleanup function. * @tests...: tests to run. * * @init_fn and @destroy_fn are used to initialize and cleanup test-related data * respectively. */ #define RUN_TEST_SUITE(init_fn, destroy_fn, tests...) \ int main(void) \ { \ tests_init = init_fn; \ tests_destroy = destroy_fn; \ return __run_tests(__FILE__, tests, NULL); \ } /** * RUN_TESTS - runs the @tests given as arguments. * @tests...: tests to run. * * Same as RUN_TEST_SUITE, but without initialization and cleanup functions */ #define RUN_TESTS(tests...) RUN_TEST_SUITE(NULL, NULL, tests) #endif /* UTEST_H */