Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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());
}
4 changes: 4 additions & 0 deletions docs/BreakingChanges.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion docs/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
1 change: 0 additions & 1 deletion docs/KnownIssues.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions docs/mkdocs/configuration/reference/cmock.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions docs/mkdocs/plugins/fff.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 8 additions & 8 deletions docs/mkdocs/testing-guide/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions docs/mkdocs/testing-guide/partials/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 5 additions & 9 deletions docs/mkdocs/testing-guide/partials/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/ceedling/config/configurator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions lib/ceedling/defaults.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions lib/ceedling/generators/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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 )
Expand Down
2 changes: 2 additions & 0 deletions lib/ceedling/test_invoker/PIPELINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 22 additions & 0 deletions lib/ceedling/test_invoker/test_build_executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -190,6 +191,7 @@ def stage_generate_mocks(state)
test: testable.name,
input_filepath: details.input,
output_path: output_path,
folder: folder,
overrides: overrides
}

Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion lib/ceedling/test_invoker/test_build_planner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ def stage_determine_files(state)
path: subdir,
source: source,
input: input,
partial: partial
partial: partial,
include_path: include.filepath
)
end

Expand Down
5 changes: 3 additions & 2 deletions lib/ceedling/test_invoker/test_invoker_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
Loading