• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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