目次


MakeMakerによるPerlプロジェクトのビルド

Makefile生成のみに留まらないモジュール機能

Comments

「プログラミング製品」と「プログラム」

「大企業が手間隙かけて開発するプログラムと同等の機能を持つプログラムを、数人の人がガレージにこもってそれよりはるかに短い時間で作り上げてしまった、という話が実に多く聞かれるのはなぜでしょうか」とFrederickBrooks氏はその著書『The Mythical Man-Month』の中で問いかけています。そして彼は、この疑問に対する答えを示すために、ソフトウェアを「プログラム」と「プログラミング製品」という2つのタイプに分類しています。

「プログラム」とは、ある環境内で機能する1つのソフトウェアです。これには、ローカルbinディレクトリーの中に置かれている一連のスクリプトも含まれます。一方、「プログラミング製品」にはプログラムの作成の他にもいくつかの要素が必要とされます。これには、詳しくて役に立つドキュメンテーション、簡単なインストール手順、プログラムの機能を保証するための一連のテスト・ケースなどが含まれます。Brooks氏は、プログラミング製品の開発にはプログラムの開発の少なくとも3倍の労力が必要であるという説を立てています。これはかなり大変なことのように思えるかもしれませんが、その努力が無駄に終わることはまずありません。それだけの努力をした以上、そのプログラムはさらに多くの人々に、ひいては開発者自身にも役立つようになります。皆さんも、スクリプトを実行する際、スクリプトのソース・コードを読むことが必要になったことが何度もあるはずです。しかし、スクリプトに関する疑問にぶつかるたびにmanページを参照するのは決して楽なことではありません。

Perlは、よく「アーミー・ナイフ」や「ダクト・テープ」のように考えられます。つまり、少しでもUNIXで作業をする人は、いつか必ず何らかのPerlコードの作成が必要になるということです。残念なことに、多くの人は、Brooks氏の言う「プログラミング製品」を開発するだけの強力なインフラストラクチャーをPerlが備えていることに気づいていません。このインフラストラクチャーの中心は、ExtUtils::MakeMakerというPerlモジュールです。MakeMakerの基本的な機能は、Makefileを生成するためのPerlインターフェースを提供することですが、それだけはありません。MakeMakerによって生成されるMakefileは、プロジェクトのビルドとインストールを行うだけではなく、manページやHTMLフォーマット・ドキュメンテーションの生成も行い、さらに、アプリケーション全体に対する一連の自動テスト・ケースも実行することができるのです。

この機能はPerlのコア・インフラストラクチャーに緊密に結合されており、学習や使用に伴うオーバーヘッドは少なくてすみます。しかしその一方で、その利点は大きなものです。アプリケーションに対する適切なテスト・ケースを作成した後は、気づかないうちにプロジェクトが逆行するようなことは決してありません。既存の機能を損なう変更を行った場合には直ちに警告が出されるため、新しいパッチを追加するごとに古いバグを再度持ち込んでしまうという危険性は大幅に軽減されます。

Brooks氏による「3倍の労力が必要」という指摘に反して、これらの方法を用いることで実際にはかなりの時間が削減されることが分かりました。一度バグの修正を行えば、まず確実にそのバグは修正されています。また、ドキュメンテーションは、コードと別に提供するよりも、コードに組み込んだ方が簡単にアップデートができます。その上、インストール・スクリプトも無料です。

この記事において示すのは、Perlプログラムの作成方法ではなく、Perlプログラムをはるかに堅固なプログラミング・プロジェクトに変える方法です。そのプロジェクトは、様々な種類のプラットフォームで広く使うことのできる十分な汎用性を備えたものとなります。

MakeMakerプロジェクトの構造

『Programming Perl, 3rd Edition』の表紙には、「There's More Than One Way To Do It(方法は1つとは限らない)」と書かれています。これはPerlコミュニティーの信条です。これは、特定の問題に対して、多種多様なソリューション(機能的に同等な)がある、というこの言語の柔軟性を示しています。

「MakeMaker」でPerlプロジェクトをビルドする場合もまったく同じで、様々な方法が可能です。ここでは、難しくならないようにするため、(「CPAN(Comprehensive Perl Archive Network、この記事の末尾にある参考文献を参照)」にあるたくさんの優れたプロジェクトを実際に扱った経験に基づき、その中から)ベスト・プラクティスと考えられる方法を1つだけ示します。MakeMakerによるプロジェクトのビルドに慣れたなら、今度は自由に独自の開発スタイルを作り上げてみてください。

基本的なPerlプロジェクトは、プロジェクトのディレクトリー内に置かれるいくつかのファイルとディレクトリーによって構成されます。まずそれぞれについて簡単に説明し、Perlアプリケーションの作成を進めながら、その都度詳しく説明していきましょう。

Makefile.PL

Perlプロジェクトの中心は、Makefile.PLファイルです。Makefile.PLは、プロジェクト全体のgnu Makefileを生成するPerlスクリプトです。これは、ExtUtils::MakeMakerライブラリー内にあるWriteMakefile関数の呼び出しによって行われます。簡単なMakefile.PLは、以下のようなものです。

簡単なMakefile.PL
    use ExtUtils::MakeMaker;
    WriteMakefile(
        'NAME'              => 'myproject',
        'VERSION_FROM'      => 'lib/MyModule.pm', # finds $VERSION
    );

この例では、必要な情報としてWriteMakefileに渡されたのは、NAMEVERSIONの2つだけです。NAME(名前)は明示的に指定します。VERSION(バージョン)は、VERSION_FROM変数によって暗黙的に指定します。これにより、プロジェクトのどのモジュールが適切な$VERSION変数を持っているかがMakeMakerに示されます。

Makefileの生成に必要な他の変数はすべて、Perlインタープリターのデフォルトから取得されます。これには、アプリケーションのインストール場所の接頭部を示すPREFIX(通常は/usrまたは/usr/localがデフォルト)、セクション1(ユーザー・コマンド)のmanページのインストール場所を示すMAN1PATH、ライブラリーのインストール場所を示すINSTALLSITELIBなどがあります。変数の完全なリストは、ExtUtils::MakeMakerのmanページか、Perl.com(参考文献を参照)に掲載されているHTMLドキュメンテーションをご覧ください。

MANIFEST(マニフェスト)

その次に重要なPerlプロジェクト・ファイルはマニフェストです。マニフェストは、プロジェクトを構成するすべてのファイルのリストです。Makefile.PLを実行すると、まずマニフェストが検証され、そのプロジェクトに必要なすべてのファイルが揃っているかどうかがチェックされます。ファイルが揃っていない場合、欠けているファイルのリストが生成され、マスターMakefileの生成は行われません。

マニフェストには、整合性検査以外にも用途があります。MakeMakerが提供する他のターゲットの1つにdistがあります。これについては、また後に説明します。make distは、マニフェストを使用してtarファイルを生成します。このファイルを最新の状態に維持することはきわめて重要です。

MakeMakerは、「manifest」という名前のマニフェストを生成するためのターゲットを提供します。make manifestと入力すると、現行ディレクトリーとそのすべてのサブディレクトリーがマニフェストに追加されます。デフォルトでは、すべてのファイルがmanifestのターゲットに含まれますが、これはMANIFEST.SKIPファイルを作成して変更することができます。詳細については、ExtUtils::Manifestのmanページをご覧ください。

libおよびbinディレクトリー

プロジェクト・ファイルの編成方法は、とかく意見が分かれがちな問題です。UNIXには、兄弟関係のディレクトリーであるlibとbinをディレクトリー構造全体にわたり様々なレベルで使用するという伝統があります。これと同じ方法をPerlプロジェクトに応用すると、各ファイルの役割の把握がはるかに容易になることに私は気づきました。これは、複数の開発者が関わる大規模なプロジェクトでは特にそうです。

UNIXでは、libディレクトリーは、複数のアプリケーションに使用される可能性のあるライブラリーを指します。Perlの場合は、一般にパッケージ宣言で始まるPerlモジュール(.pm)ファイルと、Perlライブラリー(.pl)ファイル(一般にパッケージ宣言を持たず、サブルーチン宣言だけで構成される)のことを指します。MakeMakerは、デフォルトではプロジェクトのトップレベル・ディレクトリーとlibディレクトリー内でPerlモジュールを検索しますが、別の構成も可能です。

UNIXにおいてbinディレクトリーは、バイナリーその他の実行可能プログラムの格納場所です。#!/usr/bin/perlのような形で始まるスクリプトはすべて、binディレクトリー内に置きます。「#!」行が別のプラットフォームに対しても有効であるかどうか気にする必要はありません。後で説明しますが、この問題はインストールの際にMakeMakerによって処理されます。

すべてのPerlプログラムに拡張子「.pl」を付ける人が多いようですが、これはプログラムを実行するための必要条件ではありません。MakeMakerは、接尾部の有無に関係なく機能します。このため、私の場合、どちらかと言えばPerlアプリケーションに特別の接尾部を付けることはしません。

テスト・ケース・ディレクトリー「t/」

MakeMakerの最も重要な機能の1つは、自動テスト・ケースです。この機能は、Test::Harnessモジュールによって可能となります。このモジュールは、1つまたは複数のテスト・ケースを含むテスト・スクリプトを生成する明確なインターフェースを規定しています。

Perlプロジェクトのテスト・フェーズにおいて、MakeMakerは、/tサブディレクトリー内の拡張子「.t」を持つすべてのファイルをテスト・スクリプトとして実行します。また、プロジェクトのルート・ディレクトリー内にあるtest.plもテスト・スクリプトとして実行してしまうため、Test::Harnessのインターフェースを使用することを意図する場合以外はそのようなプログラムを誤って作成しないようにしましょう。

テスト・フェーズは、Perlのビルド・プロセスに不可欠なステップであり、すべてのテストが成功しなければ、アプリケーションのインストールができない場合もよくあります。堅固なテスト・スクリプトを作成することは、プロジェクトの品質保証のみならず、他の開発者による異なる環境へのコードの移植をより容易なものとするメリットがあります。単に、特定のハードウェアでの正常動作を示すだけでなく、どのテスト・ケースが実際にうまくいかなかったのか(testabc.tの15~18など)を知らせることもできます。これにより、実際に何が問題であるのか詳しい情報が得られるため、迅速で確実な修正を行うことができます。

テスト・スクリプトおよびテスト・ケースの作成方法については、後ほど詳しく説明します。

POD(Plain Old Documentation)

多くのプロジェクトにとって最大の問題の1つは、実際に実装したコードとそれに対するドキュメンテーションをどのようにして合わせるかということです。コードの更新によって新機能の組み込みや既存動作の変更が行われた場合、その機能に関するドキュメンテーションをすべて変更することは大変な作業であり、多大な時間を要する可能性があります。「manページとHTML」などといった、複数のフォーマットによるドキュメンテーションの維持管理が必要な場合は特にそうです。

Perlが提供するきわめて重要な機能の1つに、組み込みドキュメンテーション・マークアップ言語である「POD」(Plain Old Documentation)があります。PODは、少数のきわめてシンプルなマークアップ・コマンドによって構成されます。PODのマークアップ・テキストは、Perlインタープリターにはコメントとみなされるため、コード全体にPODドキュメンテーションの配置が可能です。Perlのコア・ディストリビューションには、PODからテキスト、HTML、manページ・フォーマットへの変換を行うパーサーが付属しています。CPANには、Latex、PDF、Postscriptに直接変換するためのモジュールもあります。

Perlのビルド・プロセスの間、MakeMakerは、すべてのスクリプトとモジュールにおいてPODドキュメンテーションを検索します。UNIXプラットフォームでは、すべての実行可能ファイルに関するmanページ「セクション1」、各ライブラリーに関するmanページ「セクション3」が生成されます。また、オプションとして、インストール中にHTMLドキュメンテーションを生成することも可能です。PODによってプログラムのドキュメンテーションを作成すれば、プログラムのインストールした後、ユーザーは「man プログラム名」を実行するだけで完全なドキュメンテーションを参照することができます。ユーザーがREADMEファイルを検索するよりもこの方法の方が便利であるのと同時に、ドキュメンテーションのインターフェースも違和感のないものとなります。

PODの詳細については、man perlpodを実行するか、あるいは『Programming Perl, 3rd Edition』、またはPerl.com(参考文献を参照)をご覧ください。

初めてのMakeMakerプロジェクト(make)

このサンプルMakeMakerプロジェクトは、Colin McMillen氏のperl5-portersメーリング・リストへのNet::Pingモジュールに関する投稿にヒントを得て作成したものです。Pingは、マシンがパケットを受け付けるかどうかを確認する非常に便利なツールですが、そのマシン上のWebサーバーが実際に応答しているかどうかなど、それ以上のことを知りたい場合もよくあります。また、ICMP攻撃が日常化し、実際には標準のpingパケットの返信を無効にしているホストが多いため、もはやネットワーク接続状態の表示手段として役立たなくなっています。このプロジェクトでは、プログラム「pingwww」をビルドします。pingwwwは、外観、動作共にpingとよく似ていますが、実際にはHTTP要求を送信し、Webサーバーの動作テストを行うものです。

このプロジェクトには、実際の機能を実行するコア・モジュールのNet::Ping::HTTPと、Net::Ping::HTTPのユーザー・インターフェースを提供するプログラムであるpingwwwが含まれます。

Makefile.PLの作成

それでは、まずMakefile.PLの作成から始めましょう。最初は、最も基本的なエントリーのみです。

Net::Ping::HTTPのMakefile.PL、その1
  use ExtUtils::MakeMaker;
  WriteMakefile(
      'NAME'              => 'Net::Ping::HTTP',
      'VERSION_FROM'      => 'lib/Net/Ping/HTTP.pm', # finds $VERSION
    );

これは、プロジェクト名がNet::Ping::HTTPであることと、lib/Net/Ping/HTTP.pm内にある$VERSION変数がプロジェクトの有効なバージョンであることを指定します。

この場合、プロジェクト内にMakefile.PLの他にもう1つファイルがあるため、ビルド・プロセスが正しく実行されるようにマニフェストを作成します。

マニフェストの先頭部分は、以下のようなものです。

マニフェスト、その1
  MANIFEST
  Makefile.PL
  lib/Net/Ping/HTTP.pm

新しいファイルを追加するごとに、各ファイルに対するエントリーをマニフェスト・ファイルに追加することを忘れないでください。

Net::Ping::HTTP

このモジュールのコードは、libwww-perlパッケージに含まれるLWP::UserAgentに基づくシンプルなレイヤーです。このライブラリーは、各種LinuxおよびUNIXのディストリビューションに標準で付属します。また、ActiveStatePerl for Windowsがインストールされていれば、LWP::UserAgentもすでにインストールされています。一方、使用しているオペレーティング・システムにこのモジュールが標準で付属していない場合は、CPAN(参考文献を参照)からダウンロードすることもできます。

Net::Ping::HTTPによる方法は簡単なものです。HTTPプロトコルはメソッド「HEAD」をサポートしていますが、これはHTTPヘッダーだけを返し、内容は返さないように設計されています。Net::Ping::HTTPは、pingしようとしているWebサーバーのルート・ドキュメントに対するHTTP HEAD要求を送信し、そのWebサーバーによって返信された状況コードを返します。Webサーバーが動作中の場合、この状況コードは200になるはずです(まれに30xまたは40xコードになる場合もあります)。

以下は、このモジュールのPODドキュメンテーションを含めたNet::Ping::HTTPの先頭部分です。

Net::Ping::HTTPライブラリー
  package Net::Ping::HTTP;

  =head1 NAME

  Net::Ping::HTTP - An interface for determining whether an HTTP
  server is listening

  =head1 SYNOPSIS

  my $pinger = new Net::Ping::HTTP;
  my $rc = $pinger->ping('http://localhost');

  my $pinger = new Net::Ping::HTTP(
                                 TIMEOUT => 15,
                                 PROXY => 'http://someproxy.net',
                                );
  my $rc = $pinger->ping('http://www.ibm.com');

  =head1 DESCRIPTION

  Net::Ping::HTTP is a simple interface on top of LWP::UserAgent
  that lets you ping a Web server to see if it is responding to HTTP
  requests.  This would make it suitable for monitoring Web applications.

  =head1 AUTHOR

  Sean Dague <sldague@us.ibm.com>

  =head1 SEE ALSO

  L<perl>, <LWP::UserAgent>

  =cut

  use strict;
  use LWP::UserAgent;
  use HTTP::Request;
  use vars qw($VERSION);

  $VERSION = '0.02';

  sub new {
      my $class = shift;
      my %this = (
                PROXY => undef,
                TIMEOUT => 10,
                @_,
                );

      my $ua = new LWP::UserAgent("Net::Ping::HTTP - $VERSION");
      $this{UA} = $ua;
      $this{UA}->timeout($this{TIMEOUT});
      if($this{PROXY}) {
          $this{UA}->proxy('http',$this{PROXY});
      }

      return bless \%this, $class;
  }

  sub ping {
      my ($this, $url) = @_;

      my $request = new HTTP::Request('HEAD',"$url");
      my $response = $this->{UA}->request($request);

      return $response->code();
  }

  1;

Net::Ping::HTTPにはLWP::UserAgentHTTP::Requestの両方が必要であり、それらがなければ動作しません。モジュールをインストールしても、依存関係のあるモジュールがないためにそこで終わってしまうというのではなく、このモジュールは実行に必要な他のモジュールを特定することができます。

また、これらのモジュールをMakefile.PLの要件として追加することもできます。その場合、マスターMakefileが生成される前にモジュールの有無の検証が行われます。Makefile.PLのこの新たな変数のフォーマットは、以下の通りです。

Makefile.PLの前提条件
  'PREREQ_PM' => {
      Module::Name => minimum.version.number,
      ...
  },

このコードは、私のワークステーションにインストールされているバージョンのLWP::UserAgentおよびHTTP::Requestで問題なく動作するため、このモジュールではそれらのバージョンを前提条件とします。バージョンを確認するには、コマンド・ラインで以下のコマンドを実行します。

モジュールのバージョン確認
  perl -MLWP::UserAgent -e 'print $LWP::UserAgent::VERSION'
  perl -MHTTP::Request -e 'print $HTTP::Request::VERSION'

perlに-Mフラグを指定するとそのモジュールがロードされ、-eフラグを指定すると後続のストリングがperlスクリプトとして実行されます。

情報が収集された後のMakefile.PLは、以下のような形になります。

Makefile.PL、その2
  use ExtUtils::MakeMaker;

  WriteMakefile(
              'NAME'              => 'Net::Ping::HTTP',
              'VERSION_FROM'      => 'lib/Net/Ping/HTTP.pm', # finds $VERSION
              'PREREQ_PM' => {
                              LWP::UserAgent => 1.73,
                              HTTP::Request => 1.27,
              });

pingwww

プロジェクトのコア・ライブラリーはまだ完全ではありません。使用できるようにするには、その機能を使ってユーザー・インターフェースを提供するプログラムが必要です。そのプログラムにpingwwwという名前を付け、プロジェクト・ツリーのbin/ディレクトリー内に置きます。

pingwwwでシミュレートするとよいと思われるpingコマンドの2つの機能は「終了までのサーバーに対するパケット送出回数の指定」および「各要求に対する応答時間の同期表示」です。

それらの機能をプログラムに追加するには、Net::Ping::HTTPに必要とされなかった2つの追加モジュールが必要です。その2つとは、コマンド・ライン・オプションを解析するGetopt::Stdと、マイクロ秒単位の分解能のタイミングを提供するTime::HiResです。Getopt::Stdは、Perlのコア・ディストリビューションの一部であるためPREREQ_PMとして追加する必要はありませんが、Time::HiResは違います(ただし、Perl 5.8では標準ライブラリーの一部となるようです)。したがって、Time::HiResをMakefile.PL内のPREREQ_PM変数に追加します。

以下は、pingwwwの先頭部分です。

pingwwwプログラム
  #!/usr/bin/perl
  #
  #   This program is licensed under the same terms as Perl itself
  #
  #   Sean Dague <sldague@us.ibm.com>

  =head1 NAME

  pingwww - program for "pinging" webservers

  =head1 SYNOPSIS

  pingwww [-c iterations] [-p proxy] [-t timeout] [-h] hostname

  =head1 DESCRIPTION

  Pingwww uses the Net::Ping::HTTP module to send HEAD requests
  to the host specified by hostname.

  =head1 AUTHOR

  Sean Dague <sldague@us.ibm.com>

  =head1 SEE ALSO

  L<Net::Ping::HTTP>, L<Time::HiRes>, L<perl>

  =cut

  use strict;
  use Carp;
  use Getopt::Std;
  use Net::Ping::HTTP;
  use Time::HiRes qw(gettimeofday tv_interval);

  $| = 1;
  my %opts;

  getopt('cpth',\%opts) or croak("Couldn't parse options");

  if($opts{h}) {
      usage();
  }

  my $max = $opts{c} || 1000;

  my %vars = ();

  if($opts{p}) {
      $vars{PROXY} = $opts{p};
  }

  if($opts{t}) {
      $vars{TIMEOUT} = $opts{t};
  }

  my $pinger = new Net::Ping::HTTP(%vars);
  my $host = $ARGV[0];
  my $packedip = gethostbyname($host);
  my $ip = inet_ntoa($packedip);

  print "PING HTTP $host ($ip):\n";

  for(my $i = 0 ; $i < $max; $i++) {
      my $start = [gettimeofday()];
      my $rc = $pinger->ping("http://$host");
      if ($rc < 400) {
          print "Response $rc from $ip: ";
          my $elapsed = tv_interval ( $start );
          print "$elapsed seconds\n";
      } else {
          print "Response $rc from $ip: ";
          print "failed\n";
      }
  }

  sub usage {
      print <<USAGE;
  usage: $0 [-c number of iterations] [-p proxy] [-t timeout] [-h] hostname
    where
      -h : this message
      -c : number of iterations to ping the host.  Defaults to 1000
      -p : http proxy to use for the ping
      -t : timeout value.  Defaults to 10 secs if none is specified

  USAGE

    exit(1);
  }

EXE_FILESにpingwwwが含まれていることから、インストール中にいくつかの興味深い事象が発生します。これについては後で詳しく説明します。また、マニフェスト・ファイルにも新しいファイルに対するエントリーの追加が必要です。新しいマニフェスト・ファイルは、以下の通りです。

マニフェスト、その2
  MANIFEST
  Makefile.PL
  lib/Net/Ping/HTTP.pm
  bin/pingwww

テスト・ケースの作成(make test)

これでコードは完成しましたので、次にテスト・ケースを作成します。スタイルやコードが複雑な場合は、テスト・ケースを最初に作り、その後でそのテストに準拠したコードをビルドするのもよいでしょう。先に述べた通り、アプローチは1つとは限りません。

テスト・スクリプトは、前述の通り「t」ディレクトリーに置きます。テスト・スクリプトは、決められたフォーマットでSTDOUTに出力を行うプログラムです。この出力の先頭行には、ストリング「1..N」が必要です(Nはテスト番号)。そのストリングの後に、テストの合否に応じて「ok」または「not ok」を置きます。

Test.pm

テスト・スクリプトの作成を容易にするために、Perlのコア・ディストリビューションにはTestモジュールがあります。Testモジュールは、テスト・スクリプトの作成を自動化する関数を提供します。

まず、plan関数です。これは、1..N行の生成に使用されます。例えば、テスト・スクリプトに5つのテストがある場合は、スクリプトの先頭に以下のコードを付けます。

Testスクリプトのヘッダー
   use Test;
   BEGIN { plan tests => 5}

なぜこの方が「print "1..5\n"」よりも簡単なのでしょうか。もしplanのすることが、その同じプログラム行を生成するだけであるのならそうはいえないでしょう。しかし、Planはその他にも、後で説明するtodoリストのようないくつかの関数をサポートしています。

次に注目すべき関数はok関数です。これは、数種類の真偽テストに基づいて「ok」または「not ok」の出力を適宜生成します。1つの引数を指定してok()を呼び出すと、その引数がPerlにおいて真と評価された場合は「ok」、偽と評価された場合は「notok」を生成します。引数が2つの場合は、perlの「eq」演算子によってテストした結果、2つの引数が等しければ「ok」を生成します。このテストはASCII対応であるため、1と1.0は数値としては同じであっても等しくないことになります。

また、2つの引数を指定してOk()を呼び出す場合には特殊なケースもあります。2番目の引数は、/pattern/の形のストリングを指定するか、あるいはqr()オペレーターを使用してPerlの正規表現にすることができます。この場合、okは、1番目の引数が2番目の引数によって指定された正規表現を満たしていれば「ok」を生成します。

Testモジュールには、その他にもいくつかの機能があります。特定のテスト・ケースを「todoリスト」に載せることができます。「todoリスト」上のテストは、失敗してもエラーは生成されず、成功した場合、予期しない成功の警告が生成されます。これは通常、そのテストをtodoリストから外して必須テストにすべき時であることを示すものとして役立ちます。todoテストの指定は、plan関数にtodo配列を渡すことによって行います。以下の例では、10のテスト・ケースが設定されますが、最後の3つは「todoリスト」にあるため、テスト・スクリプト全体が成功しても「ok」は返されません。

todoリスト付きのテスト・スクリプトのヘッダー
   use Test;
   BEGIN { plan tests => 10, todo => [8,9,10] }

Testモジュールが提供するもう1つの関数はskip()関数です。テスト・ケースは、あらゆるプラットフォーム/構成に当てはまるとは限らず、当てはまる場合にのみ実行する場合もあります。skip()関数は、3つの引数を受け取ります。1番目の引数が真であれば、テストはスキップされます。偽であれば、前述の場合と同様に2番目および3番目の引数がokに渡されます。

Net::Ping::HTTPのテスト

Net::Ping::HTTPに含まれる関数はそれほど多くないため、テストはかなり簡単に短時間で行われるはずです。私はこのモジュールで7つのテストを作成しますが、Perlインタープリターのバージョン5.6.1には、1万2,967のテスト・ケースがあり、インタープリターのコンパイルごとにそれらが実行されます。

どのモジュールにおいても、モジュールが完全にロードされたかどうかのテストが最初に行われます。その後の一連のテストの実行するかどうかは、この最初のテストの成否によって決まります。失敗した場合、他のテスト・ケースを実行する必要はないため、croak()Carpモジュールによって提供されるdieの改良版)を呼び出します。Net::Ping::HTTPの場合、この呼び出しは以下のようにして行うことができます。

簡単なロード・テスト・ケース
   eval { require Net::Ping::HTTP; return 1;};
   ok($@,'');
   croak() if $@;  # If Net::Ping::HTTP didn't load... bail hard now

テスト・ケースの作成を重ねるにつれて、evalブロックが様々なテストにとても役に立つことが分かるはずです。evalを使用すると、$@が設定されているかどうかをテストによって確認するだけで、その成否を知ることができます。この場合、requireが失敗すれば$@が設定されていたことになり、したがってテストは失敗となります。

簡単なロード・テストの後、オブジェクトをテストしてそのインスタンスが正しく作成されたかどうかを確認します。これは、このケースでは行き過ぎかもしれませんが、より複雑なモジュールの場合は必ず行った方がよいでしょう。以下は、オブジェクトが適格であることを確認するための一連のテストです。

okテスト・ケース
  my $pinger = new Net::Ping::HTTP();

  ok($pinger->{TIMEOUT},10);
  ok(ref($pinger->{UA}),"LWP::UserAgent");
  ok($pinger->{TIMEOUT},$pinger->{UA}->timeout());

これは、TIMEOUTが指定されたデフォルトに正しく設定されているかどうか、LWP::UserAgentのインスタンスが正しく作成されたかどうか、タイムアウト値が正しいオブジェクトに伝搬されたかどうかをテストします。

このコードをUNIXマシンにインストールする場合、そのUNIXマシンでapacheが実行されている場合がきわめて多くあります。もしそうであれば、まず間違いなくポート80で実行されています。このことから、apacheがプロセス・リストに含まれている場合、localhostに対してpingを試みるスキップ・テストが可能になります。以下は、そのタスクを行うコードです。

スキップ・テスト・ケース
  my $count = 0;

  # eval this code in case ps is not a happy camper on this platform
  eval {
    my @process = `ps -ef`;
    $count = grep /httpd/, @process;
    my $rc = $pinger->ping("http://localhost");
    if ($rc >= 500) {
        croak();
    }
  };

  skip(!$count,$@,'');

まず、'ps -ef'を実行してプロセス・リストを取得します。psが有効なコマンドではないマシンでは、空の配列が返されるはずです。次に、プロセス・リストを照会してその中に含まれるhttpdプロセス数を取得します。その後、pingerがポート80のlocalhostに対してpingを実行し、異常な状況コードが返された場合、そのプロセスは停止していることになります。プロセス・リストにhttpdプロセスが含まれていない場合、そのテスト結果は無視されます。

httpdが別のポートで実行されている可能性があることや、localhostがローカル・インターフェース以外のものに解決されている可能性があることなど、このテスト・ケースにはいくつかの問題がありますが、紙面の都合上、あえて無視します。プログラムに必要な堅牢性のレベルによっては、これらの可能性を考慮に入れてテストを作成する必要があるかもしれません。

最後に、インターネット上の実際のサイトに対してpingを行う2つのテストを追加します。これらのサイトには、可用性の点から、www.ibm.comとwww.yahoo.comを選びました。プロキシが指定されていない場合、これらのテストを行うにはそれらのサイトへのネットワーク接続が必要ですが、すべてのマシンにそうした環境があるとは限りません。このため、BEGINブロックを変更して、これらのテストをtodoリストに載せることにします。うまくいけば、それはそれで素晴らしいことですが、うまくいかなくても問題はありません。新しいBEGINブロックは、以下のようになります。

最終的なテスト・スクリプトのヘッダー
BEGIN {plan tests => 7 , todo => [6,7]};

これで、Net::Ping::HTTPのテスト・スクリプトが完成しました。このスクリプトは、4つの一般的なテストと1つのプラットフォーム固有のテストを含み、そのうちの2つはオプションであるためtodoとしました。

このテスト・スクリプトを追加した後のマニフェストは、以下のようになります。

MANIFEST take 3
  MANIFEST
  Makefile.PL
  lib/Net/Ping/HTTP.pm
  bin/pingwww
  t/pingpm.t

バイナリーのテストについて

バイナリーのテストは、モジュールのテストよりも少し厄介です。それは、system()呼び出しによってバイナリーを実行し、単に終了コードをテストするか、あるいはバイナリーにバックティックを付けて実行してから、そのテキスト出力を解析する必要があるためです。いずれの場合も、コードが汚くなる恐れがあります。こうしたこともあって、私の場合、できる限り多くのコア関数をモジュールに組み込み、スクリプトはそれらのモジュールに対するラッパー程度にとどめるというパラダイムを好んで用いています。

以上の理由から、ここではpingwwwプログラムのテスト・ハーネスのビルドは行いません。

インストール(make install)

これで、プロジェクトをビルドできる状態になりました。次に、標準的なビルド・プロセスについて順を追って見ていきながら、実行する各ステップについて考察します。

perl Makefile.PL

最初のステップは、perl Makefile.PLの実行です。その結果、プロジェクトのMakefileが生成されます。私のワークステーションでは、perl 5.6.1が/usr/bin/perlにインストールされています。このMakefile.PLではPREFIXを指定しなかったため、この変数はperlインタープリターによって取得されることになります。この場合、PREFIXは/usrに設定されます。したがって、pingwwwプログラムは、/usr/bin/pingwwwとしてインストールされ、Net::Ping::HTTPモジュールは、/usr/lib/perl5/site_perl/5.6.1/Net/Ping/HTTP.pmとしてインストールされます。

PREFIX変数は、他の多くのMakeMaker変数と同様に、Makefile.PLやコマンド・ラインでオーバーライドすることができます。そうした変数割り当てを行う場合のコマンド・ライン構文は、「perl Makefile.PL VARIABLE=VALUE」です。例えば、pingwwwを/usr/local/bin/pingwwwとしてインストールしたい場合は、以下のようにしてPREFIXをオーバーライドすることが可能です。

Makefile.PLに渡すコマンド・ライン引数

perl Makefile.PL PREFIX=/usr/local

この場合、バイナリーは/usr/local/binに、モジュールは/usr/local/lib/perl5/site_perl/5.6.1にそれぞれ置かれることになります。/usr/local/binは通常、どのような環境でもパスに含まれていますが、/usr/local/lib/perl5/site_perl/5.6.1は、環境によってはPerlのインクルード・パスに含まれていない可能性があるため(実際にperlが/usr/local/binにインストールされていない限り)、みだりにPREFIXを変更することはお勧めしません。

perl Makefile.PLの出力は、以下のような形になります。

perl Makefile.PLの出力
  rigel:~/NetPingHTTP> perl Makefile.PL
  Checking if your kit is complete...
  Looks good
  Writing Makefile for Net::Ping::HTTP

MakeMakerは、マニフェストに必須として記載されているすべてのファイルが存在することと、PREREQ_PMにリストされているすべてのモジュールが存在することを確認しました。

make

次は、makeの実行です。これにより、すべてのperlバイナリーおよびライブラリーが一時blibディレクトリー内にビルドされ、そこからテスト/インストールが行われます。

時間を惜しむことなく、バイナリーやモジュール内にPODドキュメンテーションを作成すれば、ドキュメンテーション自動生成のメリットがあなたのものとなります。UNIXマシンでは、makeプロセスを実行すると、PODでマークアップされたすべてのバイナリーおよびモジュールのmanページが自動的に生成されます。前述の通り、ドキュメンテーションはコードに共に組み込まれ、すべてのドキュメンテーションのメインテナンスを1つのファイルのみで行うことができます。

perl 5.6.0から、MakeMakerは、モジュールに対するHTMLドキュメントの生成も可能となっています。perl Makefile.PLコマンド・ラインかMakefile.PLでINSTALLHTMLSITELIBDIRを指定すれば、インストールの際に、HTMLドキュメントがそのディレクトリーにインストールされます。ここでは、HTMLドキュメンテーションを生成するため、以下のようにperl Makefile.PLを再実行します。

HTMLドキュメンテーションの生成
perl Makefile.PL INSTALLHTMLSITELIBDIR=~/public_html

これで、私のWebディレクトリーにHTMLドキュメントが生成されます。

ビルドに伴うMakeMakerの便利な点は、その他にもあります。それは、すべてのperlバイナリーに含まれる「#!」(shebang)行を環境に合わせて修正できることです。これは、このコードがインストールされる環境によって、perlが/opt/fromsource/bin/perlや他の意外な場所に置かれ、それが原因でプログラムが動作しないという懸念を解消します。

makeを実行すると、以下の出力が生成されます。

makeの出力
  rigel:~/NetPingHTTP> make
  cp lib/Net/Ping/HTTP.pm blib/lib/Net/Ping/HTTP.pm
  cp bin/pingwww blib/script/pingwww
   /usr/bin/perl -I/usr/lib/perl5/5.6.1/i686-linux -I/usr/lib/perl5/5.6.1
          -MExtUtils::MakeMaker -e "MY->fixin(shift)" blib/script/pingwww
  Htmlifying blib/html/lib/lib/Net/Ping/HTTP.html
  /usr/bin/pod2html: lib/Net/Ping/HTTP.pm: cannot resolve L<perl> in paragraph 13. at
          /usr/lib/perl5/5.6.1/Pod/Html.pm line 1562.
  Manifying blib/man3/Net::Ping::HTTP.3
  Manifying blib/man1/pingwww.1

makeの実行中に、1つの警告が生成されました。これは、PODドキュメンテーションの中で、LE<lt>E<gt>構文を使用して外部のエンティティーの参照を指定したためです。Pod::HTMLには、任意の外部リンクの生成に関していくつかの問題点があるため、この段階では何度も警告を目にするでしょう。一般に、pod2htmlによって生成されるエラーはいずれもプロジェクトに対する影響は少ないため、気にする必要はありません。

make test

次のステップは、プロジェクトのテスト・フェーズの実行です。出力は、以下のようなものになるはずです。

make testの出力
   rigel:~/NetPingHTTP> make test
   PERL_DL_NONLAZY=1 /usr/bin/perl -Iblib/arch -Iblib/lib
          -I/usr/lib/perl5/5.6.1/i686-linux -I/usr/lib/perl5/5.6.1 -e
          'use Test::Harness qw(&runtests $verbose); $verbose=0;
          runtests @ARGV;' t/*.t
   t/pingpm............ok, 1/7 skipped: unknown reason
   All tests successful, 1 subtest skipped.
   Files=1, Tests=7, 20 wallclock secs ( 0.36 cusr +  0.03 csys =  0.39 CPU)

私のマシンでこのテストを行った際にはapacheを実行していなかったため、テスト番号5はスキップされました。

ご覧の通り、Test::Harnessは、テスト・プロセスにおいてタイミング・コードの実行も行い、テスト開始から終了までの経過時間「wallclock secs」とCPUでの合計処理時間「CPU時間」の両方を記録します。これらのテストのCPU時間が1秒未満であるのに対して、実際の実行時間が20秒であるのは、私の使用した環境ではワークステーションとwww.ibm.com、www.yahoo.comの間にファイアウォールがあるためです。LWP::UserAgentは、Net::Ping::HTTPによって10秒のタイムアウトでインスタンス化されるため、2回のタイムアウトがあったことになります。テスト6と7は、todoテストとして指定したため、これらの失敗が原因で生成されたエラーはありませんでした。

試しに、テスト6と7をtodoテストとして指定せずにmake testを実行してみましょう。この場合、私の環境では失敗となります。初めて実行するテスト・ケースでは、成功よりも失敗の方が多いため、そういった失敗の出力をどう読み取るべきか学んでおきましょう。

make testが失敗した場合の出力
   rigel:~/NetPingHTTP> make test
   PERL_DL_NONLAZY=1 /usr/bin/perl -Iblib/arch -Iblib/lib
          -I/usr/lib/perl5/5.6.1/i686-linux -I/usr/lib/perl5/5.6.1 -e
          'use Test::Harness qw(&runtests $verbose); $verbose=0;
          runtests @ARGV;' t/*.t
   t/pingpm............FAILED tests 6-7
           Failed 2/7 tests, 71.43% okay (-1 skipped test: 4 okay, 57.14%)
   Failed Test Status Wstat Total Fail  Failed  List of Failed
   --------------------------------------------------------------------------------
   t/pingpm.t                     7    2  28.57%  6-7
   1 subtest skipped.
   Failed 1/1 test scripts, 0.00% okay. 2/7 subtests failed, 71.43% okay.
   make: *** [test_dynamic] Error 29

出力はやや繁雑ですが、意味は明らかです。pingpmテスト・スクリプトは、テスト6と7で失敗しました。このことは、出力中の数箇所かで示されています。結果には、成功したテスト数と失敗したテスト数の統計的分析も示されています。複数のテスト・スクリプトを作成した場合、最後の「Failed」行を見れば、完全には成功しなかったテスト・スクリプトの数や失敗したテスト・ケースの総数が分かります。

make install

プロジェクト・ビルドの最後は、make installの実行です。特権のあるディレクトリー(/usrなど)にインストールする場合は、rootでインストールを行う必要があります。この時点で、すべてのバイナリー、モジュール、manページ、およびWebページがインストールされます。

make installの出力
   [root@rigel]# make install
   Installing /home/sdague/public_html/lib/Net/Ping/HTTP.html
   Installing /usr/man/man1/pingwww.1
   Installing /usr/man/man3/Net::Ping::HTTP.3
   Installing /usr/bin/pingwww
   Writing /usr/lib/perl5/site_perl/5.6.1/i686-linux/auto/Net/Ping/HTTP/.packlist
   Appending installation info to /usr/lib/perl5/5.6.1/i686-linux/perllocal.pod

これで、コードがインストールされ、ビルド・プロセスは完了しました。

コードの配布(make dist)

これから示す手順によって、他のユーザーでも簡単にインストールできる素晴らしいプログラムが完成します。コードの配布を行う場合のために、MakeMakerにはmake distコマンドが用意されています。このコマンドは、プロジェクト(マニフェストにリストされているすべてのファイル)のtar.gzディストリビューションを、ファイル名「プロジェクト名-バージョン.tar.gz」で生成します。これにより、このファイルは簡単に配布用ファイルとなります。

しかしその前に、いくつかのドキュメントを用意する必要があります。これらのドキュメントは、通常、perlコードと一緒に配布されるドキュメントです。

README

このファイルには、プロジェクトの内容や機能の概要を簡単に記載します。

COPYING(またはLICENSE)

このファイルには、ファイルのライセンス情報やユーザーによるコードのコピー、変更、再配布に対する制限を記載します。perlモジュールの多くは、「Licensed under the same terms as Perl itself」(Perl本体と同じ条件でライセンスされる)となっています。これはつまり、GNU Public LicenseとArtistic Licenseという2つのライセンスによって提供されるということです。しかし、作成者がそのコードにふさわしいと考えるライセンスを選ぶこともできます。

Changes

これは、ファイルの各リリースで追加された内容の変更ログです。このファイルは、どのような機能が追加され、いつメイン・ディストリビューションに組み込まれたのかユーザーがすぐに理解できるように、常に最新の状態に維持しておきましょう。

TODO

これは、今後のバグ・フィックス機能追加の予定リストです。ディストリビューションには、このようなファイルを添付することを常に心掛けましょう。そうすることで、プロジェクトの方向性をユーザーに示すことができます。もしかしたら、TODOリストに記載されている機能を自分で作って送り返してくれるような凄いユーザーもいるかもしれません。

繰り返しになりますが、ファイルを追加した場合には、マニフェストに必ずそのファイルを追加してください。それを行わなければ、make distの実行時にバンドルされません。

他のユーザーにも役に立つ再利用可能なコードができた場合には、CPAN(参考文献を参照)への提出も検討すべきでしょう。ここに示した方法を用いることによって、CPANモジュール・フォーマット準拠に近いコードの作成が可能となります。

まとめ

この記事で示した事柄は、このインフラストラクチャーのごく一部にすぎません。このインフラストラクチャーを使えば、純粋なPerlプロジェクトのみにとどまらず、C/Perlの合同プロジェクトも可能です。このインフラストラクチャーはPerlに組み込まれているため、移植性に優れ、LinuxからWindows、S/390に至るまで、様々なプラットフォームで使用できます。いったん慣れてしまえば、このインフラストラクチャーがあらゆるプロジェクトに威力を発揮することがお分かりいただけるでしょう。インストール・スクリプトを何度も作成する必要はなくなり、また、優れたテスト・ケースによって、期待通りのプログラムであるという自信をはるかに高めることができます。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux, Open source
ArticleID=226815
ArticleTitle=MakeMakerによるPerlプロジェクトのビルド
publish-date=11012001