diff --git a/.cargo/config b/.cargo/config index 7ab7cc1e2116a6e9b6c50a42a6b10bd4e12a09a2..9c2c57449d1956a8453a5eadffca45a9427986f2 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,2 +1,5 @@ +#[target.x86_64-unknown-hermit-kernel] +#runner = "uhyve -v" + [target.x86_64-unknown-hermit-kernel] -runner = "uhyve -v" +runner = "tests/hermit_test_runner.py" diff --git a/Cargo.toml b/Cargo.toml index 31fe436a7bf330c23976049f8736d23bc2734f81..474ec4ef12f1b3691f65339673fa7de66b20577c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,14 @@ travis-ci = { repository = "hermitcore/libhermit-rs" } crate-type = ["staticlib"] name = "hermit" +[[test]] +name = "basic_print" +harness = false + +[[test]] +name = "basic_math" +harness = false + [features] default = ["pci", "acpi"] vga = [] diff --git a/src/lib.rs b/src/lib.rs index 8c102b1f5bdfeb594cd16f849b22c604273e8651..4ab43587ec8a1e4e8245380263914c3e0ac68ebb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,7 @@ #![feature(allocator_api)] #![feature(const_btree_new)] #![feature(const_fn)] +#![feature(custom_test_frameworks)] #![feature(global_asm)] #![feature(lang_items)] #![feature(linkage)] @@ -39,6 +40,9 @@ #![feature(core_intrinsics)] #![feature(alloc_error_handler)] #![allow(unused_macros)] +#![test_runner(crate::test_runner)] +#![reexport_test_harness_main = "test_main"] +#![cfg_attr(test, no_main)] #![no_std] // EXTERNAL CRATES @@ -62,6 +66,7 @@ extern crate x86; use alloc::alloc::Layout; use core::alloc::GlobalAlloc; +use core::panic::PanicInfo; use core::sync::atomic::{spin_loop_hint, AtomicU32, Ordering}; use arch::percore::*; @@ -95,6 +100,20 @@ mod synch; mod syscalls; mod util; +#[doc(hidden)] +pub fn _print(args: ::core::fmt::Arguments) { + use core::fmt::Write; + crate::console::CONSOLE.lock().write_fmt(args).unwrap(); +} + +pub fn test_runner(tests: &[&dyn Fn()]) { + println!("Running {} tests", tests.len()); + for test in tests { + test(); + } + sys_exit(0); +} + #[cfg(target_os = "hermit")] #[global_allocator] static ALLOCATOR: LockedHeap = LockedHeap::empty(); diff --git a/src/macros.rs b/src/macros.rs index 214f8020c03e59658bd7a1277b48dea88e2c32f4..3d5035d09f47cc895c2041923a87d8dfc4a1fcdb 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -22,14 +22,15 @@ macro_rules! align_up { /// /// From http://blog.phil-opp.com/rust-os/printing-to-screen.html, but tweaked /// for HermitCore. +#[macro_export] macro_rules! print { ($($arg:tt)+) => ({ - use core::fmt::Write; - $crate::console::CONSOLE.lock().write_fmt(format_args!($($arg)+)).unwrap(); + $crate::_print(format_args!($($arg)*)); }); } /// Print formatted text to our console, followed by a newline. +#[macro_export] macro_rules! println { () => (print!("\n")); ($($arg:tt)+) => (print!("{}\n", format_args!($($arg)+))); diff --git a/tests/basic_math.rs b/tests/basic_math.rs new file mode 100644 index 0000000000000000000000000000000000000000..826a2e1a53bd0ad308fc9e37aafe5bd8f2e50ad6 --- /dev/null +++ b/tests/basic_math.rs @@ -0,0 +1,80 @@ +#![no_std] +#![no_main] + +extern crate hermit; +use hermit::{print, println}; + +// Workaround since the "real" runtime_entry function (defined in libstd) is not available, +// since the target-os is hermit-kernel and not hermit +#[no_mangle] +extern "C" +fn runtime_entry(argc: i32, argv: *const *const u8, _env: *const *const u8) -> ! { + let res = main(argc as isize, argv); + match res { + Ok(_) => hermit::sys_exit(0), + Err(_) => hermit::sys_exit(1), //ToDo: sys_exit exitcode doesn't seem to get passed to qemu + // sys_exit argument doesn't actually get used, gets silently dropped! + // Maybe this is not possible on QEMU? + // https://os.phil-opp.com/testing/#exiting-qemu device needed? + } +} + + + + +/* +/// assert_eq but returns Result<(),&str> instead of panicking +/// no error message possible +/// adapted from libcore assert_eq macro +macro_rules! equals { + ($left:expr, $right:expr) => ({ + match (&$left, &$right) { + (left_val, right_val) => { + if !(*left_val == *right_val) { + return Err(r#"assertion failed: `(left == right)` + left: `{:?}`, + right: `{:?}`"# &*left_val, &*right_val); + } + else { return Ok(()); } + } + } + }); + ($left:expr, $right:expr,) => ({ + $crate::assert_eq!($left, $right) + }); +} + +macro_rules! n_equals { + ($left:expr, $right:expr) => ({ + match (&$left, &$right) { + (left_val, right_val) => { + if *left_val == *right_val { + // The reborrows below are intentional. Without them, the stack slot for the + // borrow is initialized even before the values are compared, leading to a + // noticeable slow down. + return Err(r#"assertion failed: `(left == right)` + left: `{:?}`, + right: `{:?}`"#, &*left_val, &*right_val); + } + else return Ok(()); + } + } + }); + ($left:expr, $right:expr,) => { + $crate::assert_ne!($left, $right) + }; +} +*/ + +//ToDo - add a testrunner so we can group multiple similar tests + +//ToDo - Idea: pass some values into main - compute and print result to stdout +//ToDo - add some kind of assert like macro that returns a result instead of panicking, Err contains line number etc to pinpoint the issue +pub fn main(_argc: isize, _argv: *const *const u8) -> Result<(), ()>{ + let x = 25; + let y = 310; + let z = x * y; + println!("25 * 310 = {}", z); + assert_eq!(z, 7750); + Ok(()) +} diff --git a/tests/basic_print.rs b/tests/basic_print.rs new file mode 100644 index 0000000000000000000000000000000000000000..ce2096acddd6c95e9719b5e4c82fd28193320ba9 --- /dev/null +++ b/tests/basic_print.rs @@ -0,0 +1,24 @@ +#![no_std] +#![no_main] +//#![test_runner(hermit::test_runner)] +//#![feature(custom_test_frameworks)] +//#![reexport_test_harness_main = "test_main"] + +//use core::panic::PanicInfo; +extern crate hermit; +use hermit::{print, println}; + +//ToDo: Define exit code enum in hermit!!! + +// Workaround since the "real" runtime_entry function (defined in libstd) is not available, +// since the target-os is hermit-kernel and not hermit +#[no_mangle] +extern "C" fn runtime_entry(argc: i32, argv: *const *const u8, env: *const *const u8) -> ! { + main(argc as isize, argv); + hermit::sys_exit(-1); +} + +//#[test_case] +pub fn main(argc: isize, argv: *const *const u8) { + println!("hey we made it to the test function :O"); +} diff --git a/tests/hermit_test_runner.py b/tests/hermit_test_runner.py new file mode 100755 index 0000000000000000000000000000000000000000..77c899051b7b00abb4b42f757d65f0716a754de5 --- /dev/null +++ b/tests/hermit_test_runner.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +import time +import argparse +import subprocess +from subprocess import Popen, PIPE, STDOUT +import os, os.path + +SMP_CORES = 1 # Number of cores +MEMORY_MB = 64 # amount of memory +# Path if libhermit-rs was checked out via rusty-hermit repository +BOOTLOADER_PATH = '../loader/target/x86_64-unknown-hermit-loader/debug/rusty-loader' + + +# ToDo add test dependent section for custom kernel arguments / application arguments +# Idea: Use TOML format to specify things like should_panic, expected output +# Parse test executable name and check tests directory for corresponding toml file +# If it doesn't exist just assure that the return code is not a failure + +# ToDo Think about always being verbose, or hiding the output +def run_test(process_args): + print(os.getcwd()) + abs_bootloader_path = os.path.abspath(BOOTLOADER_PATH) + print("Abspath: ", abs_bootloader_path) + p = Popen(process_args, stdout=PIPE, stderr=STDOUT, text=True) + output: str = "" + for line in p.stdout: + dec_line = line + output += dec_line + #print(line, end='') # stdout will already contain line break + rc = p.wait() + # ToDo: add some timeout + return rc, output + + +def validate_test(returncode, output, test_exe_path): + print("returncode ", returncode) + # ToDo handle expected failures + if returncode != 0: + return False + # ToDo parse output for panic + return True + + +def clean_test_name(name: str): + if name.endswith('.exe'): + name = name.replace('.exe', '') + # Remove the hash from the name + parts = name.split('-') + if len(parts) > 1: + try: + _hex = int(parts[-1], base=16) # Test if last element is hex hash + clean_name = "-".join(parts[:-1]) # Rejoin with '-' as seperator in case test has it in filename + except ValueError as e: + print(e) + clean_name = name # In this case name doesn't contain a hash, so don't modify it any further + return clean_name + + +print("Test runner called") +parser = argparse.ArgumentParser(description='See documentation of cargo test runner for custom test framework') +parser.add_argument('runner_args', type=str, nargs='*') +args = parser.parse_args() +print("Arguments: {}".format(args.runner_args)) + +qemu_base_arguments = ['qemu-system-x86_64', + '-display', 'none', + '-smp', str(SMP_CORES), + '-m', str(MEMORY_MB) + 'M', + '-serial', 'stdio', + '-kernel', BOOTLOADER_PATH, + # skip initrd - it depends on test executable + '-cpu', 'qemu64,apic,fsgsbase,rdtscp,xsave,fxsr' + ] +ok_tests: int = 0 +failed_tests: int = 0 +# This is assuming test_runner only passes executable files as parameters +for arg in args.runner_args: + assert isinstance(arg, str) + curr_qemu_arguments = qemu_base_arguments.copy() + # ToDo: assert that arg is a path to an executable before calling qemu + # ToDo: Add addional test based arguments for qemu / uhyve + curr_qemu_arguments.extend(['-initrd', arg]) + rc, output = run_test(curr_qemu_arguments) + test_ok = validate_test(rc, output, arg) + test_name = os.path.basename(arg) + test_name = clean_test_name(test_name) + if test_ok: + print("Test Ok: {}".format(test_name)) + ok_tests += 1 + else: + print("Test failed: {}".format(test_name)) + failed_tests += 1 + #Todo: improve information about the test + +print("{} from {} tests successful".format(ok_tests, ok_tests + failed_tests)) +# todo print something ala x/y tests failed etc. +# maybe look at existing standards (TAP?) +# - TAP: could use tappy to convert to python style unit test output (benefit??) + +if failed_tests == 0: + exit(0) +else: + exit(1) + + + + +