Let's develop something close to embedded with TDD ~ Key input detection version ~

Introduction

Using googletest / googlemock, I am developing software that runs on embedded Linux with TDD. Since the procedure actually developed with TDD is written as it is in real time, there is a random part. I hope you enjoy the process as well. If you have any questions or mistakes, please comment. It will be encouraging.

If you would like to know more about the history so far, please see the previous articles. Try to develop something close to embedded with TDD ~ Preparation ~ Try to develop something close to embedded with TDD ~ Problem raising ~ Develop something close to embedded with TDD ~ file open ~ Try to develop something close to embedded with TDD ~ libevdev initialization / termination processing ~

input_event

Before proceeding, let's check the structure definition used in the keystroke event.

struct input_event {
        struct timeval time;  //Time of input
        __u16 type;  //Input type. EV_KEY:Key input, EV_REL:Mouse movement
        __u16 code;  //Input code. KEY_A:A key, REL_X:Mouse X-axis movement
        __s32 value; // type==EV_For KEY, 1 when the key is pressed, 0 when released
};

In the range used this time, time is not used. When you want to record and reproduce keystrokes, you can use time. This time, you only need to know if it was pressed.

reference Tummy wiki input_event

Key input detection

Envision a test that detects a particular keystroke.

TEST_F(KeyInputEventTest, DetectCondition) {
  //Input meaning that the "A" key was pressed_event
  constexpr input_event kPressA {timeval{}, EV_KEY, KEY_A, PUT_KEY_PRESSED};

  SetKeyInputDetectCondition(dev_, &kPressA);
  EXPECT_EQ(INPUT_DEV_EVENT_DETECTED, CheckKeyInput(dev_));
}

Create the input_event you want to detect and register it with * SetKeyInputDetectCondition () *. When * CheckKeyInput () * is called, if the target input event has occurred, the corresponding status code will be returned.

Now, to complete the test, we need to set the expected value of the libevdev mock called * CheckKeyInput () *. This time, the API used has ** side effects **, so it's a little complicated.

The API to get events with libevdev is as follows.

int libevdev_next_event	(struct libevdev *dev,
                         unsigned int flags,  //Ignore it because it's related now
                         struct input_event *ev)	

When successful, the return value is LIBEVDEV_READ_STATUS_SUCCESS (0), and the input_event that occurred in the third argument ev is set. Other than the return value, the side effect is that the state changes before and after the API call. In C language, passing a pointer to a parameter and having it filled with data is quite natural and frequent. Due to the language specifications, there is no choice but to design an API that causes such side effects to some extent. However, APIs that cause side effects should be reduced as much as possible. Side effects should also be evaluated in the test. Therefore, if you use a lot of side effects, you will have to write a lot of expected values for API calls, which makes testing cumbersome. (As an aside, if you use C ++, don't bring in side effects in C. If you're doing the same thing in C ++, you're making a design mistake.)

Now, let's take a look at this test. When * libevdev_next_event () * is called, input_event when the "A" key is pressed is set as the third argument.

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_));
}

It's a little hard to read because of the poor syntax highlighting. Now, the mock action * DoAll () * means to execute all of the multiple actions. There are two actions to take in * DoAll () *:

Not surprisingly, this test doesn't compile (maybe it's about time?).

First, the event to be detected is recorded by * SetKeyInputDetectCondition () * by adding a member to KeyInputDeviceStruct.

typedef struct KeyInputDeviceStruct {
  int fd;
  struct libevdev *evdev;
  struct input_event target_event;  //add to
} KeyInputDeviceStruct;

void SetKeyInputDetectCondition(KeyInputDevice dev,
                                const struct input_event *ev) {
  memcpy(&dev->target_event, ev, sizeof(struct input_event));
}
//Helper function to check if any event has occurred
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_Helper function to compare event structures
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;
}

I showed only the result, but the helper function was extracted again after passing the test once. Thanks to these two helper functions, you can write conditional expressions in if statements with clear conditions that anyone can see: * HasPendingEvent () && IsTargetEvent () *. The process of refactoring is not shown clearly, but we are always doing such small refactoring. You can do that because it's safe to change your code, thanks to testing.

Next, write a test when no event has occurred. According to libevdev's API spec, * libevdev_next_event () * returns -EAGAIN if there are no valid events.

TEST_F(KeyInputEventTest, CannotDetectEvent) {
  //If no event has occurred,-EAGAIN is returned
  EXPECT_CALL(*mock_libevdev, libevdev_next_event(_, _, _)).WillOnce(Return(-EAGAIN));

  SetKeyInputDetectCondition(dev_, &kPressA);
  EXPECT_EQ(INPUT_DEV_NO_EVENT, CheckKeyInput(dev_));
}

Let's test a slightly more complicated pattern. The next test tests a sequence of no events, two uninteresting events, and an event you want to detect. The test code is complicated and disgusting, but I have a little more patience.

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_));
}

This test passes with the implementation of the product code so far. Update your to-do list.

-[x] Detects pressing "A" key

Well, I was able to implement everything I wanted to do with key input detection. I waited for a while. Remember that there is one item you have forgotten?

After Cleanup (), using libevdev fails

Last time, I saw an important specification in the termination process of libevdev and added it to the to-do list.

-[] After Cleanup (), using libevdev fails.

Let's take a closer look at libevdev's termination process, * libevdev_free () *.

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

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

By the way, what kind of behavior does free () in C language do? I'm not sure of my memory. Certainly, even with free (), the point pointed to by the pointer should have remained the same.

TEST_F(KeyInputEventDetectionTest, TestFree) {
  int *mem = static_cast<int*>(calloc(1, sizeof(int)));
  int *tmp = mem;

  free(mem);
  EXPECT_EQ(tmp, mem);
}

The memory was certain. The above test passes.

Show test and product code.

Test code

TEST_F(KeyInputEventDetectionTest, FailOperationAfterCleanup) {
  EXPECT_CALL(*mock_libevdev, libevdev_free(_)).Times(1);

  auto dev = CreateKeyInputDevice();
  CleanupKeyInputDevice(dev);
  // Cleanup()Keystroke detection of dev that was done fails
  EXPECT_EQ(INPUT_DEV_INVALID_DEV, CheckKeyInput(dev));

  DestroyKeyInputDevice(dev);
}

Product code

int CheckKeyInput(KeyInputDevice dev) {
  //Added guard to evdev null pointer
  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;  //add to
  int rc = IO_CLOSE(dev->fd);
  if (rc < 0) return INPUT_DEV_CLEANUP_ERROR;

  return INPUT_DEV_SUCCESS;
}

Now, after calling libevdev_free (), API calls that use the libevdev structure will fail.

Arrangement of key input detection

Key input detection is now possible. APIs that cause side effects can also be tested with good use of mock.

Test code

Added 4 new tests. A total of 12 tests have passed. The syntax highlighting doesn't work and the code is hard to see, so I'll omit the code. Github is a little better, so if you want to see the whole test code, please go to ↓. github:key_input_event_test.cpp

Product code

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 (Press the "A" key twice to end the program)

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 List

The status of the to-do list regarding keystrokes is as follows. The items listed at the beginning have come to an end.

-[x] Create a test showing virtual usage -[x] Initialize the Input device -[x] Open Input device file -[x] Output a log if opening fails with Permission denied. -[x] Initialize libevdev -[x] End processing of Input device -[x] Support for multiple instances ~~? ~~ -[x] Detects pressing "A" key --[x] After Cleanup (), using libevdev fails.

Next time preview

We plan to secretly make the LED control behind the scenes and publish only the results. I think it will be easier to make than the key input process. If there is something special to mention, I will add a little explanation.

Next time, I will compare the program created in the question raising section with the program created in TDD. At the same time, I would like to think about future expansion.

Recommended Posts

Let's develop something close to embedded with TDD ~ Key input detection version ~
Let's develop something close to embedded with TDD ~ Preparation ~
Let's develop something close to embedded with TDD ~ Intermediate review ~
Let's develop something close to embedded with TDD ~ Problem raising ~
Let's develop something close to embedded with TDD ~ Design pattern ~
Let's develop something close to embedded with TDD ~ file open edition ~
Let's develop something close to embedded with TDD ~ libevdev initialization / termination processing ~
Convert mp4 to mp3 with ffmpeg (thumbnail embedded version)