| 1 | #include <colors.h> |
| 2 | #include <console/panic.h> |
| 3 | #include <console/printf.h> |
| 4 | #include <console/report.h> |
| 5 | #include <console/statusbar.h> |
| 6 | #include <console/term.h> |
| 7 | #include <crypto/prng.h> |
| 8 | #include <fs/vfs.h> |
| 9 | #include <global.h> |
| 10 | #include <irq/irq.h> |
| 11 | #include <math/align.h> |
| 12 | #include <math/sort.h> |
| 13 | #include <mem/nofail.h> |
| 14 | #include <mem/vas.h> |
| 15 | #include <ndjson.h> |
| 16 | #include <smp/core.h> |
| 17 | #include <stack_depot.h> |
| 18 | #include <stdbool.h> |
| 19 | #include <stddef.h> |
| 20 | #include <stdint.h> |
| 21 | #include <test/export.h> |
| 22 | #include <test/fleet.h> |
| 23 | #include <test/test.h> |
| 24 | #include <time/spin_sleep.h> |
| 25 | #include <time/time.h> |
| 26 | |
| 27 | #include "mem/slab/internal.h" |
| 28 | |
| 29 | /* Basically, every test and test setting wires back here */ |
| 30 | CMDLINE_DEFINE(test_root, .name = "test" , .flags = CMDLINE_ENTRY_SYMBOLIC); |
| 31 | |
| 32 | LINKER_SECTION_OBJECT(struct test_group, test_groups) |
| 33 | test_group_orphan_parent = {.name = "test_group_orphan_parent" , |
| 34 | .enabled = TEST_STATE_SENTINEL, |
| 35 | .exit_on_fail = false, |
| 36 | .incremental = false, |
| 37 | .flags = TEST_GROUP_FLAG_DEFAULT}; |
| 38 | |
| 39 | CMDLINE_CHILDREN_DEFINE( |
| 40 | test_root, |
| 41 | CMDLINE_INNER( |
| 42 | filter, .types = CMDLINE_TYPES(CMDLINE_TYPE_STRING, CMDLINE_TYPE_LIST), |
| 43 | .desc = "this makes tests and groups opt-in and enables whatever tests " |
| 44 | "and/or groups are passed in, WITHOUT namespaces, i.e. only " |
| 45 | "\"test_name\" or \"test_group_name\"" ), |
| 46 | CMDLINE_INNER_FX(global_intensity, test_global.global_intensity, |
| 47 | .desc = "global intensity override for all tests" , |
| 48 | .range = RANGE(0, FX_ONE)), |
| 49 | CMDLINE_INNER_VAR( |
| 50 | group_opt_in, test_global.group_opt_in, |
| 51 | .desc = "By default, tests are opt-out, and compiled tests will run, " |
| 52 | "and this inverts that" ), |
| 53 | CMDLINE_INNER_VAR(test_opt_in, test_global.test_opt_in, |
| 54 | .flags = CMDLINE_ENTRY_HIDDEN), |
| 55 | CMDLINE_INNER_VAR(show_output, test_global.show_output, |
| 56 | .flags = CMDLINE_ENTRY_HIDDEN), |
| 57 | CMDLINE_INNER_VAR(no_exit, test_global.no_exit, |
| 58 | .desc = "Idle after the suite completes" ), |
| 59 | CMDLINE_INNER_VAR(no_progress, test_global.no_progress, |
| 60 | .desc = "Do not show progress bar" )); |
| 61 | |
| 62 | NDJSON_DEFINE(test_begin, NDJSON_SECTION_TEST, NDJSON_KIND_BEGIN, 1, |
| 63 | NDJSON_U64(declared_total)); |
| 64 | |
| 65 | NDJSON_DEFINE(test_group_start, NDJSON_SECTION_TEST, NDJSON_KIND_GROUP_START, 1, |
| 66 | NDJSON_STR(group), NDJSON_U64(test_count), NDJSON_STR(file)); |
| 67 | |
| 68 | NDJSON_DEFINE(test_result, NDJSON_SECTION_TEST, NDJSON_KIND_RESULT, 1, |
| 69 | NDJSON_STR(group), NDJSON_STR(tier), NDJSON_STR(name), |
| 70 | NDJSON_STR(status), NDJSON_U64(duration_ms), NDJSON_STR(reason), |
| 71 | NDJSON_STR(msg), NDJSON_U64(runs_requested), |
| 72 | NDJSON_U64(runs_attempted), NDJSON_U64(runs_failed), |
| 73 | NDJSON_U64(runs_skipped)); |
| 74 | |
| 75 | NDJSON_DEFINE(test_group_end, NDJSON_SECTION_TEST, NDJSON_KIND_GROUP_END, 1, |
| 76 | NDJSON_STR(group), NDJSON_U64(duration_ms), NDJSON_U64(failed), |
| 77 | NDJSON_U64(skipped)); |
| 78 | |
| 79 | NDJSON_DEFINE(test_totals, NDJSON_SECTION_TEST, NDJSON_KIND_TOTALS, 1, |
| 80 | NDJSON_U64(total), NDJSON_U64(passed), NDJSON_U64(failed), |
| 81 | NDJSON_U64(skipped)); |
| 82 | |
| 83 | NDJSON_DEFINE(test_verdict, NDJSON_SECTION_TEST, NDJSON_KIND_VERDICT, 1, |
| 84 | NDJSON_BOOL(ok), NDJSON_U64(duration_ms)); |
| 85 | |
| 86 | static const char *test_status_plain(enum test_result r) { |
| 87 | switch (r) { |
| 88 | case TEST_RESULT_OK: return "pass" ; |
| 89 | case TEST_RESULT_FAILED: return "fail" ; |
| 90 | case TEST_RESULT_SKIPPED: return "skip" ; |
| 91 | default: return "unknown" ; |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | static const char *test_tier_plain(enum test_tier t) { |
| 96 | switch (t) { |
| 97 | case TEST_TIER_SMOKE: return "smoke" ; |
| 98 | case TEST_TIER_UNIT: return "unit" ; |
| 99 | case TEST_TIER_INTEGRATION: return "integration" ; |
| 100 | default: return "unknown" ; |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | LOG_SITE_DEFINE_PRINT(test_harness); |
| 105 | LOG_HANDLE_DEFINE_PRINT(test_harness, |
| 106 | .flags = LOG_HANDLE_PRINT | LOG_HANDLE_NO_NEWLINE); |
| 107 | |
| 108 | LOG_SITE_DEFINE(test_ndjson, .flags = LOG_SITE_DEFAULT | LOG_SITE_NDJSON, |
| 109 | .capacity = LOG_SITE_CAPACITY_DEFAULT, |
| 110 | .dump_opts = LOG_DUMP_DEFAULT, .enabled_mask = LOG_SITE_ALL); |
| 111 | LOG_HANDLE_DEFINE(test_ndjson, .flags = LOG_HANDLE_FLAGS_DEFAULT); |
| 112 | |
| 113 | #define test_ndjson_log(lvl, fmt, ...) \ |
| 114 | log(LOG_SITE(test_ndjson), LOG_HANDLE(test_ndjson), lvl, fmt, ##__VA_ARGS__) |
| 115 | |
| 116 | #define test_ndjson_err(fmt, ...) test_ndjson_log(LOG_ERROR, fmt, ##__VA_ARGS__) |
| 117 | #define test_ndjson_warn(fmt, ...) test_ndjson_log(LOG_WARN, fmt, ##__VA_ARGS__) |
| 118 | #define test_ndjson_info(fmt, ...) test_ndjson_log(LOG_INFO, fmt, ##__VA_ARGS__) |
| 119 | #define test_ndjson_debug(fmt, ...) \ |
| 120 | test_ndjson_log(LOG_DEBUG, fmt, ##__VA_ARGS__) |
| 121 | #define test_ndjson_trace(fmt, ...) \ |
| 122 | test_ndjson_log(LOG_TRACE, fmt, ##__VA_ARGS__) |
| 123 | |
| 124 | #define test_harness_log(lvl, fmt, ...) \ |
| 125 | log(LOG_SITE(test_harness), LOG_HANDLE(test_harness), lvl, fmt, \ |
| 126 | ##__VA_ARGS__) |
| 127 | |
| 128 | #define test_harness_err(fmt, ...) \ |
| 129 | test_harness_log(LOG_ERROR, fmt, ##__VA_ARGS__) |
| 130 | #define test_harness_warn(fmt, ...) \ |
| 131 | test_harness_log(LOG_WARN, fmt, ##__VA_ARGS__) |
| 132 | #define test_harness_info(fmt, ...) \ |
| 133 | test_harness_log(LOG_INFO, fmt, ##__VA_ARGS__) |
| 134 | #define test_harness_debug(fmt, ...) \ |
| 135 | test_harness_log(LOG_DEBUG, fmt, ##__VA_ARGS__) |
| 136 | #define test_harness_trace(fmt, ...) \ |
| 137 | test_harness_log(LOG_TRACE, fmt, ##__VA_ARGS__) |
| 138 | |
| 139 | #define OSC "\033]" |
| 140 | #define OSC_ST "\033\\" |
| 141 | #define OSC8_FILE_LINK(path, line) \ |
| 142 | OSC "8;;file://" CHARMOS_SOURCE_ROOT "/" path "#" line OSC_ST |
| 143 | #define OSC8_FILE_LINK_NOLINE(path) \ |
| 144 | OSC "8;;file://" CHARMOS_SOURCE_ROOT "/" path OSC_ST |
| 145 | #define OSC8_LINK_END OSC "8;;" OSC_ST |
| 146 | |
| 147 | LINKER_SECTION_EXTERN(struct test, tests); |
| 148 | LINKER_SECTION_EXTERN(struct test_group, test_groups); |
| 149 | /* no need to clean up allocations in these tests, we are supposed to |
| 150 | * reboot/poweroff after all tests complete, and the userland should |
| 151 | * not be in a state where we can boot it when running tests */ |
| 152 | struct test_globals test_global = {.global_intensity = TEST_INTENSITY_SENTINEL}; |
| 153 | |
| 154 | static void *test_instance_resolver(const char *path, size_t path_len) { |
| 155 | char name_buf[CMDLINE_ENTRY_NAME_LEN_MAX]; |
| 156 | if (path_len >= sizeof(name_buf)) |
| 157 | return NULL; |
| 158 | memcpy(name_buf, path, path_len); |
| 159 | name_buf[path_len] = '\0'; |
| 160 | |
| 161 | char *last_dot = strrchr(s: name_buf, c: '.'); |
| 162 | const char *test_name = NULL; |
| 163 | const char *group_name = NULL; |
| 164 | |
| 165 | if (last_dot) { |
| 166 | *last_dot = '\0'; |
| 167 | group_name = name_buf; |
| 168 | test_name = last_dot + 1; |
| 169 | } else { |
| 170 | test_name = name_buf; |
| 171 | } |
| 172 | |
| 173 | for (struct test *t = __skernel_tests; t < __ekernel_tests; t++) { |
| 174 | if (strcmp(str1: t->name, str2: test_name) == 0) { |
| 175 | if (!group_name || |
| 176 | (t->group && strcmp(str1: t->group->name, str2: group_name) == 0)) |
| 177 | return t; |
| 178 | } |
| 179 | } |
| 180 | return NULL; |
| 181 | } |
| 182 | |
| 183 | static void *test_group_instance_resolver(const char *path, size_t path_len) { |
| 184 | char name_buf[CMDLINE_ENTRY_NAME_LEN_MAX]; |
| 185 | if (path_len >= sizeof(name_buf)) |
| 186 | return NULL; |
| 187 | memcpy(name_buf, path, path_len); |
| 188 | name_buf[path_len] = '\0'; |
| 189 | |
| 190 | for (struct test_group *g = __skernel_test_groups; |
| 191 | g < __ekernel_test_groups; g++) { |
| 192 | if (strcmp(str1: g->name, str2: name_buf) == 0) |
| 193 | return g; |
| 194 | } |
| 195 | return NULL; |
| 196 | } |
| 197 | |
| 198 | CMDLINE_SCHEMA_DEFINE( |
| 199 | test_props, "test" , "<group>.<name>" , "Test parameters" , |
| 200 | test_instance_resolver, CMDLINE_SCHEMA_PROP(struct test, enabled), |
| 201 | CMDLINE_SCHEMA_PROP_FX(struct test, intensity, |
| 202 | .desc = "Execution intensity" , |
| 203 | .range = RANGE(0, FX_ONE)), |
| 204 | CMDLINE_SCHEMA_PROP(struct test, run_times, .desc = "Times to run the test" , |
| 205 | .range = RANGE(1, 100000)), |
| 206 | CMDLINE_SCHEMA_PROP(struct test, seed, .desc = "PRNG seed (TODO:)" ), |
| 207 | CMDLINE_SCHEMA_PROP( |
| 208 | struct test, duration_ms, |
| 209 | .desc = "Maximum runtime limit in milliseconds (TODO: DURATION)" ), |
| 210 | CMDLINE_SCHEMA_PROP(struct test, msg_cap, .desc = "Log limit" ), |
| 211 | CMDLINE_SCHEMA_PROP( |
| 212 | struct test, keep_going, |
| 213 | .desc = "If one test fails when run_times > 1, keep going" ), |
| 214 | CMDLINE_SCHEMA_PROP(struct test, print_logs, |
| 215 | .desc = "Print logs in real time" )); |
| 216 | |
| 217 | CMDLINE_SCHEMA_DEFINE( |
| 218 | test_group_props, "test_group" , "<group>" , "Test group parameters" , |
| 219 | test_group_instance_resolver, |
| 220 | CMDLINE_SCHEMA_PROP(struct test_group, enabled), |
| 221 | CMDLINE_SCHEMA_PROP(struct test_group, smoke_enabled), |
| 222 | CMDLINE_SCHEMA_PROP(struct test_group, unit_enabled), |
| 223 | CMDLINE_SCHEMA_PROP(struct test_group, integration_enabled), |
| 224 | CMDLINE_SCHEMA_PROP( |
| 225 | struct test_group, incremental, |
| 226 | .desc = "Run tiers incrementally (smoke -> unit -> integration)" ), |
| 227 | CMDLINE_SCHEMA_PROP(struct test_group, exit_on_fail, |
| 228 | .desc = "Exit after first test fails" )); |
| 229 | |
| 230 | static enum test_skip_reason test_check_preconditions(const struct test *t) { |
| 231 | if (t->min_cores && global.core_count < t->min_cores) |
| 232 | return TEST_SKIP_INSUFFICIENT_CORES; |
| 233 | |
| 234 | if (t->min_ram_mib && pmm_get_usable_ram() < MIB(t->min_ram_mib)) |
| 235 | return TEST_SKIP_RAM_LOW; |
| 236 | |
| 237 | if (t->required_fs != FS_UNKNOWN && |
| 238 | (!global.root_node || global.root_node->fs_type != t->required_fs)) |
| 239 | return TEST_SKIP_UNSUPPORTED_FS; |
| 240 | |
| 241 | return TEST_SKIP_NONE; |
| 242 | } |
| 243 | |
| 244 | static void tests_set_enabled_states(void) { |
| 245 | #ifdef TEST_ENABLED |
| 246 | |
| 247 | /* Here we just apply the globals */ |
| 248 | if (test_global.test_opt_in) { |
| 249 | for (struct test *t = __skernel_tests; t < __ekernel_tests; t++) { |
| 250 | if (t->enabled == TEST_STATE_SENTINEL) { |
| 251 | t->enabled = TEST_STATE_DISABLED; |
| 252 | } |
| 253 | } |
| 254 | } else { |
| 255 | for (struct test *t = __skernel_tests; t < __ekernel_tests; t++) { |
| 256 | if (t->enabled == TEST_STATE_SENTINEL) { |
| 257 | t->enabled = TEST_STATE_ENABLED; |
| 258 | } |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | if (test_global.group_opt_in) { |
| 263 | for (struct test_group *tg = __skernel_test_groups; |
| 264 | tg < __ekernel_test_groups; tg++) { |
| 265 | if (tg->enabled == TEST_STATE_SENTINEL) { |
| 266 | tg->enabled = TEST_STATE_DISABLED; |
| 267 | for (int i = 0; i < TEST_TIER_MAX; i++) |
| 268 | tg->tier_enabled[i] = TEST_STATE_DISABLED; |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | } else { |
| 273 | for (struct test_group *tg = __skernel_test_groups; |
| 274 | tg < __ekernel_test_groups; tg++) { |
| 275 | if (tg->enabled == TEST_STATE_SENTINEL) { |
| 276 | tg->enabled = TEST_STATE_ENABLED; |
| 277 | for (int i = 0; i < TEST_TIER_MAX; i++) |
| 278 | tg->tier_enabled[i] = TEST_STATE_ENABLED; |
| 279 | } |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | for (struct test_group *tg = __skernel_test_groups; |
| 284 | tg < __ekernel_test_groups; tg++) { |
| 285 | for (int i = 0; i < TEST_TIER_MAX; i++) { |
| 286 | tg->num_tests_enabled[i] = 0; |
| 287 | for (size_t j = 0; j < tg->num_tests[i]; j++) { |
| 288 | struct test *t = tg->tests[i][j]; |
| 289 | if (t->enabled) |
| 290 | tg->num_tests_enabled[i]++; |
| 291 | } |
| 292 | } |
| 293 | } |
| 294 | |
| 295 | for (struct test *t = __skernel_tests; t < __ekernel_tests; t++) { |
| 296 | if (t->enabled) |
| 297 | test_global.total_tests_enabled++; |
| 298 | } |
| 299 | |
| 300 | #endif |
| 301 | } |
| 302 | |
| 303 | struct test_dup_item { |
| 304 | char *name; /* Points to the test or test_group->name */ |
| 305 | uint32_t hash; /* hash_murmur3_32() */ |
| 306 | }; |
| 307 | |
| 308 | static cc_unused int test_dup_item_cmp(const void *a, const void *b) { |
| 309 | const struct test_dup_item *ta = a; |
| 310 | const struct test_dup_item *tb = b; |
| 311 | |
| 312 | if (ta->hash < tb->hash) |
| 313 | return -1; |
| 314 | else if (ta->hash > tb->hash) |
| 315 | return 1; |
| 316 | else |
| 317 | return strcmp(str1: ta->name, str2: tb->name); |
| 318 | } |
| 319 | |
| 320 | static void tests_check_duplicate_names(void) { |
| 321 | /* Rule: no two test groups may have the same name */ |
| 322 | for (struct test_group *tg1 = __skernel_test_groups; |
| 323 | tg1 < __ekernel_test_groups; tg1++) { |
| 324 | for (struct test_group *tg2 = tg1 + 1; tg2 < __ekernel_test_groups; |
| 325 | tg2++) { |
| 326 | if (strcmp(str1: tg1->name, str2: tg2->name) == 0) { |
| 327 | panic(DUPLICATE, "test_group name: %s" , tg1->name); |
| 328 | } |
| 329 | } |
| 330 | } |
| 331 | |
| 332 | /* Rule: no two tests in the same test group may have the same name */ |
| 333 | for (struct test *t1 = __skernel_tests; t1 < __ekernel_tests; t1++) { |
| 334 | for (struct test *t2 = t1 + 1; t2 < __ekernel_tests; t2++) { |
| 335 | if (t1->group == t2->group && strcmp(str1: t1->name, str2: t2->name) == 0) { |
| 336 | struct test_group *g = (struct test_group *) t1->group; |
| 337 | panic(DUPLICATE, "test name '%s' in group '%s'" , t1->name, |
| 338 | g ? g->name : "<unknown>" ); |
| 339 | } |
| 340 | } |
| 341 | } |
| 342 | } |
| 343 | |
| 344 | static void test_filter_enable(char *name) { |
| 345 | char *sep = strchr(s: name, c: '.'); |
| 346 | if (!sep) |
| 347 | sep = strchr(s: name, c: ':'); |
| 348 | |
| 349 | if (sep) { |
| 350 | char grp_buf[64]; |
| 351 | size_t grp_len = (size_t) (sep - name); |
| 352 | if (grp_len < sizeof(grp_buf)) { |
| 353 | memcpy(grp_buf, name, grp_len); |
| 354 | grp_buf[grp_len] = '\0'; |
| 355 | const char *t_name = sep + 1; |
| 356 | bool found_specific = false; |
| 357 | for (struct test *t = __skernel_tests; t < __ekernel_tests; t++) { |
| 358 | struct test_group *g = (struct test_group *) t->group; |
| 359 | if (g && strcmp(str1: g->name, str2: grp_buf) == 0 && |
| 360 | strcmp(str1: t->name, str2: t_name) == 0) { |
| 361 | t->enabled = TEST_STATE_ENABLED; |
| 362 | g->enabled = TEST_STATE_ENABLED; |
| 363 | found_specific = true; |
| 364 | } |
| 365 | } |
| 366 | if (found_specific) |
| 367 | return; |
| 368 | } |
| 369 | } |
| 370 | |
| 371 | for (struct test_group *tg = __skernel_test_groups; |
| 372 | tg < __ekernel_test_groups; tg++) { |
| 373 | if (strcmp(str1: tg->name, str2: name) == 0) { |
| 374 | tg->enabled = TEST_STATE_ENABLED; |
| 375 | |
| 376 | /* Enable all children if the group is on */ |
| 377 | for (int i = 0; i < TEST_TIER_MAX; i++) { |
| 378 | if (tg->tier_enabled[i] == TEST_STATE_SENTINEL) |
| 379 | tg->tier_enabled[i] = TEST_STATE_ENABLED; |
| 380 | } |
| 381 | |
| 382 | for (struct test *t = __skernel_tests; t < __ekernel_tests; t++) { |
| 383 | if (t->group == tg && t->enabled == TEST_STATE_SENTINEL) |
| 384 | t->enabled = TEST_STATE_ENABLED; |
| 385 | } |
| 386 | |
| 387 | return; |
| 388 | } |
| 389 | } |
| 390 | |
| 391 | bool found = false; |
| 392 | for (struct test *t = __skernel_tests; t < __ekernel_tests; t++) { |
| 393 | if (strcmp(str1: t->name, str2: name) == 0) { |
| 394 | t->enabled = TEST_STATE_ENABLED; |
| 395 | |
| 396 | struct test_group *g = (struct test_group *) t->group; |
| 397 | if (g) |
| 398 | g->enabled = TEST_STATE_ENABLED; |
| 399 | found = true; |
| 400 | } |
| 401 | } |
| 402 | if (found) |
| 403 | return; |
| 404 | |
| 405 | panic("%s in the filter is not a valid test group or test name" , name); |
| 406 | } |
| 407 | |
| 408 | static void tests_apply_filters(void) { |
| 409 | struct cmdline_entry *filter = CMDLINE_CHILD(test_root, filter); |
| 410 | if (filter->status != CMDLINE_ENTRY_FOUND) |
| 411 | return; |
| 412 | |
| 413 | /* A filter is the opt-in switch, as the option's own description says. |
| 414 | * Without this everything still sentinel stays enabled and the filter only |
| 415 | * ever adds, which makes test.filter= silently run the whole suite */ |
| 416 | test_global.test_opt_in = true; |
| 417 | test_global.group_opt_in = true; |
| 418 | |
| 419 | if (filter->value.type == CMDLINE_TYPE_STRING) { |
| 420 | char *filter_one; |
| 421 | CMDLINE_EXTRACT(&filter->value, filter_one); |
| 422 | test_filter_enable(name: filter_one); |
| 423 | } else { |
| 424 | enforce(filter->value.type == CMDLINE_TYPE_LIST); |
| 425 | struct cmdline_list list; |
| 426 | CMDLINE_EXTRACT(&filter->value, list); |
| 427 | struct cmdline_value val; |
| 428 | cmdline_list_for_each(val, &list) { |
| 429 | enforce(val.type == CMDLINE_TYPE_STRING); |
| 430 | char *filter_one; |
| 431 | CMDLINE_EXTRACT(&val, filter_one); |
| 432 | test_filter_enable(name: filter_one); |
| 433 | } |
| 434 | } |
| 435 | } |
| 436 | |
| 437 | static void tests_setup_groups(void) { |
| 438 | for (struct test_group *tg = __skernel_test_groups; |
| 439 | tg < __ekernel_test_groups; tg++) { |
| 440 | size_t num_tests[TEST_TIER_MAX] = {0}; |
| 441 | for (struct test *t = __skernel_tests; t < __ekernel_tests; t++) { |
| 442 | if (t->group == tg) { |
| 443 | num_tests[t->tier]++; |
| 444 | } |
| 445 | } |
| 446 | |
| 447 | for (int i = 0; i < TEST_TIER_MAX; i++) { |
| 448 | if (num_tests[i]) { |
| 449 | tg->tests[i] = nofail_vnews(tg->tests[i], num_tests[i]); |
| 450 | } |
| 451 | tg->num_tests[i] = num_tests[i]; |
| 452 | } |
| 453 | |
| 454 | for (struct test *t = __skernel_tests; t < __ekernel_tests; t++) { |
| 455 | if (t->group == tg) { |
| 456 | tg->tests[t->tier][num_tests[t->tier] - 1] = t; |
| 457 | num_tests[t->tier]--; |
| 458 | } |
| 459 | } |
| 460 | } |
| 461 | } |
| 462 | |
| 463 | struct test_group_result { |
| 464 | size_t totals[TEST_TIER_MAX][TEST_RESULT_MAX]; |
| 465 | }; |
| 466 | |
| 467 | static struct { |
| 468 | size_t total; |
| 469 | size_t done; |
| 470 | size_t failed; |
| 471 | size_t skipped; |
| 472 | time_ms_t started_ms; |
| 473 | time_ms_t test_started_ms; |
| 474 | } test_progress = {0}; |
| 475 | |
| 476 | static size_t tests_count_planned(void) { |
| 477 | size_t n = 0; |
| 478 | |
| 479 | for (struct test_group *tg = __skernel_test_groups; |
| 480 | tg < __ekernel_test_groups; tg++) { |
| 481 | if (!tg->enabled) |
| 482 | continue; |
| 483 | |
| 484 | for (int i = 0; i < TEST_TIER_MAX; i++) { |
| 485 | if (tg->tier_enabled[i]) |
| 486 | n += tg->num_tests[i]; |
| 487 | } |
| 488 | } |
| 489 | |
| 490 | return n; |
| 491 | } |
| 492 | |
| 493 | static void test_progress_paint(const struct test_group *tg, |
| 494 | enum test_tier tier, const char *test_name, |
| 495 | const char *intensity) { |
| 496 | char tail[96] = "" ; |
| 497 | |
| 498 | if (!test_progress.total) |
| 499 | return; |
| 500 | |
| 501 | if (test_progress.failed || test_progress.skipped) |
| 502 | snprintf(tail, (int) sizeof(tail), |
| 503 | " " ANSI_RED "%zu failed" ANSI_RESET " " ANSI_GRAY |
| 504 | "%zu skipped" ANSI_RESET, |
| 505 | test_progress.failed, test_progress.skipped); |
| 506 | |
| 507 | status_bar_progress_timed( |
| 508 | done: test_progress.done, total: test_progress.total, test_started_ms: test_progress.test_started_ms, |
| 509 | total_started_ms: test_progress.started_ms, |
| 510 | ANSI_GREEN ANSI_BOLD "running" ANSI_RESET " " ANSI_BLUE "%s" ANSI_RESET |
| 511 | " (%s" ANSI_RESET ") " ANSI_BOLD "%s" ANSI_RESET |
| 512 | "%s%s%s%s" , |
| 513 | tg->name, test_tier_to_str_color(stringify_in: tier), test_name, |
| 514 | intensity ? " [" ANSI_CYAN : "" , intensity ? intensity : "" , |
| 515 | intensity ? ANSI_RESET "]" : "" , tail); |
| 516 | } |
| 517 | |
| 518 | static void test_handle_print(const struct log_site *site, |
| 519 | const struct log_record *rec, |
| 520 | void (*print)(const char *fmt, ...)) { |
| 521 | cc_ignore(site); |
| 522 | print("[%s%zu.%03zu" ANSI_RESET "] " , log_level_color(l: rec->level), |
| 523 | MS_TO_SECONDS(rec->timestamp), rec->timestamp % 1000); |
| 524 | } |
| 525 | |
| 526 | static void test_print_link(const struct test *t) { |
| 527 | test_harness_info(ANSI_GREEN ANSI_BOLD OSC8_FILE_LINK( |
| 528 | "%s" , "%u" ) "%s" OSC8_LINK_END ANSI_RESET, |
| 529 | t->fname, t->line, t->name); |
| 530 | } |
| 531 | |
| 532 | static void test_group_run(struct test_group *tg) { |
| 533 | if (!tg->enabled) |
| 534 | return; |
| 535 | |
| 536 | bool all_disabled = true; |
| 537 | for (int i = 0; i < TEST_TIER_MAX; i++) { |
| 538 | if (tg->tier_enabled[i]) { |
| 539 | all_disabled = false; |
| 540 | break; |
| 541 | } |
| 542 | } |
| 543 | |
| 544 | if (all_disabled) |
| 545 | return; |
| 546 | |
| 547 | bool no_tests = true; |
| 548 | for (int i = 0; i < TEST_TIER_MAX; i++) { |
| 549 | if (tg->num_tests_enabled[i]) { |
| 550 | no_tests = false; |
| 551 | break; |
| 552 | } |
| 553 | } |
| 554 | |
| 555 | if (no_tests) |
| 556 | return; |
| 557 | |
| 558 | /* HACK: find a cleaner way to represent this */ |
| 559 | size_t total_tests = 0; |
| 560 | time_ms_t total_time = 0; |
| 561 | for (int i = 0; i < TEST_TIER_MAX; i++) |
| 562 | total_tests += tg->num_tests_enabled[i]; |
| 563 | |
| 564 | test_harness_info( |
| 565 | ANSI_GREEN ANSI_BOLD |
| 566 | "Running" ANSI_RESET " group " ANSI_BLUE ANSI_BOLD OSC8_FILE_LINK( |
| 567 | "%s" , "%u" ) "%s" OSC8_LINK_END ANSI_RESET " - %zu tests\n" , |
| 568 | tg->fname, tg->line, tg->name, total_tests); |
| 569 | |
| 570 | test_ndjson_info("group start: %s (%zu tests)" , tg->name, total_tests); |
| 571 | |
| 572 | ndjson_emit(test_group_start, .group = tg->name, .test_count = total_tests, |
| 573 | .file = tg->fname); |
| 574 | printf(format: "%*s | " , 20, "" ); |
| 575 | printf(format: tg->incremental ? "incremental, " : "non_incremental, " ); |
| 576 | printf(format: tg->exit_on_fail ? "exit_on_fail" : "continue_on_fail" ); |
| 577 | printf(format: "\n" ); |
| 578 | printf(format: "%*s | " , 20, "" ); |
| 579 | printf(ANSI_UNDERLINE ANSI_BOLD "enabled" ANSI_RESET ": " ); |
| 580 | for (int i = 0; i < TEST_TIER_MAX; i++) { |
| 581 | if (!tg->num_tests_enabled[i]) |
| 582 | continue; |
| 583 | |
| 584 | if (tg->tier_enabled[i]) |
| 585 | printf(ANSI_BOLD "%s" ANSI_RESET, test_tier_to_str_color(stringify_in: i)); |
| 586 | |
| 587 | /* Check if the next one exists to print a comma */ |
| 588 | for (int j = i + 1; j < TEST_TIER_MAX; j++) { |
| 589 | if (tg->tier_enabled[j] && tg->num_tests_enabled[j]) { |
| 590 | printf(format: ", " ); |
| 591 | break; |
| 592 | } |
| 593 | } |
| 594 | } |
| 595 | |
| 596 | printf(format: "\n" ); |
| 597 | |
| 598 | bool stop_outer = false; |
| 599 | LOG_SITE(test_harness)->name = (char *) tg->name; |
| 600 | |
| 601 | struct test_group_result result_totals = {0}; |
| 602 | size_t result_aggregates[TEST_RESULT_MAX] = {0}; |
| 603 | for (int i = 0; i < TEST_TIER_MAX; i++) { |
| 604 | if (stop_outer) |
| 605 | break; |
| 606 | |
| 607 | if (!tg->num_tests_enabled[i] || !tg->tier_enabled[i]) |
| 608 | continue; |
| 609 | |
| 610 | const char *tier_name = test_tier_to_str_color(stringify_in: i); |
| 611 | |
| 612 | enforce(asprintf(&LOG_SITE(test_harness)->name, |
| 613 | "%s " ANSI_RESET "(%s" ANSI_RESET ")" , tg->name, |
| 614 | tier_name), |
| 615 | OUT_OF_MEMORY); |
| 616 | |
| 617 | for (size_t test_num = 0; test_num < tg->num_tests[i]; test_num++) { |
| 618 | struct test *t = tg->tests[i][test_num]; |
| 619 | struct test_context tctx = {0}; |
| 620 | if (!t->enabled) |
| 621 | continue; |
| 622 | |
| 623 | /* Modifiable by the test */ |
| 624 | struct log_dump_options dopts = { |
| 625 | .min_level = LOG_TRACE, |
| 626 | }; |
| 627 | |
| 628 | enum log_site_flags flags = LOG_SITE_NONE; |
| 629 | if (t->print_logs && test_global.show_output) |
| 630 | flags |= LOG_SITE_PRINT; |
| 631 | |
| 632 | struct log_site_options opts = { |
| 633 | .capacity = |
| 634 | t->msg_cap == 0 ? LOG_SITE_CAPACITY_DEFAULT : t->msg_cap, |
| 635 | .name = "test" , |
| 636 | .enabled_mask = LOG_SITE_ALL, |
| 637 | .dump_opts = dopts, |
| 638 | .flags = flags, |
| 639 | }; |
| 640 | tctx.site = nofail(log_site_create(opts)); |
| 641 | test_global.current_test = &tctx; |
| 642 | tctx.handle.print = test_handle_print; |
| 643 | tctx.intensity = t->intensity; |
| 644 | tctx.seed = !t->seed ? prng_next() : t->seed; |
| 645 | |
| 646 | if (t->flags & TEST_FLAG_HONORS_INTENSITY) { |
| 647 | tctx.intensity_val = |
| 648 | scaled_param_eval(desc: &t->intensity_desc, value: tctx.intensity); |
| 649 | } else { |
| 650 | tctx.intensity_val = 0; |
| 651 | } |
| 652 | |
| 653 | size_t result_times[TEST_RESULT_MAX] = {0}, run_times = 0; |
| 654 | |
| 655 | struct test_verdict *verdicts = |
| 656 | nofail_vnews(verdicts, t->run_times, ALLOC_ZERO); |
| 657 | struct test_verdict singular_verdict = {0}; |
| 658 | |
| 659 | char intst_str[512] = {0}; |
| 660 | if (t->flags & TEST_FLAG_HONORS_INTENSITY) { |
| 661 | scaled_param_format(desc: &t->intensity_desc, value: tctx.intensity, |
| 662 | scaled_val: tctx.intensity_val, buf: intst_str, |
| 663 | cap: sizeof(intst_str)); |
| 664 | } |
| 665 | time_ms_t start_ms = time_get_ms(); |
| 666 | test_progress.test_started_ms = start_ms; |
| 667 | test_progress_paint(tg, tier: i, test_name: t->name, |
| 668 | intensity: intst_str[0] ? intst_str : NULL); |
| 669 | test_ndjson_info("test start: %s:%s (%s)" , tg->name, t->name, |
| 670 | test_tier_plain(i)); |
| 671 | |
| 672 | enum test_skip_reason precond = test_check_preconditions(t); |
| 673 | |
| 674 | for (; run_times < t->run_times; run_times++) { |
| 675 | struct test_verdict verdict; |
| 676 | if (precond != TEST_SKIP_NONE) { |
| 677 | verdict = TEST_SKIP(precond); |
| 678 | } else { |
| 679 | verdict = t->func(&tctx); |
| 680 | } |
| 681 | |
| 682 | if (test_context_teardown(ctx: &tctx) && |
| 683 | verdict.result != TEST_RESULT_FAILED) { |
| 684 | verdict = TEST_FAIL("test returned without joining its " |
| 685 | "threads" ); |
| 686 | } |
| 687 | singular_verdict = verdict; |
| 688 | verdicts[run_times] = verdict; |
| 689 | |
| 690 | if (verdict.result == TEST_RESULT_SKIPPED) { |
| 691 | result_times[TEST_RESULT_SKIPPED]++; |
| 692 | } else if (verdict.result == TEST_RESULT_FAILED) { |
| 693 | result_times[TEST_RESULT_FAILED]++; |
| 694 | } else { |
| 695 | result_times[TEST_RESULT_OK]++; |
| 696 | } |
| 697 | |
| 698 | if (result_times[TEST_RESULT_FAILED] && !t->keep_going) |
| 699 | break; |
| 700 | |
| 701 | tctx.seed = !t->seed ? prng_next() : t->seed; |
| 702 | tctx.fail_msg[0] = '\0'; |
| 703 | } |
| 704 | time_ms_t end_ms = time_get_ms(); |
| 705 | time_ms_t took = end_ms - start_ms; |
| 706 | total_time += took; |
| 707 | test_global.total_time += took; |
| 708 | |
| 709 | test_ndjson_info( |
| 710 | "test finished: %s:%s -> %s in %zu ms" , tg->name, t->name, |
| 711 | test_result_to_str_plain(singular_verdict.result), took); |
| 712 | |
| 713 | size_t non_skipped = run_times - result_times[TEST_RESULT_SKIPPED]; |
| 714 | |
| 715 | test_print_link(t); |
| 716 | if (intst_str[0]) |
| 717 | printf(format: " [" ANSI_CYAN "%s" ANSI_RESET "]" , intst_str); |
| 718 | |
| 719 | if (t->run_times > 1) { |
| 720 | char *color = non_skipped < run_times ? ANSI_RED : ANSI_BLUE; |
| 721 | printf(format: " ran (%s%zu" ANSI_RESET "/" ANSI_BLUE "%zu" ANSI_RESET |
| 722 | ") times in " ANSI_BRIGHT_WHITE "%zu" ANSI_RESET " ms," , |
| 723 | color, non_skipped, t->run_times, took); |
| 724 | if (result_times[TEST_RESULT_OK] == run_times) { |
| 725 | printf(ANSI_BLUE " all successful" ANSI_RESET); |
| 726 | } else if (result_times[TEST_RESULT_SKIPPED]) { |
| 727 | printf(ANSI_GRAY " %zu skipped" ANSI_RESET, |
| 728 | result_times[TEST_RESULT_SKIPPED]); |
| 729 | } |
| 730 | |
| 731 | if (result_times[TEST_RESULT_FAILED]) { |
| 732 | printf(ANSI_RED " %zu failed" ANSI_RESET, |
| 733 | result_times[TEST_RESULT_FAILED]); |
| 734 | } |
| 735 | |
| 736 | printf(format: "\n" ); |
| 737 | |
| 738 | for (size_t run = 0; run < run_times; run++) { |
| 739 | struct test_verdict v = verdicts[run]; |
| 740 | if (v.result != TEST_RESULT_OK) { |
| 741 | test_harness_info(" |-> run %zu %s" , run, |
| 742 | test_result_to_str(v.result)); |
| 743 | if (v.result == TEST_RESULT_SKIPPED) { |
| 744 | printf(ANSI_GRAY " (%s)" ANSI_RESET, |
| 745 | test_skip_reason_to_str(stringify_in: v.skip_reason)); |
| 746 | } else if (v.msg) { |
| 747 | printf(ANSI_RED " (%s)" ANSI_RESET, v.msg); |
| 748 | } |
| 749 | printf(format: "\n" ); |
| 750 | } |
| 751 | } |
| 752 | } else { |
| 753 | char *status; |
| 754 | char *color; |
| 755 | switch (singular_verdict.result) { |
| 756 | case TEST_RESULT_OK: |
| 757 | color = ANSI_GREEN ANSI_BOLD; |
| 758 | status = "successful" ; |
| 759 | break; |
| 760 | case TEST_RESULT_SKIPPED: |
| 761 | color = ANSI_GRAY ANSI_BOLD; |
| 762 | status = "skipped" ; |
| 763 | break; |
| 764 | case TEST_RESULT_FAILED: |
| 765 | color = ANSI_RED ANSI_BOLD; |
| 766 | status = "error" ; |
| 767 | break; |
| 768 | default: unreachable(); |
| 769 | } |
| 770 | printf(format: " %s%s" ANSI_RESET " in " ANSI_BOLD "%zu" ANSI_RESET |
| 771 | " ms" , |
| 772 | color, status, took); |
| 773 | if (singular_verdict.result == TEST_RESULT_SKIPPED) |
| 774 | printf( |
| 775 | format: " (reason: " ANSI_GRAY "%s" ANSI_RESET ")" , |
| 776 | test_skip_reason_to_str(stringify_in: singular_verdict.skip_reason)); |
| 777 | else if (singular_verdict.result == TEST_RESULT_FAILED && |
| 778 | singular_verdict.msg) |
| 779 | printf(format: " (" ANSI_RED "%s" ANSI_RESET ")" , |
| 780 | singular_verdict.msg); |
| 781 | |
| 782 | printf(format: "\n" ); |
| 783 | } |
| 784 | |
| 785 | struct test_verdict *worst = &singular_verdict; |
| 786 | for (size_t k = 0; k < run_times; k++) { |
| 787 | if (verdicts[k].result == TEST_RESULT_FAILED) { |
| 788 | worst = &verdicts[k]; |
| 789 | break; |
| 790 | } |
| 791 | } |
| 792 | |
| 793 | const char *status = test_status_plain(r: worst->result); |
| 794 | if (t->run_times > 1 && result_times[TEST_RESULT_FAILED] && |
| 795 | result_times[TEST_RESULT_FAILED] < run_times) |
| 796 | status = "flaky" ; |
| 797 | |
| 798 | ndjson_emit(test_result, .group = tg->name, |
| 799 | .tier = test_tier_plain(t->tier), .name = t->name, |
| 800 | .status = status, .duration_ms = took, |
| 801 | .reason = |
| 802 | worst->result == TEST_RESULT_SKIPPED |
| 803 | ? test_skip_reason_to_str(worst->skip_reason) |
| 804 | : NULL, |
| 805 | .msg = worst->msg, |
| 806 | .runs_requested = t->run_times > 1 ? t->run_times : 0, |
| 807 | .runs_attempted = t->run_times > 1 ? run_times : 0, |
| 808 | .runs_failed = result_times[TEST_RESULT_FAILED], |
| 809 | .runs_skipped = result_times[TEST_RESULT_SKIPPED]); |
| 810 | |
| 811 | bool has_msg = log_site_message_count(site: tctx.site) > 0; |
| 812 | bool of_interest = |
| 813 | result_times[TEST_RESULT_FAILED] || |
| 814 | (result_times[TEST_RESULT_SKIPPED] && t->run_times > 1) || |
| 815 | tctx.soft_fails; |
| 816 | bool show = test_global.show_output; |
| 817 | |
| 818 | if (has_msg && (of_interest || show)) { |
| 819 | test_harness_info("messages:\n" ); |
| 820 | log_dump_site(site: tctx.site); |
| 821 | } |
| 822 | |
| 823 | test_progress.done++; |
| 824 | if (result_times[TEST_RESULT_FAILED]) |
| 825 | test_progress.failed++; |
| 826 | else if (result_times[TEST_RESULT_SKIPPED] == run_times) |
| 827 | test_progress.skipped++; |
| 828 | |
| 829 | test_progress.test_started_ms = 0; |
| 830 | test_progress_paint(tg, tier: i, test_name: t->name, NULL); |
| 831 | |
| 832 | for (int j = 0; j < TEST_RESULT_MAX; j++) |
| 833 | result_totals.totals[i][j] += result_times[j]; |
| 834 | |
| 835 | vfree(verdicts); |
| 836 | if (result_times[TEST_RESULT_FAILED] && tg->incremental) { |
| 837 | stop_outer = true; |
| 838 | } else if (result_times[TEST_RESULT_FAILED] && tg->exit_on_fail) { |
| 839 | stop_outer = true; |
| 840 | break; |
| 841 | } |
| 842 | } |
| 843 | |
| 844 | vfree((void *) LOG_SITE(test_harness)->name); |
| 845 | } |
| 846 | |
| 847 | LOG_SITE(test_harness)->name = "test_harness" ; |
| 848 | for (int i = 0; i < TEST_TIER_MAX; i++) { |
| 849 | for (int j = 0; j < TEST_RESULT_MAX; j++) { |
| 850 | test_global.results[i][j] += result_totals.totals[i][j]; |
| 851 | result_aggregates[j] += result_totals.totals[i][j]; |
| 852 | } |
| 853 | } |
| 854 | |
| 855 | test_ndjson_info("group end: %s (duration %zu ms)" , tg->name, total_time); |
| 856 | |
| 857 | ndjson_emit(test_group_end, .group = tg->name, .duration_ms = total_time, |
| 858 | .failed = result_aggregates[TEST_RESULT_FAILED], |
| 859 | .skipped = result_aggregates[TEST_RESULT_SKIPPED]); |
| 860 | |
| 861 | if (!result_aggregates[TEST_RESULT_SKIPPED] && |
| 862 | !result_aggregates[TEST_RESULT_FAILED]) { |
| 863 | test_harness_info("Test group " ANSI_BLUE ANSI_BOLD "%s" ANSI_RESET |
| 864 | " " ANSI_GREEN ANSI_BOLD "successful" ANSI_RESET |
| 865 | " in " ANSI_BOLD "%zu" ANSI_RESET " ms\n\n\n" , |
| 866 | tg->name, total_time); |
| 867 | LOG_SITE(test_harness)->name = "test_harness" ; |
| 868 | } else { |
| 869 | test_harness_info("Test group " ANSI_BLUE ANSI_BOLD "%s" ANSI_RESET |
| 870 | " completed in " ANSI_BOLD "%zu" ANSI_RESET " ms, " , |
| 871 | tg->name, total_time); |
| 872 | |
| 873 | if (result_aggregates[TEST_RESULT_SKIPPED]) |
| 874 | printf(format: "%zu " ANSI_GRAY ANSI_BOLD "skipped" ANSI_RESET, |
| 875 | result_aggregates[TEST_RESULT_SKIPPED]); |
| 876 | |
| 877 | if (result_aggregates[TEST_RESULT_FAILED]) |
| 878 | printf(format: ", %zu " ANSI_RED ANSI_BOLD "failed" ANSI_RESET, |
| 879 | result_aggregates[TEST_RESULT_FAILED]); |
| 880 | |
| 881 | printf(format: "\n\n\n" ); |
| 882 | } |
| 883 | } |
| 884 | |
| 885 | static void test_global_aggregate_results(void) { |
| 886 | for (int i = 0; i < TEST_TIER_MAX; i++) { |
| 887 | for (int j = 0; j < TEST_RESULT_MAX; j++) { |
| 888 | test_global.results_agg[j] += test_global.results[i][j]; |
| 889 | } |
| 890 | } |
| 891 | } |
| 892 | |
| 893 | static bool sig_str_equal(const char *s1, const char *s2) { |
| 894 | while (*s1 && *s2) { |
| 895 | while (isspace(c: *s1)) |
| 896 | s1++; |
| 897 | |
| 898 | while (isspace(c: *s2)) |
| 899 | s2++; |
| 900 | |
| 901 | if (*s1 != *s2) |
| 902 | return false; |
| 903 | |
| 904 | if (*s1) { |
| 905 | s1++; |
| 906 | s2++; |
| 907 | } |
| 908 | } |
| 909 | |
| 910 | while (isspace(c: *s1)) |
| 911 | s1++; |
| 912 | |
| 913 | while (isspace(c: *s2)) |
| 914 | s2++; |
| 915 | |
| 916 | return *s1 == *s2; |
| 917 | } |
| 918 | |
| 919 | void test_verify_signatures(void) { |
| 920 | for (const struct test_signature_record *u = |
| 921 | __skernel_test_unsafe_signatures; |
| 922 | u < __ekernel_test_unsafe_signatures; u++) { |
| 923 | |
| 924 | for (const struct test_signature_record *c = |
| 925 | __skernel_test_canonical_signatures; |
| 926 | c < __ekernel_test_canonical_signatures; c++) { |
| 927 | |
| 928 | if (strcmp(str1: u->name, str2: c->name) == 0) { |
| 929 | bool ret_match = sig_str_equal(s1: u->ret_str, s2: c->ret_str); |
| 930 | bool args_match = sig_str_equal(s1: u->args_str, s2: c->args_str); |
| 931 | |
| 932 | if (!ret_match || !args_match) { |
| 933 | test_harness_warn( |
| 934 | "Unsafe import '%s' signature mismatch " |
| 935 | "at %s:%u\n" |
| 936 | " canonical: %s %s(%s) (defined at %s:%u)\n" |
| 937 | " imported : %s %s(%s)\n" , |
| 938 | u->name, u->file, u->line, c->ret_str, c->name, |
| 939 | c->args_str, c->file, c->line, u->ret_str, u->name, |
| 940 | u->args_str); |
| 941 | } |
| 942 | break; |
| 943 | } |
| 944 | } |
| 945 | } |
| 946 | } |
| 947 | |
| 948 | static void tests_resolve_imports(void) { |
| 949 | for (const struct test_export_entry *a = __skernel_test_exports; |
| 950 | a < __ekernel_test_exports; a++) { |
| 951 | for (const struct test_export_entry *b = a + 1; |
| 952 | b < __ekernel_test_exports; b++) { |
| 953 | if (strcmp(str1: a->name, str2: b->name) == 0) { |
| 954 | panic(DUPLICATE, |
| 955 | "'%s' exported more than once, use TEST_EXPORT_AS" , |
| 956 | a->name); |
| 957 | } |
| 958 | } |
| 959 | } |
| 960 | |
| 961 | for (const struct test_import_entry *imp = __skernel_test_imports; |
| 962 | imp < __ekernel_test_imports; imp++) { |
| 963 | any_fn_t resolved = {}; |
| 964 | |
| 965 | for (const struct test_export_entry *exp = __skernel_test_exports; |
| 966 | exp < __ekernel_test_exports; exp++) { |
| 967 | if (strcmp(str1: imp->name, str2: exp->name) == 0) { |
| 968 | resolved = exp->fn; |
| 969 | break; |
| 970 | } |
| 971 | } |
| 972 | |
| 973 | if (any_fn_is_null(resolved)) { |
| 974 | panic("unresolved symbol '%s' imported at %s:%u!" , imp->name, |
| 975 | imp->import_file, imp->import_line); |
| 976 | } |
| 977 | |
| 978 | *(imp->target_fn) = resolved; |
| 979 | } |
| 980 | } |
| 981 | |
| 982 | static void tests_set_intensities(void) { |
| 983 | for (struct test_group *tg = __skernel_test_groups; |
| 984 | tg < __ekernel_test_groups; tg++) { |
| 985 | if (tg->default_intensity == TEST_INTENSITY_SENTINEL) |
| 986 | tg->default_intensity = TEST_INTENSITY_DEFAULT; |
| 987 | |
| 988 | if (test_global.global_intensity != TEST_INTENSITY_SENTINEL) |
| 989 | tg->default_intensity = test_global.global_intensity; |
| 990 | } |
| 991 | |
| 992 | for (struct test *t = __skernel_tests; t < __ekernel_tests; t++) { |
| 993 | if (t->flags & TEST_FLAG_INHERITS_INTENSITY) { |
| 994 | t->flags |= TEST_FLAG_HONORS_INTENSITY; |
| 995 | if (t->intensity_desc.curve == SCALE_NONE) { |
| 996 | t->intensity_desc.curve = t->group->intensity_desc.curve; |
| 997 | t->intensity_desc.custom_scale = |
| 998 | t->group->intensity_desc.custom_scale; |
| 999 | t->intensity_desc.custom_print = |
| 1000 | t->group->intensity_desc.custom_print; |
| 1001 | |
| 1002 | if (t->intensity_desc.min_val == SIZE_MAX) |
| 1003 | t->intensity_desc.min_val = |
| 1004 | t->group->intensity_desc.min_val; |
| 1005 | |
| 1006 | if (t->intensity_desc.max_val == SIZE_MAX) |
| 1007 | t->intensity_desc.max_val = |
| 1008 | t->group->intensity_desc.max_val; |
| 1009 | |
| 1010 | if (t->intensity_desc.def_val == SIZE_MAX) |
| 1011 | t->intensity_desc.def_val = |
| 1012 | t->group->intensity_desc.def_val; |
| 1013 | } |
| 1014 | } |
| 1015 | |
| 1016 | if (t->intensity == TEST_INTENSITY_SENTINEL) { |
| 1017 | if (t->flags & TEST_FLAG_INHERITS_INTENSITY) { |
| 1018 | t->intensity = t->group->default_intensity; |
| 1019 | } else { |
| 1020 | t->intensity = TEST_INTENSITY_DEFAULT; |
| 1021 | } |
| 1022 | } |
| 1023 | |
| 1024 | if (test_global.global_intensity != TEST_INTENSITY_SENTINEL) |
| 1025 | t->intensity = test_global.global_intensity; |
| 1026 | } |
| 1027 | } |
| 1028 | |
| 1029 | void tests_run(void) { |
| 1030 | #ifdef TEST_ENABLED |
| 1031 | tests_resolve_imports(); |
| 1032 | tests_check_duplicate_names(); |
| 1033 | tests_setup_groups(); |
| 1034 | tests_apply_filters(); |
| 1035 | tests_set_intensities(); |
| 1036 | tests_set_enabled_states(); |
| 1037 | |
| 1038 | bool all_ok = true; |
| 1039 | char *msg = all_ok ? "all tests pass 🎉!" : "some errors occurred" ; |
| 1040 | char *color = all_ok ? ANSI_GREEN : ANSI_RED; |
| 1041 | |
| 1042 | test_harness_info("Running " ANSI_BOLD "%zu" ANSI_RESET " tests:\n" , |
| 1043 | test_global.total_tests_enabled); |
| 1044 | |
| 1045 | ndjson_emit(test_begin, .declared_total = test_global.total_tests_enabled); |
| 1046 | |
| 1047 | if (!test_global.no_progress) { |
| 1048 | test_progress.total = tests_count_planned(); |
| 1049 | test_progress.started_ms = time_get_ms(); |
| 1050 | status_bar_open(); |
| 1051 | status_bar_progress_timed(done: 0, total: test_progress.total, test_started_ms: 0, |
| 1052 | total_started_ms: test_progress.started_ms, detail_fmt: "starting" ); |
| 1053 | } |
| 1054 | |
| 1055 | for (struct test_group *tg = __skernel_test_groups; |
| 1056 | tg < __ekernel_test_groups; tg++) |
| 1057 | test_group_run(tg); |
| 1058 | |
| 1059 | status_bar_close(); |
| 1060 | test_global_aggregate_results(); |
| 1061 | |
| 1062 | size_t fail_count = test_global.results_agg[TEST_RESULT_FAILED]; |
| 1063 | size_t skip_count = test_global.results_agg[TEST_RESULT_SKIPPED]; |
| 1064 | size_t pass_count = test_global.results_agg[TEST_RESULT_OK]; |
| 1065 | time_ms_t total_time = test_global.total_time; |
| 1066 | all_ok = fail_count == 0; |
| 1067 | color = all_ok ? ANSI_GREEN : ANSI_RED; |
| 1068 | msg = all_ok ? "all tests pass 🎉!" : "some errors occurred" ; |
| 1069 | char *fail_color = all_ok ? ANSI_GREEN : ANSI_RED; |
| 1070 | char *skip_color = all_ok ? ANSI_GREEN : ANSI_GRAY; |
| 1071 | |
| 1072 | test_harness_info("%zu " ANSI_CYAN "total" ANSI_RESET |
| 1073 | " tests, %zu " ANSI_GREEN "passed" ANSI_RESET |
| 1074 | ", %zu %sfailed" ANSI_RESET ", %zu %sskipped" ANSI_RESET |
| 1075 | "\n" , |
| 1076 | test_global.total_tests_enabled, pass_count, fail_count, |
| 1077 | fail_color, skip_count, skip_color); |
| 1078 | |
| 1079 | test_harness_info("%s%s" ANSI_RESET " (%lu ms)\n" , color, msg, total_time); |
| 1080 | |
| 1081 | ndjson_emit(test_totals, .total = test_global.total_tests_enabled, |
| 1082 | .passed = pass_count, .failed = fail_count, |
| 1083 | .skipped = skip_count); |
| 1084 | ndjson_emit(test_verdict, .ok = all_ok, .duration_ms = total_time); |
| 1085 | |
| 1086 | /* Give it the return code */ |
| 1087 | if (!test_global.no_exit) { |
| 1088 | int code = all_ok ? TEST_EXIT_OK : TEST_EXIT_FAIL; |
| 1089 | ndjson_bye(code, reason: all_ok ? "tests passed" : "tests failed" ); |
| 1090 | qemu_exit(code); |
| 1091 | } |
| 1092 | #endif |
| 1093 | } |
| 1094 | |