3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

組込みに近いものをTDDで開発してみる〜キー入力検知編〜

Posted at

はじめに

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が活用できる。今回は、押されたかどうかだけがわかれば良い。

参考
おなかすいたwiki input_event

キー入力検知

特定のキー入力を検知するテストを思い描く。

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で作成したプログラムを比較します。
同時に、今後の拡張について考えたいと思います。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?