diff options
Diffstat (limited to 'na/test')
| -rw-r--r-- | na/test/module.mk | 14 | ||||
| -rw-r--r-- | na/test/selftest.cc | 30 | ||||
| -rw-r--r-- | na/test/test_dcache.cc | 232 | ||||
| -rw-r--r-- | na/test/test_driver_model.cc | 209 | ||||
| -rw-r--r-- | na/test/test_exec.cc | 447 | ||||
| -rw-r--r-- | na/test/test_mutex.cc | 183 | ||||
| -rw-r--r-- | na/test/test_namei.cc | 173 | ||||
| -rw-r--r-- | na/test/test_page_cache.cc | 180 | ||||
| -rw-r--r-- | na/test/test_pid.cc | 106 | ||||
| -rw-r--r-- | na/test/test_pipe_devfs.cc | 420 | ||||
| -rw-r--r-- | na/test/test_pmm.cc | 6 | ||||
| -rw-r--r-- | na/test/test_reloc.cc | 46 | ||||
| -rw-r--r-- | na/test/test_signal.cc | 193 | ||||
| -rw-r--r-- | na/test/test_slab.cc | 4 | ||||
| -rw-r--r-- | na/test/test_smp.cc | 136 | ||||
| -rw-r--r-- | na/test/test_sys_fs_extra.cc | 238 | ||||
| -rw-r--r-- | na/test/test_sys_process.cc | 93 | ||||
| -rw-r--r-- | na/test/test_uaccess.cc | 18 | ||||
| -rw-r--r-- | na/test/test_vfs_io.cc | 345 | ||||
| -rw-r--r-- | na/test/test_vmm.cc | 15 | ||||
| -rw-r--r-- | na/test/test_waitqueue.cc | 2 |
21 files changed, 3022 insertions, 68 deletions
diff --git a/na/test/module.mk b/na/test/module.mk index da6c065..47a693c 100644 --- a/na/test/module.mk +++ b/na/test/module.mk @@ -3,6 +3,8 @@ 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 @@ -12,7 +14,17 @@ 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_reloc.cc SRCS_CC += test/test_waitqueue.cc SRCS_CC += test/test_uaccess.cc SRCS_CC += test/test_sys_mm.cc +SRCS_CC += test/test_smp.cc +SRCS_CC += test/test_dcache.cc +SRCS_CC += test/test_namei.cc +SRCS_CC += test/test_vfs_io.cc +SRCS_CC += test/test_exec.cc +SRCS_CC += test/test_pipe_devfs.cc +SRCS_CC += test/test_sys_process.cc +SRCS_CC += test/test_signal.cc +SRCS_CC += test/test_pid.cc +SRCS_CC += test/test_sys_fs_extra.cc +SRCS_CC += test/test_page_cache.cc diff --git a/na/test/selftest.cc b/na/test/selftest.cc index 96962e2..e1bb2b4 100644 --- a/na/test/selftest.cc +++ b/na/test/selftest.cc @@ -14,6 +14,8 @@ 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(); @@ -23,10 +25,21 @@ void selftest_trap(); void selftest_sched(); void selftest_klog(); void selftest_random(); -void selftest_reloc(); 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() { @@ -35,6 +48,8 @@ void selftest_run_all() kprintf("========================================\n"); selftest_spinlock(); + selftest_mutex(); + selftest_driver_model(); selftest_pmm(); selftest_slab(); selftest_rbtree(); @@ -46,10 +61,21 @@ void selftest_run_all() selftest_sched(); selftest_klog(); selftest_random(); - selftest_reloc(); 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"); diff --git a/na/test/test_dcache.cc b/na/test/test_dcache.cc new file mode 100644 index 0000000..9e0eec5 --- /dev/null +++ b/na/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/na/test/test_driver_model.cc b/na/test/test_driver_model.cc new file mode 100644 index 0000000..8f105b3 --- /dev/null +++ b/na/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/na/test/test_exec.cc b/na/test/test_exec.cc new file mode 100644 index 0000000..bbfa3b6 --- /dev/null +++ b/na/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/na/test/test_mutex.cc b/na/test/test_mutex.cc new file mode 100644 index 0000000..e86c984 --- /dev/null +++ b/na/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/na/test/test_namei.cc b/na/test/test_namei.cc new file mode 100644 index 0000000..8e4ecb0 --- /dev/null +++ b/na/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/na/test/test_page_cache.cc b/na/test/test_page_cache.cc new file mode 100644 index 0000000..ee0e0e2 --- /dev/null +++ b/na/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/na/test/test_pid.cc b/na/test/test_pid.cc new file mode 100644 index 0000000..e79eb1e --- /dev/null +++ b/na/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/na/test/test_pipe_devfs.cc b/na/test/test_pipe_devfs.cc new file mode 100644 index 0000000..ed375ed --- /dev/null +++ b/na/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/na/test/test_pmm.cc b/na/test/test_pmm.cc index 53b2b2a..3330866 100644 --- a/na/test/test_pmm.cc +++ b/na/test/test_pmm.cc @@ -25,7 +25,7 @@ void selftest_pmm() { Page* page = pmm_alloc_page(); ST_ASSERT(page != nullptr); - ST_ASSERT(page->state == PageState::Allocated); + ST_ASSERT(page->type == PageType::Allocated); ST_ASSERT(page->order == 0); uint64 pa = page_to_pa(page).raw; @@ -123,11 +123,11 @@ void selftest_pmm() page_decref(page); // 2 -> 1, should NOT free ST_ASSERT(page->refcount.load() == 1); - ST_ASSERT(page->state == PageState::Allocated); + 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->state because it might be reused. + // We can't safely check page->type because it might be reused. } st_pass(); st_begin("pmm: pmm_alloc_pages natural alignment (order-3)"); diff --git a/na/test/test_reloc.cc b/na/test/test_reloc.cc deleted file mode 100644 index 54e7144..0000000 --- a/na/test/test_reloc.cc +++ /dev/null @@ -1,46 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -/* - * Boot Self-Test: relocation engine - */ - -#include <lume/relocate.h> -#include <lume/selftest.h> - -struct RelocBlob { - RelocTableHeader hdr; - RelocEntry entries[2]; -}; - -void selftest_reloc() -{ - st_begin("reloc: relative + abs64"); - - alignas(8) uint8 buf[64] = {}; - uint64 base = reinterpret_cast<uint64>(buf); - uint64 link_base = base - 0x1000; - - RelocBlob blob = {}; - blob.hdr.magic = kRelocTableMagic; - blob.hdr.version = kRelocTableVersion; - blob.hdr.arch = RELOC_ARCH_RISCV; - blob.hdr.entry_size = sizeof(RelocEntry); - blob.hdr.entry_count = 2; - - blob.entries[0].offset = 8; - blob.entries[0].addend = 0x200; - blob.entries[0].type = RELOC_RISCV_RELATIVE; - - blob.entries[1].offset = 16; - blob.entries[1].addend = link_base + 0x1234; - blob.entries[1].type = RELOC_RISCV_64; - - RelocStatus st = relocate_apply(base, sizeof(buf), link_base, - &blob, sizeof(blob)); - ST_ASSERT_EQ(st, RELOC_OK); - ST_ASSERT_EQ(*reinterpret_cast<uint64*>(base + 8), base + 0x200); - ST_ASSERT_EQ(*reinterpret_cast<uint64*>(base + 16), base + 0x1234); - - st_pass(); -} diff --git a/na/test/test_signal.cc b/na/test/test_signal.cc new file mode 100644 index 0000000..360b0e6 --- /dev/null +++ b/na/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/na/test/test_slab.cc b/na/test/test_slab.cc index 2ba1a5d..8a448b4 100644 --- a/na/test/test_slab.cc +++ b/na/test/test_slab.cc @@ -203,7 +203,7 @@ void selftest_slab() 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->state == PageState::Slab); + ST_ASSERT(page->type == PageType::Slab); ST_ASSERT(page->slab.obj_count == 32); // Free all items in this isolated slab page @@ -212,7 +212,7 @@ void selftest_slab() } // The page MUST have been returned to the PMM (either global Free or Per-CPU Cached). - ST_ASSERT(page->state == PageState::Free || page->state == PageState::PcpCached); + ST_ASSERT(page->type == PageType::Free || page->type == PageType::PcpCached); // Clean up the drain array for (int i = 0; i < drain_count; i++) { diff --git a/na/test/test_smp.cc b/na/test/test_smp.cc new file mode 100644 index 0000000..f934526 --- /dev/null +++ b/na/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/na/test/test_sys_fs_extra.cc b/na/test/test_sys_fs_extra.cc new file mode 100644 index 0000000..705ce63 --- /dev/null +++ b/na/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/na/test/test_sys_process.cc b/na/test/test_sys_process.cc new file mode 100644 index 0000000..686bed6 --- /dev/null +++ b/na/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/na/test/test_uaccess.cc b/na/test/test_uaccess.cc index d5795fc..c5a8c6c 100644 --- a/na/test/test_uaccess.cc +++ b/na/test/test_uaccess.cc @@ -7,13 +7,13 @@ #include <lume/vmm.h> #include <lume/pmm.h> +#include <lume/tlb.h> +#include <lume/mm_layout.h> #include <arch/pmap.h> -constexpr uint64 kKernelVABase = 0xFFFFFFC000000000ULL; - void selftest_uaccess() { - /* To test valid user addresses (< kKernelVABase), we need a writable page mapped there. + /* 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. @@ -33,7 +33,7 @@ void selftest_uaccess() 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 */ - asm volatile("sfence.vma" : : : "memory"); + tlb_shootdown_all(); st_begin("uaccess: copy_to_user valid range"); char kernel_buf[32] = "Hello User Space!"; @@ -67,12 +67,12 @@ void selftest_uaccess() st_pass(); st_begin("uaccess: copy_to_user invalid range (crosses boundary)"); - ret = copy_to_user(kKernelVABase - 10, kernel_buf, 20); + 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); + ret = copy_to_user(kKernelVaBase + 0x1000, kernel_buf, 20); ST_ASSERT_EQ(ret, -EFAULT); st_pass(); @@ -82,15 +82,15 @@ void selftest_uaccess() st_pass(); st_begin("uaccess: copy_from_user invalid ranges"); - ret = copy_from_user(read_buf, kKernelVABase - 5, 10); + ret = copy_from_user(read_buf, kKernelVaBase - 5, 10); ST_ASSERT_EQ(ret, -EFAULT); - ret = copy_from_user(read_buf, kKernelVABase, 10); + 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; + uint64 invalid_addr = kKernelVaBase + 0x1000; len = strncpy_from_user(str_buf, invalid_addr, 10); ST_ASSERT_EQ(len, -EFAULT); st_pass(); diff --git a/na/test/test_vfs_io.cc b/na/test/test_vfs_io.cc new file mode 100644 index 0000000..3d3b125 --- /dev/null +++ b/na/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/na/test/test_vmm.cc b/na/test/test_vmm.cc index 049570f..635e3f3 100644 --- a/na/test/test_vmm.cc +++ b/na/test/test_vmm.cc @@ -6,12 +6,17 @@ */ #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> -/* Assuming pmap namespace is accessible or we can test basic structures */ +namespace { +constexpr uint64 kUartBase = 0x10000000ULL; +constexpr uint64 kPageOffsetMask = kPageSize - 1; +} void selftest_vmm() { st_begin("vmm: virtual/physical address translation"); @@ -30,7 +35,7 @@ void selftest_vmm() { st_begin("vmm: page alignment macros"); { uint64 base = pmm_get_mem_base().raw; - uint64 addr = base + 0xFFF; + 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); @@ -40,8 +45,10 @@ void selftest_vmm() { st_begin("vmm: mapping kernel MMIO (driver API)"); { - uint64 mmio_pa = pmm_get_mem_base().raw; - vmm_map_kernel_mmio(mmio_pa, 0x1000); + /* 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; diff --git a/na/test/test_waitqueue.cc b/na/test/test_waitqueue.cc index 1bf88bb..7f50d6b 100644 --- a/na/test/test_waitqueue.cc +++ b/na/test/test_waitqueue.cc @@ -59,7 +59,7 @@ void selftest_waitqueue() kprintf("[test_wq] returned from yield 2\n"); ST_ASSERT(g_thread_woken == true); - + st_pass(); st_begin("waitqueue: wake_all"); |
