/* SPDX-License-Identifier: LGPL-2.1 */ /** * utest.h - header-only unit testing micro "framework". v2.0.0 * * Copyright (c) 2021-2026 Yaroslav de la Peña Smirnov * * Changes from v1 * =============== * * - C23 required. * - Less macro voodoo magic. * - The test declaration API changed. * - Now there's a test type with the test data. * - Now it's possible to set a time out for each test. * - It's now also possible to initialize data for each test separately. * - More importantly, each test is now isolated in its own process, so that we * beautifully report a crash and continue with the rest. * - Still less than 500loc even with all these comments! */ #ifndef UTEST_H #define UTEST_H #ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L #endif #include #include #include #include #include #include #include #include #include #include /* Copy this header and change the include path as needed */ #include "../utils.h" /** * 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" typedef struct utest utest; /** * test_fn - A test function. * @t: the test data; required for the macros. */ typedef void (*test_fn)(utest *t); typedef void (*setup_fn)(void); enum utest_result : uint8_t { UTEST_SUCCESS, UTEST_FAILURE, UTEST_TIMEOUT, }; static_assert(UTEST_SUCCESS == 0, "UTEST_SUCCESS should be equal to zero"); /** * struct utest - A single test. * @name: the test's name, usually the same as the function's (@run) name. * @run: the function with the test; must NOT be NULL. * @pre_run: optional function to initialize test data, run before the test. * @post_run: optional cleanup function, run's after a test. * @timeout_s: test timeout in secs; if 0 no timeout; the test will be killed * and marked as failed after the set time expires. * @result: used to pass the result of the test back to the wrapper func. * * This is an opaque type. Only functions in this header should access the * fields in this struct. */ struct utest { const char *name; test_fn run; setup_fn pre_run; setup_fn post_run; uint32_t timeout_s; enum utest_result result; }; static setup_fn ut_init = NULL; static setup_fn ut_cleanup = NULL; static sigset_t sset; /** * UTEST_INIT - Initialize a single test. * @_name: the test name. * @pre_run_fn: data initialization function. * @post_run_fn: data cleanup function. * @_timeout_s: timeout in seconds; 0 - no timeout. * * The test function should be named "`_name`_run". */ #define UTEST_INIT(nam, pre_run_fn, post_run_fn, timout_s) \ { \ .name = #nam, \ .run = nam##_run, \ .pre_run = pre_run_fn, \ .post_run = post_run_fn, \ .timeout_s = timout_s, \ } /** * UTEST_INIT_S - Initialize a single test, without data initializer/destroyer. * @_name: the test name. * @_timeout_s: timeout in seconds; 0 - no timeout. * * See also UTEST_INIT. */ #define UTEST_INIT_S(name, timeout_s) UTEST_INIT(name, NULL, NULL, timeout_s) #define ut_prfail(t, reason...) \ ({ \ printf(TDEL " [" TBLD TRED "×" TRST "]\t%s: " TBLD TRED \ "FAILED!\n" TRST, \ t->name); \ printf("\t﹂" reason); \ }) #define ut_prfail_errno(t, desc) ut_prfail(t, desc ": %s\n", strerror(errno)); /** * 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(t, reason...) \ ut_prfail(t, "%s:%d: ", __FILE__, __LINE__); \ printf(reason); \ printf("\n"); \ t->result = UTEST_FAILURE; \ return; /** * 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(t, a, b) \ ({ \ if ((a) != (b)) { \ FAIL_TEST(t, "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(t, a, b) \ ({ \ if ((a) == (b)) { \ FAIL_TEST(t, "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(t, cond, reason...) \ ({ \ if (!(cond)) { \ FAIL_TEST(t, reason); \ } \ }) [[noreturn]] static void ut_run_child(struct utest *t) { if (t->pre_run) t->pre_run(); printf(" [⏳]\t%s: " TBLU "running..." TRST, t->name); fflush(stdout); t->result = UTEST_SUCCESS; struct timespec ts[2]; clock_gettime(CLOCK_MONOTONIC, &ts[0]); t->run(t); clock_gettime(CLOCK_MONOTONIC, &ts[1]); ts[1].tv_sec -= ts[1].tv_sec; if (ts[1].tv_nsec < ts[0].tv_nsec) { ts[1].tv_nsec = ts[0].tv_nsec - ts[1].tv_nsec; ts[1].tv_sec -= 1; } else { ts[1].tv_nsec = ts[1].tv_nsec - ts[0].tv_nsec; } if (t->result == UTEST_SUCCESS) printf(TDEL " [" TBLD TGRN "✓" TRST "]\t%s: " TBLD TGRN "PASSED!" TRST ": ran %lu.%09lus\n", t->name, ts[1].tv_sec, ts[1].tv_nsec); if (t->post_run) t->post_run(); exit(t->result ? EXIT_FAILURE : EXIT_SUCCESS); } static enum utest_result ut_run_test(struct utest *t) { if (t->timeout_s && sigprocmask(SIG_BLOCK, &sset, NULL) < 0) { ut_prfail_errno(t, "sigprocmask"); return UTEST_FAILURE; } auto pid = fork(); if (pid < 0) { ut_prfail_errno(t, "fork"); return UTEST_FAILURE; } /* Child, the test being run */ if (pid == 0) ut_run_child(t); siginfo_t si = {0}; if (t->timeout_s) { struct timespec ts = { .tv_sec = t->timeout_s, .tv_nsec = 0, }; auto wres = sigtimedwait(&sset, &si, &ts); if (sigprocmask(SIG_UNBLOCK, &sset, NULL) < 0) { ut_prfail_errno(t, "sigprocmask"); return UTEST_FAILURE; } if (wres < 0) { kill(pid, SIGKILL); if (errno == EAGAIN) { ut_prfail(t, "test timed out after %d seconds\n", t->timeout_s); waitpid(pid, NULL, 0); return UTEST_TIMEOUT; } ut_prfail_errno(t, "sigtimedwait"); wait(NULL); return UTEST_FAILURE; } wait(NULL); } else if (waitid(P_PID, pid, &si, WEXITED | WSTOPPED) < 0) { ut_prfail_errno(t, "waitid"); return UTEST_FAILURE; } switch (si.si_code) { case CLD_EXITED: return si.si_status ? UTEST_FAILURE : UTEST_SUCCESS; case CLD_KILLED: case CLD_DUMPED: case CLD_STOPPED: auto signum = si.si_status; ut_prfail(t, "test terminated abruptly: %s\n", strsignal(signum)); return UTEST_FAILURE; } kill(pid, SIGKILL); ut_prfail(t, "unexpected test child process condition\n"); wait(NULL); return UTEST_FAILURE; } static inline int ut_run_tests(const char *const unit_name, size_t tests_cnt, utest tests[tests_cnt]) { if (sigemptyset(&sset) < 0) { perror("sigemptyset"); return -1; } if (sigaddset(&sset, SIGCHLD) < 0) { perror("sigaddset"); return -1; } size_t passed = 0, total = 0, failed = 0; if (ut_init) ut_init(); printf("[***]\t" TBLD "running:" TRST TBLU " %s " TRST "tests\n", unit_name); for (size_t i = 0; i < tests_cnt; i++) { !ut_run_test(tests + i) ? passed++ : 0; total++; } 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 (ut_cleanup) ut_cleanup(); return failed; } /** * RUN_TEST_SUITE - runs the @tests given as arguments. * @init_fn: initialization function. * @cleanup_fn: cleanup function. * @tests: tests to run (utest[]). * * @init_fn and @destroy_fn are used to initialize and cleanup test-related data * respectively. */ #define RUN_TEST_SUITE(init_fn, cleanup_fn, tests) \ int main(void) \ { \ ut_init = init_fn; \ ut_cleanup = cleanup_fn; \ return ut_run_tests(__FILE__, ARRAY_SIZE(tests), tests); \ } /** * RUN_TESTS - runs the @tests given as arguments. * @tests: tests to run (utest[]). * * Same as RUN_TEST_SUITE, but without initialization and cleanup functions */ #define RUN_TESTS(tests) RUN_TEST_SUITE(NULL, NULL, tests) #endif /* UTEST_H */