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