polyrun 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ require "json"
2
+ require "fileutils"
3
+
4
+ module Polyrun
5
+ module Reporting
6
+ # Merge per-worker / per-shard failure fragments (JSONL or RSpec JSON) into one report.
7
+ # Fragment basenames align with {Coverage::CollectorFragmentMeta} (worker index and optional matrix shard).
8
+ module FailureMerge
9
+ DEFAULT_FRAGMENT_DIR = "tmp/polyrun_failures".freeze
10
+ FRAGMENT_GLOB = "polyrun-failure-fragment-*.jsonl".freeze
11
+
12
+ module_function
13
+
14
+ def default_fragment_glob(dir = nil)
15
+ root = File.expand_path(dir || DEFAULT_FRAGMENT_DIR, Dir.pwd)
16
+ File.join(root, FRAGMENT_GLOB)
17
+ end
18
+
19
+ def merge_fragment_paths(quiet: false)
20
+ p = default_fragment_glob
21
+ Dir.glob(p).sort.tap do |paths|
22
+ Polyrun::Log.warn "merge-failures: no files matched #{p}" if paths.empty? && !quiet
23
+ end
24
+ end
25
+
26
+ # @param paths [Array<String>] fragment paths (.jsonl and/or RSpec --format json outputs)
27
+ # @param format [String] "jsonl" or "json"
28
+ # @param output [String] destination path
29
+ # @return [Integer] count of failure rows merged
30
+ def merge_files!(paths, output:, format: "jsonl")
31
+ fmt = format.to_s.downcase
32
+ rows = collect_rows(paths)
33
+ out_abs = File.expand_path(output)
34
+ FileUtils.mkdir_p(File.dirname(out_abs))
35
+ case fmt
36
+ when "json"
37
+ doc = {
38
+ "meta" => {
39
+ "polyrun_merge" => true,
40
+ "inputs" => paths.map { |p| File.expand_path(p) },
41
+ "failure_count" => rows.size
42
+ },
43
+ "failures" => rows
44
+ }
45
+ File.write(out_abs, JSON.generate(doc))
46
+ when "jsonl"
47
+ File.write(out_abs, rows.map { |h| JSON.generate(h) }.join("\n") + (rows.empty? ? "" : "\n"))
48
+ else
49
+ raise Polyrun::Error, "merge-failures: unknown format #{fmt.inspect} (use jsonl or json)"
50
+ end
51
+ rows.size
52
+ end
53
+
54
+ def collect_rows(paths)
55
+ rows = []
56
+ paths.each do |p|
57
+ rows.concat(rows_from_path(p))
58
+ end
59
+ rows
60
+ end
61
+
62
+ def rows_from_path(path)
63
+ ext = File.extname(path).downcase
64
+ if ext == ".jsonl"
65
+ return rows_from_jsonl_file(path)
66
+ end
67
+
68
+ text = File.read(path)
69
+ data =
70
+ begin
71
+ JSON.parse(text)
72
+ rescue JSON::ParserError => e
73
+ raise Polyrun::Error, "merge-failures: #{path} is not valid JSON: #{e.message}"
74
+ end
75
+ if data.is_a?(Hash) && data["examples"].is_a?(Array)
76
+ return failures_from_rspec_examples(data["examples"])
77
+ end
78
+
79
+ hint =
80
+ if data.is_a?(Hash)
81
+ keys = data.keys
82
+ "got JSON object with keys: #{keys.take(12).join(", ")}" + ((keys.size > 12) ? ", …" : "")
83
+ else
84
+ "got #{data.class}"
85
+ end
86
+ raise Polyrun::Error,
87
+ "merge-failures: #{path} is not RSpec JSON (expected top-level \"examples\" array). #{hint}. " \
88
+ "Use RSpec --format json, or polyrun failure JSONL (.jsonl fragments)."
89
+ end
90
+
91
+ def rows_from_jsonl_file(path)
92
+ acc = []
93
+ File.readlines(path, chomp: true).each_with_index do |line, idx|
94
+ line = line.strip
95
+ next if line.empty?
96
+
97
+ acc << parse_jsonl_line!(path, idx + 1, line)
98
+ end
99
+ acc
100
+ end
101
+
102
+ def parse_jsonl_line!(path, line_number, line)
103
+ JSON.parse(line)
104
+ rescue JSON::ParserError => e
105
+ raise Polyrun::Error,
106
+ "merge-failures: invalid JSONL at #{path} line #{line_number}: #{e.message}"
107
+ end
108
+
109
+ def failures_from_rspec_examples(examples)
110
+ examples.each_with_object([]) do |ex, acc|
111
+ next unless ex.is_a?(Hash)
112
+ next unless ex["status"].to_s == "failed"
113
+
114
+ acc << rspec_example_to_row(ex)
115
+ end
116
+ end
117
+
118
+ def rspec_example_to_row(ex)
119
+ ex = ex.transform_keys(&:to_s)
120
+ exc = ex["exception"] || {}
121
+ exc = exc.transform_keys(&:to_s) if exc.is_a?(Hash)
122
+ {
123
+ "id" => ex["id"],
124
+ "full_description" => ex["full_description"],
125
+ "location" => (ex["file_path"] && ex["line_number"]) ? "#{ex["file_path"]}:#{ex["line_number"]}" : ex["full_description"],
126
+ "file_path" => ex["file_path"],
127
+ "line_number" => ex["line_number"],
128
+ "message" => exc["message"] || ex["full_description"],
129
+ "exception_class" => exc["class"],
130
+ "source" => "rspec_json"
131
+ }.compact
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,95 @@
1
+ require "json"
2
+ require "fileutils"
3
+
4
+ module Polyrun
5
+ module Reporting
6
+ # RSpec formatter: appends one JSON object per failed example to the shard fragment file.
7
+ # Enable via +Polyrun::RSpec.install_failure_fragments!+ and +POLYRUN_FAILURE_FRAGMENTS=1+ (set by run-shards --merge-failures).
8
+ #
9
+ # Output: +tmp/polyrun_failures/polyrun-failure-fragment-<workerN|shardM-workerN>.jsonl+
10
+ # (same basename rules as {Coverage::CollectorFragmentMeta}.)
11
+ class RspecFailureFragmentFormatter
12
+ ::RSpec::Core::Formatters.register self, :start, :example_failed
13
+
14
+ attr_reader :output
15
+
16
+ def initialize(output)
17
+ @output = output
18
+ @path = fragment_path
19
+ end
20
+
21
+ def start(_notification)
22
+ FileUtils.mkdir_p(File.dirname(@path))
23
+ File.write(@path, "")
24
+ end
25
+
26
+ def example_failed(notification)
27
+ ex = notification.example
28
+ exc = notification.exception
29
+ row = {
30
+ "id" => ex.id,
31
+ "full_description" => ex.full_description,
32
+ "location" => ex.location,
33
+ "file_path" => ex.file_path,
34
+ "line_number" => example_line_number(ex),
35
+ "message" => exc.message.to_s,
36
+ "exception_class" => exc.class.name,
37
+ "polyrun_shard_index" => ENV["POLYRUN_SHARD_INDEX"],
38
+ "polyrun_shard_total" => ENV["POLYRUN_SHARD_TOTAL"],
39
+ "polyrun_shard_matrix_index" => matrix_env_or_nil("POLYRUN_SHARD_MATRIX_INDEX"),
40
+ "polyrun_shard_matrix_total" => matrix_env_or_nil("POLYRUN_SHARD_MATRIX_TOTAL"),
41
+ "rspec_seed" => seed_if_known,
42
+ "rspec_order" => order_if_known
43
+ }
44
+ trim_backtrace!(row, exc)
45
+ File.open(@path, "a") { |f| f.puts(JSON.generate(row.compact)) }
46
+ end
47
+
48
+ private
49
+
50
+ def example_line_number(ex)
51
+ return ex.line_number if ex.respond_to?(:line_number)
52
+
53
+ ex.metadata[:line_number]
54
+ end
55
+
56
+ def fragment_path
57
+ dir = ENV.fetch("POLYRUN_FAILURE_FRAGMENT_DIR", FailureMerge::DEFAULT_FRAGMENT_DIR)
58
+ base = Polyrun::Coverage::CollectorFragmentMeta.fragment_default_basename_from_env
59
+ File.expand_path(File.join(dir, "polyrun-failure-fragment-#{base}.jsonl"))
60
+ end
61
+
62
+ def matrix_env_or_nil(name)
63
+ v = ENV[name]
64
+ return nil if v.nil? || v.to_s.strip.empty?
65
+
66
+ v
67
+ end
68
+
69
+ def seed_if_known
70
+ return unless defined?(::RSpec) && ::RSpec.respond_to?(:configuration)
71
+
72
+ ::RSpec.configuration.seed
73
+ rescue
74
+ nil
75
+ end
76
+
77
+ def order_if_known
78
+ return unless defined?(::RSpec) && ::RSpec.respond_to?(:configuration)
79
+
80
+ ::RSpec.configuration.order.to_s
81
+ rescue
82
+ nil
83
+ end
84
+
85
+ MAX_BT = 20
86
+
87
+ def trim_backtrace!(row, exc)
88
+ bt = exc.backtrace
89
+ return unless bt.is_a?(Array) && bt.any?
90
+
91
+ row["backtrace"] = bt.first(MAX_BT)
92
+ end
93
+ end
94
+ end
95
+ end
data/lib/polyrun/rspec.rb CHANGED
@@ -30,5 +30,19 @@ module Polyrun
30
30
  config.add_formatter fmt
31
31
  end
32
32
  end
33
+
34
+ # Per-worker failure JSONL fragments for +polyrun run-shards --merge-failures+ (parity with coverage shards).
35
+ # Requires +POLYRUN_FAILURE_FRAGMENTS=1+ (set by the parent when --merge-failures is used) unless +only_if+ overrides.
36
+ # Writes +tmp/polyrun_failures/polyrun-failure-fragment-*.jsonl+ (override dir with +POLYRUN_FAILURE_FRAGMENT_DIR+).
37
+ def install_failure_fragments!(only_if: nil)
38
+ pred = only_if || -> { %w[1 true yes].include?(ENV["POLYRUN_FAILURE_FRAGMENTS"].to_s.downcase) }
39
+ return unless pred.call
40
+
41
+ require "rspec/core"
42
+ require_relative "reporting/rspec_failure_fragment_formatter"
43
+ ::RSpec.configure do |config|
44
+ config.add_formatter Polyrun::Reporting::RspecFailureFragmentFormatter
45
+ end
46
+ end
33
47
  end
34
48
  end
@@ -1,3 +1,3 @@
1
1
  module Polyrun
2
- VERSION = "1.4.0"
2
+ VERSION = "1.4.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: polyrun
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -175,6 +175,7 @@ files:
175
175
  - lib/polyrun/cli/database_commands.rb
176
176
  - lib/polyrun/cli/default_run.rb
177
177
  - lib/polyrun/cli/env_commands.rb
178
+ - lib/polyrun/cli/failure_commands.rb
178
179
  - lib/polyrun/cli/help.rb
179
180
  - lib/polyrun/cli/helpers.rb
180
181
  - lib/polyrun/cli/hooks_command.rb
@@ -206,6 +207,13 @@ files:
206
207
  - lib/polyrun/coverage/merge.rb
207
208
  - lib/polyrun/coverage/merge/formatters.rb
208
209
  - lib/polyrun/coverage/merge/formatters_html.rb
210
+ - lib/polyrun/coverage/merge/html/_file_list.html.erb
211
+ - lib/polyrun/coverage/merge/html/_file_section.html.erb
212
+ - lib/polyrun/coverage/merge/html/_groups_table.html.erb
213
+ - lib/polyrun/coverage/merge/html/_overview.html.erb
214
+ - lib/polyrun/coverage/merge/html/report.css
215
+ - lib/polyrun/coverage/merge/html/report.js
216
+ - lib/polyrun/coverage/merge/html/template.html.erb
209
217
  - lib/polyrun/coverage/merge_fragment_meta.rb
210
218
  - lib/polyrun/coverage/merge_merge_two.rb
211
219
  - lib/polyrun/coverage/rails.rb
@@ -258,8 +266,10 @@ files:
258
266
  - lib/polyrun/quick/reporter.rb
259
267
  - lib/polyrun/quick/runner.rb
260
268
  - lib/polyrun/railtie.rb
269
+ - lib/polyrun/reporting/failure_merge.rb
261
270
  - lib/polyrun/reporting/junit.rb
262
271
  - lib/polyrun/reporting/junit_emit.rb
272
+ - lib/polyrun/reporting/rspec_failure_fragment_formatter.rb
263
273
  - lib/polyrun/reporting/rspec_junit.rb
264
274
  - lib/polyrun/rspec.rb
265
275
  - lib/polyrun/templates/POLYRUN.md