Test::Moreの日本語出力処理を覗いてみる。

Test::Moreで日本語出力

Test::Moreでの出力に日本語が含まれていると、

Wide character in print〜

と怒られますよね。

  • サンプルコード
#!/usr/bin/evn perl
use strict;
use warnings;
use utf8;

use Test::More tests => 1;

is('あああ', 'いいい', 'テスト');

これはPerlの内部文字列のまま出力しているからなので、STDOUTとSTDERRの出力からUTF8フラグを落とせばいいと考えます。

  • しかしこれでもまだ「Wide character in print〜」と怒られてしまいます。
#!/usr/bin/evn perl
use strict;
use warnings;
use utf8;

use Test::More tests => 1;

# 標準出力、標準エラー出力からの出力をエンコードする。
binmode STDOUT, ':utf8';
binmode STDERR, ':utf8';

is('あああ', 'いいい', 'テスト');
  • (当然こうすれば怒られません。)
#!/usr/bin/evn perl
use strict;
use warnings;
use utf8;

use Encode qw/encode_utf8/;
use Test::More tests => 1;

# 出力文字列をエンコードする。
is(encode_utf8('あああ'), encode_utf8('いいい'), encode_utf8('テスト'));

調査

「なんでかな〜」と思ったので、調べてみました。
Test::MoreのPodで、「Wide character」で検索してみると、いきなり解決策が見つかりました。

utf8 / "Wide character in print"
If you use utf8 or other non-ASCII characters with Test::More you might get a "Wide character in print"
warning. Using "binmode STDOUT, ":utf8"" will not fix it. Test::Builder (which powers Test::More) duplicates
STDOUT and STDERR. So any changes to them, including changing their output disciplines, will not be seem by
Test::More.

The work around is to change the filehandles used by Test::Builder directly.

my $builder = Test::More->builder;
binmode $builder->output, ":utf8";
binmode $builder->failure_output, ":utf8";
binmode $builder->todo_output, ":utf8";

というわけで、書かれている通りに変更するとうまくいきます。

  • 解決策
#!/usr/bin/evn perl
use strict;
use warnings;
use utf8;

use Test::More tests => 1;

# Podに書かれていたことをそのままコピペ
my $builder = Test::More->builder;
binmode $builder->output,         ":utf8";
binmode $builder->failure_output, ":utf8";
binmode $builder->todo_output,    ":utf8";

is('あああ', 'いいい', 'テスト');

ここで、中の処理がどうなっているのか見たかったので処理を追っていくことにしました。

まずは、Test::Moreの「is」関数を見るとこんな感じでした。

  • Test::More
sub is ($$;$) {
    my $tb = Test::More->builder;

    return $tb->is_eq(@_);
}

次は、「builder」関数を検索しましたが見つからなかったので、「@ISA」にあるTest::Builder::Moduleを検索するとありました。

  • Test::Builder::Module
sub builder {
    return Test::Builder->new;
}

ということで、Test::Builderに処理を委譲してそうですので、Test::Builderのコンストラクタを見ると、

  • Test::Builder
my $Test = Test::Builder->new;

sub new {
    my($class) = shift;
    $Test ||= $class->create;
    return $Test;
}

となっており、シングルトンになっています。newって名前だったのでそうなっているとは思わなかったです。
ここまででTest::Builderクラスの「is_eq」が呼び出されていることまでがわかりました。

  • 〜この先もTest::Builder内で処理が進んでいきますが複雑かつ長いので今回は省略。。。〜


ここで少し戻り、前述の解決策としてbinmode関数にて":utf8"を指定していた「output」、「failure_output」、「todo_output」関数のreturnが何か見てみました。

  • Test::Builder
sub _autoflush {
    my($fh) = shift;
    my $old_fh = select $fh;
    $| = 1;
    select $old_fh;

    return;
}

my( $Testout, $Testerr );

sub _dup_stdhandles {
    my $self = shift;

    $self->_open_testhandles;

    # Set everything to unbuffered else plain prints to STDOUT will
    # come out in the wrong order from our own prints.
    _autoflush($Testout);
    _autoflush( \*STDOUT );
    _autoflush($Testerr);
    _autoflush( \*STDERR );

    $self->reset_outputs;

    return;
}
sub _open_testhandles {
    my $self = shift;

    return if $self->{Opened_Testhandles};

    # We dup STDOUT and STDERR so people can change them in their
    # test suites while still getting normal test output.
    open( $Testout, ">&STDOUT" ) or die "Can't dup STDOUT:  $!";
    open( $Testerr, ">&STDERR" ) or die "Can't dup STDERR:  $!";

    #    $self->_copy_io_layers( \*STDOUT, $Testout );
    #    $self->_copy_io_layers( \*STDERR, $Testerr );

    $self->{Opened_Testhandles} = 1;

    return;
}
:
:
sub reset_outputs {
    my $self = shift;

    $self->output        ($Testout);
    $self->failure_output($Testerr);
    $self->todo_output   ($Testout);

    return;
}

ここを見ると「output」、「todo_output」には「$Testout」が、「failure_output」には「$Testerr」変数がセットされています。

「$Testout」と「$Testerr」が何かを見てみると、

  • Test::Builder(_open_testhandles関数)
    open( $Testout, ">&STDOUT" ) or die "Can't dup STDOUT:  $!";
    open( $Testerr, ">&STDERR" ) or die "Can't dup STDERR:  $!";

となっていて、これは標準出力と標準エラー出力を複製してそれぞれの変数にセットしているようです。
(_autoflushの「$| = 1;」でさらにバッファリングしないように設定されています。)


この複製された標準出力と標準エラー出力を利用して結果の出力をしているので、

    # Test::Builderが使用している標準出力、エラー出力は「STDOUT」、「STDERR」ではない。
    binmode STDOUT, ":utf8";
    binmode STDERR, ":utf8";

としてもTest::Moreの出力には反映されていないみたいなのでした。
(STDOUT、STDERRを経由していないので、いくらそこをbinmodeで変更しても関係ない。)

その他

  • 実はこの辺りのことは、「続・初めてのPerl」の「18.7 独自のTest::*モジュールの開発」にも書いてありました。今回調べてみてこの章に書いてあることを少しは理解出来た気がします。
  • ModuleのソースをperldocでPodを見るように見れないかなと思ったら、「perldoc -m Test::More」のように「-m」オプションで見れるんですね。
    • (英語は得意でありませんが、最近perldocを見るのが楽しくなってきました。)