Capture::Tinyを使ってSTDERRに何も出力されていないことをテストする

経緯

  1. 数千万行あるファイルに対して1行ごとに処理するようなバッチを書く
  2. utf8フラグ付きの文字列をencode_utf8せずに処理してしまう
  3. 大量のWide character in ...という警告がログに出力される
  4. 気付かずに本番リリースされ、それが原因で障害発生
  5. 悲しみに閉ざされる

対策

あれ以来、テストでSTDERRのチェックを行うようにしている。
あと、テストデータにマルチバイト文字列を忘れずに入れる。

my ($stdout, $stderr, $exit) = capture { MyApp->new->run(@argv) };
is $stderr, '', '標準エラー出力には何も出力されていない';

Path::ClassからPath::Tinyに移行した時に書き換えた処理

バージョン

  • Path::Class 0.33
  • Path::Tiny 0.056

オブジェクト生成

Path::Classで生成したオブジェクトは、ファイルはPath::Class::File、ディレクトリはPath::Class::Dirとなる。Path::TinyはどちらもPath::Tinyとなる。

# Path::Class
$file = file($path);
$dir  = dir($path);

# Path::Tiny
$file = path($path);
$dir  = path($path);

ディレクトリ配下のファイルオブジェクト生成

# Path::Class
$file = $dir->file($name);

# Path::Tiny
$file = $dir->child($name);

ディレクトリ配下のディレクトリオブジェクト生成

# Path::Class
$dir = $dir->subdir($name);

# Path::Tiny
$dir = $dir->child($name);

ディレクトリ削除(配下ファイルごと)

# Path::Class
$dir->rmtree;

# Path::Tiny
$dir->remove_tree;

ファイルオープン

# Path::Class
$read = $file->openr;
$read = $file->open('<:encoding(cp932)') or die "Can't read $file: $!";
$read = $file->open('<:utf8') or die "Can't read $file: $!";

# Path::Tiny
$read = $file->openr;
$read = $file->openr(':encoding(cp932)');
$read = $file->openr_utf8;

ファイルの内容を配列に読み込み

# Path::Class
@lines = $file->slurp( chomp => 1 );

# Path::Tiny
@lines = $file->lines( { chomp => 1 } );

ファイルにまとめて書き込み

# Path::Class
$file->spew( iomode => ':utf8', $data );

# Path::Tiny
$file->spew( { binmode => ':utf8' }, $data );

親ディレクトリ名でファイルを作成

# Path::Class
file($file->parent)->touch;

# Path::Tiny
$file->parent->touch;

ファイルをコピー

# Path::Class
$file->copy_to($path);

# Path::Tiny
$file->copy($path);

ファイルをリネーム

# Path::Class
$file->move_to($file_path);

# Path::Tiny
$file->move($file_path);

ファイルを指定ディレクトリに移動

Path::Tinyのmoveはrenameと同等の機能なので注意が必要。

# Path::Class
$file->move_to($dir_path);

# Path::Tiny
use File::Copy 'move';
move $file, $dir_path;

ディレクトリ配下のファイルを処理

# Path::Class
$dir->recurse( callback => sub {
    my $file = shift;
    return unless -f $file;
    print $file, "\n";
});

# Path::Tiny
my $iterator = $dir->iterator( { recurse => 1 } );
while( my $file = $iterator->() ) {
    next unless $file->is_file;
    print $file, "\n";
}

ディレクトリ構成を保ったままn日以前のファイルを移動する

移動先のディレクトリ作成

find /from -type f -mtime +31 | xargs -I {} dirname {} | sort | uniq | sed 's!/from!/to!g' | xargs mkdir -pv

ファイルを移動

find /from -type f -mtime +31 | sed 's!\(/from\)\(.*\)!\1\2 /to\2!g' | xargs --max-args=2 mv -v

rsyncとか使ってコピーした後に削除とかやりたかったけど、n日以前のファイルコピー方法が分からなかった。

xargsが引数を複数取ることが出来るのを初めて知った。便利。

Perl 5.10.0以前だとFile::Spec 3.40以降のインストールに失敗する

Perl 5.8.8で試してみた。

waniji@localhost:~$ plenv versions
  system
  5.18.2
* 5.8.8 (set by /home/waniji/.plenv/version)

waniji@localhost:~$ cpanm -lextlib File::Spec
--> Working on File::Spec
Fetching http://www.cpan.org/authors/id/S/SM/SMUELLER/PathTools-3.47.tar.gz ... OK
Configuring PathTools-3.47 ... OK
Building and testing PathTools-3.47 ... FAIL
! Installing File::Spec failed. See /home/waniji/.cpanm/work/1404224796.5765/build.log for details. Retry with --force to force install it.

原因

File::Spec 3.40以降はTest::More 0.88以上に依存しているが、Makefile.PLにその記述がない。
Perlが5.10.0以前の場合はコアモジュールとして入ってるTest::Moreのバージョンが0.88より古いため、上記のようにテストに失敗してしまう。

回避策

File::Specをインストールする前に、Test::Moreを0.88以上にすれば良い。

waniji@localhost:~$ cpanm -lextlib Test::More
--> Working on Test::More
Fetching http://www.cpan.org/authors/id/E/EX/EXODIST/Test-Simple-1.001003.tar.gz ... OK
Configuring Test-Simple-1.001003 ... OK
Building and testing Test-Simple-1.001003 ... OK
Successfully installed Test-Simple-1.001003 (upgraded from 0.62)
1 distribution installed

waniji@localhost:~$ cpanm -lextlib File::Spec
--> Working on File::Spec
Fetching http://www.cpan.org/authors/id/S/SM/SMUELLER/PathTools-3.47.tar.gz ... OK
Configuring PathTools-3.47 ... OK
Building and testing PathTools-3.47 ... OK
Successfully installed PathTools-3.47 (upgraded from 3.12)
1 distribution installed

その他

この問題は既に報告されていた。

英語に自信が無いけど、多分、「"blead perl"では修正されているけど、安定版としてリリースされてないから、CPANのモジュールにはまだ適応されないよ!」って書いてる...はず。
最近、この修正が適応されたPerl 5.20.0がリリースされたから、CPANのモジュールもそろそろ修正されるんじゃないかなーと予想。

Carpは継承関係のモジュールを信頼する

初めに

以下のドキュメントにCarpの詳細説明があるので、それを読めば挙動が分かります。
Carp - モジュールのための warn と die の代替

この記事では、実際に継承してるコードを例に出して説明しようと思います。

コードと実行結果

client.pl

#!/usr/bin/env perl
use strict;
use warnings;
use Concrete;

Concrete->new->run(@ARGV);

Concrete.pm(具象クラス)

package Concrete;
use strict;
use warnings;
use Carp;
use parent 'Abstract';

sub speak {
    my $self = shift;
    croak "ダレカタスケテー";
}

1;

Abstract.pm(抽象クラス)

package Abstract;
use strict;
use warnings;

sub new {
    my $class = shift;
    bless {}, $class;
}

sub run {
    my $self = shift;
    $self->speak;
}

sub speak {}

1;

実行結果

$ ./client.pl
ダレカタスケテー at ./client.pl line 6.

挙動について

このコードを実装した際、呼び出し元がAbstract::runなので、
ダレカタスケテー at Abstract.pm line 12. と出るかと思ったら、
client.plの行数が出てしまいました。

これは何故かというと、継承関係にある呼び出し元は(具体的には@ISAに入っていれば)、
安全であると判断されスルーされます。
更に上位の呼び出し元はclient.plになるので、上記の実行結果となりました。

これだとエラーとしては分かり辛いなぁと思います。

対策

dieを使う

dieを実行した行数とそのファイル名を返します。

$ ./client.pl
ダレカタスケテー at Concrete.pm line 10.

confessを使う

confessスタックトレースを伴うエラーを発生させます。

$ ./client.pl
ダレカタスケテー at Concrete.pm line 10.
    Concrete::speak('Concrete=HASH(0x177b178)') called at Abstract.pm line 12
    Abstract::run('Concrete=HASH(0x177b178)') called at ./client.pl line 6

Carp::verboseを有功にする

全てのcroakconfessに、carpcluckにとして扱うようにします。
CPANからダウンロードして使用している外部モジュールなども対象となります。

$ perl -MCarp=verbose client.pl
ダレカタスケテー at Concrete.pm line 10.
    Concrete::speak('Concrete=HASH(0x184b1a8)') called at Abstract.pm line 12
    Abstract::run('Concrete=HASH(0x184b1a8)') called at client.pl line 6

まとめ

Template Methodパターンで作ってたらこの問題にぶち当たりました。
CPANに公開しないモジュールであれば、confessを使っておくのが良いかなぁと思ってます。
dieは情報が少ないし、Carp::verboseはやりすぎ感がある……)

おまけ

evalで例外をキャッチするとこんな表示になります。

Abstract.pm(runを書き換え)

sub run {
    my $self = shift;
    eval { $self->speak };
    if($@) {
        print "$@";
        warn "チョットマッテテー";
    }
}

実行結果

Use of uninitialized value $_ in string at Abstract.pm line 14.
ダレカタスケテー at Concrete.pm line 10.
    Concrete::speak('Concrete=HASH(0x1ec0178)') called at Abstract.pm line 12
    eval {...} called at Abstract.pm line 12
    Abstract::run('Concrete=HASH(0x1ec0178)') called at ./client.pl line 6
チョットマッテテー at Abstract.pm line 15.

eval {...} が追加されてますね!
やりたかっただけです!

Perl 5.8.8でfatpacked cartonを頑張って作った

carton bundleを実行すると、App::FatPackerを用いて、cartonを1ファイルのスクリプトにまとめてくれます。
このfatpacked cartonをCentOS5のsystem perl(5.8.8)で使用したかったのですが、carton bundleで生成されたfatpacked cartonを実行すると、エラーが発生して動きませんでした。
何とか動作させる方法を調べたので、エラーの原因と解消方法をまとめておこうと思います。

なお、carton bundleでのfatpacked carton生成は、"experimental feature"のようです。
https://github.com/miyagawa/carton/issues/141

また、fatpacked cartonが原因の課題が多いため、carton bundleではデフォルトで作成させず、別コマンドに切り離すIssueが作成されてます。
https://github.com/miyagawa/carton/issues/156

Cartonのバージョン

v1.0.12

App::FatPackerがfatpackする条件

CHECKブロックで%INCからfatpackするモジュール一覧を取り出すので、それまでにrequireされたモジュール(のはず)

エラー内容と解決方法

1. Can't locate Algorithm/C3.pm in @INC

Algorithm::C3がfatpackされていないという理由で怒られます。
この問題の原因はClass::C3です。

CartonとAlgrorithm::C3の関係は以下の通り。

Carton
 \- Moo
     \- MRO::Compat
         \- Class::C3
             \- Algorithm::C3

Class::C3はClass::C3::XSが存在する場合は、Algorithm::C3がrequireされません。
Class::C3::XSはPerlのバージョンが5.9.5より下であれば自動でインストールされるので、fatpackする時にXSが対象となり、Algorithm::C3はfatpackされません。
しかし、fatpacked cartonではXSモジュールのロードに失敗するので、Algrorithm::C3をrequireしようとしてエラーとなります。
(詳細はClass::C3のBEGINブロック参照)

対処法は、Class::C3::XSをuninstallすることです。

cpanm -U Class::C3::XS
carton bunlde

因みに、Perl 5.10.0以上だとMRO::Compatのかわりにmroが使用されるので、この問題は発生しません。
(詳細はMoo/_mro.pmを参照)

2. List::Util version 1.33 required--this is only version 1.18

List::Utilのバージョンが古いという理由で怒られます。
これは、CPAN::Metaの2.133380以降がList::Utilの1.33以上に依存していることが原因です。

CartonはCPAN::Metaの2.120921に依存していますが、Perl 5.8.8のコアモジュールにCPAN::Metaが入っていないため、CPANからインストールされます。
この時インストールされるバージョンは2.140640(2014/03/11時点)です。
このバージョンではList::Util 1.33以上に依存しているので、併せてList::Util 1.38(2014/03/11時点)がインストールされ、carton bundle時にfatpackされます。
しかし、List::UtilはXSモジュールなので、fatpacked cartonではロードに失敗します。
なので、コアのList::Utilが使われるのですが、バージョンが1.18と古いため、エラーが発生します。

対処法は、CPAN::MetaをuninstallしてCPAN::Metaの2.120921を入れ直せば良いです。

cpanm -U CPAN::Meta
cpanm CPAN:Meta@2.120921
carton bundle

因みに、Perl 5.17.1以上だとコアモジュールにCPAN::Metaの2.120921が入っているので、この問題は発生しません。

まとめ

以下のどれかを採用しましょう。

  1. End of lifeとなったPerl 5.8.8とか使ってないで、Perl 5.18.2(or later)を使おう
  2. サーバセットアップ時にPerlと一緒にCartonをインストールしよう
  3. cpan-module-bootstrapを使おう

どうしてもPerl 5.8.8でfatpacked cartonを作りたければ、上記の方法で頑張ってつくろう。