C
C++
Linux
embedded
googletest

組込みに近いものをTDDで開発してみる〜準備編〜

はじめに

組込み開発でTDDをやるために勉強してきました。
今までアプリケーション中心の勉強だったため、少し組込みっぽい領域のものを作ってみます(組込みLinuxを想定)。
見切り発車なので、途中で方針変わるかもしれません。実際やらないとわからないので、温かい目で見守って下さい。

ご意見、ご要望は遠慮なくフィードバック下さい!とても励みになります。

作ろうとしているもの

やはり組込みと言えばLチカでしょう!
ただし、簡単に試せるようにしたいので、キーボードのLEDをチカチカさせます(caps lockとかのやつ)。
高機能なライブラリは使用せず、デバイスファイルの操作でなんやかんややって行きます。

あまり簡単すぎるとツマンナイですし、実践的でないため、次の機能実現を考えていきます。

  • キーボード入力で振る舞いを変える(ボタン)
  • 設定時間になると振る舞いを変える(タイマ)

組込みなので、別ハードへの移植性は常に考えるようにします。

開発環境

OS Virtual box ubuntu 16.04
コンパイラ gcc (Ubuntu 5.4.0-6ubuntu1~16.04.5) 5.4.0 20160609
ビルドツール autotools, pkg-config
ライブラリ libevdev-1.0
テストフレームワーク googletest/googlemock@62ba5d9
プロダクトコード C or C++(悩み中)

下調べ

何をするにも下調べは重要ですね。
とりあえず、自分の環境を調べてみます。

キーボードイベント取得

Linuxでキーボード入力イベントを取得するためには、/dev/input/event〇〇ファイルを監視します。
〇〇の部分は数字が入るのですが、環境や起動時に認識された順でキーボードが何番になるか変わります(udevで固定することが可能ですが、今回はとりあえず置いておきます)。

/proc/bus/input/devicesを覗いて、キーボードを探します。
現状は、event2(input2)がキーボードに対応しています。

cat /proc/bus/input/devices
== 途中省略 ==
I: Bus=0011 Vendor=0001 Product=0001 Version=ab41
N: Name="AT Translated Set 2 keyboard"
P: Phys=isa0060/serio0/input0
S: Sysfs=/devices/platform/i8042/serio0/input/input2
U: Uniq=
H: Handlers=sysrq kbd event2 leds
B: PROP=0
B: EV=120013
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
B: MSC=10
B: LED=7
== 以下省略 ==

/dev/inputのようなイベントデバイスのために、Linuxはevdev(Linux Input drivers v1.0)インタフェースを提供しています。
このevdevのラッパーライブラリがlibevdevです。
今回は、このlibevdevを使っていきます。

キーボードLED

次に、キーボードのLEDを確認します。
LinuxではLED classが用意されており、LinuxにLEDが認識されると、sysfsの/sys/class/leds下にexportされます。
capslock, numlock, scrolllockのLEDが認識されていますね。
(私のキーボードにはnumlockとscrolllockキーにはLEDついていないんですが…要調査)

ls /sys/class/leds/
input2::capslock  input2::numlock  input2::scrolllock

ライブラリ理解のためのテスト

ハード(キーボード)もライブラリ(libevdev)もある、という恵まれた状態からのスタートなので、対象ライブラリを理解するためにテストを書いていきます。
このテストを書くことで、自分のライブラリ理解をテストします。理解したことがテストとして残るのは、とても良いことに思えます。

これがホストマシン上で実行できる場合は、ラッキーです。
1. ターゲットボードにしか対象のハード/ライブラリがない、という状況も多いのではないでしょうか。
2. ターゲット上ですら動かない、物がない、っていうのもよくある話でしょう。

1の場合は素直にクロスビルドしながら進められます。
2の場合、APIがわかっているのであれば、モックを作りましょう。本物が出てきた時に、テストで書いた使い方が正しいかどうかテストできます。
APIもわからないとなると想像でモックを作ります。実装の詳細が分かっていない状態でモックを作ることにも利点はあって、APIが程よく抽象化できる可能性があります。

libevdev理解のために書いたテスト

前置きが長くなりましたが、今回書いたテストを見ていきましょう。
コードは一部省略して掲載します。全貌が見たい方はこちら

まず、ファイルディスクリプタから、evdevの制御データを取得します。
成功したら、0以上が返ってきます。
最初、このテストは失敗しました。ルート権限が必要だとわかったので、テスト名をわかるように(AsRootを追加)します。
これで、後日同じことでハマることはないでしょう。急に誰かに引き継ぐことになっても、root権限がないと動かせないことがわかるでしょう。

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

  EXPECT_GE(rc, 0);
}

evdevの制御データが取得できたので、何も入力がない状態では、EAGAINが返ってくることを確認します。
これは一発でパスしました。

TEST_F(EvdevSampleTest, ReturnsEAGAIN) {
  input_event ev {};
  int actual = libevdev_next_event(evdev_, LIBEVDEV_READ_FLAG_NORMAL, &ev);
  EXPECT_EQ(-EAGAIN, actual);
}

次にEnterキーを押すと、LIBEVDEV_READ_STATUS_SUCCESSが返ってきて、期待通りのinput_eventが取得できることをテストします。
テスト実行後、本テスト実行中にテストが止まるので、Enterキーを押してあげると、テストがパスします(もはやユニットテストでないが気にしません)。
最後のEXPECT_EQにたどり着いている時点で、取得したイベントが期待値通りであることが確定していますが、とりあえず気にしません。

TEST_F(EvdevSampleTest, CaptureEnterKeyPress) {
  auto expect = create_key_event(KEY_ENTER, KEY_PRESSED);
  input_event actual {};
  while (true) {
    int rc = libevdev_next_event(evdev_, LIBEVDEV_READ_FLAG_NORMAL, &actual);
    if ((rc == LIBEVDEV_READ_STATUS_SUCCESS) && actual == expect) break;
  }

  EXPECT_EQ(expect, actual);
}

テストを作りながら考えたこと

他の入力デバイスでも使えるようにすることを考えると、汎用的なコールバックインタフェースがあると良さそう。
libevdevはコールバックの仕組みを提供してくれないので、コールバックの仕組みは一から作るかな。

今後の予定

次:
組込みに近いものをTDDで開発してみる〜問題提起編〜