-
Notifications
You must be signed in to change notification settings - Fork 69
Unit Testing
MentOS has two separate test suites, and it is important not to confuse them:
| Kernel tests | Userspace tests | |
|---|---|---|
| Where | kernel/src/tests/unit/ |
userspace/tests/ |
| What they are | C functions compiled into the kernel | ordinary MentOS programs |
| When they run | during kmain(), before userspace starts |
after boot, driven by /bin/runtests
|
| Enabled by | -DENABLE_KERNEL_TESTS=ON |
always built; run in test mode |
| How many | 15 subsystem suites | 46 test programs |
| Failure mode | kernel_panic() |
non-zero exit reported by the harness |
This page is about the kernel suite. For the userspace suite, see the section The userspace test suite near the end.
The kernel unit testing framework lets developers verify kernel functionality during boot without disrupting the OS or corrupting critical kernel state.
The Golden Rule: "Leave the kitchen as you found it." Tests must never modify kernel structures. If they do, the OS becomes unstable or fails to boot.
Testing during kernel initialization has several advantages:
- ✅ All kernel subsystems are already initialized and stable
- ✅ Test infrastructure runs in a controlled environment
- ✅ Easy to detect initialization failures early
- ✅ No need for complex userspace test harnesses
- ✅ Tests don't interfere with running system
Tests live in kernel/src/tests/unit/. Create a new file like test_myfeature.c:
/// @file test_myfeature.c
/// @brief Tests for my kernel feature
/// @copyright (c) 2024 See LICENSE.md
// Set up per-file logging BEFORE any other include.
#include "sys/kernel_levels.h"
#define __DEBUG_HEADER__ "[TUNIT ]"
#define __DEBUG_LEVEL__ LOGLEVEL_NOTICE
#include "io/debug.h"
#include "tests/test.h"
#include "tests/test_utils.h"
#include "my_feature.h"
TEST(my_feature_initialization)
{
TEST_SECTION_START("Verify my feature initialized correctly");
// Your test code here
ASSERT(some_condition);
TEST_SECTION_END();
}
/// @brief Aggregator: the single entry point the runner calls.
void test_myfeature(void)
{
test_my_feature_initialization();
// ...call every TEST() in this file...
}TEST(name) simply expands to void test_##name(void) — it does not register anything.
Every test should have this structure:
TEST(descriptive_test_name)
{
// 1. Start section - explains what you're testing
TEST_SECTION_START("What are you verifying?");
// 2. Setup - prepare data for testing
my_structure_t copy;
ASSERT(test_get_safe_copy(1, ©) == 0);
// 3. Verify - use assertions with messages
ASSERT_MSG(copy.field != 0, "Field should be initialized");
// 4. End section - marks test complete
TEST_SECTION_END();
}Kernel tests are not auto-discovered. kernel/src/tests/runner.c holds an explicit registry,
and you must add two lines to it:
// 1. Declare your aggregator alongside the others
extern void test_myfeature(void);
// 2. Add one entry to the registry
static const test_entry_t test_functions[] = {
{test_gdt, "GDT Subsystem"},
// ...
{test_myfeature, "My Feature Subsystem"},
};kernel_run_tests() then walks that array in order. The comment block above the extern
declarations in runner.c documents the same five steps.
cd build
cmake ..
make
make qemu # Kernel will run tests during bootRegistered suites are executed near the end of kernel initialization. Output appears in the
kernel log with the [TUNIT ] prefix.
To enable/disable tests at build time (they are OFF by default — see kernel/CMakeLists.txt):
cmake .. -DENABLE_KERNEL_TESTS=ON # Enable tests
cmake .. -DENABLE_KERNEL_TESTS=OFF # Disable tests (faster boot)TEST(structure_is_initialized)
{
TEST_SECTION_START("IDT exception entries");
// Check that critical entries are set up
for (int i = 0; i < 32; i++) {
ASSERT_MSG((idt_table[i].options & 0x80) != 0,
"Exception entry not initialized");
}
TEST_SECTION_END();
}TEST(verify_field_structure)
{
TEST_SECTION_START("GDT descriptor field layout");
// Copy and inspect
gdt_descriptor_t entry;
ASSERT(gdt_safe_copy(1, &entry) == 0);
// Verify sizes
ASSERT(sizeof(gdt_descriptor_t) == 8);
// Verify field values
ASSERT_MSG((entry.access & 0x80) != 0,
"Present bit should be set");
TEST_SECTION_END();
}TEST(verify_relationships)
{
TEST_SECTION_START("Descriptor segment relationships");
idt_descriptor_t isr;
ASSERT(idt_safe_copy(0, &isr) == 0);
// Verify expected relationships
ASSERT_MSG(isr.seg_selector == 0x8 || isr.seg_selector == 0x10,
"Should use kernel code segment");
TEST_SECTION_END();
}TEST(verify_constants)
{
TEST_SECTION_START("Kernel constants");
ASSERT(GDT_SIZE > 0);
ASSERT(IDT_SIZE == 256);
ASSERT(INT32_GATE == 0xE);
ASSERT(TRAP32_GATE == 0xF);
TEST_SECTION_END();
}-
Use
TEST_SECTION_START()andTEST_SECTION_END()to document intentTEST_SECTION_START("Verify kernel code segment"); // test code TEST_SECTION_END();
-
Use
ASSERT_MSG()with descriptive messages for clarityASSERT_MSG(value != 0, "Expected a non-zero value");
-
Copy first, then test - never modify real structures
gdt_descriptor_t copy; gdt_safe_copy(1, ©); // Test the copy, not the real GDT
-
Isolate tests - each test should be independent
// Each test cleans up after itself // No dependencies between tests
-
Test logical properties not implementation details
// ✓ Good: Verify the result makes sense uint32_t base = extract_base_address(©); ASSERT_MSG(base == 0, "Kernel base should be 0"); // ✗ Bad: Too specific to implementation // ASSERT(copy.base_low == 0 && copy.base_middle == 0 && ...);
-
Never modify real kernel structures
// ✗ BAD gdt_set_gate(1, 0x1000, 0x2000, 0x9A, 0xCF); // Breaks real GDT! // ✓ GOOD gdt_descriptor_t copy; gdt_safe_copy(1, ©); // Safe read
-
Never trigger exceptions/interrupts during tests
// ✗ BAD int x = 1 / 0; // Triggers actual exception // ✓ GOOD ASSERT(idt_table[0].offset_low != 0); // Just verify structure
-
Never use assertions with side effects
// ✗ BAD if (gdt_set_gate(5, 0, 0, 0, 0) != 0) { // Modifies GDT! ASSERT(0); } // ✓ GOOD int result = test_validate_bounds(5); ASSERT(result == 0);
-
Don't modify interrupt handlers during tests
// ✗ BAD __idt_set_gate(10, 0xDEADBEEF, 0x8, 0xE, 0); // Breaks interrupts // ✓ GOOD idt_descriptor_t copy; idt_safe_copy(10, ©); // Safe read
-
Don't write tests that crash kernel
// ✗ BAD gdt_set_gate(0, 0, 0, 0, 0); // Modifies null descriptor - crashes! // ✓ GOOD gdt_descriptor_t copy; gdt_safe_copy(0, ©); // Safe inspection
// Mark test section for documentation (kernel/inc/tests/test_utils.h)
TEST_SECTION_START("Section description");
TEST_SECTION_END();
// Assert with descriptive message (recommended) (test_utils.h)
ASSERT_MSG(condition, "A plain string explaining what went wrong");
// Basic assert without message (test.h)
ASSERT(condition);
ASSERT_MSGis not printf-style. Its second argument is a plain string, passed straight topr_emerg("... %s\n", ..., msg). WritingASSERT_MSG(v != 0, "Expected non-zero, got %d", v)will not compile — the macro takes exactly two arguments. If you need a value in the message, log it separately first:pr_notice("value = %d\n", v); ASSERT_MSG(v != 0, "Expected a non-zero value");
Both assertion macros call kernel_panic("Test failure") when the condition is false, so a
failing kernel test halts the boot rather than being reported and skipped.
The generic helpers in kernel/inc/tests/test_utils.h:
// Compare two memory regions
static inline int test_memcmp(const void *p1, const void *p2, size_t size, const char *description)
// Returns: 1 if equal, 0 if different
// Check if a memory region is zeroed
static inline int test_is_zeroed(const void *ptr, size_t size, const char *description)
// Returns: 1 if all zeros, 0 if not
// Bounds checking helper
static inline int test_bounds_check(uint32_t value, uint32_t min, uint32_t max, const char *description)
// Returns: 1 if in range, 0 if out of rangeSafe-copy helpers are per-suite, not shared. There is no global test_gdt_safe_copy() in
the framework; test_gdt.c defines its own static inline gdt_safe_copy(), and test_idt.c
defines idt_safe_copy(). Follow the same pattern in your own suite: declare the structure you
want to inspect extern, and write a small bounds-checked copy helper next to your tests.
The test called ASSERT_MSG() with a failed condition. Check the error message in kernel output.
make qemu 2>&1 | grep ASSERTA test may be in an infinite loop or waiting forever. Check:
- Are you triggering an exception? (Don't!)
- Are you calling blocking operations? (Avoid!)
- Is the test code correct? (Review logic!)
Check that:
-
ENABLE_KERNEL_TESTSisONin CMake (it isOFFby default) - Test file is in
kernel/src/tests/unit/ - Test function matches pattern
TEST(name) -
Your aggregator
test_<suite>()is declaredexternand listed intest_functions[]inkernel/src/tests/runner.c— this is the step that is easiest to forget, and it fails silently: the suite is compiled but never called
Here's a comprehensive example test following all best practices:
/// @file test_example_complete.c
/// @brief Example unit test demonstrating best practices
#include "tests/test.h"
#include "tests/test_utils.h"
#include "descriptor_tables.h"
/// Test GDT initialization is correct
TEST(gdt_comprehensive_verification)
{
TEST_SECTION_START("GDT structure and initialization");
// Part 1: Verify structure is the right size
ASSERT_MSG(sizeof(gdt_descriptor_t) == 8,
"GDT descriptor must be 8 bytes");
// Part 2: Verify null descriptor is present
gdt_descriptor_t null_desc;
ASSERT(gdt_safe_copy(0, &null_desc) == 0);
ASSERT_MSG(null_desc.access == 0x00,
"Null descriptor must be all zeros");
// Part 3: Verify kernel code segment
gdt_descriptor_t code_desc;
ASSERT(gdt_safe_copy(1, &code_desc) == 0);
// Check present bit (bit 7)
ASSERT_MSG((code_desc.access & 0x80) != 0,
"Code segment present bit must be set");
// Check privilege level (bits 5-6)
uint8_t dpl = (code_desc.access & 0x60) >> 5;
ASSERT_MSG(dpl == 0, "Kernel code DPL should be 0");
// Verify base address (should be 0 for kernel)
uint32_t base = (code_desc.base_high << 24) |
(code_desc.base_middle << 16) |
(code_desc.base_low);
ASSERT_MSG(base == 0, "Kernel code segment base should be 0");
// Verify limit is valid
uint32_t limit = ((code_desc.granularity & 0x0F) << 16) |
(code_desc.limit_low);
ASSERT_MSG(limit > 0 && limit <= 0xFFFFF, "Limit should be valid");
TEST_SECTION_END();
}
/// Test GDT bounds checking
TEST(gdt_bounds_verification)
{
TEST_SECTION_START("GDT bounds and API");
// Verify out-of-bounds access is rejected
ASSERT_MSG(gdt_safe_copy(GDT_SIZE, NULL) == -1,
"Should reject index >= GDT_SIZE");
ASSERT_MSG(gdt_safe_copy(99999, NULL) == -1,
"Should reject large invalid index");
// Verify NULL destination is handled
ASSERT_MSG(gdt_safe_copy(1, NULL) == -1,
"Should reject NULL destination buffer");
TEST_SECTION_END();
}Tests execute automatically during kernel boot in kmain():
1. Kernel initialization complete (FPU and signals are the last subsystems)
2. kmain() calls kernel_run_tests(), guarded by #ifdef ENABLE_KERNEL_TESTS
3. Each registered aggregator in test_functions[] is called in order
4. Results printed to kernel log with the [TUNIT ] prefix
5. If all pass: kmain() continues and jumps into the init process
6. If any assertion fails: kernel_panic("Test failure") — the boot stops there
Note the placement: tests run after every subsystem is up but before the first userspace process starts, so they can inspect a fully initialized kernel without any user process existing yet.
kernel/
├── inc/tests/
│ ├── test.h # TEST() and ASSERT()
│ └── test_utils.h # TEST_SECTION_*, ASSERT_MSG, comparison helpers
├── src/tests/
│ ├── runner.c # Explicit test registry + kernel_run_tests()
│ └── unit/ # 15 suites:
│ ├── test_gdt.c test_idt.c test_isr.c
│ ├── test_paging.c test_scheduler.c test_vmem.c
│ ├── test_zone_allocator.c test_slab.c test_buddy.c
│ ├── test_mm.c test_page.c test_dma.c
│ └── test_memory_adversarial.c test_fpu.c test_vfs.cThe second suite lives in userspace/tests/ and is completely separate from the kernel one.
Each test is an ordinary MentOS program, built exactly like anything in userspace/bin/ and
installed into filesystem/bin/tests/.
Counts, and where they come from:
-
TEST_LISTinuserspace/tests/CMakeLists.txtcurrently builds 46 test executables. -
all_tests[]inuserspace/bin/runtests.ccurrently runs 45 of them —t_big_writeis commented out of the run list while still being built.
Both lists are the authoritative source; prefer citing them over any number written here.
There is no test runner inside the kernel for these. Instead:
- The
cdrom_test.isoCMake target builds an ISO usingiso/boot/grub/grub.cfg.runtests, whose GRUB entry ismultiboot /boot/bootloader.bin runtests. -
kmain()sees that command line and creates/bin/runtestsinstead of/bin/init. -
/bin/runtestsforks and execs each test inall_tests[], collects the exit statuses, and writes its report to a second serial port (COM2,0x2F8). - When finished it writes to QEMU's
isa-debug-exitdevice (port0x501) so that QEMU exits with a status the harness can read.
cd build
cmake .. -DEMULATOR_OUTPUT_TYPE=OUTPUT_LOG
make qemu-testqemu-test invokes scripts/run-qemu-test, which runs QEMU under a timeout and produces two
logs in the build directory:
| File | Contents |
|---|---|
build/serial.log |
kernel messages (COM1) — this is where pr_* output lands |
build/test.log |
the test report from /bin/runtests (COM2) |
When a test fails, read test.log first to see which test, then serial.log for the kernel
side of the story.
- Write
userspace/tests/t_myfeature.cas a normal program returning 0 on success. - Add
t_myfeature.ctoTEST_LISTinuserspace/tests/CMakeLists.txt. - Add
"t_myfeature"toall_tests[]inuserspace/bin/runtests.c— building it is not enough for it to run.
The GitHub Actions workflows are the practical reference for what MentOS is expected to build and pass on:
-
.github/workflows/ubuntu.yml— builds with GCC 14 and Clang 19, then runs theqemu-testtarget and archivestest.logandserial.logas artifacts. -
.github/workflows/compiler-compatibility.yml— a build-only matrix across GCC 12, 13, 14 and Clang 17, 18, 19. -
.github/workflows/macos.ymlandmacos-compatibility.yml— build with the Homebrewi686-elf-gcccross toolchain viatools/toolchain-i686-elf.cmake.
- Debugging - Using GDB to debug tests and kernel
- Development Guide - Adding features to MentOS
- Contributing - Code style and contribution guidelines
Key Principle: A test that corrupts kernel state isn't a test—it's a bug. The goal is to verify behavior while preserving system integrity. Always leave the kernel in the state you found it.