MULE 0.0.1
Multi-Use Library Essentials
Loading...
Searching...
No Matches
test.h
1#pragma once
2/**
3 * @defgroup mod_test Test
4 * @brief A small unit-test harness that catches leaks, hangs and
5 * out-of-memory bugs.
6 *
7 * A test file is a program. Include this header first, then the `.c` files
8 * under test, then write tests and a main that runs them:
9 *
10 * @code
11 * #include "test/test.h"
12 *
13 * #include "log/log.c"
14 * #include "mem/mem.c"
15 * #include "str/str.c"
16 *
17 * TEST(appends) {
18 * Str s = { 0 };
19 * REQUIRE(str_append(&s, SV("mule")));
20 * CHECK_EQ_STR(str_cstr(&s), "mule");
21 * str_free(&s);
22 * }
23 *
24 * int main(int argc, char **argv) {
25 * test_init(argc, argv);
26 * RUN(appends);
27 * return TEST_REPORT();
28 * }
29 * @endcode
30 *
31 * - CHECK records a failure and carries on. REQUIRE records it and returns
32 * from the test, for when the rest of it would be meaningless.
33 * - Every file included after this one has malloc, calloc, realloc and free
34 * redirected to counting wrappers. RUN fails a test that ends with more
35 * live allocations than it started with, so leaks fail on every platform.
36 * - test_fail_alloc_after() makes allocation fail on demand, so
37 * out-of-memory paths can be tested deterministically.
38 * - A test still running after #TEST_TIMEOUT_S seconds is reported as
39 * `[HANG]` and fails the run.
40 * - Pass test names as arguments to run only those tests.
41 * - Results are coloured when stdout supports it, as log_color_enabled()
42 * decides. The harness needs the log module, and log needs mem and str.
43 *
44 * Every test in tests/ is written this way, so they are the best place to
45 * see the harness and the library in use.
46 * @{
47 */
48#include "base/base.h" /* first: it sets the POSIX feature macros */
49#include "log/log.h"
50
51#include <signal.h>
52#include <stdio.h>
53#include <stdlib.h>
54#include <string.h>
55#include <unistd.h>
56
57#ifndef TEST_TIMEOUT_S
58/** Seconds before a running test counts as hung. Define it before the
59 * include to change it. */
60#define TEST_TIMEOUT_S 10
61#endif
62
63/** @cond */
64static int test_failures_ = 0;
65static int test_checks_ = 0;
66static int test_cur_failed_ = 0;
67static int test_argc_ = 0;
68static char **test_argv_ = NULL;
69static const char *test_running_ = "";
70static bool test_color_ = false; /* set once by test_init */
71
72/* --- Allocator hooks ----------------------------------------------------- */
73
74static long test_live_allocs_ = 0;
75static long test_alloc_budget_ = -1; /* allocations left; -1 means no limit */
76
77/** @endcond */
78
79/** Let the next @p n allocations succeed and fail every one after. Pass -1
80 * to stop failing. RUN resets it after each test. */
81MULE_UNUSED static void test_fail_alloc_after(long n) {
82 test_alloc_budget_ = n;
83}
84
85/** @cond */
86
87static bool test_alloc_allowed_(void) {
88 if (test_alloc_budget_ == 0) return false;
89 if (test_alloc_budget_ > 0) test_alloc_budget_--;
90 return true;
91}
92
93MULE_UNUSED static void *test_malloc_(size_t n) {
94 if (!test_alloc_allowed_()) return NULL;
95 void *p = malloc(n);
96 if (p != NULL) test_live_allocs_++;
97 return p;
98}
99
100MULE_UNUSED static void *test_calloc_(size_t count, size_t n) {
101 if (!test_alloc_allowed_()) return NULL;
102 void *p = calloc(count, n);
103 if (p != NULL) test_live_allocs_++;
104 return p;
105}
106
107MULE_UNUSED static void *test_realloc_(void *old, size_t n) {
108 if (!test_alloc_allowed_()) return NULL;
109 void *p = realloc(old, n);
110 if (p != NULL && old == NULL) test_live_allocs_++;
111 return p;
112}
113
114MULE_UNUSED static void test_free_(void *p) {
115 if (p != NULL) test_live_allocs_--;
116 free(p);
117}
118
119#define malloc test_malloc_
120#define calloc test_calloc_
121#define realloc test_realloc_
122#define free test_free_
123
124/* --- Checks -------------------------------------------------------------- */
125
126static void test_fail_at_(const char *file, int line) {
127 test_cur_failed_ = 1;
128 if (test_color_)
129 printf(" " LOG_RED "%s:%d:" LOG_RESET " ", file, line);
130 else
131 printf(" %s:%d: ", file, line);
132}
133
134/* Print bytes as a C string literal, escaping anything unprintable. */
135static void test_print_bytes_(const char *p, size_t n) {
136 if (p == NULL) {
137 printf("null");
138 return;
139 }
140 putchar('"');
141 for (size_t i = 0; i < n; i++) {
142 unsigned char c = (unsigned char)p[i];
143 if (c >= 0x20 && c < 0x7f && c != '"' && c != '\\')
144 putchar(c);
145 else
146 printf("\\x%02x", c);
147 }
148 putchar('"');
149}
150
151MULE_UNUSED static bool test_check_(bool ok, const char *file, int line,
152 const char *expr) {
153 test_checks_++;
154 if (!ok) {
155 test_fail_at_(file, line);
156 printf("CHECK(%s) failed\n", expr);
157 }
158 return ok;
159}
160
161MULE_UNUSED static bool test_eq_int_(long long a, long long b, const char *file,
162 int line, const char *ea, const char *eb) {
163 test_checks_++;
164 if (a != b) {
165 test_fail_at_(file, line);
166 printf("%s == %s failed (%lld vs %lld)\n", ea, eb, a, b);
167 }
168 return a == b;
169}
170
171MULE_UNUSED static bool test_eq_str_(const char *a, const char *b,
172 const char *file, int line, const char *ea,
173 const char *eb) {
174 bool ok = (a == NULL || b == NULL) ? a == b : strcmp(a, b) == 0;
175 test_checks_++;
176 if (!ok) {
177 test_fail_at_(file, line);
178 printf("%s == %s failed (", ea, eb);
179 test_print_bytes_(a, a ? strlen(a) : 0);
180 printf(" vs ");
181 test_print_bytes_(b, b ? strlen(b) : 0);
182 printf(")\n");
183 }
184 return ok;
185}
186
187MULE_UNUSED static bool test_eq_mem_(const char *a, size_t alen, const char *b,
188 size_t blen, const char *file, int line,
189 const char *ea, const char *eb) {
190 bool ok = alen == blen && (alen == 0 || memcmp(a, b, alen) == 0);
191 test_checks_++;
192 if (!ok) {
193 test_fail_at_(file, line);
194 printf("%s == %s failed (", ea, eb);
195 test_print_bytes_(a, alen);
196 printf(" vs ");
197 test_print_bytes_(b, blen);
198 printf(")\n");
199 }
200 return ok;
201}
202
203/** @endcond */
204
205/** Define a test. Run it from main with RUN. */
206#define TEST(name) static void name(void)
207
208/** Record a failure when @p cond is false, and carry on. */
209#define CHECK(cond) ((void)test_check_((cond), __FILE__, __LINE__, #cond))
210
211/** Record a failure when @p cond is false, and return from the test. */
212#define REQUIRE(cond) \
213 do { \
214 if (!test_check_((cond), __FILE__, __LINE__, #cond)) return; \
215 } while (0)
216
217/** CHECK that two integers are equal, printing both values when not. */
218#define CHECK_EQ_INT(a, b) \
219 ((void)test_eq_int_((long long)(a), (long long)(b), __FILE__, __LINE__, \
220 #a, #b))
221
222/** CHECK that two C strings are equal. Two nulls are equal, a null and a
223 * string are not. */
224#define CHECK_EQ_STR(a, b) \
225 ((void)test_eq_str_((a), (b), __FILE__, __LINE__, #a, #b))
226
227/** CHECK that two byte ranges have the same length and bytes, embedded NULs
228 * included. */
229#define CHECK_EQ_MEM(a, alen, b, blen) \
230 ((void)test_eq_mem_((a), (alen), (b), (blen), __FILE__, __LINE__, #a, #b))
231
232/* --- Running ------------------------------------------------------------- */
233
234/** @cond */
235/* SIGALRM handler, so only async-signal-safe calls: write and _exit. */
236static void test_timeout_(int sig) {
237 (void)sig;
238 static const char plain[] = "[HANG] ";
239 static const char color[] = LOG_YELLOW "[HANG]" LOG_RESET " ";
240 if (test_color_)
241 write(STDOUT_FILENO, color, sizeof color - 1);
242 else
243 write(STDOUT_FILENO, plain, sizeof plain - 1);
244 write(STDOUT_FILENO, test_running_, strlen(test_running_));
245 write(STDOUT_FILENO, "\n", 1);
246 _exit(EXIT_FAILURE);
247}
248
249/** @endcond */
250
251/** Start a test program. Call it first in main with main's arguments. */
252MULE_UNUSED static void test_init(int argc, char **argv) {
253 test_argc_ = argc;
254 test_argv_ = argv;
255 test_color_ = log_color_enabled(stdout);
256 signal(SIGALRM, test_timeout_);
257}
258
259/** @cond */
260static void test_print_result_(const char *name) {
261 const char *tag = test_cur_failed_ ? "FAIL" : " OK ";
262 if (test_color_)
263 printf("%s[%s]" LOG_RESET " %s\n",
264 test_cur_failed_ ? LOG_RED : LOG_GREEN, tag, name);
265 else
266 printf("[%s] %s\n", tag, name);
267}
268
269MULE_UNUSED static int test_report_(const char *file) {
270 const char *on = test_color_ ? (test_failures_ ? LOG_RED : LOG_GREEN) : "";
271 printf("%s%s: %d check(s), %d failing test(s)%s\n", on, file, test_checks_,
272 test_failures_, test_color_ ? LOG_RESET : "");
273 return test_failures_ != 0;
274}
275
276static bool test_selected_(const char *name) {
277 if (test_argc_ <= 1) return true;
278 for (int i = 1; i < test_argc_; i++) {
279 if (strcmp(test_argv_[i], name) == 0) return true;
280 }
281 return false;
282}
283
284/** @endcond */
285
286/** Run a test unless arguments name others, then print `[ OK ]` or `[FAIL]`.
287 * A leak or a hang fails the test too. */
288#define RUN(name) \
289 do { \
290 if (!test_selected_(#name)) break; \
291 long live_before_ = test_live_allocs_; \
292 test_cur_failed_ = 0; \
293 test_running_ = #name; \
294 alarm(TEST_TIMEOUT_S); \
295 name(); \
296 alarm(0); \
297 test_alloc_budget_ = -1; \
298 if (test_live_allocs_ != live_before_) { \
299 test_cur_failed_ = 1; \
300 printf(" %ld allocation(s) leaked\n", \
301 test_live_allocs_ - live_before_); \
302 } \
303 if (test_cur_failed_) test_failures_++; \
304 test_print_result_(#name); \
305 fflush(stdout); /* keep finished results if a later test hangs */ \
306 } while (0)
307
308/** Print the totals. Return it from main: it is nonzero when a test failed. */
309#define TEST_REPORT() test_report_(__FILE__)
310
311/** @} */
#define LOG_YELLOW
Yellow text.
Definition log.h:99
#define LOG_GREEN
Green text.
Definition log.h:98
#define LOG_RED
Red text.
Definition log.h:97
#define LOG_RESET
Back to the terminal's default style.
Definition log.h:100
bool log_color_enabled(FILE *f)
True when colour escapes written to f will render as colour.
Definition log.c:37