generate_test_runner.rb 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  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. # ==========================================
  6. $QUICK_RUBY_VERSION = RUBY_VERSION.split('.').inject(0){|vv,v| vv * 100 + v.to_i }
  7. File.expand_path(File.join(File.dirname(__FILE__),'colour_prompt'))
  8. class UnityTestRunnerGenerator
  9. def initialize(options = nil)
  10. @options = UnityTestRunnerGenerator.default_options
  11. case(options)
  12. when NilClass then @options
  13. when String then @options.merge!(UnityTestRunnerGenerator.grab_config(options))
  14. when Hash then @options.merge!(options)
  15. else raise "If you specify arguments, it should be a filename or a hash of options"
  16. end
  17. require "#{File.expand_path(File.dirname(__FILE__))}/type_sanitizer"
  18. end
  19. def self.default_options
  20. {
  21. :includes => [],
  22. :defines => [],
  23. :plugins => [],
  24. :framework => :unity,
  25. :test_prefix => "test|spec|should",
  26. :setup_name => "setUp",
  27. :teardown_name => "tearDown",
  28. :main_name => "main", #set to :auto to automatically generate each time
  29. :main_export_decl => "",
  30. :cmdline_args => false,
  31. :use_param_tests => false,
  32. }
  33. end
  34. def self.grab_config(config_file)
  35. options = self.default_options
  36. unless (config_file.nil? or config_file.empty?)
  37. require 'yaml'
  38. yaml_guts = YAML.load_file(config_file)
  39. options.merge!(yaml_guts[:unity] || yaml_guts[:cmock])
  40. raise "No :unity or :cmock section found in #{config_file}" unless options
  41. end
  42. return(options)
  43. end
  44. def run(input_file, output_file, options=nil)
  45. tests = []
  46. testfile_includes = []
  47. used_mocks = []
  48. @options.merge!(options) unless options.nil?
  49. module_name = File.basename(input_file)
  50. #pull required data from source file
  51. source = File.read(input_file)
  52. source = source.force_encoding("ISO-8859-1").encode("utf-8", :replace => nil) if ($QUICK_RUBY_VERSION > 10900)
  53. tests = find_tests(source)
  54. headers = find_includes(source)
  55. testfile_includes = (headers[:local] + headers[:system])
  56. used_mocks = find_mocks(testfile_includes)
  57. testfile_includes = (testfile_includes - used_mocks)
  58. testfile_includes.delete_if{|inc| inc =~ /(unity|cmock)/}
  59. #build runner file
  60. generate(input_file, output_file, tests, used_mocks, testfile_includes)
  61. #determine which files were used to return them
  62. all_files_used = [input_file, output_file]
  63. all_files_used += testfile_includes.map {|filename| filename + '.c'} unless testfile_includes.empty?
  64. all_files_used += @options[:includes] unless @options[:includes].empty?
  65. return all_files_used.uniq
  66. end
  67. def generate(input_file, output_file, tests, used_mocks, testfile_includes)
  68. File.open(output_file, 'w') do |output|
  69. create_header(output, used_mocks, testfile_includes)
  70. create_externs(output, tests, used_mocks)
  71. create_mock_management(output, used_mocks)
  72. create_suite_setup_and_teardown(output)
  73. create_reset(output, used_mocks)
  74. create_main(output, input_file, tests, used_mocks)
  75. end
  76. if (@options[:header_file] && !@options[:header_file].empty?)
  77. File.open(@options[:header_file], 'w') do |output|
  78. create_h_file(output, @options[:header_file], tests, testfile_includes, used_mocks)
  79. end
  80. end
  81. end
  82. def find_tests(source)
  83. tests_and_line_numbers = []
  84. source_scrubbed = source.clone
  85. source_scrubbed = source_scrubbed.gsub(/"[^"\n]*"/, '') # remove things in strings
  86. source_scrubbed = source_scrubbed.gsub(/\/\/.*$/, '') # remove line comments
  87. source_scrubbed = source_scrubbed.gsub(/\/\*.*?\*\//m, '') # remove block comments
  88. lines = source_scrubbed.split(/(^\s*\#.*$) # Treat preprocessor directives as a logical line
  89. | (;|\{|\}) /x) # Match ;, {, and } as end of lines
  90. lines.each_with_index do |line, index|
  91. #find tests
  92. if line =~ /^((?:\s*TEST_CASE\s*\(.*?\)\s*)*)\s*void\s+((?:#{@options[:test_prefix]}).*)\s*\(\s*(.*)\s*\)/
  93. arguments = $1
  94. name = $2
  95. call = $3
  96. params = $4
  97. args = nil
  98. if (@options[:use_param_tests] and !arguments.empty?)
  99. args = []
  100. arguments.scan(/\s*TEST_CASE\s*\((.*)\)\s*$/) {|a| args << a[0]}
  101. end
  102. tests_and_line_numbers << { :test => name, :args => args, :call => call, :params => params, :line_number => 0 }
  103. end
  104. end
  105. tests_and_line_numbers.uniq! {|v| v[:test] }
  106. #determine line numbers and create tests to run
  107. source_lines = source.split("\n")
  108. source_index = 0;
  109. tests_and_line_numbers.size.times do |i|
  110. source_lines[source_index..-1].each_with_index do |line, index|
  111. if (line =~ /#{tests_and_line_numbers[i][:test]}/)
  112. source_index += index
  113. tests_and_line_numbers[i][:line_number] = source_index + 1
  114. break
  115. end
  116. end
  117. end
  118. return tests_and_line_numbers
  119. end
  120. def find_includes(source)
  121. #remove comments (block and line, in three steps to ensure correct precedence)
  122. source.gsub!(/\/\/(?:.+\/\*|\*(?:$|[^\/])).*$/, '') # remove line comments that comment out the start of blocks
  123. source.gsub!(/\/\*.*?\*\//m, '') # remove block comments
  124. source.gsub!(/\/\/.*$/, '') # remove line comments (all that remain)
  125. #parse out includes
  126. includes = {
  127. :local => source.scan(/^\s*#include\s+\"\s*(.+)\.[hH]\s*\"/).flatten,
  128. :system => source.scan(/^\s*#include\s+<\s*(.+)\s*>/).flatten.map { |inc| "<#{inc}>" }
  129. }
  130. return includes
  131. end
  132. def find_mocks(includes)
  133. mock_headers = []
  134. includes.each do |include_path|
  135. include_file = File.basename(include_path)
  136. mock_headers << include_path if (include_file =~ /^mock/i)
  137. end
  138. return mock_headers
  139. end
  140. def create_header(output, mocks, testfile_includes=[])
  141. output.puts('/* AUTOGENERATED FILE. DO NOT EDIT. */')
  142. create_runtest(output, mocks)
  143. output.puts("\n/*=======Automagically Detected Files To Include=====*/")
  144. output.puts("#include \"#{@options[:framework].to_s}.h\"")
  145. output.puts('#include "cmock.h"') unless (mocks.empty?)
  146. output.puts('#include <setjmp.h>')
  147. output.puts('#include <stdio.h>')
  148. output.puts('#include "CException.h"') if @options[:plugins].include?(:cexception)
  149. if (@options[:defines] && !@options[:defines].empty?)
  150. @options[:defines].each {|d| output.puts("#define #{d}")}
  151. end
  152. if (@options[:header_file] && !@options[:header_file].empty?)
  153. output.puts("#include \"#{File.basename(@options[:header_file])}\"")
  154. else
  155. @options[:includes].flatten.uniq.compact.each do |inc|
  156. output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h','')}.h\""}")
  157. end
  158. testfile_includes.each do |inc|
  159. output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h','')}.h\""}")
  160. end
  161. end
  162. mocks.each do |mock|
  163. output.puts("#include \"#{mock.gsub('.h','')}.h\"")
  164. end
  165. if @options[:enforce_strict_ordering]
  166. output.puts('')
  167. output.puts('int GlobalExpectCount;')
  168. output.puts('int GlobalVerifyOrder;')
  169. output.puts('char* GlobalOrderError;')
  170. end
  171. end
  172. def create_externs(output, tests, mocks)
  173. output.puts("\n/*=======External Functions This Runner Calls=====*/")
  174. output.puts("extern void #{@options[:setup_name]}(void);")
  175. output.puts("extern void #{@options[:teardown_name]}(void);")
  176. tests.each do |test|
  177. output.puts("extern void #{test[:test]}(#{test[:call] || 'void'});")
  178. end
  179. output.puts('')
  180. end
  181. def create_mock_management(output, mock_headers)
  182. unless (mock_headers.empty?)
  183. output.puts("\n/*=======Mock Management=====*/")
  184. output.puts("static void CMock_Init(void)")
  185. output.puts("{")
  186. if @options[:enforce_strict_ordering]
  187. output.puts(" GlobalExpectCount = 0;")
  188. output.puts(" GlobalVerifyOrder = 0;")
  189. output.puts(" GlobalOrderError = NULL;")
  190. end
  191. mocks = mock_headers.map {|mock| File.basename(mock)}
  192. mocks.each do |mock|
  193. mock_clean = TypeSanitizer.sanitize_c_identifier(mock)
  194. output.puts(" #{mock_clean}_Init();")
  195. end
  196. output.puts("}\n")
  197. output.puts("static void CMock_Verify(void)")
  198. output.puts("{")
  199. mocks.each do |mock|
  200. mock_clean = TypeSanitizer.sanitize_c_identifier(mock)
  201. output.puts(" #{mock_clean}_Verify();")
  202. end
  203. output.puts("}\n")
  204. output.puts("static void CMock_Destroy(void)")
  205. output.puts("{")
  206. mocks.each do |mock|
  207. mock_clean = TypeSanitizer.sanitize_c_identifier(mock)
  208. output.puts(" #{mock_clean}_Destroy();")
  209. end
  210. output.puts("}\n")
  211. end
  212. end
  213. def create_suite_setup_and_teardown(output)
  214. unless (@options[:suite_setup].nil?)
  215. output.puts("\n/*=======Suite Setup=====*/")
  216. output.puts("static void suite_setup(void)")
  217. output.puts("{")
  218. output.puts(@options[:suite_setup])
  219. output.puts("}")
  220. end
  221. unless (@options[:suite_teardown].nil?)
  222. output.puts("\n/*=======Suite Teardown=====*/")
  223. output.puts("static int suite_teardown(int num_failures)")
  224. output.puts("{")
  225. output.puts(@options[:suite_teardown])
  226. output.puts("}")
  227. end
  228. end
  229. def create_runtest(output, used_mocks)
  230. cexception = @options[:plugins].include? :cexception
  231. va_args1 = @options[:use_param_tests] ? ', ...' : ''
  232. va_args2 = @options[:use_param_tests] ? '__VA_ARGS__' : ''
  233. output.puts("\n/*=======Test Runner Used To Run Each Test Below=====*/")
  234. output.puts("#define RUN_TEST_NO_ARGS") if @options[:use_param_tests]
  235. output.puts("#define RUN_TEST(TestFunc, TestLineNum#{va_args1}) \\")
  236. output.puts("{ \\")
  237. output.puts(" Unity.CurrentTestName = #TestFunc#{va_args2.empty? ? '' : " \"(\" ##{va_args2} \")\""}; \\")
  238. output.puts(" Unity.CurrentTestLineNumber = TestLineNum; \\")
  239. output.puts(" if (UnityTestMatches()) { \\") if (@options[:cmdline_args])
  240. output.puts(" Unity.NumberOfTests++; \\")
  241. output.puts(" CMock_Init(); \\") unless (used_mocks.empty?)
  242. output.puts(" UNITY_CLR_DETAILS(); \\") unless (used_mocks.empty?)
  243. output.puts(" if (TEST_PROTECT()) \\")
  244. output.puts(" { \\")
  245. output.puts(" CEXCEPTION_T e; \\") if cexception
  246. output.puts(" Try { \\") if cexception
  247. output.puts(" #{@options[:setup_name]}(); \\")
  248. output.puts(" TestFunc(#{va_args2}); \\")
  249. output.puts(" } Catch(e) { TEST_ASSERT_EQUAL_HEX32_MESSAGE(CEXCEPTION_NONE, e, \"Unhandled Exception!\"); } \\") if cexception
  250. output.puts(" } \\")
  251. output.puts(" if (TEST_PROTECT()) \\")
  252. output.puts(" { \\")
  253. output.puts(" #{@options[:teardown_name]}(); \\")
  254. output.puts(" CMock_Verify(); \\") unless (used_mocks.empty?)
  255. output.puts(" } \\")
  256. output.puts(" CMock_Destroy(); \\") unless (used_mocks.empty?)
  257. output.puts(" UnityConcludeTest(); \\")
  258. output.puts(" } \\") if (@options[:cmdline_args])
  259. output.puts("}\n")
  260. end
  261. def create_reset(output, used_mocks)
  262. output.puts("\n/*=======Test Reset Option=====*/")
  263. output.puts("void resetTest(void);")
  264. output.puts("void resetTest(void)")
  265. output.puts("{")
  266. output.puts(" CMock_Verify();") unless (used_mocks.empty?)
  267. output.puts(" CMock_Destroy();") unless (used_mocks.empty?)
  268. output.puts(" #{@options[:teardown_name]}();")
  269. output.puts(" CMock_Init();") unless (used_mocks.empty?)
  270. output.puts(" #{@options[:setup_name]}();")
  271. output.puts("}")
  272. end
  273. def create_main(output, filename, tests, used_mocks)
  274. output.puts("\n\n/*=======MAIN=====*/")
  275. main_name = (@options[:main_name].to_sym == :auto) ? "main_#{filename.gsub('.c','')}" : "#{@options[:main_name]}"
  276. if (@options[:cmdline_args])
  277. if (main_name != "main")
  278. output.puts("#{@options[:main_export_decl]} int #{main_name}(int argc, char** argv);")
  279. end
  280. output.puts("#{@options[:main_export_decl]} int #{main_name}(int argc, char** argv)")
  281. output.puts("{")
  282. output.puts(" int parse_status = UnityParseOptions(argc, argv);")
  283. output.puts(" if (parse_status != 0)")
  284. output.puts(" {")
  285. output.puts(" if (parse_status < 0)")
  286. output.puts(" {")
  287. output.puts(" UnityPrint(\"#{filename.gsub('.c','')}.\");")
  288. output.puts(" UNITY_PRINT_EOL();")
  289. if (@options[:use_param_tests])
  290. tests.each do |test|
  291. if ((test[:args].nil?) or (test[:args].empty?))
  292. output.puts(" UnityPrint(\" #{test[:test]}(RUN_TEST_NO_ARGS)\");")
  293. output.puts(" UNITY_PRINT_EOL();")
  294. else
  295. test[:args].each do |args|
  296. output.puts(" UnityPrint(\" #{test[:test]}(#{args})\");")
  297. output.puts(" UNITY_PRINT_EOL();")
  298. end
  299. end
  300. end
  301. else
  302. tests.each { |test| output.puts(" UnityPrint(\" #{test[:test]}\");\n UNITY_PRINT_EOL();")}
  303. end
  304. output.puts(" return 0;")
  305. output.puts(" }")
  306. output.puts(" return parse_status;")
  307. output.puts(" }")
  308. else
  309. if (main_name != "main")
  310. output.puts("#{@options[:main_export_decl]} int #{main_name}(void);")
  311. end
  312. output.puts("int #{main_name}(void)")
  313. output.puts("{")
  314. end
  315. output.puts(" suite_setup();") unless @options[:suite_setup].nil?
  316. output.puts(" UnityBegin(\"#{filename.gsub(/\\/,'\\\\\\')}\");")
  317. if (@options[:use_param_tests])
  318. tests.each do |test|
  319. if ((test[:args].nil?) or (test[:args].empty?))
  320. output.puts(" RUN_TEST(#{test[:test]}, #{test[:line_number]}, RUN_TEST_NO_ARGS);")
  321. else
  322. test[:args].each {|args| output.puts(" RUN_TEST(#{test[:test]}, #{test[:line_number]}, #{args});")}
  323. end
  324. end
  325. else
  326. tests.each { |test| output.puts(" RUN_TEST(#{test[:test]}, #{test[:line_number]});") }
  327. end
  328. output.puts()
  329. output.puts(" CMock_Guts_MemFreeFinal();") unless used_mocks.empty?
  330. output.puts(" return #{@options[:suite_teardown].nil? ? "" : "suite_teardown"}(UnityEnd());")
  331. output.puts("}")
  332. end
  333. def create_h_file(output, filename, tests, testfile_includes, used_mocks)
  334. filename = File.basename(filename).gsub(/[-\/\\\.\,\s]/, "_").upcase
  335. output.puts("/* AUTOGENERATED FILE. DO NOT EDIT. */")
  336. output.puts("#ifndef _#{filename}")
  337. output.puts("#define _#{filename}\n\n")
  338. output.puts("#include \"#{@options[:framework].to_s}.h\"")
  339. output.puts('#include "cmock.h"') unless (used_mocks.empty?)
  340. @options[:includes].flatten.uniq.compact.each do |inc|
  341. output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h','')}.h\""}")
  342. end
  343. testfile_includes.each do |inc|
  344. output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h','')}.h\""}")
  345. end
  346. output.puts "\n"
  347. tests.each do |test|
  348. if ((test[:params].nil?) or (test[:params].empty?))
  349. output.puts("void #{test[:test]}(void);")
  350. else
  351. output.puts("void #{test[:test]}(#{test[:params]});")
  352. end
  353. end
  354. output.puts("#endif\n\n")
  355. end
  356. end
  357. if ($0 == __FILE__)
  358. options = { :includes => [] }
  359. yaml_file = nil
  360. #parse out all the options first (these will all be removed as we go)
  361. ARGV.reject! do |arg|
  362. case(arg)
  363. when '-cexception'
  364. options[:plugins] = [:cexception]; true
  365. when /\.*\.ya?ml/
  366. options = UnityTestRunnerGenerator.grab_config(arg); true
  367. when /--(\w+)=\"?(.*)\"?/
  368. options[$1.to_sym] = $2; true
  369. when /\.*\.h/
  370. options[:includes] << arg; true
  371. else false
  372. end
  373. end
  374. #make sure there is at least one parameter left (the input file)
  375. if !ARGV[0]
  376. puts ["\nusage: ruby #{__FILE__} (files) (options) input_test_file (output)",
  377. "\n input_test_file - this is the C file you want to create a runner for",
  378. " output - this is the name of the runner file to generate",
  379. " defaults to (input_test_file)_Runner",
  380. " files:",
  381. " *.yml / *.yaml - loads configuration from here in :unity or :cmock",
  382. " *.h - header files are added as #includes in runner",
  383. " options:",
  384. " -cexception - include cexception support",
  385. " --setup_name=\"\" - redefine setUp func name to something else",
  386. " --teardown_name=\"\" - redefine tearDown func name to something else",
  387. " --main_name=\"\" - redefine main func name to something else",
  388. " --test_prefix=\"\" - redefine test prefix from default test|spec|should",
  389. " --suite_setup=\"\" - code to execute for setup of entire suite",
  390. " --suite_teardown=\"\" - code to execute for teardown of entire suite",
  391. " --use_param_tests=1 - enable parameterized tests (disabled by default)",
  392. " --header_file=\"\" - path/name of test header file to generate too"
  393. ].join("\n")
  394. exit 1
  395. end
  396. #create the default test runner name if not specified
  397. ARGV[1] = ARGV[0].gsub(".c","_Runner.c") if (!ARGV[1])
  398. UnityTestRunnerGenerator.new(options).run(ARGV[0], ARGV[1])
  399. end