Rustのテストでsetup/teardownをする
TL;DR
同期版
fn run_test<F>(f: F)
where
F: FnOnce(),
{
setup(); // 予めやりたい処理
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
teardown(); // 後片付け処理
if let Err(err) = result {
panic::resume_unwind(err);
}
}
※ 同期版に限っては Drop
を使った実装をした方が良いと思いますが今回は割愛
非同期版
use futures::future::FutureExt as _;
async fn run_test_async<F>(test: F)
where
F: std::future::Future,
{
setup_async().await; // 予めやりたい処理
let result = std::panic::AssertUnwindSafe(test).catch_unwind().await;
teardown_async().await; // 後片付け処理
if let Err(err) = result {
panic::resume_unwind(err);
}
}
モチベーション
というわけでテストを書きましょう。
僕が今作っているアプリではデータベースにデータを投入したりするため、各テストケースの最後にはデータベースをリセットする必要があります。そうなるとteardown処理( rspecで言うところの after
)が欲しくなりますね。
panic
rustでテストを書く時は assert_eq
マクロを使うことが多いと思いますが、このマクロはアサーションに失敗するとpanicするため、そのままでは後続の処理が行われません。
#[test]
fn test_something() {
setup_database();
let result = update_database();
assert_eq!(result, 42);
// 後始末
// assert_eqでパニックすると呼ばれない!!
clean_database();
}
Result型をテストで使うことで assert_eq
などを使わないようにすれば多くの場合 clean_database
まで辿り着くことができるでしょうが、それでも予期せぬpanicが発生すると clean_database
が実行されません。
catch_unwind
rustの標準ライブラリには catch_unwind
という関数があります。この関数は渡したクロージャー内でpanicが発生するとそれを捕捉し、ハンドリングを可能にします。
また、 resume_unwind
で捕捉したpanic処理を再開することができます。
では簡単なサンプルを書いてみましょう。
#[test]
fn test_sample() {
println!("setup!!!!");
let result = std::panic::catch_unwind(|| {
assert_eq!(1, 2); // 意図的に失敗させる
});
println!("teardown!!!!");
if let Err(err) = result {
std::panic::resume_unwind(err);
}
}
実行します
running 1 test
test tests::test_sample ... FAILED
failures:
---- tests::test_sample stdout ----
setup!!!!
thread 'tests::test_sample' panicked at 'assertion failed: `(left == right)`
left: `1`,
right: `2`', src/main.rs:24:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
teardown!!!!
failures:
tests::test_sample
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--bin async_test_sample'
setup!!!!
と teardown!!!!
が表示されていますね。 test_sample
中でpanicが起きてもちゃんと setup
関数と teardown
関数が呼ばれているようです。
ちなみに「 teardown
を呼んでから捕捉したpanicを再開しているから、 setup
→ teardown
→ panicのメッセージという順序で表示されるのでは?」と思うかもしれませんが、 Rustのパニック機構 - 簡潔なQ によると、panicのメッセージは catch_unwind
で捕捉される前のパニックハンドラで出力されているため teardown
より先に出力されているように思われます。最後にteardownの表示がされた方が直感的な気がするのでこれはこれで良いと思います。
Rustのパニック機構 - 簡潔なQ はrustのパニックの仕組みについて詳しく説明されていて非常に勉強になりました。
UnwindSafe
先程の catch_unwind
関数のトレイト境界に UnwindSafe
というトレイトが出てきました。こいつは一体何者でしょうか。
是非 公式ドキュメント や Rustのパニック機構 - 簡潔なQ を読んでいただきたいのですが、「処理の途中でpanicが発生すると不変条件が壊れる可能性があるので、不変条件が壊れる可能性がある処理では警告のために UnwindSafe
が付かないようにしている」という理解をしています。
先程のサンプルは簡単過ぎて UnwindSafe
な処理だったようです。今度は UnwindSafe
じゃない処理にしてみましょう。
#[test]
fn test_sample() {
println!("setup!!!!");
+ let mut a = 0;
let result = std::panic::catch_unwind(|| {
+ a[0] = 1;
assert_eq!(1, 2); // 意図的に失敗させる
});
println!("teardown!!!!");
if let Err(err) = result {
std::panic::resume_unwind(err);
}
}
[Running 'cargo test']
Compiling async_test_sample v0.1.0 (/home/rust/async_test)
error[E0277]: the type `&mut i32` may not be safely transferred across an unwind boundary
--> src/main.rs:25:18
|
25 | let result = std::panic::catch_unwind(|| {
| __________________^^^^^^^^^^^^^^^^^^^^^^^^_-
| | |
| | `&mut i32` may not be safely transferred across an unwind boundary
26 | | a = 1;
27 | | assert_eq!(1, 2); // 意図的に失敗させる
28 | | });
| |_____- within this `[closure@src/main.rs:25:43: 28:6]`
|
= help: within `[closure@src/main.rs:25:43: 28:6]`, the trait `UnwindSafe` is not implemented for `&mut i32`
= note: `UnwindSafe` is implemented for `&i32`, but not for `&mut i32`
= note: required because it appears within the type `[closure@src/main.rs:25:43: 28:6]`
note: required by a bound in `catch_unwind`
--> /home/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panic.rs:128:40
|
128 | pub fn catch_unwind<F: FnOnce() -> R + UnwindSafe, R>(f: F) -> Result<R> {
| ^^^^^^^^^^ required by this bound in `catch_unwind`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `async_test_sample` due to previous error
コンパイルエラーになりました。
「全部 catch_unwind
に渡すクロージャーの中に入れてしまえば良いのでは?」と思いますがそうとも限らない場合があります。そのようなケースの1つが非同期処理の時です。
非同期処理
まずは UnwindSafe
のことを考えずに非同期版のteardown処理を書いてみます。
ありがたいことに futures crate にfuture版 catch_unwind
が定義されているのでこれを使います。
use futures::future::FutureExt as _;
#[tokio::test]
async fn test_sample() {
println!("setup!!!!");
let result = async {
assert_eq!(1, 2); // 意図的に失敗させる
}.catch_unwind().await;
println!("teardown!!!!");
if let Err(err) = result {
std::panic::resume_unwind(err);
}
}
これはまだコンパイルが通ります。
しかし catch_unwind
しようとするasyncブロックに適当な非同期処理を追加するとエラーになります。
use futures::future::FutureExt as _;
#[tokio::test]
async fn test_sample() {
println!("setup!!!!");
let result = async {
+ tokio::time::sleep(std::time::Duration::from_secs(1)).await;
assert_eq!(1, 2); // 意図的に失敗させる
}.catch_unwind().await;
println!("teardown!!!!");
if let Err(err) = result {
std::panic::resume_unwind(err);
}
}
長大なメッセージに驚きますがどれも「 UnwindSafe
ではない」という内容です。
error[E0277]: the type `UnsafeCell<tokio::time::driver::InnerState>` may contain interior mutability and a reference may not be safely transferrable across a catch_unwind boundary
--> src/main.rs:26:7
|
26 | }.catch_unwind().await;
| ^^^^^^^^^^^^ `UnsafeCell<tokio::time::driver::InnerState>` may contain interior mutability and a reference may not be safely transferrable across a catch_unwind boundary
|
= help: within `tokio::time::driver::Inner`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell<tokio::time::driver::InnerState>`
note: future does not implement `UnwindSafe` as it awaits another future which does not implement `UnwindSafe`
--> src/main.rs:24:9
|
24 | tokio::time::sleep(std::time::Duration::from_secs(1)).await;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ await occurs here on type `Sleep`, which does not implement `UnwindSafe`
error[E0277]: the type `UnsafeCell<Result<(), tokio::time::error::Error>>` may contain interior mutability and a reference may not be safely transferrable across a catch_unwind boundary
--> src/main.rs:26:7
|
26 | }.catch_unwind().await;
| ^^^^^^^^^^^^ `UnsafeCell<Result<(), tokio::time::error::Error>>` may contain interior mutability and a reference may not be safely transferrable across a catch_unwind boundary
|
= help: within `tokio::time::driver::entry::TimerShared`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell<Result<(), tokio::time::error::Error>>`
note: future does not implement `UnwindSafe` as it awaits another future which does not implement `UnwindSafe`
--> src/main.rs:24:9
|
24 | tokio::time::sleep(std::time::Duration::from_secs(1)).await;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ await occurs here on type `Sleep`, which does not implement `UnwindSafe`
error[E0277]: the type `UnsafeCell<tokio::util::linked_list::Pointers<tokio::time::driver::entry::TimerShared>>` may contain interior mutability and a reference may not be safely transferrable across a catch_unwind boundary
--> src/main.rs:26:7
|
26 | }.catch_unwind().await;
| ^^^^^^^^^^^^ `UnsafeCell<tokio::util::linked_list::Pointers<tokio::time::driver::entry::TimerShared>>` may contain interior mutability and a reference may not be safely transferrable across a catch_unwind boundary
|
= help: within `tokio::time::driver::entry::TimerShared`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell<tokio::util::linked_list::Pointers<tokio::time::driver::entry::TimerShared>>`
note: future does not implement `UnwindSafe` as it awaits another future which does not implement `UnwindSafe`
--> src/main.rs:24:9
|
24 | tokio::time::sleep(std::time::Duration::from_secs(1)).await;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ await occurs here on type `Sleep`, which does not implement `UnwindSafe`
error[E0277]: the type `UnsafeCell<AtomicUsize>` may contain interior mutability and a reference may not be safely transferrable across a catch_unwind boundary
--> src/main.rs:26:7
|
26 | }.catch_unwind().await;
| ^^^^^^^^^^^^ `UnsafeCell<AtomicUsize>` may contain interior mutability and a reference may not be safely transferrable across a catch_unwind boundary
|
= help: within `tokio::time::driver::entry::TimerShared`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell<AtomicUsize>`
note: future does not implement `UnwindSafe` as it awaits another future which does not implement `UnwindSafe`
--> src/main.rs:24:9
|
24 | tokio::time::sleep(std::time::Duration::from_secs(1)).await;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ await occurs here on type `Sleep`, which does not implement `UnwindSafe`
error[E0277]: the type `UnsafeCell<Option<Waker>>` may contain interior mutability and a reference may not be safely transferrable across a catch_unwind boundary
--> src/main.rs:26:7
|
26 | }.catch_unwind().await;
| ^^^^^^^^^^^^ `UnsafeCell<Option<Waker>>` may contain interior mutability and a reference may not be safely transferrable across a catch_unwind boundary
|
= help: within `tokio::time::driver::entry::TimerShared`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell<Option<Waker>>`
note: future does not implement `UnwindSafe` as it awaits another future which does not implement `UnwindSafe`
--> src/main.rs:24:9
|
24 | tokio::time::sleep(std::time::Duration::from_secs(1)).await;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ await occurs here on type `Sleep`, which does not implement `UnwindSafe`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `async_test_sample` due to 5 previous errors
AssertUnwindSafe
このエラーは AssertUnwindSafe
を使うことで解決できます。
この構造体で包むと任意のデータを UnwindSafe
にできます。「panicによるunwindが起きても不変条件は壊れない( あるいは壊れても問題ない )」ということを表明できます。
use futures::future::FutureExt as _;
#[tokio::test]
async fn test_sample() {
println!("setup!!!!");
- let result = async {
+ let result = std::panic::AssertUnwindSafe(async {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
assert_eq!(1, 2); // 意図的に失敗させる
- }.catch_unwind().await;
+ }).catch_unwind().await;
println!("teardown!!!!");
if let Err(err) = result {
std::panic::resume_unwind(err);
}
}
AssertUnwindSafe
で包んでいるfutureを引数で受け取るようにしたのがTL;DRに書いた run_test
run_test_async
になります。
まとめ
-
catch_unwind
を使うとpanicを捕捉できる -
catch_unwind
に渡す処理はUnwindSafe
である必要がある -
UnwindSafe
はpanicしても不変条件が壊れないなことのマーカー - 非同期処理は
UnwindSafe
にならないことが多い -
AssertUnwindSafe
を使うとUnwindSafe
であることを表明できる
余談
非同期版 Drop
早く来て欲しい…
Discussion