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

# はじめに

googletest/googlemockを使って、組込みLinux上で動作するソフトウェアをTDDで開発しています。
実際にTDDで開発している手順をリアルタイムでそのまま書いているため、行き当たりばったりな部分があります
過程も楽しんで頂ければ幸いです。
不明な点、間違っている点があれば、コメント下さい。励みになります。

これまでの経緯を詳しく知りたい方は、前回までの記事をご覧下さい。
[組込みに近いものをTDDで開発してみる〜準備編〜](https://qiita.com/tomoyuki-nakabayashi/items/23b064e3cea11dcc4152)
[組込みに近いものをTDDで開発してみる〜問題提起編〜](https://qiita.com/tomoyuki-nakabayashi/items/45bd4700c8e081107bd1)
[組込みに近いものをTDDで開発してみる〜file open編〜](https://qiita.com/tomoyuki-nakabayashi/items/648109594612fd4b518a)
[組込みに近いものをTDDで開発してみる〜libevdev初期化/終了処理編〜](https://qiita.com/tomoyuki-nakabayashi/items/c139c181151de3ae433a)

## input_event

先に進む前にキー入力イベントで使う構造体定義について確認しよう。

```c
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](http://wiki.onakasuita.org/pukiwiki/?input_event)

## キー入力検知

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

```cpp
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は次の通りだ。

```c
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が設定されるようにする。

```cpp
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()*で記録する。

```c
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()*は、イベントが発生している、かつ、記録したイベント条件と一致すると、検知成功を返す。

```c
// 何らかのイベントが発生しているかをチェックするヘルパー関数
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を返す。

```c
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回来る、検知したいイベントが来る、というシーケンスをテストする。
テストコードが複雑で嫌になるが、もう少しの我慢だ。

```c
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リストを更新する。

- [x] 「A」キー押下を検出する

さて、キー入力検知でやりたいことは全て実装できた。と、ちょっと待った。
1つ忘れているアイテムがあることを覚えているだろうか？

## Cleanup()後、libevdevを使うと失敗する

前回、libevdevの終了処理で重要な仕様を目にしており、ToDoリストに加えていた。

- [ ] Cleanup()後、libevdevを使うと失敗する。

libevdevの終了処理、*libevdev_free()*について少し詳しく見てみよう。
*libevdev_free()*に渡した*dev*は、*libevdev_reset()*において無効な数値で埋められ、free()される。

```c
LIBEVDEV_EXPORT void
libevdev_free(struct libevdev *dev)
{
	if (!dev)
		return;

	queue_free(dev);
	libevdev_reset(dev);
	free(dev);
}
```

さて、C言語のfree()はどういう動作をするのだったかな。記憶が定かでない。
確か、free()してもポインタが指す先は、そのままだったはずだ。

```c
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ポインタへのガードを設けよう。

テストとプロダクトコードを示す。

テストコード

```cpp
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);
}
```

プロダクトコード

```c
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](https://github.com/tomoyuki-nakabayashi/TDDforEmbeddedSystem/blob/DetectKeyInputEvent/led_controller/test/key_input_event_test.cpp)

### プロダクトコード

```c
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』キーを押すとプログラムが終了する)

```c
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リストの状況は次の通りだ。
当初列挙した項目に終わりが見えてきた。

- [x] 仮想的な利用方法を示すテストを作る
- [x] Input deviceを初期化する
  - [x] Input device fileをopenする
  - [x] Permission deniedでオープンに失敗したらログを出力する。
  - [x] libevdevを初期化する
- [x] Input deviceの終了処理をする
- [x] マルチインスタンスへの対応をする~~？~~
- [x] 「A」キー押下を検出する
- [x] Cleanup()後、libevdevを使うと失敗する。

## 次回予告

LED制御は裏でこっそり作って、結果だけ公開する予定です。キー入力処理と比較して、簡単な作りになると考えています。特筆すべき事項があれば、少し解説を入れます。

次回は、問題提起編で作成したプログラムと、TDDで作成したプログラムを比較します。
同時に、今後の拡張について考えたいと思います。