add support for mocking C++ static class member methods

This commit is contained in:
Tuc-An
2020-04-06 11:20:15 -04:00
parent b32e3cd601
commit e06540f3d7
5 changed files with 788 additions and 14 deletions
+27 -4
View File
@@ -65,6 +65,10 @@ class CMockGenerator
@file_writer.create_subdir(@subdir)
end
def create_using_statement(file, function)
file << "using namespace #{function[:namespace].join('::')};\n" unless function[:namespace].empty?
end
def create_mock_header_file(parsed_stuff)
if @include_inline == :include
@file_writer.create_file(@module_name + (@module_ext || '.h'), @subdir) do |file, _filename|
@@ -77,6 +81,7 @@ class CMockGenerator
create_mock_header_service_call_declarations(file)
create_typedefs(file, parsed_stuff[:typedefs])
parsed_stuff[:functions].each do |function|
create_using_statement(file, function)
file << @plugins.run(:mock_function_declarations, function)
end
create_mock_header_footer(file)
@@ -253,15 +258,26 @@ class CMockGenerator
args_string = function[:args_string]
args_string += (', ' + function[:var_arg]) unless function[:var_arg].nil?
# Encapsulate in namespace(s) if applicable
function[:namespace].each do |ns|
file << "namespace #{ns} {\n"
end
# Determine class prefix (if any)
cls_pre = ''
unless function[:class].nil?
cls_pre = "#{function[:class]}::"
end
# Create mock function
unless @weak.empty?
file << "#if defined (__IAR_SYSTEMS_ICC__)\n"
file << "#pragma weak #{function[:name]}\n"
file << "#pragma weak #{function[:unscoped_name]}\n"
file << "#else\n"
file << "#{function_mod_and_rettype} #{function[:name]}(#{args_string}) #{weak};\n"
file << "#{function_mod_and_rettype} #{function[:unscoped_name]}(#{args_string}) #{weak};\n"
file << "#endif\n\n"
end
file << "#{function_mod_and_rettype} #{function[:name]}(#{args_string})\n"
file << "#{function_mod_and_rettype} #{cls_pre}#{function[:unscoped_name]}(#{args_string})\n"
file << "{\n"
file << " UNITY_LINE_TYPE cmock_line = TEST_LINE_NUM;\n"
file << " CMOCK_#{function[:name]}_CALL_INSTANCE* cmock_call_instance;\n"
@@ -280,7 +296,14 @@ class CMockGenerator
file << @plugins.run(:mock_implementation, function)
file << " UNITY_CLR_DETAILS();\n"
file << " return cmock_call_instance->ReturnVal;\n" unless function[:return][:void?]
file << "}\n\n"
file << "}\n"
# Close any namespace(s) opened above
function[:namespace].each do
file << "}\n"
end
file << "\n"
end
def create_mock_interfaces(file, function)
+81 -10
View File
@@ -37,8 +37,10 @@ class CMockHeaderParser
@normalized_source = nil
function_names = []
parse_functions(import_source(source)).map do |decl|
func = parse_declaration(decl)
all_funcs = parse_functions(import_source(source)).map { |item| [item] }
all_funcs += parse_cpp_functions(import_source(source, true))
all_funcs.map do |decl|
func = parse_declaration(*decl)
unless function_names.include? func[:name]
@funcs << func
function_names << func[:name]
@@ -180,7 +182,7 @@ class CMockHeaderParser
source
end
def import_source(source)
def import_source(source, cpp = false)
# let's clean up the encoding in case they've done anything weird with the characters we might find
source = source.force_encoding('ISO-8859-1').encode('utf-8', :replace => nil)
@@ -222,7 +224,10 @@ class CMockHeaderParser
# 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)(?:register|auto|static|restrict)(\W)/, '\1\2') # remove problem keywords
# 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
source.gsub!(/^(?:[\w\s]*\W)?typedef\W[^;]*/m, '') # remove typedef statements
source.gsub!(/\)(\w)/, ') \1') # add space between parenthese and alphanumeric
@@ -234,11 +239,13 @@ class CMockHeaderParser
# scan for functions which return function pointers, because they are a pain
source.gsub!(/([\w\s\*]+)\(*\(\s*\*([\w\s\*]+)\s*\(([\w\s\*,]*)\)\)\s*\(([\w\s\*,]*)\)\)*/) do |_m|
functype = "cmock_#{@module_name}_func_ptr#{@typedefs.size + 1}"
@typedefs << "typedef #{Regexp.last_match(1).strip}(*#{functype})(#{Regexp.last_match(4)});"
"#{functype} #{Regexp.last_match(2).strip}(#{Regexp.last_match(3)});"
unless cpp # only collect once
@typedefs << "typedef #{Regexp.last_match(1).strip}(*#{functype})(#{Regexp.last_match(4)});"
"#{functype} #{Regexp.last_match(2).strip}(#{Regexp.last_match(3)});"
end
end
source = remove_nested_pairs_of_braces(source)
source = remove_nested_pairs_of_braces(source) unless cpp
if @treat_inlines == :include
# Functions having "{ }" at this point are/were inline functions,
@@ -257,7 +264,8 @@ class CMockHeaderParser
source.gsub!(/\s+/, ' ') # remove remaining extra white space
# split lines on semicolons and remove things that are obviously not what we are looking for
src_lines = source.split(/\s*;\s*/).uniq
src_lines = source.split(/\s*;\s*/)
src_lines = src_lines.uniq unless cpp # must retain closing braces for class/namespace
src_lines.delete_if { |line| line.strip.empty? } # remove blank lines
src_lines.delete_if { |line| !(line =~ /[\w\s\*]+\(+\s*\*[\*\s]*[\w\s]+(?:\[[\w\s]*\]\s*)+\)+\s*\((?:[\w\s\*]*,?)*\s*\)/).nil? } # remove function pointer arrays
@@ -272,6 +280,55 @@ class CMockHeaderParser
src_lines.delete_if(&:empty?) # drop empty lines
end
# Rudimentary C++ parser - does not handle all situations - e.g.:
# * A namespace function appears after a class with private members (should be parsed)
# * Anonymous namespace (shouldn't parse anything - no matter how nested - within it)
# * A class nested within another class
def parse_cpp_functions(source)
funcs = []
ns = []
pub = false
source.each do |line|
# Search for namespace, class, opening and closing braces
line.scan(/(?:(?:\b(?:namespace|class)\s+(?:\S+)\s*)?{)|}/).each do |item|
if item == '}'
ns.pop
else
token = item.strip.sub(/\s+/, ' ')
ns << token
pub = false if token.start_with? 'class'
pub = true if token.start_with? 'namespace'
end
end
pub = true if line =~ /public:/
pub = false if line =~ /private:/ || line =~ /protected:/
# ignore non-public and non-static
next unless pub
next unless line =~ /\bstatic\b/
line.sub!(/^.*static/, '')
next unless line =~ @declaration_parse_matcher
tmp = ns.reject { |item| item == '{' }
# Identify class name, if any
cls = nil
if tmp[-1].start_with? 'class '
cls = tmp.pop.sub(/class (\S+) {/, '\1')
end
# Assemble list of namespaces
tmp.each { |item| item.sub!(/(?:namespace|class) (\S+) {/, '\1') }
funcs << [line.strip.gsub(/\s+/, ' '), tmp, cls]
end
funcs
end
def parse_functions(source)
funcs = []
source.each { |line| funcs << line.strip.gsub(/\s+/, ' ') if line =~ @declaration_parse_matcher }
@@ -442,8 +499,10 @@ class CMockHeaderParser
end
end
def parse_declaration(declaration)
def parse_declaration(declaration, namespace = [], classname = nil)
decl = {}
decl[:namespace] = namespace
decl[:class] = classname
regex_match = @declaration_parse_matcher.match(declaration)
raise "Failed parsing function declaration: '#{declaration}'" if regex_match.nil?
@@ -454,7 +513,19 @@ class CMockHeaderParser
# process function attributes, return type, and name
parsed = parse_type_and_name(regex_match[1])
decl[:name] = parsed[:name]
# Record original name without scope prefix
decl[:unscoped_name] = parsed[:name]
# Prefix name with namespace scope (if any) and then class
decl[:name] = namespace.join('_')
unless classname.nil?
decl[:name] << '_' unless decl[:name].empty?
decl[:name] << classname
end
# Add original name to complete fully scoped name
decl[:name] << '_' unless decl[:name].empty?
decl[:name] << decl[:unscoped_name]
decl[:modifier] = parsed[:modifier]
unless parsed[:c_calling_convention].nil?
decl[:c_calling_convention] = parsed[:c_calling_convention]