こんにちは、dev.aokiです。
今年は実装よりは要件定義や設計寄りの仕事が多くてネタが少なかったのですが、
自分的に今年イチハマり散らかした残念話をせっかくなので記事にしようと思います。
自動カバレッジ計測
テストカバレッジの自動計測と可視化に外部サービスを利用するのはあるあるだと思います。
我々は以前から会社でCircleCIとCodecovとを契約してCIとカバレッジ率計測を利用させて頂いていましたのですが、
今回社内事情によってカバレッジ率計測が別サービスへの移行が必要となり、Coverallsを採用することになりました。
ちなみに、CodecovとCoverallsの差分や特徴もありますが、本論とズレるのでこの記事では網羅的には扱いません、あしからず。
PARALLEL BUILDS WEBHOOK
ということで全社内プロダクトが一斉にCIのフローに対して切替をすることになります。
単一言語のプロダクトはまぁいいとして、僕の担当しているプロダクトの1つは3言語(Scala, Clojure, Python)扱っているものがありました。
まぁCodecovのときは同じワークフローで順繰りにおくれば勝手にレポートをマージしてくれるので
同じ感じで大丈夫だろうと想定していました。
ですがCoverallsは実際には勝手にはやってくれず、意図的にPARALLEL BUILDS WEBHOOKという機能を利用して実現する必要がありました。
詳細はリンク先を見て欲しいのですが、ざっくり言うと以下2点の対応が必要になります。
- 意図的に
COVERALLS_PARALLEL=true
という環境変数と値をセットして動かす - それぞれのワークフローでカバレッジ計測が終わった後、完了した旨を専用のエンドポイントを叩いて通知する
なるほど自動じゃないのね、とがっくりくるものの、まぁこれくらいはいいでしょう。
Coverallsが引き取れるレポート形式にハマる
ではでは実際に各言語のカバレッジ計測部分の対応を始めます。
Scalaのカバレッジ送信はsbt-scoverageで作成したレポートを素直にcurl
で送ってました。
Codecovは様々なレポートタイプをサポートしているため、あまり手元で苦労することがなかったです。
ではCoverallsではどうかな?と思ってAPI仕様を調べてみましょう。
OBJECTS: SOURCE FILE
って記載のあるところに定義が示されてますが、独自の形式が必須っぽいですね。
lcov
とかみたいなノリのものではなさそうですし、当然フツーにscoverage
が出力したCobertura
形式のレポートはダメでした。
さてどうするかーと思ったところ、sbt-scoverage
のREADMEに記載が。
## 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の利用を検討します。
sbt-coverallsにハマる
sbt-coveralls
は名前の通り、scoverageが出力したレポートをCoveralls用に変換して送ってくれる便利なやつでした。
おーいーじゃんいーじゃんこれ、と思ってさっくり利用を始めようとしたのですが、ここで問題発生。
PARALLEL BUILDS WEBHOOKで記載した、並列でレポートを送る環境変数をセットする口が見当たらない。
なんじゃこりゃと思ってWebを漁ったら出てきました、こちら。
https://github.com/scoverage/sbt-coveralls/issues/125
放置されてますよこれ。ギャース。
しょうがないので、出力したCoveralls指定のJSON形式だけもらって自力でcurl
叩くかーとか思ったんですが、これも不発。
結果JSONを出力するような機能もございませんでした。無念。
結局どうしたかというと、お恥ずかしい話ですが力技です。
CoberturaをCoveralls形式にいい感じに変換してくれるツールないかなーとか探したもの見当たらず、
結局こちらを参考に、自力でCobertura形式のレポートをJSONに変換して投げつける対応にしました。
他言語でも環境変数周りでハマる
次はまとめてClojureとPythonの話ですが、こちらは割とシンプルでした。
Clojureはcloverage、Pythonはcoverallsを利用してカバレッジをCoveralls指定形式のJSONとして出力できました。
じゃあこれをcurlで送るだけ…と思ったらここも問題発生。
どういうわけか、CircleCIのビルド番号などのレポートマージで利用する情報が軒並み欠落していました。
えーっなんでーって思うんですが、出てないもんはしょうがない。
ここも力技でお恥ずかしいのですが、jqで自力でぶち込みました。
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コードでうまいことやってくれているのですが(あちらのチームで利用したものを 拝借 共有していただきました、感謝)、結果的には同じ処理です。
#!/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週間ぐらい溶かしました、大変お恥ずかしいし、チームの皆さんにはご迷惑をおかけしました、申し訳ない。
といったところで今回の記事は以上です。またどこかで。