Finish better identification of array pointer/len pairs.

Add fallback _ExpectWithArrayExtended when this happens for flexibility.
Fixes issue #520
This commit is contained in:
Mark VanderVoord
2026-06-18 16:34:24 -04:00
parent 8c1f88fb82
commit aaf9559a57
4 changed files with 237 additions and 32 deletions
+46 -7
View File
@@ -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
+61 -11
View File
@@ -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
@@ -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();
}
...
@@ -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