CircleCI で並列実行した RSpec のカバレッジを Coveralls に送る


Coveralls に以下のドキュメントがあるのだけど、書いてある通りにやってもうまくいかなかったので、なんとかうまくいった方法を書いておきます。

やりたいこと

RSpec を並列で実行し、すべてのインスタンスの終了を待って、別のジョブで各インスタンスのカバレッジの結果をマージして Coveralls に送信したい。

  • ドキュメントには Coveralls.wear_merged! を呼んで最後に coveralls:push を実行しろとあるけれど、coveralls:push (Coveralls.push!) で使われている SimpleCov::ResultMerger.merged_result同じ環境で実行された 複数のテストのカバレッジ結果をマージするものであり、違う環境で実行された結果をマージできるわけではない。
  • Webhookを使う方法もあるけれど、テストを実行するジョブと結果を送信するジョブが違うので、結果を送信するときに payload[build_num] を指定することができない。

やり方

Gemfile
group :test do
  gem 'coveralls', '~> 0.8.23', require: false
end

RSpec

Coveralls.wear! は Coveralls にカバレッジを送信するけれど、Coveralls.wear_merged! は送信しない(SimpleCov を実行してるだけ)。

spec/rails_helper.rb
if ENV['COVERALLS_REPO_TOKEN']
  require 'coveralls'
  Coveralls.wear_merged!('rails')
end

# [...]

カバレッジ結果をマージ・送信する Rake Task をつくる

SimpleCov の README にある "Merging test runs under different execution environments" の通りにやればいいのだけど、Coveralls の gem が依存している SimpleCov のバージョンが古くて、SimpleCov.collate が存在しない。よって自分でつくる必要がある。

lib/tasks/coverage.rake
namespace :coverage do
  desc 'Collate all result sets and push to Coveralls'
  task push: %i[environment collate] do
    require 'coveralls'
    Coveralls.push!
  end

  desc 'Collate all result sets generated by the different test runners'
  task collate: :environment do
    require 'simplecov'

    # coverage/.resultset-*.json を coverage/.resultset.json にまとめる
    # see: https://github.com/colszowka/simplecov/blob/v0.18.5/lib/simplecov.rb#L80

    results = Dir['coverage/.resultset-*.json'].flat_map do |filename|
      (JSON.parse(File.read(filename)) || {}).map do |command_name, coverage|
        SimpleCov::Result.from_hash(command_name => coverage)
      end
    end

    SimpleCov::ResultMerger.merge_results(*results).tap do |result|
      SimpleCov::ResultMerger.store_result(result)
    end
  end
end

CircleCI

.circleci/config.yml
version: 2.1

executors:

  app:
    docker:
      - image: circleci/ruby:2.6.5-node-browsers
        environment:
          BUNDLE_PATH: vendor/bundle
          RAILS_ENV: test

jobs:

  test:
    executor: app
    parallelism: 2
    steps:
      - checkout

      # [...]

      - persist_to_workspace:
          root: .
          paths:
            - .

      - run:
          name: RSpec in parallel
          command: bin/rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)

      # coverage/.resultset.json にカバレッジ結果が生成されているので、
      # 名前がぶつからないように別の名前を付けたファイルを persist_to_workspace する
      - run:
          name: Prepare to collate coverage results
          command: cp coverage/.resultset.json "coverage/.resultset-${CIRCLE_NODE_INDEX}.json"

      - persist_to_workspace:
          root: .
          paths:
            - coverage/.resultset-*.json

  report:
    executor: app
    steps:
      - attach_workspace:
          at: .

      # こんな感じになっているはず
      # ./coverage
      #   .resultset-0.json
      #   .resultset-1.json

      - run: bin/rake coverage:push

workflows:
  version: 2

  test-report:
    jobs:
      - test
      - report:
          requires:
            - test

できた

#!/bin/bash -eo pipefail
bin/rake coverage:push
Running via Spring preloader in process 81
[Coveralls] Submitting to https://coveralls.io/api/v1
[Coveralls] Job #132.1
[Coveralls] https://coveralls.io/jobs/XXXXXXXX
Coverage is at 100.0%.
Coverage report sent to Coveralls.
CircleCI received exit code 0