1#============================================================ 2# Author: John Theofanopoulos 3# A simple parser. Takes the output files generated during the 4# build process and extracts information relating to the tests. 5# 6# Notes: 7# To capture an output file under VS builds use the following: 8# devenv [build instructions] > Output.txt & type Output.txt 9# 10# To capture an output file under Linux builds use the following: 11# make | tee Output.txt 12# 13# This script can handle the following output formats: 14# - normal output (raw unity) 15# - fixture output (unity_fixture.h/.c) 16# - fixture output with verbose flag set ("-v") 17# - time output flag set (UNITY_INCLUDE_EXEC_TIME define enabled with milliseconds output) 18# 19# To use this parser use the following command 20# ruby parseOutput.rb [options] [file] 21# options: -xml : produce a JUnit compatible XML file 22# -suiteRequiredSuiteName 23# : replace default test suite name to 24# "RequiredSuiteName" (can be any name) 25# file: file to scan for results 26#============================================================ 27 28# Parser class for handling the input file 29class ParseOutput 30 def initialize 31 # internal data 32 @class_name_idx = 0 33 @result_usual_idx = 3 34 @path_delim = nil 35 36 # xml output related 37 @xml_out = false 38 @array_list = false 39 40 # current suite name and statistics 41 ## testsuite name 42 @real_test_suite_name = 'Unity' 43 ## classname for testcase 44 @test_suite = nil 45 @total_tests = 0 46 @test_passed = 0 47 @test_failed = 0 48 @test_ignored = 0 49 end 50 51 # Set the flag to indicate if there will be an XML output file or not 52 def set_xml_output 53 @xml_out = true 54 end 55 56 # Set the flag to indicate if there will be an XML output file or not 57 def test_suite_name=(cli_arg) 58 @real_test_suite_name = cli_arg 59 puts "Real test suite name will be '#{@real_test_suite_name}'" 60 end 61 62 def xml_encode_s(str) 63 str.encode(:xml => :attr) 64 end 65 66 # If write our output to XML 67 def write_xml_output 68 output = File.open('report.xml', 'w') 69 output << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" 70 @array_list.each do |item| 71 output << item << "\n" 72 end 73 end 74 75 # Pushes the suite info as xml to the array list, which will be written later 76 def push_xml_output_suite_info 77 # Insert opening tag at front 78 heading = "<testsuite name=#{xml_encode_s(@real_test_suite_name)} tests=\"#{@total_tests}\" failures=\"#{@test_failed}\" skips=\"#{@test_ignored}\">" 79 @array_list.insert(0, heading) 80 # Push back the closing tag 81 @array_list.push '</testsuite>' 82 end 83 84 # Pushes xml output data to the array list, which will be written later 85 def push_xml_output_passed(test_name, execution_time = 0) 86 @array_list.push " <testcase classname=#{xml_encode_s(@test_suite)} name=#{xml_encode_s(test_name)} time=#{xml_encode_s((execution_time / 1000.0).to_s)} />" 87 end 88 89 # Pushes xml output data to the array list, which will be written later 90 def push_xml_output_failed(test_name, reason, execution_time = 0) 91 @array_list.push " <testcase classname=#{xml_encode_s(@test_suite)} name=#{xml_encode_s(test_name)} time=#{xml_encode_s((execution_time / 1000.0).to_s)} >" 92 @array_list.push " <failure type=\"ASSERT FAILED\">#{reason}</failure>" 93 @array_list.push ' </testcase>' 94 end 95 96 # Pushes xml output data to the array list, which will be written later 97 def push_xml_output_ignored(test_name, reason, execution_time = 0) 98 @array_list.push " <testcase classname=#{xml_encode_s(@test_suite)} name=#{xml_encode_s(test_name)} time=#{xml_encode_s((execution_time / 1000.0).to_s)} >" 99 @array_list.push " <skipped type=\"TEST IGNORED\">#{reason}</skipped>" 100 @array_list.push ' </testcase>' 101 end 102 103 # This function will try and determine when the suite is changed. This is 104 # is the name that gets added to the classname parameter. 105 def test_suite_verify(test_suite_name) 106 # Split the path name 107 test_name = test_suite_name.split(@path_delim) 108 109 # Remove the extension and extract the base_name 110 base_name = test_name[test_name.size - 1].split('.')[0] 111 112 # Return if the test suite hasn't changed 113 return unless base_name.to_s != @test_suite.to_s 114 115 @test_suite = base_name 116 printf "New Test: %s\n", @test_suite 117 end 118 119 # Prepares the line for verbose fixture output ("-v") 120 def prepare_fixture_line(line) 121 line = line.sub('IGNORE_TEST(', '') 122 line = line.sub('TEST(', '') 123 line = line.sub(')', ',') 124 line = line.chomp 125 array = line.split(',') 126 array.map { |x| x.to_s.lstrip.chomp } 127 end 128 129 # Test was flagged as having passed so format the output. 130 # This is using the Unity fixture output and not the original Unity output. 131 def test_passed_unity_fixture(array) 132 class_name = array[0] 133 test_name = array[1] 134 test_suite_verify(class_name) 135 printf "%-40s PASS\n", test_name 136 137 push_xml_output_passed(test_name) if @xml_out 138 end 139 140 # Test was flagged as having failed so format the output. 141 # This is using the Unity fixture output and not the original Unity output. 142 def test_failed_unity_fixture(array) 143 class_name = array[0] 144 test_name = array[1] 145 test_suite_verify(class_name) 146 reason_array = array[2].split(':') 147 reason = "#{reason_array[-1].lstrip.chomp} at line: #{reason_array[-4]}" 148 149 printf "%-40s FAILED\n", test_name 150 151 push_xml_output_failed(test_name, reason) if @xml_out 152 end 153 154 # Test was flagged as being ignored so format the output. 155 # This is using the Unity fixture output and not the original Unity output. 156 def test_ignored_unity_fixture(array) 157 class_name = array[0] 158 test_name = array[1] 159 reason = 'No reason given' 160 if array.size > 2 161 reason_array = array[2].split(':') 162 tmp_reason = reason_array[-1].lstrip.chomp 163 reason = tmp_reason == 'IGNORE' ? 'No reason given' : tmp_reason 164 end 165 test_suite_verify(class_name) 166 printf "%-40s IGNORED\n", test_name 167 168 push_xml_output_ignored(test_name, reason) if @xml_out 169 end 170 171 # Test was flagged as having passed so format the output 172 def test_passed(array) 173 # ':' symbol will be valid in function args now 174 real_method_name = array[@result_usual_idx - 1..-2].join(':') 175 array = array[0..@result_usual_idx - 2] + [real_method_name] + [array[-1]] 176 177 last_item = array.length - 1 178 test_time = get_test_time(array[last_item]) 179 test_name = array[last_item - 1] 180 test_suite_verify(array[@class_name_idx]) 181 printf "%-40s PASS %10d ms\n", test_name, test_time 182 183 return unless @xml_out 184 185 push_xml_output_passed(test_name, test_time) if @xml_out 186 end 187 188 # Test was flagged as having failed so format the line 189 def test_failed(array) 190 # ':' symbol will be valid in function args now 191 real_method_name = array[@result_usual_idx - 1..-3].join(':') 192 array = array[0..@result_usual_idx - 3] + [real_method_name] + array[-2..] 193 194 last_item = array.length - 1 195 test_time = get_test_time(array[last_item]) 196 test_name = array[last_item - 2] 197 reason = "#{array[last_item].chomp.lstrip} at line: #{array[last_item - 3]}" 198 class_name = array[@class_name_idx] 199 200 if test_name.start_with? 'TEST(' 201 array2 = test_name.split(' ') 202 203 test_suite = array2[0].sub('TEST(', '') 204 test_suite = test_suite.sub(',', '') 205 class_name = test_suite 206 207 test_name = array2[1].sub(')', '') 208 end 209 210 test_suite_verify(class_name) 211 printf "%-40s FAILED %10d ms\n", test_name, test_time 212 213 push_xml_output_failed(test_name, reason, test_time) if @xml_out 214 end 215 216 # Test was flagged as being ignored so format the output 217 def test_ignored(array) 218 # ':' symbol will be valid in function args now 219 real_method_name = array[@result_usual_idx - 1..-3].join(':') 220 array = array[0..@result_usual_idx - 3] + [real_method_name] + array[-2..] 221 222 last_item = array.length - 1 223 test_time = get_test_time(array[last_item]) 224 test_name = array[last_item - 2] 225 reason = array[last_item].chomp.lstrip 226 class_name = array[@class_name_idx] 227 228 if test_name.start_with? 'TEST(' 229 array2 = test_name.split(' ') 230 231 test_suite = array2[0].sub('TEST(', '') 232 test_suite = test_suite.sub(',', '') 233 class_name = test_suite 234 235 test_name = array2[1].sub(')', '') 236 end 237 238 test_suite_verify(class_name) 239 printf "%-40s IGNORED %10d ms\n", test_name, test_time 240 241 push_xml_output_ignored(test_name, reason, test_time) if @xml_out 242 end 243 244 # Test time will be in ms 245 def get_test_time(value_with_time) 246 test_time_array = value_with_time.scan(/\((-?\d+.?\d*) ms\)\s*$/).flatten.map do |arg_value_str| 247 arg_value_str.include?('.') ? arg_value_str.to_f : arg_value_str.to_i 248 end 249 250 test_time_array.any? ? test_time_array[0] : 0 251 end 252 253 # Adjusts the os specific members according to the current path style 254 # (Windows or Unix based) 255 def detect_os_specifics(line) 256 if line.include? '\\' 257 # Windows X:\Y\Z 258 @class_name_idx = 1 259 @path_delim = '\\' 260 else 261 # Unix Based /X/Y/Z 262 @class_name_idx = 0 263 @path_delim = '/' 264 end 265 end 266 267 # Main function used to parse the file that was captured. 268 def process(file_name) 269 @array_list = [] 270 271 puts "Parsing file: #{file_name}" 272 273 @test_passed = 0 274 @test_failed = 0 275 @test_ignored = 0 276 puts '' 277 puts '=================== RESULTS =====================' 278 puts '' 279 # Apply binary encoding. Bad symbols will be unchanged 280 File.open(file_name, 'rb').each do |line| 281 # Typical test lines look like these: 282 # ---------------------------------------------------- 283 # 1. normal output: 284 # <path>/<test_file>.c:36:test_tc1000_opsys:FAIL: Expected 1 Was 0 285 # <path>/<test_file>.c:112:test_tc5004_initCanChannel:IGNORE: Not Yet Implemented 286 # <path>/<test_file>.c:115:test_tc5100_initCanVoidPtrs:PASS 287 # 288 # 2. fixture output 289 # <path>/<test_file>.c:63:TEST(<test_group>, <test_function>):FAIL: Expected 0x00001234 Was 0x00005A5A 290 # <path>/<test_file>.c:36:TEST(<test_group>, <test_function>):IGNORE 291 # Note: "PASS" information won't be generated in this mode 292 # 293 # 3. fixture output with verbose information ("-v") 294 # TEST(<test_group, <test_file>)<path>/<test_file>:168::FAIL: Expected 0x8D Was 0x8C 295 # TEST(<test_group>, <test_file>)<path>/<test_file>:22::IGNORE: This Test Was Ignored On Purpose 296 # IGNORE_TEST(<test_group, <test_file>) 297 # TEST(<test_group, <test_file>) PASS 298 # 299 # Note: Where path is different on Unix vs Windows devices (Windows leads with a drive letter)! 300 detect_os_specifics(line) 301 line_array = line.split(':') 302 303 # If we were able to split the line then we can look to see if any of our target words 304 # were found. Case is important. 305 next unless (line_array.size >= 4) || (line.start_with? 'TEST(') || (line.start_with? 'IGNORE_TEST(') 306 307 # check if the output is fixture output (with verbose flag "-v") 308 if (line.start_with? 'TEST(') || (line.start_with? 'IGNORE_TEST(') 309 line_array = prepare_fixture_line(line) 310 if line.include? ' PASS' 311 test_passed_unity_fixture(line_array) 312 @test_passed += 1 313 elsif line.include? 'FAIL' 314 test_failed_unity_fixture(line_array) 315 @test_failed += 1 316 elsif line.include? 'IGNORE' 317 test_ignored_unity_fixture(line_array) 318 @test_ignored += 1 319 end 320 # normal output / fixture output (without verbose "-v") 321 elsif line.include? ':PASS' 322 test_passed(line_array) 323 @test_passed += 1 324 elsif line.include? ':FAIL' 325 test_failed(line_array) 326 @test_failed += 1 327 elsif line.include? ':IGNORE:' 328 test_ignored(line_array) 329 @test_ignored += 1 330 elsif line.include? ':IGNORE' 331 line_array.push('No reason given') 332 test_ignored(line_array) 333 @test_ignored += 1 334 elsif line_array.size >= 4 335 # We will check output from color compilation 336 if line_array[@result_usual_idx..].any? { |l| l.include? 'PASS' } 337 test_passed(line_array) 338 @test_passed += 1 339 elsif line_array[@result_usual_idx..].any? { |l| l.include? 'FAIL' } 340 test_failed(line_array) 341 @test_failed += 1 342 elsif line_array[@result_usual_idx..-2].any? { |l| l.include? 'IGNORE' } 343 test_ignored(line_array) 344 @test_ignored += 1 345 elsif line_array[@result_usual_idx..].any? { |l| l.include? 'IGNORE' } 346 line_array.push("No reason given (#{get_test_time(line_array[@result_usual_idx..])} ms)") 347 test_ignored(line_array) 348 @test_ignored += 1 349 end 350 end 351 @total_tests = @test_passed + @test_failed + @test_ignored 352 end 353 puts '' 354 puts '=================== SUMMARY =====================' 355 puts '' 356 puts "Tests Passed : #{@test_passed}" 357 puts "Tests Failed : #{@test_failed}" 358 puts "Tests Ignored : #{@test_ignored}" 359 360 return unless @xml_out 361 362 # push information about the suite 363 push_xml_output_suite_info 364 # write xml output file 365 write_xml_output 366 end 367end 368 369# If the command line has no values in, used a default value of Output.txt 370parse_my_file = ParseOutput.new 371 372if ARGV.size >= 1 373 ARGV.each do |arg| 374 if arg == '-xml' 375 parse_my_file.set_xml_output 376 elsif arg.start_with?('-suite') 377 parse_my_file.test_suite_name = arg.delete_prefix('-suite') 378 else 379 parse_my_file.process(arg) 380 break 381 end 382 end 383end 384