2020/02/12

Test::AllModules を Windows で利用するときの罠

Test::AllModules というモジュールがありまして、任意のパス以下のモジュールを再帰的にロードしてコンパイルテストできる便利モジュールです。

以下のような感じで lib 以下全部ごっそりコンパイルテストできます。

use Test::AllModules;

all_ok(
    search_path => 'ModuleName',
    use => 1,
);

この Test::AllModules は、前職のお仕事でも使っていたこともあって、個人的にはそこそこ安定していると思っていたのですが、数年ぶりに自分のCPAN モジュールで使ってみたらえぐいことに Windows で全く動かないという絶望を抱えていることが発覚してしまいました。まったく、油断もすきもないですね。

どういう状況だったかというと CPANテスターからのレポートだとこんな感じ で Windows 環境でずっこけていることがわかります。

Output from 'C:\strawberry182\perl\bin\perl.exe ./Build test':

Can't locate Win32.pm in @INC (you may need to install the Win32 module) (@INC contains: lib) at C:/strawberry182/perl/lib/Cwd.pm line 763.
BEGIN failed--compilation aborted.
t\00_compile.t ... skipped: (no reason given)
t\100_testset.t .. ok
t\10_basic.t ..... ok
t\15_reparse.t ... ok
t\20_util.t ...... ok
t\50_cli.t ....... ok

00_compile.t の中で Test::AllModules が使われていて、コンパイルテスト以外は通っているので、問題は Test::AllModules に絞って良さそうですね。

エラーメッセージによれば

Can't locate Win32.pm in @INC (you may need to install the Win32 module) (@INC contains: lib) at C:/strawberry182/perl/lib/Cwd.pm line 763.

Win32.pm が読めねーよ、と。@INC のなかにねーよ、と。C:/strawberry182/perl/lib/Cwd.pm の 763行目でなんか呼ばれたらしいけどだめだったー、ということですね。Cwd.pm って何やってんだっけ? 推測してもしょうがないので手元の Windows マシンで再現してみましょう。

perl -Ilib t/00_compile.t
Can't locate Win32.pm in @INC (you may need to install the Win32 module) (@INC contains: lib) at C:/Strawberry/perl/lib/Cwd.pm line 605.

なるほど。行数こそ違いますが同じようにこけました。直せそうですね。

603 sub _win32_cwd {
604    my $pwd;
605    $pwd = Win32::GetCwd();
606    $pwd =~ s:\\:/:g ;
607    $ENV{'PWD'} = $pwd;
608    return $pwd;
609 }

該当行は $pwd = Win32::GetCwd(); です。ふむ、ここにくるまでに Win32.pm 読み込んでねーのかよと思いつつ、まあとにかくエラーメッセージの通り Win32.pm が読めさえすれば良さそうです。

ところでなんで読めないんだっけ?

よくわからないので、もう一度最初のエラーメッセージに立ち戻ってみると、

Can't locate Win32.pm in @INC (you may need to install the Win32 module) (@INC contains: lib)

注目は最後のカッコのなかです。 @INC contains: lib あれ、@INC の中身 lib だけになってる?

だれだ、そんなことやってるやつ。

犯人は、 Test::AllModules 自分でした。。。

sub _classes {
    my ($search_path, $lib, $shuffle) = @_;

    local @INC = @{ $lib || ['lib'] };
    my $finder = Module::Pluggable::Object->new(
        search_path => $search_path,
    );
    my @classes = ( $search_path, $finder->plugins );

    return $shuffle ? _shuffle(@classes) : sort(@classes);
}

local @INC = @{ $lib || ['lib'] }; なにこれ!!

気づいたは良いけど、自分がなぜこの @INC いじるコード書いたのか全く記憶になかったんですよね。。少なくとも7年前にコミットしたみたいなんでまあ、忘れますよね。でも、よくよく考えてみると、意図しないパスの配下にあるモジュールを誤って読んでしまわないように配慮してるわけですね。たぶん。

ということはこのコード自体は良さそうだと。7年前に仕込んだバグかと思ったけど違った。

となると、Windows のときだけ libパスを変えて実行できれば良さそうだなとなって、以下のようになりました。

use Test::AllModules;

my %win_option = ();
if ($^O eq 'MSWin32') {
    %win_option = (
        'lib' => ['lib', @INC],
    );
}

all_ok(
    search_path => 'ModuleName',
    use => 1,
    %win_option
);

すごく細かい話をすると、Windows 環境にかぎらず、コンパイルフェーズで lib 以外の場所からモジュールを動的に読むことになる場合は、同じようにパスを明示する必要があります。

という話でした。まれによくあるやつ。

サイト内検索