多分初めてのPerlネタです。
proveのプラグイン書いてみたかった
metacpanでApp::Proveのドキュメントを読んでいたら、プラグイン機構があることを知ったので素振りをしてみました。ネタはなんでもよかったんですが、今回は Jestの--shard
オプションみたいなのを作ってみます。
この記事で実装したやつを丁寧に書いたやつは以下のリポジトリにあります。
ドキュメントを読んでみる
App::Prove::Plugin::[HOGE]
という感じのモジュールを作れば、prove
の呼び出し時に -PHOGE
と指定することでプラグインを読み込ませることができるようです。今回はShard
プラグインとして作りたいので、App::Prove::Plugin::Shard
モジュールを作ればよいですね。呼び出しは-PShard
になるはずです。
肝心の実装方法ですが、サンプルを読む感じ、まぁ割と直感的かなという印象です。最小は以下の通りになりそうです。これを読み込んでも何も起きないですが。
package App::Prove::Plugin::Shard; use strict; use warnings FATAL => 'all'; use utf8; sub load { # ここにやりたいことを書いたり書かなかったりする return 1; }; 1;
すでにあるApp::Prove::Plugin::*を読んでみる
すでに公開されているプラグインをいくつか読んでみます。今回はテストしたいファイルのリストに介入したいです。似た感じのプラグインを探したところ、App::Prove::Plugin::Countが見つかりました。実装を読んでみると、App::Prove::_get_tests
をラップしてやってるみたいですね。こういう柔軟さがPerlのいいところですよねえ。
今回のShard
プラグインでも同じように App::Prove::_get_tests
をラップして、テスト対象のファイルを絞り込んでいく感じにしていこうと思います。
ざっくり実装してみる
load
にはApp::Prove
のオブジェクトが渡されます。そこにはプラグインに対して渡された引数が含まれているようです。
例えば -PShard=1
と呼び出されたとき
sub load { my (undef, $prove) = @_; my @args = @{ $prove->{args} }; # [1] }
という感じに $prove->{args}
を通じて引数の値にアクセスできるようです。Jestの--shard
ではどのシャードか、と全部で何個のシャードがあるかを渡すようになっています。例えば以下のように指定します。
# シャード1/全体で3個のシャード $ jest --shard=1/3
なのでShard
プラグインでも同じように指定したいです。サンプルやドキュメントにもある通り、プラグインへの引数はカンマ区切りで複数個渡すことができ、先のargs
メンバーに配列として格納されます。というわけで -PShard=1,3
という感じの指定方法がとれそうです。
sub load { my (undef, $prove) = @_; my @args = @{ $prove->{args} }; # -PShard=1,3 としたとき [1, 3] }
これが取れたらあとは App::Prove::_get_tests
をaround
でえいするだけですね。
# useなどは省略してます sub load { my (undef, $prove) = @_; my @args = @{ $prove->{args} }; my $n = $args[0]; # 何番目のシャードを選ぶか my $m = $args[1]; # 全体で何個のシャードがあるか around 'App::Prove::_get_tests' => sub { my $orig = shift; # m個のグループに順番に割り振り -> n番目のグループを選ぶという感じの実装 # n, mともに1以上を想定 # 振り分けが毎回同じになるように並べ替えておく # でもたぶんProveが展開したときにすでに並び変わってる気がしなくもないですね my @test_files = sort $orig->(@_); # データの流れがパイプとは逆向きだからなんか違和感ある return map { $test_files[$_] } grep { ($_ % $m) == n - 1 } 0..$#test_files; }; return 1; }
ざっくり実装できたので、100個のテストファイルがシャードに分けられるか試してみましょう
# テストファイルを作って $ vim example_tests/1.t # 100個に複製 $ seq 2 100 | rargs cp example_tests/1.t example_tests/{1}.t # Shardプラグインを試してみる。5個中の1番目のシャード $ prove -PShard=1,5 ./example_tests example_tests/1.t ... ok example_tests/13.t .. ok example_tests/18.t .. ok example_tests/22.t .. ok example_tests/27.t .. ok example_tests/31.t .. ok example_tests/36.t .. ok example_tests/40.t .. ok example_tests/45.t .. ok example_tests/5.t ... ok example_tests/54.t .. ok example_tests/59.t .. ok example_tests/63.t .. ok example_tests/68.t .. ok example_tests/72.t .. ok example_tests/77.t .. ok example_tests/81.t .. ok example_tests/86.t .. ok example_tests/90.t .. ok example_tests/95.t .. ok All tests successful. Files=20, Tests=20, 1 wallclock secs ( 0.02 usr 0.02 sys + 0.67 cusr 0.06 csys = 0.77 CPU) Result: PASS
100個を5個に分割した内の1つを選んだので、20個のテストが実行されています!想定通りですね!やりました!
シャード化したテストを並列処理させてみよう
Jestの--shard
のユースケースとしては、巨大なテストスイートを小さくし、それぞれのシャードをいくつかのテストランナーへ同時に投げることだと思います。例えば GitHub Actionsでは以下のようなワークフローを書いて実現できますね。(いろいろ省略してます)
jobs: test: strategy: matrix: shard: ["1/3", "2/3", "3/3"] steps: - run: jest --shard ${{matrix.shard}}
今回 Shard
プラグインを作りましたから、これを使って同じことができそうです。実はこれが最初のモチベだったりします。
というわけで書いてみたのが以下ですね。
name: 'CI' on: push jobs: example: runs-on: ubuntu-latest strategy: matrix: shard: ["1,5", "2,5", "3,5", "4,5", "5,5"] steps: - uses: actions/checkout@v3 - uses: shogo82148/actions-setup-perl@v1 with: perl-version: 5.34.1 install-modules-with: cpm enable-modules-cache: true - run: echo PERL5LIB=$PERL5LIB:$PWD/lib:$PWD/local/lib/perl5 >> $GITHUB_ENV - run: prove -PShard=${{matrix.shard}} ./example_tests
このワークフローを開始すると、以下のようにランナーが5つ起動し
それぞれに指定されたシャードのテストを実行させることができるということです。うれしいですね。
Perlのセットアップはactions-setup-perlがめちゃめちゃ便利でした。cache機構もあるので、actions/cache
でなんかいろいろ書いたりしなくていいところが嬉しいポイントでしたね。
その他、地味にハマった点としては、PERL5LIB
へApp::Prove::Plugin::Shard
がある場所へのパスが渡されていなければならないことでした。ずっと「prove --lib
でlib
は読み込んでるはず…なのになんでなの????」ってなってました。
まとめ
prove
にプラグイン機構があることを知ったので素振りしたかった- 題材として、Jestの
--shard
みたいなのをやってみることにした - 割とあっさりできた
- GitHub Actionsでそれぞれのシャードを別々に動かすことができた
おわり
本当は Type::Tiny
のAPIがよくわからずめちゃめちゃハマった話とか、windows-latest
なVMでstrictures
がビルドできなかったりしたなど、割とつらいことが色々ありました。でも本質じゃなかったので書きませんでした…。Perl難しいですねえ。