Module: Polyrun::Reporting::Junit

Defined in:
lib/polyrun/reporting/junit.rb,
lib/polyrun/reporting/junit_emit.rb

Overview

JUnit XML for CI (replaces rspec_junit_formatter) — stdlib only.

Input JSON may be:

  • **RSpec JSON** — output of rspec –format json –out rspec.json (examples array).

  • **Polyrun canonical** — { “name” => “…”, “testcases” => [ … ] } (see emit_xml).

Each testcase hash supports: classname, name, time, status (passed, failed, pending/skipped), and optional failure => { “message” => “…”, “body” => “…” }.

Class Method Summary collapse

Class Method Details

.emit_xml(doc) ⇒ Object

doc is { “name”, “hostname”, “testcases” => [ … ] }



7
8
9
10
11
12
13
14
15
16
17
# File 'lib/polyrun/reporting/junit_emit.rb', line 7

def emit_xml(doc)
  cases = doc["testcases"] || []
  lines = []
  lines << junit_xml_header(doc, cases)
  cases.each do |c|
    lines << junit_xml_testcase_line(c)
  end
  lines << %(</testsuite>)
  lines << %(</testsuites>)
  lines.join("\n") + "\n"
end

.esc(s) ⇒ Object



118
119
120
# File 'lib/polyrun/reporting/junit.rb', line 118

def esc(s)
  CGI.escapeHTML(s.to_s)
end

.format_float(x) ⇒ Object



114
115
116
# File 'lib/polyrun/reporting/junit.rb', line 114

def format_float(x)
  format("%.6f", x.to_f)
end

.from_polyrun_hash(data) ⇒ Object



92
93
94
95
96
97
98
# File 'lib/polyrun/reporting/junit.rb', line 92

def from_polyrun_hash(data)
  {
    "name" => (data["name"] || data["testsuite_name"] || "tests").to_s,
    "hostname" => (data["hostname"] || hostname).to_s,
    "testcases" => Array(data["testcases"])
  }
end

.from_rspec_json(data) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
# File 'lib/polyrun/reporting/junit.rb', line 53

def from_rspec_json(data)
  cases = []
  data["examples"].each do |ex|
    next unless ex.is_a?(Hash)

    cases << junit_rspec_example_to_case(ex)
  end

  name = (data.dig("summary", "summary_line") || data["name"] || "RSpec").to_s
  from_polyrun_hash("name" => name, "hostname" => hostname, "testcases" => cases)
end

.hostnameObject



100
101
102
103
104
105
# File 'lib/polyrun/reporting/junit.rb', line 100

def hostname
  require "socket"
  Socket.gethostname
rescue
  "localhost"
end

.junit_rspec_example_to_case(ex) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/polyrun/reporting/junit.rb', line 65

def junit_rspec_example_to_case(ex)
  status = (ex["status"] || "unknown").to_s
  file = ex["file_path"].to_s.sub(%r{\A\./}, "")
  tc = {
    "classname" => file.empty? ? "rspec" : file,
    "name" => (ex["full_description"] || ex["description"] || ex["id"]).to_s,
    "time" => (ex["run_time"] || ex["time"] || 0).to_f,
    "status" => status
  }
  if status == "failed"
    tc["failure"] = junit_rspec_failure_hash(ex)
  end
  tc
end

.junit_rspec_failure_hash(ex) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
# File 'lib/polyrun/reporting/junit.rb', line 80

def junit_rspec_failure_hash(ex)
  e = ex["exception"]
  if e.is_a?(Hash)
    {
      "message" => e["message"].to_s,
      "body" => Array(e["backtrace"]).join("\n")
    }
  else
    {"message" => "failed", "body" => ex.inspect}
  end
end

.junit_xml_failure_body(c) ⇒ Object



49
50
51
52
53
54
55
# File 'lib/polyrun/reporting/junit_emit.rb', line 49

def junit_xml_failure_body(c)
  f = c["failure"] || {}
  fm = f["message"] || f[:message] || status_of(c)
  fb = f["body"] || f[:body] || ""
  tag = (status_of(c) == "error") ? "error" : "failure"
  %(<#{tag} message="#{esc(fm)}">#{esc(fb)}</#{tag}>)
end

.junit_xml_header(doc, cases) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/polyrun/reporting/junit_emit.rb', line 19

def junit_xml_header(doc, cases)
  total_time = cases.sum { |c| (c["time"] || c[:time] || 0).to_f }
  failures = cases.count { |c| status_of(c) == "failed" }
  errors = cases.count { |c| status_of(c) == "error" }
  skipped = cases.count { |c| %w[pending skipped].include?(status_of(c)) }
  tests = cases.size
  lines = []
  lines << %(<?xml version="1.0" encoding="UTF-8"?>)
  lines << %(<testsuites name="#{esc(doc["name"])}">)
  lines << %(<testsuite name="#{esc(doc["name"])}" tests="#{tests}" failures="#{failures}" errors="#{errors}" skipped="#{skipped}" time="#{format_float(total_time)}" hostname="#{esc(doc["hostname"])}">)
  lines.join("\n")
end

.junit_xml_testcase_line(c) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/polyrun/reporting/junit_emit.rb', line 32

def junit_xml_testcase_line(c)
  c = c.transform_keys(&:to_s)
  classname = c["classname"].to_s
  name = c["name"].to_s
  time = (c["time"] || 0).to_f
  lines = []
  lines << %(<testcase classname="#{esc(classname)}" name="#{esc(name)}" file="#{esc(c["file"] || classname)}" line="#{esc(c["line"] || "")}" time="#{format_float(time)}">)
  case status_of(c)
  when "failed", "error"
    lines << junit_xml_failure_body(c)
  when "pending", "skipped"
    lines << %(<skipped/>)
  end
  lines << %(</testcase>)
  lines.join("\n")
end

.merge_rspec_json_files(paths, output_path:) ⇒ Object

Merge several RSpec JSON outputs (parallel shards) by concatenating examples.



23
24
25
26
27
28
29
30
31
# File 'lib/polyrun/reporting/junit.rb', line 23

def merge_rspec_json_files(paths, output_path:)
  merged = {"examples" => []}
  paths.each do |p|
    data = JSON.parse(File.read(p))
    merged["examples"].concat(data["examples"] || [])
  end
  merged["summary"] = {"summary_line" => "merged #{paths.size} RSpec JSON file(s)"}
  write_from_hash(merged, output_path: output_path)
end

.parse_input(data) ⇒ Object

Raises:



40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/polyrun/reporting/junit.rb', line 40

def parse_input(data)
  raise Polyrun::Error, "JUnit input must be a Hash" unless data.is_a?(Hash)

  if data["examples"].is_a?(Array)
    from_rspec_json(data)
  elsif data["testcases"].is_a?(Array)
    from_polyrun_hash(data)
  else
    raise Polyrun::Error,
      'JUnit input: expected top-level "examples" (RSpec JSON) or "testcases" (Polyrun schema)'
  end
end

.status_of(c) ⇒ Object



107
108
109
110
111
112
# File 'lib/polyrun/reporting/junit.rb', line 107

def status_of(c)
  s = (c["status"] || c[:status] || "passed").to_s
  return "skipped" if s == "pending"

  s
end

.write_from_hash(data, output_path:) ⇒ Object



33
34
35
36
37
38
# File 'lib/polyrun/reporting/junit.rb', line 33

def write_from_hash(data, output_path:)
  doc = parse_input(data)
  xml = emit_xml(doc)
  File.write(output_path, xml)
  output_path
end

.write_from_json_file(json_path, output_path:) ⇒ Object



17
18
19
20
# File 'lib/polyrun/reporting/junit.rb', line 17

def write_from_json_file(json_path, output_path:)
  data = JSON.parse(File.read(json_path))
  write_from_hash(data, output_path: output_path)
end