たいちょーの雑記

ぼくが3日に一度くらい雑記をかくところ

App::Prove::Plugin::の素振りをした話

多分初めてのPerlネタです。

proveのプラグイン書いてみたかった

metacpanでApp::Proveのドキュメントを読んでいたら、プラグイン機構があることを知ったので素振りをしてみました。ネタはなんでもよかったんですが、今回は Jestの--shardオプションみたいなのを作ってみます。

この記事で実装したやつを丁寧に書いたやつは以下のリポジトリにあります。

github.com

ドキュメントを読んでみる

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_testsaroundでえいするだけですね。

# 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つ起動し

example(1/5 ~ 5/5)

それぞれに指定されたシャードのテストを実行させることができるということです。うれしいですね。

うれしい


Perlのセットアップはactions-setup-perlがめちゃめちゃ便利でした。cache機構もあるので、actions/cacheでなんかいろいろ書いたりしなくていいところが嬉しいポイントでしたね。

その他、地味にハマった点としては、PERL5LIBApp::Prove::Plugin::Shardがある場所へのパスが渡されていなければならないことでした。ずっと「prove --liblibは読み込んでるはず…なのになんでなの????」ってなってました。

まとめ

  1. proveプラグイン機構があることを知ったので素振りしたかった
  2. 題材として、Jestの--shardみたいなのをやってみることにした
  3. 割とあっさりできた
  4. GitHub Actionsでそれぞれのシャードを別々に動かすことができた

おわり

本当は Type::TinyAPIがよくわからずめちゃめちゃハマった話とか、windows-latestVMstricturesがビルドできなかったりしたなど、割とつらいことが色々ありました。でも本質じゃなかったので書きませんでした…。Perl難しいですねえ。