TokuLog 改メ tokuhirom’s blog: xslate.org はじめました

http://xslate.org/

ちゃんとしたプロダクトつくったら、サイトつくって紹介するべきだから、やりなはれ!と id:gfx にいったところ、「ドメインとサーバーがあればコンテンツはつくるよ」というので、ドメインとって github でグループつくって A record 設定して、ひながたを commit したら、Shibuya.pm の愉快な面々がもろもろととのえてくれたので、見栄えのいいサイトができました。

github pages は超管理が楽なのでまじおすすめ。マニュアルはここ → http://pages.github.com/

TokuLog 改メ tokuhirom’s blog: Rakudo* がでてたのでためしてみる

http://rakudo.org/announce/rakudo-star/2010.07

Rakudo* という Perl6 の Early Adapter むけの useful and usable distribution がでていたのでためしてみる(自分は二ヶ月に一回ぐらい rakudo をビルドしてるのでとくにめあたらしくもないですが)。

% wget http://github.com/downloads/rakudo/star/rakudo-star-2010.07.tar.gz
% tar xzvf rakudo-star-2010.07.ta
% make
% make install

r.gz

% cd rakudo-star-2010.07

% perl Configure.pl --gen-parrotみたいなかんじでセットアップ完了。楽ちんです。

実行速度は以下のようになっております。

./perl6 -e 'say "hello" for 1..100'  2.07s user 0.15s system 99% cpu 2.222 total

遅い原因としては以下の説があるようです。

  • memory allocation が富豪的すぎる
  • GCがよくない

TokuLog 改メ tokuhirom’s blog: cpan-outdated が local::lib に対応してた

cpan-outdated という、使っている CPAN module が古くないかどうかを確認できる便利なスクリプトがありますが、その cpan-outdated さんが local::lib に対応してました。

% cpan-outdated -L extlib | cpanm -L extlib

などとやると、extlib/ 以下のふるいモジュールを一気にアップグレードできます。

まあ気がむいたらやってみるといいんじゃないでしょうか。

Yet Another Hackadelic: [Event] Webテクノロジーセミナー in Hokkaido と Hokkaido.pm

先日、Yahoo!モバゲーのサンドボックス環境のリリースも何とかかんとかリリースいたしました。皆さん是非使って見て下さいね。*1

それはさておき8/6, 8/7は札幌で行われるイベントにお話しに行きます。

「モバゲータウン」のディー・エヌ・エー社、Japan Perl Association(JPA)から講師陣をお迎えして、北海道札幌で、 WebとPerlのテクノロジーについてのセミナーイベントを開催致します。 モバゲータウンやMovable Type で使用されているPerl言語についての講演や、オープンプラットフォームなどのWebテクノロジーについての講演を予定しております。 本セミナーは、WebやPerlといったテクノロジーを利用したビジネスやプロジェクト、コミュニティなどにご興味のあるエンジニアの方を対象としております。 Webテクノロジーセミナー in Hokkaido

Webテクノロジーセミナーではせっかくなので旬のネタであるヤバゲーの開発裏話とか OpenSocial の話、QUnit によるテストとかその辺りの事を話そうかなーとか思ってます。北海道在住の方や旅行でその時期にいらっしゃる方などふるってご参加頂けたらと思います。

次の日にあるHokkaido.pm #1 ではぐっと Perl よりの話をしようかなーと。多分テストの話になると思います。

話は変わりますが、fukuoka.pm の発起人である @sugmak さんこと杉山誠さんの訃報が突然伝えられ、ただただ驚いております。sugmak さんとは昨年の YAPC で初めてお会いしましたが、その際には YAPC のリアルタイムレポートを担当頂きました。きっとこのレポートを楽しみにされていた方も多かったのではないでしょうか。

sugmak さんのご冥福をお祈りすると同時にこの事で挫けず、fukuoka.pm がさらに盛り上がると良いなと思いました。

次の fukuoka.pm に予定が合えば参加しようかなとちょっと思ってます。

*1:とか言いつつ法人のみですが><

TokuLog 改メ tokuhirom’s blog: [perl]Text::Xslate で commify とかしたいときのやりかた

use 5.12.0;
use Text::Xslate;

my $xslate = Text::Xslate->new(
    module => ['Number::Format' => [':subs']],
    syntax => 'TTerse',
);
say $xslate->render_string('[% 100000000 | format_number %]');

こうかな。

あるいは

my $xslate = Text::Xslate->new(
    function => {
        commify => sub {
            local $_ = shift;
            1 while s/^([-+]?\d+)(\d\d\d)/$1,$2/;
            $_;
        },
    },
    syntax => 'TTerse',
);
say $xslate->render_string('[% 100000000 | commify %]');

かな。

TokuLog 改メ tokuhirom’s blog: Archive::Tar にディレクトリトラバーサル脆弱性

http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2007-4829

つかってる人はアップグレードしておいた方がいいですね。

Kawanet Tech Blog: 〔世界遺産〕イグアス国立公園(ブラジル・アルゼンチン)イグアスの滝

南米旅行の世界遺産シリーズ その4(最終回)。 2010年7月13日~15日まで、ブラジル南部のフォズ・ド・イグアスに滞在しました。イグアス国立公園にある、世界三大瀑布の1つ、イグアスの滝を行って来ました。虹の美しさ、滝の迫力と、自然。本当に素晴らしいところでした。日本から見るとほとんど地球の裏側で、かなり遠いですが、いやあ、行って良かった。


Kawanet Tech Blog: 〔世界遺産〕リマ歴史地区 Lima(ペルー)+Larco Marで太平洋を眺めて乾杯!

南米旅行の世界遺産シリーズ その3。 2010年6月30日~7月9日まで、10日間ほどペルー国内に滞在しました。国際線はリマ発着なので、クスコ・マチュピチュ に行く往路と復路で、2回、リマに滞在しました。(大きな荷物はリマの宿に預けて移動したので、クスコの石畳でもラクできて良かった)


TokuLog 改メ tokuhirom’s blog: [perl]Web Application Framework ではなく、コードジェネレータでいいのではないか

Web Application を構築するための部品が十分にそろいつつある今日この頃ですから、今となっては Web Application Framework をつかうのではなく、ライブラリの glue 部分を Code Generator で吐いてしまうのも選択肢にはいるのではないでしょうか。

というわけで、サラっとかいてみた。400行程度のジェネレータだけで、本質的なコードはない。実はこういうので十分なのではないだろうか。

(なんとなく Path::AttrRouter をつかって Catalyst 風にしてある)

一般的な Web Application の構成要素はすべてふくんでいるが、出力されるコードはおどろくほどみじかいし、実際これで十分だとおもう。

use strict;
use warnings;
use utf8;
use Getopt::Long;
use Pod::Usage;
use Data::Section::Simple qw/get_data_section/;
use Text::Xslate;
use File::Path qw/mkpath/;
use File::Basename;
use Text::Xslate::Syntax::TTerse;

GetOptions(
    'h|help' => \my $help,
);
pod2usage() if $help;
my $appname = shift @ARGV or pod2usage();

exit;

sub main {
    my $data = get_data_section();
    my $tx = Text::Xslate->new(
        path => [ $data ],
        syntax => 'TTerse',
        tag_start => '<%',
        tag_end   => '%>',
    );

    print "making $appname\n";
    (my $appdir = $appname) =~ s/::/-/g;
    mkdir($appdir);
    chdir($appdir);
    (my $path = $appname) =~ s!::!/!g;
    (my $package = $appname);

    my $opt = {
        name      => $appdir,     # My-App
        path      => $path,       # My/App
        'package' => $package,    # My::App
    };

    while (my ($fname, $val) = each %$data) {
        $fname =~ s!^\s+!!;
        $fname =~ s{\s+$}{};
        if ($val =~ /\S/) {
            print "  rendering $fname\n";
            my $dstpath = $tx->render_string($fname, $opt);
            mkpath(dirname($dstpath));

            open my $fh, '>:utf8', $dstpath or die "cannot open file: $dstpath: $!";
            print {$fh} $tx->render($fname, $opt);
            close $fh;
        } else {
            print "  mkdir $fname\n";
            mkpath($fname);
        }
    }

    system $^X, 'Makefile.PL';
    system 'make';
    system 'make', 'test';
}

=pod

=head1 SYNOPSIS

    % webapp-starter.pl MyApp

=cut

__DATA__

@@ Makefile.PL
use inc::Module::Install;
name '<% name %>';
all_from 'lib/<% path %>.pm';

requires 'Text::Xslate' => 0.1047;
requires 'Mouse';
requires 'Log::Dispatch';
requires 'parent';
requires 'DBIx::Skinny';

tests 't/*.t t/*/*.t t/*/*/*.t t/*/*/*/*.t';
test_requires 'Test::More';
test_requires 'YAML';
test_requires 'Test::WWW::Mechanize::PSGI';
test_requires 'Test::Requires';
author_tests('xt');
auto_include;
WriteAll;

@@ <% name %>.psgi
use strict;
use warnings;
use <% package %>::Web;
use Plack::Builder;
use Plack::MIME;

delete $Plack::MIME::MIME_TYPES->{$_} for qw/.pl .pm .yml .json/;

builder {
    enable 'Plack::Middleware::Static',
        path => qr{^/static/},
        root => './htdocs/';

    <% package %>::Web->to_app();
};

@@ lib/<% path %>.pm
package <% package %>;
use Mouse;
use <% path %>::ConfigLoader;
use Cwd ();
use <% package %>::DB;

has config => (
    is       => 'ro',
    isa      => 'HashRef',
    required => 1,
);

my $root = do {
    my $p = __FILE__;
    $p = Cwd::abs_path($p) || $p;
    (my $q = __PACKAGE__) =~ s{::}{/}g;
    $p =~ s{$q\.pm$}{};
    $p =~ s{/lib/?$}{}g;
    $p =~ s{/blib/?$}{}g;
    $p;
};
sub root { $root }

sub context { die "cannot find context" }

sub bootstrap {
    my ($class) = @_;
    my $c = $class->new(config => <% package %>::ConfigLoader->load);
    no warnings 'redefine';
    *<% package %>::context = sub { $c };
    return $c;
}

sub db {
    my ($self) = @_;
    $self->{db} ||= do {
        <% package %>::DB->new($self->config->{'DB'});
    };
}

no Mouse; __PACKAGE__->meta->make_immutable;

@@ lib/<% path %>/DB.pm
package <% package %>::DB;
use DBIx::Skinny;
1;

@@ lib/<% path %>/DB/Schema.pm
package <% package %>::DB::Schema;
use DBIx::Skinny::Schema;

# install_table user => schema {
#     pk 'id';
#     columns qw/
#     id
#     name
#     /;
# };

1;

@@ lib/<% path %>/Web.pm
package <% package %>::Web;
use Mouse;
use <% path %>;
use <% path %>::ConfigLoader;
use Text::Xslate 0.1047;
use Plack::Request;
use Plack::Response;
use Path::AttrRouter;
use Module::Find qw/useall/;
use Encode;
use Log::Dispatch;

extends '<% package %>';

useall '<% path %>::Web';

our $VERSION = '0.01';

has 'log' => (
    is => 'ro',
    isa => 'Log::Dispatch',
    lazy => 1,
    default => sub {
        my $self = shift;
        Log::Dispatch->new(%{$self->config->{'Log::Dispatch'} || {}});
    },
);

has config => (
    is       => 'ro',
    isa      => 'HashRef',
    required => 1,
);

has env => (
    is => 'ro',
    isa => 'HashRef',
    required => 1,
);

has req => (
    is      => 'ro',
    isa     => 'Plack::Request',
    lazy    => 1,
    default => sub {
        my $self = shift;
        Plack::Request->new( $self->env );
    }
);

has args => (
    is       => 'rw',
    isa      => 'ArrayRef',
);

has res => (
    is      => 'ro',
    isa     => 'Plack::Response',
    default => sub {
        Plack::Response->new;
    },
);

sub request  { shift->req(@_) }
sub response { shift->res(@_) }

sub to_app {
    my ($class) = @_;

    my $router = Path::AttrRouter->new( search_path => '<% package %>::Web::C' );
    my $config = <% package %>::ConfigLoader->load;
    sub {
        my $env = shift;
        my $c = $class->new(env => $env, config => $config);
        no warnings 'redefine';
        local *<% name %>::context = sub { $c };
        if (my $m = $router->match($env->{PATH_INFO})) {
            $c->args($m->args);
            my $meth = $m->action->name;
            $m->action->controller->$meth($c);
            return $c->res->finalize;
        } else {
            my $content = 'not found';
            return [404, ['Content-Length' => length($content)], [$content]];
        }
    };
}

my $tx = Text::Xslate->new(
    syntax => 'TTerse',
    module => ['Text::Xslate::Bridge::TT2Like'],
    path   => [__PACKAGE__->root . "/tmpl"],
);
sub default_content_type { 'text/html; charset=utf-8' }
sub default_encoding     { 'utf-8' }
sub render {
    my ($self, @args) = @_;
    my $body = $tx->render(@args);
    $self->res->status(200);
    $self->res->content_type($self->default_content_type);
    $self->res->body(encode($self->default_encoding, $body));
}

no Mouse;__PACKAGE__->meta->make_immutable;

@@ lib/<% path %>/Web/C.pm
package <% package %>::Web::C;
use strict;
use base 'Path::AttrRouter::Controller';

sub index :Path {
    my ($class, $c) = @_;
    $c->render('index.tx');
}

1;

@@ lib/<% path %>/ConfigLoader.pm
package <% path %>::ConfigLoader;
use strict;
use warnings;
use File::Spec;
use Cwd ();
use <% package %>;

sub load {
    my $class = shift;
    my $env = $ENV{PLACK_ENV} || 'development';
    my $fname = File::Spec->catfile(<% package %>->root(), 'config', "${env}.pl");
    my $conf = do $fname or die "Cannot load configuration file: $fname";
    return $conf;
}

1;

@@ tmpl/
@@ tmpl/index.tx
[% INCLUDE 'include/header.tt' %]

[% INCLUDE 'include/footer.tt' %]
@@ tmpl/include/header.tt
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title><% name %></title>
    <meta http-equiv="Content-Style-Type" content="text/css" />  
    <meta http-equiv="Content-Script-Type" content="text/javascript" />  
    <link href="/static/css/app.css" rel="stylesheet" type="text/css" media="screen" />
</head>
<body id="[% IF bodyID %][% bodyID %][% ELSE %]Default[% END %]">
    <div id="Container">
        <div id="Header">
            <a href="/"><% name %></a>
        </div>
        <div id="Content">
@@ tmpl/include/footer.tt
        </div>
        <div class="clear-both"></div>
    </div>
</body>
</html>
@@ sql/
@@ t/

@@ t/Util.pm
package t::Util;
use strict;
use warnings;
use parent qw/Exporter/;
1;

@@ t/01_router.t
use strict;
use warnings;
use Path::AttrRouter;
use Test::More;
use <% package %>::Web;

my $router = Path::AttrRouter->new( search_path => '<% package %>::Web::C' );
ok $router->match('/');

done_testing;

@@ t/02_mech.t
use strict;
use warnings;
use Plack::Test;
use Plack::Util;
use Test::More;
use Test::Requires 'Test::WWW::Mechanize::PSGI';
use t::Util;

my $app = Plack::Util::load_psgi '<% name %>.psgi';

my $mech = Test::WWW::Mechanize::PSGI->new(app => $app);
$mech->get_ok('/');

done_testing;

@@ xt/
@@ htdocs/
@@ htdocs/static/img/
@@ htdocs/static/js/
@@ htdocs/static/css/app.css
/* place holder */

@@ script/dummy.pl
use strict;
use warnings;
use <% package %>;

my $c = <% package %>->bootstrap;

...

@@ config/development.pl
+{
    'Log::Dispatch' => {
        outputs => [
            [ 'Screen', min_level => 'warning' ],
        ]
    }
}
@@ config/production.pl
+{
}

Kawanet Tech Blog: 〔世界遺産〕クスコ市街 City of Cuzco(ペルー)現地発着・聖なる谷ツアー

南米旅行の世界遺産シリーズその2。「クスコ市街」編です。マチュピチュについては 前のポスト に書きました。世界遺産のクスコ市街だけでなくて、近郊ツアーも含めて紹介します。


Kawanet Tech Blog: 〔世界遺産〕マチュピチュ Machu Picchu(ペルー) ワイナピチュからの絶景

南米旅行の世界遺産シリーズ その1。 2010年7月3日、南米・ペルーの世界遺産、マチュピチュ Machu Picchu に行ってきました。今年1月の大雨で一時閉鎖されていましたが、現在は、完全復旧して入場可能になっています。素晴らしかったです。遠いので、気軽に行ける場所ではありませんが、オススメです!


Kawanet Tech Blog: メディアテクノロジーラボ人材募集中(中途採用)サバティカル休暇あり!

リクルート メディアテクノロジーラボでは、正社員(中途キャリア採用)を募集中 です。キャリア採用サイトの表示が少し分かりにくいのですが、現在、募集しているのは、


Kawanet Tech Blog: 3週間の南米旅行(ペルー・ブラジル)まとめ 格安航空券・ホステルTIPS

6月28日~7月20日まで、南米旅行に行ってきました。南米は遠いので、そうそう気軽には行ける地域ではありません。今回は、4週間の休暇を頂いて、そのうち3週間強の日程で行って来ました。ペルーとブラジルの2カ国を中心に、計9都市を回って、11フライトとなる長旅でした。この数年、海外旅行が趣味となって来ているので、いくつか TIPS も交えて紹介します。


Kawanet Tech Blog: リオデジャネイロ Rio.pm とサンパウロ Sao-Paulo.pm の人に会ってきた。#Perl

6月28日~7月20日まで、地球の裏側、南米まで旅行に行ってきました。折角の機会なので、ブラジルでは、南米大陸で最大級の Perl 開発者コミュニティであるリオ・デ・ジャネイロの Rio.pm と、サン・パウロの Sao-Paulo.pm の皆さんにお会いしてきました。


TokuLog 改メ tokuhirom’s blog: given-when における method call について

use strict;
use 5.10.0;

{
    package xai;
    use constant {cron => 1 };
}

given (2) {
    when (xai->cron) {
        print "FAIL\n";
    }
}

これは FAIL と出力する。when の中におけるメソッドコールは、その返り値の boolean 値そのものが評価値として利用され、

when ($_ ~~ xai->cron) { }

相当にはならないのだ。

ちょっとはまるかも。

TokuLog 改メ tokuhirom’s blog: 超適当な http client

<div class="section"> <pre class="syntax-highlight"> <span class="synStatement">use strict</span>; <span class="synStatement">use warnings</span>; <span class="synStatement">use </span><span class="synConstant">5.10</span>.<span class="synConstant">0</span>; <span class="synStatement">use </span>IO::Socket::INET; <span class="synStatement">use </span>HTTP::Response::Parser <span class="synConstant">qw/parse_http_response/</span>; <span class="synStatement">use </span>URI; <span class="synStatement">use </span>YAML; <span class="synStatement">my</span> <span class="synIdentifier">$CRLF</span> = <span class="synConstant">&#34;</span><span class="synSpecial">\015\012</span><span class="synConstant">&#34;</span>; <span class="synStatement">warn</span> Dump(get(<span class="synConstant">'http://mixi.jp/'</span>)); <span class="synStatement">sub</span><span class="synIdentifier"> get </span>{ <span class="synStatement">my</span> (<span class="synIdentifier">$url</span>) = <span class="synIdentifier">@_</span>; <span class="synStatement">my</span> <span class="synIdentifier">$uri</span> = URI-&#62;<span class="synStatement">new</span>(<span class="synIdentifier">$url</span>); <span class="synStatement">my</span> <span class="synIdentifier">$sock</span> = IO::Socket::INET-&#62;<span class="synStatement">new</span>( <span class="synConstant">PeerHost </span>=&#62; <span class="synIdentifier">$uri</span>-&#62;host, <span class="synConstant">PeerPort </span>=&#62; <span class="synIdentifier">$uri</span>-&#62;port || <span class="synConstant">80</span>, ) <span class="synStatement">or</span> <span class="synStatement">die</span> <span class="synConstant">&#34;cannot open socket</span><span class="synIdentifier">$!</span><span class="synConstant">&#34;</span>; <span class="synStatement">print</span> {<span class="synIdentifier">$sock</span>} <span class="synStatement">sprintf</span>(<span class="synConstant">&#34;GET </span><span class="synIdentifier">%s</span><span class="synConstant"> HTTP/1.0${CRLF}Host: </span><span class="synIdentifier">%s</span><span class="synConstant">${CRLF}${CRLF}&#34;</span>, <span class="synIdentifier">$uri</span>-&#62;path || <span class="synConstant">'/'</span>, <span class="synIdentifier">$uri</span>-&#62;host); <span class="synStatement">my</span> <span class="synIdentifier">$ret</span>; <span class="synStatement">my</span> <span class="synIdentifier">$buf</span> = <span class="synStatement">do</span> { <span class="synStatement">local</span> <span class="synIdentifier">$/</span>; &#60;<span class="synIdentifier">$sock</span>&#62; }; <span class="synStatement">my</span> <span class="synIdentifier">$res</span> = HTTP::Response::Parser::parse(<span class="synIdentifier">$buf</span>); <span class="synStatement">if</span> (<span class="synIdentifier">$res</span>) { <span class="synStatement">return</span> <span class="synIdentifier">$res</span>; } <span class="synStatement">else</span> { <span class="synStatement">die</span> <span class="synConstant">&#34;something wrong&#34;</span> } } </pre> </div>

mixi Engineers' Blog: Buildbot で継続的インテグレーション

こんにちは。パートナーサービス部の加藤和良です。

前回、mixi における開発者テスト について説明しました。だいぶ間があいてしまいましたが、今回は、そのテストを定期的に実行する 継続的インテグレーション の仕組みを紹介したいと思います。

テストが遅い

実は、mixi のテストは「遅い」という大きな問題を抱えています。

Micheal Feathers は『レガシーコード改善ガイド』のなかで、単体テストが高速に実行できることの重要性を解き「単体テスト」を厳しく定義します。

次に当てはまるものは単体テストではない。

  1. データベースとやり取りする
  2. ネットワークを介した通信をする
  3. ファイルシステムにアクセスする
  4. 実行するために特別な環境設定を必要とする (環境設定ファイルの編集など)

上記に該当するテストが悪いというわけではない。多くの場合において、そのようなテストを書く価値はあり、しばしばテストハーネス内に記述される。しかし単体テストは、そのようなテストと切り分けて、変更を行うたびに高速で実行できるように保ち続けることが重要である。

この定義にしたがうと、mixi のテストで「単体テスト」と呼べるものはごくわずかで、それらは実際「変更を行うたびに高速で実行できる」速度に達していません。

そのためか mixi のテストは実行をさぼられがちで

  • 安定版からのブランチを作成して、開発を開始
  • 開発が一段落したので、そのブランチで (or マージしてから) テストを走らせる
  • テストが失敗するので調べてみたら、そもそも安定版のほうでもテストが失敗していた

といったことが時折ありました。

Buildbot

継続的インテグレーション導入の目的は、こういった状況を改善することでした。現在は、Subversion レポジトリ上の安定版のツリーにコミットが行われると、自動でテストが実行され、結果が IRC に通知されます。テストの結果は Web からも確認できます。

テストが失敗したときの IRC

これら一連の動作は Buildbot をつかって実現しています。Buildbot は Python で書かれた継続的インテグレーションのためのソフトウェアで、たとえば WebKit でも使われています。

継続的インテグレーションはテストを高速に実行するためのものではありません。ただ、テストが遅い状況でも、遅さに起因する問題をやや軽減させる効果はあるんじゃないかとは思います。

テストのアルファベット分割

mixi では継続的インテグレーションからのフィードバックをはやく得るために、2つの工夫をおこなっています。ひとつがテストのアルファベット順の分割です。

Buildbot では全行程がおわるまでの個々の処理を BuildStep と呼ばれる単位に分割します。BuildStep が成功すれば Web インターフェースで緑色に、失敗すれば赤色に表示されます。

mixi の場合、当初は

  • svn checkout
  • make test

と2つの BuildStep だけがあったのですが、この設定では make test の最中の進行状況がわかりにくいという問題がありました。前述のとおり mixi のテストは遅いので、わかりにくい時間もやや長めです。

そこでテストを何段階かに分割することを考えました。一番低レイヤーのライブラリから一番上のアプリケーションまで順に BuildStep に切る、みたいなのが真っ当なやりかただとは思うのですが、mixi のコードはお互いの依存関係がちょっと混迷を極めていて、あんまり良い分割単位が見出せません。

そこでいまは、単純かつ機械的に

  • ^t/lib/Mixi/A
  • ^t/lib/Mixi/B
  • それ以外

とファイル名をベースに分割するようにしています。

アルファベット順

アルファベットごとのテストの量には大分ばらつきがあり ^t/lib/Mixi/W みたいにマッチしないこともあります。ただ、テストの進行中および失敗した際のわかりやすさは大分改善されました。

最近変更した部分に対するテスト

もうひとつの工夫が「最近変更した部分に対するテスト」の別実行です。繰り返しになりますが mixi のテストは遅いため

  • Buildbot が全てのテストを実行し、失敗を報告
  • 失敗に対応する修正をコミット
  • Buildbot が再度全てのテストを実行し、成功を報告

というサイクルがあまり早く回せません。アルファベット分割も、例えていうなら「遅い処理の間はプログレスバーを出しましょう」という話でしかありません。

そこで mixi では、全テストの実行とは別に、最近修正されたソースコードに対するテストだけの実行も行っています。

最近変更した部分に対するテスト

現在は、1分ごとにレポジトリをポーリングして

full
コミットが10分間ないとすべてのテストを実行しはじめる
recent
コミットがあるごとに最近変更されたコードに対応するテストを実行する

という二系統のテストを (Buildbot 用語でいう別の Builder で) 行っています。

mixi には lib/Mixi/Member.pm へのテストは

  • t/lib/Mixi/Member.t
  • t/lib/Mixi/Member/ 以下

に置くというゆるやかな慣例があるため、変更されたファイルから実行すべきテストを探すのは比較的簡単です。もちろん、この方法では「ある変更が依存関係の彼方にあるコードを壊す」といった状況は検出できませんが、そこは full に任せています。

まとめ

というわけで、mixi における継続的インテグレーションの仕組みと、こまごました工夫について紹介しました。

説明の順番が、まず開発者テスト、次に継続的インテグレーション、というふうになってしまいましたが、実際には、このふたつは並行して進めていました。

継続的インテグレーションの存在は「テストを書いたけど実行されない」という問題をなくし、すでにテストを書いている人のリターンを増やします。また、Buildbot が IRC にテストの失敗を通知し、実際に本番でもバグがみつかる、という流れには、いままで「テストってなんだろう?」と思ってたひとが「書くと良さそうだ」となる効果があったんじゃないかと思います。

開発者テストが書きにくかったり、遅かったり、いまひとつ開発者のなかに浸透していなかったりする環境でも、とりあえず継続的インテグレーションからはじめてみるのはおすすめですよ、というのが伝われば幸いです。それではまた。

id:antipop: [日記]岡北のうどん、阿以波のうちわ、鶏の胸肉

午後、Rさんが府立図書館へ本を返す必要があるというので、ついでに岡崎探訪。用事を済ませて、二条から岡崎通を上がった蕎麦とうどんのお店「岡北」へ。冷やし山かけうどん、親子丼のミニ丼、冷やし中華をいただく。うどんは、麺に腰がありつつもとてもつるつるしていて、食べていて気持ちがいい。お出汁もさっぱり、かつ、しっかり風味が出ていていい感じ。親子丼は、卵がやたらぷりぷりしていて、どうやって作ったのかさっぱり謎な感じなのだけど、そんなことはどうでもよくて、おいしい。冷やし中華は、前日にいただいた冷やし担々麺が大しておいしくなかったこともあって、なおさらおいしく感じられた。ともあれ、いただいたものが全部おいしくて、感激。

岡北

食べログ岡北

先日訪ねた時は、普通にお茶をいただきにいったものの、うつわの展示即売会が行われていて、うつわが好きなのでそれはそれでいいのだけど、お茶をいただけなくてちょっと残念だった好日居に、せっかく岡崎にきたのだからってんで、再度よってみる。結果、またしても蚤の市とのことで、ゆっくりお茶をいただける感じではなかった……。

好日居

食べログ好日居

さて、どこ行こうかってんで、とりあえず神宮道へ出ると、きねや岡崎店にて「京うつわ 阿以波展」などという催しをやっているのを見つけた。Rさんは、ずっとここのうちわが欲しかったのだといって、大興奮。確かに、なかなか素敵なものが多くて、うちわを2枚、うちわ立てを購入。思わぬ出費だったけど、よい買い物。

それにしても暑い。歩いているだけで暑さに体力を奪われていくので涼を取ろうと、四条河原町、かき氷をいただこうというわけで「弥次喜多」へ。2階へ通されたのだが、クーラーが入っておらず、わりと往生する。が、僕らにはいましがた購入したばかりのうちわがある!!1ってんで、風情のある日本建築で、うちわを扇いで風を受けつつかき氷をいただくのは、それはそれで趣のある感じであった。

弥次喜多

食べログ弥次喜多

f:id:antipop:20100719161324j:image:w400

帰宅して、買ってきたうちわをしつらえてみると、なかなかかわいらしくていい感じ。

f:id:antipop:20100719180636j:image:w400

夕食。僕はとうもろこしの天麩羅を作ったのだけど、なかなかうまく行かず難儀をした。難しい。しかしまあ、とうもろこし自体がおいしいので、味はまあまあだった。次はもっと上手に作りたいところ。ところで、Rさんが作ったゆで鶏がとてもおいしくて、胸肉ってあんまり好きではないのだけど、これはとてもやわらかくぷりぷりしていて、とてもよかった。

f:id:antipop:20100719211144j:image:w400

id:antipop: [日記]「借りぐらしのアリエッティ」を観たり

昨日の山鉾巡行と同時に梅雨が明け、異常に暑い。昼食はさっぱりと。盛り付けが微妙な感じだ……。

  • 鶏ささみのレモン焼き
  • きゅうり、トマト、鶏ささみのサラダ
  • たまねぎとニンジンのスープ

f:id:antipop:20100718145444j:image:w400

たまった洗濯などを片付ける。日射しが強い分、洗濯ものがすぐに乾くのはいいですね。

夜、二条に「借りぐらしのアリエッティ」を観にでかける。その前に「大鵬」で食事を、と思ったけど満席でひとが溢れていたので諦めて、ViViの中のなんとかいう中華料理屋さん。

「アリエッティ」は、家政婦のオバサンがやたら腹立たしく描かれていて鬱陶しい気分にさせられる以外は、あっさりとした映画だった。

ところで、最近ベランダ菜園を始めて、バジルとパクチーを育て始めました。植物を育てること自体に興味はあるのだけど、いかんせん根気が続かないので、食べられるものを。バジルはすぐに芽が出たのだけどパクチーは全然出なくてやきもきしていたのが、いったん出始めるとどんどん伸びるので楽しい。早く食べられるぐらい大きくなってほしい。楽しみ。写真の手前がバジル、奥がパクチー。

f:id:antipop:20100718161158j:image:w400

夜は吉井勇の『東京・京都・大阪』を読んだり。面白エピソードがいっぱい。

東京・京都・大阪 (平凡社ライブラリー)

東京・京都・大阪 (平凡社ライブラリー)

id:antipop: うつわめぐりの日曜日

恵文社一乗寺店で行われている「うつわハートフル展  器はもっと、愛おしい。 - 恵文社一乗寺店|スタッフ日記」へ。「ハートフル」ってどうなの。。。とか思ってたのだけど、好きな作家さんのものが多く出ていたので、けっこう買ってしまった。村田森さん、尾形アツシさんなど。

f:id:antipop:20100711153700j:image:w400

おなかがすいたので、恵文社にほど近い「つばめ」でごはん。

つばめ

食べログつばめ

先日行った時にはお休みだったので、今度こそってんで、壬生の骨董屋さん「幾一里」へ。今度は開いていて、よかった。おじいさんの代から住んでいらっしゃる町屋を改装したという店内は、うつわはもちろん、古布や板切れなどいろいろとあるものが、どれも素敵な感じで、とてもよかった。古伊万里の真っ白なそば猪口を購入。またちょくちょく寄らせてもらいたいと思う。

夜は、さっそく購入した猪口で晩酌。竹鶴、しばらくおいておいたら味がまろやかになってきて、よくなった。おいしい。

f:id:antipop:20100711233322j:image:w400