以前から Markdown 内の Rust コードブロックのテストをするためのクレートとして skeptic
を推していたのですが、コンパイラのバージョンが更新された場合などにクレートの参照が上手くいかなくなる問題があったので、代用としてコードブロックのテスト用のクレートを作ってみました。
仕組み
skeptic
は、build.rs
内で Markdown 内のコードブロックを抽出し、それをもとに生成したテストコードを rustc
に渡すことでコードブロックをテストしています。このときに依存しているクレートを指定するためのオプションの計算は(cargo
が提供する機能を用いず)自力で行っています。cargo
のソースを詳しく読んだことがないため詳細は不明ですがおそらくこの部分の処理が現在 cargo
内部で用いられているものと食い違っており、その結果 target/
内に異なるバージョンのコンパイラでビルドされた成果物が残っていると使用するクレートの解決が失敗することがあります。これは主に CI でキャッシュを有効化しているときに頻繁に発生し、その度にキャッシュを削除してすべての依存クレートをビルドし直す必要が生じることになります。
一方、rustdoc
には #[doc(include = "...")]
という属性を用いることで外部の Markdown ファイルをドキュメンテーションコメントとして取り込んでくれる機能があります。残念ながら現時点では不安定な機能ですが1、これを使用することで Markdown ファイル内のコードブロックを cargo test --doc
時に検査することが可能になります。このときコードブロックをテストするために用いられるのは cargo
そのものであり、前述した問題が発生する可能性はかなり低くなると期待できます(当然ゼロではありませんが、公式に配布されるものなのでまず放置されることはないでしょう…)。
doubter
では、この #[doc(include = "...")]
を手続き的マクロを用いてエミュレートし、Markdown ファイルをドキュメンテーションコメントとして取り込むことでコードブロックのテストを実現しています。具体的には、次のようにマクロを呼び出すことで指定した Markdown ファイルの内容をドキュメンテーションコメントとして持つダミーの定数定義がコードに挿入されます。
#[macro_use]
extern crate doubter;
// これが
doubter! {
file = "README.md",
file = "docs/getting-started.md",
...
}
// ↓ 次のように展開される
mod readme_md {
#![doc = "... (README.md の中身) ..."]
}
mod docs {
mod getting_started {
#![doc = "... (docs/getting-started.md の中身) ..."]
}
}
現在の仕様では、テストしたい Markdown ファイルへの相対パスを一つずつ記述する必要があります。glob pattern への対応などは将来的にサポートする予定です。
(追記: 2018-10-13): v0.0.5 で glob pattern に対応しました。
使い方
あるクレートの README.md
内のコードブロックをテストすることを考えます。ディレクトリ構成は次のようになっていると仮定します。
.
├── Cargo.toml
├── README.md
└── src
└── lib.rs
1 directory, 3 files
doubter
は Markdown ファイルをコメントとして展開するため、それをテストとして実行するためのクレートを別途用意する必要があります。target/
を共有するため、テスト対象のクレートの Cargo.toml
を編集し、追加したクレートを [workspace.members]
に追加しておきます。
$ cargo new --lib testcrates/test_markdown_files
$ edit Cargo.toml
...
[workspace]
members = [
"testcrates/test_markdown_files",
]
テスト用のクレートの依存関係に doubter
とテスト対象のクレートを追加し、src/lib.rs
を編集してテスト対象となる Markdown ファイルへの(src/lib.rs
の属するクレートの Cargo.toml
が置かれた場所から見た)相対パスを記述していきます。
[dependencies]
doubter = "0.0.3"
[dev-dependencies]
foo = { path = "../.." }
#[macro_use]
extern crate doubter;
doubter! {
file = "README.md",
}
最後に、作成したクレートのテストを実行して正しくテストが実行されているかどうかを確認したら完了です。
$ cargo test -p test_markdown_files
#![feature(external_doc)]
で有効化出来る↩