ys memos
Blog

Rustでcoverage計測する ~2. 可視化~


rust

2025/06/08



ターミナルでカバレッジ計測結果を表示するのはお手軽に状態を確認できて便利である。しかし、このままだと、カバレッジへの対処が若干難しい。

具体的には、どこがカバーされているか(あるいはされていないのか)の切り分けや、多くのファイルが並んでいる場合に、どこから手を付けるのかの判断が難しい。

そこで、カバー状態を、よりコードにフレンドリーに表示・閲覧する手法を2つ紹介する。


準備してあるサンプルコードがmain()のみのものだったので、カバレッジを可視化する意義が薄いので、少しコードを複雑にしてみる。

src/main.rs
type SampleResult<T> = Result<T, Box<dyn std::error::Error>>;

fn main() -> SampleResult<()> {
    let args = std::env::args().collect::<Vec<String>>();
    let message = greet(args)?;
    println!("{message}");
    Ok(())
}

fn greet(args: Vec<String>) -> SampleResult<String> {
    let name = args.get(1).ok_or("")?;
    let age = match args.get(2) {
        Some(value) => Some(value.parse::<u32>()?),
        None => None,
    };

    Ok(match age {
        Some(age) => format!("Hello, {name}! You are {age} years old."),
        None => format!("Hello, {name}!"),
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_main() {
        assert!(main().is_err());
    }
}

サンプルとし、このような構成とした。

  • type SamplResult: サンプル内で使いまわす汎用エラー型
  • fn main(): 実行のエントリポイントのみとして働き、標準出力も行う
  • fn greet(): 処理の本体。これをTest対象として入力のバリエーションを受け入れられ、メッセージを戻り値にしておく

ここでは、エラーハンドリングなどはかなり簡略化している。

fn test_main()でのmain()はargsを読み込むが、引数がない場合はエラーになるのが望ましいので、アサーションを入れておく。


$ cargo llvm-cov

info: cargo-llvm-cov currently setting cfg(coverage); you can opt-out it by passing --no-cfg-coverage
   Compiling coverage v0.1.0 (/<path>/<to>/<project>/coverage)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.23s
     Running unittests src/main.rs (target/llvm-cov-target/debug/deps/coverage-90ad100d3a119901)

running 1 test
test tests::test_main ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Filename                                         Regions    Missed Regions     Cover   Functions  Missed Functions  Executed       Lines      Missed Lines     Cover    Branches   Missed Branches     Cover
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
/<path>/<to>/<project>/coverage/src/main.rs           20                11    45.00%           3                 0   100.00%          18                 8    55.56%           0                 0         -
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL                                                 20                11    45.00%           3                 0   100.00%          18                 8    55.56%           0                 0         -

まず、サンプルコード更新の期待通りにカバレッジは低下し、45%となった。実際にTestの処理を追っていくと、greet()に入ってすぐにargs.get(1)が失敗するので、続く処理は行われず、カバーされないのが原因である。

Executedの数値が100%となっているのは、main()から greet()が呼び出されているためである。

このシンプルなサンプルにおいては、単純に処理を追うだけで十分カバレッジの状態を把握できる。しかし、複数ファイル・modがあり、さらにそれらをまたがってTestされる場合、完全に脳内でカバー範囲を把握するのは難しい。

そのようなシチュエーションでは、カバレッジの可視化を行うと便利である。


追加の準備なく、最も簡単に可視化する方法は、HTMLであろう。 llvm-cov--htmlオプションが組み込まれているので、これを使う。

$ cargo llvm-cov --html

とし、 file:///<path>/<to>/<project>/coverage/target/llvm-cov/html/index.htmlをブラウザで開くと、カバレッジが可視化される。

$ cargo llvm-cov --open

とすると、htmlを生成しつつブラウザが自動で開く。

index.htmlはクレート全体の各ファイルのカバレッジが表示され、ここは主にterminal表示に近い情報を提供する。

ファイル名をクリックすると、ファイルのソースコードとカバーエリアと通過回数が表示され、通っていない行は赤くハイライトされる。


Coverage GuttersというVSCodeの拡張機能を使うと、カバレッジを視覚的に表示できた。

そのためには、lcov.infoとしてカバレッジ計測結果を出力する必要がある。

拡張機能をインストールしたあと、

$ cargo llvm-cov --lcov --output-path target/lcov.info

と実行し、 Command Palette (Ctrl+Shift+p, Cmd+Shift+p) から Coverage Gutters: Display Coverage を選択すると、カバレッジが表示される。

Coverage Gutters: Watchを選択すると、再度上記コマンドを実行したときに自動で表示が更新される。


上のサンプルコードのTestは不足しており、greet()はアサーションがないため、カバレッジは特に低い。

そこで、カバレッジの計測をしつつ、Testをいくつか追加していった。

Testの最終形としては、以下のようになり、

src/main.rs
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_main() {
        assert!(main().is_err());
    }

    #[test]
    fn greet_empty() {
        let args = vec![];
        assert!(greet(args).is_err());
    }

    #[test]
    fn greet_with_name() -> SampleResult<()> {
        let args = vec!["program".to_string(), "Alice".to_string()];
        let message = greet(args)?;
        assert_eq!(message, "Hello, Alice!");
        Ok(())
    }

    #[test]
    fn greet_with_name_and_age() -> SampleResult<()> {
        let args = vec!["program".to_string(), "Bob".to_string(), "30".to_string()];
        let message = greet(args)?;
        assert_eq!(message, "Hello, Bob! You are 30 years old.");
        Ok(())
    }
}

カバレッジはかなり向上した。

$ cargo llvm-cov

info: cargo-llvm-cov currently setting cfg(coverage); you can opt-out it by passing --no-cfg-coverage
   Compiling coverage v0.1.0 (/<path>/<to>/<project>/coverage)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.23s
     Running unittests src/main.rs (target/llvm-cov-target/debug/deps/coverage-90ad100d3a119901)

running 4 tests
test tests::greet_empty ... ok
test tests::greet_with_name_and_age ... ok
test tests::greet_with_name ... ok
test tests::test_main ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Filename                                         Regions    Missed Regions     Cover   Functions  Missed Functions  Executed       Lines      Missed Lines     Cover    Branches   Missed Branches     Cover
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
/<path>/<to>/<project>/coverage/src/main.rs           37                 5    86.49%           6                 0   100.00%          34                 2    94.12%           0                 0         -
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL                                                 37                 5    86.49%           6                 0   100.00%          34                 2    94.12%           0                 0         -

line coverageが100%になっていないのは、 main()の中のgreet()が成功したパターンを通っていないため。

Testの追加によってカバレッジが改善していく様子は、このtestコードを上から順に追加していき、都度カバレッジを計測すると差がわかりやすい。


使ってみた感覚的には、上に上げた可視化手法はだいたい以下の使い分けができそう。

  • html出力 : 広域のカバレッジ計測を一望するのに便利
  • vscode表示: カバレッジ改善

関連タグを探す