Workflowsでもcircleci tests split --split-by=timings使いたい

この記事は[CircleCI Advent Calendar 2019](https://qiita.com/advent-calendar/2019/circleci)の12日目の記事です。

CircleCIにはテストを並列実行させるためのコマンドが用意されています。
その中でも一番効率のよさそうなテストの実行時間をもとに分割して並列実行させるコマンドが、Workflowsとの相性が悪いためこれをなんとかした方法について紹介します。

## CircleCI CLIとは

CircleCI CLIは多くの便利なツールを利用できるコマンドラインです。
今回はその中でも、テストファイルを分割する`circleci test split`コマンドについて説明します。

`circleci test split`にテストファイルのファイル名リストを渡すと使用可能なコンテナ数で分割することができます。

```sh
$ circleci tests split test_filenames.txt
```

「使用可能なコンテナ数」は`parallelism`キーで指定されたものを参照しますが、分割数や分割後のどの組のファイルリストを出力するかを任意に指定することもできます。

```sh
$ circleci tests split --total=4 --index=0 test_filenames.txt
```

## 「タイミングデータに基づいた分割」のなにがうれしいか

`circleci tests split`での分割ルールには3種類あり、それぞれ以下のような特徴があります。

|ルール|概要|
|---|---|
|ファイル名に<br>基づいた分割|テスト名によってテストをアルファベット順に分割<br>デフォルトのルール|
|ファイルサイズに<br>基づいた分割|ファイルサイズで分割<br>`--split-by=filesize`オプションを付ける|
|タイミングデータに<br>基づいた分割|テスト実行後の`store_test_results`データよりタイミングデータを生成して実行時間で分割<br>`--split-by=timings`オプションを付ける|

タイミングデータに基づいた分割についてとてもわかりやすくまとめてある記事が[CircleCI Advent Calendar 2018](https://qiita.com/advent-calendar/2018/circleci)の[24日目](https://hoshinotsuyoshi.com/post/circleci_tests_split/)にあるので引用します :bow: 


> 例えば今、テストファイルが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

## キャッシュを駆使する

使えないと言われたからといって諦められませんｗ
幸いWorkflowsを使っていてもCircleCI CLIも使えるようですし、いろいろ調べてみたところタイミングデータを伝搬させるところだけが問題のようでした。
Workflowsはjobの下流と横へのcache受け渡しがいろいろできるのでそれらを駆使して考えてみました。

<img width="600" alt="依存関係のキャッシュ_-_CircleCI.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14794/430222b7-f913-3938-0036-25b9b20b8999.png">

## これがconfig.ymlだ

```yaml:.circleci/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だけで実現できるはずということで今回の記事にまとめてみました。

万が一、うまくいかないんだけど。。というのがありましたらご一報いただけると幸いです :bow: 

## 引用・参考記事

- [circleci tests split --split-by=timingsについて調べた・自作してみた](https://hoshinotsuyoshi.com/post/circleci_tests_split/)
- [[CircleCI] テストの並列実行](https://circleci.com/docs/ja/2.0/parallelism-faster-jobs/)
- [[CircleCI] 依存関係のキャッシュ](https://circleci.com/docs/ja/2.0/caching/)