1/* @title: Tests */
2#pragma once
3#include <cmdline.h>
4#include <compiler.h>
5#include <console/printf.h>
6#include <errno.h>
7#include <log.h>
8#include <mem/alloc.h>
9#include <mem/pmm.h>
10#include <stdbool.h>
11
12typedef void (*test_fn_t)(void);
13struct test_context;
14
15enum test_cmdline_entry {
16 TEST_CMDLINE_ENTRY_ENABLED,
17 TEST_CMDLINE_ENTRY_MAX,
18};
19
20enum test_tier {
21 TEST_TIER_SMOKE,
22 TEST_TIER_UNIT,
23 TEST_TIER_INTEGRATION,
24 TEST_TIER_MAX,
25};
26
27enum test_options {
28 TEST_OPTION_NONE = 0, /* These are passed from cmdline */
29};
30
31/* These are immutable properties of the group */
32enum test_group_flags {
33 TEST_GROUP_FLAG_NONE = 0,
34 TEST_GROUP_FLAG_DEFAULT = 1, /* orphans get adopted by this group */
35};
36
37enum test_result {
38 TEST_RESULT_OK,
39 TEST_RESULT_FAILED,
40 TEST_RESULT_SKIPPED,
41 TEST_RESULT_MAX,
42};
43
44enum test_flags {
45 TEST_FLAG_NONE = 0,
46
47 /* Test flags will be structured in what they *enable* or honor/respect */
48 TEST_FLAG_HONORS_INTENSITY = 1,
49};
50
51enum test_skip_reason {
52 TEST_SKIP_NONE,
53 TEST_SKIP_RAM_LOW,
54 TEST_SKIP_UPSTREAM_FAILED, /* a prior tier in this group
55 failed */
56 TEST_SKIP_DISABLED, /* test.x=off */
57};
58
59struct test_group {
60 const char *name;
61 const enum test_group_flags flags;
62 struct cmdline_entry *ent;
63 struct test_verdict (*setup)(struct test_context *);
64 struct test_verdict (*teardown)(struct test_context *);
65
66 fx32_32_t default_intensity;
67
68 /* These all have to be bool for cmdline QOL */
69 bool enabled; /* Does anything run at all? */
70 bool incremental; /* First run smoke, then unit, then integration */
71 bool exit_on_fail; /* Different from incremental: exits after one failure,
72 * whereas incremental still completes the tier */
73
74 /* [tier][num] */
75 struct test **tests[TEST_TIER_MAX];
76 size_t num_tests[TEST_TIER_MAX];
77};
78
79struct test {
80 const char *name;
81
82 struct test_verdict (*func)(struct test_context *ctx);
83
84 const struct test_group *group;
85 enum test_tier tier;
86
87 size_t msg_cap;
88 enum test_flags flags;
89
90 time_t duration_ms;
91
92 /* A lot of these have to be full bools for the cmdline parse */
93 bool print_logs; /* Prints logs *as* they are logged */
94 bool enabled;
95 bool keep_going; /* This basically means that with run_times,
96 * if it fails once, don't bother, and keep going
97 * until it runs run_times, however, it is overridden
98 * by exit_on_fail from the test_group */
99
100 fx32_32_t intensity; /* also [0, 1]. the point of this one is that
101 * it can be set by the command line, and the
102 * context reads and passes this into the test.
103 *
104 * the reason for this indirection is that the
105 * struct test_context that tests get might be
106 * changed by other subsystems that may not
107 * necessarily keep the same .intensity as the
108 * boot time command line option/default */
109
110 uint64_t seed; /* Can be passed in through cmdline, NOTE:
111 * if a seed is set through the cmdline, and
112 * run_times > 1, this seed will be used in every
113 * run during run_times */
114
115 size_t run_times;
116
117 size_t inject_count;
118 struct cmdline_entry *base_entry;
119 struct cmdline_entry *cmdline_entries[TEST_CMDLINE_ENTRY_MAX];
120 struct inject_site *inject[];
121};
122#define TEST_SEED_SENTINEL 0
123
124/* The reason this exists is because the actual structures in struct
125 * test are not meant to be accessed from inside the test's func,
126 * and this is the way the test interacts with the variables.
127 *
128 * This might make one think "why not just interact with struct test?",
129 * and the answer is that struct test_context gives us extensibility,
130 * and allows a test to get run numerous different times with varied
131 * contexts, seeds, etc. without having to fiddle around with struct test */
132struct test_context {
133 struct log_site *site;
134 struct log_handle handle;
135
136 uint32_t soft_fails;
137 fx32_32_t intensity; /* [0, 1] */
138 time_t duration_ms;
139
140 uint64_t seed;
141};
142
143struct test_verdict {
144 enum test_result result;
145 enum test_skip_reason skip_reason;
146 const char *msg; /* optional, e.g. the failed assertion */
147};
148
149struct test_globals {
150 size_t results[TEST_TIER_MAX][TEST_RESULT_MAX];
151 size_t results_agg[TEST_RESULT_MAX];
152 struct test_context *current_test;
153 time_t total_time;
154};
155
156#define TEST(id) __test_##id
157#define TEST_DECLARE(id, ...) \
158 static struct test_verdict id(struct test_context *ctx); \
159 extern struct test __test_##id; \
160 CMDLINE_ENTRY_DECLARE(test_##id, .name = #id, \
161 .flags = CMDLINE_ENTRY_SYMBOLIC); \
162 CMDLINE_ENTRY_DECLARE_TYPED( \
163 test_##id##_enabled, __test_##id.enabled, .name = "enabled", \
164 .parent = CMDLINE_ENTRY(test_##id), .private = &__test_##id); \
165 CMDLINE_ENTRY_DECLARE_TYPED( \
166 test_##id##_intensity, __test_##id.intensity, .name = "intensity", \
167 .parent = CMDLINE_ENTRY(test_##id), .private = &__test_##id); \
168 CMDLINE_ENTRY_DECLARE_TYPED( \
169 test_##id##_run_times, __test_##id.run_times, .name = "run_times", \
170 .parent = CMDLINE_ENTRY(test_##id), .private = &__test_##id); \
171 CMDLINE_ENTRY_DECLARE_TYPED( \
172 test_##id##_seed, __test_##id.seed, .name = "seed", \
173 .parent = CMDLINE_ENTRY(test_##id), .private = &__test_##id); \
174 CMDLINE_ENTRY_DECLARE_TYPED( \
175 test_##id##_duration_ms, __test_##id.duration_ms, \
176 .name = "duration_ms", .parent = CMDLINE_ENTRY(test_##id), \
177 .private = &__test_##id); \
178 CMDLINE_ENTRY_DECLARE_TYPED( \
179 test_##id##_msg_cap, __test_##id.msg_cap, .name = "msg_cap", \
180 .parent = CMDLINE_ENTRY(test_##id), .private = &__test_##id); \
181 CMDLINE_ENTRY_DECLARE_TYPED( \
182 test_##id##_keep_going, __test_##id.keep_going, .name = "keep_going", \
183 .parent = CMDLINE_ENTRY(test_##id), .private = &__test_##id); \
184 CMDLINE_ENTRY_DECLARE_TYPED( \
185 test_##id##_print_logs, __test_##id.print_logs, .name = "print_logs", \
186 .parent = CMDLINE_ENTRY(test_##id), .private = &__test_##id); \
187 LINKER_SECTION_OBJECT(struct test, tests) \
188 __test_##id = {.name = #id, \
189 .func = id, \
190 .run_times = 1, \
191 .group = &test_group_orphan_parent, \
192 .enabled = true, \
193 .base_entry = CMDLINE_ENTRY(test_##id), \
194 .inject_count = 0, \
195 .msg_cap = 0, \
196 .seed = 0, \
197 .print_logs = false, \
198 .tier = TEST_TIER_UNIT, \
199 .cmdline_entries[TEST_CMDLINE_ENTRY_ENABLED] = \
200 CMDLINE_ENTRY(test_##id##_enabled), \
201 __VA_ARGS__}; \
202 \
203 static struct test_verdict id(struct test_context *ctx __unused)
204
205#define TEST_CMDLINE_ENTRY_DECLARE(id, var, n) \
206 CMDLINE_ENTRY_DECLARE_TYPED(test_##id##_##n, var, .name = #n, \
207 .parent = CMDLINE_ENTRY(test_##id), \
208 .private = &__test_##id)
209
210#define TEST_GROUP_DECLARE(n, ...) \
211 extern struct test_group __test_group_##n; \
212 CMDLINE_ENTRY_DECLARE(test_group_##n, .name = #n, \
213 .parent = CMDLINE_ENTRY(test_root), \
214 .flags = CMDLINE_ENTRY_SYMBOLIC) \
215 CMDLINE_ENTRY_DECLARE_TYPED(test_group_##n##_enabled, \
216 __test_group_##n.enabled, .name = "enabled", \
217 .parent = CMDLINE_ENTRY(test_group_##n), \
218 .private = &__test_group_##n); \
219 CMDLINE_ENTRY_DECLARE_TYPED( \
220 test_group_##n##_incremental, __test_group_##n.incremental, \
221 .name = "incremental", .parent = CMDLINE_ENTRY(test_group_##n), \
222 .private = &__test_group_##n); \
223 CMDLINE_ENTRY_DECLARE_TYPED( \
224 test_group_##n##_exit_on_fail, __test_group_##n.exit_on_fail, \
225 .name = "exit_on_fail", .parent = CMDLINE_ENTRY(test_group_##n), \
226 .private = &__test_group_##n); \
227 LINKER_SECTION_OBJECT(struct test_group, test_groups) \
228 __test_group_##n = {.name = #n, \
229 .incremental = false, \
230 .exit_on_fail = false, \
231 .enabled = true, \
232 __VA_ARGS__}
233
234#define TEST_GROUP(name) __test_group_##name
235
236#define TEST_SUCCESS ((struct test_verdict) {.result = TEST_RESULT_OK})
237#define TEST_FAIL(m) \
238 ((struct test_verdict) {.result = TEST_RESULT_FAILED, .msg = (m)})
239#define TEST_SKIP(r) \
240 ((struct test_verdict) {.result = TEST_RESULT_SKIPPED, .skip_reason = (r)})
241
242#define TEST_ASSERT(x) \
243 do { \
244 if (!(x)) { \
245 printf(" assert \"%s\" failed at %s:%d ", #x, __FILE__, __LINE__); \
246 return TEST_FAIL(#x); \
247 } \
248 } while (0)
249
250#define TEST_ASSERT_VOID(x) \
251 do { \
252 if (!(x)) { \
253 printf(" assert \"%s\" failed at %s:%d ", #x, __FILE__, __LINE__); \
254 return; \
255 } \
256 } while (0)
257
258#define test_log(lvl, fmt, ...) \
259 log(test_global.current_test->site, &test_global.current_test->handle, \
260 lvl, fmt, ##__VA_ARGS__)
261
262#define test_err(fmt, ...) test_log(LOG_ERROR, fmt, ##__VA_ARGS__)
263#define test_warn(fmt, ...) test_log(LOG_WARN, fmt, ##__VA_ARGS__)
264#define test_info(fmt, ...) test_log(LOG_INFO, fmt, ##__VA_ARGS__)
265#define test_debug(fmt, ...) test_log(LOG_DEBUG, fmt, ##__VA_ARGS__)
266#define test_trace(fmt, ...) test_log(LOG_TRACE, fmt, ##__VA_ARGS__)
267
268#define FAIL_IF_FATAL(op) TEST_ASSERT(!ERR_IS_FATAL(op))
269
270#define ABORT_IF_RAM_LOW() \
271 if (pmm_get_usable_ram() < 1024 * 1024 * 8) { \
272 test_info("RAM too low for test to continue!\n"); \
273 return TEST_SKIP(TEST_SKIP_RAM_LOW); \
274 }
275
276void tests_run(void);
277void tests_hook_boot();
278CMDLINE_ENTRY_DEFINE(test_root);
279
280extern struct test_globals test_global;
281extern const char *large_test_string;
282extern struct test_group test_group_orphan_parent;
283extern struct cmdline_entry test_cmdline_default;
284
285static inline size_t test_current_message_count(void) {
286 return log_site_message_count(site: test_global.current_test->site);
287}
288
289static inline const char *test_tier_to_str(enum test_tier tier) {
290 switch (tier) {
291 case TEST_TIER_SMOKE: return "smoke";
292 case TEST_TIER_UNIT: return "unit";
293 case TEST_TIER_INTEGRATION: return "integration";
294 default: kassert_unreachable();
295 }
296}
297
298static inline const char *test_result_to_str(enum test_result result) {
299 switch (result) {
300 case TEST_RESULT_OK: return ANSI_BLUE "ok" ANSI_RESET;
301 case TEST_RESULT_FAILED: return ANSI_RED "failed" ANSI_RESET;
302 case TEST_RESULT_SKIPPED: return ANSI_GRAY "skipped" ANSI_RESET;
303 default: kassert_unreachable();
304 }
305}
306
307static inline const char *
308test_skip_reason_to_str(enum test_skip_reason reason) {
309 switch (reason) {
310 case TEST_SKIP_NONE: return "none";
311 case TEST_SKIP_RAM_LOW: return "RAM low";
312 case TEST_SKIP_UPSTREAM_FAILED: return "upstream failed";
313 case TEST_SKIP_DISABLED: return "disabled";
314 default: kassert_unreachable();
315 }
316}
317