第四节 组件测试和模块测试

Rust 工具链本身对测试的支持很方便,下面将基于这一有利条件,对内核的组件和模块进行功能测试验证。组件测试相当于 crate 级的测试,直接在 crate 根目录下建立 tests 模块,对组件进行基于公开接口的黑盒测试;模块测试对应于单元测试,在模块内建立子模块tests,对模块的内部方法进行白盒测试。在 Makefile 中,预留了一个test 命令入口,用于启动上述测试。

我们需要注意一个容易忽略的事实,通过 make run 编译并运行内核,与通过 make test 编译并执行测试,这二者的编译和运行环境是截然不同的,前者的编译目标是 RiscV64 体系结构,并通过 qemu-riscv64 模拟器运行;后者则是针对本地的 x86_64 体系结构,在本地 Linux 上作为应用直接运行(Linux on x86_64 这应该是我们大多数人的本地开发环境)。因此,通过 Rust 工具链进行测试是有局限的,测试的功能与实际将要运行的功能可能存在差异,测试时还有可能需要一些 dummy 测试桩的辅助。我们需要清醒的保持这一认识,避免在后面的实验开发和测试中陷入困惑。

下面对现有的工程项目进行一些改造,满足测试的需要:

  1. Makefile 中 test 命令

    调用 cargo test 启动测试:

    ifeq ($(filter $(MAKECMDGOALS),test),)  # not run `cargo test`
    RUSTFLAGS := -C link-arg=-T$(LD_SCRIPT) -C link-arg=-no-pie
    export RUSTFLAGS
    endif
    
    test:
    	cargo test --workspace --exclude "axorigin" -- --nocapture
    

    测试针对整个工程 workspace 下包含的所有组件,只是排除 axorigin 应用本身。我们希望测试用例中能够通过 println! 直接输出信息到屏幕,所以指定了 --nocapture 参数。

    特别需要注意的是上面的 1~4 行,环境变量 RUSTFLAGS 不能出现在 make test 的过程中,原因前面已经说了,测试时是基于 x86_64 的体系结构,这个环境变量却要求使用面向 RiscV64 的 linker.lds 链接脚本和参数。如果不注意这一点,就会出现如下的错误:

    = note: /usr/bin/ld: cannot represent machine `riscv'
              collect2: error: ld returned 1 exit status
     error: could not compile `axhal` (lib test) due to previous error
    

    x86 工具链的链接器在解析 linker.lds 第一行时就发现错误,它不能识别 OUTPUT_ARCH(riscv),直接报错退出了。

  2. 工程 Workspace 级的 Cargo.toml

    [workspace]
    resolver = "2"
    
    members = [
        "axorigin",
        "axhal",
    ]
    
    [profile.release]
    lto = true
    

    以后增加新组件时,需要扩展 members 列表。

  3. 改造 axhal 组件的代码组织结构

    组件 axhal 负责屏蔽体系结构的差异,所以它对 make test 所要求的另类环境最为敏感。

    首先改造 crate 的 lib.rs:

    // axhal/src/lib.rs
    #![no_std]
    
    #[cfg(target_arch = "riscv64")]
    mod riscv64;
    #[cfg(target_arch = "riscv64")]
    pub use self::riscv64::*;
    
    // axhal/src/riscv64.rs
    mod lang_items;
    mod boot;
    pub mod console;
    
    unsafe extern "C" fn rust_entry(_hartid: usize, _dtb: usize) {
        extern "C" {
            fn main(hartid: usize, dtb: usize);
        }
        main(_hartid, _dtb);
    }

    原来 lib.rs 只是针对 RriscV64 的实现,现在下移到 riscv64.rs 模块文件中;相应的,lang_items.rs、boot.rs和 console.rs 三个文件,也移到到目录 riscv64 下。这样只有当目标体系结构是 RriscV64 时,才会引用这些实现,因而执行测试时不会编译它们。

    改造 axhal 的 Cargo.toml:

    [target.'cfg(target_arch = "riscv64")'.dependencies]
    sbi-rt = { version = "0.0.2", features = ["legacy"] }
    

    把 [dependencies] 改成带体系结构条件的形式,避免在执行测试时引入 sbi-rt 这个无效引用。

git show --stat 看一下改造后的情况:

 Cargo.toml                            | 10 ++++++++++
 Makefile                              |  7 ++++++-
 axhal/Cargo.toml                      |  2 +-
 axhal/src/lib.rs                      | 15 ++++-----------
 axhal/src/riscv64.rs                  | 10 ++++++++++
 axhal/src/{ => riscv64}/boot.rs       |  0
 axhal/src/{ => riscv64}/console.rs    |  0
 axhal/src/{ => riscv64}/lang_items.rs |  0
 8 files changed, 31 insertions(+), 13 deletions(-)

分别执行 make runmake test,验证我们的成果。

前者的输出没有变化,而执行后者时,显示如下:

cargo test --workspace --exclude "axorigin" -- --nocapture Compiling axhal v0.1.0 (/home/cloud/gitStudy/arceos_tutorial/axhal) Finished test [unoptimized + debuginfo] target(s) in 0.20s Running unittests src/lib.rs (target/debug/deps/axhal-0770fa3f3cc1ea75)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests axhal

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

目前还没有写任何测试用例,测试框架只是空转一次。

下节开始,我们将使用测试用例来驱动内核的开发。