diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1b7d107..9c03a20 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - build_mode: ["", -Drelease-fast=true, -Drelease-safe=true, -Drelease-small=true] + build_mode: ["", -Drelease-fast, -Drelease-safe, -Drelease-small] steps: - uses: actions/checkout@v1 @@ -27,11 +27,13 @@ jobs: run: | sudo apt-get update sudo apt-get install qemu qemu-system --fix-missing + - name: Check formatting + run: zig*/zig fmt --check src - name: Build kernel run: zig*/zig build ${{ matrix.build_mode }} - name: Run unit tests run: zig*/zig build test ${{ matrix.build_mode }} - - name: Run runtime tests - run: zig*/zig build test -Drt-test=true ${{ matrix.build_mode }} - - name: Check formatting - run: zig*/zig fmt --check src + - name: Run runtime test - Initialisation + run: zig*/zig build rt-test -Ddisable-display -Dtest-mode=Initialisation ${{ matrix.build_mode }} + - name: Run runtime test - Panic + run: zig*/zig build rt-test -Ddisable-display -Dtest-mode=Panic ${{ matrix.build_mode }} diff --git a/README.md b/README.md index d5d19c5..5483225 100644 --- a/README.md +++ b/README.md @@ -41,25 +41,38 @@ Launch a gdb-multiarch instance and connect to qemu. zig build debug ``` -## Test +## Unit testing -Run the unit tests or runtime tests. +Run the unit tests. ```Shell zig build test ``` +## Runtime testing + +Run the runtime tests. + +```Shell +zig build rt-test -Dtest-mode= +``` + +Available test modes: + +* `None`: This is the default, this will run the OS normally. +* `Initialisation`: Run the OS's initialisation runtime tests to ensure the OS is properly set up. +* `Panic`: Run the panic runtime test. + ## Options -* `-Drt-test=`: Boolean (default `false`). - * **build**: Build with runtime testing enabled. Makes the kernel bigger and slower but tests important functionality. - * **test**: Run the runtime testing script instead of the unit tests. Checks for the expected log statements and fails if any are missing. * `-D[build-mode]=`: Boolean (default `false`). * **build**: Build a certain build mode (*release-safe*, *release-fast*, *release-small*). Don't set in order to use the *debug* build mode. * **test**: Test a certain build mode (*release-safe*, *release-fast*, *release-small*). Don't set in order to use the *debug* build mode. * `-Darch=`: String (default `x86`). Currently the only supported value is `x86`. * **build**: Build for a certain architecture. * **test**: Test a certain architecture. +* `-Ddisable-display`: Boolean (default `false`) + * This disables the display output of QEMU. ## Contribution diff --git a/build.zig b/build.zig index a9285a9..1b5d4bc 100644 --- a/build.zig +++ b/build.zig @@ -1,5 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); +const rt = @import("test/runtime_test.zig"); +const RuntimeStep = rt.RuntimeStep; const Builder = std.build.Builder; const LibExeObjStep = std.build.LibExeObjStep; const Step = std.build.Step; @@ -7,6 +9,8 @@ const Target = std.Target; const CrossTarget = std.zig.CrossTarget; const fs = std.fs; const Mode = builtin.Mode; +const TestMode = rt.TestMode; +const ArrayList = std.ArrayList; const x86_i686 = CrossTarget{ .cpu_arch = .i386, @@ -33,88 +37,100 @@ pub fn build(b: *Builder) !void { const main_src = "src/kernel/kmain.zig"; const arch_root = "src/kernel/arch"; const constants_path = try fs.path.join(b.allocator, &[_][]const u8{ arch_root, arch, "constants.zig" }); - - const build_mode = b.standardReleaseOptions(); - const rt_test = b.option(bool, "rt-test", "enable/disable runtime testing") orelse false; - - const exec = b.addExecutable("pluto.elf", main_src); - exec.addPackagePath("constants", constants_path); - exec.setOutputDir(b.cache_root); - exec.addBuildOption(bool, "rt_test", rt_test); - exec.setBuildMode(build_mode); const linker_script_path = try fs.path.join(b.allocator, &[_][]const u8{ arch_root, arch, "link.ld" }); - exec.setLinkerScriptPath(linker_script_path); - exec.setTarget(target); - const output_iso = try fs.path.join(b.allocator, &[_][]const u8{ b.exe_dir, "pluto.iso" }); const iso_dir_path = try fs.path.join(b.allocator, &[_][]const u8{ b.exe_dir, "iso" }); const boot_path = try fs.path.join(b.allocator, &[_][]const u8{ b.exe_dir, "iso", "boot" }); const modules_path = try fs.path.join(b.allocator, &[_][]const u8{ b.exe_dir, "iso", "modules" }); + const build_mode = b.standardReleaseOptions(); + comptime var test_mode_desc: []const u8 = "\n "; + inline for (@typeInfo(TestMode).Enum.fields) |field| { + const tm = @field(TestMode, field.name); + test_mode_desc = test_mode_desc ++ field.name ++ " (" ++ TestMode.getDescription(tm) ++ ")"; + test_mode_desc = test_mode_desc ++ "\n "; + } + + const test_mode = b.option(TestMode, "test-mode", "Run a specific runtime test. This option is for the rt-test step. Available options: " ++ test_mode_desc) orelse .None; + const disable_display = b.option(bool, "disable-display", "Disable the qemu window") orelse false; + + const exec = b.addExecutable("pluto.elf", main_src); + exec.addPackagePath("constants", constants_path); + exec.setOutputDir(b.cache_root); + exec.addBuildOption(TestMode, "test_mode", test_mode); + exec.setBuildMode(build_mode); + exec.setLinkerScriptPath(linker_script_path); + exec.setTarget(target); + const make_iso = switch (target.getCpuArch()) { .i386 => b.addSystemCommand(&[_][]const u8{ "./makeiso.sh", boot_path, modules_path, iso_dir_path, exec.getOutputPath(), output_iso }), else => unreachable, }; - make_iso.step.dependOn(&exec.step); + b.default_step.dependOn(&make_iso.step); const test_step = b.step("test", "Run tests"); - if (rt_test) { - const script = b.addSystemCommand(&[_][]const u8{ "python3", "test/rt-test.py", arch, b.zig_exe }); - test_step.dependOn(&script.step); - } else { - const mock_path = "\"../../test/mock/kernel/\""; - const arch_mock_path = "\"../../../../test/mock/kernel/\""; - const unit_tests = b.addTest(main_src); - unit_tests.setBuildMode(build_mode); - unit_tests.setMainPkgPath("."); - unit_tests.addPackagePath("constants", constants_path); - unit_tests.addBuildOption(bool, "rt_test", rt_test); - unit_tests.addBuildOption([]const u8, "mock_path", mock_path); - unit_tests.addBuildOption([]const u8, "arch_mock_path", arch_mock_path); + const mock_path = "\"../../test/mock/kernel/\""; + const arch_mock_path = "\"../../../../test/mock/kernel/\""; + const unit_tests = b.addTest(main_src); + unit_tests.setBuildMode(build_mode); + unit_tests.setMainPkgPath("."); + unit_tests.addPackagePath("constants", constants_path); + unit_tests.addBuildOption(TestMode, "test_mode", test_mode); + unit_tests.addBuildOption([]const u8, "mock_path", mock_path); + unit_tests.addBuildOption([]const u8, "arch_mock_path", arch_mock_path); - if (builtin.os.tag != .windows) unit_tests.enable_qemu = true; - - unit_tests.setTarget(.{ .cpu_arch = target.cpu_arch }); - - test_step.dependOn(&unit_tests.step); + if (builtin.os.tag != .windows) { + unit_tests.enable_qemu = true; } + unit_tests.setTarget(.{ .cpu_arch = target.cpu_arch }); + test_step.dependOn(&unit_tests.step); + + const rt_test_step = b.step("rt-test", "Run runtime tests"); + const build_mode_str = switch (build_mode) { + .Debug => "", + .ReleaseSafe => "-Drelease-safe", + .ReleaseFast => "-Drelease-fast", + .ReleaseSmall => "-Drelease-small", + }; + + var qemu_args_al = ArrayList([]const u8).init(b.allocator); + defer qemu_args_al.deinit(); + + switch (target.getCpuArch()) { + .i386 => try qemu_args_al.append("qemu-system-i386"), + else => unreachable, + } + try qemu_args_al.append("-serial"); + try qemu_args_al.append("stdio"); + switch (target.getCpuArch()) { + .i386 => { + try qemu_args_al.append("-boot"); + try qemu_args_al.append("d"); + try qemu_args_al.append("-cdrom"); + try qemu_args_al.append(output_iso); + }, + else => unreachable, + } + if (disable_display) { + try qemu_args_al.append("-display"); + try qemu_args_al.append("none"); + } + + var qemu_args = qemu_args_al.toOwnedSlice(); + + const rt_step = RuntimeStep.create(b, test_mode, qemu_args); + rt_step.step.dependOn(&make_iso.step); + rt_test_step.dependOn(&rt_step.step); + const run_step = b.step("run", "Run with qemu"); const run_debug_step = b.step("debug-run", "Run with qemu and wait for a gdb connection"); - const qemu_bin = switch (target.getCpuArch()) { - .i386 => "qemu-system-i386", - else => unreachable, - }; - const qemu_args = &[_][]const u8{ - qemu_bin, - "-serial", - "stdio", - }; - const qemu_machine_args = switch (target.getCpuArch()) { - .i386 => &[_][]const u8{ - "-boot", - "d", - "-cdrom", - output_iso, - }, - else => null, - }; const qemu_cmd = b.addSystemCommand(qemu_args); const qemu_debug_cmd = b.addSystemCommand(qemu_args); qemu_debug_cmd.addArgs(&[_][]const u8{ "-s", "-S" }); - if (qemu_machine_args) |machine_args| { - qemu_cmd.addArgs(machine_args); - qemu_debug_cmd.addArgs(machine_args); - } - - if (rt_test) { - const qemu_rt_test_args = &[_][]const u8{ "-display", "none" }; - qemu_cmd.addArgs(qemu_rt_test_args); - qemu_debug_cmd.addArgs(qemu_rt_test_args); - } qemu_cmd.step.dependOn(&make_iso.step); qemu_debug_cmd.step.dependOn(&make_iso.step); diff --git a/src/kernel/arch/x86/gdt.zig b/src/kernel/arch/x86/gdt.zig index f615d46..a11a7c8 100644 --- a/src/kernel/arch/x86/gdt.zig +++ b/src/kernel/arch/x86/gdt.zig @@ -3,7 +3,7 @@ const expect = std.testing.expect; const expectEqual = std.testing.expectEqual; const builtin = @import("builtin"); const is_test = builtin.is_test; - +const panic = @import("../../panic.zig").panic; const build_options = @import("build_options"); const mock_path = build_options.arch_mock_path; const arch = if (is_test) @import(mock_path ++ "arch_mock.zig") else @import("arch.zig"); @@ -438,7 +438,10 @@ pub fn init() void { // Load the TSS arch.ltr(TSS_OFFSET); - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } fn mock_lgdt(ptr: *const GdtPtr) void { @@ -671,8 +674,13 @@ test "init" { /// fn rt_loadedGDTSuccess() void { const loaded_gdt = arch.sgdt(); - expect(gdt_ptr.limit == loaded_gdt.limit); - expect(gdt_ptr.base == loaded_gdt.base); + if (gdt_ptr.limit != loaded_gdt.limit) { + panic(@errorReturnTrace(), "FAILURE: GDT not loaded properly: 0x{X} != 0x{X}\n", .{ gdt_ptr.limit, loaded_gdt.limit }); + } + if (gdt_ptr.base != loaded_gdt.base) { + panic(@errorReturnTrace(), "FAILURE: GDT not loaded properly: 0x{X} != {X}\n", .{ gdt_ptr.base, loaded_gdt.base }); + } + log.logInfo("GDT: Tested loading GDT\n", .{}); } /// @@ -680,5 +688,4 @@ fn rt_loadedGDTSuccess() void { /// fn runtimeTests() void { rt_loadedGDTSuccess(); - log.logInfo("GDT: Tested loading GDT\n", .{}); } diff --git a/src/kernel/arch/x86/idt.zig b/src/kernel/arch/x86/idt.zig index 33c7b2a..3e311f8 100644 --- a/src/kernel/arch/x86/idt.zig +++ b/src/kernel/arch/x86/idt.zig @@ -4,7 +4,7 @@ const expectEqual = std.testing.expectEqual; const expectError = std.testing.expectError; const builtin = @import("builtin"); const is_test = builtin.is_test; - +const panic = @import("../../panic.zig").panic; const build_options = @import("build_options"); const mock_path = build_options.arch_mock_path; const gdt = if (is_test) @import(mock_path ++ "gdt_mock.zig") else @import("gdt.zig"); @@ -101,7 +101,7 @@ var idt_ptr: IdtPtr = IdtPtr{ .base = 0, }; -/// The IDT entry table of NUMBER_OF_ENTRIES entries. Initially all zero'ed. +/// The IDT entry table of NUMBER_OF_ENTRIES entries. Initially all zeroed. var idt_entries: [NUMBER_OF_ENTRIES]IdtEntry = [_]IdtEntry{IdtEntry{ .base_low = 0, .selector = 0, @@ -186,7 +186,10 @@ pub fn init() void { arch.lidt(&idt_ptr); - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } fn testHandler0() callconv(.Naked) void {} @@ -325,8 +328,12 @@ test "init" { /// fn rt_loadedIDTSuccess() void { const loaded_idt = arch.sidt(); - expect(idt_ptr.limit == loaded_idt.limit); - expect(idt_ptr.base == loaded_idt.base); + if (idt_ptr.limit != loaded_idt.limit) { + panic(@errorReturnTrace(), "FAILURE: IDT not loaded properly: 0x{X} != 0x{X}\n", .{ idt_ptr.limit, loaded_idt.limit }); + } + if (idt_ptr.base != loaded_idt.base) { + panic(@errorReturnTrace(), "FAILURE: IDT not loaded properly: 0x{X} != {X}\n", .{ idt_ptr.base, loaded_idt.base }); + } log.logInfo("IDT: Tested loading IDT\n", .{}); } diff --git a/src/kernel/arch/x86/irq.zig b/src/kernel/arch/x86/irq.zig index 7ce9609..7f1defe 100644 --- a/src/kernel/arch/x86/irq.zig +++ b/src/kernel/arch/x86/irq.zig @@ -88,7 +88,7 @@ fn openIrq(index: u8, handler: idt.InterruptHandler) void { /// IN irq_num: u8 - The IRQ index to test. /// /// Return: bool -/// Whether the IRQ index if valid. +/// Whether the IRQ index is valid. /// pub fn isValidIrq(irq_num: u32) bool { return irq_num < NUMBER_OF_ENTRIES; @@ -136,7 +136,10 @@ pub fn init() void { openIrq(i, interrupts.getInterruptStub(i)); } - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } fn testFunction0() callconv(.Naked) void {} @@ -227,13 +230,13 @@ test "registerIrq invalid irq index" { } /// -/// Test that all handers are null at initialisation. +/// Test that all handlers are null at initialisation. /// fn rt_unregisteredHandlers() void { // Ensure all ISR are not registered yet for (irq_handlers) |h, i| { if (h) |_| { - panic(@errorReturnTrace(), "Handler found for IRQ: {}-{}\n", .{ i, h }); + panic(@errorReturnTrace(), "FAILURE: Handler found for IRQ: {}-{}\n", .{ i, h }); } } @@ -250,7 +253,7 @@ fn rt_openedIdtEntries() void { for (idt_entries) |entry, i| { if (i >= IRQ_OFFSET and isValidIrq(i - IRQ_OFFSET)) { if (!idt.isIdtOpen(entry)) { - panic(@errorReturnTrace(), "IDT entry for {} is not open\n", .{i}); + panic(@errorReturnTrace(), "FAILURE: IDT entry for {} is not open\n", .{i}); } } } diff --git a/src/kernel/arch/x86/isr.zig b/src/kernel/arch/x86/isr.zig index b8fddd5..2292fd0 100644 --- a/src/kernel/arch/x86/isr.zig +++ b/src/kernel/arch/x86/isr.zig @@ -130,7 +130,7 @@ pub const SECURITY: u8 = 30; /// The of exception handlers initialised to null. Need to open a ISR for these to be valid. var isr_handlers: [NUMBER_OF_ENTRIES]?IsrHandler = [_]?IsrHandler{null} ** NUMBER_OF_ENTRIES; -/// The syscall hander. +/// The syscall handler. var syscall_handler: ?IsrHandler = null; /// @@ -188,7 +188,7 @@ fn openIsr(index: u8, handler: idt.InterruptHandler) void { /// IN isr_num: u16 - The isr number to check /// /// Return: bool -/// Whether a ISR hander index if valid. +/// Whether a ISR handler index is valid. /// pub fn isValidIsr(isr_num: u32) bool { return isr_num < NUMBER_OF_ENTRIES or isr_num == syscalls.INTERRUPT; @@ -244,7 +244,10 @@ pub fn init() void { openIsr(syscalls.INTERRUPT, interrupts.getInterruptStub(syscalls.INTERRUPT)); - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } fn testFunction0() callconv(.Naked) void {} @@ -356,18 +359,18 @@ test "registerIsr invalid isr index" { } /// -/// Test that all handers are null at initialisation. +/// Test that all handlers are null at initialisation. /// fn rt_unregisteredHandlers() void { // Ensure all ISR are not registered yet for (isr_handlers) |h, i| { if (h) |_| { - panic(@errorReturnTrace(), "Handler found for ISR: {}-{}\n", .{ i, h }); + panic(@errorReturnTrace(), "FAILURE: Handler found for ISR: {}-{}\n", .{ i, h }); } } if (syscall_handler) |h| { - panic(@errorReturnTrace(), "Pre-testing failed for syscall: {}\n", .{h}); + panic(@errorReturnTrace(), "FAILURE: Pre-testing failed for syscall: {}\n", .{h}); } log.logInfo("ISR: Tested registered handlers\n", .{}); @@ -383,7 +386,7 @@ fn rt_openedIdtEntries() void { for (idt_entries) |entry, i| { if (isValidIsr(i)) { if (!idt.isIdtOpen(entry)) { - panic(@errorReturnTrace(), "IDT entry for {} is not open\n", .{i}); + panic(@errorReturnTrace(), "FAILURE: IDT entry for {} is not open\n", .{i}); } } } diff --git a/src/kernel/arch/x86/paging.zig b/src/kernel/arch/x86/paging.zig index b02a7a9..54b94d9 100644 --- a/src/kernel/arch/x86/paging.zig +++ b/src/kernel/arch/x86/paging.zig @@ -11,7 +11,7 @@ const log = @import("../../log.zig"); const mem = @import("../../mem.zig"); const vmm = @import("../../vmm.zig"); const multiboot = @import("multiboot.zig"); -const options = @import("build_options"); +const build_options = @import("build_options"); const testing = std.testing; /// An array of directory entries and page tables. Forms the first level of paging and covers the entire 4GB memory space. @@ -138,7 +138,7 @@ inline fn virtToTableEntryIdx(virt: usize) usize { /// /// Arguments: /// val: *align(1) u32 - The entry to modify -/// attr: u32 - The bits corresponding to the atttribute to set +/// attr: u32 - The bits corresponding to the attribute to set /// inline fn setAttribute(val: *align(1) u32, attr: u32) void { val.* |= attr; @@ -149,7 +149,7 @@ inline fn setAttribute(val: *align(1) u32, attr: u32) void { /// /// Arguments: /// val: *align(1) u32 - The entry to modify -/// attr: u32 - The bits corresponding to the atttribute to clear +/// attr: u32 - The bits corresponding to the attribute to clear /// inline fn clearAttribute(val: *align(1) u32, attr: u32) void { val.* &= ~attr; @@ -157,7 +157,7 @@ inline fn clearAttribute(val: *align(1) u32, attr: u32) void { /// /// Map a page directory entry, setting the present, size, writable, write-through and physical address bits. -/// Clears the user and cache disabled bits. Entry should be zero'ed. +/// Clears the user and cache disabled bits. Entry should be zeroed. /// /// Arguments: /// IN virt_addr: usize - The start of the virtual space to map @@ -297,11 +297,11 @@ fn mapTableEntry(entry: *align(1) TableEntry, phys_addr: usize, attrs: vmm.Attri /// Arguments: /// IN virtual_start: usize - The start of the virtual region to map /// IN virtual_end: usize - The end (exclusive) of the virtual region to map -/// IN physical_start: usize - The start of the physical region to mape to +/// IN physical_start: usize - The start of the physical region to map to /// IN physical_end: usize - The end (exclusive) of the physical region to map to /// IN attrs: vmm.Attributes - The attributes to apply to this mapping -/// INOUT allocator: *std.mem.Allocator - The allocator to use to allocate any intermediate data structures required to map this region -/// INOUT dir: *Directory - The page directory to map within +/// IN/OUT allocator: *std.mem.Allocator - The allocator to use to allocate any intermediate data structures required to map this region +/// IN/OUT dir: *Directory - The page directory to map within /// /// Error: vmm.MapperError || std.mem.Allocator.Error /// * - See mapDirEntry @@ -326,7 +326,7 @@ pub fn map(virt_start: usize, virt_end: usize, phys_start: usize, phys_end: usiz /// Arguments: /// IN virtual_start: usize - The start of the virtual region to unmap /// IN virtual_end: usize - The end (exclusive) of the virtual region to unmap -/// INOUT dir: *Directory - The page directory to unmap within +/// IN/OUT dir: *Directory - The page directory to unmap within /// /// Error: std.mem.Allocator.Error || vmm.MapperError /// vmm.MapperError.NotMapped - If the region being unmapped wasn't mapped in the first place @@ -396,7 +396,7 @@ pub fn init(mb_info: *multiboot.multiboot_info_t, mem_profile: *const MemProfile log.logInfo("Init paging\n", .{}); defer log.logInfo("Done paging\n", .{}); - isr.registerIsr(isr.PAGE_FAULT, if (options.rt_test) rt_pageFault else pageFault) catch |e| { + isr.registerIsr(isr.PAGE_FAULT, if (build_options.test_mode == .Initialisation) rt_pageFault else pageFault) catch |e| { panic(@errorReturnTrace(), "Failed to register page fault ISR: {}\n", .{e}); }; const dir_physaddr = @ptrToInt(mem.virtToPhys(&kernel_directory)); @@ -405,7 +405,10 @@ pub fn init(mb_info: *multiboot.multiboot_info_t, mem_profile: *const MemProfile : [addr] "{eax}" (dir_physaddr) ); const v_end = std.mem.alignForward(@ptrToInt(mem_profile.vaddr_end) + mem.FIXED_ALLOC_SIZE, PAGE_SIZE_4KB); - if (options.rt_test) runtimeTests(v_end); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(v_end), + else => {}, + } } fn checkDirEntry(entry: DirectoryEntry, virt_start: usize, virt_end: usize, phys_start: usize, attrs: vmm.Attributes, table: *Table, present: bool) void { @@ -541,7 +544,7 @@ test "map and unmap" { } // The labels to jump to after attempting to cause a page fault. This is needed as we don't want to cause an -// infinite loop by jummping to the same instruction that caused the fault. +// infinite loop by jumping to the same instruction that caused the fault. extern var rt_fault_callback: *u32; extern var rt_fault_callback2: *u32; @@ -560,26 +563,32 @@ fn rt_accessUnmappedMem(v_end: u32) void { // Accessing unmapped mem causes a page fault var ptr = @intToPtr(*u8, v_end); var value = ptr.*; + // Need this as in release builds the above is optimised out so it needs to be use + log.logError("FAILURE: Value: {}\n", .{value}); // This is the label that we return to after processing the page fault asm volatile ( \\.global rt_fault_callback \\rt_fault_callback: ); - testing.expect(faulted); + if (!faulted) { + panic(@errorReturnTrace(), "FAILURE: Paging should have faulted\n", .{}); + } log.logInfo("Paging: Tested accessing unmapped memory\n", .{}); } fn rt_accessMappedMem(v_end: u32) void { use_callback2 = true; faulted = false; - // Accessing mapped memory does't cause a page fault + // Accessing mapped memory doesn't cause a page fault var ptr = @intToPtr(*u8, v_end - PAGE_SIZE_4KB); var value = ptr.*; asm volatile ( \\.global rt_fault_callback2 \\rt_fault_callback2: ); - testing.expect(!faulted); + if (faulted) { + panic(@errorReturnTrace(), "FAILURE: Paging shouldn't have faulted\n", .{}); + } log.logInfo("Paging: Tested accessing mapped memory\n", .{}); } diff --git a/src/kernel/arch/x86/pic.zig b/src/kernel/arch/x86/pic.zig index a122658..d8aad23 100644 --- a/src/kernel/arch/x86/pic.zig +++ b/src/kernel/arch/x86/pic.zig @@ -468,7 +468,10 @@ pub fn init() void { // Clear the IRQ for the slave clearMask(IRQ_CASCADE_FOR_SLAVE); - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } test "sendCommandMaster" { @@ -814,11 +817,11 @@ test "init" { fn rt_picAllMasked() void { // The master will have interrupt 2 clear because this is the link to the slave (third bit) if (readDataMaster() != 0xFB) { - panic(@errorReturnTrace(), "Master masks are not set, found: {}\n", .{readDataMaster()}); + panic(@errorReturnTrace(), "FAILURE: Master masks are not set, found: {}\n", .{readDataMaster()}); } if (readDataSlave() != 0xFF) { - panic(@errorReturnTrace(), "Slave masks are not set, found: {}\n", .{readDataSlave()}); + panic(@errorReturnTrace(), "FAILURE: Slave masks are not set, found: {}\n", .{readDataSlave()}); } log.logInfo("PIC: Tested masking\n", .{}); diff --git a/src/kernel/arch/x86/pit.zig b/src/kernel/arch/x86/pit.zig index ff0a617..d9f733f 100644 --- a/src/kernel/arch/x86/pit.zig +++ b/src/kernel/arch/x86/pit.zig @@ -391,7 +391,10 @@ pub fn init() void { }, }; - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } test "sendCommand" { @@ -551,7 +554,7 @@ fn rt_waitTicks() void { const difference = getTicks() - waiting; if (previous_count + epsilon < difference or previous_count > difference + epsilon) { - panic(@errorReturnTrace(), "Waiting failed. difference: {}, previous_count: {}. Epsilon: {}\n", .{ difference, previous_count, epsilon }); + panic(@errorReturnTrace(), "FAILURE: Waiting failed. difference: {}, previous_count: {}. Epsilon: {}\n", .{ difference, previous_count, epsilon }); } log.logInfo("PIT: Tested wait ticks\n", .{}); @@ -575,13 +578,13 @@ fn rt_waitTicks2() void { const difference = getTicks() + 15 - waiting; if (previous_count + epsilon < difference or previous_count > difference + epsilon) { - panic(@errorReturnTrace(), "Waiting failed. difference: {}, previous_count: {}. Epsilon: {}\n", .{ difference, previous_count, epsilon }); + panic(@errorReturnTrace(), "FAILURE: Waiting failed. difference: {}, previous_count: {}. Epsilon: {}\n", .{ difference, previous_count, epsilon }); } - log.logInfo("PIT: Tested wait ticks 2\n", .{}); - // Reset ticks ticks = 0; + + log.logInfo("PIT: Tested wait ticks 2\n", .{}); } /// @@ -593,7 +596,7 @@ fn rt_initCounter_0() void { const expected_hz: u32 = 10027; if (time_ns != expected_ns or time_under_1_ns != expected_ps or getFrequency() != expected_hz) { - panic(@errorReturnTrace(), "Frequency not set properly. Hz: {}!={}, ns: {}!={}, ps: {}!= {}\n", .{ + panic(@errorReturnTrace(), "FAILURE: Frequency not set properly. Hz: {}!={}, ns: {}!={}, ps: {}!= {}\n", .{ getFrequency(), expected_hz, time_ns, @@ -611,19 +614,19 @@ fn rt_initCounter_0() void { irq_exists = true; }, error.InvalidIrq => { - panic(@errorReturnTrace(), "IRQ for PIT, IRQ number: {} is invalid", .{pic.IRQ_PIT}); + panic(@errorReturnTrace(), "FAILURE: IRQ for PIT, IRQ number: {} is invalid", .{pic.IRQ_PIT}); }, }; if (!irq_exists) { - panic(@errorReturnTrace(), "IRQ for PIT doesn't exists\n", .{}); + panic(@errorReturnTrace(), "FAILURE: IRQ for PIT doesn't exists\n", .{}); } const expected_mode = OCW_READ_LOAD_DATA | OCW_MODE_SQUARE_WAVE_GENERATOR | OCW_SELECT_COUNTER_0 | OCW_BINARY_COUNT_BINARY; const actual_mode = readBackCommand(CounterSelect.Counter0); if (expected_mode != actual_mode) { - panic(@errorReturnTrace(), "Operating mode don't not set properly. Found: {}, expecting: {}\n", .{ actual_mode, expected_mode }); + panic(@errorReturnTrace(), "FAILURE: Operating mode don't not set properly. Found: {}, expecting: {}\n", .{ actual_mode, expected_mode }); } log.logInfo("PIT: Tested init\n", .{}); diff --git a/src/kernel/arch/x86/rtc.zig b/src/kernel/arch/x86/rtc.zig index c61e1fe..3500885 100644 --- a/src/kernel/arch/x86/rtc.zig +++ b/src/kernel/arch/x86/rtc.zig @@ -49,9 +49,9 @@ var ticks: u32 = 0; /// registers so don't get inconsistent values. /// /// Return: bool -/// Whether the CMOS chip is buzzy and a update is in progress. +/// Whether the CMOS chip is busy and a update is in progress. /// -fn isBuzzy() bool { +fn isBusy() bool { return (cmos.readStatusRegister(cmos.StatusRegister.A, false) & 0x80) != 0; } @@ -122,7 +122,7 @@ fn bcdToBinary(bcd: u32) u32 { /// fn readRtcRegisters() DateTime { // Make sure there isn't a update in progress - while (isBuzzy()) {} + while (isBusy()) {} var date_time = DateTime{ .second = cmos.readRtcRegister(cmos.RtcRegister.SECOND), @@ -283,10 +283,13 @@ pub fn init() void { // Read status register C to clear any interrupts that may have happened during set up const reg_c = cmos.readStatusRegister(cmos.StatusRegister.C, false); - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } -test "isBuzzy not buzzy" { +test "isBusy not busy" { cmos.initTest(); defer cmos.freeTest(); @@ -295,10 +298,10 @@ test "isBuzzy not buzzy" { .{ cmos.StatusRegister.A, false, @as(u8, 0x60) }, ); - expect(!isBuzzy()); + expect(!isBusy()); } -test "isBuzzy buzzy" { +test "isBusy busy" { cmos.initTest(); defer cmos.freeTest(); @@ -307,7 +310,7 @@ test "isBuzzy buzzy" { .{ cmos.StatusRegister.A, false, @as(u8, 0x80) }, ); - expect(isBuzzy()); + expect(isBusy()); } test "calcDayOfWeek" { @@ -446,7 +449,7 @@ test "readRtcRegisters" { cmos.initTest(); defer cmos.freeTest(); - // Have 2 buzzy loops + // Have 2 busy loops cmos.addTestParams( "readStatusRegister", .{ cmos.StatusRegister.A, false, @as(u8, 0x80), cmos.StatusRegister.A, false, @as(u8, 0x80), cmos.StatusRegister.A, false, @as(u8, 0x00) }, @@ -481,7 +484,7 @@ test "readRtc unstable read" { cmos.initTest(); defer cmos.freeTest(); - // No buzzy loop + // No busy loop cmos.addTestParams( "readStatusRegister", .{ cmos.StatusRegister.A, false, @as(u8, 0x00), cmos.StatusRegister.A, false, @as(u8, 0x00) }, @@ -504,7 +507,7 @@ test "readRtc unstable read" { }); // Will try again, and now stable - // No buzzy loop + // No busy loop cmos.addTestParams( "readStatusRegister", .{ cmos.StatusRegister.A, false, @as(u8, 0x00), cmos.StatusRegister.A, false, @as(u8, 0x00) }, @@ -555,7 +558,7 @@ test "readRtc is BCD" { cmos.initTest(); defer cmos.freeTest(); - // No buzzy loop + // No busy loop cmos.addTestParams( "readStatusRegister", .{ cmos.StatusRegister.A, false, @as(u8, 0x00), cmos.StatusRegister.A, false, @as(u8, 0x00) }, @@ -608,7 +611,7 @@ test "readRtc is 12 hours" { cmos.initTest(); defer cmos.freeTest(); - // No buzzy loop + // No busy loop cmos.addTestParams( "readStatusRegister", .{ cmos.StatusRegister.A, false, @as(u8, 0x00), cmos.StatusRegister.A, false, @as(u8, 0x00) }, @@ -695,24 +698,24 @@ fn rt_init() void { irq_exists = true; }, error.InvalidIrq => { - panic(@errorReturnTrace(), "IRQ for RTC, IRQ number: {} is invalid", .{pic.IRQ_REAL_TIME_CLOCK}); + panic(@errorReturnTrace(), "FAILURE: IRQ for RTC, IRQ number: {} is invalid\n", .{pic.IRQ_REAL_TIME_CLOCK}); }, }; if (!irq_exists) { - panic(@errorReturnTrace(), "IRQ for RTC doesn't exists\n", .{}); + panic(@errorReturnTrace(), "FAILURE: IRQ for RTC doesn't exists\n", .{}); } // Check the rate const status_a = cmos.readStatusRegister(cmos.StatusRegister.A, false); if (status_a & @as(u8, 0x0F) != 7) { - panic(@errorReturnTrace(), "Rate not set properly, got: {}\n", .{status_a & @as(u8, 0x0F)}); + panic(@errorReturnTrace(), "FAILURE: Rate not set properly, got: {}\n", .{status_a & @as(u8, 0x0F)}); } // Check if interrupts are enabled const status_b = cmos.readStatusRegister(cmos.StatusRegister.B, true); if (status_b & ~@as(u8, 0x40) == 0) { - panic(@errorReturnTrace(), "Interrupts not enabled\n", .{}); + panic(@errorReturnTrace(), "FAILURE: Interrupts not enabled\n", .{}); } log.logInfo("RTC: Tested init\n", .{}); @@ -727,7 +730,7 @@ fn rt_interrupts() void { pit.waitTicks(100); if (prev_ticks == ticks) { - panic(@errorReturnTrace(), "No interrupt happened\n", .{}); + panic(@errorReturnTrace(), "FAILURE: No interrupt happened\n", .{}); } log.logInfo("RTC: Tested interrupts\n", .{}); diff --git a/src/kernel/arch/x86/serial.zig b/src/kernel/arch/x86/serial.zig index 3388857..5163571 100644 --- a/src/kernel/arch/x86/serial.zig +++ b/src/kernel/arch/x86/serial.zig @@ -65,7 +65,7 @@ fn lcrValue(char_len: u8, stop_bit: bool, parity_bit: bool, msb: u1) SerialError } /// -/// The serial controller accepts a divisor rather than a raw badrate, as that is more space efficient. +/// The serial controller accepts a divisor rather than a raw baudrate, as that is more space efficient. /// This function computes the divisor for a desired baudrate. Note that multiple baudrates can have the same divisor. /// /// Arguments: diff --git a/src/kernel/arch/x86/syscalls.zig b/src/kernel/arch/x86/syscalls.zig index 28caf31..c314518 100644 --- a/src/kernel/arch/x86/syscalls.zig +++ b/src/kernel/arch/x86/syscalls.zig @@ -3,7 +3,8 @@ const testing = @import("std").testing; const assert = @import("std").debug.assert; const isr = @import("isr.zig"); const log = @import("../../log.zig"); -const options = @import("build_options"); +const build_options = @import("build_options"); +const panic = @import("../../panic.zig").panic; /// The isr number associated with syscalls pub const INTERRUPT: u16 = 0x80; @@ -241,39 +242,43 @@ pub fn init() void { defer log.logInfo("Done syscalls\n", .{}); isr.registerIsr(INTERRUPT, handle) catch unreachable; - if (options.rt_test) runtimeTests(); + + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } /// Tests -var testInt: u32 = 0; +var test_int: u32 = 0; fn testHandler0(ctx: *arch.InterruptContext, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { - testInt += 1; + test_int += 1; return 0; } fn testHandler1(ctx: *arch.InterruptContext, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { - testInt += arg1; + test_int += arg1; return 1; } fn testHandler2(ctx: *arch.InterruptContext, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { - testInt += arg1 + arg2; + test_int += arg1 + arg2; return 2; } fn testHandler3(ctx: *arch.InterruptContext, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { - testInt += arg1 + arg2 + arg3; + test_int += arg1 + arg2 + arg3; return 3; } fn testHandler4(ctx: *arch.InterruptContext, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { - testInt += arg1 + arg2 + arg3 + arg4; + test_int += arg1 + arg2 + arg3 + arg4; return 4; } fn testHandler5(ctx: *arch.InterruptContext, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { - testInt += arg1 + arg2 + arg3 + arg4 + arg5; + test_int += arg1 + arg2 + arg3 + arg4 + arg5; return 5; } @@ -286,29 +291,40 @@ test "registerSyscall returns SyscallExists" { } fn runtimeTests() void { - registerSyscall(123, testHandler0) catch unreachable; - registerSyscall(124, testHandler1) catch unreachable; - registerSyscall(125, testHandler2) catch unreachable; - registerSyscall(126, testHandler3) catch unreachable; - registerSyscall(127, testHandler4) catch unreachable; - registerSyscall(128, testHandler5) catch unreachable; - assert(testInt == 0); + registerSyscall(123, testHandler0) catch panic(@errorReturnTrace(), "FAILURE registering handler 0\n", .{}); + registerSyscall(124, testHandler1) catch panic(@errorReturnTrace(), "FAILURE registering handler 1\n", .{}); + registerSyscall(125, testHandler2) catch panic(@errorReturnTrace(), "FAILURE registering handler 2\n", .{}); + registerSyscall(126, testHandler3) catch panic(@errorReturnTrace(), "FAILURE registering handler 3\n", .{}); + registerSyscall(127, testHandler4) catch panic(@errorReturnTrace(), "FAILURE registering handler 4\n", .{}); + registerSyscall(128, testHandler5) catch panic(@errorReturnTrace(), "FAILURE registering handler 5\n", .{}); - if (syscall0(123) == 0 and testInt == 1) - log.logInfo("Syscalls: Tested no args\n", .{}); + if (test_int != 0) { + panic(@errorReturnTrace(), "FAILURE initial test_int not 0: {}\n", .{test_int}); + } - if (syscall1(124, 2) == 1 and testInt == 3) - log.logInfo("Syscalls: Tested 1 arg\n", .{}); + if (syscall0(123) != 0 or test_int != 1) { + panic(@errorReturnTrace(), "FAILURE syscall0\n", .{}); + } - if (syscall2(125, 2, 3) == 2 and testInt == 8) - log.logInfo("Syscalls: Tested 2 args\n", .{}); + if (syscall1(124, 2) != 1 or test_int != 3) { + panic(@errorReturnTrace(), "FAILURE syscall2\n", .{}); + } - if (syscall3(126, 2, 3, 4) == 3 and testInt == 17) - log.logInfo("Syscalls: Tested 3 args\n", .{}); + if (syscall2(125, 2, 3) != 2 or test_int != 8) { + panic(@errorReturnTrace(), "FAILURE syscall2\n", .{}); + } - if (syscall4(127, 2, 3, 4, 5) == 4 and testInt == 31) - log.logInfo("Syscalls: Tested 4 args\n", .{}); + if (syscall3(126, 2, 3, 4) != 3 or test_int != 17) { + panic(@errorReturnTrace(), "FAILURE syscall3\n", .{}); + } - if (syscall5(128, 2, 3, 4, 5, 6) == 5 and testInt == 51) - log.logInfo("Syscalls: Tested 5 args\n", .{}); + if (syscall4(127, 2, 3, 4, 5) != 4 or test_int != 31) { + panic(@errorReturnTrace(), "FAILURE syscall4\n", .{}); + } + + if (syscall5(128, 2, 3, 4, 5, 6) != 5 or test_int != 51) { + panic(@errorReturnTrace(), "FAILURE syscall5\n", .{}); + } + + log.logInfo("Syscall: Tested all args\n", .{}); } diff --git a/src/kernel/arch/x86/tty.zig b/src/kernel/arch/x86/tty.zig index 2de30bf..436a304 100644 --- a/src/kernel/arch/x86/tty.zig +++ b/src/kernel/arch/x86/tty.zig @@ -548,7 +548,10 @@ pub fn init() void { row = ROW_MIN; } - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } const test_colour: u8 = vga.orig_entryColour(vga.COLOUR_LIGHT_GREY, vga.COLOUR_BLACK); diff --git a/src/kernel/arch/x86/vga.zig b/src/kernel/arch/x86/vga.zig index 74a25ec..002d756 100644 --- a/src/kernel/arch/x86/vga.zig +++ b/src/kernel/arch/x86/vga.zig @@ -293,7 +293,10 @@ pub fn init() void { // Set by default the underline cursor setCursorShape(CursorShape.UNDERLINE); - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } test "entryColour" { @@ -531,7 +534,7 @@ fn rt_correctMaxScanLine() void { const max_scan_line = getPortData(REG_MAXIMUM_SCAN_LINE); if (max_scan_line != CURSOR_SCANLINE_END) { - panic(@errorReturnTrace(), "Max scan line not {}, found {}\n", .{ CURSOR_SCANLINE_END, max_scan_line }); + panic(@errorReturnTrace(), "FAILURE: Max scan line not {}, found {}\n", .{ CURSOR_SCANLINE_END, max_scan_line }); } log.logInfo("VGA: Tested max scan line\n", .{}); @@ -543,14 +546,14 @@ fn rt_correctMaxScanLine() void { fn rt_correctCursorShape() void { // Check the global variables are correct if (cursor_scanline_start != CURSOR_SCANLINE_MIDDLE or cursor_scanline_end != CURSOR_SCANLINE_END) { - panic(@errorReturnTrace(), "Global cursor scanline incorrect. Start: {}, end: {}\n", .{ cursor_scanline_start, cursor_scanline_end }); + panic(@errorReturnTrace(), "FAILURE: Global cursor scanline incorrect. Start: {}, end: {}\n", .{ cursor_scanline_start, cursor_scanline_end }); } const cursor_start = getPortData(REG_CURSOR_START); const cursor_end = getPortData(REG_CURSOR_END); if (cursor_start != CURSOR_SCANLINE_MIDDLE or cursor_end != CURSOR_SCANLINE_END) { - panic(@errorReturnTrace(), "Cursor scanline are incorrect. Start: {}, end: {}\n", .{ cursor_start, cursor_end }); + panic(@errorReturnTrace(), "FAILURE: Cursor scanline are incorrect. Start: {}, end: {}\n", .{ cursor_start, cursor_end }); } log.logInfo("VGA: Tested cursor shape\n", .{}); @@ -579,7 +582,7 @@ fn rt_setCursorGetCursor() void { const actual_y_loc = @truncate(u8, actual_linear_loc / WIDTH); if (x != actual_x_loc or y != actual_y_loc) { - panic(@errorReturnTrace(), "VGA cursor not the same: a_x: {}, a_y: {}, e_x: {}, e_y: {}\n", .{ x, y, actual_x_loc, actual_y_loc }); + panic(@errorReturnTrace(), "FAILURE: VGA cursor not the same: a_x: {}, a_y: {}, e_x: {}, e_y: {}\n", .{ x, y, actual_x_loc, actual_y_loc }); } // Restore the previous x and y diff --git a/src/kernel/bitmap.zig b/src/kernel/bitmap.zig index 505cb03..cff88f8 100644 --- a/src/kernel/bitmap.zig +++ b/src/kernel/bitmap.zig @@ -67,7 +67,7 @@ pub fn Bitmap(comptime BitmapType: type) type { /// Set an entry within a bitmap as occupied. /// /// Arguments: - /// INOUT self: *Self - The bitmap to modify. + /// IN/OUT self: *Self - The bitmap to modify. /// IN idx: usize - The index within the bitmap to set. /// /// Error: BitmapError. @@ -86,7 +86,7 @@ pub fn Bitmap(comptime BitmapType: type) type { /// Set an entry within a bitmap as unoccupied. /// /// Arguments: - /// INOUT self: *Self - The bitmap to modify. + /// IN/OUT self: *Self - The bitmap to modify. /// IN idx: usize - The index within the bitmap to clear. /// /// Error: BitmapError. @@ -106,7 +106,7 @@ pub fn Bitmap(comptime BitmapType: type) type { /// /// Arguments: /// IN self: *const Self - The bitmap to use. - /// IN idx: usize - The index into all of the bitmap's entries. + /// IN idx: usize - The index into all of the bitmaps entries. /// /// Return: BitmapType. /// The bit corresponding to that index but within a single BitmapType. @@ -119,7 +119,7 @@ pub fn Bitmap(comptime BitmapType: type) type { /// Find a number of contiguous free entries and set them. /// /// Arguments: - /// INOUT self: *Self - The bitmap to modify. + /// IN/OUT self: *Self - The bitmap to modify. /// IN num: usize - The number of entries to set. /// /// Return: ?usize diff --git a/src/kernel/heap.zig b/src/kernel/heap.zig index ffb5721..4fb1324 100644 --- a/src/kernel/heap.zig +++ b/src/kernel/heap.zig @@ -158,7 +158,7 @@ const Heap = struct { /// Attempt to allocate a portion of memory within a heap. It is recommended to not call this directly and instead use the Allocator interface. /// /// Arguments: - /// INOUT self: *Heap - The heap to allocate within + /// IN/OUT self: *Heap - The heap to allocate within /// IN size: usize - The size of the allocation /// IN alignment: ?u29 - The alignment that the returned address should have, else null if no alignment is required /// @@ -187,7 +187,7 @@ const Heap = struct { /// Free previously allocated memory. It is recommended to not call this directly and instead use the Allocator interface. /// /// Arguments: - /// INOUT self: *Heap - The heap to free within + /// IN/OUT self: *Heap - The heap to free within /// IN ptr: usize - The address of the allocation to free. Should have been returned from a prior call to alloc. /// IN len: usize - The size of the allocated region. /// diff --git a/src/kernel/kmain.zig b/src/kernel/kmain.zig index 0b73c20..eded63a 100644 --- a/src/kernel/kmain.zig +++ b/src/kernel/kmain.zig @@ -81,6 +81,12 @@ export fn kmain(boot_payload: arch.BootPayload) void { tty.print("Hello Pluto from kernel :)\n", .{}); - // The panic runtime tests must run last as they never return - if (build_options.rt_test) panic_root.runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => { + log.logInfo("SUCCESS\n", .{}); + }, + else => {}, + } + + arch.spinWait(); } diff --git a/src/kernel/log.zig b/src/kernel/log.zig index 7126604..40bd29d 100644 --- a/src/kernel/log.zig +++ b/src/kernel/log.zig @@ -9,6 +9,7 @@ const LoggingError = error{}; /// The OutStream for the format function const OutStream = std.io.OutStream(void, LoggingError, logCallback); +/// The different levels of logging that can be outputted. pub const Level = enum { INFO, DEBUG, @@ -18,6 +19,20 @@ pub const Level = enum { var serial: Serial = undefined; +/// +/// The call back function for the std library format function. +/// +/// Arguments: +/// context: void - The context of the printing. There isn't a need for a context for this +/// so is void. +/// str: []const u8 - The string to print to the serial terminal. +/// +/// Return: usize +/// The number of bytes written. This will always be the length of the string to print. +/// +/// Error: LoggingError +/// {} - No error as LoggingError is empty. +/// fn logCallback(context: void, str: []const u8) LoggingError!usize { serial.writeBytes(str); return str.len; @@ -27,8 +42,10 @@ fn logCallback(context: void, str: []const u8) LoggingError!usize { /// Write a message to the log output stream with a certain logging level. /// /// Arguments: -/// IN comptime level: Level - The logging level to use. Determines the message prefix and whether it is filtered. -/// IN comptime format: []const u8 - The message format. Uses the standard format specification options. +/// IN comptime level: Level - The logging level to use. Determines the message prefix and +/// whether it is filtered. +/// IN comptime format: []const u8 - The message format. Uses the standard format specification +/// options. /// IN args: var - A struct of the parameters for the format string. /// pub fn log(comptime level: Level, comptime format: []const u8, args: var) void { @@ -39,7 +56,8 @@ pub fn log(comptime level: Level, comptime format: []const u8, args: var) void { /// Write a message to the log output stream with the INFO level. /// /// Arguments: -/// IN comptime format: []const u8 - The message format. Uses the standard format specification options. +/// IN comptime format: []const u8 - The message format. Uses the standard format specification +/// options. /// IN args: var - A struct of the parameters for the format string. /// pub fn logInfo(comptime format: []const u8, args: var) void { @@ -50,7 +68,8 @@ pub fn logInfo(comptime format: []const u8, args: var) void { /// Write a message to the log output stream with the DEBUG level. /// /// Arguments: -/// IN comptime format: []const u8 - The message format. Uses the standard format specification options. +/// IN comptime format: []const u8 - The message format. Uses the standard format specification +/// options. /// IN args: var - A struct of the parameters for the format string. /// pub fn logDebug(comptime format: []const u8, args: var) void { @@ -61,7 +80,8 @@ pub fn logDebug(comptime format: []const u8, args: var) void { /// Write a message to the log output stream with the WARNING level. /// /// Arguments: -/// IN comptime format: []const u8 - The message format. Uses the standard format specification options. +/// IN comptime format: []const u8 - The message format. Uses the standard format specification +/// options. /// IN args: var - A struct of the parameters for the format string. /// pub fn logWarning(comptime format: []const u8, args: var) void { @@ -72,7 +92,8 @@ pub fn logWarning(comptime format: []const u8, args: var) void { /// Write a message to the log output stream with the ERROR level. /// /// Arguments: -/// IN comptime format: []const u8 - The message format. Uses the standard format specification options. +/// IN comptime format: []const u8 - The message format. Uses the standard format specification +/// options. /// IN args: var - A struct of the parameters for the format string. /// pub fn logError(comptime format: []const u8, args: var) void { @@ -88,9 +109,15 @@ pub fn logError(comptime format: []const u8, args: var) void { pub fn init(ser: Serial) void { serial = ser; - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } +/// +/// The logging runtime tests that will test all logging levels. +/// pub fn runtimeTests() void { inline for (@typeInfo(Level).Enum.fields) |field| { const level = @field(Level, field.name); diff --git a/src/kernel/mem.zig b/src/kernel/mem.zig index ebc735d..98bfd69 100644 --- a/src/kernel/mem.zig +++ b/src/kernel/mem.zig @@ -46,7 +46,7 @@ pub const MemProfile = struct { /// The virtual regions of reserved memory. Should not include what is tracked by the vaddr_* fields but should include the regions occupied by the modules. These are reserved and mapped by the VMM virtual_reserved: []Map, - /// The phsyical regions of reserved memory. Should not include what is tracked by the physaddr_* fields but should include the regions occupied by the modules. These are reserved by the PMM + /// The physical regions of reserved memory. Should not include what is tracked by the physaddr_* fields but should include the regions occupied by the modules. These are reserved by the PMM physical_reserved: []Range, /// The allocator to use before a heap can be set up. diff --git a/src/kernel/panic.zig b/src/kernel/panic.zig index 70842df..8c61322 100644 --- a/src/kernel/panic.zig +++ b/src/kernel/panic.zig @@ -5,6 +5,7 @@ const arch = @import("arch.zig").internals; const log = @import("log.zig"); const multiboot = @import("multiboot.zig"); const mem = @import("mem.zig"); +const build_options = @import("build_options"); const ArrayList = std.ArrayList; const testing = std.testing; @@ -15,7 +16,7 @@ const PanicError = error{ InvalidSymbolFile, }; -/// An entry within a symbol map. Corresponds to one entry in a symbole file +/// An entry within a symbol map. Corresponds to one entry in a symbol file const MapEntry = struct { /// The address that the entry corresponds to addr: usize, @@ -59,7 +60,7 @@ const SymbolMap = struct { /// Error: std.mem.Allocator.Error /// * - See ArrayList.append /// - pub fn add(self: *SymbolMap, name: []const u8, addr: u32) !void { + pub fn add(self: *SymbolMap, name: []const u8, addr: usize) !void { try self.addEntry(MapEntry{ .addr = addr, .func_name = name }); } @@ -130,7 +131,7 @@ pub fn panic(trace: ?*builtin.StackTrace, comptime format: []const u8, args: var /// whitespace character. /// /// Arguments: -/// INOUT ptr: *[*]const u8 - The address at which to start looking, updated after all +/// IN/OUT ptr: *[*]const u8 - The address at which to start looking, updated after all /// characters have been consumed. /// IN end: *const u8 - The end address at which to start looking. A whitespace character must /// be found before this. @@ -166,7 +167,9 @@ fn parseAddr(ptr: *[*]const u8, end: *const u8) !usize { /// PanicError.InvalidSymbolFile: The address given is greater than or equal to the end address. /// fn parseChar(ptr: [*]const u8, end: *const u8) PanicError!u8 { - if (@ptrToInt(ptr) >= @ptrToInt(end)) return PanicError.InvalidSymbolFile; + if (@ptrToInt(ptr) >= @ptrToInt(end)) { + return PanicError.InvalidSymbolFile; + } return ptr[0]; } @@ -219,7 +222,7 @@ fn parseNonWhitespace(ptr: [*]const u8, end: *const u8) PanicError![*]const u8 { /// character. /// /// Arguments: -/// INOUT ptr: *[*]const u8 - The address at which to start looking, updated after all +/// IN/OUT ptr: *[*]const u8 - The address at which to start looking, updated after all /// characters have been consumed. /// IN end: *const u8 - The end address at which to start looking. A whitespace character must /// be found before this. @@ -242,7 +245,7 @@ fn parseName(ptr: *[*]const u8, end: *const u8) PanicError![]const u8 { /// in the format of '\d+\w+[a-zA-Z0-9]+'. Must be terminated by a whitespace character. /// /// Arguments: -/// INOUT ptr: *[*]const u8 - The address at which to start looking, updated once after the +/// IN/OUT ptr: *[*]const u8 - The address at which to start looking, updated once after the /// address has been consumed and once again after the name has been consumed. /// IN end: *const u8 - The end address at which to start looking. A whitespace character must /// be found before this. @@ -281,8 +284,9 @@ pub fn init(mem_profile: *const mem.MemProfile, allocator: *std.mem.Allocator) ! defer log.logInfo("Done panic\n", .{}); // Exit if we haven't loaded all debug modules - if (mem_profile.modules.len < 1) + if (mem_profile.modules.len < 1) { return; + } var kmap_start: usize = 0; var kmap_end: usize = 0; for (mem_profile.modules) |module| { @@ -296,8 +300,9 @@ pub fn init(mem_profile: *const mem.MemProfile, allocator: *std.mem.Allocator) ! } // Don't try to load the symbols if there was no symbol map file. This is a valid state so just // exit early - if (kmap_start == 0 or kmap_end == 0) + if (kmap_start == 0 or kmap_end == 0) { return; + } var syms = SymbolMap.init(allocator); errdefer syms.deinit(); @@ -307,6 +312,11 @@ pub fn init(mem_profile: *const mem.MemProfile, allocator: *std.mem.Allocator) ! try syms.addEntry(entry); } symbol_map = syms; + + switch (build_options.test_mode) { + .Panic => runtimeTests(), + else => {}, + } } test "parseChar" { @@ -441,7 +451,13 @@ test "SymbolMap" { testing.expectEqual(map.search(2345), "jkl"); } +/// +/// Runtime test for panic. This will trigger a integer overflow. +/// pub fn runtimeTests() void { + @setRuntimeSafety(true); var x: u8 = 255; x += 1; + // If we get here, then a panic was not triggered so fail + panic(@errorReturnTrace(), "FAILURE: No integer overflow\n", .{}); } diff --git a/src/kernel/pmm.zig b/src/kernel/pmm.zig index 5058029..732d1f3 100644 --- a/src/kernel/pmm.zig +++ b/src/kernel/pmm.zig @@ -120,43 +120,12 @@ pub fn init(mem: *const MemProfile, allocator: *std.mem.Allocator) void { } } - if (build_options.rt_test) { - runtimeTests(mem, allocator); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(mem, allocator), + else => {}, } } -/// -/// Allocate all blocks and make sure they don't overlap with any reserved addresses. -/// -/// Arguments: -/// IN mem: *const MemProfile - The memory profile to check for reserved memory regions. -/// INOUT allocator: *std.mem.Allocator - The allocator to use when needing to create intermediate structures used for testing -/// -fn runtimeTests(mem: *const MemProfile, allocator: *std.mem.Allocator) void { - // Make sure that occupied memory can't be allocated - var prev_alloc: usize = std.math.maxInt(usize); - var alloc_list = std.ArrayList(usize).init(allocator); - defer alloc_list.deinit(); - while (alloc()) |alloced| { - if (prev_alloc == alloced) { - panic(null, "PMM allocated the same address twice: 0x{x}", .{alloced}); - } - prev_alloc = alloced; - for (mem.physical_reserved) |entry| { - var addr = std.mem.alignBackward(entry.start, BLOCK_SIZE); - if (addr == alloced) { - panic(null, "PMM allocated an address that should be reserved by the memory map: 0x{x}", .{addr}); - } - } - alloc_list.append(alloced) catch |e| panic(@errorReturnTrace(), "Failed to add PMM allocation to list: {}", .{e}); - } - // Clean up - for (alloc_list.items) |alloced| { - free(alloced) catch |e| panic(@errorReturnTrace(), "Failed freeing allocation in PMM rt test: {}", .{e}); - } - log.logInfo("PMM: Tested allocation\n", .{}); -} - test "alloc" { bitmap = try Bitmap(u32).init(32, std.heap.page_allocator); comptime var addr = 0; @@ -227,3 +196,35 @@ test "setAddr and isSet" { } } } + +/// +/// Allocate all blocks and make sure they don't overlap with any reserved addresses. +/// +/// Arguments: +/// IN mem: *const MemProfile - The memory profile to check for reserved memory regions. +/// IN/OUT allocator: *std.mem.Allocator - The allocator to use when needing to create intermediate structures used for testing +/// +fn runtimeTests(mem: *const MemProfile, allocator: *std.mem.Allocator) void { + // Make sure that occupied memory can't be allocated + var prev_alloc: usize = std.math.maxInt(usize); + var alloc_list = std.ArrayList(usize).init(allocator); + defer alloc_list.deinit(); + while (alloc()) |alloced| { + if (prev_alloc == alloced) { + panic(null, "FAILURE: PMM allocated the same address twice: 0x{x}", .{alloced}); + } + prev_alloc = alloced; + for (mem.physical_reserved) |entry| { + var addr = std.mem.alignBackward(@intCast(usize, entry.start), BLOCK_SIZE); + if (addr == alloced) { + panic(null, "FAILURE: PMM allocated an address that should be reserved by the memory map: 0x{x}", .{addr}); + } + } + alloc_list.append(alloced) catch |e| panic(@errorReturnTrace(), "FAILURE: Failed to add PMM allocation to list: {}", .{e}); + } + // Clean up + for (alloc_list.items) |alloced| { + free(alloced) catch |e| panic(@errorReturnTrace(), "FAILURE: Failed freeing allocation in PMM rt test: {}", .{e}); + } + log.logInfo("PMM: Tested allocation\n", .{}); +} diff --git a/src/kernel/serial.zig b/src/kernel/serial.zig index 7cabc5e..2ee6ea7 100644 --- a/src/kernel/serial.zig +++ b/src/kernel/serial.zig @@ -31,7 +31,10 @@ pub const Serial = struct { /// pub fn init(boot_payload: arch.BootPayload) Serial { const serial = arch.initSerial(boot_payload); - if (build_options.rt_test) runtimeTests(serial); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(serial), + else => {}, + } return serial; } diff --git a/src/kernel/vmm.zig b/src/kernel/vmm.zig index c27ee67..7016df2 100644 --- a/src/kernel/vmm.zig +++ b/src/kernel/vmm.zig @@ -64,7 +64,7 @@ pub fn Mapper(comptime Payload: type) type { /// IN physical_start: usize - The start of the physical memory to map to /// IN physical_end: usize - The end of the physical memory to map to /// IN attrs: Attributes - The attributes to apply to this region of memory - /// INOUT allocator: std.mem.Allocator - The allocator to use when mapping, if required + /// IN/OUT allocator: std.mem.Allocator - The allocator to use when mapping, if required /// IN spec: Payload - The payload to pass to the mapper /// /// Error: std.mem.AllocatorError || MapperError @@ -73,7 +73,7 @@ pub fn Mapper(comptime Payload: type) type { mapFn: fn (virtual_start: usize, virtual_end: usize, physical_start: usize, physical_end: usize, attrs: Attributes, allocator: *std.mem.Allocator, spec: Payload) (std.mem.Allocator.Error || MapperError)!void, /// - /// Unmap a region (can span more than one block) of virtual memory from its physical memory. After a call to this function, the memory should not be accesible without error. + /// Unmap a region (can span more than one block) of virtual memory from its physical memory. After a call to this function, the memory should not be accessible without error. /// /// Arguments: /// IN virtual_start: usize - The start of the virtual region to unmap @@ -148,7 +148,7 @@ pub fn VirtualMemoryManager(comptime Payload: type) type { /// Arguments: /// IN start: usize - The start of the memory region to manage /// IN end: usize - The end of the memory region to manage. Must be greater than the start - /// INOUT allocator: *std.mem.Allocator - The allocator to use when allocating and freeing regions + /// IN/OUT allocator: *std.mem.Allocator - The allocator to use when allocating and freeing regions /// IN mapper: Mapper - The mapper to use when allocating and freeing regions /// IN payload: Payload - The payload data to be passed to the mapper /// @@ -193,13 +193,13 @@ pub fn VirtualMemoryManager(comptime Payload: type) type { /// Map a region (can span more than one block) of virtual memory to a specific region of memory /// /// Arguments: - /// INOUT self: *Self - The manager to modify + /// IN/OUT self: *Self - The manager to modify /// IN virtual: mem.Range - The virtual region to set /// IN physical: ?mem.Range - The physical region to map to or null if only the virtual region is to be set /// IN attrs: Attributes - The attributes to apply to the memory regions /// /// Error: VmmError || Bitmap(u32).BitmapError || std.mem.Allocator.Error || MapperError - /// VmmError.AlreadyAllocated - The virtual address has arlready been allocated + /// VmmError.AlreadyAllocated - The virtual address has already been allocated /// VmmError.PhysicalAlreadyAllocated - The physical address has already been allocated /// VmmError.PhysicalVirtualMismatch - The physical region and virtual region are of different sizes /// VmmError.InvalidVirtAddresses - The start virtual address is greater than the end address @@ -256,7 +256,7 @@ pub fn VirtualMemoryManager(comptime Payload: type) type { /// Allocate a number of contiguous blocks of virtual memory /// /// Arguments: - /// INOUT self: *Self - The manager to allocate for + /// IN/OUT self: *Self - The manager to allocate for /// IN num: usize - The number of blocks to allocate /// IN attrs: Attributes - The attributes to apply to the mapped memory /// @@ -298,7 +298,7 @@ pub fn VirtualMemoryManager(comptime Payload: type) type { /// Free a previous allocation /// /// Arguments: - /// INOUT self: *Self - The manager to free within + /// IN/OUT self: *Self - The manager to free within /// IN vaddr: usize - The start of the allocation to free. This should be the address returned from a prior `alloc` call /// /// Error: Bitmap.BitmapError || VmmError @@ -316,12 +316,12 @@ pub fn VirtualMemoryManager(comptime Payload: type) type { for (physical.items) |block, i| { // Clear the address space entry, unmap the virtual memory and free the physical memory try self.bmp.clearEntry(entry + i); - pmm.free(block) catch |e| panic(@errorReturnTrace(), "Failed to free PMM reserved memory at {x}: {}\n", .{ block * BLOCK_SIZE, e }); + pmm.free(block) catch |e| panic(@errorReturnTrace(), "Failed to free PMM reserved memory at 0x{X}: {}\n", .{ block * BLOCK_SIZE, e }); } // Unmap the entire range const region_start = entry * BLOCK_SIZE; const region_end = (entry + num_physical_allocations) * BLOCK_SIZE; - self.mapper.unmapFn(region_start, region_end, self.payload) catch |e| panic(@errorReturnTrace(), "Failed to unmap VMM reserved memory from {x} to {x}: {}\n", .{ region_start, region_end, e }); + self.mapper.unmapFn(region_start, region_end, self.payload) catch |e| panic(@errorReturnTrace(), "Failed to unmap VMM reserved memory from 0x{X} to 0x{X}: {}\n", .{ region_start, region_end, e }); // The allocation is freed so remove from the map self.allocations.removeAssertDiscard(vaddr); } else { @@ -336,7 +336,7 @@ pub fn VirtualMemoryManager(comptime Payload: type) type { /// /// Arguments: /// IN mem_profile: *const mem.MemProfile - The system's memory profile. This is used to find the kernel code region and boot modules -/// INOUT allocator: *std.mem.Allocator - The allocator to use when needing to allocate memory +/// IN/OUT allocator: *std.mem.Allocator - The allocator to use when needing to allocate memory /// /// Return: VirtualMemoryManager /// The virtual memory manager created with all reserved virtual regions allocated @@ -367,7 +367,10 @@ pub fn init(mem_profile: *const mem.MemProfile, allocator: *std.mem.Allocator) s }; } - if (build_options.rt_test) runtimeTests(arch.VmmPayload, vmm, mem_profile); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(arch.VmmPayload, vmm, mem_profile), + else => {}, + } return vmm; } @@ -391,7 +394,7 @@ test "alloc and free" { std.testing.expectEqual(@as(?usize, null), result); should_be_set = false; } else { - // Else it should have succedded and allocated the correct address + // Else it should have succeeded and allocated the correct address std.testing.expectEqual(@as(?usize, vmm.start + entry * BLOCK_SIZE), result); try virtual_allocations.append(result orelse unreachable); } @@ -528,7 +531,7 @@ fn testInit(num_entries: u32) std.mem.Allocator.Error!VirtualMemoryManager(u8) { /// IN pstart: usize - The start of the physical region to map /// IN pend: usize - The end of the physical region to map /// IN attrs: Attributes - The attributes to map with -/// INOUT allocator: *std.mem.Allocator - The allocator to use. Ignored +/// IN/OUT allocator: *std.mem.Allocator - The allocator to use. Ignored /// IN payload: u8 - The payload value. Expected to be 39 /// fn testMap(vstart: usize, vend: usize, pstart: usize, pend: usize, attrs: Attributes, allocator: *std.mem.Allocator, payload: u8) (std.mem.Allocator.Error || MapperError)!void { diff --git a/test/kernel/arch/x86/rt-test.py b/test/kernel/arch/x86/rt-test.py deleted file mode 100644 index eb82bf9..0000000 --- a/test/kernel/arch/x86/rt-test.py +++ /dev/null @@ -1,44 +0,0 @@ -def get_test_cases(TestCase): - return [ - TestCase("GDT init", [r"Init gdt"]), - TestCase("GDT tests", [r"GDT: Tested loading GDT"]), - TestCase("GDT done", [r"Done gdt"]), - - TestCase("IDT init", [r"Init idt"]), - TestCase("IDT tests", [r"IDT: Tested loading IDT"]), - TestCase("IDT done", [r"Done idt"]), - - TestCase("PIC init", [r"Init pic"]), - TestCase("PIC tests", [r"PIC: Tested masking"]), - TestCase("PIC done", [r"Done pic"]), - - TestCase("ISR init", [r"Init isr"]), - TestCase("ISR tests", [r"ISR: Tested registered handlers", r"ISR: Tested opened IDT entries"]), - TestCase("ISR done", [r"Done isr"]), - - TestCase("IRQ init", [r"Init irq"]), - TestCase("IRQ tests", [r"IRQ: Tested registered handlers", r"IRQ: Tested opened IDT entries"]), - TestCase("IRQ done", [r"Done irq"]), - - TestCase("Paging init", [r"Init paging"]), - TestCase("Paging tests", [r"Paging: Tested accessing unmapped memory", r"Paging: Tested accessing mapped memory"]), - TestCase("Paging done", [r"Done paging"]), - - TestCase("PIT init", [r"Init pit"]), - TestCase("PIT init", [r".+"], r"\[DEBUG\] "), - TestCase("PIT tests", [r"PIT: Tested init", r"PIT: Tested wait ticks", r"PIT: Tested wait ticks 2"]), - TestCase("PIT done", [r"Done pit"]), - - TestCase("RTC init", [r"Init rtc"]), - TestCase("RTC tests", [r"RTC: Tested init", r"RTC: Tested interrupts"]), - TestCase("RTC done", [r"Done rtc"]), - - TestCase("Syscalls init", [r"Init syscalls"]), - TestCase("Syscalls tests", [r"Syscalls: Tested no args", r"Syscalls: Tested 1 arg", r"Syscalls: Tested 2 args", r"Syscalls: Tested 3 args", r"Syscalls: Tested 4 args", r"Syscalls: Tested 5 args"]), - TestCase("Syscalls done", [r"Done syscalls"]), - TestCase("VGA init", [r"Init vga"]), - TestCase("VGA tests", [r"VGA: Tested max scan line", r"VGA: Tested cursor shape", r"VGA: Tested updating cursor"]), - TestCase("VGA done", [r"Done vga"]), - TestCase("TTY tests", [r"TTY: Tested globals", r"TTY: Tested printing"]), - - ] diff --git a/test/mock/kernel/mock_framework.zig b/test/mock/kernel/mock_framework.zig index 6c7da66..833b7c1 100644 --- a/test/mock/kernel/mock_framework.zig +++ b/test/mock/kernel/mock_framework.zig @@ -639,7 +639,7 @@ pub fn freeTest() void { /// IN/OUT self: *Self - Self. This is the mocking object to be modified to add /// the test parameters. /// IN fun_name: []const u8 - The function name to add the test parameters to. -/// IN params: arglist - The parameters to add. +/// IN params: var - The parameters to add. /// pub fn addTestParams(comptime fun_name: []const u8, params: var) void { var mock_obj = getMockObject(); diff --git a/test/rt-test.py b/test/rt-test.py deleted file mode 100644 index a79231a..0000000 --- a/test/rt-test.py +++ /dev/null @@ -1,118 +0,0 @@ -import atexit -import queue -import threading -import subprocess -import signal -import re -import sys -import datetime -import os -import importlib.util - -msg_queue = queue.Queue(-1) -proc = None - -class TestCase: - def __init__(self, name, expected, prefix=r"\[INFO\] "): - self.name = name - self.expected = expected - self.prefix = prefix - -def failure(msg): - print("FAILURE: %s" %(msg)) - sys.exit(1) - -def test_failure(case, exp, expected_idx, found): - failure("%s #%d, expected '%s', found '%s'" %(case.name, expected_idx + 1, exp, found)) - -def test_pass(case, exp, expected_idx, found): - print("PASS: %s #%d, expected '%s', found '%s'" %(case.name, expected_idx + 1, exp, found)) - -def get_pre_archinit_cases(): - return [ - TestCase("Serial tests", [r"c", r"123"], ""), - - TestCase("Log info tests", [r"Test INFO level", r"Test INFO level with args a, 1", r"Test INFO function", r"Test INFO function with args a, 1"], r"\[INFO\] "), - TestCase("Log debug tests", [r"Test DEBUG level", r"Test DEBUG level with args a, 1", r"Test DEBUG function", r"Test DEBUG function with args a, 1"], r"\[DEBUG\] "), - TestCase("Log warning tests", [r"Test WARNING level", r"Test WARNING level with args a, 1", r"Test WARNING function", r"Test WARNING function with args a, 1"], r"\[WARNING\] "), - TestCase("Log error tests", [r"Test ERROR level", r"Test ERROR level with args a, 1", r"Test ERROR function", r"Test ERROR function with args a, 1"], r"\[ERROR\] "), - - TestCase("Mem init", [r"Init mem"]), - TestCase("Mem done", [r"Done mem"]), - - TestCase("Panic init", [r"Init panic"]), - TestCase("Panic done", [r"Done panic"]), - - TestCase("PMM init", [r"Init pmm"]), - TestCase("PMM tests", [r"PMM: Tested allocation"]), - TestCase("PMM done", [r"Done pmm"]), - - TestCase("VMM init", [r"Init vmm"]), - TestCase("VMM tests", [r"VMM: Tested allocations"]), - TestCase("VMM done", [r"Done vmm"]), - TestCase("Arch init starts", [r"Init arch \w+"]) - ] - -def get_post_archinit_cases(): - return [ - TestCase("Arch init finishes", [r"Arch init done"]), - - TestCase("Heap", [r"Init heap", r"Done heap"]), - - TestCase("TTY init", [r"Init tty"]), - TestCase("TTY done", [r"Done tty"]), - - TestCase("Init finishes", [r"Init done"]), - - TestCase("Panic tests", [r"Kernel panic: integer overflow", r"c[a-z\d]+: panic", r"c[a-z\d]+: panic.runtimeTests", r"c[a-z\d]+: kmain", r"c[a-z\d]+: start_higher_half"], r"\[ERROR\] ") - ] - -def read_messages(proc): - while True: - line = proc.stdout.readline().decode("utf-8") - msg_queue.put(line) - -def cleanup(): - global proc - os.killpg(os.getpgid(proc.pid), signal.SIGTERM) - -if __name__ == "__main__": - arch = sys.argv[1] - zig_path = sys.argv[2] - spec = importlib.util.spec_from_file_location("arch", "test/kernel/arch/" + arch + "/rt-test.py") - arch_module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(arch_module) - - # The list of log statements to look for before arch init is called + - # All log statements to look for, including the arch-specific ones + - # The list of log statements to look for after arch init is called - cases = get_pre_archinit_cases() + arch_module.get_test_cases(TestCase) + get_post_archinit_cases() - - if len(cases) > 0: - proc = subprocess.Popen(zig_path + " build run -Drt-test=true -Darch=" + arch, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setsid) - atexit.register(cleanup) - case_idx = 0 - read_thread = threading.Thread(target=read_messages, args=(proc,)) - read_thread.daemon = True - read_thread.start() - # Go through the cases - while case_idx < len(cases): - case = cases[case_idx] - expected_idx = 0 - # Go through the expected log messages - while expected_idx < len(case.expected): - e = case.prefix + case.expected[expected_idx] - try: - line = msg_queue.get(block=True, timeout=5) - except queue.Empty: - failure("Timed out waiting for '%s'" %(e)) - line = line.strip() - pattern = re.compile(e) - # Pass if the line matches the expected pattern, else fail - if pattern.fullmatch(line): - test_pass(case, e, expected_idx, line) - else: - test_failure(case, e, expected_idx, line) - expected_idx += 1 - case_idx += 1 - sys.exit(0) diff --git a/test/runtime_test.zig b/test/runtime_test.zig new file mode 100644 index 0000000..bef9918 --- /dev/null +++ b/test/runtime_test.zig @@ -0,0 +1,277 @@ +const std = @import("std"); +const ChildProcess = std.ChildProcess; +const Thread = std.Thread; +const Allocator = std.mem.Allocator; +const Builder = std.build.Builder; +const Step = std.build.Step; +const Queue = std.atomic.Queue([]const u8); +const Node = std.TailQueue([]const u8).Node; + +// Creating a new runtime test: +// 1. Add a enum to `TestMode`. The name should try to describe the test in one word :P +// 2. Add a description for the new runtime test to explain to the use what this will test. +// 3. Create a function with in the RuntimeStep struct that will perform the test. At least this +// should use `self.get_msg()` which will get the serial log lines from the OS. Look at +// test_init or test_panic for examples. +// 4. In the create function, add your test mode and test function to the switch. +// 5. Celebrate if it works lel + +/// The enumeration of tests with all the runtime tests. +pub const TestMode = enum { + /// This is for the default test mode. This will just run the OS normally. + None, + + /// Run the OS's initialisation runtime tests to ensure the OS is properly set up. + Initialisation, + + /// Run the panic runtime test. + Panic, + + /// + /// Return a string description for the test mode provided. + /// + /// Argument: + /// IN mode: TestMode - The test mode. + /// + /// Return: []const u8 + /// The string description for the test mode. + /// + pub fn getDescription(mode: TestMode) []const u8 { + return switch (mode) { + .None => "Runs the OS normally (Default)", + .Initialisation => "Initialisation runtime tests", + .Panic => "Panic runtime tests", + }; + } +}; + +/// The runtime step for running the runtime tests for the OS. +pub const RuntimeStep = struct { + /// The Step, that is all you need to know + step: Step, + + /// The builder pointer, also all you need to know + builder: *Builder, + + /// The message queue that stores the log lines + msg_queue: Queue, + + /// The qemu process, this is needed for the `read_logs` thread. + os_proc: *ChildProcess, + + /// The argv of the qemu process so can create the qemu process + argv: [][]const u8, + + /// The test function that will be run for the current runtime test. + test_func: TestFn, + + /// The error set for the RuntimeStep + const Error = error{ + /// The error for if a test fails. If the test function returns false, this will be thrown + /// at the wnd of the make function as we need to clean up first. This will ensure the + /// build fails. + TestFailed, + + /// This is used for `self.get_msg()` when the queue is empty after a timeout. + QueueEmpty, + }; + + /// The type of the test function. + const TestFn = fn (self: *RuntimeStep) bool; + + /// The time used for getting message from the message queue. This is in milliseconds. + const queue_timeout: usize = 5000; + + /// + /// This will just print all the serial logs. + /// + /// Arguments: + /// IN/OUT self: *RuntimeStep - Self. + /// + /// Return: bool + /// This will always return true + /// + fn print_logs(self: *RuntimeStep) bool { + while (true) { + const msg = self.get_msg() catch return true; + defer self.builder.allocator.free(msg); + std.debug.warn("{}\n", .{msg}); + } + } + + /// + /// This tests the OS is initialised correctly by checking that we get a `SUCCESS` at the end. + /// + /// Arguments: + /// IN/OUT self: *RuntimeStep - Self. + /// + /// Return: bool + /// Whether the test has passed or failed. + /// + fn test_init(self: *RuntimeStep) bool { + while (true) { + const msg = self.get_msg() catch return false; + defer self.builder.allocator.free(msg); + // Print the line to see what is going on + std.debug.warn("{}\n", .{msg}); + if (std.mem.startsWith(u8, msg, "[ERROR] FAILURE")) { + return false; + } else if (std.mem.eql(u8, msg, "[INFO] SUCCESS")) { + return true; + } + } + } + + /// + /// This tests the OS's panic by checking that we get a kernel panic for integer overflow. + /// + /// Arguments: + /// IN/OUT self: *RuntimeStep - Self. + /// + /// Return: bool + /// Whether the test has passed or failed. + /// + fn test_panic(self: *RuntimeStep) bool { + while (true) { + const msg = self.get_msg() catch return false; + defer self.builder.allocator.free(msg); + // Print the line to see what is going on + std.debug.warn("{}\n", .{msg}); + if (std.mem.eql(u8, msg, "[ERROR] Kernel panic: integer overflow")) { + return true; + } + } + } + + /// + /// The make function that is called by the builder. This will create the qemu process with the + /// stdout as a Pipe. Then create the read thread to read the logs from the qemu stdout. Then + /// will call the test function to test a specifics part of the OS defined by the test mode. + /// + /// Arguments: + /// IN/OUT step: *Step - The step of this step. + /// + /// Error: Thread.SpawnError || ChildProcess.SpawnError || Allocator.Error || Error + /// Thread.SpawnError - If there is an error spawning the real logs thread. + /// ChildProcess.SpawnError - If there is an error spawning the qemu process. + /// Allocator.Error.OutOfMemory - If there is no more memory to allocate. + /// Error.TestFailed - The error if the test failed. + /// + fn make(step: *Step) (Thread.SpawnError || ChildProcess.SpawnError || Allocator.Error || Error)!void { + const self = @fieldParentPtr(RuntimeStep, "step", step); + + // Create the qemu process + self.os_proc = try ChildProcess.init(self.argv, self.builder.allocator); + defer self.os_proc.deinit(); + + self.os_proc.stdout_behavior = .Pipe; + self.os_proc.stdin_behavior = .Inherit; + self.os_proc.stderr_behavior = .Inherit; + + try self.os_proc.spawn(); + + // Start up the read thread + var thread = try Thread.spawn(self, read_logs); + + // Call the testing function + const res = self.test_func(self); + + // Now kill our baby + _ = try self.os_proc.kill(); + + // Join the thread + thread.wait(); + + // Free the rest of the queue + while (self.msg_queue.get()) |node| { + self.builder.allocator.free(node.data); + self.builder.allocator.destroy(node); + } + + // If the test function returns false, then fail the build + if (!res) { + return Error.TestFailed; + } + } + + /// + /// This is to only be used in the read logs thread. This reads the stdout of the qemu process + /// and stores each line in the queue. + /// + /// Arguments: + /// IN/OUT self: *RuntimeStep - Self. + /// + fn read_logs(self: *RuntimeStep) void { + const stream = self.os_proc.stdout.?.reader(); + // Line shouldn't be longer than this + const max_line_length: usize = 128; + while (true) { + const line = stream.readUntilDelimiterAlloc(self.builder.allocator, '\n', max_line_length) catch |e| switch (e) { + error.EndOfStream => { + // When the qemu process closes, this will return a EndOfStream, so can catch and return so then can + // join the thread to exit nicely :) + return; + }, + else => unreachable, + }; + + // put line in the queue + var node = self.builder.allocator.create(Node) catch unreachable; + node.* = Node.init(line); + self.msg_queue.put(node); + } + } + + /// + /// This return a log message from the queue in the order it would appear in the qemu process. + /// The line will need to be free with allocator.free(line) then finished with the line. + /// + /// Arguments: + /// IN/OUT self: *RuntimeStep - Self. + /// + /// Return: []const u8 + /// A log line from the queue. + /// + /// Error: Error + /// error.QueueEmpty - If the queue is empty for more than the timeout, this will be thrown. + /// + fn get_msg(self: *RuntimeStep) Error![]const u8 { + var i: usize = 0; + while (i < queue_timeout) : (i += 1) { + if (self.msg_queue.get()) |node| { + defer self.builder.allocator.destroy(node); + return node.data; + } + std.time.sleep(std.time.ns_per_ms); + } + return Error.QueueEmpty; + } + + /// + /// Create a runtime step with a specific test mode. + /// + /// Argument: + /// IN builder: *Builder - The builder. This is used for the allocator. + /// IN test_mode: TestMode - The test mode. + /// IN qemu_args: [][]const u8 - The qemu arguments used to create the OS process. + /// + /// Return: *RuntimeStep + /// The Runtime step pointer to add to the build process. + /// + pub fn create(builder: *Builder, test_mode: TestMode, qemu_args: [][]const u8) *RuntimeStep { + const runtime_step = builder.allocator.create(RuntimeStep) catch unreachable; + runtime_step.* = RuntimeStep{ + .step = Step.init(.Custom, builder.fmt("Runtime {}", .{@tagName(test_mode)}), builder.allocator, make), + .builder = builder, + .msg_queue = Queue.init(), + .os_proc = undefined, + .argv = qemu_args, + .test_func = switch (test_mode) { + .None => print_logs, + .Initialisation => test_init, + .Panic => test_panic, + }, + }; + return runtime_step; + } +};