From e90b72ec3082588b296956730bd2930cf8d5d671 Mon Sep 17 00:00:00 2001 From: Richard Qin Date: Fri, 10 Jul 2026 11:31:46 +0800 Subject: refactor: promote new architecture to repository root Archive the deprecated implementation under lume-legacy, make the new kernel the default root-level build, and update repository entry points without changing kernel behavior. --- na/test/module.mk | 30 --- na/test/selftest.cc | 83 -------- na/test/test_atomic.cc | 102 ---------- na/test/test_dcache.cc | 232 ---------------------- na/test/test_driver_model.cc | 209 -------------------- na/test/test_exec.cc | 447 ------------------------------------------- na/test/test_fdt.cc | 50 ----- na/test/test_klog.cc | 175 ----------------- na/test/test_mutex.cc | 183 ------------------ na/test/test_namei.cc | 173 ----------------- na/test/test_page_cache.cc | 180 ----------------- na/test/test_pid.cc | 106 ---------- na/test/test_pipe_devfs.cc | 420 ---------------------------------------- na/test/test_pmm.cc | 283 --------------------------- na/test/test_random.cc | 49 ----- na/test/test_rbtree.cc | 119 ------------ na/test/test_sched.cc | 257 ------------------------- na/test/test_signal.cc | 193 ------------------- na/test/test_slab.cc | 255 ------------------------ na/test/test_smp.cc | 136 ------------- na/test/test_spinlock.cc | 135 ------------- na/test/test_sys_fs_extra.cc | 238 ----------------------- na/test/test_sys_mm.cc | 178 ----------------- na/test/test_sys_process.cc | 93 --------- na/test/test_trap.cc | 197 ------------------- na/test/test_uaccess.cc | 97 ---------- na/test/test_vfs_io.cc | 345 --------------------------------- na/test/test_vma.cc | 282 --------------------------- na/test/test_vmm.cc | 58 ------ na/test/test_waitqueue.cc | 91 --------- 30 files changed, 5396 deletions(-) delete mode 100644 na/test/module.mk delete mode 100644 na/test/selftest.cc delete mode 100644 na/test/test_atomic.cc delete mode 100644 na/test/test_dcache.cc delete mode 100644 na/test/test_driver_model.cc delete mode 100644 na/test/test_exec.cc delete mode 100644 na/test/test_fdt.cc delete mode 100644 na/test/test_klog.cc delete mode 100644 na/test/test_mutex.cc delete mode 100644 na/test/test_namei.cc delete mode 100644 na/test/test_page_cache.cc delete mode 100644 na/test/test_pid.cc delete mode 100644 na/test/test_pipe_devfs.cc delete mode 100644 na/test/test_pmm.cc delete mode 100644 na/test/test_random.cc delete mode 100644 na/test/test_rbtree.cc delete mode 100644 na/test/test_sched.cc delete mode 100644 na/test/test_signal.cc delete mode 100644 na/test/test_slab.cc delete mode 100644 na/test/test_smp.cc delete mode 100644 na/test/test_spinlock.cc delete mode 100644 na/test/test_sys_fs_extra.cc delete mode 100644 na/test/test_sys_mm.cc delete mode 100644 na/test/test_sys_process.cc delete mode 100644 na/test/test_trap.cc delete mode 100644 na/test/test_uaccess.cc delete mode 100644 na/test/test_vfs_io.cc delete mode 100644 na/test/test_vma.cc delete mode 100644 na/test/test_vmm.cc delete mode 100644 na/test/test_waitqueue.cc (limited to 'na/test') diff --git a/na/test/module.mk b/na/test/module.mk deleted file mode 100644 index 47a693c..0000000 --- a/na/test/module.mk +++ /dev/null @@ -1,30 +0,0 @@ -# test/module.mk -SRCS_CC += test/selftest.cc -SRCS_CC += test/test_pmm.cc -SRCS_CC += test/test_slab.cc -SRCS_CC += test/test_spinlock.cc -SRCS_CC += test/test_mutex.cc -SRCS_CC += test/test_driver_model.cc -SRCS_CC += test/test_rbtree.cc -SRCS_CC += test/test_vmm.cc -SRCS_CC += test/test_vma.cc -SRCS_CC += test/test_atomic.cc -SRCS_CC += test/test_fdt.cc -SRCS_CC += test/test_trap.cc -SRCS_CC += test/test_sched.cc -SRCS_CC += test/test_klog.cc -SRCS_CC += test/test_random.cc -SRCS_CC += test/test_waitqueue.cc -SRCS_CC += test/test_uaccess.cc -SRCS_CC += test/test_sys_mm.cc -SRCS_CC += test/test_smp.cc -SRCS_CC += test/test_dcache.cc -SRCS_CC += test/test_namei.cc -SRCS_CC += test/test_vfs_io.cc -SRCS_CC += test/test_exec.cc -SRCS_CC += test/test_pipe_devfs.cc -SRCS_CC += test/test_sys_process.cc -SRCS_CC += test/test_signal.cc -SRCS_CC += test/test_pid.cc -SRCS_CC += test/test_sys_fs_extra.cc -SRCS_CC += test/test_page_cache.cc diff --git a/na/test/selftest.cc b/na/test/selftest.cc deleted file mode 100644 index e1bb2b4..0000000 --- a/na/test/selftest.cc +++ /dev/null @@ -1,83 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * Boot Self-Test Runner - * - * Entry point that invokes all registered self-test suites. - * Called from kernel_main() after all subsystem initialization. - */ - -#include - -/* Individual test suite entry points */ -void selftest_pmm(); -void selftest_slab(); -void selftest_spinlock(); -void selftest_mutex(); -void selftest_driver_model(); -void selftest_rbtree(); -void selftest_vmm(); -void selftest_fdt(); -void selftest_vma(); -void selftest_atomic(); -void selftest_trap(); -void selftest_sched(); -void selftest_klog(); -void selftest_random(); -void selftest_waitqueue(); -void selftest_uaccess(); -void selftest_sys_mm(); -void selftest_dcache(); -void selftest_namei(); -void selftest_vfs_io(); -void selftest_exec(); -void selftest_devfs(); -void selftest_pipe(); -void selftest_fcntl(); -void selftest_sys_process(); -void selftest_signal(); -void selftest_pid(); -void selftest_sys_fs_extra(); -void selftest_page_cache(); - -void selftest_run_all() -{ - kprintf("\n========================================\n"); - kprintf(" Boot Self-Test Suite\n"); - kprintf("========================================\n"); - - selftest_spinlock(); - selftest_mutex(); - selftest_driver_model(); - selftest_pmm(); - selftest_slab(); - selftest_rbtree(); - selftest_vmm(); - selftest_vma(); - selftest_atomic(); - selftest_fdt(); - selftest_trap(); - selftest_sched(); - selftest_klog(); - selftest_random(); - selftest_waitqueue(); - selftest_uaccess(); - selftest_sys_mm(); - selftest_dcache(); - selftest_namei(); - selftest_vfs_io(); - selftest_devfs(); - selftest_pipe(); - selftest_fcntl(); - selftest_sys_process(); - selftest_signal(); - selftest_pid(); - selftest_sys_fs_extra(); - selftest_page_cache(); - selftest_exec(); - - kprintf("========================================\n"); - kprintf(" All self-tests PASSED\n"); - kprintf("========================================\n\n"); -} diff --git a/na/test/test_atomic.cc b/na/test/test_atomic.cc deleted file mode 100644 index 9851e7b..0000000 --- a/na/test/test_atomic.cc +++ /dev/null @@ -1,102 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * Boot Self-Test: lume::atomic - * - * Tests: - * 1. Default construction (zero-initialized) - * 2. Explicit construction - * 3. store / load round-trip with different memory orders - * 4. exchange returns old value - * 5. fetch_add / fetch_sub arithmetic - * 6. compare_exchange success and failure paths - */ - -#include -#include - -void selftest_atomic() { - st_begin("atomic: default construction is zero"); - { - lume::atomic a; - ST_ASSERT_EQ(a.load(), 0U); - } - st_pass(); - - st_begin("atomic: explicit construction"); - { - lume::atomic a(42); - ST_ASSERT_EQ(a.load(), 42ULL); - } - st_pass(); - - st_begin("atomic: store/load round-trip"); - { - lume::atomic a; - a.store(0xDEAD, __ATOMIC_RELEASE); - ST_ASSERT_EQ(a.load(__ATOMIC_ACQUIRE), 0xDEADU); - - a.store(0, __ATOMIC_SEQ_CST); - ST_ASSERT_EQ(a.load(__ATOMIC_SEQ_CST), 0U); - } - st_pass(); - - st_begin("atomic: exchange returns old value"); - { - lume::atomic a(10); - uint32 old = a.exchange(20); - ST_ASSERT_EQ(old, 10U); - ST_ASSERT_EQ(a.load(), 20U); - } - st_pass(); - - st_begin("atomic: fetch_add / fetch_sub"); - { - lume::atomic a(100); - - uint32 old = a.fetch_add(5); - ST_ASSERT_EQ(old, 100U); - ST_ASSERT_EQ(a.load(), 105U); - - old = a.fetch_sub(10); - ST_ASSERT_EQ(old, 105U); - ST_ASSERT_EQ(a.load(), 95U); - - // Underflow wraps (unsigned) - lume::atomic b(0); - old = b.fetch_sub(1); - ST_ASSERT_EQ(old, 0U); - ST_ASSERT_EQ(b.load(), 0xFFFFFFFFU); - } - st_pass(); - - st_begin("atomic: compare_exchange success and failure"); - { - lume::atomic a(42); - - // Success path: expected matches, swap happens - uint32 expected = 42; - bool ok = a.compare_exchange(expected, 99); - ST_ASSERT(ok); - ST_ASSERT_EQ(a.load(), 99U); - ST_ASSERT_EQ(expected, 42U); // expected unchanged on success - - // Failure path: expected doesn't match, expected gets updated - expected = 0; // wrong - ok = a.compare_exchange(expected, 200); - ST_ASSERT(!ok); - ST_ASSERT_EQ(a.load(), 99U); // value unchanged - ST_ASSERT_EQ(expected, 99U); // expected updated to actual value - } - st_pass(); - - st_begin("atomic: 64-bit fetch_add correctness"); - { - lume::atomic a(0xFFFFFFFF00000000ULL); - uint64 old = a.fetch_add(1); - ST_ASSERT_EQ(old, 0xFFFFFFFF00000000ULL); - ST_ASSERT_EQ(a.load(), 0xFFFFFFFF00000001ULL); - } - st_pass(); -} diff --git a/na/test/test_dcache.cc b/na/test/test_dcache.cc deleted file mode 100644 index 9e0eec5..0000000 --- a/na/test/test_dcache.cc +++ /dev/null @@ -1,232 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * test_dcache.cc — Self-tests for the dentry cache layer - */ - -#include -#include -#include -#include -#include -#include - -/* Root dentry/mount from VFS init */ -extern dentry* g_root_dentry; -extern vfsmount* g_root_mnt; - -void selftest_dcache() -{ - /* ---- d_alloc: basic allocation under root ---- */ - st_begin("dcache: d_alloc basic"); - { - qstr name{}; - name.name = "test_child"; - name.len = 10; - name.hash = full_name_hash(name.name, name.len); - - dentry* child = d_alloc(g_root_dentry, &name); - ST_ASSERT(child != nullptr); - ST_ASSERT_EQ(child->d_name.len, 10u); - ST_ASSERT(child->d_parent == g_root_dentry); - ST_ASSERT(child->d_inode == nullptr); /* not instantiated yet */ - ST_ASSERT(child->d_sb == g_root_dentry->d_sb); - - /* Name was copied into inline buffer */ - bool match = true; - for (uint32 i = 0; i < name.len; i++) { - if (child->d_name.name[i] != name.name[i]) { match = false; break; } - } - ST_ASSERT(match); - - d_drop(child); - dput(child); - } - st_pass(); - - /* ---- d_lookup: find existing child ---- */ - st_begin("dcache: d_lookup finds child"); - { - qstr name{}; - name.name = "lookup_me"; - name.len = 9; - name.hash = full_name_hash(name.name, name.len); - - dentry* child = d_alloc(g_root_dentry, &name); - ST_ASSERT(child != nullptr); - - /* d_lookup should find it */ - dentry* found = d_lookup(g_root_dentry, &name); - ST_ASSERT(found != nullptr); - ST_ASSERT(found == child); - dput(found); /* drop d_lookup ref */ - - d_drop(child); - dput(child); - } - st_pass(); - - /* ---- d_lookup: miss returns nullptr ---- */ - st_begin("dcache: d_lookup miss returns nullptr"); - { - qstr name{}; - name.name = "nonexistent_xyz"; - name.len = 15; - name.hash = full_name_hash(name.name, name.len); - - dentry* found = d_lookup(g_root_dentry, &name); - ST_ASSERT(found == nullptr); - } - st_pass(); - - /* ---- d_instantiate: bind inode to dentry ---- */ - st_begin("dcache: d_instantiate binds inode"); - { - qstr name{}; - name.name = "has_inode"; - name.len = 9; - name.hash = full_name_hash(name.name, name.len); - - dentry* child = d_alloc(g_root_dentry, &name); - ST_ASSERT(child != nullptr); - ST_ASSERT(child->d_inode == nullptr); - - /* Create a dummy inode */ - inode* ip = new_inode(g_root_dentry->d_sb); - ST_ASSERT(ip != nullptr); - ip->i_mode = S_IFREG | 0644; - - d_instantiate(child, ip); - ST_ASSERT(child->d_inode == ip); - ST_ASSERT(child->d_flags & DCACHE_REGULAR_TYPE); - - d_drop(child); - dput(child); - } - st_pass(); - - /* ---- dget/dput: refcount ---- */ - st_begin("dcache: dget/dput refcount"); - { - qstr name{}; - name.name = "reftest"; - name.len = 7; - name.hash = full_name_hash(name.name, name.len); - - dentry* child = d_alloc(g_root_dentry, &name); - ST_ASSERT(child != nullptr); - /* d_alloc sets refcount = 1 */ - int rc1 = child->d_lockref.load(); - ST_ASSERT(rc1 >= 1); - - dget(child); - int rc2 = child->d_lockref.load(); - ST_ASSERT_EQ(rc2, rc1 + 1); - - dput(child); - int rc3 = child->d_lockref.load(); - ST_ASSERT_EQ(rc3, rc1); - - d_drop(child); - dput(child); - } - st_pass(); - - /* ---- d_drop: removes from parent children list ---- */ - st_begin("dcache: d_drop removes from parent"); - { - qstr name{}; - name.name = "dropme"; - name.len = 6; - name.hash = full_name_hash(name.name, name.len); - - dentry* child = d_alloc(g_root_dentry, &name); - ST_ASSERT(child != nullptr); - - /* Should be findable */ - dentry* f1 = d_lookup(g_root_dentry, &name); - ST_ASSERT(f1 != nullptr); - dput(f1); - - /* Drop it */ - d_drop(child); - - /* Should no longer be findable */ - dentry* f2 = d_lookup(g_root_dentry, &name); - ST_ASSERT(f2 == nullptr); - - dput(child); - } - st_pass(); - - /* ---- d_path: root returns "/" ---- */ - st_begin("dcache: d_path root returns /"); - { - path p{}; - p.de = g_root_dentry; - p.mnt = g_root_mnt; - - char buf[64]; - char* result = d_path(&p, buf, sizeof(buf)); - ST_ASSERT(result != nullptr); - ST_ASSERT(result[0] == '/'); - ST_ASSERT(result[1] == '\0'); - } - st_pass(); - - /* ---- d_path: child path ---- */ - st_begin("dcache: d_path child returns /name"); - { - qstr name{}; - name.name = "pathtest"; - name.len = 8; - name.hash = full_name_hash(name.name, name.len); - - dentry* child = d_alloc(g_root_dentry, &name); - ST_ASSERT(child != nullptr); - - path p{}; - p.de = child; - p.mnt = g_root_mnt; - - char buf[64]; - char* result = d_path(&p, buf, sizeof(buf)); - ST_ASSERT(result != nullptr); - - /* Should be "/pathtest" */ - const char* expected = "/pathtest"; - bool eq = true; - for (int i = 0; expected[i]; i++) { - if (result[i] != expected[i]) { eq = false; break; } - } - ST_ASSERT(eq); - - d_drop(child); - dput(child); - } - st_pass(); - - /* ---- d_alloc_anon: root-like dentry ---- */ - st_begin("dcache: d_alloc_anon"); - { - dentry* anon = d_alloc_anon(g_root_dentry->d_sb); - ST_ASSERT(anon != nullptr); - ST_ASSERT(anon->d_parent == anon); /* root: parent is self */ - ST_ASSERT(anon->d_sb == g_root_dentry->d_sb); - dput(anon); - } - st_pass(); - - /* ---- full_name_hash: deterministic ---- */ - st_begin("dcache: full_name_hash deterministic"); - { - uint32 h1 = full_name_hash("hello", 5); - uint32 h2 = full_name_hash("hello", 5); - ST_ASSERT_EQ(h1, h2); - - uint32 h3 = full_name_hash("world", 5); - ST_ASSERT_NE(h1, h3); - } - st_pass(); -} diff --git a/na/test/test_driver_model.cc b/na/test/test_driver_model.cc deleted file mode 100644 index 8f105b3..0000000 --- a/na/test/test_driver_model.cc +++ /dev/null @@ -1,209 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * test_driver_model — exercises kref, kobject, and the bus/device/driver - * core. Uses a dummy bus + dummy driver + dummy device so the test is - * self-contained and doesn't depend on FDT/platform_device. - */ - -#include -#include -#include -#include - -/* ------------------------------------------------------------------ */ -/* Test 1: kref get/put + release fires exactly once */ -/* ------------------------------------------------------------------ */ - -namespace { - -struct krefable { - kref ref; - int release_count; -}; - -void krefable_release(kref* r) -{ - auto* k = container_of(r, krefable, ref); - k->release_count++; -} - -void test_kref() -{ - st_begin("kref: init/get/put"); - - krefable k = {}; - kref_init(&k.ref); - - ST_ASSERT_EQ(kref_read(&k.ref), 1); - - kref_get(&k.ref); - kref_get(&k.ref); - ST_ASSERT_EQ(kref_read(&k.ref), 3); - - ST_ASSERT_EQ(kref_put(&k.ref, krefable_release), 0); - ST_ASSERT_EQ(kref_put(&k.ref, krefable_release), 0); - ST_ASSERT_EQ(k.release_count, 0); - - ST_ASSERT_EQ(kref_put(&k.ref, krefable_release), 1); - ST_ASSERT_EQ(k.release_count, 1); - - st_pass(); -} - -/* ------------------------------------------------------------------ */ -/* Test 2: kobject parent/child + release on final put */ -/* ------------------------------------------------------------------ */ - -int g_child_released = 0; -int g_parent_released = 0; - -void child_release(kobject*) { g_child_released++; } -void parent_release(kobject*) { g_parent_released++; } - -const kobj_type child_ktype = { .release = child_release }; -const kobj_type parent_ktype = { .release = parent_release }; - -void test_kobject_hierarchy() -{ - st_begin("kobject: parent/child + ref counting"); - - g_child_released = 0; - g_parent_released = 0; - - kobject parent; - kobject_init(&parent, &parent_ktype); - - kobject child; - kobject_init(&child, &child_ktype); - kobject_add(&child, &parent, "child"); - - /* Adding child took a ref on parent. */ - ST_ASSERT_EQ(kref_read(&parent.refcount), 2); - ST_ASSERT(child.parent == &parent); - - /* Drop child's "added" reference — release should NOT fire because - * we (the test) still hold the original kobject_init ref. */ - kobject_del(&child); - ST_ASSERT(child.parent == nullptr); - ST_ASSERT_EQ(kref_read(&parent.refcount), 1); - ST_ASSERT_EQ(g_child_released, 0); - ST_ASSERT_EQ(g_parent_released, 0); - - /* Drop final refs. */ - kobject_put(&child); - ST_ASSERT_EQ(g_child_released, 1); - - kobject_put(&parent); - ST_ASSERT_EQ(g_parent_released, 1); - - st_pass(); -} - -/* ------------------------------------------------------------------ */ -/* Test 3: bus / device / driver bind via match + probe */ -/* ------------------------------------------------------------------ */ - -struct dummy_dev { device dev; int probed; }; -struct dummy_drv { device_driver drv; const char* want; }; - -int g_probes = 0; - -int dummy_match(device* dev, device_driver* drv) -{ - auto* dd = container_of(drv, dummy_drv, drv); - return (dev->init_name != nullptr && - dd->want != nullptr && - __builtin_strcmp(dev->init_name, dd->want) == 0) ? 1 : 0; -} - -int dummy_probe(device* dev) -{ - auto* dd = container_of(dev, dummy_dev, dev); - dd->probed++; - g_probes++; - return 0; -} - -bus_type dummy_bus = { - .name = "test_dummy", - .match = dummy_match, - .probe = nullptr, /* fall through to drv->probe */ - .devices = {}, - .drivers = {}, - .lock = {}, - .registered = false, -}; - -void test_bus_bind() -{ - st_begin("driver model: bus / driver / device bind"); - - g_probes = 0; - ST_ASSERT_EQ(bus_register(&dummy_bus), 0); - - /* Register driver before any matching device exists. */ - dummy_drv drv_a = {}; - drv_a.drv.name = "drv_a"; - drv_a.drv.bus = &dummy_bus; - drv_a.drv.probe = dummy_probe; - drv_a.want = "compat_a"; - ST_ASSERT_EQ(driver_register(&drv_a.drv), 0); - - /* Register a device that should bind to drv_a immediately. */ - dummy_dev dev_a = {}; - dev_a.dev.bus = &dummy_bus; - dev_a.dev.init_name = "compat_a"; - ST_ASSERT_EQ(device_register(&dev_a.dev), 0); - ST_ASSERT_EQ(dev_a.probed, 1); - ST_ASSERT(dev_a.dev.driver == &drv_a.drv); - - /* Register a device first, then a driver — should also bind. */ - dummy_dev dev_b = {}; - dev_b.dev.bus = &dummy_bus; - dev_b.dev.init_name = "compat_b"; - ST_ASSERT_EQ(device_register(&dev_b.dev), 0); - ST_ASSERT_EQ(dev_b.probed, 0); - - dummy_drv drv_b = {}; - drv_b.drv.name = "drv_b"; - drv_b.drv.bus = &dummy_bus; - drv_b.drv.probe = dummy_probe; - drv_b.want = "compat_b"; - ST_ASSERT_EQ(driver_register(&drv_b.drv), 0); - ST_ASSERT_EQ(dev_b.probed, 1); - ST_ASSERT(dev_b.dev.driver == &drv_b.drv); - - /* Non-matching device sits unbound. */ - dummy_dev dev_c = {}; - dev_c.dev.bus = &dummy_bus; - dev_c.dev.init_name = "compat_unknown"; - ST_ASSERT_EQ(device_register(&dev_c.dev), 0); - ST_ASSERT_EQ(dev_c.probed, 0); - ST_ASSERT(dev_c.dev.driver == nullptr); - - ST_ASSERT_EQ(g_probes, 2); - - /* Cleanup. */ - device_unregister(&dev_a.dev); - device_unregister(&dev_b.dev); - device_unregister(&dev_c.dev); - driver_unregister(&drv_a.drv); - driver_unregister(&drv_b.drv); - - st_pass(); -} - -} // namespace - -/* ------------------------------------------------------------------ */ -/* Entry */ -/* ------------------------------------------------------------------ */ - -void selftest_driver_model() -{ - test_kref(); - test_kobject_hierarchy(); - test_bus_bind(); -} diff --git a/na/test/test_exec.cc b/na/test/test_exec.cc deleted file mode 100644 index bbfa3b6..0000000 --- a/na/test/test_exec.cc +++ /dev/null @@ -1,447 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * test_exec.cc — Self-tests for execve / ELF loader - * - * Tests the ELF validation, binary loading, and user stack construction - * by creating synthetic ELF binaries in RamFS and invoking do_execve - * in a controlled environment. - * - * We cannot test the actual user-space transition (that requires - * sret to U-mode), but we CAN test everything up to and including - * the trapframe setup. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* Extern: do_execve (kernel/exec.cc) */ -int do_execve(const char* pathname, const char* const* argv, - const char* const* envp); - -/* Extern: VFS file creation syscalls */ -int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_write(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_close(uint64, uint64, uint64, uint64, uint64, uint64); - -/* ================================================================== */ -/* Helper: build a minimal valid RISC-V ELF64 in a buffer */ -/* ================================================================== */ - -struct TestElf { - Elf64_Ehdr ehdr; - Elf64_Phdr phdr; - uint8 code[16]; /* Tiny "program" — just a few NOPs */ -}; - -/* - * Build a minimal static ELF64 for RISC-V. - * Single PT_LOAD segment mapping the tiny code at 0x10000. - * Entry point = 0x10000 + offsetof(code). - */ -static void build_minimal_elf(TestElf* elf) -{ - /* Zero everything */ - for (uint64 i = 0; i < sizeof(*elf); i++) - reinterpret_cast(elf)[i] = 0; - - /* ELF header */ - Elf64_Ehdr* e = &elf->ehdr; - e->e_ident[EI_MAG0] = ELFMAG0; - e->e_ident[EI_MAG1] = ELFMAG1; - e->e_ident[EI_MAG2] = ELFMAG2; - e->e_ident[EI_MAG3] = ELFMAG3; - e->e_ident[EI_CLASS] = ELFCLASS64; - e->e_ident[EI_DATA] = ELFDATA2LSB; - e->e_ident[EI_VERSION] = 1; - e->e_type = ET_EXEC; - e->e_machine = EM_RISCV; - e->e_version = 1; - e->e_phoff = sizeof(Elf64_Ehdr); - e->e_phentsize = sizeof(Elf64_Phdr); - e->e_phnum = 1; - e->e_ehsize = sizeof(Elf64_Ehdr); - - /* Code section offset within the file */ - uint64 code_offset = sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr); - - /* Entry point = vaddr of code */ - uint64 load_vaddr = 0x10000; - e->e_entry = load_vaddr + code_offset; - - /* Program header — single PT_LOAD covering entire file */ - Elf64_Phdr* p = &elf->phdr; - p->p_type = PT_LOAD; - p->p_flags = PF_R | PF_X; - p->p_offset = 0; - p->p_vaddr = load_vaddr; - p->p_paddr = load_vaddr; - p->p_filesz = sizeof(TestElf); - p->p_memsz = sizeof(TestElf); - p->p_align = 0x1000; - - /* Code: RISC-V "unimp" (all zeros = illegal instruction, but good enough for testing) */ - /* We just need the loader to succeed; we won't actually execute this. */ - elf->code[0] = 0x13; /* nop = addi x0, x0, 0 */ - elf->code[4] = 0x13; - elf->code[8] = 0x73; /* ebreak */ -} - -/* ================================================================== */ -/* Helper: write a buffer to a file in RamFS */ -/* ================================================================== */ - -static int create_test_file(const char* path, const void* data, uint64 size) -{ - /* Open with O_CREAT | O_WRONLY | O_TRUNC */ - int64 fd = sys_openat( - static_cast(-100), /* AT_FDCWD */ - reinterpret_cast(path), - O_CREAT | O_WRONLY | O_TRUNC, - 0755, 0, 0); - if (fd < 0) return static_cast(fd); - - int64 written = sys_write( - static_cast(fd), - reinterpret_cast(data), - size, 0, 0, 0); - - sys_close(static_cast(fd), 0, 0, 0, 0, 0); - - if (written < 0) return static_cast(written); - if (static_cast(written) != size) return -EIO; - return 0; -} - -/* ================================================================== */ -/* Helper: ensure current task has VFS context for file ops */ -/* ================================================================== */ - -struct ExecTestCtx { - TaskControlBlock* curr; - VmSpace* old_mm; - arch::TrapFrame fake_tf; - arch::TrapFrame* old_tf; - uint64 saved_satp; -}; - -static void exec_test_setup(ExecTestCtx* ctx) -{ - ctx->curr = current_task(); - - /* Save kernel SATP so we can restore it in teardown */ - asm volatile("csrr %0, satp" : "=r"(ctx->saved_satp)); - - /* Save and replace mm with a fresh one */ - ctx->old_mm = ctx->curr->mm; - ctx->curr->mm = VmSpace::create(); - - /* Set up a fake trapframe so do_execve can modify it */ - for (uint64 i = 0; i < sizeof(ctx->fake_tf); i++) - reinterpret_cast(&ctx->fake_tf)[i] = 0; - ctx->old_tf = ctx->curr->trapframe; - ctx->curr->trapframe = &ctx->fake_tf; -} - -static void exec_test_teardown(ExecTestCtx* ctx) -{ - /* Restore trapframe */ - ctx->curr->trapframe = ctx->old_tf; - - /* Destroy the test mm (may have been replaced by do_execve) */ - VmSpace* test_mm = ctx->curr->mm; - ctx->curr->mm = ctx->old_mm; - if (test_mm && test_mm != ctx->old_mm) { - /* If do_execve succeeded, it activated new_mm's page table. - * Switch back to the kernel page table before destroying. */ - asm volatile("csrw satp, %0" :: "r"(ctx->saved_satp)); - asm volatile("sfence.vma"); - test_mm->put(); - } -} - -/* ================================================================== */ -/* Test cases */ -/* ================================================================== */ - -void selftest_exec() -{ - /* ---- 1a. Non-existent path under missing directory ---- */ - st_begin("exec: missing path returns error"); - { - const char* argv[] = { "/no_such_dir/no_file", nullptr }; - const char* envp[] = { nullptr }; - int ret = do_execve("/no_such_dir/no_file", argv, envp); - ST_ASSERT(ret < 0); /* -ENOENT from filename_lookup */ - } - st_pass(); - - /* ---- 1b. Directory is not executable ---- */ - st_begin("exec: directory returns -EACCES"); - { - /* "/" exists and is a directory */ - const char* argv[] = { "/dev", nullptr }; - const char* envp[] = { nullptr }; - int ret = do_execve("/dev", argv, envp); - ST_ASSERT_EQ(ret, -EACCES); - } - st_pass(); - - /* ---- 2. Bad ELF magic ---- */ - st_begin("exec: bad ELF magic returns -ENOEXEC"); - { - uint8 garbage[128]; - for (int i = 0; i < 128; i++) garbage[i] = 0xAA; - int err = create_test_file("/test_bad_magic", garbage, 128); - ST_ASSERT_EQ(err, 0); - - ExecTestCtx ctx; - exec_test_setup(&ctx); - - const char* argv[] = { "/test_bad_magic", nullptr }; - const char* envp[] = { nullptr }; - int ret = do_execve("/test_bad_magic", argv, envp); - ST_ASSERT_EQ(ret, -ENOEXEC); - - exec_test_teardown(&ctx); - } - st_pass(); - - /* ---- 3. Wrong ELF class (32-bit) ---- */ - st_begin("exec: 32-bit ELF returns -ENOEXEC"); - { - TestElf elf; - build_minimal_elf(&elf); - elf.ehdr.e_ident[EI_CLASS] = 1; /* ELFCLASS32 */ - int err = create_test_file("/test_elf32", &elf, sizeof(elf)); - ST_ASSERT_EQ(err, 0); - - ExecTestCtx ctx; - exec_test_setup(&ctx); - - const char* argv[] = { "/test_elf32", nullptr }; - const char* envp[] = { nullptr }; - int ret = do_execve("/test_elf32", argv, envp); - ST_ASSERT_EQ(ret, -ENOEXEC); - - exec_test_teardown(&ctx); - } - st_pass(); - - /* ---- 4. Wrong endianness (big-endian) ---- */ - st_begin("exec: big-endian ELF returns -ENOEXEC"); - { - TestElf elf; - build_minimal_elf(&elf); - elf.ehdr.e_ident[EI_DATA] = 2; /* ELFDATA2MSB */ - int err = create_test_file("/test_be", &elf, sizeof(elf)); - ST_ASSERT_EQ(err, 0); - - ExecTestCtx ctx; - exec_test_setup(&ctx); - - const char* argv[] = { "/test_be", nullptr }; - const char* envp[] = { nullptr }; - int ret = do_execve("/test_be", argv, envp); - ST_ASSERT_EQ(ret, -ENOEXEC); - - exec_test_teardown(&ctx); - } - st_pass(); - - /* ---- 5. Wrong machine (x86_64 instead of RISC-V) ---- */ - st_begin("exec: wrong machine type returns -ENOEXEC"); - { - TestElf elf; - build_minimal_elf(&elf); - elf.ehdr.e_machine = 62; /* EM_X86_64 */ - int err = create_test_file("/test_x86", &elf, sizeof(elf)); - ST_ASSERT_EQ(err, 0); - - ExecTestCtx ctx; - exec_test_setup(&ctx); - - const char* argv[] = { "/test_x86", nullptr }; - const char* envp[] = { nullptr }; - int ret = do_execve("/test_x86", argv, envp); - ST_ASSERT_EQ(ret, -ENOEXEC); - - exec_test_teardown(&ctx); - } - st_pass(); - - /* ---- 6. Truncated ELF (too small for header) ---- */ - st_begin("exec: truncated ELF returns -ENOEXEC"); - { - /* Valid magic but file is too short for full header */ - uint8 tiny[8] = { 0x7f, 'E', 'L', 'F', 2, 1, 1, 0 }; - int err = create_test_file("/test_tiny", tiny, sizeof(tiny)); - ST_ASSERT_EQ(err, 0); - - ExecTestCtx ctx; - exec_test_setup(&ctx); - - const char* argv[] = { "/test_tiny", nullptr }; - const char* envp[] = { nullptr }; - int ret = do_execve("/test_tiny", argv, envp); - ST_ASSERT_EQ(ret, -ENOEXEC); - - exec_test_teardown(&ctx); - } - st_pass(); - - /* ---- 7. Zero program headers ---- */ - st_begin("exec: zero phnum returns -ENOEXEC"); - { - TestElf elf; - build_minimal_elf(&elf); - elf.ehdr.e_phnum = 0; - int err = create_test_file("/test_nophdr", &elf, sizeof(elf)); - ST_ASSERT_EQ(err, 0); - - ExecTestCtx ctx; - exec_test_setup(&ctx); - - const char* argv[] = { "/test_nophdr", nullptr }; - const char* envp[] = { nullptr }; - int ret = do_execve("/test_nophdr", argv, envp); - ST_ASSERT_EQ(ret, -ENOEXEC); - - exec_test_teardown(&ctx); - } - st_pass(); - - /* ---- 8. Valid ELF: successful load and trapframe setup ---- */ - st_begin("exec: valid ELF loads and sets up trapframe"); - { - TestElf elf; - build_minimal_elf(&elf); - int err = create_test_file("/test_valid", &elf, sizeof(elf)); - ST_ASSERT_EQ(err, 0); - - ExecTestCtx ctx; - exec_test_setup(&ctx); - - const char* argv[] = { "/test_valid", nullptr }; - const char* envp[] = { "HOME=/", nullptr }; - int ret = do_execve("/test_valid", argv, envp); - ST_ASSERT_EQ(ret, 0); - - /* Verify trapframe was set up correctly */ - arch::TrapFrame* tf = &ctx.fake_tf; - - /* Entry point should match what we set in the ELF */ - uint64 code_offset = sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr); - uint64 expected_entry = 0x10000 + code_offset; - ST_ASSERT_EQ(tf->epc, expected_entry); - - /* Stack pointer should be in user space, below the stack top */ - ST_ASSERT(tf->sp > 0); - ST_ASSERT(tf->sp < kUserStackTop); - ST_ASSERT(tf->sp >= kUserStackBase); - - /* sstatus: SPP=0 (U-mode), SPIE=1 (interrupts enabled on sret) */ - uint64 spp_bit = (1ULL << 8); - uint64 spie_bit = (1ULL << 5); - ST_ASSERT_EQ(tf->sstatus & spp_bit, 0ULL); /* Not S-mode */ - ST_ASSERT_EQ(tf->sstatus & spie_bit, spie_bit); /* SPIE set */ - - /* General registers should be zeroed */ - ST_ASSERT_EQ(tf->ra, 0ULL); - ST_ASSERT_EQ(tf->gp, 0ULL); - ST_ASSERT_EQ(tf->tp, 0ULL); - for (int i = 0; i < 7; i++) - ST_ASSERT_EQ(tf->t[i], 0ULL); - for (int i = 0; i < 12; i++) - ST_ASSERT_EQ(tf->s[i], 0ULL); - for (int i = 0; i < 8; i++) - ST_ASSERT_EQ(tf->a[i], 0ULL); - - exec_test_teardown(&ctx); - } - st_pass(); - - /* ---- 9. Verify user stack layout (argc/argv) ---- */ - st_begin("exec: user stack has correct argc/argv layout"); - { - TestElf elf; - build_minimal_elf(&elf); - int err = create_test_file("/test_stack", &elf, sizeof(elf)); - ST_ASSERT_EQ(err, 0); - - ExecTestCtx ctx; - exec_test_setup(&ctx); - - const char* argv[] = { "/test_stack", "arg1", nullptr }; - const char* envp[] = { "A=B", nullptr }; - int ret = do_execve("/test_stack", argv, envp); - ST_ASSERT_EQ(ret, 0); - - /* The stack was set up in the new mm. - * Read argc from the top of the stack using pmap lookup. */ - VmSpace* mm = ctx.curr->mm; - uint64 sp = ctx.fake_tf.sp; - - /* Look up the physical page for sp */ - uint64 pa = 0; - bool mapped = pmap::lookup(mm->root_pa, sp & ~(kPageSize - 1), &pa); - ST_ASSERT(mapped); - - /* Read argc (first uint64 on stack) */ - uint64 page_offset = sp & (kPageSize - 1); - uint8* kva = reinterpret_cast(pa_to_va(PhysAddr{pa}).raw); - uint64 argc = *reinterpret_cast(kva + page_offset); - ST_ASSERT_EQ(argc, 2ULL); /* "/test_stack" and "arg1" */ - - exec_test_teardown(&ctx); - } - st_pass(); - - /* ---- 10. Multiple consecutive execve replaces mm correctly ---- */ - st_begin("exec: consecutive execve replaces address space"); - { - TestElf elf; - build_minimal_elf(&elf); - int err = create_test_file("/test_reexec", &elf, sizeof(elf)); - ST_ASSERT_EQ(err, 0); - - ExecTestCtx ctx; - exec_test_setup(&ctx); - - /* First exec */ - const char* argv[] = { "/test_reexec", nullptr }; - const char* envp[] = { nullptr }; - int ret = do_execve("/test_reexec", argv, envp); - ST_ASSERT_EQ(ret, 0); - - VmSpace* first_mm = ctx.curr->mm; - ST_ASSERT(first_mm != nullptr); - - /* Second exec — should replace the first mm */ - ret = do_execve("/test_reexec", argv, envp); - ST_ASSERT_EQ(ret, 0); - - /* mm should be different (new address space) */ - VmSpace* second_mm = ctx.curr->mm; - ST_ASSERT(second_mm != nullptr); - ST_ASSERT_NE(first_mm, second_mm); - - exec_test_teardown(&ctx); - } - st_pass(); -} diff --git a/na/test/test_fdt.cc b/na/test/test_fdt.cc deleted file mode 100644 index cf09029..0000000 --- a/na/test/test_fdt.cc +++ /dev/null @@ -1,50 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * Boot Self-Test: FDT (Flattened Device Tree) - */ - -#include -#include - -// Manually declare what we need to avoid header conflicts with toolchain libc -extern "C" int strcmp(const char *s1, const char *s2); - -void selftest_fdt() { - st_begin("fdt: tree root access"); - { - DeviceNode* root = fdt_get_node_by_path("/"); - ST_ASSERT(root != nullptr); - ST_ASSERT_EQ(strcmp(root->get_name(), ""), 0); - } - st_pass(); - - st_begin("fdt: find existing compatible node (UART)"); - { - // QEMU virt RISC-V platform always has a ns16550a UART - DeviceNode* uart = fdt_find_compatible("ns16550a"); - ST_ASSERT(uart != nullptr); - - // Use get_prop_string which is convenience helper - const char* compat = uart->get_prop_string("compatible"); - ST_ASSERT(compat != nullptr); - - // Check raw property access - const FdtProperty* reg = uart->get_prop("reg"); - ST_ASSERT(reg != nullptr); - ST_ASSERT(reg->value != nullptr); - ST_ASSERT(reg->len > 0); - } - st_pass(); - - st_begin("fdt: non-existent node/compatible queries"); - { - DeviceNode* missing1 = fdt_get_node_by_path("/definitely_does_not_exist_node_12345"); - ST_ASSERT(missing1 == nullptr); - - DeviceNode* missing2 = fdt_find_compatible("invalid_compatible_string_xyz"); - ST_ASSERT(missing2 == nullptr); - } - st_pass(); -} diff --git a/na/test/test_klog.cc b/na/test/test_klog.cc deleted file mode 100644 index 896e7ed..0000000 --- a/na/test/test_klog.cc +++ /dev/null @@ -1,175 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * Boot Self-Test: klog - * - * Tests: - * 1. read vs read_consume semantics - * 2. subsystem mask filtering - * 3. ratelimit bookkeeping - * 4. flush advances console tail when console filtering is active - * 5. seq counter increments on writes - */ - -#include -#include -#include - -static void drain_klog() -{ - static char buf[512]; - while (true) { - int n = klog_read_consume(buf, sizeof(buf)); - if (n <= 0) { - break; - } - } -} - -static bool test_read_peek_and_consume() -{ - drain_klog(); - - klog_ex(KLOG_INFO, KLOG_SUBSYS_TEST, "klog_test_a"); - klog_ex(KLOG_INFO, KLOG_SUBSYS_TEST, "klog_test_b"); - - KlogStats s0; - klog_stats(&s0); - - char buf[1024]; - int n = klog_read(buf, sizeof(buf) - 1); - if (n <= 0) { - return false; - } - buf[n] = '\0'; - if (!strstr(buf, "klog_test_a") || !strstr(buf, "klog_test_b")) { - return false; - } - - KlogStats s1; - klog_stats(&s1); - if (s1.tail != s0.tail) { - return false; - } - - int n2 = klog_read_consume(buf, sizeof(buf) - 1); - if (n2 <= 0) { - return false; - } - buf[n2] = '\0'; - if (!strstr(buf, "klog_test_a") || !strstr(buf, "klog_test_b")) { - return false; - } - - KlogStats s2; - klog_stats(&s2); - return s2.tail != s0.tail; -} - -static bool test_subsys_mask() -{ - drain_klog(); - - const uint64 all_mask = (1ULL << KLOG_SUBSYS_MAX) - 1; - klog_set_subsys_mask(1ULL << KLOG_SUBSYS_TEST); - - klog_ex(KLOG_INFO, KLOG_SUBSYS_TEST, "klog_subsys_ok"); - klog_ex(KLOG_INFO, KLOG_SUBSYS_GENERIC, "klog_subsys_drop"); - - char buf[1024]; - int n = klog_read_consume(buf, sizeof(buf) - 1); - klog_set_subsys_mask(all_mask); - - if (n <= 0) { - return false; - } - buf[n] = '\0'; - return strstr(buf, "klog_subsys_ok") && !strstr(buf, "klog_subsys_drop"); -} - -static bool test_ratelimit() -{ - KlogRateLimit rl = {}; - rl.interval = 100; - rl.burst = 2; - - bool a = klog_ratelimit(&rl, 10); - bool b = klog_ratelimit(&rl, 20); - bool c = klog_ratelimit(&rl, 30); - - if (!a || !b || c) { - return false; - } - if (rl.printed != 2 || rl.suppressed != 1) { - return false; - } - - bool d = klog_ratelimit(&rl, 200); - if (!d) { - return false; - } - return rl.printed == 1 && rl.suppressed == 0; -} - -static bool test_flush_advances_console_tail() -{ - drain_klog(); - - klog_set_level(KLOG_ERR); - klog_ex(KLOG_INFO, KLOG_SUBSYS_TEST, "klog_flush_msg"); - - KlogStats s1; - klog_stats(&s1); - if (s1.console_tail == s1.head) { - klog_set_level(static_cast(KLOG_DEFAULT_LEVEL)); - return false; - } - - klog_flush(); - - KlogStats s2; - klog_stats(&s2); - - klog_set_level(static_cast(KLOG_DEFAULT_LEVEL)); - return s2.console_tail == s2.head; -} - -static bool test_seq_increments() -{ - drain_klog(); - - KlogStats s0; - klog_stats(&s0); - - klog_ex(KLOG_INFO, KLOG_SUBSYS_TEST, "klog_seq_1"); - klog_ex(KLOG_INFO, KLOG_SUBSYS_TEST, "klog_seq_2"); - - KlogStats s1; - klog_stats(&s1); - - return s1.seq >= s0.seq + 2; -} - -void selftest_klog() -{ - st_begin("klog: read vs read_consume"); - ST_ASSERT(test_read_peek_and_consume()); - st_pass(); - - st_begin("klog: subsys mask filters"); - ST_ASSERT(test_subsys_mask()); - st_pass(); - - st_begin("klog: ratelimit bookkeeping"); - ST_ASSERT(test_ratelimit()); - st_pass(); - - st_begin("klog: flush advances console tail"); - ST_ASSERT(test_flush_advances_console_tail()); - st_pass(); - - st_begin("klog: seq increments on write"); - ST_ASSERT(test_seq_increments()); - st_pass(); -} diff --git a/na/test/test_mutex.cc b/na/test/test_mutex.cc deleted file mode 100644 index e86c984..0000000 --- a/na/test/test_mutex.cc +++ /dev/null @@ -1,183 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * test_mutex — self-tests for the Mutex sleeping lock. - * - * Coverage: - * - basic lock/unlock single-thread - * - try_lock when free and when held - * - mutual exclusion across kthreads (no lost increments) - * - FIFO wake order (A acquires, B/C queue; release lets B in then C) - */ - -#include -#include -#include -#include -#include - -extern TaskControlBlock* kthread_create(const char* name, - void (*entry)(void*), void* arg, - uint8 priority); -extern void task_exit(int code); - -/* ------------------------------------------------------------------ */ -/* Test 1: basic lock/unlock */ -/* ------------------------------------------------------------------ */ - -static void test_basic() -{ - st_begin("mutex: basic lock/unlock"); - - Mutex m; - m.init("test_basic"); - - ST_ASSERT(!m.is_locked()); - m.lock(); - ST_ASSERT(m.is_locked()); - ST_ASSERT(m.is_owned_by_current()); - m.unlock(); - ST_ASSERT(!m.is_locked()); - - st_pass(); -} - -/* ------------------------------------------------------------------ */ -/* Test 2: try_lock */ -/* ------------------------------------------------------------------ */ - -static void test_trylock() -{ - st_begin("mutex: try_lock"); - - Mutex m; - m.init("test_trylock"); - - ST_ASSERT(m.try_lock() == true); - ST_ASSERT(m.is_locked()); - - /* A second try_lock from the same task: since owner == current, - * we still return false (try_lock is non-recursive). */ - ST_ASSERT(m.try_lock() == false); - - m.unlock(); - ST_ASSERT(!m.is_locked()); - ST_ASSERT(m.try_lock() == true); - m.unlock(); - - st_pass(); -} - -/* ------------------------------------------------------------------ */ -/* Test 3: mutual exclusion across kthreads */ -/* ------------------------------------------------------------------ */ - -static Mutex g_mut; -static volatile int32 g_counter = 0; -static volatile int32 g_done = 0; -static constexpr int32 kIters = 200; - -static void mutex_worker(void*) -{ - for (int i = 0; i < kIters; i++) { - g_mut.lock(); - int32 v = g_counter; - /* Yield inside the critical section to expose missing mutex. - * If the mutex were broken we'd see lost increments. */ - sched_yield(); - g_counter = v + 1; - g_mut.unlock(); - } - __sync_fetch_and_add(&g_done, 1); - task_exit(0); -} - -static void test_mutual_exclusion() -{ - st_begin("mutex: mutual exclusion (2 workers)"); - - g_mut.init("test_mutex_mx"); - g_counter = 0; - g_done = 0; - - kthread_create("mut_w1", mutex_worker, nullptr, 0); - kthread_create("mut_w2", mutex_worker, nullptr, 0); - - /* Spin-yield until both workers finish. */ - while (g_done < 2) { - sched_yield(); - } - - ST_ASSERT_EQ(g_counter, 2 * kIters); - ST_ASSERT(!g_mut.is_locked()); - - st_pass(); -} - -/* ------------------------------------------------------------------ */ -/* Test 4: FIFO wake order */ -/* ------------------------------------------------------------------ */ - -static Mutex g_fifo_mut; -static volatile int32 g_fifo_seq = 0; // monotonically increases -static volatile int32 g_fifo_order[3]; // order each worker observed -static volatile int32 g_fifo_done = 0; - -static void fifo_worker(void* arg) -{ - int slot = static_cast(reinterpret_cast(arg)); - g_fifo_mut.lock(); - g_fifo_order[slot] = __sync_fetch_and_add(&g_fifo_seq, 1); - g_fifo_mut.unlock(); - __sync_fetch_and_add(&g_fifo_done, 1); - task_exit(0); -} - -static void test_fifo_order() -{ - st_begin("mutex: FIFO wake order (3 waiters)"); - - g_fifo_mut.init("test_mutex_fifo"); - g_fifo_seq = 0; - g_fifo_done = 0; - g_fifo_order[0] = g_fifo_order[1] = g_fifo_order[2] = -1; - - /* Main grabs the lock first so workers will queue up behind. */ - g_fifo_mut.lock(); - - /* Spawn workers in deterministic order. After each create() yield - * once so the new thread reaches mutex.lock() and enqueues itself - * before we spawn the next one. */ - kthread_create("fifo_A", fifo_worker, reinterpret_cast(0), 0); - sched_yield(); - kthread_create("fifo_B", fifo_worker, reinterpret_cast(1), 0); - sched_yield(); - kthread_create("fifo_C", fifo_worker, reinterpret_cast(2), 0); - sched_yield(); - - /* Release: A should fire next, then B, then C. */ - g_fifo_mut.unlock(); - - while (g_fifo_done < 3) { - sched_yield(); - } - - ST_ASSERT_EQ(g_fifo_order[0], 0); - ST_ASSERT_EQ(g_fifo_order[1], 1); - ST_ASSERT_EQ(g_fifo_order[2], 2); - - st_pass(); -} - -/* ------------------------------------------------------------------ */ -/* Entry */ -/* ------------------------------------------------------------------ */ - -void selftest_mutex() -{ - test_basic(); - test_trylock(); - test_mutual_exclusion(); - test_fifo_order(); -} diff --git a/na/test/test_namei.cc b/na/test/test_namei.cc deleted file mode 100644 index 8e4ecb0..0000000 --- a/na/test/test_namei.cc +++ /dev/null @@ -1,173 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * test_namei.cc — Self-tests for pathname resolution (namei.cc) - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -extern dentry* g_root_dentry; -extern vfsmount* g_root_mnt; - -/* sys_mkdirat from kernel/sys_fs.cc */ -int64 sys_mkdirat(uint64, uint64, uint64, uint64, uint64, uint64); - -void selftest_namei() -{ - /* ---- lookup root "/" ---- */ - st_begin("namei: lookup root /"); - { - path p{}; - int err = filename_lookup(AT_FDCWD, "/", 0, &p); - ST_ASSERT_EQ(err, 0); - ST_ASSERT(p.de == g_root_dentry); - ST_ASSERT(p.de->d_inode != nullptr); - ST_ASSERT(S_ISDIR(p.de->d_inode->i_mode)); - } - st_pass(); - - /* ---- lookup /dev (exists from boot) ---- */ - st_begin("namei: lookup /dev"); - { - path p{}; - int err = filename_lookup(AT_FDCWD, "/dev", 0, &p); - ST_ASSERT_EQ(err, 0); - ST_ASSERT(p.de != nullptr); - ST_ASSERT(p.de->d_inode != nullptr); - ST_ASSERT(S_ISDIR(p.de->d_inode->i_mode)); - } - st_pass(); - - /* ---- lookup nonexistent path returns -ENOENT ---- */ - st_begin("namei: lookup nonexistent returns -ENOENT"); - { - path p{}; - int err = filename_lookup(AT_FDCWD, "/this_does_not_exist", 0, &p); - ST_ASSERT_EQ(err, -ENOENT); - } - st_pass(); - - /* ---- lookup /dev/console (chardev) ---- */ - st_begin("namei: lookup /dev/console"); - { - path p{}; - int err = filename_lookup(AT_FDCWD, "/dev/console", 0, &p); - ST_ASSERT_EQ(err, 0); - ST_ASSERT(p.de->d_inode != nullptr); - ST_ASSERT(S_ISCHR(p.de->d_inode->i_mode)); - } - st_pass(); - - /* ---- lookup with LOOKUP_DIRECTORY on non-dir returns -ENOTDIR ---- */ - st_begin("namei: LOOKUP_DIRECTORY on file returns -ENOTDIR"); - { - path p{}; - int err = filename_lookup(AT_FDCWD, "/dev/console", LOOKUP_DIRECTORY, &p); - ST_ASSERT_EQ(err, -ENOTDIR); - } - st_pass(); - - /* ---- lookup with LOOKUP_DIRECTORY on dir succeeds ---- */ - st_begin("namei: LOOKUP_DIRECTORY on dir succeeds"); - { - path p{}; - int err = filename_lookup(AT_FDCWD, "/dev", LOOKUP_DIRECTORY, &p); - ST_ASSERT_EQ(err, 0); - ST_ASSERT(S_ISDIR(p.de->d_inode->i_mode)); - } - st_pass(); - - /* ---- kern_path shortcut ---- */ - st_begin("namei: kern_path works like filename_lookup"); - { - path p{}; - int err = kern_path("/dev", 0, &p); - ST_ASSERT_EQ(err, 0); - ST_ASSERT(p.de->d_inode != nullptr); - } - st_pass(); - - /* ---- dot path: /dev/. resolves to /dev ---- */ - st_begin("namei: /dev/. resolves to /dev"); - { - path p1{}, p2{}; - int e1 = filename_lookup(AT_FDCWD, "/dev", 0, &p1); - int e2 = filename_lookup(AT_FDCWD, "/dev/.", 0, &p2); - ST_ASSERT_EQ(e1, 0); - ST_ASSERT_EQ(e2, 0); - ST_ASSERT(p1.de == p2.de); - } - st_pass(); - - /* ---- dotdot path: /dev/.. resolves to root ---- */ - st_begin("namei: /dev/.. resolves to root"); - { - path p{}; - int err = filename_lookup(AT_FDCWD, "/dev/..", 0, &p); - ST_ASSERT_EQ(err, 0); - ST_ASSERT(p.de == g_root_dentry); - } - st_pass(); - - /* ---- lookup into non-dir returns -ENOTDIR ---- */ - st_begin("namei: path through non-dir returns -ENOTDIR"); - { - path p{}; - int err = filename_lookup(AT_FDCWD, "/dev/console/child", 0, &p); - ST_ASSERT_EQ(err, -ENOTDIR); - } - st_pass(); - - /* ---- null/empty paths ---- */ - st_begin("namei: null name returns -EINVAL"); - { - path p{}; - int err = filename_lookup(AT_FDCWD, nullptr, 0, &p); - ST_ASSERT_EQ(err, -EINVAL); - } - st_pass(); - - /* ---- LOOKUP_PARENT: returns parent dentry ---- */ - st_begin("namei: LOOKUP_PARENT returns parent"); - { - path p{}; - int err = filename_lookup(AT_FDCWD, "/dev/console", LOOKUP_PARENT, &p); - ST_ASSERT_EQ(err, 0); - /* Should return /dev, not /dev/console */ - path dev_path{}; - int e2 = filename_lookup(AT_FDCWD, "/dev", 0, &dev_path); - ST_ASSERT_EQ(e2, 0); - ST_ASSERT(p.de == dev_path.de); - } - st_pass(); - - /* ---- mkdir + lookup round-trip ---- */ - st_begin("namei: mkdir then lookup succeeds"); - { - /* Create /test_namei_dir via sys_mkdirat */ - int64 ret = sys_mkdirat( - static_cast(-100), - reinterpret_cast("/test_namei_dir"), - 0755, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)0); - - /* Now lookup should find it */ - path p{}; - int err = filename_lookup(AT_FDCWD, "/test_namei_dir", 0, &p); - ST_ASSERT_EQ(err, 0); - ST_ASSERT(p.de->d_inode != nullptr); - ST_ASSERT(S_ISDIR(p.de->d_inode->i_mode)); - } - st_pass(); - - /* NOTE: trailing slash "/dev/" not tested — Phase 1 namei - * doesn't handle empty trailing components. */ -} diff --git a/na/test/test_page_cache.cc b/na/test/test_page_cache.cc deleted file mode 100644 index ee0e0e2..0000000 --- a/na/test/test_page_cache.cc +++ /dev/null @@ -1,180 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * test_page_cache.cc — Self-tests for the page cache hash table - * - * Tests: insert, get, remove, duplicate insert, invalidate, - * get_or_alloc, null args. - */ - -#include -#include -#include -#include -#include - -void selftest_page_cache() -{ - /* Use sentinel pointers as fake "mapping" identifiers. - * These are never dereferenced — only used as hash keys. */ - int mapping_a_tag = 0; - int mapping_b_tag = 0; - void* mapping_a = &mapping_a_tag; - void* mapping_b = &mapping_b_tag; - - /* ---- insert and get ---- */ - st_begin("page_cache: insert then get returns same page"); - { - Page* page = pmm_alloc_page(); - ST_ASSERT(page != nullptr); - - int ret = page_cache_insert(mapping_a, 0, page); - ST_ASSERT_EQ(ret, 0); - - Page* got = page_cache_get(mapping_a, 0); - ST_ASSERT_EQ(got, page); - - /* put the reference from get */ - page_cache_put(got); - - /* Clean up */ - page_cache_remove(mapping_a, 0); - pmm_free_page(page); - } - st_pass(); - - /* ---- get nonexistent returns nullptr ---- */ - st_begin("page_cache: get nonexistent returns nullptr"); - { - Page* got = page_cache_get(mapping_a, 99999); - ST_ASSERT(got == nullptr); - } - st_pass(); - - /* ---- duplicate insert returns -EEXIST ---- */ - st_begin("page_cache: duplicate insert returns -EEXIST"); - { - Page* page = pmm_alloc_page(); - ST_ASSERT(page != nullptr); - - int ret1 = page_cache_insert(mapping_a, 1, page); - ST_ASSERT_EQ(ret1, 0); - - Page* page2 = pmm_alloc_page(); - ST_ASSERT(page2 != nullptr); - - int ret2 = page_cache_insert(mapping_a, 1, page2); - ST_ASSERT_EQ(ret2, -EEXIST); - - /* Clean up */ - page_cache_remove(mapping_a, 1); - pmm_free_page(page); - pmm_free_page(page2); - } - st_pass(); - - /* ---- remove returns page ---- */ - st_begin("page_cache: remove returns page and deletes entry"); - { - Page* page = pmm_alloc_page(); - ST_ASSERT(page != nullptr); - - page_cache_insert(mapping_a, 2, page); - - Page* removed = page_cache_remove(mapping_a, 2); - ST_ASSERT_EQ(removed, page); - - /* Should be gone now */ - Page* got = page_cache_get(mapping_a, 2); - ST_ASSERT(got == nullptr); - - pmm_free_page(page); - } - st_pass(); - - /* ---- remove nonexistent returns nullptr ---- */ - st_begin("page_cache: remove nonexistent returns nullptr"); - { - Page* got = page_cache_remove(mapping_a, 77777); - ST_ASSERT(got == nullptr); - } - st_pass(); - - /* ---- different mappings are independent ---- */ - st_begin("page_cache: different mappings are independent"); - { - Page* pa = pmm_alloc_page(); - Page* pb = pmm_alloc_page(); - ST_ASSERT(pa != nullptr); - ST_ASSERT(pb != nullptr); - - page_cache_insert(mapping_a, 5, pa); - page_cache_insert(mapping_b, 5, pb); - - Page* got_a = page_cache_get(mapping_a, 5); - Page* got_b = page_cache_get(mapping_b, 5); - ST_ASSERT_EQ(got_a, pa); - ST_ASSERT_EQ(got_b, pb); - ST_ASSERT_NE(got_a, got_b); - - page_cache_put(got_a); - page_cache_put(got_b); - page_cache_remove(mapping_a, 5); - page_cache_remove(mapping_b, 5); - pmm_free_page(pa); - pmm_free_page(pb); - } - st_pass(); - - /* ---- invalidate removes all entries for a mapping ---- */ - st_begin("page_cache: invalidate removes all entries for mapping"); - { - Page* pages[4]; - for (int i = 0; i < 4; i++) { - pages[i] = pmm_alloc_page(); - ST_ASSERT(pages[i] != nullptr); - page_cache_insert(mapping_a, 10 + i, pages[i]); - } - - /* Also insert one for mapping_b */ - Page* pb = pmm_alloc_page(); - page_cache_insert(mapping_b, 10, pb); - - /* Invalidate mapping_a */ - page_cache_invalidate(mapping_a); - - /* All mapping_a entries should be gone */ - for (int i = 0; i < 4; i++) { - Page* got = page_cache_get(mapping_a, 10 + i); - ST_ASSERT(got == nullptr); - pmm_free_page(pages[i]); - } - - /* mapping_b should still be there */ - Page* got_b = page_cache_get(mapping_b, 10); - ST_ASSERT_EQ(got_b, pb); - page_cache_put(got_b); - page_cache_remove(mapping_b, 10); - pmm_free_page(pb); - } - st_pass(); - - /* ---- insert null mapping returns -EINVAL ---- */ - st_begin("page_cache: insert null mapping returns -EINVAL"); - { - Page* page = pmm_alloc_page(); - int ret = page_cache_insert(nullptr, 0, page); - ST_ASSERT_EQ(ret, -EINVAL); - pmm_free_page(page); - } - st_pass(); - - /* ---- insert null page returns -EINVAL ---- */ - st_begin("page_cache: insert null page returns -EINVAL"); - { - int ret = page_cache_insert(mapping_a, 0, nullptr); - ST_ASSERT_EQ(ret, -EINVAL); - } - st_pass(); -} diff --git a/na/test/test_pid.cc b/na/test/test_pid.cc deleted file mode 100644 index e79eb1e..0000000 --- a/na/test/test_pid.cc +++ /dev/null @@ -1,106 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * test_pid.cc — Self-tests for PID allocator - * - * Tests: alloc, free, re-use, exhaustion guard, reserved PIDs. - */ - -#include -#include - -void selftest_pid() -{ - /* Create a local PidAllocator for isolated testing */ - PidAllocator pa; - pa.init(); - - /* ---- PID 0 is reserved (idle/swapper) ---- */ - st_begin("pid: PID 0 is reserved"); - { - /* First alloc should return 1 (PID 0 reserved for idle) */ - int32 first = pa.alloc(); - ST_ASSERT(first >= 1); - } - st_pass(); - - /* ---- Sequential allocation returns unique PIDs ---- */ - st_begin("pid: sequential allocation returns unique PIDs"); - { - PidAllocator pa2; - pa2.init(); - - int32 p1 = pa2.alloc(); - int32 p2 = pa2.alloc(); - int32 p3 = pa2.alloc(); - ST_ASSERT(p1 >= 1); - ST_ASSERT(p2 >= 1); - ST_ASSERT(p3 >= 1); - ST_ASSERT_NE(p1, p2); - ST_ASSERT_NE(p2, p3); - ST_ASSERT_NE(p1, p3); - } - st_pass(); - - /* ---- Freed PID can be reused ---- */ - st_begin("pid: freed PID can be reused"); - { - PidAllocator pa3; - pa3.init(); - - /* Allocate all PIDs from 2..N */ - constexpr int kAllocCount = 60; - int32 pids[kAllocCount]; - for (int i = 0; i < kAllocCount; i++) { - pids[i] = pa3.alloc(); - ST_ASSERT(pids[i] >= 1); - } - - /* Free one in the middle */ - int32 freed_pid = pids[kAllocCount / 2]; - pa3.free(freed_pid); - - /* Allocate again — should eventually get the freed PID */ - bool found = false; - for (int i = 0; i < kAllocCount; i++) { - int32 p = pa3.alloc(); - if (p == freed_pid) { found = true; break; } - pa3.free(p); - } - ST_ASSERT(found); - } - st_pass(); - - /* ---- free invalid PID is safe ---- */ - st_begin("pid: free invalid PID is no-op"); - { - PidAllocator pa4; - pa4.init(); - - /* These should not crash */ - pa4.free(-1); - pa4.free(kMaxPid); - pa4.free(kMaxPid + 100); - - /* Allocator should still work — PID 0 still reserved */ - int32 p = pa4.alloc(); - ST_ASSERT(p >= 1); - } - st_pass(); - - /* ---- Monotonic allocation before wrap ---- */ - st_begin("pid: monotonic allocation"); - { - PidAllocator pa5; - pa5.init(); - - int32 prev = pa5.alloc(); - for (int i = 0; i < 10; i++) { - int32 next = pa5.alloc(); - ST_ASSERT(next > prev); - prev = next; - } - } - st_pass(); -} diff --git a/na/test/test_pipe_devfs.cc b/na/test/test_pipe_devfs.cc deleted file mode 100644 index ed375ed..0000000 --- a/na/test/test_pipe_devfs.cc +++ /dev/null @@ -1,420 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * test_pipe_devfs.cc — Self-tests for pipe, /dev/null, /dev/zero, fcntl - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -/* Syscall entry points */ -int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_close(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_read(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_write(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_pipe2(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_fcntl(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_fstat_wrapper(uint64, uint64, uint64, uint64, uint64, uint64); - -/* ================================================================== */ -/* Helpers */ -/* ================================================================== */ - -static int64 open_dev(const char* path, int flags) -{ - return sys_openat( - static_cast(-100), /* AT_FDCWD */ - reinterpret_cast(path), - static_cast(flags), - 0, 0, 0); -} - -static int64 do_close(int64 fd) -{ - return sys_close(static_cast(fd), 0, 0, 0, 0, 0); -} - -static int64 do_read(int64 fd, void* buf, uint64 count) -{ - return sys_read(static_cast(fd), - reinterpret_cast(buf), count, 0, 0, 0); -} - -static int64 do_write(int64 fd, const void* buf, uint64 count) -{ - return sys_write(static_cast(fd), - reinterpret_cast(buf), count, 0, 0, 0); -} - -/* fcntl command constants — must match sys_fs.cc */ -inline constexpr int F_DUPFD = 0; -inline constexpr int F_GETFD = 1; -inline constexpr int F_SETFD = 2; -inline constexpr int F_GETFL = 3; -inline constexpr int F_SETFL = 4; -inline constexpr int F_DUPFD_CLOEXEC = 1030; -inline constexpr int FD_CLOEXEC = 1; - -/* ================================================================== */ -/* /dev/null tests */ -/* ================================================================== */ - -void selftest_devfs() -{ - /* ---- /dev/null: read returns EOF ---- */ - st_begin("devfs: /dev/null read returns 0 (EOF)"); - { - int64 fd = open_dev("/dev/null", O_RDONLY); - ST_ASSERT(fd >= 0); - - char buf[16]; - int64 n = do_read(fd, buf, sizeof(buf)); - ST_ASSERT_EQ(n, (int64)0); - do_close(fd); - } - st_pass(); - - /* ---- /dev/null: write discards data ---- */ - st_begin("devfs: /dev/null write discards data"); - { - int64 fd = open_dev("/dev/null", O_WRONLY); - ST_ASSERT(fd >= 0); - - const char* msg = "discarded"; - int64 n = do_write(fd, msg, 9); - ST_ASSERT_EQ(n, (int64)9); - do_close(fd); - } - st_pass(); - - /* ---- /dev/null: fstat reports S_IFCHR ---- */ - st_begin("devfs: /dev/null fstat is S_IFCHR"); - { - int64 fd = open_dev("/dev/null", O_RDONLY); - ST_ASSERT(fd >= 0); - - kstat st{}; - int64 ret = sys_fstat_wrapper( - static_cast(fd), - reinterpret_cast(&st), 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)0); - ST_ASSERT(S_ISCHR(st.st_mode)); - do_close(fd); - } - st_pass(); - - /* ---- /dev/zero: read returns zeros ---- */ - st_begin("devfs: /dev/zero read returns zeros"); - { - int64 fd = open_dev("/dev/zero", O_RDONLY); - ST_ASSERT(fd >= 0); - - char buf[32]; - /* Pre-fill with non-zero */ - for (int i = 0; i < 32; i++) buf[i] = 0xFF; - - int64 n = do_read(fd, buf, 32); - ST_ASSERT_EQ(n, (int64)32); - - bool all_zero = true; - for (int i = 0; i < 32; i++) { - if (buf[i] != 0) { all_zero = false; break; } - } - ST_ASSERT(all_zero); - do_close(fd); - } - st_pass(); - - /* ---- /dev/zero: write discards data ---- */ - st_begin("devfs: /dev/zero write discards data"); - { - int64 fd = open_dev("/dev/zero", O_WRONLY); - ST_ASSERT(fd >= 0); - - char buf[16] = {1, 2, 3}; - int64 n = do_write(fd, buf, 16); - ST_ASSERT_EQ(n, (int64)16); - do_close(fd); - } - st_pass(); -} - -/* ================================================================== */ -/* Pipe tests */ -/* ================================================================== */ - -void selftest_pipe() -{ - /* ---- pipe2: basic creation ---- */ - st_begin("pipe: pipe2 creates two valid fds"); - { - int fds[2] = {-1, -1}; - int64 ret = sys_pipe2( - reinterpret_cast(fds), 0, 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)0); - ST_ASSERT(fds[0] >= 0); - ST_ASSERT(fds[1] >= 0); - ST_ASSERT_NE(fds[0], fds[1]); - - do_close(fds[0]); - do_close(fds[1]); - } - st_pass(); - - /* ---- pipe: write then read ---- */ - st_begin("pipe: write then read returns correct data"); - { - int fds[2]; - sys_pipe2(reinterpret_cast(fds), 0, 0, 0, 0, 0); - - const char* msg = "pipe_test"; - int64 w = do_write(fds[1], msg, 9); - ST_ASSERT_EQ(w, (int64)9); - - char buf[16] = {}; - int64 r = do_read(fds[0], buf, 16); - ST_ASSERT_EQ(r, (int64)9); - - bool match = true; - for (int i = 0; i < 9; i++) { - if (buf[i] != msg[i]) { match = false; break; } - } - ST_ASSERT(match); - - do_close(fds[0]); - do_close(fds[1]); - } - st_pass(); - - /* ---- pipe: read from closed write end returns EOF ---- */ - st_begin("pipe: read after writer close returns EOF"); - { - int fds[2]; - sys_pipe2(reinterpret_cast(fds), 0, 0, 0, 0, 0); - - /* Write some data, then close write end */ - const char* msg = "eof"; - do_write(fds[1], msg, 3); - do_close(fds[1]); - - /* Read the data */ - char buf[16]; - int64 r1 = do_read(fds[0], buf, 16); - ST_ASSERT_EQ(r1, (int64)3); - - /* Next read should return 0 (EOF) */ - int64 r2 = do_read(fds[0], buf, 16); - ST_ASSERT_EQ(r2, (int64)0); - - do_close(fds[0]); - } - st_pass(); - - /* ---- pipe: write to closed read end returns -EPIPE ---- */ - st_begin("pipe: write after reader close returns -EPIPE"); - { - int fds[2]; - sys_pipe2(reinterpret_cast(fds), 0, 0, 0, 0, 0); - - /* Close read end */ - do_close(fds[0]); - - /* Write should fail */ - const char* msg = "broken"; - int64 w = do_write(fds[1], msg, 6); - ST_ASSERT_EQ(w, (int64)-EPIPE); - - do_close(fds[1]); - } - st_pass(); - - /* ---- pipe: multiple writes accumulate ---- */ - st_begin("pipe: multiple writes accumulate in buffer"); - { - int fds[2]; - sys_pipe2(reinterpret_cast(fds), 0, 0, 0, 0, 0); - - do_write(fds[1], "AAA", 3); - do_write(fds[1], "BBB", 3); - - char buf[16] = {}; - int64 r = do_read(fds[0], buf, 16); - ST_ASSERT_EQ(r, (int64)6); - ST_ASSERT_EQ(buf[0], 'A'); - ST_ASSERT_EQ(buf[3], 'B'); - - do_close(fds[0]); - do_close(fds[1]); - } - st_pass(); - - /* ---- pipe: read partial ---- */ - st_begin("pipe: partial read leaves remainder"); - { - int fds[2]; - sys_pipe2(reinterpret_cast(fds), 0, 0, 0, 0, 0); - - do_write(fds[1], "ABCDEF", 6); - - char buf[3] = {}; - int64 r1 = do_read(fds[0], buf, 3); - ST_ASSERT_EQ(r1, (int64)3); - ST_ASSERT_EQ(buf[0], 'A'); - ST_ASSERT_EQ(buf[2], 'C'); - - int64 r2 = do_read(fds[0], buf, 3); - ST_ASSERT_EQ(r2, (int64)3); - ST_ASSERT_EQ(buf[0], 'D'); - ST_ASSERT_EQ(buf[2], 'F'); - - do_close(fds[0]); - do_close(fds[1]); - } - st_pass(); -} - -/* ================================================================== */ -/* fcntl tests */ -/* ================================================================== */ - -void selftest_fcntl() -{ - /* ---- fcntl F_DUPFD ---- */ - st_begin("fcntl: F_DUPFD duplicates fd"); - { - int64 fd = open_dev("/dev/null", O_RDONLY); - ST_ASSERT(fd >= 0); - - int64 fd2 = sys_fcntl(static_cast(fd), F_DUPFD, 0, 0, 0, 0); - ST_ASSERT(fd2 >= 0); - ST_ASSERT_NE(fd, fd2); - - /* Both should be valid reads */ - char buf[4]; - int64 r = do_read(fd2, buf, 4); - ST_ASSERT_EQ(r, (int64)0); /* /dev/null → EOF */ - - do_close(fd); - do_close(fd2); - } - st_pass(); - - /* ---- fcntl F_DUPFD with min_fd ---- */ - st_begin("fcntl: F_DUPFD respects min_fd"); - { - int64 fd = open_dev("/dev/null", O_RDONLY); - ST_ASSERT(fd >= 0); - - int64 fd2 = sys_fcntl(static_cast(fd), F_DUPFD, 100, 0, 0, 0); - ST_ASSERT(fd2 >= 100); - - do_close(fd); - do_close(fd2); - } - st_pass(); - - /* ---- fcntl F_GETFD / F_SETFD ---- */ - st_begin("fcntl: F_GETFD/F_SETFD cloexec"); - { - int64 fd = open_dev("/dev/null", O_RDONLY); - ST_ASSERT(fd >= 0); - - /* Initially no CLOEXEC */ - int64 flags = sys_fcntl(static_cast(fd), F_GETFD, 0, 0, 0, 0); - ST_ASSERT_EQ(flags, (int64)0); - - /* Set CLOEXEC */ - int64 ret = sys_fcntl(static_cast(fd), F_SETFD, FD_CLOEXEC, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)0); - - /* Verify it was set */ - flags = sys_fcntl(static_cast(fd), F_GETFD, 0, 0, 0, 0); - ST_ASSERT_EQ(flags, (int64)FD_CLOEXEC); - - /* Clear it */ - sys_fcntl(static_cast(fd), F_SETFD, 0, 0, 0, 0); - flags = sys_fcntl(static_cast(fd), F_GETFD, 0, 0, 0, 0); - ST_ASSERT_EQ(flags, (int64)0); - - do_close(fd); - } - st_pass(); - - /* ---- fcntl F_GETFL ---- */ - st_begin("fcntl: F_GETFL returns open flags"); - { - int64 fd = open_dev("/dev/null", O_RDONLY); - ST_ASSERT(fd >= 0); - - int64 fl = sys_fcntl(static_cast(fd), F_GETFL, 0, 0, 0, 0); - /* O_RDONLY == 0, so result should have no write bits */ - ST_ASSERT_EQ(fl & O_WRONLY, (int64)0); - - do_close(fd); - } - st_pass(); - - /* ---- fcntl F_SETFL O_NONBLOCK ---- */ - st_begin("fcntl: F_SETFL toggles O_NONBLOCK"); - { - int64 fd = open_dev("/dev/null", O_RDONLY); - ST_ASSERT(fd >= 0); - - /* Set O_NONBLOCK */ - sys_fcntl(static_cast(fd), F_SETFL, O_NONBLOCK, 0, 0, 0); - int64 fl = sys_fcntl(static_cast(fd), F_GETFL, 0, 0, 0, 0); - ST_ASSERT(fl & O_NONBLOCK); - - /* Clear it */ - sys_fcntl(static_cast(fd), F_SETFL, 0, 0, 0, 0); - fl = sys_fcntl(static_cast(fd), F_GETFL, 0, 0, 0, 0); - ST_ASSERT_EQ(fl & O_NONBLOCK, (int64)0); - - do_close(fd); - } - st_pass(); - - /* ---- fcntl F_DUPFD_CLOEXEC ---- */ - st_begin("fcntl: F_DUPFD_CLOEXEC sets cloexec"); - { - int64 fd = open_dev("/dev/null", O_RDONLY); - ST_ASSERT(fd >= 0); - - int64 fd2 = sys_fcntl(static_cast(fd), F_DUPFD_CLOEXEC, 0, 0, 0, 0); - ST_ASSERT(fd2 >= 0); - - int64 flags = sys_fcntl(static_cast(fd2), F_GETFD, 0, 0, 0, 0); - ST_ASSERT_EQ(flags, (int64)FD_CLOEXEC); - - do_close(fd); - do_close(fd2); - } - st_pass(); - - /* ---- fcntl bad fd ---- */ - st_begin("fcntl: bad fd returns -EBADF"); - { - int64 ret = sys_fcntl(999, F_GETFD, 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)-EBADF); - } - st_pass(); - - /* ---- fcntl unknown cmd ---- */ - st_begin("fcntl: unknown cmd returns -EINVAL"); - { - int64 fd = open_dev("/dev/null", O_RDONLY); - ST_ASSERT(fd >= 0); - - int64 ret = sys_fcntl(static_cast(fd), 9999, 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)-EINVAL); - do_close(fd); - } - st_pass(); -} diff --git a/na/test/test_pmm.cc b/na/test/test_pmm.cc deleted file mode 100644 index 3330866..0000000 --- a/na/test/test_pmm.cc +++ /dev/null @@ -1,283 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * Boot Self-Test: Physical Memory Manager (PMM) - * - * Tests: - * 1. Single page alloc/free round-trip - * 2. Page address alignment (4KB) - * 3. No duplicate pages from consecutive allocs - * 4. Free then re-alloc returns the same page (PCP cache) - * 5. Multi-page (order > 0) alloc/free - * 6. Bulk alloc stress test (alloc many, free all) - * 7. Reference counting (incref/decref) - * 8. page_to_pa / pa_to_page round-trip - */ - -#include -#include -#include - -void selftest_pmm() -{ - st_begin("pmm: single page alloc/free"); - { - Page* page = pmm_alloc_page(); - ST_ASSERT(page != nullptr); - ST_ASSERT(page->type == PageType::Allocated); - ST_ASSERT(page->order == 0); - - uint64 pa = page_to_pa(page).raw; - ST_ASSERT_EQ(pa % kPageSize, 0ULL); // 4KB aligned - - uint64 va = page_to_va(page).raw; - ST_ASSERT_EQ(va, pa_to_va(phys_addr(pa)).raw); - - pmm_free_page(page); - } - st_pass(); - - st_begin("pmm: no duplicate pages"); - { - Page* p1 = pmm_alloc_page(); - Page* p2 = pmm_alloc_page(); - ST_ASSERT(p1 != nullptr); - ST_ASSERT(p2 != nullptr); - ST_ASSERT_NE(p1, p2); - ST_ASSERT_NE(page_to_pa(p1).raw, page_to_pa(p2).raw); - pmm_free_page(p2); - pmm_free_page(p1); - } - st_pass(); - - st_begin("pmm: free then re-alloc (PCP)"); - { - Page* p1 = pmm_alloc_page(); - uint64 pa1 = page_to_pa(p1).raw; - pmm_free_page(p1); - - // PCP should return the most recently freed page - Page* p2 = pmm_alloc_page(); - uint64 pa2 = page_to_pa(p2).raw; - ST_ASSERT_EQ(pa1, pa2); - pmm_free_page(p2); - } - st_pass(); - - st_begin("pmm: multi-page alloc (order 2 = 4 pages)"); - { - Page* page = pmm_alloc_pages(2); - ST_ASSERT(page != nullptr); - ST_ASSERT(page->order == 2); - - uint64 pa = page_to_pa(page).raw; - ST_ASSERT_EQ(pa % (kPageSize * 4), 0ULL); // Naturally aligned - - pmm_free_pages(page, 2); - } - st_pass(); - - st_begin("pmm: bulk alloc/free stress (256 pages)"); - { - constexpr int N = 256; - Page* pages[N]; - - // Allocate all - for (int i = 0; i < N; i++) { - pages[i] = pmm_alloc_page(); - ST_ASSERT(pages[i] != nullptr); - } - - // Verify no duplicates (spot check: consecutive pairs) - for (int i = 1; i < N; i++) { - ST_ASSERT_NE(pages[i], pages[i - 1]); - } - - // Free all - for (int i = 0; i < N; i++) { - pmm_free_page(pages[i]); - } - } - st_pass(); - - st_begin("pmm: page_to_pa / pa_to_page round-trip"); - { - Page* page = pmm_alloc_page(); - ST_ASSERT(page != nullptr); - uint64 pa = page_to_pa(page).raw; - Page* page2 = pa_to_page(phys_addr(pa)); - ST_ASSERT_EQ(page, page2); - pmm_free_page(page); - } - st_pass(); - - st_begin("pmm: refcount incref/decref"); - { - Page* page = pmm_alloc_page(); - ST_ASSERT(page != nullptr); - ST_ASSERT(page->refcount.load() == 1); - - page_incref(page); - ST_ASSERT(page->refcount.load() == 2); - - page_decref(page); // 2 -> 1, should NOT free - ST_ASSERT(page->refcount.load() == 1); - ST_ASSERT(page->type == PageType::Allocated); - - page_decref(page); // 1 -> 0, should free to pool - // After decref to 0, page is returned to pool. - // We can't safely check page->type because it might be reused. - } - st_pass(); - st_begin("pmm: pmm_alloc_pages natural alignment (order-3)"); - { - Page* page = pmm_alloc_pages(3); - ST_ASSERT(page != nullptr); - uint64 pa = page_to_pa(page).raw; - ST_ASSERT_EQ(pa % (kPageSize * 8), 0ULL); - pmm_free_pages(page, 3); - } - st_pass(); - - st_begin("pmm: page_decref to 0 and reuse (order-0)"); - { - Page* page = pmm_alloc_page(); - uint64 pa = page_to_pa(page).raw; - page_decref(page); // drops 1 -> 0, freeing it - - // Next alloc from PCP should give back the same page - Page* page2 = pmm_alloc_page(); - ST_ASSERT_EQ(page_to_pa(page2).raw, pa); - pmm_free_page(page2); - } - st_pass(); - - st_begin("pmm: page_decref to 0 on multi-page blocks (order-3)"); - { - Page* page = pmm_alloc_pages(3); - uint64 pa = page_to_pa(page).raw; - page_decref(page); // Should internally route to pmm_free_pages(page, 3) - - // Re-allocate order-3 and see if we get it back (buddy coalescing was respected) - Page* page2 = pmm_alloc_pages(3); - ST_ASSERT_EQ(page_to_pa(page2).raw, pa); - pmm_free_pages(page2, 3); - } - st_pass(); - - st_begin("pmm: buddy split and block coalescing correctness"); - { - // Force split: allocate order-1, free it, allocate two order-0 - Page* page_ord1 = pmm_alloc_pages(1); - uint64 pa1 = page_to_pa(page_ord1).raw; - pmm_free_pages(page_ord1, 1); - - Page* p0_a = pmm_alloc_page(); - Page* p0_b = pmm_alloc_page(); - - // Free them to trigger coalesce - pmm_free_page(p0_a); - pmm_free_page(p0_b); - - // Allocate order-1 again to verify successful coalescing - Page* page_ord1_again = pmm_alloc_pages(1); - ST_ASSERT_EQ(page_to_pa(page_ord1_again).raw, pa1); - pmm_free_pages(page_ord1_again, 1); - } - st_pass(); - - st_begin("pmm: mixed order cross allocation"); - { - Page* p0 = pmm_alloc_page(); - Page* p2 = pmm_alloc_pages(2); - Page* p0b = pmm_alloc_page(); - - ST_ASSERT(p0 != nullptr); - ST_ASSERT(p2 != nullptr); - ST_ASSERT(p0b != nullptr); - ST_ASSERT_NE(page_to_pa(p0).raw, page_to_pa(p2).raw); - - pmm_free_page(p0); - pmm_free_pages(p2, 2); - pmm_free_page(p0b); - } - st_pass(); - - st_begin("pmm: PCP drain/refill boundaries"); - { - // kPcpHighWatermark = 64. Allocating and freeing 65 blocks triggers drain. - Page* pages[65]; - for (int i=0; i<65; i++) pages[i] = pmm_alloc_page(); - for (int i=0; i<65; i++) pmm_free_page(pages[i]); - - // The system shouldn't crash and PCP logic should handle this. - Page* verify_page = pmm_alloc_page(); - ST_ASSERT(verify_page != nullptr); - pmm_free_page(verify_page); - } - st_pass(); - - st_begin("pmm: OOM exhaustion stability"); - { - Page* head = nullptr; - // First allocate large chunks to be fast - while (true) { - Page* page = pmm_alloc_pages(5); - if (!page) break; - uint64* ptr = reinterpret_cast(page_to_va(page).raw); - *ptr = reinterpret_cast(head); - // encode order in the second word - *(ptr + 1) = 5; - head = page; - } - // Then allocate single frames until absolute zero - while (true) { - Page* page = pmm_alloc_page(); - if (!page) break; - uint64* ptr = reinterpret_cast(page_to_va(page).raw); - *ptr = reinterpret_cast(head); - *(ptr + 1) = 0; - head = page; - } - - // We hit nullptr. System is completely exhausted. - ST_ASSERT(pmm_alloc_page() == nullptr); - - // Free everything - while (head) { - uint64* ptr = reinterpret_cast(page_to_va(head).raw); - Page* nxt = reinterpret_cast(*ptr); - uint64 order = *(ptr + 1); - if (order == 5) pmm_free_pages(head, 5); - else pmm_free_page(head); - head = nxt; - } - } - st_pass(); - - st_begin("pmm: out of bounds order allocation gracefully fails"); - { - // kMaxOrder is 11, so passing 11 or higher should safely return nullptr. - Page* page = pmm_alloc_pages(11); - ST_ASSERT(page == nullptr); - page = pmm_alloc_pages(255); - ST_ASSERT(page == nullptr); - } - st_pass(); - - st_begin("pmm: pmm_get_mem_base / pmm_get_mem_size sanity"); - { - uint64 base = pmm_get_mem_base().raw; - uint64 size = pmm_get_mem_size(); - - // We no longer assert on QEMU specific memory address - // ST_ASSERT(base >= 0x80000000ULL); - ST_ASSERT(size > 0); - // Size must be page-aligned - ST_ASSERT_EQ(size % kPageSize, 0ULL); - // Base must be page-aligned - ST_ASSERT_EQ(base % kPageSize, 0ULL); - } - st_pass(); -} diff --git a/na/test/test_random.cc b/na/test/test_random.cc deleted file mode 100644 index d546410..0000000 --- a/na/test/test_random.cc +++ /dev/null @@ -1,49 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -#include -#include -#include - -void selftest_random() -{ - /* Test 1: Basic 64-bit generation */ - kprintf("[test] random: basic 64-bit generation ... "); - uint64 v1 = get_random_u64(); - uint64 v2 = get_random_u64(); - if (v1 == v2) { - kernel_panic("random: two consecutive generations were identical!"); - } - kprintf("PASS\n"); - - /* Test 2: Basic 32-bit generation */ - kprintf("[test] random: basic 32-bit generation ... "); - uint32 v3 = get_random_u32(); - uint32 v4 = get_random_u32(); - if (v3 == v4) { - kernel_panic("random: two consecutive 32-bit generations were identical!"); - } - kprintf("PASS\n"); - - /* Test 3: Byte generation */ - kprintf("[test] random: byte generation ... "); - uint8 buf1[16]; - uint8 buf2[16]; - get_random_bytes(buf1, sizeof(buf1)); - get_random_bytes(buf2, sizeof(buf2)); - - bool all_zero1 = true; - bool all_zero2 = true; - bool same = true; - - for (int i = 0; i < 16; i++) { - if (buf1[i] != 0) all_zero1 = false; - if (buf2[i] != 0) all_zero2 = false; - if (buf1[i] != buf2[i]) same = false; - } - - if (all_zero1 || all_zero2 || same) { - kernel_panic("random: byte generation statistical failure!"); - } - kprintf("PASS\n"); -} diff --git a/na/test/test_rbtree.cc b/na/test/test_rbtree.cc deleted file mode 100644 index 27f049a..0000000 --- a/na/test/test_rbtree.cc +++ /dev/null @@ -1,119 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -#include -#include - -struct TestNode { - int key; - struct rb_node rb; -}; - -static bool node_less(struct rb_node *node, const struct rb_node *parent) { - TestNode *n = rb_entry(node, TestNode, rb); - const TestNode *p = reinterpret_cast( - reinterpret_cast(parent) - __builtin_offsetof(TestNode, rb)); - return n->key < p->key; -} - -static int node_cmp(const void *key, const struct rb_node *node) { - int k = *static_cast(key); - const TestNode *n = reinterpret_cast( - reinterpret_cast(node) - __builtin_offsetof(TestNode, rb)); - if (k < n->key) return -1; - if (k > n->key) return 1; - return 0; -} - -void selftest_rbtree() { - st_begin("rbtree: basic insert, find, erase"); - { - struct rb_root tree = RB_ROOT; - TestNode n1{10, {}}, n2{20, {}}, n3{5, {}}; - - rb_add(&n1.rb, &tree, node_less); - rb_add(&n2.rb, &tree, node_less); - rb_add(&n3.rb, &tree, node_less); - - int search_key = 20; - struct rb_node *found = rb_find(&search_key, &tree, node_cmp); - ST_ASSERT(found == &n2.rb); - - search_key = 99; - found = rb_find(&search_key, &tree, node_cmp); - ST_ASSERT(found == nullptr); - - rb_erase(&n2.rb, &tree); - // Verify erased - search_key = 20; - found = rb_find(&search_key, &tree, node_cmp); - ST_ASSERT(found == nullptr); - } - st_pass(); - - st_begin("rbtree: ordered iteration"); - { - struct rb_root tree = RB_ROOT; - TestNode nodes[5] = { {50, {}}, {10, {}}, {40, {}}, {20, {}}, {30, {}} }; - for (int i=0; i<5; i++) { - rb_add(&nodes[i].rb, &tree, node_less); - } - - int expected[] = {10, 20, 30, 40, 50}; - int idx = 0; - struct rb_node *node = rb_first(&tree); - while (node) { - TestNode *n = rb_entry(node, TestNode, rb); - ST_ASSERT_EQ(n->key, expected[idx]); - idx++; - node = rb_next(node); - } - ST_ASSERT_EQ(idx, 5); - } - st_pass(); - - st_begin("rbtree: large scale insert/erase balance"); - { - struct rb_root tree = RB_ROOT; - constexpr int N = 200; - static TestNode nodes[N]; // Static to avoid blowing up the BSP stack - for (int i=0; i -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* ======================================================================== - * Helper: create a minimal test TCB (no real kernel stack, just metadata) - * ======================================================================== */ - -static TaskControlBlock g_test_tcbs[8]; - -static TaskControlBlock* make_test_tcb(int index, const char* name, - uint8 priority, uint8 cpu) -{ - TaskControlBlock* t = &g_test_tcbs[index]; - t->context = {}; - t->wait_chld.init("test_wq"); - t->name = name; - t->priority = priority; - t->cpu_id = cpu; - t->pid = 100 + index; - t->tgid = 100 + index; - t->state = TaskState::Runnable; - t->time_slice = priority_to_timeslice(priority); - t->run_link.init(); - t->all_link.init(); - t->children.init(); - t->sibling.init(); - t->mm = nullptr; - t->trapframe = nullptr; - t->kstack_pa = 0; - t->kstack_va = 0; - t->flags = TF_KTHREAD; - t->sched_class = &g_sched_class_o1; - t->sched_entity = &t->run_link; - return t; -} - -/* ======================================================================== - * Test cases - * ======================================================================== */ - -static bool test_current_task() -{ - /* After sched_init, current_task() should be the BSP boot TCB */ - TaskControlBlock* curr = current_task(); - return curr != nullptr && curr->state == TaskState::Running; -} - -static bool test_priority_to_timeslice() -{ - /* Higher priority (lower number) should get longer time slice */ - int32 ts_high = priority_to_timeslice(0); - int32 ts_mid = priority_to_timeslice(15); - int32 ts_low = priority_to_timeslice(31); - - return ts_high > ts_mid && ts_mid > ts_low && ts_low >= 1; -} - -static bool test_o1_pick_next_priority() -{ - RunQueue rq; - rq.init(0); - g_sched_class_o1.init_rq(&rq); - - TaskControlBlock* t_lo = make_test_tcb(0, "test_p5", 5, 0); - TaskControlBlock* t_hi = make_test_tcb(1, "test_p2", 2, 0); - - g_sched_class_o1.enqueue_task(&rq, t_lo); - g_sched_class_o1.enqueue_task(&rq, t_hi); - - TaskControlBlock* next = g_sched_class_o1.pick_next_task(&rq); - return next == t_hi; -} - -static bool test_o1_fifo_order() -{ - RunQueue rq; - rq.init(0); - g_sched_class_o1.init_rq(&rq); - - TaskControlBlock* t0 = make_test_tcb(0, "test_q0", 10, 0); - TaskControlBlock* t1 = make_test_tcb(1, "test_q1", 10, 0); - TaskControlBlock* t2 = make_test_tcb(2, "test_q2", 10, 0); - - g_sched_class_o1.enqueue_task(&rq, t0); - g_sched_class_o1.enqueue_task(&rq, t1); - g_sched_class_o1.enqueue_task(&rq, t2); - - TaskControlBlock* next = g_sched_class_o1.pick_next_task(&rq); - if (next != t0) return false; - g_sched_class_o1.dequeue_task(&rq, t0); - - next = g_sched_class_o1.pick_next_task(&rq); - if (next != t1) return false; - g_sched_class_o1.dequeue_task(&rq, t1); - - next = g_sched_class_o1.pick_next_task(&rq); - if (next != t2) return false; - - return true; -} - -static bool test_o1_nr_running() -{ - RunQueue rq; - rq.init(0); - g_sched_class_o1.init_rq(&rq); - - TaskControlBlock* t0 = make_test_tcb(0, "test_n0", 8, 0); - TaskControlBlock* t1 = make_test_tcb(1, "test_n1", 9, 0); - - if (rq.nr_running() != 0 || rq.o1().nr_running() != 0) return false; - - g_sched_class_o1.enqueue_task(&rq, t0); - if (rq.nr_running() != 1 || rq.o1().nr_running() != 1) return false; - - g_sched_class_o1.enqueue_task(&rq, t1); - if (rq.nr_running() != 2 || rq.o1().nr_running() != 2) return false; - - g_sched_class_o1.dequeue_task(&rq, t0); - if (rq.nr_running() != 1 || rq.o1().nr_running() != 1) return false; - - g_sched_class_o1.dequeue_task(&rq, t1); - return rq.nr_running() == 0 && rq.o1().nr_running() == 0; -} - -static bool test_o1_pick_next_empty() -{ - RunQueue rq; - rq.init(0); - g_sched_class_o1.init_rq(&rq); - - return g_sched_class_o1.pick_next_task(&rq) == nullptr; -} - -static bool test_priority_to_timeslice_bounds() -{ - /* Boundary values */ - int32 ts0 = priority_to_timeslice(0); - int32 ts31 = priority_to_timeslice(31); - - return ts0 == 20 && ts31 >= 1; -} - -static bool test_enqueue_dequeue() -{ - /* Create a test task and enqueue/dequeue it */ - TaskControlBlock* t = make_test_tcb(0, "test_ed", 10, 0); - - sched_enqueue(t); - if (t->state != TaskState::Runnable) return false; - - sched_dequeue(t); - /* After dequeue, run_link should be self-pointing */ - return t->run_link.next == &t->run_link; -} - -static bool test_multiple_enqueue_dequeue() -{ - /* Enqueue 3 tasks at different priorities, dequeue all */ - TaskControlBlock* t0 = make_test_tcb(0, "test_m0", 5, 0); - TaskControlBlock* t1 = make_test_tcb(1, "test_m1", 10, 0); - TaskControlBlock* t2 = make_test_tcb(2, "test_m2", 20, 0); - - sched_enqueue(t0); - sched_enqueue(t1); - sched_enqueue(t2); - - sched_dequeue(t0); - sched_dequeue(t1); - sched_dequeue(t2); - - return t0->run_link.is_empty() && - t1->run_link.is_empty() && - t2->run_link.is_empty(); -} - -static bool test_current_task_name() -{ - TaskControlBlock* curr = current_task(); - if (!curr || !curr->name) return false; - - /* BSP boot TCB should be named "kernel_main" */ - const char* expected = "kernel_main"; - const char* actual = curr->name; - while (*expected && *actual) { - if (*expected != *actual) return false; - expected++; - actual++; - } - return *expected == *actual; -} - -/* ======================================================================== - * Test runner - * ======================================================================== */ - -void selftest_sched() -{ - st_begin("sched: current_task is valid after init"); - ST_ASSERT(test_current_task()); - st_pass(); - - st_begin("sched: current_task name is kernel_main"); - ST_ASSERT(test_current_task_name()); - st_pass(); - - st_begin("sched: priority_to_timeslice ordering"); - ST_ASSERT(test_priority_to_timeslice()); - st_pass(); - - st_begin("sched: priority_to_timeslice bounds"); - ST_ASSERT(test_priority_to_timeslice_bounds()); - st_pass(); - - st_begin("sched: enqueue/dequeue round-trip"); - ST_ASSERT(test_enqueue_dequeue()); - st_pass(); - - st_begin("sched: multiple enqueue/dequeue"); - ST_ASSERT(test_multiple_enqueue_dequeue()); - st_pass(); - - st_begin("sched: o1 pick_next priority"); - ST_ASSERT(test_o1_pick_next_priority()); - st_pass(); - - st_begin("sched: o1 FIFO order"); - ST_ASSERT(test_o1_fifo_order()); - st_pass(); - - st_begin("sched: o1 nr_running accounting"); - ST_ASSERT(test_o1_nr_running()); - st_pass(); - - st_begin("sched: o1 pick_next empty"); - ST_ASSERT(test_o1_pick_next_empty()); - st_pass(); -} - diff --git a/na/test/test_signal.cc b/na/test/test_signal.cc deleted file mode 100644 index 360b0e6..0000000 --- a/na/test/test_signal.cc +++ /dev/null @@ -1,193 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * test_signal.cc — Self-tests for signal infrastructure - * - * Tests the kernel-internal signal APIs directly (not via syscalls, - * since syscalls use copy_to_user which rejects kernel-space buffers). - */ - -#include -#include -#include -#include -#include -#include -#include - -/* From signal.cc */ -extern int send_signal(int sig, kernel_siginfo* info, TaskControlBlock* task); - -/* Drain all queued sigqueue entries from a task's pending list */ -static void flush_pending(TaskControlBlock* task) -{ - task->pending.signal = sigset_t{}; - list_node* head = &task->pending.list; - while (head->next != head) { - list_node* node = head->next; - head->next = node->next; - node->next->prev = head; - auto* q = list_entry(node); - delete q; - } -} - -void selftest_signal() -{ - TaskControlBlock* curr = current_task(); - - /* Ensure sighand exists for this task */ - if (!curr->sighand) { - curr->sighand = new sighand_struct; - curr->sighand->init(); - } - - /* Ensure pending list is initialized */ - if (curr->pending.list.next == nullptr) { - curr->pending.list.next = &curr->pending.list; - curr->pending.list.prev = &curr->pending.list; - curr->pending.signal = sigset_t{}; - } - - /* ---- send_signal queues a signal ---- */ - st_begin("signal: send_signal queues to pending"); - { - curr->pending.signal = sigset_t{}; - - kernel_siginfo info{}; - info.si_signo = SIGUSR1; - info.si_code = 0; - - int ret = send_signal(SIGUSR1, &info, curr); - ST_ASSERT_EQ(ret, 0); - ST_ASSERT(curr->pending.signal.has(SIGUSR1)); - - flush_pending(curr); - } - st_pass(); - - /* ---- send_signal: invalid signal returns -EINVAL ---- */ - st_begin("signal: send_signal invalid sig returns -EINVAL"); - { - int ret = send_signal(0, nullptr, curr); - ST_ASSERT_EQ(ret, -EINVAL); - - ret = send_signal(_NSIG + 1, nullptr, curr); - ST_ASSERT_EQ(ret, -EINVAL); - } - st_pass(); - - /* ---- send_signal: null task returns -ESRCH ---- */ - st_begin("signal: send_signal null task returns -ESRCH"); - { - int ret = send_signal(SIGUSR1, nullptr, nullptr); - ST_ASSERT_EQ(ret, -ESRCH); - } - st_pass(); - - /* ---- duplicate standard signal is coalesced ---- */ - st_begin("signal: duplicate standard signal coalesced"); - { - curr->pending.signal = sigset_t{}; - - send_signal(SIGUSR1, nullptr, curr); - ST_ASSERT(curr->pending.signal.has(SIGUSR1)); - - int ret = send_signal(SIGUSR1, nullptr, curr); - ST_ASSERT_EQ(ret, 0); - - flush_pending(curr); - } - st_pass(); - - /* ---- sigset_t: add/del/has ---- */ - st_begin("signal: sigset_t add/del/has"); - { - sigset_t s{}; - ST_ASSERT(!s.has(SIGUSR1)); - s.add(SIGUSR1); - ST_ASSERT(s.has(SIGUSR1)); - s.del(SIGUSR1); - ST_ASSERT(!s.has(SIGUSR1)); - } - st_pass(); - - /* ---- sigset_t: bitwise operators ---- */ - st_begin("signal: sigset_t bitwise OR and AND"); - { - sigset_t a{}; - a.add(SIGUSR1); - sigset_t b{}; - b.add(SIGUSR2); - - sigset_t c = a | b; - ST_ASSERT(c.has(SIGUSR1)); - ST_ASSERT(c.has(SIGUSR2)); - - sigset_t d = c & a; - ST_ASSERT(d.has(SIGUSR1)); - ST_ASSERT(!d.has(SIGUSR2)); - } - st_pass(); - - /* ---- blocked mask: direct manipulation ---- */ - st_begin("signal: blocked mask direct manipulation"); - { - sigset_t saved = curr->blocked; - curr->blocked = sigset_t{}; - - curr->blocked.add(SIGUSR1); - curr->blocked.add(SIGUSR2); - ST_ASSERT(curr->blocked.has(SIGUSR1)); - ST_ASSERT(curr->blocked.has(SIGUSR2)); - - curr->blocked.del(SIGUSR1); - ST_ASSERT(!curr->blocked.has(SIGUSR1)); - ST_ASSERT(curr->blocked.has(SIGUSR2)); - - curr->blocked = saved; - } - st_pass(); - - /* ---- sighand action table ---- */ - st_begin("signal: sighand action table defaults to SIG_DFL"); - { - /* All handlers should default to SIG_DFL (0) */ - kernel_sigaction* ka = &curr->sighand->action[SIGUSR1 - 1]; - ST_ASSERT_EQ(ka->sa_handler, (uint64)SIG_DFL); - - /* Set a handler and verify */ - ka->sa_handler = 0x12345678ULL; - ST_ASSERT_EQ(ka->sa_handler, (uint64)0x12345678ULL); - - /* Reset */ - ka->sa_handler = SIG_DFL; - } - st_pass(); - - /* ---- sig_kernel_only rejects SIGKILL/SIGSTOP handlers ---- */ - st_begin("signal: SIGKILL/SIGSTOP are kernel-only"); - { - ST_ASSERT(sig_kernel_only(SIGKILL)); - ST_ASSERT(sig_kernel_only(SIGSTOP)); - ST_ASSERT(!sig_kernel_only(SIGUSR1)); - ST_ASSERT(!sig_kernel_only(SIGINT)); - } - st_pass(); - - /* ---- sig_default_action correctness ---- */ - st_begin("signal: sig_default_action returns correct actions"); - { - ST_ASSERT_EQ((int)sig_default_action(SIGCHLD), (int)SIG_ACT_IGN); - ST_ASSERT_EQ((int)sig_default_action(SIGKILL), (int)SIG_ACT_TERM); - ST_ASSERT_EQ((int)sig_default_action(SIGSTOP), (int)SIG_ACT_STOP); - ST_ASSERT_EQ((int)sig_default_action(SIGCONT), (int)SIG_ACT_CONT); - ST_ASSERT_EQ((int)sig_default_action(SIGSEGV), (int)SIG_ACT_CORE); - } - st_pass(); - - /* Clean up */ - flush_pending(curr); - curr->blocked = sigset_t{}; -} diff --git a/na/test/test_slab.cc b/na/test/test_slab.cc deleted file mode 100644 index 8a448b4..0000000 --- a/na/test/test_slab.cc +++ /dev/null @@ -1,255 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * Boot Self-Test: Slab Allocator (kmalloc/kfree) - * - * Tests: - * 1. Small allocation (8 bytes) and free - * 2. Multiple size classes - * 3. No aliasing between consecutive allocs - * 4. Alignment check (minimum 8-byte aligned) - * 5. Bulk alloc/free stress - * 6. operator new / operator delete - * 7. Large allocation (> 2048, falls back to PMM) - * 8. Zero-size and null-free edge cases - */ - -#include -#include -#include -#include - -/* Forward declarations — defined in slab.cc */ -void* kmalloc(uint32 size); -void kfree(void* ptr); - -void selftest_slab() -{ - st_begin("slab: small alloc (8 bytes)"); - { - void* p = kmalloc(8); - ST_ASSERT(p != nullptr); - // Write and read back to verify the memory is usable - *static_cast(p) = 0xDEADBEEFCAFEBABEULL; - ST_ASSERT_EQ(*static_cast(p), 0xDEADBEEFCAFEBABEULL); - kfree(p); - } - st_pass(); - - st_begin("slab: multiple size classes"); - { - uint32 sizes[] = {8, 16, 32, 64, 128, 256, 512, 1024, 2048}; - void* ptrs[9]; - for (int i = 0; i < 9; i++) { - ptrs[i] = kmalloc(sizes[i]); - ST_ASSERT(ptrs[i] != nullptr); - // Write a marker - *static_cast(ptrs[i]) = static_cast(i); - } - // Verify markers (no cross-contamination) - for (int i = 0; i < 9; i++) { - ST_ASSERT_EQ(*static_cast(ptrs[i]), static_cast(i)); - } - for (int i = 0; i < 9; i++) { - kfree(ptrs[i]); - } - } - st_pass(); - - st_begin("slab: no aliasing"); - { - void* a = kmalloc(32); - void* b = kmalloc(32); - ST_ASSERT(a != nullptr); - ST_ASSERT(b != nullptr); - ST_ASSERT_NE(a, b); - kfree(b); - kfree(a); - } - st_pass(); - - st_begin("slab: alignment (8-byte minimum)"); - { - for (int i = 0; i < 16; i++) { - void* p = kmalloc(7); // odd size, should still be 8-aligned - ST_ASSERT(p != nullptr); - ST_ASSERT_EQ(reinterpret_cast(p) % 8, 0ULL); - kfree(p); - } - } - st_pass(); - - st_begin("slab: bulk alloc/free stress (128 x 64B)"); - { - constexpr int N = 128; - void* ptrs[N]; - for (int i = 0; i < N; i++) { - ptrs[i] = kmalloc(64); - ST_ASSERT(ptrs[i] != nullptr); - } - // Free in reverse order - for (int i = N - 1; i >= 0; i--) { - kfree(ptrs[i]); - } - } - st_pass(); - - st_begin("slab: operator new/delete"); - { - struct TestObj { - uint64 a; - uint64 b; - }; - TestObj* obj = new TestObj{42, 99}; - ST_ASSERT(obj != nullptr); - ST_ASSERT_EQ(obj->a, 42ULL); - ST_ASSERT_EQ(obj->b, 99ULL); - delete obj; - } - st_pass(); - - st_begin("slab: large alloc (4096 bytes, PMM fallback)"); - { - void* p = kmalloc(4096); - ST_ASSERT(p != nullptr); - ST_ASSERT_EQ(reinterpret_cast(p) % kPageSize, 0ULL); - kfree(p); - } - st_pass(); - - st_begin("slab: edge cases (zero size, null free)"); - { - void* p = kmalloc(0); - ST_ASSERT(p == nullptr); - kfree(nullptr); // should not crash - } - st_pass(); - st_begin("slab: LIFO freelist reallocation (same address)"); - { - void* a = kmalloc(32); - kfree(a); - void* b = kmalloc(32); - ST_ASSERT_EQ(a, b); - kfree(b); - } - st_pass(); - - st_begin("slab: boundary conditions (2048 bytes size class)"); - { - // 2048 bytes should still use slab (2 objects per page) - // We allocate 3 to guarantee at least two fall in the exact same slab page - void* p1 = kmalloc(2048); - void* p2 = kmalloc(2048); - void* p3 = kmalloc(2048); - - auto diff = [](void* a, void* b) -> uint64 { - uint64 ua = reinterpret_cast(a); - uint64 ub = reinterpret_cast(b); - return ua > ub ? ua - ub : ub - ua; - }; - - // At least one adjacent pair MUST be exactly separated by 2048 bytes - bool found_2048 = (diff(p1, p2) == 2048ULL) || (diff(p2, p3) == 2048ULL); - ST_ASSERT(found_2048); - - kfree(p1); - kfree(p2); - kfree(p3); - } - st_pass(); - - st_begin("slab: cross-page allocation (> 1 page equivalent objects)"); - { - // Allocating more than one page worth of 32-byte objects (4096 / 32 = 128) - constexpr int NUM = 150; - void* ptrs[NUM]; - for (int i=0; i(p) % kPageSize == 0) { - base_ptr = p; - break; - } - drain[drain_count++] = p; - } - - // base_ptr is the very first object in a fresh slab page! - // The page holds exactly 32 objects (4096 / 128 = 32). We already hold 1 (base_ptr). - void* ptrs[32]; - ptrs[0] = base_ptr; - for (int i = 1; i < 32; i++) { - ptrs[i] = kmalloc(128); - } - - // At this point, we completely own all 32 objects of this slab page. - uint64 page_pa = va_to_pa(virt_addr(reinterpret_cast(base_ptr))).raw; - Page* page = pa_to_page(phys_addr(page_pa)); - - ST_ASSERT(page->type == PageType::Slab); - ST_ASSERT(page->slab.obj_count == 32); - - // Free all items in this isolated slab page - for (int i = 0; i < 32; i++) { - kfree(ptrs[i]); - } - - // The page MUST have been returned to the PMM (either global Free or Per-CPU Cached). - ST_ASSERT(page->type == PageType::Free || page->type == PageType::PcpCached); - - // Clean up the drain array - for (int i = 0; i < drain_count; i++) { - kfree(drain[i]); - } - } - st_pass(); - - st_begin("slab: continuous operator new/delete memory leak loop"); - { - struct BigObj { uint8 data[512]; }; - BigObj* ptr = nullptr; - // 10K allocations to ensure no memory leak causes OOM - for (int i = 0; i < 10000; i++) { - ptr = new BigObj; - ST_ASSERT(ptr != nullptr); - delete ptr; - } - } - st_pass(); - - st_begin("slab: allocation pressure (mixed sizes)"); - { - constexpr int MAX_ITEMS = 100; - void* ptrs[MAX_ITEMS]; - uint32 sizes[MAX_ITEMS]; - - for (int i = 0; i < MAX_ITEMS; i++) { - sizes[i] = (i * 17) % 2000 + 8; // Pseudo-random sizes between 8 and 2008 - ptrs[i] = kmalloc(sizes[i]); - ST_ASSERT(ptrs[i] != nullptr); - ST_ASSERT(reinterpret_cast(ptrs[i]) % 8 == 0); - } - - for (int i = 0; i < MAX_ITEMS; i++) { - kfree(ptrs[i]); - } - } - st_pass(); -} diff --git a/na/test/test_smp.cc b/na/test/test_smp.cc deleted file mode 100644 index f934526..0000000 --- a/na/test/test_smp.cc +++ /dev/null @@ -1,136 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * test_smp.cc — Post-SMP boot tests - * - * These tests run after all APs are online. They verify: - * 1. IPI delivery — BSP sends IPI to each AP, AP increments counter. - * 2. Task migration — a task moves from one runqueue to another. - * 3. TLB shootdown IPI — cross-CPU flush. - * - * Called from the init thread (PID 1) context, NOT from the pre-SMP - * self-test suite. - */ - -#include -#include -#include -#include -#include -#include - -/* Shared counter incremented by APs when they receive an IPI */ -static lume::atomic g_ipi_ack_count{0}; - -/* ------------------------------------------------------------------ */ -/* IPI delivery test */ -/* ------------------------------------------------------------------ */ - -void selftest_ipi_delivery() -{ - st_begin("smp: IPI delivery to all APs"); - - /* Reset counter */ - g_ipi_ack_count.store(0, __ATOMIC_RELAXED); - - uint32 bsp = static_cast(arch::cpu::id()); - uint32 num_cpus = 4; /* TODO: derive from FDT */ - - /* Build mask of all APs */ - uint64 ap_mask = 0; - uint32 expected = 0; - for (uint32 i = 0; i < num_cpus; i++) { - if (i == bsp) continue; - ap_mask |= (1ULL << i); - expected++; - } - - /* Send reschedule IPI to all APs */ - smp_send_ipi(ap_mask, IPI_RESCHEDULE); - - /* Wait a short while for APs to process the interrupt. - * APs are in wfi (idle loop), so they'll wake immediately - * on interrupt delivery. We just need to give them time - * to run the handler. */ - for (volatile int i = 0; i < 100000; i++) { - /* spin-wait */ - } - - /* We can't directly check that IPI was received because - * IPI_RESCHEDULE just sets need_resched — no ack. - * Instead verify no crash occurred and the IPI infrastructure - * didn't deadlock. This is a liveness test. */ - - st_pass(); -} - -/* ------------------------------------------------------------------ */ -/* IPI halt test (send to self, skip) */ -/* ------------------------------------------------------------------ */ - -void selftest_ipi_self_skip() -{ - st_begin("smp: IPI reason bitmask accumulation"); - - uint32 cpu = static_cast(arch::cpu::id()); - - /* Manually set multiple reasons atomically */ - g_ipi_data[cpu].pending.store(0, __ATOMIC_RELAXED); - g_ipi_data[cpu].pending.fetch_or(IPI_RESCHEDULE, __ATOMIC_RELEASE); - g_ipi_data[cpu].pending.fetch_or(IPI_TLB_SHOOTDOWN, __ATOMIC_RELEASE); - - uint32 val = g_ipi_data[cpu].pending.load(__ATOMIC_ACQUIRE); - ST_ASSERT_EQ(val, IPI_RESCHEDULE | IPI_TLB_SHOOTDOWN); - - /* Clean up */ - g_ipi_data[cpu].pending.store(0, __ATOMIC_RELAXED); - - st_pass(); -} - -/* ------------------------------------------------------------------ */ -/* TLB shootdown IPI test */ -/* ------------------------------------------------------------------ */ - -void selftest_tlb_shootdown() -{ - st_begin("smp: TLB shootdown IPI (cross-CPU flush)"); - - uint32 bsp = static_cast(arch::cpu::id()); - uint32 num_cpus = 4; - - /* Build AP mask */ - uint64 ap_mask = 0; - for (uint32 i = 0; i < num_cpus; i++) { - if (i == bsp) continue; - ap_mask |= (1ULL << i); - } - - /* Set TLB range on each AP's IpiData */ - for (uint32 i = 0; i < num_cpus; i++) { - if (i == bsp) continue; - g_ipi_data[i].tlb_start = 0; - g_ipi_data[i].tlb_end = 0; /* 0,0 = full flush */ - } - - /* Send TLB shootdown */ - smp_send_ipi(ap_mask, IPI_TLB_SHOOTDOWN); - - /* Wait for APs to process */ - for (volatile int i = 0; i < 100000; i++) {} - - /* If we got here without panic, TLB shootdown succeeded */ - st_pass(); -} - -/* ------------------------------------------------------------------ */ -/* Public entry point */ -/* ------------------------------------------------------------------ */ - -void selftest_smp() -{ - selftest_ipi_delivery(); - selftest_ipi_self_skip(); - selftest_tlb_shootdown(); -} diff --git a/na/test/test_spinlock.cc b/na/test/test_spinlock.cc deleted file mode 100644 index be794a0..0000000 --- a/na/test/test_spinlock.cc +++ /dev/null @@ -1,135 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * Boot Self-Test: Spinlock - * - * Tests: - * 1. Basic acquire/release - * 2. LockGuard RAII acquire/release - * 3. Nested locking (two different locks) - * 4. Interrupt state preservation across lock/unlock - * 5. is_held_by_current_cpu correctness - * - * Note: We cannot test multi-core contention in BSP-only self-test. - * That requires AP participation (future Phase 2 test). - */ - -#include -#include -#include -#include - -void selftest_spinlock() -{ - st_begin("spinlock: basic acquire/release"); - { - Spinlock lock; - lock.init("test_basic"); - - ST_ASSERT(!lock.is_held_by_current_cpu()); - - lock.acquire(); - ST_ASSERT(lock.is_held_by_current_cpu()); - - lock.release(); - ST_ASSERT(!lock.is_held_by_current_cpu()); - } - st_pass(); - - st_begin("spinlock: LockGuard RAII"); - { - Spinlock lock; - lock.init("test_guard"); - - { - LockGuard guard(lock); - ST_ASSERT(lock.is_held_by_current_cpu()); - } - // After scope exit, lock should be released - ST_ASSERT(!lock.is_held_by_current_cpu()); - } - st_pass(); - - st_begin("spinlock: nested locking (two locks)"); - { - Spinlock lock_a, lock_b; - lock_a.init("test_nest_a"); - lock_b.init("test_nest_b"); - - lock_a.acquire(); - ST_ASSERT(lock_a.is_held_by_current_cpu()); - - lock_b.acquire(); - ST_ASSERT(lock_b.is_held_by_current_cpu()); - - lock_b.release(); - ST_ASSERT(!lock_b.is_held_by_current_cpu()); - ST_ASSERT(lock_a.is_held_by_current_cpu()); // A still held - - lock_a.release(); - ST_ASSERT(!lock_a.is_held_by_current_cpu()); - } - st_pass(); - - st_begin("spinlock: interrupt state preservation"); - { - Spinlock lock; - lock.init("test_intr"); - - // Interrupts are currently off (BSP init period), but let's - // verify the lock doesn't corrupt the state. - bool before = arch::cpu::intr_enabled(); - - lock.acquire(); - // Inside lock, interrupts must be disabled - ST_ASSERT(!arch::cpu::intr_enabled()); - lock.release(); - - // After release, interrupt state should be restored - ST_ASSERT_EQ(arch::cpu::intr_enabled(), before); - } - st_pass(); - st_begin("spinlock: deep nesting tracking (> 2)"); - { - Spinlock l1, l2, l3; - l1.init("l1"); - l2.init("l2"); - l3.init("l3"); - - bool before = arch::cpu::intr_enabled(); - - l1.acquire(); - l2.acquire(); - l3.acquire(); - - ST_ASSERT(!arch::cpu::intr_enabled()); - - l3.release(); - ST_ASSERT(!arch::cpu::intr_enabled()); - - l2.release(); - ST_ASSERT(!arch::cpu::intr_enabled()); - - l1.release(); - ST_ASSERT_EQ(arch::cpu::intr_enabled(), before); - } - st_pass(); - - st_begin("spinlock: interrupt leak detection logic (lock_depth metadata)"); - { - Spinlock l; - l.init("l"); - - auto& state = sync::g_cpu_sync_states[arch::cpu::id()]; - int initial_depth = state.lock_depth; - - l.acquire(); - // The lock depth should strictly increase - ST_ASSERT_EQ(state.lock_depth, initial_depth + 1); - l.release(); - - ST_ASSERT_EQ(state.lock_depth, initial_depth); - } - st_pass(); -} diff --git a/na/test/test_sys_fs_extra.cc b/na/test/test_sys_fs_extra.cc deleted file mode 100644 index 705ce63..0000000 --- a/na/test/test_sys_fs_extra.cc +++ /dev/null @@ -1,238 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * test_sys_fs_extra.cc — Self-tests for additional filesystem syscalls - * - * Tests: dup3, chdir/getcwd, writev, ioctl (stub), unlinkat (stub). - * Complements test_vfs_io.cc which covers the core read/write/open/close. - */ - -#include -#include -#include -#include -#include -#include - -/* Syscall entry points */ -int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_close(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_read(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_write(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_dup3(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_chdir(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_getcwd(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_writev(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_ioctl(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_unlinkat(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_mkdirat(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_lseek(uint64, uint64, uint64, uint64, uint64, uint64); - -/* ================================================================== */ -/* Helpers */ -/* ================================================================== */ - -static int64 open_dev(const char* path, int flags) -{ - return sys_openat( - static_cast(-100), - reinterpret_cast(path), - static_cast(flags), 0, 0, 0); -} - -static int64 do_close(int64 fd) -{ - return sys_close(static_cast(fd), 0, 0, 0, 0, 0); -} - -static int64 do_read(int64 fd, void* buf, uint64 count) -{ - return sys_read(static_cast(fd), - reinterpret_cast(buf), count, 0, 0, 0); -} - - -/* iovec must match sys_fs.cc definition */ -struct test_iovec { - void* iov_base; - uint64 iov_len; -}; - -/* ================================================================== */ -/* Tests */ -/* ================================================================== */ - -void selftest_sys_fs_extra() -{ - /* ---- dup3: duplicate fd to specific target ---- */ - st_begin("sys_fs: dup3 duplicates to specific fd"); - { - int64 fd = open_dev("/dev/null", O_RDONLY); - ST_ASSERT(fd >= 0); - - /* dup3 to fd 50 */ - int64 fd2 = sys_dup3(static_cast(fd), 50, 0, 0, 0, 0); - ST_ASSERT_EQ(fd2, (int64)50); - - /* Both should work */ - char buf[4]; - int64 r = do_read(fd2, buf, 4); - ST_ASSERT_EQ(r, (int64)0); /* /dev/null → EOF */ - - do_close(fd); - do_close(fd2); - } - st_pass(); - - /* ---- dup3: same fd returns -EINVAL ---- */ - st_begin("sys_fs: dup3 same fd returns -EINVAL"); - { - int64 fd = open_dev("/dev/null", O_RDONLY); - ST_ASSERT(fd >= 0); - - int64 ret = sys_dup3(static_cast(fd), - static_cast(fd), 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)-EINVAL); - - do_close(fd); - } - st_pass(); - - /* ---- chdir to /dev, getcwd returns /dev ---- */ - st_begin("sys_fs: chdir then getcwd returns new dir"); - { - /* Save cwd */ - char saved_cwd[64] = {}; - sys_getcwd(reinterpret_cast(saved_cwd), 64, 0, 0, 0, 0); - - /* chdir to /dev */ - int64 ret = sys_chdir( - reinterpret_cast("/dev"), 0, 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)0); - - /* getcwd should return /dev */ - char buf[64] = {}; - int64 len = sys_getcwd( - reinterpret_cast(buf), 64, 0, 0, 0, 0); - ST_ASSERT(len > 0); - ST_ASSERT_EQ(buf[0], '/'); - ST_ASSERT_EQ(buf[1], 'd'); - ST_ASSERT_EQ(buf[2], 'e'); - ST_ASSERT_EQ(buf[3], 'v'); - - /* Restore cwd to / */ - sys_chdir(reinterpret_cast("/"), 0, 0, 0, 0, 0); - } - st_pass(); - - /* ---- chdir to nonexistent returns -ENOENT ---- */ - st_begin("sys_fs: chdir nonexistent returns -ENOENT"); - { - int64 ret = sys_chdir( - reinterpret_cast("/no_such_dir"), 0, 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)-ENOENT); - } - st_pass(); - - /* ---- chdir to file returns -ENOTDIR ---- */ - st_begin("sys_fs: chdir to file returns -ENOTDIR"); - { - int64 ret = sys_chdir( - reinterpret_cast("/dev/null"), 0, 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)-ENOTDIR); - } - st_pass(); - - /* ---- writev: scatter write to file ---- */ - st_begin("sys_fs: writev scatter write"); - { - /* Create a file, write with writev, read back */ - int64 fd = sys_openat( - static_cast(-100), - reinterpret_cast("/writev_test"), - O_CREAT | O_RDWR, 0644, 0, 0); - ST_ASSERT(fd >= 0); - - char a[] = "AAA"; - char b[] = "BBB"; - test_iovec iov[2]; - iov[0].iov_base = a; - iov[0].iov_len = 3; - iov[1].iov_base = b; - iov[1].iov_len = 3; - - int64 w = sys_writev( - static_cast(fd), - reinterpret_cast(iov), 2, 0, 0, 0); - ST_ASSERT_EQ(w, (int64)6); - - /* Seek back to start and read */ - sys_lseek(static_cast(fd), 0, 0 /* SEEK_SET */, 0, 0, 0); - - char buf[8] = {}; - int64 r = do_read(fd, buf, 8); - ST_ASSERT_EQ(r, (int64)6); - ST_ASSERT_EQ(buf[0], 'A'); - ST_ASSERT_EQ(buf[3], 'B'); - - do_close(fd); - } - st_pass(); - - /* ---- writev: bad fd returns -EBADF ---- */ - st_begin("sys_fs: writev bad fd returns -EBADF"); - { - test_iovec iov[1]; - char dummy[] = "x"; - iov[0].iov_base = dummy; - iov[0].iov_len = 1; - - int64 ret = sys_writev(999, - reinterpret_cast(iov), 1, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)-EBADF); - } - st_pass(); - - /* ---- ioctl stub returns -ENOTTY ---- */ - st_begin("sys_fs: ioctl stub returns -ENOTTY"); - { - int64 fd = open_dev("/dev/null", O_RDONLY); - ST_ASSERT(fd >= 0); - - int64 ret = sys_ioctl(static_cast(fd), 0, 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)-ENOTTY); - - do_close(fd); - } - st_pass(); - - /* ---- unlinkat stub returns -ENOSYS ---- */ - st_begin("sys_fs: unlinkat stub returns -ENOSYS"); - { - int64 ret = sys_unlinkat( - static_cast(-100), - reinterpret_cast("/some_file"), - 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)-ENOSYS); - } - st_pass(); - - /* ---- getcwd: null buf returns -EINVAL ---- */ - st_begin("sys_fs: getcwd null buf returns -EINVAL"); - { - int64 ret = sys_getcwd(0, 64, 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)-EINVAL); - } - st_pass(); - - /* ---- getcwd: zero size returns -EINVAL ---- */ - st_begin("sys_fs: getcwd zero size returns -EINVAL"); - { - char buf[4]; - int64 ret = sys_getcwd( - reinterpret_cast(buf), 0, 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)-EINVAL); - } - st_pass(); -} diff --git a/na/test/test_sys_mm.cc b/na/test/test_sys_mm.cc deleted file mode 100644 index 2c1a7dc..0000000 --- a/na/test/test_sys_mm.cc +++ /dev/null @@ -1,178 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -#include -#include -#include -#include -#include - -/* sys_mm.cc APIs */ -int64 sys_brk(uint64 new_brk, uint64, uint64, uint64, uint64, uint64); -int64 sys_mmap(uint64 addr, uint64 length, uint64 prot, - uint64 flags, uint64 fd, uint64 offset); -int64 sys_munmap(uint64 addr, uint64 length, uint64, uint64, uint64, uint64); -int64 sys_mprotect(uint64 addr, uint64 len, uint64 prot, - uint64, uint64, uint64); - -void selftest_sys_mm() -{ - st_begin("sys_mm: mmap invalid arguments"); - { - TaskControlBlock* curr = current_task(); - VmSpace* old_mm = curr->mm; - - VmSpace* space = VmSpace::create(); - ST_ASSERT(space != nullptr); - curr->mm = space; - - ST_ASSERT_EQ(sys_mmap(0, 0, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0), -EINVAL); - ST_ASSERT_EQ(sys_mmap(0, 0x1000, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, 0, 1), -EINVAL); - ST_ASSERT_EQ(sys_mmap(0, 0x1000, PROT_READ, 0, 0, 0), -EINVAL); - ST_ASSERT_EQ(sys_mmap(0, 0x1000, PROT_READ, MAP_PRIVATE | MAP_SHARED, 0, 0), -EINVAL); - ST_ASSERT_EQ(sys_mmap(0x123, 0x1000, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0), -EINVAL); - - curr->mm = old_mm; - space->destroy(); - } - st_pass(); - - st_begin("sys_mm: mmap/munmap/mprotect basic flow"); - { - TaskControlBlock* curr = current_task(); - VmSpace* old_mm = curr->mm; - - VmSpace* space = VmSpace::create(); - ST_ASSERT(space != nullptr); - curr->mm = space; - - int64 addr = sys_mmap(0, 0x3000, PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANONYMOUS, 0, 0); - ST_ASSERT(addr > 0); - - ST_ASSERT_EQ(vmm_handle_page_fault(space, addr, VmFaultCause::Read), 0); - - int64 mp = sys_mprotect(addr + kPageSize, kPageSize, PROT_READ, 0, 0, 0); - ST_ASSERT_EQ(mp, 0); - ST_ASSERT_EQ(vmm_handle_page_fault(space, addr + kPageSize, VmFaultCause::Write), -EFAULT); - - int64 mu = sys_munmap(addr, 0x3000, 0, 0, 0, 0); - ST_ASSERT_EQ(mu, 0); - ST_ASSERT_EQ(vmm_handle_page_fault(space, addr, VmFaultCause::Read), -EFAULT); - - curr->mm = old_mm; - space->destroy(); - } - st_pass(); - - st_begin("sys_mm: MAP_FIXED replaces existing mapping"); - { - TaskControlBlock* curr = current_task(); - VmSpace* old_mm = curr->mm; - - VmSpace* space = VmSpace::create(); - ST_ASSERT(space != nullptr); - curr->mm = space; - - uint64 base = 0x600000; - ST_ASSERT_EQ(sys_mmap(base, 0x2000, PROT_READ, - MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0), - static_cast(base)); - - ST_ASSERT_EQ(sys_mmap(base, 0x1000, PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0), - static_cast(base)); - - ST_ASSERT_EQ(vmm_handle_page_fault(space, base, VmFaultCause::Read), 0); - ST_ASSERT_EQ(vmm_handle_page_fault(space, base, VmFaultCause::Write), 0); - - curr->mm = old_mm; - space->destroy(); - } - st_pass(); - - st_begin("sys_mm: mmap hint fallback on overlap"); - { - TaskControlBlock* curr = current_task(); - VmSpace* old_mm = curr->mm; - - VmSpace* space = VmSpace::create(); - ST_ASSERT(space != nullptr); - curr->mm = space; - - uint64 base = 0x700000; - ST_ASSERT_EQ(sys_mmap(base, 0x2000, PROT_READ, - MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0), - static_cast(base)); - - int64 hint = sys_mmap(base, 0x1000, PROT_READ, - MAP_PRIVATE | MAP_ANONYMOUS, 0, 0); - ST_ASSERT(hint > 0); - ST_ASSERT(hint != static_cast(base)); - ST_ASSERT_EQ(vmm_handle_page_fault(space, hint, VmFaultCause::Read), 0); - - curr->mm = old_mm; - space->destroy(); - } - st_pass(); - - st_begin("sys_mm: brk growth and shrink"); - { - TaskControlBlock* curr = current_task(); - VmSpace* old_mm = curr->mm; - - VmSpace* space = VmSpace::create(); - ST_ASSERT(space != nullptr); - curr->mm = space; - - int64 brk0 = sys_brk(0x400000, 0, 0, 0, 0, 0); - ST_ASSERT_EQ(brk0, 0x400000); - - int64 brk1 = sys_brk(0x402000, 0, 0, 0, 0, 0); - ST_ASSERT_EQ(brk1, 0x402000); - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x400000, VmFaultCause::Read), 0); - - int64 brk2 = sys_brk(0x400000, 0, 0, 0, 0, 0); - ST_ASSERT_EQ(brk2, 0x400000); - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x401000, VmFaultCause::Read), -EFAULT); - - curr->mm = old_mm; - space->destroy(); - } - st_pass(); - - st_begin("sys_mm: brk invalid shrink below start"); - { - TaskControlBlock* curr = current_task(); - VmSpace* old_mm = curr->mm; - - VmSpace* space = VmSpace::create(); - ST_ASSERT(space != nullptr); - curr->mm = space; - - ST_ASSERT_EQ(sys_brk(0x500000, 0, 0, 0, 0, 0), 0x500000); - ST_ASSERT_EQ(sys_brk(0x4FF000, 0, 0, 0, 0, 0), -EINVAL); - - curr->mm = old_mm; - space->destroy(); - } - st_pass(); - - st_begin("sys_mm: mprotect invalid range"); - { - TaskControlBlock* curr = current_task(); - VmSpace* old_mm = curr->mm; - - VmSpace* space = VmSpace::create(); - ST_ASSERT(space != nullptr); - curr->mm = space; - - ST_ASSERT_EQ(sys_mmap(0, 0x1000, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0) > 0, true); - ST_ASSERT_EQ(sys_mprotect(0x123, 0x1000, PROT_READ, 0, 0, 0), -EINVAL); - ST_ASSERT_EQ(sys_mprotect(0x900000, 0x1000, PROT_READ, 0, 0, 0), -ENOMEM); - - curr->mm = old_mm; - space->destroy(); - } - st_pass(); -} diff --git a/na/test/test_sys_process.cc b/na/test/test_sys_process.cc deleted file mode 100644 index 686bed6..0000000 --- a/na/test/test_sys_process.cc +++ /dev/null @@ -1,93 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * test_sys_process.cc — Self-tests for process-control syscalls - * - * Tests: getpid, gettid, getppid, getuid, getgid, set_tid_address, - * sched_yield, uname. - */ - -#include -#include -#include -#include - -/* Syscall entry points */ -int64 sys_getpid(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_gettid(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_getppid(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_getuid(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_getgid(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_set_tid_address(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_sched_yield_wrapper(uint64, uint64, uint64, uint64, uint64, uint64); - -void selftest_sys_process() -{ - /* ---- getpid returns current tgid ---- */ - st_begin("sys_process: getpid returns valid pid"); - { - int64 pid = sys_getpid(0, 0, 0, 0, 0, 0); - /* Boot task pid is -1 but tgid may differ; just verify it returns */ - TaskControlBlock* curr = current_task(); - ST_ASSERT_EQ(pid, (int64)curr->tgid); - } - st_pass(); - - /* ---- gettid returns current pid (tid) ---- */ - st_begin("sys_process: gettid returns current tid"); - { - int64 tid = sys_gettid(0, 0, 0, 0, 0, 0); - TaskControlBlock* curr = current_task(); - ST_ASSERT_EQ(tid, (int64)curr->pid); - } - st_pass(); - - /* ---- getppid returns parent tgid ---- */ - st_begin("sys_process: getppid returns parent tgid"); - { - int64 ppid = sys_getppid(0, 0, 0, 0, 0, 0); - TaskControlBlock* curr = current_task(); - int64 expected = curr->parent ? (int64)curr->parent->tgid : 0; - ST_ASSERT_EQ(ppid, expected); - } - st_pass(); - - /* ---- getuid returns 0 (root) ---- */ - st_begin("sys_process: getuid returns 0 (root)"); - { - int64 uid = sys_getuid(0, 0, 0, 0, 0, 0); - ST_ASSERT_EQ(uid, (int64)0); - } - st_pass(); - - /* ---- getgid returns 0 (root) ---- */ - st_begin("sys_process: getgid returns 0 (root)"); - { - int64 gid = sys_getgid(0, 0, 0, 0, 0, 0); - ST_ASSERT_EQ(gid, (int64)0); - } - st_pass(); - - /* ---- set_tid_address stores pointer and returns tid ---- */ - st_begin("sys_process: set_tid_address returns tid"); - { - uint64 dummy_addr = 0x12345678ULL; - int64 tid = sys_set_tid_address(dummy_addr, 0, 0, 0, 0, 0); - TaskControlBlock* curr = current_task(); - ST_ASSERT_EQ(tid, (int64)curr->pid); - ST_ASSERT_EQ(curr->clear_child_tid, dummy_addr); - - /* Reset it to avoid side effects */ - curr->clear_child_tid = 0; - } - st_pass(); - - /* ---- sched_yield returns 0 ---- */ - st_begin("sys_process: sched_yield returns 0"); - { - int64 ret = sys_sched_yield_wrapper(0, 0, 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)0); - } -} - diff --git a/na/test/test_trap.cc b/na/test/test_trap.cc deleted file mode 100644 index c91948a..0000000 --- a/na/test/test_trap.cc +++ /dev/null @@ -1,197 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * Boot self-tests for the Trap/IRQ subsystem. - * - * Tests MD→MI translation, IrqManager register/dispatch/unregister, - * and stvec installation verification. - */ - -#include -#include -#include -#include -#include -#include -#include - -TrapCause trap_translate_scause(uint64 scause); - -/* ======================================================================== - * IrqManager test helpers - * ======================================================================== */ - -static volatile uint32 g_test_irq_fired = 0; -static volatile void* g_test_irq_arg = nullptr; - -static void test_irq_handler(uint32 irq, void* arg) -{ - g_test_irq_fired = irq; - g_test_irq_arg = arg; -} - -/* ======================================================================== - * Test cases - * ======================================================================== */ - -static bool test_stvec_installed() { - uint64 vec = arch::trap::get_vector(); - return vec != 0; -} - -static bool test_scause_exception_translation() { - /* Verify all exception causes map correctly */ - bool ok = true; - ok = ok && (trap_translate_scause(EXC_LOAD_PAGE_FAULT) == TrapCause::PageFaultLoad); - ok = ok && (trap_translate_scause(EXC_STORE_PAGE_FAULT) == TrapCause::PageFaultStore); - ok = ok && (trap_translate_scause(EXC_INST_PAGE_FAULT) == TrapCause::PageFaultExec); - ok = ok && (trap_translate_scause(EXC_ECALL_FROM_U) == TrapCause::Syscall); - ok = ok && (trap_translate_scause(EXC_ILLEGAL_INST) == TrapCause::IllegalInst); - ok = ok && (trap_translate_scause(EXC_BREAKPOINT) == TrapCause::Breakpoint); - ok = ok && (trap_translate_scause(EXC_LOAD_ACCESS_FAULT) == TrapCause::AccessFault); - ok = ok && (trap_translate_scause(EXC_INST_MISALIGNED) == TrapCause::AlignFault); - return ok; -} - -static bool test_scause_interrupt_translation() { - bool ok = true; - ok = ok && (trap_translate_scause(SCAUSE_INTERRUPT | IRQ_S_TIMER) == TrapCause::IrqTimer); - ok = ok && (trap_translate_scause(SCAUSE_INTERRUPT | IRQ_S_SOFTWARE) == TrapCause::IrqSoftware); - ok = ok && (trap_translate_scause(SCAUSE_INTERRUPT | IRQ_S_EXTERNAL) == TrapCause::IrqExternal); - ok = ok && (trap_translate_scause(SCAUSE_INTERRUPT | 99) == TrapCause::Unknown); - return ok; -} - -static bool test_scause_unknown() { - /* ecall from S-mode should map to Unknown (we only handle U-mode ecall) */ - return trap_translate_scause(EXC_ECALL_FROM_S) == TrapCause::Unknown; -} - -static bool test_irqmanager_register_dispatch() { - IrqManager mgr; - mgr.init(); - - g_test_irq_fired = 0; - g_test_irq_arg = nullptr; - - int dummy_arg = 42; - int ret = mgr.register_handler(10, test_irq_handler, &dummy_arg); - if (ret != 0) return false; - - /* Dispatch IRQ 10 — handler should be called */ - mgr.dispatch(10); - - if (g_test_irq_fired != 10) return false; - if (g_test_irq_arg != &dummy_arg) return false; - - return true; -} - -static bool test_irqmanager_unregister() { - IrqManager mgr; - mgr.init(); - - g_test_irq_fired = 0; - - mgr.register_handler(5, test_irq_handler, nullptr); - mgr.unregister_handler(5); - - /* Dispatch after unregister — handler should NOT be called */ - mgr.dispatch(5); - - return g_test_irq_fired == 0; -} - -static bool test_irqmanager_double_register() { - IrqManager mgr; - mgr.init(); - - int ret1 = mgr.register_handler(7, test_irq_handler, nullptr); - int ret2 = mgr.register_handler(7, test_irq_handler, nullptr); - - return ret1 == 0 && ret2 == -EBUSY; -} - -static bool test_irqmanager_invalid_irq() { - IrqManager mgr; - mgr.init(); - - int ret = mgr.register_handler(999, test_irq_handler, nullptr); - return ret == -EINVAL; -} - -static bool test_irqmanager_has_handler() { - IrqManager mgr; - mgr.init(); - - if (mgr.has_handler(10)) return false; /* not yet registered */ - - mgr.register_handler(10, test_irq_handler, nullptr); - if (!mgr.has_handler(10)) return false; /* now registered */ - - mgr.unregister_handler(10); - if (mgr.has_handler(10)) return false; /* unregistered */ - - return true; -} - -static bool test_irqmanager_spurious_dispatch() { - IrqManager mgr; - mgr.init(); - - g_test_irq_fired = 0; - - /* Dispatch an unregistered IRQ — should not crash or fire */ - mgr.dispatch(0); - mgr.dispatch(63); - - return g_test_irq_fired == 0; -} - -/* ======================================================================== - * Test runner - * ======================================================================== */ - -void selftest_trap() -{ - st_begin("trap: stvec is installed"); - ST_ASSERT(test_stvec_installed()); - st_pass(); - - st_begin("trap: scause exception translation"); - ST_ASSERT(test_scause_exception_translation()); - st_pass(); - - st_begin("trap: scause interrupt translation"); - ST_ASSERT(test_scause_interrupt_translation()); - st_pass(); - - st_begin("trap: scause unknown (ecall from S)"); - ST_ASSERT(test_scause_unknown()); - st_pass(); - - st_begin("trap: IrqManager register/dispatch"); - ST_ASSERT(test_irqmanager_register_dispatch()); - st_pass(); - - st_begin("trap: IrqManager unregister"); - ST_ASSERT(test_irqmanager_unregister()); - st_pass(); - - st_begin("trap: IrqManager double register"); - ST_ASSERT(test_irqmanager_double_register()); - st_pass(); - - st_begin("trap: IrqManager invalid IRQ"); - ST_ASSERT(test_irqmanager_invalid_irq()); - st_pass(); - - st_begin("trap: IrqManager has_handler"); - ST_ASSERT(test_irqmanager_has_handler()); - st_pass(); - - st_begin("trap: IrqManager spurious dispatch"); - ST_ASSERT(test_irqmanager_spurious_dispatch()); - st_pass(); -} diff --git a/na/test/test_uaccess.cc b/na/test/test_uaccess.cc deleted file mode 100644 index c5a8c6c..0000000 --- a/na/test/test_uaccess.cc +++ /dev/null @@ -1,97 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -#include -#include -#include - -#include -#include -#include -#include -#include - -void selftest_uaccess() -{ - /* To test valid user addresses (< kKernelVaBase), we need a writable page mapped there. - * We cannot use VmSpace::create() because it does not yet inherit kernel mappings, - * so switching to it hangs. We also cannot map it with the U bit because S-mode - * would need the SUM bit enabled. - * So we manually allocate a physical page and map it into the CURRENT page table - * at 0x100000 without the U bit, just for this test. - */ - uint64 mock_user_addr = 0x100000; - - uint64 satp; - asm volatile("csrr %0, satp" : "=r"(satp)); - uint64 root_pa = (satp & 0xFFFFFFFFFFFULL) << 12; - - Page* page = pmm_alloc_page(); - ST_ASSERT(page != nullptr); - uint64 paddr = page_to_pa(page).raw; - - pmap::map(root_pa, mock_user_addr, paddr, pmap::PTE_R | pmap::PTE_W | pmap::PTE_V | pmap::PTE_A | pmap::PTE_D); - - /* Flush TLB to see the new mapping */ - tlb_shootdown_all(); - - st_begin("uaccess: copy_to_user valid range"); - char kernel_buf[32] = "Hello User Space!"; - int ret = copy_to_user(mock_user_addr, kernel_buf, 18); - ST_ASSERT_EQ(ret, 0); - st_pass(); - - st_begin("uaccess: copy_from_user valid range"); - char read_buf[32] = {0}; - /* We wrote "Hello User Space!" to mock_user_addr previously */ - ret = copy_from_user(read_buf, mock_user_addr, 18); - ST_ASSERT_EQ(ret, 0); - ST_ASSERT(read_buf[0] == 'H'); - ST_ASSERT(read_buf[1] == 'e'); - ST_ASSERT(read_buf[6] == 'U'); - st_pass(); - - st_begin("uaccess: strncpy_from_user valid range"); - char str_buf[32] = {0}; - int64 len = strncpy_from_user(str_buf, mock_user_addr, 32); - ST_ASSERT_EQ(len, 17); /* "Hello User Space!" is 17 chars long */ - ST_ASSERT(str_buf[0] == 'H'); - st_pass(); - - st_begin("uaccess: strncpy_from_user max_len truncation"); - char trunc_buf[32] = {0}; - len = strncpy_from_user(trunc_buf, mock_user_addr, 6); - ST_ASSERT_EQ(len, 5); /* Will copy 5 chars and NUL terminate */ - ST_ASSERT(trunc_buf[4] == 'o'); - ST_ASSERT(trunc_buf[5] == '\0'); - st_pass(); - - st_begin("uaccess: copy_to_user invalid range (crosses boundary)"); - ret = copy_to_user(kKernelVaBase - 10, kernel_buf, 20); - ST_ASSERT_EQ(ret, -EFAULT); - st_pass(); - - st_begin("uaccess: copy_to_user invalid range (entirely in kernel)"); - ret = copy_to_user(kKernelVaBase + 0x1000, kernel_buf, 20); - ST_ASSERT_EQ(ret, -EFAULT); - st_pass(); - - st_begin("uaccess: copy_to_user invalid range (overflow)"); - ret = copy_to_user(0xFFFFFFFFFFFFFFF0ULL, kernel_buf, 0x100); - ST_ASSERT_EQ(ret, -EFAULT); - st_pass(); - - st_begin("uaccess: copy_from_user invalid ranges"); - ret = copy_from_user(read_buf, kKernelVaBase - 5, 10); - ST_ASSERT_EQ(ret, -EFAULT); - - ret = copy_from_user(read_buf, kKernelVaBase, 10); - ST_ASSERT_EQ(ret, -EFAULT); - st_pass(); - - st_begin("uaccess: strncpy_from_user invalid range"); - uint64 invalid_addr = kKernelVaBase + 0x1000; - len = strncpy_from_user(str_buf, invalid_addr, 10); - ST_ASSERT_EQ(len, -EFAULT); - st_pass(); -} diff --git a/na/test/test_vfs_io.cc b/na/test/test_vfs_io.cc deleted file mode 100644 index 3d3b125..0000000 --- a/na/test/test_vfs_io.cc +++ /dev/null @@ -1,345 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * test_vfs_io.cc — Self-tests for VFS file I/O via syscalls - * - * Tests openat, read, write, close, lseek, dup, fstat, getdents64, - * getcwd, chdir, and mkdirat through the kernel syscall layer. - */ - -#include -#include -#include -#include -#include -#include -#include - -/* Syscall entry points */ -int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_close(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_read(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_write(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_lseek(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_dup(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_dup3(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_fstat_wrapper(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_getcwd(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_chdir(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_mkdirat(uint64, uint64, uint64, uint64, uint64, uint64); -int64 sys_getdents64(uint64, uint64, uint64, uint64, uint64, uint64); - -/* Helper: open a file, returns fd or negative error */ -static int64 open_file(const char* path, int flags, uint32 mode) -{ - return sys_openat( - static_cast(-100), /* AT_FDCWD */ - reinterpret_cast(path), - static_cast(flags), - static_cast(mode), 0, 0); -} - -void selftest_vfs_io() -{ - /* ---- openat O_CREAT creates a file ---- */ - st_begin("vfs_io: openat O_CREAT creates file"); - { - int64 fd = open_file("/vfs_test_file", O_CREAT | O_WRONLY, 0644); - ST_ASSERT(fd >= 0); - sys_close(static_cast(fd), 0, 0, 0, 0, 0); - } - st_pass(); - - /* ---- openat existing file ---- */ - st_begin("vfs_io: openat existing file succeeds"); - { - int64 fd = open_file("/vfs_test_file", O_RDONLY, 0); - ST_ASSERT(fd >= 0); - sys_close(static_cast(fd), 0, 0, 0, 0, 0); - } - st_pass(); - - /* ---- openat nonexistent without O_CREAT returns -ENOENT ---- */ - st_begin("vfs_io: openat nonexistent returns -ENOENT"); - { - int64 fd = open_file("/no_such_file_ever", O_RDONLY, 0); - ST_ASSERT_EQ(fd, (int64)-ENOENT); - } - st_pass(); - - /* ---- write then read ---- */ - st_begin("vfs_io: write then read back data"); - { - /* Write */ - int64 fd = open_file("/vfs_rw_test", O_CREAT | O_WRONLY, 0644); - ST_ASSERT(fd >= 0); - - const char* msg = "Hello VFS!"; - int64 written = sys_write( - static_cast(fd), - reinterpret_cast(msg), - 10, 0, 0, 0); - ST_ASSERT_EQ(written, (int64)10); - sys_close(static_cast(fd), 0, 0, 0, 0, 0); - - /* Read */ - fd = open_file("/vfs_rw_test", O_RDONLY, 0); - ST_ASSERT(fd >= 0); - - char buf[32] = {}; - int64 nread = sys_read( - static_cast(fd), - reinterpret_cast(buf), - sizeof(buf), 0, 0, 0); - ST_ASSERT_EQ(nread, (int64)10); - - bool match = true; - for (int i = 0; i < 10; i++) { - if (buf[i] != msg[i]) { match = false; break; } - } - ST_ASSERT(match); - sys_close(static_cast(fd), 0, 0, 0, 0, 0); - } - st_pass(); - - /* ---- read past EOF returns 0 ---- */ - st_begin("vfs_io: read past EOF returns 0"); - { - int64 fd = open_file("/vfs_rw_test", O_RDONLY, 0); - ST_ASSERT(fd >= 0); - - char buf[64]; - /* First read: 10 bytes */ - int64 n1 = sys_read(static_cast(fd), - reinterpret_cast(buf), 64, 0, 0, 0); - ST_ASSERT_EQ(n1, (int64)10); - - /* Second read: should be 0 (EOF) */ - int64 n2 = sys_read(static_cast(fd), - reinterpret_cast(buf), 64, 0, 0, 0); - ST_ASSERT_EQ(n2, (int64)0); - - sys_close(static_cast(fd), 0, 0, 0, 0, 0); - } - st_pass(); - - /* ---- O_TRUNC clears file content ---- */ - st_begin("vfs_io: O_TRUNC clears file content"); - { - int64 fd = open_file("/vfs_rw_test", O_WRONLY | O_TRUNC, 0); - ST_ASSERT(fd >= 0); - sys_close(static_cast(fd), 0, 0, 0, 0, 0); - - fd = open_file("/vfs_rw_test", O_RDONLY, 0); - ST_ASSERT(fd >= 0); - - char buf[16]; - int64 n = sys_read(static_cast(fd), - reinterpret_cast(buf), 16, 0, 0, 0); - ST_ASSERT_EQ(n, (int64)0); /* truncated = empty */ - sys_close(static_cast(fd), 0, 0, 0, 0, 0); - } - st_pass(); - - /* ---- close invalid fd returns -EBADF ---- */ - st_begin("vfs_io: close bad fd returns -EBADF"); - { - int64 ret = sys_close(999, 0, 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)-EBADF); - } - st_pass(); - - /* ---- read/write on bad fd returns -EBADF ---- */ - st_begin("vfs_io: read/write bad fd returns -EBADF"); - { - char buf[8]; - int64 r = sys_read(999, reinterpret_cast(buf), 8, 0, 0, 0); - ST_ASSERT_EQ(r, (int64)-EBADF); - - int64 w = sys_write(999, reinterpret_cast(buf), 8, 0, 0, 0); - ST_ASSERT_EQ(w, (int64)-EBADF); - } - st_pass(); - - /* ---- lseek SEEK_SET / SEEK_CUR / SEEK_END ---- */ - st_begin("vfs_io: lseek SEEK_SET/CUR/END"); - { - /* Write 20 bytes */ - int64 fd = open_file("/vfs_lseek_test", O_CREAT | O_RDWR, 0644); - ST_ASSERT(fd >= 0); - - char data[20]; - for (int i = 0; i < 20; i++) data[i] = static_cast('A' + i); - sys_write(static_cast(fd), - reinterpret_cast(data), 20, 0, 0, 0); - - /* SEEK_SET to 5 */ - int64 pos = sys_lseek(static_cast(fd), 5, SEEK_SET, 0, 0, 0); - ST_ASSERT_EQ(pos, (int64)5); - - /* Read 1 byte, should be 'F' */ - char c = 0; - sys_read(static_cast(fd), - reinterpret_cast(&c), 1, 0, 0, 0); - ST_ASSERT_EQ(c, 'F'); - - /* SEEK_CUR +3 → pos 9 */ - pos = sys_lseek(static_cast(fd), 3, SEEK_CUR, 0, 0, 0); - ST_ASSERT_EQ(pos, (int64)9); - - /* SEEK_END -1 → pos 19 */ - pos = sys_lseek(static_cast(fd), -1, SEEK_END, 0, 0, 0); - ST_ASSERT_EQ(pos, (int64)19); - - sys_read(static_cast(fd), - reinterpret_cast(&c), 1, 0, 0, 0); - ST_ASSERT_EQ(c, 'T'); - - sys_close(static_cast(fd), 0, 0, 0, 0, 0); - } - st_pass(); - - /* ---- lseek invalid whence returns -EINVAL ---- */ - st_begin("vfs_io: lseek invalid whence"); - { - int64 fd = open_file("/vfs_lseek_test", O_RDONLY, 0); - ST_ASSERT(fd >= 0); - int64 ret = sys_lseek(static_cast(fd), 0, 99, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)-EINVAL); - sys_close(static_cast(fd), 0, 0, 0, 0, 0); - } - st_pass(); - - /* ---- dup: duplicate fd shares file position ---- */ - st_begin("vfs_io: dup shares file position"); - { - int64 fd1 = open_file("/vfs_lseek_test", O_RDONLY, 0); - ST_ASSERT(fd1 >= 0); - - int64 fd2 = sys_dup(static_cast(fd1), 0, 0, 0, 0, 0); - ST_ASSERT(fd2 >= 0); - ST_ASSERT_NE(fd1, fd2); - - /* Both should work for reading */ - char c = 0; - int64 n = sys_read(static_cast(fd2), - reinterpret_cast(&c), 1, 0, 0, 0); - ST_ASSERT_EQ(n, (int64)1); - - sys_close(static_cast(fd1), 0, 0, 0, 0, 0); - sys_close(static_cast(fd2), 0, 0, 0, 0, 0); - } - st_pass(); - - /* ---- fstat: basic stat ---- */ - st_begin("vfs_io: fstat returns valid kstat"); - { - int64 fd = open_file("/vfs_lseek_test", O_RDONLY, 0); - ST_ASSERT(fd >= 0); - - kstat st{}; - int64 ret = sys_fstat_wrapper( - static_cast(fd), - reinterpret_cast(&st), 0, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)0); - ST_ASSERT(S_ISREG(st.st_mode)); - ST_ASSERT_EQ(st.st_size, (int64)20); - - sys_close(static_cast(fd), 0, 0, 0, 0, 0); - } - st_pass(); - - /* ---- getcwd: returns "/" ---- */ - st_begin("vfs_io: getcwd returns /"); - { - char buf[64] = {}; - int64 len = sys_getcwd( - reinterpret_cast(buf), 64, 0, 0, 0, 0); - ST_ASSERT(len > 0); - ST_ASSERT_EQ(buf[0], '/'); - } - st_pass(); - - /* ---- mkdirat: create directory ---- */ - st_begin("vfs_io: mkdirat creates directory"); - { - int64 ret = sys_mkdirat( - static_cast(-100), - reinterpret_cast("/vfs_io_dir"), - 0755, 0, 0, 0); - ST_ASSERT_EQ(ret, (int64)0); - - /* Open it to confirm it's a directory */ - int64 fd = open_file("/vfs_io_dir", O_RDONLY, 0); - ST_ASSERT(fd >= 0); - - kstat st{}; - sys_fstat_wrapper(static_cast(fd), - reinterpret_cast(&st), 0, 0, 0, 0); - ST_ASSERT(S_ISDIR(st.st_mode)); - - sys_close(static_cast(fd), 0, 0, 0, 0, 0); - } - st_pass(); - - /* ---- getdents64: list root directory ---- */ - st_begin("vfs_io: getdents64 lists root children"); - { - int64 fd = open_file("/", O_RDONLY, 0); - ST_ASSERT(fd >= 0); - - char buf[512] = {}; - int64 n = sys_getdents64( - static_cast(fd), - reinterpret_cast(buf), - 512, 0, 0, 0); - ST_ASSERT(n > 0); /* At least "." and ".." */ - - sys_close(static_cast(fd), 0, 0, 0, 0, 0); - } - st_pass(); - - /* ---- write to O_RDONLY fd returns -EBADF ---- */ - st_begin("vfs_io: write to O_RDONLY returns -EBADF"); - { - int64 fd = open_file("/vfs_rw_test", O_RDONLY, 0); - ST_ASSERT(fd >= 0); - - char buf[] = "x"; - int64 w = sys_write(static_cast(fd), - reinterpret_cast(buf), 1, 0, 0, 0); - ST_ASSERT_EQ(w, (int64)-EBADF); - sys_close(static_cast(fd), 0, 0, 0, 0, 0); - } - st_pass(); - - /* ---- double close returns -EBADF ---- */ - st_begin("vfs_io: double close returns -EBADF"); - { - int64 fd = open_file("/vfs_rw_test", O_RDONLY, 0); - ST_ASSERT(fd >= 0); - - int64 r1 = sys_close(static_cast(fd), 0, 0, 0, 0, 0); - ST_ASSERT_EQ(r1, (int64)0); - - int64 r2 = sys_close(static_cast(fd), 0, 0, 0, 0, 0); - ST_ASSERT_EQ(r2, (int64)-EBADF); - } - st_pass(); - - /* ---- fd reuse after close ---- */ - st_begin("vfs_io: fd reuse after close"); - { - int64 fd1 = open_file("/vfs_rw_test", O_RDONLY, 0); - ST_ASSERT(fd1 >= 0); - sys_close(static_cast(fd1), 0, 0, 0, 0, 0); - - int64 fd2 = open_file("/vfs_rw_test", O_RDONLY, 0); - ST_ASSERT(fd2 >= 0); - /* fd2 should reuse the slot of fd1 */ - ST_ASSERT(fd2 <= fd1); - sys_close(static_cast(fd2), 0, 0, 0, 0, 0); - } - st_pass(); -} diff --git a/na/test/test_vma.cc b/na/test/test_vma.cc deleted file mode 100644 index 5278914..0000000 --- a/na/test/test_vma.cc +++ /dev/null @@ -1,282 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -#include -#include -#include -#include -#include - -void selftest_vma() { - st_begin("vma: create and destroy VmSpace"); - { - VmSpace* space = VmSpace::create(); - ST_ASSERT(space != nullptr); - ST_ASSERT(space->root_pa != 0); - - // Add a few regions to ensure destruction works - vmm_map_user(space, 0x1000, 0x1000, VM_READ); - vmm_map_user(space, 0x3000, 0x1000, VM_READ | VM_WRITE); - - space->destroy(); - } - st_pass(); - - st_begin("vma: map user region and handle demand paging"); - { - VmSpace* space = VmSpace::create(); - - // Map 8KB (2 pages) at 0x400000 using MI-layer flags - int ret = vmm_map_user(space, 0x400000, 0x2000, VM_READ | VM_WRITE); - ST_ASSERT_EQ(ret, 0); - - // Fault in the first page - ret = vmm_handle_page_fault(space, 0x400500, VmFaultCause::Read); - ST_ASSERT_EQ(ret, 0); - - // Verify mapping in hardware page table - uint64 pa = 0; - bool mapped = pmap::lookup(space->root_pa, 0x400000, &pa); - ST_ASSERT(mapped); - ST_ASSERT_NE(pa, 0ULL); - - // Fault in the second page - ret = vmm_handle_page_fault(space, 0x401FFF, VmFaultCause::Read); - ST_ASSERT_EQ(ret, 0); - - // Should reject fault outside VMA with -EFAULT - ret = vmm_handle_page_fault(space, 0x402000, VmFaultCause::Read); - ST_ASSERT_EQ(ret, -EFAULT); - - space->destroy(); - } - st_pass(); - - st_begin("vma: unmap user region prevents page fault"); - { - VmSpace* space = VmSpace::create(); - vmm_map_user(space, 0x10000, 0x2000, VM_READ); - - // Inside the region — should succeed - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x10500, VmFaultCause::Read), 0); - - // Unmap exactly - vmm_unmap_user(space, 0x10000, 0x2000); - - // Should fail now with -EFAULT - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x10500, VmFaultCause::Read), -EFAULT); - - space->destroy(); - } - st_pass(); - - st_begin("vma: duplicate page fault on same page returns 0"); - { - VmSpace* space = VmSpace::create(); - vmm_map_user(space, 0x200000, 0x1000, VM_READ | VM_WRITE); - - // First fault allocates the physical page - int ret = vmm_handle_page_fault(space, 0x200000, VmFaultCause::Read); - ST_ASSERT_EQ(ret, 0); - - // Second fault on the same page should hit "already mapped" path - ret = vmm_handle_page_fault(space, 0x200100, VmFaultCause::Read); - ST_ASSERT_EQ(ret, 0); - - // Verify both land on the same physical page - uint64 pa1 = 0, pa2 = 0; - pmap::lookup(space->root_pa, 0x200000, &pa1); - pmap::lookup(space->root_pa, 0x200100, &pa2); - // Same page, so PA for both addresses should be the same base page - ST_ASSERT_EQ(pa1, pa2); - - space->destroy(); - } - st_pass(); - - st_begin("vma: vm_perm_to_pte flag translation correctness"); - { - // Test each MI flag maps to correct hardware PTE bit - uint64 pte_r = pmap::vm_perm_to_pte(VM_READ); - ST_ASSERT(pte_r & pmap::PTE_R); - ST_ASSERT(!(pte_r & pmap::PTE_W)); - ST_ASSERT(!(pte_r & pmap::PTE_X)); - ST_ASSERT(!(pte_r & pmap::PTE_U)); - - uint64 pte_rw = pmap::vm_perm_to_pte(VM_READ | VM_WRITE); - ST_ASSERT(pte_rw & pmap::PTE_R); - ST_ASSERT(pte_rw & pmap::PTE_W); - - uint64 pte_rx = pmap::vm_perm_to_pte(VM_READ | VM_EXEC); - ST_ASSERT(pte_rx & pmap::PTE_R); - ST_ASSERT(pte_rx & pmap::PTE_X); - ST_ASSERT(!(pte_rx & pmap::PTE_W)); - - uint64 pte_u = pmap::vm_perm_to_pte(VM_READ | VM_USER); - ST_ASSERT(pte_u & pmap::PTE_R); - ST_ASSERT(pte_u & pmap::PTE_U); - - // All combined - uint64 pte_all = pmap::vm_perm_to_pte(VM_READ | VM_WRITE | VM_EXEC | VM_USER); - ST_ASSERT(pte_all & pmap::PTE_R); - ST_ASSERT(pte_all & pmap::PTE_W); - ST_ASSERT(pte_all & pmap::PTE_X); - ST_ASSERT(pte_all & pmap::PTE_U); - ST_ASSERT(pte_all & pmap::PTE_V); // Always valid - } - st_pass(); - - st_begin("vma: unmap subset only removes contained VMAs"); - { - VmSpace* space = VmSpace::create(); - // Map three separate regions - vmm_map_user(space, 0x50000, 0x1000, VM_READ); - vmm_map_user(space, 0x60000, 0x1000, VM_READ); - vmm_map_user(space, 0x70000, 0x1000, VM_READ); - - // Unmap only the middle one (range covers 0x60000-0x61000) - vmm_unmap_user(space, 0x60000, 0x1000); - - // First and third should still be reachable - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x50000, VmFaultCause::Read), 0); - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x70000, VmFaultCause::Read), 0); - - // Middle should fail - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x60000, VmFaultCause::Read), -EFAULT); - - space->destroy(); - } - st_pass(); - - st_begin("vma: null space returns -EINVAL"); - { - ST_ASSERT_EQ(vmm_map_user(nullptr, 0x1000, 0x1000, VM_READ), -EINVAL); - ST_ASSERT_EQ(vmm_unmap_user(nullptr, 0x1000, 0x1000), -EINVAL); - ST_ASSERT_EQ(vmm_handle_page_fault(nullptr, 0x1000, VmFaultCause::Read), -EINVAL); - } - st_pass(); - - st_begin("vma: clone and copy-on-write"); - { - VmSpace* parent = VmSpace::create(); - vmm_map_user(parent, 0x80000, 0x1000, VM_READ | VM_WRITE); - - /* Trigger read fault in parent to allocate physical page */ - ST_ASSERT_EQ(vmm_handle_page_fault(parent, 0x80000, VmFaultCause::Read), 0); - uint64 parent_pa_initial; - ST_ASSERT(pmap::lookup(parent->root_pa, 0x80000, &parent_pa_initial)); - - /* Clone the address space */ - VmSpace* child = vmm_clone(parent); - ST_ASSERT(child != nullptr); - - /* Verify child has the same physical page mapped but COW */ - uint64 child_pa; - ST_ASSERT(pmap::lookup(child->root_pa, 0x80000, &child_pa)); - ST_ASSERT_EQ(child_pa, parent_pa_initial); - ST_ASSERT(pmap::is_cow(child->root_pa, 0x80000)); - ST_ASSERT(pmap::is_cow(parent->root_pa, 0x80000)); /* Parent also marked COW */ - - /* Trigger Store Page Fault (cause=15) in child */ - ST_ASSERT_EQ(vmm_handle_page_fault(child, 0x80000, VmFaultCause::Write), 0); - - /* Child should now have a new physical page, no longer COW */ - uint64 child_pa_new; - ST_ASSERT(pmap::lookup(child->root_pa, 0x80000, &child_pa_new)); - ST_ASSERT(child_pa_new != parent_pa_initial); - ST_ASSERT(!pmap::is_cow(child->root_pa, 0x80000)); - - /* Parent is still mapped to the old page, still COW because it hasn't written */ - uint64 parent_pa_current; - ST_ASSERT(pmap::lookup(parent->root_pa, 0x80000, &parent_pa_current)); - ST_ASSERT_EQ(parent_pa_current, parent_pa_initial); - ST_ASSERT(pmap::is_cow(parent->root_pa, 0x80000)); - - /* Trigger Store Page Fault in parent */ - ST_ASSERT_EQ(vmm_handle_page_fault(parent, 0x80000, VmFaultCause::Write), 0); - ST_ASSERT(!pmap::is_cow(parent->root_pa, 0x80000)); - - child->destroy(); - parent->destroy(); - } - st_pass(); - - st_begin("vma: gap search finds next free range"); - { - VmSpace* space = VmSpace::create(); - vmm_map_user(space, 0x1000, 0x3000, VM_READ); - - uint64 va = mm_find_free_va(space, 0x1000, kPageSize); - ST_ASSERT_EQ(va, 0x4000ULL); - - space->destroy(); - } - st_pass(); - - st_begin("vma: reject overlapping mapping"); - { - VmSpace* space = VmSpace::create(); - ST_ASSERT_EQ(vmm_map_user(space, 0x20000, 0x2000, VM_READ), 0); - ST_ASSERT_EQ(vmm_map_user(space, 0x21000, 0x1000, VM_READ), -EINVAL); - space->destroy(); - } - st_pass(); - - st_begin("vma: partial unmap splits a single VMA"); - { - VmSpace* space = VmSpace::create(); - ST_ASSERT_EQ(vmm_map_user(space, 0x80000, 0x3000, VM_READ | VM_WRITE), 0); - - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x80000, VmFaultCause::Read), 0); - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x81000, VmFaultCause::Read), 0); - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x82000, VmFaultCause::Read), 0); - - ST_ASSERT_EQ(vmm_unmap_user(space, 0x81000, 0x1000), 0); - - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x80000, VmFaultCause::Read), 0); - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x82000, VmFaultCause::Read), 0); - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x81000, VmFaultCause::Read), -EFAULT); - - space->destroy(); - } - st_pass(); - - st_begin("vma: mprotect read-only blocks write faults"); - { - VmSpace* space = VmSpace::create(); - ST_ASSERT_EQ(vmm_map_user(space, 0x90000, 0x2000, VM_READ | VM_WRITE), 0); - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x90000, VmFaultCause::Read), 0); - - ST_ASSERT_EQ(vmm_protect_user(space, 0x90000, 0x1000, VM_READ), 0); - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x90000, VmFaultCause::Write), -EFAULT); - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x91000, VmFaultCause::Write), 0); - - space->destroy(); - } - st_pass(); - - st_begin("vma: PROT_NONE unmaps existing pages"); - { - VmSpace* space = VmSpace::create(); - ST_ASSERT_EQ(vmm_map_user(space, 0xA0000, 0x1000, VM_READ | VM_WRITE), 0); - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0xA0000, VmFaultCause::Read), 0); - - ST_ASSERT_EQ(vmm_protect_user(space, 0xA0000, 0x1000, 0), 0); - ST_ASSERT_EQ(vmm_handle_page_fault(space, 0xA0000, VmFaultCause::Read), -EFAULT); - - space->destroy(); - } - st_pass(); - - st_begin("vma: range covered check"); - { - VmSpace* space = VmSpace::create(); - ST_ASSERT_EQ(vmm_map_user(space, 0xB0000, 0x2000, VM_READ), 0); - - ST_ASSERT(vma_range_covered(space, 0xB0000, 0xB1000)); - ST_ASSERT(!vma_range_covered(space, 0xB0000, 0xB3000)); - - space->destroy(); - } - st_pass(); -} diff --git a/na/test/test_vmm.cc b/na/test/test_vmm.cc deleted file mode 100644 index 635e3f3..0000000 --- a/na/test/test_vmm.cc +++ /dev/null @@ -1,58 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * Boot Self-Test: VMM and Address Translation - */ - -#include -#include -#include -#include -#include -#include -#include - -namespace { -constexpr uint64 kUartBase = 0x10000000ULL; -constexpr uint64 kPageOffsetMask = kPageSize - 1; -} - -void selftest_vmm() { - st_begin("vmm: virtual/physical address translation"); - { - PhysAddr pa = pmm_get_mem_base(); - VirtAddr va = pa_to_va(pa); - ST_ASSERT_EQ(va.raw, arch::g_direct_map_base + pa.raw); - ST_ASSERT_EQ(va_to_pa(va).raw, pa.raw); - - Pfn p = pa_to_pfn(pa); - ST_ASSERT_EQ(p.raw, pa.raw / kPageSize); - ST_ASSERT_EQ(pfn_to_pa(p).raw, pa.raw); - } - st_pass(); - - st_begin("vmm: page alignment macros"); - { - uint64 base = pmm_get_mem_base().raw; - uint64 addr = base + kPageOffsetMask; - ST_ASSERT_EQ(page_align_down(addr), base); - ST_ASSERT_EQ(page_align_up(addr), base + kPageSize); - ST_ASSERT_EQ(page_align_down(base), base); - ST_ASSERT_EQ(page_align_up(base), base); - } - st_pass(); - - st_begin("vmm: mapping kernel MMIO (driver API)"); - { - /* Use UART MMIO address (0x10000000 on RISC-V virt). - * Must NOT use pmm_get_mem_base() — that's PMP-protected firmware. */ - uint64 mmio_pa = kUartBase; - vmm_map_kernel_mmio(mmio_pa, kPageSize); - - // Verify it doesn't crash and address is reachable in higher half - uint64 va = pa_to_va(phys_addr(mmio_pa)).raw; - ST_ASSERT(va >= arch::g_direct_map_base); - } - st_pass(); -} diff --git a/na/test/test_waitqueue.cc b/na/test/test_waitqueue.cc deleted file mode 100644 index 7f50d6b..0000000 --- a/na/test/test_waitqueue.cc +++ /dev/null @@ -1,91 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -#include -#include -#include -#include - -static WaitQueue g_test_wq; -static volatile bool g_thread_woken = false; -static volatile bool g_thread_started = false; -static volatile int g_wake_count = 0; - -extern TaskControlBlock* kthread_create(const char* name, void (*entry)(void*), void* arg, uint8 priority); - -static void waitqueue_test_thread(void*) -{ - g_thread_started = true; - g_test_wq.sleep(); - g_thread_woken = true; - - extern void task_exit(int code); - task_exit(0); -} - -static void waitqueue_test_thread_all(void*) -{ - g_test_wq.sleep(); - __sync_fetch_and_add(&g_wake_count, 1); - - extern void task_exit(int code); - task_exit(0); -} - -void selftest_waitqueue() -{ - st_begin("waitqueue: sleep and wake_one"); - - g_test_wq.init("test_wq"); - g_thread_started = false; - g_thread_woken = false; - - TaskControlBlock* child = kthread_create("test_wq_child", waitqueue_test_thread, nullptr, 0); - ST_ASSERT(child != nullptr); - - sched_yield(); - - ST_ASSERT(g_thread_started == true); - ST_ASSERT(g_thread_woken == false); - ST_ASSERT(!g_test_wq.is_empty()); - - kprintf("[test_wq] waking child\n"); - bool woken = g_test_wq.wake_one(); - ST_ASSERT(woken == true); - ST_ASSERT(g_test_wq.is_empty()); - - kprintf("[test_wq] yielding to child again\n"); - sched_yield(); - kprintf("[test_wq] returned from yield 2\n"); - - ST_ASSERT(g_thread_woken == true); - - st_pass(); - - st_begin("waitqueue: wake_all"); - g_wake_count = 0; - for (int i = 0; i < 3; i++) { - kthread_create("test_wq_all", waitqueue_test_thread_all, nullptr, 0); - } - - /* Give all 3 threads a chance to start and sleep */ - kprintf("[test_wq] yielding 1 for wake_all\n"); - sched_yield(); - sched_yield(); - sched_yield(); - - ST_ASSERT(!g_test_wq.is_empty()); - - int woken_count = g_test_wq.wake_all(); - - ST_ASSERT_EQ(woken_count, 3); - ST_ASSERT(g_test_wq.is_empty()); - - /* Give all 3 threads a chance to wake up and run */ - sched_yield(); - sched_yield(); - sched_yield(); - - ST_ASSERT_EQ(g_wake_count, 3); - st_pass(); -} -- cgit v1.2.3