-
Notifications
You must be signed in to change notification settings - Fork 63
Unit Testing
MentOS includes a kernel unit testing framework that allows developers to 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
#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();
}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();
}cd build
cmake ..
make
make qemu # Kernel will run tests during bootTests are auto-discovered and executed during kernel initialization. Output appears in the kernel log with [TUNIT] prefix.
To enable/disable tests at build time:
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 %d not initialized", i);
}
TEST_SECTION_END();
}TEST(verify_field_structure)
{
TEST_SECTION_START("GDT descriptor field layout");
// Copy and inspect
gdt_descriptor_t entry;
ASSERT(test_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(test_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 non-zero, got %d", value);
-
Copy first, then test - never modify real structures
gdt_descriptor_t copy; test_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; test_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; test_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; test_gdt_safe_copy(0, ©); // Safe inspection
// Mark test section for documentation
TEST_SECTION_START("Section description");
TEST_SECTION_END();
// Assert with descriptive message (recommended)
ASSERT_MSG(condition, "Error message %d", value);
// Basic assert without message (legacy)
ASSERT(condition);// Copy GDT entry safely (read-only)
int test_gdt_safe_copy(size_t idx, gdt_descriptor_t *dest)
// Returns: 0 on success, -1 if index invalid
// Copy IDT entry safely (read-only)
int test_idt_safe_copy(size_t idx, idt_descriptor_t *dest)
// Returns: 0 on success, -1 if index invalid
// Check if memory region is zeroed
int test_is_zeroed(const void *ptr, size_t size, const char *description)
// Returns: 1 if all zeros, 0 if not
// Bounds checking helper
int test_bounds_check(uint32_t value, uint32_t min, uint32_t max, const char *field)
// Returns: 1 if in range, 0 if out of rangeThe 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_UNIT_TESTSisONin CMake - Test file is in
kernel/src/tests/unit/ - Test function matches pattern
TEST(name) - CMakeLists.txt includes the source file
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, got %zu",
sizeof(gdt_descriptor_t));
// Part 2: Verify null descriptor is present
gdt_descriptor_t null_desc;
ASSERT(test_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(test_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, got %d", dpl);
// 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, got 0x%x", base);
// 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, got 0x%x", limit);
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(test_gdt_safe_copy(GDT_SIZE, NULL) == -1,
"Should reject index >= GDT_SIZE");
ASSERT_MSG(test_gdt_safe_copy(99999, NULL) == -1,
"Should reject large invalid index");
// Verify NULL destination is handled
ASSERT_MSG(test_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
2. test_runner_init() called
3. All TEST(name) functions executed sequentially
4. Results printed to kernel log with [TUNIT] prefix
5. If all pass: OS continues normally
6. If any fail: Kernel panics with error details
kernel/
├── inc/tests/
│ ├── test.h # Core test framework
│ └── test_utils.h # Safe utility functions
├── src/tests/
│ ├── runner.c # Test runner (auto-loads tests)
│ └── unit/
│ ├── test_gdt.c # GDT tests
│ ├── test_idt.c # IDT tests
│ ├── test_memory.c # Memory subsystem tests
│ └── test_*.c # Other tests- 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.