株式会社クリアコード > ククログ

ククログ


手動でインベントリーファイルを作らずにVagrantとAnsibleを一緒に使う方法

最近、Hatoholというオープンソースの運用管理ツールの開発にも参加しています。

開発環境はDebian GNU/Linux sidなのですが、Hatoholのサポート環境にはCentOS 6も含まれているため、ドキュメントを書くときなどはCentOS 6で動作を確認する必要があります。ただ、「Hatohol用のCentOS 6環境」を常時用意しておくのは管理が面倒*1です。そのため、仮想マシン上に何度でも簡単にセットアップできるようにしました。

「何度でも簡単にセットアップできる」の部分にはAnsibleを使いました。これは、HatoholのリポジトリーにすでにAnsibleのplaybookファイルが存在したためです。playbookファイルはあるので、Vagrantと組み合わせて仮想マシン上にセットアップできるようにしました。

VagrantとAnsibleの連携方法を調べると、日本語の情報のものはだいたい手動でインベントリーファイルを作っています。Vagrantのサイトにあるドキュメント(英語)ではインベントリーファイルはVagrantが自動で作ってやるよと書いているのに、です。

インベントリーファイルを自動で作ってもらう方が手間が少なく、IPアドレスやホスト名などを明示しなくてもよいので管理が簡単になります。そのため、Vagrantに自動で作ってもらうときの使い方を日本語で紹介します。

インベントリーファイルを自動で作る方法

インベントリーファイルを自動で作るには、特別なことを何もしなければよいです。つまり、Vagrantはデフォルトでインベントリーファイルを自動で作るようになっています。

特別なことというのは、例えば、ansible.cfgを作ったり、ansible.inventory_pathを指定したり、といったことです。

インベントリーファイルを自動で作ると困ること

インベントリーファイルを自動で作ることは何もしなければよいだけなので簡単なのですが、playbookの書き方によってはうまく動きません。具体的には次のケースはひと手間かけないとうまく動きません。

  • hostsall以外を指定している
  • userを指定していて、さらに指定した値がvagrantではない(user自体を設定していない場合はひと手間は必要ない)
  • root権限で実行することを想定している(sudoを指定していない)

それぞれどのようにひと手間かければよいか説明します。なお、どれもHatoholの既存のplaybookファイルが該当しているケースです。

hostsall以外を指定している

hostsall以外を指定していると、Vagrantで作った仮想マシンは指定したグループに所属していなければいけません。ホストがどのグループに所属するかはインベントリーファイルで指定します。今はインベントリーファイルはVagrantが自動で作る前提なので、Vagrantにグループとホストの対応を伝えてうまく作ってもらう必要があります。

具体的には次のようにansible.groupsを使います。この例はplaybookファイルでhosts: targetsと指定されていたケース用です。

1
2
3
4
5
6
7
8
9
10
11
12
id = "centos-6-x86_64"
box_url = "http://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_centos-6.5_chef-provisionerless.box"
config.vm.define(id) do |node|
  node.vm.box = id
  node.vm.box_url = box_url
  node.vm.provision("ansible") do |ansible|
    ansible.playbook = "setup-hatohol-dev.yaml"
    ansible.groups = {
      "targets" => [id],
    }
  end
end

これでインベントリーファイルに次のような内容が入ります。

[targets]
centos-6-x86_64

playbookファイルが対象としているグループにVagrantで作ったホストが入っているので、playbookファイルで指定したタスクが実行されます。

userを指定していて、さらに指定した値がvagrantではない

Vagrantが作った仮想マシンにはvagrantユーザーでログインします。playbookファイルでuserを指定しているとその設定が優先され、仮想マシンにログインできません。この場合はansible.extra_vars:ansible_ssh_userを指定することでSSHするユーザーを変更できます。

具体的には次のようにansible.extra_varsを使います。

1
2
3
4
5
6
7
8
9
10
11
12
id = "centos-6-x86_64"
box_url = "http://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_centos-6.5_chef-provisionerless.box"
config.vm.define(id) do |node|
  node.vm.box = id
  node.vm.box_url = box_url
  node.vm.provision("ansible") do |ansible|
    ansible.playbook = "setup-hatohol-dev.yaml"
    ansible.extra_vars = {
      :ansible_ssh_user => "vagrant",
    }
  end
end

これでplaybookファイルで指定されているユーザーではなく、vagrantユーザーでSSHしてくれるようになります。

root権限で実行することを想定している

vagrantユーザーは一般ユーザーです。root権限が必要な場合はsudoを使わなければいけません。playbookファイルにsudo: trueという設定がない場合はsudoを使ってくれないのでVagrant側からsudoを使ってほしいということを伝える必要があります。

具体的には次のようにansible.sudo=を使います。

1
2
3
4
5
6
7
8
9
10
id = "centos-6-x86_64"
box_url = "http://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_centos-6.5_chef-provisionerless.box"
config.vm.define(id) do |node|
  node.vm.box = id
  node.vm.box_url = box_url
  node.vm.provision("ansible") do |ansible|
    ansible.playbook = "setup-hatohol-dev.yaml"
    ansible.sudo = true
  end
end

これで、たとえplaybookファイル中でsudo: trueを指定していなくてもsudoを使って実行してくれるようになります。

まとめ

VagrantとAnsibleを一緒に使う方法を検索すると、日本語の情報ではインベントリーファイルを手動で作成する情報が多かったので、より手間の少ないVagrantに自動で作成してもらう方法を日本語で紹介しました。自動で作成する方法はVagrantのサイトにあるドキュメント(英語)には書いているので、本家の情報も参考にしてください。よく知らないツールの使い方を調べるときは、念のため、本家の情報も参考にした方がよいでしょう。

VagrantとAnsibleの連携方法は具体的なVagrantファイルの書き方を示しました。実際に使っているVagrantファイルも参考にしてください。

また、Hatoholというオープンソースの運用管理ツールの開発にも参加していることを匂わしました。

*1 別のマシンで開発しようとした時、「Hatohol用のCentOS 6環境」にアクセスできないかもしれない。

2014-09-10

SSHポートフォワード(トンネリング)を使って、遠隔地からLAN内のコンピュータにログインする

こんにちは。クリアコードの結城です。

SSHを使うと、手元のコンピュータから別のコンピュータへネットワーク越しにログインして、bashやzshなどのコマンドラインシェルを使ってそのコンピュータをリモート操作できます。scpを使えば、ネットワーク越しにファイルをコピーすることもできます。

しかし、以下のコマンド列を見ると分かる通り、SSH経由で接続できるコンピュータは基本的には、手元で操作しているコンピュータから直接ホスト名またはIPアドレスで参照できるコンピュータに限られます。

% ssh www.example.com # 接続先をホスト名で指定
% scp 192.168.1.10:/var/log/apache2/access.log /tmp/ # 接続先をIPアドレスで指定

さて、以下のような事をしたくなった・する必要に迫られたとしましょう。

  • ホテル、喫茶店、ネカフェ、街角のWi-Fiアクセスポイント、スマホのテザリング、などなど、外出先のネットワークから、社内のPCにログインしたい。しかしVPN等は特に用意していない。
  • 社内のファイル共有サーバにしか置いていないファイルを、外出先のネットワークに接続したPCにコピー(ダウンロード)したい。VPNは用意していない。
  • 社内のネットワークから、データセンターに設置されているデータベースサーバにログインしたい。データベースサーバはファイアウォールの向う側にあり、データセンターの外からはログインできず、同じデータセンター内に設置されている別のサーバからしかログインできない。

これらの場合に出てくるネットワークとコンピュータの位置関係を大まかに図で示すと、こんな感じです:

図:2つのLANがある様子

図の上の方にあるのが「ログインしたいコンピュータが所属しているLAN(接続先LAN)」、下の方にあるのが「自分が今ログインしているコンピュータと、その所属ネットワーク(接続元LAN)」と思って下さい。2つのネットワークを見分けやすいように、接続先LANは192.168.1.0/24のネットワーク、接続元LANは192.168.10.0/24だとします。(※この表記の仕方の意味については、過去の記事も併せて参照して下さい。) 192.168.1.0/24には、192.168.1.10(work1)と192.168.1.11(work2)の2台が存在しています。また、192.168.10.0/24には192.168.10.10(mobile1)が存在しています。

図中で説明している通り、2つのLANはどちらもルーターを経由してインターネットに接続されていますが、LAN同士の間には直接の繋がりはなく、互いにパケットが転送されるということもありません。仮にmobile1からwork1に向けてping 192.168.1.10とPINGを送っても、到達不能でタイムアウトしてしまいます。

このような場合にも、SSHのポートフォワード機能を使えば、やりたいことを実現できます。

ポートフォワードとは、あるコンピュータの特定のポート番号に対して送られる通信内容を、別のコンピュータの特定のポート番号への接続として転送する事を言います。 インターネットとLANの間でルーターは日常的にこれをこなしていますが、SSHのポートフォワード機能を使うと、様々な設定でポートフォワードすることができます。あるLANの中にあるPCから別のLANの中にあるPCにSSHで接続するということも、この技術を使って実現できます。

ここでは、例として「インターネット上の中継サーバを使う方法」と「LAN内の中継サーバを使う方法」の2通りのやり方を紹介します。

インターネット上の中継サーバを使う方法

先の構成に、インターネット上に存在するサーバを1台加えてみました:

画像:インターネット上のwww.example.comを挟んで2つのLANが向かい合っている様子

このサーバには固定のIPアドレスと、www.example.comというホスト名が割り当てられているとします。また、sshdも動作していて、以下のようにSSHでログインできるとします。

% ssh www.example.com
(ここからwww.example.comにログイン済み)
% hostname
www.example.com

このサーバを中継サーバとして利用して、mobile1からwork1やwork2にログインしたり、scpでファイルを転送したりすることができます。

そのためにはまず、接続先LAN内にあるログインしたいコンピュータと、中継サーバの間で、ポートフォワードのためのSSH接続を確立しておく必要があります

ポートフォワードのためのSSH接続

mobile1からログインしたいコンピュータであるwork1で、以下のようなコマンド列を実行します:

(work1にログイン済み)
% ssh www.example.com -R 10022:192.168.1.10:22

sshコマンドに-R 転送元ポート:転送先ホスト:転送先ポート というオプションを指定すると、SSH接続が維持されている間、接続先サーバから接続元PCへのポートフォワードが行われるようになります。 これを、俗に「トンネルを掘る」と言います。

上記の例だと、www.example.comの10022番ポートに対する通信が、work1(192.168.1.10)の22番ポートに転送されます。 実際、www.example.comにログインした状態から、そのサーバ自身の10022番ポートを指定してssh localhost -p 10022とSSH接続を行うと、www.example.comのサーバ自身ではなく、work1の方にログインできるようになっています:

(www.example.comにログイン済み)
% ssh localhost -p 10022
(ここからwork1にログイン済み)
% hostname
work1

ここまでの流れを図にすると、こんな感じです:

画像:接続先となるLAN内のコンピュータと、中継用サーバとの間での接続テストの様子

192.168.1.0/24の外にあるサーバから、192.168.1.0/24の中にあるwork1へと、本来できないはずのSSH接続ができているという所がポイントです。

インターネットからwork1(192.168.1.10)に接続する

「ちょっと待った!!! インターネット上にあるサーバからLAN内のPCにSSH接続できるようになってるって、セキュリティ的に超危ないんじゃないの!?!?!」

はい、そのように危惧するのも無理はないですが、心配する必要はありません。この状態はローカル転送といって、www.example.comのサーバの上から自分自身の10022番ポートに対して行う通信だけが192.168.1.10に転送されます。仮に第三者が他のコンピュータからssh www.example.com -p 10022と接続を試みても、その通信は転送されないため、接続は失敗します。安心ですね。

ではどうやってインターネットからwork1に接続すればいいのかというと、話は単純で、中継サーバであるwww.example.com自体に一旦ログインすればいいのです。www.example.comにログインできてしまえば、そこからssh localhost -p 10022でwork1にログインできるというわけです。

(mobile1にログイン済み)
% ssh www.example.com
(ここからwww.example.comにログイン済み)
% ssh localhost -p 10022
(ここからwork1にログイン済み)
% hostname
work1

こうして、一度192.168.1.0/24のネットワークの内側に入れてしまえば、あとは何でもし放題です。そこからさらに他のPCであるwork2にログインすることもできますね。

(work1にログイン済み)
% ssh 192.168.1.11
(ここからwork2にログイン済み)
% hostname
work2
出かける前にすること

さて、準備ができましたので会社を出発しましょう……と言いたい所ですが、もう少しだけ待って下さい。

この状態だと、work1とwww.example.comの間で接続が不意に切れてしまったら、それでもうおしまいです。外出先からwww.example.comには接続できても、そこからwork1に接続することはできなくなってしまいます。

ですので、work1とwww.example.comとのSSH接続は、自動的に再接続するようにしておきましょう。これは、sshコマンドの代わりにautosshというコマンドを使えばOKです。autosshをインストールして、sshコマンドと同様の引数・オプションを指定し実行するだけで、準備は完了です:

% autossh www.example.com -R 10022:192.168.1.10:22

これで、もしwork1とwww.example.comの間で接続が不意に切れてしまっても、自動的に再接続してくれるようになります。

また、work1がUbuntuのデスクトップ環境を備えているなら、スクリーンをロックして出掛ければよいのですが、そうでないなら、シェルにログインしっぱなしのまま出掛けるというのは不用心すぎます。work1の上で仮想端末を起動して、autosshでの接続は仮想端末上で行うようにしましょう。例えばtmuxを使うのであれば、こんな感じです:

(work1にログイン済み)
% tmux
(ここから仮想端末の中)
% autossh www.example.com -R 10022:192.168.1.10:22
(ここからwww.example.comにログイン済み)
(Ctrl-B, Dで仮想端末から切断)
(ここからwork1にログイン済み)
% exit

以上ですべての準備は終わりです。会社を出発して、外出先から社内のPCにSSHでログインし、快適リモート操作を存分に楽しみましょう!

ここまでの流れを図にすると、以下の通りです:

画像:SSHポートフォワードを使ってインターネット経由で社内LAN内のコンピュータに接続する様子

LAN内のサーバを中継サーバにする方法

接続元となるLAN内にサーバが置かれていて、Dynamic DNSを用いるなどしてインターネットからそのサーバにログインできるようになっている場合には、そのサーバを中継サーバにすることもできます:

画像:自宅LAN内に中継サーバとなるサーバがある様子

先の例とは違うネットワークであることが分かりやすいように、今度は接続元LANを192.168.20.0/24としています。この自宅ネットワーク内に、自分が操作するhomepcという名前のPCと、homeserverという名前のホームサーバ(192.168.20.100)があり、インターネットからdynamic.example.comというホストの22番ポートにSSH接続するとhomeserverにSSHでログインできる、という状態になっていると仮定します。

% ssh dynamic.example.com
(ここからhomeserverにログイン済み)
% hostname
homeserver

ここで、帰宅後に自宅のhomepcから、社内にあるwork1やwork2に接続したいという場合を考えます。 この場合も、ポートフォワードを使った接続のやり方は、基本的に先程の例と同じ要領です。

まず、homepcからログインしたいコンピュータであるwork1で、以下のようなコマンド列を実行して、homeserverとの間にトンネルを掘ります:

(work1にログイン済み)
% ssh dynamic.example.com -R 10022:192.168.1.10:22

これで、homeserverの10022番ポートに対する通信が、work1(192.168.1.10)の22番ポートに転送されるようになりました。試しにwork1にログインしてみましょう:

(work1にログイン済み)
% ssh dynamic.example.com -R 10022:192.168.1.10:22
(ここからhomeserverにログイン済み)
% ssh localhost -p 10022
(ここからwork1にログイン済み)
% hostname
work1

ここまでの流れを図にすると、こんな感じです:

画像:社内LANのコンピュータと自宅LANのコンピュータの間での接続テストの様子。

ポートフォワードが期待通りに働いていることを確認できたら、sshコマンドの代わりにautosshコマンドを使うようにしましょう。また、直接ログインした端末ではなく仮想端末の中から、work1とhomeserverにトンネルを掘っておきましょう:

(work1にログイン済み)
% tmux
(ここから仮想端末の中)
% autossh dynamic.example.com -R 10022:192.168.1.10:22
(ここからhomeserverにログイン済み)
(Ctrl-B, Dで仮想端末から切断)
(ここからwork1にログイン済み)
% exit

これで準備は完了です。颯爽と帰宅し、自宅から社内のPCにSSHでログインして、快適リモート勤務を満喫しましょう!

(homepcにログイン済み)
% ssh 192.168.20.100
(ここからhomeserverにログイン済み)
% ssh localhost -p 10022
(ここからwork1にログイン済み)
% hostname
work1
% ssh 192.168.1.11
(ここからwork2にログイン済み)
% hostname
work2

ここまでの流れを図にすると、以下の通りです:

画像:LANからLANへの、ポートフォワードを利用したログインの様子。

まとめ

以上、SSHのポートフォワード機能を使って、インターネット上のサーバやLAN内のサーバを中継サーバに利用し、外部からインターネット経由でLAN内のコンピュータにSSH接続する手順を紹介しました。

SSHのポートフォワードの指定は、どのオプションのどの部分に何を指定すればよいか、ぱっと見で分かりにくいのが難点です。この記事では具体的な例を挙げて、それぞれのコンピュータの関係を分かりやすく示した状態で、オプションに指定する内容を示してみました。

自分の場合のネットワーク環境でも、この記事を参考に、ぜひ一度試してみて下さい。

なお、筆者が執筆した「まんがでわかるLinux シス管系女子3」という書籍では、SSHポートフォワードの様々な例についてまんが形式でさらに詳しい解説を行っています。SSHポートフォワードをより自在に使いこなせるようになりたいという方は、そちらも併せてご覧下さい。

2014-09-12

2014年10月30日(木)にリーダブルコードワークショップを開催予定

クリアコードは社名の通り、クリアなコード(書いた人の意図が明確なコード)を大事にしている会社です。自分たちが日々クリアなコードを書くことはもちろんですが、自分たち以外の人たちもクリアなコードを書いて、世の中のクリアなコードが増えていくとうれしいと考えています。

リーダブルコードの解説を書いたこともクリアなコードを広めることにつながると考えています。理解しやすさを大事にしているリーダブルコードと、書いた人の意図が明確であることを大事にしているクリアなコードは、同じ方向性だという考えです。そのため、SEゼミでの学生向けのリーダブルコード勉強会も私たちにとっては大事な活動でした。

この度、リーダブルコードの翻訳者の角さんからの紹介で、2014年10月30日(木)に社会人向けの有料のリーダブルコードワークショップを開催することになりました。アジャイルアカデミーの実践リーダブルコードという講座がそれです。

好評だったSEゼミの内容をベースに社会人向けにアレンジをした内容になります。リーダブルコードを読んだけどリーダブルコードは書けない、という人は「読み手の視点」が欠けています。落ち着いて考えてみれば、読み手の視点がないと読み手が理解しやすいコードを書くことは難しい、ということは「たしかにその通り」という話です。たくさんコードを読んでいる人ほど読みやすいコードを書いていませんか?

このワークショップではリーダブルコードを書くために必要な「読み手の視点」を実際に体験します。読み手の視点を意識しながらコードを書くことで、日々、リーダブルコードを書くことができるようになります。リーダブルなコードが当たり前の世界です。

リーダブルコードを読んだけど実践できていないという方のご参加をお待ちしています。

なお、このワークショップで使う資料はGitHubで公開しています。CC BY-SA 4.0(著作者:株式会社クリアコード)のライセンスの範囲で自由に利用できます。詳しい内容が気になるという方は確認してみてください。

なお、このワークショップでは実際にコードを書きます。書いたコードはGitHubにpushしていきます。そのため、検索すればだれがこのワークショップに参加したかわかります。もし、ワークショップに参加したいけど、参加したことは秘密にしたいという方がいましたら、クリアコードのお問い合わせフォームからお問い合わせください。

ワークショップ内でGitHubを使うのはコードを共有するためです*1。自前で共有する仕組みを作るより、既存の仕組みを使った方が、参加者の準備*2も講師側の準備*3も楽になるためGitHubを使っています。もし、GitHubを使うことが参加の障害になるようであれば、別の方法を検討します*4

あわせてもう一度読みたい:リーダブルコードの解説

*1 「コードを交換する」というこのワークショップの肝になる部分でコードを共有します。

*2 リポジトリーへのアクセス手段のセットアップなど。

*3 ワークショップ用に専用ローカルリポジトリーをセットアップするなど。

*4 たとえば、Bitbucketを使うなど。Bitbucketは無料プランでも非公開リポジトリーを作れます。

2014-09-16

AC_ARG_WITHでオプションを省略するとき、しないとき

はじめに

クリアコードの林です。今回はAutotools *1 にまつわる、最近遭遇したAC_ARG_WITH(に限らず)マクロの省略可能引数の落し穴の事例を紹介します。

ことの始まり

今回の記事を書くきっかけは、とあるアプリケーションのGStreamer 0.10/1.0対応をしていたときのことです。 GStreamer 0.10に対応していたアプリケーションを1.0系にも対応させることになりましたが、GStreamerは0.10系と1.0系とではAPIがかなり変更されています。 まだ0.10系を使いたいという需要もあったことから、0.10系と1.0系両方対応することになりました。

そのアプリケーションでは、ビルドシステムとしてAutotools *2 を使用していました。 そのため、configureのオプションでどちらのバージョンを使うか切り替えられるようにすることにしました。 Autoconfには --with-foo-bar という形式でオプションに指定した値を受けとることのできるマクロとして AC_ARG_WITH があるので、これを使うことにしました。

configureで実現しようとしていたこと

configureスクリプトを実行するときに実現したかったことは次の通りです。

  • 明示的にGStreamer 0.10/1.0を指定してビルドすることができる
  • 特に指定していない場合、GStreamer 1.0があればそちらを使い、なければGStreamer 0.10を使ってビルドすることができる

前者は --with-gstreamer-version というオプションを追加し、 --with-gstreamer-version=0.10 であればGStreamer 0.10系を使い、 --with-gstreamer-version=1.0 であればGStreamer 1.0系を使う指定となるように AC_ARG_WITH を追加しました。

AC_ARG_WITH([gstreamer],
            AS_HELP_STRING([--with-gstreamer-version=@<:@auto/1.0/0.10@:>@],
                           [Gstreamer version (default: auto)]),
            [with_gstreamer_version=auto])

後者は --with-gstreamer-version の値を見て PKG_CHECK_EXISTS マクロでGStreamer 1.0の有無、GStreamer 0.10の有無を順にチェックするようにしました。

case "$with_gstreamer_version" in
    "0.10")
        GSTREAMER_REQUIRED=0.10.35
        GST_MAJORMINOR=0.10
        ;;
    "1.0")
        GSTREAMER_REQUIRED=1.0.0
        GST_MAJORMINOR=1.0
        ;;
    *)
        PKG_CHECK_EXISTS(gstreamer-1.0,
                         [GSTREAMER_REQUIRED=1.0.0 GST_MAJORMINOR=1.0],
                         [GSTREAMER_REQUIRED=0.10.36 GST_MAJORMINOR=0.10])
       ;;
esac

GSTREAMER_REQUIRED というのが必須となるバージョン指定で、GST_MAJORMINORというのがGStreamer 0.10/1.0どっちを使うかという変数です。 それぞれの変数を使った具体的な0.10/1.0の対応については割愛しますが、バージョンチェック自体はこんな感じにしていました。

AC_ARG_WITHの奇妙な挙動

前述のように AC_ARG_WITH を追加した後で、警告がでることに気づきました。

% ./configure --with-gstreamer-version=0.10
configure: WARNING: unrecognized options: --with-gstreamer-version

configure実行時にオプションが認識できていません。これは、 AC_ARG_WITH の第1引数の指定ミスでした。

% diff -u just-work/configure.ac right-thing/configure.ac
--- just-work/configure.ac        2014-09-11 16:39:34.392214521 +0900
+++ right-thing/configure.ac 2014-09-11 16:39:34.392214521 +0900
@@ -13,9 +13,10 @@

 # Checks for libraries.

-AC_ARG_WITH([gstreamer],
+AC_ARG_WITH([gstreamer-version],
             AS_HELP_STRING([--with-gstreamer-version=@<:@auto/1.0/0.10@:>@],
                            [Gstreamer version (default: auto)]),

ただ、修正前の状態で奇妙な挙動を示していることに気づきました。 オプション指定が間違っているのに、明示的に指定した0.10を使えています。

明示的に0.10を指定した場合、次のようになりました。

% ./configure --with-gstreamer-version=0.10
configure: WARNING: unrecognized options: --with-gstreamer-version
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /bin/mkdir -p
checking for gawk... gawk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking for gcc... gcc
checking whether the C compiler works... yes
checking for C compiler default output file name... a.out
checking for suffix of executables... 
checking whether we are cross compiling... no
checking for suffix of object files... o
checking whether we are using the GNU C compiler... yes
checking whether gcc accepts -g... yes
checking for gcc option to accept ISO C89... none needed
checking whether gcc understands -c and -o together... yes
checking for style of include used by make... GNU
checking dependency style of gcc... gcc3
0.10
checking for GST... yes
checking that generated files are newer than configure... done
configure: creating ./config.status
config.status: creating Makefile
config.status: creating config.h
config.status: config.h is unchanged
config.status: executing depfiles commands
configure: WARNING: unrecognized options: --with-gstreamer-version
GStreamer: 0.10.36

明示的に1.0を指定した場合、次のようになりました。

% ./configure --with-gstreamer-version=1.0
configure: WARNING: unrecognized options: --with-gstreamer-version
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /bin/mkdir -p
checking for gawk... gawk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking for gcc... gcc
checking whether the C compiler works... yes
checking for C compiler default output file name... a.out
checking for suffix of executables... 
checking whether we are cross compiling... no
checking for suffix of object files... o
checking whether we are using the GNU C compiler... yes
checking whether gcc accepts -g... yes
checking for gcc option to accept ISO C89... none needed
checking whether gcc understands -c and -o together... yes
checking for style of include used by make... GNU
checking dependency style of gcc... gcc3
1.0
checking for GST... yes
checking that generated files are newer than configure... done
configure: creating ./config.status
config.status: creating Makefile
config.status: creating config.h
config.status: config.h is unchanged
config.status: executing depfiles commands
configure: WARNING: unrecognized options: --with-gstreamer-version
GStreamer: 1.2.4

AC_ARG_WITH の定義が間違っているので、元々意図していた --with-gstreamer-version= が機能しないように思えますが、実際にはうまく動いています。 どういうことでしょうか。*3

AC_ARG_WITHとフォールバック

おかしいなと感じたら、まずは公式ドキュメントを確認してみましょう。 AC_ARG_WITH()のドキュメント のシグネチャは次の通りです。

Macro: AC_ARG_WITH (package, help-string, [action-if-given], [action-if-not-given])

間違っていた定義では、 packagegstreamer-version ではなく gstreamer なので --with-gstreamer-versionunrecognized option 扱いになっています。 そのため、以下の警告が出ていました。

configure: WARNING: unrecognized options: --with-gstreamer-version

このとき、Autoconfはフォールバックとして with_gstreamer_version に指定した値をいれます。

そこで、以下のようにAC_ARG_WITHそのものを削除してみました。

% diff -u just-work/configure.ac fallback/configure.ac
--- just-work/configure.ac      2014-09-11 16:39:34.392214521 +0900
+++ fallback/configure.ac       2014-09-11 16:39:34.392214521 +0900
@@ -13,10 +13,6 @@

 # Checks for libraries.

-AC_ARG_WITH([gstreamer],
-            AS_HELP_STRING([--with-gstreamer-version=@<:@auto/1.0/0.10@:>@],
-                           [Gstreamer version (default: auto)]),
-            [with_gstreamer_version=auto])
 echo $with_gstreamer_version

結果からいうと上記の AC_ARG_WITH を削除しても挙動がまったく一緒でした。 つまり、Autoconfがフォールバックとして $with_gstreamer_version に指定した値を設定してくれているため、たまたまうまく動作していたのです。

AC_ARG_WITH の正しい指定

まずは、 AC_ARG_WITHの第1引数である packagegstreamer ではなく、 gstreamer-version にします。

AC_ARG_WITH([gstreamer-version],
            AS_HELP_STRING([--with-gstreamer-version=@<:@auto/1.0/0.10@:>@],
                           [Gstreamer version (default: auto)]),
            [with_gstreamer_version=auto])

次に [with_gstreamer_version=auto] は、何も指定していなかったらautoにするためのもの *4 なので、 action-if-not-given に対応していないといけません。 action-if-not-given は第4引数として指定することになっています。 よく見てみましょう。「,」 不足で第4引数ではなく、第3引数になってしまっていますね。

したがって、正しい定義は次の通りです。

AC_ARG_WITH([gstreamer-version],
            AS_HELP_STRING([--with-gstreamer-version=@<:@auto/1.0/0.10@:>@],
                           [Gstreamer version (default: auto)]),,
            [with_gstreamer_version=auto])

でも、,, で第3引数が省略されているのはとてもわかりにくいですね。明示的に書くことにしましょう。 最終的な定義はこうなりました。

AC_ARG_WITH([gstreamer-version],
            AS_HELP_STRING([--with-gstreamer-version=@<:@auto/1.0/0.10@:>@],
                           [Gstreamer version (default: auto)]),
            [with_gstreamer_version=$withval],
            [with_gstreamer_version=auto])

まとめ

今回はAutotoolsの AC_ARG_WITH マクロでオプションを省略するとき、しないときの挙動にまつわる事例を紹介しました。 必要最小限の記述をするために、省略可能な引数であれば省略するというのは正しいやりかたです。ただし、省略のしかたによっては、(ミスをしたときに)意図しない挙動にめんくらうことにもなります。

もし意図しない挙動に遭遇したら、まずは公式のドキュメントを参照してみましょう。うっかり勘違いして使っていないか確認できます。 途中に出ているログも注意深く見ておきましょう。今回のように明らかな警告がでているかもしれません。

GitHubに今回使用した サンプルのリポジトリ を置いてあるので、git cloneして手元でも動かして試すことができます。

git clone https://github.com/kenhys/ac-arg-with-sample.git

上記のリポジトリには以下の3つのサンプルが含まれています。

  • fallback AC_ARG_WITH を削除したバージョン
  • just-work 今回の記事のきっかけとなった奇妙な挙動を示すバージョン
  • right-thing 修正済みの AC_ARG_WITH バージョン

動作を試すには、それぞれのディレクトリに移動した後、最初に ../autogen.sh 、 次に ./configure を実行してみてください。

% ../autogen.sh
% ./configure --with-gstreamer-version=0.10
% ./configure --with-gstreamer-version=1.0

*1 Autotoolsに関しては Autotools事始め という過去記事もあるのでAutotoolsをまだよく知らない人は参考にしてみてください。

*2 Autotoolsを使用していると、./configure; make; make installでインストールまで行える。

*3  途中で出力している0.10とか1.0というのはデバッグのためにあえて出力しているもの。

*4 autoだったらcase文で分岐してPKG_CHECK_EXISTSで再度チェックするようにした部分が実行される。

2014-09-22

RubyKaigi 2014:Three Ruby usages - High-level interface, Glue and Embed #rubykaigi

RubyKaigi 2014の最終日(2014/9/20)にThree Ruby usagesというタイトルでRubyの使い方を紹介しました。

関連リンク:

内容

この発表では特に目新しい技術的な話題はありません。これまで何年もRubyを使ってきた経験からRubyの使い方を大きく3つに分けて整理した、という話です。

応募したときは、それぞれの使い方についてメリット・デメリットを説明しこの使い方をするときのトレードオフはどこになるかまで示して、「聞いている人たちがどの使い方を選べばよいか(選ばないほうがよいか)を判断できるようになること」を目指すつもりでした。しかし、資料をまとめていると時間が足りないことがわかったので、「聞いている人たちがどのような使い方があるかを知ること(実際に使えなくてもよい。後で思い出せればよい。)」を目指しました。残骸はリポジトリーの中に転がっています。

この発表で紹介したRubyの使い方は次の3つです。

  • High-level interface:ハイレベルなインターフェイスを実装するためにRubyを使う
  • Glue:既存の機能を再利用するためにRubyを使う
  • Embed:アプリケーションに組み込んでRubyを使う

それぞれ簡単に説明します。

High-level interface

ハイレベルなインターフェイスを実装するときに大事なことは、低いレイヤーの機能を使いやすい形で使えるようにすることです。

使いにくいなら余計なレイヤーを挟まず低いレイヤーの機能を直接使った方がよいです。そっちの方がわかりやすいからです。この場合は、ハイレベルなインターフェイスの存在意義がありません。

このRubyの使い方はふだんRubyだけでプログラムを書いている人向けの使い方です。低いレイヤーの機能が一通り揃っていることが前提になるので、すべてRubyの世界だけで完結することができます。

使いやすいインターフェイスを実装するためにRubyの柔軟性が役に立ちます。

このRubyの使い方をしているソフトウェアはたとえばVagrantやActive Recordです。

VagrantはVagrantfileという設定ファイルを使います。このファイルはRubyスクリプトです。この設定ファイルで次のように書けばUbuntu 14.04の環境を用意できます。どのように環境を用意するかといった低いレイヤーの機能を隠蔽して簡単に使えるようになっています。

1
2
config.vm.box = "ubuntu-14.04-x86_64"
config.vm.box_url = "http://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_ubuntu-14.04_chef-provisionerless.box"

Active RecordはRDBMSにあるデータにアクセスするためにオブジェクトを中心に据えたAPIを提供しています。これはプログラム中で直接SQLを記述するよりもまわりのRubyのコードになじむため使いやすいAPIです。

使いやすいAPIの作り方についてはRubyKaigi 2013で話した「Be a library developer」の解説がるびまにあるので、それも参考にしてください。

今まで意識せずにハイレベルなインターフェイスを実装するためにRubyを使っていた人も多いでしょう。そのとき、「使いやすさ」については考えていましたか?今度からは「使いやすさ」についても考えてRubyを使ってみてください。

Glue

Glue(グルー)とは元々は接着剤という意味です。プログラムの世界では「複数の機能をつなぎ合わせる」という意味で使います。ここでの「機能」はすでに存在していることが前提です。そのため、グルーを使う理由は「既存の機能を再利用したい」なのです。

グルーとしてRubyを使うには既存の機能のことも知らなければいけません。そのため、Ruby以外の知識が必要になります。よって、GlueとしてRubyを使う場合はRubyだけでなく、必要ならC/C++を書けるくらいの技術や、他のシステムの知識なども必要になります。

Rubyをグルー言語として使うというとバインディング(拡張ライブラリー)のことだと思う人が多いでしょう。しかし、バインディングだけがグルー言語としての使い方ではありません。次のような使い方もグルー言語としての使い方です。この使い方の本質は「既存の機能を再利用すること」だということを思い出してください。

  • 既存のツールの機能を使う(組み合わせる)
  • 既存のサービスの機能を使う(組み合わせる)

「既存のライブラリーの機能を使う」ための方法がバインディングです。Active Recordで言えばmysql2 gemがバインディングタイプのグルーです。mysql2 gemはlibmysqlclient.soが提供している「MySQLサーバーにアクセスする機能」をRubyから使えるようにします。

「既存のツールの機能を使う」ためには外部コマンドを実行します。多くの場合、PID(プロセスID)などを管理したり、外部コマンドであることを意識させずに便利に使えるようにするためにクラスを作ります。たとえば次のような感じです。

1
2
3
4
5
6
7
8
class Git
  # ...
end

git = Git.new
git.clone("git@github.com/...")
git.push

Vagrantは内部でVirtualBoxやChefなど外部のツールの機能を組み合わせて「開発環境の構築機能」を実現しています。Vagrantのメインの機能はGlueとしてRubyを使うという使い方だったのです。気付いていましたか?

「既存のサービスの機能を使う」ためにはクライアントを実装します。Twitterの検索サービスを使うためのTwitterクライアントライブラリーはGlueとしてRubyを使っています。

GlueとしてRubyを使うときは既存の機能のよさを潰さないようにすることが重要です。たとえば、省メモリーで動くことがウリの機能なのにGlueを通したらたくさんメモリーを使うようになった、となるならメリットがだいぶ薄れます。同じようにたくさんのことができることがウリならそこはGlueを使っても活かさないといけません*1。速度についても同様です。

スライドでは全文検索エンジンGroongaのバインディングであるRroongaを例にして速度が重要なGlueの作り方のコツを少し紹介しました。

Glueは「機能を再利用したい」という要求を満たすための使い方です。これを忘れなければよいグルーを作るためにRubyを使えるでしょう。また、Rubyでは既存の機能のよさを潰してしまうことがわかったらRubyをグルー言語として使うことをやめることもできるはずです。Rubyが向いていることも多いですが、Rubyが向いていないこともあります。「機能を再利用したい」からGlueとして使うんだということを思い出してください。

Embed

Embed(エンベッド)はC/C++で書かれたアプリケーション・ライブラリーにRubyを組み込む使い方です。いわゆる組み込み機器で使う、という使い方は意図していません。

組み込む理由は次の通りです。

  • 開発速度をあげる(C/C++よりRubyの方が短い時間で同じ機能を実現できる)
  • 柔軟性を導入する(C/C++だとできない動的な処理を実現する)

この使い方はC/C++で書かれたアプリケーション・ライブラリーの中身をよく知らないといけないので、Rubyと同じようにC/C++も使えるという人向けの使い方です。

Rubyを組み込むというと「Rubyでプラグインを書けるようにする」という使い方を思い浮かべることが多いでしょう。しかし、それだけではありません。アプリケーション・ライブラリーのコアの処理の一部を実装するためにRubyを使うという方法もあります。

プラグインとして使っているソフトウェアは、たとえばmod_mrubyです。コアの機能はApacheが提供していて、ApacheはCで実装されています。mod_mrubyはApacheの設定ファイル中でmrubyを使って処理を書けます。

コアの処理の一部を実装するためにRubyを使っているソフトウェアは、たとえば前述のGroongaです。Groongaはクエリーオプティマイザーという処理をmrubyで実装しています*2

コアの処理の一部を実装するために使う場合は速度に気をつける必要があります。具体的には「相対的に重い処理では使わない」という点に気をつけます。

「相対的」というのがポイントです。もし、重い処理であっても最初に一度だけ実行するだけで、その後は長い期間動き続けるなら重い処理にRubyを使っても悪影響は小さいです。何度も実行される処理なら軽い処理でだけRubyを使うべきです。そうしないとRubyを組み込むことによるデメリットが大きくなってしまいます。

Groongaに組み込むケースでどのようにしているかはスライドを参照してください。

なお、この例ではどちらもmrubyを組み込んでいますが、CRubyもアプリケーションに組み込めます*3。性質が違う実装なので目的と照らし合わせて選んでください。

  • CRuby
    • Active Recordなど既存のgemをたくさん使いたいとき
    • Rubyインタープリターがプロセスに1つで十分なとき
  • mruby
    • Rubyの基本的な機能で十分なとき
    • Rubyインタープリターをプロセスで複数作りたいとき(たとえば、スレッドごとにRubyインタープリターを持ちたいなど)

GroongaはスレッドごとにRubyインタープリターを持ちたかった(マルチスレッドをサポートしたかった)のでmrubyを選択しました。

Rubyを組み込むという使い方を選択する理由の1つは「開発速度をあげる」ことでした。実は、Rubyを組み込むという使い方は導入コスト*4がとても高いのです。そのため、「開発速度をあげる」ために要した労力がわりにあわない場合もあります。コストをかけて導入しても速度がでなくて実用に耐えない場合もあります。

この使い方をするときは、なぜ埋め込みたいのか、それがわりにあうのかということを考えてください。「Rubyを使いたい」というのが出発点になることも多いでしょうが、それだけでなく、客観的な視点も入れてください。

まとめ

これまでの経験を元にRubyの使い方を大きく3つにわけて整理して紹介しました。使い方を整理することで、その使い方では本質的に大事にしなければいけないことが見えてきます。何気なくRubyを使っている人が多いでしょうが、一度その使い方で大事なことはなにか考えてみてはいかがでしょうか。

この発表での整理をきっかけに今まで意識したことがなかった使い方でRubyを使う人がでてくるとうれしいです。また、自分の使い方を見なおす機会になってもうれしいです。

もし、使い方で悩むことがあったら、イベントなどでばったり会ったときにでも声をかけてくれれば相談にのります*5。直近では10月18日(土)にOSC 2014 Tokyo/Fallで「いろいろ考えると日本語の全文検索もMySQLがいいね!」という発表をしたり、11月13日(木)にRubyWorld Conference 2014で発表したりする予定です。

お仕事での相談ならお問い合わせフォームから連絡してください。

なお、RubyKaigi 2014もスポンサーとして参加しましたが、今年も効果があった気配があります。スポンサーしてよかったです。

*1 使い勝手を特に気にしないならSWIGなどを使ってバインディングを自動生成するのもアリです。その上にHigh-level interfaceを作って使い勝手をよくすることもできます。バインディングの作り方についてはスクリプト言語の拡張機能の作り方とGObject Introspectionの紹介も参考にしてください。

*2 正確にはビルドオプションでmrubyでもクエリーオプティマイザーを書けるようになる。

*3 実際にCRubyを組み込んだ経験があります。たとえば、milter managerはCRubyを組み込んでいます。

*4 バインディングを書かないといけないですし、ビルドシステムに組み込む必要もあります。正規表現も使いたいなら鬼車あるいはOnigmoも追加でビルドしないといけません。他にもテストはどうするの?デバッグは?などいろいろあります。

*5 インターネット上なら@ktouで相談にのります。

タグ: Ruby
2014-09-24

Cutterで画像を使ったテストを書くには

はじめに

今回は弊社が中心となって開発しているCutterという、書きやすさ・デバッグのしやすさを重視したC言語・C++言語用のテスティングフレームワークを使って、画像を使ったテストを簡単に書く方法を紹介します。

画像を使ったテストを書くとき

たとえば、画像を生成するアプリケーションを開発していて、機能追加によってその機能が壊れていないことを保証するにはどうすればよいでしょうか。 これは、以前生成した画像と比較するテストで検証できれば良いですね。

Cutterによるテストでは、テスティングフレームワークの機能のひとつとして画像差分をサポートしています。 そのため、画像だからといって特別なことをせず、フレームワークの枠内で簡単にテストを書くことができます。

では、実際にどんな風に簡単にテストを書くことができるのかみてみましょう。

Cutterをインストール

まずは、Cutterをインストールしましょう。 各種ディストリビューション向けのインストールのドキュメントがあります。そちらを参考にしてください。

Ubuntuの場合には、以下の手順で必要なパッケージをインストールすることができます。

% sudo apt-get -y install software-properties-common
% sudo add-apt-repository -y universe
% sudo add-apt-repository -y ppa:cutter-testing-framework/ppa
% sudo apt-get update
% sudo apt-get -y install cutter-testing-framework

普通のCutterのテストを書く

Cutterでテストを書くには、test_XXXという名前の関数を定義します。

CUT_EXPORT void test_equal(void)
{
}

このようにすると、Cutterがテストを実行するときに、定義されたテストをtest_XXX自動検出して実行します。

画像を比較するassertionを追加する

画像の場合、Cutterのテストに画像を比較するためのassertionを追加するだけです。 gdkcut_pixbuf_assert_equalというのがそのためのassertionです。*1

CUT_EXPORT void test_equal(void)
{
  GdkPixbuf *expected, *actual;
  expected = load_fixture_image("base.png");
  actual = load_fixture_image("copy.png");

  gdkcut_pixbuf_assert_equal(expected, actual, 0);
}

ここでのポイントはGdkPixbuf*で比較していることです。gdk-pixbufは幅広い画像形式をサポートしているので、読み込みさえできてしまえば、あとはとても簡単ですね。*2 期待する結果をexpectedに、実際の処理で得られた結果をactualとして比較することができます。

画像を含むテストを実行する

テストを定義したので実行してみましょう。以下はカレントディレクトリ以下にある test_equalという名前のテストを実行しています。

% cutter . -n '/test_equal/'
.

Finished in 0.020999 seconds (total: 0.001533 seconds)

1 test(s), 3 assertion(s), 0 failure(s), 0 error(s), 0 pending(s), 0 omission(s), 0 notification(s)
100% passed

無事テストが通りました。 *3

画像差分機能を使って確認する

では、画像に差分がでたときにどう表示されるのでしょうか。 次のような、失敗するテストを書いてみます。

CUT_EXPORT void test_diff(void)
{
  GdkPixbuf *expected, *actual;
  expected = load_fixture_image("OK.png");
  actual = load_fixture_image("NG.png");

  gdkcut_pixbuf_assert_equal(expected, actual, 0);
}

実行してみましょう。

% cutter . -n '/test_diff/'
F
===============================================================================
Failure: test_diff
<expected == actual> (0)
  expected: <#<GdkPixbuf:0x155f680 colorspace=<#<GdkColorspace: rgb(GDK_COLORSPACE_RGB:0)>>, n-channels=<3>, has-alpha=<FALSE>, bits-per-sample=<8>, width=<100>, height=<100>, rowstride=<300>, pixels=<((gpointer) 0x156fef0)>>>
    actual: <#<GdkPixbuf:0x155f6d0 colorspace=<#<GdkColorspace: rgb(GDK_COLORSPACE_RGB:0)>>, n-channels=<3>, has-alpha=<FALSE>, bits-per-sample=<8>, width=<100>, height=<100>, rowstride=<300>, pixels=<((gpointer) 0x1578210)>>>
 threshold: <0>
diff image: <test-sample.c-45.png>
test-sample.c:45: test_diff(): gdkcut_pixbuf_assert_equal(expected, actual, 0)
===============================================================================


Finished in 0.184523 seconds (total: 0.014664 seconds)

1 test(s), 2 assertion(s), 1 failure(s), 0 error(s), 0 pending(s), 0 omission(s), 0 notification(s)
0% passed

確かに失敗しましたが、これだけだとわかりにくいですね。ここでのポイントは diff image: <test-sample.c-45.png> という行です。 これが、テストに失敗したときに生成される差分画像です。

Cutterによる差分画像の例

差分画像を表示すると、4分割されているのがわかります。上半分がテストに使用した画像です。左が期待する結果、右が実際の結果の画像です。 下半分が違いを示す差分です。重ね合わせた違いがわかりますね。

gdkcut_pixbuf_assert_equalの第三引数は何なのか

ここまでで、Cutterで画像を使ったテストを書けるようになりました。でもgdkcut_pixbuf_assert_equalには第三引数があります。 これは何なのでしょうか。

リファレンスマニュアルを見てみましょう。gdk-pixbufサポート付きの検証には、「ピクセルの違いを検出するために使われるしきい値」とあります。

ピクセル値の違いが指定した範囲に収まれば、それは同じ画像であるとみなすということです。

では、thresholdをうまく使ってだいだい同じ画像とみなせるならテストを通るようにする、というのをやってみましょう。

例として、元画像を gdk-pixbuf-scale-simpleを使って縮小した画像同士を比較してみることにします。

gdk-pixbuf-scale-simpleは画像を縮小する方法をいくつか選択することができます。ここでは、GDK_INTERP_BILINEARGDK_INTERP_HYPERでそれぞれ縮小したサンプルでテストします。

まずは、thresholdを0にしてテストしてみましょう。

% cutter . -n '/test_bilinear_and_hyper/'
F
===============================================================================
Failure: test_bilinear_and_hyper
<expected == actual> (0)
  expected: <#<GdkPixbuf:0xa40280 colorspace=<#<GdkColorspace: rgb(GDK_COLORSPACE_RGB:0)>>, n-channels=<3>, has-alpha=<FALSE>, bits-per-sample=<8>, width=<80>, height=<80>, rowstride=<240>, pixels=<((gpointer) 0xa50eb0)>>>
    actual: <#<GdkPixbuf:0xa402d0 colorspace=<#<GdkColorspace: rgb(GDK_COLORSPACE_RGB:0)>>, n-channels=<3>, has-alpha=<FALSE>, bits-per-sample=<8>, width=<80>, height=<80>, rowstride=<240>, pixels=<((gpointer) 0xa56a70)>>>
 threshold: <0>
diff image: <test-sample.c-81.png>
test-sample.c:81: test_bilinear_and_hyper(): gdkcut_pixbuf_assert_equal(expected, actual, 0)
===============================================================================
.

Finished in 0.022689 seconds (total: 0.007011 seconds)

2 test(s), 5 assertion(s), 1 failure(s), 0 error(s), 0 pending(s), 0 omission(s), 0 notification(s)
50% passed

縮小方法が違うので、テストが失敗しました。差分画像を見てみましょう。

BILINEARとHYPERの差分画像

これくらいなら、まぁ許容範囲だなぁという気もするので、thresholdを調整します。

CUT_EXPORT void test_bilinear_and_hyper_threshold(void)
{
  GdkPixbuf *expected, *actual;
  expected = load_fixture_image("NG-bilinear.png");
  actual = load_fixture_image("NG-hyper.png");

  gdkcut_pixbuf_assert_equal(expected, actual, 30);
}

では、thresholdを30にしたテストを実行してみます。

% cutter . -n '/test_bilinear_and_hyper_threshold/'
.

Finished in 0.016168 seconds (total: 0.002432 seconds)

1 test(s), 3 assertion(s), 0 failure(s), 0 error(s), 0 pending(s), 0 omission(s), 0 notification(s)
100% passed

テストが通りましたね。これで、NG-bilinear.pngNG-hyper.png程度の違いは許容するテストを書くことができました。

まとめ

今回はCutterというテスティングフレームワークの画像差分機能を使って簡単にテストを書く方法を紹介しました。 Cutterにはほかにもテスト環境を便利にする機能があります。まだCutterを使ったことがない人はチュートリアルからはじめるとよいでしょう。詳しく知りたい人はリファレンスマニュアルを参照してください。

GitHubに今回使用した サンプルのリポジトリ を置いてあるので、git cloneして手元でも動かして試すことができます。

git clone https://github.com/kenhys/cutter-with-image-test.git

動作を試すには、次のコマンドを実行してください。

% ./autogen.sh
% ./configure
% make
% cutter .

*1 load_fixture_imageという画像を読みこんでGdkPixbuf*として返す関数を定義してあることにします。本質的でないので詳細は割愛します。

*2  gdk-pixbuf-query-loadersを実行するとサポートされている画像形式を確認できます。

*3 期待する結果と、実際の結果にファイル名だけ違う画像を使ったのであたりまえなのですが。

タグ: Cutter | テスト
2014-09-30

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|