Merge pull request #325 from farrrb/fix-parse.rb

Bugfixes and refactoring in parse.rb (thanks @farrrb and @jeremyhannon )
This commit is contained in:
Mark VanderVoord
2018-05-03 07:25:16 -04:00
committed by GitHub

View File

@ -1,28 +1,43 @@
#============================================================ #============================================================
# Author: John Theofanopoulos # Author: John Theofanopoulos
# A simple parser. Takes the output files generated during the build process and # A simple parser. Takes the output files generated during the
# extracts information relating to the tests. # build process and extracts information relating to the tests.
# #
# Notes: # Notes:
# To capture an output file under VS builds use the following: # To capture an output file under VS builds use the following:
# devenv [build instructions] > Output.txt & type Output.txt # devenv [build instructions] > Output.txt & type Output.txt
# #
# To capture an output file under GCC/Linux builds use the following: # To capture an output file under Linux builds use the following:
# make | tee Output.txt # 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 # To use this parser use the following command
# ruby parseOutput.rb [options] [file] # ruby parseOutput.rb [options] [file]
# options: -xml : produce a JUnit compatible XML file # options: -xml : produce a JUnit compatible XML file
# file: file to scan for results # file: file to scan for results
#============================================================ #============================================================
# Parser class for handling the input file
class ParseOutput class ParseOutput
def initialize def initialize
@test_flag = false # internal data
@class_name_idx = 0
@path_delim = nil
# xml output related
@xml_out = false @xml_out = false
@array_list = false @array_list = false
@total_tests = false
@class_index = false # current suite name and statistics
@test_suite = nil
@total_tests = 0
@test_passed = 0
@test_failed = 0
@test_ignored = 0
end end
# Set the flag to indicate if there will be an XML output file or not # Set the flag to indicate if there will be an XML output file or not
@ -30,79 +45,121 @@ class ParseOutput
@xml_out = true @xml_out = true
end end
# if write our output to XML # If write our output to XML
def write_xml_output def write_xml_output
output = File.open('report.xml', 'w') output = File.open('report.xml', 'w')
output << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" output << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
@array_list.each do |item| @array_list.each do |item|
output << item << "\n" output << item << "\n"
end end
output << "</testsuite>\n" 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="Unity" tests="' + @total_tests.to_s + '" failures="' + @test_failed.to_s + '"' + ' skips="' + @test_ignored.to_s + '">'
@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)
@array_list.push ' <testcase classname="' + @test_suite + '" name="' + test_name + '"/>'
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 ' <testcase classname="' + @test_suite + '" name="' + test_name + '">'
@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)
@array_list.push ' <testcase classname="' + @test_suite + '" name="' + test_name + '">'
@array_list.push ' <skipped type="TEST IGNORED">' + reason + '</skipped>'
@array_list.push ' </testcase>'
end end
# This function will try and determine when the suite is changed. This is # This function will try and determine when the suite is changed. This is
# is the name that gets added to the classname parameter. # is the name that gets added to the classname parameter.
def test_suite_verify(test_suite_name) def test_suite_verify(test_suite_name)
return if @test_flag
@test_flag = true
# Split the path name # Split the path name
test_name = if @class_name == 1 test_name = test_suite_name.split(@path_delim)
test_suite_name.split('\\') # Windows
else # Remove the extension and extract the base_name
test_suite_name.split('/') # Unix based 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 end
# Remove the extension # Prepares the line for verbose fixture output ("-v")
base_name = test_name[test_name.size - 1].split('.') def prepare_fixture_line(line)
@test_suite = 'test.' + base_name[0] line = line.sub('IGNORE_TEST(', '')
printf "New Test: %s\n", @test_suite 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 end
# Test was flagged as having passed so format the output # Test was flagged as having passed so format the output
def test_passed(array) def test_passed(array)
last_item = array.length - 1 last_item = array.length - 1
test_name = array[last_item - 1] test_name = array[last_item - 1]
test_suite_verify(array[@class_name]) test_suite_verify(array[@class_name_idx])
printf "%-40s PASS\n", test_name printf "%-40s PASS\n", test_name
return unless @xml_out return unless @xml_out
@array_list.push ' <testcase classname="' + @test_suite + '" name="' + test_name + '"/>' push_xml_output_passed(test_name) if @xml_out
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)
test_suite = array[0].sub('TEST(', '')
test_suite = test_suite.sub(',', '')
test_name = array[1].sub(')', '')
return unless @xml_out
@array_list.push ' <testcase classname="' + test_suite + '" name="' + test_name + '"/>'
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
test_suite_verify(array[@class_name])
printf "%-40s IGNORED\n", test_name
if test_name.start_with? 'TEST('
array2 = test_name.split(' ')
@test_suite = array2[0].sub('TEST(', '')
@test_suite = @test_suite.sub(',', '')
test_name = array2[1].sub(')', '')
end
return unless @xml_out
@array_list.push ' <testcase classname="' + @test_suite + '" name="' + test_name + '">'
@array_list.push ' <skipped type="TEST IGNORED">' + reason + '</skipped>'
@array_list.push ' </testcase>'
end end
# Test was flagged as having failed so format the line # Test was flagged as having failed so format the line
@ -110,106 +167,143 @@ class ParseOutput
last_item = array.length - 1 last_item = array.length - 1
test_name = array[last_item - 2] test_name = array[last_item - 2]
reason = array[last_item].chomp.lstrip + ' at line: ' + array[last_item - 3] reason = array[last_item].chomp.lstrip + ' at line: ' + array[last_item - 3]
test_suite_verify(array[@class_name]) class_name = array[@class_name_idx]
printf "%-40s FAILED\n", test_name
if test_name.start_with? 'TEST(' if test_name.start_with? 'TEST('
array2 = test_name.split(' ') array2 = test_name.split(' ')
@test_suite = array2[0].sub('TEST(', '')
@test_suite = @test_suite.sub(',', '') test_suite = array2[0].sub('TEST(', '')
test_suite = test_suite.sub(',', '')
class_name = test_suite
test_name = array2[1].sub(')', '') test_name = array2[1].sub(')', '')
end end
return unless @xml_out test_suite_verify(class_name)
printf "%-40s FAILED\n", test_name
@array_list.push ' <testcase classname="' + @test_suite + '" name="' + test_name + '">' push_xml_output_failed(test_name, reason) if @xml_out
@array_list.push ' <failure type="ASSERT FAILED">' + reason + '</failure>'
@array_list.push ' </testcase>'
end end
# Figure out what OS we are running on. For now we are assuming if it's not Windows it must # Test was flagged as being ignored so format the output
# be Unix based. def test_ignored(array)
def detect_os last_item = array.length - 1
os = RUBY_PLATFORM.split('-') test_name = array[last_item - 2]
@class_name = if os.size == 2 reason = array[last_item].chomp.lstrip
if os[1] == 'mingw32' class_name = array[@class_name_idx]
1
else if test_name.start_with? 'TEST('
0 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 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 set_os_specifics(line)
if line.include? '\\'
# Windows X:\Y\Z
@class_name_idx = 1
@path_delim = '\\'
else else
0 # Unix Based /X/Y/Z
@class_name_idx = 0
@path_delim = '/'
end end
end end
# Main function used to parse the file that was captured. # Main function used to parse the file that was captured.
def process(name) def process(file_name)
@test_flag = false
@array_list = [] @array_list = []
detect_os puts 'Parsing file: ' + file_name
puts 'Parsing file: ' + name @test_passed = 0
@test_failed = 0
test_pass = 0 @test_ignored = 0
test_fail = 0
test_ignore = 0
puts '' puts ''
puts '=================== RESULTS =====================' puts '=================== RESULTS ====================='
puts '' puts ''
File.open(name).each do |line| File.open(file_name).each do |line|
# Typical test lines look like this: # 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: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:112:test_tc5004_initCanChannel:IGNORE: Not Yet Implemented
# <path>/<test_file>.c:115:test_tc5100_initCanVoidPtrs:PASS # <path>/<test_file>.c:115:test_tc5100_initCanVoidPtrs:PASS
# #
# where path is different on Unix vs Windows devices (Windows leads with a drive letter) # 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)!
set_os_specifics(line)
line_array = line.split(':') line_array = line.split(':')
# If we were able to split the line then we can look to see if any of our target words # 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. # were found. Case is important.
if (line_array.size >= 4) || (line.start_with? 'TEST(') if (line_array.size >= 4) || (line.start_with? 'TEST(') || (line.start_with? 'IGNORE_TEST(')
# Determine if this test passed
if line.include? ':PASS' # 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(line_array)
test_pass += 1 @test_passed += 1
elsif line.include? ':FAIL' elsif line.include? ':FAIL'
test_failed(line_array) test_failed(line_array)
test_fail += 1 @test_failed += 1
elsif line.include? ':IGNORE:' elsif line.include? ':IGNORE:'
test_ignored(line_array) test_ignored(line_array)
test_ignore += 1 @test_ignored += 1
elsif line.include? ':IGNORE' elsif line.include? ':IGNORE'
line_array.push('No reason given') line_array.push('No reason given')
test_ignored(line_array) test_ignored(line_array)
test_ignore += 1 @test_ignored += 1
elsif line.start_with? 'TEST('
if line.include? ' PASS'
line_array = line.split(' ')
test_passed_unity_fixture(line_array)
test_pass += 1
end end
# If none of the keywords are found there are no more tests for this suite so clear @total_tests = @test_passed + @test_failed + @test_ignored
# the test flag
else
@test_flag = false
end
else
@test_flag = false
end end
end end
puts '' puts ''
puts '=================== SUMMARY =====================' puts '=================== SUMMARY ====================='
puts '' puts ''
puts 'Tests Passed : ' + test_pass.to_s puts 'Tests Passed : ' + @test_passed.to_s
puts 'Tests Failed : ' + test_fail.to_s puts 'Tests Failed : ' + @test_failed.to_s
puts 'Tests Ignored : ' + test_ignore.to_s puts 'Tests Ignored : ' + @test_ignored.to_s
@total_tests = test_pass + test_fail + test_ignore
return unless @xml_out return unless @xml_out
heading = '<testsuite name="' + @test_suite.to_s + '" tests="' + @total_tests.to_s + '" failures="' + test_fail.to_s + '"' + ' skips="' + test_ignore.to_s + '">' # push information about the suite
@array_list.insert(0, heading) push_xml_output_suite_info
# write xml output file
write_xml_output write_xml_output
end end
end end
@ -218,11 +312,11 @@ end
parse_my_file = ParseOutput.new parse_my_file = ParseOutput.new
if ARGV.size >= 1 if ARGV.size >= 1
ARGV.each do |a| ARGV.each do |arg|
if a == '-xml' if arg == '-xml'
parse_my_file.set_xml_output parse_my_file.set_xml_output
else else
parse_my_file.process(a) parse_my_file.process(arg)
break break
end end
end end