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 */
28CMDLINE_DECLARE(test_root, .name = "test", .flags = CMDLINE_ENTRY_SYMBOLIC);
29
30LINKER_SECTION_OBJECT(struct test_group, test_groups)
31test_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
37CMDLINE_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
60NDJSON_DECLARE(test_begin, NDJSON_SECTION_TEST, NDJSON_KIND_BEGIN, 1,
61 NDJSON_U64(declared_total));
62
63NDJSON_DECLARE(test_group_start, NDJSON_SECTION_TEST, NDJSON_KIND_GROUP_START,
64 1, NDJSON_STR(group), NDJSON_U64(test_count), NDJSON_STR(file));
65
66NDJSON_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
73NDJSON_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
77NDJSON_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
81NDJSON_DECLARE(test_verdict, NDJSON_SECTION_TEST, NDJSON_KIND_VERDICT, 1,
82 NDJSON_BOOL(ok), NDJSON_U64(duration_ms));
83
84static 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
93static 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
102LOG_SITE_DECLARE_PRINT(test_harness);
103LOG_HANDLE_DECLARE_PRINT(test_harness,
104 .flags = LOG_HANDLE_PRINT | LOG_HANDLE_NO_NEWLINE);
105
106LOG_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);
109LOG_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
145LINKER_SECTION_DEFINE(struct test, tests);
146LINKER_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 */
150struct test_globals test_global = {.global_intensity = TEST_INTENSITY_SENTINEL};
151
152static 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
181static 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
196CMDLINE_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
215CMDLINE_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
228static 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
287struct test_dup_item {
288 char *name; /* Points to the test or test_group->name */
289 uint32_t hash; /* hash_murmur3_32() */
290};
291
292static 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
304static 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
328static 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
392static 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
421static 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
448struct test_group_result {
449 size_t totals[TEST_TIER_MAX][TEST_RESULT_MAX];
450};
451
452static 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
461static 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
478static 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
503static 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
511static 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
517static 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
855static 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
863static 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
883void 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
912static 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
944static 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
991void 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