💯

ruby のカバレッジを vscode で可視化する

2023/05/31に公開

ruby には Coverage という組み込み class があり、簡単にカバレッジを計測できます。
https://docs.ruby-lang.org/ja/latest/class/Coverage.html
以下のようにするだけで rspec を実行しつつカバレッジを取得できます。

RSpec.configure do |config|
  config.before :suite do
    Coverage.start
  end
  config.after :suite do
    # Coverage.result を出力
  end
end

vscode でカバレッジを表示するツールを検索し、以下の2つを発見しました。

まず1つ目は、ファイル内の行ごとにインジケータを表示してくれる Coverage Gutters
https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters

2つ目は、ファイルツリーでカバレッジをパーセンテージ表示してくれる Koverage
https://marketplace.visualstudio.com/items?itemName=tenninebt.vscode-koverage

どちらも lcov というフォーマットに対応しているので、 Coverage の結果を lcov に変換すればこれらのツールを使って可視化ができます。
lcov はシンプルなテキストファイルで、以下の comment に仕様がまとまっていました。

https://github.com/linux-test-project/lcov/issues/113#issuecomment-762335134

TN: test name
SF: source file path
FN: line number,function name
FNF:  number functions found
FNH: number hit
BRDA: branch data: line, block, (expressions,count)+
BRF: branches found
DA: line number, hit count
LF: lines found
LH:  lines hit.

この中から、 Coverage Gutters と Koverage が対応している SFDA をとりあえず出力することにします。

def write_lcov(result)
  # result には gem の中のファイルも全て含んでいるので、カレントディレクトリ以下のみを抽出
  ret = result.filter{|k,v| k.starts_with? Dir.pwd }

  open('lcov.info', 'w') do |f|
    ret.each do |k,v|
      f.puts "SF:#{k}" # source file path
      
      # line_stub は ruby ファイル内の実行可能行のリストを返す
      line_stub = Coverage.line_stub(k)
      line_stub.each.with_index(1) do |l,i|
        next if l.nil?
        hit = v[:oneshot_lines].include?(i)
        f.puts "DA:#{i},#{hit ? 1 : 0}" # line number, hit count
      end
      f.puts "end_of_record"
    end
  end
end

後は、 Coverage の result を渡すように先ほどの RSpec.configure を少し書き換えます。

RSpec.configure do |config|
  if ENV['COV']
    config.before(:suite) { Coverage.start(oneshot_lines: true) }
    config.after :suite do
      write_lcov(Coverage.result)
    end
  end
end

これで、 COV=1 bin/rspec などと実行すれば lcov.info が出力され、 Coverage gutters と koverage が結果を可視化してくれます。

ハートレイルズ

Discussion