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

ククログ

タグ:

GStreamer 0.10からGStreamer 1.0へ移行するには

はじめに

C/C++に対応しているテスティングフレームワークの一つにCutterがあります。

以前、CutterのGStreamerサポートについて というCutterのテストをGStreamerの仕組みと組み合わせて実行する方法についての記事を書きました。

今回は、Cutterで当初サポートしていた GStreamer 0.10系に代わり、GStreamer 1.0への移行をどのように行ったかについて紹介します。Cutterの特徴について知りたい方はCutterの機能を参照してください。 Cutterそのものについては過去に何度かククログでもとりあげていますので、そちらも併せて参照するとよいでしょう。

GStreamer 0.10時代の終わり

CutterにGStreamerのサポートがはいったのは、2008年のことでした。まだ、GStreamerのバージョンが 0.10.20ぐらいのときのことです。 その後、GStreamerやGLibのバージョンアップにともない動作しなくなったことから、Cutter 1.2.3ではGStreamer 0.10向けのサポートを打ち切りました。

そしてGStreamer 1.0へ

Cutter 1.2.3のリリース以降、GStreamer 0.10からGStreamer 1.0へのAPIの変更に追従できないまま *1 になっていましたが、最近になってようやくその対応が入りました。

Cutterが提供しているエレメントは、あくまでテストをGStreamerの枠組みで行うためのもので、映像や音声を扱う一般的なエレメントとは異なります。 そのため、API等の変更の影響はそれほどありませんでした。とはいえ細かな修正をいくつか実施する必要がありました。

例えば、以下のような変更が必要でした。

  • 廃止された定数の修正
  • 廃止されたエレメントの修正
  • 廃止されたマクロの修正
  • 廃止されたvirtual methodの修正
  • マクロ定義の変更への追従

それぞれ、どんな変更をしたのかを説明します。

なお、移行にあたっては、GStreamer 0.10 to 1.0 porting guideをまず最初に参照しました。

廃止された定数の修正

Cutterでは、廃止された GST_FLOW_UNEXPECTED を 使っていたため、同等の GST_FLOW_EOS へと置き換えました。

diff --git a/gst-plugins/gst-cutter-test-runner.c b/gst-plugins/gst-cutter-test-runner.c
index 8b1ccbc..bd38dcd 100644
--- a/gst-plugins/gst-cutter-test-runner.c
+++ b/gst-plugins/gst-cutter-test-runner.c
@@ -284,7 +284,7 @@ create (GstBaseSrc *base_src, guint64 offset,
     GST_BUFFER_OFFSET_END(buf) = offset + send_size;
     *buffer = buf;

-    return !is_end_of_buffer ? GST_FLOW_OK : GST_FLOW_UNEXPECTED;
+    return !is_end_of_buffer ? GST_FLOW_OK : GST_FLOW_EOS;
 }

 static GstStateChangeReturn
廃止されたエレメントの修正

Cutterでは、廃止された GstElementDetails を使っていました。 そのため、メタデータを設定するためのAPIである、 gst_element_class_set_metadata に置き換える必要がありました。

diff --git a/gst-plugins/gst-cutter-server.c b/gst-plugins/gst-cutter-server.c
index 40f4e8d..1723d22 100644
--- a/gst-plugins/gst-cutter-server.c
+++ b/gst-plugins/gst-cutter-server.c
@@ -30,11 +30,6 @@
 GST_DEBUG_CATEGORY_STATIC(cutter_server_debug);
 #define GST_CAT_DEFAULT cutter_server_debug

-static const GstElementDetails cutter_server_details =
-    GST_ELEMENT_DETAILS("Cutter test server",
-                        "Cutter test server",
-                        "Cutter test server",
-                        "g新部 Hiroyuki Ikezoe  <poincare@ikezoe.net>");
 static GstStaticPadTemplate cutter_server_src_template_factory =
     GST_STATIC_PAD_TEMPLATE("src",
                             GST_PAD_SRC,
@@ -97,7 +92,11 @@ gst_cutter_server_base_init (gpointer klass)
     gst_element_class_add_pad_template(element_class,
         gst_static_pad_template_get(&cutter_server_sink_template_factory));

-    gst_element_class_set_details(element_class, &cutter_server_details);
+    gst_element_class_set_metadata(element_class,
+                                   "Cutter test server",
+                                   "Cutter test server",
+                                   "Cutter test server",
+                                   "g新部 Hiroyuki Ikezoe  <poincare@ikezoe.net>");
 }
廃止されたマクロの修正

Cutterでは、廃止された GST_BUFFER_XXX 系のマクロを使っていました。そのため、その部分について修正する必要がありました。

例えば、プログラムの文脈に応じて GstBuffer を扱う上でより適切と思われるAPIである gst_buffer_map を使う、gst_buffer_fill に置き換える、などです。

また、GST_BOILERPLATE が廃止されたため、G_DEFINE_TYPE に置き換える必要がありました。 gst_cutter_test_runner_init はその影響を受け、引数の定義を変更する必要がありました。

diff --git a/gst-plugins/gst-cutter-test-runner.c b/gst-plugins/gst-cutter-test-runner.c
index f00b0b6..edcc55a 100644
--- a/gst-plugins/gst-cutter-test-runner.c
+++ b/gst-plugins/gst-cutter-test-runner.c
@@ -49,7 +49,7 @@ struct _GstCutterTestRunnerPrivate
     GString *xml_string;
 };

-GST_BOILERPLATE(GstCutterTestRunner, gst_cutter_test_runner, GstBaseSrc, GST_TYPE_BASE_SRC);
+G_DEFINE_TYPE(GstCutterTestRunner, gst_cutter_test_runner, GST_TYPE_BASE_SRC);

 enum
 {
@@ -129,7 +129,7 @@ gst_cutter_test_runner_class_init (GstCutterTestRunnerClass * klass)
 }

 static void
-gst_cutter_test_runner_init (GstCutterTestRunner *cutter_test_runner, GstCutterTestRunnerClass * klass)
+gst_cutter_test_runner_init (GstCutterTestRunner *cutter_test_runner)
 {
     GstCutterTestRunnerPrivate *priv = GST_CUTTER_TEST_RUNNER_GET_PRIVATE(cutter_test_runner);

それだけでなく、 _base_init が呼ばれなくなるため、_class_init 内で同等の処理を行う必要がありました。

diff --git a/gst-plugins/gst-cutter-test-runner.c b/gst-plugins/gst-cutter-test-runner.c
index edcc55a..b3f4c99 100644
--- a/gst-plugins/gst-cutter-test-runner.c
+++ b/gst-plugins/gst-cutter-test-runner.c
@@ -80,21 +80,6 @@ static GstStateChangeReturn change_state (GstElement *element,
                                           GstStateChange transition);

 static void
-gst_cutter_test_runner_base_init (gpointer klass)
-{
-    GstElementClass *element_class = GST_ELEMENT_CLASS(klass);
-
-    gst_element_class_add_pad_template(element_class,
-        gst_static_pad_template_get(&cutter_test_runner_src_template_factory));
-
-    gst_element_class_set_metadata(element_class,
-                                   "Cutter test runner",
-                                   "Cutter test runner",
-                                   "Cutter test runner",
-                                   "g新部 Hiroyuki Ikezoe  <poincare@ikezoe.net>");
-}
-
-static void
 gst_cutter_test_runner_class_init (GstCutterTestRunnerClass * klass)
 {
     GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
@@ -126,6 +111,15 @@ gst_cutter_test_runner_class_init (GstCutterTestRunnerClass * klass)
     g_type_class_add_private(gobject_class, sizeof(GstCutterTestRunnerPrivate));

     GST_DEBUG_CATEGORY_INIT(cutter_test_runner_debug, "cutter-test", 0, "Cutter test elements");
+
+    gst_element_class_add_pad_template(element_class,
+        gst_static_pad_template_get(&cutter_test_runner_src_template_factory));
+
+    gst_element_class_set_metadata(element_class,
+                                   "Cutter test runner",
+                                   "Cutter test runner",
+                                   "Cutter test runner",
+                                   "g新部 Hiroyuki Ikezoe  <poincare@ikezoe.net>");
 }
廃止されたvirtual methodの修正

Cutterでは GstBaseSrc を使っていたことから、check_get_range の廃止に対応する必要がありました。

そのため、元々どういう挙動が期待されていたのかを確認したうえで、同等の振舞いとなるように修正する必要がありました。

元々の振舞いについては、過去のドキュメントを参照しました。

  /* check whether the source would support pull-based operation if
   * it were to be opened now. This vfunc is optional, but should be
   * implemented if possible to avoid unnecessary start/stop cycles.
   * The default implementation will open and close the resource to
   * find out whether get_range is supported and that is usually
   * undesirable. */

元のコードでは、 check_get_range で明示的に FALSE を返しており、上記の説明にあるpullモードはサポートしていません。 GstBaseSrcリファレンスマニュアルを参照すると、pullモードがサポートされる条件が明記されており、is_seekableFALSE を返せばpull モードをサポートしない(従来と挙動が同じになる)ことがわかりました。 たまたま、元のコードでも is_seekableFALSE を返すようになっていたので、最終的には、check_get_rangeがらみのコードを削除するだけでよいことになりました。

diff --git a/gst-plugins/gst-cutter-test-runner.c b/gst-plugins/gst-cutter-test-runner.c
index 9b05c5b..0cedf9d 100644
--- a/gst-plugins/gst-cutter-test-runner.c
+++ b/gst-plugins/gst-cutter-test-runner.c
@@ -75,7 +75,6 @@ static GstFlowReturn create          (GstBaseSrc *basr_src,
                                       guint64     offset,
                                       guint       length,
                                       GstBuffer **buffer);
-static gboolean      check_get_range (GstBaseSrc *base_src);

 static GstStateChangeReturn change_state (GstElement *element,
                                           GstStateChange transition);
@@ -100,7 +99,6 @@ gst_cutter_test_runner_class_init (GstCutterTestRunnerClass * klass)
     base_src_class->stop            = stop;
     base_src_class->is_seekable     = is_seekable;
     base_src_class->create          = create;
-    base_src_class->check_get_range = check_get_range;

     spec = g_param_spec_string("test-directory",
                                "Test directory",
@@ -248,12 +246,6 @@ is_seekable (GstBaseSrc *base_src)
     return FALSE;
 }

-static gboolean
-check_get_range (GstBaseSrc *base_src)
-{
-    return FALSE;
-}
-
 static GstFlowReturn
マクロ定義の変更への追従

従来問題なかったものの、マクロ定義の変更に伴い警告がでるようになってしまった箇所への対応も行いました。 具体的には、プラグインを定義する GST_PLUGIN_DEFINE の引数の変更です。

これまでは、第三引数は文字列を渡すのが正しいやりかたでした。 しかし、文字列化もマクロ展開時に行われるようになったので、それに合わせるようにしました。

diff --git a/gst-plugins/gst-cutter-test.c b/gst-plugins/gst-cutter-test.c
index b373b76..c44ef66 100644
--- a/gst-plugins/gst-cutter-test.c
+++ b/gst-plugins/gst-cutter-test.c
@@ -42,7 +42,7 @@ plugin_init (GstPlugin * plugin)
 }

 GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR, 
-                   "cutter-test", "Cutter element",
+                   cutter-test, "Cutter element",
                    plugin_init, VERSION, "LGPL",
                    "GstCutterTest", "http://cutter.sf.net");

このようにして、CutterはGstreamer 0.10からGStreamer 1.0系への対応を行いました。

まとめ

今回はCutterというテスティングフレームワークのGStreamer 1.0対応について紹介しました。GStreamer 1.0対応が入ったバージョンをCutter 1.2.5として今月リリースする予定です。

*1 GStreamer 1.0対応は優先度が低かった。

つづき: 2016-01-06
タグ: Cutter
2015-07-17

CutterのGStreamerサポートについて

はじめに

C/C++に対応しているテスティングフレームワークの一つにCutterがあります。

今回はCutterのGStreamerサポートについて紹介します。Cutterの特徴について知りたい方はCutterの機能を参照してください。 Cutterそのものについては過去に何度かククログでもとりあげていますので、そちらも併せて参照するとよいでしょう。

CutterのGStreamerサポートとは

CutterのGStreamerサポートと聞いて、何を思い浮かべるでしょうか。

  • CutterでGStreamerを使っているアプリケーションやライブラリのテストを書きやすくする仕組みのこと
  • GStreamerの仕組みを使ってテストを実行すること

前者のことと思ったかも知れません。正解は後者の「GStreamerの仕組みを使ってテストを実行すること」です。

GStreamerを使ってテストを実行するには

では、実際に実行してみましょう。そのためには、この記事を書いている時点ではまだリリースされていないCutterの最新版を使う必要があります。GStreamer 1.0に対応したのがごく最近であるためです。

Cutterの最新版をビルドする

GitHubからソースコードを入手してビルドをはじめるために、次のコマンドを実行します。

% git clone https://github.com/clear-code/cutter.git
% cd cutter
% ./autogen.sh
% ./configure

ここで、 configure の結果で「GStreamer : yes」が含まれていることを確認します。ここで yes になっていないとGStreamerサポートが有効になりません。GStreamerの開発パッケージがインストール済であるか確認してください。 Debian系のディストリビューションでは、あらかじめ libgstreamer1.0-dev をインストールしておく必要があります。

Libraries:
  ...
  GStreamer                        : yes
  ...

うまくGStreamerを検出できていると、次のようにプラグインのディレクトリも正しいものが表示されます。

  GStreamer plugins directory      : /usr/lib/x86_64-linux-gnu/gstreamer-1.0

ここまできたら、あとは make を実行するだけです。

% make
テストを実行する

Cutterをビルドできたので、テストを実行してみましょう。 *1

cloneしたソースコードに gst-plugins というディレクトリがあるのでそちらに移動します。

% cd gst-plugins

すると、いくつかサンプルのシェルスクリプトがあるのがわかります。

% ls -la *.sh
-rwxr-xr-x 1 kenhys kenhys 584  7月 11 01:52 run-client.sh
-rwxr-xr-x 1 kenhys kenhys 580  7月 11 01:52 run-server.sh
-rwxr-xr-x 1 kenhys kenhys 679  7月 11 01:52 run-test.sh

ここでは、一番簡単な run-test.sh の内容を見てみましょう。

% cat run-test.sh
#!/bin/sh

export BASE_DIR="`dirname $0`"

if test x"$NO_MAKE" != x"yes"; then
    make -C $BASE_DIR/../ > /dev/null || exit 1
fi

export CUT_UI_MODULE_DIR=$BASE_DIR/../module/ui/.libs
export CUT_UI_FACTORY_MODULE_DIR=$BASE_DIR/../module/ui/.libs
export CUT_REPORT_MODULE_DIR=$BASE_DIR/../module/report/.libs
export CUT_REPORT_FACTORY_MODULE_DIR=$BASE_DIR/../module/report/.libs
export CUT_STREAM_MODULE_DIR=$BASE_DIR/../module/stream/.libs
export CUT_STREAM_FACTORY_MODULE_DIR=$BASE_DIR/../module/stream/.libs

export GST_PLUGIN_PATH=$BASE_DIR/.libs
gst-launch-1.0 \
  cutter-test-runner test-directory=$BASE_DIR/test ! \
  cutter-console-output verbose-level=v use-color=true

ポイントは最後の行で gst-launch-1.0 を使っているところです。

gst-launch-1.0 \
  cutter-test-runner test-directory=$BASE_DIR/test ! \
  cutter-console-output verbose-level=v use-color=true

Cutter特有のエレメントである、「cutter-test-runner」や、「cutter-console-output」を指定してパイプラインを組み立てているのがわかります。 *2

では、run-test.sh を実行してみましょう。

% ./run-test.sh
パイプラインを一時停止 (PAUSED) にしています...
Pipeline is PREROLLING ...
Pipeline is PREROLLED ...
パイプラインを再生中 (PLAYING) にしています...
New clock: GstSystemClock
dummy_loader_test:
  test_dummy_function2:                                 .: (0.000001)
  test_dummy_function3:                                 .: (0.000001)
  test_dummy_function1:                                 .: (0.000000)
  test_abcdefghijklmnopqratuvwzyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:.: (0.000000)

Finished in 0.000935 seconds (total: 0.000002 seconds)

4 test(s), 0 assertion(s), 0 failure(s), 0 error(s), 0 pending(s), 0 omission(s), 0 notification(s)
100% passed
Got EOS from element "pipeline0".
Execution ended after 0:00:00.000993224
パイプラインを一時停止 (PAUSED) にしています...
Setting pipeline to READY ...
Setting pipeline to NULL ...
Freeing pipeline ...

GStreamerでよくみるメッセージとともに、テストの実行結果が表示されているのがわかります。

この例ではテストを実行するのも、結果を表示するのも同一PCで行っています。

テストの実行と結果を別の環境で行う

先程の例では、テストの実行と結果を表示するのは同一のPCにて行っていました。今度はそれを、別の環境で行ってみましょう。 GStreamerに付属の tcpserversrctcpclientsink といったエレメントを使って実現できます。

テストを実行する側では tcpclientsink を指定し、テスト結果を受けとって表示する側では tcpserversrc を指定して TCPによるテスト結果の送受信をするのがポイントです。

gst-launch-1.0 のパイプライン指定はそれぞれ次のようになります。

テスト実施側のコマンドライン:

% gst-launch-1.0 \
    cutter-test-runner test-directory=$BASE_DIR/test ! \
    tcpclientsink host=(テスト結果受信側のIP) port=50000

テスト結果の受信側のコマンドライン:

% gst-launch-1.0 \
    tcpserversrc host=(テスト結果受信側のIP) port=50000 ! \
    cutter-console-output verbose-level=v use-color=true

簡単に実行するために、テスト実施側用として run-server.sh があります。また、テスト結果の受信側用に run-client.sh があります。

それぞれのスクリプトを実行する順番は次の通りです。

  • run-client.sh をテスト結果受信側で実行する
  • run-server.sh をテスト実施側で実行する
$ ./run-client.sh 
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
New clock: GstSystemClock
dummy_loader_test:
  test_dummy_function2:                                 .: (0.000000)
  test_dummy_function3:                                 .: (0.000000)
  test_dummy_function1:                                 .: (0.000001)
  test_abcdefghijklmnopqratuvwzyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:.: (0.000001)

Finished in 0.003666 seconds (total: 0.000002 seconds)

4 test(s), 0 assertion(s), 0 failure(s), 0 error(s), 0 pending(s), 0 omission(s), 0 notification(s)
100% passed
Got EOS from element "pipeline0".
Execution ended after 0:00:00.005115664
Setting pipeline to PAUSED ...
Setting pipeline to READY ...
Setting pipeline to NULL ...
Freeing pipeline ...

これで、テストの実行環境とは別の環境で結果を表示することができました。 リモートで実行中のCutterのテスト結果のログを手元に保存する、というのが簡単にできるようになります。

まとめ

今回はCutterというテスティングフレームワークのGStreamerサポートについて紹介しました。

Cutterにはほかにもテスト環境を便利にする機能があります。まだCutterを使ったことがない人はチュートリアルからはじめるとよいでしょう。詳しく知りたい人はリファレンスマニュアルを参照してください。

*1 付属のサンプルを実行するだけなので、make installは実行していない。

*2 エレメントの詳細は gst-inspect-1.0 で確認できる。

タグ: Cutter
2015-07-13

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

CutterをWindowsでgrowlnotifyと一緒に使う方法

C/C++に対応しているテスティングフレームワークの一つにCutterがあります。今回はテスト結果の通知という観点からCutterの紹介をします。

Cutterそのものについては何度かククログでもとりあげています。 Cutterの特徴について知りたい方はCutterの機能を参照してください。

テスト結果の通知

Cutterに限らず、テスティングフレームワークではテスト結果の通知方法を工夫しています。例えば、コンソールに色分けして結果を表示したり、HTML形式のレポートを生成したりします。

Cutterでもそのようなテスト結果の通知方法の工夫の一つとして、TDDきのたんによるテスト結果の通知をサポートしています。

せっかくテストを書いても、それが頻繁に実行されなかったり、実行した結果を確認して修正する継続的なサイクルが維持されなければ意味がありません。テストが充実するのに伴って、時間のかかるテストの実行中に他の作業をするということも増えてきます*1。他の作業をしている間にテストが失敗したら、まずはそのテストをパスするための作業に取り組む必要があります。しかし、他の作業をしていると失敗を見逃してしまうこともあります。他の作業をしていてもテストの失敗に素早く気づける仕組みが必要です。

そのため、Cutterではテスト結果を「通知」する仕組みを取り入れています。

TDDきのたんによるテスト結果の通知

CutterではTDDきのたんでテスト結果を通知します。GNOMEやMax OS Xでは以下のように通知されます。

GNOME上での通知

Mac OS X上での通知

Ubuntuではnotify-osdもしくはnotification-daemonをインストールすれば、TDDきのたんを利用してテスト結果を通知できます。TDDきのたんによるテスト結果の通知は1.1.6からサポートされています。

Windowsでもテスト結果を通知する

前項ではTDDきのたんでどのようにテスト結果が通知されるかを紹介しました。

CutterはWindowsでも利用することができますが、TDDきのたんによるテスト結果の通知はできませんでした。

Cutter 1.2.1からWindowsでもTDDきのたんによるテスト結果の通知ができるようになりました。なお、この機能を利用するにはGrowl for Windowsgrowlnotifyが必要です。

Growl for Windowsとgrowlnotifyについて

Windowsでも通知系のアプリケーションがあり、Ubuntuでのnotify-osdやnotification-daemonに相当するものがGrowl for Windowsで、notify-sendに相当するのがgrowlnotifyです。

Growl for Windowsとgrowlnotifyをセットアップすれば、テスト結果を通知することができます。

セットアップ

Windowsでテスト結果を通知できるようにするための環境構築手順を紹介します。

CutterのWindows向けのバイナリは提供されていないため、ソースコードからコンパイルしてインストールする必要があります。インストール方法は以下の通りです。

  1. Growl for Windowsをインストールする
  2. growlnotifyをインストールする
  3. MinGWをインストールする
  4. GTK+ Windowsバイナリをインストールする
  5. intltoolをインストールする
  6. Cutter 1.2.2をインストールする
Growl for Windowsをインストールする

Growl for Windowsのインストーラーをダウンロード、インストールします。

インストールしたら、スタートメニューのGrowlアイコンをクリックしGrowlを起動します。

growlnotify.exeをインストールする

growlnotify.exeが含まれているzipをダウンロードします。

ダウンロードしたzipアーカイブにはgrowlnotify.exeとgrowlnotify.comの2つが含まれています。以降の説明ではc:\WinApp\growlnotify以下にzipアーカイブに含まれていたバイナリが配置されているものとします。

実際にコマンドプロンプト*2を起動し、growlnotifyコマンドが正常に動作することを確認します。

c:\Users\ユーザー名>c:\WinApp\growlnotify\growlnotify.exe /t:title body

growlnotify動作確認結果

正しくセットアップできていれば、スクリーンショットのように「title」と「body」を含んだ通知が表示されます。

MinGWをインストールする

CutterをビルドするためのツールはMinGWのものを利用します。ここではネットワーク経由でのインストールを行うことのできるインストーラーmingw-get-inst-20120426.exeを使ってインストールします。

インストールの途中にインストールするコンポーネントを選択する箇所があります。そこでは以下を選択します。

  • C Compiler
  • C++ Compiler
  • MSYS Basic System
  • MinGW Developer Toolkit

以降の説明ではc:\MinGWへインストールしたものとして説明します。

正しくインストールできていれば、c:\MinGW\msys\1.0\msys.batがインストールされています。msys.batを実行すると以下のようなウィンドウが表示されます。このウィンドウが表示されれば正しくインストールできています。

msys.bat動作確認結果

GTK+ Windowsバイナリをインストールする

Cutterの動作にはGLib 2.16以降が必要です。そのため、GLibのWindowsバイナリをインストールします。

The GTK+ Projectが配布しているGTK+のWindowsバイナリにGLibも含まれているので、これを使います。all-in-one bundleをダウンロードして、c:\WinApp\GTKへ展開します。

c:\MinGW\msys\1.0\msys.batを実行し、シェルを起動します。以降シェルでの操作はプロンプトに$をつけて説明します。

シェル上で以下のコマンドを実行します。GTK+のデモアプリケーションが起動したら正常にインストールできています。

$ /c/WinApp/GTK/bin/gtk-demo.exe
intltoolをインストールする

Cutterをビルドする際にはconfigureのチェックでintltoolが古いとエラーになる*3ため、あらかじめ最新のintltoolをインストールします。依存するPerlのモジュールについてはMinGWをインストールした時点で一緒にインストールされています。

intltoolのサイトからintltool-0.50.2.tar.gzをダウンロードします。

あとは通常の手順でコンパイルおよびインストールします。

$ tar xvf intltool-0.50.2.tar.gz
$ cd intltool-0.50.2
$ ./configure --prefix=/c/WinApp/GTK
$ make
$ make install

正しくイントールできていれば、以下のコマンドを実行するとバージョンが表示されます。

$ /c/WinApp/GTK/bin/intltoolize --version
intltoolize (GNU intltool) 0.50.2
Cutter 1.2.2をインストールする

Cutterの最新リリース版のソースコードのダウンロードと展開には以下のコマンドを実行します。

$ mingw-get install msys-wget
$ wget http://downloads.sourceforge.net/cutter/cutter-1.2.2.tar.gz
$ tar xvf cutter-1.2.2.tar.gz

GTK+関連で必要な環境変数の設定を行います。

$ export PATH=$PATH:/c/WinApp/GTK/bin
$ export PKG_CONFIG_PATH=/c/WinApp/GTK/lib/pkgconfig

pkg-configコマンドが実行できることを確認します。*4

$ pkg-config --version
0.26

展開したソースコードのディレクトリへと移動し、インストールします。

$ cd cutter-1.2.2
$ ./configure --prefix=/c/WinApp/GTK
$ make
$ make install

正しくインストールできていれば、Cutterのバージョンが表示されます。

$ cutter --version
1.2.2

Cutterをインストールできたので、実際に小さなサンプルプログラムを用意します。

1
2
3
4
5
6
7
#include <cutter.h>

CUT_EXPORT void
test_sample(void)
{
  cut_assert_true(FALSE);
}

これは必ず失敗するテストです。

このサンプルを以下のようにしてコンパイルします。

$ gcc -shared -o sample.dll sample.c `pkg-config --cflags --libs cutter`

以下のコマンドで実際にテストを実行します。

$ cutter .

F
===============================================================================
Failure: test_sample
expected: <FALSE> is TRUE value
test.c:5: test_sample(): cut_assert_true(0, )
===============================================================================

Finished in 0.109200 seconds (total: 0.000000 seconds)

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

上記のようにテストが失敗します。

growlnotifyでテスト結果を通知する

テスト結果をgrowlnotifyで通知するにはgrowlnotifyコマンドへのパスを通してから、cutterコマンドを実行します。

$ export PATH=$PATH:/c/WinApp/growlnotify
$ cutter .

テスト失敗時の通知結果

失敗したことが通知されます。

先程のサンプルプログラムで、FALSEとなっていたところをTRUEへと修正し、必ずパスするようにします。

1
2
3
4
5
6
7
#include <cutter.h>

CUT_EXPORT void
test_sample(void)
{
  cut_assert_true(TRUE);
}

再度コンパイルし、cutterコマンドを実行します。

$ gcc -shared -o sample.dll sample.c `pkg-config --cflags --libs cutter`
$ cutter .

テスト成功時の通知結果

成功したことが通知されます。

これで、growlnotifyを使ってCutterのテスト結果を通知することができるようになりました。

最初にテストを書いておくと、実装が終わった時点で通知がグリーンに変わるので、テストがうまくいったことがわかりやすいです。

growlnotifyでの通知を抑制する場合には、以下のように--notify=noオプションを指定します。

$ cutter . --notify=no

なお、Cutter自身のテストはCutterで書かれているため、CutterをCutterでテストできます。

その場合、cutter-1.2.2ディレクトリにて以下のコマンドを実行します*5

$ make check
...(途中略)...
Finished in 22.851902 seconds (total: 21.826769 seconds)

555 test(s), 2352 assertion(s), 45 failure(s), 2 error(s), 0 pending(s), 9 omission(s), 0 notification(s)
91.5315% passed
FAIL: run-test.sh

Cutterをテストしたときの通知結果

まとめ

CutterをWindowsでgrowlnotifyと一緒に使う方法を紹介しました。

開発・テストを実行するサイクルに時間がかかる場合は、「通知」によりテストの結果にすぐ気づける仕組みがあると便利です。MinGW環境のビルドは遅いことが多いので、このような「通知」の仕組みをCutterと一緒に導入してみるのをオススメします。

Cutterについて興味がわいてきたら、まずはチュートリアルから始めるとよいでしょう。

*1 その前に、既存のテストについて「本当にそのテストは必要なのか」を確認するべきです。必要のないテストがあった場合はその削除してテスト時間を短くします。

*2 cmd.exe

*3 0.35.0以降が必要。

*4 pkg-configはGTK+に含まれています。

*5 たくさん失敗していますね。。。

タグ: Cutter
2012-12-12

TDDきのたん

今日は年に一度の肉の日だからか、いろいろなソフトウェアがリリースされていますね。

このうち、Cutter 1.1.6について紹介します。

Cutterとは

CutterはC/C++用の単体テストフレームワークです。スクリプト言語の単体テストフレームワークのように簡単にテストを書けること、テストが失敗した時にデバッグしやすいことを重視しています。どちらも「テストが苦痛」にならないために大事なことです。

Cutter 1.1.6ではテストをより頻繁に実行しやすくするための機能を強化しました。それがTDDきのたんのサポートです。

TDDきのたんとは

TDDきのたんとはMayu & Co.さんが描いた色違いのかさのきのこたちです。Cutter 1.1.6を使うとテストを実行するたびに愛らしいTDDきのたんに会うことができます。頻繁にテストを実行したくなりますね。

TDDきのたんのスクリーンショット

テストとグリーンとレッドとフィードバック

TDD(テスト駆動開発)ではテストの結果を色で表すことが多いです。グリーンならパスで、レッドなら失敗です。グリーンになったら気持ちいいよね、レッドは落ち着かないね、早くグリーンにしたいね、そんな風に色を使っています。色を使うことで、テストをすべてパスしている状態をキープしたくなる力を推進しています。

RSpecやCutterなど最近も開発が続いている単体テストフレームワークは結果を表示する時にグリーンやレッドなどの色を使っています。でも、それだけで十分でしょうか。

テストを頻繁に実行するようになると、開発の中で自然にテストを実行するようになります。それはもうlsコマンドを実行するように*1テストを実行します。そうなると、テストの実行中に違う作業をして、合間にテスト結果を見るようになります。そのため、テストの失敗に気付くのが遅れる場合があります。

それを解決するためには、これまで通り端末に色付きの結果を表示させているだけでは不十分ではないでしょうか。

通知

デスクトップ環境では多くのアプリケーションが動いていて、常にすべてのアプリケーションを見ていることはできません。そこで必要なときにアプリケーションから「通知」してきます*2。Linuxや*BSDではDesktop Notification、Mac OS XではGrowlなどを利用します。

テスト結果も「通知」した方がよいのではないでしょうか。

そういうわけで、Cutter 1.1.6では「notify-send」コマンドを使ってTDDきのたん付きの通知を行うようになりました。test-unit 2では2ヶ月くらい前からTDDきのたん付きの通知をサポートしています。test-unit 2で利用する場合はtest-unit-notifyを使います。こちらもTDDきのたんを使っています。

まわりくどかったですね。

まとめ

まわりくどくCutter 1.1.6の新機能である「テスト結果通知」を紹介しました。単体テストフレームワークに限らず、うるさくない程度に「通知」に対応するとより使いやすくなるかもしれません。

「通知」を使う場合は画像も入れることをおすすめします。画像を入れるとよりピンときます。例えば、Mayu & Co.さんのTDDきのたんは愛らしくて何度も会いたくなりますよね。それでは、テストを実行して会いにいきましょう。

*1 慣用句です。実際にlsコマンドを実行しまくっている人はあまりシェルを使いこなしていないのでしょう。

*2 デスクトップだけではなくスマートフォンなどの携帯端末でも「通知」してきます。

タグ: Cutter | Ruby | テスト
2011-02-09

Cutter 1.1.3リリース

C・C++言語用の単体テストフレームワークCutterのバージョン1.1.3をリリースしました。

ハイライト

今回のリリースではデータ駆動テストのサポートを強化しています。サポートしている型が増えたので、これまで以上にテストデータを作りやすくなっています。

例えば、このようにテストデータを作ることができます。

1
2
3
4
5
gcut_add_datum("normal case",
               "expected", G_TYPE_CHAR, 'e',
               "input", G_TYPE_STRING, "Cutter",
               "n", G_TYPE_UINT, 4,
               NULL);

詳しくはリファレンスマニュアルの便利なテストデータ用API - gcut_add_datum()あたりを見てください。

Cutterユーザ(徐々に)増加中

Cutterが今のようなGLibベースの作りになってから2年半くらい経ちますが、徐々にユーザが増えてきました。最近、nfc-toolsという近距離無線通信(NFC)用のツールを開発しているオープンソースプロジェクトでも使われているのを見つけました。

groongaもCutterを採用しているプロジェクトで、Cutterのカバレッジ支援機能なども使っています。groongaプロジェクトではCutterが提供している支援機能だけではなく、コミットした人毎のカバレッジ率などもグラフ化して公開しています。今後のリリースで、Cutter本体がこのような見せ方を支援する機能を提供したいですね。

まとめ

Cutter 1.1.3の目玉機能とCutter採用プロジェクトについて紹介しました。

C・C++のプロジェクトで使うテスティングフレームワークを探しているならCutterも検討してみてはいかがでしょうか。

タグ: Cutter
2010-04-14

test-unit 2.0.7とCutter 1.1.1をリリース

先日、Ruby用のxUnit系テスティングフレームワークtest-unit 2.0.7とC・C++用のxUnit系テスティングフレームワークCutter 1.1.1がリリースされました。

どちらも、テストの書きやすさ(テストをキレイに書けるとテストを保守しやすい)だけではなく、テストが失敗した時に「できるだけ素早く問題の原因にたどり着ける」ことも重視しています。

Rails/Rack界隈ではCucumberWebratcapybaraなどを使って、「"ログイン"ボタンをクリックする」とか「click_link("ログイン")」などと、直感的にテストを書けるようになっています。では、テストが失敗したときの結果はどのように表示されるでしょうか。HTML全体やテキスト全体が表示されて、「"ログイン"というボタン(リンク)はなかったよ」と言われたらどうでしょう。あなたのコードはどこが悪かったのでしょう。

そういうときに、失敗結果を見て、すぐに「あぁ、ここが悪いかも!」と作業を進めていけるようなテスティングフレームワークにしたいものです。開発はデバッグの連続なのですから、よりスムーズにデバッグ作業を進める手助けとなるツールを使って開発したいですよね。

test-unitやCutterはWebアプリケーション用に特別なサポート機能は提供していないので上記のようなことをうまい具合に解決できるわけではないのですが、ライブラリのテストでは上記のようなことをうまい具合に解決する機能を提供しています。

Cutterのインストール方法がより簡単に

一応、リリースで変わったことを少し書いておきます。

機能面でもよくなっているのですが、インストールまわりだけにしておきます。

Cutterプロジェクトでは、これまでDebian, Ubuntu用のapt-lineとMacPortsのPortfileを提供していましたが、今回のリリースから、FedoraのYumリポジトリも提供するようにしました。以下でインストールできます。yumの管理下に入るのでアップデートも簡単ですね。

% sudo rpm -Uvh http://cutter.sourceforge.net/fedora/cutter-repository-1.0.0-0.noarch.rpm
% sudo yum install cutter

test-unitの方は変わらずgemをサポートしているので、こちらもインストール・アップデートが簡単ですね。

書きやすさだけではなくデバッグしやすさも重視したテスティングフレームワークに興味のある方は使ってみてはいかがでしょうか。

タグ: テスト | Ruby | Cutter
2010-03-11

C++用xUnitでのテストの書き方

注: 長いです。

スクリプト言語でのxUnit実装を使ったことがある方なら、テストを定義するだけでテストが実行されることが当たり前ではないでしょうか。c2.comのWikiによると、これはTest Collectorというそうです。定義したテストを自動的に集めてくる機能のことです。

一般的にTest Collectorの機能は言語が提供するリフレクション機能やメタプログラミング機能を使って実現されます。

例えば、Rubyのtest-unit 2.xでは、リフレクションを使う方法とメタプログラミングを使う方法の両方をサポートしています。リフレクションを使う方法ではObjectSpace.each_object(Class)ですべてのクラスを取得し、その中のTest::Unit::TestCaseのサブクラスを集めます。メタプログラミングを使う方法ではTest::Unit::TestCase.inheritedを定義して、サブクラスが定義された時のフックでそのサブクラスを集めます。

Pythonでもモジュールオブジェクトからモジュール内のオブジェクトにアクセスすることができるので、同様の方法でテストを集めることができます。JavaScriptでも、オブジェクトに定義されているプロパティを列挙することができるので、同様の方法でテストを集めることができます。(JavaScriptとXULで実装されているUxUも同様のことをしています。)

一方、CやC++ではリフレクション機能やクラスをファーストクラスオブジェクトとして扱う機能がないため、自動でテストを集めるためには一工夫必要になります。一昔前のxUnitでは、定義したテストを手動で登録していました。

それでは、C/C++での一工夫の方法として以下の4つをxUnit実装と一緒に紹介します。C++用のxUnitを選択する時の参考にしてください。

  • マクロでごまかす
  • プリコンパイルする
  • マクロでテスト定義と同時に登録する
  • 共有ライブラリから探す

マクロでごまかす

最初はCppUnitのケースです。CppUnitではテストを定義するだけでは、自動でテストを集めてはくれません。しかし、便利マクロを用意して、手動でテストを登録する面倒さを減らしています。

以下はCppUnit Cookbookにあるソースコードをベースにしています。public内のテスト定義とは別にCPPUNIT_TEST_SUITEからCPPUNIT_TEST_SUITE_ENDの間でテストを登録しています。

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
// complex-number-test.cpp
#include <cppunit/extensions/HelperMacros.h>

class ComplexNumberTest : public CppUnit::TestFixture  {
  CPPUNIT_TEST_SUITE( ComplexNumberTest );
  CPPUNIT_TEST( testEquality );
  CPPUNIT_TEST( testAddition );
  CPPUNIT_TEST_SUITE_END();

private:
  Complex *m_10_1, *m_1_1, *m_11_2;

public:
  void setUp()
  {
     m_10_1 = new Complex( 10, 1 );
     m_1_1 = new Complex( 1, 1 );
     m_11_2 = new Complex( 11, 2 );
  }

  void tearDown()
  {
    delete m_10_1;
    delete m_1_1;
    delete m_11_2;
  }

  void testEquality()
  {
    CPPUNIT_ASSERT( *m_10_1 == *m_10_1 );
    CPPUNIT_ASSERT( !(*m_10_1 == *m_11_2) );
  }

  void testAddition()
  {
    CPPUNIT_ASSERT( *m_10_1 + *m_1_1 == *m_11_2 );
  }
};
CPPUNIT_TEST_SUITE_REGISTRATION( ComplexNumberTest );

実行する場合は以下のようなmain関数を定義する必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
// main.cpp
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>

int main( int argc, char **argv)
{
  CppUnit::TextUi::TestRunner runner;
  CppUnit::TestFactoryRegistry &registry =
    CppUnit::TestFactoryRegistry::getRegistry();
  runner.addTest( registry.makeTest() );
  bool wasSuccessful = runner.run( "", false );
  return wasSuccessful;
}

上記の二つのファイル(とComplexNumberの実装)を使ってビルドします。

% g++ -o complex-number-test complex-number-test.cpp main.cpp \
    -lcomplex-number -lcppunit

テストを実行するにはビルドしたバイナリを実行します。

% ./complex-number-test
..


OK (2 tests)

マクロを使ってテスト登録を簡単にしている(自動化まではしていない)例としてCppUnitを紹介しました。

CPPUNIT_TEST_SUITEなどの便利マクロを使わない場合は定義したテスト名(上の例ではtestEqualitytestAddition)以外のことも気にしなければいけなくなります。便利マクロを使うと、テスト名だけわかっていればよいので、それに比べるとだいぶテスト作成が楽になっています。

しかし、テストを定義だけして登録し忘れたということを回避することができません。また、テストケース定義とは別にmain関数も定義する必要があり、テスト以外のことにも気を配る必要があることにも注意が必要です。

プリコンパイルする

C++で書かれたテストコードを直接C++コンパイラでコンパイルするのではなく、テストコードに必要なコードを追加したC++ソースコードを生成して、それをコンパイルする方法です。C++コンパイラでのビルドする前に一度変換処理を行えるので、テストコードへの記述が減ることが利点ですが、変換処理を行うのがやや面倒です。自動化されれば気にならなくなるでしょう。

CxxTest

まずはCxxTestのケースです。CxxTestではソースコードを直接ビルドするのではなく、C++のソースコードからテスト登録処理などを付加したC++ソースコードを生成し、それをビルドします。

以下はCxxTest User Guideにあるソースコードをベースにしています。テストの定義だけでテスト登録処理は含まれていません。

1
2
3
4
5
6
7
8
9
10
11
12
// MyTestSuite.h
#include <cxxtest/TestSuite.h>

class MyTestSuite : public CxxTest::TestSuite
{
public:
   void testAddition( void )
   {
      TS_ASSERT( 1 + 1 > 1 );
      TS_ASSERT_EQUALS( 1 + 1, 2 );
   }
};

以下のようにビルドします。

% cxxtestgen --error-printer -o cxxunit-tests.cpp MyTestSuite.h
% g++ -o cxxunit-tests cxxunit-tests.cpp

cxxtestgenMyTestSuite.hにあるテスト定義にテスト登録処理などを加えてcxxunit-tests.cppを生成します。余談ですが、cxxtestgenはPythonスクリプトです。また、CxxUnitはライブラリを提供せず、ヘッダーファイルのみを提供します。

バイナリを実行するとテストが走ります。

% ./cxxunit-tests
Running 1 test.OK!

テスト登録が完全に自動化されているのでCppUnitよりも新規テストの追加が容易です。テストの登録しわすれもありません。ただ、cxxtestgenとC++コンパイラで2回コンパイルする必要があることが少し手間だと言えます。

QTestLib

続いてQtが提供するQTestLibのケースです。QTestではQtが提供するスロットの仕組みを使って、定義されているテストを集めます。スロットがどのように定義されているかをプログラム中から扱うために、QtはC++のソースコードをプリコンパイルしますが、QTestでも同様にプリコンパイルする必要があります*1

以下はQTestLibのチュートリアルにあるソースコードををベースにしています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// test-qstring.cpp
#include <QtTest/QTest>

class TestQString: public QObject
{
    Q_OBJECT
private slots:
    void toUpper()
    {
        QString str = "Hello";
        QCOMPARE(str.toUpper(), QString("HELLO"));
    }
};

QTEST_MAIN(TestQString)
#include "test-qstring.moc"

QTestLibでもmain関数を定義しなければいけませんが、QTEST_MAINという便利マクロが用意されています。

以下のようにビルドします。

% mkdir test-qstring
% mv test-qstring.cpp test-qstring
% cd test-qstring
% qmake -project "QT += testlib"
% qmake
% make

バイナリを実行するとテストが走ります。

% ./test-qstring
********* Start testing of TestQString *********
Config: Using QTest library 4.5.3, Qt 4.5.3
PASS   : TestQString::initTestCase()
PASS   : TestQString::toUpper()
PASS   : TestQString::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of TestQString *********

このようにQTestLibではテスト登録のために必要なコードはQTEST_MAINでクラスを指定している部分だけです。個々のテストは指定する必要がありません。

メタオブジェクト情報を生成すること、また、それを読み込んでいる#include "test-qstring.moc"のところはQTestLib独自のことではなく、Qt全般のことです。そのため、Qtを利用している場合は追加で必要な作業とはいえないでしょう。つまり、QTestLibのテストを集める方法は完全には自動化されていませんが、Qt開発者にはそれほど負担もかからず自然に書けるようになっている使いやすいAPIといえます。一方、Qt開発者でない場合は、面倒に見えるでしょう。

マクロでテスト定義と同時に登録する

CppUnitでもマクロでテストを登録していますが、それをもう一歩進めたのがこの方法です。CppUnitでは、テスト定義は通常の関数定義でしたが、この方法ではそこでマクロを使い、テスト定義と同時にテストを登録します。

Google Test

まずは、Google Testです。Google Testではテスト定義時にTESTマクロを使います。以下はGoogleTestSamplesにあるソースコードをベースにしています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// test-factorial.cpp
#include <gtest/gtest.h>

int Factorial(int n) {
  int result = 1;
  for (int i = 1; i <= n; i++) {
    result *= i;
  }

  return result;
}

TEST(FactorialTest, Negative) {
  EXPECT_EQ(1, Factorial(-5));
  EXPECT_EQ(1, Factorial(-1));
  EXPECT_TRUE(Factorial(-10) > 0);
}

int main(int argc, char **argv) {
  testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

プリコンパイル方式でもテストの登録忘れはありませんが、この方法でも登録を忘れることがありません。テスト定義の方法が通常の関数定義とは異なる書式になることに慣れることができるのであれば、この方式で負担が少なくテストを書けるようになるでしょう。

以下のようにビルドします。

% g++ -o test-factorial test-factorial.cpp -lgtest

バイナリを実行するとテストが走ります。

% ./test-factorial
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from FactorialTest
[ RUN      ] FactorialTest.1
[       OK ] FactorialTest.1 (0 ms)
[----------] 1 test from FactorialTest (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (0 ms total)
[  PASSED  ] 1 test.

上記の例では触れていませんが、フィクスチャ(setup/teardown)を使う場合は、テストクラス名を揃える必要があるなど、同じグループのテストを作る場合は重複する部分がでてしまいます。例えば、QTestLibのようにクラス内にテストを定義する方法では以下のようになります。

1
2
3
4
5
6
7
8
9
class MyTest
{
   void setup() {...}
   void teardown() {...}

   void test1() {...}
   void test2() {...}
   void test3() {...}
}

一方、Google Testの場合は、スコープが使えず、以下のようにクラス名を複数回書く必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FooTest : public testing::Test {
 protected:
  virtual void SetUp() { b_.AddElement(3); }

  Foo a_;
  Foo b_;
};

TEST_F(FooTest, InitializesCorrectly) {
  EXPECT_TRUE(a_.StatusIsOK());
}

TEST_F(FooTest, ReturnsElementCountCorrectly) {
  EXPECT_EQ(0, a_.size());
  EXPECT_EQ(1, b_.size());
}

マクロを使っている場合は、間違ったテストクラス名を指定するなどコンパイルエラーになったときに意味の分からないエラーメッセージを目にすることがあるというのも注意しなければいけないポイントです。エラーメッセージを使えないと問題を発見することが難しくなります。

Boost Test Library

次に、Boost Test Libraryです。やり方はGoogle Testとだいたい同じで、Boost Test LibraryではBOOST_AUTO_TEST_CASEを使います。

1
2
3
4
5
6
7
8
9
10
11
// test-add.cpp
#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE AddTest
#include <boost/test/unit_test.hpp>

int add( int i, int j ) { return i+j; }

BOOST_AUTO_TEST_CASE( add_test )
{
    BOOST_CHECK_EQUAL( add( 2,3 ), 5);
}

最初にBOOST_TEST_DYN_LINKBOOST_TEST_MODULEを定義しておくと、Boost Test Libraryではmain関数を定義する必要はありません。

以下のようにビルドします。

% g++ -o test-add test-add.cpp -lboost_unit_test_framework

バイナリを実行するとテストが走ります。

% ./test-add
Running 1 test case...

*** No errors detected

Google Testと同じくマクロが気にならない場合やBoostに慣れている場合はテストが書きやすいでしょう。

共有ライブラリから探す

マクロを利用する方法は言語の構文を工夫してテストを集めています。プリコンパイル方式では言語の構文はそのままで、コンパイル前に付加情報を加えることでテストを集めています。

一方、最後の共有ライブラリから探す方法ではコンパイル後の共有ライブラリから情報を取得してテストを集めます。この方式では、テストを共有ライブラリとして作成し、テスト実行用のコマンドからその共有ライブラリを読み込み、テストを実行します。こうすることにより、テスト側にテスト登録処理を埋め込む必要がなくなります*2。共有ライブラリの中からテストを見つける処理はテスト実行コマンドが頑張るからです。

WinUnit

最初はWinUnitです。やり方は共有ライブラリからテストを集める方式なのですが、書き方はマクロを使う方式です。テストを定義するときはBEGIN_TESTEND_TESTで囲みます。

1
2
3
4
5
6
7
#include "WinUnit.h"

BEGIN_TEST(AddTest)
{
   WIN_ASSERT_TRUE(3 == add(1, 2));
}
END_TEST

すでにGoogle TestやBoost Test Libraryで見たように、この使い方であれば、共有ライブラリにする必要はありません。マクロの中で一工夫することでテストの自動登録を実現できるからです。

WinUnitの利点はVisual C++で使いやすいことでしょう。マクロを使ったAPIが気にならないVisual C++開発者には有力な選択肢です。

Cutter

最後はCutterです。CutterはC言語用の単体テストフレームワークとして開発されていましたが、先日リリースされた1.1.0で大きくC++対応を強化しています。

CutterではWinUnitとは違いマクロを利用しません。通常通り関数を定義するとテストとして認識されます。ただし、すべての関数がテストとして認識されるのではなく、test_からはじまる名前の関数だけをテストとして認識します。

1
2
3
4
5
6
7
8
9
#include <cppcutter.h>

namespace calc
{
   void test_add()
   {
       cppcut_assert_equal(5, add(2, 3));
   }
}

マクロを利用してテストを自動登録する方式では、フィクスチャ定義時に名前を揃える必要がありましたが、Cutterでは以下のようにnamespace内にsetup()/teardown()を定義するだけです。namespaceでグループ化されたテスト全体でフィクスチャを共有します。

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

namespace calc
{
   void setup() {...}
   void teardown() {...}

   void test_add() // calc::setup()/calc::teardown()が呼び出される
   {
       cppcut_assert_equal(5, add(2, 3));
   }

   void test_sub() // calc::setup()/calc::teardown()が呼び出される
   {
       cppcut_assert_equal(5, add(8, 3));
   }
}

この方式では通常のC++プログラムと同様にテストを書くことができるため、新しくテストを書くことの敷居が低くなります。しかし、tes_などタイプミスをしてしまった場合に、どうしてテストが実行されないのかに気づきにくいという問題点があります。

テストも通常のプログラムと同様に開発したい場合はマクロを使わないこの方式がオススメです。

まとめ

C++用の各種xUnitでのテストの書き方を、方式毎に分類して紹介しました。どんなバックグラウンドを持っているかにより、選びやすいxUnitは変わるでしょう。Visual C++開発者であればWinUnitを選ぶことが多いでしょうし、Qt開発者であればQtTestLibを選ぶことが多いでしょう。しかし、バックグラウンドから選ぶだけではなく、テストの書きやすさも判断材料に加えてみてはいかがでしょうか。

継続して開発すればそれに伴ってテストも増えていきます。しかし、テストは面倒くさがって飛ばしてしまいがちです。新しくテストを書く敷居が下がれば、テストを面倒くさがることが少なくなり、安心して開発を続けていくための土台を固めることができます。新しくテストを書く敷居を下げることは開発を継続するのであれば割に合うということです。

今回は「新しいテストの書きやすさ」を軸に様々なxUnitのやり方を紹介しました。C++用xUnitを選択する時の参考にしてみてください。

念のため書きますが、オススメはCutterです。

*1 プリコンパイルにはmoc(メタオブジェクトコンパイラ)を使います。

*2 「これがテストだよ」という目印は埋め込む必要があります。

タグ: Cutter | テスト
2009-11-07

Cutter 1.0.8リリース

本日、C言語用単体テストフレームワークであるCutterの新バージョン1.0.8が肉リリースされました。

新機能

先日の2つの記事(その1その2)でも紹介しましたが、1.0.8の重要な新機能はHTTPインターフェースのテスト機能 SoupCutter です。SoupCutter では GNOMEプロジェクトで開発されている HTTPサーバー・クライアントライブラリの libsoup をバックエンドに利用しており、HTTPサーバーやクライアントプログラムのテストを簡単に記述できるようになっています。

SoupCutter の使い方は、前回の紹介記事に詳しく書いてあるので是非これを参考に使ってみてください。この記事では SoupCutterの使用例として groonga のHTTPインターフェースのテストを作成しているのですが、SoupCutter を使った HTTP インターフェースのテストは実際に groonga 本体にも取り込まれています。

また、1.0.8からFedoraのrpmパッケージや、Mac OS Xのportsパッケージ(MacPorts)、Debian、Ubuntuのdebパッケージもサポートするようになったので、これまでよりも手軽に Cutter を導入できるようになりました。

まとめ

汎用的なHTTPのライブラリを使ったとしても、C言語でHTTPインターフェースのテストを開発しようと思うと一手間かかってしまうのではないかと思います。しかし、HTTPのテストに特化したSoupCutterを利用すれば簡潔にテストを記述できる上に、その気になれば libsoup の豊富な機能をフル活用することもできるようになります。

ますます便利になった Cutter を使って、皆さんが関わっているプロジェクトのテストを作成してみませんか?

つづき: 2009-12-21
タグ: Cutter | テスト
2009-08-29

SoupCutter で全文検索エンジンgroongaのHTTPインターフェースのテスト作成

前回に引き続き、クリアコードインターン記事の2回目です。前回の記事で紹介したCutterのHTTPテストモジュールであるSoupCutterを使って、全文検索エンジンgroongaのHTTPインターフェースのテストを作成したので、今回はその紹介をしたいと思います。SoupCutterが実際どのように使えるかという実例として、よい題材なのではないかと思います。

全文検索エンジンgroongaはHTTPサーバー機能を備えており、Webブラウザからアクセスすることでテーブルを作成したり、データベースの中身を調べたりすることができます。このようにブラウザからデータベースを管理するために、groongaではデータベースを操作するための基本的なAPIをHTTPリクエストによって呼び出せるようにしています。例えば、localhost:10041 で groonga のサーバーを実行しているときに、http://localhost:10041/table_list を GET すると、テーブルの一覧を取得することができます。

Cutterでテスト開発:下準備

それでは、Cutter でどのようにしてテストを開発していくかを見ていきましょう。

今回は groonga のHTTPサーバー機能のテストを行うため、まずは groonga でHTTPサーバーを走らせなければなりません。Cutterには外部コマンドを簡単に扱うことができる GCutEgg というオブジェクトがあります。groonga でポート 4545 を listen する HTTPサーバーを起動するには、以下のようなコマンドを実行します。

groonga -s -p 4545 -n /path/to/dbfile

このコマンドをテストのプログラムから実行しなければなりません。GCutEgg を使うと、以下のように簡単にコマンドを実行することができます。

1
2
3
GCutEgg *egg = gcut_egg_new("groonga", "-s", "-p", "4545",
                            "-n", "/path/to/dbfile", NULL);
gcut_egg_hatch(egg, NULL);

たったこれだけで、簡単に groonga のHTTPサーバーを準備することができました。このサーバーはテストの間は実行していて、テストが終わるごとに終了してほしいので、setup で実行を始めて、tear down で終了してあげればよいでしょう。

また、前回の記事で紹介した Cutter のHTTPクライアント SoupCutClient も setup で準備しておくとよいでしょう。

1
2
client = soupcut_client_new();
soupcut_client_set_base(client, "http://localhost:4545/");

soupcut_client_set_base で SoupCutClient にベースURIを設定しておくことで、実際にGETリクエストを送信するときのURI指定で楽をすることができます。SoupCutter では、soupcut_client_get(client, "http://localhost:4545/path/to/something") のようにGETリクエストを送ることができるのですが、ベースURIを設定ておけば soupcut_client_get(client, "/path/to/something") と書くだけで、 http://localhost:4545/path/to/something にGETリクエストを送ることができるようになります。

さらにもうひとつ。GCutEgg と SoupCutClient はどちらも GLib のオブジェクトとして実装されており、解放するときは g_object_unref を呼ぶだけでデストラクタが呼ばれ、適切にオブジェクトを解放してくれます。Cutter では、オブジェクトを破棄する関数と共にオブジェクトを登録しておくと、テストの tear down 時に自動でオブジェクトを解放してくれるという便利機能があります。どうやるかというと、下記のようにするだけです。

1
2
cut_take(client, g_object_unref);
cut_take(egg, g_object_unref);

またこれらは、GLibをサポートしたGCutterの関数を使うと、

1
2
gcut_take_object(G_OBJECT(client));
gcut_take_object(G_OBJECT(egg));

と書くこともできます。 これで client と egg は自動的に tear down 時に解放されるようになります。Cutterでは適切な下準備をしておくと、tear down 用の関数でわざわざ後片付けをしなくてもOKです。便利ですね。

ここまでをまとめると、テストのセットアップは次のように書くことができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static GCutEgg *egg;
static SoupCutClient *client;

void
cut_setup(void)
{
  client = soupcut_client_new();
  soupcut_client_set_base(client, "http://localhost:4545/");
  gcut_take_object(G_OBJECT(client));

  egg = gcut_egg_new("groonga", "-s", "-p", "4545", "-n", "/tmp/http.db", NULL);
  gcut_egg_hatch(egg, NULL);
  gcut_take_object(G_OBJECT(egg));

  g_usleep(G_USEC_PER_SEC); /* groonga の listen が完了するまで適当な時間待つ */
}

SoupCutterでテスト開発:HTTPサーバーが実行できているかのテスト

まずは簡単なところからテストしていきましょう。groongaのHTTPサーバーはルートにGETリクエストを送ると、本文無しで 200 STATUS OK を返してくるのでこれをテストしてみます。

1
2
3
4
5
6
7
8
9
void
test_get_root(void)
{
  soupcut_client_get(client, "/", NULL); /* http://localhost:4545/ を GET */

  soupcut_client_assert_response(client); /* status code は 2XX かチェック */
  soupcut_client_assert_equal_content_type("text/javascript", client); /* Content-Type をチェック */
  soupcut_client_assert_equal_body("", client); /* 本文が空かをチェック */
}

このように、非常に簡潔にテストを書くことができます*1

もう1つテストを書いてみましょう。groongaのHTTPサーバーは、/status にリクエストを送ると、{"starttime":1251190614,"uptime":39} というようにサーバーが開始した時刻とuptime をJSON形式でレスポンスとして応答します。starttimeもuptimeも開始した時刻や現在時刻によって刻々と変化するため、単純に assert_equal_body で期待した文字列と一致するかどうかを調べるには無理があります。このような要求に答えるために、SoupCutterでは正規表現に本文がマッチするかをテストできる soupcut_client_assert_match_body という関数を提供しています。

1
2
3
4
5
6
7
8
9
10
void
test_get_status(void)
{
  soupcut_client_get(client, "/status", NULL);

  soupcut_client_assert_response(client);
  soupcut_client_assert_equal_content_type("text/javascript", client);
  soupcut_client_assert_match_body("{\"starttime\":\\d+,\"uptime\":\\d+}",
                                   client);
}

soupcut_client_assert_match_body を利用すると、このようにして /status をGETしたときのテストを実装することができます。

このように、柔軟なテストも簡単に作成できるのが Cutter の特徴であり、開発方針でもあります。

その他のHTTPインターフェースのテストも、groongaの側でテーブルを作っておいたりカラムを作っておいたりというコードを書かなければならないことを除けば、ほとんど上記の2つのテストと同様に開発してゆくことができます。テーブルを作成する API は、/table_create にクエリーパラメータとしてテーブル作成に必要な情報を渡すことで呼び出すことができますが、これも SoupCutter では次のように簡潔に書くことができます。

1
2
3
4
5
6
7
8
soupcut_client_get(client,
                   "/table_create",
                   "name", "newtable1",
                   "flags", flags,
                   "key_type", "Int8",
                   "value_type", "Object",
                   "default_tokenizer", "",
                   NULL);

まとめ

今回はテストを開発する実例を通して、SoupCutter の使い方について紹介しました。SoupCutter を使って開発された groonga のテストは、実際に groonga のレポジトリにも取り込まれています。

SoupCutter を含めた Cutter は今週中にリリース予定なので、是非みなさん使ってみてください。

*1  関数名がやや長いのは御愛嬌ということで ;-)

タグ: テスト | Cutter
2009-08-26

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|
タグ: