Unit testing RTOS dependent code – RTOS Test-Double – Part 3

I’m going to bend the example a little, and look at how to write tests that interact with an interrupt service routine and again with a semaphore. I’ll also get some help with the tedious parts of building fakes by using Mike Long’s Fake Function Framework. (BTW: I am using a beta version, that has changes the released version does not yet have.)

This figure shows the concurrent runtime relationship between the MessageProcessor and its SerialDriver.



The MessageProcessor tells the SerialDriver that is wants to wait for some characters, also passing along the Semaphore it will wait on. MessageProcessor then waits on its Semaphore. The ISR collects characters one at a time until it has satisfied its request. Then wakes up the MessageProcessor by posting to its Semaphore.

The MessageProcessor is oblivious to what happens concurrently. It just wants some characters and the request is satisfied asynchronously. Here is the MessageProcessor code that interacts with the ISR and the RTOS.

int MessageProcessor_WaitForMessage(char * buffer, size_t length)
{
    INT8U error = 0;
    SerialInterrupt_WaitForString(buffer, length, int_sync);
    OSSemPend(int_sync, 1000, &error);
    return error == OS_ERR_NONE;
}

Like in the last article, The OSSempend test-double satisfies what the caller is waiting for. This picture helps to visualize the faked concurrency.

In addition to simulating that MessageProcessor is given a message, the test should check that all the right parameters are passed to SerialDriver and OSSemPend. We will also need tests for the timeout case.

Instead of hand crafting the test-double like the last two articles, we can generate the boring parts of the test-double with the Fake Function Framework (fff). To use it, we need an fff file. Here is SerialInterruptFake.fff:

#include "SerialInterrupt.h"
#include "fff2.h"
 
FAKE_VALUE_FUNCTION(int, SerialInterrupt_WaitingFor, 
      char *, int, OS_EVENT *);

An fff file is like a header file but without include guards. I use the file extension fff so I can tell an fff file from an h file. The macro expansion for FAKE_VALUE_FUNCTION declares all the boiler plate structs and functions needed to build a decent test-double. The parameters of FAKE_VALUE_FUNCTION exactly match the signature of the function you are faking. Here is the signature for SerialInterrupt_WaitingFor:

int SerialInterrupt_WaitingFor(char *, int, OS_EVENT *);

The fff file is included in test case files so the fake can be setup and checked. The fff file is also included in a .c or .cpp file to generate the implementation of the test-double. Here is SerialInterruptFake.c:

#include "SerialInterruptFake.fff"
#define GENERATE_FAKES
#include "SerialInterruptFake.fff"

Here is a snip of uCosII_TestDouble.fff for the uCos-II RTOS fake:

#include "fff2.h"
#include "ucos_ii.h"
 
//snip...
 
FAKE_VALUE_FUNCTION(OS_EVENT *, OSSemCreate, INT16U)
 
FAKE_VALUE_FUNCTION(OS_EVENT *, OSSemDel, OS_EVENT *,\
                    INT8U,\
                    INT8U *)
 
FAKE_VOID_FUNCTION(OSSemPend, OS_EVENT *,\
                    INT32U,\
                    INT8U *)
 
//snip...

By the way, if you need a fake for some 3rd party code (like an RTOS), you only need to define fakes for the functions that you use.

Let’s look at an all inclusive test, and then we’ll refactor it to make it read better.

static const char * fakeInput;
static char buffer[100];
void Successful_OSSemPend(OS_EVENT *event, INT32U timeout, INT8U *error)
{
    memcpy(buffer, fakeInput, strlen(fakeInput));
    *error = OS_ERR_NONE;
}
 
TEST(MessageProcessor, WaitForMessage_succeeds)
{
  OSSemPend_reset();
  SerialInterrupt_WaitingFor_reset();
  OSSemPend_fake.custom_fake = Successful_OSSemPend;
  fakeInput = "sched lightOn 5 Monday 20:00";
  CHECK(MessageProcessor_WaitForMessage(
          (char*)&buffer, sizeof(buffer)));
  LONGS_EQUAL(1, SerialInterrupt_WaitingFor_fake.call_count);
  POINTERS_EQUAL(&buffer, SerialInterrupt_WaitingFor_fake.arg0_val);
  LONGS_EQUAL(sizeof(buffer), 
          SerialInterrupt_WaitingFor_fake.arg1_val);
  LONGS_EQUAL(1, OSSemPend_fake.call_count);
  POINTERS_EQUAL(SerialInterrupt_WaitingFor_fake.arg2_val, 
          OSSemPend_fake.arg0_val);
  STRCMP_EQUAL(fakeInput, buffer);
}

Look that test over carefully. The functions OSSemPend_reset and SerialInterrupt_WaitingFor_reset as well as the data structures SerialInterrupt_WaitingFor_fake and OSSemPend_fake were generated by fff.

The good news is that the tedium of creating fakes goes ways, but it makes for tests that don’t tell their story so well. No worries, it can be refactored onto a nice clean test.

  TEST(MessageProcessor, WaitForMessage_succeeds)
  {
      fakeInput = "sched lightOn 5 Monday 20:00";
      result = MessageProcessor_WaitForMessage(buffer, length);
      CHECK_TRUE(result);
      CheckSerialInterruptCalledWith(buffer, length);
      CheckOSSPendCall();
      CheckBufferContains(fakeInput);
  }

With the test refactored, let’s look at the TEST_GROUP and other elements needed.

extern "C"
{
#include "MessageProcessor.h"
#include "SerialInterruptFake.fff"
#include "uCosII_TestDouble.fff"
}
 
#include "CppUTest/TestHarness.h"
 
static const char * fakeInput;
static char buf[100];
void Successful_OSSemPend(OS_EVENT *event, 
                          INT32U timeout, INT8U *error)
{
    memcpy(buf, fakeInput, strlen(fakeInput));
    *error = OS_ERR_NONE;
}
 
TEST_GROUP(MessageProcessor)
{
    int result;
    size_t length;
    char * buffer;
 
    void setup()
    {
        OSSemCreate_reset();
        OSSemDel_reset();
        OSSemPend_reset();
        OSSemPend_fake.custom_fake = Successful_OSSemPend;
        SerialInterrupt_WaitingFor_reset();
        length = sizeof(buffer);
        buffer = (char*)&buf;
        MessageProcessor_Create();
    }
 
    void teardown()
    {
       MessageProcessor_Destroy();
    }
 
    void CheckSerialInterruptCalledWith(char * buffer, int length)
    {
        LONGS_EQUAL(1, SerialInterrupt_WaitingFor_fake.call_count);
        POINTERS_EQUAL(buffer,
                SerialInterrupt_WaitingFor_fake.arg0_val);
        LONGS_EQUAL(length, 
                SerialInterrupt_WaitingFor_fake.arg1_val);
    }
 
    void CheckOSSPendCall()
    {
        LONGS_EQUAL(1, OSSemPend_fake.call_count);
        POINTERS_EQUAL(SerialInterrupt_WaitingFor_fake.arg2_val,
                OSSemPend_fake.arg0_val);
    }
 
    void CheckBufferContains(const char * expected)
    {
        STRCMP_EQUAL(expected, buffer);
    }
};

setup() is pretty straightforward, except maybe the installation of Successful_OSSemPend into the custom_fake member of OSSemPend_fake. When a test provides a custom_fake, the generated fake calls it after collecting the arguments.

Notice that there is no custom_fake for OSSemCreate, OSSemDel, and SerialInterrupt_WaitingFor. They do not need custom_fake implementations. OSSemPend needs the custom_fake to do the pseudo concurrent activities.

CheckSerialInterruptCalledWith makes sure that OSSemPend is only called once. It also checks each of the passed arguments that were captured in arg0_val and arg1_val.

The CheckOSSPendCall test helper is kind of interesting. To check its arguments we need access to a hidden part of the MessageProcessor. Not shown in this article is that MessageProcessor creates a Semaphore during its creation. The Semaphore‘s data structure is returned as an OS_EVENT pointer. Even though the pointer is hidden in MessageProcessor it is passed to both SerialDriver and OSSemPend. The test helper makes sure the passed pointers are equal. If we were really worried about it, the fake for OSSemCreate could be customized to pass a pointer that we could check.

This next test simulates a timeout condition from the SerialDriver. We need a different custom_fake to get the right error code to be returned by OSSemPend in error.

void TimeOut_OSSemPend(OS_EVENT *event, INT32U timeout, INT8U *error)
{
  *error = OS_ERR_TIMEOUT;
}
 
TEST(MessageProcessor, WaitForMessage_timeout)
{
  OSSemPend_fake.custom_fake = TimeOut_OSSemPend;
  strncpy(buffer, "some leftover garbage", length);
  result = MessageProcessor_WaitForMessage(buffer, length);
  CHECK_FALSE(result);
  CheckBufferContains("");
}

Had OSSemPend returned its result as a return value instead of through *error, we could have provided the return value through OSSemPend_fake‘s return_val or return_val_seq[] members. Then a custom_fake would not be needed.

I pruned the above test to only include the essential differences from the prior test. I made sure the buffer contains some garbage, that we get a FALSE result and an empty string. There is no need to re-check the parameters passed to MessageProcessor‘s collaborators.

That test drove me to add the error handling to MessageProcessor like this:

int MessageProcessor_WaitForMessage(char * buffer, size_t length)
{
    INT8U error = 0;
    SerialInterrupt_WaitingFor(buffer, length, int_sync);
    OSSemPend(int_sync, 1000, &error);
 
    if (error == OS_ERR_NONE)
        return TRUE;
 
    buffer[0] = 0;
    return FALSE;
}

I’m treating all OSSemPend errors the same, but if the clients of MessageProcessor_WaitForMessage needed to know about the specific error, you can see that we could cause other OSSemPend errors easily. I would resist letting the uCos-II error codes work their way higher in the application, but now I am off on a design tangent.

You can see an RTOS can be stubbed out with no problem with fff and test can be written for code that is concurrency aware. It took me about an hour the edit the Micrium uCos-II header file into submission. The I was off and writing tests for my concurrent code that interacts with the RTOS. I hope you saw the importance of test case refactoring. It will help with future maintenance and increase the documentation value of the tests.

If you want the code that goes with this example, register at my web site. Use course code BLOG-READER. Then look at Registered User Extras.

4 thoughts on “Unit testing RTOS dependent code – RTOS Test-Double – Part 3

  1. Pingback: Recent Readings Week 30 | Larry's Notebook

  2. Pingback: Designing and testing embedded systems | jonaslinde.se

  3. Pingback: Japanese article on C, TDD, and Fake Function Framework (fff) at Futurismo

Leave a Reply

Your email address will not be published. Required fields are marked *

Be gone spammers * Time limit is exhausted. Please reload the CAPTCHA.