• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/ruby
2#
3# unity_to_junit.rb
4#
5require 'fileutils'
6require 'optparse'
7require 'ostruct'
8require 'set'
9
10require 'pp'
11
12VERSION = 1.0
13
14class ArgvParser
15  #
16  # Return a structure describing the options.
17  #
18  def self.parse(args)
19    # The options specified on the command line will be collected in *options*.
20    # We set default values here.
21    options = OpenStruct.new
22    options.results_dir = '.'
23    options.root_path = '.'
24    options.out_file = 'results.xml'
25
26    opts = OptionParser.new do |o|
27      o.banner = 'Usage: unity_to_junit.rb [options]'
28
29      o.separator ''
30      o.separator 'Specific options:'
31
32      o.on('-r', '--results <dir>', 'Look for Unity Results files here.') do |results|
33        # puts "results #{results}"
34        options.results_dir = results
35      end
36
37      o.on('-p', '--root_path <path>', 'Prepend this path to files in results.') do |root_path|
38        options.root_path = root_path
39      end
40
41      o.on('-o', '--output <filename>', 'XML file to generate.') do |out_file|
42        # puts "out_file: #{out_file}"
43        options.out_file = out_file
44      end
45
46      o.separator ''
47      o.separator 'Common options:'
48
49      # No argument, shows at tail.  This will print an options summary.
50      o.on_tail('-h', '--help', 'Show this message') do
51        puts o
52        exit
53      end
54
55      # Another typical switch to print the version.
56      o.on_tail('--version', 'Show version') do
57        puts "unity_to_junit.rb version #{VERSION}"
58        exit
59      end
60    end
61
62    opts.parse!(args)
63    options
64  end
65end
66
67class UnityToJUnit
68  include FileUtils::Verbose
69  attr_reader :report, :total_tests, :failures, :ignored
70  attr_writer :targets, :root, :out_file
71
72  def initialize
73    @report = ''
74    @unit_name = ''
75  end
76
77  def run
78    # Clean up result file names
79    results = @targets.map { |target| target.tr('\\', '/') }
80    # puts "Output File: #{@out_file}"
81    f = File.new(@out_file, 'w')
82    write_xml_header(f)
83    write_suites_header(f)
84    results.each do |result_file|
85      lines = File.readlines(result_file).map(&:chomp)
86
87      raise "Empty test result file: #{result_file}" if lines.empty?
88
89      result_output = get_details(result_file, lines)
90      tests, failures, ignored = parse_test_summary(lines)
91      result_output[:counts][:total] = tests
92      result_output[:counts][:failed] = failures
93      result_output[:counts][:ignored] = ignored
94      result_output[:counts][:passed] = (result_output[:counts][:total] - result_output[:counts][:failed] - result_output[:counts][:ignored])
95
96      # use line[0] from the test output to get the test_file path and name
97      test_file_str = lines[0].tr('\\', '/')
98      test_file_str = test_file_str.split(':')
99      test_file = if test_file_str.length < 2
100                    result_file
101                  else
102                    test_file_str[0] + ':' + test_file_str[1]
103                  end
104      result_output[:source][:path] = File.dirname(test_file)
105      result_output[:source][:file] = File.basename(test_file)
106
107      # save result_output
108      @unit_name = File.basename(test_file, '.*')
109
110      write_suite_header(result_output[:counts], f)
111      write_failures(result_output, f)
112      write_tests(result_output, f)
113      write_ignored(result_output, f)
114      write_suite_footer(f)
115    end
116    write_suites_footer(f)
117    f.close
118  end
119
120  def usage(err_msg = nil)
121    puts "\nERROR: "
122    puts err_msg if err_msg
123    puts 'Usage: unity_to_junit.rb [options]'
124    puts ''
125    puts 'Specific options:'
126    puts '    -r, --results <dir>              Look for Unity Results files here.'
127    puts '    -p, --root_path <path>           Prepend this path to files in results.'
128    puts '    -o, --output <filename>          XML file to generate.'
129    puts ''
130    puts 'Common options:'
131    puts '    -h, --help                       Show this message'
132    puts '        --version                    Show version'
133
134    exit 1
135  end
136
137  protected
138
139  def get_details(_result_file, lines)
140    results = results_structure
141    lines.each do |line|
142      line = line.tr('\\', '/')
143      _src_file, src_line, test_name, status, msg = line.split(/:/)
144      case status
145      when 'IGNORE' then results[:ignores] << { test: test_name, line: src_line, message: msg }
146      when 'FAIL'   then results[:failures] << { test: test_name, line: src_line, message: msg }
147      when 'PASS'   then results[:successes] << { test: test_name, line: src_line, message: msg }
148      end
149    end
150    results
151  end
152
153  def parse_test_summary(summary)
154    raise "Couldn't parse test results: #{summary}" unless summary.find { |v| v =~ /(\d+) Tests (\d+) Failures (\d+) Ignored/ }
155
156    [Regexp.last_match(1).to_i, Regexp.last_match(2).to_i, Regexp.last_match(3).to_i]
157  end
158
159  private
160
161  def results_structure
162    {
163      source: { path: '', file: '' },
164      successes: [],
165      failures: [],
166      ignores: [],
167      counts: { total: 0, passed: 0, failed: 0, ignored: 0 },
168      stdout: []
169    }
170  end
171
172  def write_xml_header(stream)
173    stream.puts "<?xml version='1.0' encoding='utf-8' ?>"
174  end
175
176  def write_suites_header(stream)
177    stream.puts '<testsuites>'
178  end
179
180  def write_suite_header(counts, stream)
181    stream.puts "\t<testsuite errors=\"0\" skipped=\"#{counts[:ignored]}\" failures=\"#{counts[:failed]}\" tests=\"#{counts[:total]}\" name=\"unity\">"
182  end
183
184  def write_failures(results, stream)
185    result = results[:failures]
186    result.each do |item|
187      filename = File.join(results[:source][:path], File.basename(results[:source][:file], '.*'))
188      stream.puts "\t\t<testcase classname=\"#{@unit_name}\" name=\"#{item[:test]}\" time=\"0\">"
189      stream.puts "\t\t\t<failure message=\"#{item[:message]}\" type=\"Assertion\"/>"
190      stream.puts "\t\t\t<system-err>&#xD;[File] #{filename}&#xD;[Line] #{item[:line]}&#xD;</system-err>"
191      stream.puts "\t\t</testcase>"
192    end
193  end
194
195  def write_tests(results, stream)
196    result = results[:successes]
197    result.each do |item|
198      stream.puts "\t\t<testcase classname=\"#{@unit_name}\" name=\"#{item[:test]}\" time=\"0\" />"
199    end
200  end
201
202  def write_ignored(results, stream)
203    result = results[:ignores]
204    result.each do |item|
205      filename = File.join(results[:source][:path], File.basename(results[:source][:file], '.*'))
206      puts "Writing ignored tests for test harness: #{filename}"
207      stream.puts "\t\t<testcase classname=\"#{@unit_name}\" name=\"#{item[:test]}\" time=\"0\">"
208      stream.puts "\t\t\t<skipped message=\"#{item[:message]}\" type=\"Assertion\"/>"
209      stream.puts "\t\t\t<system-err>&#xD;[File] #{filename}&#xD;[Line] #{item[:line]}&#xD;</system-err>"
210      stream.puts "\t\t</testcase>"
211    end
212  end
213
214  def write_suite_footer(stream)
215    stream.puts "\t</testsuite>"
216  end
217
218  def write_suites_footer(stream)
219    stream.puts '</testsuites>'
220  end
221end
222
223if $0 == __FILE__
224  # parse out the command options
225  options = ArgvParser.parse(ARGV)
226
227  # create an instance to work with
228  utj = UnityToJUnit.new
229  begin
230    # look in the specified or current directory for result files
231    targets = "#{options.results_dir.tr('\\', '/')}**/*.test*"
232
233    results = Dir[targets]
234
235    raise "No *.testpass, *.testfail, or *.testresults files found in '#{targets}'" if results.empty?
236
237    utj.targets = results
238
239    # set the root path
240    utj.root = options.root_path
241
242    # set the output XML file name
243    # puts "Output File from options: #{options.out_file}"
244    utj.out_file = options.out_file
245
246    # run the summarizer
247    puts utj.run
248  rescue StandardError => e
249    utj.usage e.message
250  end
251end
252