Gitlab のマージリクエストで PHPUnit のコードカバー率の差分を表示する

PHPUnit のコードカバー率がマージリクエスト(MR)の前後でどのように変化したかの差分を MR の画面に表示するようにしてみたメモ。

.gitlab-ci.yml は次のような内容になります。phpunit でテストを実行するジョブと phpcov でコードカバレッジを計測するジョブが分かれていますがこれは本題とは関係ありません。実際のプロジェクトでそうしていることが多いため、この例でも同じようにしているだけです。

image: ngyuki/php-dev

stages:
  - test
  - coverage

test:
  stage: test
  only:
    - master
    - merge_requests
  script:
    - composer install --prefer-dist --no-progress --no-suggest --ansi
    - mkdir -p phpcov/
    - phpdbg -qrr vendor/bin/phpunit --coverage-php=phpcov/test.cov
  cache:
    paths:
      - vendor/
  artifacts:
    paths:
      - phpcov/
    expire_in: 1 days

.coverage:
  stage: coverage
  needs:
    - test
  script: &coverage_script
    - composer install --prefer-dist --no-progress --no-suggest --ansi
    - vendor/bin/phpcov merge phpcov/ --text=coverage.txt
    - sed -r '/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/d' -i coverage.txt
  cache:
    paths:
      - coverage.txt
      - vendor/

coverage:
  extends: .coverage
  only:
    - master
  cache:
    key: coverage--$CI_COMMIT_REF_NAME
    policy: push

coverage-mr:
  extends: .coverage
  only:
    - merge_requests
  cache:
    key: coverage--$CI_MERGE_REQUEST_TARGET_BRANCH_NAME
    policy: pull
  script:
    - test -e coverage.txt && mv -f coverage.txt coverage.orig.txt || true
    - *coverage_script
    - test -e coverage.orig.txt && diff -u -w coverage.orig.txt coverage.txt

コードカバレッジを計測するジョブで phpcov merge--text でテキストのコードカバレッジをファイルに書き出します。そのファイルを diff で比較してその結果をジョブのログに出力します。

ポイントは、master で実行されるジョブのキャッシュと MR で実行されるジョブのキャッシュを同名にすることで、master のジョブで作成された coverage.txt を MR のジョブで参照する、ところです。

master のジョブではキャッシュ名を coverage--$CI_COMMIT_REF_NAME としています。$CI_COMMIT_REF_NAME はブランチ名になるのでキャッシュ名は coverage--master となります。

MR のジュブではキャッシュ名を coverage--$CI_MERGE_REQUEST_TARGET_BRANCH_NAME としています。$CI_MERGE_REQUEST_TARGET_BRANCH_NAME はマージリクエストのマージ先ブランチなので、master へのマージリクエストならキャッシュ名は coverage--master となり、master のジョブのキャッシュ名と同名になります。

さらに master のジョブでは policy: push でキャッシュを更新するのみ、MR のジョブでは policy: pull を取得のみとします。

これで MR のジョブの実行時に master のジョブによって作成されたキャッシュを取得できます。キャッシュに phpcov --text の出力ファイルを入れてやれば、MR のジョブで次のような内容をログに出力することができます。

--- coverage.orig.txt
+++ coverage.txt
@@ -3,11 +3,11 @@
 Code Coverage Report:

  Summary:
-  Classes:  0.00% (0/2)
-  Methods: 50.00% (3/6)
-  Lines:   50.00% (3/6)
+  Classes: 50.00% (1/2)
+  Methods: 83.33% (5/6)
+  Lines:   83.33% (5/6)

 App\Sample1
-  Methods:  66.67% ( 2/ 3)   Lines:  66.67% (  2/  3)
+  Methods: 100.00% ( 3/ 3)   Lines: 100.00% (  3/  3)
 App\Sample2
-  Methods:  33.33% ( 1/ 3)   Lines:  33.33% (  1/  3)
+  Methods:  66.67% ( 2/ 3)   Lines:  66.67% (  2/  3)

MR のコメントにカバー率の差分を追記する

↑だけだとジョブのログに出力されるだけなので、MR の画面からパッと確認できなくて不便です。ので、カバー率の差分を MR のコメントとして追記するようにします。

まず、次のようなスクリプトを ci/coverage-reporter.sh のようなファイル名で用意します。

#!/bin/sh

: ${COVERAGE_REPORTER_TOKEN:?}
: ${CI_API_V4_URL:?}
: ${CI_PROJECT_ID:?}
: ${CI_MERGE_REQUEST_IID:?}

user_id=$(
  curl -s -H "PRIVATE-TOKEN: $COVERAGE_REPORTER_TOKEN" "$CI_API_V4_URL/user" \
  | jq .id -r
)

note_id=$(
  curl -s -H "PRIVATE-TOKEN: $COVERAGE_REPORTER_TOKEN" \
    "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" \
  | jq '.[] | select(.author.id|tostring == $user_id).id' --arg user_id "$user_id" -r
)

diff=$(cat -)

if [ -z "$diff" ]; then
  diff="no difference in code coverage"
fi

body=$(printf '```diff\n%s\n```\n' "$diff")
data=$(jq -n '{body:$body}' --arg body "$body")

if [ -z "$note_id" ]; then
  curl -s -H "PRIVATE-TOKEN: $COVERAGE_REPORTER_TOKEN" \
    "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" \
    -X POST -H content-type:application/json --data-raw "$data" \
  > /dev/null
else
  curl -s -H "PRIVATE-TOKEN: $COVERAGE_REPORTER_TOKEN" \
    "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes/$note_id" \
    -X PUT -H content-type:application/json --data-raw "$data" \
  > /dev/null
fi

標準入力から入ってきたテキストを、まだ MR にコメントしていなければ新規コメントを、既に MR にコメントしているならそのコメントの修正を、行うスクリプトです。

MR へのコメントのためにアクセストークンが必要なので、プロジェクトレベルのアクセストークンなどを作成の上、Gitlab CI の Variable で COVERAGE_REPORTER_TOKEN という名前で設定しておく必要があります。

そして、coverage-mr ジョブの script の最後を次のように修正します。

coverage-mr:
  # ...snip...
  script:
    - if test -e coverage.txt; then mv -f coverage.txt coverage.orig.txt; fi
    - *coverage_script
    #- if test -e coverage.orig.txt; then diff -u -w coverage.orig.txt coverage.txt || true; fi
    - if test -e coverage.orig.txt; then (diff -u -w coverage.orig.txt coverage.txt || true) | ci/coverage-reporter.sh; fi

これで次のようなコメントが MR に追記されるようになります。

f:id:ngyuki:20210116141411p:plain

さいごに

Github で coverallscodecov などの SaaS を組み合わせればテストのカバレッジの可視化やカバー率の差分が記録できるらしいです。

例えば PHPUnit だと次のように codecov でカバー率の差分が PR に追記されています。

Gitlab 単体でも PHPUnit のコードカバレッジの HTML レポートをアーティファクトに保存したり Gitlab Pages で公開したりすればカバレッジの可視化には十分なのですが・・・コードカバー率の差分も表示できると有用かもー、と思ったので簡易的にやってみました。

欠点としては・・・MR のコメントに追記すると、カバー率が減っていても増えていても変わらなくても、最初の1回だけは常にメールで通知されてきます。レビュー時の参考情報ぐらいの位置づけにしたかったので通知は不要なのですが。

なお、今回はキャッシュを使って master のジョブの実行結果を MR のジョブで取得するようにしましたが、下記の API を使えばアーティファクト経由でもできそうです。