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

Class Method Summary collapse

Class Method Details

.collect_rows(paths) ⇒ Object



54
55
56
57
58
59
60
# File 'lib/polyrun/reporting/failure_merge.rb', line 54

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

.default_fragment_glob(dir = nil) ⇒ Object



14
15
16
17
# File 'lib/polyrun/reporting/failure_merge.rb', line 14

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



109
110
111
112
113
114
115
116
# File 'lib/polyrun/reporting/failure_merge.rb', line 109

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” or “json”

  • output (String)

    destination path

Returns:

  • (Integer)

    count of failure rows merged



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/polyrun/reporting/failure_merge.rb', line 30

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"
    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))
  when "jsonl"
    File.write(out_abs, rows.map { |h| JSON.generate(h) }.join("\n") + (rows.empty? ? "" : "\n"))
  else
    raise Polyrun::Error, "merge-failures: unknown format #{fmt.inspect} (use jsonl or json)"
  end
  rows.size
end

.merge_fragment_paths(quiet: false) ⇒ Object



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

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



102
103
104
105
106
107
# File 'lib/polyrun/reporting/failure_merge.rb', line 102

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



91
92
93
94
95
96
97
98
99
100
# File 'lib/polyrun/reporting/failure_merge.rb', line 91

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:



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

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



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/polyrun/reporting/failure_merge.rb', line 118

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