diff --git a/include/ktest.h b/include/ktest.h new file mode 100644 index 0000000..c3a9667 --- /dev/null +++ b/include/ktest.h @@ -0,0 +1,82 @@ +#ifndef FROSTBYTE_KTEST_H +#define FROSTBYTE_KTEST_H + +#include "types.h" + +/* + * In-kernel self-test harness (KUnit-style). + * + * A test is written as: + * + * #include "ktest.h" + * + * KTEST(mySubsystemBasics) + * { + * KT_EQ(t, 2 + 2, 4); + * KT_TRUE(t, somePredicate()); + * } + * + * The KTEST macro both declares the test body and registers a pointer to it + * into the ".ktests" linker section, so no shared registry file has to be + * edited when a new test is added. ktestRunAll() (called once from kmain + * before the scheduler starts) iterates the section, runs every test, prints + * per-test results and a final machine-readable line: + * + * KTEST SUMMARY: tests, checks, failed + * + * The boot gate (tools/ci-test.sh) requires that line to report 0 failures, + * so a broken subsystem makes the gate fail instead of silently passing. + * + * Tests run with interrupts disabled, before the scheduler starts. They must + * be deterministic and must NOT depend on timer ticks, preemption or user + * threads. They may use kmalloc/kfree, the string helpers, pmm and any + * subsystem they explicitly initialise themselves. + */ + +typedef struct KtestCtx { + const char* name; + uint64 checks; + uint64 failures; +} KtestCtx; + +typedef void (*KtestFn)(KtestCtx* t); + +typedef struct KtestEntry { + const char* name; + KtestFn fn; +} KtestEntry; + +/* Runs every registered test, prints results, returns the number of failed + checks across all tests (0 means the whole battery is green). */ +uint64 ktestRunAll(void); + +/* Records one check. On failure it prints "FAIL file:line expr" and marks + the current test context as failed. Returns the boolean result so callers + can branch (e.g. skip a dereference after a null check failed). */ +bool ktestCheck(KtestCtx* t, bool ok, const char* expr, const char* file, int line); + +/* Null-safe string comparison used by KT_STREQ. */ +bool ktestStrEq(const char* a, const char* b); + +#define KTEST(testName) \ + static void testName##KtestFn(KtestCtx* t); \ + static const KtestEntry testName##KtestEntry = { #testName, testName##KtestFn }; \ + __attribute__((section(".ktests"), used)) \ + static const KtestEntry* const testName##KtestPtr = &testName##KtestEntry; \ + static void testName##KtestFn(KtestCtx* t) + +#define KT_TRUE(t, cond) ktestCheck((t), (cond) ? true : false, #cond, __FILE__, __LINE__) +#define KT_FALSE(t, cond) ktestCheck((t), (cond) ? false : true, "!(" #cond ")", __FILE__, __LINE__) +#define KT_EQ(t, a, b) ktestCheck((t), (int64)(a) == (int64)(b), #a " == " #b, __FILE__, __LINE__) +#define KT_NE(t, a, b) ktestCheck((t), (int64)(a) != (int64)(b), #a " != " #b, __FILE__, __LINE__) +#define KT_LT(t, a, b) ktestCheck((t), (int64)(a) < (int64)(b), #a " < " #b, __FILE__, __LINE__) +#define KT_LE(t, a, b) ktestCheck((t), (int64)(a) <= (int64)(b), #a " <= " #b, __FILE__, __LINE__) +#define KT_GT(t, a, b) ktestCheck((t), (int64)(a) > (int64)(b), #a " > " #b, __FILE__, __LINE__) +#define KT_GE(t, a, b) ktestCheck((t), (int64)(a) >= (int64)(b), #a " >= " #b, __FILE__, __LINE__) +#define KT_EQU(t, a, b) ktestCheck((t), (uint64)(a) == (uint64)(b), #a " ==u " #b, __FILE__, __LINE__) +#define KT_EQP(t, a, b) ktestCheck((t), (const void*)(a) == (const void*)(b), #a " ==p " #b, __FILE__, __LINE__) +#define KT_NULL(t, p) ktestCheck((t), (const void*)(p) == nullptr, #p " == null", __FILE__, __LINE__) +#define KT_NOTNULL(t, p) ktestCheck((t), (const void*)(p) != nullptr, #p " != null", __FILE__, __LINE__) +#define KT_STREQ(t, a, b) ktestCheck((t), ktestStrEq((a), (b)), #a " streq " #b, __FILE__, __LINE__) + +#endif