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

ククログ


Rabbit 0.5.7リリース

昨日、 RDるびまの記事 )でスラ イドが書けるプレゼンテーションツール Rabbit がリリースされました。

サンプル

Rabbitではスライドを画像 HTML(+画像) PDF一覧表示 )などで出力することができます。

発表した後に資料を公開する場合や配布資料を作成する場合に利用 するとよいでしょう。

対象ユーザ

RDで書けることがウリというくらいなので、テキストエディタでス ライドを作成したい人が対象になります。おそらく、そのような人 はプログラマであることが多いと思うので、Rabbitはプログラマ向 けのプレゼンテーションツールといえるかもしれません。

スライドをテキストで作成すると以下のような利点があります。

  • バージョンコントロールシステムとの親和性が高い (diffの表示など)
  • 使い慣れたテキストエディタで編集できるため、編集作業の効 率がよい
  • 単なるテキストなので、専用のスライド表示ソフトウェアを用 いなくても内容を確認できる

一方、GUIの編集インターフェイスを備えたプレゼンテーションツー ル(wikipedia:PowerPointwikipedia:Keynoteなど)と比較すると、以下のような欠 点があります。

  • 見た目を微調整しずらい
  • 簡単な図を挿入することが面倒
    • 画像作成ソフトを起動して図を作成し、スライドに挿入

RabbitはRDで書かれたテキストだけではなく、PDFを入力としても 受け付けます。つまり、PDFビューアにもなります。

そこで、上記のような編集時の欠点を解決するために、別途PDF出 力ができるソフトウェアでスライドを作成し、Rabbitで表示すると いうことができます。RabbitをPDFビューアとして使うことにより、 Rabbitのユニークで実用的なユーザインターフェイスを使うことが できます。Rabbitの使い勝手に興味がある場合はこの方法を試して みるとよいかもしれません。

Rabbitのユーザインターフェイスに関してはまた別の機会にしてお きます。

まとめ

Rabbitの外面だけを紹介しました。難易度が高いと言われているイ ンストール方法や特徴的なユーザインターフェイスなどについては 触れませんでした。

タグ: Ruby
2008-08-01

ActiveScaffoldの地域化

現在、Railsに対応した国際化の仕組みがいくつかあります。しかし、それぞれが 独自の方法で実現しているため、それらを組み合わせて使うと混沌 とした状態に陥ることも少なくありません。

ここでは、モデルから動的にきれいな画面とコントローラ部分を生 成するActiveScaffoldを用 いた場合の国際化(i18n)と地域化(l10n)の実現方法のひとつを 紹介します。この方法では、 ActiveScaffoldLocalizeRuby-GetText-Package を組み合わせます。混沌とする部分はそれなりになじませます。

国際化の仕組み

Railsで使用できる国際化の仕組みの比較はRails Wiki (英語)が詳しいです。

Ruby-GetText-Package には、以下のような地域化対象のメンテナン スのことを考慮した機能があるので、地域化対象メッセージが増加 したり更新される場合には有力な候補になるでしょう。

  • 地域化対象のメッセージを抽出する機能
    • テーブルにカラムを追加した場合、画面に表示するメッセージを追加・更新した場合などに利用
  • 抽出したメッセージを既存の翻訳済みメッセージにマージする機能
    • 翻訳者が新しい地域化対象のメッセージを翻訳する場合に利用
  • wikipedia:gettext用の翻訳支援ツールを利用可能(.po のフォーマットがGNU gettextと互換性があるため)
    • Emacs用のpo-modeや.po専用のエディタ

Railsやプラグインなどが提供しているメッセージだけを地域化した いなど、地域対象メッセージが変化しない場合はその他の仕組みも 有力な候補になるでしょう。例えば、ActiveScaffold用の ActiveScaffoldLocalizeがその場合です。

ActiveScaffoldLocalize

ActiveScaffoldは、国際化の仕組みとしてObject#as_を提供してい ます。その仕組みを利用して国際化・地域化を実現しているのが ActiveScaffoldLocalizeです。

ActiveScaffoldLocalizeには日本語用のメッセージも含まれている ので、以下のようにすればActiveScaffoldのメッセージを日本語に することができます。

% rails shelf
% cd shelf
% script/generate resource book title:string
% rake db:migrate
% script/plugin install git://github.com/activescaffold/active_scaffold.git
% script/plugin install git://github.com/edwinmoss/active_scaffold_localize.git

config/routes.rb:

1
2
-  map.resources :books
+  map.resources :books, :active_scaffold => true

app/controllers/books_controller.rb:

1
2
3
class BooksController < ApplicationController
  active_scaffold :book
end

app/views/layouts/application.html.erb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!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>ActiveScaffold l10n</title>
    <%= javascript_include_tag(:defaults) %>
    <%= active_scaffold_includes %>
  </head>

  <body>
    <h1>ActiveScaffold l10n</h1>
    <%= yield %>
  </body>
</html>

app/controllers/applications.rb:

1
2
3
4
5
6
7
8
9
class ApplicationController < ActionController::Base
  # ...
  private
  before_filter :localize_active_scaffold
  def localize_active_scaffold
    ActiveScaffold::Localization.lang = "ja-jp"
    true
  end
end

サーバを起動してhttp://localhost:3000/books/にアクセスします。

% script/server
% firefox http://localhost:3000/books/

ActiveScaffold + ActiveScaffoldLocalize

見ての通り、「検索」などのメニューは日本語になりますが、テー ブル名からきている「Books」やカラム名の「Title」などは日本語 になりません。

ActiveScaffoldLocalizeの方針では、これらを日本語にするために以下のような内容の config/initializers/lang/ja-jp.rb*1を作成します。

config/initializers/lang/ja-jp.rb:

1
2
3
4
5
6
7
# -*- coding: utf-8 -*-

ActiveScaffold::Localization.define('ja-jp') do |lang|
  lang["Books"] = "本一覧"
  lang["Book"] = ""
  lang["Title"] = "タイトル"
end

config/initializers/以下を変更したので、サーバを再起動してか ら再度アクセスすると、日本語で表示されます。

ActiveScaffold + ActiveScaffoldLocalize + モデルの地域化

(「本を作成」ではなく「本一覧を作成」になっているのはこ のパッチ で直ります。)

ActiveScaffoldLocalizeのこのやり方は手軽ですが、地域化対象の メッセージが変更になった場合(例: 「Title」から「Name」に変更) や、地域化対象のメッセージをtypoした場合(例: 「Title」ではな く「title」としていた)に気づきにくいという問題があります。 このような問題に対してはRuby-GetText-Packageが有効です。

ということで、ActiveScaffoldのメッセージは ActiveScaffoldLocalizeで地域化し、それ以外は Ruby-GetText-Packageで地域化するようにします。

Ruby-GetText-Package

ActiveScaffoldLocalizeとRuby-GetText-Packageのすみわけは上述 の通りですが、エラーメッセージの地域化はRuby-GetText-Package ではなく、ActiveScaffldLocalizeに任せます。これは、 ActiveScaffoldがエラーメッセージ部分を上書きしているため、 Ruby-GetText-Packageが提供するエラーメッセージ国際化処理とな じまないためです。

また、Ruby-GetText-Packageが取得したロケール情報を使って ActiveScaffoldLocalizeのlangを設定していることもコツのひとつ です。

config/environment.rb:

1
2
3
4
5
6
# ...
Rails::Initializer.run do |config|
  # ...
  config.gem "gettext", :lib => "gettext/rails"
  # ...
end

lib/active_scaffold_gettext.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
module ActiveScaffoldGetText
  include GetText::Rails

  bindtextdomain(GETTEXT_DOMAIN)
end

class Object
  def as__with_gettext(message, *args)
    return nil if message.nil?
    localized_message = ActiveScaffoldGetText.send(:sgettext, message)
    if localized_message == message
      as__without_gettext(message, *args)
    else
      localized_message % args
    end
  end
  alias_method_chain :as_, :gettext
end

module ActiveScaffold::DataStructures
  class Column
    def initialize_with_gettext(name, active_record_class)
      initialize_without_gettext(name, active_record_class)
      self.label = "#{active_record_class.name.demodulize}|#{@label.humanize}"
    end
    alias_method_chain :initialize, :gettext
  end
end

config/initializers/gettext.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
GETTEXT_DOMAIN = "your-rails-application"
require 'active_scaffold_gettext'

class ActiveRecord::Errors
  # restore default error messages overridden by Ruby-GetText-Package.
  @@default_error_messages = {
    :inclusion => "is not included in the list",
    :exclusion => "is reserved",
    :invalid => "is invalid",
    :confirmation => "doesn't match confirmation",
    :accepted  => "must be accepted",
    :empty => "can't be empty",
    :blank => "can't be blank",
    :too_long => "is too long (maximum is %d characters)",
    :too_short => "is too short (minimum is %d characters)",
    :wrong_length => "is the wrong length (should be %d characters)",
    :taken => "has already been taken",
    :not_a_number => "is not a number",
    :greater_than => "must be greater than %d",
    :greater_than_or_equal_to => "must be greater than or equal to %d",
    :equal_to => "must be equal to %d",
    :less_than => "must be less than %d",
    :less_than_or_equal_to => "must be less than or equal to %d",
    :odd => "must be odd",
    :even => "must be even"
  }

  alias_method :on, :on_without_gettext
  alias_method :[], :on
end

lib/tasks/gettext.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
namespace :gettext do
  namespace :po do
    desc "Update pot/po files."
    task :update => :environment do
      require 'gettext/utils'

      module GetText::ActiveRecordParser
        class << self
          alias_method :add_target_original, :add_target
          def add_target(targets, file, msgid)
            if /\|/ !~ msgid
              add_target_original(targets, file, msgid.classify)
              add_target_original(targets, file, msgid.classify.pluralize)
            end
            add_target_original(targets, file, msgid)
          end
        end
      end

      targets = Dir.glob("{app,config,components,lib}/**/*.{rb,erb,rjs}")
      GetText.update_pofiles(GETTEXT_DOMAIN, targets, "#{GETTEXT_DOMAIN} 0.0.1")
    end
  end

  namespace :mo do
    desc "Create mo-files"
    task :create do
      require 'gettext/utils'
      GetText.create_mofiles(true, "po", "locale")
    end
  end
end

app/controllers/application.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
class ApplicationController < ActionController::Base
  init_gettext GETTEXT_DOMAIN
  # ...
  private
  before_filter :localize_active_scaffold
  def localize_active_scaffold
    posix_locale = GetText.locale.to_posix
    posix_locale = "#{posix_locale}-#{posix_locale}" if /_/ !~ posix_locale
    lang = posix_locale.gsub(/_/, '-').downcase
    ActiveScaffold::Localization.lang = lang
    true
  end
end

翻訳メッセージのファイルpoを作って翻訳します。

% rake gettext:po:update
% mkdir po/ja
% msginit -i po/your-rails-application.pot -o po/ja/your-rails-application.po -l ja_JP
# 途中でメールアドレスを聞かれるので入力する

po/ja/your-rails-application.po:

# ...
#: app/models/book.rb:-
msgid "Book"
msgstr "本"

#: app/models/book.rb:-
msgid "Books"
msgstr "本一覧"
# ...
#: app/models/book.rb:-
msgid "Book|Title"
msgstr "タイトル"
# ...

翻訳メッセージをmoにコンパイルしてアクセスするとテーブル名や カラム名などが日本語になります。

% rake gettext:mo:create

config/initializers/以下を変更したので、サーバを再起動してか ら再度アクセスすると、日本語で表示されます。

ActiveScaffold + Ruby-GetText-Package

まとめ

ActiveScaffoldLocalizeとRuby-GetText-Packageを使って、 ActiveScaffoldを用いたアプリケーションの国際化・地域化を実現する方法 のひとつを紹介しました。

基本的に複数の国際化のしくみを同時に使うと問題が起きますが、 今回は以下のようにそれぞれの長所を活かすようにすみわけて、問 題を回避しています。

  • ActiveScaffoldが利用する固定のメッセージは ActiveScaffoldLocalizeで地域化
  • モデル関連や追加・更新が行われるメッセージについては Ruby-GetText-Packageで地域化

*1 config/initializers/lang/以下にファイルを作るというのはActiveScaffoldLocalizeの方針ではありません。ファイルの場所は特に方針はないようです。

タグ: Ruby
2008-08-12

Cutter導入事例: Senna (2)

ちょうど1ヶ月前の話の続きです。

前回でCutterでテストを作成するための環境ができたので、実際にテストを作成していきます。と、思ったのですが、もう一点やらなければいけないことが残っていました。テスト対象のライブラリの初期化についてです。

今回はテスト対象ライブラリの初期化について説明してからテスト作成に入ります。

前回同様、コードの断片がでてきます。完全なものはSennaのリポジトリを見てください。

ライブラリの初期化

Sennaのようにライブラリ初期化・終了関数 (sen_init()/sen_fin())を用意している場合は、テストの作成に 入る前に、もう一つ用意しておかなければいけない仕組みがありま す。このような関数を持っているライブラリをテストする場合は、 テスト全体を実行する前に初期化関数を、実行した後に終了関数を 呼び出す必要があります。これを行う仕組みを用意する必要があり ます。

cutterコマンドは指定されたディレクトリ以下の共有ライブラリを かき集めて、その中からテストを検出して実行します。その時に以 下の条件にあう共有ライブラリを見つけると、テスト全体を実行す る前後に特定の関数を実行することができます。これは、今まさに 必要としている機能です。

  • ファイル名が「suite_」からはじまっている

この共有ライブラリが以下の名前のシンボルを公開している場合は、 その関数をテスト全体を実行する前後に実行します。ここでは、共 有ライブラリのファイル名はsuite_senna_test.soとします。

  • senna_test_warmup(): テスト全体を実行する前に実行
  • senna_test_cooldown(): テスト全体を実行した後に実行

「_warmup」と「_cooldown」の前の「senna_test」の部分は共有ラ イブラリのファイル名から先頭の「suite_」と拡張子を除いた部分 です。

Sennaの場合は以下のようなsuite-senna-test.cを作成します。

test/unit/suite-senna-test.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <senna.h>

void senna_test_warmup(void);
void senna_test_cooldown(void);

void
senna_test_warmup(void)
{
  sen_init();
}

void
senna_test_cooldown(void)
{
  sen_fin();
}

suite-senna-test.cをビルドするためにMakefile.amに以下を追加 します。

test/unit/Makefile.am:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if WITH_CUTTER
...

noinst_LTLIBRARIES =                                \
        suite_senna_test.la
endif

INCLUDES =                        \
        -I$(srcdir)                \
        -I$(top_srcdir)                \
        -I$(top_srcdir)/lib        \
        $(SENNA_INCLUDEDIR)

AM_CFLAGS = $(GCUTTER_CFLAGS)
AM_LDFLAGS = -module -rpath $(libdir) -avoid-version

LIBS =                                                \
        $(top_builddir)/lib/libsenna.la                \
        $(GCUTTER_LIBS)

suite_senna_test_la_SOURCES = suite-senna-test.c

よくあるMakefile.amの書き方です。noinst_LTLIBRARIESをif WITH_CUTTER ... endifの中に入れているのは、Cutterがない環境で はビルド対象からはずし、ビルドエラーにならないようにするため です。

これで、test/unit/.libs/suite_senna_test.soがビルドされるよう になり、テスト全体を実行する前後にSennaの初期化・終了処理を行 うことができます。

テストの作成

テスト実行環境が整ったのでテストを作成します。ここでは検索キー ワードの周辺テキストを取得するsnippet APIのテスト を1つ作成します。

テストの流れは以下の通りです。

  1. sen_snip_open()でsen_snipオブジェクトの生成
  2. sen_snip_add_cond()でキーワードを指定
  3. sen_snip_exec()でsnippetを生成
  4. sen_snip_get_result()で取得した結果が期待していたものか を検証
  5. sen_snip_close()で生成したsen_snipオブジェクトを開放

基本的なCutterのテスト作成方法についてはチュートリアル を参考にしてください。

sen_snip_open()のテスト

まずは、sen_snip_open()でsen_snipオブジェクトを生成する部分 と、sen_snip_close()で生成したsen_snipオブジェクトを開放する 部分を作成します。

今回はGLibサポート付きでCutterを使用するgcutterパッケージを 使っているので、テストは以下のようにgcutter.hを利用します。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <senna.h>
#include <gcutter.h>

void test_simple_exec(void);

static const gchar default_open_tag[] = "[[";
static const gchar default_close_tag[] = "]]";

void
test_simple_exec(void)
{
  sen_snip *snip;

  snip = sen_snip_open(sen_enc_default, 0, 100, 10,
                       default_open_tag, strlen(default_open_tag),
                       default_close_tag, strlen(default_close_tag),
                       NULL);
  cut_assert_not_null(snip);
  sen_snip_close(snip);
}

sen_snip_open()は引数が多いですが、ここでは気にする必要はあ りません。sen_snip_open()によりsen_snip *が生成されることだけ知っ ていれば問題ありません。

cut_assert_not_null(snip) でsen_snip *が正常に生成されているかを確認します。これは、 sen_snip_open()は失敗時にはNULLを返すからです。

最後にsen_snip_close()で生成したsen_snip *を開放します。

sen_snip_add_cond()のテスト

次はsen_snip_add_cond()でキーワードを指定する処理を追加しま す。sen_snip_add_cond()の戻り値はsen_rcです。sen_rcはエラー 番号を示す数値でsen_success(0)以外はエラーになります。よって テストは以下のようになります。sen_snip_open()のときと同じく、 sen_snip_add_cond()の引数は気にしなくても構いません。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
void
test_simple_exec(void)
{
  sen_snip *snip;
  const gchar keyword[] = "Senna";

  snip = sen_snip_open(sen_enc_default, 0, 100, 10,
                       default_open_tag, strlen(default_open_tag),
                       default_close_tag, strlen(default_close_tag),
                       NULL);
  cut_assert_not_null(snip);

  cut_assert_equal_int(sen_success,
                       sen_snip_add_cond(snip, keyword, strlen(keyword),
                                         NULL, 0, NULL, 0));

  sen_snip_close(snip);
}

sen_snip_add_cond()の結果は cut_assert_equal_int で検証しています。

ただし、ここで問題があります。cut_assert*()は検証が失敗する とその時点でテスト関数からreturnし、それ以降のコードは実行し ません。つまり、cut_assert_equal_int()が失敗した場合は、 sen_snip_open()で生成したsen_snip *が開放されないことになり ます。この問題を解決するためにsetup()/teardown()という仕組み があります。

setup()はテストが実行される前に必ず実行され、teardown()はテ ストが実行された後に成功・失敗に関わらず必ず実行されます。こ の仕組みを利用することで確実にメモリ開放処理を行うことができ ます。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
...
static sen_snip *snip;
...

void
setup(void)
{
  snip = NULL;
}

void
teardown(void)
{
  if (snip) {
    sen_snip_close(snip);
  }
}

void
test_simple_exec(void)
{
  const gchar keyword[] = "Senna";

  snip = sen_snip_open(sen_enc_default, 0, 100, 10,
                       default_open_tag, strlen(default_open_tag),
                       default_close_tag, strlen(default_close_tag),
                       NULL);
  cut_assert_not_null(snip);

  cut_assert_equal_int(sen_success,
                       sen_snip_add_cond(snip, keyword, strlen(keyword),
                                         NULL, 0, NULL, 0));
}

これでcut_assert_equal_int()が成功しても失敗してもsen_snip * は開放されます。Cutterではメモリ開放処理のためにstatic変数と setup()/teardown()を使うことが定石になっています。

sen_snip_exec()のテスト

次はsen_snip_add_cond()で設定したキーワード用のsnippetを生成 するsen_snip_exec()のテストです。sen_snip_exec()もsen_rcを返 すので、それを検証します。また、引数でsnippet数とsnippet文字 列のバイト数も受けとるのでそれも検証します。特に目立った部分 はありません。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
static const gchar text[] =
  "Senna is an embeddable fulltext search engine, which you can use in\n"
  "conjunction with various scripting languages and databases. Senna is\n"
  "an inverted index based engine, & combines the best of n-gram\n"
  "indexing and word indexing to achieve fast, precise searches. While\n"
  "senna codebase is rather compact it is scalable enough to handle large\n"
  "amounts of data and queries.";
...
void
test_simple_exec(void)
{
  ...
  unsigned int n_results;
  unsigned int max_tagged_len;

  ...

  cut_assert_equal_int(sen_success,
                       sen_snip_exec(snip, text, strlen(text),
                                     &n_results, &max_tagged_len));
  cut_assert_equal_uint(2, n_results);
  cut_assert_equal_uint(105, max_tagged_len);
}
sen_snip_get_result()のテスト

最後はsen_snip_exec()で生成したsnippetの内容が正しいかどうか のテストです。snippetはsen_snip_get_result()で取得できるので その結果を検証します。n_resultsが2なので2回 sen_snip_get_result()を呼び出す必要があります。

snippetを格納する場所のサイズは動的に決まります。そのため、 snippetを格納する領域を動的に確保する必要があります。 setup()/teardown()の仕組みを用いてメモリを開放するようにしま す。ここ以外は特に目立った部分はありません。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
...
static gchar *result;

void
setup(void)
{
  ...
  result = NULL;
}

void
teardown(void)
{
  ...
  if (result) {
    g_free(result);
  }
}

void
test_simple_exec(void)
{
  ...
  unsigned int result_len;

  ...
  result = g_new(gchar, max_tagged_len);

  cut_assert_equal_int(sen_success,
                       sen_snip_get_result(snip, 0, result, &result_len));
  cut_assert_equal_string("[[Senna]] is an embeddable fulltext search engine, "
                          "which you can use in\n"
                          "conjunction with various scripti",
                          result);
  cut_assert_equal_uint(104, result_len);

  cut_assert_equal_int(sen_success,
                       sen_snip_get_result(snip, 1, result, &result_len));
  cut_assert_equal_string("ng languages and databases. [[Senna]] is\n"
                          "an inverted index based engine, & combines "
                          "the best of n-gram\ni",
                          result);
  cut_assert_equal_uint(104, result_len);
}

これで単純にsnippet APIを使った場合のテストが1つできました。 同様に、異常な場合や違ったデータを用いた場合などのテストを作 成していきます。

diff

せっかくなのでCutterのテスト結果の出力方法を紹介します。

Cutterは cut_assert_equal_string で文字列の比較が失敗したときには、どの部分が異なったかという 差分情報を表示します。

例えば、今回のテストの最後のcut_assert_equal_string()が失敗 した場合は以下のような差分情報が表示されます。

diff:
  - ng languages and DBes. [[Senna]] is
  ?                  ^^
  + ng languages and databases. [[Senna]] is
  ?                  ^^^^^^^
  - an Inverted Index Based Engine, & combines the best of n-gram
  ?    ^        ^     ^     ^
  + an inverted index based engine, & combines the best of n-gram
  ?    ^        ^     ^     ^
    i

このときの期待した結果は以下の通りです。

ng languages and DBes. [[Senna]] is
an Inverted Index Based Engine, & combines the best of n-gram
i

実際の結果は以下の通りです。

ng languages and databases. [[Senna]] is
an inverted index based engine, & combines the best of n-gram
i

差分を見てもらうと分かる通り、異なっている行を示すだけではな くて、行内で異なっている文字まで示しています。(例えば、DBの 下に^^が付いている。)

広く使われているunified diff形式では行内で異なる文字は表示し ません。テストでは1行のみの比較を行うことも多く、行単位だけ の差分よりも文字単位での差分表示も行った方がデバッグが行いや すいという判断からこのような形式になっています。

この形式はPythonのdifflibにあるndiffの形式と同じものです。

テスト全体

今回作成したテストは以下の通りです。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <senna.h>
#include <gcutter.h>

void test_simple_exec(void);

static sen_snip *snip;
static const gchar default_open_tag[] = "[[";
static const gchar default_close_tag[] = "]]";
static const gchar text[] =
  "Senna is an embeddable fulltext search engine, which you can use in\n"
  "conjunction with various scripting languages and databases. Senna is\n"
  "an inverted index based engine, & combines the best of n-gram\n"
  "indexing and word indexing to achieve fast, precise searches. While\n"
  "senna codebase is rather compact it is scalable enough to handle large\n"
  "amounts of data and queries.";
static gchar *result;

void
setup(void)
{
  snip = NULL;
  result = NULL;
}

void
teardown(void)
{
  if (snip) {
    sen_snip_close(snip);
  }
  if (result) {
    g_free(result);
  }
}

void
test_simple_exec(void)
{
  const gchar keyword[] = "Senna";
  unsigned int n_results;
  unsigned int max_tagged_len;
  unsigned int result_len;

  snip = sen_snip_open(sen_enc_default, 0, 100, 10,
                       default_open_tag, strlen(default_open_tag),
                       default_close_tag, strlen(default_close_tag),
                       NULL);
  cut_assert_not_null(snip);

  cut_assert_equal_int(sen_success,
                       sen_snip_add_cond(snip, keyword, strlen(keyword),
                                         NULL, 0, NULL, 0));

  cut_assert_equal_int(sen_success,
                       sen_snip_exec(snip, text, strlen(text),
                                     &n_results, &max_tagged_len));
  cut_assert_equal_uint(2, n_results);
  cut_assert_equal_uint(105, max_tagged_len);

  result = g_new(gchar, max_tagged_len);

  cut_assert_equal_int(sen_success,
                       sen_snip_get_result(snip, 0, result, &result_len));
  cut_assert_equal_string("[[Senna]] is an embeddable fulltext search engine, "
                          "which you can use in\n"
                          "conjunction with various scripti",
                          result);
  cut_assert_equal_uint(104, result_len);

  cut_assert_equal_int(sen_success,
                       sen_snip_get_result(snip, 1, result, &result_len));
  cut_assert_equal_string("ng languages and databases. [[Senna]] is\n"
                          "an inverted index based engine, & combines "
                          "the best of n-gram\ni",
                          result);
  cut_assert_equal_uint(104, result_len);
}

問題発生時に有用なデバッグ情報を増やしたり、より読みやすいテ ストにするなど、いろいろ改良するべき点は残っていますが、今回 はこれで終了します。実際のコードはSennaのリポジトリを参照 してください。

まとめ

2回に分けて以下のことについて説明しました。

  • GNUビルドシステムを採用した既存のプロジェクトへのCutter の組み込み方法
    • cutter.m4で提供するM4マクロの使用方法
    • Cutterをインストールしていないユーザへの対応
    • Cutterをインストールしていない開発者への対応
  • Cutterを用いたテスト環境の構築方法
    • 便利なテスト起動スクリプトrun-test.shの作成方法
    • 初期化・終了関数があるライブラリのテスト方法
  • Cutterを用いたテストの作成方法
    • setup()/teardown()を用いたメモリ管理の方法
    • diffの出力

Cで書かれたプロジェクトに単体テストフレームワークを導入する 場合はCutterも検討してみてはいかがでしょうか。

タグ: Cutter | テスト
2008-08-25

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
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|