pcunit_mock.rbを使ってC言語用のモック(スタブ)を生成する(シンボル重複エラーも解決)
pcunit_mock.rbについて
pcunit_mock.rbはPCUnitで使えるモック(スタブ)生成スクリプトです。
pcunit_mock.rbの主な機能は以下のとおりです。
- モックのソースを生成し、それをテストプロジェクトのビルドに追加することでモックが使用可能になる。
- エクスペクテーション(期待される引数や呼び出し回数や戻り値など)の設定APIを提供
- コールバック関数の登録APIを提供
使い方はREADMEのpcunit_mock.rbを参照してください。
id:ylgbk さんにpcunit_mock.rbの使用例を書いていただいたのでリンクを貼っておきます。わかりやすいモックの解説もあります。
ETロボコンにおけるモックの役割と作り方 - .logbook
ETロボコンで使うAPIをモック化してみた - .logbook
pcunit_mock.rbは、Unity用のモック生成ツールであるCMockを参考にしました。機能的には、CMockとほぼ同じです。
ただし、CMockは生成するモックのコードの内部でエクスペクテーションのオブジェクトの確保にmallocを使用しています。
pcunit_mock.rbは、エクスペクテーションのオブジェクトの確保はテストコード側でローカル変数やstatic変数で行うようにすることでmalloc使用を回避しています。PCUnitは、PC上でテストしたテストコードを、ターゲット上で動作させるのに移植しやすいことをコンセプトにした単体テストツールなので、生成するモックも標準関数依存を取り除いています。
生成コードの解説
モックのAPIで例にあげた、Foo.hのfunc関数のモックのコードが実際にどんな処理をしているか見てみます。
このヘッダファイルFoo.hのモックを生成します。
#ifndef FOO_H_INCLUDED #define FOO_H_INCLUDED int func(int a, const char *str); #endif /* FOO_H_INCLUDED */
pcunit_mock.rbでモックを生成します。
$ ruby path/to/pcunit_mock.rb Foo.h
生成されたモックのヘッダファイルmock_Foo.hは以下のとおりです。
#ifndef MOCK_FOO_H_INCLUDED #define MOCK_FOO_H_INCLUDED #include "Foo.h" void mock_Foo_init(void); #define mock_Foo_verify() mock_Foo_verify_aux(__FILE__, __LINE__) void mock_Foo_verify_aux(const char *file, unsigned int line); typedef struct { int retval; struct { int a; const char *str; } expected; struct { unsigned int a:1; unsigned int str:1; } ignored; } func_Expectation; typedef int (*func_Callback)(int a, const char *str); #define func_expect(expectations, num) func_expect_aux(expectations, num, __FILE__, __LINE__) void func_expect_aux(const func_Expectation *expectations, int num, const char *file, unsigned int line); #define func_set_callback(callback, expected_num_calls) func_set_callback_aux(callback, expected_num_calls, __FILE__, __LINE__) void func_set_callback_aux(func_Callback callback, int expected_num_calls, const char *file, unsigned int line); int func_num_calls(void); #endif /* MOCK_FOO_H_INCLUDED */
呼び出し側のファイル名と行番号がわかるようにAPIがマクロ化されていること以外は、マニュアルに書いてあるとおりです。
生成されたモックのソースファイルmock_Foo.cを見てみます。(実際の出力ファイルに適宜改行とコメントを追記しています)
#include "mock_Foo.h" #include "PCUnit/PCUnit.h" struct mock_Foo_t { /* func_set_callbackで登録した関数ポインタ */ func_Callback func_funcptr; /* 予期されたfuncの呼び出し回数 */ int func_expected_num_calls; /* 実際にfuncを呼び出した回数 */ int func_actual_num_calls; /* func_expectで登録したエクスペクテーションの配列 */ const func_Expectation *func_expectations; /* func_set_callbackまたはfunc_expectを呼んだファイル名 */ const char *func_file; /* func_set_callbackまたはfunc_expectを呼んだ行番号 */ unsigned int func_line; }; /* モックを管理するオブジェクト */ static struct mock_Foo_t mock_Foo; void mock_Foo_init(void) { char *p = (char *) &mock_Foo; size_t i; /* フリースタンディング環境で実行されることを考慮して、 * あえてmemsetを使用せず0クリア */ for (i = 0; i < sizeof mock_Foo; i++) { *(p++) = 0; } } #ifdef _MSC_VER #define LINE_FORMAT "(%u) " #else #define LINE_FORMAT ":%u" #endif void mock_Foo_verify_aux(const char *file, unsigned int line) { /* verifyはteardownで呼ばれるので、テスト関数でアサーション失敗した場合も呼ばれる。 * 2重に失敗を出力しないように、テスト関数で失敗していた場合は即抜ける。 */ if (PCU_test_has_failed()) { return; } if ((mock_Foo.func_expectations || mock_Foo.func_funcptr) && mock_Foo.func_expected_num_calls >= 0) { /* funcの呼び出し回数が予期された回数より少なかった場合に失敗を出力 */ PCU_ASSERT_EQUAL_MESSAGE(mock_Foo.func_expected_num_calls, mock_Foo.func_actual_num_calls, PCU_format("%s" LINE_FORMAT ": Check the number of calls of func().", file, line)); if (PCU_test_has_failed()) { return; } } } static const char *mock_Foo_ordinal(int num) { switch (num) { case 1: return "st"; case 2: return "nd"; case 3: return "rd"; default: return "th"; } } /* funcのテストダブル関数 */ int func(int a, const char *str) { int ret; if (mock_Foo.func_expectations && mock_Foo.func_expected_num_calls >= 0) { /* func_expectでエクスペクテーションを設定した場合 */ const func_Expectation *e; /* funcの呼び出し回数が予期された回数を超えた場合に失敗を出力 */ PCU_ASSERT_OPERATOR_MESSAGE(mock_Foo.func_expected_num_calls, >, mock_Foo.func_actual_num_calls, PCU_format("%s" LINE_FORMAT ": Check the number of calls of func().", mock_Foo.func_file, mock_Foo.func_line)); e = mock_Foo.func_expectations + mock_Foo.func_actual_num_calls; /* 引数の無視設定がされていなければ、引数の値が予期された値かどうかチェック */ if (!e->ignored.a) { PCU_ASSERT_EQUAL_MESSAGE(e->expected.a, a, PCU_format("%s" LINE_FORMAT ": Check the parameter 'a' of func() called for the %d%s time.", mock_Foo.func_file, mock_Foo.func_line, mock_Foo.func_actual_num_calls, mock_Foo_ordinal(mock_Foo.func_actual_num_calls))); } if (!e->ignored.str) { PCU_ASSERT_STRING_EQUAL_MESSAGE(e->expected.str, str, PCU_format("%s" LINE_FORMAT ": Check the parameter 'str' of func() called for the %d%s time.", mock_Foo.func_file, mock_Foo.func_line, mock_Foo.func_actual_num_calls, mock_Foo_ordinal(mock_Foo.func_actual_num_calls))); } /* 戻り値を設定 */ ret = e->retval; } else if (mock_Foo.func_funcptr) { /* func_set_callbackで関数ポインタを設定した場合 */ if (mock_Foo.func_expected_num_calls >= 0) { /* funcの呼び出し回数が予期された回数を超えた場合に失敗を出力 */ PCU_ASSERT_OPERATOR_MESSAGE(mock_Foo.func_expected_num_calls, >, mock_Foo.func_actual_num_calls, PCU_format("%s" LINE_FORMAT ": Check the number of calls of func().", mock_Foo.func_file, mock_Foo.func_line)); } /* 登録した関数を呼び出す */ ret = mock_Foo.func_funcptr(a, str); } else { /* func_expect/func_set_callbackで設定していないのにfuncが呼ばれた場合は失敗 */ PCU_FAIL("Call func_expect() or func_set_callback()."); } mock_Foo.func_actual_num_calls++; return ret; } void func_expect_aux(const func_Expectation *expectations, int num, const char *file, unsigned int line) { mock_Foo.func_expectations = expectations; mock_Foo.func_expected_num_calls = num; mock_Foo.func_file = file; mock_Foo.func_line = line; } void func_set_callback_aux(func_Callback callback, int expected_num_calls, const char *file, unsigned int line) { mock_Foo.func_funcptr = callback; mock_Foo.func_expected_num_calls = expected_num_calls; mock_Foo.func_file = file; mock_Foo.func_line = line; } int func_num_calls(void) { return mock_Foo.func_actual_num_calls; }
funcのテストダブル関数では、func_expectでエクスペクテーションを設定した場合とfunc_set_callbackでコールバック関数を設定した場合とで処理を分岐します。
エクスペクテーションを設定した場合は、呼び出し回数と引数のチェックをし、設定された戻り値を返します。
コールバック関数が登録されている場合は、呼び出し回数のチェックをし、コールバック関数を呼び出します。
どちらも設定されていなければ失敗を出力します。
テストダブル関数とオリジナル関数の共存
C言語のユニットテストでテストダブル関数(スタブやモック)を使うと、テストダブル関数とオリジナルの関数のシンボル重複のリンクエラーが問題になります。*1
http://hccweb6.bai.ne.jp/~hfk45601/ctdd_html/c_tdd1.htmlのサイトの4の(1)の図のような場合です。
同サイトの5.問題点に対する解決方法 や テスト駆動開発による組み込みプログラミングの9章にあるようにプロダクトコードのインターフェイスを関数ポインタに変更すれば解決できます。が、プロダクトコードに手を加えたくない場合もあります。
pcunit_mock.rbでは、プロダクトコードに#ifdef TESTを埋め込んだりインターフェイスの関数ポインタ化をせず、テストダブル関数とオリジナル関数を共存させることができます。
先ほどの例のFoo.hを使って、func()のテストダブル関数とオリジナル関数(Foo.cに定義されている)を共存させるモックを生成してみます。pcunit_mock.rbコマンドの-sオプションを使います。
$ ruby path/to/pcunit_mock.rb Foo.h -s
すると、mock_Foo.cのコードの一部が以下のように変わります。
#include "mock_Foo.h" #include "PCUnit/PCUnit.h" /* funcをfunc_originalに置換して、オリジナルのソースをインクルードすることで、 * シンボル重複を回避 */ int func_original(int a, const char *str); #define func func_original #include "Foo.c" /* 下でfuncのテストダブル関数を定義するので、funcのマクロ定義を解除 */ #undef func struct mock_Foo_t { func_Callback func_funcptr; int func_expected_num_calls; int func_actual_num_calls; const func_Expectation *func_expectations; const char *func_file; unsigned int func_line; }; static struct mock_Foo_t mock_Foo = { func_original, /* デフォルトでオリジナル関数が設定される */ -1, /* 呼び出し回数は無制限の設定 */ }; void mock_Foo_init(void) { char *p = (char *) &mock_Foo; size_t i; for (i = 0; i < sizeof mock_Foo; i++) { *(p++) = 0; } /* デフォルトでオリジナル関数が設定される */ mock_Foo.func_funcptr = func_original; mock_Foo.func_expected_num_calls = -1; } void mock_Foo_verify_aux(const char *file, unsigned int line) { if (PCU_test_has_failed()) { goto end; } if ((mock_Foo.func_expectations || mock_Foo.func_funcptr) && mock_Foo.func_expected_num_calls >= 0) { PCU_ASSERT_EQUAL_MESSAGE(mock_Foo.func_expected_num_calls, mock_Foo.func_actual_num_calls, PCU_format("%s" LINE_FORMAT ": Check the number of calls of func().", file, line)); if (PCU_test_has_failed()) { goto end; } } end: /* 関数ポインタ設定をオリジナル関数に戻す */ mock_Foo_init(); } /* 以下、変更なし */
ソースファイル(Foo.c)をインクルードするというのは通常のCのコーディングではやらないことですが、シンボル重複回避のためにあえて行っています。
やっていることはhttp://hccweb6.bai.ne.jp/~hfk45601/ctdd_html/c_tdd1.htmlの5.問題点に対する解決方法 の手順1と2を自動化したものです。これによりfunc()の呼び出しはデフォルトでオリジナルの関数が呼ばれ、func_expect()やfunc_set_callback()を設定した時だけモックやスタブとして振る舞うようにすることができます。
注意点としては、Foo.cをmock_Foo.cがインクルードしているので、テストプロジェクトのビルド対象からFoo.cを外す必要があります(外さないとfunc()のシンボル重複エラーになる)。また、mock_Foo.cがFoo.cに依存するようなビルドの設定にしないと、Foo.cを修正してもmock_Foo.cのコンパイルがされずFoo.cの修正が反映されないということが起こります(自動的に依存関係を設定してくれるビルドツールならば問題ない)。
オリジナルの関数は、テストダブル関数を経由してmock_Foo.func_funcptrに登録された関数として呼ばれるので、多少のオーバーヘッドはありますがユニットテストなので問題ないかと思います。
C標準関数やシステムコールのモック化
pcunit_mock.rbを使って、C標準関数やシステムコールなど、ソースがなく処理系に組み込まれている関数のモック化もできますが、少し工夫が必要になります。
モック化したい標準関数はmallocとfreeとします。
まず、標準関数をプリプロセッサで置換するヘッダーファイルを新規作成します。このファイルをMyStdFunc.hとして、置換後の関数名は、my_のプレフィックスを付けることにします。my_malloc/my_freeのプロトタイプ宣言もしておきます。
#ifndef MY_STD_FUNC_H_INCLUDED #define MY_STD_FUNC_H_INCLUDED /* 標準ヘッダ内で置換が発生しないように、先にインクルードしておく */ #include <stdlib.h> /* 標準関数呼び出しを置換するマクロ */ #define malloc my_malloc #define free my_free /* 置換後の標準関数のプロトタイプ宣言。my_のプレフィックスをつけた。 * これらのモックを生成する */ void *my_malloc(size_t size); void my_free(void *ptr); #endif /* MY_STD_FUNC_H_INCLUDED */
このヘッダーファイルをテストプロジェクトのビルド設定で、すべてのソースファイルの先頭でインクルードされるようにします。
例えば、gccなら-includeオプションを指定、Visual StudioならプロジェクトのプロパティでC/C++→詳細設定→必ず使用されるインクルードファイル(/FIオプション)でMyStdFunc.hを指定します。
これで、プロダクトコードを変更することなく、すべてのmallocとfreeの呼び出しがmy_mallocとmy_freeに置換されます。*2
そして、pcunit_mock.rbでMyStdFunc.hのモックのソースmock_MyStdFunc.hとmock_MyStdFunc.cを生成すれば、標準関数をモック化することができます。
テストコードでは、例えば以下のようにmallocを失敗させるテストを書くことができます。
static int setup(void) { mock_MyStdFunc_init(); return 0; } static int teardown(void) { mock_MyStdFunc_verify(); return 0; } static void test_Hoge_new_fail(void) { Hoge *hoge; my_malloc_Expectation e[1] = {{0}}; /* メモリ確保失敗の設定をする */ e[0].retval = NULL; e[0].expected.size = sizeof(Hoge); my_malloc_expect(e, sizeof e / sizeof e[0]); /* Hoge_newの内部で呼び出すmallocが失敗し、NULLを返す */ hoge = Hoge_new(); PCU_ASSERT_PTR_NULL(hoge); }
デフォルトで標準関数が呼ばれるようにする
すべてのmy_malloc/my_freeの呼び出し箇所でモックの設定をするのは面倒で、デフォルトでは標準関数のmalloc/freeを呼び出して欲しいと思います。
そこで、前述したようにpcunit_mock.rbに-sを付けてモックを生成します。すると、MyStdFunc.cをインクルードするソースが生成されます。MyStdFunc.cはまだ存在しないので、MyStdFunc.cを新規作成します。
MyStdFunc.cの内容は以下のようにします。このファイルはmock_MyStdFunc.cからインクルードのみで使用されるので、ビルド対象に追加しません。(マクロ定義だけなので追加しても実害はないですが。)
#undef malloc #undef free #define my_malloc_original malloc #define my_free_original free
これで、テストコードでmy_(malloc|free)_expectかmy_(malloc|free)_set_callbackでモックの設定をしなければ、デフォルトで標準関数のmalloc/freeが呼ばれるようになります。
static void test_Hoge_new_OK(void) { Hoge *hoge; /* my_malloc_expect/my_malloc_set_callbackでモックの設定しなければ、mallocの実体が呼ばれる */ hoge = Hoge_new(); PCU_ASSERT_PTR_NOT_NULL(hoge); /* my_free_expect/my_free_set_callbackでモックの設定しなければ、freeの実体が呼ばれる */ Hoge_delete(hoge); }
ちなみに、モック化したい標準関数が増えた場合は、MyStdFunc.hとMyStdFunc.cにmallocと同じように追加し、pcunit_mock.rb -sでモックを生成し直せばOKです。
*1:Unity/CMockの場合はmain生成自動化を使うと、テストコード毎にmainが生成され、実行ファイルが別々になるので問題になりません。しかし、ターゲット上で動かすときは、実行ファイルをひとつにしたい。
*2:この方法は、テスト駆動開発による組み込みプログラミング の7.4 Cでだますにはどうするか の、プリプロセッサによる置き換えの例でCppUTestのmalloc置換のやり方を参考にしました。
PCUnit 1.7.0 リリース
PCUnit 1.7.0をリリースしました。
変更点は以下の通りです。
- pcunit_mock.rbユーティリティでモック(スタブ)を生成できるようになった。
- アサートマクロにPCU_ASSERT_MEMORY_EQUAL/PCU_ASSERT_MEMORY_NOT_EQUAL追加。
- pcunit_register.rbに検索対象から除外するオプション(-e)を追加。
- PCU_console_runの戻り値をPCU_runに合わせて追加。
1.7.0のタグをつけた後のコミットだけど、pcunit_register.rbでテストコードを検索するディレクトリを指定するオプション(-d)を複数指定できるようにしました。
1.6.0の変更点もブログに書いてなかったので、今さらですがここに書いておきます。主にJenkinsなどのCIツール対応。
- PCU_runの戻り値を追加。テストに失敗がなければ0、1つでも失敗があれば非0を返す。
- PCUnitメモ(Hishidama's C-lang PCUnit Memo) でご指摘いただきました。ありがとうございました。
- PCU_set_verboseを追加。テスト結果出力を冗長モード(成功/スキップしたテストも表示)にする。XML出力で集計するため。
- pcunit_xml_output.rbを追加。テスト結果をJUnit形式のXMLファイルで出力する。
- mallocを使用しないようにした。
- C99のvsnprintfの使用を、C89のvsprintfに変更した。
pcunit_mock.rbについての記事もそのうち書きます。CMockを参考にしたので、C言語用モック生成ツールとしてはそれなりに使えると思います。
WindowsでRuby/Tkを使うためのメモ
Ruby-mswin32 + ActiveTclの場合
ActiveStateからActiveTcl for Windows 8.4をダウンロードしてインストールする。
8.5ではなく8.4を選ぶこと。(現在のRuby-mswin32(ruby-1.9.2-p136-i386-mswin32)がtk84.dllをリンクするようになっていた)
http://www.activestate.com/activetcl/downloads
C:\Tclにインストールしたとして、C:\Tcl\binにパスを通す。
rubyはmswin32版を使用すればtcltklib.soが同梱されている。
http://www.garbagecollect.jp/ruby/mswin32/ja/download/release.html
ダウンロードして適当なディレクトリに展開してbinにパスを通す。
Cygwinの場合
CygwinのTcl/Tkが8.5.11からXサーバーに依存するようになって使いづらくなってしまった。(このせいでCygwinのgitでgitkが使えなくなったという人もいたらしい)
http://cygwin.com/ml/cygwin/2012-02/msg00115.html
Cygwinのsetup.exeで以下のパッケージをインストールする。依存するパッケージも一緒にインストールする。
.bashrcに以下のように環境変数DISPLAYを追加する。Windowsの環境変数でもいい。
export DISPLAY=':0.0'
Cygwinを起動してホームディレクトリでtouch .startxwinrcで空の.startxwinrcファイルを作成する。startxwinでxtermを起動しないようにするため。
startxwinを実行する。
wishを実行してウィンドウが表示されたらTcl/TkはOK。
ruby -rtk -e 'p Tk::TK_VERSION'を実行してTkのバージョンが表示されたらRuby/TkはOK。
ただCygwin版はRuby/Tkを使う前にstartxwinの実行が必要だし、ディレクトリ指定にC:\hoge\piyoという書き方が使えないのでRuby-mswin32 + ActiveTclを使ったほうがいいかもしれない。
pcunit_template.rb用のGUIフロントエンドを作った
[twitter:@goyoki]さんからEclipseでPCUnitを使う場合は、ターミナルからではなくEclipseから直接pcunit_template.rbを実行できたらいいというご意見をいただきました。
そこでpcunit_template.rb用のGUIフロントエンドを作ってみた。マルチプラットフォームで使えて簡単に実装できることを考えて、Ruby/Tkで作った。RubyとTcl/Tkのインストールが必要。
とりあえずgistに置いた。
pcunit_template_tk.rb
pcunit_template_tk.rbをダウンロードしてpcunit_template.rbと同じディレクトリに置いてください。
EclipseからPCUnitを使う方法はgoyokiさんの以下の記事を参照してください。
Eclipse CDTを使ったMacでのPCUnitの環境構築
Eclipseでpcunit_template_tk.rbを登録する
- メニューの実行→外部ツール→外部ツールの構成を選択
- 左側の"プログラム"を右クリック→新規
- 名前はpcunit_template_tk.rbを指定
- メインタブの"ロケーション"にrubyをフルパスで指定
- メインタブの"引数"にpcunit_template_tk.rbをフルパスで指定
- (WindowsでCygwinのRuby/Tkを使う場合は環境タブから環境変数DISPLAYに:0.0を指定。またCygwinのXサーバーが起動していること。Ruby-mswin32とActiveTclを使う場合は不要*1 )
実行するとこのようなダイアログが出る。
Suite Nameにテストスイート名をOutput Directoryに出力先ディレクトリを入力してGenerateボタンを押すとファイルを生成する。
Suite Nameはスペース区切りで複数指定できる。
Visual C++でpcunit_template_tk.rbを登録する
TortoiseHGをMATEデスクトップ環境のcajaファイルマネージャの右クリックメニューに出す方法
MercurialのGUIフロントエンドのTortoiseHGはマルチプラットフォームなのでLinuxでも使いたい。
TortoiseHGはGNOMEのnautilusファイルマネージャの右クリックメニューから起動できるらしいのだが、MATEデスクトップ環境のcajaでは右クリックメニューに出てこない。*1
cajaもほとんどnautilusと同じだからできるはず。
Linux Mint 12 Lisaで、 http://d.hatena.ne.jp/katono123/20120401/1333209590 のようにapt-lineにdebianを追加してある環境で試した。
まず、tortoisehg-nautilusとpython-cajaをインストールする。
$ sudo apt-get install tortoisehg-nautilus $ sudo apt-get install python-caja
次に /usr/lib/nautilus/extensions-2.0/python/nautilus-thg.py を~/.caja/python-extensions/にcaja-thg.pyとリネームしてコピーする。
$ mkdir -p ~/.caja/python-extensions $ cp /usr/lib/nautilus/extensions-2.0/python/nautilus-thg.py ~/.caja/python-extensions/caja-thg.py
caja-thg.pyを編集してnautilusをすべてcajaと置換する。
$ vim caja-thg.py :%s/nautilus/caja/g :wq
caja -qでいったん終了させてから再度cajaを起動すると、右クリックメニューに「TortoiseHG」が追加される。
TODO
ワークベンチのメニューのリポジトリ→エクスプローラやリポジトリ一覧のリポジトリを右クリック→エクスプローラで、cajaではなくnautilus3が起動してしまうのをcajaにするにはどうすればいいのだろうか?
*1:tortoisehgのパッケージがインストールされていれば右クリックメニューでは出なくてもthgコマンドでワークベンチを起動することはできる。ワークベンチさえ起動すればほとんど何でもできるので十分ではあるのだが。
Linux Mint 12 LisaのMATEデスクトップ環境で、cajaファイルマネージャからターミナルを開けるようにする
Linux Mint 12 Lisaをインストールしたときのメモ。
デフォルトのデスクトップ環境であるGNOME3では、nautilusファイルマネージャの任意のディレクトリから右クリックメニューでターミナルを開くことができる。(nautilus-open-terminalがインストールされている)
ただ、nautilusがバージョン3から少し使いづらくなったので、GNOME2のフォークであるMATEデスクトップ環境を選択してみた。
nautilusのバージョン2に相当するものがcaja。(リネームしただけでほとんどnautilus2と同じにみえる)
cajaからターミナルを開くプラグインがcaja-open-terminalだがLinux Mint 12 Lisaのパッケージには入っていなかった。
Linux MintのDebianパッケージには入っていたので/etc/apt/sources.listに以下を追加した(とりあえず必要そうなimportだけ)。
deb http://packages.linuxmint.com/ debian import
その後、$ sudo apt-get update でパッケージリストを更新するとcaja-open-terminalが追加されるので $ sudo apt-get install caja-open-terminal でインストールする。
caja -qでいったん終了させてから再度cajaを起動すると、右クリックメニューに「端末の中に開く」が追加されている。
UMLの状態遷移図をC言語のStateパターンで実装&単体テストしてみる
UMLの状態遷移図(ステートマシン図/ステートチャート図)の実装
サンプルとして、こちらの図11を実装した。
http://labo.mamezou.com/special/sp_002/sp_002_003.html
最上位の状態は「停止中」と「運転中」の2つの状態。「運転中」はコンポジット状態(入れ子のある状態)であり、サブ状態として「冷房」「暖房」「除湿」の3つの状態を持つ。
最上位の初期状態は停止中で、サブ状態の初期状態は冷房。ただし、運転中から停止中に戻る時にサブ状態は記憶され、次回運転中状態になった時は前回のサブ状態になる。
イベントは「運転開始」「運転停止」「運転切替」の3つ。
状態遷移図を実装する方法で一番簡単なのは、状態変数をswitch文で分岐する方法だと思うけど、ここではGoFのデザインパターンのStateパターンを参考にして実装してみた。
ソースのリポジトリは以下。
https://bitbucket.org/katono/cstatepattern
FSM.h
#ifndef FSM_H_INCLUDED #define FSM_H_INCLUDED struct State; /* 有限状態機械(FSM)クラス */ typedef struct FSM { const struct State *current_state; /* 現在の状態 */ const struct State *current_substate; /* 現在のサブ状態 */ const struct State *last_substate; /* サブ状態の前回の状態 */ } FSM; void FSM_init(FSM *self); void FSM_run(FSM *self); /* 運転開始イベント */ void FSM_stop(FSM *self); /* 運転停止イベント */ void FSM_switch(FSM *self); /* 運転切替イベント */ /* 状態遷移 */ void FSM_change_state(FSM *self, const struct State *new_state); /* 状態遷移(サブ状態用) */ void FSM_change_substate(FSM *self, const struct State *new_state); #endif /* FSM_H_INCLUDED */
有限状態機械(FSM)クラス定義。
FSMクラスはGoFのStateパターンでContextクラスに相当する。
3つのイベントはFSM_run(), FSM_stop(), FSM_switch()で処理される。
現在の状態を示す変数と状態遷移関数は、最上位の状態とサブ状態のそれぞれに必要となる。
C言語だからグローバルになってしまうけど、FSMのメンバ変数とFSM_change_state(), FSM_change_substate()はStateクラスのみに公開しているつもり。
State.h
#ifndef STATE_H_INCLUDED #define STATE_H_INCLUDED #include "FSM.h" /* 状態クラス */ typedef struct State { void (*entry)(FSM *fsm); /* 入状イベント */ void (*exit)(FSM *fsm); /* 出状イベント */ void (*event_run)(FSM *fsm); /* 運転開始イベント */ void (*event_stop)(FSM *fsm); /* 運転停止イベント */ void (*event_switch)(FSM *fsm); /* 運転切替イベント */ } State; void State_init(FSM *fsm); #endif /* STATE_H_INCLUDED */
状態(State)クラス定義。
Stateクラスは各イベントの仮想関数(関数ポインタ)を持つだけ。状態に入ったときに実行されるentryイベントと状態から出たときに実行されるexitイベントにも対応できるようにしておく。
FSM.c
#include <stdio.h> #include "FSM.h" #include "State.h" void FSM_init(FSM *self) { State_init(self); } /* 運転開始イベント */ void FSM_run(FSM *self) { if (self->current_state && self->current_state->event_run) { printf("Event: Run\n"); self->current_state->event_run(self); } } /* 運転停止イベント */ void FSM_stop(FSM *self) { if (self->current_state && self->current_state->event_stop) { printf("Event: Stop\n"); self->current_state->event_stop(self); } } /* 運転切替イベント */ void FSM_switch(FSM *self) { if (self->current_substate && self->current_substate->event_switch) { printf("Event: Switch\n"); self->current_substate->event_switch(self); } } /* 状態遷移(共通) */ static void change_state_common(FSM *self, const State **current, const State *new_state) { if (*current && (*current)->exit) { (*current)->exit(self); } *current = new_state; if (new_state && new_state->entry) { new_state->entry(self); } } /* 状態遷移 */ void FSM_change_state(FSM *self, const State *new_state) { change_state_common(self, &self->current_state, new_state); } /* 状態遷移(サブ状態用) */ void FSM_change_substate(FSM *self, const State *new_state) { change_state_common(self, &self->current_substate, new_state); }
FSMクラスの実装。
各イベントの処理は、現在の状態の仮想関数を呼び出す。
状態遷移関数は現在の状態のexitを呼び出し、現在の状態を更新して、新しい状態のentryを呼び出す。FSM_change_state()とFSM_change_substate()は使用するcurrent_*stateが違うだけで同じ処理なので共通化している。
State.c
#include <stdio.h> #include "State.h" static void StateStopped_run(FSM *fsm); static void StateRunning_entry(FSM *fsm); static void StateRunning_exit(FSM *fsm); static void StateRunning_stop(FSM *fsm); /* 各Stateの唯一のオブジェクト */ static const State state_Stopped = { 0 , 0 , StateStopped_run , 0 , 0 }; static const State state_Running = { StateRunning_entry , StateRunning_exit , 0 , StateRunning_stop , 0 }; /* 初期状態の設定 */ void State_init(FSM *fsm) { void init_last_substate(FSM *fsm); fsm->current_state = &state_Stopped; fsm->current_substate = 0; /* NULLだと何のイベントも処理しない */ init_last_substate(fsm); } /* * 停止中状態 */ /* 運転開始イベント */ static void StateStopped_run(FSM *fsm) { printf(" State: Stopped -> Running\n"); FSM_change_state(fsm, &state_Running); } /* * 運転中状態(コンポジット状態) */ /* 入状イベント */ static void StateRunning_entry(FSM *fsm) { /* サブ状態をNULLから前回の状態に遷移(サブ状態のentryがあれば呼ばれる) */ FSM_change_substate(fsm, fsm->last_substate); } /* 出状イベント */ static void StateRunning_exit(FSM *fsm) { /* 現在のサブ状態を保存 */ fsm->last_substate = fsm->current_substate; /* 運転中状態以外の時にサブ状態へイベントが飛ばないようにするために、 * サブ状態をNULLにする(サブ状態のexitがあれば呼ばれる) */ FSM_change_substate(fsm, 0); } /* 運転停止イベント */ static void StateRunning_stop(FSM *fsm) { printf(" State: Running -> Stopped\n"); FSM_change_state(fsm, &state_Stopped); }
最上位の2つのStateクラスの実装。
Stateクラスはデータを持たないのでstatic constによるシングルトンでオブジェクトを定義する。ここで定義したオブジェクトがGoFのConcreteStateクラスのオブジェクトに相当する。Stateオブジェクトの定義で0が指定されたイベントは、そのイベントが発生しても何も処理をしない。シングルトンなのでもしデータの操作をする必要がある場合はStateには持たせずFSMのメンバに追加する。
イベントの関数の中ではアクションや状態遷移を実装する。
状態遷移はFSM_change_state()で行う。FSM_change_state()の後はもう状態遷移し終わっているので何もせずに関数から戻ること。
コンポジット状態のentryとexitでは、サブ状態の設定をすること。
SubState.c
#include <stdio.h> #include "State.h" static void SubStateCooling_switch(FSM *fsm); static void SubStateWarming_switch(FSM *fsm); static void SubStateDehumidifying_switch(FSM *fsm); /* 各Stateの唯一のオブジェクト */ static const State substate_Cooling = { 0, 0, 0, 0 ,SubStateCooling_switch }; static const State substate_Warming = { 0, 0, 0, 0 ,SubStateWarming_switch }; static const State substate_Dehumidifying = { 0, 0, 0, 0 ,SubStateDehumidifying_switch }; /* サブ状態の初期状態の設定 */ void init_last_substate(FSM *fsm) { fsm->last_substate = &substate_Cooling; } /* * 冷房状態 */ /* 運転切替イベント */ static void SubStateCooling_switch(FSM *fsm) { printf(" SubState: Cooling -> Warming\n"); FSM_change_substate(fsm, &substate_Warming); } /* * 暖房状態 */ /* 運転切替イベント */ static void SubStateWarming_switch(FSM *fsm) { printf(" SubState: Warming -> Dehumidifying\n"); FSM_change_substate(fsm, &substate_Dehumidifying); } /* * 除湿状態 */ /* 運転切替イベント */ static void SubStateDehumidifying_switch(FSM *fsm) { printf(" SubState: Dehumidifying -> Cooling\n"); FSM_change_substate(fsm, &substate_Cooling); }
サブ状態の3つのStateクラスの実装。
サブ状態のオブジェクトにもStateを使う。
Stateオブジェクトやイベントの実装方法は最上位の場合と同じだが、サブ状態では状態遷移には専用のFSM_change_substate()を使う。
サブ状態の実装からは上位の状態について何も気にしなくていい。
main.c
#include <stdio.h> #include "FSM.h" int main(void) { int c; FSM fsm; FSM_init(&fsm); while ((c = getchar()) != EOF) { switch (c) { case 'x': FSM_run(&fsm); break; case 'y': FSM_stop(&fsm); break; case 'z': FSM_switch(&fsm); break; default: break; } } return 0; }
FSMクラスを使用したmain関数。標準入力からx, y, zの入力がそれぞれのイベントになる。
現在の状態を気にせずイベントを通知できる。
単体テスト
単体テストはPCUnitを使った。
どの状態の時にどのイベントが発生したらどの状態に遷移するか、のテストをする。
"単体"テストと言ってもFSM.c, State.c, SubState.cそれぞれのソースを別々にテストするわけじゃなく、これらをまとめて1つのFSMクラスをテストするという扱い。
TODO
テストしているのは状態遷移のテストだけで、アクションのテストと状態遷移時のentry, exitが正しく呼ばれているかのテストはできてない。この方法はそのうち考える。
FSMTest.c
#include "PCUnit/PCUnit.h" #include "../FSM.h" #include "../State.h" /* static変数を使うためにソースをインクルードする */ #include "../State.c" #include "../SubState.c" static FSM fsm; static int setup(void) { FSM_init(&fsm); return 0; } static void test_init(void) { PCU_ASSERT_PTR_EQUAL(&state_Stopped, fsm.current_state); PCU_ASSERT_PTR_NULL(fsm.current_substate); } static void test_run(void) { /* 変化しない */ FSM_switch(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Stopped, fsm.current_state); PCU_ASSERT_PTR_NULL(fsm.current_substate); FSM_run(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Cooling, fsm.current_substate); /* 変化しない */ FSM_run(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Cooling, fsm.current_substate); /* 変化しない */ FSM_run(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Cooling, fsm.current_substate); } static void test_stop(void) { /* 変化しない */ FSM_stop(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Stopped, fsm.current_state); PCU_ASSERT_PTR_NULL(fsm.current_substate); /* 変化しない */ FSM_switch(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Stopped, fsm.current_state); PCU_ASSERT_PTR_NULL(fsm.current_substate); FSM_run(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Cooling, fsm.current_substate); FSM_stop(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Stopped, fsm.current_state); PCU_ASSERT_PTR_NULL(fsm.current_substate); } static void test_switch(void) { FSM_run(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Cooling, fsm.current_substate); FSM_switch(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Warming, fsm.current_substate); FSM_switch(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Dehumidifying, fsm.current_substate); FSM_switch(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Cooling, fsm.current_substate); FSM_stop(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Stopped, fsm.current_state); PCU_ASSERT_PTR_NULL(fsm.current_substate); } static void test_history(void) { FSM_run(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Cooling, fsm.current_substate); FSM_switch(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Warming, fsm.current_substate); FSM_stop(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Stopped, fsm.current_state); PCU_ASSERT_PTR_NULL(fsm.current_substate); FSM_run(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Warming, fsm.current_substate); FSM_switch(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Dehumidifying, fsm.current_substate); FSM_stop(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Stopped, fsm.current_state); PCU_ASSERT_PTR_NULL(fsm.current_substate); FSM_run(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Dehumidifying, fsm.current_substate); FSM_switch(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Cooling, fsm.current_substate); FSM_stop(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Stopped, fsm.current_state); PCU_ASSERT_PTR_NULL(fsm.current_substate); FSM_run(&fsm); PCU_ASSERT_PTR_EQUAL(&state_Running, fsm.current_state); PCU_ASSERT_PTR_EQUAL(&substate_Cooling, fsm.current_substate); } PCU_Suite *FSMTest_suite(void) { static PCU_Test tests[] = { PCU_TEST(test_init), PCU_TEST(test_run), PCU_TEST(test_stop), PCU_TEST(test_switch), PCU_TEST(test_history), }; static PCU_Suite suite = { "FSMTest", tests, sizeof tests / sizeof tests[0], setup }; return &suite; }
状態遷移の単体テストは、FSMクラスに現在の状態を取得するAPIがないため、内部実装に依存したテストになってしまった。本来ユーザー側にはprivateにするべきFSMクラスのメンバ変数にアクセスしている。
また、シングルトンのStateオブジェクトはstatic変数なのでテストコードのファイルからアクセスできない。苦肉の策として、「ソースファイル(.c)をインクルードする」という手段を使った。この方法はstaticの変数や関数の名前が衝突する場合は使えない。
テストコードの保守性が悪い。もっといいやり方はあるだろうか?
その他の状態遷移の実装例
ガード条件による状態遷移の分岐
A状態の時にXイベントが発生したら、data >= 100ならB状態へ遷移し、50 <= data < 100ならC状態へ遷移し、そうでなければ遷移しない、という例。
static void StateA_eventX(FSM *fsm) { /* アクション */ ... if (fsm->data >= 100) { FSM_change_state(fsm, &state_B); return; } else if (fsm->data >= 50) { FSM_change_state(fsm, &state_C); return; } }
普通にif文で条件分岐すればいい。
returnを書いているのは、状態遷移後この関数内では何の処理も行わないようにするため。(マクロにして常にreturnするようにしてもいいかも。#define FSM_CHANGE_STATE(f, s) do { FSM_change_state(f, s); return; } while (0) みたいに)
自己遷移
A状態の時にXイベントが発生したら、いったんA状態から出て、またA状態へ遷移する、という例。
static void StateA_eventX(FSM *fsm) { /* アクション */ ... FSM_change_state(fsm, &state_A); }
普通の状態遷移のやり方と同じ。exitとentryも呼ばれる。
内部遷移
A状態の時にXイベントが発生したら、状態遷移はせずアクションのみ実行する、という例。
static void StateA_eventX(FSM *fsm) { /* アクション */ ... /* FSM_change_state()を呼ばない */ }
イベントの関数の中でアクションを実行するのみ。FSM_change_state()を呼ばない。
自動遷移
ある状態からA状態に遷移したら何のイベントも待たずに自動的にB状態へ遷移する、という例。
static void StateA_entry(FSM *fsm) { /* アクション */ ... FSM_change_state(fsm, &state_B); }
entryの中でFSM_change_state()を呼び出せばOK。ガード条件との組み合わせも可。
スタックオーバーフローを引き起こすので以下のように自動遷移の無限ループに陥らないように注意。
/* 再帰的にA->B->A->B->A...と遷移してしまうのでNG */ static void StateA_entry(FSM *fsm) { FSM_change_state(fsm, &state_B); } static void StateB_entry(FSM *fsm) { FSM_change_state(fsm, &state_A); }
引数のあるイベント
Xイベント発生時に何らかのデータを渡したい、という例。
void FSM_eventX(FSM *self, const unsigned char *data, size_t num); typedef struct State { void (*eventX)(FSM *fsm, const unsigned char *data, size_t num); } State;
イベント関数に任意の引数をつけるだけでOK。Stateクラスの仮想関数にも同じ引数を追加する。
サブ状態が最終状態へ遷移→コンポジット状態が完了遷移
サブ状態CがイベントXが発生して最終状態へ遷移すると、自動的にコンポジット状態Aが状態Bに遷移する(完了遷移という)例
/* サブ状態に最終状態を追加する。最終状態では何もできないのでメンバはすべて0 */ static const State substate_Final = {0}; /* サブ状態が最終状態かどうか判定する関数を追加 */ int SubState_is_final(FSM *fsm) { return fsm->current_substate == &substate_Final; } /* 最終状態に遷移するイベント処理を追加 */ static void SubStateC_eventX(FSM *fsm) { FSM_change_substate(fsm, &substate_Final); } typedef struct State { ... void (*event_completion)(FSM *fsm); /* 完了遷移を発生させるための完了イベントを追加 */ } State; static void StateA_event_complettion(FSM *fsm) { /* 完了遷移 */ FSM_change_state(fsm, &state_B); } void FSM_eventX(FSM *self) { if (self->current_substate && self->current_substate->eventX) { self->current_substate->eventX(self); if (SubState_is_final(self) && self->current_state->event_completion) { /* サブ状態のXイベントを処理後、最終状態になっていたら完了イベントを発生させる */ self->current_state->event_completion(self); } } }