diff --git a/lib/cmock_generator_plugin_expect.rb b/lib/cmock_generator_plugin_expect.rb index 4d311c1..43ba1f7 100644 --- a/lib/cmock_generator_plugin_expect.rb +++ b/lib/cmock_generator_plugin_expect.rb @@ -30,7 +30,7 @@ class CMockGeneratorPluginExpect lines << " #{function[:return][:type]} ReturnVal;\n" unless function[:return][:void?] lines << " int CallOrder;\n" if @ordered function[:args].each do |arg| - lines << " #{arg[:type]} Expected_#{arg[:name]};\n" + lines << " #{arg[:volatile?] ? "volatile #{arg[:type]}" : arg[:type]} Expected_#{arg[:name]};\n" end lines end diff --git a/lib/cmock_generator_plugin_return_thru_ptr.rb b/lib/cmock_generator_plugin_return_thru_ptr.rb index a821826..4ecc983 100644 --- a/lib/cmock_generator_plugin_return_thru_ptr.rb +++ b/lib/cmock_generator_plugin_return_thru_ptr.rb @@ -79,11 +79,12 @@ class CMockGeneratorPluginReturnThruPtr arg_name = arg[:name] next unless @utils.ptr_or_str?(arg[:type]) && !(arg[:const?]) + dest_cast = arg[:volatile?] ? '(void*)(uintptr_t)' : '(void*)' lines << " if (Mock.#{function[:name]}_IgnoreBool && cmock_call_instance != NULL &&\n" lines << " cmock_call_instance->ReturnThruPtr_#{arg_name}_Used)\n" lines << " {\n" lines << " UNITY_TEST_ASSERT_NOT_NULL(#{arg_name}, cmock_line, CMockStringPtrIsNULL);\n" - lines << " CMOCK_MEMCPY((void*)#{arg_name}, (const void*)cmock_call_instance->ReturnThruPtr_#{arg_name}_Val,\n" + lines << " CMOCK_MEMCPY(#{dest_cast}#{arg_name}, (const void*)cmock_call_instance->ReturnThruPtr_#{arg_name}_Val,\n" lines << " cmock_call_instance->ReturnThruPtr_#{arg_name}_Size);\n" lines << " }\n" end @@ -133,10 +134,11 @@ class CMockGeneratorPluginReturnThruPtr arg_name = arg[:name] next unless @utils.ptr_or_str?(arg[:type]) && !(arg[:const?]) + dest_cast = arg[:volatile?] ? '(void*)(uintptr_t)' : '(void*)' lines << " if (cmock_call_instance->ReturnThruPtr_#{arg_name}_Used)\n" lines << " {\n" lines << " UNITY_TEST_ASSERT_NOT_NULL(#{arg_name}, cmock_line, CMockStringPtrIsNULL);\n" - lines << " CMOCK_MEMCPY((void*)#{arg_name}, (const void*)cmock_call_instance->ReturnThruPtr_#{arg_name}_Val,\n" + lines << " CMOCK_MEMCPY(#{dest_cast}#{arg_name}, (const void*)cmock_call_instance->ReturnThruPtr_#{arg_name}_Val,\n" lines << " cmock_call_instance->ReturnThruPtr_#{arg_name}_Size);\n" lines << " }\n" end diff --git a/lib/cmock_generator_utils.rb b/lib/cmock_generator_utils.rb index d20f3e2..d4547f8 100644 --- a/lib/cmock_generator_utils.rb +++ b/lib/cmock_generator_utils.rb @@ -25,12 +25,13 @@ class CMockGeneratorUtils end def self.arg_type_with_const(arg) - # Restore any "const" that was removed in header parsing - if arg[:type].include?('*') - arg[:const_ptr?] ? "#{arg[:type]} const" : arg[:type] - else - arg[:const?] ? "const #{arg[:type]}" : arg[:type] - end + # Restore any "const" or "volatile" that was removed in header parsing + type = if arg[:type].include?('*') + arg[:const_ptr?] ? "#{arg[:type]} const" : arg[:type] + else + arg[:const?] ? "const #{arg[:type]}" : arg[:type] + end + arg[:volatile?] ? "volatile #{type}" : type end def arg_type_with_const(arg) diff --git a/lib/cmock_header_parser.rb b/lib/cmock_header_parser.rb index ef789f8..3a1d670 100644 --- a/lib/cmock_header_parser.rb +++ b/lib/cmock_header_parser.rb @@ -449,6 +449,11 @@ class CMockHeaderParser arg_info.delete(:modifier) # don't care about this arg_info.delete(:c_calling_convention) # don't care about this + # Strip volatile from pointer-to-volatile arg types so internal storage and + # comparisons use the clean type; volatile? flag lets generators reconstruct + # it where needed (e.g. function signatures via arg_type_with_const). + arg_info[:type] = arg_info[:type].gsub(/\bvolatile\s*/, '').gsub(/\s+\*/, '*').strip if arg_info[:volatile?] + arg_info[:array_dims] = array_dims_by_name[arg_info[:name]] if array_dims_by_name.key?(arg_info[:name]) # Handle pointer-to-array args: (*name)[dims] was rewritten to * name before clean_args @@ -542,12 +547,18 @@ class CMockHeaderParser end end + def divine_volatile(arg) + # only flag pointer types where volatile applies to the pointed-to type (before the last *) + arg.include?('*') && (/(^|\s|\*)volatile(\s(\w|\s)*)?\*(?!.*\*)/ =~ arg ? true : false) + end + def divine_ptr_and_const(arg) divination = {} divination[:ptr?] = divine_ptr(arg) divination[:string?] = !divination[:ptr?] && (/(^|\s)(const\s+)?char(\s+const)?\s*\*(?!.*\*)/ =~ arg ? true : false) divination[:const?] = divine_const(arg) + divination[:volatile?] = true if divine_volatile(arg) # an arg containing "const" after the last * is a constant pointer divination[:const_ptr?] = /\*(?!.*\*)\s*const(\s|$)/ =~ arg ? true : false diff --git a/test/system/test_interactions/volatile_pointer_handling.yml b/test/system/test_interactions/volatile_pointer_handling.yml new file mode 100644 index 0000000..a94eec2 --- /dev/null +++ b/test/system/test_interactions/volatile_pointer_handling.yml @@ -0,0 +1,291 @@ +# ========================================================================= +# CMock - Automatic Mock Generation for C +# ThrowTheSwitch.org +# Copyright (c) 2007-26 Mike Karlesky, Mark VanderVoord, & Greg Williams +# SPDX-License-Identifier: MIT +# ========================================================================= + +--- +:cmock: + :mock_path: test/mocks + :mock_prefix: mock_ + :plugins: + - :array + - :cexception + - :ignore + - :callback + - :return_thru_ptr + - :ignore_arg + - :expect_any_args + :callback_after_arg_check: true + :callback_include_count: false + +:systest: + :types: | + typedef struct { + int x; + int y; + } point_t; + + :mockable: | + #include "CException.h" + void update_point(volatile point_t *p); + void update_points(volatile point_t *p, int n); + void update_int(volatile int *v); + int get_value(volatile point_t *p); + void mixed_volatile(int a, volatile int *v); + + :source: + :header: | + #include "CException.h" + + :code: | + + :tests: + :common: | + #include "CException.h" + void setUp(void) {} + void tearDown(void) {} + void my_update_point_callback(volatile point_t *p) { p->x += 1; } + void my_update_int_callback(volatile int *v) { *v = 99; } + void my_update_points_callback(volatile point_t *p, int n) + { + int i; + for (i = 0; i < n; i++) { p[i].x = 100 + i; } + } + + :units: + + # --- expect plugin (base) --- + + - :pass: TRUE + :should: "Expect passes when volatile struct pointer arg matches" + :code: | + test() + { + volatile point_t p = { 1, 2 }; + update_point_Expect(&p); + update_point(&p); + } + + - :pass: FALSE + :should: "Expect fails when volatile struct pointer arg does not match" + :code: | + test() + { + volatile point_t p = { 1, 2 }; + volatile point_t wrong = { 9, 9 }; + update_point_Expect(&wrong); + update_point(&p); + } + + - :pass: TRUE + :should: "ExpectAndReturn works with a volatile struct pointer arg" + :code: | + test() + { + volatile point_t p = { 5, 7 }; + get_value_ExpectAndReturn(&p, 42); + TEST_ASSERT_EQUAL(42, get_value(&p)); + } + + # --- return_thru_ptr plugin --- + + - :pass: TRUE + :should: "ReturnThruPtr writes through a volatile struct pointer arg" + :code: | + test() + { + volatile point_t p = { 0, 0 }; + point_t result = { 10, 20 }; + update_point_Expect(&p); + update_point_ReturnThruPtr_p(&result); + update_point(&p); + TEST_ASSERT_EQUAL(10, p.x); + TEST_ASSERT_EQUAL(20, p.y); + } + + - :pass: TRUE + :should: "ReturnThruPtr writes through a volatile int pointer arg" + :code: | + test() + { + volatile int v = 0; + int result = 42; + update_int_Expect(&v); + update_int_ReturnThruPtr_v(&result); + update_int(&v); + TEST_ASSERT_EQUAL(42, v); + } + + - :pass: TRUE + :should: "ReturnThruPtr macros are defined for volatile pointer args" + :code: | + test() + { + #if !defined(update_point_ReturnThruPtr_p) + TEST_FAIL_MESSAGE("ReturnThruPtr not defined for volatile struct pointer arg."); + #endif + #if !defined(update_int_ReturnThruPtr_v) + TEST_FAIL_MESSAGE("ReturnThruPtr not defined for volatile int pointer arg."); + #endif + } + + # --- array plugin --- + + - :pass: TRUE + :should: "ExpectWithArray passes when all elements of a volatile array match" + :code: | + test() + { + volatile point_t p[3] = { {1,2}, {3,4}, {5,6} }; + point_t expected[3] = { {1,2}, {3,4}, {5,6} }; + update_points_ExpectWithArray(expected, 3, 3); + update_points(p, 3); + } + + - :pass: FALSE + :should: "ExpectWithArray fails when one element of a volatile array does not match" + :code: | + test() + { + volatile point_t p[3] = { {1,2}, {3,4}, {5,6} }; + point_t expected[3] = { {1,2}, {3,4}, {5,9} }; + update_points_ExpectWithArray(expected, 3, 3); + update_points(p, 3); + } + + # --- ignore plugin --- + + - :pass: TRUE + :should: "Ignore suppresses calls to a volatile-arg function" + :code: | + test() + { + volatile point_t p = { 1, 2 }; + update_point_Ignore(); + update_point(&p); + update_point(&p); + } + + - :pass: TRUE + :should: "IgnoreAndReturn suppresses calls and returns value for volatile-arg function" + :code: | + test() + { + volatile point_t p = { 1, 2 }; + get_value_IgnoreAndReturn(77); + TEST_ASSERT_EQUAL(77, get_value(&p)); + TEST_ASSERT_EQUAL(77, get_value(&p)); + } + + # --- ignore_arg plugin --- + + - :pass: TRUE + :should: "IgnoreArg allows any value for the volatile pointer arg" + :code: | + test() + { + volatile int v1 = 10; + volatile int v2 = 20; + mixed_volatile_Expect(5, &v1); + mixed_volatile_IgnoreArg_v(); + mixed_volatile(5, &v2); + } + + - :pass: FALSE + :should: "IgnoreArg ignores volatile arg but still checks other args" + :code: | + test() + { + volatile int v = 10; + mixed_volatile_Expect(5, &v); + mixed_volatile_IgnoreArg_v(); + mixed_volatile(99, &v); + } + + # --- expect_any_args plugin --- + + - :pass: TRUE + :should: "ExpectAnyArgs accepts any volatile array regardless of contents" + :code: | + test() + { + volatile point_t p1[2] = { {1,2}, {3,4} }; + volatile point_t p2[2] = { {9,9}, {8,8} }; + update_points_ExpectAnyArgs(); + update_points_ExpectAnyArgs(); + update_points(p1, 2); + update_points(p2, 2); + } + + - :pass: TRUE + :should: "ExpectAnyArgsAndReturn works with a volatile array arg" + :code: | + test() + { + volatile point_t p[2] = { {1,2}, {3,4} }; + get_value_ExpectAnyArgsAndReturn(55); + TEST_ASSERT_EQUAL(55, get_value(p)); + } + + - :pass: TRUE + :should: "ExpectAnyArgs and ReturnThruPtr can be combined on a volatile pointer arg" + :code: | + test() + { + volatile point_t p = { 0, 0 }; + point_t result = { 7, 8 }; + update_point_ExpectAnyArgs(); + update_point_ReturnThruPtr_p(&result); + update_point(&p); + TEST_ASSERT_EQUAL(7, p.x); + TEST_ASSERT_EQUAL(8, p.y); + } + + # --- callback plugin --- + + - :pass: TRUE + :should: "StubWithCallback can read and write all elements of a volatile array" + :code: | + test() + { + volatile point_t p[3] = { {0,0}, {0,0}, {0,0} }; + point_t expected[3] = { {0,0}, {0,0}, {0,0} }; + update_points_Expect(expected, 3); + update_points_StubWithCallback(my_update_points_callback); + update_points(p, 3); + TEST_ASSERT_EQUAL(100, p[0].x); + TEST_ASSERT_EQUAL(101, p[1].x); + TEST_ASSERT_EQUAL(102, p[2].x); + } + + - :pass: TRUE + :should: "StubWithCallback receives a volatile int pointer and can modify it" + :code: | + test() + { + volatile int v = 0; + update_int_Expect(&v); + update_int_StubWithCallback(my_update_int_callback); + update_int(&v); + TEST_ASSERT_EQUAL(99, v); + } + + # --- cexception plugin --- + + - :pass: TRUE + :should: "ExpectAndThrow throws when a volatile-arg function is called" + :code: | + test() + { + CEXCEPTION_T e = 0; + volatile point_t p = { 1, 2 }; + update_point_ExpectAndThrow(&p, 42); + Try { + update_point(&p); + TEST_FAIL_MESSAGE("Expected exception was not thrown."); + } Catch(e) { + TEST_ASSERT_EQUAL(42, e); + } + } diff --git a/test/unit/cmock_generator_plugin_return_thru_ptr_test.rb b/test/unit/cmock_generator_plugin_return_thru_ptr_test.rb index d38cde2..c6a7a06 100644 --- a/test/unit/cmock_generator_plugin_return_thru_ptr_test.rb +++ b/test/unit/cmock_generator_plugin_return_thru_ptr_test.rb @@ -56,6 +56,17 @@ describe CMockGeneratorPluginReturnThruPtr, "Verify CMockGeneratorPluginReturnTh :return => test_return[:void], :contains_ptr? => true } + # void Cedar(volatile struct foo_obj *foo_handle) + # arg[:type] has volatile stripped at parse time; volatile? flag carries the information + @volatile_ptr_func = {:name => "Cedar", + :args => [{ :type => "struct foo_obj*", + :name => "foo_handle", + :ptr? => true, + :volatile? => true, + }], + :return => test_return[:void], + :contains_ptr? => true } + #no strict ordering @config.expect :plugins, [] @cmock_generator_plugin_return_thru_ptr = CMockGeneratorPluginReturnThruPtr.new(@config, @utils) @@ -82,6 +93,10 @@ describe CMockGeneratorPluginReturnThruPtr, "Verify CMockGeneratorPluginReturnTh @config.expect :treat_as_void, ['MY_FANCY_VOID'] end + def volatile_ptr_func_expect + @utils.expect :ptr_or_str?, true, ['struct foo_obj*'] + end + it "have set up internal priority correctly on init" do assert_equal(9, @cmock_generator_plugin_return_thru_ptr.priority) end @@ -207,6 +222,51 @@ describe CMockGeneratorPluginReturnThruPtr, "Verify CMockGeneratorPluginReturnTh assert_equal(expected, returned) end + it "has no volatile in the Val typedef member for a volatile pointer arg (type is pre-stripped)" do + volatile_ptr_func_expect() + + # arg[:type] = "struct foo_obj*" (volatile stripped at parse time) + # ptr_to_const("struct foo_obj*") => "struct foo_obj const*" + expected = " char ReturnThruPtr_foo_handle_Used;\n" + + " struct foo_obj const* ReturnThruPtr_foo_handle_Val;\n" + + " size_t ReturnThruPtr_foo_handle_Size;\n" + + returned = @cmock_generator_plugin_return_thru_ptr.instance_typedefs(@volatile_ptr_func) + assert_equal(expected, returned) + end + + it "has no volatile in the _CMockReturnMemThruPtr_ declaration for a volatile pointer arg" do + volatile_ptr_func_expect() + + # arg[:type] = "struct foo_obj*" (volatile stripped), so sizeof and param type are clean. + expected = + "#define Cedar_ReturnThruPtr_foo_handle(foo_handle)" + + " Cedar_CMockReturnMemThruPtr_foo_handle(__LINE__, foo_handle, sizeof(struct foo_obj))\n" + + "#define Cedar_ReturnArrayThruPtr_foo_handle(foo_handle, cmock_len)" + + " Cedar_CMockReturnMemThruPtr_foo_handle(__LINE__, foo_handle, (cmock_len * sizeof(*foo_handle)))\n" + + "#define Cedar_ReturnMemThruPtr_foo_handle(foo_handle, cmock_size)" + + " Cedar_CMockReturnMemThruPtr_foo_handle(__LINE__, foo_handle, (cmock_size))\n" + + "void Cedar_CMockReturnMemThruPtr_foo_handle(UNITY_LINE_TYPE cmock_line, struct foo_obj const* foo_handle, size_t cmock_size);\n" + + returned = @cmock_generator_plugin_return_thru_ptr.mock_function_declarations(@volatile_ptr_func) + assert_equal(expected, returned) + end + + it "uses (void*)(uintptr_t) cast in mock_implementation for volatile pointer arg" do + volatile_ptr_func_expect() + + expected = + " if (cmock_call_instance->ReturnThruPtr_foo_handle_Used)\n" + + " {\n" + + " UNITY_TEST_ASSERT_NOT_NULL(foo_handle, cmock_line, CMockStringPtrIsNULL);\n" + + " CMOCK_MEMCPY((void*)(uintptr_t)foo_handle, (const void*)cmock_call_instance->ReturnThruPtr_foo_handle_Val,\n" + + " cmock_call_instance->ReturnThruPtr_foo_handle_Size);\n" + + " }\n" + + returned = @cmock_generator_plugin_return_thru_ptr.mock_implementation(@volatile_ptr_func).join("") + assert_equal(expected, returned) + end + it "converts single pointer type to pointer-to-const via ptr_to_const" do plugin = @cmock_generator_plugin_return_thru_ptr assert_equal("int const*", plugin.ptr_to_const("int*")) diff --git a/test/unit/cmock_header_parser_test.rb b/test/unit/cmock_header_parser_test.rb index 56f5667..b62dac5 100644 --- a/test/unit/cmock_header_parser_test.rb +++ b/test/unit/cmock_header_parser_test.rb @@ -1119,6 +1119,98 @@ describe CMockHeaderParser, "Verify CMockHeaderParser Module" do assert_equal(expected, @parser.parse("module", source)[:functions]) end + it "properly parses volatile pointer argument types" do + + source = "int16_t foo(volatile struct foo_obj *foo_handle, int plain, volatile int not_a_ptr);\n" + + expected = [{ :name => "foo", + :unscoped_name => "foo", + :namespace=>[], + :class=>nil, + :modifier => "", + :return => { :type => "int16_t", + :name => "cmock_to_return", + :str => "int16_t cmock_to_return", + :void? => false, + :ptr? => false, + :const? => false, + :const_ptr? => false + }, + :var_arg => nil, + :args_string => "volatile struct foo_obj* foo_handle, int plain, volatile int not_a_ptr", + :args => [{ :type => "struct foo_obj*", :name => "foo_handle", + :ptr? => true, :string? => false, :const? => false, :const_ptr? => false, :volatile? => true }, + { :type => "int", :name => "plain", + :ptr? => false, :string? => false, :const? => false, :const_ptr? => false }, + { :type => "volatile int", :name => "not_a_ptr", + :ptr? => false, :string? => false, :const? => false, :const_ptr? => false }], + :args_call => "foo_handle, plain, not_a_ptr", + :contains_ptr? => true + }] + assert_equal(expected, @parser.parse("module", source)[:functions]) + end + + it "properly parses T volatile* style volatile pointer argument types" do + # volatile can appear before OR after the base type; both mean "pointer to volatile T" + # e.g. "int volatile*" is identical to "volatile int*" in C + + source = "void bar(int volatile* a, struct foo_obj volatile* b);\n" + + expected = [{ :name => "bar", + :unscoped_name => "bar", + :namespace=>[], + :class=>nil, + :modifier => "", + :return => { :type => "void", + :name => "cmock_to_return", + :str => "void cmock_to_return", + :void? => true, + :ptr? => false, + :const? => false, + :const_ptr? => false + }, + :var_arg => nil, + :args_string => "int volatile* a, struct foo_obj volatile* b", + :args => [{ :type => "int*", :name => "a", + :ptr? => true, :string? => false, :const? => false, :const_ptr? => false, :volatile? => true }, + { :type => "struct foo_obj*", :name => "b", + :ptr? => true, :string? => false, :const? => false, :const_ptr? => false, :volatile? => true }], + :args_call => "a, b", + :contains_ptr? => true + }] + assert_equal(expected, @parser.parse("module", source)[:functions]) + end + + it "treats int*volatile (volatile pointer to non-volatile type) as a plain pointer without volatile? flag" do + # "int * volatile p" = volatile pointer to int (the pointer itself is volatile, not the pointed-to value) + # This is distinct from "volatile int *p" (pointer to volatile int). + # CMock must NOT set volatile? here: the pointed-to type is not volatile, so no cast-qual issue. + + source = "void baz(int * volatile p);\n" + + expected = [{ :name => "baz", + :unscoped_name => "baz", + :namespace=>[], + :class=>nil, + :modifier => "", + :return => { :type => "void", + :name => "cmock_to_return", + :str => "void cmock_to_return", + :void? => true, + :ptr? => false, + :const? => false, + :const_ptr? => false + }, + :var_arg => nil, + :args_string => "int* volatile p", + :args => [{ :type => "int* volatile", :name => "p", + :ptr? => true, :string? => false, :const? => false, :const_ptr? => false }], + :args_call => "p", + :contains_ptr? => true + }] + assert_equal(expected, @parser.parse("module", source)[:functions]) + end + it "converts typedef'd array arguments to pointers" do source = "Book AddToBook(Book book, const IntArray values);\n"