#[test] 属性
多くの Rust プログラマーは、#[test] と呼ばれる組み込み属性を利用しています。必要なのは、次のように関数にマークを付け、いくつかのアサーションを含めることだけです。
#[test]
fn my_test() {
assert!(2+2 == 4);
}
このプログラムを rustc --test または cargo test を使用してコンパイルすると、このテスト関数や他の任意のテスト関数を実行できる実行可能ファイルが生成されます。このテスト方法により、テストを自然な形でコードのそばに配置できます。テストをプライベートモジュール内に置くことさえできます。
mod my_priv_mod {
fn my_priv_func() -> bool {}
#[test]
fn test_priv_func() {
assert!(my_priv_func());
}
}
したがって、プライベート項目は、何らかの外部テスト装置に公開する方法を気にすることなく、簡単にテストできます。これは Rust におけるテストのエルゴノミクスにとって重要です。しかし意味論的には、これはかなり奇妙です。見えないはずのこれらのテストを、何らかの main 関数はどのように呼び出すのでしょうか? rustc --test は正確には何をしているのでしょうか?
#[test] は、コンパイラの rustc_ast 内部で構文変換として実装されています。本質的には、クレートを 3 つのステップで書き換える高度な macro です。
ステップ 1: 再エクスポート
前述のとおり、テストはプライベートモジュール内に存在できるため、既存のコードを壊さずに、それらを main 関数に公開する方法が必要です。そのために、rustc_ast は __test_reexports というローカルモジュールを作成し、テストを再帰的に再エクスポートします。この展開により、上記の例は次のように変換されます。
mod my_priv_mod {
fn my_priv_func() -> bool {}
pub fn test_priv_func() {
assert!(my_priv_func());
}
pub mod __test_reexports {
pub use super::test_priv_func;
}
}
これで、テストには my_priv_mod::__test_reexports::test_priv_func としてアクセスできます。より深いモジュール構造では、__test_reexports はテストを含むモジュールを再エクスポートするため、a::b::my_test にあるテストは a::__test_reexports::b::__test_reexports::my_test になります。このプロセスはかなり安全に見えますが、既存の __test_reexports モジュールが存在する場合はどうなるのでしょうか?答えは、何も起こりません。
説明するには、Rust の 抽象構文木 が 識別子 をどのように表現するかを理解する必要があります。すべての関数、変数、モジュールなどの名前は文字列として格納されるのではなく、不透明な Symbol として格納されます。これは本質的に、各識別子に対する ID 番号です。コンパイラは、必要に応じて(構文エラーを出力するときなど)Symbol の人間が読める名前を復元できるように、別個のハッシュテーブルを保持しています。コンパイラが __test_reexports モジュールを生成するとき、その識別子に対して新しい Symbol を生成するため、コンパイラ生成の __test_reexports は手書きのものと名前を共有することはあっても、Symbol を共有することはありません。この手法はコード生成時の名前衝突を防ぎ、Rust の macro 衛生性の基盤となっています。
ステップ 2: ハーネスの生成
これでテストにクレートのルートからアクセスできるようになったので、rustc_ast を使用してそれらに対して何かを行う必要があります。これは次のようなモジュールを生成します。
#[main]
pub fn main() {
extern crate test;
test::test_main_static(&[&path::to::test1, /*...*/]);
}
ここで、path::to::test1 は test::TestDescAndFn 型の定数です。
この変換は単純ですが、テストが実際にどのように実行されるかについて多くの洞察を与えてくれます。テストは配列に集約され、test_main_static というテストランナーに渡されます。TestDescAndFn が正確には何であるかについては後で戻りますが、現時点で重要なのは、Rust core の一部であり、テスト用のすべてのランタイムを実装する test というクレートが存在するということです。test のインターフェイスは不安定であるため、それとやり取りする唯一の安定した方法は #[test] マクロを介することです。
ステップ 3: テストオブジェクトの生成
以前に Rust でテストを書いたことがあるなら、テスト関数で利用できるいくつかの任意属性に馴染みがあるかもしれません。たとえば、テストがパニックを引き起こすことを期待する場合、テストに #[should_panic] を注釈できます。これは次のようになります。
#[test]
#[should_panic]
fn foo() {
panic!("intentional");
}
これは、テストが単なる単純な関数以上のものであり、設定情報も持っていることを意味します。test はこの設定データを TestDesc という struct にエンコードします。クレート内の各テスト関数について、rustc_ast はその属性を解析し、TestDesc インスタンスを生成します。その後、TestDesc とテスト関数を、予測どおりの名前である TestDescAndFn struct に結合し、test_main_static はそれを操作します。
あるテストについて、生成される TestDescAndFn インスタンスは次のようになります。
self::test::TestDescAndFn{
desc: self::test::TestDesc{
name: self::test::StaticTestName("foo"),
ignore: false,
should_panic: self::test::ShouldPanic::Yes,
allow_fail: false,
},
testfn: self::test::StaticTestFn(||
self::test::assert_test_result(::crate::__test_reexports::foo())),
}
これらのテストオブジェクトの配列を構築したら、ステップ 2 で生成されたハーネスを介してテストランナーに渡されます。
生成されたコードの調査
nightly の rustc には、macro 展開後のモジュールソースを出力するために使用できる unpretty という不安定なフラグがあります。
$ rustc my_mod.rs -Z unpretty=hir