Module: Polyrun::Reporting::FailureMerge

Defined in:
lib/polyrun/reporting/failure_merge.rb

Overview

Merge per-worker / per-shard failure fragments (JSONL or RSpec JSON) into one report. Fragment basenames align with Coverage::CollectorFragmentMeta (worker index and optional matrix shard).

Constant Summary collapse

DEFAULT_FRAGMENT_DIR =
"tmp/polyrun_failures".freeze
FRAGMENT_GLOB =
"polyrun-failure-fragment-*.jsonl".freeze
CSV_HEADERS =
%w[
  id full_description location file_path line_number message exception_class
  polyrun_shard_index polyrun_shard_total source
].freeze

Class Method Summary collapse

Class Method Details

.collect_rows(paths) ⇒ Object



90
91
92
93
94
95
96
# File 'lib/polyrun/reporting/failure_merge.rb', line 90

def collect_rows(paths)
  rows = []
  paths.each do |p|
    rows.concat(rows_from_path(p))
  end
  rows
end

.default_fragment_glob(dir = nil) ⇒ Object



21
22
23
24
# File 'lib/polyrun/reporting/failure_merge.rb', line 21

def default_fragment_glob(dir = nil)
  root = File.expand_path(dir || DEFAULT_FRAGMENT_DIR, Dir.pwd)
  File.join(root, FRAGMENT_GLOB)
end

.failures_from_rspec_examples(examples) ⇒ Object



145
146
147
148
149
150
151
152
# File 'lib/polyrun/reporting/failure_merge.rb', line 145

def failures_from_rspec_examples(examples)
  examples.each_with_object([]) do |ex, acc|
    next unless ex.is_a?(Hash)
    next unless ex["status"].to_s == "failed"

    acc << rspec_example_to_row(ex)
  end
end

.merge_files!(paths, output:, format: "jsonl") ⇒ Integer

Returns count of failure rows merged.

Parameters:

  • paths (Array<String>)

    fragment paths (.jsonl and/or RSpec --format json outputs)

  • format (String) (defaults to: "jsonl")

    "jsonl", "json", "csv", or "markdown"

  • output (String)

    destination path

Returns:

  • (Integer)

    count of failure rows merged



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/polyrun/reporting/failure_merge.rb', line 37

def merge_files!(paths, output:, format: "jsonl")
  fmt = format.to_s.downcase
  rows = collect_rows(paths)
  out_abs = File.expand_path(output)
  FileUtils.mkdir_p(File.dirname(out_abs))
  case fmt
  when "json"
    write_json_output!(out_abs, paths, rows)
  when "jsonl"
    write_jsonl_output!(out_abs, rows)
  when "csv"
    write_csv_output!(out_abs, rows)
  when "markdown", "md"
    write_markdown_output!(out_abs, rows)
  else
    raise Polyrun::Error, "merge-failures: unknown format #{fmt.inspect} (use jsonl, json, csv, or markdown)"
  end
  rows.size
end

.merge_fragment_paths(quiet: false) ⇒ Object



26
27
28
29
30
31
# File 'lib/polyrun/reporting/failure_merge.rb', line 26

def merge_fragment_paths(quiet: false)
  p = default_fragment_glob
  Dir.glob(p).sort.tap do |paths|
    Polyrun::Log.warn "merge-failures: no files matched #{p}" if paths.empty? && !quiet
  end
end

.parse_jsonl_line!(path, line_number, line) ⇒ Object



138
139
140
141
142
143
# File 'lib/polyrun/reporting/failure_merge.rb', line 138

def parse_jsonl_line!(path, line_number, line)
  JSON.parse(line)
rescue JSON::ParserError => e
  raise Polyrun::Error,
    "merge-failures: invalid JSONL at #{path} line #{line_number}: #{e.message}"
end

.rows_from_jsonl_file(path) ⇒ Object



127
128
129
130
131
132
133
134
135
136
# File 'lib/polyrun/reporting/failure_merge.rb', line 127

def rows_from_jsonl_file(path)
  acc = []
  File.readlines(path, chomp: true).each_with_index do |line, idx|
    line = line.strip
    next if line.empty?

    acc << parse_jsonl_line!(path, idx + 1, line)
  end
  acc
end

.rows_from_path(path) ⇒ Object

Raises:



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
# File 'lib/polyrun/reporting/failure_merge.rb', line 98

def rows_from_path(path)
  ext = File.extname(path).downcase
  if ext == ".jsonl"
    return rows_from_jsonl_file(path)
  end

  text = File.read(path)
  data =
    begin
      JSON.parse(text)
    rescue JSON::ParserError => e
      raise Polyrun::Error, "merge-failures: #{path} is not valid JSON: #{e.message}"
    end
  if data.is_a?(Hash) && data["examples"].is_a?(Array)
    return failures_from_rspec_examples(data["examples"])
  end

  hint =
    if data.is_a?(Hash)
      keys = data.keys
      "got JSON object with keys: #{keys.take(12).join(", ")}" + ((keys.size > 12) ? ", …" : "")
    else
      "got #{data.class}"
    end
  raise Polyrun::Error,
    "merge-failures: #{path} is not RSpec JSON (expected top-level \"examples\" array). #{hint}. " \
    "Use RSpec --format json, or polyrun failure JSONL (.jsonl fragments)."
end

.rspec_example_to_row(ex) ⇒ Object



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/polyrun/reporting/failure_merge.rb', line 154

def rspec_example_to_row(ex)
  ex = ex.transform_keys(&:to_s)
  exc = ex["exception"] || {}
  exc = exc.transform_keys(&:to_s) if exc.is_a?(Hash)
  {
    "id" => ex["id"],
    "full_description" => ex["full_description"],
    "location" => (ex["file_path"] && ex["line_number"]) ? "#{ex["file_path"]}:#{ex["line_number"]}" : ex["full_description"],
    "file_path" => ex["file_path"],
    "line_number" => ex["line_number"],
    "message" => exc["message"] || ex["full_description"],
    "exception_class" => exc["class"],
    "source" => "rspec_json"
  }.compact
end

.write_csv_output!(out_abs, rows) ⇒ Object



73
74
75
76
# File 'lib/polyrun/reporting/failure_merge.rb', line 73

def write_csv_output!(out_abs, rows)
  csv_rows = rows.map { |row| CSV_HEADERS.map { |key| row[key] } }
  File.write(out_abs, Export::Csv.generate(CSV_HEADERS, csv_rows))
end

.write_json_output!(out_abs, paths, rows) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
# File 'lib/polyrun/reporting/failure_merge.rb', line 57

def write_json_output!(out_abs, paths, rows)
  doc = {
    "meta" => {
      "polyrun_merge" => true,
      "inputs" => paths.map { |p| File.expand_path(p) },
      "failure_count" => rows.size
    },
    "failures" => rows
  }
  File.write(out_abs, JSON.generate(doc))
end

.write_jsonl_output!(out_abs, rows) ⇒ Object



69
70
71
# File 'lib/polyrun/reporting/failure_merge.rb', line 69

def write_jsonl_output!(out_abs, rows)
  File.write(out_abs, rows.map { |h| JSON.generate(h) }.join("\n") + (rows.empty? ? "" : "\n"))
end

.write_markdown_output!(out_abs, rows) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
# File 'lib/polyrun/reporting/failure_merge.rb', line 78

def write_markdown_output!(out_abs, rows)
  sections = rows.map.with_index(1) do |row, index|
    {
      heading: "Failure #{index}: #{row["full_description"] || row["location"] || row["id"]}",
      headers: %w[field value],
      rows: row.sort_by { |key, _| key.to_s }.map { |key, value| [key, value] }
    }
  end
  sections = [{heading: "Failures", body: "(none)"}] if sections.empty?
  File.write(out_abs, Export::Markdown.document("Polyrun failure report", sections))
end