1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
|
require "socket"
require "time"
require "rspec/core"
require "rspec/core/formatters/base_formatter"
# Dumps rspec results as a JUnit XML file.
# Based on XML schema: http://windyroad.org/dl/Open%20Source/JUnit.xsd
class RSpecJUnitFormatter < RSpec::Core::Formatters::BaseFormatter
# rspec 2 and 3 implements are in separate files.
private
def xml_dump
output << %{<?xml version="1.0" encoding="UTF-8"?>\n}
output << %{<testsuite}
output << %{ name="rspec#{escape(ENV["TEST_ENV_NUMBER"].to_s)}"}
output << %{ tests="#{example_count}"}
output << %{ skipped="#{pending_count}"}
output << %{ failures="#{failure_count}"}
output << %{ errors="0"}
output << %{ time="#{escape("%.6f" % duration)}"}
output << %{ timestamp="#{escape(started.iso8601)}"}
output << %{ hostname="#{escape(Socket.gethostname)}"}
output << %{>\n}
output << %{<properties>\n}
output << %{<property}
output << %{ name="seed"}
output << %{ value="#{escape(RSpec.configuration.seed.to_s)}"}
output << %{/>\n}
output << %{</properties>\n}
xml_dump_examples
output << %{</testsuite>\n}
end
def xml_dump_examples
examples.each do |example|
case result_of(example)
when :pending
xml_dump_pending(example)
when :failed
xml_dump_failed(example)
else
xml_dump_example(example)
end
end
end
def xml_dump_pending(example)
xml_dump_example(example) do
output << %{<skipped/>}
end
end
def xml_dump_failed(example)
xml_dump_example(example) do
output << %{<failure}
output << %{ message="#{escape(failure_message_for(example))}"}
output << %{ type="#{escape(failure_type_for(example))}"}
output << %{>}
output << escape(failure_for(example))
output << %{</failure>}
end
end
def xml_dump_example(example)
output << %{<testcase}
output << %{ classname="#{escape(classname_for(example))}"}
output << %{ name="#{escape(description_for(example))}"}
output << %{ file="#{escape(example_group_file_path_for(example))}"}
output << %{ time="#{escape("%.6f" % duration_for(example))}"}
output << %{>}
yield if block_given?
xml_dump_output(example)
output << %{</testcase>\n}
end
def xml_dump_output(example)
if (stdout = stdout_for(example)) && !stdout.empty?
output << %{<system-out>}
output << escape(stdout)
output << %{</system-out>}
end
if (stderr = stderr_for(example)) && !stderr.empty?
output << %{<system-err>}
output << escape(stderr)
output << %{</system-err>}
end
end
# Inversion of character range from https://www.w3.org/TR/xml/#charsets
ILLEGAL_REGEXP = Regexp.new(
"[^" <<
"\u{9}" << # => \t
"\u{a}" << # => \n
"\u{d}" << # => \r
"\u{20}-\u{d7ff}" <<
"\u{e000}-\u{fffd}" <<
"\u{10000}-\u{10ffff}" <<
"]"
)
# Replace illegals with a Ruby-like escape
ILLEGAL_REPLACEMENT = Hash.new { |_, c|
x = c.ord
if x <= 0xff
"\\x%02X".freeze % x
elsif x <= 0xffff
"\\u%04X".freeze % x
else
"\\u{%X}".freeze % x
end.freeze
}.update(
"\0".freeze => "\\0".freeze,
"\a".freeze => "\\a".freeze,
"\b".freeze => "\\b".freeze,
"\f".freeze => "\\f".freeze,
"\v".freeze => "\\v".freeze,
"\e".freeze => "\\e".freeze,
).freeze
# Discouraged characters from https://www.w3.org/TR/xml/#charsets
# Plus special characters with well-known entity replacements
DISCOURAGED_REGEXP = Regexp.new(
"[" <<
"\u{22}" << # => "
"\u{26}" << # => &
"\u{27}" << # => '
"\u{3c}" << # => <
"\u{3e}" << # => >
"\u{7f}-\u{84}" <<
"\u{86}-\u{9f}" <<
"\u{fdd0}-\u{fdef}" <<
"\u{1fffe}-\u{1ffff}" <<
"\u{2fffe}-\u{2ffff}" <<
"\u{3fffe}-\u{3ffff}" <<
"\u{4fffe}-\u{4ffff}" <<
"\u{5fffe}-\u{5ffff}" <<
"\u{6fffe}-\u{6ffff}" <<
"\u{7fffe}-\u{7ffff}" <<
"\u{8fffe}-\u{8ffff}" <<
"\u{9fffe}-\u{9ffff}" <<
"\u{afffe}-\u{affff}" <<
"\u{bfffe}-\u{bffff}" <<
"\u{cfffe}-\u{cffff}" <<
"\u{dfffe}-\u{dffff}" <<
"\u{efffe}-\u{effff}" <<
"\u{ffffe}-\u{fffff}" <<
"\u{10fffe}-\u{10ffff}" <<
"]"
)
# Translate well-known entities, or use generic unicode hex entity
DISCOURAGED_REPLACEMENTS = Hash.new { |_, c| "&#x#{c.ord.to_s(16)};".freeze }.update(
?".freeze => """.freeze,
?&.freeze => "&".freeze,
?'.freeze => "'".freeze,
?<.freeze => "<".freeze,
?>.freeze => ">".freeze,
).freeze
def escape(text)
# Make sure it's utf-8, replace illegal characters with ruby-like escapes, and replace special and discouraged characters with entities
text.to_s.encode(Encoding::UTF_8).gsub(ILLEGAL_REGEXP, ILLEGAL_REPLACEMENT).gsub(DISCOURAGED_REGEXP, DISCOURAGED_REPLACEMENTS)
end
STRIP_DIFF_COLORS_BLOCK_REGEXP = /^ ( [ ]* ) Diff: (?: \e\[ 0 m )? (?: \n \1 \e\[ \d+ (?: ; \d+ )* m .* )* /x
STRIP_DIFF_COLORS_CODES_REGEXP = /\e\[ \d+ (?: ; \d+ )* m/x
def strip_diff_colors(string)
# XXX: RSpec diffs are appended to the message lines fairly early and will
# contain ANSI escape codes for colorizing terminal output if the global
# rspec configuration is turned on, regardless of which notification lines
# we ask for. We need to strip the codes from the diff part of the message
# for XML output here.
#
# We also only want to target the diff hunks because the failure message
# itself might legitimately contain ansi escape codes.
#
string.sub(STRIP_DIFF_COLORS_BLOCK_REGEXP) { |match| match.gsub(STRIP_DIFF_COLORS_CODES_REGEXP, "".freeze) }
end
end
RspecJunitFormatter = RSpecJUnitFormatter
if Gem::Version.new(RSpec::Core::Version::STRING) >= Gem::Version.new("3")
require "rspec_junit_formatter/rspec3"
else
require "rspec_junit_formatter/rspec2"
end
|