module Danger # Report, or inspect any JUnit XML formatted test suite report. # # Testing frameworks have standardized on the JUnit XML format for # reporting results, this means that projects using Rspec, Jasmine, Mocha, # XCTest and more - can all use the same Danger error reporting. Perfect. # # You can see some examples on [this page from Circle CI](https://circleci.com/docs/test-metadata/) # and on this [project's README](https://github.com/orta/danger-junit.git) about how you # can add JUnit XML output for your testing projects. # # @example Parse the XML file, and let the plugin do your reporting # # junit.parse "/path/to/output.xml" # junit.report # # @example Parse multiple XML files by passing multiple file names # # junit.parse_files "/path/to/integration-tests.xml", "/path/to/unit-tests.xml" # junit.report # # @example Parse multiple XML files by passing an array # result_files = %w(/path/to/integration-tests.xml /path/to/unit-tests.xml) # junit.parse_files result_files # junit.report # # @example Let the plugin parse the XML file, and report yourself # # junit.parse "/path/to/output.xml" # fail("Tests failed") unless junit.failures.empty? # # @example Warn on a report about skipped tests # # junit.parse "/path/to/output.xml" # junit.show_skipped_tests = true # junit.report # # @example Only show specific parts of your results # # junit.parse "/path/to/output.xml" # junit.headers = [:name, :file] # junit.report # # @example Only show specific parts of your results # # junit.parse "/path/to/output.xml" # all_test = junit.tests.map(&:attributes) # slowest_test = sort_by { |attributes| attributes[:time].to_f }.last # message "#{slowest_test[:time]} took #{slowest_test[:time]} seconds" # # # @see orta/danger-junit # @see danger/danger # @see artsy/eigen # @tags testing, reporting, junit, rspec, jasmine, jest, xcpretty # class DangerJunit < Plugin # All the tests for introspection # # @return [Array] attr_accessor :tests # An array of XML elements that represent passed tests. # # @return [Array] attr_accessor :passes # An array of XML elements that represent failed tests. # # @return [Array] attr_accessor :failures # An array of XML elements that represent passed tests. # # @return [Array] attr_accessor :errors # An array of XML elements that represent skipped tests. # # @return [Array] attr_accessor :skipped # An attribute to make the plugin show a warning on skipped tests. # # @return [Bool] attr_accessor :show_skipped_tests # An array of symbols that become the columns of your tests, # if `nil`, the default, it will be all of the attributes for a single parse # or all of the common attributes between multiple files # # @return [Array] attr_accessor :headers # An array of symbols that become the columns of your skipped tests, # if `nil`, the default, it will be all of the attributes for a single parse # or all of the common attributes between multiple files # # @return [Array] attr_accessor :skipped_headers # Parses an XML file, which fills all the attributes, # will `raise` for errors # @return [void] def parse(file) parse_files(file) end # Parses multiple XML files, which fills all the attributes, # will `raise` for errors # @return [void] def parse_files(*files) require 'ox' @tests = [] failed_tests = [] Array(files).flatten.each do |file| raise "No JUnit file was found at #{file}" unless File.exist? file xml_string = File.read(file) doc = Ox.parse(xml_string) suite_root = doc.nodes.first.value == 'testsuites' ? doc.nodes.first : doc @tests += suite_root.nodes.map(&:nodes).flatten.select { |node| node.kind_of?(Ox::Element) && node.value == 'testcase' } failed_suites = suite_root.nodes.select { |suite| suite[:failures].to_i > 0 || suite[:errors].to_i > 0 } failed_tests += failed_suites.map(&:nodes).flatten.select { |node| node.kind_of?(Ox::Element) && node.value == 'testcase' } end @failures = failed_tests.select do |test| test.nodes.count > 0 end.select do |test| node = test.nodes.first node.kind_of?(Ox::Element) && node.value == 'failure' end @errors = failed_tests.select do |test| test.nodes.count > 0 end.select do |test| node = test.nodes.first node.kind_of?(Ox::Element) && node.value == 'error' end @skipped = tests.select do |test| test.nodes.count > 0 end.select do |test| node = test.nodes.first node.kind_of?(Ox::Element) && node.value == 'skipped' end @passes = tests - @failures - @errors - @skipped end # Causes a build fail if there are test failures, # and outputs a markdown table of the results. # # @return [void] def report return if failures.nil? # because danger calls `report` before loading a file if show_skipped_tests && skipped.count > 0 warn("Skipped #{skipped.count} tests.") message = "### Skipped: \n\n" message << get_report_content(skipped, skipped_headers) markdown message end unless failures.empty? && errors.empty? fail('Tests have failed, see below for more information.', sticky: false) message = "### Tests: \n\n" tests = (failures + errors) message << get_report_content(tests, headers) markdown message end end private def get_report_content(tests, headers) message = '' common_attributes = tests.map{|test| test.attributes.keys }.inject(&:&) # check the provided headers are available unless headers.nil? not_available_headers = headers.select { |header| not common_attributes.include?(header) } raise "Some of headers provided aren't available in the JUnit report (#{not_available_headers})" unless not_available_headers.empty? end keys = headers || common_attributes attributes = keys.map(&:to_s).map(&:capitalize) # Create the headers message << attributes.join(' | ') + "|\n" message << attributes.map { |_| '---' }.join(' | ') + "|\n" # Map out the keys to the tests tests.each do |test| row_values = keys.map { |key| test.attributes[key] }.map { |v| auto_link(v) } message << row_values.join(' | ') + "|\n" end message end def auto_link(value) if File.exist?(value) && defined?(@dangerfile.github) github.html_link(value, full_path: false) else value end end end end