Kinda works

This commit is contained in:
Attila Body 2021-10-31 00:33:00 +02:00
parent f3d345e2e3
commit 662a7a9b12
40 changed files with 2851 additions and 26 deletions

View file

@ -0,0 +1,12 @@
; DO NOT EDIT (unless you know what you are doing)
;
; This subdirectory is a git "subrepo", and this file is maintained by the
; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme
;
[subrepo]
remote = git@10.183.3.92:mississippi/mississippi-mcu-platform-f40x_mx.git
branch = master
commit = 8bb5394e3d7194db11059efde6e41c6456ae5e6c
method = merge
cmdver = 0.4.0
parent = 0a503c37d418e11e363d1aa66977b91757f7d31c

View file

@ -0,0 +1,10 @@
SELF_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
REL_DIR := $(patsubst %/,%,$(dir $(lastword $(MAKEFILE_LIST))))
ifeq ($(MKDBG), 1)
$(info >>> $(REL_DIR)/component.mk)
endif
#$(eval C_SOURCES += $(wildcard $(REL_DIR)/*.c))
$(eval COMMON_INCLUDES += -I$(REL_DIR))
ifeq ($(MKDBG), 1)
$(info <<<)
endif

View file

@ -0,0 +1,14 @@
/*
* core_ll.h
*
* Created on: Feb 12, 2020
* Author: abody
*/
#ifndef FIRMWARE_PLATFORM_CORE_LL_H_
#define FIRMWARE_PLATFORM_CORE_LL_H_
#include <stm32f4xx_ll_cortex.h>
#include <stm32f4xx_ll_utils.h>
#endif /* FIRMWARE_PLATFORM_CORE_LL_H_ */

View file

@ -0,0 +1,6 @@
#ifndef __PLATFORM_CRC_LL_H_INCLUDED
#define __PLATFORM_CRC_LL_H_INCLUDED
#include "crc.h"
#endif // __PLATFORM_CRC_LL_H_INCLUDED

View file

@ -0,0 +1,6 @@
#ifndef __PLATFORM_DMA_LL_H_INCLUDED
#define __PLATFORM_DMA_LL_H_INCLUDED
#include "stm32f4xx_ll_dma.h"
#endif // __PLATFORM_DMA_LL_H_INCLUDED

View file

@ -0,0 +1,97 @@
/*
* mockme.h
*
* Created on: Nov 25, 2019
* Author: abody
*/
#ifndef PLATFORM_MOCKME_H_
#define PLATFORM_MOCKME_H_
//#define TOSTR(x) #x
#ifdef __cplusplus
#define DECLARE_MOCK(F) \
extern decltype(F) F ## __, *test_ ## F
#else
#define DECLARE_MOCK(F) \
extern typeof(F) F ## __, *test_ ## F
#endif
#define MOCKABLE(F) __attribute__((section(\
".bss\n\t"\
".globl test_" #F "\n\t"\
".align 4\n\t"\
".type test_" #F ", @object\n\t"\
".size test_" #F ", 4\n"\
"test_" #F ":\n\t"\
".zero 4\n\t"\
".text\n\t"\
".p2align 4,,15\n\t"\
".globl " #F "\n\t"\
".type " #F ", @function\n"\
#F ":\n\t"\
".cfi_startproc\n\t"\
"push %edx\n\t"\
"push %edx\n\t"\
"push %eax\n\t"\
"movl test_" #F ", %eax\n\t"\
"leal " #F "__, %edx\n\t"\
"test %eax, %eax\n\t"\
"cmove %edx, %eax\n\t"\
"mov %eax, 8(%esp)\n\t"\
"pop %eax\n\t"\
"pop %edx\n\t"\
"ret\n\t"\
".cfi_endproc\n\t"\
".size " #F ", .-" #F "\n\t"\
".section .text"))) F ## __
#define DEFINE_MOCK_RET(rettype, fn, decor, ...) \
static int fn ## _ ## decor ## _callcount; \
static rettype fn ## _ ## decor ## _retval; \
static rettype fn ## _ ## decor(__VA_ARGS__) { \
++fn ## _ ## decor ## _callcount;
#define DEFINE_MOCK(fn, decor, ...) \
static int fn ## _ ## decor ## _callcount; \
static void fn ## _ ## decor(__VA_ARGS__) { \
++fn ## _ ## decor ## _callcount;
#define RETURN_MOCK(fn, decor, ret) \
fn##_##decor##_retval = ret; \
return ret; }
#define RETURN_MOCK_PREDEF(fn, decor) \
return fn##_##decor##_retval; }
#define LEAVE_MOCK }
#define DEFINE_MOCK_VAR(type, fn, decor, varname) type fn##_##decor##_##varname
#define MOCK_STORE(fn, decor, varname, value) fn##_##decor##_##varname = value
#define MOCK_VAR(fn, decor, varname) fn##_##decor##_##varname
#define MOCK_CALLCOUNT(fn, decor) fn ## _ ## decor ## _callcount
#ifdef __cplusplus
namespace mockme {
template <typename T> class mockguard {
T* m_guarded;
public:
mockguard(T* guarded, T testFunc) : m_guarded(guarded) { *m_guarded = testFunc; }
~mockguard() { *m_guarded = nullptr; }
};
} // namespace mockme
#define ACTIVATE_MOCK(fn, decor) \
fn ## _ ## decor ## _callcount = 0; \
mockme::mockguard<decltype(fn)*> fn ## _ ## decor ## _mockguard(&test_ ## fn, fn ## _ ## decor)
#define ACTIVATE_MOCK_RV(fn, decor, ret) \
fn##_##decor##_callcount = 0; \
fn##_##decor##_retval = ret; \
mockme::mockguard<decltype(fn)*> fn##_##decor##_mockguard(&test_ ## fn, fn##_##decor)
#endif // __cplusplus
#endif /* PLATFORM_MOCKME_H_ */

View file

@ -0,0 +1,6 @@
#ifndef __PLATFORM_USART_LL_H_INCLUDED
#define __PLATFORM_USART_LL_H_INCLUDED
#include "usart.h"
#endif // __PLATFORM_USART_LL_H_INCLUDED

3
platforms/test/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
gtest_i386/
googletest/**/generated/

12
platforms/test/.gitrepo Normal file
View file

@ -0,0 +1,12 @@
; DO NOT EDIT (unless you know what you are doing)
;
; This subdirectory is a git "subrepo", and this file is maintained by the
; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme
;
[subrepo]
remote = git@10.183.3.92:mississippi/mississippi-mcu-platform-x86_gtest.git
branch = master
commit = 71472440d8296fe81d0891a972d6677288c64590
parent = 24552e91a4f68cd15d944c09c2a7a8d7d7e2183c
method = merge
cmdver = 0.4.0

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

@ -0,0 +1,9 @@
#!/bin/sh
set -x
SCRIPTDIR=$(dirname "$0")
OUTDIR="$1"
cd "$SCRIPTDIR"/googletest
cmake -DCMAKE_CXX_FLAGS=-m32 -DCMAKE_INSTALL_PREFIX:PATH="../$OUTDIR" .
make -j8 && make install
rm install_manifest.txt

View file

@ -0,0 +1,17 @@
SELF_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
REL_DIR := $(patsubst %/,%,$(dir $(lastword $(MAKEFILE_LIST))))
ifeq ($(MKDBG), 1)
$(info >>> $(REL_DIR)/component.mk)
endif
$(eval C_SOURCES += $(wildcard $(REL_DIR)/platform/*.c))
$(eval COMMON_INCLUDES += -I$(REL_DIR) -I$(REL_DIR)/gtest_i386/include)
$(eval LIBDIR += -L$(REL_DIR)/gtest_i386/lib)
$(eval LIBS += -lgtest -lgtest_main -lpthread)
$(eval COMPONENT_DEPS += $(REL_DIR)/gtest_i386)
$(REL_DIR)/gtest_i386:
$(REL_DIR)/build-googletest.sh gtest_i386
ifeq ($(MKDBG), 1)
$(info <<<)
endif