5 Commits

Author SHA1 Message Date
Mark VanderVoord d482f56066 Protect against more function-looking macros (Fixes#502) 2026-06-19 15:58:08 -04:00
Mark VanderVoord 1ef72ca854 Protect against function-looking structs (Fixes #513) 2026-06-19 15:10:14 -04:00
Mark VanderVoord 85058d1407 Fixed skeleton path handling, etc (#488) 2026-06-19 14:42:09 -04:00
Mark VanderVoord 411f6852f9 Add documentation to avoid problems like #518 and #521. 2026-06-19 13:50:21 -04:00
Mark VanderVoord 383c43246c Merge pull request #531 from ThrowTheSwitch/feature/better_arrays
Feature/better arrays
2026-06-19 10:53:54 -04:00
6 changed files with 123 additions and 21 deletions
+17
View File
@@ -378,6 +378,23 @@ generate a skeleton instead:
ruby cmock.rb --skeleton ../create/c/for/this.h
```
Using CMock Without Ceedling
----------------------------
CMock depends on the Unity test framework, but it does not *require* Ceedling. You can use the
generated mocks directly with the Unity test framework in whatever build system you prefer. One
important thing to remember when doing this is that you will need to call the `_Init` function
for each of your mocks BEFORE the tests and the `_Verify` function for each mock AFTER each test.
This allows CMock to perform all of its internal accounting. If you're running into problems where
some errors aren't getting caught, this is likely what you are missing.
There are many ways to accomplish this. Any is valid:
- These actions can be performed as part of `setUp` and `tearDown` in each test file
- You can hand-write your own RUN_TEST macro. If so, protect `_Verify` calls in `TEST_PROTECT`
- You can use Unity's test runner generator and it will automatically take care of this for you.
- You can use Ceedling and it will automatically take care of this for you.
Config Options:
---------------
+37
View File
@@ -16,6 +16,17 @@ class CMockFileWriter
require 'fileutils'
FileUtils.mkdir_p "#{@config.mock_path}/" unless Dir.exist?("#{@config.mock_path}/")
FileUtils.mkdir_p "#{@config.mock_path}/#{"#{subdir}/" if subdir}" if subdir && !Dir.exist?("#{@config.mock_path}/#{"#{subdir}/" if subdir}")
rescue SystemCallError => e
raise "Unable to create mock output directory: #{e.message}. Check :mock_path ('#{@config.mock_path}') configuration."
end
def create_skeleton_subdir(subdir)
require 'fileutils'
base = effective_skeleton_path
FileUtils.mkdir_p base
FileUtils.mkdir_p "#{base}/#{subdir}" if subdir
rescue SystemCallError => e
raise "Unable to create skeleton output directory: #{e.message}. Check :skeleton_path ('#{base}') configuration."
end
def create_file(filename, subdir)
@@ -27,6 +38,27 @@ class CMockFileWriter
yield(file, filename)
end
update_file(full_file_name_done, full_file_name_temp)
rescue SystemCallError => e
raise "Unable to write mock file '#{full_file_name_done}': #{e.message}. Check :mock_path ('#{@config.mock_path}') and :subdir ('#{subdir}') configuration."
end
def create_skeleton_file(filename, subdir)
raise "Where's the block of data to create?" unless block_given?
base = effective_skeleton_path
full_file_name_temp = "#{base}/#{"#{subdir}/" if subdir}#{filename}.new"
full_file_name_done = "#{base}/#{"#{subdir}/" if subdir}#{filename}"
File.open(full_file_name_temp, 'w') do |file|
yield(file, filename)
end
update_file(full_file_name_done, full_file_name_temp)
rescue SystemCallError => e
raise "Unable to write skeleton file '#{full_file_name_done}': #{e.message}. Check :skeleton_path ('#{base}') and :subdir ('#{subdir}') configuration."
end
def skeleton_file_path(filename, subdir)
base = effective_skeleton_path
"#{base}/#{"#{subdir}/" if subdir}#{filename}"
end
def append_file(filename, subdir)
@@ -40,6 +72,11 @@ class CMockFileWriter
private ###################################
def effective_skeleton_path
path = @config.skeleton_path
path.nil? || path.empty? ? @config.mock_path : path
end
def update_file(dest, src)
require 'fileutils'
FileUtils.rm(dest, :force => true)
+27 -20
View File
@@ -85,6 +85,7 @@ class CMockGenerator
:skeleton => true
}
@file_writer.create_skeleton_subdir(@subdir)
create_skeleton_source_file(mock_project)
end
@@ -133,9 +134,9 @@ class CMockGenerator
end
def create_skeleton_source_file(mock_project)
filename = "#{@config.mock_path}/#{"#{@subdir}/" if @subdir}#{mock_project[:module_name]}.c"
filename = @file_writer.skeleton_file_path("#{mock_project[:module_name]}.c", @subdir)
existing = File.exist?(filename) ? File.read(filename) : ''
@file_writer.create_file("#{mock_project[:module_name]}.c", @subdir) do |file, fullname|
@file_writer.create_skeleton_file("#{mock_project[:module_name]}.c", @subdir) do |file, fullname|
blank_project = mock_project.clone
blank_project[:parsed_stuff] = { :functions => [] }
if existing.empty?
@@ -210,24 +211,30 @@ class CMockGenerator
def create_source_header_section(file, filename, mock_project)
header_file = (mock_project[:folder] || '') + filename.sub(/.*\K\.c/, mock_project[:module_ext])
file << "/* AUTOGENERATED FILE. DO NOT EDIT. */\n" unless mock_project[:parsed_stuff][:functions].empty?
file << "#include <string.h>\n"
file << "#include <stdlib.h>\n"
unless @exclude_setjmp_h
file << "#include <setjmp.h>\n"
end
file << "#include \"cmock.h\"\n"
@includes_c_pre_header.each { |inc| file << "#include #{inc}\n" }
file << "#include \"#{header_file}\"\n"
@includes_c_post_header.each { |inc| file << "#include #{inc}\n" }
file << "\n"
strs = []
mock_project[:parsed_stuff][:functions].each do |func|
strs << func[:name]
func[:args].each { |arg| strs << arg[:name] }
end
strs.uniq.sort.each do |str|
file << "static const char* CMockString_#{str} = \"#{str}\";\n"
if mock_project[:skeleton]
@includes_c_pre_header.each { |inc| file << "#include #{inc}\n" }
file << "#include \"#{header_file}\"\n"
@includes_c_post_header.each { |inc| file << "#include #{inc}\n" }
else
file << "/* AUTOGENERATED FILE. DO NOT EDIT. */\n" unless mock_project[:parsed_stuff][:functions].empty?
file << "#include <string.h>\n"
file << "#include <stdlib.h>\n"
unless @exclude_setjmp_h
file << "#include <setjmp.h>\n"
end
file << "#include \"cmock.h\"\n"
@includes_c_pre_header.each { |inc| file << "#include #{inc}\n" }
file << "#include \"#{header_file}\"\n"
@includes_c_post_header.each { |inc| file << "#include #{inc}\n" }
file << "\n"
strs = []
mock_project[:parsed_stuff][:functions].each do |func|
strs << func[:name]
func[:args].each { |arg| strs << arg[:name] }
end
strs.uniq.sort.each do |str|
file << "static const char* CMockString_#{str} = \"#{str}\";\n"
end
end
file << "\n"
end
+13 -1
View File
@@ -253,12 +253,23 @@ class CMockHeaderParser
# enums, unions, structs, and typedefs can all contain things (e.g. function pointers) that parse like function prototypes, so yank them
# forward declared structs are removed before struct definitions so they don't mess up real thing later. we leave structs keywords in function prototypes
source.gsub!(/^[\w\s]*struct[^;{}()]+;/m, '') # remove forward declared structs
source.gsub!(/^[\w\s]*(enum|union|struct|typedef)[\w\s]*\{[^}]+\}[\w\s*,]*;/m, '') # remove struct, union, and enum definitions and typedefs with braces
source.gsub!(/^[\w\s]*(enum|union|struct|typedef)[\w\s()]*\{[^}]+\}[\w\s*,]*;/m, '') # remove struct, union, and enum definitions and typedefs with braces
# remove problem keywords
source.gsub!(/(\W)(?:register|auto|restrict)(\W)/, '\1\2')
source.gsub!(/(\W)(?:static)(\W)/, '\1\2') unless cpp
source.gsub!(/\s*=\s*['"a-zA-Z0-9_.]+\s*/, '') # remove default value statements from argument lists
# strip macro decorator patterns that cannot be C function prototypes.
# must run after default-value removal so "= \"str\"" doesn't trigger string detection.
# neutralize string literals (never valid in C prototype args), eliminating any parentheses
# inside string content (e.g. "msg()") that would fool subsequent brace-matching.
source.gsub!(/"[^"]*"/, '""')
# strip WORD("") -- any call with a string literal arg cannot be a C function prototype
source.gsub!(/\b\w+\s*\([^)]*""[^)]*\)/, '')
# strip WORD(N...) -- any call whose first arg starts with a digit cannot be a C prototype
source.gsub!(/\b\w+\s*\(\s*\d[^)]*\)/, '')
source.gsub!(/^(?:[\w\s]*\W)?typedef\W[^;]*/m, '') # remove typedef statements
source.gsub!(/\)(\w)/, ') \1') # add space between parenthese and alphanumeric
source.gsub!(/(^|\W+)(?:#{@c_strippables.join('|')})(?=$|\W+)/, '\1') unless @c_strippables.empty? # remove known attributes slated to be stripped
@@ -636,6 +647,7 @@ class CMockHeaderParser
rettype = parsed[:type]
rettype = 'void' if @local_as_void.include?(rettype.strip)
rettype = 'void' if rettype.empty? && !(@standards + @local_as_void).include?(parsed[:name]) # all return-type tokens were stripped (e.g. bare decorator macros)
retstr = parsed[:const_ptr?] ? "#{rettype} const" : rettype
decl[:return] = { :type => rettype,
:name => 'cmock_to_return',
+3
View File
@@ -15,3 +15,6 @@
:treat_as_void:
- OSEK_TASK
- VOID_TYPE_CRAZINESS
:strippables:
- SAMPLE_EXTERN
- SAMPLE_MODE
+26
View File
@@ -18,6 +18,16 @@ typedef struct _POINT_T
int y;
} POINT_T;
/* The comments in the following enum are important, as are the newlines and commas */
/* This combination caused a curious bug when used together. Make sure it doesn't come back. */
typedef enum
{
MY_ERROR_ID = -18,/**< Driver not ready */
MY_OTHER_ID = -19 /**< Node-id is in LSS unconfigured
state. If objects are handled properly,
his may not be an error. */
} MY_STATE_ID_T;
/* typedef edge case;
not ANSI C but it has been done and will break cmock if not handled */
typedef void VOID_TYPE_CRAZINESS;
@@ -101,3 +111,19 @@ inline int stuff(int num)
}
return reg;
}
/* it seems like CMock didn't love a func-looking struct before a func */
#define FOO_TYPE(a) foo_##a
struct FOO_TYPE(bar) { int baz; };
char b(void);
/* Here are more macros that sorta look like functions. Only sample_func is real */
#define SAMPLE_EXTERN
#define SAMPLE_MODE
#define SAMPLE_DEPRECATED(a,b)
struct struct_a;
struct struct_b;
SAMPLE_EXTERN SAMPLE_MODE SAMPLE_DEPRECATED(1.1.2, "It was bad. real bad()")
void sample_func(struct struct_a **a,
struct struct_b **b,
...);