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