この記事はCircleCI Advent Calendar 2019の12日目の記事です。
CircleCIにはテストを並列実行させるためのコマンドが用意されています。
その中でも一番効率のよさそうなテストの実行時間をもとに分割して並列実行させるコマンドが、Workflowsとの相性が悪いためこれをなんとかした方法について紹介します。
CircleCI CLIとは
CircleCI CLIは多くの便利なツールを利用できるコマンドラインです。
今回はその中でも、テストファイルを分割するcircleci test split
コマンドについて説明します。
circleci test split
にテストファイルのファイル名リストを渡すと使用可能なコンテナ数で分割することができます。
$ circleci tests split test_filenames.txt
「使用可能なコンテナ数」はparallelism
キーで指定されたものを参照しますが、分割数や分割後のどの組のファイルリストを出力するかを任意に指定することもできます。
$ circleci tests split --total=4 --index=0 test_filenames.txt
「タイミングデータに基づいた分割」のなにがうれしいか
circleci tests split
での分割ルールには3種類あり、それぞれ以下のような特徴があります。
ルール | 概要 |
---|---|
ファイル名に 基づいた分割 |
テスト名によってテストをアルファベット順に分割 デフォルトのルール |
ファイルサイズに 基づいた分割 |
ファイルサイズで分割--split-by=filesize オプションを付ける |
タイミングデータに 基づいた分割 |
テスト実行後のstore_test_results データよりタイミングデータを生成して実行時間で分割--split-by=timings オプションを付ける |
タイミングデータに基づいた分割についてとてもわかりやすくまとめてある記事がCircleCI Advent Calendar 2018の24日目にあるので引用します
例えば今、テストファイルが7個あって、それぞれのテストにかかる時間が経験上「10秒、6秒、5秒、4秒、3秒、2秒かかる」ということがわかっているとします。
この場合、普通に1プロセスで実行すると10+6+5+4+3+2で30秒かかります。ここで、CircleCIでparallelism: 3(3並列)で分割テストすることを考えます。
まず悪い例として「[10, 3], [6, 5], [4, 2]」と分割してみます。そうすると以下のようになり、一番遅いやつが13秒(10+3)なので全体として13秒かかることになります。
次に、それよりは少しましな例として「[10], [6, 3, 2], [5, 4]」と分割します。すると以下のようになり、一番遅いやつが11秒(6+3+2)ということになります。
つまり、分割の仕方によって全体のテスト時間が変わってしまう!(13秒 vs 11秒) ということになります。
タイミングデータに基づいた分割とは個々のテストにかかった時間をもとに、分割したテスト群それぞれにかかる時間をだいたい同じくらいになるように「いい感じ」に調整してくれるルールということです。
どうしてWorkflowsと相性が悪いのか
Workflowsを使っていない場合は.circleci/config.yml
ファイルで1つのbuild job扱いになるため、store_test_results
の結果が次のbuild jobに特に設定を意識することなく伝搬させることができます。
しかし、Workflowsを使っているとstore_test_results
でタイミングデータが生成されても、次回の同じjobに伝搬してくれませんでした。
古い記事ですが、当時はWorkflowsでは使うことができないと回答がありました。
https://discuss.circleci.com/t/problem-using-split-by-timings-in-workflows/14780
キャッシュを駆使する
使えないと言われたからといって諦められませんw
幸いWorkflowsを使っていてもCircleCI CLIも使えるようですし、いろいろ調べてみたところタイミングデータを伝搬させるところだけが問題のようでした。
Workflowsはjobの下流と横へのcache受け渡しがいろいろできるのでそれらを駆使して考えてみました。
これがconfig.ymlだ
workflows:
test_flow:
jobs:
- test
- save_timing_data:
requires:
- test
jobs:
test:
... # <- 環境準備
parallelism: 3
steps:
- restore_cache:
keys:
- taiming-data-v1-{{ .Branch }}
- run: mkdir ~/rspec
- run:
name: Run test
command: |
# circle-test-results/results.jsonがなかったらファイル名分割
if [ -e "circle-test-results/results.json" ]; then
TESTFILES=$(circleci tests glob "spec/**/*.rb" | circleci tests split --split-by=timings)
else
TESTFILES=$(circleci tests glob "spec/**/*.rb" | circleci tests split)
fi
bundle exec rspec -f RspecJunitFormatter -o ~/rspec/rspec.xml -- ${TESTFILES}
- store_test_results:
path: ~/rspec
- run:
name: Cache build number
command: echo $CIRCLE_BUILD_NUM > .build_number
- persist_to_workspace:
root: ~/app # <- 設定したworking_directory
paths:
- .build_number
save_timing_data:
... # <- 環境準備
steps:
- attach_workspace:
at: ~/app # <- 設定したworking_directory
- run:
name: Get test metadata(timing-data)
command: |
mkdir circle-test-results
curl "https://circleci.com/api/v1.1/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/$(cat .build_number)/tests?circle-token=$CIRCLE_API_TOKEN" > circle-test-results/results.json
- save_cache:
key: taiming-data-v1-{{ .Branch }}
paths:
- circle-test-results
さいごに
こんな感じでWorkflowsでもタイミングデータに基づいた分割を使ってテストを効率よく分割して実行できるようになるはずです。
「はず」というのは、もともとテスト実行部分だけをAWS CodeBuildに投げてCIをまわすということをやったことがあるので、CircleCIだけで実現できるはずということで今回の記事にまとめてみました。
万が一、うまくいかないんだけど。。というのがありましたらご一報いただけると幸いです