Improve handling of volatile keyword (Fixes #110 and #135)

This commit is contained in:
Mark VanderVoord
2026-06-29 16:02:54 -04:00
parent 3541b31c56
commit a2ac5c2c63
7 changed files with 466 additions and 9 deletions
+1 -1
View File
@@ -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
@@ -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
+7 -6
View File
@@ -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)
+11
View File
@@ -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
@@ -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);
}
}
@@ -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*"))
+92
View File
@@ -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"