#============================================================ # Author: John Theofanopoulos # A simple parser. Takes the output files generated during the # build process and extracts information relating to the tests. # # Notes: # To capture an output file under VS builds use the following: # devenv [build instructions] > Output.txt & type Output.txt # # To capture an output file under Linux builds use the following: # make | tee Output.txt # # This script can handle the following output formats: # - normal output (raw unity) # - fixture output (unity_fixture.h/.c) # - fixture output with verbose flag set ("-v") # # To use this parser use the following command # ruby parseOutput.rb [options] [file] # options: -xml : produce a JUnit compatible XML file # file: file to scan for results #============================================================ # Parser class for handling the input file class ParseOutput def initialize # internal data @class_name_idx = 0 @path_delim = nil # xml output related @xml_out = false @array_list = false # current suite name and statistics @test_suite = nil @total_tests = 0 @test_passed = 0 @test_failed = 0 @test_ignored = 0 end # Set the flag to indicate if there will be an XML output file or not def set_xml_output @xml_out = true end # If write our output to XML def write_xml_output output = File.open('report.xml', 'w') output << "\n" @array_list.each do |item| output << item << "\n" end end # Pushes the suite info as xml to the array list, which will be written later def push_xml_output_suite_info # Insert opening tag at front heading = '' @array_list.insert(0, heading) # Push back the closing tag @array_list.push '' end # Pushes xml output data to the array list, which will be written later def push_xml_output_passed(test_name) @array_list.push ' ' end # Pushes xml output data to the array list, which will be written later def push_xml_output_failed(test_name, reason) @array_list.push ' ' @array_list.push ' ' + reason + '' @array_list.push ' ' end # Pushes xml output data to the array list, which will be written later def push_xml_output_ignored(test_name, reason) @array_list.push ' ' @array_list.push ' ' + reason + '' @array_list.push ' ' end # This function will try and determine when the suite is changed. This is # is the name that gets added to the classname parameter. def test_suite_verify(test_suite_name) # Split the path name test_name = test_suite_name.split(@path_delim) # Remove the extension and extract the base_name base_name = test_name[test_name.size - 1].split('.')[0] # Return if the test suite hasn't changed return unless base_name.to_s != @test_suite.to_s @test_suite = base_name printf "New Test: %s\n", @test_suite end # Prepares the line for verbose fixture output ("-v") def prepare_fixture_line(line) line = line.sub('IGNORE_TEST(', '') line = line.sub('TEST(', '') line = line.sub(')', ',') line = line.chomp array = line.split(',') array.map { |x| x.to_s.lstrip.chomp } end # Test was flagged as having passed so format the output. # This is using the Unity fixture output and not the original Unity output. def test_passed_unity_fixture(array) class_name = array[0] test_name = array[1] test_suite_verify(class_name) printf "%-40s PASS\n", test_name push_xml_output_passed(test_name) if @xml_out end # Test was flagged as having failed so format the output. # This is using the Unity fixture output and not the original Unity output. def test_failed_unity_fixture(array) class_name = array[0] test_name = array[1] test_suite_verify(class_name) reason_array = array[2].split(':') reason = reason_array[-1].lstrip.chomp + ' at line: ' + reason_array[-4] printf "%-40s FAILED\n", test_name push_xml_output_failed(test_name, reason) if @xml_out end # Test was flagged as being ignored so format the output. # This is using the Unity fixture output and not the original Unity output. def test_ignored_unity_fixture(array) class_name = array[0] test_name = array[1] reason = 'No reason given' if array.size > 2 reason_array = array[2].split(':') tmp_reason = reason_array[-1].lstrip.chomp reason = tmp_reason == 'IGNORE' ? 'No reason given' : tmp_reason end test_suite_verify(class_name) printf "%-40s IGNORED\n", test_name push_xml_output_ignored(test_name, reason) if @xml_out end # Test was flagged as having passed so format the output def test_passed(array) last_item = array.length - 1 test_name = array[last_item - 1] test_suite_verify(array[@class_name_idx]) printf "%-40s PASS\n", test_name return unless @xml_out push_xml_output_passed(test_name) if @xml_out end # Test was flagged as having failed so format the line def test_failed(array) last_item = array.length - 1 test_name = array[last_item - 2] reason = array[last_item].chomp.lstrip + ' at line: ' + array[last_item - 3] class_name = array[@class_name_idx] if test_name.start_with? 'TEST(' array2 = test_name.split(' ') test_suite = array2[0].sub('TEST(', '') test_suite = test_suite.sub(',', '') class_name = test_suite test_name = array2[1].sub(')', '') end test_suite_verify(class_name) printf "%-40s FAILED\n", test_name push_xml_output_failed(test_name, reason) if @xml_out end # Test was flagged as being ignored so format the output def test_ignored(array) last_item = array.length - 1 test_name = array[last_item - 2] reason = array[last_item].chomp.lstrip class_name = array[@class_name_idx] if test_name.start_with? 'TEST(' array2 = test_name.split(' ') test_suite = array2[0].sub('TEST(', '') test_suite = test_suite.sub(',', '') class_name = test_suite test_name = array2[1].sub(')', '') end test_suite_verify(class_name) printf "%-40s IGNORED\n", test_name push_xml_output_ignored(test_name, reason) if @xml_out end # Adjusts the os specific members according to the current path style # (Windows or Unix based) def detect_os_specifics(line) if line.include? '\\' # Windows X:\Y\Z @class_name_idx = 1 @path_delim = '\\' else # Unix Based /X/Y/Z @class_name_idx = 0 @path_delim = '/' end end # Main function used to parse the file that was captured. def process(file_name) @array_list = [] puts 'Parsing file: ' + file_name @test_passed = 0 @test_failed = 0 @test_ignored = 0 puts '' puts '=================== RESULTS =====================' puts '' File.open(file_name).each do |line| # Typical test lines look like these: # ---------------------------------------------------- # 1. normal output: # /.c:36:test_tc1000_opsys:FAIL: Expected 1 Was 0 # /.c:112:test_tc5004_initCanChannel:IGNORE: Not Yet Implemented # /.c:115:test_tc5100_initCanVoidPtrs:PASS # # 2. fixture output # /.c:63:TEST(, ):FAIL: Expected 0x00001234 Was 0x00005A5A # /.c:36:TEST(, ):IGNORE # Note: "PASS" information won't be generated in this mode # # 3. fixture output with verbose information ("-v") # TEST()/:168::FAIL: Expected 0x8D Was 0x8C # TEST(, )/:22::IGNORE: This Test Was Ignored On Purpose # IGNORE_TEST() # TEST() PASS # # Note: Where path is different on Unix vs Windows devices (Windows leads with a drive letter)! detect_os_specifics(line) line_array = line.split(':') # If we were able to split the line then we can look to see if any of our target words # were found. Case is important. next unless (line_array.size >= 4) || (line.start_with? 'TEST(') || (line.start_with? 'IGNORE_TEST(') # check if the output is fixture output (with verbose flag "-v") if (line.start_with? 'TEST(') || (line.start_with? 'IGNORE_TEST(') line_array = prepare_fixture_line(line) if line.include? ' PASS' test_passed_unity_fixture(line_array) @test_passed += 1 elsif line.include? 'FAIL' test_failed_unity_fixture(line_array) @test_failed += 1 elsif line.include? 'IGNORE' test_ignored_unity_fixture(line_array) @test_ignored += 1 end # normal output / fixture output (without verbose "-v") elsif line.include? ':PASS' test_passed(line_array) @test_passed += 1 elsif line.include? ':FAIL' test_failed(line_array) @test_failed += 1 elsif line.include? ':IGNORE:' test_ignored(line_array) @test_ignored += 1 elsif line.include? ':IGNORE' line_array.push('No reason given') test_ignored(line_array) @test_ignored += 1 end @total_tests = @test_passed + @test_failed + @test_ignored end puts '' puts '=================== SUMMARY =====================' puts '' puts 'Tests Passed : ' + @test_passed.to_s puts 'Tests Failed : ' + @test_failed.to_s puts 'Tests Ignored : ' + @test_ignored.to_s return unless @xml_out # push information about the suite push_xml_output_suite_info # write xml output file write_xml_output end end # If the command line has no values in, used a default value of Output.txt parse_my_file = ParseOutput.new if ARGV.size >= 1 ARGV.each do |arg| if arg == '-xml' parse_my_file.set_xml_output else parse_my_file.process(arg) break end end end