feature image

2023年3月18日 | ブログ記事

code_coverage.rs

この記事は新歓ブログリレー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種類あるようです。

  1. GCCと互換性がある、gcovをベースとしたカバレッジ実装。-Z profileで有効になる
  2. LLVMネイティブの機能を用いた、ソースコードに基づくカバレッジ実装。-C instrument-coverageで有効になる

オプションの具体的な与え方は後述します。-Zのオプションはexperimentalなので、Nightly Rustを使っていない私は-C instrument-coverageを使うことにします。

-C instrument-coverageを与えると、RustコンパイラはRustプログラムに対して次の操作を施すようです:

わからなくなってきました。要するに、-C instrument-coverageを与えるとコードの各所で実行回数を数えるようになる、ということでしょうか。

また、実行時にはカウント結果が生成され、profrawファイルに吐き出されるようです。LLVMにはこのファイルから様々な形式でレポートを生成するツールが入っているので、いろんなところに流用できるんですね〜

具体的には?

ここからは冒頭でインストールしたllvm-toolsが必要になります。

ここからはサンプルプロジェクトとして↓のリポジトリを使用します。

GitHub - H1rono/traq-bot-http-rs: traQ BOTのHTTPリクエストパーサー
traQ BOTのHTTPリクエストパーサー. Contribute to H1rono/traq-bot-http-rs development by creating an account on GitHub.

ちなみにこのライブラリは部内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サーバーを立てて、ブラウザで開いてみてください。次のようにカバレッジ結果が表示されるはずです。

coverage-html

リンクをクリックしていくと各ファイルの行単位でのカバレッジも見ることができます。

function単位でのカバレッジが異様に低いですが、これに関する考察は後ほど行います。

LCOVファイル生成

LCOVファイルとは、profrawファイルからカバレッジ結果をまとめたものです(多分)。LCOVがなんたるかについては↓のリポジトリを参照してください。

GitHub - linux-test-project/lcov: LCOV
LCOV. Contribute to linux-test-project/lcov development by creating an account on GitHub.

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ボタンを押すと、カバレッジ結果がエディタに表示されます。

coverage-vscode

しかしこの状態だとブレークポイントを置けなくなるので注意してください。次の3つの設定を変更することで、カバレッジ表示の場所を変えることができます。

  1. Coverage Gutters: Show Gutter Coverage
  2. Coverage Gutters: Show Line Coverage
  3. Coverage Gutters: Show Ruler Coverage

デフォルトでは1のみがオンになっています。1をオフ、2と3をオンにすると次のようになります。

coverage-vscode-2

GitHub Actions

Codecovを使用して、次の2つを自動化します。

  1. READMEに貼れるカバレッジ結果のバッジ生成
  2. 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}を開くとカバレッジ結果が出ているはずです。

codecov-report

次は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)

codecov

また、Pull Requestを作成するとCodecovが自動的にカバレッジレポートをつけてくれるようになります。

codecov-pr-report-1

便利ですね。

Appendix: どうしてカバレッジが低いのか

HTML形式でカバレッジ結果を見た時に、Functionsのカバレッジが低いことに言及しました。ここではその原因を考察したいと思います。

どこが悪いのかパッと見てもわかりませんが、よく見るとderiveにもカバレッジがついていることがわかります。

coverage-derive

ここから考えられるのは、deriveを用いて生成された関数に対してもテストを書く必要があるということでしょう。deriveはマクロなので、これと同様のことはマクロを使用している場所全てで考慮する必要がありそうです。

deriveを読んでテストを生成するマクロとかできませんかね...

おしまい

明日の担当は@mehm8128さんです!

参考文献

H1rono_K icon
この記事を書いた人
H1rono_K

趣味プログラマー(大学生)

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2023年4月17日
ポケモンを飼いたい夢を叶える
tqk icon tqk
2023年4月25日
【驚愕】作曲4年目だった男が大学3年間ゲームサウンドに関わった末路...【ゲームサウンドのお仕事について】
tenya icon tenya
2023年3月20日
traPグラフィック班の活動紹介(Ver.2023)
NABE icon NABE
2023年4月27日
Vulkanのデバイスドライバを自作してみた
kegra icon kegra
2023年4月25日
15時間でゲームを作った #Oxygenator
Komichi icon Komichi
2022年9月26日
競プロしかシラン人間が web アプリ QK Judge を作った話
tqk icon tqk
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記