LoginSignup
7
2

More than 5 years have passed since last update.

CircleCI x Gradleマルチプロジェクトのテスト並列実行

Last updated at Posted at 2018-12-17

この記事は CircleCI Advent Calendar 2018 の12月18日の記事です。

やりたいこと

  • Gradleマルチプロジェクトのテストを複数コンテナで並列実行する

前提

  • CircleCI2.0
  • 下記のようなGradleマルチプロジェクト(ここではSpring Boot x Groovy)
プロジェクト構成
.
├── build.gradle
├── gradle
├── gradlew
├── gradlew.bat
├── project-one
├── project-two
├── project-three
├── project-four
├── project-five
├── project-six
└── settings.gradle
  • docker-composeを動かしてその上でテスト実行
  • コンテナを3つ利用

docker-composeを動かす

docker executerだとvolumeマウントできないのでmachine executerにする必要があります
refs.
CircleCI 2.0 で docker-compose を動かすなら、Machine Executor にしないとハマる
Docker コンテナにデータボリュームをマウントする方法

テスト分割と割り当て

並列実行するためにテストクラスをコンテナの数だけ分割してあげる必要があります
最初にイメージしていたのは

  • 1.ルートプロジェクト配下の全テストクラスをスクリプトで全取得
  • 2.1をcircleci tests splitに渡して自動分割
  • 3.2をGradleのtestタスクの引数に渡して一括実行

のような流れですが、Gradleには絶対パスで指定したテストクラスをプロジェクト横断で一括実行するという機能がないため詰まりました(あったら教えてください..)
下記のようにプロジェクト指定でのテストクラス指定実行は可能です
./gradlew :project-one:test --tests "com.example.service.SampleServiceTest"

結果どうしたかというと、苦肉の策として
CIRCLE_NODE_INDEXという環境変数に実行コンテナのindexが格納されているので、どのコンテナにどのプロジェクトのテストクラスを割り当てるか手動で指定することにしました

下記のコードで

  • コンテナ1
    • project-one
    • project-two
  • コンテナ2
    • project-three
    • project-four
  • コンテナ3
    • project-five
    • project-six

という割り当てでテスト実行が可能になります

split_test.sh
#!/bin/bash

set -e

EXECUTE_MODULE=()
if [[ ${CIRCLE_NODE_INDEX} -eq 0 ]]; then
    EXECUTE_MODULE+=(one two)
elif [[ ${CIRCLE_NODE_INDEX} -eq 1 ]]; then
    EXECUTE_MODULE+=(three four)
elif [[ ${CIRCLE_NODE_INDEX} -eq 2 ]]; then
    EXECUTE_MODULE+=(five six)
fi

#コマンドの生成
COMMAND="./gradlew --parallel --max-workers=3 "
for item in ${EXECUTE_MODULE[@]}; do
    COMMAND=${COMMAND}":project-$item:clean :project-$item:test "
done

#実行
eval ${COMMAND}

refs.
Using Environment Variables to Split Tests
Running Tests in Parallel

キャッシュの設定

CircleCIのビルドは揮発性なのでキャッシュの設定をなにも行わないと都度ライブラリやパッケージのインストール等が行われてしまいます
今回はgradleのキャッシュ設定を行いました
プロジェクト配下のbuild.gradle/settings.gradle/gradle-wrapper.propertiesに変更があったときは新しくキャッシュkeyを生成し直します

create_cache_key.sh
#!/bin/bash
#Gradleのキャッシュを使うかどうか判定するためのキー生成
#refs.https://medium.com/@chrisbanes/circleci-cache-key-over-many-files-c9e07f4d471a
RESULT_FILE=$1

if [ -f ${RESULT_FILE} ]; then
  rm ${RESULT_FILE}
fi
touch ${RESULT_FILE}

checksum_file() {
  echo `openssl md5 $1 | awk '{print $2}'`
}

# Gradleのビルドファイルを取得する
FILES=()
while read -r -d ''; do
    FILES+=("$REPLY")
done < <(find . \( -name 'build.gradle' -o -name 'settings.gradle' -o -name 'gradle-wrapper.properties' \) -type f -print0)

for FILE in ${FILES[@]}; do
    echo `checksum_file ${FILE}` >> ${RESULT_FILE}
done

# ソートする
sort ${RESULT_FILE} -o ${RESULT_FILE}

refs. Caching Dependencies

ちなみにこのキャッシュKeyもコンテナごとに一意のものにしてあげないと、利用する依存ライブラリに差異が出てしまって毎回差分をfetchする形になるので指定してあげます

config.yml
      - restore_cache:
          keys:
            - v1-gradle-dependencies-{{ .Environment.CIRCLE_NODE_INDEX }}-{{ checksum "/tmp/checksum.txt" }}

      - save_cache:
          #キャッシュは一度設定すると消せないのでgradleファイルに変更はないが消したい場合にはバージョンをインクリメントする
          #res:https://circleci.com/docs/2.0/caching/#clearing-cache
          key: v1-gradle-dependencies-{{ .Environment.CIRCLE_NODE_INDEX }}-{{ checksum "/tmp/checksum.txt" }}

ターミナルの指定

なんでもいいのでターミナルを指定しないとgradlewrapper実行時に * What went wrong:
Could not open terminal for stdout: $TERM not set
というエラーになるので指定します

export TERM=dumb

テスト結果/成果物の保存

テスト結果や実行時間を綺麗なUIでみれることってめっちゃ大事ですよね
store_test_resultsを使うことで簡単に実現できます

config.yml
      - store_test_results:
          path: ~/junit
      - store_artifacts:
          path: ~/junit

refs.Collecting Test Metadata

全体像

config.yml
version: 2
jobs:
  build:
    working_directory: ~/project
    machine:
      image: circleci/classic:201808-01
    environment:
      COMPOSE_FILE: ./docker-compose.yml
      SPLIT_TEST_PATH: ./circleci
    parallelism: 3
    steps:
      - checkout
      - run:
          name: Generate Cache Key
          command: bash ${SPLIT_TEST_PATH}/create_cache_key.sh /tmp/checksum.txt
      - run:
          name: Lancuh docker containers
          command: docker-compose up
      - restore_cache:
          keys:
            - v1-gradle-dependencies-{{ .Environment.CIRCLE_NODE_INDEX }}-{{ checksum "/tmp/checksum.txt" }}
      - run:
          name: Run Unit Test
          #ターミナルを指定しないと下記のようなエラーがでる
          #refs.https://stackoverflow.com/questions/49163104/how-to-resolve-term-not-set-on-gradlew-assemblerelease-on-circleci
          command: |
            export TERM=dumb
            bash ${SPLIT_TEST_PATH}/split_test.sh
      - save_cache:
          #キャッシュは一度設定すると消せないのでgradleファイルに変更はないが消したい場合にはバージョンをインクリメントする
          #res:https://circleci.com/docs/2.0/caching/#clearing-cache
          key: v1-gradle-dependencies-{{ .Environment.CIRCLE_NODE_INDEX }}-{{ checksum "/tmp/checksum.txt" }}
          paths:
            ~/.gradle
          when: always
      - run:
          name: Save Test Results
          command: |
            mkdir -p ~/junit/
            find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/junit/ \;
          when: always
      - store_test_results:
          path: ~/junit
      - store_artifacts:
          path: ~/junit

今後やってみたいこと

  • 2.1への書き換え
  • Docker Layer Cachingの利用
    • 毎回dockerイメージを取り直してるしてるのでdockerのコンテナを立ち上げるだけで毎回ビルドが1~2mかかっています
    • これを利用するとdocker imageをキャッシュしてくれるのでビルドの高速につながるようです。ただ有料なのでおかねに余裕ができたらつかってみたいです

最後に

CircleCIは公式のドキュメントが非常に充実していてわかりやすく、初心者の自分でもあまり困ることはありませんでした 
今後も機会があったら活用したいと思います

以上、最後までみていただきありがとうございました

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2