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 11 def initialize(options = nil) 12 @options = { :includes => [], :plugins => [], :framework => :unity } 13 case(options) 14 when NilClass then @options 15 when String then @options.merge!(UnityTestRunnerGenerator.grab_config(options)) 16 when Hash then @options.merge!(options) 17 else raise "If you specify arguments, it should be a filename or a hash of options" 18 end 19 end 20 21 def self.grab_config(config_file) 22 options = { :includes => [], :plugins => [], :framework => :unity } 23 unless (config_file.nil? or config_file.empty?) 24 require 'yaml' 25 yaml_guts = YAML.load_file(config_file) 26 options.merge!(yaml_guts[:unity] ? yaml_guts[:unity] : yaml_guts[:cmock]) 27 raise "No :unity or :cmock section found in #{config_file}" unless options 28 end 29 return(options) 30 end 31 32 def run(input_file, output_file, options=nil) 33 tests = [] 34 includes = [] 35 used_mocks = [] 36 37 @options.merge!(options) unless options.nil? 38 module_name = File.basename(input_file) 39 40 #pull required data from source file 41 File.open(input_file, 'r') do |input| 42 tests = find_tests(input) 43 includes = find_includes(input) 44 used_mocks = find_mocks(includes) 45 end 46 47 #build runner file 48 File.open(output_file, 'w') do |output| 49 create_header(output, used_mocks) 50 create_externs(output, tests, used_mocks) 51 create_mock_management(output, used_mocks) 52 create_suite_setup_and_teardown(output) 53 create_reset(output, used_mocks) 54 create_main(output, input_file, tests) 55 end 56 57 all_files_used = [input_file, output_file] 58 all_files_used += includes.map {|filename| filename + '.c'} unless includes.empty? 59 all_files_used += @options[:includes] unless @options[:includes].empty? 60 return all_files_used.uniq 61 end 62 63 def find_tests(input_file) 64 tests_raw = [] 65 tests_args = [] 66 tests_and_line_numbers = [] 67 68 input_file.rewind 69 source_raw = input_file.read 70 source_scrubbed = source_raw.gsub(/\/\/.*$/, '') # remove line comments 71 source_scrubbed = source_scrubbed.gsub(/\/\*.*?\*\//m, '') # remove block comments 72 lines = source_scrubbed.split(/(^\s*\#.*$) # Treat preprocessor directives as a logical line 73 | (;|\{|\}) /x) # Match ;, {, and } as end of lines 74 75 lines.each_with_index do |line, index| 76 #find tests 77 if line =~ /^((?:\s*TEST_CASE\s*\(.*?\)\s*)*)\s*void\s+(test.*?)\s*\(\s*(.*)\s*\)/ 78 name = $2 79 call = $3 80 args = (@options[:use_param_tests] and $1) ? ($1.gsub(/\s*TEST_CASE\s*\(\s*/,'').strip.split(/\s*\)/).compact) : nil 81 tests_and_line_numbers << { :name => name, :args => args, :call => call, :line_number => 0 } 82 tests_args = [] 83 end 84 end 85 86 #determine line numbers and create tests to run 87 source_lines = source_raw.split("\n") 88 source_index = 0; 89 tests_and_line_numbers.size.times do |i| 90 source_lines[source_index..-1].each_with_index do |line, index| 91 if (line =~ /#{tests_and_line_numbers[i][:name]}/) 92 source_index += index 93 tests_and_line_numbers[i][:line_number] = source_index + 1 94 break 95 end 96 end 97 end 98 99 return tests_and_line_numbers 100 end 101 102 def find_includes(input_file) 103 input_file.rewind 104 includes = [] 105 input_file.readlines.each do |line| 106 scan_results = line.scan(/^\s*#include\s+\"\s*(.+)\.[hH]\s*\"/) 107 includes << scan_results[0][0] if (scan_results.size > 0) 108 end 109 return includes 110 end 111 112 def find_mocks(includes) 113 mock_headers = [] 114 includes.each do |include_file| 115 mock_headers << File.basename(include_file) if (include_file =~ /^mock/i) 116 end 117 return mock_headers 118 end 119 120 def create_header(output, mocks) 121 output.puts('/* AUTOGENERATED FILE. DO NOT EDIT. */') 122 create_runtest(output, mocks) 123 output.puts("\n//=======Automagically Detected Files To Include=====") 124 output.puts("#include \"#{@options[:framework].to_s}.h\"") 125 output.puts('#include "cmock.h"') unless (mocks.empty?) 126 @options[:includes].flatten.uniq.compact.each do |includes| 127 output.puts("#include \"#{includes.gsub('.h','')}.h\"") 128 end 129 output.puts('#include <setjmp.h>') 130 output.puts('#include <stdio.h>') 131 output.puts('#include "CException.h"') if @options[:plugins].include?(:cexception) 132 mocks.each do |mock| 133 output.puts("#include \"#{mock.gsub('.h','')}.h\"") 134 end 135 if @options[:enforce_strict_ordering] 136 output.puts('') 137 output.puts('int GlobalExpectCount;') 138 output.puts('int GlobalVerifyOrder;') 139 output.puts('char* GlobalOrderError;') 140 end 141 end 142 143 def create_externs(output, tests, mocks) 144 output.puts("\n//=======External Functions This Runner Calls=====") 145 output.puts("extern void setUp(void);") 146 output.puts("extern void tearDown(void);") 147 output.puts("void resetTest(void);") 148 tests.each do |test| 149 output.puts("extern void #{test[:name]}(#{test[:call]});") 150 end 151 output.puts('') 152 end 153 154 def create_mock_management(output, mocks) 155 unless (mocks.empty?) 156 output.puts("\n//=======Mock Management=====") 157 output.puts("static void CMock_Init(void)") 158 output.puts("{") 159 if @options[:enforce_strict_ordering] 160 output.puts(" GlobalExpectCount = 0;") 161 output.puts(" GlobalVerifyOrder = 0;") 162 output.puts(" GlobalOrderError = NULL;") 163 end 164 mocks.each do |mock| 165 output.puts(" #{mock}_Init();") 166 end 167 output.puts("}\n") 168 169 output.puts("static void CMock_Verify(void)") 170 output.puts("{") 171 mocks.each do |mock| 172 output.puts(" #{mock}_Verify();") 173 end 174 output.puts("}\n") 175 176 output.puts("static void CMock_Destroy(void)") 177 output.puts("{") 178 mocks.each do |mock| 179 output.puts(" #{mock}_Destroy();") 180 end 181 output.puts("}\n") 182 end 183 end 184 185 def create_suite_setup_and_teardown(output) 186 unless (@options[:suite_setup].nil?) 187 output.puts("\n//=======Suite Setup=====") 188 output.puts("static int suite_setup(void)") 189 output.puts("{") 190 output.puts(@options[:suite_setup]) 191 output.puts("}") 192 end 193 unless (@options[:suite_teardown].nil?) 194 output.puts("\n//=======Suite Teardown=====") 195 output.puts("static int suite_teardown(int num_failures)") 196 output.puts("{") 197 output.puts(@options[:suite_teardown]) 198 output.puts("}") 199 end 200 end 201 202 def create_runtest(output, used_mocks) 203 cexception = @options[:plugins].include? :cexception 204 va_args1 = @options[:use_param_tests] ? ', ...' : '' 205 va_args2 = @options[:use_param_tests] ? '__VA_ARGS__' : '' 206 output.puts("\n//=======Test Runner Used To Run Each Test Below=====") 207 output.puts("#define RUN_TEST_NO_ARGS") if @options[:use_param_tests] 208 output.puts("#define RUN_TEST(TestFunc, TestLineNum#{va_args1}) \\") 209 output.puts("{ \\") 210 output.puts(" Unity.CurrentTestName = #TestFunc#{va_args2.empty? ? '' : " \"(\" ##{va_args2} \")\""}; \\") 211 output.puts(" Unity.CurrentTestLineNumber = TestLineNum; \\") 212 output.puts(" Unity.NumberOfTests++; \\") 213 output.puts(" if (TEST_PROTECT()) \\") 214 output.puts(" { \\") 215 output.puts(" CEXCEPTION_T e; \\") if cexception 216 output.puts(" Try { \\") if cexception 217 output.puts(" CMock_Init(); \\") unless (used_mocks.empty?) 218 output.puts(" setUp(); \\") 219 output.puts(" TestFunc(#{va_args2}); \\") 220 output.puts(" CMock_Verify(); \\") unless (used_mocks.empty?) 221 output.puts(" } Catch(e) { TEST_ASSERT_EQUAL_HEX32_MESSAGE(CEXCEPTION_NONE, e, \"Unhandled Exception!\"); } \\") if cexception 222 output.puts(" } \\") 223 output.puts(" CMock_Destroy(); \\") unless (used_mocks.empty?) 224 output.puts(" if (TEST_PROTECT() && !TEST_IS_IGNORED) \\") 225 output.puts(" { \\") 226 output.puts(" tearDown(); \\") 227 output.puts(" } \\") 228 output.puts(" UnityConcludeTest(); \\") 229 output.puts("}\n") 230 end 231 232 def create_reset(output, used_mocks) 233 output.puts("\n//=======Test Reset Option=====") 234 output.puts("void resetTest()") 235 output.puts("{") 236 output.puts(" CMock_Verify();") unless (used_mocks.empty?) 237 output.puts(" CMock_Destroy();") unless (used_mocks.empty?) 238 output.puts(" tearDown();") 239 output.puts(" CMock_Init();") unless (used_mocks.empty?) 240 output.puts(" setUp();") 241 output.puts("}") 242 end 243 244 def create_main(output, filename, tests) 245 output.puts("\nchar *progname;\n") 246 output.puts("\n\n//=======MAIN=====") 247 248 output.puts("int main(int argc, char *argv[])") 249 output.puts("{") 250 #new stuff added 251 #output.puts("\nu_long current_time = 4; // needed by authkeys. Used only in to calculate lifetime.\n"); 252 253 output.puts(" progname = argv[0];\n") 254 #not necessary after all 255 #output.puts(" init_lib();\n") 256 #output.puts(" init_auth();\n") 257 output.puts(" suite_setup();") unless @options[:suite_setup].nil? 258 output.puts(" Unity.TestFile = \"#{filename}\";") 259 output.puts(" UnityBegin(\"#{filename}\");") 260 261 if (@options[:use_param_tests]) 262 tests.each do |test| 263 if ((test[:args].nil?) or (test[:args].empty?)) 264 output.puts(" RUN_TEST(#{test[:name]}, #{test[:line_number]}, RUN_TEST_NO_ARGS);") 265 else 266 test[:args].each {|args| output.puts(" RUN_TEST(#{test[:name]}, #{test[:line_number]}, #{args});")} 267 end 268 end 269 else 270 tests.each { |test| output.puts(" RUN_TEST(#{test[:name]}, #{test[:line_number]});") } 271 end 272 output.puts() 273 output.puts(" return #{@options[:suite_teardown].nil? ? "" : "suite_teardown"}(UnityEnd());") 274 output.puts("}") 275 end 276end 277 278 279if ($0 == __FILE__) 280 options = { :includes => [] } 281 yaml_file = nil 282 283 #parse out all the options first 284 ARGV.reject! do |arg| 285 case(arg) 286 when '-cexception' 287 options[:plugins] = [:cexception]; true 288 when /\w+\.yml/ 289 options = UnityTestRunnerGenerator.grab_config(arg); true 290 else false 291 end 292 end 293 294 #make sure there is at least one parameter left (the input file) 295 if !ARGV[0] 296 puts ["usage: ruby #{__FILE__} (yaml) (options) input_test_file output_test_runner (includes)", 297 " blah.yml - will use config options in the yml file (see docs)", 298 " -cexception - include cexception support"].join("\n") 299 exit 1 300 end 301 302 #create the default test runner name if not specified 303 ARGV[1] = ARGV[0].gsub(".c","_Runner.c") if (!ARGV[1]) 304 305 #everything else is an include file 306 options[:includes] = (ARGV.slice(2..-1).flatten.compact) if (ARGV.size > 2) 307 308 UnityTestRunnerGenerator.new(options).run(ARGV[0], ARGV[1]) 309end 310