本文为本人在按照GitHub博客《writing an os in rust》的时候所记录的实验资料与笔记,大部分内容来自原网站的翻译,但实验过程为本人原创。
如有需要,各位可以上源外文网站看去原文,现附上原文链接——https://os.phil-opp.com/testing/
首先,本次实验时建立在前三章的基础上进行的,需要在前三章写成的文件上进行修改,关于前三章网上有很多的翻译,这里就不进行讲解了,直接开始。
本次实验主要完成在[no_std]情况下对文件的单元测试和集成测试。
rust有一个内置的测试框架,能够进行单元测试,只需要将#[test]属性添加到函数头即可。但是,对于我们的实验,是建立在[no_std]情况下的,所以没办法调用内置的测试框架,我们需要自己编写一个。如果我们强行对当前的项目使用
cargo xtest
进行测试的时候,会看到这样的情况:
由于test是依赖于标准库的,所以我们的裸机目标不可用,当我们的test移植到[no_std]的时候,会变得非常不稳定
如果出现这样的情况:
这是因为我们使用的是校园网,无法访问GitHub网站的资源,换成流量或者其他WiFi即可。
如果虚拟机出现无法联网的情况:
请在虚拟机设置中将连接模式从NAT模式切换到桥接模式,并重启虚拟机内部的网络连接并等待。
所以,我们需要通过不稳定的custom_test_frameworks,也就是我们自己写的测试框架来代替掉默认的测试框架,这样我们就不需要外部库,也就可以在[no_std]环境中工作。
这个框架的工作方式是收集所有带有#[test_case]属性注释的函数,然后以测试列表作为参数调用用户指定的Runner函数。从而实现了对测试过程的最大控制。与默认测试框架相比,该框架的缺点是许多高级特性(如WARE_PARY测试)都不可用。相反,如果需要的话,应该由实现自己来提供这些特性。这对我们来说非常理想,因为我们有一个非常特殊的执行环境,这种高级特性的默认实现可能无论如何都无法工作。
这里我们需要修改我们的main.rs文件,增加一个如下的函数,注意,头两行要加在开头,否则会有如下错误
// in src/main.rs
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#[cfg(test)]
fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
}
我们定义的runner函数只是打印一条语句,然后调用列表中的每一个测试函数。
现在我们使用
cargo xrun
运行,但是发现并没有什么不同,仍旧是打印一个hello world出来。而不是我们想要的test。
因为我们的_start函数仍然用作入口点。自定义测试框架特性生成一个调用test_run的主函数,但是这个函数被忽略了,因为我们使用了#[no_main]属性并提供了我们自己的入口点。
所以,我们首先需要通过reexport_test_harness_main属性将生成函数的名称更改为与main不同的名称。然后,我们可以从_start函数调用重命名的函数,这里需要在main函数中修改如下代码:
// in src/main.rs
#![reexport_test_harness_main = "test_main"]
#[no_mangle]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
这里,我们将测试框架条目函数的名称设置为test_main,并从_start入口点调用它。因为函数不是在正常运行时生成的,我们只在测试上下文中使用条件编译来添加对test_main的调用。当我们现在执行Cargo xtest时,我们会在屏幕上看到来自test_run的“running 0 test”消息:
我们现在准备创建我们的第一个测试函数,我们在main文件中加入如下代码,这个代码将会在我们测试的时候打印字符串:
// in src/main.rs
#[test_case]
fn trivial_assertion() {
print!("trivial assertion... ");
assert_eq!(1, 1);
println!("[ok]");
}
结果如图所示
这是因为test构架传递给test_runnner函数的片中包含了一个对trivial_assertion函数的引用,从我们所打印的trivial assertion… [ok]来看我们的测试被调用,并成功执行,test_run返回到test_main函数,然后又返回到_start函数,在_start结束的时候,我们进入了一个循环,因为我们的入口不允许返回,下面我们要实现如何在做完所有测试之后退出。
现在,在我们的_start函数的末尾有一个循环回路,并且需要在每次执行cargo xtest时手动关闭QEMU。我们可以使用一种便捷的方式来退出qemu,但是,要启用这个方式,我们需要将一个设备参数传递给qemu。我们需要在我们的cargo.toml文件中加入如下代码:
# in Cargo.toml
[package.metadata.bootimage]
test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]
bootimage runner将会把test-args附加到所有的测试可执行文件的默认qemu命令中。对于正常的执行cargo xrun的时候,这个参数将会被忽略。在这里我们传递两个参数iobase和iosize,他们将指定可以从内核到达设备的I/O端口
由于我们需要使用x86_64库所提供的抽象,所以我们需要将这个库添加到cargo.toml文件中去:
# in Cargo.toml
[dependencies]
x86_64 = "0.7.5"
至此,我们的cargo.toml文件已经变成了这个模样:
[package]
name = "junmo2_os"
version = "0.1.0"
authors = ["junmo"]
edition = "2018"
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
# in Cargo.toml
[dependencies]
bootloader = "0.6.0"
volatile = "0.2.3"
spin = "0.4.9"
x86_64 = "0.7.5"
[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]
[package.metadata.bootimage]
test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]
现在,我们可以使用x86_64所提供的端口类型来创建我们的exit_qemu函数,并通过这个函数来实现退出,我们需要将main文件修改如下,加入如下代码:
// in src/main.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
该函数在0xF4创建一个新端口,该端口是ISA-debug-exit设备的iobase。则它将所传递的退出代码写入端口。我们使用U32,因为我们指定ISA-debug-exit设备的IOSIZE为4字节。这两种操作都不安全,因为写入I/O端口通常会导致任意行为。要指定退出状态,我们将创建QEMUExitCode枚举。
我们想要当测试成功的时候退出当前指定状态并汇报成功,否则就退出代码并汇报失败。
ENUM标记为#[REPR(U32)]以代表U32整数表示每个变体。我们将退出代码0x10用于成功,0x11用于失败。只要不与QEMU的默认退出代码冲突,实际的退出代码就无关紧要了。
现在我们可以更新test-runnner函数,在所有测试运行成功后退出qemu。我们需要将main文件修改如下;
fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
/// new
exit_qemu(QemuExitCode::Success);
}
现在我们运行cargo xtest,我们可以看到qemu被关闭了,但是我们的cargo xtest汇报了一个错误,即便我们通过了所有的测试,但仍旧解释为失败。
这是因为我们的cargo xtest将所有除0以外的报错代码都认为是失败的。所以,我们需要使用bootimage提供的一个test-success-exit-code配制键,他将指定的退出代码映射到退出代码0,这里我们为了使用,将要修改我们的toml文件,增加如下的代码;
[package.metadata.bootimage]
test-success-exit-code = 33 # (0x10 << 1) | 1
使用了这个配制,bootimage将会将成功退出代码映射为退出代码0,以便cargo xtest正确识别成功案例,而不将测试计算为失败。我们的测试运行程序现在自动关闭QEMU并正确地报告测试结果。
我们仍然看到QEMU窗口打开了很短的时间,但不足以读取结果。所以我们需要将测试结果打印到控制台,以便在QEMU退出后仍然可以看到它们。为了实现这个,我们需要以某种方式将数据从内核发送到主机系统,我们将选择一个较为简单的解决方案。
发送数据的一种简单方法是使用串口,这是一种在现代计算机中已不复存在的旧接口标准。它很容易编程,QEMU可以将通过串行发送的字节重定向到主机的标准输出或文件。实现串行接口的芯片称为UARTS。在x86上有很多UART模型,但幸运的是,它们之间唯一的区别是一些我们不需要的高级特性。现在常见的UART都与16550 UART兼容,所以我们将使用该模型作为测试框架。我们将使用uart_16550库初始化uart并通过串口发送数据。要将其添加为依赖项,我们更新Cargo.toml:
# in Cargo.toml
[dependencies]
uart_16550 = "0.2.0"
UART_16550库包含一个表示UART寄存器的SerialPortstruct,但是我们仍然需要自己构造它的一个实例。所以我们将main文件修改如下:
// in src/main.rs
mod serial;
然后我们新建一个名叫serial.rs的文件,并加入如下代码:
// in src/serial.rs
use uart_16550::SerialPort;
use spin::Mutex;
use lazy_static::lazy_static;
lazy_static! {
pub static ref SERIAL1: Mutex<SerialPort> = {
let mut serial_port = unsafe { SerialPort::new(0x3F8) };
serial_port.init();
Mutex::new(serial_port)
};
}
与VGA文本缓冲区一样,我们使用LAY_STATIC和Spinlock来创建静态写入器实例。这样,我们可以确保init方法在第一次使用时准确地被调用一次。与Isa调试退出设备一样,UART是使用端口I/O编程的,因为UART比较复杂,它使用多个I/O端口来编程不同的设备寄存器。不安全的SerialPort::New Function要求UART的第一个I/O端口的地址作为参数,它可以从中计算所有需要的端口的地址。我们正在传递端口地址0x3F8,这是第一个串行接口的标准端口号。为了使串口易于使用,我们添加了serial_print!还有serial_println!宏。我们在serial文件中加入如下代码:
#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {
use core::fmt::Write;
SERIAL1.lock().write_fmt(args).expect("Printing to serial failed");
}
/// Prints to the host through the serial interface.
#[macro_export]
macro_rules! serial_print {
($($arg:tt)*) => {
$crate::serial::_print(format_args!($($arg)*));
};
}
/// Prints to the host through the serial interface, appending a newline.
#[macro_export]
macro_rules! serial_println {
() => ($crate::serial_print!("\n"));
($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(
concat!($fmt, "\n"), $($arg)*));
}
该实现与我们的打印和println宏的实现非常相似。由于SerialPort类型已经实现了fmt::write特性,所以我们不需要提供自己的实现。现在我们可以打印到串行接口,而不是我们的测试代码中的VGA文本缓冲区。我们在main文件里调用我们所写的serial_printf宏,将main文件修改如下;
// in src/main.rs
#[cfg(test)]
fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test();
}
/// new
exit_qemu(QemuExitCode::Success);
}
#[test_case]
fn trivial_assertion() {
serial_print!("trivial assertion... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
现在我们使用cargo xtest试一下,发现qemu闪了一下就不见了,然后在命令行中打印了如下信息:
这代表我们还没有完成。要查看QEMU的串行输出,我们需要使用-Series参数将输出重定向到stdout,我们需要在cargo.toml文件中做出如下修改:
# in Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio"
]
现在我们运行,可以看到这样的效果:
表明还是运行正常的。
但是,当测试失败时,我们仍然会看到QEMU中的输出,因为我们的panic处理函数仍然使用的是prinln宏。
想要在panic处理函数中使用错误信息退出qemu,我们可以使用条件编译在测试模式中使用不同的panic处理程序,简单地将,就是增加一个panic处理程序,将main文件修改如下:
// our existing panic handler
#[cfg(not(test))] // new attribute
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
// our panic handler in test mode
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
对于我们的测试panic处理程序,我们使用串行_println代替println,然后使用一个失败的退出代码退出QEMU。注意,在EXIT_QEMU调用之后,我们仍然需要一个无尽循环,因为编译器不知道 isa-debug-exit 设备会导致程序退出。现在,QEMU还会退出失败的测试,并在控制台上打印一条有用的错误消息。
但我们现在是正确的,怎么才能看到错误的信息呢?很简单,我们将main文件里的trivial_assertion函数中的assert_eq!(1,1)修改为assert_eq(0,1)就可以了:
现在的运行结果是这样的:
貌似没什么毛病。
因为我们现在看到控制台上的所有测试输出,所以我们不再需要弹出短时间的QEMU窗口。这样我们就能把它完全藏起来。我们可以通过传递-display none参数传递给qemu来隐藏他,即在cargo.toml文件中做出如下修改:
# in Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio",
"-display", "none"
]
现在QEMU完全在后台运行,不再打开窗口。我们运行cargo xtest的时候不会看到一闪而过的黑框了。
因为Cargo xtest等待测试运行程序退出,所以永远不会返回的测试会永远阻止测试运行程序。就是说一个测试因为某些错误不终止的时候,我们就会一直的等待下去。比方说我们在我们的trivial_assertion文件的最后加一个无限循环loop{},各位可以试一下,看吃过午饭后回来他会不会运行结束。
所以我们需要在cargo.toml文件中加入时间限定,如下:
# in Cargo.toml
[package.metadata.bootimage]
test-timeout = 300 # (in seconds)
如果不想等待5分钟的琐碎断言测试超时,可以减少300。
现在我们有了一个工作的测试框架,我们可以为我们的VGA缓冲区实现创建一些测试。首先,我们创建了一个非常简单的测试来验证println在没有panic的情况下工作。我们在我们的vga_buffer文件中加入如下的测试函数代码:
// in src/vga_buffer.rs
#[cfg(test)]
use crate::{serial_print, serial_println};
#[test_case]
fn test_println_simple() {
serial_print!("test_println... ");
println!("test_println_simple output");
serial_println!("[ok]");
}
测试只是将一些东西打印到VGA缓冲区。如果它在没有panic的情况下结束,这意味着println调用也不会panic。因为我们只需要test模式下的serial_println导入,所以我们添加了cfg(Test)属性,以避免对正常的cargo xbuild使用未使用的导入警告。
为了确保即使打印了许多行并将行移出屏幕,也不会出现panic,我们可以创建另一个测试,继续在vga_buffer文件中加入如下代码:
// in src/vga_buffer.rs
#[test_case]
fn test_println_many() {
serial_print!("test_println_many... ");
for _ in 0..200 {
println!("test_println_many output");
}
serial_println!("[ok]");
}
我们还可以创建测试功能,以验证打印的行是否真正出现在屏幕上
// in src/vga_buffer.rs
#[test_case]
fn test_println_output() {
serial_print!("test_println_output... ");
let s = "Some test string that fits on a single line";
println!("{}", s);
for (i, c) in s.chars().enumerate() {
let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();
assert_eq!(char::from(screen_char.ascii_character), c);
}
serial_println!("[ok]");
}
函数定义了一个测试字符串,使用println打印它,然后在代表VGA文本缓冲区的静态写入器的屏幕字符上迭代。由于println打印到最后一个屏幕行,然后立即添加新行,字符串应该出现在行缓冲区height-2上。
通过枚举,我们统计变量i中的迭代次数,然后用于加载对应于c的屏幕字符。通过比较屏幕字符的ASCII_字符与c,我们确保字符串的每个字符真正出现在VGA文本缓冲区中。
在rust中进行集成测试的惯例是将它们放入项目根目录中的测试目录中(即,在src目录旁边)。默认测试框架和自定义测试框架均将自动拾取并执行该目录中的所有测试。所有集成测试都是它们自己的可执行文件,完全独立于我们的main.rs.,这意味着每个测试都需要定义自己的入口点功能。
现在,我们新建一个test的文件夹,并在里面新建一个文件,名叫basic_boot.rs
在其中加入如下代码:
// in tests/basic_boot.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
#[no_mangle] // don't mangle the name of this function
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
fn test_runner(tests: &[&dyn Fn()]) {
unimplemented!();
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
loop {}
}
由于集成测试是独立的可执行文件,因此我们需要提供所有的属性(no_std、no_main、test_runner等)。我们还需要创建一个新的入口点函数_start,它调用测试入口点函数test_main。我们不需要任何CFG(测试)属性,因为集成测试可执行文件从未在非测试模式中构建。
理想情况下,我们希望通过使用serial_println宏和exit_qemu函数在我们的main.rs中正确执行这些功能。问题是我们没有访问这些功能,因为测试是完全独立于我们的main.rs可执行的。如果您在此阶段运行“cargo xtest”,则会得到一个死循环,因为panic处理程序会无休止地循环。您需要使用CTRL+C键盘快捷键退出QEMU。当然,你可以骗你的队友去试一下~。
为了使集成测试可以使用所需的函数,我们需要从main.rs中分离出一个库,这个库可以由其他文件和集成测试可执行程序包含。为此,我们创建一个新的src/lib.rs文件:
并在其中加入如下代码:
#![no_std]
与main.rs一样,lib.rs是cargo自动识别的特殊文件。库是一个单独的编译单元,因此我们需要再次指定#![no_std]属性。为了使我们的库与Cargo xtest一起工作,我们还需要添加测试函数和属性,在lib文件中添加如下代码:
// in src/lib.rs
#![cfg_attr(test, no_main)]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
pub fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test();
}
exit_qemu(QemuExitCode::Success);
}
pub fn test_panic_handler(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
/// Entry point for `cargo xtest`
#[cfg(test)]
#[no_mangle]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
test_panic_handler(info)
}
为了使我们的test_run可用于可执行文件和集成测试,我们不对其应用cfg(Test)属性,我们还将我们的panic处理程序的实现分解为一个公共的test_panic_Handler函数,这样它也可以用于可执行文件。由于lib.rs是独立于main.rs进行测试的,所以在测试模式下编译库时,需要添加一个_start入口点和一个panic处理程序。我们还转移了QemuExitCode枚举和EXIT_QEMU函数。即在lib文件中添加如下代码,并将main中的去掉:
// in src/lib.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
现在,可执行文件和集成测试可以从库中导入这些函数,并且不需要定义它们自己的实现。为了使println和serial_println可用,我们还移动模块声明:
// in src/lib.rs
pub mod serial;
pub mod vga_buffer;
现在,我们可以更新我们的main.rs来使用库,将main修改如下:
// src/main.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(junmo3_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use junmo3_os::println;
use core::panic::PanicInfo;
#[no_mangle]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
/// This function is called on panic.
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
junmo3_os::test_panic_handler(info)
}
此时可以看到,我的junmo3_os文件夹已经可以像一个库一样被引用了。
现在,用cargo xrun进行测试:
没有问题,但是不要用cargo xtest进行试验,你会后悔的。仍然是无休止的循环(您可以使用ctrl c退出)。让我们在集成测试中使用所需的库函数来解决这个问题。
与src/main.rs一样,我们的test/basic_boot.rs可执行文件可以从我们的新库导入类型。这允许我们导入缺少的组件来完成测试。我们在basic_boot文件中修改如下:
// in tests/basic_boot.rs
#![test_runner(blog_os::test_runner)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
我们不用重新实现测试运行程序,而是使用库中的test_run函数。
对于我们的panic处理程序,我们像在main.rs中所做的那样,调用junmo3_os::test_panic_Handler函数。当我们运行它时会看到它为我们的lib.rs、main.rs和basic_boot.rs分别构建和运行测试。
对于main.rs和basic_boot集成测试,它报告“running 0 test”,因为这些文件没有任何带有#[test_case]注释的函数。
现在,我们可以将测试添加到Basic_boot.rs中。例如,我们可以测试println是否正常工作,就像我们在VGA缓冲区测试中所做的那样,我们加入如下代码:
// in tests/basic_boot.rs
use junmo3_os::{println, serial_print, serial_println};
#[test_case]
fn test_println() {
serial_print!("test_println... ");
println!("test_println output");
serial_println!("[ok]");
}
现在,我们运行cargo xtest可以得到这样的结果:
标准库的测试框架支持一个允许构建应该失败的测试的#[shall_pallo]属性。这例如用于验证在传递了无效参数时函数失败。但是,由于我们是在[no_std]下进行的,所以不能实现这样的情况,那么,我们尝试通过创建一个集成测试来获得类似的行为,该测试从死机处理程序中的成功错误代码退出。我们在test文件夹下新建一个should_panic文件,并加入如下代码
// in tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{QemuExitCode, exit_qemu, serial_println};
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
该测试仍然不完整,因为它尚未定义_start函数或任何自定义的测试运行程序属性。让我们添加缺失的部分:
// in tests/should_panic.rs
#![feature(custom_test_frameworks)]
#![test_runner(test_runner)]
#![reexport_test_harness_main = "test_main"]
#[no_mangle]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
pub fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
}
exit_qemu(QemuExitCode::Success);
}
测试没有重用lib.rs中的test_Runner,而是定义了它自己的test_run函数,当测试返回时,该函数使用失败的退出代码退出。如果未定义测试函数,则运行程序将使用成功的错误代码退出。由于运行单个测试后,运行程序总是退出,因此定义多个#[test_case]函数是没有意义的。
现在我们可以创建一个应该失败的测试:
// in tests/should_panic.rs
use junmo3_os::serial_print;
#[test_case]
fn should_fail() {
serial_print!("should_fail... ");
assert_eq!(0, 1);
}
对于只具有单个测试功能的集成测试(比如应该panic测试),实际上并不需要测试运行程序。对于这种情况,我们可以完全禁用测试运行程序,并直接在_start函数中运行测试。其中的关键是禁用Cargo.toml中测试的线束标志,该标记定义测试运行程序是否用于集成测试。当它被设置为false时,默认的测试运行程序和自定义的测试运行程序功能都会被禁用,这样测试就被视为一个普通的可执行文件。
让我们禁用应该panic测试的线束标志,将cargo.toml文件修改如下:
# in Cargo.toml
[[test]]
name = "should_panic"
harness = false
现在,我们通过删除测试Runner相关代码,大大简化了我们的should_panic测试。将should_panic文件修改如下:
// in tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{exit_qemu, serial_print, serial_println, QemuExitCode};
#[no_mangle]
pub extern "C" fn _start() -> ! {
should_fail();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
loop{}
}
fn should_fail() {
serial_print!("should_fail... ");
assert_eq!(0, 1);
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
我们现在直接从_start函数调用should_fail函数,并在返回失败退出代码时退出。当我们运行cargo xtest --test should_panic时,我们看到测试的行为与以前完全相同。
除了创建sshould_panic测试之外,禁用线束属性还可用于复杂的集成测试,例如,当单个测试功能具有副作用并且需要以指定的顺序运行时。
最终的最终,我们得到的文件如下:
main文件:
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(junmo3_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use junmo3_os::println;
use core::panic::PanicInfo;
#[no_mangle]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
/// This function is called on panic.
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
junmo3_os::test_panic_handler(info)
}
lib文件:
// in src/lib.rs
#![no_std]
#![cfg_attr(test, no_main)]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
pub mod serial;
pub mod vga_buffer;
pub fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test();
}
exit_qemu(QemuExitCode::Success);
}
pub fn test_panic_handler(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
/// Entry point for `cargo xtest`
#[cfg(test)]
#[no_mangle]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
test_panic_handler(info)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
serial文件:
// in src/serial.rs
use uart_16550::SerialPort;
use spin::Mutex;
use lazy_static::lazy_static;
lazy_static! {
pub static ref SERIAL1: Mutex<SerialPort> = {
let mut serial_port = unsafe { SerialPort::new(0x3F8) };
serial_port.init();
Mutex::new(serial_port)
};
}
#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {
use core::fmt::Write;
SERIAL1.lock().write_fmt(args).expect("Printing to serial failed");
}
/// Prints to the host through the serial interface.
#[macro_export]
macro_rules! serial_print {
($($arg:tt)*) => {
$crate::serial::_print(format_args!($($arg)*));
};
}
/// Prints to the host through the serial interface, appending a newline.
#[macro_export]
macro_rules! serial_println {
() => ($crate::serial_print!("\n"));
($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(
concat!($fmt, "\n"), $($arg)*));
}
vga_buffer文件:
// in src/vga_buffer.rs
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
Pink = 13,
Yellow = 14,
White = 15,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);
impl ColorCode {
fn new(foreground: Color, background: Color) -> ColorCode {
ColorCode((background as u8) << 4 | (foreground as u8))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
ascii_character: u8,
color_code: ColorCode,
}
const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;
/*
#[repr(transparent)]
struct Buffer {
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
*/
use volatile::Volatile;
struct Buffer {
chars: [[Volatile<ScreenChar>; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
pub struct Writer {
column_position: usize,
color_code: ColorCode,
buffer: &'static mut Buffer,
}/*
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= BUFFER_WIDTH {
self.new_line();
}
let row = BUFFER_HEIGHT - 1;
let col = self.column_position;
let color_code = self.color_code;
self.buffer.chars[row][col] = ScreenChar {
ascii_character: byte,
color_code,
};
self.column_position += 1;
}
}
}*/
//fn new_line(&mut self) {/* TODO */}
//}
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= BUFFER_WIDTH {
self.new_line();
}
let row = BUFFER_HEIGHT - 1;
let col = self.column_position;
let color_code = self.color_code;
self.buffer.chars[row][col].write(ScreenChar {
ascii_character: byte,
color_code: color_code,
});
self.column_position += 1;
}
}
}
//fn new_line(&mut self) {/* TODO */}
//}
//impl Writer {
pub fn write_string(&mut self, s: &str) {
for byte in s.bytes() {
match byte {
// 可以是能打印的ASCII码字节,也可以是换行符
0x20...0x7e | b'\n' => self.write_byte(byte),
// 不包含在上述范围之内的字节
_ => self.write_byte(0xfe),
}
}
}
//}
//impl Writer {
fn new_line(&mut self) {
for row in 1..BUFFER_HEIGHT {
for col in 0..BUFFER_WIDTH {
let character = self.buffer.chars[row][col].read();
self.buffer.chars[row - 1][col].write(character);
}
}
self.clear_row(BUFFER_HEIGHT - 1);
self.column_position = 0;
}
//fn clear_row(&mut self, row: usize) {/* TODO */}
//}
//impl Writer {
fn clear_row(&mut self, row: usize) {
let blank = ScreenChar {
ascii_character: b' ',
color_code: self.color_code,
};
for col in 0..BUFFER_WIDTH {
self.buffer.chars[row][col].write(blank);
}
}
//修改
fn clear_clr(&mut self, row: usize) {
let blank = ScreenChar {
ascii_character: b' ',
color_code: self.color_code,
};
for i in 0..row{
for col in 0..BUFFER_WIDTH {
self.buffer.chars[i][col].write(blank);
}
}
}
}
use core::fmt;
impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_string(s);
Ok(())
}
}
/*
pub fn print_something() {
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello ");
writer.write_string("Wörld!");
}*/
/*
pub fn print_something() {
use core::fmt::Write;
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello! ");
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
}*/
/*
pub static WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
*/
use lazy_static::lazy_static;
/*
lazy_static! {
pub static ref WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
}
*/
use spin::Mutex;
//...
lazy_static! {
pub static ref WRITER: Mutex<Writer> = Mutex::new(Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
});
}
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
//修改
//换个思路,直接写函数,成功
pub fn clear(){
WRITER.lock().clear_clr(BUFFER_HEIGHT);
}
pub fn time(a:usize){
for i in 0 .. a{}
}
/*
#[macro_export]
macro_rules! clear {
($($arg:tt)*) => ($crate::vga_buffer::clr);
}
#[doc(hidden)]
pub fn clr() {
//Writer::clear_row();
}
*/
//
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
WRITER.lock().write_fmt(args).unwrap();
}
//尝试编写宏
#[macro_export]
macro_rules! clear {
($($arg:tt)*) => ($crate::vga_buffer::_clear());
}
#[doc(hidden)]
pub fn _clear(){
WRITER.lock().clear_clr(BUFFER_HEIGHT);
}
#[macro_export]
macro_rules! time {
($($arg:tt)*) => ($crate::vga_buffer::time(format_args!($($arg)*)));
}
#[cfg(test)]
use crate::{serial_print, serial_println};
#[test_case]
fn test_println_simple() {
serial_print!("test_println... ");
println!("test_println_simple output");
serial_println!("[ok]");
}
#[test_case]
fn test_println_many() {
serial_print!("test_println_many... ");
for _ in 0..200 {
println!("test_println_many output");
}
serial_println!("[ok]");
}
#[test_case]
fn test_println_output() {
serial_print!("test_println_output... ");
let s = "Some test string that fits on a single line";
println!("{}", s);
for (i, c) in s.chars().enumerate() {
let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();
assert_eq!(char::from(screen_char.ascii_character), c);
}
serial_println!("[ok]");
}
basic_boot文件:
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(junmo3_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use junmo3_os::{println, serial_print, serial_println};
use core::panic::PanicInfo;
#[no_mangle] // don't mangle the name of this function
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
junmo3_os::test_panic_handler(info)
}
#[test_case]
fn test_println() {
serial_print!("test_println... ");
println!("test_println output");
serial_println!("[ok]");
}
should_panic文件:
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use junmo3_os::{exit_qemu, serial_print, serial_println, QemuExitCode};
#[no_mangle]
pub extern "C" fn _start() -> ! {
should_fail();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
loop{}
}
fn should_fail() {
serial_print!("should_fail... ");
assert_eq!(0, 1);
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
cargo.toml文件:
[package]
name = "junmo3_os"
version = "0.1.0"
authors = ["junmo"]
edition = "2018"
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
# in Cargo.toml
[[test]]
name = "should_panic"
harness = false
[dependencies]
bootloader = "0.6.0"
volatile = "0.2.3"
spin = "0.4.9"
x86_64 = "0.7.5"
uart_16550 = "0.2.0"
[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio",
"-display", "none"
]
test-success-exit-code = 33 # (0x10 << 1) | 1