はじめに
googletest/googlemockを使って、組込みLinux上で動作するソフトウェアをTDDで開発しています。
実際にTDDで開発している手順をリアルタイムでそのまま書いているため、行き当たりばったりな部分があります
過程も楽しんで頂ければ幸いです。
不明な点、間違っている点があれば、コメント下さい。励みになります。
これまでの経緯を詳しく知りたい方は、前回までの記事をご覧下さい。
組込みに近いものをTDDで開発してみる〜準備編〜
組込みに近いものをTDDで開発してみる〜問題提起編〜
組込みに近いものをTDDで開発してみる〜file open編〜
組込みに近いものをTDDで開発してみる〜libevdev初期化/終了処理編〜
input_event
先に進む前にキー入力イベントで使う構造体定義について確認しよう。
struct input_event {
struct timeval time; // 入力のあった時間
__u16 type; // 入力タイプ。EV_KEY: キー入力、EV_REL: マウスの動き
__u16 code; // 入力コード。KEY_A: Aキー、REL_X: マウスX軸の動き
__s32 value; // type==EV_KEYの場合、キーを押したときは1、離したときは0
};
今回利用する範囲では、timeは利用しない。
キー入力を記録して、再現するような場合、timeが活用できる。今回は、押されたかどうかだけがわかれば良い。
キー入力検知
特定のキー入力を検知するテストを思い描く。
TEST_F(KeyInputEventTest, DetectCondition) {
// 「A」キーが押されたことを意味するinput_event
constexpr input_event kPressA {timeval{}, EV_KEY, KEY_A, PUT_KEY_PRESSED};
SetKeyInputDetectCondition(dev_, &kPressA);
EXPECT_EQ(INPUT_DEV_EVENT_DETECTED, CheckKeyInput(dev_));
}
検知したいinput_eventを作成して、*SetKeyInputDetectCondition()*で登録する。
*CheckKeyInput()*を呼び出したとき、ターゲットの入力イベントが発生していたら、該当のステータスコードが返ってくる、という寸法だ。
*SetKeyInputDetectCondition()*は、無条件で成功することにしよう。今は、それ以上のことをやる必要性を感じない。
さて、テストを完成させるには、*CheckKeyInput()*で呼ばれるlibevdevモックの期待値を設定する必要がある。今回は利用するAPIが副作用を持つため若干複雑だ。
libevdevでイベントを取得するAPIは次の通りだ。
int libevdev_next_event (struct libevdev *dev,
unsigned int flags, // 今は関係なので無視
struct input_event *ev)
成功時は、返り値がLIBEVDEV_READ_STATUS_SUCCESS(0)、第3引数のevに発生したinput_eventが設定される。
このような、返り値以外で、API呼び出し前後で状態が変化するものが副作用だ。
C言語において、パラメータにポインタを渡して、そこにデータを詰めてもらう、ということは、ごく自然に、頻繁に行われる。
言語仕様上、ある程度このような副作用を起こすAPI設計は仕方ない。ただ、出来る限り副作用を起こすAPIは減らすべきだ。
テストにおいては副作用も評価しなければならない。そのため、副作用を多用すると、API呼び出しに対して、期待値を多く書く必要が出てくるため、テストが面倒になる。
(余談だが、C++を使う人は、C言語での副作用を持ち込まないようにしよう。C++で同じことをやっている場合、設計をミスっている。)
さて、今回のテストを見てみよう。
*libevdev_next_event()*を呼び出すと、第3引数に『A』キーを押した場合のinput_eventが設定されるようにする。
TEST_F(KeyInputEventTest, DetectCondition) {
input_event kPressA {timeval{}, EV_KEY, KEY_A, INPUT_KEY_PRESSED};
EXPECT_CALL(*mock_libevdev, libevdev_next_event(_, _, _)).WillOnce(
DoAll(SetArgPointee<2>(kPressA), Return(LIBEVDEV_READ_STATUS_SUCCESS)));
SetKeyInputDetectCondition(dev_, &kPressA);
EXPECT_TRUE(KeyInputDetected(dev_));
}
シンタックスハイライトの効きが悪いことも合わさって、少し読みにくい。
さて、モックのアクションである*DoAll()*は複数のアクションを全て実行することを意味している。
*DoAll()*の中で実行するアクションは次の2つだ。
- *SetArgPointee()*は、第3引数(0から数えるので数値は"2"となっている)が指す変数に、kPressAをコピーする。
- *Return()*はこれまで通り、関数の返り値として、LIBEVDEV_READ_STATUS_SUCCESSを返す。
当然のように、このテストのコンパイルは通らない(そろそろクドイかな?)。
まず、検知するイベントは、KeyInputDeviceStructにメンバを追加して、*SetKeyInputDetectCondition()*で記録する。
typedef struct KeyInputDeviceStruct {
int fd;
struct libevdev *evdev;
struct input_event target_event; // 追加
} KeyInputDeviceStruct;
void SetKeyInputDetectCondition(KeyInputDevice dev,
const struct input_event *ev) {
memcpy(&dev->target_event, ev, sizeof(struct input_event));
}
*CheckKeyInput()*は、イベントが発生している、かつ、記録したイベント条件と一致すると、検知成功を返す。
// 何らかのイベントが発生しているかをチェックするヘルパー関数
static bool HasPendingEvent(struct libevdev *evdev, struct input_event *ev) {
return libevdev_next_event(evdev, LIBEVDEV_READ_FLAG_NORMAL, ev)
== LIBEVDEV_READ_STATUS_SUCCESS;
}
// input_event構造体を比較するヘルパー関数
static bool IsTargetEvent(const struct input_event *target,
const struct input_event *ev) {
return target->type == ev->type
&& target->code == ev->code
&& target->value == ev->value;
}
bool CheckKeyInput(KeyInputDevice dev) {
struct input_event ev = {};
if (HasPendingEvent(dev->evdev, &ev) && IsTargetEvent(&dev->target_event, &ev)) {
return INPUT_DEV_EVENT_DETECTED;
}
return INPUT_DEV_NO_EVENT;
}
結果だけを見せたが、ヘルパー関数は一度テストを通した後、改めて抽出したものだ。
この2つのヘルパー関数のおかげで、*HasPendingEvent() && IsTargetEvent()*という誰が見ても明確な条件でif文の中に条件式を書ける。
リファクタリングの過程は明確に見せていないが、このような細々としたリファクタリングを常に行っている。
それができるのは、テストのおかげで、コードを変更しても安全だからだ。
次にイベントが発生していない場合のテストを書く。libevdevのAPI仕様によると、有効なイベントがない場合、*libevdev_next_event()*は-EAGAINを返す。
TEST_F(KeyInputEventTest, CannotDetectEvent) {
// イベントが発生していないと、-EAGAINが返ってくる
EXPECT_CALL(*mock_libevdev, libevdev_next_event(_, _, _)).WillOnce(Return(-EAGAIN));
SetKeyInputDetectCondition(dev_, &kPressA);
EXPECT_EQ(INPUT_DEV_NO_EVENT, CheckKeyInput(dev_));
}
もう少し複雑なパターンをテストしておこう。
次のテストでは、イベントがない、興味ないイベントが2回来る、検知したいイベントが来る、というシーケンスをテストする。
テストコードが複雑で嫌になるが、もう少しの我慢だ。
TEST_F(KeyInputEventTest, DetectOnlyInterestedEvent) {
constexpr input_event kPressB {timeval{}, EV_KEY, KEY_B, INPUT_KEY_PRESSED};
constexpr input_event kReleaseA {timeval{}, EV_KEY, KEY_A, INPUT_KEY_RELEASED};
constexpr auto kSuccess = LIBEVDEV_READ_STATUS_SUCCESS;
EXPECT_CALL(*mock_libevdev, libevdev_next_event(_, _, _))
.WillOnce(Return(-EAGAIN))
.WillOnce(DoAll(SetArgPointee<2>(kPressB), Return(kSuccess)))
.WillOnce(DoAll(SetArgPointee<2>(kReleaseA), Return(kSuccess)))
.WillOnce(DoAll(SetArgPointee<2>(kPressA), Return(kSuccess)));
SetKeyInputDetectCondition(dev_, &kPressA);
EXPECT_FALSE(KeyInputDetected(dev_));
EXPECT_FALSE(KeyInputDetected(dev_));
EXPECT_FALSE(KeyInputDetected(dev_));
EXPECT_TRUE(KeyInputDetected(dev_));
}
これまでのプロダクトコードの実装で、このテストは通る。
ToDoリストを更新する。
- 「A」キー押下を検出する
さて、キー入力検知でやりたいことは全て実装できた。と、ちょっと待った。
1つ忘れているアイテムがあることを覚えているだろうか?
Cleanup()後、libevdevを使うと失敗する
前回、libevdevの終了処理で重要な仕様を目にしており、ToDoリストに加えていた。
- Cleanup()後、libevdevを使うと失敗する。
libevdevの終了処理、*libevdev_free()*について少し詳しく見てみよう。
libevdev_free()に渡したdevは、*libevdev_reset()*において無効な数値で埋められ、free()される。
LIBEVDEV_EXPORT void
libevdev_free(struct libevdev *dev)
{
if (!dev)
return;
queue_free(dev);
libevdev_reset(dev);
free(dev);
}
さて、C言語のfree()はどういう動作をするのだったかな。記憶が定かでない。
確か、free()してもポインタが指す先は、そのままだったはずだ。
TEST_F(KeyInputEventDetectionTest, TestFree) {
int *mem = static_cast<int*>(calloc(1, sizeof(int)));
int *tmp = mem;
free(mem);
EXPECT_EQ(tmp, mem);
}
記憶は確かだった。上記のテストはパスする。
*libevdev_free()*ではNULLの代入までは面倒を見てくれない。
*libevdev_free()*を呼び出した後は、NULLを代入し、evdevを使うときには、NULLポインタへのガードを設けよう。
テストとプロダクトコードを示す。
テストコード
TEST_F(KeyInputEventDetectionTest, FailOperationAfterCleanup) {
EXPECT_CALL(*mock_libevdev, libevdev_free(_)).Times(1);
auto dev = CreateKeyInputDevice();
CleanupKeyInputDevice(dev);
// Cleanup()されたdevのキー入力検知は失敗する
EXPECT_EQ(INPUT_DEV_INVALID_DEV, CheckKeyInput(dev));
DestroyKeyInputDevice(dev);
}
プロダクトコード
int CheckKeyInput(KeyInputDevice dev) {
// evdevのNULLポインタへのガードを追加
if (dev == NULL || dev->evdev == NULL) return INPUT_DEV_INVALID_DEV;
struct input_event ev = {};
if (HasPendingEvent(dev->evdev, &ev) && IsTargetEvent(&dev->target_event, &ev)) {
return INPUT_DEV_EVENT_DETECTED;
}
return INPUT_DEV_NO_EVENT;
}
int CleanupKeyInputDevice(KeyInputDevice dev) {
if(dev == NULL) return INPUT_DEV_INVALID_DEV;
libevdev_free(dev->evdev);
dev->evdev = NULL; // 追加
int rc = IO_CLOSE(dev->fd);
if (rc < 0) return INPUT_DEV_CLEANUP_ERROR;
return INPUT_DEV_SUCCESS;
}
これで、libevdev_free()を呼び出した後、libevdev構造体を使用するAPI呼び出しは失敗するようになる。
キー入力検知編の整理
キー入力検知ができるようになった。
副作用を起こすAPIも、モックをうまく使うことでテスト可能だ。
テストコード
新たに4つのテストを追加した。
合計、12個のテストがパスしている。
シンタックスハイライトが効かず、コードが見にくいので、コードは省略する。
githubは幾分ましなので、テストコードの全貌が見たい方は↓へどうぞ。
github:key_input_event_test.cpp
プロダクトコード
typedef struct KeyInputDeviceStruct {
int fd;
struct libevdev *evdev;
struct input_event target_event;
} KeyInputDeviceStruct;
KeyInputDevice CreateKeyInputDevice() {
KeyInputDevice dev = calloc(1, sizeof(KeyInputDeviceStruct));
dev->fd = -1;
dev->evdev = NULL;
return dev;
}
int InitKeyInputDevice(KeyInputDevice dev, const char *device_file) {
if(dev == NULL) return INPUT_DEV_INVALID_DEV;
dev->fd = IO_OPEN(device_file, O_RDONLY|O_NONBLOCK);
if (dev->fd < 0) {
if (errno == EACCES)
DEBUG_LOG("Fail to open file. You may need root permission.");
return INPUT_DEV_INIT_ERROR;
}
int rc = libevdev_new_from_fd(dev->fd, &dev->evdev);
if (rc < 0) return INPUT_DEV_INIT_ERROR;
return INPUT_DEV_SUCCESS;
}
int SetKeyInputDetectCondition(KeyInputDevice dev, const struct input_event *ev) {
if (dev == NULL) return INPUT_DEV_INVALID_DEV;
// Should I validate ev, here?
memcpy(&dev->target_event, ev, sizeof(struct input_event));
return INPUT_DEV_SUCCESS;
}
static bool HasPendingEvent(struct libevdev *evdev, struct input_event *ev) {
return libevdev_next_event(evdev, LIBEVDEV_READ_FLAG_NORMAL, ev)
== LIBEVDEV_READ_STATUS_SUCCESS;
}
static bool IsTargetEvent(const struct input_event *target,
const struct input_event *ev) {
return (target->type == ev->type
&& target->code == ev->code
&& target->value == ev->value);
}
int CheckKeyInput(KeyInputDevice dev) {
if (dev == NULL || dev->evdev == NULL) return INPUT_DEV_INVALID_DEV;
struct input_event ev = {};
if (HasPendingEvent(dev->evdev, &ev) && IsTargetEvent(&dev->target_event, &ev)) {
return INPUT_DEV_EVENT_DETECTED;
}
return INPUT_DEV_NO_EVENT;
}
int CleanupKeyInputDevice(KeyInputDevice dev) {
if(dev == NULL) return INPUT_DEV_INVALID_DEV;
libevdev_free(dev->evdev);
dev->evdev = NULL;
int rc = IO_CLOSE(dev->fd);
if (rc < 0) return INPUT_DEV_CLEANUP_ERROR;
return INPUT_DEV_SUCCESS;
}
void DestroyKeyInputDevice(KeyInputDevice dev) {
if(dev == NULL) return;
free(dev);
dev = NULL;
}
main.c (2回『A』キーを押すとプログラムが終了する)
int main(void) {
KeyInputDevice dev = CreateKeyInputDevice();
InitKeyInputDevice(dev, "/dev/input/event2");
struct timeval time = {};
const struct input_event kPressA = {time, EV_KEY, KEY_A, INPUT_KEY_PRESSED};
SetKeyInputDetectCondition(dev, &kPressA);
int count = 0;
while(count < 2) {
if(CheckKeyInput(dev) == INPUT_DEV_EVENT_DETECTED) count++;
}
CleanupKeyInputDevice(dev);
DestroyKeyInputDevice(dev);
return 0;
}
To Doリスト
キー入力に関するTo Doリストの状況は次の通りだ。
当初列挙した項目に終わりが見えてきた。
- 仮想的な利用方法を示すテストを作る
-
Input deviceを初期化する
- Input device fileをopenする
- Permission deniedでオープンに失敗したらログを出力する。
- libevdevを初期化する
- Input deviceの終了処理をする
- マルチインスタンスへの対応をする~~?~~
- 「A」キー押下を検出する
- Cleanup()後、libevdevを使うと失敗する。
次回予告
LED制御は裏でこっそり作って、結果だけ公開する予定です。キー入力処理と比較して、簡単な作りになると考えています。特筆すべき事項があれば、少し解説を入れます。
次回は、問題提起編で作成したプログラムと、TDDで作成したプログラムを比較します。
同時に、今後の拡張について考えたいと思います。