| 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 | |
| 12 | typedef void (*test_fn_t)(void); |
| 13 | struct test_context; |
| 14 | |
| 15 | enum test_cmdline_entry { |
| 16 | TEST_CMDLINE_ENTRY_ENABLED, |
| 17 | TEST_CMDLINE_ENTRY_MAX, |
| 18 | }; |
| 19 | |
| 20 | enum test_tier { |
| 21 | TEST_TIER_SMOKE, |
| 22 | TEST_TIER_UNIT, |
| 23 | TEST_TIER_INTEGRATION, |
| 24 | TEST_TIER_MAX, |
| 25 | }; |
| 26 | |
| 27 | enum test_options { |
| 28 | TEST_OPTION_NONE = 0, /* These are passed from cmdline */ |
| 29 | }; |
| 30 | |
| 31 | /* These are immutable properties of the group */ |
| 32 | enum test_group_flags { |
| 33 | TEST_GROUP_FLAG_NONE = 0, |
| 34 | TEST_GROUP_FLAG_DEFAULT = 1, /* orphans get adopted by this group */ |
| 35 | }; |
| 36 | |
| 37 | enum test_result { |
| 38 | TEST_RESULT_OK, |
| 39 | TEST_RESULT_FAILED, |
| 40 | TEST_RESULT_SKIPPED, |
| 41 | TEST_RESULT_MAX, |
| 42 | }; |
| 43 | |
| 44 | enum 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 | |
| 51 | enum 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 | |
| 59 | struct 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 | |
| 79 | struct 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 */ |
| 132 | struct 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 | |
| 143 | struct 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 | |
| 149 | struct 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 | |
| 276 | void tests_run(void); |
| 277 | void tests_hook_boot(); |
| 278 | CMDLINE_ENTRY_DEFINE(test_root); |
| 279 | |
| 280 | extern struct test_globals test_global; |
| 281 | extern const char *large_test_string; |
| 282 | extern struct test_group test_group_orphan_parent; |
| 283 | extern struct cmdline_entry test_cmdline_default; |
| 284 | |
| 285 | static inline size_t test_current_message_count(void) { |
| 286 | return log_site_message_count(site: test_global.current_test->site); |
| 287 | } |
| 288 | |
| 289 | static 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 | |
| 298 | static 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 | |
| 307 | static inline const char * |
| 308 | test_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 | |