type holyshared = Engineer<mixed>

技術的なことなど色々

Rustでlcovのレポートファイルをマージする

f:id:holyshared:20161207124650p:plain

これは Rust Advent Calendar 2016 (2) の 14日目の記事です。

lcovが出力するレポートのパーサー&マージャーをRustと実装しました。

lcovはgcovの拡張版みたいなもので、gcovの出力ファイルから、HTML形式のようなコードカバレッジレポートを出力するツールです。

lcovは出力の過程で、処理しやすいようにgcovの出力ファイルから独自のレポート形式に変換します。

そのレポートをパース&マージできるようにしたものが、今回実装したライブラリになります。

レポートのパース

lcovのレポートはデータをレコードで表現した単純なテキスト形式のファイルです。
各レコードは次のようになっています。
原文はこちらを参考してください。

No. レコード 説明
1 TN:<test name> テスト名
2 SF:<absolute path to the source file> ソースファイルのパス(絶対パス)
3 DA:<line number>,<execution count>[,<checksum>] 左から行番号、実行された回数、該当行のコードのMD5
4 FN:<line number of function> 関数の開始行
5 FNDA:<execution count>,<function name> 左から関数の実行された回数、関数名
6 FNF:<number of functions found> 見つかった関数の数
7 FNH:<number of function hit> 実行された関数の数
8 LH:<number of lines with an execution> 見つかった行の数
9 LF:<number of instrumented lines> 実行された行の数
10 BRDA:<line number>,<block number>,<branch number>,<taken> 左から行番号、ブロック番号、分岐番号、実行された回数
11 BRF:<number of branches found> 見つかった分岐の数
12 BRH:<number of branches hit> 実行された分岐の数
13 end_of_record 終了を示すレコード、ソースコード単位で出力される

レポートをパースするには、次のようにファイルからパーサーを生成して、1レコードずつパースします。

extern crate lcov_parser;

use lcov_parser:: { LCOVParser, LCOVRecord, FromFile };

fn main() {
    let mut parser = LCOVParser::from_file("../../../fixture/report.lcov").unwrap();

    loop {
        match parser.next().expect("parse the report") {
            None => { break; },
            Some(record) => match record {
                LCOVRecord::SourceFile(file_name) => println!("File: {}", file_name),
                LCOVRecord::EndOfRecord => println!("Finish"),
                _ => { continue; }
            }
        }
    }
}

レポートのマージ

さて本題のレポートマージです。
レポートをマージする際に、気をつけなければならいない点はマージするレポートのバージョンです。

マージしたいレポートが同じバージョンのソフトウェアのレポート場合、マージすることは可能ですが、違う場合は基本的にマージすることができません。

なぜマージできないかというと、どのバージョンの時、コードカバレッジはどの程度だったか?が知りたいわけで、異なるバージョンのレポートをマージすると、正しいレポートにならないためです。

結果
v1+ v1 v1
v1+ v2 マージできない(バージョンが違う)

この為、マージする際にレポートのチェックサムを利用します。
チェックサムは、DAレコードに情報として含まれています。(lcovのchecksumオプションを使用した場合)

このチェックサム値はソースコードの特定の行のMD5値になっており、同じバージョンのソフトウェアの場合、 値が同値になります。

下記の例だと、fixture.cの6行目のMD5値がPF4Rz2r7RTliO9u6bZ7h6gという意味になります。

SF:/Users/holyshared/Documents/projects/lcov-parser/tests/fixtures/merge/fixture.c
......
......
......
......
DA:6,2,PF4Rz2r7RTliO9u6bZ7h6g

このようなチェックを行いつつ、マージしてくれる関数も実装しています。
save_asを使用することで、出力ファイルを指定できます。

extern crate lcov_parser;

use lcov_parser:: { merge_files };

fn main() {
    let trace_files = [
        "../../../tests/fixtures/fixture1.info",
        "../../../tests/fixtures/fixture2.info"
    ];
    let _ = match merge_files(&trace_files) {
        Ok(report) => report.save_as("/tmp/merged_report.info"),
        Err(err) => panic!(err)
    };
}

まとめ

最初は、パーサーだけ実装する予定でしたが、lcovのPerlのコードを読んでいて、ついでだからマージもできるようにするかと思い実装してみました。 既存のプロダクトのコードの移植していく作業は、普段つかっているものの理解力があがるので、勉強になりますね。