Quantcast
Channel: James Grenning's Blog » Unit Testing
Viewing all articles
Browse latest Browse all 13

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

$
0
0

In the last article, the OSSemPend() test-double was coded to handle a specific OSSemPend() application and test need. The semaphore was being used to signal when there is a message to process. It was the first need for a OSSemPend() test double and was quickly developed. As more RTOS dependent code is brought under test, a more general solution will be needed.

In this article, we’ll look at a test double that can be customized for each application.

It’s hard to anticipate what every application that blocks on a semaphore is waiting for. I think it is safe to say that it is impossible. So we need each test case that uses the test-double to be able to customize it.

A few generic capabilities that users of the test-double may want are: to look at parameters passed, to simulate output results, and to count the number of calls. That’s all easy. Let’s look at this easy stuff first, then we’ll look at how to customize the test-double to establish application specific conditions that would normally be done asynchronously in a separate thread or ISR that posts to the semaphore.

Here are the tests that document how the generic behavior of the test-double works:

#include "CppUTest/TestHarness.h"
 
extern "C"
{
#include "ucos_ii.h"
#include "uCosIITestDouble.h"
}
 
TEST_GROUP(uCosIITestDouble)
{
    OS_EVENT event;
    INT8U error;
 
    void setup()
    {
        error = -1;
        OSSemPend_Fake_Reset();
    }
    void teardown() { }
};
 
TEST(uCosIITestDouble, OSSemPend_sets_error_to_zero_by_default)
{
    OSSemPend(&event, 0, &error);
}
 
TEST(uCosIITestDouble, OSSemPend_counts_calls)
{
    OSSemPend(&event, 0, &error);
    LONGS_EQUAL(1, OSSemPend_fake.call_count);
}
 
TEST(uCosIITestDouble, OSSemPend_remembers_parameters)
{
    OSSemPend(&event, 1000, &error);
    POINTERS_EQUAL(&event, OSSemPend_fake.event);
    LONGS_EQUAL(1000, OSSemPend_fake.timeout);
    POINTERS_EQUAL(&error, OSSemPend_fake.error);
}
 
TEST(uCosIITestDouble, OSSemPend_returns_what_i_tell_it_to)
{
    OSSemPend_fake.return_this = 4;
    OSSemPend(&event, 0, &error);
    LONGS_EQUAL(4, error);
}

Here is the implementation for the OSSemPend() test-double that remembers parameters, counts calls and lets the caller control the the return result.

#include "uCosIITestDouble.h"
#include <string.h>
 
OSSemPend_Fake OSSemPend_fake;
 
void  OSSemPend  (OS_EVENT *event, INT32U timeout, INT8U *error)
{
    OSSemPend_fake.event = event;
    OSSemPend_fake.timeout = timeout;
    OSSemPend_fake.error = error;
    *error = OSSemPend_fake.return_this;
    OSSemPend_fake.call_count++;
}
 
 
void OSSemPend_Fake_Reset(void)
{
    memset(&OSSemPend_fake, 0, sizeof(OSSemPend_fake));
}

The header file looks like this:

#ifndef uCosIITestDouble_H_
#define uCosIITestDouble_H_
 
#include "ucos_ii.h"
 
typedef struct OSSemPend_Fake
{
    OS_EVENT * event;
    INT32U timeout;
    INT8U * error;
    int return_this;
    int call_count;
} OSSemPend_Fake;
 
extern OSSemPend_Fake OSSemPend_fake;
 
void OSSemPend_Fake_Reset(void);
 
#endif /* uCosIITestDouble_H_ */

There is nothing too surprising about this test-double implementation above. It is simple and can be written in minutes. Notice that the test-double includes the production code header file ucos_ii.h. This provides all the production code defines and function declarations.

If OSSemPend() is called more than once in a test case, we’ll need to capture a series of parameters and be able to provide a series of return results too. This may be a bit tedious to setup, but its just arrays and indexes. Let’s agree we may need those things, but not now.

Let’s add the hooks to the test-double to allow it to do anything while it is pretending to be waiting for some concurrent activity to OSSemPost() it’s OS_EVENT. Let’s see the test:

extern "C"
{
#include "ucos_ii.h"
#include "uCosIITestDouble.h"
static int my_custom_OSSemPend_called;
void my_custom_OSSemPend(OS_EVENT *pevent, INT32U timeout, INT8U *perr)
{
    my_custom_OSSemPend_called++;
}
 
}
 
TEST_GROUP(uCosIITestDouble)
{
    OS_EVENT event;
    INT8U error;
 
    void setup()
    {
        error = -1;
        OSSemPend_Fake_Reset();
        my_custom_OSSemPend_called = 0;
    }
    void teardown() { }
};
 
TEST(uCosIITestDouble, OSSemPend_custom_ation)
{
    OSSemPend_fake.custom_action = my_custom_OSSemPend;
    OSSemPend(&event, 0, &error);
    LONGS_EQUAL(1, my_custom_OSSemPend_called);
}

The custom_action is a function pointer with the same signature as OSSemPend(). When provided, custom_action is called by the test-double implementation of OSSemPend(). This means we can have the test-double delegate to a function to do anything, allowing it to simulate things happening concurrently.

The header now looks like this:

#ifndef uCosIITestDouble_H_
#define uCosIITestDouble_H_
 
#include "ucos_ii.h"
 
typedef void (*OSSemPend_FPointer)(OS_EVENT *pevent, 
                                  INT32U timeout, 
                                  INT8U *perr);
 
typedef struct OSSemPend_Fake
{
    OS_EVENT * event;
    INT32U timeout;
    INT8U * error;
    int return_this;
    int call_count;
    OSSemPend_FPointer custom_action;
} OSSemPend_Fake;
 
extern OSSemPend_Fake OSSemPend_fake;
void OSSemPend_Fake_Reset(void);
 
#endif /* uCosIITestDouble_H_ */

The test-double implementation looks like this:

#include "uCosIITestDouble.h"
#include <string.h>
 
OSSemPend_Fake OSSemPend_fake;
 
void  OSSemPend(OS_EVENT *event, INT32U timeout, INT8U *error)
{
    OSSemPend_fake.event = event;
    OSSemPend_fake.timeout = timeout;
    OSSemPend_fake.error = error;
    *error = OSSemPend_fake.return_this;
    OSSemPend_fake.call_count++;
    if (OSSemPend_fake.custom_action)
        OSSemPend_fake.custom_action(event, timeout, error);
}
 
void OSSemPend_Fake_Reset(void)
{
    memset(&OSSemPend_fake, 0, sizeof(OSSemPend_fake));
}

When the test case does not need the custom behavior, the test leaves custom_action set to zero, and is not called.

Let’s see how this would be used to test the MessageProcessor in a home automation system that turns on and off lights at scheduled times.

TEST(MessageProcessor, schedule_a_light)
{
    input = "sched light 5 turnon Monday 20:00";
    MessageProcessor_ProcessNextMessage();
    CHECK_TRUE(LightScheduler_Cancel(5, MONDAY, 1200));
}

The input message is asking to schedule light number 5. The MessageProcessor_ProcessNextMessage() waits at the semaphore for a message then interprets it and causes the light to be scheduled. The last line of the test tries to cancel the schedule. If light 5 is scheduled for the 1200th minute of Monday, LightScheduler_Cancel returns TRUE. if its not in the schedule, it returns FALSE.

We’re testing RTOS dependent code off the target in a repeatable and productive manner!

Here is the test fixture:

extern "C"
{
#include "ucos_ii.h"
#include "uCosIITestDouble.h"
#include "MessageProcessor.h"
#include "LightScheduler.h"
void InputQueue_Put(char);
int MessageProcessor_ProcessNextMessage(void);
const char * input;
void fake_populates_InputQueue(OS_EVENT *pevent, INT32U timeout, INT8U *perr)
{
    {
        while (*input)
        {
          InputQueue_Put(*input);
          input++;
        }
    }
}
 
}
 
TEST_GROUP(MessageProcessor)
{
    OS_EVENT event;
    INT8U error;
 
    void setup()
    {
        error = -1;
        OSSemPend_Fake_Reset();
        input = "";
        OSSemPend_fake.custom_action = fake_populates_InputQueue;
        LightScheduler_Create();
        MessageProcessor_Create();
    }
    void teardown() { }
};

I’ve left out some of the application details, but you should have a good view of this really useful RTOS test-double function.

It is rather tedious to build. I need to do this for each RTOS function. Should I show you an easier way?

©2013 James Grenning's Blog. All Rights Reserved.

.

Viewing all articles
Browse latest Browse all 13

Trending Articles