kubell Creator's Note

ビジネスチャット「Chatwork」のエンジニアのブログです。

ビジネスチャット「Chatwork」のエンジニアのブログです。

読者になる

Xcode CloudからSonarQube Cloudへカバレッジデータを送ってみた

Xcode Cloudとは

developer.apple.com

Xcode Cloudとは、Appleが提供するCI/CDサービスです。

他のCI/CDサービスと比べて独特な仕様もありますが、比較的安価だったり、純正なだけあって証明書まわりの管理が楽になるなどのメリットがあり、弊社のiOSアプリ開発でも利用しています。

SonarQube Cloudとは

www.sonarsource.com

SonarQube Cloudとは、コードの品質やセキュリティの問題を自動的に検出してくれるツールです。例えばPull Requestを作成すると差分のコードを解析してPRにコメントしてれたりします。

ただし、テストカバレッジが知りたい場合は自動で計算してくれないので、CIサービスなどから送信する必要があります。

docs.sonarsource.com

本記事では、Xcode Cloudでのテスト実行からSonarQube Cloudへのデータ送信までのプロセスと、その過程で直面した課題について解説したいと思います。

Xcode Cloudのテスト結果からコードカバレッジを取得

Xcodeでのビルド結果からカバレッジを見つけるのはそんなに苦労しませんが、Xcode Cloud上で取得するのは一筋縄ではいきません。少し強引なワークアラウンドを使ってようやく取得できたので紹介したいと思います。

カスタムスクリプト

Xcode CloudではWorkflowの一連の処理で、カスタムスクリプトを実行できるカスタマイズポイントが 3 つあります

  • post_clone.sh(クローン後、依存解決前)
  • pre_xcodebuild.sh(依存解決後、ビルド前)
  • post_xcodebuild.sh(ビルド後)

ビルド後の .xcresult からカバレッジデータを取り出したいのでpost_xcodebuild.shにスクリプトを記述していきます。

テストアクション

Xcode Cloudでテストアクションを実行すると build-for-testingtest-without-buildという2回のフェーズで Xcode Build Action が実行されます。

カバレッジデータはtest-without-buildの実行後に生成されるので、下記のように"build-for-testing"のタイミングでは早期終了する方法で実装してみました。

if [ "$CI_XCODEBUILD_ACTION" = "build-for-testing" ]; then
    exit 0
fi

カバレッジレポートを生成

.xcresultファイルはCI_RESULT_BUNDLE_PATHという環境変数で取得することができます。しかしそのままでは送信できないので SonarQubeCloudへカバレッジデータを送信するためにxmlファイルへの変換が必要です。 github.com ↑を参考にスクリプト内で実装できそうです。

あとはSonarQubeCloudへ送信するだけ、、、?

あとはSonarScanを実行するとうまくいくはずです。 そのためにいくつかプロパティを設定する必要があります。

その中でもソースのパスは必須です。 ソースのパスは環境変数が用意されているので簡単です。

sonar.sources=${CI_PRIMARY_REPOSITORY_PATH}

しかしここに罠が潜んでいました。

罠: ソースコードはどこ?

実はtest-without-buildのタイミングではCI_PRIMARY_REPOSITORY_PATHの環境変数が取得できません、いやそもそもクローンしないのでソースコード自体がありません、、、

ただci_scriptsが存在するのでバックアップは取れるのかもしれないと考えました。

build-for-testingの段階でソースコード全体を別のディレクトリにバックアップを試みましたが残念ながらうまくいきませんでした。

test-for-building testing-without-building
ソースコード ある ない
カバレッジ 取れない 取れる

八方塞がりです。。。

ワークアラウンド

苦肉の策ですがカスタムスクリプトでもう一度テストビルドを実行するワークアラウンドを見つけました。

developer.apple.com

テストを2度実行するのは本当はやりたくありません。CIの待ち時間は可能な限り減らしたいはずです。 しかし幸いカバレッジデータはバージョン毎に取得する程度の頻度で済みそうなので、今回はこのワークアラウンドを採用してみようと思います。

スクリプトでテスト実行

今度はbuild-for-testingのタイミングでのみ実行したいので、先ほどの早期終了の判定を修正します。

if [ "$CI_XCODEBUILD_ACTION" = "test-without-build" ]; then
    exit 0
fi

では早速テストビルドの設定をしていきます。 resultBundlePathにはワークフローのテストアクションの.xcresultファイルが上書きされないようにしておきます。

# テスト実行
RESULT_BUNDLE_PATH=$CI_DERIVED_DATA_PATH/Logs/Test/ResultBundle.xcresult
xcodebuild \
  -scheme "$CI_XCODE_SCHEME" \
  -destination "id=$CI_TEST_DESTINATION_UDID" \
  -derivedDataPath $CI_DERIVED_DATA_PATH \
  -enableCodeCoverage YES \
  -resultBundlePath $RESULT_BUNDLE_PATH \
  clean test

しかしこれでは実行できませんでした。 実はCI_TEST_DESTINATION_UDIDが空っぽなのです。

罠: シミュレータを探せ

ワークフローの設定でテストアクションのために適切なシミュレータの設定をしているはずなので、できればそれを使いたいのですがCI_TEST_DESTINATION_UDIDが空っぽで利用できません。 post_xcodebuild.shのタイミングではシミュレータを閉じてしまっているようです。

そこで利用可能なシミュレータを下記のスクリプトで探し出しました。 シミュレーターの機種は変えたくなる可能性があるので、ソースを触らないで良いようにワークフローの環境変数COVERAGE_BUILD_DEVICE_NAMEを用意しておきます。

# シミュレーター設定
SIMULATOR_ID=$(xcrun simctl list devices | grep "$COVERAGE_BUILD_DEVICE_NAME" | grep -oE '[0-9A-F-]{36}' | head -n 1)

[ -z "$SIMULATOR_ID" ] && {
    echo "Error: $COVERAGE_BUILD_DEVICE_NAME <Simulator ID: $SIMULATOR_ID> not found."
    exit 1
}

見つかったらビルドするためにbootしておきます。

echo "Boot $COVERAGE_BUILD_DEVICE_NAME <Simulator ID: $SIMULATOR_ID>"
xcrun simctl boot $SIMULATOR_ID

これでようやくビルドが実行できました。

あとは先ほどと同じようにカバレッジレポートを生成してSonarScanを実行することでようやくカバレッジの送信ができました🎉🎉

まとめ

少し強引な方法ですが、Xcode CloudからSonarQube Cloudへのカバレッジデータ送信までのプロセスを確立することができました。この方法は完璧とは言えませんが、バージョン毎にカバレッジデータを取得する目的では十分に機能しそうです。

今後の改善点としては、テストの二重実行を避ける方法や、より効率的なカバレッジデータの取得方法を探ることが考えられます。また、Xcode CloudやSonarQube Cloudのバージョンアップにより、将来的にはよりスムーズなプロセスが実現される可能性もあります。

この記事が、同様の課題に直面している開発者の方々にとって参考になれば幸いです。

参考記事