ThrowTheSwitch / Ceedling

Ruby-based unit testing and build system for C projects
http://throwtheswitch.org
Other
593 stars 247 forks source link

Mocking nested headers (or something like it) #171

Open krish2487 opened 7 years ago

krish2487 commented 7 years ago

Hello, I apologize if someone has already asked this question before. I looked around and could not find anything that addressed my query.

I am familiar with the tool suite having used it on several occassions. I have recently shifted to using the STM32 family with libopenCM3 libraries. It is here that I am having some issues.

I am using the discovery STM32F030R8 board for example. The libopenCM3 directory structure is such that the there are a bunch of common headers in the root of the headers directory which in turn include headers for the specific processor family under the same name. The processor family specific header then again include a bunch of cross family headers under a "common" headers folder under the root.

The directory structure looks so. stm32/ ├── common ├── f0 ├── f1 ├── f2 ├── f3 ├── f4 ├── f7 ├── l0 ├── l1 └── l4

The stm32 folder contains a header which we have to include in our application code which then calls headers under the stm32/f0 folder, which in turn call common header files under stm32/common folder.

Now this is my issue. I need to mock the headers here to enable me to test my next level code which makes calls to functions as defined by the headers here.

However I am unsure of how to proceed, since there is no single header which i can "mock_" and simulate my calls. Each header in turn includes another header. How do I mock these too??

I tried adding the paths in the yaml file and adding the "mock_" prefix to the headers manually, but rake just threw an error stating that the specified header was not found and did not even run any test task..

I ll be grateful for any help or suggestions. Thank you and have a nice day!

kashperanto commented 7 years ago

I know this is probably too late to be helpful to you specifically, but what you describe is the use case of the :paths: configuration option. The support folder (defaults to test/support) is searched before the source and include folders, so you can still have the regular header in your path (like if you're building the binary with ceedling). I have a project for a tiny 8-bit PIC, and Microchip has a similar method where you include the xc.h header in your code, and that header includes a few other headers to get to the true header for your specific device.

I just took the actual header for my specific device (a PIC16F series) and made my own xc.h file that I put into the search path (I put it in the include folder since I do not use ceedling to build my binary, but I think it still makes more sense in support now that I'm more familiar with the tool). I also had to remove any compiler-specific stuff that GCC doesn't understand, and I had to remove the direct mapping of registers to memory and leave only the prototypes of the structures used to manipulate registers. It takes a bit of work, but all of your hardware-accessing code will share this file, so it makes sense to put in a bit of effort.

herrmanthegerman commented 5 years ago

I have the same question.

I'd like to unit test my code that I'm writing for an NXP LPC microcontroller. NXP provides a driver framework called LPCOpen to speed up sw development. The framework is provided as a bunch of .c and .h files (see folder lpc_chip_175x_6x in LPCopen. Its structure is simple:

lpc_chip_175x_6x/
├── inc
│   ├── chip.h
│   ├── chip_lpc175x_6x.h
│   ├── cmsis_175x_6x.h
│   ├── cmsis.h
│   ├── sys_config.h
│   ...
│   └── i2c_17xx_40xx.h
└── src
    ├── adc_17xx_40xx.c
    ├── chip_17xx_40xx.c
    ├── clock_17xx_40xx.c
    ├── i2c_17xx_40xx.c
     ...

To use the framework, one usually just includes chip.h. This will pull in sys_config.h which defines the processor variant to use. Based on the define, chip.h will include chip_lpc175x_6x.h and that will include cmsis.h (for ARM Cortex support) and all the other header files relevant for the microcontroller (e.g., i2c_17xx_40xx.h). For example for using I2C communication, one just writes (simplified):

/* \file example.c */
#include "example.h"
#include "chip.h"

int send() {
  uint8_t byte = 80;
  int ret = Chip_I2C_MasterSend(I2C0, 0x40 , &byte, 1);
  return ret;
}

To unit test above file example.c, I tried this test_example.c:

/* \file test/test_example.c */
#include "unity.h"
#include "example.h"
#include "mock_chip.h"

void setUp(void)
{
}

void tearDown(void)
{
}

void test_example_NeedToImplement(void)
{
    Chip_I2C_MasterSend_ExpectAndReturn(2);
    int ret = send();

    TEST_ASSERT_EQUAL(2, ret);
}

(NB: I added the "expect_any_args" plugin to the cmock configuration in project.yml.) Running ceedling gives me:

Test 'test_example.c'
---------------------
Creating mock for chip...
Generating runner for test_example.c...
Compiling test_example_runner.c...
Compiling test_example.c...
test/test_example.c: In function ‘test_example_NeedToImplement’:
test/test_example.c:18:5: warning: implicit declaration of function ‘Chip_I2C_MasterSend_ExpectAnyArgsAndReturn’ [-Wimplicit-function-declaration]
     Chip_I2C_MasterSend_ExpectAnyArgsAndReturn(2);
     ^

Obviously, just triggering cmock by including mock_chip.h did not suffice.

To make this test work, I added my own version of chip.c to the test folder (which overrides the original chip.c). It includes copies of only the required prototypes and defines.

/* \file test/chip.c */
#ifndef CHIP_H_
#define CHIP_H_

#include <stdint.h>

#define I2C0 0
int Chip_I2C_MasterSend(int id, char slaveAddr, const uint8_t *buffer, int len);

#endif

Now the test succeeds:

Test 'test_example.c'
---------------------
Creating mock for chip...
Generating runner for test_example.c...
Compiling test_example_runner.c...
Compiling test_example.c...
Compiling mock_chip.c...
Compiling unity.c...
Compiling example.c...
Compiling cmock.c...
Linking test_example.out...
Running test_example.out...

--------------------
OVERALL TEST SUMMARY
--------------------
TESTED:  1
PASSED:  1
FAILED:  0
IGNORED: 0

Is this the way ceedling and cmock is supposed to be used? I don't think so. I expected cmock or ceedling to automatically identify and extract the prototypes required to make the unit test compile. What am I missing? Thanks.

laurensmiers commented 5 years ago

@herrmanthegerman It looks like ceedling/cmock isn't producing the mocked function. What does the mock look like that cmock produces with the original chip.h header file? It's located in /test/mocks . I'm not sure how/if cmock can handle nested mocking. I think a possible workaround for you is to mock the specific header containing the I2C stuff (lpc_chip_175x_6x/inc/i2c_17xx_40xx.h), I think you don't have to make your own header in this case.

But I think @kashperanto solution with the ceedling support folder is IMO the best solution in the long run. I did something similar for an NXP chip. That particular SDK was full with inline functions you can't mock so there is some tweaking of the headers required. It takes some work to 'convert' their header to what you want but it's worth the hassle. And you can do it header by header as you go along (don't have to do it all in one go ofcourse). And normally, SDKs try not to change APIs that often, so it's mostly a one-time thing you have to do and some maintenance after with SDK updates. However, I don't know your SDK, maybe you can keep it simple and just mock the specific headers as I mentioned in the beginning.

kashperanto commented 5 years ago

@herrmanthegerman The short answer is both yes and no.

You have encountered the unfortunate truth that embedded software is not written with testability in mind - even if it's from a vendor. Freescale's Processor Expert is the worst offender that I have personally had to deal with, but it is also very convenient if you're into debug later development. They do a lot of weird things to make their platform as easy as possible to use. In more cases than not the best option I've found is to create a simplified dummy header with only the functionality you need, and let CMock mock that.

Do note that this need only applies to code which uses these low-level (or mid-level) drivers. Once you get to your application code you really shouldn't be using direct calls to drivers if you can help it (they make the code more difficult to test and tie your application to that vendor unnecessarily). With the power of modern micros you will have a hard time selling me on the idea that you can't afford the extra layer of abstraction.

If you are working on a new project this limitation will tend to drive you to create very thin wrappers around the driver functions to improve testability. As with most things there will be a trade-off between a simpler design and a more robust/modular design. Once you're in the mindset you'll see that a lot of "embedded" code can be written without relying on direct linkage to hardware at all (using function pointers for "inversion of control"/"dependency injection").

Even if you need to use direct calls in your code it is much easier to simply add a few function prototypes into a testing header file than it is to write a full mock on your own. I even use this technique when testing files that use callbacks: just throw a bunch of prototypes in a bogus header and cmock will implement them for you (with all the call and argument tracking).

However, I do understand the frustration when you've got a legacy project with huge files full of calls to a dozen or more headers (with non-standard int and bool types because 20-year-old standards are too new). Just remind yourself that you only need to do this once per file, and that you only need to mock what is used.

Also, don't write tests that don't give you value. With a lot of these driver-level functions you tend to have a lot of setup calls (gpio pin directions, prescaler values, etc.). Unit tests that check these are of questionable value in my experience.

herrmanthegerman commented 5 years ago

@kashperanto Thanks for your elaborate comment. I'm sure your advice will be helpful for others, too.

@laurensmiers You assumption is correct. The original chip.h file contains five function prototypes (for system initialization). The mocked header appears at build/test/mocks/mock_chip.h and contains the additional prototypes for CMock (with the _Expect(), _Ignore() etc. suffixes) and the mock_chip.c files contains the mocked implementation for the five functions only. I also tried your suggestion to mock i2c_17xx_40xx.h, so I changed the includes in 'test_example.c' to

#include "unity.h"
#include "chip.h"
#include "mock_i2c_17xx_40xx.h"

Still, this was not sufficient to make the test run as the test_example_runner would not compile. I also had to add chip.h as a default include to the :cmock: configuration in project.yml. Then the test succeeded.

haemishkyd commented 4 years ago

Thanks for all of this info but I have a problem where I cannot mock the nested headers themselves (they have guards to stop being included). So my alternative is to create my own header file with the relevant prototypes? Is this really the best option available given that the problem here is simply that we don't have a way to traverse an include file tree? Is there a reason this has never been implemented. Before I go through all the pain I wanted to check if this was REALLY the best option.

Letme commented 4 years ago

Why don't you just point a cmock to the i2c file like @herrmanthegerman did above?

I would not create your own files, but truth of the matter is that if you are running tests on PC instead of on target (or simulator) you will need to mock registers anyway (as memory addresses), so processor specific header file needs to be "rewritten" anyway in that case. But that is only that 1 file, all rest you just point mock to (with mock_ ) and it will most likely do it for you (especially with latest additions to static function parsing in cmock).

haemishkyd commented 4 years ago

Hi and thanks for your response. I did try that. The problem is that the lower level header files are guarded against direct inclusion. If I mock them, the mock_.c includes the lower level header file and the preprocessor #error that guards the file and throws an error. There seems to be some reason that they don't want the lower level files included and only the higher level file. I could go in and edit the header file to remove the #error in the header file but this doesn't seem like the "right" solution?

Letme commented 4 years ago

Or you could add #error directive to cmock strippables?

haemishkyd commented 4 years ago

Thanks for that suggestion, I did not know about strippables that is a handy concept. In this instance it doesn't work because the mocked file still then includes other header files that still have the #error left in them. This complicated structure is probably why they make sure one only includes the higher level header file. At this point in time I think I have two options:

  1. Create a module under the test/support folder or in the test folder (I am not sure what is best) that duplicates this function - but in this instance I lose all the mocking functionality relating to testing (ExpectAndReturn etc)
  2. Understand if there is a way to manually generate mocks (since I didn't know about strippable who knows what other tidbits are there)

Thanks again for your help.

Letme commented 4 years ago

Now I am interested in how they actually make this restriction work :wink:

Maybe just a blunt define to avoid the error? The strippables strip the error before the mock is created, so I can assume why this would actually fail in your case, because as mentioned original chip.h would be included as well. If you can share more details we can get to help you, but again generating the mocks manually is going to be a huge pain which cmock actually avoids.

haemishkyd commented 4 years ago

I will explain the whole thing here and if something jumps out you can let me know if there is a specific way to get around my issue. I will appreciate any advice, but I realise this is a very specific problem I have 😉

In my instance this is not truly embedded in that it is running on a 486 with a very stripped down version of linux. We run a real time kernel since we talk to time critical devices. None of this really matters except for the fact that the thing I can't mock is glib. We use certain functions within the glib library and I am trying to mock these. The glib.h file (the higher level file) is just a list of header files that get included such as glib/gstring.h. These header files have a #error in them that forces you to include glib.h and not gstring.h. For instance the output from ceedling test:all when I mock gstring.h is:

build/temp/_gstring.h:29:2: error: #error "Only <glib.h> can be included directly."
 #error "Only <glib.h> can be included directly."

I have tried to provide the #defines that glib.h provides in the test file (i.e. define the GLIB parameters to make gstring think it is being called from glib) but this doesn't seem to work I think because the files are being called from various places. I have considered just including glib (i.e. compiling in the library) but that takes away the option of me testing responses to various outputs of the glib function calls - it also breaks the model of the unit test I would think.

I want to also state that whereas I have done unit testing before this is my first attempt to use CMock so there may be an obvious solution to this problem or (more likely) a complete misconception about how to deal with this situation. Don't hold back if this is all just me being an idiot. For instance there could be a way to handle third party libraries that I just don't know about...

In any event, I thank you for your time and this conclude my sad story.

kashperanto commented 4 years ago

@haemishkyd That certainly seems like a difficult library to mock. My first instinct would be to just make your own glib.h file in the test/support/ folder and copy the function prototypes and typedefs that you use from each of those nested headers. The test/support/ folder is earlier in ceedlings path, so it shouldn't ever touch the real glib.h.

I know this seems to be a hassle, but you only need to do this for what you use, and only once. You will spend far less time on this task than you will in writing the tests.

I think a tool to do this for you (that works for any convoluted include scheme you may have) would not provide much value for what it is worth. CMock will still do all of the hard work of creating the mocks for you.

All that being said, I do understand the desire to have the tool "just work".

haemishkyd commented 4 years ago

@kashperanto and @Letme thanks for your help. Thank you for the suggestion of creating my own glib - I was heading in this direction but I wanted to make sure that this was the "correct" approach. What I am taking away from this is (and for anyone else that faces these problems this hopefully serves as a summary):

I do understand that a tool cannot expect to cater for all manner of situations - some of them will be too niched to really be worth catering for. I am happy to approach this problem in this way, I just needed to know that this was a viable approach.

kashperanto commented 4 years ago

@haemishkyd No problem, this tool definitely takes time to learn, and in my experience getting it set up for your project has been more challenging than just writing the tests.

It is interesting that Ceedling itself does support deep dependencies like this (via use_deep_dependencies if you have gcc), but only for build dependency checks. I wonder if the same technique might allow CMock to handle this...

mvandervoord commented 4 years ago

Hi. I just thought I'd jump in here with a different perspective.

Both CMock and Ceedling purposefully don't support diving deep into header files to pull out headers down the chain. This is very rarely desirable.

The purpose of mocking is to abstract away the rest of the system and to just deal with the public API. When we also provide hooks to all the included headers, we're breaking that promise.

I DO completely understand the situation you're dealing with now. When working with large API's that haven't been designed to have a single public header, this gets a lot more complex. There IS a potential solution in the works with Ceedling, but it won't be directly built into CMock (primarily because CMock doesn't have enough information to actually perform the task properly).