Current State: Tests are baked into kernel binary and only run during boot
- ❌ Kernel must boot for tests to run
- ❌ If kernel crashes during init, tests never execute
- ❌ Slow feedback loop (10+ seconds per iteration)
- ❌ Can't catch bugs BEFORE building ISO
Required: True unit tests that run on host machine (macOS/Linux)
- ✅ Instant feedback (<1 second)
- ✅ No QEMU, no bootloader, no kernel required
- ✅ Catch bugs BEFORE wasting time on kernel build
- ✅ Standard development workflow
kernel/
├── tests/ # Host-side unit tests
│ ├── host_test.h # Test harness (simple, no dependencies)
│ ├── pmm_test.c # PMM unit tests
│ ├── mmu_test.c # MMU unit tests
│ └── bitmap_test.c # Bitmap operations
├── mm/
│ ├── pmm.c # Kernel PMM (with #ifdef HOST_TEST for mocking)
│ └── pmm.h
├── test_build/ # Compiled test binaries
│ └── test_runner # Single executable
└── Makefile # With 'make test' target
# Developer workflow:
make test # Compiles and runs host tests (< 1 second)
make build # Only builds if tests pass
make run # Boots kernel in QEMU
# CI/CD workflow:
make test && make build && make integration-testFile: tests/host_test.h
#ifndef HOST_TEST_H
#define HOST_TEST_H
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
// Test result tracking
static int tests_run = 0;
static int tests_passed = 0;
static int tests_failed = 0;
// Color output
#define COLOR_RED "\x1b[31m"
#define COLOR_GREEN "\x1b[32m"
#define COLOR_YELLOW "\x1b[33m"
#define COLOR_RESET "\x1b[0m"
// Assert macros
#define TEST_ASSERT(condition, message) do { \
if (!(condition)) { \
printf(COLOR_RED " FAIL: %s\n" COLOR_RESET, message); \
return 0; \
} \
} while(0)
#define TEST_ASSERT_EQ(a, b, message) do { \
if ((a) != (b)) { \
printf(COLOR_RED " FAIL: %s (expected %lu, got %lu)\n" COLOR_RESET, \
message, (unsigned long)(b), (unsigned long)(a)); \
return 0; \
} \
} while(0)
#define TEST_ASSERT_NEQ(a, b, message) do { \
if ((a) == (b)) { \
printf(COLOR_RED " FAIL: %s (values should differ)\n" COLOR_RESET, message); \
return 0; \
} \
} while(0)
// Test registration
#define TEST(name) \
static int test_##name(void); \
static void __attribute__((constructor)) register_##name(void) { \
run_test(#name, test_##name); \
} \
static int test_##name(void)
// Test runner
static void run_test(const char* name, int (*test_fn)(void)) {
tests_run++;
printf("[ TEST ] %s ... ", name);
fflush(stdout);
if (test_fn()) {
printf(COLOR_GREEN "PASS\n" COLOR_RESET);
tests_passed++;
} else {
tests_failed++;
}
}
// Summary
static void __attribute__((destructor)) print_summary(void) {
printf("\n");
printf("========================================\n");
printf("Tests run: %d\n", tests_run);
printf(COLOR_GREEN "Passed: %d\n" COLOR_RESET, tests_passed);
if (tests_failed > 0) {
printf(COLOR_RED "Failed: %d\n" COLOR_RESET, tests_failed);
exit(1);
} else {
printf(COLOR_GREEN "ALL TESTS PASSED!\n" COLOR_RESET);
exit(0);
}
}
#endif // HOST_TEST_HProblem: PMM uses kprintf(), kassert(), etc. which don't exist on host
Solution: Conditional compilation
File: mm/pmm.c (modified)
// At top of file:
#ifdef HOST_TEST
// Host-side mocks
#define kprintf printf
#define kassert(cond) assert(cond)
#define kassert_aligned(addr, align) assert(((addr) % (align)) == 0)
#define kassert_not_null(ptr) assert((ptr) != NULL)
#define PRECONDITION(msg) (void)msg
#define POSTCONDITION(msg) (void)msg
#define INVARIANT(msg) (void)msg
#else
// Kernel includes
#include <kernel/assert.h>
#include <drivers/vga.h>
#endifFile: tests/pmm_test.c
#include "host_test.h"
#include "../include/kernel/pmm.h"
#include "../include/kernel/types.h"
// Mock multiboot info for testing
static struct multiboot_info test_mbi = {
.flags = MULTIBOOT_FLAG_MMAP,
.mmap_addr = 0, // Will be set dynamically
.mmap_length = 0
};
// Test: Frame addresses are 4K-aligned
TEST(pmm_frame_aligned) {
// Initialize PMM with test data
pmm_init(MULTIBOOT_MAGIC, &test_mbi);
// Allocate 10 frames
for (int i = 0; i < 10; i++) {
phys_addr_t addr = pmm_alloc_page();
TEST_ASSERT(addr != 0, "allocation succeeded");
TEST_ASSERT((addr & 0xFFF) == 0, "address is 4K-aligned");
TEST_ASSERT(addr >= 0x1000, "address >= 4096");
pmm_free_page(addr);
}
return 1; // PASS
}
// Test: Frame calculation correctness
TEST(pmm_frame_calculation) {
pmm_init(MULTIBOOT_MAGIC, &test_mbi);
phys_addr_t addr = pmm_alloc_page();
TEST_ASSERT(addr != 0, "allocation succeeded");
// Verify frame*4096 calculation
uint32_t frame_num = addr / 4096;
phys_addr_t reconstructed = frame_num * 4096;
TEST_ASSERT_EQ(addr, reconstructed, "frame*4096 is reversible");
// Specifically test frame 33 → 0x21000 (not 0xd34!)
if (frame_num == 33) {
TEST_ASSERT_EQ(addr, 0x21000, "frame 33 = 0x21000");
}
pmm_free_page(addr);
return 1;
}
// Test: Total memory is reasonable
TEST(pmm_memory_sane) {
pmm_init(MULTIBOOT_MAGIC, &test_mbi);
struct pmm_stats stats;
pmm_get_stats(&stats);
// Should have at least 1MB (256 frames)
TEST_ASSERT(stats.total_frames >= 256, "total >= 1MB");
// Free should be <= total
TEST_ASSERT(stats.free_frames <= stats.total_frames, "free <= total");
return 1;
}# Host test configuration
HOST_CC := gcc
HOST_CFLAGS := -std=gnu99 -O0 -g -Wall -Wextra -DHOST_TEST \
-I$(INCLUDE_DIR) -fno-builtin
# Test sources (not compiled into kernel)
TEST_SOURCES := tests/pmm_test.c \
tests/mmu_test.c \
tests/bitmap_test.c
# Extract testable code (with mocks)
TESTABLE_SOURCES := mm/pmm.c
# Build host test runner
test: test_build/test_runner
@echo "Running host-side unit tests..."
@./test_build/test_runner
test_build/test_runner: $(TEST_SOURCES) $(TESTABLE_SOURCES)
@mkdir -p test_build
@$(HOST_CC) $(HOST_CFLAGS) -o $@ $(TEST_SOURCES) $(TESTABLE_SOURCES)
# Clean test artifacts
clean: clean-tests
clean-tests:
@rm -rf test_build
.PHONY: test clean-tests$ make test
Running host-side unit tests...
[ TEST ] pmm_frame_aligned ... PASS
[ TEST ] pmm_frame_calculation ... PASS
[ TEST ] pmm_memory_sane ... PASS
========================================
Tests run: 3
Passed: 3
ALL TESTS PASSED!
real 0m0.143s # <1 second!$ make test
Running host-side unit tests...
[ TEST ] pmm_frame_aligned ... PASS
[ TEST ] pmm_frame_calculation ...
FAIL: frame 33 = 0x21000 (expected 135168, got 3383)
========================================
Tests run: 2
Passed: 1
Failed: 1
make: *** [test] Error 1
# BUG CAUGHT BEFORE BUILDING KERNEL!# Write test first
$ vim tests/new_feature_test.c
# Test fails (feature not implemented)
$ make test
FAIL
# Implement feature
$ vim mm/new_feature.c
# Test passes
$ make test
PASS
# Now build kernel
$ make build- PMM allocation/deallocation
- Frame number calculations
- Bitmap operations
- MMU address calculations (not actual paging)
- String operations
- Keep existing kernel self-tests for:
- Actual paging (requires CPU MMU)
- Hardware timer calibration
- Interrupt handling
- Hardware-specific code
| Component | Host Test | Kernel Test |
|---|---|---|
| Frame calculations | ✅ YES | Optional |
| Bitmap operations | ✅ YES | Optional |
| PMM allocation logic | ✅ YES | Optional |
| Actual paging | ❌ NO | ✅ YES |
| Timer calibration | ❌ NO | ✅ YES |
| IDT/interrupts | ❌ NO | ✅ YES |
- Create
tests/host_test.h - Add
make testto Makefile - Write 3 PMM tests as proof-of-concept
- Add
#ifdef HOST_TESTto PMM - Mock kprintf, kassert, etc.
- Verify tests run successfully
- Add MMU tests (address calculations)
- Add bitmap tests
- Add string operation tests
- Target: 80% coverage of non-hardware code
- Add to GitHub Actions (if using)
- Block merges if tests fail
- Generate coverage reports
- Feedback time: 10-30 seconds (QEMU boot)
- Can't run if kernel crashes
- No coverage measurement
- Manual testing required
- Feedback time: <1 second
- Always runs (no kernel required)
- Coverage reports available
- Automated CI/CD
- Custom harness in
host_test.h - Zero external dependencies
- Works anywhere
- Google Test (C++)
- Coverage with
gcov/lcov - Sanitizers (ASAN, UBSAN)
- Valgrind for memory leaks
# Day 1: Write feature
$ vim mm/new_allocator.c
$ vim mm/new_allocator.h
# Day 1: Write tests FIRST (TDD)
$ vim tests/new_allocator_test.c
TEST(allocator_basic) {
void* ptr = new_alloc(4096);
TEST_ASSERT(ptr != NULL, "allocation succeeds");
TEST_ASSERT(IS_ALIGNED(ptr, 4096), "aligned to 4K");
new_free(ptr);
return 1;
}
# Day 1: Run tests (they fail - feature not implemented)
$ make test
[ TEST ] allocator_basic ... FAIL
make: *** [test] Error 1
# Day 1: Implement feature
$ vim mm/new_allocator.c
# ... implementation ...
# Day 1: Run tests (now pass!)
$ make test
[ TEST ] allocator_basic ... PASS
ALL TESTS PASSED!
# Day 1: Build kernel
$ make build
Build complete: kernel.iso
# Day 1: Test in QEMU
$ make run
# Everything works!
# Day 2: Add to CI
$ git commit -m "Add new allocator with tests"
$ git push
# CI runs 'make test' automatically
# Merge only if greenHost-side unit testing is essential for:
- Fast iteration
- Early bug detection
- Confidence in refactoring
- Professional development workflow
Next Steps:
- Implement
tests/host_test.h - Add
make testto Makefile - Write 5 PMM tests
- Verify all pass
- Make
make builddepend onmake test
Time Investment: ~4 hours to set up, saves hours per day afterward