CIを高速に回す手法
今回は、
なぜCIに速度が求められるのか
作り始めのプロダクトは小さく、
個別のテストを高速化する手法は先述した
CIの高速化アプローチ
マスタノードとなるサーバが定期的にフルテスト未実行のブランチを探し、

筆者の環境では、
マスタノード側の実装
マスタノードの仕事は次のとおりです。
- ① テスト対象のファイルをピックアップしてシャッフルして、実行対象テストをクラスタノード数で等分する
- ② クラスタノードに実行対象テストの実行を依頼し、結果を受け取る
- ③ クラスタノードからの実行結果をパースする
- ④ パースした結果をUkigumo::ServerへPOSTする
①でシャッフルを行うのは次のためです。
- 並び順によって実行時間が遅いものが集中しないようばらけさせる
- 実行順序に依存のあるテストを書いてしまったまま気づかないことを防止する
① テスト対象のピックアップ/シャッフル/等分
まずテスト対象のファイルをピックアップしてシャッフルし等分する部分ですが、
use File::Find;
use List::Util qw/shuffle/;
use List::MoreUtils qw/part/;
my @tests;
File::Find::find(
sub {
return unless /\.t$/ and -f $_;
my $full_path = $File::Find::name;
push(@tests, $full_path)
}, "/path/to/repo/t"
);
@tests = shuffle @tests;
# ['test1.t', 'test6.t', 'test2.t', 'test5.t', 'test3.t', 'test4.t']
my @test_clusters = ( 'node1', 'node2', 'node3' );
my $cluster_num = @test_clusters;
my $i = 0;
@tests = part { $i++ % $cluster_num } @tests;
# ['test1.t', 'test6.t'], ['test3.t', 'test5.t'], ['test2.t', 'test4.t']
my $node_task = +{};
for my $node (@test_clusters) {
$node_task->{$node} = shift @tests;
}
この例では、.t
で終わる名前のファイルを取得し、
取得した実行対象テストはList::MoreUtils::partでサーバ台数分に配列を分割し、
② テストの実行依頼と、実行結果の受け取り
次に各クラスタノードへの実行依頼と集約ですが、ssh
できる状態になっていることを前提とします。
use Parallel::ForkManager;
use Net::OpenSSH;
my $output = '';
my $pm = Parallel::ForkManager->new($cluster_num); # (1)
my %nodes;
for my $cluster_nodename (@{ config->{test_clusters} }) { # (2)
if (my $pid = $pm->start) {
$nodes{$pid} = $cluster_nodename;
next;
}
my $task_list =
join '||', @{ $node_task->{$cluster_nodename} };
my $ssh = Net::OpenSSH->new($cluster_nodename);
my $sha1 = $repo->sha1($branch);
$ssh->system("
cd /path/to/repo;
git fetch;
git reset --hard HEAD;
git clean -fd;
git checkout $sha1"); # (3)
my ($ret, $err) = $ssh->capture2(
"perl /path/to/test_launcher.pl --tests='$task_list'
"); # (4)
$ret .= "\n[STDERR]\n$err" if $err;
$pm->finish(
0,
{
pid => $$,
result => $ret,
branch => $branch,
sha1 => $sha1
}); # (5)
}
$pm->wait_all_children;
$pm->run_on_finish(sub { # (6)
my (
$pid, $exit_code, $ident,
$exit_signal, $core_dump, $data) = @_;
my ($branch, $sha1, $test_result) =
($data->{branch}, $data->{sha1}, $data->{result});
my $nodename = $nodes{$pid};
if ($exit_code != 0) { # Emulate output of prove.
($test_result //= '') .=
"\n$0 (Wstat: 0 Tests: 0 Failed: 1)\n
got exit code=$exit_code\n";
}
my $result =
sprintf "run on %s \n%s=-=-=-=-=-=\n",
$nodename, $test_result;
$output = $output . $result;
});
まずssh
で実行依頼を投げます。
test_
は、
run_
に渡る$data
にあたります。Parallel::ForkManager 0.run_
でforkした子プロセスからデータを受信できるので、
最終的に、$output
にまとめています。
③ 実行結果のパース
ここまでで、
my @all;
while ($output =? m{^(\S+\.t)\s+?\.}gsm) {
push @all, $1;
}
my @fails;
while ($output =?
m{^(\S+)\s*?\(Wstat:(.*?)Tests:(.*?)Failed:(.*?)\)$}gsm) {
my ($name, $wstat, $tests, $failed) =
($1, int $2, int $3, int $4);
push @fails, $1 unless $wstat ==
0 && $tests > 0 && $failed <= 0;
}
@all
は実行されたすべてのテストケースをカウントし、@fails
は失敗したテストケースをカウントします。
④ パース結果のUkigumo::ServerへのPOST
最後に、fail
数がカウントされているので、
my $ua = LWP::UserAgent->new;
my $res = $ua->post(
"http://ukigumo.example.com/api/v1/report/add", [
status => @$fails ? 2 : 1,
# status code: SUCCESS:1, FAIL:2, N/A:3
project => 'My Project', # project name
branch => 'my-branch', # branch name
revision => 3, # revision
repo => "-",
body => <<__BODY__,
Failed ${\ scalar @$fails} / ${\ scalar @$all}.
==========
$body
__BODY__
]);
POSTの際に渡しているブランチ名やプロジェクト名は適宜対象リポジトリから取得しましょう。
そのほかIRCやメールなどで通知を行いたい場合は、
クラスタノード側の実装
クラスタノード側は先に紹介したTAP::Harnessを用いて、ssh
経由でtest_
を呼び出していましたが、
use Smart::Options;
use TAP::Harness;
my $argv = Smart::Options->new()->parse;
my $harness = TAP::Harness->new({
exec => [
'perl',
'-Ilib',
'-It/lib',
],
});
my @tests = split /\|\|/, $argv->{tests};
$harness->runtests(@tests);
$argv->{tests}
にはマスタノード側で||をデリミタとして連結した文字列を渡しているので、$harness->runtests
に渡すだけでテストを実行してくれます。結果出力はマスタノード側で受信し、
クラスタノード側で何らかの問題が起こった際のフォローは、
さらなる高速化を目指して
ここまでの施策で、
仮想サーバやコンテナを用いた高速化
マルチコアを活かす手法として、
単体サーバでの実行高速化
単体サーバでの並列実行の方法として、prove
コマンドには-jという並列実行のオプション$ENV{HARNESS_
)
そのようなケースでは、
package My::Test::Module;
use Test::Synchronized;
# ...
ただし、
Test::mysqldやTest::Memcachedなどを用いてテストごとに個別のサービスを立ち上げると、
まとめ
本稿では、
プロダクトは大きくなるにつれ関わる人数や実行されるテスト量が増え、
さて、