この記事は新歓ブログリレー2023の10日目です。
何をするんですか?
RustでCode Coverageをとります。Rustとは何か、Code Coverageとは何かについての説明は省略します。
Requirements
まずは必要なツールたちをインストールしましょう。
$ rustup component add llvm-tools
$ cargo install grcov
cargo install
は1日かかる(かからない)のでその間に次に進むことをお勧めします。
どうやってるの?
ここでの説明はほとんどInstrumentation-based Code Coverage - The rustc bookの拙訳+@です。正直よくわかっていない部分があるので雰囲気のみの解説になります。詳細かつ正確な説明は原文を参照してください。
そもそもRustがカバレッジを取る方法は2種類あるようです。
- GCCと互換性がある、gcovをベースとしたカバレッジ実装。
-Z profile
で有効になる - LLVMネイティブの機能を用いた、ソースコードに基づくカバレッジ実装。
-C instrument-coverage
で有効になる
オプションの具体的な与え方は後述します。-Z
のオプションはexperimentalなので、Nightly Rustを使っていない私は-C instrument-coverage
を使うことにします。
-C instrument-coverage
を与えると、RustコンパイラはRustプログラムに対して次の操作を施すようです:
- 自動的にLLVM組み込み(?)の命令を埋め込み、実行の度に増えるようなカウンターをコードの各所に仕込む。
- 各ライブラリおよびバイナリに追加の情報を与え、カウントされるコードの範囲を指定する。
わからなくなってきました。要するに、-C instrument-coverage
を与えるとコードの各所で実行回数を数えるようになる、ということでしょうか。
また、実行時にはカウント結果が生成され、profraw
ファイルに吐き出されるようです。LLVMにはこのファイルから様々な形式でレポートを生成するツールが入っているので、いろんなところに流用できるんですね〜
具体的には?
ここからは冒頭でインストールしたllvm-tools
が必要になります。
ここからはサンプルプロジェクトとして↓のリポジトリを使用します。
ちなみにこのライブラリは部内SNSのtraQでBOT作成を楽にするためのものです。
ここではv0.4.0のソースコードで進めていきます。まずはリポジトリをクローンしてHEADを切り替えてからビルド、テストを走らせてみましょう。
$ git clone https://github.com/H1rono/traq-bot-http-rs
$ cd traq-bot-http-rs
$ git switch --detach v0.4.0
$ cargo build
$ cargo test
-C instrument-coverage
を与えることでビルド、実行時にprofraw
ファイルを吐き出すようになります。このオプションは環境変数のRUSTFLAGS
に設定します。
$ export RUSTFLAGS="-C instrument-coverage"
$ cargo build
$ cargo test
RUSTFLAGS="-C instrument-coverage" cargo build
のように指定してもいいのですが、この後にcargo test
とするとRUSTFLAGS
が設定されていない状態で再びビルドが行われてしまうので気をつけてください。
テスト後にls
などで何ができたか確認すると、現在のディレクトリ直下に大量の.profraw
ファイルが生成されていることがわかります。
リポジトリ直下にできてしまうのは嫌ですね。とりあえず、今できたものは消してしまいましょう。
$ rm -f *.profraw
$ cargo clean
今度は環境変数LLVM_PROFILE_FILE
に出力先の設定を施します。profraw
ファイルをリポジトリに上げることはないので、gitignoreに入っているtarget
フォルダ以下に新しくフォルダ(ここではprofile
という名前にします)を作り、その中に結果を吐き出させるといいでしょう。
$ export RUSTFLAGS="-C instrument-coverage"
$ export LLVM_PROFILE_FILE="target/profile/traq-bot-http-rs-%p-%m.profraw"
$ cargo build
$ cargo test
LLVM_PROFILE_FILE
では出力先のパスだけでなく、出力するファイル名のフォーマットも指定します。%p
はプロセスID, %m
はバイナリのシグネチャです。フォーマット指定子の詳細についてはSource-based Code Coverage — Clang 17.0.0git documentation #Running the instrumented programを参考にしてください。とりあえずtarget/profile/{package_name}-%p-%m.profraw
にしておけば問題はないかと思います。(ファイルの出力先、使用場所がパッケージで閉じているのでファイル名は%p-%m.profraw
でもいいかも)
これでprofraw
ファイルをよしなに得ることができました。次はこのprofraw
ファイルからカバレッジ結果を様々な形式で出力していきます。
ここからは冒頭でインストールしたgrcov
が必要になります。
また、ここから先の内容はgrcovのREADMEを参考にしたものです。
HTML
$ grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing -o ./target/coverage/
grcov
のオプションについてはmozilla/grcov #man grcovを参照してください。これでtarget/coverage
フォルダ以下にカバレッジ結果がHTMLで吐き出されます。
$ tree target/coverage
target/coverage
├── badges
│ ├── flat.svg
│ ├── flat_square.svg
│ ├── for_the_badge.svg
│ ├── plastic.svg
│ └── social.svg
├── coverage.json
├── index.html
├── src
│ ├── events.rs.html
│ ├── index.html
│ ├── lib.rs.html
│ ├── parser.rs.html
│ └── payloads
│ ├── channel.rs.html
│ ├── index.html
│ ├── message.rs.html
│ ├── stamp.rs.html
│ ├── system.rs.html
│ ├── tag.rs.html
│ ├── types.rs.html
│ └── user.rs.html
└── tests
├── index.html
├── parser.rs.html
└── payloads.rs.html
5 directories, 22 files
python3 -m http.server -d target/coverage
などでtarget/coverage
をルートとする静的Webサーバーを立てて、ブラウザで開いてみてください。次のようにカバレッジ結果が表示されるはずです。
リンクをクリックしていくと各ファイルの行単位でのカバレッジも見ることができます。
function単位でのカバレッジが異様に低いですが、これに関する考察は後ほど行います。
LCOVファイル生成
LCOVファイルとは、profraw
ファイルからカバレッジ結果をまとめたものです(多分)。LCOVがなんたるかについては↓のリポジトリを参照してください。
grcovでLCOVファイルを得るには次のコマンドを実行します。
$ grcov . -s . --binary-path ./target/debug/ -t lcov --branch --ignore-not-existing -o ./target/coverage.lcov
これでtarget/coverage.lcov
にLCOVファイルが吐き出されます。HTML生成時のオプションとよく似ているので、比較用に並べて置いておきます。
grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing -o ./target/coverage/
grcov . -s . --binary-path ./target/debug/ -t lcov --branch --ignore-not-existing -o ./target/coverage.lcov
VSCode上に表示
Coverage Guttersという拡張機能を使用します。この拡張機能はLCOVファイルからカバレッジ情報をソースコードの表示に反映させてくれるものです。
先程のコマンドでLCOVファイルが得られたら、そのファイルをCoverage Guttersに教えてあげましょう。VSCodeの設定で、Coverage Gutters: Coverage File Namesを編集します。settings.json
に次のように追加しましょう。
json "coverage-gutters.coverageFileNames": [
+ "target/coverage.lcov",
"lcov.info",
"cov.xml",
"coverage.xml",
"jacoco.xml",
"coverage.cobertura.xml"
]
この設定はワークスペース設定、ユーザー設定のどちらでも構いません。他のリポジトリでもすぐにカバレッジを表示できるように、"**/*.lcov"
をユーザー設定に追加するのがもしかすると最適かもしれません。
この状態でステータスバーにあるWatch
ボタンを押すと、カバレッジ結果がエディタに表示されます。
しかしこの状態だとブレークポイントを置けなくなるので注意してください。次の3つの設定を変更することで、カバレッジ表示の場所を変えることができます。
- Coverage Gutters: Show Gutter Coverage
- Coverage Gutters: Show Line Coverage
- Coverage Gutters: Show Ruler Coverage
デフォルトでは1のみがオンになっています。1をオフ、2と3をオンにすると次のようになります。
GitHub Actions
Codecovを使用して、次の2つを自動化します。
- READMEに貼れるカバレッジ結果のバッジ生成
- PR時のカバレッジレポート
Coverallsも同様のことができるらしいですが、ここでは触れません。grcovはCoverallsで使用できるデータ生成もサポートしているので、そちらを参照してください。
まずはabout.codecov.ioでLoginします。GitHubとアカウントを連携させてください。
連携ができたら、CodecovのGitHub Appを開き、設定をいじります。カバレッジを取りたいリポジトリへのアクセス権を与えましょう。
最初に連携した時に自動的に設定画面に移動するかもしれません。最初の設定時に記録を取っていなかったためどうだったか曖昧です。
設定の次はリポジトリでGitHub Actionsを設定します。ここではcodecov/codecov-actionを使用します。↓はpush, pull request時にLCOVファイルを生成してCodecovにアップロードするアクションの例です。
name: Rust
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
env:
RUSTFLAGS: "-C instrument-coverage"
LLVM_PROFILE_FILE: "target/profile/binum-%p-%m.profraw"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Add components
run: rustup component add llvm-tools
- name: Build
run: cargo build
- name: Run tests
run: |
mkdir -p target/profile
cargo test
- name: install grcov
run: cargo install grcov
- name: generate LCOV
run: grcov . -s . --binary-path ./target/debug/ -t lcov --branch --ignore-not-existing -o ./target/coverage.lcov
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
# token: ${{ secrets.CODECOV_TOKEN }}
files: target/coverage.lcov
token
はプライベートリポジトリでCodecovを使用する際に必要になります。トークンの取得はhttps://app.codecov.io/gh/{user}/{repo}/settings
を開くとわかります。GitHubリポジトリのsecretsに設定してあげましょう。
また、この設定ではrustfmtやclippyの適用などをしておらず、キャッシュ設定も行っていないため実際に使用するには物足りません。実際の設定例はtraq-bot-http-rs/rust.yml at main · H1rono/traq-bot-http-rsを参照してください。
カバレッジをCodecovにアップロードできたらどのように表示されるか確認してみましょう。https://app.codecov.io/gh/{user}/{repo}
を開くとカバレッジ結果が出ているはずです。
次はREADMEにCodecovのバッジを貼りましょう。↑と同様の画面でSettings→Badges&Graphsと進むと貼るべきURLが表示されます。
[![codecov](https://codecov.io/gh/H1rono/traq-bot-http-rs/branch/main/graph/badge.svg?token=UEA9118L9I)](https://codecov.io/gh/H1rono/traq-bot-http-rs)
また、Pull Requestを作成するとCodecovが自動的にカバレッジレポートをつけてくれるようになります。
便利ですね。
Appendix: どうしてカバレッジが低いのか
HTML形式でカバレッジ結果を見た時に、Functionsのカバレッジが低いことに言及しました。ここではその原因を考察したいと思います。
どこが悪いのかパッと見てもわかりませんが、よく見るとderive
にもカバレッジがついていることがわかります。
ここから考えられるのは、derive
を用いて生成された関数に対してもテストを書く必要があるということでしょう。derive
はマクロなので、これと同様のことはマクロを使用している場所全てで考慮する必要がありそうです。
derive
を読んでテストを生成するマクロとかできませんかね...
おしまい
明日の担当は@mehm8128さんです!