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 # parse() 65end # class OptparseExample 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 [Regexp.last_match(1).to_i, Regexp.last_match(2).to_i, Regexp.last_match(3).to_i] 156 end 157 158 def here 159 File.expand_path(File.dirname(__FILE__)) 160 end 161 162 private 163 164 def results_structure 165 { 166 source: { path: '', file: '' }, 167 successes: [], 168 failures: [], 169 ignores: [], 170 counts: { total: 0, passed: 0, failed: 0, ignored: 0 }, 171 stdout: [] 172 } 173 end 174 175 def write_xml_header(stream) 176 stream.puts "<?xml version='1.0' encoding='utf-8' ?>" 177 end 178 179 def write_suites_header(stream) 180 stream.puts '<testsuites>' 181 end 182 183 def write_suite_header(counts, stream) 184 stream.puts "\t<testsuite errors=\"0\" skipped=\"#{counts[:ignored]}\" failures=\"#{counts[:failed]}\" tests=\"#{counts[:total]}\" name=\"unity\">" 185 end 186 187 def write_failures(results, stream) 188 result = results[:failures] 189 result.each do |item| 190 filename = File.join(results[:source][:path], File.basename(results[:source][:file], '.*')) 191 stream.puts "\t\t<testcase classname=\"#{@unit_name}\" name=\"#{item[:test]}\" time=\"0\">" 192 stream.puts "\t\t\t<failure message=\"#{item[:message]}\" type=\"Assertion\"/>" 193 stream.puts "\t\t\t<system-err>
[File] #{filename}
[Line] #{item[:line]}
</system-err>" 194 stream.puts "\t\t</testcase>" 195 end 196 end 197 198 def write_tests(results, stream) 199 result = results[:successes] 200 result.each do |item| 201 stream.puts "\t\t<testcase classname=\"#{@unit_name}\" name=\"#{item[:test]}\" time=\"0\" />" 202 end 203 end 204 205 def write_ignored(results, stream) 206 result = results[:ignores] 207 result.each do |item| 208 filename = File.join(results[:source][:path], File.basename(results[:source][:file], '.*')) 209 puts "Writing ignored tests for test harness: #{filename}" 210 stream.puts "\t\t<testcase classname=\"#{@unit_name}\" name=\"#{item[:test]}\" time=\"0\">" 211 stream.puts "\t\t\t<skipped message=\"#{item[:message]}\" type=\"Assertion\"/>" 212 stream.puts "\t\t\t<system-err>
[File] #{filename}
[Line] #{item[:line]}
</system-err>" 213 stream.puts "\t\t</testcase>" 214 end 215 end 216 217 def write_suite_footer(stream) 218 stream.puts "\t</testsuite>" 219 end 220 221 def write_suites_footer(stream) 222 stream.puts '</testsuites>' 223 end 224end # UnityToJUnit 225 226if __FILE__ == $0 227 # parse out the command options 228 options = ArgvParser.parse(ARGV) 229 230 # create an instance to work with 231 utj = UnityToJUnit.new 232 begin 233 # look in the specified or current directory for result files 234 targets = "#{options.results_dir.tr('\\', '/')}**/*.test*" 235 236 results = Dir[targets] 237 raise "No *.testpass, *.testfail, or *.testresults files found in '#{targets}'" if results.empty? 238 utj.targets = results 239 240 # set the root path 241 utj.root = options.root_path 242 243 # set the output XML file name 244 # puts "Output File from options: #{options.out_file}" 245 utj.out_file = options.out_file 246 247 # run the summarizer 248 puts utj.run 249 rescue StandardError => e 250 utj.usage e.message 251 end 252end 253