diff --git a/assets/fixtures/same_named_modules/test/test_both_configs_mocked_traditional.c b/assets/fixtures/same_named_modules/test/test_both_configs_mocked_traditional.c new file mode 100644 index 000000000..f10ae7c17 --- /dev/null +++ b/assets/fixtures/same_named_modules/test/test_both_configs_mocked_traditional.c @@ -0,0 +1,19 @@ +#include "unity.h" + +// Two modules sharing the basename `config`, each mocked by directory. +#include "drivers/uart/mock_config.h" +#include "drivers/spi/mock_config.h" + +void setUp(void) {} +void tearDown(void) {} + +// Each module's own public function is mocked separately, so an expectation set on one +// cannot be satisfied by the other. +void test_each_module_is_mocked_separately(void) +{ + uart_config_baud_ExpectAndReturn(7); + spi_config_clock_div_ExpectAndReturn(9); + + TEST_ASSERT_EQUAL_INT(7, uart_config_baud()); + TEST_ASSERT_EQUAL_INT(9, spi_config_clock_div()); +} diff --git a/docs/BreakingChanges.md b/docs/BreakingChanges.md index e9168d2af..e44d466d2 100644 --- a/docs/BreakingChanges.md +++ b/docs/BreakingChanges.md @@ -10,6 +10,10 @@ These breaking changes are complemented by three other documents: # [1.2.0] — Prerelease +## Mocks included through a folder are named by that folder + +`:cmock` ↳ `:mock_name_include_folder` is on by default. A mock included through a folder, such as `#include "drivers/uart/mock_config.h"`, now gets an include guard and `Init()`, `Verify()`, and `Destroy()` functions named by that folder (e.g. `drivers_uart_mock_config_Init()`). This lets one test file mock two same-named headers. The runner Ceedling generates calls the new names, so most projects need no change. A test or support file that calls a mock’s lifecycle functions directly, or references its include guard, must use the new names or set `:mock_name_include_folder` to `FALSE`. + ## `-P` and `--no-line-commands` rejected with test preprocessing Ceedling now refuses these preprocessor options at startup whenever test preprocessing is enabled. That is the case when `:project` ↳ `:use_test_preprocessor` is `:mocks`, `:tests`, or `:all`, or when Partials are enabled. diff --git a/docs/Changelog.md b/docs/Changelog.md index c5ca3c88d..e440b4c1a 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -33,7 +33,10 @@ In Ceedling’s early history simplicity won out with the assumption that every Previously, Partial directive macros took only a bare module name with no means to provide path information to distinguish modules of the same name. Every Partials `_MODULE` and `_CONFIG` macro now has an `_AT` variant taking that module’s directory as a separate first argument (e.g. `TEST_PARTIAL_PRIVATE_MODULE_AT(sensors/soil, Calibration)`). As with `#include` directives elsewhere, the directory is matched against the trailing end of a real file’s path. Supply only as much of it as it takes to name the module you mean. A bare module name in the original macros continues to work unchanged. -A test file can now **test** two same-named modules together. It cannot **mock** both of them because of a CMock limitation not yet resolved.Mocking either module on its own continues to work unaffected. See [Known Issues](https://github.com/ThrowTheSwitch/Ceedling/blob/master/docs/KnownIssues.md). +A test file can now **test** two same-named modules together, and it can **mock** both of them too (see the next section). + +### Mocking same-named headers by directory +A test file can now mock two headers that share a filename, such as `drivers/uart/config.h` and `drivers/spi/config.h`, by including each mock through its folder (`#include "drivers/uart/mock_config.h"`). This works for traditional mocks, `MOCK_PARTIAL_*_MODULE_AT()` Partials, and the FFF plugin’s fakes. The new [`:cmock` ↳ `:mock_name_include_folder`](https://docs.throwtheswitch.org/Ceedling/1.2.0/configuration/reference/cmock/#mock_name_include_folder) setting, enabled by default, names each mock’s internals by that folder so the two mocks no longer collide. It relies on matching features in CMock and Unity, which Ceedling’s vendored copies include. ### Multiple file extensions per file type Added support for multiple [file extensions](https://docs.throwtheswitch.org/Ceedling/1.2.0/configuration/reference/extension/) per type (e.g. `:extension` ↳ `:source` ⇒ `['.c', '.C']`) such as requested in [#947](https://github.com/ThrowTheSwitch/Ceedling/issues/947). diff --git a/docs/KnownIssues.md b/docs/KnownIssues.md index 3503ebf7f..faded290e 100644 --- a/docs/KnownIssues.md +++ b/docs/KnownIssues.md @@ -26,7 +26,6 @@ Ceedling 1.2.0 requires Ruby 3+ but only unoficially supports Ruby 4. ### Partials -1. A single test file cannot **mock** two same-named modules using Partials or traditional mocking, even now that it can name each of them by directory. A temporary CMock limitation causes duplicate symbols and missing definitions. A future CMock release will lift the limitation without a change to your test files. 1. A module whose header protects itself with `#pragma once` instead of a traditional `#ifndef` include guard can still hit a `redefinition` or `conflicting types` error, if any other header your module uses reaches that header in turn. Ceedling suppresses the real header by defining its include guard, and `#pragma once` provides no guard to define. Converting the module’s header to a traditional include guard works around it. 1. A type created by a macro invocation rather than written out — the list and queue building macros in `sys/queue.h`, for instance — is not recognized as a type, and a Partial using one fails to compile with an `unknown type name` error. 1. A `struct`, `union`, or `enum` written directly on a variable, as in `static struct { int depth; } slot;`, is treated as a variable alone. The generated Partial states the type twice and fails to compile. diff --git a/docs/mkdocs/configuration/reference/cmock.md b/docs/mkdocs/configuration/reference/cmock.md index 1d399969d..8cd0cc790 100644 --- a/docs/mkdocs/configuration/reference/cmock.md +++ b/docs/mkdocs/configuration/reference/cmock.md @@ -23,6 +23,21 @@ Tests fail if expected call order is not same as source order **Default**: TRUE +## `:mock_name_include_folder` + +Names each mock’s include guard and `Init()`, `Verify()`, and `Destroy()` +functions by the folder its `#include` names. A mock included as +`drivers/uart/mock_config.h` (`#include "drivers/uart/mock_config.h"`) gets +functions such as `drivers_uart_mock_config_Init()`. A mock included without +a folder keeps its plain name. + +This is what lets one test file mock two headers sharing a filename, such as +`drivers/uart/config.h` and `drivers/spi/config.h`. Include each mock through +its own folder. Ceedling copies this setting into the test runner +configuration, so runners always call the names the mocks define. + +**Default**: TRUE + ## `:verbosity` If not set, defaults to Ceedling’s verbosity level diff --git a/docs/mkdocs/plugins/fff.md b/docs/mkdocs/plugins/fff.md index bf4ba4cc1..c3ad2a454 100644 --- a/docs/mkdocs/plugins/fff.md +++ b/docs/mkdocs/plugins/fff.md @@ -223,6 +223,19 @@ We still use some CMock configuration options for setting things like the mock p :includes_c_post_header: ``` +### Fakes of same-named headers + +With `:cmock` ↳ `:mock_name_include_folder` enabled, Ceedling’s default, a fake +included through a folder is named by that folder, as a CMock mock is. A fake +included as `drivers/uart/mock_config.h` (`#include "drivers/uart/mock_config.h"`) +is written to that folder below the test’s mock directory, and its `Init()`, +`Verify()`, and `Destroy()` functions become `drivers_uart_mock_config_Init()` +and so on. One test file can then fake two headers sharing a filename by +including each through its own folder. + +The fakes of the functions themselves keep the functions’ own names, so the +two headers must not declare the same function. + ## FFF examples See the [FFF example project][fff-example-project]. This project illustrates how to use the plugin with full-size examples. diff --git a/docs/mkdocs/testing-guide/conventions.md b/docs/mkdocs/testing-guide/conventions.md index fd1a38039..b5d13b0ba 100644 --- a/docs/mkdocs/testing-guide/conventions.md +++ b/docs/mkdocs/testing-guide/conventions.md @@ -49,11 +49,11 @@ meant. to provide a path to distinguish source files of the same name to be compiled and linked with a test executable. -!!! warning "One test file cannot mock two same-named headers" - Path information distinguishes which header a mock is generated _from_, but a - single test file cannot presently mock two headers that share a filename - because of a (temporary) CMock limitation. See - [Known Issues](https://github.com/ThrowTheSwitch/Ceedling/blob/master/docs/KnownIssues.md). +!!! tip "Mocking two same-named headers in one test file" + Include each mock through its folder, as in `#include "drivers/uart/mock_config.h"` + and `#include "drivers/spi/mock_config.h"`. With + [`:cmock` ↳ `:mock_name_include_folder`](../configuration/reference/cmock.md#mock_name_include_folder), + on by default, each mock is named by that folder, and the two do not collide. `TEST_SOURCE_FILE()` also takes precedence over the header/source correspondence convention above. If a test both `#include`s a header and separately names a @@ -224,9 +224,9 @@ The `#include` list directs Ceedling in two ways: A mock is generated into the directory mirroring the header it came from, so two same-named headers in different directories produce two mocks in different -directories. Even so, a single test file cannot mock both because of a temporary -CMock limitation that will cause duplicated symbol names and missing defintions. -Mock either header alone, or split the two across separate test files. +directories. A single test file can mock both when it includes each mock through +its folder (e.g. `#include "folder/mock_foo.h"`), since each mock is then named +by that folder. Sometimes the source file you need to add to your test executable has no corresponding header file — e.g. `file_abc.h` contains symbols diff --git a/docs/mkdocs/testing-guide/partials/conventions.md b/docs/mkdocs/testing-guide/partials/conventions.md index cd9fb0cfc..5061effbc 100644 --- a/docs/mkdocs/testing-guide/partials/conventions.md +++ b/docs/mkdocs/testing-guide/partials/conventions.md @@ -56,10 +56,8 @@ sufficient when no duplicate modules exist. And, when multiple modules are present, it continues to resolve the way a compiler's own header search does — the first match among the ordered search paths wins. -Naming two same-named modules by directory lets one test file **test** both. It -cannot **mock** both — a limitation of how CMock names a mock's contents, covered -under [Naming a module by -directory](directives.md#naming-a-module-by-directory). +Naming two same-named modules by directory lets one test file **test** both, and +**mock** both, as covered under [Naming a module by directory](directives.md#naming-a-module-by-directory). !!! tip "When Ceedling cannot tell two modules apart" A bare module name matching more than one module is reported with the diff --git a/docs/mkdocs/testing-guide/partials/directives.md b/docs/mkdocs/testing-guide/partials/directives.md index 99c837cae..32d4ce18a 100644 --- a/docs/mkdocs/testing-guide/partials/directives.md +++ b/docs/mkdocs/testing-guide/partials/directives.md @@ -97,15 +97,11 @@ therefore generate two distinct files rather than overwriting one another. | Test function list | `TEST_PARTIAL_CONFIG(module, ...)` | `TEST_PARTIAL_CONFIG_AT(dir, module, ...)` | | Mock function list | `MOCK_PARTIAL_CONFIG(module, ...)` | `MOCK_PARTIAL_CONFIG_AT(dir, module, ...)` | -!!! warning "Two same-named modules cannot both be mocked in one test file" - `TEST_PARTIAL_*_MODULE_AT()` works for two modules sharing a basename where each - is tested through its own Partial. `MOCK_PARTIAL_*_MODULE_AT()` does not because - of a temporary limitation of CMock that causes duplicate symbols and missing - definitions. - - Mocking either module on its own works, and so does Partials testing both - together. To mock both Partials, split them across separate test files. - See [Known Issues](https://github.com/ThrowTheSwitch/Ceedling/blob/master/docs/KnownIssues.md). +!!! tip "Two same-named modules in one test file" + `TEST_PARTIAL_*_MODULE_AT()` and `MOCK_PARTIAL_*_MODULE_AT()` both work for two + modules sharing a basename. Each mock Partial is named by its directory through + [`:cmock` ↳ `:mock_name_include_folder`](../../configuration/reference/cmock.md#mock_name_include_folder), + enabled by default, so the two mocks do not collide. A `_CONFIG_AT` macro must name the same directory as the `_MODULE_AT` macro it refines. The directory and module together identify which Partial a function diff --git a/lib/ceedling/config/configurator.rb b/lib/ceedling/config/configurator.rb index 5b0a89ce8..132fa57fe 100644 --- a/lib/ceedling/config/configurator.rb +++ b/lib/ceedling/config/configurator.rb @@ -31,7 +31,7 @@ class Configurator ].freeze # CMock and Unity options that test runner generation also reads - RUNNER_CMOCK_OPTIONS = [:mock_prefix, :mock_suffix, :enforce_strict_ordering].freeze + RUNNER_CMOCK_OPTIONS = [:mock_prefix, :mock_suffix, :mock_name_include_folder, :enforce_strict_ordering].freeze RUNNER_UNITY_OPTIONS = [:use_param_tests, :shuffle_tests].freeze # A minimal subset of the directives-only preprocessor tool: no defines, no include paths, diff --git a/lib/ceedling/defaults.rb b/lib/ceedling/defaults.rb index 2f9d9eb0f..c48749943 100644 --- a/lib/ceedling/defaults.rb +++ b/lib/ceedling/defaults.rb @@ -375,6 +375,8 @@ # (B) Test runner generator uses these same configuration values :mock_prefix => 'Mock', :mock_suffix => '', + # Names each mock by the folder its #include names so same-named headers stay distinct + :mock_name_include_folder => true, # CMock's default duplicated here. # We need a value present so preprocessing logic can safely reference it. :treat_inlines => :exclude, diff --git a/lib/ceedling/generators/generator.rb b/lib/ceedling/generators/generator.rb index 10f7d375c..86e02eb22 100644 --- a/lib/ceedling/generators/generator.rb +++ b/lib/ceedling/generators/generator.rb @@ -114,7 +114,7 @@ def generate_partial_implementation( return @generator_partials.generate_implementation( **arg_hash ) end - def generate_mock(context:, mock:, test:, input_filepath:, output_path:, overrides: {}) + def generate_mock(context:, mock:, test:, input_filepath:, output_path:, folder: nil, overrides: {}) arg_hash = { :header_file => input_filepath, :test => test, @@ -133,7 +133,9 @@ def generate_mock(context:, mock:, test:, input_filepath:, output_path:, overrid # - Make CMock thread-safe # Get default config created by Ceedling and customize it - config = @generator_mocks.build_configuration( output_path, overrides: overrides ) + # CMock writes to its root plus `folder`. That folder always ends `output_path`. + mock_root = folder ? output_path.delete_suffix( File.join( '', folder ) ) : output_path + config = @generator_mocks.build_configuration( mock_root, overrides: overrides ) # Generate mock msg = @reportinator.generate_module_progress( @@ -144,7 +146,7 @@ def generate_mock(context:, mock:, test:, input_filepath:, output_path:, overrid @loginator.log( msg ) cmock = @generator_mocks.manufacture( config ) - cmock.setup_mocks( arg_hash[:header_file] ) + cmock.setup_mocks( arg_hash[:header_file], folder ) rescue StandardError => ex # Re-raise execption but decorate it with CMock to better identify it raise( ex, "CMock >> #{ex.message}", ex.backtrace ) diff --git a/lib/ceedling/test_invoker/PIPELINE.md b/lib/ceedling/test_invoker/PIPELINE.md index bc8966789..251ebd522 100644 --- a/lib/ceedling/test_invoker/PIPELINE.md +++ b/lib/ceedling/test_invoker/PIPELINE.md @@ -66,6 +66,8 @@ Stages 2 and 3 run before stage 4, so anything they derive from the `#include`s **Mocks (stages 9-10)** run only when the project uses mocks and this run has any. A transform flattens every test's planned mocks into one list, as the Partials transform does. Stage 9, with mock preprocessing on (`:mocks` or `:all`), preprocesses each header to be mocked. Stage 10 generates each mock's source and header with CMock, into a directory that mirrors its real header's location below the test's mock root. The `:mocking` stop point ends the run here. +Two same-named headers mocked by directory stay distinct through `:cmock` ↳ `:mock_name_include_folder`, on by default. CMock names a mock's include guard and lifecycle functions by the folder its `#include` names, and the runner names its calls by the same `#include`, so the two always agree. `TestBuildExecutor#mock_folder_and_path` hands CMock that folder. When the folder ends the mirrored directory, the mock stays there. An `#include` spelling more path than the mirror holds puts its mock at the written path below the test's mock root instead, which the root search path still reaches. + **Test file preprocessing and runners (stages 11-13)** work on the test file itself. Stages 11 and 12 run only with test preprocessing on. Stage 11 preprocesses the test file and extracts its `TEST_SOURCE_FILE()` directives from the result. Stage 12 parses test case names from that output. Stage 13 always runs and generates the Unity test runner, from the preprocessed test file when there is one and the original otherwise. The `:test_runner` stop point ends the run here. Each of these stages is skipped when nothing its output is built from has changed, which takes in more than the test file. Stage 11's full expansion reads every header the test reaches, so the project headers named in its line markers are among stage 11's dependencies, as they are for stage 4's second pass. Editing the macro in a header that enables a test case therefore reruns stage 11. When stage 11 is skipped, its `TEST_SOURCE_FILE()` directives are recalled from a cache. Stages 12 and 13 check one runner target. It depends on the test file, the file test case names are parsed from, the runner and Unity configuration, and the test's mock and include lists. With test preprocessing, those lists come from stage 4's reconciliation, so a header that changes which mocks a test has regenerates its runner although the test file is unchanged. diff --git a/lib/ceedling/test_invoker/test_build_executor.rb b/lib/ceedling/test_invoker/test_build_executor.rb index 3793b0066..e7ee8497b 100644 --- a/lib/ceedling/test_invoker/test_build_executor.rb +++ b/lib/ceedling/test_invoker/test_build_executor.rb @@ -149,6 +149,7 @@ def stage_generate_mocks(state) testable = mock.testable output_path = @file_path_utils.form_mock_output_path( testable.paths[:mocks], details.path ) + folder, output_path = mock_folder_and_path( testable.paths[:mocks], output_path, details.include_path ) @file_wrapper.mkdir( output_path ) # `details.input` -- not the stage 9 preprocessed target -- is the @@ -190,6 +191,7 @@ def stage_generate_mocks(state) test: testable.name, input_filepath: details.input, output_path: output_path, + folder: folder, overrides: overrides } @@ -542,6 +544,26 @@ def get_library_paths_to_arguments() private + # Finds the folder a mock's #include names and the directory the mock is written to. + # + # CMock names a mock's guard and lifecycle functions by that folder, and the runner names + # its calls by the same #include. Two same-named mocks stay distinct only if the folder + # reaches CMock. CMock writes below its own root at that folder, so the folder must end the + # mock's directory. A folder trailing the mirrored location keeps that location. An #include + # spelling more path than the mirror holds moves its mock below the per-test root at the + # written path, which the root search path still reaches. A bare #include has no folder. + # Without :mock_name_include_folder no folder is passed, and CMock names by filename alone. + def mock_folder_and_path(mocks_root, output_path, include_path) + return [nil, output_path] unless @configurator.cmock_mock_name_include_folder + + folder = File.dirname( include_path.to_s ).sub( %r{\A(?:\./)+}, '' ) + + return [nil, output_path] if folder.empty? || folder == '.' + return [folder, output_path] if output_path.end_with?( File.join( '', folder ) ) + + return [folder, File.join( mocks_root, folder )] + end + # Compile a single C or assembly source file into an object file. Returns # whether a real compile actually happened, so the caller can report how # many objects across the whole build needed nothing done. diff --git a/lib/ceedling/test_invoker/test_build_planner.rb b/lib/ceedling/test_invoker/test_build_planner.rb index d4acac9b8..5ed36392b 100644 --- a/lib/ceedling/test_invoker/test_build_planner.rb +++ b/lib/ceedling/test_invoker/test_build_planner.rb @@ -82,7 +82,8 @@ def stage_determine_files(state) path: subdir, source: source, input: input, - partial: partial + partial: partial, + include_path: include.filepath ) end diff --git a/lib/ceedling/test_invoker/test_invoker_types.rb b/lib/ceedling/test_invoker/test_invoker_types.rb index e02cc6aa3..553fd7df3 100644 --- a/lib/ceedling/test_invoker/test_invoker_types.rb +++ b/lib/ceedling/test_invoker/test_invoker_types.rb @@ -35,8 +35,9 @@ def initialize(**kwargs) # actually read (`input` -- the raw header or its preprocessed output, # depending on whether mock preprocessing is enabled), and whether this # mock is Ceedling's own generated Partial content rather than a mock of a - # real header (`partial`). - MockDetails = Struct.new(:name, :filepath, :path, :source, :input, :partial, keyword_init: true) unless const_defined?(:MockDetails, false) + # real header (`partial`). `include_path` is the #include as the test wrote + # it. It names the mock for CMock and the runner alike. + MockDetails = Struct.new(:name, :filepath, :path, :source, :input, :partial, :include_path, keyword_init: true) unless const_defined?(:MockDetails, false) # A test's generated Unity runner: the C file to be compiled (`output_filepath`) # and the (possibly preprocessed) test file it was generated from (`input_filepath`). diff --git a/plugins/fff/lib/fff.rb b/plugins/fff/lib/fff.rb index b6f7af404..03bf7ae0e 100644 --- a/plugins/fff/lib/fff.rb +++ b/plugins/fff/lib/fff.rb @@ -48,9 +48,11 @@ def initialize(options=nil) @includes_c_post_header = (@cm_config.includes_c_post_header || []).map{|h| h.match?(/