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 */
30CMDLINE_DEFINE(test_root, .name = "test", .flags = CMDLINE_ENTRY_SYMBOLIC);
31
32LINKER_SECTION_OBJECT(struct test_group, test_groups)
33test_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
39CMDLINE_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
62NDJSON_DEFINE(test_begin, NDJSON_SECTION_TEST, NDJSON_KIND_BEGIN, 1,
63 NDJSON_U64(declared_total));
64
65NDJSON_DEFINE(test_group_start, NDJSON_SECTION_TEST, NDJSON_KIND_GROUP_START, 1,
66 NDJSON_STR(group), NDJSON_U64(test_count), NDJSON_STR(file));
67
68NDJSON_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
75NDJSON_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
79NDJSON_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
83NDJSON_DEFINE(test_verdict, NDJSON_SECTION_TEST, NDJSON_KIND_VERDICT, 1,
84 NDJSON_BOOL(ok), NDJSON_U64(duration_ms));
85
86static 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
95static 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
104LOG_SITE_DEFINE_PRINT(test_harness);
105LOG_HANDLE_DEFINE_PRINT(test_harness,
106 .flags = LOG_HANDLE_PRINT | LOG_HANDLE_NO_NEWLINE);
107
108LOG_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);
111LOG_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
147LINKER_SECTION_EXTERN(struct test, tests);
148LINKER_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 */
152struct test_globals test_global = {.global_intensity = TEST_INTENSITY_SENTINEL};
153
154static 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
183static 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
198CMDLINE_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
217CMDLINE_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
230static 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
244static 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
303struct test_dup_item {
304 char *name; /* Points to the test or test_group->name */
305 uint32_t hash; /* hash_murmur3_32() */
306};
307
308static 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
320static 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
344static 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
408static 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
437static 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
463struct test_group_result {
464 size_t totals[TEST_TIER_MAX][TEST_RESULT_MAX];
465};
466
467static 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
476static 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
493static 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
518static 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
526static 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
532static 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
885static 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
893static 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
919void 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
948static 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
982static 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
1029void 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