Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue with mocking header inline functions and defines #868

Closed
lafonso-gnrc opened this issue Apr 3, 2024 · 18 comments
Closed

Issue with mocking header inline functions and defines #868

lafonso-gnrc opened this issue Apr 3, 2024 · 18 comments

Comments

@lafonso-gnrc
Copy link

Hello,

I'm trying to unit test a piece of software that will use some embedded libraries for a microcontroller.
Said libraries have quite a few inline functions in the header.

I tried using the CMock option :treat_inlines: :include and I do have use_test_preprocessor: TRUE. This does not work because all the defines get erased.

With use_test_preprocessor: FALSE the problem I have is that I get the following errors:

build/test/mocks/mock_stm32h5xx_ll_tim.h:2627:1: error: multiple storage classes in declaration specifiers
2627 | typedef __STATIC_INLINE void (* CMOCK_LL_TIM_GenerateEvent_BRK2_CALLBACK)(TIM_TypeDef* TIMx, int cmock_num_calls);

Any advice on how to proceed? Am I missing some option?

@mvandervoord
Copy link
Member

Are you in a position to try the preview release candidate? (You'd need to download the gem from releases and then install it using gem install --local <filename>). If so, I believe that the new gem fixes this issue.

The second version of the problem above is caused by having the mocked version of the function AND the original version of the function both existing when we attempt to link. The release candidate solves this by cloning the header file and removing the inline version so that it can be linked properly during tests.

@lafonso-gnrc
Copy link
Author

I am currently using a snapshot 0.32.0-164e950.

Do you have any advised snapshot to get or just the latest one? I might try this over the weekend if I don't have time in the meantime.

@mvandervoord
Copy link
Member

Ah. If you're already using a release candidate, that suggests that we likely still have a problem in this area. I'll add this to our priority list! Thanks!

@lafonso-gnrc
Copy link
Author

lafonso-gnrc commented Apr 3, 2024

let me know if you need details to replicate the issue or for me to try anything.
It was using STM32 LL libraries (specifically the timer ones)

thank you

@LuisAfonso95
Copy link

Hello,

I've tried this with snapshot 0.32.0-9b8d8a9.

I created a new project and attempted to mock one of the LL headers from ST. I wanted to add my project.yml to the post but it unformats everything.
This is using GCC on WSL Linux (previous one was on windows with MSYS2)

I get an issue with the include:

In file included from build/test/mocks/test_gpio_ll/mock_stm32h7xx_ll_gpio.h:6,
from test/test_gpio_ll.c:8:
build/test/mocks/test_gpio_ll/stm32h7xx_ll_gpio.h:6:10: fatal error: STM32Cube_FW_H7_V1.11.0/Drivers/CMSIS/Device/ST/STM32H7xx/Include/stm32h7xx.h: No such file or directory
6 | #include "STM32Cube_FW_H7_V1.11.0/Drivers/CMSIS/Device/ST/STM32H7xx/Include/stm32h7xx.h"
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
compilation terminated.

Other than that I can see looking at the mock files that it does create the function prototypes and the typedefs. But it doesn't include the defines.

@mkarlesky
Copy link
Member

mkarlesky commented Apr 9, 2024

@LuisAfonso95 If you are able please do provide your configuration. You can use the code block feature of the Github comment editor to preserve the formatting. Or, you can use Markdown formatting directly by surrounding your configuration file text with ``` in a line before and after the text.

@LuisAfonso95
Copy link

Hello, This is what I tried in this last one:

:project:
  # how to use ceedling. If you're not sure, leave this as `gem` and `?`
  :which_ceedling: gem
  :ceedling_version: 0.32.0

  # optional features. If you don't need them, keep them turned off for performance
  :use_mocks: TRUE
  :use_test_preprocessor: TRUE
  :use_backtrace: FALSE

  # tweak the way ceedling handles automatic tasks
  :build_root: build
  :test_file_prefix: test_
  :default_tasks:
    - test:all

  # performance options. If your tools start giving mysterious errors, consider 
  # dropping this to 1 to force single-tasking
  :test_threads: 8
  :compile_threads: 8

  # you can specify different yaml config files which modify the existing one
  :options_paths: []

  # enable release build (more details in release_build section below)
  :release_build: FALSE

# specify additional yaml files to automatically load. This is helpful if you
# want to create project files which specify your tools, and then include those
# shared tool files into each project-specific project.yml file.
:import: []

# further details to configure the way Ceedling handles test code
:test_build:
  :use_assembly: FALSE

# further details to configure the way Ceedling handles release code
:release_build:
  :output: MyApp.out
  :use_assembly: FALSE
  :artifacts: []

# Plugins are optional Ceedling features which can be enabled. Ceedling supports
# a variety of plugins which may effect the way things are compiled, reported, 
# or may provide new command options. Refer to the readme in each plugin for 
# details on how to use it.
:plugins:
  :load_paths: []
  :enabled:
    #- beep                           # beeps when finished, so you don't waste time waiting for ceedling
    - module_generator               # handy for quickly creating source, header, and test templates
    #- gcov                           # test coverage using gcov. Requires gcc, gcov, and a coverage analyzer like gcovr
    #- bullseye                       # test coverage using bullseye. Requires bullseye for your platform
    #- command_hooks                  # write custom actions to be called at different points during the build process
    #- compile_commands_json_db          # generate a compile_commands.json file
    #- dependencies                   # automatically fetch 3rd party libraries, etc.
    #- subprojects                    # managing builds and test for static libraries
    #- fake_function_framework        # use FFF instead of CMock

    # Report options (You'll want to choose one stdout option, but may choose multiple stored options if desired)
    #- test_suite_reporter
    #- report_tests_raw_output_log
    - report_tests_pretty_stdout
    #- report_tests_ide_stdout
    #- report_tests_gtestlike_stdout
    #- teamcity_tests_report
    #- warnings_report

# override the default extensions for your system and toolchain
:extension:
  #:header: .h
  #:source: .c
  #:assembly: .s
  #:dependencies: .d
  #:object: .o
  :executable: .out
  #:testpass: .pass
  #:testfail: .fail
  #:subprojects: .a

# This is where Ceedling should look for your source and test files.
# see documentation for the many options for specifying this.
:paths:
  :test:
    - +:test/**
    - -:test/support
  :source:
    - src/**
    - STM32Cube_FW_H7_V1.11.0/Drivers/**
  :include:
    - src/** # In simple projects, this entry often duplicates :source
    - STM32Cube_FW_H7_V1.11.0/Drivers/**
  :support:
    - test/support
  :libraries: []

# You can even specify specific files to add or remove from your test
# and release collections. Usually it's better to use paths and let
# Ceedling do the work for you!
:files:
  :test: []
  :source: []

# Compilation symbols to be injected into builds
# See documentation for advanced options:
#  - Test name matchers for different symbols per test executable build
#  - Referencing symbols in multiple lists using advanced YAML
#  - Specifiying symbols used during test preprocessing
:defines:
  :common: &common_defines 
    - STM32H743xx
    - USE_FULL_LL_DRIVER
  :test:
    - *common_defines
    - TEST_CEEDLING # Simple list option to add symbol 'TEST' to compilation of all files in all test executables
  :test_preprocess:
    - *common_defines
    - TEST_CEEDLING
  :release: []

  # Enable to inject name of a test as a unique compilation symbol into its respective executable build. 
  :use_test_definition: FALSE 

# Configure additional command line flags provided to tools used in each build step
# :flags:
#   :release:
#     :compile:         # Add '-Wall' and '--02' to compilation of all files in release target
#       - -Wall
#       - --O2
#   :test:
#     :compile:
#       '(_|-)special': # Add '-pedantic' to compilation of all files in all test executables with '_special' or '-special' in their names
#         - -pedantic
#       '*':            # Add '-foo' to compilation of all files in all test executables
#         - -foo

# Configuration Options specific to CMock. See CMock docs for details
:cmock:
  :mock_prefix: mock_
  :when_no_prototypes: :warn
  :enforce_strict_ordering: TRUE
  :plugins:
    - :ignore
    - :callback
  :treat_as:
    uint8:    HEX8
    uint16:   HEX16
    uint32:   UINT32
    int8:     INT8
    bool:     UINT8
  :treat_inlines: :include

# Configuration options specific to Unity. 
:unity:
  :defines:
    - UNITY_EXCLUDE_FLOAT

# You can optionally have ceedling create environment variables for you before
# performing the rest of its tasks.
:environment: []

# LIBRARIES
# These libraries are automatically injected into the build process. Those specified as
# common will be used in all types of builds. Otherwise, libraries can be injected in just
# tests or releases. These options are MERGED with the options in supplemental yaml files.
:libraries:
  :placement: :end
  :flag: "-l${1}"
  :path_flag: "-L ${1}"
  :system: []    # for example, you might list 'm' to grab the math library
  :test: []
  :release: []

################################################################
# PLUGIN CONFIGURATION
################################################################

# Add -gcov to the plugins list to make sure of the gcov plugin
# You will need to have gcov and gcovr both installed to make it work.
# For more information on these options, see docs in plugins/gcov
:gcov:
  :utilities:
    - gcovr           # Use gcovr to create the specified reports (default).
    #- ReportGenerator # Use ReportGenerator to create the specified reports.
  :reports: # Specify one or more reports to generate.
    # Make an HTML summary report.
    - HtmlBasic
    # - HtmlDetailed
    # - Text
    # - Cobertura
    # - SonarQube
    # - JSON
    # - HtmlInline
    # - HtmlInlineAzure
    # - HtmlInlineAzureDark
    # - HtmlChart
    # - MHtml
    # - Badges
    # - CsvSummary
    # - Latex
    # - LatexSummary
    # - PngChart
    # - TeamCitySummary
    # - lcov
    # - Xml
    # - XmlSummary
  :gcovr:
    # :html_artifact_filename: TestCoverageReport.html
    # :html_title: Test Coverage Report
    :html_medium_threshold: 75
    :html_high_threshold: 90
    # :html_absolute_paths: TRUE
    # :html_encoding: UTF-8

# :module_generator:
#   :project_root: ./
#   :source_root: source/
#   :inc_root: includes/
#   :test_root: tests/
#   :naming: :snake #options: :bumpy, :camel, :caps, or :snake
#   :includes:
#     :tst: []
#     :src: []
#   :boilerplates:
#     :src: ""
#     :inc: ""
#     :tst: ""

# :dependencies:
#   :libraries:
#     - :name: WolfSSL
#       :source_path:   third_party/wolfssl/source
#       :build_path:    third_party/wolfssl/build
#       :artifact_path: third_party/wolfssl/install
#       :fetch:
#         :method: :zip
#         :source: \\shared_drive\third_party_libs\wolfssl\wolfssl-4.2.0.zip
#       :environment:
#         - CFLAGS+=-DWOLFSSL_DTLS_ALLOW_FUTURE
#       :build:
#         - "autoreconf -i"
#         - "./configure --enable-tls13 --enable-singlethreaded"
#         - make
#         - make install
#       :artifacts:
#         :static_libraries:
#           - lib/wolfssl.a
#         :dynamic_libraries:
#           - lib/wolfssl.so
#         :includes:
#           - include/**

# :subprojects:  
#   :paths:
#    - :name: libprojectA
#      :source:
#        - ./subprojectA/source
#      :include:
#        - ./subprojectA/include
#      :build_root: ./subprojectA/build
#      :defines: []

################################################################
# TOOLCHAIN CONFIGURATION
################################################################

#:tools:
# Ceedling defaults to using gcc for compiling, linking, etc.
# As [:tools] is blank, gcc will be used (so long as it's in your system path)
# See documentation to configure a given toolchain for use
# :tools:
#   :test_compiler: 
#     :executable:
#     :arguments: []
#     :name: 
#     :stderr_redirect: :auto
#     :optional: FALSE
#   :test_linker: 
#     :executable:
#     :arguments: []
#     :name: 
#     :stderr_redirect: :auto
#     :optional: FALSE
#   :test_assembler: 
#     :executable:
#     :arguments: []
#     :name: 
#     :stderr_redirect: :auto
#     :optional: FALSE
#   :test_fixture: 
#     :executable:
#     :arguments: []
#     :name: 
#     :stderr_redirect: :auto
#     :optional: FALSE
#   :test_includes_preprocessor: 
#     :executable:
#     :arguments: []
#     :name: 
#     :stderr_redirect: :auto
#     :optional: FALSE
#   :test_file_preprocessor: 
#     :executable:
#     :arguments: []
#     :name: 
#     :stderr_redirect: :auto
#     :optional: FALSE
#   :test_file_preprocessor_directives: 
#     :executable:
#     :arguments: []
#     :name: 
#     :stderr_redirect: :auto
#     :optional: FALSE
#   :test_dependencies_generator: 
#     :executable:
#     :arguments: []
#     :name: 
#     :stderr_redirect: :auto
#     :optional: FALSE
#   :release_compiler: 
#     :executable:
#     :arguments: []
#     :name: 
#     :stderr_redirect: :auto
#     :optional: FALSE
#   :release_linker: 
#     :executable:
#     :arguments: []
#     :name: 
#     :stderr_redirect: :auto
#     :optional: FALSE
#   :release_assembler: 
#     :executable:
#     :arguments: []
#     :name: 
#     :stderr_redirect: :auto
#     :optional: FALSE
#   :release_dependencies_generator: 
#     :executable:
#     :arguments: []
#     :name: 
#     :stderr_redirect: :auto
#     :optional: FALSE
# #These tools can be filled out when command_hooks plugin is enabled
#   :pre_mock_preprocess
#   :post_mock_preprocess
#   :pre_mock_generate
#   :post_mock_generate
#   :pre_runner_preprocess
#   :post_runner_preprocess
#   :pre_runner_generate
#   :post_runner_generate
#   :pre_compile_execute
#   :post_compile_execute
#   :pre_link_execute
#   :post_link_execute
#   :pre_test_fixture_execute
#   :pre_test
#   :post_test
#   :pre_release
#   :post_release
#   :pre_build
#   :post_build
#   :post_error
...

@Letme
Copy link
Contributor

Letme commented Apr 15, 2024

You have include path error, because I assume you would like to run this on x86_64 architecture, but CMSIS from ARM has few guards against compiling for wrong architecture.

So after you fix your include, you will also have to create mock for the chip (replace hardcoded addresses as variables):

stm32h7mock.h

volatile USART_TypeDef	  usart0_base; /* Create global variable instead of the fixed base address */
#define USART0_BASE       (&usart0_base) /**< USART0 base address  */
volatile USART_TypeDef	  usart1_base;  /* Create global variable instead of the fixed base address */
#define USART1_BASE       (&usart1_base) /**< USART1 base address  */

@lafonso-gnrc
Copy link
Author

The issue only occurs in the mock with that specific snapshot. The path seems to be correct.

I do have mock registers, this one was just a quick test of the new snapshot to see if it could mock the header inline functions.

@lafonso-gnrc
Copy link
Author

lafonso-gnrc commented Apr 15, 2024

I believe pull request #728 seems like it does exactly what I need. Might try to pull and build it to see if it works.

@M-Bab
Copy link

M-Bab commented May 29, 2024

Yeah that sounds very familiar. Have you been successful with my modified version of Ceedling @lafonso-gnrc ?

I think the fastest way to get it installed is the following:

sudo gem install specific_install
sudo gem specific_install https://github.com/KLSMartin/Ceedling working

@mkarlesky
Copy link
Member

This bug was fixed as of a prerelease of Ceedling 1.0.0 from two or three weeks ago. CMock's inline handling and Ceedling’s preprocessing work together now.

@lafonso-gnrc
Copy link
Author

Hello, I have tried this with pre-release 1.0.0-95edb5b. I have :treat_inlines: :include in cmock settings and :use_test_preprocessor: :all in project. It still removes all #defines.

I have defines from ST's LL like the following that don-t appear in the generated header.

/** @defgroup TIM_LL_EC_IC_FILTER Input Configuration Filter
  * @{
  */
#define LL_TIM_IC_FILTER_FDIV1                 0x00000000U                                                        /*!< No filter, sampling is done at fDTS */
#define LL_TIM_IC_FILTER_FDIV1_N2              (TIM_CCMR1_IC1F_0 << 16U)                                          /*!< fSAMPLING=fCK_INT, N=2 */
#define LL_TIM_IC_FILTER_FDIV1_N4              (TIM_CCMR1_IC1F_1 << 16U)                                          /*!< fSAMPLING=fCK_INT, N=4 */
#define LL_TIM_IC_FILTER_FDIV1_N8              ((TIM_CCMR1_IC1F_1 | TIM_CCMR1_IC1F_0) << 16U)                     /*!< fSAMPLING=fCK_INT, N=8 */
#define LL_TIM_IC_FILTER_FDIV2_N6              (TIM_CCMR1_IC1F_2 << 16U)                                          /*!< fSAMPLING=fDTS/2, N=6 */
#define LL_TIM_IC_FILTER_FDIV2_N8              ((TIM_CCMR1_IC1F_2 | TIM_CCMR1_IC1F_0) << 16U)                     /*!< fSAMPLING=fDTS/2, N=8 */
#define LL_TIM_IC_FILTER_FDIV4_N6              ((TIM_CCMR1_IC1F_2 | TIM_CCMR1_IC1F_1) << 16U)                     /*!< fSAMPLING=fDTS/4, N=6 */
#define LL_TIM_IC_FILTER_FDIV4_N8              ((TIM_CCMR1_IC1F_2 | TIM_CCMR1_IC1F_1 | TIM_CCMR1_IC1F_0) << 16U)  /*!< fSAMPLING=fDTS/4, N=8 */
#define LL_TIM_IC_FILTER_FDIV8_N6              (TIM_CCMR1_IC1F_3 << 16U)                                          /*!< fSAMPLING=fDTS/8, N=6 */
#define LL_TIM_IC_FILTER_FDIV8_N8              ((TIM_CCMR1_IC1F_3 | TIM_CCMR1_IC1F_0) << 16U)                     /*!< fSAMPLING=fDTS/8, N=8 */
#define LL_TIM_IC_FILTER_FDIV16_N5             ((TIM_CCMR1_IC1F_3 | TIM_CCMR1_IC1F_1) << 16U)                     /*!< fSAMPLING=fDTS/16, N=5 */
#define LL_TIM_IC_FILTER_FDIV16_N6             ((TIM_CCMR1_IC1F_3 | TIM_CCMR1_IC1F_1 | TIM_CCMR1_IC1F_0) << 16U)  /*!< fSAMPLING=fDTS/16, N=6 */
#define LL_TIM_IC_FILTER_FDIV16_N8             ((TIM_CCMR1_IC1F_3 | TIM_CCMR1_IC1F_2) << 16U)                     /*!< fSAMPLING=fDTS/16, N=8 */
#define LL_TIM_IC_FILTER_FDIV32_N5             ((TIM_CCMR1_IC1F_3 | TIM_CCMR1_IC1F_2 | TIM_CCMR1_IC1F_0) << 16U)  /*!< fSAMPLING=fDTS/32, N=5 */
#define LL_TIM_IC_FILTER_FDIV32_N6             ((TIM_CCMR1_IC1F_3 | TIM_CCMR1_IC1F_2 | TIM_CCMR1_IC1F_1) << 16U)  /*!< fSAMPLING=fDTS/32, N=6 */
#define LL_TIM_IC_FILTER_FDIV32_N8             (TIM_CCMR1_IC1F << 16U)                                            /*!< fSAMPLING=fDTS/32, N=8 */

@M-Bab
Copy link

M-Bab commented Jul 31, 2024

@lafonso-gnrc They will always be removed in the copied header. The question is: Does it work though or does it not work? And if it doesn't work is it due to missing defines?

And did you ever tried my fix with the above description? #868 (comment) - even if it is only as a cross-check?

@lafonso-gnrc
Copy link
Author

@M-Bab oh it's a fully separate branch, not a pre-release. I got to try that at some point. Just been very busy, completely lost that detail.

To answer your question, it doesn't work at all because those defines are necessary for the test and code to compile

@mkarlesky
Copy link
Member

@lafonso-gnrc Let me first explain how inline mocking and mockable header preprocessing are working together. Then, let me try to state back what I think the problem you're experiencing is with a possible solution.

Preprocessing a mockable header simply means running it through the GCC preprocessor to expand all macros and conditional preprocessor statements to arrive at nothing but C code. From this, CMock can then extract the function signatures and types it needs. In the case of handling inlines CMock takes the extra step of generating a copy of a header file with inlines stripped in order to mock the function signatures. If inline statements are embedded in a macro, preprocessing must happen in order to expose the actual inline key words for CMock.

So, in a complicated project it goes like this: <header file> → Ceedling preprocessing → <expanded header file> → CMock inline processing → <modified header file copy> → CMock mocking → mock <header file> + <source file>.

In previous versions of Ceedling and CMock various complexities and bugs were preventing the intended behavior above from working properly. This has been fixed.

Generally speaking, it's just practically impossible to preserve an arbitrary set of macros through preprocessing. If you have hidden function signatures in header files, you gotta reveal them with preprocessing. Similarly, if you have conditionals around (not in) your test cases, you need preprocessing as well.

I think what is happening is that your inlines are now being handled properly. But, the casualty in the process is macros that you need in your tests.

Macros can get quite complicated particularly when it comes to providing access to bit flags, memory mapped registers, etc. I suspect that's where your need is here, but, of course, I can't say. I gather you need them in your test cases, but, again, I'm guessing.

I may be oversimplifying the problem / misunderstanding with this suggestion, but an approach that might work is this:

  1. Enable inline processing for CMock.
  2. Use the latest Ceedling prerelease and enable only mockable header preprocessing (:use_test_preprocessor: :mocks). This assumes that you don't need complicated conditional macro processing in your test file (e.g. #ifdef blocks around entire test cases enabling and disabling — this will foil test runner generation without preprocessing).
  3. Make use of the needed macros in your test cases.

If you do have complex test files with #ifdef blocks around test cases, your test code may not be as cohesive as you'd like, but you could probably use Ceedling's new features for treating each test file as a mini project to break up test cases into different files to solve this problem.

We just recently expanded and clarified the documentation on Ceedling's preprocessing features. I'd suggest looking in CeedlingPacket to digest this. If I'm not understanding your problem please come on back here with more details. I can't guarantee it, but with a little effort your problem may be solvable.

@M-Bab
Copy link

M-Bab commented Oct 7, 2024

We also had a chance to test it and can unfortunately confirm it is not working as expected. Your explanation above @mkarlesky also shows you chose intentionally the opposite approach of my pull request.

Your argument in the combined usage of treat_inlines and use_preprocessor:

  1. The inline keyword might be hidden behind complex compile macros.
  2. Therefore the preprocessor must run befor cmock can find the inline functions.
  3. Defines and macros are lost in the process because they are consumed by the preprocessor.

My approach was:

  1. The defines and the macros must not be consumed by the preprocessor.
  2. Therefore cmock must be served the original header file (which was done by a tricky bypass). Compare: Allow the combination of use_test_preprocessor and CMock's treat_inlines #728 (comment)
  3. You get headers that are still fully functional.

I know see what you aimed for but I am still confused because CMock has already a very flexible configuration to help it find all the inline keywords: inline_function_patterns

We actually had to extend this parameter well to grep all the inline functions:

  :inline_function_patterns:
   - '__STATIC_FORCEINLINE'
   - '__STATIC_INLINE'
   - '__INLINE'
   - '(static\s+inline|inline\s+static)\s*'
   - '(\bstatic\b|\binline\b)\s*'

But in the end it worked very well and without any hacks or helper headers for the whole Microcontroller HALs.

@M-Bab
Copy link

M-Bab commented Oct 8, 2024

It also feels like the new configuration feature :use_test_preprocessor: :mocks is also mainly a workaround developed because the inverse approach might ruin the headers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants