1284990Scy# ========================================== 2284990Scy# Unity Project - A Test Framework for C 3284990Scy# Copyright (c) 2007 Mike Karlesky, Mark VanderVoord, Greg Williams 4284990Scy# [Released under MIT License. Please refer to license.txt for details] 5289764Sglebius# ========================================== 6284990Scy 7289764Sglebius$QUICK_RUBY_VERSION = RUBY_VERSION.split('.').inject(0){|vv,v| vv * 100 + v.to_i } 8284990ScyFile.expand_path(File.join(File.dirname(__FILE__),'colour_prompt')) 9284990Scy 10284990Scyclass UnityTestRunnerGenerator 11284990Scy 12284990Scy def initialize(options = nil) 13289764Sglebius @options = UnityTestRunnerGenerator.default_options 14289764Sglebius 15284990Scy case(options) 16284990Scy when NilClass then @options 17284990Scy when String then @options.merge!(UnityTestRunnerGenerator.grab_config(options)) 18284990Scy when Hash then @options.merge!(options) 19284990Scy else raise "If you specify arguments, it should be a filename or a hash of options" 20284990Scy end 21289764Sglebius require "#{File.expand_path(File.dirname(__FILE__))}/type_sanitizer" 22284990Scy end 23289764Sglebius 24289764Sglebius def self.default_options 25289764Sglebius { 26289764Sglebius :includes => [], 27289764Sglebius :plugins => [], 28289764Sglebius :framework => :unity, 29289764Sglebius :test_prefix => "test|spec|should", 30289764Sglebius :setup_name => "setUp", 31289764Sglebius :teardown_name => "tearDown", 32289764Sglebius } 33289764Sglebius end 34289764Sglebius 35289764Sglebius 36284990Scy def self.grab_config(config_file) 37289764Sglebius options = self.default_options 38289764Sglebius 39284990Scy unless (config_file.nil? or config_file.empty?) 40284990Scy require 'yaml' 41284990Scy yaml_guts = YAML.load_file(config_file) 42289764Sglebius options.merge!(yaml_guts[:unity] || yaml_guts[:cmock]) 43284990Scy raise "No :unity or :cmock section found in #{config_file}" unless options 44284990Scy end 45284990Scy return(options) 46284990Scy end 47284990Scy 48284990Scy def run(input_file, output_file, options=nil) 49284990Scy tests = [] 50289764Sglebius testfile_includes = [] 51284990Scy used_mocks = [] 52289764Sglebius 53289764Sglebius 54284990Scy @options.merge!(options) unless options.nil? 55284990Scy module_name = File.basename(input_file) 56289764Sglebius 57289764Sglebius 58284990Scy #pull required data from source file 59289764Sglebius source = File.read(input_file) 60289764Sglebius source = source.force_encoding("ISO-8859-1").encode("utf-8", :replace => nil) if ($QUICK_RUBY_VERSION > 10900) 61289764Sglebius tests = find_tests(source) 62289764Sglebius headers = find_includes(source) 63289764Sglebius testfile_includes = headers[:local] + headers[:system] 64289764Sglebius used_mocks = find_mocks(testfile_includes) 65284990Scy 66289764Sglebius 67284990Scy #build runner file 68289764Sglebius generate(input_file, output_file, tests, used_mocks, testfile_includes) 69289764Sglebius 70289764Sglebius #determine which files were used to return them 71289764Sglebius all_files_used = [input_file, output_file] 72289764Sglebius all_files_used += testfile_includes.map {|filename| filename + '.c'} unless testfile_includes.empty? 73289764Sglebius all_files_used += @options[:includes] unless @options[:includes].empty? 74289764Sglebius return all_files_used.uniq 75289764Sglebius end 76289764Sglebius 77289764Sglebius def generate(input_file, output_file, tests, used_mocks, testfile_includes) 78284990Scy File.open(output_file, 'w') do |output| 79289764Sglebius create_header(output, used_mocks, testfile_includes) 80284990Scy create_externs(output, tests, used_mocks) 81284990Scy create_mock_management(output, used_mocks) 82284990Scy create_suite_setup_and_teardown(output) 83284990Scy create_reset(output, used_mocks) 84289764Sglebius create_main(output, input_file, tests, used_mocks) 85284990Scy end 86289764Sglebius 87289764Sglebius 88289764Sglebius 89289764Sglebius 90289764Sglebius 91284990Scy end 92289764Sglebius 93289764Sglebius 94289764Sglebius def find_tests(source) 95289764Sglebius 96289764Sglebius 97284990Scy tests_and_line_numbers = [] 98289764Sglebius 99289764Sglebius 100289764Sglebius 101289764Sglebius 102289764Sglebius source_scrubbed = source.gsub(/\/\/.*$/, '') # remove line comments 103284990Scy source_scrubbed = source_scrubbed.gsub(/\/\*.*?\*\//m, '') # remove block comments 104284990Scy lines = source_scrubbed.split(/(^\s*\#.*$) # Treat preprocessor directives as a logical line 105284990Scy | (;|\{|\}) /x) # Match ;, {, and } as end of lines 106284990Scy 107284990Scy lines.each_with_index do |line, index| 108284990Scy #find tests 109289764Sglebius if line =~ /^((?:\s*TEST_CASE\s*\(.*?\)\s*)*)\s*void\s+((?:#{@options[:test_prefix]}).*)\s*\(\s*(.*)\s*\)/ 110289764Sglebius arguments = $1 111284990Scy name = $2 112284990Scy call = $3 113289764Sglebius args = nil 114289764Sglebius if (@options[:use_param_tests] and !arguments.empty?) 115289764Sglebius args = [] 116289764Sglebius arguments.scan(/\s*TEST_CASE\s*\((.*)\)\s*$/) {|a| args << a[0]} 117289764Sglebius end 118289764Sglebius tests_and_line_numbers << { :test => name, :args => args, :call => call, :line_number => 0 } 119289764Sglebius 120284990Scy end 121284990Scy end 122289764Sglebius tests_and_line_numbers.uniq! {|v| v[:test] } 123284990Scy 124284990Scy #determine line numbers and create tests to run 125289764Sglebius source_lines = source.split("\n") 126284990Scy source_index = 0; 127284990Scy tests_and_line_numbers.size.times do |i| 128284990Scy source_lines[source_index..-1].each_with_index do |line, index| 129289764Sglebius if (line =~ /#{tests_and_line_numbers[i][:test]}/) 130284990Scy source_index += index 131284990Scy tests_and_line_numbers[i][:line_number] = source_index + 1 132284990Scy break 133284990Scy end 134284990Scy end 135284990Scy end 136289764Sglebius 137289764Sglebius 138284990Scy return tests_and_line_numbers 139284990Scy end 140284990Scy 141289764Sglebius def find_includes(source) 142289764Sglebius 143289764Sglebius #remove comments (block and line, in three steps to ensure correct precedence) 144289764Sglebius source.gsub!(/\/\/(?:.+\/\*|\*(?:$|[^\/])).*$/, '') # remove line comments that comment out the start of blocks 145289764Sglebius source.gsub!(/\/\*.*?\*\//m, '') # remove block comments 146289764Sglebius source.gsub!(/\/\/.*$/, '') # remove line comments (all that remain) 147289764Sglebius 148289764Sglebius #parse out includes 149289764Sglebius 150289764Sglebius includes = { 151289764Sglebius 152289764Sglebius :local => source.scan(/^\s*#include\s+\"\s*(.+)\.[hH]\s*\"/).flatten, 153289764Sglebius :system => source.scan(/^\s*#include\s+<\s*(.+)\s*>/).flatten.map { |inc| "<#{inc}>" } 154289764Sglebius } 155289764Sglebius 156289764Sglebius 157284990Scy return includes 158284990Scy end 159289764Sglebius 160289764Sglebius 161284990Scy def find_mocks(includes) 162284990Scy mock_headers = [] 163284990Scy includes.each do |include_file| 164284990Scy mock_headers << File.basename(include_file) if (include_file =~ /^mock/i) 165284990Scy end 166289764Sglebius return mock_headers 167284990Scy end 168289764Sglebius 169289764Sglebius 170289764Sglebius def create_header(output, mocks, testfile_includes=[]) 171284990Scy output.puts('/* AUTOGENERATED FILE. DO NOT EDIT. */') 172284990Scy create_runtest(output, mocks) 173284990Scy output.puts("\n//=======Automagically Detected Files To Include=====") 174284990Scy output.puts("#include \"#{@options[:framework].to_s}.h\"") 175284990Scy output.puts('#include "cmock.h"') unless (mocks.empty?) 176289764Sglebius @options[:includes].flatten.uniq.compact.each do |inc| 177289764Sglebius output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h','')}.h\""}") 178284990Scy end 179284990Scy output.puts('#include <setjmp.h>') 180284990Scy output.puts('#include <stdio.h>') 181284990Scy output.puts('#include "CException.h"') if @options[:plugins].include?(:cexception) 182289764Sglebius testfile_includes.delete_if{|inc| inc =~ /(unity|cmock)/} 183289764Sglebius testrunner_includes = testfile_includes - mocks 184289764Sglebius testrunner_includes.each do |inc| 185289764Sglebius output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h','')}.h\""}") 186289764Sglebius end 187284990Scy mocks.each do |mock| 188284990Scy output.puts("#include \"#{mock.gsub('.h','')}.h\"") 189284990Scy end 190284990Scy if @options[:enforce_strict_ordering] 191289764Sglebius output.puts('') 192289764Sglebius output.puts('int GlobalExpectCount;') 193289764Sglebius output.puts('int GlobalVerifyOrder;') 194289764Sglebius output.puts('char* GlobalOrderError;') 195284990Scy end 196284990Scy end 197289764Sglebius 198289764Sglebius 199284990Scy def create_externs(output, tests, mocks) 200284990Scy output.puts("\n//=======External Functions This Runner Calls=====") 201289764Sglebius output.puts("extern void #{@options[:setup_name]}(void);") 202289764Sglebius output.puts("extern void #{@options[:teardown_name]}(void);") 203289764Sglebius 204284990Scy tests.each do |test| 205289764Sglebius output.puts("extern void #{test[:test]}(#{test[:call] || 'void'});") 206284990Scy end 207284990Scy output.puts('') 208284990Scy end 209289764Sglebius 210289764Sglebius 211284990Scy def create_mock_management(output, mocks) 212284990Scy unless (mocks.empty?) 213284990Scy output.puts("\n//=======Mock Management=====") 214284990Scy output.puts("static void CMock_Init(void)") 215284990Scy output.puts("{") 216284990Scy if @options[:enforce_strict_ordering] 217284990Scy output.puts(" GlobalExpectCount = 0;") 218289764Sglebius output.puts(" GlobalVerifyOrder = 0;") 219289764Sglebius output.puts(" GlobalOrderError = NULL;") 220284990Scy end 221284990Scy mocks.each do |mock| 222289764Sglebius mock_clean = TypeSanitizer.sanitize_c_identifier(mock) 223289764Sglebius output.puts(" #{mock_clean}_Init();") 224284990Scy end 225284990Scy output.puts("}\n") 226284990Scy 227284990Scy output.puts("static void CMock_Verify(void)") 228284990Scy output.puts("{") 229284990Scy mocks.each do |mock| 230289764Sglebius mock_clean = TypeSanitizer.sanitize_c_identifier(mock) 231289764Sglebius output.puts(" #{mock_clean}_Verify();") 232284990Scy end 233284990Scy output.puts("}\n") 234284990Scy 235284990Scy output.puts("static void CMock_Destroy(void)") 236284990Scy output.puts("{") 237284990Scy mocks.each do |mock| 238289764Sglebius mock_clean = TypeSanitizer.sanitize_c_identifier(mock) 239289764Sglebius output.puts(" #{mock_clean}_Destroy();") 240284990Scy end 241284990Scy output.puts("}\n") 242284990Scy end 243284990Scy end 244289764Sglebius 245289764Sglebius 246284990Scy def create_suite_setup_and_teardown(output) 247284990Scy unless (@options[:suite_setup].nil?) 248284990Scy output.puts("\n//=======Suite Setup=====") 249330106Sdelphij output.puts("static void suite_setup(void)") 250284990Scy output.puts("{") 251284990Scy output.puts(@options[:suite_setup]) 252284990Scy output.puts("}") 253284990Scy end 254284990Scy unless (@options[:suite_teardown].nil?) 255284990Scy output.puts("\n//=======Suite Teardown=====") 256284990Scy output.puts("static int suite_teardown(int num_failures)") 257284990Scy output.puts("{") 258284990Scy output.puts(@options[:suite_teardown]) 259284990Scy output.puts("}") 260284990Scy end 261284990Scy end 262289764Sglebius 263289764Sglebius 264284990Scy def create_runtest(output, used_mocks) 265284990Scy cexception = @options[:plugins].include? :cexception 266284990Scy va_args1 = @options[:use_param_tests] ? ', ...' : '' 267284990Scy va_args2 = @options[:use_param_tests] ? '__VA_ARGS__' : '' 268284990Scy output.puts("\n//=======Test Runner Used To Run Each Test Below=====") 269289764Sglebius output.puts("#define RUN_TEST_NO_ARGS") if @options[:use_param_tests] 270284990Scy output.puts("#define RUN_TEST(TestFunc, TestLineNum#{va_args1}) \\") 271284990Scy output.puts("{ \\") 272284990Scy output.puts(" Unity.CurrentTestName = #TestFunc#{va_args2.empty? ? '' : " \"(\" ##{va_args2} \")\""}; \\") 273284990Scy output.puts(" Unity.CurrentTestLineNumber = TestLineNum; \\") 274284990Scy output.puts(" Unity.NumberOfTests++; \\") 275289764Sglebius output.puts(" CMock_Init(); \\") unless (used_mocks.empty?) 276284990Scy output.puts(" if (TEST_PROTECT()) \\") 277284990Scy output.puts(" { \\") 278284990Scy output.puts(" CEXCEPTION_T e; \\") if cexception 279284990Scy output.puts(" Try { \\") if cexception 280289764Sglebius output.puts(" #{@options[:setup_name]}(); \\") 281289764Sglebius 282289764Sglebius 283284990Scy output.puts(" TestFunc(#{va_args2}); \\") 284289764Sglebius 285284990Scy output.puts(" } Catch(e) { TEST_ASSERT_EQUAL_HEX32_MESSAGE(CEXCEPTION_NONE, e, \"Unhandled Exception!\"); } \\") if cexception 286284990Scy output.puts(" } \\") 287289764Sglebius 288284990Scy output.puts(" if (TEST_PROTECT() && !TEST_IS_IGNORED) \\") 289284990Scy output.puts(" { \\") 290289764Sglebius output.puts(" #{@options[:teardown_name]}(); \\") 291289764Sglebius output.puts(" CMock_Verify(); \\") unless (used_mocks.empty?) 292289764Sglebius 293284990Scy output.puts(" } \\") 294289764Sglebius output.puts(" CMock_Destroy(); \\") unless (used_mocks.empty?) 295284990Scy output.puts(" UnityConcludeTest(); \\") 296284990Scy output.puts("}\n") 297284990Scy end 298289764Sglebius 299289764Sglebius 300284990Scy def create_reset(output, used_mocks) 301284990Scy output.puts("\n//=======Test Reset Option=====") 302289764Sglebius output.puts("void resetTest(void);") 303289764Sglebius output.puts("void resetTest(void)") 304289764Sglebius 305284990Scy output.puts("{") 306284990Scy output.puts(" CMock_Verify();") unless (used_mocks.empty?) 307284990Scy output.puts(" CMock_Destroy();") unless (used_mocks.empty?) 308289764Sglebius output.puts(" #{@options[:teardown_name]}();") 309289764Sglebius 310289764Sglebius output.puts(" CMock_Init();") unless (used_mocks.empty?) 311289764Sglebius output.puts(" #{@options[:setup_name]}();") 312289764Sglebius 313284990Scy output.puts("}") 314284990Scy end 315289764Sglebius 316289764Sglebius 317289764Sglebius def create_main(output, filename, tests, used_mocks) 318289764Sglebius output.puts("\nchar const *progname;\n") 319284990Scy output.puts("\n\n//=======MAIN=====") 320289764Sglebius 321284990Scy output.puts("int main(int argc, char *argv[])") 322284990Scy output.puts("{") 323289764Sglebius output.puts(" progname = argv[0];\n") 324289764Sglebius 325289764Sglebius 326330106Sdelphij modname = filename.split(/[\/\\]/).last 327289764Sglebius 328289764Sglebius 329289764Sglebius 330284990Scy output.puts(" suite_setup();") unless @options[:suite_setup].nil? 331289764Sglebius 332330106Sdelphij output.puts(" UnityBegin(\"#{modname}\");") 333284990Scy 334284990Scy if (@options[:use_param_tests]) 335284990Scy tests.each do |test| 336284990Scy if ((test[:args].nil?) or (test[:args].empty?)) 337289764Sglebius output.puts(" RUN_TEST(#{test[:test]}, #{test[:line_number]}, RUN_TEST_NO_ARGS);") 338284990Scy else 339289764Sglebius test[:args].each {|args| output.puts(" RUN_TEST(#{test[:test]}, #{test[:line_number]}, #{args});")} 340284990Scy end 341284990Scy end 342284990Scy else 343289764Sglebius tests.each { |test| output.puts(" RUN_TEST(#{test[:test]}, #{test[:line_number]});") } 344284990Scy end 345284990Scy output.puts() 346289764Sglebius output.puts(" CMock_Guts_MemFreeFinal();") unless used_mocks.empty? 347284990Scy output.puts(" return #{@options[:suite_teardown].nil? ? "" : "suite_teardown"}(UnityEnd());") 348284990Scy output.puts("}") 349284990Scy end 350284990Scyend 351284990Scy 352284990Scy 353284990Scyif ($0 == __FILE__) 354284990Scy options = { :includes => [] } 355284990Scy yaml_file = nil 356289764Sglebius 357289764Sglebius 358289764Sglebius #parse out all the options first (these will all be removed as we go) 359289764Sglebius ARGV.reject! do |arg| 360284990Scy case(arg) 361289764Sglebius when '-cexception' 362284990Scy options[:plugins] = [:cexception]; true 363289764Sglebius when /\.*\.ya?ml/ 364289764Sglebius 365284990Scy options = UnityTestRunnerGenerator.grab_config(arg); true 366289764Sglebius when /\.*\.h/ 367289764Sglebius options[:includes] << arg; true 368289764Sglebius when /--(\w+)=\"?(.*)\"?/ 369289764Sglebius options[$1.to_sym] = $2; true 370284990Scy else false 371284990Scy end 372289764Sglebius end 373289764Sglebius 374289764Sglebius 375284990Scy #make sure there is at least one parameter left (the input file) 376284990Scy if !ARGV[0] 377289764Sglebius puts ["\nusage: ruby #{__FILE__} (files) (options) input_test_file (output)", 378289764Sglebius "\n input_test_file - this is the C file you want to create a runner for", 379289764Sglebius " output - this is the name of the runner file to generate", 380289764Sglebius " defaults to (input_test_file)_Runner", 381289764Sglebius " files:", 382289764Sglebius " *.yml / *.yaml - loads configuration from here in :unity or :cmock", 383289764Sglebius " *.h - header files are added as #includes in runner", 384289764Sglebius " options:", 385289764Sglebius 386289764Sglebius " -cexception - include cexception support", 387289764Sglebius " --setup_name=\"\" - redefine setUp func name to something else", 388289764Sglebius " --teardown_name=\"\" - redefine tearDown func name to something else", 389289764Sglebius " --test_prefix=\"\" - redefine test prefix from default test|spec|should", 390289764Sglebius " --suite_setup=\"\" - code to execute for setup of entire suite", 391289764Sglebius " --suite_teardown=\"\" - code to execute for teardown of entire suite", 392289764Sglebius " --use_param_tests=1 - enable parameterized tests (disabled by default)", 393289764Sglebius ].join("\n") 394284990Scy exit 1 395284990Scy end 396289764Sglebius 397289764Sglebius 398284990Scy #create the default test runner name if not specified 399284990Scy ARGV[1] = ARGV[0].gsub(".c","_Runner.c") if (!ARGV[1]) 400289764Sglebius 401289764Sglebius 402289764Sglebius 403289764Sglebius 404289764Sglebius 405289764Sglebius 406284990Scy UnityTestRunnerGenerator.new(options).run(ARGV[0], ARGV[1]) 407284990Scyend 408289764Sglebius 409