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