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] 5290000Sglebius# ========================================== 6284990Scy 7290000Sglebius$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) 13290000Sglebius @options = UnityTestRunnerGenerator.default_options 14290000Sglebius 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 21290000Sglebius require "#{File.expand_path(File.dirname(__FILE__))}/type_sanitizer" 22284990Scy end 23290000Sglebius 24290000Sglebius def self.default_options 25290000Sglebius { 26290000Sglebius :includes => [], 27290000Sglebius :plugins => [], 28290000Sglebius :framework => :unity, 29290000Sglebius :test_prefix => "test|spec|should", 30290000Sglebius :setup_name => "setUp", 31290000Sglebius :teardown_name => "tearDown", 32290000Sglebius } 33290000Sglebius end 34290000Sglebius 35290000Sglebius 36284990Scy def self.grab_config(config_file) 37290000Sglebius options = self.default_options 38290000Sglebius 39284990Scy unless (config_file.nil? or config_file.empty?) 40284990Scy require 'yaml' 41284990Scy yaml_guts = YAML.load_file(config_file) 42290000Sglebius 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 = [] 50290000Sglebius testfile_includes = [] 51284990Scy used_mocks = [] 52290000Sglebius 53290000Sglebius 54284990Scy @options.merge!(options) unless options.nil? 55284990Scy module_name = File.basename(input_file) 56290000Sglebius 57290000Sglebius 58284990Scy #pull required data from source file 59290000Sglebius source = File.read(input_file) 60290000Sglebius source = source.force_encoding("ISO-8859-1").encode("utf-8", :replace => nil) if ($QUICK_RUBY_VERSION > 10900) 61290000Sglebius tests = find_tests(source) 62290000Sglebius headers = find_includes(source) 63290000Sglebius testfile_includes = headers[:local] + headers[:system] 64290000Sglebius used_mocks = find_mocks(testfile_includes) 65284990Scy 66290000Sglebius 67284990Scy #build runner file 68290000Sglebius generate(input_file, output_file, tests, used_mocks, testfile_includes) 69290000Sglebius 70290000Sglebius #determine which files were used to return them 71290000Sglebius all_files_used = [input_file, output_file] 72290000Sglebius all_files_used += testfile_includes.map {|filename| filename + '.c'} unless testfile_includes.empty? 73290000Sglebius all_files_used += @options[:includes] unless @options[:includes].empty? 74290000Sglebius return all_files_used.uniq 75290000Sglebius end 76290000Sglebius 77290000Sglebius def generate(input_file, output_file, tests, used_mocks, testfile_includes) 78284990Scy File.open(output_file, 'w') do |output| 79290000Sglebius 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) 84290000Sglebius create_main(output, input_file, tests, used_mocks) 85284990Scy end 86290000Sglebius 87290000Sglebius 88290000Sglebius 89290000Sglebius 90290000Sglebius 91284990Scy end 92290000Sglebius 93290000Sglebius 94290000Sglebius def find_tests(source) 95290000Sglebius 96290000Sglebius 97284990Scy tests_and_line_numbers = [] 98290000Sglebius 99290000Sglebius 100290000Sglebius 101290000Sglebius 102290000Sglebius 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 109290000Sglebius if line =~ /^((?:\s*TEST_CASE\s*\(.*?\)\s*)*)\s*void\s+((?:#{@options[:test_prefix]}).*)\s*\(\s*(.*)\s*\)/ 110290000Sglebius arguments = $1 111284990Scy name = $2 112284990Scy call = $3 113290000Sglebius args = nil 114290000Sglebius if (@options[:use_param_tests] and !arguments.empty?) 115290000Sglebius args = [] 116290000Sglebius arguments.scan(/\s*TEST_CASE\s*\((.*)\)\s*$/) {|a| args << a[0]} 117290000Sglebius end 118290000Sglebius tests_and_line_numbers << { :test => name, :args => args, :call => call, :line_number => 0 } 119290000Sglebius 120284990Scy end 121284990Scy end 122290000Sglebius tests_and_line_numbers.uniq! {|v| v[:test] } 123284990Scy 124284990Scy #determine line numbers and create tests to run 125290000Sglebius 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| 129290000Sglebius 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 136290000Sglebius 137290000Sglebius 138284990Scy return tests_and_line_numbers 139284990Scy end 140284990Scy 141290000Sglebius def find_includes(source) 142290000Sglebius 143290000Sglebius #remove comments (block and line, in three steps to ensure correct precedence) 144290000Sglebius source.gsub!(/\/\/(?:.+\/\*|\*(?:$|[^\/])).*$/, '') # remove line comments that comment out the start of blocks 145290000Sglebius source.gsub!(/\/\*.*?\*\//m, '') # remove block comments 146290000Sglebius source.gsub!(/\/\/.*$/, '') # remove line comments (all that remain) 147290000Sglebius 148290000Sglebius #parse out includes 149290000Sglebius 150290000Sglebius includes = { 151290000Sglebius 152290000Sglebius :local => source.scan(/^\s*#include\s+\"\s*(.+)\.[hH]\s*\"/).flatten, 153290000Sglebius :system => source.scan(/^\s*#include\s+<\s*(.+)\s*>/).flatten.map { |inc| "<#{inc}>" } 154290000Sglebius } 155290000Sglebius 156290000Sglebius 157284990Scy return includes 158284990Scy end 159290000Sglebius 160290000Sglebius 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 166290000Sglebius return mock_headers 167284990Scy end 168290000Sglebius 169290000Sglebius 170290000Sglebius 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?) 176290000Sglebius @options[:includes].flatten.uniq.compact.each do |inc| 177290000Sglebius 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) 182290000Sglebius testfile_includes.delete_if{|inc| inc =~ /(unity|cmock)/} 183290000Sglebius testrunner_includes = testfile_includes - mocks 184290000Sglebius testrunner_includes.each do |inc| 185290000Sglebius output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h','')}.h\""}") 186290000Sglebius end 187284990Scy mocks.each do |mock| 188284990Scy output.puts("#include \"#{mock.gsub('.h','')}.h\"") 189284990Scy end 190284990Scy if @options[:enforce_strict_ordering] 191290000Sglebius output.puts('') 192290000Sglebius output.puts('int GlobalExpectCount;') 193290000Sglebius output.puts('int GlobalVerifyOrder;') 194290000Sglebius output.puts('char* GlobalOrderError;') 195284990Scy end 196284990Scy end 197290000Sglebius 198290000Sglebius 199284990Scy def create_externs(output, tests, mocks) 200284990Scy output.puts("\n//=======External Functions This Runner Calls=====") 201290000Sglebius output.puts("extern void #{@options[:setup_name]}(void);") 202290000Sglebius output.puts("extern void #{@options[:teardown_name]}(void);") 203290000Sglebius 204284990Scy tests.each do |test| 205290000Sglebius output.puts("extern void #{test[:test]}(#{test[:call] || 'void'});") 206284990Scy end 207284990Scy output.puts('') 208284990Scy end 209290000Sglebius 210290000Sglebius 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;") 218290000Sglebius output.puts(" GlobalVerifyOrder = 0;") 219290000Sglebius output.puts(" GlobalOrderError = NULL;") 220284990Scy end 221284990Scy mocks.each do |mock| 222290000Sglebius mock_clean = TypeSanitizer.sanitize_c_identifier(mock) 223290000Sglebius 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| 230290000Sglebius mock_clean = TypeSanitizer.sanitize_c_identifier(mock) 231290000Sglebius 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| 238290000Sglebius mock_clean = TypeSanitizer.sanitize_c_identifier(mock) 239290000Sglebius output.puts(" #{mock_clean}_Destroy();") 240284990Scy end 241284990Scy output.puts("}\n") 242284990Scy end 243284990Scy end 244290000Sglebius 245290000Sglebius 246284990Scy def create_suite_setup_and_teardown(output) 247284990Scy unless (@options[:suite_setup].nil?) 248284990Scy output.puts("\n//=======Suite Setup=====") 249284990Scy output.puts("static int 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 262290000Sglebius 263290000Sglebius 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=====") 269290000Sglebius 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++; \\") 275290000Sglebius 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 280290000Sglebius output.puts(" #{@options[:setup_name]}(); \\") 281290000Sglebius 282290000Sglebius 283284990Scy output.puts(" TestFunc(#{va_args2}); \\") 284290000Sglebius 285284990Scy output.puts(" } Catch(e) { TEST_ASSERT_EQUAL_HEX32_MESSAGE(CEXCEPTION_NONE, e, \"Unhandled Exception!\"); } \\") if cexception 286284990Scy output.puts(" } \\") 287290000Sglebius 288284990Scy output.puts(" if (TEST_PROTECT() && !TEST_IS_IGNORED) \\") 289284990Scy output.puts(" { \\") 290290000Sglebius output.puts(" #{@options[:teardown_name]}(); \\") 291290000Sglebius output.puts(" CMock_Verify(); \\") unless (used_mocks.empty?) 292290000Sglebius 293284990Scy output.puts(" } \\") 294290000Sglebius output.puts(" CMock_Destroy(); \\") unless (used_mocks.empty?) 295284990Scy output.puts(" UnityConcludeTest(); \\") 296284990Scy output.puts("}\n") 297284990Scy end 298290000Sglebius 299290000Sglebius 300284990Scy def create_reset(output, used_mocks) 301284990Scy output.puts("\n//=======Test Reset Option=====") 302290000Sglebius output.puts("void resetTest(void);") 303290000Sglebius output.puts("void resetTest(void)") 304290000Sglebius 305284990Scy output.puts("{") 306284990Scy output.puts(" CMock_Verify();") unless (used_mocks.empty?) 307284990Scy output.puts(" CMock_Destroy();") unless (used_mocks.empty?) 308290000Sglebius output.puts(" #{@options[:teardown_name]}();") 309290000Sglebius 310290000Sglebius output.puts(" CMock_Init();") unless (used_mocks.empty?) 311290000Sglebius output.puts(" #{@options[:setup_name]}();") 312290000Sglebius 313284990Scy output.puts("}") 314284990Scy end 315290000Sglebius 316290000Sglebius 317290000Sglebius def create_main(output, filename, tests, used_mocks) 318290000Sglebius output.puts("\nchar const *progname;\n") 319284990Scy output.puts("\n\n//=======MAIN=====") 320290000Sglebius 321284990Scy output.puts("int main(int argc, char *argv[])") 322284990Scy output.puts("{") 323290000Sglebius output.puts(" progname = argv[0];\n") 324290000Sglebius 325290000Sglebius 326290000Sglebius 327290000Sglebius 328290000Sglebius 329290000Sglebius 330284990Scy output.puts(" suite_setup();") unless @options[:suite_setup].nil? 331290000Sglebius 332284990Scy output.puts(" UnityBegin(\"#{filename}\");") 333284990Scy 334284990Scy if (@options[:use_param_tests]) 335284990Scy tests.each do |test| 336284990Scy if ((test[:args].nil?) or (test[:args].empty?)) 337290000Sglebius output.puts(" RUN_TEST(#{test[:test]}, #{test[:line_number]}, RUN_TEST_NO_ARGS);") 338284990Scy else 339290000Sglebius test[:args].each {|args| output.puts(" RUN_TEST(#{test[:test]}, #{test[:line_number]}, #{args});")} 340284990Scy end 341284990Scy end 342284990Scy else 343290000Sglebius tests.each { |test| output.puts(" RUN_TEST(#{test[:test]}, #{test[:line_number]});") } 344284990Scy end 345284990Scy output.puts() 346290000Sglebius 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 356290000Sglebius 357290000Sglebius 358290000Sglebius #parse out all the options first (these will all be removed as we go) 359290000Sglebius ARGV.reject! do |arg| 360284990Scy case(arg) 361290000Sglebius when '-cexception' 362284990Scy options[:plugins] = [:cexception]; true 363290000Sglebius when /\.*\.ya?ml/ 364290000Sglebius 365284990Scy options = UnityTestRunnerGenerator.grab_config(arg); true 366290000Sglebius when /\.*\.h/ 367290000Sglebius options[:includes] << arg; true 368290000Sglebius when /--(\w+)=\"?(.*)\"?/ 369290000Sglebius options[$1.to_sym] = $2; true 370284990Scy else false 371284990Scy end 372290000Sglebius end 373290000Sglebius 374290000Sglebius 375284990Scy #make sure there is at least one parameter left (the input file) 376284990Scy if !ARGV[0] 377290000Sglebius puts ["\nusage: ruby #{__FILE__} (files) (options) input_test_file (output)", 378290000Sglebius "\n input_test_file - this is the C file you want to create a runner for", 379290000Sglebius " output - this is the name of the runner file to generate", 380290000Sglebius " defaults to (input_test_file)_Runner", 381290000Sglebius " files:", 382290000Sglebius " *.yml / *.yaml - loads configuration from here in :unity or :cmock", 383290000Sglebius " *.h - header files are added as #includes in runner", 384290000Sglebius " options:", 385290000Sglebius 386290000Sglebius " -cexception - include cexception support", 387290000Sglebius " --setup_name=\"\" - redefine setUp func name to something else", 388290000Sglebius " --teardown_name=\"\" - redefine tearDown func name to something else", 389290000Sglebius " --test_prefix=\"\" - redefine test prefix from default test|spec|should", 390290000Sglebius " --suite_setup=\"\" - code to execute for setup of entire suite", 391290000Sglebius " --suite_teardown=\"\" - code to execute for teardown of entire suite", 392290000Sglebius " --use_param_tests=1 - enable parameterized tests (disabled by default)", 393290000Sglebius ].join("\n") 394284990Scy exit 1 395284990Scy end 396290000Sglebius 397290000Sglebius 398284990Scy #create the default test runner name if not specified 399284990Scy ARGV[1] = ARGV[0].gsub(".c","_Runner.c") if (!ARGV[1]) 400290000Sglebius 401290000Sglebius 402290000Sglebius 403290000Sglebius 404290000Sglebius 405290000Sglebius 406284990Scy UnityTestRunnerGenerator.new(options).run(ARGV[0], ARGV[1]) 407284990Scyend 408290000Sglebius 409