polyrun 1.5.0 → 2.1.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +2 -2
  4. data/docs/SETUP_PROFILE.md +2 -0
  5. data/lib/polyrun/cli/coverage_commands.rb +1 -1
  6. data/lib/polyrun/cli/failure_commands.rb +1 -1
  7. data/lib/polyrun/cli/help.rb +20 -17
  8. data/lib/polyrun/cli/helpers.rb +16 -0
  9. data/lib/polyrun/cli/init_command.rb +8 -1
  10. data/lib/polyrun/cli/partition_diagnostics.rb +22 -0
  11. data/lib/polyrun/cli/plan_command.rb +47 -18
  12. data/lib/polyrun/cli/queue_command.rb +25 -2
  13. data/lib/polyrun/cli/run_queue_command.rb +145 -0
  14. data/lib/polyrun/cli/run_shards_command.rb +6 -1
  15. data/lib/polyrun/cli/run_shards_parallel_children.rb +2 -1
  16. data/lib/polyrun/cli/run_shards_parallel_wait.rb +5 -1
  17. data/lib/polyrun/cli/run_shards_plan_boot_phases.rb +47 -2
  18. data/lib/polyrun/cli/run_shards_plan_options.rb +14 -4
  19. data/lib/polyrun/cli/run_shards_planning.rb +20 -12
  20. data/lib/polyrun/cli/run_shards_run.rb +22 -5
  21. data/lib/polyrun/cli/spec_quality_commands.rb +140 -0
  22. data/lib/polyrun/cli.rb +16 -2
  23. data/lib/polyrun/coverage/example_diff.rb +122 -0
  24. data/lib/polyrun/coverage/merge/formatters_html.rb +5 -5
  25. data/lib/polyrun/data/factory_counts.rb +14 -1
  26. data/lib/polyrun/database/clone_shards.rb +2 -0
  27. data/lib/polyrun/database/shard.rb +2 -1
  28. data/lib/polyrun/minitest.rb +9 -0
  29. data/lib/polyrun/partition/hrw.rb +40 -3
  30. data/lib/polyrun/partition/paths_build.rb +8 -3
  31. data/lib/polyrun/partition/plan.rb +88 -19
  32. data/lib/polyrun/partition/plan_lpt.rb +49 -7
  33. data/lib/polyrun/partition/plan_sharding.rb +8 -0
  34. data/lib/polyrun/partition/reports.rb +139 -0
  35. data/lib/polyrun/partition/timing_diagnostics.rb +139 -0
  36. data/lib/polyrun/partition/timing_keys.rb +2 -1
  37. data/lib/polyrun/queue/duration.rb +30 -0
  38. data/lib/polyrun/queue/file_store.rb +114 -3
  39. data/lib/polyrun/quick/example_runner.rb +2 -0
  40. data/lib/polyrun/quick/runner.rb +21 -0
  41. data/lib/polyrun/rspec.rb +10 -0
  42. data/lib/polyrun/spec_quality/config.rb +134 -0
  43. data/lib/polyrun/spec_quality/fragment.rb +39 -0
  44. data/lib/polyrun/spec_quality/merge.rb +78 -0
  45. data/lib/polyrun/spec_quality/minitest_hook.rb +42 -0
  46. data/lib/polyrun/spec_quality/plan_loader.rb +47 -0
  47. data/lib/polyrun/spec_quality/profile.rb +91 -0
  48. data/lib/polyrun/spec_quality/report.rb +261 -0
  49. data/lib/polyrun/spec_quality/rspec_hook.rb +55 -0
  50. data/lib/polyrun/spec_quality/sql_counter.rb +34 -0
  51. data/lib/polyrun/spec_quality.rb +205 -0
  52. data/lib/polyrun/templates/POLYRUN.md +6 -0
  53. data/lib/polyrun/templates/ci_matrix.polyrun.yml +4 -0
  54. data/lib/polyrun/templates/polyrun_hooks_spec_quality.rb +12 -0
  55. data/lib/polyrun/templates/polyrun_spec_quality.yml +20 -0
  56. data/lib/polyrun/templates/rails_prepare.polyrun.yml +5 -0
  57. data/lib/polyrun/timing/merge.rb +5 -5
  58. data/lib/polyrun/timing/rspec_example_formatter.rb +14 -7
  59. data/lib/polyrun/timing/stats.rb +76 -0
  60. data/lib/polyrun/timing/summary.rb +5 -2
  61. data/lib/polyrun/timing/variance_report.rb +51 -0
  62. data/lib/polyrun/version.rb +1 -1
  63. metadata +22 -1
@@ -0,0 +1,205 @@
1
+ # rubocop:disable Polyrun/FileLength -- per-example recorder API
2
+ require "json"
3
+ require "fileutils"
4
+
5
+ require_relative "coverage/example_diff"
6
+ require_relative "partition/timing_keys"
7
+ require_relative "spec_quality/config"
8
+ require_relative "spec_quality/profile"
9
+ require_relative "spec_quality/fragment"
10
+ require_relative "spec_quality/sql_counter"
11
+ require_relative "spec_quality/merge"
12
+ require_relative "spec_quality/plan_loader"
13
+ require_relative "spec_quality/report"
14
+ require_relative "spec_quality/rspec_hook"
15
+ require_relative "spec_quality/minitest_hook"
16
+
17
+ module Polyrun
18
+ # Per-example spec quality: coverage line deltas, resource use, optional SQL counts.
19
+ # Opt-in with +POLYRUN_SPEC_QUALITY=1+ or +Polyrun::SpecQuality.start!+.
20
+ module SpecQuality
21
+ class << self
22
+ def start!(root: Dir.pwd, config_path: nil, output_path: nil, **overrides)
23
+ return if Config.disabled?
24
+
25
+ @config = Config.load(root: root, config_path: config_path, **overrides)
26
+ @output_path = output_path || Fragment.default_fragment_path
27
+ @pause_depth = 0
28
+ @current = nil
29
+ @rng = Random.new(Process.pid ^ Time.now.to_i)
30
+
31
+ Fragment.truncate_fragment!(@output_path) unless fragment_append_mode?
32
+ SqlCounter.install! if @config["sql_counter"]
33
+ self
34
+ end
35
+
36
+ def started?
37
+ instance_variable_defined?(:@config) && @config
38
+ end
39
+
40
+ def enabled?
41
+ Config.enabled? && !Config.disabled?
42
+ end
43
+
44
+ attr_reader :config
45
+
46
+ attr_reader :output_path
47
+
48
+ def recording?
49
+ started? && @current && @pause_depth.zero?
50
+ end
51
+
52
+ def start_example!(location:, wall_start: nil)
53
+ return unless started?
54
+ return if paused?
55
+ return if Config.ignored_example?(location, @config["ignore_examples"])
56
+ return unless sample_example?
57
+
58
+ @current = {
59
+ location: normalize_location(location),
60
+ wall_start: wall_start || Process.clock_gettime(Process::CLOCK_MONOTONIC),
61
+ coverage_before: Coverage::ExampleDiff.peek_blob,
62
+ profile_before: Profile.snapshot,
63
+ sql_count: 0,
64
+ sql_fingerprints: Hash.new(0),
65
+ factory_counts: {}
66
+ }
67
+ Polyrun::Data::FactoryCounts.reset_example! if defined?(Polyrun::Data::FactoryCounts)
68
+ nil
69
+ end
70
+
71
+ def finish_example!(location: nil, pending: false)
72
+ return unless started?
73
+ cur = @current
74
+ @current = nil
75
+ return if cur.nil? || paused?
76
+ return if pending
77
+
78
+ loc = location || cur[:location]
79
+ return if loc.nil? || loc.to_s.empty?
80
+
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
+ )
89
+
90
+ profile_after = Profile.snapshot
91
+ profile_delta = Profile.diff(cur[:profile_before], profile_after)
92
+ wall = Process.clock_gettime(Process::CLOCK_MONOTONIC) - cur[:wall_start]
93
+ profile_delta["wall"] = wall
94
+
95
+ factory_counts =
96
+ if defined?(Polyrun::Data::FactoryCounts)
97
+ Polyrun::Data::FactoryCounts.example_counts
98
+ else
99
+ {}
100
+ end
101
+
102
+ row = build_row(cur, loc, delta, profile_delta, factory_counts)
103
+ Fragment.append_row!(@output_path, row)
104
+ row
105
+ end
106
+
107
+ def pause
108
+ @pause_depth += 1
109
+ if block_given?
110
+ begin
111
+ yield
112
+ ensure
113
+ resume
114
+ end
115
+ end
116
+ end
117
+
118
+ def resume
119
+ @pause_depth -= 1 if @pause_depth.positive?
120
+ end
121
+
122
+ def paused?
123
+ @pause_depth.positive?
124
+ end
125
+
126
+ def record_sql!(sql)
127
+ return unless recording?
128
+
129
+ @current[:sql_count] += 1
130
+ fp = normalize_sql(sql)
131
+ @current[:sql_fingerprints][fp] += 1
132
+ end
133
+
134
+ def spec_quality_requested_for_quick?(root = Dir.pwd)
135
+ return false if Config.disabled?
136
+ return true if %w[1 true yes].include?(ENV["POLYRUN_SPEC_QUALITY"]&.to_s&.downcase)
137
+ return true if %w[1 true yes].include?(ENV["POLYRUN_SPEC_QUALITY_FRAGMENTS"]&.to_s&.downcase)
138
+
139
+ path = File.join(File.expand_path(root), Config::DEFAULT_CONFIG_RELATIVE)
140
+ return false unless File.file?(path)
141
+
142
+ %w[1 true yes].include?(ENV["POLYRUN_QUICK_SPEC_QUALITY"]&.to_s&.downcase)
143
+ end
144
+
145
+ private
146
+
147
+ def fragment_append_mode?
148
+ truthy?(ENV["POLYRUN_SPEC_QUALITY_FRAGMENT_APPEND"])
149
+ end
150
+
151
+ def sample_example?
152
+ rate = @config["sample"].to_f
153
+ return true if rate >= 1.0
154
+ return false if rate <= 0.0
155
+
156
+ @rng.rand < rate
157
+ end
158
+
159
+ def normalize_location(location)
160
+ s = location.to_s.strip
161
+ return s if s.empty?
162
+
163
+ root = @config["root"]
164
+ if (m = s.match(/\A(.+):(\d+)\z/)) && m[2].match?(/\A\d+\z/)
165
+ fp = Polyrun::Partition::TimingKeys.canonical_file_path(File.expand_path(m[1], root))
166
+ return "#{fp}:#{m[2]}"
167
+ end
168
+
169
+ Polyrun::Partition::TimingKeys.canonical_file_path(File.expand_path(s, root))
170
+ end
171
+
172
+ def build_row(cur, location, delta, profile_delta, factory_counts)
173
+ profile = Profile.slice_profile(profile_delta, @config["profile"])
174
+ repeated_sql = cur[:sql_fingerprints].select { |_sql, n| n >= min_query_count }.transform_keys(&:to_s)
175
+
176
+ {
177
+ "example" => location.to_s,
178
+ "unique_lines" => delta[:unique_lines],
179
+ "line_churn" => delta[:line_churn],
180
+ "max_line_churn" => delta[:max_line_churn],
181
+ "lines" => delta[:lines],
182
+ "profile" => profile,
183
+ "sql_count" => cur[:sql_count],
184
+ "repeated_sql" => repeated_sql,
185
+ "factory_counts" => factory_counts.transform_keys(&:to_s),
186
+ "polyrun_shard_index" => ENV["POLYRUN_SHARD_INDEX"],
187
+ "polyrun_shard_total" => ENV["POLYRUN_SHARD_TOTAL"]
188
+ }.compact
189
+ end
190
+
191
+ def min_query_count
192
+ @config["min_query_count"].to_i
193
+ end
194
+
195
+ def normalize_sql(sql)
196
+ sql.to_s.gsub(/\s+/, " ").strip[0, 500]
197
+ end
198
+
199
+ def truthy?(value)
200
+ Config.send(:truthy?, value)
201
+ end
202
+ end
203
+ end
204
+ end
205
+ # rubocop:enable Polyrun/FileLength
@@ -40,6 +40,12 @@ Do not combine Model A and Model B in one workflow without a documented reason (
40
40
  - `spec/spec_helper.rb`: `require "polyrun"` and collector or Rails helper as appropriate.
41
41
  - Fragments: `coverage/polyrun-fragment-<shard>.json` → `polyrun merge-coverage` → `polyrun report-coverage` / `report-junit` for CI.
42
42
 
43
+ ## Spec quality (optional)
44
+
45
+ - `POLYRUN_SPEC_QUALITY=1` and `Polyrun::RSpec.install_spec_quality!` in `spec_helper` (requires coverage enabled for line deltas).
46
+ - `config/polyrun_spec_quality.yml` — thresholds and CI gates (`polyrun init --profile spec-quality`).
47
+ - Parallel: `run-shards --merge-spec-quality` → `coverage/polyrun-spec-quality.json` → `report-spec-quality`.
48
+
43
49
  ## Further reading
44
50
 
45
51
  - Polyrun README: partition, prepare, databases, merge-coverage, `Polyrun::Env::Ci`
@@ -16,3 +16,7 @@ partition:
16
16
  paths_build:
17
17
  all_glob: spec/**/*_spec.rb
18
18
  stages: []
19
+
20
+ # Optional reporting (see docs/SPEC_QUALITY.md):
21
+ # reporting:
22
+ # merge_spec_quality: true
@@ -0,0 +1,12 @@
1
+ # Optional Polyrun hooks — copy to config/polyrun_hooks.rb and reference from polyrun.yml:
2
+ # hooks:
3
+ # ruby_file: config/polyrun_hooks.rb
4
+ #
5
+ # Worker-phase hooks run in each parallel test child (around bundle exec rspec).
6
+
7
+ before(:each) do |env|
8
+ next unless env["POLYRUN_HOOK_ORCHESTRATOR"] == "0"
9
+ next unless defined?(Polyrun::SpecQuality) && Polyrun::SpecQuality.enabled?
10
+
11
+ Polyrun::SpecQuality::RspecHook.ensure_started!
12
+ end
@@ -0,0 +1,20 @@
1
+ # Opt-in per-example spec quality (coverage deltas, resource use). See docs/SPEC_QUALITY.md.
2
+ track_under:
3
+ - lib
4
+ - app
5
+ min_line_churn: 50
6
+ min_query_count: 20
7
+ hot_line_example_overlap: 10
8
+ strict: false
9
+ sample: 1.0
10
+ ignore_examples: []
11
+ ignore_paths: []
12
+ ignore_query_patterns: []
13
+ profile:
14
+ - cpu
15
+ - mem
16
+ sql_counter: false
17
+ # CI gates (used with POLYRUN_SPEC_QUALITY_STRICT=1 or strict: true):
18
+ # minimum_unique_lines_per_example: 1
19
+ # max_zero_hit_examples: 0
20
+ # max_hot_line_overlap: 100
@@ -17,6 +17,11 @@ prepare:
17
17
  rails_root: .
18
18
  command: bundle exec ruby bin/test_prepare.rb
19
19
 
20
+ # Optional reporting (see docs/SPEC_QUALITY.md):
21
+ # reporting:
22
+ # merge_spec_quality: true
23
+ # merge_spec_quality_output: coverage/polyrun-spec-quality.json
24
+
20
25
  # Uncomment and edit when using Postgres template + per-shard DB names from polyrun env:
21
26
  # databases:
22
27
  # template_db: my_app_template
@@ -1,12 +1,11 @@
1
1
  require "json"
2
2
 
3
3
  require_relative "../debug"
4
+ require_relative "stats"
5
+ require_relative "variance_report"
4
6
 
5
7
  module Polyrun
6
8
  module Timing
7
- # Merges per-shard timing JSON files (spec2 §2.4): path => wall seconds (float), or (experimental)
8
- # +absolute_path:line+ => seconds for per-example timing.
9
- # Disjoint suites: values merged by taking the maximum per path when duplicates appear.
10
9
  module Merge
11
10
  module_function
12
11
 
@@ -18,8 +17,8 @@ module Polyrun
18
17
 
19
18
  data.each do |file, sec|
20
19
  f = file.to_s
21
- t = sec.to_f
22
- merged[f] = merged.key?(f) ? [merged[f], t].max : t
20
+ entry = sec
21
+ merged[f] = merged.key?(f) ? Stats.merge_entries(merged[f], entry) : Stats.normalize_entry(entry)
23
22
  end
24
23
  end
25
24
  merged
@@ -29,6 +28,7 @@ module Polyrun
29
28
  Polyrun::Debug.log_kv(merge_timing: "merge_and_write", input_count: paths.size, output_path: output_path)
30
29
  merged = Polyrun::Debug.time("Timing::Merge.merge_files") { merge_files(paths) }
31
30
  Polyrun::Debug.time("Timing::Merge.write JSON") { File.write(output_path, JSON.pretty_generate(merged)) }
31
+ Timing::VarianceReport.emit_warnings!(merged)
32
32
  merged
33
33
  end
34
34
  end
@@ -1,6 +1,6 @@
1
1
  require "json"
2
2
 
3
- require "rspec/core/formatters/base_formatter"
3
+ require "rspec/core/formatters"
4
4
 
5
5
  module Polyrun
6
6
  module Timing
@@ -13,23 +13,23 @@ module Polyrun
13
13
  # Or {Polyrun::RSpec.install_example_timing!} (+output_path:+ avoids touching +ENV+).
14
14
  #
15
15
  # Default output path: +ENV["POLYRUN_EXAMPLE_TIMING_OUT"]+ if set, else +polyrun_timing_examples.json+.
16
- class RSpecExampleFormatter < RSpec::Core::Formatters::BaseFormatter
17
- RSpec::Core::Formatters.register self, :example_finished, :close
16
+ class RSpecExampleFormatter
17
+ ::RSpec::Core::Formatters.register self, :example_finished, :close
18
18
 
19
19
  def initialize(output)
20
- super
20
+ @output = output
21
21
  @times = {}
22
22
  end
23
23
 
24
24
  def example_finished(notification)
25
25
  ex = notification.example
26
- result = ex.execution_result
27
- return if result.pending?
26
+ return if ex.pending?
28
27
 
28
+ result = ex.execution_result
29
29
  t = result.run_time
30
30
  return unless t
31
31
 
32
- path = ex.metadata[:absolute_path]
32
+ path = example_absolute_path(ex)
33
33
  return unless path
34
34
 
35
35
  line = ex.metadata[:line_number]
@@ -48,6 +48,13 @@ module Polyrun
48
48
  def timing_output_path
49
49
  ENV["POLYRUN_EXAMPLE_TIMING_OUT"] || "polyrun_timing_examples.json"
50
50
  end
51
+
52
+ private
53
+
54
+ def example_absolute_path(example)
55
+ metadata = example.metadata
56
+ metadata[:absolute_file_path] || metadata[:file_path] || metadata[:absolute_path]
57
+ end
51
58
  end
52
59
  end
53
60
  end
@@ -0,0 +1,76 @@
1
+ module Polyrun
2
+ module Timing
3
+ # Normalizes scalar or object timing entries for merge and binpack weight lookup.
4
+ module Stats
5
+ STAT_KEYS = %w[last_seconds min max mean p95 runs failures timeouts].freeze
6
+
7
+ module_function
8
+
9
+ def normalize_entry(value)
10
+ case value
11
+ when Hash
12
+ normalize_hash(value)
13
+ else
14
+ sec = value.to_f
15
+ {
16
+ "last_seconds" => sec,
17
+ "min" => sec,
18
+ "max" => sec,
19
+ "mean" => sec,
20
+ "p95" => sec,
21
+ "runs" => 1,
22
+ "failures" => 0,
23
+ "timeouts" => 0
24
+ }
25
+ end
26
+ end
27
+
28
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- timing hash key coercion
29
+ def normalize_hash(h)
30
+ out = {}
31
+ sec = h["last_seconds"] || h[:last_seconds] || h["seconds"] || h[:seconds]
32
+ sec = sec.to_f if sec
33
+ mean = (h["mean"] || h[:mean] || sec)&.to_f
34
+ out["last_seconds"] = (sec || mean || 0.0).to_f
35
+ out["min"] = (h["min"] || h[:min] || out["last_seconds"]).to_f
36
+ out["max"] = (h["max"] || h[:max] || out["last_seconds"]).to_f
37
+ out["mean"] = (mean || out["last_seconds"]).to_f
38
+ out["p95"] = (h["p95"] || h[:p95] || out["max"]).to_f
39
+ out["runs"] = Integer(h["runs"] || h[:runs] || 1)
40
+ out["failures"] = Integer(h["failures"] || h[:failures] || 0)
41
+ out["timeouts"] = Integer(h["timeouts"] || h[:timeouts] || 0)
42
+ out
43
+ end
44
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
45
+
46
+ def binpack_weight(entry)
47
+ h = normalize_entry(entry)
48
+ h["last_seconds"].positive? ? h["last_seconds"] : h["mean"]
49
+ end
50
+
51
+ # rubocop:disable Metrics/AbcSize -- weighted mean merge
52
+ def merge_entries(a, b)
53
+ ha = normalize_entry(a)
54
+ hb = normalize_entry(b)
55
+ runs = ha["runs"] + hb["runs"]
56
+ mean =
57
+ if runs.positive?
58
+ ((ha["mean"] * ha["runs"]) + (hb["mean"] * hb["runs"])) / runs.to_f
59
+ else
60
+ 0.0
61
+ end
62
+ {
63
+ "last_seconds" => [ha["last_seconds"], hb["last_seconds"]].max,
64
+ "min" => [ha["min"], hb["min"]].min,
65
+ "max" => [ha["max"], hb["max"]].max,
66
+ "mean" => mean,
67
+ "p95" => [ha["p95"], hb["p95"]].max,
68
+ "runs" => runs,
69
+ "failures" => ha["failures"] + hb["failures"],
70
+ "timeouts" => ha["timeouts"] + hb["timeouts"]
71
+ }
72
+ end
73
+ # rubocop:enable Metrics/AbcSize
74
+ end
75
+ end
76
+ end
@@ -1,14 +1,17 @@
1
+ require_relative "stats"
2
+
1
3
  module Polyrun
2
4
  module Timing
3
5
  # Human-readable slow-file list from merged timing JSON (per-file cost).
4
6
  module Summary
5
7
  module_function
6
8
 
7
- # +merged+ is path (String) => seconds (Float), as produced by +Timing::Merge.merge_files+.
9
+ # +merged+ is path (String) => seconds (Float) or stats Hash, as produced by +Timing::Merge.merge_files+.
8
10
  def format_slow_files(merged, top: 30, title: "Polyrun slowest files (by wall time, seconds)")
9
11
  return "#{title}\n (no data)\n" if merged.nil? || merged.empty?
10
12
 
11
- pairs = merged.sort_by { |_, sec| -sec.to_f }.first(Integer(top))
13
+ pairs = merged.map { |path, sec| [path, Stats.binpack_weight(sec)] }
14
+ .sort_by { |(_, sec)| -sec.to_f }.first(Integer(top))
12
15
  lines = [title, ""]
13
16
  pairs.each_with_index do |(path, sec), i|
14
17
  lines << format(" %2d. %s %.4f", i + 1, path, sec.to_f)
@@ -0,0 +1,51 @@
1
+ module Polyrun
2
+ module Timing
3
+ # Flags high-variance, flaky, and regression timing entries.
4
+ module VarianceReport
5
+ module_function
6
+
7
+ # rubocop:disable Metrics/AbcSize -- variance flag scan
8
+ def analyze(merged_stats)
9
+ flags = []
10
+ merged_stats.each do |path, entry|
11
+ h = Stats.normalize_entry(entry)
12
+ next if h["runs"] < 2
13
+
14
+ median = h["mean"]
15
+ if median.positive? && (h["p95"] / median) > 2.0
16
+ flags << {path: path, kind: "high_variance", detail: "p95/mean=#{format("%.2f", h["p95"] / median)}"}
17
+ end
18
+
19
+ if h["runs"] >= 3 && (h["failures"].to_f / h["runs"]) > 0.3
20
+ flags << {path: path, kind: "often_failed", detail: "failures=#{h["failures"]}/#{h["runs"]}"}
21
+ end
22
+
23
+ if h["timeouts"].to_i >= 2
24
+ flags << {path: path, kind: "timeout_cluster", detail: "timeouts=#{h["timeouts"]}"}
25
+ end
26
+
27
+ if h["mean"].positive? && h["last_seconds"] > (2.0 * h["mean"])
28
+ flags << {path: path, kind: "runtime_regression", detail: "last=#{h["last_seconds"]} mean=#{h["mean"]}"}
29
+ end
30
+ end
31
+ flags
32
+ end
33
+ # rubocop:enable Metrics/AbcSize
34
+
35
+ def emit_warnings!(merged_stats)
36
+ analyze(merged_stats).each do |f|
37
+ Polyrun::Log.warn "polyrun timing #{f[:kind]}: #{f[:path]} (#{f[:detail]})"
38
+ end
39
+ end
40
+
41
+ def format_report(merged_stats)
42
+ lines = ["Polyrun timing variance report", ""]
43
+ analyze(merged_stats).each do |f|
44
+ lines << " [#{f[:kind]}] #{f[:path]} — #{f[:detail]}"
45
+ end
46
+ lines << " (none)" if lines.size == 2
47
+ lines.join("\n") + "\n"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,3 +1,3 @@
1
1
  module Polyrun
2
- VERSION = "1.5.0"
2
+ VERSION = "2.1.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.5.0
4
+ version: 2.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -180,12 +180,14 @@ files:
180
180
  - lib/polyrun/cli/helpers.rb
181
181
  - lib/polyrun/cli/hooks_command.rb
182
182
  - lib/polyrun/cli/init_command.rb
183
+ - lib/polyrun/cli/partition_diagnostics.rb
183
184
  - lib/polyrun/cli/plan_command.rb
184
185
  - lib/polyrun/cli/prepare_command.rb
185
186
  - lib/polyrun/cli/prepare_recipe.rb
186
187
  - lib/polyrun/cli/queue_command.rb
187
188
  - lib/polyrun/cli/quick_command.rb
188
189
  - lib/polyrun/cli/report_commands.rb
190
+ - lib/polyrun/cli/run_queue_command.rb
189
191
  - lib/polyrun/cli/run_shards_command.rb
190
192
  - lib/polyrun/cli/run_shards_parallel_children.rb
191
193
  - lib/polyrun/cli/run_shards_parallel_wait.rb
@@ -194,6 +196,7 @@ files:
194
196
  - lib/polyrun/cli/run_shards_planning.rb
195
197
  - lib/polyrun/cli/run_shards_run.rb
196
198
  - lib/polyrun/cli/run_shards_worker_interrupt.rb
199
+ - lib/polyrun/cli/spec_quality_commands.rb
197
200
  - lib/polyrun/cli/start_bootstrap.rb
198
201
  - lib/polyrun/cli/timing_command.rb
199
202
  - lib/polyrun/config.rb
@@ -204,6 +207,7 @@ files:
204
207
  - lib/polyrun/coverage/collector.rb
205
208
  - lib/polyrun/coverage/collector_finish.rb
206
209
  - lib/polyrun/coverage/collector_fragment_meta.rb
210
+ - lib/polyrun/coverage/example_diff.rb
207
211
  - lib/polyrun/coverage/filter.rb
208
212
  - lib/polyrun/coverage/formatter.rb
209
213
  - lib/polyrun/coverage/merge.rb
@@ -252,11 +256,14 @@ files:
252
256
  - lib/polyrun/partition/plan.rb
253
257
  - lib/polyrun/partition/plan_lpt.rb
254
258
  - lib/polyrun/partition/plan_sharding.rb
259
+ - lib/polyrun/partition/reports.rb
255
260
  - lib/polyrun/partition/stable_shuffle.rb
261
+ - lib/polyrun/partition/timing_diagnostics.rb
256
262
  - lib/polyrun/partition/timing_keys.rb
257
263
  - lib/polyrun/prepare/artifacts.rb
258
264
  - lib/polyrun/prepare/assets.rb
259
265
  - lib/polyrun/process_stdio.rb
266
+ - lib/polyrun/queue/duration.rb
260
267
  - lib/polyrun/queue/file_store.rb
261
268
  - lib/polyrun/queue/file_store_pending.rb
262
269
  - lib/polyrun/quick.rb
@@ -274,13 +281,27 @@ files:
274
281
  - lib/polyrun/reporting/rspec_failure_fragment_formatter.rb
275
282
  - lib/polyrun/reporting/rspec_junit.rb
276
283
  - lib/polyrun/rspec.rb
284
+ - lib/polyrun/spec_quality.rb
285
+ - lib/polyrun/spec_quality/config.rb
286
+ - lib/polyrun/spec_quality/fragment.rb
287
+ - lib/polyrun/spec_quality/merge.rb
288
+ - lib/polyrun/spec_quality/minitest_hook.rb
289
+ - lib/polyrun/spec_quality/plan_loader.rb
290
+ - lib/polyrun/spec_quality/profile.rb
291
+ - lib/polyrun/spec_quality/report.rb
292
+ - lib/polyrun/spec_quality/rspec_hook.rb
293
+ - lib/polyrun/spec_quality/sql_counter.rb
277
294
  - lib/polyrun/templates/POLYRUN.md
278
295
  - lib/polyrun/templates/ci_matrix.polyrun.yml
279
296
  - lib/polyrun/templates/minimal_gem.polyrun.yml
297
+ - lib/polyrun/templates/polyrun_hooks_spec_quality.rb
298
+ - lib/polyrun/templates/polyrun_spec_quality.yml
280
299
  - lib/polyrun/templates/rails_prepare.polyrun.yml
281
300
  - lib/polyrun/timing/merge.rb
282
301
  - lib/polyrun/timing/rspec_example_formatter.rb
302
+ - lib/polyrun/timing/stats.rb
283
303
  - lib/polyrun/timing/summary.rb
304
+ - lib/polyrun/timing/variance_report.rb
284
305
  - lib/polyrun/version.rb
285
306
  - lib/polyrun/worker_ping.rb
286
307
  - polyrun.gemspec