xref: /third_party/unity/auto/parse_output.rb (revision 35375f98)
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