1 # ==========================================
2 # Unity Project - A Test Framework for C
3 # Copyright (c) 2007 Mike Karlesky, Mark VanderVoord, Greg Williams
4 # [Released under MIT License. Please refer to license.txt for details]
5 # ==========================================
7 $QUICK_RUBY_VERSION = RUBY_VERSION.split('.').inject(0){|vv,v| vv * 100 + v.to_i }
8 File.expand_path(File.join(File.dirname(__FILE__),'colour_prompt'))
10 class UnityTestRunnerGenerator
12 def initialize(options = nil)
13 @options = UnityTestRunnerGenerator.default_options
16 when NilClass then @options
17 when String then @options.merge!(UnityTestRunnerGenerator.grab_config(options))
18 when Hash then @options.merge!(options)
19 else raise "If you specify arguments, it should be a filename or a hash of options"
21 require "#{File.expand_path(File.dirname(__FILE__))}/type_sanitizer"
24 def self.default_options
29 :test_prefix => "test|spec|should",
30 :setup_name => "setUp",
31 :teardown_name => "tearDown",
36 def self.grab_config(config_file)
37 options = self.default_options
39 unless (config_file.nil? or config_file.empty?)
41 yaml_guts = YAML.load_file(config_file)
42 options.merge!(yaml_guts[:unity] || yaml_guts[:cmock])
43 raise "No :unity or :cmock section found in #{config_file}" unless options
48 def run(input_file, output_file, options=nil)
50 testfile_includes = []
54 @options.merge!(options) unless options.nil?
55 module_name = File.basename(input_file)
58 #pull required data from source file
59 source = File.read(input_file)
60 source = source.force_encoding("ISO-8859-1").encode("utf-8", :replace => nil) if ($QUICK_RUBY_VERSION > 10900)
61 tests = find_tests(source)
62 headers = find_includes(source)
63 testfile_includes = headers[:local] + headers[:system]
64 used_mocks = find_mocks(testfile_includes)
68 generate(input_file, output_file, tests, used_mocks, testfile_includes)
70 #determine which files were used to return them
71 all_files_used = [input_file, output_file]
72 all_files_used += testfile_includes.map {|filename| filename + '.c'} unless testfile_includes.empty?
73 all_files_used += @options[:includes] unless @options[:includes].empty?
74 return all_files_used.uniq
77 def generate(input_file, output_file, tests, used_mocks, testfile_includes)
78 File.open(output_file, 'w') do |output|
79 create_header(output, used_mocks, testfile_includes)
80 create_externs(output, tests, used_mocks)
81 create_mock_management(output, used_mocks)
82 create_suite_setup_and_teardown(output)
83 create_reset(output, used_mocks)
84 create_main(output, input_file, tests, used_mocks)
94 def find_tests(source)
97 tests_and_line_numbers = []
102 source_scrubbed = source.gsub(/\/\/.*$/, '') # remove line comments
103 source_scrubbed = source_scrubbed.gsub(/\/\*.*?\*\//m, '') # remove block comments
104 lines = source_scrubbed.split(/(^\s*\#.*$) # Treat preprocessor directives as a logical line
105 | (;|\{|\}) /x) # Match ;, {, and } as end of lines
107 lines.each_with_index do |line, index|
109 if line =~ /^((?:\s*TEST_CASE\s*\(.*?\)\s*)*)\s*void\s+((?:#{@options[:test_prefix]}).*)\s*\(\s*(.*)\s*\)/
114 if (@options[:use_param_tests] and !arguments.empty?)
116 arguments.scan(/\s*TEST_CASE\s*\((.*)\)\s*$/) {|a| args << a[0]}
118 tests_and_line_numbers << { :test => name, :args => args, :call => call, :line_number => 0 }
122 tests_and_line_numbers.uniq! {|v| v[:test] }
124 #determine line numbers and create tests to run
125 source_lines = source.split("\n")
127 tests_and_line_numbers.size.times do |i|
128 source_lines[source_index..-1].each_with_index do |line, index|
129 if (line =~ /#{tests_and_line_numbers[i][:test]}/)
130 source_index += index
131 tests_and_line_numbers[i][:line_number] = source_index + 1
138 return tests_and_line_numbers
141 def find_includes(source)
143 #remove comments (block and line, in three steps to ensure correct precedence)
144 source.gsub!(/\/\/(?:.+\/\*|\*(?:$|[^\/])).*$/, '') # remove line comments that comment out the start of blocks
145 source.gsub!(/\/\*.*?\*\//m, '') # remove block comments
146 source.gsub!(/\/\/.*$/, '') # remove line comments (all that remain)
152 :local => source.scan(/^\s*#include\s+\"\s*(.+)\.[hH]\s*\"/).flatten,
153 :system => source.scan(/^\s*#include\s+<\s*(.+)\s*>/).flatten.map { |inc| "<#{inc}>" }
161 def find_mocks(includes)
163 includes.each do |include_file|
164 mock_headers << File.basename(include_file) if (include_file =~ /^mock/i)
170 def create_header(output, mocks, testfile_includes=[])
171 output.puts('/* AUTOGENERATED FILE. DO NOT EDIT. */')
172 create_runtest(output, mocks)
173 output.puts("\n//=======Automagically Detected Files To Include=====")
174 output.puts("#include \"#{@options[:framework].to_s}.h\"")
175 output.puts('#include "cmock.h"') unless (mocks.empty?)
176 @options[:includes].flatten.uniq.compact.each do |inc|
177 output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h','')}.h\""}")
179 output.puts('#include <setjmp.h>')
180 output.puts('#include <stdio.h>')
181 output.puts('#include "CException.h"') if @options[:plugins].include?(:cexception)
182 testfile_includes.delete_if{|inc| inc =~ /(unity|cmock)/}
183 testrunner_includes = testfile_includes - mocks
184 testrunner_includes.each do |inc|
185 output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h','')}.h\""}")
188 output.puts("#include \"#{mock.gsub('.h','')}.h\"")
190 if @options[:enforce_strict_ordering]
192 output.puts('int GlobalExpectCount;')
193 output.puts('int GlobalVerifyOrder;')
194 output.puts('char* GlobalOrderError;')
199 def create_externs(output, tests, mocks)
200 output.puts("\n//=======External Functions This Runner Calls=====")
201 output.puts("extern void #{@options[:setup_name]}(void);")
202 output.puts("extern void #{@options[:teardown_name]}(void);")
205 output.puts("extern void #{test[:test]}(#{test[:call] || 'void'});")
211 def create_mock_management(output, mocks)
212 unless (mocks.empty?)
213 output.puts("\n//=======Mock Management=====")
214 output.puts("static void CMock_Init(void)")
216 if @options[:enforce_strict_ordering]
217 output.puts(" GlobalExpectCount = 0;")
218 output.puts(" GlobalVerifyOrder = 0;")
219 output.puts(" GlobalOrderError = NULL;")
222 mock_clean = TypeSanitizer.sanitize_c_identifier(mock)
223 output.puts(" #{mock_clean}_Init();")
227 output.puts("static void CMock_Verify(void)")
230 mock_clean = TypeSanitizer.sanitize_c_identifier(mock)
231 output.puts(" #{mock_clean}_Verify();")
235 output.puts("static void CMock_Destroy(void)")
238 mock_clean = TypeSanitizer.sanitize_c_identifier(mock)
239 output.puts(" #{mock_clean}_Destroy();")
246 def create_suite_setup_and_teardown(output)
247 unless (@options[:suite_setup].nil?)
248 output.puts("\n//=======Suite Setup=====")
249 output.puts("static void suite_setup(void)")
251 output.puts(@options[:suite_setup])
254 unless (@options[:suite_teardown].nil?)
255 output.puts("\n//=======Suite Teardown=====")
256 output.puts("static int suite_teardown(int num_failures)")
258 output.puts(@options[:suite_teardown])
264 def create_runtest(output, used_mocks)
265 cexception = @options[:plugins].include? :cexception
266 va_args1 = @options[:use_param_tests] ? ', ...' : ''
267 va_args2 = @options[:use_param_tests] ? '__VA_ARGS__' : ''
268 output.puts("\n//=======Test Runner Used To Run Each Test Below=====")
269 output.puts("#define RUN_TEST_NO_ARGS") if @options[:use_param_tests]
270 output.puts("#define RUN_TEST(TestFunc, TestLineNum#{va_args1}) \\")
272 output.puts(" Unity.CurrentTestName = #TestFunc#{va_args2.empty? ? '' : " \"(\" ##{va_args2} \")\""}; \\")
273 output.puts(" Unity.CurrentTestLineNumber = TestLineNum; \\")
274 output.puts(" Unity.NumberOfTests++; \\")
275 output.puts(" CMock_Init(); \\") unless (used_mocks.empty?)
276 output.puts(" if (TEST_PROTECT()) \\")
278 output.puts(" CEXCEPTION_T e; \\") if cexception
279 output.puts(" Try { \\") if cexception
280 output.puts(" #{@options[:setup_name]}(); \\")
283 output.puts(" TestFunc(#{va_args2}); \\")
285 output.puts(" } Catch(e) { TEST_ASSERT_EQUAL_HEX32_MESSAGE(CEXCEPTION_NONE, e, \"Unhandled Exception!\"); } \\") if cexception
288 output.puts(" if (TEST_PROTECT() && !TEST_IS_IGNORED) \\")
290 output.puts(" #{@options[:teardown_name]}(); \\")
291 output.puts(" CMock_Verify(); \\") unless (used_mocks.empty?)
294 output.puts(" CMock_Destroy(); \\") unless (used_mocks.empty?)
295 output.puts(" UnityConcludeTest(); \\")
300 def create_reset(output, used_mocks)
301 output.puts("\n//=======Test Reset Option=====")
302 output.puts("void resetTest(void);")
303 output.puts("void resetTest(void)")
306 output.puts(" CMock_Verify();") unless (used_mocks.empty?)
307 output.puts(" CMock_Destroy();") unless (used_mocks.empty?)
308 output.puts(" #{@options[:teardown_name]}();")
310 output.puts(" CMock_Init();") unless (used_mocks.empty?)
311 output.puts(" #{@options[:setup_name]}();")
317 def create_main(output, filename, tests, used_mocks)
318 output.puts("\nchar const *progname;\n")
319 output.puts("\n\n//=======MAIN=====")
321 output.puts("int main(int argc, char *argv[])")
323 output.puts(" progname = argv[0];\n")
326 modname = filename.split(/[\/\\]/).last
330 output.puts(" suite_setup();") unless @options[:suite_setup].nil?
332 output.puts(" UnityBegin(\"#{modname}\");")
334 if (@options[:use_param_tests])
336 if ((test[:args].nil?) or (test[:args].empty?))
337 output.puts(" RUN_TEST(#{test[:test]}, #{test[:line_number]}, RUN_TEST_NO_ARGS);")
339 test[:args].each {|args| output.puts(" RUN_TEST(#{test[:test]}, #{test[:line_number]}, #{args});")}
343 tests.each { |test| output.puts(" RUN_TEST(#{test[:test]}, #{test[:line_number]});") }
346 output.puts(" CMock_Guts_MemFreeFinal();") unless used_mocks.empty?
347 output.puts(" return #{@options[:suite_teardown].nil? ? "" : "suite_teardown"}(UnityEnd());")
354 options = { :includes => [] }
358 #parse out all the options first (these will all be removed as we go)
359 ARGV.reject! do |arg|
362 options[:plugins] = [:cexception]; true
365 options = UnityTestRunnerGenerator.grab_config(arg); true
367 options[:includes] << arg; true
368 when /--(\w+)=\"?(.*)\"?/
369 options[$1.to_sym] = $2; true
375 #make sure there is at least one parameter left (the input file)
377 puts ["\nusage: ruby #{__FILE__} (files) (options) input_test_file (output)",
378 "\n input_test_file - this is the C file you want to create a runner for",
379 " output - this is the name of the runner file to generate",
380 " defaults to (input_test_file)_Runner",
382 " *.yml / *.yaml - loads configuration from here in :unity or :cmock",
383 " *.h - header files are added as #includes in runner",
386 " -cexception - include cexception support",
387 " --setup_name=\"\" - redefine setUp func name to something else",
388 " --teardown_name=\"\" - redefine tearDown func name to something else",
389 " --test_prefix=\"\" - redefine test prefix from default test|spec|should",
390 " --suite_setup=\"\" - code to execute for setup of entire suite",
391 " --suite_teardown=\"\" - code to execute for teardown of entire suite",
392 " --use_param_tests=1 - enable parameterized tests (disabled by default)",
398 #create the default test runner name if not specified
399 ARGV[1] = ARGV[0].gsub(".c","_Runner.c") if (!ARGV[1])
406 UnityTestRunnerGenerator.new(options).run(ARGV[0], ARGV[1])