class Minitest::Reporters::JUnitReporter
Also inspired by minitest-ci (see github.com/bhenderson/minitest-ci)
Also inspired by Marc Seeger’s attempt at producing a JUnitReporter (see github.com/rb2k/minitest-reporters/commit/e13d95b5f884453a9c77f62bc5cba3fa1df30ef5)
Inspired by ci_reporter (see github.com/nicksieger/ci_reporter)
Intended for easy integration with CI servers - tested on JetBrains TeamCity
A reporter for writing JUnit test reports
def analyze_suite(tests)
def analyze_suite(tests) result = Hash.new(0) result[:time] = 0 tests.each do |test| result[:"#{result(test)}_count"] += 1 result[:assertion_count] += test.assertions result[:test_count] += 1 result[:time] += test.time result[:timestamp] = Time.now.iso8601 if @timestamp_report end result end
def filename_for(suite)
def filename_for(suite) file_counter = 0 # restrict max filename length, to be kind to filesystems suite_name = suite.to_s[0..240].gsub(/[^a-zA-Z0-9]+/, '-') filename = "TEST-#{suite_name}.xml" while File.exist?(File.join(@reports_path, filename)) # restrict number of tries, to avoid infinite loops file_counter += 1 filename = "TEST-#{suite_name}-#{file_counter}.xml" if file_counter >= 99 puts "Too many duplicate files, overwriting earlier report #{filename}" break end end File.join(@reports_path, filename) end
def get_relative_path(result)
def get_relative_path(result) file_path = Pathname.new(get_source_location(result).first) base_path = Pathname.new(@base_path) if file_path.absolute? file_path.relative_path_from(base_path) else file_path end end
def get_source_location(result)
def get_source_location(result) if result.respond_to? :source_location result.source_location else result.method(result.name).source_location end end
def initialize(reports_dir = DEFAULT_REPORTS_DIR, empty = true, options = {})
def initialize(reports_dir = DEFAULT_REPORTS_DIR, empty = true, options = {}) super({}) @reports_path = File.absolute_path(ENV.fetch("MINITEST_REPORTERS_REPORTS_DIR", reports_dir)) @single_file = options[:single_file] @base_path = options[:base_path] || Dir.pwd @timestamp_report = options[:include_timestamp] return unless empty puts "Emptying #{@reports_path}" FileUtils.mkdir_p(@reports_path) File.delete(*Dir.glob("#{@reports_path}/TEST-*.xml")) end
def location(exception)
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 message_for(test)
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? "Error:\n#{name}(#{suite}):\n#{e.message}" end end
def parse_xml_for(xml, suite, tests)
def parse_xml_for(xml, suite, tests) suite_result = analyze_suite(tests) file_path = get_relative_path(tests.first) if @timestamp_report xml.testsuite(:name => suite, :filepath => file_path, :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], :timestamp => suite_result[:timestamp]) do tests.each do |test| lineno = get_source_location(test).last xml.testcase(:name => test.name, :lineno => lineno, :classname => suite, :assertions => test.assertions, :time => test.time, :file => file_path) do xml << xml_message_for(test) unless test.passed? end end end else xml.testsuite(:name => suite, :filepath => file_path, :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| lineno = get_source_location(test).last xml.testcase(:name => test.name, :lineno => lineno, :classname => suite, :assertions => test.assertions, :time => test.time, :file => file_path) do xml << xml_message_for(test) unless test.passed? end end end end end
def report
def report super puts "Writing XML reports to #{@reports_path}" suites = tests.group_by do |test| test_class(test) end if @single_file xml = Builder::XmlMarkup.new(:indent => 2) xml.instruct! xml.testsuites do suites.each do |suite, tests| parse_xml_for(xml, suite, tests) end end File.open(filename_for('minitest'), "w") { |file| file << xml.target! } else suites.each do |suite, tests| xml = Builder::XmlMarkup.new(:indent => 2) xml.instruct! xml.testsuites do parse_xml_for(xml, suite, tests) end File.open(filename_for(suite), "w") { |file| file << xml.target! } end end end
def xml_message_for(test)
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 failure = test.failure if test.skipped? xml.skipped(:type => failure.error.class.name) elsif test.error? xml.error(:type => failure.error.class.name, :message => xml.trunc!(failure.message)) do xml.text!(message_for(test)) end elsif failure xml.failure(:type => failure.error.class.name, :message => xml.trunc!(failure.message)) do xml.text!(message_for(test)) end end end