Now in REALITY Tech #47 iOSプロジェクトにおけるCIでのコードカバレッジ生成処理時間を短縮させた話

今週の「Now in REALITY Tech」は、REALITY iOSのCIで実行しているコードカバレッジ生成処理時間を短縮させた方法についてiOSチームからお送りします。

iOSアプリ開発におけるコードカバレッジは、Xcodeを用いてテスト実行時に測定でき、プロダクションコードに対するテストでのカバー比率を視覚化してくれます。
コードカバレッジを有効活用することで、新規追加したコードもしくは既存コードの変更において適切にテストが実行されているかを測る指標となります。
REALITYのiOSアプリ開発では、コードカバレッジをGitHub Pull Requestに投稿することで有効活用していました。
しかし、追加したコードカバレッジ生成およびPRへの投稿処理によってCI時間が増加するデメリットがあったため、この問題を解決しました。

結論から述べると、コードカバレッジ生成処理時間は改善前後で約3分〜6分程 短縮されました!
以降、具体的な改善方法についてお伝えします。

改善前のCIで行っているコードカバレッジ生成処理について

改善前はCI(Bitrise)でDangerを使用しているため、コードカバレッジを生成しPRで差分があるファイルに対してのみコードカバレッジ結果をPRにコメントする処理をDanger PluginのDanger Slatherを使用して行われていました。

問題点

Danger Slatherでは、アプリターゲットのテスト実行とは別でビルドを行いテストにおけるカバレッジを生成しているためビルド時間が約3分〜6分程かかっていました。

改善前のBitriseでのテスト実行時間とカバレッジ生成処理時間

改善方法

改善方法としては、Danger Slatherは使用せず、独自のコードカバレッジ生成処理 + GitHub PRにコメント投稿するscriptを実装しました。

コードカバレッジ生成処理のscriptについて

  1. fastlane scanでテストを実行し、xcresultファイルを出力する

  2. xcresultファイルを使用して、テストのカバレッジ結果をtxtファイルに出力

  3. 2で出力した結果から、PRで差分があるファイルをフィルタリング

1. fastlane scanでテストを実行し、xcresultファイルを出力する

scriptは下記のようになっており、生成されたxcresultファイルのpathはShared value SharedValues::SCAN_GENERATED_XCRESULT_PATH で取得可能なのでxcresultのpathをカバレッジscriptの実行時渡します。

# Fastfile

lane :coverage do
    scan
    xcresult_path = Actions.lane_context[SharedValues::SCAN_GENERATED_XCRESULT_PATH]
    sh("sh coverage.sh " + xcresult_path)
end

fastlane scanを使用せず、BitriseのiOS用Xcodeテスト stepでテスト実行する場合はBitriseの環境変数にエクスポートされるのでそちらを使用してxcresultを取得可能です。
参考:https://devcenter.bitrise.io/ja/testing/running-xcode-tests.html

xcresultとは

テスト結果をまとめたファイルで、テストレポートやテストカバレッジ、ビルドログが含まれています。
下記の記事で詳細に記載されているので、こちらをご参照ください。

カバレッジの設定方法について

カバレッジの設定方法については下記ドキュメントをご参照ください。

2. xcresultファイルを使用して、テストのカバレッジ結果をtxtファイルに出力

上記で紹介した通り、xcresultにはカバレッジ以外の内容も含まれるのでカバレッジ結果のみを抽出するためにOSSのCLIツール xcparse を用いて、xccovreport を出力します。
また、xccovreportからファイル名およびカバレッジ率のみ取得しておきます。

XCRESULT_PATH=$1

# .xcresultをもとにxccovreportを出力
xcparse codecov ${XCRESULT_PATH} ./

# xccovreport からファイルごとの名前とcoverage rateのみ出力
xcrun xccov view --files-for-target {アプリターゲット名} ./action.xccovreport | awk '{print $2 " | " $4}' >> all_coverage_report.txt

3. 2で出力した結果から、PRで差分があるファイルをフィルタリング

PRのdiffからswiftファイルを抽出し、抽出したファイルのコードカバレッジをPRにコメントするために変数 comment に格納しています。

# merge先のbranchとのdiffを取得
diff_files=$(cd ../; git diff origin/develop --diff-filter=d --name-only -- "*.swift" -- ':(exclude){テストディレクトリ}/*')

# diffがあるファイルのみのcoverageを出力
if [ -z "${diff_files}" ]; then
    #差分なければcoverage commentは投稿しない
    echo "diff file is none, not output coverage!"
    exit 0
fi

# PRのdiffにあるファイルのコードカバレッジのみ抽出する
comment='## Code coverage\n File | Coverage\n :---|:---:\n'
for filename in ${diff_files[@]}; do
    report=$(grep $filename all_coverage_report.txt)

    if [ -n "${report}" ]; then
        # reportがあれば追記
        comment=$comment$report'\n'
    fi
done

GitHub PRにコメント投稿するscriptについて

Danger Slatherの機能として、カバレッジ結果をGitHub PRにコメント投稿およびカバレッジ結果のコメントを更新する処理があるため、この機能においても独自でscriptを実装しました。

PRへのコメント投稿および更新はGitHub Web APIを使用すれば良いのですが、ここではカバレッジコメントを新規投稿するか更新するかを判定するためにBitriseのcacheを使用して判定する方法を紹介します。

  1. 新規カバレッジコメント投稿時にPRのコメントIDをtxtファイルに残して、cacheする

  2. カバレッジ処理時にPRのコメントIDがcacheされていれば、更新する

カバレッジコメント投稿およびcache方法

PRのコメントIDは新規投稿時のAPIレスポンスから取得可能なので、txtファイルに保存しておきます。
このscript実行後のBitrise cache-push step にてtxtファイルをcacheしておきます。

# 1. 新規カバレッジコメント投稿時にPRのコメントIDをtxtファイルに残して、cacheする

comment_id=$(curl \
  -X POST \
  -H "Accept: application/vnd.github.v3+json" \
  -H "Authorization: token ${GHE_AUTH_TOKEN}" \
  https://api.github.com/repos/{owner}/{repo}/issues/${BITRISE_PULL_REQUEST}/comments \
  -d "$commentJson" | jq '.id')

echo $comment_id > github_pr_coverage_comment_id.txt 

カバレッジコメントの更新方法

更新方法は、投稿されているカバレッジコメントを削除して再度投稿するようにしています。
理由としては、投稿されたカバレッジコメントがcommitや他のコードレビューコメントによって古くなるのを防ぐためです。
cache-pull stepでcacheされたtxtファイルがあれば、PRのコメントIDを取得してコメントを削除し、再度コメント投稿します。

# 2. カバレッジ処理時にPRのコメントIDがcacheされていれば、更新する

comment_id=$(cat github_pr_coverage_comment_id.txt)

if [ -n "${comment_id}" ]; then
    # coverage commentのdelete
    curl \
      -X DELETE \
      -H "Accept: application/vnd.github.v3+json" \
      -H "Authorization: token ${GHE_AUTH_TOKEN}" \
      https://api.github.com/repos/{owner}/{repo}/issues/comments/${comment_id}
fi

改善後のコードカバレッジ生成処理時間

改善後はコードカバレッジ生成の処理時間が1〜2秒で済むようになりました。

改善後のコードカバレッジ生成時間

まとめ

今回はコードカバレッジ生成処理の時間短縮方法を紹介しました。
コードカバレッジのために再ビルドしていた時間が大幅に削減されたため、全体的なCIでの実行時間を減らすことが出来ました。
xcresultについての情報やカバレッジについては情報が少ないので、役に立てれば幸いです。

REALITYでは一緒に働く仲間を募集しております。
興味がある方は下記リンクからぜひご応募ください!