jest、vitestなどの --testNamePatternのようにPerlのテストで、subtestの名前でテスト対象をフィルタリングできるTest2::Plugin::SubtestFilterをリリースしました。特定のテストだけ実行したい時、便利だと思います!
# t/test.t use Test2::V0; use Test2::Plugin::SubtestFilter; subtest 'foo' => sub { subtest 'nested foo1' => sub { ok 1 }; subtest 'nested foo2' => sub { ok 1 }; }; subtest 'baz' => sub { ok 1; }; done_testing;
# foo1 にマッチするテストだけ実行されている様子
❯ SUBTEST_FILTER=foo1 prove -lvr t/test.t
t/test.t ..
# Seeded srand with seed '20251020' from local date.
ok 1 - foo {
ok 1 - nested foo1 {
ok 1
1..1
}
ok 2 - nested foo2 # skip
1..2
}
ok 2 - baz # skip
1..2
ok
All tests successful.
Files=1, Tests=2, 0 wallclock secs ( 0.00 usr 0.00 sys + 0.04 cusr 0.00 csys = 0.04 CPU)
Result: PASS
仕組み解説
1. subtest 関数のオーバーライド
プラグインは Test2 の subtest 関数を動的に置き換えます:
# lib/Test2/Plugin/SubtestFilter.pm:14-26 sub import { my $class = shift; my $caller = caller; # 呼び出し元の名前空間から元の subtest 関数を取得 my $orig = $caller->can('subtest') or return; # subtest をフィルタリング機能付きのものに置き換え no strict 'refs'; no warnings 'redefine'; *{"${caller}::subtest"} = _create_filtered_subtest($orig, $caller); }
2. Test2 API によるサブテスト名の追跡
Test2 の context と hub を使って、ネストされたサブテストの階層を追跡します:
# lib/Test2/Plugin/SubtestFilter.pm:67-72 my $ctx = context(); my $hub = $ctx->hub; # 現在のサブテスト名をメタデータに保存 $hub->set_meta(subtest_name => $name); # スタック上のすべてのサブテスト名を取得 my @stacked_subtest_names = map { $_->get_meta('subtest_name') } $ctx->stack->all; # 完全なサブテスト名を構築(例: "parent child grandchild") my $current_subtest_fullname = join $SEPARATOR, @stacked_subtest_names;
3. B::Deparse による静的コード解析
子サブテストの存在を事前に検出するために、B::Deparse を使用してコードを文字列に変換し、正規表現で解析します:
# lib/Test2/Plugin/SubtestFilter.pm:82-92 my $obj = B::svref_2object(\$code); my $source = $deparse->coderef2text($code); # サブテストの呼び出しをパターンマッチで抽出 my @child_subtest_names = $source =~ /subtest\(['"](.+?)['"]/g; # UTF-8デコード処理(\x{XXXX} 形式を文字に変換) if (@child_subtest_names) { my @child_subtest_fullnames = map { my $decoded = $_; $decoded =~ s/\\x\{([0-9a-fA-F]+)\}/chr(hex($1))/ge; join $SEPARATOR, $current_subtest_fullname, $decoded; } @child_subtest_names; ... }
4. フィルタリングのロジック
3段階のチェックでフィルタリングを実行します:
# ステップ1: 現在のサブテスト名がフィルタにマッチ? if ($current_subtest_fullname =~ $filter) { # マッチした場合、このサブテストとすべての子を実行 my $pass = $original_subtest->($name, $params, $code, @args); $ctx->release; return $pass; } # ステップ2: 子サブテストのいずれかがマッチ? if (@child_subtest_names) { my @child_subtest_fullnames = map { ... } @child_subtest_names; if (any { $_ =~ $filter } @child_subtest_fullnames) { # 子がマッチした場合、親を実行(子のフィルタリングは再帰的に行われる) my $pass = $original_subtest->($name, $params, $code, @args); $ctx->release; return $pass; } } # ステップ3: マッチしない場合はスキップ $ctx->skip($name); $ctx->release; return 1;
以上です!