はじめに
googletest/googlemockを使って、組込みLinux上で動作するソフトウェアをTDDで開発しています。
実際にTDDで開発している手順をリアルタイムでそのまま書いているため、行き当たりばったりな部分があります
過程も楽しんで頂ければ幸いです。
不明な点、間違っている点があれば、コメント下さい。励みになります。
これまでの経緯を詳しく知りたい方は、前回までの記事をご覧下さい。
組込みに近いものをTDDで開発してみる〜準備編〜
組込みに近いものをTDDで開発してみる〜問題提起編〜
組込みに近いものをTDDで開発してみる〜file open編〜
組込みに近いものをTDDで開発してみる〜libevdev初期化/終了処理編〜
組込みに近いものをTDDで開発してみる〜キー入力検知編〜
今回の記事は有用度が低いかもしれません。
次の記事はおもしろくなる予定です。
LED制御
前回予告した通り、LED制御は裏で作りました。
特筆することがなかったため、結果だけ掲載します。もちろんTDDで実装しました。
少しサボって、LED書き込みのエラー処理はしていません。良い子は真似しないように!
ここでも、このLED制御モジュールのユーザーには内部構造を見せない実装にしています。
また、マルチインスタンス対応です。
LEDは複数個制御することが多くあると思うので、このように作っておけば、後々スケールします。
typedef struct LedDriverStruct {
int fd;
LedStatus status;
} LedDriverStruct;
LedDriver CreateLedDriver() {
LedDriver led = calloc(1, sizeof(LedDriverStruct));
led->fd = -1;
led->status = LED_UNKNOWN;
return led;
}
int InitLedDriver(LedDriver self, const char* device_file) {
self->fd = IO_OPEN(device_file, O_WRONLY|O_NONBLOCK);
self->status = LED_TURN_OFF;
if (self->fd < 0) {
return LED_DRIVER_INIT_ERROR;
}
return LED_DRIVER_SUCCESS;
}
void TurnOnLed(LedDriver self) {
if (self == NULL) return;
self->status = LED_TURN_ON;
IO_WRITE(self->fd, "1\n", 2);
}
void TurnOffLed(LedDriver self) {
if (self == NULL) return;
self->status = LED_TURN_OFF;
IO_WRITE(self->fd, "0\n", 2);
}
void ToggleLed(LedDriver self) {
if (self == NULL || self->status == LED_UNKNOWN) return;
if (self->status == LED_TURN_OFF) {
TurnOnLed(self);
} else {
TurnOffLed(self);
}
}
int CleanupLedDriver(LedDriver self) {
if (self == NULL) return LED_DRIVER_CLEANUP_ERROR;
int rc = IO_CLOSE(self->fd);
if (rc < 0) {
return LED_DRIVER_CLEANUP_ERROR;
}
return LED_DRIVER_SUCCESS;
}
void DestroyLedDriver(LedDriver self) {
if (self == NULL) return;
free(self);
self = NULL;
}
問題提起編で作ったコード
さて、久々にこのコードを見てどう思うでしょうか?
#define KEYBOARD_DEVICE "/dev/input/event2"
#define LED_DEVICE "/sys/class/leds/input2::capslock/brightness"
#define KEY_RELEASED 0
#define KEY_PRESSED 1
static void mainloop() {
struct libevdev *dev = NULL;
int key_fd = open(KEYBOARD_DEVICE, O_RDONLY|O_NONBLOCK);
int rc = libevdev_new_from_fd(key_fd, &dev);
if (rc < 0) {
fprintf(stderr, "Failed to init libevdev (%s)\n", strerror(-rc));
exit(1);
}
int led_fd = open(LED_DEVICE, O_WRONLY|O_NONBLOCK);
if (led_fd < 0) {
fprintf(stderr, "Failed to init LED device.\n");
exit(1);
}
bool led_on = false;
do {
struct input_event ev;
rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev);
if (rc == 0) {
if (ev.type == EV_KEY && ev.code == KEY_A && ev.value == KEY_PRESSED) {
led_on = !led_on;
char buf[2];
snprintf(buf, 2, "%d", led_on ? 1 : 0);
write(led_fd, buf, 2);
}
}
} while (rc == 1 || rc == 0 || rc == -EAGAIN);
libevdev_free(dev);
close(key_fd);
close(led_fd);
}
int main() {
mainloop();
return 0;
}
これだけの規模のコードですが、このコードどうやってテストするのでしょう?
手動で、プログラム全体をテストするしかなさそうです。(相当頑張ればユニットテストを書けますが…)
新しい機能を追加したとき、デグレが起こっていないことをどうやって保証できるのでしょうか?
テストがない状況では、前に進んでいるのか、後ろに戻っているのかもわかりません。
リファクタリングしたくなったとき、思い切って手を入れられるでしょうか?
リファクタリングする勇気は、テストによってもたらされます。
テストがないコードはリファクタリングできないので、次第に腐っていきます。
利用ライブラリが不安定だったらどうでしょうか?
今回は、libevdevという安定したライブラリを使っています。
利用ライブラリが不安定である場合、ライブラリ変更に伴う影響範囲は、プログラム全体(正確にはmainloop関数内ですが)、といって差し支えないでしょう。
例えば、もう1つキーボードを繋げて、今度は、「B」キーでNumLock LEDを制御したい、となったらとしたら、どうでしょうか。
下のようになるともう、かなり腐臭が漂ってきます。
static void mainloop() {
struct libevdev *dev1 = NULL;
struct libevdev *dev2 = NULL;
int key_fd1 = open(KEYBOARD_DEVICE1, O_RDONLY|O_NONBLOCK);
int key_fd2 = open(KEYBOARD_DEVICE2, O_RDONLY|O_NONBLOCK);
int rc = libevdev_new_from_fd(key_fd1, &dev1);
int rc2 = libevdev_new_from_fd(key_fd2, &dev2);
// これ以上書くのは忍びない
}
もう少し理性的な人であれば、配列を使うかもしません。(コードは適当です。コンパイルも試してません)
typedef struct LedStruct {
struct libevdev *dev;
int fd;
bool led_on;
}
static void mainloop() {
LedStruct leds[2];
leds[0].fd = open(KEYBOARD_DEVICE1, O_RDONLY|O_NONBLOCK);
leds[1].fd = open(KEYBOARD_DEVICE2, O_RDONLY|O_NONBLOCK);
...
do {
for (int i = 0; i < 2; i++) {
struct input_event ev;
rc = libevdev_next_event(leds[i].dev, LIBEVDEV_READ_FLAG_NORMAL, &ev);
if (rc == 0) {
if (ev.type == target_keys[i] && ev.code == target_codes[i] && ev.value == target_values[i]) {
leds[i].led_on = !leds[i].led_on;
char buf[2];
snprintf(buf, 2, "%d", leds[i].led_on ? 1 : 0);
write(leds[i].fd, buf, 2);
}
}
}
} while (rc == 1 || rc == 0 || rc == -EAGAIN);
}
だいぶくらくらしてきます。
whileの条件式で、iが1のときのreturn codeしかチェックしていませんね(気が付きましたか?)。
具象(デバイス操作)がベタ書きされており、抽象化を阻害する、という点も、ソフトウェアを拡張していく上で大きな障害となります。
TDDで作ったコード
TDDでユニットテストをうまく活用して、良い設計を導いてきました。
これまで作ったコードをまとめて見てみましょう。
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;
}
typedef struct LedDriverStruct {
int fd;
LedStatus status;
} LedDriverStruct;
LedDriver CreateLedDriver() {
LedDriver led = calloc(1, sizeof(LedDriverStruct));
led->fd = -1;
led->status = LED_UNKNOWN;
return led;
}
int InitLedDriver(LedDriver self, const char* device_file) {
self->fd = IO_OPEN(device_file, O_WRONLY|O_NONBLOCK);
if (self->fd < 0) {
// TODO: Look into possible errors.
return LED_DRIVER_INIT_ERROR;
}
return LED_DRIVER_SUCCESS;
}
void TurnOnLed(LedDriver self) {
if (self == NULL) return;
self->status = LED_TURN_ON;
IO_WRITE(self->fd, "1\n", 2);
}
void TurnOffLed(LedDriver self) {
if (self == NULL) return;
self->status = LED_TURN_OFF;
IO_WRITE(self->fd, "0\n", 2);
}
void ToggleLed(LedDriver self) {
if (self == NULL || self->status == LED_UNKNOWN) return;
if (self->status == LED_TURN_OFF) {
TurnOnLed(self);
} else {
TurnOffLed(self);
}
}
int CleanupLedDriver(LedDriver self) {
if (self == NULL) return LED_DRIVER_CLEANUP_ERROR;
int rc = IO_CLOSE(self->fd);
if (rc < 0) {
return LED_DRIVER_CLEANUP_ERROR;
}
return LED_DRIVER_SUCCESS;
}
void DestroyLedDriver(LedDriver self) {
if (self == NULL) return;
free(self);
self = NULL;
}
#define KEYBOARD_DEVICE "/dev/input/event2"
#define LED_DEVICE "/sys/class/leds/input2::capslock/brightness"
int main(void) {
KeyInputDevice press_a = CreateKeyInputDevice();
InitKeyInputDevice(press_a, KEYBOARD_DEVICE);
struct timeval time = {};
const struct input_event kPressA = {time, EV_KEY, KEY_A, INPUT_KEY_PRESSED};
SetKeyInputDetectCondition(press_a, &kPressA);
LedDriver caps_led = CreateLedDriver();
InitLedDriver(caps_led, LED_DEVICE);
while(1) {
if(CheckKeyInput(press_a) == INPUT_DEV_EVENT_DETECTED)
ToggleLed(caps_led);
}
CleanupKeyInputDevice(press_a);
DestroyKeyInputDevice(press_a);
CleanupLedDriver(caps_led);
DestroyLedDriver(caps_led);
return 0;
}
エラー処理、マルチインスタンス対応、などが入っているので、機能が等価とは言えないですが、コード量は3倍以上になっています。
さらに、テストコードはプロダクトコードより行数が多いです。
これは、小さな単位で問題に取り組んでいるので、「あれ、このケースどうなるんだっけ?」ということが目につきやすく、今回はそういう部分を(ある程度)真面目に実装してきた結果だと思います。
全体のコード量は多くなっていますが、各関数は短くまとまっており、1つのことしかやっていないです(単一責務の原則)。
新しい機能を追加したとき、デグレが起こっていないことをどうやって保証できるのでしょうか?
既存のテストがあります。既存のテストがパスしていれば、前に進めています。
リファクタリングしたくなったとき、思い切って手を入れられるでしょうか?
テストを頼りに容赦ないリファクタリングをしましょう。
(次回分でかなりリファクタリングしました。これもテストがあるおかげです。)
利用ライブラリが不安定だったらどうでしょうか?
キー入力のロジックはkey_input_event.c内で閉じています。
また、main.cではlibevdevのデータ構造を意識していません。
key_input_event.cの中で多少ライブラリの利用方法が変更されても、その変更はモジュールで閉じており、上位モジュールに伝播しません(オープン・クローズドの原則のクローズド)。
※input_eventはlinuxの入力イベントの仕組みなので、libevdev固有のものではありません。もし、ここも抽象化したい場合、独自にイベント形式を定義すれば良いでしょう。
もう1つキーボードを繋げて、今度は、「B」キーでNumLock LEDを制御したい、となったらとしたら、どうでしょうか?
初期化は個別に行う必要があります。
ただし、検知部分はおおよそ次のようになるでしょう。(可変長配列をライブラリから持ってくるともう少しスマート)
while(1) {
for (int i = 0; i < 2; i ++) {
if(CheckKeyInput(key_events[i]) == INPUT_DEV_EVENT_DETECTED)
ToggleLed(led_devices[i]);
}
}
key_input_event.cのAPIを見ると、次のAPIを持っていれば、同じインタフェースで統一できそうです。
- 初期化(検知条件設定含む)
- 検知
- 終了処理
このように単一責務の原則を関数レベルで実現できていると、共通部分が見つけやすく、抽象化を促進できる、と思います。
特に私のような未熟モノは、1つ具象を作ってみて初めて、「あ、こう抽象化すればうまくまとめられそう」とか、逆に「こう抽象化できそう、と思っていたけど、これいらんかったな」ということがまぁまぁあります。
準備編ではコールバックの仕組みが欲しいと考えていましたが、現時点では必要ありませんでした(CPU100%でループするのが気になる場合は、10msくらいスリープすれば十分でしょう)。
ソースコード
Release1.0として公開中。
今後の拡張
今までは、次の仕様を実現してきました。
- キーボードの「A」ボタンを押すと、caps lockのLEDがON/OFFする。
これを次のような仕様に変更します。要素としてタイマーが必要になります。
- キーボードの「A」ボタンを押すと、caps lockのLEDが点灯し、5秒後に消灯する。消灯するまでは、「A」キー押下を無視する。
これをTDDで拡張開発していきます。