背景
- 新機能の追加が継続している
- 既存機能の改修、改善が継続している
- 時とともに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言語特有の機能を組み合わせると、直接的に依存するテストスクリプトを列挙できる。
- %INC / perldoc.jp
do/require,use
したパッケージを%INC
へ格納する
- perlrun / perldoc.jp
- 指定したスクリプトの構文チェックを実行する
perl -c
- 指定したスクリプトの構文チェックを実行する
- BEGIN, UNITCHECK, CHECK, INIT, END / perldoc.jp
CHECK
ブロックは登録した順に、スクリプトを開始する直前に評価する
- perlrun / perldoc.jp
- 任意のパッケージをスクリプトの実行前に読み込ませることができる
perl -d:Foo
でDevel::Foo
を読み込むperl -mFoo
やperl -MFoo
でFoo
を読み込む
require
や eval
は拾えないけど、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