Merge pull request #164 from SamTebbs33/feature/replace-python-rt-with-zig

Replace python rt with zig
This commit is contained in:
Edward Dean 2020-06-23 12:47:04 +01:00 committed by GitHub
commit 10bf2439d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 667 additions and 405 deletions

View file

@ -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 }}

View file

@ -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=<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

112
build.zig
View file

@ -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(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;
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);

View file

@ -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", .{});
}

View file

@ -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", .{});
}

View file

@ -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});
}
}
}

View file

@ -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});
}
}
}

View file

@ -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", .{});
}

View file

@ -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", .{});

View file

@ -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", .{});

View file

@ -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", .{});

View file

@ -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:

View file

@ -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 (syscall1(124, 2) == 1 and testInt == 3)
log.logInfo("Syscalls: Tested 1 arg\n", .{});
if (syscall2(125, 2, 3) == 2 and testInt == 8)
log.logInfo("Syscalls: Tested 2 args\n", .{});
if (syscall3(126, 2, 3, 4) == 3 and testInt == 17)
log.logInfo("Syscalls: Tested 3 args\n", .{});
if (syscall4(127, 2, 3, 4, 5) == 4 and testInt == 31)
log.logInfo("Syscalls: Tested 4 args\n", .{});
if (syscall5(128, 2, 3, 4, 5, 6) == 5 and testInt == 51)
log.logInfo("Syscalls: Tested 5 args\n", .{});
if (test_int != 0) {
panic(@errorReturnTrace(), "FAILURE initial test_int not 0: {}\n", .{test_int});
}
if (syscall0(123) != 0 or test_int != 1) {
panic(@errorReturnTrace(), "FAILURE syscall0\n", .{});
}
if (syscall1(124, 2) != 1 or test_int != 3) {
panic(@errorReturnTrace(), "FAILURE syscall2\n", .{});
}
if (syscall2(125, 2, 3) != 2 or test_int != 8) {
panic(@errorReturnTrace(), "FAILURE syscall2\n", .{});
}
if (syscall3(126, 2, 3, 4) != 3 or test_int != 17) {
panic(@errorReturnTrace(), "FAILURE syscall3\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", .{});
}

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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.
///

View file

@ -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();
}

View file

@ -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);

View file

@ -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.

View file

@ -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", .{});
}

View file

@ -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", .{});
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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"]),
]

View file

@ -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();

View file

@ -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)

277
test/runtime_test.zig Normal file
View file

@ -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;
}
};