CIの時間をボトムアップに短縮する方策

背景

  • 新機能の追加が継続している
  • 既存機能の改修、改善が継続している
  • 時とともにCIの実行時間が増加し続けている
  • 自動テストはときどき失敗する(flaky)
  • CIの成功が、コードレビューを始めるきっかけになっている(CIが成功するまでコードレビューが始まらない)

問題と原因

  • 問題
    • CIの待ち時間が長く感じる
    • 変更セットと無関係な自動テストがときどき失敗する(再試行すると成功する)
  • 原因
    • CIはすべての自動テストを実行している

解決方法

1. テストの実行を並列化する

  • すべての自動テストを実行すること にこだわりがあるときの戦略
    • プロジェクトによっては、変更セットの正しさを保証する最低限の線引きかもしれない
  • CIやテストフレームワークの仕組みに応じていろいろなやり方がある
    • マシンの並列化
    • プロセスの並列化
    • スレッドの並列化

2. 変更セットに含まれるプログラム部品に依存する自動テストだけを抽出する

  • 差分コンパイルと同じように、変更のあったプログラム部品(コード行、関数、クラス、モジュール、パッケージなどなど)に依存する自動テストが成功すればよい、というところまで、正しさを保証する線を後退させる戦略
  • GitHubソースコードを管理し、GitHub ActionsでCIを実行しているなら次のようなやり方がある

変更セットに含まれるプログラム部品

例えば Perl 言語だと関数単位の差分を上手く計算できないから1、1ファイルに1パッケージという平和協定を信じて git diff --name-only で抽出したファイル名から、パッケージ名を推測するのが無難だと思う。

    steps:
      - id: part of changes
        run: |
          git diff --name-only ${{ github.base_ref }} .. HEAD | grep -v -E '.*\.t' | \
          # "lib/Xxx/Yyy/Zzz.pm"
          sort --dictionary > .part-of-changes

プログラム部品に依存する自動テスト

(もっとスマートなやり方が紹介されているので先に読めばよかった YAPC::NA 2016 Talk)

Perl言語特有の機能を組み合わせると、直接的に依存するテストスクリプトを列挙できる。

requireeval は拾えないけど、CHECK ブロックで %INC を検査すれば、そのテストスクリプトが依存しているパッケージを列挙できる。

こういうパッケージを用意して

package Devel::FindDeps;
use strict;
use warnings;
use utf8;

CHECK {
    my @depends;
    foreach my $module (sort keys %INC) {
        next if $module eq 'Devel/FindDeps.pm';
        push @depends, qq{lib/$module} if -e qq{lib/$module};
    }
    print qq{$0:$_\n} foreach @depends;
    exit;
}

1;

こういうふうに実行すれば テストスクリプト名:依存パッケージ名 の文字列が得られる。

    steps:
      - id: dependent of test
        run: |
          find t -type f -name \*.t | \
          # "t/Xxx/Yyy/Zzz.t"
          xargs --max-args 1 --max-procs 32 perl -Ilib -It/lib -d:FindDeps -c 2>/dev/null | \
          # "t/Xxx/Yyy/Zzz.t:lib/Xxx/Yyy/Zzz.pm"
          sort --dictionary > .dependent-of-test

組み合わせると、変更セットに含まれる部品に依存するテストだけを抽出できる。

    steps:
      - id: dependent of test
        run: |
          find t -type f -name \*.t | \
          # "t/Xxx/Yyy/Zzz.t"
          xargs --max-args 1 --max-procs 32 perl -Ilib -It/lib -d:FindDeps -c 2>/dev/null | \
          # "t/Xxx/Yyy/Zzz.t:lib/Xxx/Yyy/Zzz.pm"
          sort --dictionary > .dependent-of-test
      - id: part of changes
        run: |
          git diff --name-only ${{ github.base_ref }} .. HEAD | grep -v -E '.*\.t' | \
          # "lib/Xxx/Yyy/Zzz.pm"
          sort --dictionary > .part-of-changes
      - id: run dependent test
        run: |
          cat .part-of-changes | \
          xargs --max-args 1 --max-procs 32 grep --no-filename .dependent-of-test | \
          # "t/Xxx/Yyy/Zzz.t:lib/Xxx/Yyy/Zzz.pm"
          cut --delimiter=: --field=1 | \
          # "t/Xxx/Yyy/Zzz.t"
          sort --dictionary --unique | \
          # "t/Xxx/Yyy/Zzz.t"
          prove -lvr --timer --trap -j4

  1. たいていのプログラミング言語git diff --function-context で関数単位の差分を計算できる