| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- #============================================================
- # 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")
- # - time output flag set (UNITY_INCLUDE_EXEC_TIME define enabled with milliseconds output)
- #
- # To use this parser use the following command
- # ruby parseOutput.rb [options] [file]
- # options: -xml : produce a JUnit compatible XML file
- # -suiteRequiredSuiteName
- # : replace default test suite name to
- # "RequiredSuiteName" (can be any name)
- # file: file to scan for results
- #============================================================
- # Parser class for handling the input file
- class ParseOutput
- def initialize
- # internal data
- @class_name_idx = 0
- @result_usual_idx = 3
- @path_delim = nil
- # xml output related
- @xml_out = false
- @array_list = false
- # current suite name and statistics
- ## testsuite name
- @real_test_suite_name = 'Unity'
- ## classname for testcase
- @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
- # Set the flag to indicate if there will be an XML output file or not
- def test_suite_name=(cli_arg)
- @real_test_suite_name = cli_arg
- puts "Real test suite name will be '#{@real_test_suite_name}'"
- end
- def xml_encode_s(str)
- str.encode(:xml => :attr)
- end
- # If write our output to XML
- def write_xml_output
- output = File.open('report.xml', 'w')
- output << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\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 = "<testsuite name=#{xml_encode_s(@real_test_suite_name)} tests=\"#{@total_tests}\" failures=\"#{@test_failed}\" skips=\"#{@test_ignored}\">"
- @array_list.insert(0, heading)
- # Push back the closing tag
- @array_list.push '</testsuite>'
- end
- # Pushes xml output data to the array list, which will be written later
- def push_xml_output_passed(test_name, execution_time = 0)
- @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)} />"
- end
- # Pushes xml output data to the array list, which will be written later
- def push_xml_output_failed(test_name, reason, execution_time = 0)
- @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)} >"
- @array_list.push " <failure type=\"ASSERT FAILED\">#{reason}</failure>"
- @array_list.push ' </testcase>'
- end
- # Pushes xml output data to the array list, which will be written later
- def push_xml_output_ignored(test_name, reason, execution_time = 0)
- @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)} >"
- @array_list.push " <skipped type=\"TEST IGNORED\">#{reason}</skipped>"
- @array_list.push ' </testcase>'
- 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)
- # ':' symbol will be valid in function args now
- real_method_name = array[@result_usual_idx - 1..-2].join(':')
- array = array[0..@result_usual_idx - 2] + [real_method_name] + [array[-1]]
- last_item = array.length - 1
- test_time = get_test_time(array[last_item])
- test_name = array[last_item - 1]
- test_suite_verify(array[@class_name_idx])
- printf "%-40s PASS %10d ms\n", test_name, test_time
- return unless @xml_out
- push_xml_output_passed(test_name, test_time) if @xml_out
- end
- # Test was flagged as having failed so format the line
- def test_failed(array)
- # ':' symbol will be valid in function args now
- real_method_name = array[@result_usual_idx - 1..-3].join(':')
- array = array[0..@result_usual_idx - 3] + [real_method_name] + array[-2..]
- last_item = array.length - 1
- test_time = get_test_time(array[last_item])
- 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 %10d ms\n", test_name, test_time
- push_xml_output_failed(test_name, reason, test_time) if @xml_out
- end
- # Test was flagged as being ignored so format the output
- def test_ignored(array)
- # ':' symbol will be valid in function args now
- real_method_name = array[@result_usual_idx - 1..-3].join(':')
- array = array[0..@result_usual_idx - 3] + [real_method_name] + array[-2..]
- last_item = array.length - 1
- test_time = get_test_time(array[last_item])
- 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 %10d ms\n", test_name, test_time
- push_xml_output_ignored(test_name, reason, test_time) if @xml_out
- end
- # Test time will be in ms
- def get_test_time(value_with_time)
- test_time_array = value_with_time.scan(/\((-?\d+.?\d*) ms\)\s*$/).flatten.map do |arg_value_str|
- arg_value_str.include?('.') ? arg_value_str.to_f : arg_value_str.to_i
- end
- test_time_array.any? ? test_time_array[0] : 0
- 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 ''
- # Apply binary encoding. Bad symbols will be unchanged
- File.open(file_name, 'rb').each do |line|
- # Typical test lines look like these:
- # ----------------------------------------------------
- # 1. normal output:
- # <path>/<test_file>.c:36:test_tc1000_opsys:FAIL: Expected 1 Was 0
- # <path>/<test_file>.c:112:test_tc5004_initCanChannel:IGNORE: Not Yet Implemented
- # <path>/<test_file>.c:115:test_tc5100_initCanVoidPtrs:PASS
- #
- # 2. fixture output
- # <path>/<test_file>.c:63:TEST(<test_group>, <test_function>):FAIL: Expected 0x00001234 Was 0x00005A5A
- # <path>/<test_file>.c:36:TEST(<test_group>, <test_function>):IGNORE
- # Note: "PASS" information won't be generated in this mode
- #
- # 3. fixture output with verbose information ("-v")
- # TEST(<test_group, <test_file>)<path>/<test_file>:168::FAIL: Expected 0x8D Was 0x8C
- # TEST(<test_group>, <test_file>)<path>/<test_file>:22::IGNORE: This Test Was Ignored On Purpose
- # IGNORE_TEST(<test_group, <test_file>)
- # TEST(<test_group, <test_file>) 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
- elsif line_array.size >= 4
- # We will check output from color compilation
- if line_array[@result_usual_idx..].any? { |l| l.include? 'PASS' }
- test_passed(line_array)
- @test_passed += 1
- elsif line_array[@result_usual_idx..].any? { |l| l.include? 'FAIL' }
- test_failed(line_array)
- @test_failed += 1
- elsif line_array[@result_usual_idx..-2].any? { |l| l.include? 'IGNORE' }
- test_ignored(line_array)
- @test_ignored += 1
- elsif line_array[@result_usual_idx..].any? { |l| l.include? 'IGNORE' }
- line_array.push("No reason given (#{get_test_time(line_array[@result_usual_idx..])} ms)")
- test_ignored(line_array)
- @test_ignored += 1
- end
- end
- @total_tests = @test_passed + @test_failed + @test_ignored
- end
- puts ''
- puts '=================== SUMMARY ====================='
- puts ''
- puts "Tests Passed : #{@test_passed}"
- puts "Tests Failed : #{@test_failed}"
- puts "Tests Ignored : #{@test_ignored}"
- 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
- elsif arg.start_with?('-suite')
- parse_my_file.test_suite_name = arg.delete_prefix('-suite')
- else
- parse_my_file.process(arg)
- break
- end
- end
- end
|