LoginSignup
5
4

More than 5 years have passed since last update.

組込みに近いものをTDDで開発してみる〜libevdev初期化/終了処理編〜

Last updated at Posted at 2018-02-21

はじめに

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

これまでの経緯を詳しく知りたい方は、前回までの記事をご覧下さい。
組込みに近いものをTDDで開発してみる〜準備編〜
組込みに近いものをTDDで開発してみる〜問題提起編〜
組込みに近いものをTDDで開発してみる〜file open編〜

libevdev初期化

初期化成功のテスト

さて、libevdevを初期化するためのテストを考えよう。
前回作成したopen()に加え、libevdevの初期化関数が呼ばれれば、良いはずだ。

TEST_F(KeyInputEventTest, CanInitEvdev) {
  const int kFd = 3;
  // モックはIO_OPEN()でファイルディスクリプタ"3"を返す
  EXPECT_CALL(*mock_io, IO_OPEN(_, _)).WillOnce(Return(kFd));
  // 新しく追加したlibevdevのモックに対する期待値
  EXPECT_CALL(*mock_libevdev, libevdev_new_from_fd(kFd, _))
    .WillOnece(Return(0));

  EXPECT_TRUE(InitKeyInputDevice("./test_event"));
}

今回のEXPECT_CALL()では、libevdev_new_from_fd()の第一引数にopenしたファイルディスクリプタが渡されることを期待している("_"は引数がdon't careであることを意味する)。
モックは、該当の関数が、指定の引数を与えられて呼び出されたかどうか、をテストする。

TDDのお約束だが、このテストはコンパイルすら通らない。
まずは、モックを作って、コンパイルが通るようにしよう。

スペルミスなどの些細なミスを避けるために、ヘッダは本物のlibevdev.hをインクルードする。

#include <gmock/gmock.h>
// このヘッダは本物
#include <libevdev/libevdev.h>

class MOCK_LIBEVDEV {
 public:
    MOCK_METHOD2(libevdev_new_from_fd, int(int, libevdev**));
};

extern MOCK_LIBEVDEV *mock_libevdev;

extern "C" {
  // プロダクトコードがlibevdev_new_from_fd()を呼び出すとこの関数が呼ばれ、
  // その中でモックが呼ばれる。
  int libevdev_new_from_fd(int fd, libevdev **dev)
  {
    return mock_libevdev->libevdev_new_from_fd(fd, dev);
  }
}

モックを実装すると、コンパイルが通るようになる。テストでは、本物のライブラリをリンクせず、モックをリンクすることに気を付けよう。
コンパイルが通っても、Init()内でlibevdev_new_from_fd()を呼び出すという期待値を満たさないため、テストは失敗する。

[ RUN      ] KeyInputEventTest.CanInitEvdev
led_controller/test/key_input_event_test.cpp:76: Failure
Actual function call count doesn't match EXPECT_CALL(*mock_libevdev, libevdev_new_from_fd(kFd, _))...
         Expected: to be called once
           Actual: never called - unsatisfied and active
[  FAILED  ] KeyInputEventTest.CanInitEvdev (0 ms)

Init()に次の2行を追加すると、テストが成功する。

  struct libevdev *evdev = NULL;
  libevdev_new_from_fd(fd, &evdev);

初期化エラー

では、libevdevの初期化エラーが発生した場合のテストを考えよう。
libevdevのドキュメントを参照すると、libevdev_new_from_fd()は、成功時は0、それ以外は負のエラーコードが返るようだ。
(先ほど追加したEXPECT_CALL()で、libevdev_new_from_fd()の返り値を0としていたのはこの仕様を知っていたからだ)
ドキュメントからは発生し得るエラーコードが読み取れない。

libevdev libevdev_new_from_fd()

どんなエラーコードが返ってくるか、libevdevのソースコードに付属しているユニットテストを見てみよう。

libevdev github mirror

お目当てのテストがあった。

START_TEST(test_init_from_invalid_fd)
{
    int rc;
    struct libevdev *dev = NULL;

    rc = libevdev_new_from_fd(-1, &dev);

    ck_assert(dev == NULL);
    ck_assert_int_eq(rc, -EBADF);

    rc = libevdev_new_from_fd(STDIN_FILENO, &dev);
    ck_assert(dev == NULL);
    ck_assert_int_eq(rc, -ENOTTY);
}
END_TEST

libevdevに与えるfdが-1のときEBADF、fdが標準入力のときENOTTYが返るようだ。
このほかに失敗に関するテストケースは見つからない。
直観だが、これ以外のエラーが返らないのは奇妙に感じる。ソースコードを見に行く。
メモリ確保失敗でENOMEM, デバイスがない場合にENOENT, 初期化のために渡すlibevdev構造体がすでに初期化済みの場合にEBADF、その他errnoが返るような事態ではそのerrnoが返るようだ。
興味のある方は、下記コードを見てほしい。

github libevdev.c

今のところ特別対処しなければいけないエラーはなさそうだ。
libevdev_new_from_fd()が負数を返したら、Init()はfalseを返すようにしよう。
テストは次のようにした。
libevdev_new_from_fd()への引数は興味がないので、don't careとする。

TEST_F(KeyInputEventTest, InitEvdevFailed) {
  EXPECT_CALL(*mock_libevdev, libevdev_new_from_fd(_, _))
    .WillOnce(Return(-EBADF));  // 代表でEBADF。厳密性にこだわるのであれば境界値をテストしても良い。

  EXPECT_FALSE(InitKeyInputDevice("./test_event"));
}

このテストを満たすプロダクトコードにするには、次の2行を加えれば良い。

  int rc = libevdev_new_from_fd(fd, &evdev);
  if (rc < 0) return false;

この通りコードを書くと、googlemockは次のような警告を出力する。
これはgooglemockの正常な動作であって、テストコード、プロダクトコード共に問題ない。

[ RUN      ] KeyInputEventTest.InitEvdevFailed

GMOCK WARNING:
Uninteresting mock function call - returning default value.
    Function call: IO_OPEN(0x4e6015 pointing to "./test_event", 2048)
          Returns: 0
NOTE: You can safely ignore the above warning unless this call should not happen.  Do not suppress it by blindly adding an EXPECT_CALL() if you don't mean to enforce the call.  See https://github.com/google/googletest/blob/master/googlemock/docs/CookBook.md#knowing-when-to-expect for details.
[       OK ] KeyInputEventTest.InitEvdevFailed (0 ms)

警告文の原因は、Init()を呼び出すと、IO_OPEN()を呼び出すが、その期待値を書いていないことだ。
警告文にもある通り、このテストでは、IO_OPEN()に呼び出しに興味がないため、安全に無視することができる。
デバッグ上必要であれば、テスト実行時に次のオプションを指定することで、この警告の出力を制限できる。
--gmock_verbose=error
ただし、意図せぬ呼び出しをしていないことを確認するために、警告はきちんと読むべきだ。

libevdev_new_from_fd()で扱うエラー(読み飛ばし可能)

libevdev_new_from_fd()では、file openが失敗した理由を扱えないようだ。
file openの失敗がPermission deniedだろうが、No such file or directoryだろうが、Bad file descriptorとして扱われる。

試しに、次のテストを書いて、root権限なしで実行してみた。
root権限がない状態でイベントデバイスをopenしようとし、Permission deniedエラーが発生するので、libevdev_new_from_fd()が返す期待値を-EACCESとした。

TEST_F(EvdevSampleOpenTest, TestEvdevError) {
  struct libevdev *dev {nullptr};
  int fd = open("/dev/input/event2", O_RDONLY|O_NONBLOCK);
  int rc = libevdev_new_from_fd(fd, &dev);

  EXPECT_EQ(-EACCES, rc);
}

結果は次の通り失敗する。

[ RUN      ] EvdevSampleOpenTest.TestEvdevError
/home/tomoyuki/work/02.TDD/TDDforEmbeddedSystem/evdev_test/test/evdev_sample_test.cpp:46: Failure
Expected equality of these values:
  -13
  rc
    Which is: -9
[  FAILED  ] EvdevSampleOpenTest.TestEvdevError (0 ms)

-EACCES(-13)になってほしかったが、実際は、Bad file descriptorのエラー、すなわち-EBADF(-9)だった。

libevdev終了処理

終了処理のテストはうまくテストを思い描けない時がある。
兎にも角にも一旦書いてみよう。

TEST_F(KeyInputEventTest, CleanupKeyInputDevice) {
  EXPECT_TRUE(CleanupKeyInputDevice());
}

Cleanup()がtrueが返すとはどういうことだろうか?
falseを返す場合があるのだろうか?
焦らずに、libevdevとcloseの仕様を確認しよう。

libevdev libevdev_free()

void libevdev_free(struct libevdev *dev)    

特にエラーが返ってきそうにない。
ただ、今後役に立ちそうな情報を見つけた。

After completion, the struct libevdev is invalid and must not be used.

つまり、Cleanup()を呼んだ後に、libevdevを使う関数呼び出しは、失敗すべきだ。
ToDoリストに追加しておく。

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

続いてMan page of close

close() は成功した場合は 0 を返す。 エラーが発生した場合は -1 を返して、 errno を適切に設定する。

EBADF fd が有効なオープンされたディスクリプターでない。
EINTR close() コールがシグナルにより中断 (interrupt) された。 signal(7) 参照。
EIO I/O エラーが発生した。

EBADFは発生しそうだ。openしていないディスクリプターがcloseに渡るパターンだ。
libevdevの終了に成功するテストは次のようになりそうだ。

TEST_F(KeyInputEventTest, CanCleanupKeyInputDevice) {
  InitKeyInputDevice(kFilePath);
  EXPECT_TRUE(CleanupKeyInputDevice());
}

今のプロダクトコードでは、Init()内のローカル変数として、fdとlibevdevを定義している。このままでは、Cleanup()で処理する対象が不明なので、fdとlibevdevをInit()と共有する。
必要なデータを構造体にまとめて、ファイルスコープで定義する。
明示的に無効なファイルディスクリプターで初期化しておこう。

typedef struct {
  int fd;
  struct libevdev *evdev;
} KeyInputDeviceStruct;

enum {INVALID_FD = -1};
static KeyInputDeviceStruct dev = {INVALID_FD, NULL};

// Init()はdev のメンバー変数を初期化する。
bool InitKeyInputDevice(const char *device_file) {
  dev.fd = IO_OPEN(device_file, O_RDONLY|O_NONBLOCK);
  if (dev.fd < 0) {
  ...
}

どうだろうか?特に問題ないように思える。
Cleanup()を実装しよう。コードは省略するが、必要なモックも一緒に実装している。

bool CleanupKeyInputDevice() {
  libevdev_free(dev.evdev);  // テストではモックを呼び出す
  int rc = IO_CLOSE(dev.fd);  // テストではモックを呼び出す
  if (rc < 0) return false;  // 最初はこの行がない実装をしても良い

  return true;
}

上のコードをテストする実際のテストは、こうなる。
(実際は先にテストを書いている)

// 今後も使いそうなのでInitのヘルパーを用意した。
static void InitHelper(const char *path, int fd, int res_evdev_new) {
  EXPECT_CALL(*mock_io, IO_OPEN(path, _)).WillOnce(Return(fd));
  EXPECT_CALL(*mock_libevdev, libevdev_new_from_fd(fd, _))
    .WillOnce(Return(res_evdev_new));

  InitKeyInputDevice(path);
}

TEST_F(KeyInputEventTest, CanCleanupKeyInputDevice) {
  constexpr int kFd = 3;  // ファイルディスクリプターは"3"
  InitHelper(kFilePath, kFd, 0);

  // libevdev_free()は呼ばれることだけ確認する
  EXPECT_CALL(*mock_libevdev, libevdev_free(_)).Times(1);
  // ファイルディスクリプター"3"をcloseすることが期待値
  EXPECT_CALL(*mock_io, IO_CLOSE(kFd)).WillOnce(Return(0));

  EXPECT_TRUE(CleanupKeyInputDevice());
}

テストは無事パスする!

では、続いては、これだ。Init前にCleanupを呼び出すと失敗すべきだ。

TEST_F(KeyInputEventTest, CleanupKeyInputDeviceFileNotOpenYet) {
  EXPECT_FALSE(CleanupKeyInputDevice());
}

さて、dev構造体は、次のように初期化した。
であるので、IO_CLOSE()のモックは、引数がINVALID_FDのときに、-1を返せば、テストがパスしそうだ。

enum {INVALID_FD = -1};
static KeyInputDeviceStruct dev = {INVALID_FD, NULL};

bool CleanupKeyInputDevice() {
  libevdev_free(dev.evdev);
  int rc = IO_CLOSE(dev.fd);  // 未初期化では、dev.fdはINVALID_FD(-1)
  if (rc < 0) return false;

  return true;
}

つまり、テストこうだ。

TEST_F(KeyInputEventTest, CleanupKeyInputDevice) {
  EXPECT_CALL(*mock_io, IO_CLOSE(-1)).WillOnce(Return(-1));

  EXPECT_FALSE(CleanupKeyInputDevice());
}

さて、ビルドして、テストはパス…しない!

[ RUN      ] KeyInputEventTest.CleanupKeyInputDevice
Unexpected mock function call - returning default value.
    Function call: IO_CLOSE(3)
          Returns: 0
Google Mock tried the following 1 expectation, but it didn't match:

led_controller/test/key_input_event_test.cpp:111: EXPECT_CALL(*mock_io, IO_CLOSE(-1))...
  Expected arg #0: is equal to -1
           Actual: 3
         Expected: to be called any number of times
           Actual: never called - satisfied and active
[  FAILED  ] KeyInputEventTest.CleanupKeyInputDevice (0 ms)

IO_CLOSE()の引数は、"3"だ。
この結果は、当然だ。なぜなら、このテストが実行される前に、dev.fdに"3"を代入するテストが走っているからだ!
devはstaticな構造体であるため、生存期間は、プログラムが実行されている間になる。
つまり、このテストは、他のテスト実行に依存して結果が変わる。

シングルインスタンスのモジュール(もしくはシングルトンパターンを適用したクラス)はテストが困難になりがちだ。
前のテストで変化したステートを全て、元に戻さないと独立したテストが書けないからだ。
シングルインスタンスであることが必須なモジュール以外は、なるべくマルチインスタンス化を前提とした方が良い設計になると思う。

さて、では、マルチインスタンス化可能なようにコードを修正していこう。
この修正では、既存APIの変更が余儀なくされる。ただ、今まで作ったテストが、新しいAPIでもパスすることを確認しながら進めれば良い。

マルチインスタンス化するために、APIを次のように追加・変更する。

struct KeyInputDeviceStruct;
typedef struct KeyInputDeviceStruct *KeyInputDevice;

KeyInputDevice CreateKeyInputDevice();
bool InitKeyInputDevice(KeyInputDevice dev, const char *device_file);
bool CleanupKeyInputDevice(KeyInputDevice dev);
void DestroyKeyInputDevice(KeyInputDevice dev);

KeyInputDeviceStructは次の通り、ファイルディスクリプタとlibevdev構造体のポインタを持つ。

typedef struct KeyInputDeviceStruct {
  int fd;
  struct libevdev *evdev;
} KeyInputDeviceStruct;

Create()は、次の通り、メモリ確保と初期値設定を行い、そのポインタを返す。

KeyInputDevice CreateKeyInputDevice() {
  KeyInputDevice dev = calloc(1, sizeof(KeyInputDeviceStruct));
  dev->fd = -1;
  dev->evdev = NULL;

  return dev;
}

Destroy()では、渡されたポインタを解放する。

void DestroyKeyInputDevice(KeyInputDevice dev) {
  if(!dev) {
    free(dev);
    dev = NULL;
  }
}

Init(), Cleanup()は、これまでstatic変数として定義されていた構造体の代わりに、引数で与えられたポインタを操作するだけだ。

bool InitKeyInputDevice(KeyInputDevice dev, const char *device_file) {
  //  dev.fd = IO_OPEN(device_file, O_RDONLY|O_NONBLOCK);
  dev->fd = IO_OPEN(device_file, O_RDONLY|O_NONBLOCK);
...

このようにAPIを変更することで、先ほど失敗した下記のCleanup()テストは成功する。

TEST_F(KeyInputEventTest, CleanupKeyInputDevice) {
  EXPECT_CALL(*mock_io, IO_CLOSE(-1)).WillRepeatedly(Return(-1));

  EXPECT_FALSE(CleanupKeyInputDevice(dev_));
}

ちなみに、全てのテストで、Create()とDestroy()でKeyInputDeviceの生成・破棄を行う。テストコードの重複をなくすため、共通処理は、テストフィクスチャーのSetUp()/TearDown()に書く。テストコードもDRY(Don't Repeat Yourself)な状態を維持しよう。

    virtual void SetUp()
    {
      dev_ = CreateKeyInputDevice();
    }

    virtual void TearDown()
    {
      DestroyKeyInputDevice(dev_);
    }

さて、全てのAPIでnull pointerに備える必要がある。
例えば、次のようなテストを書くと、テストはセグメンテーションフォルトで死ぬ。

TEST_F(KeyInputEventTest, AllApiHaveNullPointerGuard) {
  const KeyInputDevice kNullPointer = NULL;
  EXPECT_FALSE(InitKeyInputDevice(kNullPointer, kFilePath));
  EXPECT_FALSE(CleanupKeyInputDevice(kNullPointer));
}

関数の入り口で、null pointerへのガードを設けよう。

bool InitKeyInputDevice(KeyInputDevice dev, const char *device_file) {
  if(dev == NULL) return false;
  // if(!dev)としないのは趣味

内部構造をカプセル化する

今作成しているモジュールは、イベントデバイスのファイルディスクリプターとlibevdevをデータとして持つ。
この内部構造は、本モジュールを使うユーザーが知る必要のないことだ。
ユーザーは、KeyInputDeviceのインスタンスをCreate()で作成し、作成したインスタンスを各関数に渡してあげる、という使い方だけを知っていればよい。
下手に内部構造をユーザーに晒すと、ユーザーが内部構造に依存した実装をしてしまっても文句が言えない。結果、内部構造の修正による影響がユーザーのコードに及び、最悪、内部構造を修正することができない事態になりかねない。

今回は、key_input_event.hとkey_input_event_private.hにヘッダを分割した。
前者にはユーザーが本モジュールを使うのに必要な情報だけを書いている。

【key_input_event.h】

// 構造体の前方宣言と、そのポインタ型をKeyInputDeviceとして定義
struct KeyInputDeviceStruct;
typedef struct KeyInputDeviceStruct *KeyInputDevice;

KeyInputDevice CreateKeyInputDevice();
bool InitKeyInputDevice(KeyInputDevice dev, const char *device_file);
bool CleanupKeyInputDevice(KeyInputDevice dev);
void DestroyKeyInputDevice(KeyInputDevice dev);

実装の詳細は、privateなヘッダで定義する。
【key_input_event_private.h】

typedef struct KeyInputDeviceStruct {
  int fd;
  struct libevdev *evdev;
} KeyInputDeviceStruct;

このようにすることで、内部構造を変更をユーザーに波及させずに行うことができる。
(ユーザーが粗相をしている場合、話は別だが!)

【追記】
今の時点で他の誰かがKeyInputDeviceStructを参照することはない。
そのため、key_input_event.cに構造体の定義を書くように修正した。

libevdev初期化/終了処理編の整理

テストコード

新たに5個のテストを追加した。
あまりシンタックスハイライトが効かず、コードが見にくいので、コードは省略する。
githubは幾分ましなので、テストコードの全貌が見たい方は↓へどうぞ。
github:key_input_event_test.cpp

プロダクトコード

テスト可能なコードを考えたことで、自然とマルチインスタンスなモジュールに進化した。
APIは次の通りだ。

struct KeyInputDeviceStruct;
typedef struct KeyInputDeviceStruct *KeyInputDevice;

KeyInputDevice CreateKeyInputDevice();
bool InitKeyInputDevice(KeyInputDevice dev, const char *device_file);
bool CleanupKeyInputDevice(KeyInputDevice dev);
void DestroyKeyInputDevice(KeyInputDevice dev);

実装は次のようになっている。

KeyInputDevice CreateKeyInputDevice() {
  KeyInputDevice dev = calloc(1, sizeof(KeyInputDeviceStruct));
  dev->fd = -1;
  dev->evdev = NULL;

  return dev;
}

bool InitKeyInputDevice(KeyInputDevice dev, const char *device_file) {
  if(dev == NULL) return false;

  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 false;
  }

  int rc = libevdev_new_from_fd(dev->fd, &dev->evdev);
  if (rc < 0) return false;

  return true;
}

bool CleanupKeyInputDevice(KeyInputDevice dev) {
  if(dev == NULL) return false;

  libevdev_free(dev->evdev);
  int rc = IO_CLOSE(dev->fd);
  if (rc < 0) return false;

  return true;
}

void DestroyKeyInputDevice(KeyInputDevice dev) {
  if(dev == NULL) return;

  free(dev);
  dev = NULL;
}

今の時点では、特に何ができる、ということもないが、プロダクトコード側もビルド・実行可能だ。プロダクトコードのmain.cは次のようになっている。

int main(void) {
  KeyInputDevice dev = CreateKeyInputDevice();
  InitKeyInputDevice(dev, "/dev/input/event2");
  CleanupKeyInputDevice(dev);
  DestroyKeyInputDevice(dev);

  return 0;
}

To Doリスト

キー入力に関するTo Doリストの状況は次の通りだ。
当初列挙した項目に終わりが見えてきた。

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

次回予告

「A」キー押下を検出する、を実装し、libevdev関係のモジュールの実装を完了する。

追記

Init()とCleanup()の返り値がboolなのは意味が取りにくい気がしてきた。
次のenumを追加し、コードを返す実装に変更した。
テストを修正してから、プロダクトコードを修正することで、自信を持ちながらコードを大胆に修正できる。

enum {
  INPUT_DEV_SUCCESS = 0,
  INPUT_DEV_INIT_ERROR = -1,
  INPUT_DEV_CLEANUP_ERROR = -2,
};
5
4
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
5
4