diff --git a/boot/kernel.ld b/boot/kernel.ld index 7d08952..5c0606a 100644 --- a/boot/kernel.ld +++ b/boot/kernel.ld @@ -50,6 +50,11 @@ SECTIONS KEEP(*(.initgraph.edges)) __ld_initgraph_edges_end = .; + . = ALIGN(8); + __ld_ktest_start = .; + KEEP(*(.ktest)) + __ld_ktest_end = .; + *(.got .got.*) __ld_rodata_end = .; diff --git a/include/zinnia/ktest.h b/include/zinnia/ktest.h new file mode 100644 index 0000000..38f0451 --- /dev/null +++ b/include/zinnia/ktest.h @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: ISC + +#pragma once + +#include +#include +#include // IWYU pragma: keep +#include // IWYU pragma: keep + +struct ktest { + const char* suite; + const char* name; + bool failed; + bool skipped; + const char* skip_reason; +}; + +struct ktest_case { + const char* suite; + const char* name; + void (*func)(struct ktest* test); +}; + +#define KTEST(suite_name, case_name) \ + static void ktest_##suite_name##_##case_name(struct ktest* test); \ + [[__used, __section(".ktest"), gnu::aligned(8)]] \ + static const struct ktest_case ktest_##suite_name##_##case_name##_case = { \ + .suite = #suite_name, \ + .name = #case_name, \ + .func = ktest_##suite_name##_##case_name, \ + }; \ + static void ktest_##suite_name##_##case_name(struct ktest* test) + +[[gnu::format(printf, 4, 5)]] +void ktest_fail(struct ktest* test, const char* file, int line, const char* fmt, ...); + +void ktest_main(); + +#define _KTEST_CHECK(cond, on_fail, ...) \ + do { \ + if (!(cond)) { \ + ktest_fail(test, __FILE__, __LINE__, __VA_ARGS__); \ + on_fail; \ + } \ + } while (0) + +#define _KTEST_CMP(a, op, b, on_fail) \ + do { \ + const typeof(true ? (a) : (b)) _ktest_a = (a); \ + const typeof(true ? (a) : (b)) _ktest_b = (b); \ + _KTEST_CHECK( \ + _ktest_a op _ktest_b, \ + on_fail, \ + "expected %s %s %s, got %#llx and %#llx", \ + #a, \ + #op, \ + #b, \ + (unsigned long long)_ktest_a, \ + (unsigned long long)_ktest_b \ + ); \ + } while (0) + +#define _KTEST_STREQ(a, b, on_fail) \ + do { \ + const char* _ktest_a = (a); \ + const char* _ktest_b = (b); \ + _KTEST_CHECK( \ + _ktest_a && _ktest_b && !strcmp(_ktest_a, _ktest_b), \ + on_fail, \ + "expected %s == %s, got \"%s\" and \"%s\"", \ + #a, \ + #b, \ + _ktest_a ? _ktest_a : "(null)", \ + _ktest_b ? _ktest_b : "(null)" \ + ); \ + } while (0) + +#define KTEST_EXPECT_TRUE(cond) _KTEST_CHECK(cond, (void)0, "expected %s", #cond) +#define KTEST_EXPECT_FALSE(cond) _KTEST_CHECK(!(cond), (void)0, "expected !(%s)", #cond) +#define KTEST_EXPECT_EQ(a, b) _KTEST_CMP(a, ==, b, (void)0) +#define KTEST_EXPECT_NE(a, b) _KTEST_CMP(a, !=, b, (void)0) +#define KTEST_EXPECT_LT(a, b) _KTEST_CMP(a, <, b, (void)0) +#define KTEST_EXPECT_LE(a, b) _KTEST_CMP(a, <=, b, (void)0) +#define KTEST_EXPECT_GT(a, b) _KTEST_CMP(a, >, b, (void)0) +#define KTEST_EXPECT_GE(a, b) _KTEST_CMP(a, >=, b, (void)0) +#define KTEST_EXPECT_STREQ(a, b) _KTEST_STREQ(a, b, (void)0) +#define KTEST_EXPECT_NULL(ptr) _KTEST_CHECK((ptr) == nullptr, (void)0, "expected %s to be null", #ptr) +#define KTEST_EXPECT_NOT_NULL(ptr) _KTEST_CHECK((ptr) != nullptr, (void)0, "expected %s to be non-null", #ptr) + +#define KTEST_ASSERT_TRUE(cond) _KTEST_CHECK(cond, return, "expected %s", #cond) +#define KTEST_ASSERT_FALSE(cond) _KTEST_CHECK(!(cond), return, "expected !(%s)", #cond) +#define KTEST_ASSERT_EQ(a, b) _KTEST_CMP(a, ==, b, return) +#define KTEST_ASSERT_NE(a, b) _KTEST_CMP(a, !=, b, return) +#define KTEST_ASSERT_LT(a, b) _KTEST_CMP(a, <, b, return) +#define KTEST_ASSERT_LE(a, b) _KTEST_CMP(a, <=, b, return) +#define KTEST_ASSERT_GT(a, b) _KTEST_CMP(a, >, b, return) +#define KTEST_ASSERT_GE(a, b) _KTEST_CMP(a, >=, b, return) +#define KTEST_ASSERT_STREQ(a, b) _KTEST_STREQ(a, b, return) +#define KTEST_ASSERT_NULL(ptr) _KTEST_CHECK((ptr) == nullptr, return, "expected %s to be null", #ptr) +#define KTEST_ASSERT_NOT_NULL(ptr) _KTEST_CHECK((ptr) != nullptr, return, "expected %s to be non-null", #ptr) + +#define KTEST_SKIP(reason) \ + do { \ + (test)->skipped = true; \ + (test)->skip_reason = (reason); \ + return; \ + } while (0) diff --git a/lib/main.c b/lib/main.c index e1f377b..29cefd0 100644 --- a/lib/main.c +++ b/lib/main.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +49,10 @@ static void kernel_main_task(uintptr_t arg0) { // Load all initramfs files. initramfs_load_boot_modules(info); +#if CONFIG_KTEST + ktest_main(); +#endif + const char* init_argv[] = {init_path, nullptr}; const char* init_envp[] = {"HOME=/", "TERM=console", nullptr}; diff --git a/meson.build b/meson.build index d180462..4fc969c 100644 --- a/meson.build +++ b/meson.build @@ -105,6 +105,10 @@ subdir('syscalls') subdir('dev') subdir('fs') +if kconfig.get('CONFIG_KTEST', 'n') == 'y' + subdir('tests/kernel') +endif + configure_file( configuration: cfg, output: 'config.h', diff --git a/tests/kernel/example.c b/tests/kernel/example.c new file mode 100644 index 0000000..8e0fc50 --- /dev/null +++ b/tests/kernel/example.c @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: ISC + +#include + +KTEST(example, arithmetic) { + KTEST_EXPECT_EQ(1 + 1, 2); + KTEST_EXPECT_LT(1, 2); +} diff --git a/tests/kernel/meson.build b/tests/kernel/meson.build new file mode 100644 index 0000000..0bbc9ec --- /dev/null +++ b/tests/kernel/meson.build @@ -0,0 +1,4 @@ +kernel_sources += files( + 'example.c', + 'runner.c', +) diff --git a/tests/kernel/runner.c b/tests/kernel/runner.c new file mode 100644 index 0000000..52080a6 --- /dev/null +++ b/tests/kernel/runner.c @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: ISC + +#include +#include +#include +#include + +extern const struct ktest_case __ld_ktest_start[]; +extern const struct ktest_case __ld_ktest_end[]; + +static bool ktest_enabled = false; + +static void ktest_option(bool value) { + ktest_enabled = value; +} +CMDLINE_OPTION("ktest", "Run all kernel tests", ktest_option); + +void ktest_fail(struct ktest* test, const char* file, int line, const char* fmt, ...) { + char buf[256]; + va_list args; + va_start(args, fmt); + kvsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + test->failed = true; + log("# %s.%s: %s:%d: %s\n", test->suite, test->name, file, line, buf); +} + +void ktest_main() { + if (!ktest_enabled) + return; + + size_t index = 0; + for (const struct ktest_case* c = __ld_ktest_start; c < __ld_ktest_end; c++) { + struct ktest test = {.suite = c->suite, .name = c->name}; + c->func(&test); + + index++; + if (test.failed) + log("not ok %zu %s.%s\n", index, c->suite, c->name); + else if (test.skipped) + log("ok %zu %s.%s # SKIP %s\n", index, c->suite, c->name, test.skip_reason ? test.skip_reason : ""); + else + log("ok %zu %s.%s\n", index, c->suite, c->name); + } + + log("1..%zu\n", index); +} diff --git a/tests/meson.build b/tests/meson.build index 477e1af..8dafb77 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -86,6 +86,10 @@ qemu = [ ] test_images = { + 'kernel': { + 'cmdline': 'ktest=1', + 'files': [], + }, 'user': { 'cmdline': 'init=/init initrd=/initramfs.tar', 'files': [test_initramfs],