From aaf9559a57cd151d92d852221a2180290273e7a5 Mon Sep 17 00:00:00 2001 From: Mark VanderVoord Date: Thu, 18 Jun 2026 16:34:24 -0400 Subject: [PATCH] Finish better identification of array pointer/len pairs. Add fallback _ExpectWithArrayExtended when this happens for flexibility. Fixes issue #520 --- docs/CMock_Summary.md | 53 +++++++++-- lib/cmock_generator_plugin_array.rb | 72 ++++++++++++--- .../test_interactions/array_handling.yml | 87 ++++++++++++++++--- .../unit/cmock_generator_plugin_array_test.rb | 57 ++++++++++++ 4 files changed, 237 insertions(+), 32 deletions(-) diff --git a/docs/CMock_Summary.md b/docs/CMock_Summary.md index 0496b50..e5a8447 100644 --- a/docs/CMock_Summary.md +++ b/docs/CMock_Summary.md @@ -147,6 +147,27 @@ that they are pointing to the same memory address. * `retval func(void)` => (nothing. In fact, an additional function is only generated if the params list contains pointers) * `retval func(other, ptr* param)` => `void func_ExpectWithArrayAndReturn(other, ptr* param, int param_depth, retval_to_return)` +When the `:array_size_name` and `:array_size_type` options are configured, CMock +can recognize that a scalar parameter is the size of an adjacent pointer parameter +and pair them together. When a size parameter is paired with a pointer, the `_Expect` +call automatically uses it as the array depth, and `_ExpectWithArray` preserves +the original argument order without adding a separate depth argument. + +When performing this type of automatic identification of arguments, CMock will also +generate an additional `_ExpectWithArrayExtended`, which accepts an explicit +depth for every pointer — allowing you to override the inferred depth when the +pairing heuristic guesses wrong: + +* `void func(int size, ptr* buf)` => + * `void func_ExpectWithArray(int size, ptr* buf)` _(depth inferred from `size`)_ + * `void func_ExpectWithArrayExtended(int size, ptr* buf, int buf_Depth)` _(explicit override)_ + +`_ExpectWithArrayExtended` is only generated for functions where at least one +size parameter has been automatically identified. For all other functions the short +`_ExpectWithArray` already gives full depth control. This function can be used to correct +poor assumptions that CMock has made. It can also be used to separately verify a passed +length, but compare the actual contents for a DIFFERENT number of elements. + Ignore: ------- @@ -722,11 +743,7 @@ from the defaults. We've tried to specify what the defaults are below. * `void GoBananas(Banana * bananas, int num_bananas)` * `int write_data(int fd, const uint8_t * data, uint32_t size)` - - To recognize functions like these, CMock looks for a parameter list - containing a pointer (which could be an array) followed by something that - could be an array size. "Something", by default, means an `int` or `size_t` - parameter with a name containing "size" or "len". + * `void store_data(int buf_size, uint8_t * buf)` `:array_size_type` is a list of additional types (besides `int` and `size_t`) that could be used for an array size parameter. For example, to get CMock to @@ -741,8 +758,30 @@ from the defaults. We've tried to specify what the defaults are below. cfg[:array_size_name] = 'size|len|num_' - Parameters must match *both* `:array_size_type` and `:array_size_name` (and - must come right after a pointer parameter) to be treated as an array size. + A parameter must match *both* `:array_size_type` and `:array_size_name` to be + treated as an array size. + + **Pairing heuristic:** CMock scores each candidate size parameter against each + pointer parameter using name similarity. It strips size-words (e.g. "size", + "len") from the size parameter's name to derive a root, then checks whether + that root matches the pointer's name exactly (score 10), as a prefix or suffix + (score 7), or as a substring (score 5). Adjacency in the argument list adds a + small bonus (score +2). The highest-scoring pairing wins. This means CMock + correctly pairs `buff_size` with `buffer` even when another pointer appears + adjacent to `buff_size`. + + Size parameters may appear either **before or after** the pointer they describe. + CMock recognizes both orderings: + + * Size after pointer: `void func(uint8_t* buf, int buf_size)` — the `_Expect` + call uses `buf_size` as the depth automatically. `_ExpectWithArray` adds an + explicit `buf_Depth` argument after `buf` for manual override. + + * Size before pointer: `void func(int buf_size, uint8_t* buf)` — the `_Expect` + call again uses `buf_size` as the depth automatically, and `_ExpectWithArray` + keeps arguments in their original order with no extra depth argument added. + An additional `_ExpectWithArrayExtended` variant is generated that does accept + an explicit depth, allowing you to override the inferred value when needed. Once you've told it how to recognize your arrays, CMock will give you `_Expect` calls that work more like `_ExpectWithArray`, and compare an array of objects diff --git a/lib/cmock_generator_plugin_array.rb b/lib/cmock_generator_plugin_array.rb index ae250a3..5a72c91 100644 --- a/lib/cmock_generator_plugin_array.rb +++ b/lib/cmock_generator_plugin_array.rb @@ -28,20 +28,58 @@ class CMockGeneratorPluginArray def mock_function_declarations(function) return nil unless function[:contains_ptr?] - args_call_i = function[:args].map { |m| m[:ptr?] && m[:array_size_order] != :before ? "#{m[:name]}, #{m[:name]}_Depth" : (m[:name]).to_s }.join(', ') - args_call_o = function[:args].map { |m| m[:ptr?] && m[:array_size_order] != :before ? "#{m[:name]}, (#{m[:name]}_Depth)" : (m[:name]).to_s }.join(', ') + func_name = function[:name] + + # C function signature: always explicit depth for every pointer arg args_string = function[:args].map do |m| - m[:ptr?] && m[:array_size_order] != :before ? "#{@utils.arg_declaration(m)}, int #{m[:name]}_Depth" : @utils.arg_declaration(m) + m[:ptr?] ? "#{@utils.arg_declaration(m)}, int #{m[:name]}_Depth" : @utils.arg_declaration(m) end.join(', ') + + # Short macro params: paired ptrs (before OR after) omit _Depth (auto-filled from paired size arg) + args_call_i = function[:args].map do |m| + m[:ptr?] && !m[:array_size_name] ? "#{m[:name]}, #{m[:name]}_Depth" : m[:name].to_s + end.join(', ') + + # Short macro call: paired ptrs pass paired size name as depth automatically + args_call_o = function[:args].map do |m| + if m[:ptr?] && m[:array_size_name] + "#{m[:name]}, (#{m[:array_size_name]})" + elsif m[:ptr?] + "#{m[:name]}, (#{m[:name]}_Depth)" + else + m[:name].to_s + end + end.join(', ') + + # Extended macro params: every ptr gets an explicit _Depth + args_call_ext_i = function[:args].map do |m| + m[:ptr?] ? "#{m[:name]}, #{m[:name]}_Depth" : m[:name].to_s + end.join(', ') + + # Extended macro call: every ptr passes (name_Depth) + args_call_ext_o = function[:args].map do |m| + m[:ptr?] ? "#{m[:name]}, (#{m[:name]}_Depth)" : m[:name].to_s + end.join(', ') + + has_paired = function[:args].any? { |m| m[:array_size_name] } + lines = '' if function[:return][:void?] - lines << "#define #{function[:name]}_ExpectWithArrayAndReturn(#{args_call_i}, cmock_retval) TEST_FAIL_MESSAGE(\"#{function[:name]} requires _ExpectWithArray (not AndReturn)\");\n" if @error_stubs - lines << "#define #{function[:name]}_ExpectWithArray(#{args_call_i}) #{function[:name]}_CMockExpectWithArray(__LINE__, #{args_call_o})\n" - lines << "void #{function[:name]}_CMockExpectWithArray(UNITY_LINE_TYPE cmock_line, #{args_string});\n" + if @error_stubs + lines << "#define #{func_name}_ExpectWithArrayAndReturn(#{args_call_i}, cmock_retval) TEST_FAIL_MESSAGE(\"#{func_name} requires _ExpectWithArray (not AndReturn)\");\n" + lines << "#define #{func_name}_ExpectWithArrayExtendedAndReturn(#{args_call_ext_i}, cmock_retval) TEST_FAIL_MESSAGE(\"#{func_name} requires _ExpectWithArrayExtended (not AndReturn)\");\n" if has_paired + end + lines << "#define #{func_name}_ExpectWithArray(#{args_call_i}) #{func_name}_CMockExpectWithArray(__LINE__, #{args_call_o})\n" + lines << "#define #{func_name}_ExpectWithArrayExtended(#{args_call_ext_i}) #{func_name}_CMockExpectWithArray(__LINE__, #{args_call_ext_o})\n" if has_paired + lines << "void #{func_name}_CMockExpectWithArray(UNITY_LINE_TYPE cmock_line, #{args_string});\n" else - lines << "#define #{function[:name]}_ExpectWithArray(#{args_call_i}) TEST_FAIL_MESSAGE(\"#{function[:name]} requires _ExpectWithArrayAndReturn\");\n" if @error_stubs - lines << "#define #{function[:name]}_ExpectWithArrayAndReturn(#{args_call_i}, cmock_retval) #{function[:name]}_CMockExpectWithArrayAndReturn(__LINE__, #{args_call_o}, cmock_retval)\n" - lines << "void #{function[:name]}_CMockExpectWithArrayAndReturn(UNITY_LINE_TYPE cmock_line, #{args_string}, #{function[:return][:str]});\n" + if @error_stubs + lines << "#define #{func_name}_ExpectWithArray(#{args_call_i}) TEST_FAIL_MESSAGE(\"#{func_name} requires _ExpectWithArrayAndReturn\");\n" + lines << "#define #{func_name}_ExpectWithArrayExtended(#{args_call_ext_i}) TEST_FAIL_MESSAGE(\"#{func_name} requires _ExpectWithArrayExtendedAndReturn\");\n" if has_paired + end + lines << "#define #{func_name}_ExpectWithArrayAndReturn(#{args_call_i}, cmock_retval) #{func_name}_CMockExpectWithArrayAndReturn(__LINE__, #{args_call_o}, cmock_retval)\n" + lines << "#define #{func_name}_ExpectWithArrayExtendedAndReturn(#{args_call_ext_i}, cmock_retval) #{func_name}_CMockExpectWithArrayAndReturn(__LINE__, #{args_call_ext_o}, cmock_retval)\n" if has_paired + lines << "void #{func_name}_CMockExpectWithArrayAndReturn(UNITY_LINE_TYPE cmock_line, #{args_string}, #{function[:return][:str]});\n" end lines end @@ -51,10 +89,17 @@ class CMockGeneratorPluginArray lines = [] func_name = function[:name] + + # C function: always explicit depth for every pointer arg args_string = function[:args].map do |m| - m[:ptr?] && m[:array_size_order] != :before ? "#{@utils.arg_declaration(m)}, int #{m[:name]}_Depth" : @utils.arg_declaration(m) + m[:ptr?] ? "#{@utils.arg_declaration(m)}, int #{m[:name]}_Depth" : @utils.arg_declaration(m) end.join(', ') - call_string = function[:args].map { |m| m[:ptr?] && m[:array_size_order] != :before ? "#{m[:name]}, #{m[:name]}_Depth" : m[:name] }.join(', ') + + # Call to CMockExpectParameters_: :before-paired ptrs pass only ptr name (their depth is set from paired size arg) + call_string = function[:args].map do |m| + m[:ptr?] && m[:array_size_order] != :before ? "#{m[:name]}, #{m[:name]}_Depth" : m[:name] + end.join(', ') + lines << if function[:return][:void?] "void #{func_name}_CMockExpectWithArray(UNITY_LINE_TYPE cmock_line, #{args_string})\n" else @@ -63,6 +108,11 @@ class CMockGeneratorPluginArray lines << "{\n" lines << @utils.code_add_base_expectation(func_name) lines << " CMockExpectParameters_#{func_name}(cmock_call_instance, #{call_string});\n" + # Override depths for :before-paired pointers. CMockExpectParameters_ sets these from the paired + # size arg; the explicit _Depth param here allows _ExpectWithArrayExtended to override that value. + function[:args].each do |arg| + lines << " cmock_call_instance->Expected_#{arg[:name]}_Depth = #{arg[:name]}_Depth;\n" if arg[:ptr?] && arg[:array_size_order] == :before + end lines << " cmock_call_instance->ReturnVal = cmock_to_return;\n" unless function[:return][:void?] lines << "}\n\n" end diff --git a/test/system/test_interactions/array_handling.yml b/test/system/test_interactions/array_handling.yml index 03d4895..52dc258 100644 --- a/test/system/test_interactions/array_handling.yml +++ b/test/system/test_interactions/array_handling.yml @@ -334,23 +334,34 @@ } - :pass: TRUE - :should: 'compare a subset of sensor elements via ExpectWithArray and pass' + :should: 'compare all sensor elements via collapsed ExpectWithArray and pass' + :code: | + test() + { + SENSOR_T expected[] = {{1, 100}, {2, 200}, {3, 300}}; + process_sensors_ExpectWithArray(expected, 3); + + function_c(); + } + + - :pass: TRUE + :should: 'compare a subset of sensor elements via ExpectWithArrayExtended and pass' :code: | test() { SENSOR_T expected[] = {{1, 100}, {2, 200}}; - process_sensors_ExpectWithArray(expected, 2, 3); + process_sensors_ExpectWithArrayExtended(expected, 2, 3); function_c(); } - :pass: FALSE - :should: 'detect mismatch when comparing sensor subset via ExpectWithArray' + :should: 'detect mismatch when comparing sensor subset via ExpectWithArrayExtended' :code: | test() { SENSOR_T expected[] = {{1, 100}, {2, 999}}; - process_sensors_ExpectWithArray(expected, 2, 3); + process_sensors_ExpectWithArrayExtended(expected, 2, 3); function_c(); } @@ -389,23 +400,23 @@ } - :pass: TRUE - :should: 'compare a subset of status elements via ExpectWithArray and pass' + :should: 'compare a subset of status elements via ExpectWithArrayExtended and pass' :code: | test() { STATUS_E expected[] = {STATUS_OK, STATUS_ERROR}; - process_statuses_ExpectWithArray(expected, 2, 3); + process_statuses_ExpectWithArrayExtended(expected, 2, 3); function_d(); } - :pass: FALSE - :should: 'detect mismatch in status subset via ExpectWithArray' + :should: 'detect mismatch in status subset via ExpectWithArrayExtended' :code: | test() { STATUS_E expected[] = {STATUS_OK, STATUS_PENDING}; - process_statuses_ExpectWithArray(expected, 2, 3); + process_statuses_ExpectWithArrayExtended(expected, 2, 3); function_d(); } @@ -453,27 +464,27 @@ } - :pass: TRUE - :should: 'compare a subset of sample elements via ExpectWithArray and pass' + :should: 'compare a subset of sample elements via ExpectWithArrayExtended and pass' :code: | test() { SAMPLE_U expected[3]; expected[0].raw = 111; expected[1].raw = 222; - process_samples_ExpectWithArray(expected, 2, 3); + process_samples_ExpectWithArrayExtended(expected, 2, 3); function_e(); } - :pass: FALSE - :should: 'detect mismatch in sample subset via ExpectWithArray' + :should: 'detect mismatch in sample subset via ExpectWithArrayExtended' :code: | test() { SAMPLE_U expected[3]; expected[0].raw = 111; expected[1].raw = 999; - process_samples_ExpectWithArray(expected, 2, 3); + process_samples_ExpectWithArrayExtended(expected, 2, 3); function_e(); } @@ -638,12 +649,12 @@ } - :pass: TRUE - :should: 'pass when comparing subset of const void pointer array via ExpectWithArray' + :should: 'pass when comparing subset of const void pointer array via ExpectWithArrayExtended' :code: | test() { void *const expected[] = {(void*)1, (void*)2}; - process_handles_ExpectWithArray(expected, 2, 3); + process_handles_ExpectWithArrayExtended(expected, 2, 3); function_j(); } @@ -696,4 +707,52 @@ function_k(); } + - :pass: TRUE + :should: 'pass using ExpectWithArrayExtended with explicit depth matching buf_size' + :code: | + test() + { + int exp_count = 0; + int exp_data[] = {10, 20, 30}; + store_data_ExpectWithArrayExtended(&exp_count, 1, 3, exp_data, 3); + + function_k(); + } + + - :pass: FALSE + :should: 'detect content mismatch in buf via ExpectWithArrayExtended' + :code: | + test() + { + int exp_count = 0; + int exp_data[] = {10, 99, 30}; + store_data_ExpectWithArrayExtended(&exp_count, 1, 3, exp_data, 3); + + function_k(); + } + + - :pass: TRUE + :should: 'pass using ExpectWithArrayExtended with depth override that skips mismatched last element' + :code: | + test() + { + int exp_count = 0; + int exp_data[] = {10, 20, 99}; + store_data_ExpectWithArrayExtended(&exp_count, 1, 3, exp_data, 2); + + function_k(); + } + + - :pass: FALSE + :should: 'detect mismatch within overridden depth via ExpectWithArrayExtended' + :code: | + test() + { + int exp_count = 0; + int exp_data[] = {10, 99, 30}; + store_data_ExpectWithArrayExtended(&exp_count, 1, 3, exp_data, 2); + + function_k(); + } + ... diff --git a/test/unit/cmock_generator_plugin_array_test.rb b/test/unit/cmock_generator_plugin_array_test.rb index ebea6e4..1b7b416 100644 --- a/test/unit/cmock_generator_plugin_array_test.rb +++ b/test/unit/cmock_generator_plugin_array_test.rb @@ -118,6 +118,42 @@ describe CMockGeneratorPluginArray, "Verify CMockPGeneratorluginArray Module" do assert_equal(expected, returned) end + it "add mock function declarations for functions with size arg after pointer, short macro auto-fills depth" do + function = {:name => "Birch", + :args => [ + { :type => "int*", :name => "sensors", :ptr? => true, :array_size_order => :after, :array_size_name => "count" }, + { :type => "int", :name => "count", :ptr? => false, :array_size? => true } + ], + :return => test_return[:void], + :contains_ptr? => true } + + expected = "#define Birch_ExpectWithArrayAndReturn(sensors, count, cmock_retval) TEST_FAIL_MESSAGE(\"Birch requires _ExpectWithArray (not AndReturn)\");\n" + + "#define Birch_ExpectWithArrayExtendedAndReturn(sensors, sensors_Depth, count, cmock_retval) TEST_FAIL_MESSAGE(\"Birch requires _ExpectWithArrayExtended (not AndReturn)\");\n" + + "#define Birch_ExpectWithArray(sensors, count) Birch_CMockExpectWithArray(__LINE__, sensors, (count), count)\n" + + "#define Birch_ExpectWithArrayExtended(sensors, sensors_Depth, count) Birch_CMockExpectWithArray(__LINE__, sensors, (sensors_Depth), count)\n" + + "void Birch_CMockExpectWithArray(UNITY_LINE_TYPE cmock_line, int* sensors, int sensors_Depth, int count);\n" + returned = @cmock_generator_plugin_array.mock_function_declarations(function) + assert_equal(expected, returned) + end + + it "add mock function declarations for functions with size arg before pointer, short macro auto-fills depth" do + function = {:name => "Willow", + :args => [ + { :type => "int", :name => "buf_size", :ptr? => false, :array_size? => true }, + { :type => "int*", :name => "buf", :ptr? => true, :array_size_order => :before, :array_size_name => "buf_size" } + ], + :return => test_return[:void], + :contains_ptr? => true } + + expected = "#define Willow_ExpectWithArrayAndReturn(buf_size, buf, cmock_retval) TEST_FAIL_MESSAGE(\"Willow requires _ExpectWithArray (not AndReturn)\");\n" + + "#define Willow_ExpectWithArrayExtendedAndReturn(buf_size, buf, buf_Depth, cmock_retval) TEST_FAIL_MESSAGE(\"Willow requires _ExpectWithArrayExtended (not AndReturn)\");\n" + + "#define Willow_ExpectWithArray(buf_size, buf) Willow_CMockExpectWithArray(__LINE__, buf_size, buf, (buf_size))\n" + + "#define Willow_ExpectWithArrayExtended(buf_size, buf, buf_Depth) Willow_CMockExpectWithArray(__LINE__, buf_size, buf, (buf_Depth))\n" + + "void Willow_CMockExpectWithArray(UNITY_LINE_TYPE cmock_line, int buf_size, int* buf, int buf_Depth);\n" + returned = @cmock_generator_plugin_array.mock_function_declarations(function) + assert_equal(expected, returned) + end + it "not have a mock function implementation" do assert(!@cmock_generator_plugin_array.respond_to?(:mock_implementation)) end @@ -146,4 +182,25 @@ describe CMockGeneratorPluginArray, "Verify CMockPGeneratorluginArray Module" do assert_equal(expected, returned) end + it "add mock interfaces with depth override line for :before-paired pointer" do + function = {:name => "Willow", + :args => [ + { :type => "int", :name => "buf_size", :ptr? => false, :array_size? => true }, + { :type => "int*", :name => "buf", :ptr? => true, :array_size_order => :before, :array_size_name => "buf_size" } + ], + :args_string => "int buf_size, int* buf", + :return => test_return[:void], + :contains_ptr? => true } + + expected = ["void Willow_CMockExpectWithArray(UNITY_LINE_TYPE cmock_line, int buf_size, int* buf, int buf_Depth)\n", + "{\n", + "mock_retval_0", + " CMockExpectParameters_Willow(cmock_call_instance, buf_size, buf);\n", + " cmock_call_instance->Expected_buf_Depth = buf_Depth;\n", + "}\n\n" + ].join + returned = @cmock_generator_plugin_array.mock_interfaces(function).join + assert_equal(expected, returned) + end + end