5-6. テスト

テストを活用することは、組込みやベアメタルの開発でも非常に重要です。しかし、Rustのテストフレームワークは標準ライブラリに依存しており、#[no_std]環境で使うことができません。ここでは、組込み / ベアメタルRustのプロジェクトで利用されているテストやCIについて紹介します。

デュアルターゲット

部品をcrateに切り出し、ホスト上でテストする方法です。

テスト時には、#![no_std]でビルドしないようにします。そうすることで、標準ライブラリに依存するRustのテストフレームワークを利用することができます。

lib.rsでクレートレベルのアトリビュートを次のように指定します。


#![allow(unused)]
#![cfg_attr(not(test), no_std)]
fn main() {
}

これで、テスト時には、#![no_std]アトリビュートが有効になりません。後は、通常通りテストを書くだけです。heaplessクレートのテストが、参考になります。

カスタムテストフレームワーク

Writing an OS in Rust Testingで紹介されている方法です。unstableなcustom_test_frameworksフィーチャを利用します。

Rust標準のテストフレームワークと比較すると、パニックすることをテストするshould_panicなどの機能が利用できません。

カスタムテストフレームワークを実装するには、次のコードを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();
    }
}

test_runnerの引数はFn()トレイトのトレイトオブジェクトのスライスです。custom_test_frameworksによると、#[test_case]アトリビュートのついたアイテムが、test_runnerアトリビュートで指定した関数に渡されます。

プロダクトコードのエントリポイントに、テストビルド時のみ、テストハーネスのtest_mainを呼び出すコードを追加します。


#![allow(unused)]
#![reexport_test_harness_main = "test_main"]

fn main() {
#[no_mangle]
pub extern "C" fn _start() -> ! {
    println!("Hello World{}", "!");

    #[cfg(test)]
    test_main();

    loop {}
}
}

テストケースを書きます。


#![allow(unused)]
fn main() {
#[test_case]
fn trivial_assertion() {
    print!("trivial assertion... ");
    assert_eq!(1, 1);
    println!("[ok]");
}
}

後は、テストを実行するだけです。

より詳しい情報は、Writing an OS in Rust Testingを参照して下さい。

インテグレーションテスト

QEMUを利用して、特定デバイスのペリフェラルに依存しない試験を実施することができます。RTFMでは、QEMUでバイナリを実行し、semi-hosting機能で標準出力に表示した文字列と期待値とを比較しています。

$ cargo run --example binds
   Compiling cortex-m-rtfm v0.5.0-alpha.1
     Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/debug/examples/binds`
init
foo called 1 time
idle
foo called 2 times
$ cat ci/expected/binds.run 
init
foo called 1 time
idle
foo called 2 times
$ cargo run --example binds | diff -u ci/expected/binds.run -

期待通りの動作結果の場合、差分は出力されません。テストの実行結果、差分が出力されるかどうかを検証することで、インテグレーションテストを実施しています。

コンパイルテスト

複雑な (手続き) マクロを利用するクレートでは、様々な利用方法でコンパイルが通るかどうか、をテストします。

RTFMtestsディレクトリにコンパイルテストのテストケースcompiletest.rsがあります。

ここでは、cfailディレクトリにコンパイルが失敗するソースファイルが、cpassにコンパイルが成功するソースファイルが置かれています。


#![allow(unused)]
fn main() {
use std::{fs, path::PathBuf, process::Command};

use compiletest_rs::{common::Mode, Config};

#[test]
fn cfail() {
    let mut config = Config::default();

    config.mode = Mode::CompileFail;
    config.src_base = PathBuf::from("tests/cfail");
    config.link_deps();
// 中略
    compiletest_rs::run_tests(&config);
}
}

コンパイルテストの設定compiletest_rs::Configを作成し、compiletest_rs::run_testsでテストを実行します。これで、cfailディレクトリ内の全てのRustソースファイルのコンパイルに失敗すると、テストがパス、という扱いになります。

一方、コンパイルが成功するテストは、同テストケース内で次のように実装されています。


#![allow(unused)]
fn main() {
use tempdir::TempDir;
// ...
    let td = TempDir::new("rtfm").unwrap();
    for f in fs::read_dir("tests/cpass").unwrap() {
        let f = f.unwrap().path();
        let name = f.file_stem().unwrap().to_str().unwrap();

        assert!(Command::new("rustc")
            .args(s.split_whitespace())
            .arg(f.display().to_string())
            .arg("-o")
            .arg(td.path().join(name).display().to_string())
            .arg("-C")
            .arg("linker=true")
            .status()
            .unwrap()
            .success());
    }
}

システムコマンドでrustcを呼び出して、終了ステータスがsuccessかどうか、をテストしています。