[GO] Let's develop something close to embedded with TDD ~ libevdev initialization / termination processing ~

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 ~

libevdev initialization

Initialization success test

Now consider a test to initialize libevdev. It would be nice if libevdev's initialization function was called in addition to the open () created last time.

TEST_F(KeyInputEventTest, CanInitEvdev) {
  const int kFd = 3;
  //Mock is IO_OPEN()File descriptor in"3"return it
  EXPECT_CALL(*mock_io, IO_OPEN(_, _)).WillOnce(Return(kFd));
  //Expected value for the newly added libevdev mock
  EXPECT_CALL(*mock_libevdev, libevdev_new_from_fd(kFd, _))
    .WillOnece(Return(0));

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

In this EXPECT_CALL (), we expect that the opened file descriptor is passed to the first argument of libevdev_new_from_fd () ("_" means that the argument is don't care). The mock tests whether the function in question was called with the specified arguments.

As promised by TDD, this test doesn't even compile. First, let's make a mock so that it can be compiled.

The header includes the real libevdev.h to avoid minor mistakes such as spelling mistakes.

#include <gmock/gmock.h>
//This header is real
#include <libevdev/libevdev.h>

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

extern MOCK_LIBEVDEV *mock_libevdev;

extern "C" {
  //Product code is libevdev_new_from_fd()This function is called when you call
  //Mock is called in it.
  int libevdev_new_from_fd(int fd, libevdev **dev)
  {
    return mock_libevdev->libevdev_new_from_fd(fd, dev);
  }
}

If you implement a mock, it will compile. In your tests, be careful not to link the real library, but the mock. Even if the compilation passes, the test fails because it does not meet the expected value of calling libevdev_new_from_fd () in Init ().

[ 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)

Add the following two lines to Init () and the test will succeed.

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

Initialization error

Now let's consider a test when a libevdev initialization error occurs. Looking at the libevdev documentation, libevdev_new_from_fd () seems to return 0 on success and a negative error code otherwise. (In the EXPECT_CALL () added earlier, the return value of libevdev_new_from_fd () was set to 0 because I knew this specification.) Possible error codes cannot be read from the document.

libevdev libevdev_new_from_fd()

Let's take a look at the unit tests that come with the libevdev source code to see what error codes are returned.

[libevdev github mirror] (https://github.com/whot/libevdev/blob/master/test/test-libevdev-init.c)

There was a test I was looking for.

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

It seems that EBADF is returned when fd given to libevdev is -1, and ENOTTY is returned when fd is standard input. No other test cases for failure can be found. Intuitively, it feels strange that no other error is returned. Go see the source code. ENOMEM due to memory allocation failure, ENOENT when there is no device, EBADF when the libevdev structure passed for initialization has already been initialized, and other situations where errno is returned seems to return that errno. If you are interested, please see the code below.

github libevdev.c

So far, there seems to be no error that needs special attention. If libevdev_new_from_fd () returns a negative number, let Init () return false. The test was as follows. I'm not interested in the argument to libevdev_new_from_fd (), so don't care.

TEST_F(KeyInputEventTest, InitEvdevFailed) {
  EXPECT_CALL(*mock_libevdev, libevdev_new_from_fd(_, _))
    .WillOnce(Return(-EBADF));  //EBADF as a representative. Boundary values may be tested if strictness is a concern.

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

To get a product code that meets this test, add the following two lines:

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

If you write the code like this, googlemock will output the following warning. This is normal operation of googlemock, and there is no problem with both test code and product code.

[ 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)

The cause of the warning is that when you call Init (), you call IO_OPEN (), but you don't write the expected value. As you can see in the warning, this test is not interested in calling IO_OPEN () and can be safely ignored. If you want to debug, you can limit the output of this warning by specifying the following options when running the test. --gmock_verbose=error However, the warning should be read properly to make sure you are not making an unintended call.

Error handled by libevdev_new_from_fd () (can be skipped)

libevdev_new_from_fd () doesn't seem to be able to handle why file open failed. Whether the file open failure is Permission denied or No such file or directory, it is treated as a Bad file descriptor.

As a test, I wrote the following test and ran it without root privileges. Attempting to open the event device without root privileges results in a Permission denied error, so the expected value returned by libevdev_new_from_fd () is set to -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);
}

The result fails as follows.

[ 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)

I wanted it to be -EACCES (-13), but it was actually a Bad file descriptor error, namely -EBADF (-9).

libevdev termination process

There are times when the end processing test does not work well. Let's write it on both the rabbit and the corner.

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

What does Cleanup () return for true? Can it return false? Don't rush, check the specifications of libevdev and close.

libevdev libevdev_free()

void libevdev_free(struct libevdev *dev)	

Especially, it is unlikely that an error will be returned. However, I found information that might be useful in the future.

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

That is, a function call that uses libevdev after calling Cleanup () should fail. Add it to your to-do list.

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

Then Man page of close.

close () returns 0 on success. If an error occurs, return -1 and set errno appropriately. EBADF fd is not a valid open descriptor. The EINTR close () call was interrupted by a signal. See signal (7). An EIO I / O error has occurred.

EBADF is likely to occur. It is a pattern that a descriptor that is not open passes to close. A test that successfully exits libevdev looks like this:

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

In the current product code, fd and libevdev are defined as local variables in Init (). As it is, the target to be processed by Cleanup () is unknown, so share fd and libevdev with Init (). Collect the necessary data in a structure and define it in the file scope. Let's initialize it with an explicitly invalid file descriptor.

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

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

// Init()Initializes the member variables of dev.
bool InitKeyInputDevice(const char *device_file) {
  dev.fd = IO_OPEN(device_file, O_RDONLY|O_NONBLOCK);
  if (dev.fd < 0) {
  ...
}

how about that? There seems to be no particular problem. Let's implement Cleanup (). The code is omitted, but the necessary mock is also implemented.

bool CleanupKeyInputDevice() {
  libevdev_free(dev.evdev);  //Call a mock in the test
  int rc = IO_CLOSE(dev.fd);  //Call a mock in the test
  if (rc < 0) return false;  //You may implement without this line at first

  return true;
}

The actual test to test the above code looks like this: (Actually, I wrote the test first)

//I've prepared an Init helper because I think I'll use it in the future.
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;  //File descriptor"3"
  InitHelper(kFilePath, kFd, 0);

  // libevdev_free()Just make sure that is called
  EXPECT_CALL(*mock_libevdev, libevdev_free(_)).Times(1);
  //File descriptor"3"Expected value to close
  EXPECT_CALL(*mock_io, IO_CLOSE(kFd)).WillOnce(Return(0));

  EXPECT_TRUE(CleanupKeyInputDevice());
}

The test passes successfully!

Then, this is the next. Calling Cleanup before Init should fail.

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

Now, the dev structure is initialized as follows. So, the mock of IO_CLOSE () is likely to pass the test if it returns -1 when the argument is INVALID_FD.

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

bool CleanupKeyInputDevice() {
  libevdev_free(dev.evdev);
  int rc = IO_CLOSE(dev.fd);  //In uninitialized, dev.fd is INVALID_FD(-1)
  if (rc < 0) return false;

  return true;
}

In other words, it's a test.

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

  EXPECT_FALSE(CleanupKeyInputDevice());
}

Well, build and test pass ... no!

[ 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)

The argument of IO_CLOSE () is "3". This result is natural. Because, before this test is run, there is a test running that assigns "3" to dev.fd! Since dev is a static structure, its lifetime is as long as the program is running. In other words, the result of this test depends on other test executions.

Single-instance modules (or classes that apply the Singleton pattern) tend to be difficult to test. This is because you cannot write an independent test unless you restore all the states that changed in the previous test. Except for modules that must be single-instance, I think it is better to assume multi-instance as much as possible.

Now, let's modify the code so that it can be multi-instantiated. This fix forces changes to existing APIs. However, you can proceed while confirming that the tests you have created so far pass even with the new API.

Add / modify API as follows for multi-instance.

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 has pointers to file descriptors and libevdev structures as follows:

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

Create () allocates memory and sets initial values as follows, and returns a pointer to it.

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

  return dev;
}

Destroy () releases the passed pointer.

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

Init () and Cleanup () only operate the pointer given as an argument instead of the structure previously defined as a static variable.

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);
...

By changing the API in this way, the following Cleanup () test that failed earlier will succeed.

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

  EXPECT_FALSE(CleanupKeyInputDevice(dev_));
}

By the way, in all tests, Create () and Destroy () are used to create and destroy KeyInputDevice. In order to eliminate duplication of test code, common processing is written in SetUp () / TearDown () of the test fixture. Keep your test code DRY (Don't Repeat Yourself).

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

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

Now, all APIs need to be prepared for null pointers. For example, if you write a test like this, the test will die from a segmentation fault.

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

At the entrance of the function, let's put a guard against the null pointer.

bool InitKeyInputDevice(KeyInputDevice dev, const char *device_file) {
  if(dev == NULL) return false;
  // if(!dev)Is not a hobby

Encapsulate internal structure

The module we are creating now has an event device file descriptor and libevdev as data. ** This internal structure is something that users of this module do not need to know. ** ** The user only needs to know how to create an instance of KeyInputDevice with Create () and pass the created instance to each function. If you expose the internal structure to the user poorly, you cannot complain even if the user implements it depending on the internal structure. As a result, ** the effect of modifying the internal structure extends to the user's code, and in the worst case, the internal structure cannot be modified. ** **

This time, I split the header into key_input_event.h and key_input_event_private.h. In the former, only the information ** that the user needs to use this module ** is written.

【key_input_event.h】

//Define forward declaration of structure and its pointer type as 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);

Implementation details are defined in private headers. 【key_input_event_private.h】

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

By doing so, the internal structure can be changed without spreading to the user. (Except if the user is in a rough state!)

[Addition] No one else sees the KeyInputDeviceStruct at this point. Therefore, it was modified to write the structure definition in key_input_event.c.

Organize libevdev initialization / termination processing

Test code

Added 5 new tests. The syntax highlighting doesn't work very well 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

By thinking of testable code, it naturally evolved into a multi-instance module. The API is as follows.

struct KeyInputDeviceStruct;
typedef struct KeyInputDeviceStruct *KeyInputDevice;

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

The implementation is as follows.

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

At this point, there is nothing that can be done, but the product code side can also be built and executed. The main.c of the product code is as follows.

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

  return 0;
}

To Do List

The status of the to-do list for 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 ~~? ~~ -[] Detects pressing the "A" key

Next time preview

Implement "A" key press detection, and complete the implementation of libevdev related modules.

Postscript

I feel that it is difficult to make sense that the return values of Init () and Cleanup () are bool. I added the following enum and changed it to an implementation that returns code. By modifying the test and then the product code, you can confidently modify the code boldly.

enum {
  INPUT_DEV_SUCCESS = 0,
  INPUT_DEV_INIT_ERROR = -1,
  INPUT_DEV_CLEANUP_ERROR = -2,
};

Recommended Posts

Let's develop something close to embedded with TDD ~ libevdev initialization / termination processing ~
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 ~ Key input detection version ~
[Let's play with Python] Image processing to monochrome and dots