👌

Buildkite で実行される RSpec のカバレッジを SimpleCov で計測してレポートを見る

2024/11/05に公開

今回やったこと

  • SimpleCov による RSpec カバレッジの計測
  • Buildkite で並列にテストを実行してレポートをまとめる
  • (PR への push の場合)レポートページのリンクを PR にコメント

実装

カバレッジ計測

SimpleCov を Gemfile に追加し spec_helper.rb に以下のコードを追加します。
SimpleCov.start に 'rails' 引数を渡さないと後々並列テストのレポートをマージする時にうまくいきませんでした。

require 'simplecov'
SimpleCov.start 'rails'

テスト実行

並列テスト

テストの並列実行は builkite の parallelism 設定と knapsack を利用しています。
knapsack すでには knapsack pro というサービスに移行していてレポートに応じて最適な時間配分でテストを割り振ることはできなくなってしまっていますが、実行時間を考慮しないで単純にテストを割り振るのは knapsack でも可能なので、今回は knapsack で単純な分割のみを行なっています。
knapsack による並列テストは spec_helper.rb に以下のコードを追加して knapsack コマンドから実行します。

require 'knapsack'
Knapsack::Adapters::RSpecAdapter.bind
bundle exec rails knapsack:rspec

カバレッジレポートのマージ

SimpleCov では SimpleCov.collate という複数のレポートをマージする機能が提供されています。
この機能を使うには並列テストを実行したコンテナごとに生成されたカバレッジレポートを一箇所に集める必要があり、今回は各並列テストのレポートを S3 にアップロードする法方をとりました。
今回はテストの実行やレポートのマージといったステップごとの処理をシェルにして pipeline.yml から呼び出す構成にしています。

.buildkite
├ pipeline.yml
└ script
  ├ rspec.sh
  └ coverage.sh
lib
└ tasks
  └ coverage.rake

pipeline.yml

env:
  BUILDKITE_PLUGIN_DOCKER_COMPOSE_CONFIG: docker-compose.yml
  BUILDKITE_PLUGIN_DOCKER_COMPOSE_IMAGE_REPOSITORY: 265438772420.dkr.ecr.us-west-2.amazonaws.com/buildkite-prebuild

steps:
  - label: "Build"
    plugins:
      - docker-compose#v3.9.0:
          build: web

  - wait
    
  - label: "rspec"
    commands: . /script/rspec.sh
    parallelism: 5
    plugins: &plugins
      - docker-compose#v3.9.0:
          run: web

  - label: "coverage"
    commands: . /script/coverage.sh
    plugins: *plugins

rspec.sh

knapsack を利用した rspec の実行とカバレッジレポートの S3 へのアップロード。
カバレッジレポートは SimpleCov を入れて rspec を実行すると自動で出力される。

#!/bin/bash

bundle exec rails knapsack:rspec

base_url=s3://buildkite/${BUILDKITE_PIPELINE_SLUG}/tmp_${BUILDKITE_BUILD_NUMBER}/coverage_report
filename=${BUILDKITE_PARALLEL_JOB}.json
aws s3 cp ./coverage/.resultset.json ${base_url}/${filename};

coverage.sh

各テストコンテナから S3 にアップしたレポートのダウンロードとマージ。
マージ後は coverage ディレクトリにマージされたレポートが出力される。

#!/bin/bash

tmp_dir=s3://buildkite/${BUILDKITE_PIPELINE_SLUG}/tmp_${BUILDKITE_BUILD_NUMBER}/coverage_report

# download reports from S3
mkdir -p tmp/coverage
aws s3 cp ${tmp_dir}/ tmp/coverage --recursive

# aggregate reports
result=$(bundle exec rails coverage:report)
echo $result

# clear temp files
rm -f ${filename}*.json
rm -f tmp.json
aws s3 rm ${tmp_dir}/ --recursive

lib/tasks/coverage.rake

namespace :coverage do
  desc "Collates all result sets generated by the different test runners"
  task report: :environment do
    require 'simplecov'
    SimpleCov.start

    dir = Rails.root.join('tmp/coverage/*.json')
    SimpleCov.collate Dir[dir], 'rails'
  end
end

PR へのレポートコメント

今のままだと buildkite の coverage ステップのコンテナ内でレポートがマージされて終了になっているので、これを参照できるようにします。
今回は SimpleCov が出力するカバレッジ可視化用の HTML を buildkite の artifact としてアップロードすることにしました。
また、毎回 buildkite のレポートページから artifact を開くのは少し面倒なので、GitHub の PR にレポート結果とレポートページへのリンクをコメントするようにもします。

artifact アップロード

artifact のアップロードは buildkite-agent artifact upload を利用します。
buildkite-agent を利用するためには buildkite 上で実行されるコンテナが buildkite-agent コマンドを利用できるようにする必要があるので、pipeline.yml で docker-compose プラグインに mount-buildkite-agent を追加します。

  - label: "rspec"
    commands: . /script/rspec.sh
    parallelism: 5
    plugins: &plugins
      - docker-compose#v3.9.0:
          run: web
          mount-buildkite-agent: true  # New

次に buildkite の artifact としてカバレッジレポートをアップロードするために coverage.sh を編集します。
マージ後に出力される HTML はローカルの assets を参照しており HTML 単品では見れたものじゃありませんが、SimpleCov の HTML 用のリポジトリが GitHub で公開されているので、github-cdn-converter という GitHub 上の CDN のように HTML から読み込めるようにしてくれるツールを使って GitHub の assets を参照するようにします。

# aggregate reports
result=$(bundle exec rails coverage:report)
echo $result
sed -i 's#\./assets/0.12.3#https://cdn.jsdelivr.net/gh/simplecov-ruby/simplecov-html@main/public#g' coverage/index.html  # assets の参照先をローカルから GitHub に差し替え

# upload reports as buildkite artifact
buildkite-agent artifact upload coverage/.resultset.json
buildkite-agent artifact upload coverage/index.html

こうすることで buildkite の coverage ステップの artifact にレポートが表示されるようになります。

alt text

PR へのコメント

PR へのコメントは以下のようなフォーマットにします。

Coverage of [${commit}](${commit_url}) is ${coverage} [Report](${artifact_url})

commit = short commit hash
commit_url = GitHub のコミットページへの URL
coverage = レポートマージ時に出力されるメッセージに含まれる 'N / M LOC (XX.X%).'
artifact_url = artifact としてアップロードした HTML の URL

最終的に以下のコードを coverage.sh に追加しました。

if [ -n "${BUILDKITE_PULL_REQUEST}" ]; then
  coverage=$(echo "$result" | cut -c134-)
  commit=$(echo "$BUILDKITE_COMMIT" | cut -c1-7)
  commit_url=https://github.com/git-hub-user/repository/pull/${BUILDKITE_PULL_REQUEST}/commits/${commit}

  buildkite_url=https://api.buildkite.com/v2/organizations/git-hub-user/pipelines/repository/builds/${BUILDKITE_BUILD_NUMBER}/jobs/${BUILDKITE_JOB_ID}/artifacts
  artifacts=$(curl ${buildkite_url} -X GET -H "Authorization: Bearer ${BUILDKITE_API_TOKEN}")
  artifact_id=$(echo "$artifacts" | jq -r '.[] | select(.path == "coverage/index.html") | .id')
  artifact_url=https://buildkite.com/organizations/git-hub-user/pipelines/repository/builds/${BUILDKITE_BUILD_NUMBER}/jobs/${BUILDKITE_JOB_ID}/artifacts/${artifact_id}

  message="Coverage of [${commit}](${commit_url}) is ${coverage} [Report](${artifact_url})"
  clean_message=$(echo "$message" | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g')
  body=$(jq -n --arg message "$clean_message" '{body: $message}')

  curl -X POST \
       -H "Authorization: Bearer ${GITHUB_API_TOKEN}" \
       -H "application/vnd.github+json" \
       -H "X-GitHub-Api-Version: 2022-11-28" \
       "https://api.github.com/repos/git-hub-user/repository/issues/${BUILDKITE_PULL_REQUEST}/comments" \
       -d "$body"
fi

PR かどうかの判定

PR への push ではない場合はコメント先がないので PR への push の場合のみ処理をするようにしています。
BUILDKITE_PULL_REQUEST で PR の ID が取得できるので、これの有無で判定しています。

artifact へのリンク取得

artifact にアップロードした HTML へのリンクは buildkite が提供している Artifact API を使って取得しています。
URL には artifact の ID が必要なため JOB(ステップ)の artifact を全て取得して path で判定して HTML artifact の ID を取得しています。

ENECHANGE

Discussion