diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/module.mk | 30 | ||||
| -rw-r--r-- | test/selftest.cc | 83 | ||||
| -rw-r--r-- | test/test_atomic.cc | 102 | ||||
| -rw-r--r-- | test/test_dcache.cc | 232 | ||||
| -rw-r--r-- | test/test_driver_model.cc | 209 | ||||
| -rw-r--r-- | test/test_exec.cc | 447 | ||||
| -rw-r--r-- | test/test_fdt.cc | 50 | ||||
| -rw-r--r-- | test/test_klog.cc | 175 | ||||
| -rw-r--r-- | test/test_mutex.cc | 183 | ||||
| -rw-r--r-- | test/test_namei.cc | 173 | ||||
| -rw-r--r-- | test/test_page_cache.cc | 180 | ||||
| -rw-r--r-- | test/test_pid.cc | 106 | ||||
| -rw-r--r-- | test/test_pipe_devfs.cc | 420 | ||||
| -rw-r--r-- | test/test_pmm.cc | 283 | ||||
| -rw-r--r-- | test/test_random.cc | 49 | ||||
| -rw-r--r-- | test/test_rbtree.cc | 119 | ||||
| -rw-r--r-- | test/test_sched.cc | 257 | ||||
| -rw-r--r-- | test/test_signal.cc | 193 | ||||
| -rw-r--r-- | test/test_slab.cc | 255 | ||||
| -rw-r--r-- | test/test_smp.cc | 136 | ||||
| -rw-r--r-- | test/test_spinlock.cc | 135 | ||||
| -rw-r--r-- | test/test_sys_fs_extra.cc | 238 | ||||
| -rw-r--r-- | test/test_sys_mm.cc | 178 | ||||
| -rw-r--r-- | test/test_sys_process.cc | 93 | ||||
| -rw-r--r-- | test/test_trap.cc | 197 | ||||
| -rw-r--r-- | test/test_uaccess.cc | 97 | ||||
| -rw-r--r-- | test/test_vfs_io.cc | 345 | ||||
| -rw-r--r-- | test/test_vma.cc | 282 | ||||
| -rw-r--r-- | test/test_vmm.cc | 58 | ||||
| -rw-r--r-- | test/test_waitqueue.cc | 91 |
30 files changed, 5396 insertions, 0 deletions
diff --git a/test/module.mk b/test/module.mk new file mode 100644 index 0000000..47a693c --- /dev/null +++ b/test/module.mk @@ -0,0 +1,30 @@ +# 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/test/selftest.cc b/test/selftest.cc new file mode 100644 index 0000000..e1bb2b4 --- /dev/null +++ b/test/selftest.cc @@ -0,0 +1,83 @@ +/* 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 <lume/selftest.h> + +/* 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/test/test_atomic.cc b/test/test_atomic.cc new file mode 100644 index 0000000..9851e7b --- /dev/null +++ b/test/test_atomic.cc @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * Boot Self-Test: lume::atomic<T> + * + * 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 <lume/selftest.h> +#include <lume/atomic.h> + +void selftest_atomic() { + st_begin("atomic: default construction is zero"); + { + lume::atomic<uint32> a; + ST_ASSERT_EQ(a.load(), 0U); + } + st_pass(); + + st_begin("atomic: explicit construction"); + { + lume::atomic<uint64> a(42); + ST_ASSERT_EQ(a.load(), 42ULL); + } + st_pass(); + + st_begin("atomic: store/load round-trip"); + { + lume::atomic<uint32> 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<uint32> 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<uint32> 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<uint32> 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<uint32> 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<uint64> a(0xFFFFFFFF00000000ULL); + uint64 old = a.fetch_add(1); + ST_ASSERT_EQ(old, 0xFFFFFFFF00000000ULL); + ST_ASSERT_EQ(a.load(), 0xFFFFFFFF00000001ULL); + } + st_pass(); +} diff --git a/test/test_dcache.cc b/test/test_dcache.cc new file mode 100644 index 0000000..9e0eec5 --- /dev/null +++ b/test/test_dcache.cc @@ -0,0 +1,232 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * test_dcache.cc — Self-tests for the dentry cache layer + */ + +#include <lume/selftest.h> +#include <lume/dcache.h> +#include <lume/fs.h> +#include <lume/mount.h> +#include <lume/errno.h> +#include <lume/slab.h> + +/* 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/test/test_driver_model.cc b/test/test_driver_model.cc new file mode 100644 index 0000000..8f105b3 --- /dev/null +++ b/test/test_driver_model.cc @@ -0,0 +1,209 @@ +/* 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 <lume/selftest.h> +#include <lume/kref.h> +#include <lume/kobject.h> +#include <lume/device.h> + +/* ------------------------------------------------------------------ */ +/* 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/test/test_exec.cc b/test/test_exec.cc new file mode 100644 index 0000000..bbfa3b6 --- /dev/null +++ b/test/test_exec.cc @@ -0,0 +1,447 @@ +/* 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 <lume/selftest.h> +#include <lume/elf.h> +#include <lume/task.h> +#include <lume/vmm.h> +#include <lume/pmm.h> +#include <lume/addr.h> +#include <lume/errno.h> +#include <lume/fdtable.h> +#include <lume/fs.h> +#include <lume/fs_struct.h> +#include <lume/dcache.h> +#include <lume/namei.h> +#include <lume/file.h> +#include <lume/mm_layout.h> +#include <arch/trapframe.h> +#include <arch/pmap.h> + +/* 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<uint8*>(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<uint64>(-100), /* AT_FDCWD */ + reinterpret_cast<uint64>(path), + O_CREAT | O_WRONLY | O_TRUNC, + 0755, 0, 0); + if (fd < 0) return static_cast<int>(fd); + + int64 written = sys_write( + static_cast<uint64>(fd), + reinterpret_cast<uint64>(data), + size, 0, 0, 0); + + sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0); + + if (written < 0) return static_cast<int>(written); + if (static_cast<uint64>(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<uint8*>(&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<uint8*>(pa_to_va(PhysAddr{pa}).raw); + uint64 argc = *reinterpret_cast<uint64*>(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/test/test_fdt.cc b/test/test_fdt.cc new file mode 100644 index 0000000..cf09029 --- /dev/null +++ b/test/test_fdt.cc @@ -0,0 +1,50 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * Boot Self-Test: FDT (Flattened Device Tree) + */ + +#include <lume/selftest.h> +#include <lume/fdt.h> + +// 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/test/test_klog.cc b/test/test_klog.cc new file mode 100644 index 0000000..896e7ed --- /dev/null +++ b/test/test_klog.cc @@ -0,0 +1,175 @@ +/* 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 <lume/selftest.h> +#include <lume/klog.h> +#include <string.h> + +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<KlogLevel>(KLOG_DEFAULT_LEVEL)); + return false; + } + + klog_flush(); + + KlogStats s2; + klog_stats(&s2); + + klog_set_level(static_cast<KlogLevel>(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/test/test_mutex.cc b/test/test_mutex.cc new file mode 100644 index 0000000..e86c984 --- /dev/null +++ b/test/test_mutex.cc @@ -0,0 +1,183 @@ +/* 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 <lume/selftest.h> +#include <lume/mutex.h> +#include <lume/task.h> +#include <lume/sched.h> +#include <lume/kprintf.h> + +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<int>(reinterpret_cast<uint64>(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<void*>(0), 0); + sched_yield(); + kthread_create("fifo_B", fifo_worker, reinterpret_cast<void*>(1), 0); + sched_yield(); + kthread_create("fifo_C", fifo_worker, reinterpret_cast<void*>(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/test/test_namei.cc b/test/test_namei.cc new file mode 100644 index 0000000..8e4ecb0 --- /dev/null +++ b/test/test_namei.cc @@ -0,0 +1,173 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * test_namei.cc — Self-tests for pathname resolution (namei.cc) + */ + +#include <lume/selftest.h> +#include <lume/namei.h> +#include <lume/dcache.h> +#include <lume/fs.h> +#include <lume/file.h> +#include <lume/mount.h> +#include <lume/task.h> +#include <lume/errno.h> + +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<uint64>(-100), + reinterpret_cast<uint64>("/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/test/test_page_cache.cc b/test/test_page_cache.cc new file mode 100644 index 0000000..ee0e0e2 --- /dev/null +++ b/test/test_page_cache.cc @@ -0,0 +1,180 @@ +/* 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 <lume/selftest.h> +#include <lume/page_cache.h> +#include <lume/page.h> +#include <lume/pmm.h> +#include <lume/errno.h> + +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/test/test_pid.cc b/test/test_pid.cc new file mode 100644 index 0000000..e79eb1e --- /dev/null +++ b/test/test_pid.cc @@ -0,0 +1,106 @@ +/* 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 <lume/selftest.h> +#include <lume/pid.h> + +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/test/test_pipe_devfs.cc b/test/test_pipe_devfs.cc new file mode 100644 index 0000000..ed375ed --- /dev/null +++ b/test/test_pipe_devfs.cc @@ -0,0 +1,420 @@ +/* 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 <lume/selftest.h> +#include <lume/errno.h> +#include <lume/file.h> +#include <lume/fs.h> +#include <lume/fs_types.h> +#include <lume/task.h> +#include <lume/fdtable.h> +#include <lume/pipe.h> + +/* 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<uint64>(-100), /* AT_FDCWD */ + reinterpret_cast<uint64>(path), + static_cast<uint64>(flags), + 0, 0, 0); +} + +static int64 do_close(int64 fd) +{ + return sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0); +} + +static int64 do_read(int64 fd, void* buf, uint64 count) +{ + return sys_read(static_cast<uint64>(fd), + reinterpret_cast<uint64>(buf), count, 0, 0, 0); +} + +static int64 do_write(int64 fd, const void* buf, uint64 count) +{ + return sys_write(static_cast<uint64>(fd), + reinterpret_cast<uint64>(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<uint64>(fd), + reinterpret_cast<uint64>(&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<uint64>(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<uint64>(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<uint64>(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<uint64>(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<uint64>(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<uint64>(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<uint64>(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<uint64>(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<uint64>(fd), F_GETFD, 0, 0, 0, 0); + ST_ASSERT_EQ(flags, (int64)0); + + /* Set CLOEXEC */ + int64 ret = sys_fcntl(static_cast<uint64>(fd), F_SETFD, FD_CLOEXEC, 0, 0, 0); + ST_ASSERT_EQ(ret, (int64)0); + + /* Verify it was set */ + flags = sys_fcntl(static_cast<uint64>(fd), F_GETFD, 0, 0, 0, 0); + ST_ASSERT_EQ(flags, (int64)FD_CLOEXEC); + + /* Clear it */ + sys_fcntl(static_cast<uint64>(fd), F_SETFD, 0, 0, 0, 0); + flags = sys_fcntl(static_cast<uint64>(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<uint64>(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<uint64>(fd), F_SETFL, O_NONBLOCK, 0, 0, 0); + int64 fl = sys_fcntl(static_cast<uint64>(fd), F_GETFL, 0, 0, 0, 0); + ST_ASSERT(fl & O_NONBLOCK); + + /* Clear it */ + sys_fcntl(static_cast<uint64>(fd), F_SETFL, 0, 0, 0, 0); + fl = sys_fcntl(static_cast<uint64>(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<uint64>(fd), F_DUPFD_CLOEXEC, 0, 0, 0, 0); + ST_ASSERT(fd2 >= 0); + + int64 flags = sys_fcntl(static_cast<uint64>(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<uint64>(fd), 9999, 0, 0, 0, 0); + ST_ASSERT_EQ(ret, (int64)-EINVAL); + do_close(fd); + } + st_pass(); +} diff --git a/test/test_pmm.cc b/test/test_pmm.cc new file mode 100644 index 0000000..3330866 --- /dev/null +++ b/test/test_pmm.cc @@ -0,0 +1,283 @@ +/* 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 <lume/selftest.h> +#include <lume/pmm.h> +#include <lume/addr.h> + +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<uint64*>(page_to_va(page).raw); + *ptr = reinterpret_cast<uint64>(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<uint64*>(page_to_va(page).raw); + *ptr = reinterpret_cast<uint64>(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<uint64*>(page_to_va(head).raw); + Page* nxt = reinterpret_cast<Page*>(*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/test/test_random.cc b/test/test_random.cc new file mode 100644 index 0000000..d546410 --- /dev/null +++ b/test/test_random.cc @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +#include <lume/selftest.h> +#include <lume/random.h> +#include <lume/kprintf.h> + +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/test/test_rbtree.cc b/test/test_rbtree.cc new file mode 100644 index 0000000..27f049a --- /dev/null +++ b/test/test_rbtree.cc @@ -0,0 +1,119 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +#include <lume/selftest.h> +#include <lume/rbtree.h> + +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<const TestNode*>( + reinterpret_cast<const char*>(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<const int*>(key); + const TestNode *n = reinterpret_cast<const TestNode*>( + reinterpret_cast<const char*>(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<N; i++) { + nodes[i].key = (i * 17) % N; // Pseudo-random insertions + rb_add(&nodes[i].rb, &tree, node_less); + } + + int c = 0; + for (struct rb_node *n = rb_first(&tree); n; n = rb_next(n)) c++; + ST_ASSERT_EQ(c, N); + + for (int i=0; i<N; i+=2) { + rb_erase(&nodes[i].rb, &tree); + } + c = 0; + for (struct rb_node *n = rb_first(&tree); n; n = rb_next(n)) c++; + ST_ASSERT_EQ(c, N / 2); + } + st_pass(); + + st_begin("rbtree: empty tree operations"); + { + struct rb_root tree = RB_ROOT; + ST_ASSERT(RB_EMPTY_ROOT(&tree)); + ST_ASSERT(rb_first(&tree) == nullptr); + ST_ASSERT(rb_last(&tree) == nullptr); + } + st_pass(); + + st_begin("rbtree: rb_erase_init after effects"); + { + struct rb_root tree = RB_ROOT; + TestNode n1{100, {}}; + rb_add(&n1.rb, &tree, node_less); + ST_ASSERT(!RB_EMPTY_NODE(&n1.rb)); + + rb_erase_init(&n1.rb, &tree); + ST_ASSERT(RB_EMPTY_NODE(&n1.rb)); + ST_ASSERT(RB_EMPTY_ROOT(&tree)); + } + st_pass(); +} diff --git a/test/test_sched.cc b/test/test_sched.cc new file mode 100644 index 0000000..d90c30a --- /dev/null +++ b/test/test_sched.cc @@ -0,0 +1,257 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * Boot self-tests for the Scheduler subsystem. + * + * Tests RunQueue bitmap operations, priority selection, + * round-robin ordering, current_task(), and time slice policy. + */ + +#include <lume/types.h> +#include <lume/task.h> +#include <lume/sched.h> +#include <lume/sched_class.h> +#include <lume/config.h> +#include <lume/selftest.h> +#include <lume/pmm.h> +#include <lume/addr.h> +#include <lume/new.h> +#include <kernel/sched_internal.h> + +/* ======================================================================== + * 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/test/test_signal.cc b/test/test_signal.cc new file mode 100644 index 0000000..360b0e6 --- /dev/null +++ b/test/test_signal.cc @@ -0,0 +1,193 @@ +/* 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 <lume/selftest.h> +#include <lume/signal.h> +#include <lume/signal_types.h> +#include <lume/signal_nr.h> +#include <lume/task.h> +#include <lume/errno.h> +#include <lume/new.h> + +/* 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<sigqueue, &sigqueue::list>(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/test/test_slab.cc b/test/test_slab.cc new file mode 100644 index 0000000..8a448b4 --- /dev/null +++ b/test/test_slab.cc @@ -0,0 +1,255 @@ +/* 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 <lume/selftest.h> +#include <lume/new.h> +#include <lume/pmm.h> +#include <lume/addr.h> + +/* 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<uint64*>(p) = 0xDEADBEEFCAFEBABEULL; + ST_ASSERT_EQ(*static_cast<uint64*>(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<uint8*>(ptrs[i]) = static_cast<uint8>(i); + } + // Verify markers (no cross-contamination) + for (int i = 0; i < 9; i++) { + ST_ASSERT_EQ(*static_cast<uint8*>(ptrs[i]), static_cast<uint8>(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<uint64>(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<uint64>(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<uint64>(a); + uint64 ub = reinterpret_cast<uint64>(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<NUM; i++) { + ptrs[i] = kmalloc(32); + ST_ASSERT(ptrs[i] != nullptr); + } + for (int i=0; i<NUM; i++) { + kfree(ptrs[i]); + } + } + st_pass(); + + st_begin("slab: empty page reclamation (verifiable by page availability)"); + { + // Allocate 128-byte objects until we hit the start of a completely new page. + void* base_ptr = nullptr; + void* drain[64]; + int drain_count = 0; + + while (true) { + void* p = kmalloc(128); + // Because 128 divides 4096 evenly, a new page starts at an exact multiple of kPageSize. + if (reinterpret_cast<uint64>(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<uint64>(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<uintptr>(ptrs[i]) % 8 == 0); + } + + for (int i = 0; i < MAX_ITEMS; i++) { + kfree(ptrs[i]); + } + } + st_pass(); +} diff --git a/test/test_smp.cc b/test/test_smp.cc new file mode 100644 index 0000000..f934526 --- /dev/null +++ b/test/test_smp.cc @@ -0,0 +1,136 @@ +/* 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 <lume/selftest.h> +#include <lume/smp.h> +#include <lume/sched.h> +#include <lume/atomic.h> +#include <lume/config.h> +#include <arch/cpu.h> + +/* Shared counter incremented by APs when they receive an IPI */ +static lume::atomic<uint32> 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<uint32>(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<uint32>(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<uint32>(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/test/test_spinlock.cc b/test/test_spinlock.cc new file mode 100644 index 0000000..be794a0 --- /dev/null +++ b/test/test_spinlock.cc @@ -0,0 +1,135 @@ +/* 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 <lume/selftest.h> +#include <kernel/sync/spinlock.h> +#include <kernel/sync/cpu_state.h> +#include <arch/cpu.h> + +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/test/test_sys_fs_extra.cc b/test/test_sys_fs_extra.cc new file mode 100644 index 0000000..705ce63 --- /dev/null +++ b/test/test_sys_fs_extra.cc @@ -0,0 +1,238 @@ +/* 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 <lume/selftest.h> +#include <lume/errno.h> +#include <lume/file.h> +#include <lume/fs.h> +#include <lume/fs_types.h> +#include <lume/task.h> + +/* 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<uint64>(-100), + reinterpret_cast<uint64>(path), + static_cast<uint64>(flags), 0, 0, 0); +} + +static int64 do_close(int64 fd) +{ + return sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0); +} + +static int64 do_read(int64 fd, void* buf, uint64 count) +{ + return sys_read(static_cast<uint64>(fd), + reinterpret_cast<uint64>(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<uint64>(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<uint64>(fd), + static_cast<uint64>(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<uint64>(saved_cwd), 64, 0, 0, 0, 0); + + /* chdir to /dev */ + int64 ret = sys_chdir( + reinterpret_cast<uint64>("/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<uint64>(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<uint64>("/"), 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<uint64>("/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<uint64>("/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<uint64>(-100), + reinterpret_cast<uint64>("/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<uint64>(fd), + reinterpret_cast<uint64>(iov), 2, 0, 0, 0); + ST_ASSERT_EQ(w, (int64)6); + + /* Seek back to start and read */ + sys_lseek(static_cast<uint64>(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<uint64>(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<uint64>(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<uint64>(-100), + reinterpret_cast<uint64>("/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<uint64>(buf), 0, 0, 0, 0, 0); + ST_ASSERT_EQ(ret, (int64)-EINVAL); + } + st_pass(); +} diff --git a/test/test_sys_mm.cc b/test/test_sys_mm.cc new file mode 100644 index 0000000..2c1a7dc --- /dev/null +++ b/test/test_sys_mm.cc @@ -0,0 +1,178 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +#include <lume/selftest.h> +#include <lume/task.h> +#include <lume/vmm.h> +#include <lume/mman.h> +#include <lume/errno.h> + +/* 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<int64>(base)); + + ST_ASSERT_EQ(sys_mmap(base, 0x1000, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0), + static_cast<int64>(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<int64>(base)); + + int64 hint = sys_mmap(base, 0x1000, PROT_READ, + MAP_PRIVATE | MAP_ANONYMOUS, 0, 0); + ST_ASSERT(hint > 0); + ST_ASSERT(hint != static_cast<int64>(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/test/test_sys_process.cc b/test/test_sys_process.cc new file mode 100644 index 0000000..686bed6 --- /dev/null +++ b/test/test_sys_process.cc @@ -0,0 +1,93 @@ +/* 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 <lume/selftest.h> +#include <lume/errno.h> +#include <lume/task.h> +#include <lume/types.h> + +/* 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/test/test_trap.cc b/test/test_trap.cc new file mode 100644 index 0000000..c91948a --- /dev/null +++ b/test/test_trap.cc @@ -0,0 +1,197 @@ +/* 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 <lume/types.h> +#include <lume/selftest.h> +#include <lume/trap.h> +#include <lume/irq.h> +#include <lume/errno.h> +#include <lume/console.h> +#include <arch/trap.h> + +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/test/test_uaccess.cc b/test/test_uaccess.cc new file mode 100644 index 0000000..c5a8c6c --- /dev/null +++ b/test/test_uaccess.cc @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +#include <lume/selftest.h> +#include <lume/uaccess.h> +#include <lume/errno.h> + +#include <lume/vmm.h> +#include <lume/pmm.h> +#include <lume/tlb.h> +#include <lume/mm_layout.h> +#include <arch/pmap.h> + +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/test/test_vfs_io.cc b/test/test_vfs_io.cc new file mode 100644 index 0000000..3d3b125 --- /dev/null +++ b/test/test_vfs_io.cc @@ -0,0 +1,345 @@ +/* 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 <lume/selftest.h> +#include <lume/errno.h> +#include <lume/file.h> +#include <lume/fs.h> +#include <lume/fs_types.h> +#include <lume/task.h> +#include <lume/fdtable.h> + +/* 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<uint64>(-100), /* AT_FDCWD */ + reinterpret_cast<uint64>(path), + static_cast<uint64>(flags), + static_cast<uint64>(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<uint64>(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<uint64>(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<uint64>(fd), + reinterpret_cast<uint64>(msg), + 10, 0, 0, 0); + ST_ASSERT_EQ(written, (int64)10); + sys_close(static_cast<uint64>(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<uint64>(fd), + reinterpret_cast<uint64>(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<uint64>(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<uint64>(fd), + reinterpret_cast<uint64>(buf), 64, 0, 0, 0); + ST_ASSERT_EQ(n1, (int64)10); + + /* Second read: should be 0 (EOF) */ + int64 n2 = sys_read(static_cast<uint64>(fd), + reinterpret_cast<uint64>(buf), 64, 0, 0, 0); + ST_ASSERT_EQ(n2, (int64)0); + + sys_close(static_cast<uint64>(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<uint64>(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<uint64>(fd), + reinterpret_cast<uint64>(buf), 16, 0, 0, 0); + ST_ASSERT_EQ(n, (int64)0); /* truncated = empty */ + sys_close(static_cast<uint64>(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<uint64>(buf), 8, 0, 0, 0); + ST_ASSERT_EQ(r, (int64)-EBADF); + + int64 w = sys_write(999, reinterpret_cast<uint64>(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<char>('A' + i); + sys_write(static_cast<uint64>(fd), + reinterpret_cast<uint64>(data), 20, 0, 0, 0); + + /* SEEK_SET to 5 */ + int64 pos = sys_lseek(static_cast<uint64>(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<uint64>(fd), + reinterpret_cast<uint64>(&c), 1, 0, 0, 0); + ST_ASSERT_EQ(c, 'F'); + + /* SEEK_CUR +3 → pos 9 */ + pos = sys_lseek(static_cast<uint64>(fd), 3, SEEK_CUR, 0, 0, 0); + ST_ASSERT_EQ(pos, (int64)9); + + /* SEEK_END -1 → pos 19 */ + pos = sys_lseek(static_cast<uint64>(fd), -1, SEEK_END, 0, 0, 0); + ST_ASSERT_EQ(pos, (int64)19); + + sys_read(static_cast<uint64>(fd), + reinterpret_cast<uint64>(&c), 1, 0, 0, 0); + ST_ASSERT_EQ(c, 'T'); + + sys_close(static_cast<uint64>(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<uint64>(fd), 0, 99, 0, 0, 0); + ST_ASSERT_EQ(ret, (int64)-EINVAL); + sys_close(static_cast<uint64>(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<uint64>(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<uint64>(fd2), + reinterpret_cast<uint64>(&c), 1, 0, 0, 0); + ST_ASSERT_EQ(n, (int64)1); + + sys_close(static_cast<uint64>(fd1), 0, 0, 0, 0, 0); + sys_close(static_cast<uint64>(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<uint64>(fd), + reinterpret_cast<uint64>(&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<uint64>(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<uint64>(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<uint64>(-100), + reinterpret_cast<uint64>("/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<uint64>(fd), + reinterpret_cast<uint64>(&st), 0, 0, 0, 0); + ST_ASSERT(S_ISDIR(st.st_mode)); + + sys_close(static_cast<uint64>(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<uint64>(fd), + reinterpret_cast<uint64>(buf), + 512, 0, 0, 0); + ST_ASSERT(n > 0); /* At least "." and ".." */ + + sys_close(static_cast<uint64>(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<uint64>(fd), + reinterpret_cast<uint64>(buf), 1, 0, 0, 0); + ST_ASSERT_EQ(w, (int64)-EBADF); + sys_close(static_cast<uint64>(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<uint64>(fd), 0, 0, 0, 0, 0); + ST_ASSERT_EQ(r1, (int64)0); + + int64 r2 = sys_close(static_cast<uint64>(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<uint64>(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<uint64>(fd2), 0, 0, 0, 0, 0); + } + st_pass(); +} diff --git a/test/test_vma.cc b/test/test_vma.cc new file mode 100644 index 0000000..5278914 --- /dev/null +++ b/test/test_vma.cc @@ -0,0 +1,282 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +#include <lume/selftest.h> +#include <lume/vmm.h> +#include <lume/addr.h> +#include <lume/errno.h> +#include <arch/pmap.h> + +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/test/test_vmm.cc b/test/test_vmm.cc new file mode 100644 index 0000000..635e3f3 --- /dev/null +++ b/test/test_vmm.cc @@ -0,0 +1,58 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * Boot Self-Test: VMM and Address Translation + */ + +#include <lume/selftest.h> +#include <lume/klog.h> +#include <lume/config.h> +#include <lume/addr.h> +#include <lume/vmm.h> +#include <lume/pmm.h> +#include <arch/config.h> + +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/test/test_waitqueue.cc b/test/test_waitqueue.cc new file mode 100644 index 0000000..7f50d6b --- /dev/null +++ b/test/test_waitqueue.cc @@ -0,0 +1,91 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +#include <lume/selftest.h> +#include <lume/waitqueue.h> +#include <lume/task.h> +#include <lume/sched.h> + +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(); +} |
