trybuild crate での derive macro のテスト
前回は derive macro の helper attributes について書きました。今回は前回の例を trybuild crate を使ってテストします。
trybuild crate とは
trybuild crate は rustc を呼び出してそのエラーメッセージに対しての assert を書けるようにする crate です。
GitHub リポジトリは dtolnay/trybuild です。そちらの README を読んだほうが正確です。
用途は手続き的マクロ (procedural macro) やその一種である derive macro に限定されるわけではないですが、マクロの利用方法の誤りなどはコンパイルエラーになるので、それをテストするのに便利です。
使用の流れ
テストケースに次のような trybuild を使用したコードを記載します。
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/fail1.rs");
}
ファイル名には *.rs のような glob の指定もできます。
compile_fail で指定された各ファイルは拡張子を .stderr にした標準エラー出力を期待しているものとされます。
.stderr のファイルを置かずに cargo test すると wip ディレクトリに標準エラー出力を記録したファイル (wip/*.stderr) が生成されます。それが期待値で良いなら tests/ui ディレクトリに移動してやれば良いです。
ある種の snapshot testing ですね。
前回の例に適用
前回は derive macro に rename という helper attributes を追加しました。次のようなものです。
#[derive(derive1::VariantsFn)]
enum E1 {
#[rename = "X"]
A,
B(i32),
C { b: bool },
}
fn main() {
assert_eq!(E1::variants(), &["X", "B", "C"]);
}
これをテストしていきます。
サンプルの全体は https://github.com/bouzuya/rust-examples/tree/cdbbab3cdaee194a6c77bb8613bb5e5024d7f6ba/derive_macro1/crates/derive1 にあります。
tests/lib.rs に trybuild crate のテストケースを書きます。
#[test]
fn main() {
let test_cases = trybuild::TestCases::new();
test_cases.compile_fail("tests/ui/compile-fail-0.rs");
test_cases.pass("tests/ui/pass-no-variants.rs");
test_cases.pass("tests/ui/pass-with-helper-attr.rs");
test_cases.pass("tests/ui/pass.rs");
}
今回は compile_fail のほかに pass も使用しています。
tests/ui/*.rs にファイルを配置します。パスは ui/ でなくてもいいのですが、参考にした thiserror や trybuild 本体が UI テストの一種として扱っているからか ui/ の下に配置していたので、それを踏襲しています。
tests/ui/compile-fail-0.rs の例です。
fn main() {
#[derive(derive1::VariantsFn)]
enum E {
#[rename]
A,
B,
C,
}
}
rename には = "X" のような指定が必要であり、これはコンパイルエラーになります。
tests/ui/compile-fail-0.stderr に次のようなファイルを配置します。
error: expected `#[rename = "name"]` attribute
--> tests/ui/compile-fail-0.rs:4:9
|
4 | #[rename]
| ^
先ほども書いたとおり cargo test すれば、 wip の下に自動で↑のファイルを生成してくれるので、それを移動するだけで OK です。
tests/ui/pass-with-helper-attr.rs の例です。
fn main() {
#[derive(derive1::VariantsFn)]
enum E {
#[rename = "X"]
A,
B,
C,
}
assert_eq!(E::variants(), &["X", "B", "C"]);
}
これはパスする例です。コンパイルエラーのメッセージをテストできますが、パスする例も書けます。
他にも前々回の内容などをテストするかんたんな例を追加しました。詳細は省略します。
$ cargo test
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.05s
Running unittests src/lib.rs (/home/bouzuya/ghq/github.com/bouzuya/rust-examples/derive_macro1/target/debug/deps/derive1-6dcbf849355ffcd5)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/lib.rs (/home/bouzuya/ghq/github.com/bouzuya/rust-examples/derive_macro1/target/debug/deps/lib-2b054ca7f2c12bea)
running 1 test
Compiling derive1-tests v0.0.0 (/home/bouzuya/ghq/github.com/bouzuya/rust-examples/derive_macro1/target/tests/trybuild/derive1)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.34s
test tests/ui/compile-fail-0.rs [should fail to compile] ... ok
test tests/ui/pass-no-variants.rs [should pass] ... ok
test tests/ui/pass-with-helper-attr.rs [should pass] ... ok
test tests/ui/pass.rs [should pass] ... ok
test main ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.83s
Doc-tests derive1
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
いいですね。
*.stderr を消してテストすると次のような出力がされます。
test tests/ui/compile-fail-0.rs [should fail to compile] ... wip
NOTE: writing the following output to `wip/compile-fail-0.stderr`.
Move this file to `tests/ui/compile-fail-0.stderr` to accept it as correct.
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
error: expected `#[rename = "name"]` attribute
--> tests/ui/compile-fail-0.rs:4:9
|
4 | #[rename]
| ^
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
指示通りに配置すると先の表示になります。
おわりに
今回は trybuild crate で derive macro をテストしました。すごいお手軽にマクロのテストができるので助かりますね。 *.stderr をコピーするところの体験も良い感じです。
次回は基本にかえって Debug trait について書きます。
Discussion