aboutsummaryrefslogtreecommitdiff
path: root/utest
diff options
context:
space:
mode:
authorYaroslav de la Peña Smirnov <yps@yaroslavps.com>2026-10-15 22:51:45 +0300
committerYaroslav de la Peña Smirnov <yps@yaroslavps.com>2026-10-15 22:51:45 +0300
commit662c7c620e6db915c5705bd0afa84925bb84704d (patch)
tree3179c8fc3a8c36b2461e9dc6572ac5812d3a6112 /utest
parent491216939bd9824cc0da6d7ca7d7108595cb691d (diff)
downloadc-wares-662c7c620e6db915c5705bd0afa84925bb84704d.tar.gz
c-wares-662c7c620e6db915c5705bd0afa84925bb84704d.zip
utest: v2, now with more isolationHEADmaster
* 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 the comments!
Diffstat (limited to 'utest')
-rw-r--r--utest/Makefile2
-rw-r--r--utest/utest-test.c40
-rw-r--r--utest/utest.h372
3 files changed, 269 insertions, 145 deletions
diff --git a/utest/Makefile b/utest/Makefile
index 0ecb1c5..3cecbd1 100644
--- a/utest/Makefile
+++ b/utest/Makefile
@@ -1,5 +1,5 @@
CC?=gcc
-CFLAGS+=-Wall -Werror -Wno-unused -std=gnu11 -Og -g
+CFLAGS+=-Wall -Werror -Wno-unused -std=c23 -Og -g
all:
$(CC) $(CFLAGS) -o utest-test utest-test.c
diff --git a/utest/utest-test.c b/utest/utest-test.c
index 591ee32..df7d51b 100644
--- a/utest/utest-test.c
+++ b/utest/utest-test.c
@@ -2,23 +2,28 @@
static int a, b;
-/* This should pass */
-TEST_BEGIN(test_pass)
+void test_pass_run(utest *t)
{
- asserteq(a, 0);
- assertneq(a, b);
- expect(b > a, "expected b > a");
- TEST_OUT
+ asserteq(t, a, 0);
+ assertneq(t, a, b);
+ expect(t, b > a, "expected b > a");
}
-TEST_END
-/* This should fail */
-TEST_BEGIN(test_fail)
+void test_fail_run(utest *t)
{
- expect(0, "oopsie!");
- TEST_OUT
+ expect(t, 0, "oopsie!");
+}
+
+void test_crash_run(utest *t)
+{
+ int *x = NULL;
+ *x = 8;
+}
+
+void test_timeout_run(utest *t)
+{
+ sleep(10);
}
-TEST_END
void test_init(void)
{
@@ -26,9 +31,16 @@ void test_init(void)
printf("hello, world!\n\n");
}
-void test_destroy(void)
+void test_cleanup(void)
{
printf("\ngood bye!\n");
}
-RUN_TEST_SUITE(test_init, test_destroy, test_pass, test_fail);
+utest tests[] = {
+ UTEST_INIT_S(test_pass, 0),
+ UTEST_INIT_S(test_fail, 0),
+ UTEST_INIT_S(test_crash, 0),
+ UTEST_INIT_S(test_timeout, 1),
+};
+
+RUN_TEST_SUITE(test_init, test_cleanup, tests);
diff --git a/utest/utest.h b/utest/utest.h
index 4222183..30dc229 100644
--- a/utest/utest.h
+++ b/utest/utest.h
@@ -1,26 +1,42 @@
/* SPDX-License-Identifier: LGPL-2.1 */
/**
- * utest.h - header-only unit testing micro "framework". v1.0.0
+ * utest.h - header-only unit testing micro "framework". v2.0.0
*
- * Copyright (c) 2021-2023 Yaroslav de la Peña Smirnov
+ * 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
-#include <stdarg.h>
+
+#ifndef _POSIX_C_SOURCE
+#define _POSIX_C_SOURCE 200809L
+#endif
+
+#include <errno.h>
+#include <signal.h>
+#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
+#include <string.h>
+#include <sys/wait.h>
#include <time.h>
+#include <unistd.h>
-/*
- * TODO?: test runners that run in different processes in order to not crash the
- * whole test unit when a single test crashes.
- */
-
-/**
- * Constants
- * =========
- */
+/* Copy this header and change the include path as needed */
+#include "../utils.h"
/**
* Terminal escape codes
@@ -48,51 +64,108 @@
#endif
#define TDEL "\33[2K\r"
+typedef struct utest utest;
+
/**
- * Typedefs
- * ========
+ * test_fn - A test function.
+ * @t: the test data; required for the macros.
*/
-
-typedef bool (*test_fn)(void);
+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");
+
/**
- * Internal variables
- * ==================
+ * 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 tests_init = NULL;
-static setup_fn tests_destroy = NULL;
+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, \
+ }
/**
- * Functions and function-like macros
- * ==================================
+ * 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(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
+#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(a, b) \
- ({ \
- if ((a) != (b)) { \
- FAIL_TEST("assertion " TBLD TBLU #a " == " #b TRST " failed"); \
- } \
+#define asserteq(t, a, b) \
+ ({ \
+ if ((a) != (b)) { \
+ FAIL_TEST(t, "assertion " TBLD TBLU #a " == " #b TRST " failed"); \
+ } \
})
/**
@@ -100,11 +173,11 @@ static setup_fn tests_destroy = NULL;
* @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"); \
- } \
+#define assertneq(t, a, b) \
+ ({ \
+ if ((a) == (b)) { \
+ FAIL_TEST(t, "assertion " TBLD TBLU #a " != " #b TRST " failed"); \
+ } \
})
/**
@@ -113,107 +186,146 @@ static setup_fn tests_destroy = NULL;
* @reason: a variable list of arguments, where the first argument is the format
* text.
*/
-#define expect(cond, reason...) \
- ({ \
- if (!(cond)) { \
- FAIL_TEST(reason); \
- } \
+#define expect(t, cond, reason...) \
+ ({ \
+ if (!(cond)) { \
+ FAIL_TEST(t, 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);
+[[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);
-/**
- * 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:;
+ 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;
+ }
-/**
- * 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; \
+ 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;
}
-#define BENCH_BEGIN(name) \
- int name(void) \
- { \
- const char *__this_name = #name; \
- struct timespec __t1, __t2; \
- clock_gettime(CLOCK_MONOTONIC, &__t1); \
- printf(" [⏳]\t%s: " TBLU "running..." TRST, __this_name); \
- fflush(stdout);
+ /* Child, the test being run */
+ if (pid == 0)
+ ut_run_child(t);
-#define BENCH_END \
- clock_gettime(CLOCK_MONOTONIC, &__t2); \
- __t2.tv_sec -= __t1.tv_sec; \
- if (__t2.tv_nsec < __t1.tv_nsec) { \
- __t2.tv_nsec = __t1.tv_nsec - __t2.tv_nsec; \
- __t2.tv_sec -= 1; \
- } else { \
- __t2.tv_nsec = __t2.tv_nsec - __t1.tv_nsec; \
- } \
- printf(TDEL " [" TBLD TGRN "✓" TRST "]\t%s: ran in\t" TBLD TBLU \
- "%lu.%09lus\n" TRST, \
- __this_name, __t2.tv_sec, __t2.tv_nsec); \
- return true; \
+ 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;
}
-/**
- * __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;
+ 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;
+}
- if (tests_init) {
- tests_init();
+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);
+ unit_name);
- va_start(args, unit_name);
- test = va_arg(args, test_fn);
- while (test) {
- test() ? passed++ : 0;
+ for (size_t i = 0; i < tests_cnt; i++) {
+ !ut_run_test(tests + i) ? 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);
+ "%lu" TRST ", passed: " TGRN "%lu" TRST ", failed: " TRED "%lu" TRST
+ "\n",
+ unit_name,
+ total,
+ passed,
+ failed);
- if (tests_destroy) {
- tests_destroy();
- }
+ if (ut_cleanup)
+ ut_cleanup();
return failed;
}
@@ -221,26 +333,26 @@ static inline int __run_tests(const char *const unit_name, ...)
/**
* RUN_TEST_SUITE - runs the @tests given as arguments.
* @init_fn: initialization function.
- * @destroy_fn: cleanup function.
- * @tests...: tests to run.
+ * @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, destroy_fn, tests...) \
- int main(void) \
- { \
- tests_init = init_fn; \
- tests_destroy = destroy_fn; \
- return __run_tests(__FILE__, tests, NULL); \
+#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.
+ * @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)
+#define RUN_TESTS(tests) RUN_TEST_SUITE(NULL, NULL, tests)
#endif /* UTEST_H */