Cleaning up/documenting C unit testing infrastructure

This commit is contained in:
Attila Body 2020-01-28 11:39:12 +01:00
parent 92c6ff2baa
commit 5e7279edc6
3 changed files with 285 additions and 27 deletions

View file

@ -39,21 +39,23 @@ static uint8_t expectedSuccess;
static uint32_t expectedCrc;
//////////////////////////////////////////////////////////////////////////////
DEFINE_MOCK_RET(uint32_t, __get_PRIMASK, mock) {
RETURN_MOCK_PREDEF(__get_PRIMASK, mock);
}
uint32_t effective_primask = 0;
DEFINE_MOCK_VAR(uint32_t, __set_PRIMASK, mock, lastprimask);
DEFINE_MOCK(__set_PRIMASK, mock, uint32_t primask) {
MOCK_STORE(__set_PRIMASK, mock, lastprimask, primask);
effective_primask = primask;
LEAVE_MOCK;
}
DEFINE_MOCK_VAR(crcslot_t *, __disable_irq, mock, compare);
DEFINE_MOCK_RET(uint32_t, __get_PRIMASK, mock) {
RETURN_MOCK(__get_PRIMASK, mock, effective_primask);
}
DEFINE_MOCK_VAR(crcslot_t *, __disable_irq, mock, firstslot_required);
DEFINE_MOCK(__disable_irq, mock) {
if(!MOCK_VAR(__disable_irq, mock, callcount)) {
EXPECT_EQ(crcStatus.firstSlot, MOCK_VAR(__disable_irq, mock, compare));
if(MOCK_CALLCOUNT(__disable_irq, mock) < 2) {
EXPECT_EQ(crcStatus.firstSlot, MOCK_VAR(__disable_irq, mock, firstslot_required));
}
effective_primask = 1;
LEAVE_MOCK;
}
@ -90,22 +92,19 @@ DEFINE_MOCK(LL_DMA_SetDataLength, mock, DMA_TypeDef *dma, uint32_t stream, uint3
LEAVE_MOCK;
}
DEFINE_MOCK(LL_DMA_EnableStream, mock, DMA_TypeDef *dma, uint32_t stream)
{
DEFINE_MOCK(LL_DMA_EnableStream, mock, DMA_TypeDef *dma, uint32_t stream) {
EXPECT_EQ(expectedDma, dma);
EXPECT_EQ(expectedStream, stream);
LEAVE_MOCK;
}
DEFINE_MOCK(LL_DMA_DisableStream, mock, DMA_TypeDef *dma, uint32_t stream)
{
DEFINE_MOCK(LL_DMA_DisableStream, mock, DMA_TypeDef *dma, uint32_t stream) {
EXPECT_EQ(expectedDma, dma);
EXPECT_EQ(expectedStream, stream);
LEAVE_MOCK;
}
DEFINE_MOCK(Crc_StartNextTask, mock, struct crcstatus_t *status)
{
DEFINE_MOCK(Crc_StartNextTask, mock, struct crcstatus_t *status) {
EXPECT_EQ(status, &crcStatus);
LEAVE_MOCK
}
@ -149,19 +148,20 @@ TEST(CrcScheduler, InitStatus)
TEST(CrcScheduler, AttachTask_single)
{
ACTIVATE_MOCK_RV(__get_PRIMASK, mock, 1);
ACTIVATE_MOCK(__set_PRIMASK, mock);
ACTIVATE_MOCK(__disable_irq, mock);
MOCK_STORE(__disable_irq, mock, compare, nullptr);
DMA1 = &dma1;
DMA2 = &dma2;
effective_primask = 0;
Crc_InitStatus(&crcStatus, &fakeCrc, DMA2, LL_DMA_STREAM_4);
ACTIVATE_MOCK_RV(__get_PRIMASK, mock, 0);
ACTIVATE_MOCK(__set_PRIMASK, mock);
ACTIVATE_MOCK(__disable_irq, mock);
MOCK_STORE(__disable_irq, mock, firstslot_required, nullptr);
Crc_AttachTasks(&crcStatus, &slot1, tasks1, 2);
EXPECT_EQ(MOCK_VAR(__get_PRIMASK, mock, callcount), 1);
EXPECT_EQ(MOCK_VAR(__set_PRIMASK, mock, callcount), 1);
EXPECT_EQ(MOCK_VAR(__disable_irq, mock, callcount), 1);
EXPECT_EQ(MOCK_CALLCOUNT(__get_PRIMASK, mock), 1);
EXPECT_EQ(MOCK_CALLCOUNT(__set_PRIMASK, mock), 1);
EXPECT_EQ(MOCK_CALLCOUNT(__disable_irq, mock), 1);
EXPECT_EQ(crcStatus.firstSlot, &slot1);
EXPECT_EQ(slot1.next, nullptr);
EXPECT_EQ(slot1.count, 2);
@ -174,18 +174,18 @@ TEST(CrcScheduler, AttachTask_multiple)
ACTIVATE_MOCK_RV(__get_PRIMASK, mock, 1);
ACTIVATE_MOCK(__set_PRIMASK, mock);
ACTIVATE_MOCK(__disable_irq, mock);
MOCK_STORE(__disable_irq, mock, compare, nullptr);
MOCK_STORE(__disable_irq, mock, firstslot_required, nullptr);
DMA1 = &dma1;
DMA2 = &dma2;
Crc_InitStatus(&crcStatus, NULL, DMA2, LL_DMA_STREAM_4);
Crc_AttachTasks(&crcStatus, &slot1, tasks1, 2);
MOCK_STORE(__disable_irq, mock, compare, &slot1);
MOCK_STORE(__disable_irq, mock, firstslot_required, &slot1);
Crc_AttachTasks(&crcStatus, &slot2, tasks2, 2);
EXPECT_EQ(__get_PRIMASK_mock_callcount, 2);
EXPECT_EQ(__set_PRIMASK_mock_callcount, 2);
EXPECT_EQ(__disable_irq_mock_callcount, 2);
EXPECT_EQ(MOCK_CALLCOUNT(__get_PRIMASK, mock), 2);
EXPECT_EQ(MOCK_CALLCOUNT(__set_PRIMASK, mock), 2);
EXPECT_EQ(MOCK_CALLCOUNT(__disable_irq, mock), 2);
EXPECT_EQ(crcStatus.firstSlot, &slot2);
EXPECT_EQ(slot2.next, &slot1);
EXPECT_EQ(slot1.next, nullptr);

259
platforms/test/Readme.md Normal file
View file

@ -0,0 +1,259 @@
<h1>Mocking (low level bare-metal embedded) C code</h1>
<h2>Preparations</h2>
As Google test framework expect your test code to be written in c++, declaring your C functions as extern "C" is mandatory. Use
#ifdef __cplusplus
extern "C" {
#endif
at the beginning of your headers and
#ifdef __cplusplus
}
#endif
at the end.
Keep in mind that STM32 is a 32 bit platform so unfortunately we cannot use amd64 for running our tests as pointer sizes would not match.
<h2>Creating stubs for external code</h2>
Unfortunately there is no way to make C functions dynamically mockable without source code modifications. We should use some macro magic to declare/define the functions need to be dynamically mockable.
<h3>Original code</h3>
foo.h:
...
uint8_t* Pu_GetTxBuffer(struct usartstatus_t *status);
...
foo.c:
...
uint8_t* Pu_GetTxBuffer(struct usartstatus_t *status)
{
return status->txBuffer.packet.payload;
}
...
<h3>Prepared for dynamic mocking</h3>
foo.h:
...
uint8_t* Pu_GetTxBuffer(struct usartstatus_t *status);
...
DECLARE_MOCK(Pu_GetTxBuffer);
...
foo.c:
...
uint8_t* MOCKABLE(Pu_GetTxBuffer)(struct usartstatus_t *status)
{
return status->txBuffer.packet.payload;
}
...
During normal build `DECLARE_MOCK(Pu_GetTxBuffer)` expands to nothing and `MOCKABLE(Pu_GetTxBuffer)` expands to `Pu_GetTxBuffer`, so using them have no effect on the compiled binary.
When compiling the code for unit tests `DECLARE_MOCK(Pu_GetTxBuffer)` will do two things:
* Declares a function with the same signature as `Pu_GetTxBuffer` named `Pu_GetTxBuffer__`
* Declares a function pointer named `test_GetTxBuffer`
`MOCKABLE` macro is the tricikiest part of the whole framework. It injects x86 assembly code into the intermediate assembly source generated from the C source to achieve the following:
Hijacks `Pu_GetTxBuffer` and injects a code which check s the value of `test_GetTextBuffer` pointer and calls the function it points to if the pointer is not NULL. Defines `Pu_GetTxBuffer__` (using the original implementation) which is called in case `test_GetTextBuffer` contains NULL.
As result we have an extra pointer for each prepared function to divert the execution when we need it and leave the original implementation in place when not.
<h2>Mocking/stubing platform code</h2>
Platform (like CMSIS, STM32 HAL or LL) has plenty of declarations and definitions necessary your code might be using. Unfortunately unit tests will need them too to compile. In most of the cases they even should be mockable. As the platform code written for ARM (sometimes even containing ARM assembly inserts) it would be extremely hard to make it compile on x86. there is no other way to make your own code compilable but copying necessary declarations and provide trivial (stub) definition in your own "fake" platform code.
It is also a good idea to make (at least parts of) your stub implementation mockable using the macros described above (see headers and sources in platforms/test/platform for example)
<h2>Writing your unit tests</h2>
There are several macros provided to make writing unit test as convinient as possible.
<h3>Defining mocks</h3>
Defining mock functions with no (void) return value:
DEFINE_MOCK(<function_name>, <decoration>, [<parameter list>]) {
<mock_function_implementation>
LEAVE_MOCK;
}
Example:
DEFINE_MOCK(LL_DMA_SetM2MDstAddress, mock, DMA_TypeDef *dma, uint32_t stream, uint32_t address) {
<implementation>
LEAVE_MOCK;
}
The above will generate the following C code:
static int LL_DMA_SetM2MDstAddress_mock_callcount;
static void LL_DMA_SetM2MDstAddress_mock(DMA_TypeDef *dma, uint32_t stream, uint32_t address) {
++LL_DMA_SetM2MDstAddress_mock_callcount;
{
<implementation>
}
}
Defining mock functions with return (non-void) value:
DEFINE_MOCK_RET(<return_type>, <function_name>, <decoration>, [<parameter list>]) {
<mock_function_implementation>
RETURN_MOCK_PREDEF(<function_name>, <decoration> | RETURN_MOCK(<function_name>, <decoration>, <return value>);
}
Example:
DEFINE_MOCK_RET(uint32_t, LL_USART_IsActiveFlag_IDLE, mock, USART_TypeDef *usart) {
<implementation>
RETURN_MOCK_PREDEF(LL_USART_IsActiveFlag_IDLE, mock)
}
The above will generate the following C code:
static int LL_USART_IsActiveFlag_IDLE_mock_callcount;
static uint32_t LL_USART_IsActiveFlag_IDLE_mock_retval;
static uint32_t LL_USART_IsActiveFlag_IDLE_mock(USART_TypeDef *usart) {
++LL_USART_IsActiveFlag_IDLE_mock_callcount;
{
<implementation>
}
return LL_USART_IsActiveFlag_IDLE_mock_retval;
}
Return value of the function above can be set by
MOCK_STORE(LL_USART_IsActiveFlag_IDLE, mock, retval, <value>);
or
MOCK_VAR(LL_USART_IsActiveFlag_IDLE, mock, retval) = <value>;
or
LL_USART_IsActiveFlag_IDLE_mock_retval = <value>;
during the test setup.
<h3>Helper variables for mocking</h3>
It is quite common that you need to store some data in global variables (can be checked later in from the test code or can be used by other mocks). There are few helper macros to make this easier. You can define a mock helper variable using
DEFINE_MOCK_VAR(<type>, <function_name>, <decoration>, <variable_name>);
Example:
DEFINE_MOCK_VAR(uint32_t, __set_PRIMASK, mock, lastprimask);
Which will expand to:
uint32_t __set_PRIMASK_mock_lastprimask;
You can access these variables using `MOCK_VAR(<function_name>, <decoration>, <name>)` (e.g. `if(MOCK_VAR(__set_PRIMASK, mock, lastprimask) != 0)` or `MOCK_VAR(__set_PRIMASK, mock, lastprimask) = 1;` ) but for setting the value of a mock helper variable you can also use `MOCK_STORE(<function_name>, <decoration>, <varable_name>, <value>);` (e.g `MOCK_STORE(__set_PRIMASK, mock, lastprimask, 1)` which is equivalent to setting the variable using `MOCK_VAR`.
As it was descibed above, defining a mock function also defines (and administers) a call count variable for that function. For easier access of those variables we have `MOCK_CALLCOUNT(<function_name>, <decoration>)` (e.g. `if(MOCK_CALLCOUNT(__set_PRIMASK, mock) != 5) ...`)
<h2>Real-life example</h2>
Test writing using the infrastructure described above is quite straight-forward and easy. Let's assume we would like to write a unit test for the following function:
void MOCKABLE(Crc_AttachTasks)(struct crcstatus_t *status, struct crcslot_t *slot,
struct crctask_t *tasks, uint8_t taskCount)
{
slot->count = taskCount;
slot->tasks = tasks;
memset(tasks, 0, sizeof(*tasks)*taskCount);
uint32_t prim = __get_PRIMASK();
__disable_irq();
slot->next = status->firstSlot;
status->firstSlot = slot;
__set_PRIMASK(prim);
}
This function attaches a new tasks to one of the slots of CRC scheduler. As this ;lis is also processed from interrupt context it needs to disable interrupts for the period of the modification and restore the original interrupt enablement status on the end.
We can identify three platform specific function calls: `__get_PRIMASK(), __disable_irq()` and `__set_PRIMASK()` so we need stubs for them somewhere in the platform stub code:
Platform stub header:
void __disable_irq();
uint32_t __get_PRIMASK();
void __set_PRIMASK(uint32_t priMask);
Platform stub source:
void MOCKABLE(__disable_irq)() {}
uint32_t MOCKABLE(__get_PRIMASK)() { return 0; }
void MOCKABLE(__set_PRIMASK)(uint32_t primask) {}
In our test code we need to mock these function (making possible to verify that they're called as modification of the linked list of slots need to be guarded against interrupts)
uint32_t effective_primask = 0;
DEFINE_MOCK(__set_PRIMASK, mock, uint32_t primask) {
effective_primask = primask;
LEAVE_MOCK;
}
DEFINE_MOCK_RET(uint32_t, __get_PRIMASK, mock) {
RETURN_MOCK(__get_PRIMASK, mock, effective_primask);
}
DEFINE_MOCK_VAR(crcslot_t *, __disable_irq, mock, firstslot_required);
DEFINE_MOCK(__disable_irq, mock) {
if(MOCK_CALLCOUNT(__disable_irq, mock) < 2) {
EXPECT_EQ(crcStatus.firstSlot, MOCK_VAR(__disable_irq, mock, firstslot_required));
}
effective_primask = 1;
LEAVE_MOCK;
}
With these mock functions we actually mock the behaviour of he ARM Cortex PRIMASK register API. We also add some check to `_disable_irq_mock()` that verifies that the firstSlot member of the crcStatus has not changed before disabling interrupts.
Now we prepared everything for writing our first unit test for Crc_AttachTasks function:
TEST(CrcScheduler, AttachTask_single) {
DMA1 = &dma1;
DMA2 = &dma2;
effective_primask = 0;
Crc_InitStatus(&crcStatus, &fakeCrc, DMA2, LL_DMA_STREAM_4);
ACTIVATE_MOCK_RV(__get_PRIMASK, mock, 0);
ACTIVATE_MOCK(__set_PRIMASK, mock);
ACTIVATE_MOCK(__disable_irq, mock);
MOCK_STORE(__disable_irq, mock, firstslot_required, nullptr);
Crc_AttachTasks(&crcStatus, &slot1, tasks1, 2);
EXPECT_EQ(MOCK_CALLCOUNT(__get_PRIMASK, mock), 1);
EXPECT_EQ(MOCK_CALLCOUNT(__set_PRIMASK, mock), 1);
EXPECT_EQ(MOCK_CALLCOUNT(__disable_irq, mock), 1);
EXPECT_EQ(crcStatus.firstSlot, &slot1);
EXPECT_EQ(slot1.next, nullptr);
EXPECT_EQ(slot1.count, 2);
EXPECT_EQ(crcStatus.activeSlot, nullptr);
}
There are two ways to activate a mock:
ACTIVATE_MOCK(<function>, <decoration>);
and
ACTIVATE_MOCK_RV(<function>, <decoration>, <return_value>)
Both macros reset the corresponding `callcount` variable of the mock function to zero and divert the mocked function to the mock. In addition to this `ACTIVATE_MOCK_RV` also sets the return value variable of the mock function (created by `DEFINE_MOCK_RET`) to the supplied value. This can be used to define the return valuse of the mock (if the test writer decides to write the mock function this way).
After preparing everything for the test wi actually call `Crc_AttachTasks` with the appropriate parameters then verifying the results.

View file

@ -8,7 +8,6 @@
#ifndef PLATFORM_MOCKME_H_
#define PLATFORM_MOCKME_H_
//#define TOSTR(x) #x
#ifdef __cplusplus
#define DECLARE_MOCK(F) \
extern decltype(F) F ## __, *test_ ## F