Masteries

技術的なことや仕事に関することを書いていきます.

小ネタ: Perlで関数の返り値の一部を無視する

例えば, func という関数があって, これが次のような実装になっていて, 3つの返り値を返すとします.

sub func {
    ...
    return ($x, $y, $z);
}

func を呼び出す際, 「返り値の1つ目と3つ目は利用するけれど, 2つ目は利用しない」という時は, undefを使って次のように書けます:

my ($x, undef, $z) = func();

ここでのundefは, Go言語におけるアンダースコア変数みたいなもの... と捉えると良さそうです.

x, _, z := func()

myundef組み合わせるの, 実は未定義動作だったりしないかな...?」と一瞬思ったのですが, perldocでも,

perldoc.jp

my ($x, $y, undef, $z) = foo(); # Ignore third value returned

...という形で紹介されているので, 普通に使える小技(?)と思って良さそうです.

不必要な変数は宣言しないに越したことはない(例えば2つ目の返り値を, 使わないのに$yとして定義すると, 後で$yはどこで使っているのだろう...? と混乱してしまう)ので, 使えるシーンがあれば積極的に使っていきたいところですね.

今だからこそ「リモートワークの達人」読んだ

昨今このような情勢で, そろそろ1年近く在宅で勤務しているので, 今あらためて「リモートワークの達人」を読んでみました.

読書メモ

- 毎日4時間はみんな同じ時間に働いたほうがいい
- 週に一度「最近やっていること」というテーマで話し合いの場を設けるとよい
-- 1週間でやったこと, 翌週やることを手短に書き込む
-- 作業の調整は不要で, 一緒にやっているという感覚を持てると良い
- リモートワーカーが孤独になりやすいのは事実. だから意識的に外に出た方がいい
- リモートワークにおいては, 働かないより働きすぎる方を心配するべき
-- 気がついた時には完全に燃え尽きている可能性がある
- リモートワークをうまくやるには?
-- こまめに成果を見せる
-- いつでも連絡が取れるようにする
- リモートワークでは, オフィスで働く以上に人の繋がりが重要
-- 文字だけでやり取りする時, 人は悪い方に流されやすくなる
-- 前向きな人間を集め, チームメンバーを思いやり雰囲気を盛り上げるタイプの人が必要
- 「嫌な言葉」, 「感情的な対立」, 「悪いムード」を徹底的に排除していくことが大切
- メールやチャットや掲示板で話し合いをするので, リモートワークには文章力が欠かせない
-- 採用するなら, 判定基準に入れた方が良い
-- 文章がうまくなる方法は読むこと. 文体は二の次, まずは明晰さ
- 2ヶ月に1度のペースで1on1を実施する. 毎月出来るといいが, 2ヶ月でうまくまわっている
-- ゆるく話をする. やる気は脆いので, ちょっとした不満で仕事が進まなくなる. 1on1で定期的にチェックする
- リモートで働いていて, 一向に手が動かないと思ったら注意信号
-- 今の仕事の問題点を明らかにして, 改善する必要がある

感想/考察

週に一度「最近やっていること」というテーマで話し合いの場を設けるとよいというのは, 異動前のチームで「成果発表会」という催しがあり, 1ヶ月単位でやったことや所感などを共有して, フィードバックをしあっていたので, 割と良かったので納得度がありました. 今のチームはチームの人数が多いこともあってそういった催しはないのですが, 人数が多くてもやれる良い方法はありそう, と思ったので模索していきたいですね.

「リモートワークでうまく仕事をするには?」のところで, コツとして「こまめに成果を見せる」, 「いつでも連絡を取れるようにする(反応する)」というのが書かれていて, これは最近意識していることなので裏付け(?)が得られた気持ちになりました. こういう振る舞いが安定して出来ると, 信頼貯金を貯めやすくなるように思います.

あとは, (「あわせてよみたい」で紹介しているshibayuさんのエントリでも触れられていますが)「嫌な言葉」, 「感情的な対立」, 「悪いムード」を徹底的に排除していくことが大切というのは本当にそうですね. オフィスで働いている時より, リモートワークの方がそういった状態に陥った時のリカバリーコストが高い気がしているので, そうならないように割れ窓理論的に先手打って対応していくのが大事そう, と思いました. チームでは割とシニア寄りなので, そういったところ俯瞰して気付けるようになれると良さそうです.

元々, 京都/東京という2拠点で仕事をしていたこともあって, 昨今のコロナ禍で在宅勤務になっても, 割とスムーズに移行できたのではないかと思っています. うまいことやっていくための暗黙知? みたいなものを, この本を通して改めて文章化できたなーと思います.

あわせてよみたい

blog.shibayu36.org

LWP::UserAgentのタイムアウトがうまく効かなかった事象の調査 (序章)

皆様, メリークリスマス! この記事は, 「Perl Advent Calendar 2020」の25日目の記事です.

qiita.com

昨日は, id:hitode909 さんの「Perlアプリケーションの依存モジュールの更新についてWEB+DB PRESS vol.120のPerl Hackers Hubに寄稿しました」でした.

blog.sushi.money


さて, 本題です. Perlにおいて, HTTPリクエストを送る時のデファクトスタンダードと言えばLWP::UserAgentではないでしょうか.

metacpan.org

LWP::UserAgentには, timeoutというオプションがあります. 2020年12月25日現在の最新版は6.50ですが, このバージョンではtimeoutのデフォルトは180秒となっていて, この場合HTTPリクエストを送って180秒経過するとその通信を打ち切ってくれます(リクエスト先のサーバーで処理に時間がかかり, 180秒で返せなかった場合など).

先日, ある特定の状況下でこのタイムアウトが効かず, しばらく処理が続いてしまうという事象に遭遇しました. LWP::UserAgentのログを手がかりに調査した様子を紹介することで, 「Perl Advent Calendar 2020」の25日目の記事としようと思います.

...ただ先に述べておくと, 今回は時間の制約上「こうかな...?」という仮説を立てる所で力尽きてしまいました. なので, どちらかというと, 「ログを参考にして実装を追う」時の一例, として見てもらった方が良いかもしれません.

LWP::UserAgent

該当のリクエストにおいて, LWP::UserAgentは以下のようなリクエストを出力していました:

500 Can't connect to example.com:80

これを手がかりに, まずはLWP::UserAgentCan't connect toといったログを出力している場所を探してみました. ちなみにこういう時は,

  • metacpanでGitHubのLWP::UserAgentの実装があるリポジトリを探す
  • 見つけたリポジトリを, ghqで手元にclone
  • kazuhoさんのブログで紹介されていた「peco改」で探し, エディタで開く

... という流れが多いです.

github.com

blog.kazuhooku.com

さて, そうすると, LWP::Protocol::http に次のようなコードがあることがわかりました. LWP::Protocol::http は, その名の通りHTTPプロトコルによる通信を司るモジュールなので, ここを調べていくと良さそうです.

github.com

   # IO::Socket::INET leaves additional error messages in $@
    my $status = "Can't connect to $host:$port";

前後のコードを追いかけていきましょう. すると, この直前で $self->socket_class->new() を使って, $sock という変数を定義していることがわかります.

github.com

    my $sock = $self->socket_class->new(PeerAddr => $host,
                    PeerPort => $port,
                    LocalAddr => $self->{ua}{local_address},
                    Proto    => 'tcp',
                    Timeout  => $timeout,
                    KeepAlive => !!$self->{ua}{conn_cache},
                    SendTE    => $self->{ua}{send_te},
                    $self->_extra_sock_opts($host, $port),
                       );

LWP::Protocol::http における socket_class の実装は以下のとおりです.

github.com

sub socket_class
{
    my $self = shift;
    (ref($self) || $self) . "::Socket";
}

この場合, $selfLWP::Protocol::http のオブジェクト, もしくはLWP::Protocol::httpという文字列になるので, つまり $self->socket_classLWP::Protocol::http::Socket を返します.

続いて, LWP::Protocol::http::Socket を探します. するとLWP::Protocol::httpと同じファイルの下に, 以下のように定義されていました.

github.com

package # hide from PAUSE
    LWP::Protocol::http::Socket;

use parent -norequire, qw(LWP::Protocol::http::SocketMethods Net::HTTP);

余談ですが, hide from PAUSEについて. これは id:Songmu さんの以下のエントリにある説明を読むとよいでしょう.

songmu.jp

...話を戻します. ここまでをまとめると, $self->socket_class->new は実際には LWP::Protocol::http::Socket->new が実行されることがわかりました. そして, LWP::Protocol::http::Socket は, LWP::Protocol::http::SocketMethodsNet::HTTPを継承したものということもわかりました.

つまり, $self->socket_class->new を呼び出した時に実行されるnew メソッドは, LWP::Protocol::http::SocketMethodsNet::HTTPのいずれかに定義されているはずです. 結論から言うと(名前からも察せると思いますが), LWP::Protocol::http::SocketMethodsにはnewメソッドがなく, Net::HTTPnewメソッドが呼び出されていることがわかります.

github.com

package # hide from PAUSE
    LWP::Protocol::http::SocketMethods;

sub ping {
    my $self = shift;
    !$self->can_read(0);
}

sub increment_response_count {
    my $self = shift;
    return ++${*$self}{'myhttp_response_count'};
}

Net::HTTP

続いて, Net::HTTPを調べていきます. 対象は, 2020年12月25日現在の最新版, 6.19です.

metacpan.org

our @ISA = ($SOCKET_CLASS, 'Net::HTTP::Methods');
 
sub new {
    my $class = shift;
    Carp::croak("No Host option provided") unless @_;
    $class->SUPER::new(@_);
}

このコードを見ると, Net::HTTPもまた何かのモジュール($SOCKET_CLASS)を継承しており, $class->SUPER::new(@_);でそのモジュールのnewメソッドを呼び出していることがわかります.

$SOCKET_CLASSを宣言している部分はと言うと, 次のような実装になっています:

metacpan.org

use vars qw($SOCKET_CLASS);
unless ($SOCKET_CLASS) {
    # Try several, in order of capability and preference
    if (eval { require IO::Socket::IP }) {
       $SOCKET_CLASS = "IO::Socket::IP";    # IPv4+IPv6
    } elsif (eval { require IO::Socket::INET6 }) {
       $SOCKET_CLASS = "IO::Socket::INET6"; # IPv4+IPv6
    } elsif (eval { require IO::Socket::INET }) {
       $SOCKET_CLASS = "IO::Socket::INET";  # IPv4 only
    } else {
       require IO::Socket;
       $SOCKET_CLASS = "IO::Socket::INET";
    }
}

つまり, Net::HTTPは,

  • IO::Socket::IPが利用可能なら, IO::Socket::IP
  • IO::Socket::INET6が利用可能なら, IO::Socekt::INET6
  • いずれも利用できないなら, IO::Socket::INET

...継承し, 利用するようになっています.

$ corelist IO::Socket::IP
Data for 2020-06-01
IO::Socket::IP was first released with perl v5.19.8

corelistコマンドによれば, IO::Socket::IPは, 5.19.8でコアモジュールとなっているので, 最近のPerlであればこれが使われる... と考えて良いでしょう. ということで, 次はIO::Socket::IPを調べていきます.

IO::Socket::IP

※この辺りから, だいぶ自信がなくなってきています. 間違ったことを書いていたら, 指摘頂けると幸いです...

IO::Socket::IPは, Family-neutral IP socket supporting both IPv4 and IPv6つまりIPv4とIPv6の両方に対応したソケット通信を提供するモジュールのようです. 今回は, 2020年12月25日現在の最新版である, 0.41を参照しました.

IO::Socket::IPのドキュメントを見ると, こちらにもまたタイムアウトのオプションがありそうです(Timeout). が...

If defined, gives a maximum time in seconds to block per connect() call when in blocking mode. If missing, no timeout is applied other than that provided by the underlying operating system. When in non-blocking mode this parameter is ignored.

  • connect()を呼び出すごとにブロックする最大時間をTimeoutで定義できる
  • 非ブロッキングモードの場合, このパラメータは無視される

This behviour is copied inspired by IO::Socket::INET; for more fine grained control over connection timeouts, consider performing a nonblocking connect directly.

  • もしタイムアウトを細かく制御したければ, 非ブロッキングモードで実行することを検討せよ

...といったことが書かれている, ように見えます. ちなみに"ブロッキングモード"は, Blockingというパラメータで制御できるらしく, 説明を読むと...

If defined but false, the socket will be set to non-blocking mode.

...つまり, デフォルトはブロッキングモードが有効で, 偽値を指定すると無効(= 非ブロッキングモード)になる... ようです.

ちょっと複雑なので説明を端折りますが, LWP::Protocol::http$self->socket_class->new()すると, 最終的にはIO::Socket::IPconnectメソッドが呼ばれます.

metacpan.org

ここで, Perlのconnect()関数が呼ばれているようなのですが, ここにTimeoutが渡っていない(渡せない)です.

というわけで, 万が一, 例えばリクエスト先のサーバが不調などの原因で, このconnect()関数の呼び出しに時間がかかってしまった場合, LWP::UserAgentに渡したtimeoutオプションを無視して待ち続ける... ということが起こりうるのでは...? という仮説を立てるところまで至りました.

ソケット周りの知識, 大学で学んだはずなのですが大昔すぎて忘れてしまったりということもあり, ちょっと中途半端ではありますが, 今回はこの辺りで一旦終了としようと思います... が, 流石に不完全燃焼なので, 本当にconnect()関数の呼び出しの部分に原因があるのか? であったり, IO::Socket::IPの非ブロッキングモードで回避する方法はあるか? といった所について, 改めて調べてみたいと思います.

...中途半端なオチですいません.


さて, このエントリで「Perl Advent Calendar 2020」も無事終了です!!! 最終日まで, だれ1人欠けることなく, 珠玉の25記事が集まりました.

qiita.com

2020年といえば, 開催を予定していたYAPC::Kyotoが新型コロナウイルスの影響で中止となってしまいました. しかしながら, 2021年に向けてオンライン形式でのイベント, 「Japan.pm」の計画が進みつつあります.

2021年が, Perl Mongerの皆様にとって実りの多い1年になることを祈りつつ, 「Perl Advent Calendar 2020」の25日目の記事を終えたいと思います.

他チームの振り返りを支援するという行い

このエントリは, 「はてなエンジニア Advent Calender 2020」の16日目のエントリです.

qiita.com

昨日の担当は, id:astj さんでした.

blog.astj.space

他チームの振り返りを支援するという行い

はてな社内には「すくすく開発会」という有志にのチームがあります. 「社内の炎上プロジェクトをゼロにする」を合言葉に, ソフトウェア開発におけるマネジメントを中心とした様々な領域について, 知見の共有をしています.

また, 参加メンバー同士で知見を共有するだけでなく, 実際に開発チームの手助けをするという活動も行っています. その中の1つが「振り返りのファシリテーションを手伝う」というもので, 自分も何度か他チームの振り返りを支援させてもらったことがあります.

「振り返り」は, これまでの出来事を振り返り, 次に活かすための大事なイベントです. そういうイベントのファシリテーションをするというのは, 大変に責任を感じますし, 正直難しいです.

そういうとき, 自分は「まあ, チーム外の自分がファシリテーションするだけで, だいぶメリットは提供できてるということで...」と, 半ば開き直るようにしています.

「振り返りの参加者」と「ファシリテーター」が同一人物であるときの弊害

前述のように, 「振り返り」という場でファシリテーションをするのは難しいです. そのため, 逆にチーム外からファシリテーターを呼ばないと, チーム内の誰かが「振り返りをしながら, ファシリテーションもする」という事態に陥ります. そうなると, 「振り返り」と「ファシリテーション」のどちらかが疎かになってしまう可能性があります.

チーム内で振り返りを実施するとき, チーム歴の長い人や, リーダー的な役割の人がファシリテーターを務める事が多いのではないでしょうか. もし, その人がファシリテーターとしての役割に意識を使いすぎて振り返りに集中できなくなると, 振り返りの中でその人の意見や経験が露見せず, 一切が失われてしまい, 次に繋げることが出来なくなってしまいます.

また一方で, 振り返りに意識を使いすぎると, 当然ファシリテーションが疎かになります. 自分自身うまく出来ているとは思っていないのですが, ファシリテーターは「振り返り」という場の中で, そのゴールを達成するために, 参加者の様子を観察したり, それを踏まえて式次第などを柔軟に組み立てる必要があります.

例えば, 最近読んだ「SCRUMMASTER THE BOOK」には, ファシリテーターの態度や振る舞いを次のように記しています:

聞き上手で、全員の声を聞き、ポジティブさを高め、柔軟性があり、直感を使いますが、1つのアイデアに執着しすぎません。ファシリテーターは場の熱量を把握し、それに応じてフォーマットを調整する必要があります。

...正直, 「振り返り」に参加しながらファシリテーションもする, というのは, よほど経験豊富でない限り難しいと思います.

さらに言えば, 「振り返りの参加者」と「ファシリテーター」という役割を切り替えるのが大変難しい, という話題もあります. その発言は「振り返りの参加者」としてのものなのか? 「ファシリテーター」としてのものなのか? ...これを常に明白にしないと, 2つの立場を利用して, 振り返り会の方向を捻じ曲げてしまう可能性もあるでしょう.

まとめ

...というわけで, 自分たちのチームが(特に大きな施策が完了した時の)振り返り会を実施する時は, なるべく他チームのメンバーにファシリテーションをお願いしています. また, 逆にファシリテーターをお願いした時に(当然, その人やその人が所属するチームの作業時間を奪う事になるので)「いいですよ!」と快く言ってもらえるように, 逆に他チームが振り返り会を実施する時には, 依頼があって余裕や元気があれば, ファシリテーターを引き受けるようにしています.

単純に, ファシリテーターとしての経験を積めるのもありがたいですし, 他チームの様子や工夫を振り返りを通じて知ることができる, というのもファシリテーターの役得なのかなと思います. 逆に, (振り返りの流れや方向性を誘導しない範囲で)自分が知っている知見を, 振り返り会の中で披露するという事もあります.

もし, 日々の定期的な振り返り会はともかくとして, 数ヶ月に渡る施策が終わった時の振り返り会も, ファシリテーター含めてチームで完結している... という状況にあるのなら, 一度他チームの人(その施策に関わりが薄かった人)に, ファシリテーターをお願いしてみてはいかがでしょう. きっと, より多くのものを次に活かすことが出来るようになると思います.


明日の担当は id:YaaMaa さんです. よろしくおねがいします.

PerlでスナップショットテストをするTest::Snapshotのご紹介

このエントリは, 「Perl Advent Calendar 2020」の9日目の記事です.

qiita.com

昨日のエントリは, id:xtetsuji さんの「xargs や find と合わせて使う・代わりに使う Perl」でした.

qiita.com


実は最近異動をしていた id:papix です. 異動後もPerlをモリモリ書いている日々ですが, 移動先のチームのプロダクトで同僚の id:mizdra が導入していた Test::Snapshot が便利だったので紹介します.

metacpan.org

Test::Snapshot

Test::Snapshotは, その名の通り「スナップショットテスト」を提供するモジュールです. スナップショットテストとは, 予め「スナップショット」と呼ばれる期待値を生成しておき, テストを実行する際には実行結果とスナップショットを比較してテストをする手法です.

使い方

まず初めに, Test::Snapshotを利用したテストの実例を見てみましょう:

use Test2::V0;
use Test::Snapshot;

sub method {
    return {
        a => 1,
        b => 2,
        c => [3, 4, 5],
    };
};

is_deeply_snapshot method(), 'hashref';

done_testing;

これは, method() の返り値をスナップショットテストで検証するコードになります. なおここでは, 単純にするためにテストスクリプトに直接テスト対象となる実装(method)を書いています.

このコードを, snapshot.t という名前で保存し, prove で実行してみます.

$ prove -v snapshot.t
snapshot.t ..
# Seeded srand with seed '20201209' from local date.
# No snapshot filename '/path/to/dir/snapshots/snapshot_t/hashref' found
not ok 1 - hashref

#   Failed test 'hashref'
#   at snapshot.t line 12.
# @@ -1 +1,9 @@
# -undef
# +{
# +  'a' => 1,
# +  'b' => 2,
# +  'c' => [
# +    3,
# +    4,
# +    5
# +  ]
# +}
1..1
# Looks like you failed 1 test of 1.
Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/1 subtests

Test Summary Report
-------------------
snapshot.t (Wstat: 256 Tests: 1 Failed: 1)
  Failed test:  1
  Non-zero exit status: 1
Files=1, Tests=1,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.07 cusr  0.02 csys =  0.11 CPU)
Result: FAIL

まだスナップショットを生成していないため, 当然ですがテストは失敗しますね.

# -undef
# +{
# +  'a' => 1,
# +  'b' => 2,
# +  'c' => [
# +    3,
# +    4,
# +    5
# +  ]
# +}

Test::Snapshotの便利なところとして, このようにスナップショットと実行結果の差をいい感じにダンプしてくれる機能があります. ここでは, スナップショットがまだ存在しないため undef となっていることがわかります.

スナップショットの生成

さて, 続いてスナップショットを生成してみましょう. スナップショットを生成するには, TEST_SNAPSHOT_UPDATE という環境変数に真値をセットした上でテストを実行すればよいです.

$ TEST_SNAPSHOT_UPDATE=1 prove -v snapshot.t
snapshot.t ..
# Seeded srand with seed '20201209' from local date.
# No snapshot filename '/path/to/dir/snapshots/snapshot_t/hashref' found
not ok 1 - hashref

#   Failed test 'hashref'
#   at snapshot.t line 12.
# @@ -1 +1,12 @@
# -undef
# +{
# +  'a' => 1,
# +  'b' => 2,
# +  'c' => [
# +    3,
# +    4,
# +    5,
# +    {
# +      'd' => 10
# +    }
# +  ]
# +}
1..1
# Looks like you failed 1 test of 1.
Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/1 subtests

Test Summary Report
-------------------
snapshot.t (Wstat: 256 Tests: 1 Failed: 1)
  Failed test:  1
  Non-zero exit status: 1
Files=1, Tests=1,  0 wallclock secs ( 0.02 usr  0.01 sys +  0.08 cusr  0.02 csys =  0.13 CPU)
Result: FAIL

実行すると, テストは失敗しますが, snapshot.t があるディレクトリに snapshots というディレクトリが設置されます.

$ tree
.
├── snapshot.t
└── snapshots
    └── snapshot_t
        └── hashref

2 directories, 2 files

tree するとこんな感じ. shapshots ディレクトリの中に, テストファイルに基づいたディレクトリ(テストがsnapshot.tなので, snapshot_t)が設置され, 更にその中にスナップショットの実態であるhashref というファイルが設置されます. ちなみにこのファイル名は, is_deeply_snapshot の第2引数(description)から生成されます.

注意点として, ファイル名はdescriptionに含まれる文字から, aからzまでのアルファベット(大文字小文字問わず)と0から9までの数字, そして-以外を全てまとめて_に置換したものになります.

そのため, 次のようなテストを書いてしまうと, method()other_method()のスナップショットがどちらも_というファイルに書き込まれてしまいます(!?).

is_deeply_snapshot method(), 'ひとつめのテストです';
is_deeply_snapshot other_method(), 'ふたつめのテストです';

スナップショット生成後のテスト

さて, スナップショットが生成できたので, 改めてテストを実行してみましょう.

$ prove -v snapshot.t
snapshot.t ..
# Seeded srand with seed '20201209' from local date.
ok 1 - hashref
1..1
ok
All tests successful.
Files=1, Tests=1,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.08 cusr  0.02 csys =  0.12 CPU)
Result: PASS

method()の実行結果とスナップショットは等しいので, 当然テストは成功します.

ここで改めて, methodの実装を変更してテストを実行してみましょう.

use Test2::V0;
use Test::Snapshot;

sub method {
    return {
        a => 1,
        b => 2,
        c => [3, 4, 5, { d => 10 }],
    };
};

is_deeply_snapshot method(), 'hashref';

done_testing;

意図したようにテストは失敗し, その差分を表示してくれます.

$ prove -v snapshot.t
snapshot.t ..
# Seeded srand with seed '20201209' from local date.
not ok 1 - hashref

#   Failed test 'hashref'
#   at snapshot.t line 12.
# @@ -4,6 +4,9 @@
#    'c' => [
#      3,
#      4,
# -    5
# +    5,
# +    {
# +      'd' => 10
# +    }
#    ]
#  }
1..1
# Looks like you failed 1 test of 1.
Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/1 subtests

Test Summary Report
-------------------
snapshot.t (Wstat: 256 Tests: 1 Failed: 1)
  Failed test:  1
  Non-zero exit status: 1
Files=1, Tests=1,  1 wallclock secs ( 0.02 usr  0.00 sys +  0.08 cusr  0.02 csys =  0.12 CPU)
Result: FAIL

まとめ

さくっとではありますが, Perlでスナップショットテストを実現する, Test::Snapshotを紹介しました. スナップショットを配置するディレクトリやファイル名に若干クセがある(変更はできなさそうでした)ものの, シンプルで便利なモジュールでした. 依存モジュールがかなり少なく, 基本的な(よく使う)モジュールが多いところも嬉しいです.

明日の担当は, @Taroupho さんです. よろしくおねがいします.