From 05831f07487993a444dafb95a16243eed2faf1e8 Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Thu, 8 Oct 2026 13:05:49 -0400 Subject: [PATCH 1/7] Cover mocking two same-named headers in one test A test mocking two headers that share a basename, each named by its directory, is the traditional-mock counterpart of the pending Partial example. It stays pending until CMock and Unity can name a mock by the folder its #include names. Co-Authored-By: Claude Opus 5.5 --- .../test_both_configs_mocked_traditional.c | 19 ++++++++++++ spec/system/same_named_modules_spec.rb | 31 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 assets/fixtures/same_named_modules/test/test_both_configs_mocked_traditional.c 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/spec/system/same_named_modules_spec.rb b/spec/system/same_named_modules_spec.rb index af8675288..859c4d71d 100644 --- a/spec/system/same_named_modules_spec.rb +++ b/spec/system/same_named_modules_spec.rb @@ -23,6 +23,9 @@ ## intermediate file, and that is where two same-basename sources in one test can ## collide, so each setting gets its own example. ## +## Mocking both modules in one test is the second subject. Each mock must be +## named by the directory its #include names or the two mocks collide. +## ## Assets: assets/fixtures/same_named_modules/ ## @@ -83,5 +86,33 @@ def expect_green end end + # Mocking is the other half of naming two same-named modules in one test. Each + # needs its own mock, held apart by the directory its #include names. + context "with both same-named modules mocked in one test" do + before do + copy_same_named_modules('uart', 'spi') + in_project do + copy_fixture("same_named_modules/test/test_both_configs_mocked_traditional.c", 'test') + end + end + + it "generates a mock per module and keeps their expectations separate" do + # CMock derives a mock's #include guard and its own _Init/_Verify/_Destroy names + # from the mock's filename alone, and Unity's runner calls them by that same name. + # Two same-named mocks therefore share a guard and lifecycle symbols. Closing this + # needs folder-qualified mock naming in both CMock and Unity. + pending 'CMock names a mock by filename alone, so two same-named mocks collide' + + in_project do + output = @c.ceedling_build_exec("test:all") + + expect(@c.last_exit_status).to eq(0) + expect(output).to match(/TESTED:\s+1/) + expect(output).to match(/PASSED:\s+1/) + expect(output).to match(/FAILED:\s+0/) + end + end + end + end end From 8cb79fd6ad14f546c0b791b53a8edc16dc828d4e Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Thu, 8 Oct 2026 13:06:01 -0400 Subject: [PATCH 2/7] Name each mock by the folder its #include names CMock and Unity's runner generator name a mock's include guard and lifecycle functions by its filename alone, so two same-named mocks in one test collide. With :cmock -> :mock_name_include_folder both tools fold the directory a test's #include names into that identity instead. Ceedling enables the option by default and mirrors it into :test_runner. Each mock records its #include as written, and stage_generate_mocks hands that directory to CMock as its folder. The mock keeps its mirrored location whenever the folder ends it. Co-Authored-By: Claude Opus 5.5 --- lib/ceedling/config/configurator.rb | 2 +- lib/ceedling/defaults.rb | 2 + lib/ceedling/generators/generator.rb | 8 +-- .../test_invoker/test_build_executor.rb | 22 ++++++++ .../test_invoker/test_build_planner.rb | 3 +- .../test_invoker/test_invoker_types.rb | 5 +- spec/units/config/configurator_spec.rb | 3 +- spec/units/test_build_executor_spec.rb | 54 +++++++++++++++++++ spec/units/test_build_planner_spec.rb | 6 ++- 9 files changed, 95 insertions(+), 10 deletions(-) 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/test_build_executor.rb b/lib/ceedling/test_invoker/test_build_executor.rb index 3793b0066..0c73a5103 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 behaves as it always has. + 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/spec/units/config/configurator_spec.rb b/spec/units/config/configurator_spec.rb index 898d4831c..7f2c9d777 100644 --- a/spec/units/config/configurator_spec.rb +++ b/spec/units/config/configurator_spec.rb @@ -242,7 +242,7 @@ def status(success) = instance_double(Process::Status, success?: success) def runner_config { project: { use_backtrace: :none }, - cmock: { mock_prefix: 'Mock', mock_suffix: '_x', enforce_strict_ordering: true }, + cmock: { mock_prefix: 'Mock', mock_suffix: '_x', mock_name_include_folder: true, enforce_strict_ordering: true }, unity: { defines: ['UNITY_DEFINE'], use_param_tests: false, shuffle_tests: false }, test_runner: { cmdline_args: false, defines: ['RUNNER_DEFINE'] }, } @@ -255,6 +255,7 @@ def runner_config expect( config[:test_runner][:mock_prefix] ).to eq( 'Mock' ) expect( config[:test_runner][:mock_suffix] ).to eq( '_x' ) + expect( config[:test_runner][:mock_name_include_folder] ).to eq( true ) expect( config[:test_runner][:enforce_strict_ordering] ).to eq( true ) end diff --git a/spec/units/test_build_executor_spec.rb b/spec/units/test_build_executor_spec.rb index 93bcea87f..1bd9e2955 100644 --- a/spec/units/test_build_executor_spec.rb +++ b/spec/units/test_build_executor_spec.rb @@ -1486,6 +1486,7 @@ def build_args allow(@configurator).to receive(:project_config_hash).and_return( {} ) allow(@configurator).to receive(:get_cmock_config).and_return( { mock_prefix: 'Mock' } ) + allow(@configurator).to receive(:cmock_mock_name_include_folder).and_return( true ) allow(@file_wrapper).to receive(:mkdir) # Where a mock's files sit is composed in one shared place, so the stage that # compiles a mock and the stage that generates it cannot disagree. @@ -1590,6 +1591,59 @@ def build_args @executor.stage_generate_mocks( @state ) end + + # CMock and the runner both name a mock by the folder its #include names. CMock writes + # below its own root at that folder, so the folder must also end the mock's directory. + context "naming a mock by its #include's folder" do + before(:each) do + allow(@dependinator).to receive(:stale?).and_return( true ) + allow(@dependinator).to receive(:mark_fresh) + end + + def include_as(path) + @state.mocks_list.first.details.include_path = path + end + + it "passes no folder for a bare #include and keeps the mirrored location" do + include_as( 'MockFoo.h' ) + + expect(@generator).to receive(:generate_mock) + .with( hash_including( output_path: 'build/test/mocks/sub', folder: nil ) ) + + @executor.stage_generate_mocks( @state ) + end + + it "passes the folder and keeps the mirrored location when the folder ends it" do + include_as( 'sub/MockFoo.h' ) + + expect(@generator).to receive(:generate_mock) + .with( hash_including( output_path: 'build/test/mocks/sub', folder: 'sub' ) ) + + @executor.stage_generate_mocks( @state ) + end + + # The mirror cannot hold a folder longer than itself, so the mock moves to the + # written path below the per-test root, and its tracked target moves with it. + it "places the mock at its written path when the #include spells more than the mirror" do + include_as( 'more/sub/MockFoo.h' ) + + expect(@generator).to receive(:generate_mock) + .with( hash_including( output_path: 'build/test/mocks/more/sub', folder: 'more/sub' ) ) + expect(@dependinator).to receive(:mark_fresh).with( 'build/test/mocks/more/sub/MockFoo.c' ) + + @executor.stage_generate_mocks( @state ) + end + + it "passes no folder when :mock_name_include_folder is disabled" do + allow(@configurator).to receive(:cmock_mock_name_include_folder).and_return( false ) + include_as( 'sub/MockFoo.h' ) + + expect(@generator).to receive(:generate_mock) + .with( hash_including( output_path: 'build/test/mocks/sub', folder: nil ) ) + + @executor.stage_generate_mocks( @state ) + end + end end context "#tailor_search_paths" do diff --git a/spec/units/test_build_planner_spec.rb b/spec/units/test_build_planner_spec.rb index 24672d295..bc28d91eb 100644 --- a/spec/units/test_build_planner_spec.rb +++ b/spec/units/test_build_planner_spec.rb @@ -189,7 +189,8 @@ path: 'drivers', source: 'src/drivers/foo.h', input: 'src/drivers/foo.h', - partial: false + partial: false, + include_path: 'MockFoo.h' ) ) end @@ -228,7 +229,8 @@ path: '', source: 'build/test/partials/a_test/ceedling_partial_foo_interface.h', input: 'build/test/partials/a_test/ceedling_partial_foo_interface.h', - partial: true + partial: true, + include_path: 'Mockceedling_partial_foo_interface.h' ) ) end From d035209e5528c286fa00815cc4500f900f12f52d Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Thu, 8 Oct 2026 13:06:03 -0400 Subject: [PATCH 3/7] Expect same-named mocks to build Co-Authored-By: Claude Opus 5.5 --- spec/system/partials_module_disambiguation_spec.rb | 13 +++---------- spec/system/same_named_modules_spec.rb | 9 +++------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/spec/system/partials_module_disambiguation_spec.rb b/spec/system/partials_module_disambiguation_spec.rb index d27ab6f0e..04d116092 100644 --- a/spec/system/partials_module_disambiguation_spec.rb +++ b/spec/system/partials_module_disambiguation_spec.rb @@ -112,17 +112,10 @@ end end + # Both mocks share a filename, so only the directory each #include names tells them + # apart. CMock and the runner fold that directory into each mock's guard and + # lifecycle function names. it "generates a mock per module and keeps their expectations separate" do - # CMock derives a mock's #include guard and its own _Init/_Verify/_Destroy names - # from the mock's filename alone. Two modules sharing a basename produce two mocks - # of the same filename in different directories, so the second is emptied by the - # first's guard and the two would define the same lifecycle symbols anyway. The - # directive macro fixes that filename -- the C preprocessor cannot fold a - # directory into it -- so closing this needs folder-qualified naming in CMock or a - # forwarding scheme here. Generation, placement, objects, and resolution are all - # correct up to that point. - pending 'CMock names a mock by filename alone, so two same-named mocks collide' - in_project do output = @c.ceedling_build_exec("test:all") diff --git a/spec/system/same_named_modules_spec.rb b/spec/system/same_named_modules_spec.rb index 859c4d71d..5db71d358 100644 --- a/spec/system/same_named_modules_spec.rb +++ b/spec/system/same_named_modules_spec.rb @@ -96,13 +96,10 @@ def expect_green end end + # Both mocks share a filename, so only the directory each #include names tells them + # apart. CMock and the runner fold that directory into each mock's guard and + # lifecycle function names. it "generates a mock per module and keeps their expectations separate" do - # CMock derives a mock's #include guard and its own _Init/_Verify/_Destroy names - # from the mock's filename alone, and Unity's runner calls them by that same name. - # Two same-named mocks therefore share a guard and lifecycle symbols. Closing this - # needs folder-qualified mock naming in both CMock and Unity. - pending 'CMock names a mock by filename alone, so two same-named mocks collide' - in_project do output = @c.ceedling_build_exec("test:all") From 25d88be65f00468c84029b780688f09502859f38 Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Sat, 17 Oct 2026 17:59:07 -0400 Subject: [PATCH 4/7] Say what Module Generator's NAMING holds Co-Authored-By: Claude Opus 5.5 --- plugins/module_generator/lib/module_generator_options.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_generator/lib/module_generator_options.rb b/plugins/module_generator/lib/module_generator_options.rb index cb08763f5..5f8b6ca44 100644 --- a/plugins/module_generator/lib/module_generator_options.rb +++ b/plugins/module_generator/lib/module_generator_options.rb @@ -16,9 +16,9 @@ # collection, or to that collection's first path when the hint matches nothing there. # A hint that matches nothing in any collection is a mistake and stops the task. # Without include paths, headers go where the source goes. -# class ModuleGeneratorOptions + # The :naming values Unity's module generator accepts, passed to it as given NAMING = %w[bumpy camel snake caps].freeze KINDS = %i[src inc tst].freeze From 040f875f1262814679e6b04262620af5affddd53 Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Sat, 17 Oct 2026 18:12:12 -0400 Subject: [PATCH 5/7] Cover where a mock is written when CMock names it by folder Generator#generate_mock roots CMock above the folder, hands it the folder, and reports the mock's own directory to plugins. A leading ./ is dropped from the folder, as the runner drops it. PIPELINE.md describes the naming and placement. Co-Authored-By: Claude Opus 5.5 --- lib/ceedling/test_invoker/PIPELINE.md | 2 + .../test_invoker/test_build_executor.rb | 2 +- spec/units/generators/generator_spec.rb | 42 ++++++++++++++++++- spec/units/test_build_executor_spec.rb | 10 +++++ 4 files changed, 53 insertions(+), 3 deletions(-) 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 0c73a5103..e7ee8497b 100644 --- a/lib/ceedling/test_invoker/test_build_executor.rb +++ b/lib/ceedling/test_invoker/test_build_executor.rb @@ -552,7 +552,7 @@ def get_library_paths_to_arguments() # 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 behaves as it always has. + # 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 diff --git a/spec/units/generators/generator_spec.rb b/spec/units/generators/generator_spec.rb index 760dddf80..627539c72 100644 --- a/spec/units/generators/generator_spec.rb +++ b/spec/units/generators/generator_spec.rb @@ -9,7 +9,8 @@ require 'ceedling/generators/generator' require 'ceedling/constants' -# Scoped to Generator#generate_object_file_c's choice of which file the compiler receives. +# Scoped to Generator#generate_object_file_c's choice of which file the compiler receives, +# and to where Generator#generate_mock has CMock write a mock. describe Generator do before(:each) do @tool_executor = double('tool_executor') @@ -26,8 +27,15 @@ :file_finder, :file_path_utils, :reportinator, :loginator, :test_runner_manager ].to_h { |name| [name, double( name.to_s ).as_null_object] } + @generator_mocks = double('generator_mocks') + @cmock = double('cmock', setup_mocks: nil) + allow(@generator_mocks).to receive(:build_configuration).and_return( {} ) + allow(@generator_mocks).to receive(:manufacture).and_return( @cmock ) + allow(@plugin_manager).to receive(:pre_mock_generate) + allow(@plugin_manager).to receive(:post_mock_generate) + @generator = described_class.new( - collaborators.merge( tool_executor: @tool_executor, plugin_manager: @plugin_manager ) + collaborators.merge( tool_executor: @tool_executor, plugin_manager: @plugin_manager, generator_mocks: @generator_mocks ) ) end @@ -54,4 +62,34 @@ def compile(**extra) expect(@plugin_manager).to have_received(:post_compile_execute).with( hash_including( source: 'src/gpio.c' ) ) end end + + describe '#generate_mock' do + def generate(**extra) + @generator.generate_mock( + context: :test, mock: :mock_config, test: 'test_both', input_filepath: 'src/drivers/uart/config.h', + output_path: 'build/test/mocks/test_both/drivers/uart', **extra + ) + end + + it 'has CMock write straight into the mock\'s directory without a folder' do + generate() + + expect(@generator_mocks).to have_received(:build_configuration).with( 'build/test/mocks/test_both/drivers/uart', overrides: {} ) + expect(@cmock).to have_received(:setup_mocks).with( 'src/drivers/uart/config.h', nil ) + end + + # CMock writes below its root at the folder, so the root is the directory less the folder + it 'roots CMock above the folder and hands it the folder, which names the mock' do + generate( folder: 'drivers/uart' ) + + expect(@generator_mocks).to have_received(:build_configuration).with( 'build/test/mocks/test_both', overrides: {} ) + expect(@cmock).to have_received(:setup_mocks).with( 'src/drivers/uart/config.h', 'drivers/uart' ) + end + + it 'still reports the mock\'s own directory to plugins' do + generate( folder: 'drivers/uart' ) + + expect(@plugin_manager).to have_received(:pre_mock_generate).with( hash_including( output_path: 'build/test/mocks/test_both/drivers/uart' ) ) + end + end end diff --git a/spec/units/test_build_executor_spec.rb b/spec/units/test_build_executor_spec.rb index 1bd9e2955..819155e82 100644 --- a/spec/units/test_build_executor_spec.rb +++ b/spec/units/test_build_executor_spec.rb @@ -1622,6 +1622,16 @@ def include_as(path) @executor.stage_generate_mocks( @state ) end + # The runner ignores a leading ./ too, so both name the mock alike + it "drops a leading ./ from the folder" do + include_as( './sub/MockFoo.h' ) + + expect(@generator).to receive(:generate_mock) + .with( hash_including( output_path: 'build/test/mocks/sub', folder: 'sub' ) ) + + @executor.stage_generate_mocks( @state ) + end + # The mirror cannot hold a folder longer than itself, so the mock moves to the # written path below the per-test root, and its tracked target moves with it. it "places the mock at its written path when the #include spells more than the mirror" do From 758c5f01c3ee7c6efc2054b1b2179a7174c60528 Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Sat, 17 Oct 2026 18:42:26 -0400 Subject: [PATCH 6/7] Name FFF fakes by the folder their #include names Ceedling now hands the mock generator the folder each mock's #include names, and the runner names its lifecycle calls by that folder. The FFF plugin's stand-in for CMock took no folder, so mock generation failed. It now takes the folder, writes the fake below its root at that folder, and names the fake's guard and control functions as CMock does under :mock_name_include_folder, so the runner's calls link. Co-Authored-By: Claude Opus 5.5 --- plugins/fff/lib/fff.rb | 26 +++++++++++++++---- plugins/fff/lib/fff_mock_generator.rb | 14 +++++----- .../spec/fff_mock_header_generator_spec.rb | 15 +++++++++++ .../spec/fff_mock_source_generator_spec.rb | 14 ++++++++++ 4 files changed, 58 insertions(+), 11 deletions(-) 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?(/ Date: Sat, 17 Oct 2026 21:36:58 -0400 Subject: [PATCH 7/7] Document mocking same-named headers by directory [skip ci] Co-Authored-By: Claude Opus 5.5 --- docs/BreakingChanges.md | 4 ++++ docs/Changelog.md | 5 ++++- docs/KnownIssues.md | 1 - docs/mkdocs/configuration/reference/cmock.md | 15 +++++++++++++++ docs/mkdocs/plugins/fff.md | 13 +++++++++++++ docs/mkdocs/testing-guide/conventions.md | 16 ++++++++-------- .../mkdocs/testing-guide/partials/conventions.md | 6 ++---- docs/mkdocs/testing-guide/partials/directives.md | 14 +++++--------- 8 files changed, 51 insertions(+), 23 deletions(-) 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