polyrun 2.1.3 → 2.2.0

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -1
  3. data/CONTRIBUTING.md +22 -0
  4. data/docs/SETUP_PROFILE.md +1 -1
  5. data/ext/polyrun_coverage_merge/coverage_merge.c +492 -0
  6. data/ext/polyrun_coverage_merge/extconf.rb +3 -0
  7. data/lib/polyrun/benchmark/profile.rb +191 -0
  8. data/lib/polyrun/benchmark/report.rb +87 -0
  9. data/lib/polyrun/cli/benchmark_commands.rb +66 -0
  10. data/lib/polyrun/cli/coverage_commands.rb +4 -4
  11. data/lib/polyrun/cli/coverage_merge_io.rb +30 -4
  12. data/lib/polyrun/cli/failure_commands.rb +10 -3
  13. data/lib/polyrun/cli/help.rb +12 -8
  14. data/lib/polyrun/cli/report_commands.rb +25 -11
  15. data/lib/polyrun/cli/run_shards_parallel_children.rb +1 -1
  16. data/lib/polyrun/cli/run_shards_plan_options.rb +1 -1
  17. data/lib/polyrun/cli/spec_quality_commands.rb +16 -7
  18. data/lib/polyrun/cli.rb +8 -1
  19. data/lib/polyrun/coverage/example_diff.rb +127 -60
  20. data/lib/polyrun/coverage/example_diff_snapshot.rb +44 -0
  21. data/lib/polyrun/coverage/example_diff_track_filter.rb +51 -0
  22. data/lib/polyrun/coverage/file_stats_report.rb +36 -0
  23. data/lib/polyrun/coverage/formatter.rb +22 -1
  24. data/lib/polyrun/coverage/merge/formatters.rb +11 -3
  25. data/lib/polyrun/coverage/merge_merge_two.rb +112 -63
  26. data/lib/polyrun/coverage/merge_native.rb +37 -0
  27. data/lib/polyrun/coverage/reporting.rb +1 -1
  28. data/lib/polyrun/export/csv.rb +25 -0
  29. data/lib/polyrun/export/markdown.rb +47 -0
  30. data/lib/polyrun/hooks/shell_runner.rb +33 -0
  31. data/lib/polyrun/hooks.rb +4 -7
  32. data/lib/polyrun/reporting/failure_merge.rb +48 -12
  33. data/lib/polyrun/reporting/junit.rb +8 -7
  34. data/lib/polyrun/reporting/junit_report.rb +87 -0
  35. data/lib/polyrun/rspec/example_debug.rb +1 -1
  36. data/lib/polyrun/rspec/sharded_formatter_compat.rb +30 -7
  37. data/lib/polyrun/spec_quality/profile.rb +24 -12
  38. data/lib/polyrun/spec_quality/report.rb +93 -0
  39. data/lib/polyrun/spec_quality.rb +19 -11
  40. data/lib/polyrun/timing/summary.rb +55 -5
  41. data/lib/polyrun/version.rb +1 -1
  42. data/polyrun.gemspec +8 -1
  43. metadata +58 -2
@@ -1,27 +1,50 @@
1
1
  module Polyrun
2
2
  module RSpec
3
- # Formatter tweaks for progress bars (e.g. Fuubar) under +POLYRUN_SHARD_*+ workers.
3
+ # Formatter tweaks for progress formatters under +POLYRUN_SHARD_*+ workers.
4
4
  module ShardedFormatterCompat
5
+ TextFormatterSilencer = Module.new do
6
+ def seed(_notification)
7
+ end
8
+
9
+ def dump_summary(_notification)
10
+ end
11
+
12
+ def dump_pending(_notification)
13
+ end
14
+ end
15
+
16
+ FuubarSeedSilencer = Module.new do
17
+ def seed(_notification)
18
+ end
19
+ end
20
+
5
21
  module_function
6
22
 
7
23
  def install!(rspec_config: fetch_rspec_configuration!)
8
24
  return unless sharded_worker?
9
25
 
10
26
  rspec_config.silence_filter_announcements = true
11
- noop_fuubar_seed! if defined?(Fuubar)
27
+ silence_text_formatter_noise!
28
+ silence_fuubar_seed! if defined?(Fuubar)
12
29
  end
13
30
 
14
31
  def sharded_worker?
15
32
  !ENV["POLYRUN_SHARD_TOTAL"].to_s.strip.empty?
16
33
  end
17
34
 
18
- def noop_fuubar_seed!
35
+ def silence_text_formatter_noise!
36
+ require "rspec/core/formatters/base_text_formatter"
37
+ klass = ::RSpec::Core::Formatters::BaseTextFormatter
38
+ return if klass.ancestors.include?(TextFormatterSilencer)
39
+
40
+ klass.prepend(TextFormatterSilencer)
41
+ end
42
+
43
+ def silence_fuubar_seed!
19
44
  return unless defined?(Fuubar)
45
+ return if Fuubar.ancestors.include?(FuubarSeedSilencer)
20
46
 
21
- Fuubar.class_eval do
22
- def seed(_notification)
23
- end
24
- end
47
+ Fuubar.prepend(FuubarSeedSilencer)
25
48
  end
26
49
 
27
50
  def fetch_rspec_configuration!
@@ -4,18 +4,30 @@ module Polyrun
4
4
  module Profile
5
5
  module_function
6
6
 
7
- def snapshot
8
- cpu = Process.times
9
- gc = GC.stat
10
- io = read_proc_io
11
- {
12
- "cpu_user" => cpu.utime,
13
- "cpu_system" => cpu.stime,
14
- "gc_allocated" => gc[:total_allocated_objects],
15
- "gc_heap_live" => gc[:heap_live_slots],
16
- "io_read_bytes" => io[:read_bytes],
17
- "io_write_bytes" => io[:write_bytes]
18
- }
7
+ def snapshot(dimensions: %w[cpu mem io wall])
8
+ dims = enabled_dimensions(dimensions)
9
+ capture_all = dims.empty?
10
+ out = {}
11
+
12
+ if capture_all || dims.include?("cpu") || dims.include?("wall")
13
+ cpu = Process.times
14
+ out["cpu_user"] = cpu.utime
15
+ out["cpu_system"] = cpu.stime
16
+ end
17
+
18
+ if capture_all || dims.include?("mem")
19
+ gc = GC.stat
20
+ out["gc_allocated"] = gc[:total_allocated_objects]
21
+ out["gc_heap_live"] = gc[:heap_live_slots]
22
+ end
23
+
24
+ if capture_all || dims.include?("io")
25
+ io = read_proc_io
26
+ out["io_read_bytes"] = io[:read_bytes]
27
+ out["io_write_bytes"] = io[:write_bytes]
28
+ end
29
+
30
+ out
19
31
  end
20
32
 
21
33
  def diff(before, after)
@@ -1,4 +1,7 @@
1
1
  # rubocop:disable Polyrun/FileLength, Metrics/ModuleLength -- report analysis + text formatting
2
+ require_relative "../export/csv"
3
+ require_relative "../export/markdown"
4
+
2
5
  module Polyrun
3
6
  module SpecQuality
4
7
  # Human-readable spec quality report from merged JSON.
@@ -55,6 +58,96 @@ module Polyrun
55
58
  lines.join("\n") + "\n"
56
59
  end
57
60
 
61
+ EXAMPLE_CSV_HEADERS = %w[example unique_lines line_churn max_line_churn shard_index].freeze
62
+
63
+ def format_csv(merged, cfg: {}, top: 30, profile: nil, plan_shards: nil)
64
+ rows = merged.fetch("examples", {}).map do |location, row|
65
+ [
66
+ location,
67
+ row["unique_lines"],
68
+ row["line_churn"],
69
+ row["max_line_churn"],
70
+ row["polyrun_shard_index"]
71
+ ]
72
+ end.sort_by { |row| [-row[2].to_i, row[0].to_s] }
73
+ rows = rows.first(top) if top
74
+ Export::Csv.generate(EXAMPLE_CSV_HEADERS, rows)
75
+ end
76
+
77
+ def format_markdown(merged, cfg: {}, top: 30, profile: nil, plan_shards: nil)
78
+ analysis = analyze(merged, cfg, plan_shards: plan_shards)
79
+ sections = markdown_sections(analysis, top).compact
80
+ Export::Markdown.document("Polyrun spec quality report", sections)
81
+ end
82
+
83
+ def markdown_sections(analysis, top)
84
+ [
85
+ markdown_shard_section(analysis[:shard_summary]),
86
+ markdown_zero_hit_section(analysis[:zero_hit], top),
87
+ markdown_hot_lines_section(analysis[:hot_lines], top),
88
+ markdown_churn_section(analysis[:line_churn], top)
89
+ ]
90
+ end
91
+
92
+ def markdown_shard_section(shard_summary)
93
+ shard_rows = (shard_summary || {}).sort_by { |shard, _| shard.to_s }.map do |shard, stats|
94
+ [shard, stats["examples"], stats["zero_hit"], stats["line_churn"]]
95
+ end
96
+ return if shard_rows.empty?
97
+
98
+ {
99
+ heading: "Shard attribution",
100
+ headers: %w[shard examples zero_hit line_churn],
101
+ rows: shard_rows
102
+ }
103
+ end
104
+
105
+ def markdown_zero_hit_section(zero_hit, top)
106
+ zero_rows = zero_hit.keys.sort.first(top).zip
107
+ {
108
+ heading: "Zero production lines",
109
+ headers: %w[example],
110
+ rows: zero_rows.empty? ? [["(none)"]] : zero_rows
111
+ }
112
+ end
113
+
114
+ def markdown_hot_lines_section(hot_lines, top)
115
+ hot_rows = hot_lines.first(top).map do |line, stats|
116
+ [line, stats["example_count"], stats["total_hits"]]
117
+ end
118
+ {
119
+ heading: "Hot lines",
120
+ headers: %w[line example_count total_hits],
121
+ rows: hot_rows.empty? ? [["(none)", 0, 0]] : hot_rows
122
+ }
123
+ end
124
+
125
+ def markdown_churn_section(churn_rows, top)
126
+ rows = churn_rows.first(top).map do |location, row|
127
+ [location, row["line_churn"], row["max_line_churn"]]
128
+ end
129
+ {
130
+ heading: "Per-example line churn",
131
+ headers: %w[example line_churn max_line_churn],
132
+ rows: rows.empty? ? [["(none)", 0, 0]] : rows
133
+ }
134
+ end
135
+
136
+ def render(merged, format: "text", **kwargs)
137
+ case format.to_s.downcase
138
+ when "text", "console", "txt"
139
+ format_report(merged, **kwargs)
140
+ when "json"
141
+ JSON.pretty_generate(analyze(merged, kwargs[:cfg] || {}, plan_shards: kwargs[:plan_shards]))
142
+ when "csv"
143
+ format_csv(merged, **kwargs)
144
+ when "markdown", "md"
145
+ format_markdown(merged, **kwargs)
146
+ else
147
+ raise Polyrun::Error, "report-spec-quality: unknown format #{format.inspect} (use text, json, csv, or markdown)"
148
+ end
149
+ end
150
+
58
151
  def gate_violations(merged, cfg = {})
59
152
  cfg = default_cfg(cfg)
60
153
  analysis = analyze(merged, cfg)
@@ -58,8 +58,12 @@ module Polyrun
58
58
  @current = {
59
59
  location: normalize_location(location),
60
60
  wall_start: wall_start || Process.clock_gettime(Process::CLOCK_MONOTONIC),
61
- coverage_before: Coverage::ExampleDiff.peek_blob,
62
- profile_before: Profile.snapshot,
61
+ coverage_before: Coverage::ExampleDiff.peek_blob(
62
+ root: @config["root"],
63
+ track_under: @config["track_under"],
64
+ ignore_paths: @config["ignore_paths"]
65
+ ),
66
+ profile_before: Profile.snapshot(dimensions: @config["profile"]),
63
67
  sql_count: 0,
64
68
  sql_fingerprints: Hash.new(0),
65
69
  factory_counts: {}
@@ -78,16 +82,9 @@ module Polyrun
78
82
  loc = location || cur[:location]
79
83
  return if loc.nil? || loc.to_s.empty?
80
84
 
81
- after_cov = Coverage::ExampleDiff.peek_blob
82
- delta = Coverage::ExampleDiff.diff(cur[:coverage_before], after_cov)
83
- delta = Coverage::ExampleDiff.apply_track_under(
84
- delta,
85
- root: @config["root"],
86
- track_under: @config["track_under"],
87
- ignore_paths: @config["ignore_paths"]
88
- )
85
+ delta = coverage_delta_for_example(cur)
89
86
 
90
- profile_after = Profile.snapshot
87
+ profile_after = Profile.snapshot(dimensions: @config["profile"])
91
88
  profile_delta = Profile.diff(cur[:profile_before], profile_after)
92
89
  wall = Process.clock_gettime(Process::CLOCK_MONOTONIC) - cur[:wall_start]
93
90
  profile_delta["wall"] = wall
@@ -169,6 +166,17 @@ module Polyrun
169
166
  Polyrun::Partition::TimingKeys.canonical_file_path(File.expand_path(s, root))
170
167
  end
171
168
 
169
+ def coverage_delta_for_example(cur)
170
+ after_source = Coverage::ExampleDiff.coverage_active? ? ::Coverage.peek_result : {}
171
+ Coverage::ExampleDiff.diff(
172
+ cur[:coverage_before],
173
+ after_source,
174
+ root: @config["root"],
175
+ track_under: @config["track_under"],
176
+ ignore_paths: @config["ignore_paths"]
177
+ )
178
+ end
179
+
172
180
  def build_row(cur, location, delta, profile_delta, factory_counts)
173
181
  profile = Profile.slice_profile(profile_delta, @config["profile"])
174
182
  repeated_sql = cur[:sql_fingerprints].select { |_sql, n| n >= min_query_count }.transform_keys(&:to_s)
@@ -1,26 +1,76 @@
1
1
  require_relative "stats"
2
+ require_relative "../export/csv"
3
+ require_relative "../export/markdown"
2
4
 
3
5
  module Polyrun
4
6
  module Timing
5
7
  # Human-readable slow-file list from merged timing JSON (per-file cost).
6
8
  module Summary
9
+ CSV_HEADERS = %w[rank path last_seconds min max mean p95 runs failures timeouts].freeze
10
+ MARKDOWN_HEADERS = %w[rank path seconds].freeze
11
+
7
12
  module_function
8
13
 
14
+ def ranked_entries(merged, top: 30)
15
+ return [] if merged.nil? || merged.empty?
16
+
17
+ merged.map { |path, entry| [path, Stats.normalize_entry(entry)] }
18
+ .sort_by { |(_, entry)| -Stats.binpack_weight(entry) }
19
+ .first(Integer(top))
20
+ end
21
+
9
22
  # +merged+ is path (String) => seconds (Float) or stats Hash, as produced by +Timing::Merge.merge_files+.
10
23
  def format_slow_files(merged, top: 30, title: "Polyrun slowest files (by wall time, seconds)")
11
24
  return "#{title}\n (no data)\n" if merged.nil? || merged.empty?
12
25
 
13
- pairs = merged.map { |path, sec| [path, Stats.binpack_weight(sec)] }
14
- .sort_by { |(_, sec)| -sec.to_f }.first(Integer(top))
15
26
  lines = [title, ""]
16
- pairs.each_with_index do |(path, sec), i|
17
- lines << format(" %2d. %s %.4f", i + 1, path, sec.to_f)
27
+ ranked_entries(merged, top: top).each_with_index do |(path, entry), index|
28
+ lines << format(" %2d. %s %.4f", index + 1, path, Stats.binpack_weight(entry).to_f)
18
29
  end
19
30
  lines.join("\n") + "\n"
20
31
  end
21
32
 
33
+ def format_csv(merged, top: 30)
34
+ rows = ranked_entries(merged, top: top).each_with_index.map do |(path, entry), index|
35
+ [
36
+ index + 1,
37
+ path,
38
+ entry["last_seconds"],
39
+ entry["min"],
40
+ entry["max"],
41
+ entry["mean"],
42
+ entry["p95"],
43
+ entry["runs"],
44
+ entry["failures"],
45
+ entry["timeouts"]
46
+ ]
47
+ end
48
+ Export::Csv.generate(CSV_HEADERS, rows)
49
+ end
50
+
51
+ def format_markdown(merged, top: 30, title: "Polyrun slowest files")
52
+ rows = ranked_entries(merged, top: top).each_with_index.map do |(path, entry), index|
53
+ [index + 1, path, format("%.4f", Stats.binpack_weight(entry).to_f)]
54
+ end
55
+ Export::Markdown.document(
56
+ title,
57
+ [{heading: "Slow files", headers: MARKDOWN_HEADERS, rows: rows}]
58
+ )
59
+ end
60
+
61
+ def render(merged, format: "text", **kwargs)
62
+ case format.to_s.downcase
63
+ when "text", "console", "txt" then format_slow_files(merged, **kwargs)
64
+ when "csv" then format_csv(merged, **kwargs)
65
+ when "markdown", "md" then format_markdown(merged, **kwargs)
66
+ else
67
+ raise Polyrun::Error, "report-timing: unknown format #{format.inspect} (use text, csv, or markdown)"
68
+ end
69
+ end
70
+
22
71
  def write_file(merged, path, **kwargs)
23
- File.write(path, format_slow_files(merged, **kwargs))
72
+ format = kwargs.delete(:format) || "text"
73
+ File.write(path, render(merged, format: format, **kwargs))
24
74
  path
25
75
  end
26
76
  end
@@ -1,3 +1,3 @@
1
1
  module Polyrun
2
- VERSION = "2.1.3"
2
+ VERSION = "2.2.0"
3
3
  end
data/polyrun.gemspec CHANGED
@@ -10,10 +10,14 @@ Gem::Specification.new do |spec|
10
10
  spec.license = "MIT"
11
11
  spec.required_ruby_version = ">= 3.1.0"
12
12
 
13
- spec.files = Dir["lib/**/*", "sig/**/*.rbs", "bin/polyrun", "README.md", "CHANGELOG.md", "docs/SETUP_PROFILE.md", "LICENSE", "CONTRIBUTING.md", "CODE_OF_CONDUCT.md", "SECURITY.md", "polyrun.gemspec"].reject { |f| File.directory?(f) }
13
+ spec.files = (
14
+ Dir["lib/**/*", "sig/**/*.rbs", "bin/polyrun", "README.md", "CHANGELOG.md", "docs/SETUP_PROFILE.md", "LICENSE", "CONTRIBUTING.md", "CODE_OF_CONDUCT.md", "SECURITY.md", "polyrun.gemspec"] +
15
+ Dir["ext/**/extconf.rb", "ext/**/*.c", "ext/**/*.h"]
16
+ ).uniq.reject { |f| File.directory?(f) }
14
17
  spec.bindir = "bin"
15
18
  spec.executables = ["polyrun"]
16
19
  spec.require_paths = ["lib"]
20
+ spec.extensions = ["ext/polyrun_coverage_merge/extconf.rb"]
17
21
 
18
22
  spec.metadata = {
19
23
  "homepage_uri" => spec.homepage,
@@ -25,6 +29,9 @@ Gem::Specification.new do |spec|
25
29
  # Normative: zero runtime dependencies (stdlib + vendored/native code only).
26
30
  # Ruby 3.5+: `benchmark` is no longer a default gem; keep scripts and `rake bench_merge` warning-free.
27
31
  spec.add_development_dependency "benchmark", ">= 0.3"
32
+ spec.add_development_dependency "benchmark-ips", "~> 2"
33
+ spec.add_development_dependency "memory_profiler", "~> 1"
34
+ spec.add_development_dependency "stackprof", "~> 0.2"
28
35
  spec.add_development_dependency "rake", "~> 13.0"
29
36
  spec.add_development_dependency "rspec", "~> 3.12"
30
37
  spec.add_development_dependency "appraisal", "~> 2.5"
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: 2.1.3
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -23,6 +23,48 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: benchmark-ips
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2'
40
+ - !ruby/object:Gem::Dependency
41
+ name: memory_profiler
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: stackprof
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.2'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.2'
26
68
  - !ruby/object:Gem::Dependency
27
69
  name: rake
28
70
  requirement: !ruby/object:Gem::Requirement
@@ -153,7 +195,8 @@ email:
153
195
  - contact@kiskolabs.com
154
196
  executables:
155
197
  - polyrun
156
- extensions: []
198
+ extensions:
199
+ - ext/polyrun_coverage_merge/extconf.rb
157
200
  extra_rdoc_files: []
158
201
  files:
159
202
  - CHANGELOG.md
@@ -164,8 +207,13 @@ files:
164
207
  - SECURITY.md
165
208
  - bin/polyrun
166
209
  - docs/SETUP_PROFILE.md
210
+ - ext/polyrun_coverage_merge/coverage_merge.c
211
+ - ext/polyrun_coverage_merge/extconf.rb
167
212
  - lib/polyrun.rb
213
+ - lib/polyrun/benchmark/profile.rb
214
+ - lib/polyrun/benchmark/report.rb
168
215
  - lib/polyrun/cli.rb
216
+ - lib/polyrun/cli/benchmark_commands.rb
169
217
  - lib/polyrun/cli/ci_shard_hooks.rb
170
218
  - lib/polyrun/cli/ci_shard_run_command.rb
171
219
  - lib/polyrun/cli/ci_shard_run_parse.rb
@@ -208,6 +256,9 @@ files:
208
256
  - lib/polyrun/coverage/collector_finish.rb
209
257
  - lib/polyrun/coverage/collector_fragment_meta.rb
210
258
  - lib/polyrun/coverage/example_diff.rb
259
+ - lib/polyrun/coverage/example_diff_snapshot.rb
260
+ - lib/polyrun/coverage/example_diff_track_filter.rb
261
+ - lib/polyrun/coverage/file_stats_report.rb
211
262
  - lib/polyrun/coverage/filter.rb
212
263
  - lib/polyrun/coverage/formatter.rb
213
264
  - lib/polyrun/coverage/merge.rb
@@ -222,6 +273,7 @@ files:
222
273
  - lib/polyrun/coverage/merge/html/template.html.erb
223
274
  - lib/polyrun/coverage/merge_fragment_meta.rb
224
275
  - lib/polyrun/coverage/merge_merge_two.rb
276
+ - lib/polyrun/coverage/merge_native.rb
225
277
  - lib/polyrun/coverage/rails.rb
226
278
  - lib/polyrun/coverage/reporting.rb
227
279
  - lib/polyrun/coverage/result.rb
@@ -242,8 +294,11 @@ files:
242
294
  - lib/polyrun/database/url_builder/template_prepare.rb
243
295
  - lib/polyrun/debug.rb
244
296
  - lib/polyrun/env/ci.rb
297
+ - lib/polyrun/export/csv.rb
298
+ - lib/polyrun/export/markdown.rb
245
299
  - lib/polyrun/hooks.rb
246
300
  - lib/polyrun/hooks/dsl.rb
301
+ - lib/polyrun/hooks/shell_runner.rb
247
302
  - lib/polyrun/hooks/worker_runner.rb
248
303
  - lib/polyrun/hooks/worker_shell.rb
249
304
  - lib/polyrun/log.rb
@@ -279,6 +334,7 @@ files:
279
334
  - lib/polyrun/reporting/failure_merge.rb
280
335
  - lib/polyrun/reporting/junit.rb
281
336
  - lib/polyrun/reporting/junit_emit.rb
337
+ - lib/polyrun/reporting/junit_report.rb
282
338
  - lib/polyrun/reporting/rspec_failure_fragment_formatter.rb
283
339
  - lib/polyrun/reporting/rspec_junit.rb
284
340
  - lib/polyrun/rspec.rb