| 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 <math/fixed.h> |
| 9 | #include <mem/alloc.h> |
| 10 | #include <mem/pmm.h> |
| 11 | #include <scaled_param.h> |
| 12 | #include <stdbool.h> |
| 13 | |
| 14 | typedef void (*test_fn_t)(void); |
| 15 | struct test_context; |
| 16 | |
| 17 | enum test_tier { |
| 18 | TEST_TIER_SMOKE, |
| 19 | TEST_TIER_UNIT, |
| 20 | TEST_TIER_INTEGRATION, |
| 21 | TEST_TIER_MAX, |
| 22 | }; |
| 23 | |
| 24 | enum test_options { |
| 25 | TEST_OPTION_NONE = 0, /* These are passed from cmdline */ |
| 26 | }; |
| 27 | |
| 28 | /* These are immutable properties of the group */ |
| 29 | enum test_group_flags { |
| 30 | TEST_GROUP_FLAG_NONE = 0, |
| 31 | TEST_GROUP_FLAG_DEFAULT = 1, /* orphans get adopted by this group */ |
| 32 | }; |
| 33 | |
| 34 | enum test_result { |
| 35 | TEST_RESULT_OK, |
| 36 | TEST_RESULT_FAILED, |
| 37 | TEST_RESULT_SKIPPED, |
| 38 | TEST_RESULT_MAX, |
| 39 | }; |
| 40 | |
| 41 | enum test_flags { |
| 42 | TEST_FLAG_NONE = 0, |
| 43 | |
| 44 | /* Test flags will be structured in what they *enable* or honor/respect */ |
| 45 | TEST_FLAG_HONORS_INTENSITY = 1, |
| 46 | |
| 47 | /* If this is enabled, TEST_FLAG_HONORS_INTENSITY is auto-enabled */ |
| 48 | TEST_FLAG_INHERITS_INTENSITY = 1 << 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, /* not the same as .enabled, just that the test |
| 57 | * won't be getting run for some reason */ |
| 58 | }; |
| 59 | |
| 60 | enum test_state : uint8_t { |
| 61 | TEST_STATE_DISABLED = 0, |
| 62 | TEST_STATE_ENABLED = 1, |
| 63 | TEST_STATE_SENTINEL = 0xFF, |
| 64 | }; |
| 65 | |
| 66 | struct test_group { |
| 67 | const char *name; |
| 68 | const char *fname; |
| 69 | uint32_t line; |
| 70 | const enum test_group_flags flags; |
| 71 | struct test_verdict (*setup)(struct test_context *); |
| 72 | struct test_verdict (*teardown)(struct test_context *); |
| 73 | |
| 74 | fx32_32_t default_intensity; |
| 75 | |
| 76 | /* This is given to all children that TEST_FLAG_INHERITS_INTENSITY */ |
| 77 | struct scaled_param intensity_desc; |
| 78 | |
| 79 | enum test_state enabled; |
| 80 | union { |
| 81 | enum test_state tier_enabled[TEST_TIER_MAX]; |
| 82 | struct { |
| 83 | enum test_state smoke_enabled; |
| 84 | enum test_state unit_enabled; |
| 85 | enum test_state integration_enabled; |
| 86 | }; |
| 87 | }; |
| 88 | |
| 89 | bool incremental; /* First run smoke, then unit, then integration */ |
| 90 | bool exit_on_fail; /* Different from incremental: exits after one failure, |
| 91 | * whereas incremental still completes the tier */ |
| 92 | |
| 93 | /* [tier][num] */ |
| 94 | struct test **tests[TEST_TIER_MAX]; |
| 95 | size_t num_tests[TEST_TIER_MAX]; |
| 96 | size_t num_tests_enabled[TEST_TIER_MAX]; |
| 97 | }; |
| 98 | |
| 99 | struct test { |
| 100 | const char *name; |
| 101 | const char *fname; |
| 102 | uint32_t line; |
| 103 | |
| 104 | struct test_verdict (*func)(struct test_context *ctx); |
| 105 | |
| 106 | const struct test_group *group; |
| 107 | enum test_tier tier; |
| 108 | |
| 109 | size_t msg_cap; |
| 110 | enum test_flags flags; |
| 111 | |
| 112 | time_ms_t duration_ms; |
| 113 | |
| 114 | /* A lot of these have to be full bools for the cmdline parse */ |
| 115 | bool print_logs; /* Prints logs *as* they are logged */ |
| 116 | enum test_state enabled; /* If this is set to TEST_SENTINEL, |
| 117 | * it means we default */ |
| 118 | bool keep_going; /* This basically means that with run_times, |
| 119 | * if it fails once, don't bother, and keep going |
| 120 | * until it runs run_times, however, it is overridden |
| 121 | * by exit_on_fail from the test_group */ |
| 122 | |
| 123 | fx32_32_t intensity; /* also [0, 1]. the point of this one is that |
| 124 | * it can be set by the command line, and the |
| 125 | * context reads and passes this into the test. |
| 126 | * |
| 127 | * the reason for this indirection is that the |
| 128 | * struct test_context that tests get might be |
| 129 | * changed by other subsystems that may not |
| 130 | * necessarily keep the same .intensity as the |
| 131 | * boot time command line option/default */ |
| 132 | |
| 133 | struct scaled_param intensity_desc; |
| 134 | |
| 135 | uint64_t seed; /* Can be passed in through cmdline, NOTE: |
| 136 | * if a seed is set through the cmdline, and |
| 137 | * run_times > 1, this seed will be used in every |
| 138 | * run during run_times */ |
| 139 | |
| 140 | size_t run_times; |
| 141 | |
| 142 | size_t inject_count; |
| 143 | struct inject_site *inject[]; |
| 144 | }; |
| 145 | |
| 146 | /* The reason this exists is because the actual structures in struct |
| 147 | * test are not meant to be accessed from inside the test's func, |
| 148 | * and this is the way the test interacts with the variables. |
| 149 | * |
| 150 | * This might make one think "why not just interact with struct test?", |
| 151 | * and the answer is that struct test_context gives us extensibility, |
| 152 | * and allows a test to get run numerous different times with varied |
| 153 | * contexts, seeds, etc. without having to fiddle around with struct test */ |
| 154 | struct test_context { |
| 155 | struct log_site *site; |
| 156 | struct log_handle handle; |
| 157 | |
| 158 | uint32_t soft_fails; |
| 159 | |
| 160 | fx32_32_t intensity; /* Raw [0, 1] fixed-point intensity */ |
| 161 | size_t intensity_val; /* Pre-computed scaled integer based on SCALE */ |
| 162 | time_ms_t duration_ms; |
| 163 | |
| 164 | uint64_t seed; |
| 165 | }; |
| 166 | |
| 167 | struct test_verdict { |
| 168 | enum test_result result; |
| 169 | enum test_skip_reason skip_reason; |
| 170 | const char *msg; /* optional, e.g. the failed assertion */ |
| 171 | }; |
| 172 | |
| 173 | #define TEST_EXIT_OK 0 |
| 174 | #define TEST_EXIT_FAIL 1 |
| 175 | #define TEST_INTENSITY_SENTINEL ((fx32_32_t) - 1LL) |
| 176 | #define TEST_INTENSITY_DEFAULT FX(0.5) |
| 177 | |
| 178 | /* Intended for use with TEST_FLAG_INHERITS_INTENSITY, so that tests can still |
| 179 | * set custom min, def, max outside of what they would inherit from group */ |
| 180 | #define TEST_INTENSITY(min, def, max) \ |
| 181 | .flags = TEST_FLAG_INHERITS_INTENSITY, \ |
| 182 | .intensity_desc = {.min_val = (min), .def_val = (def), .max_val = (max)} |
| 183 | #define TEST_INTENSITY_DESC_SENTINEL \ |
| 184 | (struct scaled_param) { \ |
| 185 | .min_val = SIZE_MAX, .def_val = SIZE_MAX, .max_val = SIZE_MAX \ |
| 186 | } |
| 187 | |
| 188 | struct test_globals { |
| 189 | fx32_32_t global_intensity; |
| 190 | size_t total_tests_enabled; |
| 191 | size_t results[TEST_TIER_MAX][TEST_RESULT_MAX]; |
| 192 | size_t results_agg[TEST_RESULT_MAX]; |
| 193 | struct test_context *current_test; |
| 194 | time_ms_t total_time; |
| 195 | bool show_output; |
| 196 | bool group_opt_in; |
| 197 | bool test_opt_in; |
| 198 | bool no_exit; |
| 199 | bool no_progress; |
| 200 | }; |
| 201 | |
| 202 | #define TEST_GROUP_NONE test_group_orphan_parent |
| 203 | |
| 204 | /* Goofy macros needed for the DECLARE macro */ |
| 205 | #define __test_group_TEST_GROUP_NONE test_group_orphan_parent |
| 206 | #define __test_group_none test_group_orphan_parent |
| 207 | #define __test_group_orphan test_group_orphan_parent |
| 208 | #define __test_group_test_group_orphan_parent test_group_orphan_parent |
| 209 | |
| 210 | #define TEST_GROUP(name) &(__test_group_##name) |
| 211 | #define TEST_GROUP_DEFINE(name) extern struct test_group __test_group_##name |
| 212 | |
| 213 | #define TEST(grp, id) __test_##grp##_##id |
| 214 | #define TEST_DECLARE(grp, id, ...) \ |
| 215 | static struct test_verdict __test_fn_##grp##_##id( \ |
| 216 | struct test_context *ctx); \ |
| 217 | extern struct test_group __test_group_##grp; \ |
| 218 | extern struct test __test_##grp##_##id; \ |
| 219 | LINKER_SECTION_OBJECT(struct test, tests) \ |
| 220 | __test_##grp##_##id = {.name = #id, \ |
| 221 | .fname = __RELFILE__, \ |
| 222 | .line = __LINE__, \ |
| 223 | .func = __test_fn_##grp##_##id, \ |
| 224 | .run_times = 1, \ |
| 225 | .group = TEST_GROUP(grp), \ |
| 226 | .enabled = TEST_STATE_SENTINEL, \ |
| 227 | .inject_count = 0, \ |
| 228 | .msg_cap = 0, \ |
| 229 | .seed = 0, \ |
| 230 | .print_logs = false, \ |
| 231 | .tier = TEST_TIER_UNIT, \ |
| 232 | .intensity = TEST_INTENSITY_SENTINEL, \ |
| 233 | .intensity_desc = TEST_INTENSITY_DESC_SENTINEL, \ |
| 234 | ##__VA_ARGS__}; \ |
| 235 | \ |
| 236 | static struct test_verdict __test_fn_##grp##_##id( \ |
| 237 | struct test_context *ctx __unused) |
| 238 | |
| 239 | #define TEST_DECLARE_SMOKE(grp, id, ...) \ |
| 240 | TEST_DECLARE(grp, id, .tier = TEST_TIER_SMOKE, ##__VA_ARGS__) |
| 241 | #define TEST_DECLARE_UNIT(grp, id, ...) \ |
| 242 | TEST_DECLARE(grp, id, .tier = TEST_TIER_UNIT, ##__VA_ARGS__) |
| 243 | #define TEST_DECLARE_INTEGRATION(grp, id, ...) \ |
| 244 | TEST_DECLARE(grp, id, .tier = TEST_TIER_INTEGRATION, ##__VA_ARGS__) |
| 245 | |
| 246 | #define TEST_GROUP_DECLARE(n, ...) \ |
| 247 | extern struct test_group __test_group_##n; \ |
| 248 | LINKER_SECTION_OBJECT(struct test_group, test_groups) \ |
| 249 | __test_group_##n = {.name = #n, \ |
| 250 | .incremental = false, \ |
| 251 | .exit_on_fail = false, \ |
| 252 | .fname = __RELFILE__, \ |
| 253 | .line = __LINE__, \ |
| 254 | .enabled = TEST_STATE_SENTINEL, \ |
| 255 | .smoke_enabled = TEST_STATE_SENTINEL, \ |
| 256 | .unit_enabled = TEST_STATE_SENTINEL, \ |
| 257 | .integration_enabled = TEST_STATE_SENTINEL, \ |
| 258 | .default_intensity = TEST_INTENSITY_SENTINEL, \ |
| 259 | .intensity_desc = TEST_INTENSITY_DESC_SENTINEL, \ |
| 260 | ##__VA_ARGS__} |
| 261 | |
| 262 | /* 1D piecewise-log */ |
| 263 | #define TEST_INTENSITY_LOG(min, def, max, unit_str) \ |
| 264 | .flags = TEST_FLAG_HONORS_INTENSITY, .intensity_desc = { \ |
| 265 | .curve = SCALE_PIECEWISE_LOG, \ |
| 266 | .min_val = (min), \ |
| 267 | .def_val = (def), \ |
| 268 | .max_val = (max), \ |
| 269 | .unit = (unit_str), \ |
| 270 | } |
| 271 | |
| 272 | /* Linear scaling */ |
| 273 | #define TEST_INTENSITY_LINEAR(min, def, max, unit_str) \ |
| 274 | .flags = TEST_FLAG_HONORS_INTENSITY, .intensity_desc = { \ |
| 275 | .curve = SCALE_PIECEWISE_LINEAR, \ |
| 276 | .min_val = (min), \ |
| 277 | .def_val = (def), \ |
| 278 | .max_val = (max), \ |
| 279 | .unit = (unit_str), \ |
| 280 | } |
| 281 | |
| 282 | /* Core-scaled thread counts (threads = base * core_count) */ |
| 283 | #define TEST_INTENSITY_CORES(min_per_core, def_per_core, max_per_core, \ |
| 284 | unit_str) \ |
| 285 | .flags = TEST_FLAG_HONORS_INTENSITY, .intensity_desc = { \ |
| 286 | .curve = SCALE_CORE_MULTIPLIER, \ |
| 287 | .min_val = (min_per_core), \ |
| 288 | .def_val = (def_per_core), \ |
| 289 | .max_val = (max_per_core), \ |
| 290 | .unit = (unit_str), \ |
| 291 | } |
| 292 | |
| 293 | /* set scale with min and max */ |
| 294 | #define TEST_INTENSITY_CUSTOM_PRINT(scale, min, def, max, unit_str, print_fn) \ |
| 295 | .flags = TEST_FLAG_HONORS_INTENSITY, .intensity_desc = { \ |
| 296 | .curve = scale, \ |
| 297 | .min_val = (min), \ |
| 298 | .def_val = (def), \ |
| 299 | .max_val = (max), \ |
| 300 | .unit = (unit_str), \ |
| 301 | .custom_print = (print_fn), \ |
| 302 | } |
| 303 | |
| 304 | /* Entirely custom */ |
| 305 | #define TEST_INTENSITY_CUSTOM(scale_fn, print_fn) \ |
| 306 | .flags = TEST_FLAG_HONORS_INTENSITY, .intensity_desc = { \ |
| 307 | .curve = SCALE_CUSTOM, \ |
| 308 | .custom_scale = (scale_fn), \ |
| 309 | .custom_print = (print_fn), \ |
| 310 | } |
| 311 | |
| 312 | #define TEST_SUCCESS ((struct test_verdict) {.result = TEST_RESULT_OK}) |
| 313 | #define TEST_FAIL(m) \ |
| 314 | ((struct test_verdict) {.result = TEST_RESULT_FAILED, .msg = (m)}) |
| 315 | #define TEST_SKIP(r) \ |
| 316 | ((struct test_verdict) {.result = TEST_RESULT_SKIPPED, .skip_reason = (r)}) |
| 317 | |
| 318 | #define TEST_ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) |
| 319 | |
| 320 | #define test_log(lvl, fmt, ...) \ |
| 321 | log(test_global.current_test->site, &test_global.current_test->handle, \ |
| 322 | lvl, fmt, ##__VA_ARGS__) |
| 323 | |
| 324 | #define test_err(fmt, ...) test_log(LOG_ERROR, fmt, ##__VA_ARGS__) |
| 325 | #define test_warn(fmt, ...) test_log(LOG_WARN, fmt, ##__VA_ARGS__) |
| 326 | #define test_info(fmt, ...) test_log(LOG_INFO, fmt, ##__VA_ARGS__) |
| 327 | #define test_debug(fmt, ...) test_log(LOG_DEBUG, fmt, ##__VA_ARGS__) |
| 328 | #define test_trace(fmt, ...) test_log(LOG_TRACE, fmt, ##__VA_ARGS__) |
| 329 | |
| 330 | #include <test/assert.h> |
| 331 | |
| 332 | #define ABORT_IF_RAM_LOW() \ |
| 333 | if (pmm_get_usable_ram() < 1024 * 1024 * 8) { \ |
| 334 | test_info("RAM too low for test to continue!\n"); \ |
| 335 | return TEST_SKIP(TEST_SKIP_RAM_LOW); \ |
| 336 | } |
| 337 | |
| 338 | void tests_run(void); |
| 339 | CMDLINE_DEFINE(test_root); |
| 340 | |
| 341 | extern struct test_globals test_global; |
| 342 | extern const char *large_test_string; |
| 343 | extern struct test_group test_group_orphan_parent; |
| 344 | |
| 345 | static inline size_t test_current_message_count(void) { |
| 346 | return log_site_message_count(site: test_global.current_test->site); |
| 347 | } |
| 348 | |
| 349 | static inline const char *test_tier_to_str(enum test_tier tier) { |
| 350 | switch (tier) { |
| 351 | case TEST_TIER_SMOKE: return "smoke" ; |
| 352 | case TEST_TIER_UNIT: return "unit" ; |
| 353 | case TEST_TIER_INTEGRATION: return "integration" ; |
| 354 | default: unreachable(); |
| 355 | } |
| 356 | } |
| 357 | |
| 358 | static inline const char *test_tier_to_str_color(enum test_tier tier) { |
| 359 | switch (tier) { |
| 360 | case TEST_TIER_SMOKE: return ANSI_GRAY "smoke" ; |
| 361 | case TEST_TIER_UNIT: return ANSI_MAGENTA "unit" ; |
| 362 | case TEST_TIER_INTEGRATION: return ANSI_YELLOW "integration" ; |
| 363 | default: unreachable(); |
| 364 | } |
| 365 | } |
| 366 | |
| 367 | static inline const char *test_result_to_str(enum test_result result) { |
| 368 | switch (result) { |
| 369 | case TEST_RESULT_OK: return ANSI_BLUE "ok" ANSI_RESET; |
| 370 | case TEST_RESULT_FAILED: return ANSI_RED "failed" ANSI_RESET; |
| 371 | case TEST_RESULT_SKIPPED: return ANSI_GRAY "skipped" ANSI_RESET; |
| 372 | default: unreachable(); |
| 373 | } |
| 374 | } |
| 375 | |
| 376 | static inline const char *test_result_plain(enum test_result result) { |
| 377 | switch (result) { |
| 378 | case TEST_RESULT_OK: return "ok" ; |
| 379 | case TEST_RESULT_FAILED: return "failed" ; |
| 380 | case TEST_RESULT_SKIPPED: return "skipped" ; |
| 381 | default: return "unknown" ; |
| 382 | } |
| 383 | } |
| 384 | |
| 385 | static inline const char * |
| 386 | test_skip_reason_to_str(enum test_skip_reason reason) { |
| 387 | switch (reason) { |
| 388 | case TEST_SKIP_NONE: return "none" ; |
| 389 | case TEST_SKIP_RAM_LOW: return "RAM low" ; |
| 390 | case TEST_SKIP_UPSTREAM_FAILED: return "upstream failed" ; |
| 391 | case TEST_SKIP_DISABLED: return "disabled" ; |
| 392 | default: unreachable(); |
| 393 | } |
| 394 | } |
| 395 | |