複数言語利用プロダクトのカバレッジ計測をCodecovからCoverallsに移行したらかなり消耗した話

こんにちは、dev.aokiです。
今年は実装よりは要件定義や設計寄りの仕事が多くてネタが少なかったのですが、
自分的に今年イチハマり散らかした残念話をせっかくなので記事にしようと思います。

## 自動カバレッジ計測
テストカバレッジの自動計測と可視化に外部サービスを利用するのはあるあるだと思います。
我々は以前から会社で[CircleCI](https://circleci.com/ja/)と[Codecov](https://codecov.io/)とを契約してCIとカバレッジ率計測を利用させて頂いていましたのですが、
今回社内事情によってカバレッジ率計測が別サービスへの移行が必要となり、[Coveralls](https://coveralls.io/)を採用することになりました。

ちなみに、CodecovとCoverallsの差分や特徴もありますが、本論とズレるのでこの記事では網羅的には扱いません、あしからず。

## PARALLEL BUILDS WEBHOOK
ということで全社内プロダクトが一斉にCIのフローに対して切替をすることになります。
単一言語のプロダクトはまぁいいとして、僕の担当しているプロダクトの1つは3言語(Scala, Clojure, Python)扱っているものがありました。
まぁCodecovのときは同じワークフローで順繰りにおくれば[勝手にレポートをマージしてくれる](https://docs.codecov.io/docs/merging-reports)ので
同じ感じで大丈夫だろうと想定していました。

ですがCoverallsは実際には勝手にはやってくれず、意図的に[PARALLEL BUILDS WEBHOOK](https://docs.coveralls.io/parallel-build-webhook)という機能を利用して実現する必要がありました。
詳細はリンク先を見て欲しいのですが、ざっくり言うと以下2点の対応が必要になります。

1. 意図的に `COVERALLS_PARALLEL=true` という環境変数と値をセットして動かす
2. それぞれのワークフローでカバレッジ計測が終わった後、完了した旨を専用のエンドポイントを叩いて通知する

なるほど自動じゃないのね、とがっくりくるものの、まぁこれくらいはいいでしょう。

## Coverallsが引き取れるレポート形式にハマる
ではでは実際に各言語のカバレッジ計測部分の対応を始めます。
Scalaのカバレッジ送信は[sbt-scoverage](https://github.com/scoverage/sbt-scoverage)で作成したレポートを素直に`curl`で送ってました。
Codecovは[様々なレポートタイプをサポートしている](https://docs.codecov.io/docs/supported-report-formats)ため、あまり手元で苦労することがなかったです。
ではCoverallsではどうかな？と思ってAPI仕様を調べてみましょう。

https://docs.coveralls.io/api-reference

`OBJECTS: SOURCE FILE` って記載のあるところに定義が示されてますが、独自の形式が必須っぽいですね。
`lcov`とかみたいなノリのものではなさそうですし、当然フツーに`scoverage`が出力した`Cobertura`形式のレポートはダメでした。

さてどうするかーと思ったところ、`sbt-scoverage`のREADMEに記載が。

```markdown
## Coveralls
If you have an open source project then you can add code coverage metrics with the excellent website https://coveralls.io/ Scoverage will integrate with coveralls using the sbt-coveralls plugin.
```

というわけで[sbt-coveralls](https://github.com/scoverage/sbt-coveralls)の利用を検討します。

## sbt-coverallsにハマる
`sbt-coveralls`は名前の通り、scoverageが出力したレポートをCoveralls用に変換して送ってくれる便利なやつでした。
おーいーじゃんいーじゃんこれ、と思ってさっくり利用を始めようとしたのですが、ここで問題発生。
[PARALLEL BUILDS WEBHOOK](#PARALLEL_BUILDS_WEBHOOK)で記載した、並列でレポートを送る環境変数をセットする口が見当たらない。
なんじゃこりゃと思ってWebを漁ったら出てきました、こちら。
https://github.com/scoverage/sbt-coveralls/issues/125

放置されてますよこれ。ギャース。

しょうがないので、出力したCoveralls指定のJSON形式だけもらって自力で`curl`叩くかーとか思ったんですが、これも不発。
結果JSONを出力するような機能もございませんでした。無念。

結局どうしたかというと、お恥ずかしい話ですが力技です。
CoberturaをCoveralls形式にいい感じに変換してくれるツールないかなーとか探したもの見当たらず、
結局[こちら](https://medium.com/@probablyup/multi-language-single-repository-code-coverage-analysis-made-simple-972b2e11b2d2)を参考に、自力でCobertura形式のレポートをJSONに変換して投げつける対応にしました。

## 他言語でも環境変数周りでハマる
次はまとめてClojureとPythonの話ですが、こちらは割とシンプルでした。
Clojureは[cloverage](https://github.com/cloverage/cloverage)、Pythonは[coveralls](https://pypi.org/project/coveralls/)を利用してカバレッジをCoveralls指定形式のJSONとして出力できました。

じゃあこれをcurlで送るだけ…と思ったらここも問題発生。
どういうわけか、CircleCIのビルド番号などのレポートマージで利用する情報が軒並み欠落していました。

えーっなんでーって思うんですが、出てないもんはしょうがない。
ここも力技でお恥ずかしいのですが、jqで自力でぶち込みました。

```bash
cat coveralls.json \
  | jq ". |= .+ {\"service_number\": \"${CIRCLE_WORKFLOW_ID}\"}" \
  | jq ". |= .+ {\"service_job_number\": \"${CIRCLE_BUILD_NUM}\"}" \
  | jq ". |= .+ {\"service_name\": \"circleci\"}" \
  | jq ". |= .+ {\"parallel\": ${COVERALLS_PARALLEL}}" \
  > ./coveralls_enriched.json
```

力技が過ぎる…ま、まぁ動いているので。
Clojureの方は厳密には @lagenorhynque さんが書いてくれたかっこいいClojureコードでうまいことやってくれているのですが(あちらのチームで利用したものを ~~拝借~~ 共有していただきました、感謝)、結果的には同じ処理です。

```clojure
#!/usr/bin/env bb
(ns coveralls
  (:require [babashka.curl :as curl]
            [cheshire.core :as cheshire]
            [clojure.java.io :as io]
            [clojure.java.shell :refer [sh]]))

(def original-json-file "target/coverage/coveralls.json")
(def api-source-directory-path "product-dir/src/clj/")

(defn git-head [id]
  (->> (sh "git" "cat-file" "-p" id)
       :out
       (re-find #"(?m)\nauthor (.+?) <([^>]*)>.+\ncommitter (.+?) <([^>]*)>.+[\S\s]*?\n\n(.*)")
       rest
       (zipmap [:author_name
                :author_email
                :committer_name
                :committer_email
                :message])
       (merge {:id id})))

(defn add-source-path-prefix [m]
  (update m :source_files
          (partial mapv #(update % :name (partial str api-source-directory-path)))))

(defn enrich-json [m]
  (-> m
      (dissoc :service_job_id)
      (assoc :repo_token (System/getenv "COVERALLS_REPO_TOKEN")
             :service_number (System/getenv "CIRCLE_WORKFLOW_ID")
             :service_job_number (System/getenv "CIRCLE_BUILD_NUM")
             :service_pull_request (re-find #"\d+$"
                                            (or (System/getenv "CI_PULL_REQUEST") ""))
             :git {:head (git-head (System/getenv "CIRCLE_SHA1"))
                   :branch (System/getenv "CIRCLE_BRANCH")})
      (cond-> (System/getenv "COVERALLS_SERVICE_JOB_NUMBER") (assoc :service_job_number
                                                                    (System/getenv "COVERALLS_SERVICE_JOB_NUMBER"))
              (System/getenv "COVERALLS_SERVICE_JOB_ID") (assoc :service_job_id
                                                                (System/getenv "COVERALLS_SERVICE_JOB_ID"))
              (System/getenv "COVERALLS_PARALLEL") (assoc :parallel true)
              (System/getenv "COVERALLS_SERVICE_NAME") (assoc :service_name
                                                              (System/getenv "COVERALLS_SERVICE_NAME"))
              (System/getenv "COVERALLS_FLAG_NAME") (assoc :flag_name
                                                           (System/getenv "COVERALLS_FLAG_NAME")))))

(defn -main [file-name]
  (-> original-json-file
      slurp
      (cheshire/parse-string true)
      add-source-path-prefix
      enrich-json
      (cheshire/generate-string {:pretty true})
      (#(spit file-name %)))
  (curl/post "https://coveralls.io/api/v1/jobs"
             {:form-params {"json_file" (io/file file-name)}}))

(-main "coveralls.json")
```

という訳でこんな対応をして無事Coveralls対応が完了しましたとさ。

## 感想
Coveralls、Codecovと比較すると色々と使う側にコストを求める作りだなとは思いました。
よしなに送り付けてよしなにレポートまとめて出してくれるという感じではなく、
あちらが定義した形を求めるし、フローもきっちり指定しないとその通りは動かないよ！っていうスタンスだなと。

加えてちょっと不便なのは、エコシステムがあまり発展していない感じがするのと、
TravisCIで使われることが多いためか、特にCircleCIについての対応が未整備な印象を持っています。
時間が解決するのか、いやあんまり困ってる人いないのかな、うーん、という感じです。

ちなみに結果だけ見るとそんなに大して苦労してなさそうに見えますが、原因がわからなくてウンウン唸ったりしてた時間も長く、何気にこれ対応するのに2週間ぐらい溶かしました、大変お恥ずかしいし、チームの皆さんにはご迷惑をおかけしました、申し訳ない。


といったところで今回の記事は以上です。またどこかで。