require 'ansi/code' require 'builder' require 'fileutils' module Minitest module Reporters # A reporter for writing JUnit test reports # Intended for easy integration with CI servers - tested on JetBrains TeamCity # # Inspired by ci_reporter (see https://github.com/nicksieger/ci_reporter) # Also inspired by Marc Seeger's attempt at producing a JUnitReporter (see https://github.com/rb2k/minitest-reporters/commit/e13d95b5f884453a9c77f62bc5cba3fa1df30ef5) # Also inspired by minitest-ci (see https://github.com/bhenderson/minitest-ci) class JUnitReporter < BaseReporter def initialize(reports_dir = "test/reports", empty = true) super({}) @reports_path = File.absolute_path(reports_dir) if empty puts "Emptying #{@reports_path}" FileUtils.remove_dir(@reports_path) if File.exists?(@reports_path) FileUtils.mkdir_p(@reports_path) end end def report super puts "Writing XML reports to #{@reports_path}" suites = tests.group_by(&:class) suites.each do |suite, tests| suite_result = analyze_suite(tests) xml = Builder::XmlMarkup.new(:indent => 2) xml.instruct! xml.testsuite(:name => suite, :skipped => suite_result[:skip_count], :failures => suite_result[:fail_count], :errors => suite_result[:error_count], :tests => suite_result[:test_count], :assertions => suite_result[:assertion_count], :time => suite_result[:time]) do tests.each do |test| xml.testcase(:name => test.name, :classname => suite, :assertions => test.assertions, :time => test.time) do xml << xml_message_for(test) unless test.passed? end end end File.open(filename_for(suite), "w") { |file| file << xml.target! } end end private def xml_message_for(test) # This is a trick lifted from ci_reporter xml = Builder::XmlMarkup.new(:indent => 2, :margin => 2) def xml.trunc!(txt) txt.sub(/\n.*/m, '...') end e = test.failure if test.skipped? xml.skipped(:type => test.name) elsif test.error? xml.error(:type => test.name, :message => xml.trunc!(e.message)) do xml.text!(message_for(test)) end elsif test.failure xml.failure(:type => test.name, :message => xml.trunc!(e.message)) do xml.text!(message_for(test)) end end end def message_for(test) suite = test.class name = test.name e = test.failure if test.passed? nil elsif test.skipped? "Skipped:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n" elsif test.failure "Failure:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n" elsif test.error? bt = filter_backtrace(test.exception.backtrace).join "\n " "Error:\n#{name}(#{suite}):\n#{e.class}: #{e.message}\n #{bt}\n" end end def location(exception) last_before_assertion = '' exception.backtrace.reverse_each do |s| break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/ last_before_assertion = s end last_before_assertion.sub(/:in .*$/, '') end def analyze_suite(tests) result = Hash.new(0) tests.each do |test| result[:"#{result(test)}_count"] += 1 result[:assertion_count] += test.assertions result[:test_count] += 1 result[:time] += test.time end result end def filename_for(suite) file_counter = 0 filename = "TEST-#{suite.to_s[0..240].gsub(/[^a-zA-Z0-9]+/, '-')}.xml" #restrict max filename length, to be kind to filesystems while File.exists?(File.join(@reports_path, filename)) # restrict number of tries, to avoid infinite loops file_counter += 1 filename = "TEST-#{suite}-#{file_counter}.xml" puts "Too many duplicate files, overwriting earlier report #{filename}" and break if file_counter >= 99 end File.join(@reports_path, filename) end end end end