[GO] Let's develop something close to embedded with TDD ~ file open edition ~

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 can not say that file open alone will be such an amount ...). 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 ~

Make a to-do list

I am a person who is faithful to the basics. So, follow the teachings of Kent Beck and James W. Grenning to create a to-do list.

Key input event

-[] Create a test showing virtual usage -[] Initialize the Input device -[] End processing of Input device -[] Detects pressing the "A" key

LED brightnesscontrol

-[] Create a test showing virtual usage -[] Initialize the LED device -[] Terminate the LED device -[] Turn on the LED -[] Turn off the LED

I arranged them roughly as I came up with. I can't think of anything else now. As you proceed with the implementation, you will come up with new items.

Now, where is it easy to touch? Now we have an understanding of libevdev. It seems good to start by implementing a keystroke event.

Write a test of the expected usage

How should KeyInputEvent be used? Envision the final format and write a virtual test. This test can be a real test or a fix in the process of development. It's just a starting point.

TEST_F(KeyInputEventTest, AbstractUse) {
  EXPECT_TRUE(InitKeyInputDevice());
  SetKeyInputEventListener(condition);

  for (int i = 0; i < 10; i++)
    EXPECT_FALSE(EventDetected());
  EXPECT_TRUE(EventDetected());

  EXPECT_TRUE(FinalizeKeyInputDevice());
}

Here's how to use the input event module assumed in this test.

  1. When I initialize the device, true is returned.
  2. Set the event to be detected (press the "A" key).
  3. EventDetected () returns false until the "A" key is pressed.
  4. EventDetected () returns true when the "A" key is pressed.
  5. Shut down the device.

There is no particular discomfort. Comment out this test and move on to the first test.

First test

There are the following two items when the initialization of the Input event is subdivided. We will create a test for Input event initialization, but first we will open the device file.

-[] Open the Input device file -[] Initialize libevdev

Test Input event initialization

The test is as follows. Expect true to come back when you call initialization.

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

If I try to run the test at this point, it doesn't even compile. The implementation to pass this test is as follows. At first, you may implement it by just returning true. This time, the content to be implemented is clear (file open!), So I won't take that detailed step.

#define DEVICE_FILE "/dev/input/event2"

bool InitKeyInputDevice() {
  int fd = open(DEVICE_FILE, O_RDONLY|O_NONBLOCK);
  if (fd < 0) return false;

  return true;
}

Build and run the code. (Root authority is required depending on the environment) The test passes.

At this point, I suddenly became curious. Is this module a single instance assumption? I haven't got an answer yet. Add questions to your to-do list to move on.

-[] Do you support multiple instances?

In the question raising section, it was unclear whether error handling would work. How about this time? For example, I want the following test to succeed. For that purpose, it is necessary to fail the initialization.

TEST_F(KeyInputEventTest, FailToInitInputDevice) {
  EXPECT_FALSE(InitKeyInputDevice());
}

It is difficult to generate an error with the current product code. If you delete the event device file, you will get an error, but you can't destroy the system for a successful unit test. The problem is clear. It's because the macro hard-codes the device file path in the product code.

Therefore, the interface of the initialization function is changed. Give the device file to open from the outside.

bool InitKeyInputDevice(const char *device_file);

Here's the code that tests the initialization to fail: If you pass the path of an impossible device file, you should get an error with ENOENT (the specified file cannot be found).

TEST_F(KeyInputEventTest, FailToInitInputDevice) {
  EXPECT_FALSE(InitKeyInputDevice("/dev/input/not_found"));
}

The test succeeds. Having a testable implementation made the code more flexible. This feels pretty good.

At this point, I suddenly thought. In a successful initialization test, I purposely opened a real input event device, do I need to do that? I can't think of a logical reason to use the real thing. The tests for successful initialization are now equivalent.

TEST_F(KeyInputEventTest, CanInitInputDevice) {
  std::ofstream("./test_event");
  EXPECT_TRUE(InitKeyInputDevice("./test_event"));
  std::remove("./test_event");  //Do not muddy the standing bird traces
}

The test passes. Alright, now you don't need root privileges to run the test. The test will be run tens of thousands of times. It should be easy to carry out even a little.

And next I think like this. The only file open error that can be tested with the current code is ENOENT. Do I have to test other errors? You should write the tests required for the product (but time and consultation). This time around, we know that real input event devices can't be opened without permission. If you couldn't open it without permission, it would be nice to log it out. Add the following items to your to-do list.

-[] Output the log if opening fails with Permission denied.

However, it is difficult to mock the POSIX standard library. The definition of write () is done in libc, which is automatically linked when creating a C language program. You can do your best to deceive the linker, but it seems easier to prepare a wrapper function.

  int IO_OPEN(const char *pathname, int flags);

In the test, link the mock of IO_OPEN (), and in the product code, call open () in IO_OPEN ().

The test using the mock is as follows. On success, a positive number of file descriptors are returned. Normally it should be 3 or more. As a side note to EXPECT_CALL (), this test tests that IO_OPEN () is called once. When the product code calls IO_OPEN (), it returns 3 as the return value.

TEST_F(KeyInputEventTest, CanInitInputDevice) {
  EXPECT_CALL(*mock_io, IO_OPEN(_, _)).WillOnce(Return(3));
  EXPECT_TRUE(InitKeyInputDevice("./test_event"));
}

Reviewing the product code, InitKeyInputDevice () returns true when IO_OPEN () returns a number greater than or equal to 0. The mock returns 3 so the ↑ test succeeds.

bool InitKeyInputDevice(const char *device_file) {
  int fd = IO_OPEN(device_file, O_RDONLY|O_NONBLOCK);
  if (fd < 0) return false;

  return true;
}

In case of file open failure, according to Man page of OPEN

Returns -1 if an error occurs (errno is set appropriately in that case).

So, to test for IO_OPEN () failure, you just have to return -1 when the mock is called.

TEST_F(KeyInputEventTest, FailToInitInputDevice) {
  EXPECT_CALL(*mock_io, IO_OPEN(_, _)).WillOnce(Return(-1));
  EXPECT_FALSE(InitKeyInputDevice("./file_not_found"));
}

Well, the introduction has become long, but what I'm going to do now is ↓.

-[] Output the log if opening fails with Permission denied.

We want to test that errno changes the behavior of the product code, so we set errno in the mock call action. With googlemock, it is possible to execute any function when calling a mock. This time, in the lambda expression, set EACCES to errno so that the function that returns -1 is executed as the return value of IO_OPEN ().

TEST_F(KeyInputEventTest, FileOpenPermissionDenied) {
  EXPECT_CALL(*mock_io, IO_OPEN(_, _)).WillOnce(
    Invoke([](constchar*,int){errno=EACCES;return-1;}));
  EXPECT_FALSE(InitKeyInputDevice("./event"));
}

Well, it's still halfway. How can I test that the log is output? All you have to do is report the log output by the product code to the spy.

TEST_F(KeyInputEventTest, FileOpenPermissionDenied) {
  //Spy settings
  char *spy[] {new char[128]};
  set_DEBUG_LOG_spy(spy, 128);

  InitKeyInputDevice("./event");  //Execution of the test target
  EXPECT_STREQ("Fail to open file. You may need root permission.",
               spy);
}

What the spy is doing is not a big deal. Just wait for the product code to call DEBUG_LOG () and copy the output to the pre-passed buffer.

static char *buffer = NULL;
static int buffer_size = 0;

void DEBUG_LOG(const char *message) {
  strncpy(buffer, message, buffer_size-1);
}

void set_DEBUG_LOG_spy(char *b, const int size) {
  buffer = b;
  buffer_size = size;
}

The product code is as follows. The point is that the log is not output directly using a function such as printf (). There is a chance for spies to get in there.

bool InitKeyInputDevice(const char *device_file) {
  int fd = IO_OPEN(device_file, O_RDONLY|O_NONBLOCK);
  if (fd < 0) {
    if (errno == EACCES)
      DEBUG_LOG("Fail to open file. You may need root permission.");
    return false;
  }

  return true;
}

Even in actual development, you will often use a debug-only API that controls the debug level. Use the linker to guide such APIs to spies only during testing!

Organize file open edition

The status of the to-do list is as follows.

-[x] Create a test showing virtual usage -[] Initialize the Input device -[x] Open Input device file -[x] Output a log if opening fails with Permission denied. -[] Initialize libevdev -[] End processing of Input device -[] Detects pressing the "A" key

The code at this point can be found in the FileOpen tag. The source code created this time is under led_controller. https://github.com/tomoyuki-nakabayashi/TDDforEmbeddedSystem

Test code

It has passed three tests.

TEST_F(KeyInputEventTest, CanInitInputDevice) {
  EXPECT_CALL(*mock_io, IO_OPEN(_, _)).WillOnce(Return(3));
  EXPECT_TRUE(InitKeyInputDevice("./test_event"));
}

TEST_F(KeyInputEventTest, FailToInitInputDevice) {
  EXPECT_CALL(*mock_io, IO_OPEN(_, _)).WillOnce(
    Invoke([](constchar*,int){errno=ENOENT;return-1;}));
  EXPECT_FALSE(InitKeyInputDevice("./file_not_found"));
}

TEST_F(KeyInputEventTest, FileOpenPermissionDenied) {
  std::unique_ptr<char[]> spy {new char[128]};
  set_DEBUG_LOG_spy(spy.get(), 128);

  EXPECT_CALL(*mock_io, IO_OPEN(_, _)).WillOnce(
    Invoke([](constchar*,int){errno=EACCES;return-1;}));

  EXPECT_FALSE(InitKeyInputDevice("./test_event"));
  EXPECT_STREQ("Fail to open file. You may need root permission.",
               spy.get());
}

Product code

bool InitKeyInputDevice(const char *device_file) {
  int fd = IO_OPEN(device_file, O_RDONLY|O_NONBLOCK);
  if (fd < 0) {
    if (errno == EACCES)
      DEBUG_LOG("Fail to open file. You may need root permission.");
    return false;
  }

  return true;
}
int main(void) {
  InitKeyInputDevice("/dev/input/event2");
  return 0;
}

int IO_OPEN(const char *pathname, int flags) {
  return open(pathname, flags);
}

void DEBUG_LOG(const char *message) {
  printf("%s\n", message);
}

If your environment requires root privileges to open the input event, running the product code without root privileges will result in the following:

./led_controller
Fail to open file. You may need root permission.

If you specify a random file path, nothing is output. This is what I intended in 100% testing. You should have a lot of confidence in how your product code works.

Next time preview

We will develop the initialization and termination of the Input device with TDD. Detecting the "A" key press is considered to be a volume for one article.

As I wrote at the top, if you have any questions or mistakes, please comment.

Recommended Posts

Let's develop something close to embedded with TDD ~ file open edition ~
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 ~ Design pattern ~
Let's develop something close to embedded with TDD ~ Key input detection version ~
Let's develop something close to embedded with TDD ~ libevdev initialization / termination processing ~
File operations with open — "../"
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 ~ 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 develop something close to embedded with TDD ~ libevdev initialization / termination processing ~
Steps to develop Django with VSCode
[Introduction to WordCloud] Let's play with scraping ♬
[Introduction to Python] Let's use foreach with Python
How to read problem data with paiza
Let's develop an investment algorithm with Python 1
Steps to develop Django with VSCode
[Python] Write to csv file with Python
Output to csv file with Python
Output cell to file with Colaboratory
Open the file with the default app
I tried to make an open / close sensor (Twitter cooperation) with TWE-Lite-2525A