polyrun 2.2.7 → 2.2.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae7b48f9387b3b00e9bb18163b450f29ac09b93c313548b182a837595a1bed02
4
- data.tar.gz: 41497f023278ecd79942d184263640653accb8bb34740310c0f21d2981fe3746
3
+ metadata.gz: 8f6a7a0e969581e53f92c3e1bd4dfbb046c8321fb25601d58be12d4cc8cb6a96
4
+ data.tar.gz: 0c4625804e0ee0ce3f695570001658f9d907d9102c059258efa9c00fdf16d669
5
5
  SHA512:
6
- metadata.gz: '09ca1c1d70818a1d7c41f5084109ff96d2b2560f90840ece694b3af3db558f65d0844d0a2e62c0fc15a4b1bce01cb065abb68793620bd08e2d38b4f96948be39'
7
- data.tar.gz: f4b992eba7b8fe215ac9e7a66dd3002482f73e2c56cd1566e4021336f280351dd2f6ad5a003a323445f56bd7d445af06d0e07da8bed303542001cc55657afadb
6
+ metadata.gz: 663f89b311212915efcb030db4cf88feb7f3f78b981746d63ee8c5548fec1abd23e13df3b307f4173601e4b3b829dab9ac56664caecfd929ed48c585deec58c2
7
+ data.tar.gz: be540c7af7282a2868408a6e8fa7837976ae2ffe443acbec7e65bb6e881d0bce419a344cf75573cadb2af0f562272032eabaf30564e2fab9a998f76029e37c07
data/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
1
  # CHANGELOG
2
2
 
3
- ## Unreleased
3
+ ## 2.2.8 (2026-11-04)
4
+
5
+ - Honor top-level `workers:` in `polyrun.yml` when `POLYRUN_WORKERS` is unset (`polyrun config workers`, `start`, `run-shards`, `run-queue`, `doctor`).
6
+ - Resolve coverage from `config/polyrun_coverage.yml` when that file is present, and otherwise from inline `coverage:` in `polyrun.yml`. Map `formats` and a directory `output_path` to the report keys.
7
+ - Apply the selected suite and an explicit `-c` or `POLYRUN_CONFIG` to coverage collection, the merge gate, and `polyrun doctor`.
8
+ - Add `polyrun validate` for unknown top-level keys and a non-mapping YAML root.
9
+ - Add `version`, `start`, empty `hooks`, and `merge_failures` commentary to the Rails `polyrun init` template.
10
+ - Add named `suites:` overlays selected with `--suite` or `POLYRUN_SUITE` (partition membership, workers, and related top-level keys).
11
+ - Add `polyrun doctor` for the effective config summary (file, suite, start gates, database adapter and shards, coverage source).
12
+ - Accept `POLYRUN_SKIP_PREPARE` and `POLYRUN_SKIP_DATABASES` as the skip switches. Keep `POLYRUN_START_SKIP_*` working, with a deprecation warning.
13
+ - Add `polyrun init --profile rails-prepare-task`, which writes `lib/tasks/polyrun_prepare.rake`.
4
14
 
5
15
  ## 2.2.7 (2026-11-04)
6
16
 
@@ -100,7 +100,7 @@ module Polyrun
100
100
  code = merge_coverage_after_shards_run_merge(files, output, format_list, config_path)
101
101
  return code unless code == 0
102
102
 
103
- merge_coverage_after_shards_strict_gate(output, code)
103
+ merge_coverage_after_shards_strict_gate(output, code, config_path: config_path)
104
104
  rescue JSON::ParserError => e
105
105
  Polyrun::Log.warn "polyrun run-shards: merged coverage JSON parse failed: #{e.message}"
106
106
  1
@@ -126,8 +126,8 @@ module Polyrun
126
126
  end
127
127
  end
128
128
 
129
- def merge_coverage_after_shards_strict_gate(output, code)
130
- gate = coverage_minimum_line_gate_from_polyrun_coverage_yml
129
+ def merge_coverage_after_shards_strict_gate(output, code, config_path: nil)
130
+ gate = coverage_minimum_line_gate_from_polyrun_coverage_yml(config_path: config_path)
131
131
  Polyrun::Debug.log_kv(
132
132
  coverage_gate_config: "polyrun_coverage.yml",
133
133
  gate: gate.inspect
@@ -0,0 +1,94 @@
1
+ require_relative "../coverage/config_resolve"
2
+ require_relative "skip_env"
3
+
4
+ module Polyrun
5
+ class CLI
6
+ module DoctorCommand
7
+ private
8
+
9
+ def cmd_doctor(argv, config_path)
10
+ unless argv.empty?
11
+ Polyrun::Log.warn "polyrun doctor: unexpected arguments: #{argv.join(" ")}"
12
+ return 2
13
+ end
14
+
15
+ cfg = Polyrun::Config.load(path: config_path || ENV["POLYRUN_CONFIG"])
16
+ lines = doctor_lines(cfg)
17
+ lines.each { |line| Polyrun::Log.puts line }
18
+ 0
19
+ end
20
+
21
+ def doctor_lines(cfg)
22
+ workers = Polyrun::Config::Resolver.parallel_worker_count(cfg).clamp(1, Polyrun::Config::MAX_PARALLEL_WORKERS)
23
+ cov = Polyrun::Coverage::ConfigResolve.load(root: Dir.pwd, polyrun_config_path: cfg.path)
24
+ lines = []
25
+ lines << "polyrun doctor"
26
+ lines << " config: #{cfg.path || "(none)"}"
27
+ lines << " suite: #{cfg.suite || "(none)"}"
28
+ lines << " workers: #{workers}"
29
+ lines.concat(doctor_start_lines(cfg))
30
+ lines.concat(doctor_database_lines(cfg, workers))
31
+ lines.concat(doctor_coverage_lines(cov))
32
+ lines.concat(doctor_warning_lines(cfg, workers))
33
+ lines
34
+ end
35
+
36
+ def doctor_start_lines(cfg)
37
+ prep = start_run_prepare?(cfg)
38
+ dbs = start_databases_provision_wanted?(cfg)
39
+ [
40
+ " start.prepare: #{doctor_yes_no(prep, SkipEnv.skip_prepare?)}",
41
+ " start.databases: #{doctor_yes_no(dbs, SkipEnv.skip_databases?)}"
42
+ ]
43
+ end
44
+
45
+ def doctor_yes_no(wanted, skipped)
46
+ text = wanted ? "yes" : "no"
47
+ skipped ? "#{text} (skipped by env)" : text
48
+ end
49
+
50
+ def doctor_database_lines(cfg, workers)
51
+ dh = cfg.databases
52
+ return [" databases: (none)"] unless dh.is_a?(Hash) && !dh.empty?
53
+
54
+ adapter = Polyrun::Database::UrlBuilder::ConnectionInfer.infer_adapter_name(dh)
55
+ lines = [" databases.adapter: #{adapter || "(unknown)"}"]
56
+ if adapter == "postgresql"
57
+ names = (0...workers).map do |i|
58
+ Polyrun::Database::UrlBuilder.shard_database_name(dh, shard_index: i)
59
+ rescue Polyrun::Error
60
+ nil
61
+ end.compact
62
+ lines << " databases.shards: #{names.join(", ")}" unless names.empty?
63
+ lines << " databases.provision: CloneShards (PostgreSQL)"
64
+ else
65
+ lines << " databases.provision: URL shaping only (no auto clone)"
66
+ end
67
+ lines
68
+ rescue Polyrun::Error => e
69
+ [" databases: error (#{e.message})"]
70
+ end
71
+
72
+ def doctor_coverage_lines(cov)
73
+ disable = %w[1 true yes].include?(ENV["POLYRUN_COVERAGE_DISABLE"].to_s.downcase)
74
+ [
75
+ " coverage.source: #{cov["coverage_config_source"] || "none"}",
76
+ " coverage.disable: #{disable ? "yes (POLYRUN_COVERAGE_DISABLE)" : "no"}"
77
+ ]
78
+ end
79
+
80
+ def doctor_warning_lines(cfg, workers)
81
+ warnings = []
82
+ matrix_total = ENV["POLYRUN_SHARD_TOTAL"].to_s
83
+ if !matrix_total.empty? && matrix_total.to_i > 1 && workers > 1
84
+ warnings << " warn: matrix POLYRUN_SHARD_TOTAL=#{matrix_total} and in-process workers=#{workers} are both set; confirm fragment naming and merge story"
85
+ end
86
+ suite_files = Dir.glob("polyrun.*.yml").reject { |p| p == "polyrun.yml" }
87
+ if cfg.path && !suite_files.empty? && cfg.suite.nil?
88
+ warnings << " warn: named suite files present (#{suite_files.sort.join(", ")}); prefer suites: in root polyrun.yml + --suite"
89
+ end
90
+ warnings
91
+ end
92
+ end
93
+ end
94
+ end
@@ -9,6 +9,7 @@ module Polyrun
9
9
 
10
10
  global:
11
11
  -c, --config PATH polyrun.yml path (or POLYRUN_CONFIG)
12
+ --suite NAME apply suites.NAME overlay from polyrun.yml (or POLYRUN_SUITE)
12
13
  -v, --verbose
13
14
  -h, --help
14
15
 
@@ -18,11 +19,13 @@ module Polyrun
18
19
  Hooks shell output: POLYRUN_HOOKS_VERBOSE=1 or -v / POLYRUN_VERBOSE=1 (or DEBUG); failures always print
19
20
  Merge profiling (stderr): POLYRUN_PROFILE_MERGE=1 (or verbose / DEBUG)
20
21
  Post-merge formats (run-shards): POLYRUN_MERGE_FORMATS (default: json,lcov,cobertura,console,html,csv,markdown)
21
- Start skips: POLYRUN_SKIP_BUILD_SPEC_PATHS=1, POLYRUN_START_SKIP_PREPARE=1, POLYRUN_START_SKIP_DATABASES=1
22
+ Start skips: POLYRUN_SKIP_PREPARE=1, POLYRUN_SKIP_DATABASES=1 (legacy aliases: POLYRUN_START_SKIP_PREPARE, POLYRUN_START_SKIP_DATABASES); POLYRUN_SKIP_BUILD_SPEC_PATHS=1
22
23
  Paths build skip: POLYRUN_SKIP_PATHS_BUILD=1
24
+ Named suite: --suite NAME or POLYRUN_SUITE (see suites: in polyrun.yml)
23
25
  Slow merge warning (seconds, default 10; 0 disables): POLYRUN_MERGE_SLOW_WARN_SECONDS
24
26
  Failure merge: run-shards --merge-failures (enable failure fragments in test setup); POLYRUN_MERGE_FAILURES=1, POLYRUN_FAILURE_FRAGMENT_DIR, POLYRUN_MERGED_FAILURES_OUT
25
- Parallel workers: POLYRUN_WORKERS default 5, max 10 (run-shards / parallel-rspec / start). CI local processes per job: POLYRUN_SHARD_PROCESSES or ci-shard --shard-processes (not POLYRUN_WORKERS)
27
+ Parallel workers: POLYRUN_WORKERS, else top-level workers: in polyrun.yml, else default 5, max 10 (run-shards / parallel-rspec / start). CI local processes per job: POLYRUN_SHARD_PROCESSES or ci-shard --shard-processes (not POLYRUN_WORKERS)
28
+ Coverage config: config/polyrun_coverage.yml overlays inline coverage: in polyrun.yml (formats -> report_formats; directory output_path -> report_output_dir)
26
29
  Per-worker wall timeout: --worker-timeout SEC or POLYRUN_WORKER_TIMEOUT_SEC. Exit 124; parent stops remaining workers.
27
30
  Per-worker idle timeout: --worker-idle-timeout SEC or POLYRUN_WORKER_IDLE_TIMEOUT_SEC after a progress ping (POLYRUN_WORKER_PING_FILE). Enable pings in test setup. Exit 125. Optional periodic pings: POLYRUN_WORKER_PING_THREAD=1 (POLYRUN_WORKER_PING_INTERVAL_SEC).
28
31
  Worker output routing (opt-in): POLYRUN_WORKER_OUTPUT_ROUTING=1 or POLYRUN_WORKER_LOG_DIR; per-shard logs under tmp/polyrun/workers (POLYRUN_WORKER_OUTPUT_PREFIX=0 for log-only)
@@ -61,6 +64,8 @@ module Polyrun
61
64
  merge-spec-quality merge polyrun-spec-quality-fragment-*.jsonl shards
62
65
  report-spec-quality spec quality report from merged JSON (text/json/csv/markdown)
63
66
  config print effective config by dotted path (loaded YAML plus merged prepare.env, resolved partition shard fields, workers)
67
+ validate check polyrun.yml top-level keys (unknown keys exit 1)
68
+ doctor print effective config summary (file, suite, start gates, DBs, coverage)
64
69
  env print shard + database env (see polyrun.yml databases)
65
70
  db:setup-template migrate template DB (PostgreSQL)
66
71
  db:setup-shard CREATE DATABASE shard FROM template (one POLYRUN_SHARD_INDEX)
@@ -89,24 +89,10 @@ module Polyrun
89
89
  [abs]
90
90
  end
91
91
 
92
- # Same rounding/strict semantics as {Polyrun::Coverage::Collector} for +config/polyrun_coverage.yml+.
93
- def coverage_minimum_line_gate_from_polyrun_coverage_yml
94
- path = File.join(Dir.pwd, "config", "polyrun_coverage.yml")
95
- return nil unless File.file?(path)
96
-
97
- data = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: true)
98
- return nil unless data.is_a?(Hash)
99
-
100
- min = data["minimum_line_percent"] || data[:minimum_line_percent]
101
- return nil if min.nil?
102
-
103
- sv = data["strict"] if data.key?("strict")
104
- sv = data[:strict] if !data.key?("strict") && data.key?(:strict)
105
- strict = sv.nil? || sv
106
-
107
- {minimum: min.to_f, strict: strict != false}
108
- rescue Psych::SyntaxError, ArgumentError, TypeError
109
- nil
92
+ # Same rounding/strict semantics as {Polyrun::Coverage::Collector}; see {Coverage::ConfigResolve}.
93
+ def coverage_minimum_line_gate_from_polyrun_coverage_yml(config_path: nil)
94
+ require_relative "../coverage/config_resolve"
95
+ Polyrun::Coverage::ConfigResolve.minimum_line_gate(root: Dir.pwd, polyrun_config_path: config_path)
110
96
  end
111
97
 
112
98
  def load_partition_constraints(pc, constraints_path)
@@ -7,6 +7,7 @@ module Polyrun
7
7
  INIT_PROFILES = {
8
8
  "gem" => "minimal_gem.polyrun.yml",
9
9
  "rails" => "rails_prepare.polyrun.yml",
10
+ "rails-prepare-task" => "rails_prepare_task.rake",
10
11
  "ci-matrix" => "ci_matrix.polyrun.yml",
11
12
  "spec-quality" => "polyrun_spec_quality.yml",
12
13
  "doc" => "POLYRUN.md"
@@ -98,6 +99,7 @@ module Polyrun
98
99
  case profile
99
100
  when "doc" then "POLYRUN.md"
100
101
  when "spec-quality" then "config/polyrun_spec_quality.yml"
102
+ when "rails-prepare-task" then "lib/tasks/polyrun_prepare.rake"
101
103
  else "polyrun.yml"
102
104
  end
103
105
  end
@@ -42,7 +42,7 @@ module Polyrun
42
42
  plan_command_register_partition_options!(opts, ctx)
43
43
  end.parse!(head)
44
44
 
45
- workers ||= env_int("POLYRUN_WORKERS", Polyrun::Config::DEFAULT_PARALLEL_WORKERS)
45
+ workers ||= Polyrun::Config::Resolver.parallel_worker_count(cfg)
46
46
  unless workers.is_a?(Integer) && workers.between?(1, Polyrun::Config::MAX_PARALLEL_WORKERS)
47
47
  Polyrun::Log.warn "polyrun run-queue: --workers must be between 1 and #{Polyrun::Config::MAX_PARALLEL_WORKERS}"
48
48
  return 2
@@ -8,8 +8,8 @@ module Polyrun
8
8
  private
9
9
 
10
10
  # @return [:fail, Integer] | [:ok, Hash, Array<String>]
11
- def run_shards_plan_phase_a(head, cmd, pc)
12
- o = run_shards_plan_options(head, pc)
11
+ def run_shards_plan_phase_a(head, cmd, pc, cfg = nil)
12
+ o = run_shards_plan_options(head, pc, cfg: cfg)
13
13
  code = Polyrun::Partition::PathsBuild.apply!(partition: pc, cwd: Dir.pwd)
14
14
  return [:fail, code] if code != 0
15
15
 
@@ -5,17 +5,17 @@ module Polyrun
5
5
  module RunShardsPlanOptions
6
6
  private
7
7
 
8
- def run_shards_plan_options(head, pc)
9
- st = run_shards_plan_options_state(pc)
8
+ def run_shards_plan_options(head, pc, cfg: nil)
9
+ st = run_shards_plan_options_state(pc, cfg: cfg)
10
10
  run_shards_plan_options_parse!(head, st)
11
11
  st[:paths_file] ||= pc["paths_file"] || pc[:paths_file]
12
12
  st[:timing_granularity] = resolve_partition_timing_granularity(pc, st[:timing_granularity])
13
13
  st
14
14
  end
15
15
 
16
- def run_shards_plan_options_state(pc)
16
+ def run_shards_plan_options_state(pc, cfg: nil)
17
17
  {
18
- workers: env_int("POLYRUN_WORKERS", Polyrun::Config::DEFAULT_PARALLEL_WORKERS),
18
+ workers: Polyrun::Config::Resolver.parallel_worker_count(cfg),
19
19
  paths_file: nil,
20
20
  strategy: (pc["strategy"] || pc[:strategy] || "round_robin").to_s,
21
21
  strategy_explicit: !!(pc["strategy"] || pc[:strategy]),
@@ -16,7 +16,7 @@ module Polyrun
16
16
  return boot if boot.size == 2
17
17
 
18
18
  run_t0, head, cmd, cfg, pc = boot
19
- phase = run_shards_plan_phase_a(head, cmd, pc)
19
+ phase = run_shards_plan_phase_a(head, cmd, pc, cfg)
20
20
  return [phase[1], nil] if phase[0] == :fail
21
21
 
22
22
  _tag, o, cmd = phase
@@ -0,0 +1,34 @@
1
+ module Polyrun
2
+ class CLI
3
+ # Canonical start skip flags are +POLYRUN_SKIP_PREPARE+ / +POLYRUN_SKIP_DATABASES+.
4
+ # Legacy +POLYRUN_START_SKIP_*+ aliases remain accepted during the deprecation window.
5
+ module SkipEnv
6
+ module_function
7
+
8
+ def skip_prepare?(env = ENV)
9
+ truthy?(env, "POLYRUN_SKIP_PREPARE") ||
10
+ legacy_truthy?(env, "POLYRUN_START_SKIP_PREPARE", "POLYRUN_SKIP_PREPARE")
11
+ end
12
+
13
+ def skip_databases?(env = ENV)
14
+ truthy?(env, "POLYRUN_SKIP_DATABASES") ||
15
+ legacy_truthy?(env, "POLYRUN_START_SKIP_DATABASES", "POLYRUN_SKIP_DATABASES")
16
+ end
17
+
18
+ def truthy?(env, name)
19
+ %w[1 true yes].include?(env[name].to_s.downcase)
20
+ end
21
+ private_class_method :truthy?
22
+
23
+ def legacy_truthy?(env, legacy, canonical)
24
+ return false unless truthy?(env, legacy)
25
+
26
+ unless truthy?(env, canonical)
27
+ Polyrun::Log.warn "polyrun: #{legacy}=1 is deprecated; use #{canonical}=1"
28
+ end
29
+ true
30
+ end
31
+ private_class_method :legacy_truthy?
32
+ end
33
+ end
34
+ end
@@ -1,3 +1,5 @@
1
+ require_relative "skip_env"
2
+
1
3
  module Polyrun
2
4
  class CLI
3
5
  # +polyrun start+ only: run +prepare+ and/or Postgres template+shard DBs before parallel RSpec.
@@ -5,14 +7,14 @@ module Polyrun
5
7
  private
6
8
 
7
9
  def start_bootstrap!(cfg, argv, config_path)
8
- if start_run_prepare?(cfg) && !truthy_env?("POLYRUN_START_SKIP_PREPARE")
10
+ if start_run_prepare?(cfg) && !SkipEnv.skip_prepare?
9
11
  recipe = cfg.prepare["recipe"] || cfg.prepare[:recipe] || "default"
10
12
  Polyrun::Log.warn "polyrun start: running prepare (recipe=#{recipe})" if @verbose
11
13
  code = cmd_prepare([], config_path)
12
14
  return code if code != 0
13
15
  end
14
16
 
15
- return 0 unless start_databases_provision_wanted?(cfg) && !truthy_env?("POLYRUN_START_SKIP_DATABASES")
17
+ return 0 unless start_databases_provision_wanted?(cfg) && !SkipEnv.skip_databases?
16
18
 
17
19
  dh = cfg.databases
18
20
  adapter = Polyrun::Database::UrlBuilder::ConnectionInfer.infer_adapter_name(dh)
@@ -20,7 +22,7 @@ module Polyrun
20
22
  return start_non_postgresql_databases_outcome(cfg, adapter)
21
23
  end
22
24
 
23
- workers = parse_workers_from_start_argv(argv)
25
+ workers = parse_workers_from_start_argv(argv, cfg)
24
26
  Polyrun::Log.warn "polyrun start: provisioning test DBs (template + shards 0..#{workers - 1})" if @verbose
25
27
  begin
26
28
  Polyrun::Database::CloneShards.provision!(
@@ -94,10 +96,10 @@ module Polyrun
94
96
  0
95
97
  end
96
98
 
97
- def parse_workers_from_start_argv(argv)
99
+ def parse_workers_from_start_argv(argv, cfg = nil)
98
100
  sep = argv.index("--")
99
101
  head = sep ? argv[0...sep] : argv
100
- workers = env_int("POLYRUN_WORKERS", Polyrun::Config::DEFAULT_PARALLEL_WORKERS)
102
+ workers = Polyrun::Config::Resolver.parallel_worker_count(cfg)
101
103
  i = 0
102
104
  while i < head.size
103
105
  if head[i] == "--workers" && head[i + 1]
@@ -0,0 +1,26 @@
1
+ require_relative "../config/schema"
2
+
3
+ module Polyrun
4
+ class CLI
5
+ module ValidateCommand
6
+ private
7
+
8
+ def cmd_validate(argv, config_path)
9
+ unless argv.empty?
10
+ Polyrun::Log.warn "polyrun validate: unexpected arguments: #{argv.join(" ")}"
11
+ return 2
12
+ end
13
+
14
+ cfg = Polyrun::Config.load(path: config_path || ENV["POLYRUN_CONFIG"])
15
+ result = Polyrun::Config::Schema.validate(cfg)
16
+ result[:messages].each { |m| Polyrun::Log.warn m }
17
+ if result[:ok]
18
+ Polyrun::Log.puts "polyrun validate: ok (#{cfg.path || "no file"})"
19
+ 0
20
+ else
21
+ 1
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/polyrun/cli.rb CHANGED
@@ -1,4 +1,4 @@
1
- # rubocop:disable Polyrun/FileLength -- CLI dispatch + require wiring
1
+ # rubocop:disable Polyrun/FileLength, Metrics/ClassLength -- CLI dispatch + require wiring
2
2
  require "optparse"
3
3
 
4
4
  require_relative "cli/helpers"
@@ -20,6 +20,8 @@ require_relative "cli/quick_command"
20
20
  require_relative "cli/ci_shard_run_parse"
21
21
  require_relative "cli/ci_shard_run_command"
22
22
  require_relative "cli/config_command"
23
+ require_relative "cli/validate_command"
24
+ require_relative "cli/doctor_command"
23
25
  require_relative "cli/default_run"
24
26
  require_relative "cli/hooks_command"
25
27
  require_relative "cli/help"
@@ -35,7 +37,7 @@ module Polyrun
35
37
  DISPATCH_SUBCOMMAND_NAMES = %w[
36
38
  plan prepare merge-coverage merge-failures merge-spec-quality report-coverage report-junit report-timing report-spec-quality report-benchmark bench
37
39
  env config merge-timing db:setup-template db:setup-shard db:clone-shards
38
- run-shards parallel-rspec parallel-minitest parallel-quick start build-paths init queue run-queue quick hook
40
+ run-shards parallel-rspec parallel-minitest parallel-quick start build-paths init queue run-queue quick hook validate doctor
39
41
  ].freeze
40
42
 
41
43
  # First argv token that is a normal subcommand (not a path); if argv[0] is not here but looks like paths, run implicit parallel.
@@ -62,6 +64,8 @@ module Polyrun
62
64
  include CiShardRunParse
63
65
  include CiShardRunCommand
64
66
  include ConfigCommand
67
+ include ValidateCommand
68
+ include DoctorCommand
65
69
  include DefaultRun
66
70
  include HooksCommand
67
71
  include Help
@@ -71,10 +75,14 @@ module Polyrun
71
75
  end
72
76
 
73
77
  def run(argv)
78
+ original_suite = ENV["POLYRUN_SUITE"]
79
+ original_config = ENV["POLYRUN_CONFIG"]
74
80
  argv = argv.dup
75
81
  config_path = parse_global_cli!(argv)
76
82
  return config_path if config_path.is_a?(Integer)
77
83
 
84
+ ENV["POLYRUN_CONFIG"] = File.expand_path(config_path) if config_path
85
+
78
86
  if argv.empty?
79
87
  Polyrun::Debug.log_kv(
80
88
  command: "(default)",
@@ -111,6 +119,9 @@ module Polyrun
111
119
  rescue Polyrun::Error => e
112
120
  Polyrun::Log.warn "polyrun: #{e.message}"
113
121
  2
122
+ ensure
123
+ ENV["POLYRUN_SUITE"] = original_suite
124
+ ENV["POLYRUN_CONFIG"] = original_config
114
125
  end
115
126
 
116
127
  private
@@ -123,6 +134,18 @@ module Polyrun
123
134
  when "-c", "--config"
124
135
  argv.shift
125
136
  config_path = argv.shift or break
137
+ when "--suite"
138
+ argv.shift
139
+ name = argv.shift
140
+ raise Polyrun::Error, "--suite requires a name" if name.nil? || name.strip.empty? || name.start_with?("-")
141
+
142
+ ENV["POLYRUN_SUITE"] = name
143
+ when /\A--suite=(.*)\z/
144
+ name = Regexp.last_match(1)
145
+ raise Polyrun::Error, "--suite requires a name" if name.strip.empty?
146
+
147
+ ENV["POLYRUN_SUITE"] = name
148
+ argv.shift
126
149
  when "-v", "--verbose"
127
150
  @verbose = true
128
151
  ENV["POLYRUN_VERBOSE"] = "1"
@@ -170,6 +193,10 @@ module Polyrun
170
193
  cmd_env(argv, config_path)
171
194
  when "config"
172
195
  cmd_config(argv, config_path)
196
+ when "validate"
197
+ cmd_validate(argv, config_path)
198
+ when "doctor"
199
+ cmd_doctor(argv, config_path)
173
200
  when "merge-timing"
174
201
  cmd_merge_timing(argv)
175
202
  when "merge-spec-quality"
@@ -223,4 +250,4 @@ module Polyrun
223
250
  end
224
251
  end
225
252
  end
226
- # rubocop:enable Polyrun/FileLength
253
+ # rubocop:enable Polyrun/FileLength, Metrics/ClassLength
@@ -49,7 +49,8 @@ module Polyrun
49
49
  )
50
50
  base["partition"] = part
51
51
 
52
- base["workers"] = r.parallel_worker_count_default(env)
52
+ base["workers"] = r.parallel_worker_count(cfg, env)
53
+ base["suite"] = cfg.suite if cfg.respond_to?(:suite) && cfg.suite
53
54
 
54
55
  base
55
56
  end
@@ -73,6 +73,33 @@ module Polyrun
73
73
  def parallel_worker_count_default(env = ENV)
74
74
  env_int("POLYRUN_WORKERS", Polyrun::Config::DEFAULT_PARALLEL_WORKERS, env)
75
75
  end
76
+
77
+ # +POLYRUN_WORKERS+ then top-level YAML +workers:+ then {Config::DEFAULT_PARALLEL_WORKERS}.
78
+ def parallel_worker_count(cfg = nil, env = ENV)
79
+ s = env["POLYRUN_WORKERS"]
80
+ unless s.nil? || s.empty?
81
+ i = Integer(s, exception: false)
82
+ return i unless i.nil?
83
+ end
84
+
85
+ yaml_workers = yaml_top_level_workers(cfg)
86
+ return yaml_workers unless yaml_workers.nil?
87
+
88
+ Polyrun::Config::DEFAULT_PARALLEL_WORKERS
89
+ end
90
+
91
+ def yaml_top_level_workers(cfg)
92
+ return nil unless cfg
93
+
94
+ raw = cfg.respond_to?(:raw) ? cfg.raw : cfg
95
+ return nil unless raw.is_a?(Hash)
96
+
97
+ v = raw["workers"] || raw[:workers]
98
+ return nil if v.nil? || v.to_s.empty?
99
+
100
+ Integer(v, exception: false)
101
+ end
102
+ private_class_method :yaml_top_level_workers
76
103
  end
77
104
  end
78
105
  end
@@ -0,0 +1,34 @@
1
+ module Polyrun
2
+ class Config
3
+ # Lightweight top-level key checks for +polyrun validate+.
4
+ module Schema
5
+ KNOWN_TOP_LEVEL = %w[
6
+ version
7
+ partition
8
+ prepare
9
+ coverage
10
+ databases
11
+ start
12
+ hooks
13
+ reporting
14
+ workers
15
+ suites
16
+ ].freeze
17
+
18
+ module_function
19
+
20
+ def validate(cfg)
21
+ raw = cfg.raw
22
+ return {ok: true, messages: ["polyrun validate: no config file found (empty defaults)"]} if cfg.path.nil?
23
+
24
+ unless raw.is_a?(Hash)
25
+ return {ok: false, messages: ["polyrun validate: config root must be a mapping"]}
26
+ end
27
+
28
+ unknown = raw.keys.map(&:to_s) - KNOWN_TOP_LEVEL
29
+ messages = unknown.sort.map { |k| "polyrun validate: unknown top-level key #{k.inspect}" }
30
+ {ok: messages.empty?, messages: messages}
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,104 @@
1
+ module Polyrun
2
+ class Config
3
+ # Applies a named +suites:+ overlay onto a loaded polyrun.yml hash.
4
+ module Suite
5
+ PARTITION_KEYS = %w[
6
+ paths_file shard_total shard_index strategy seed timing_file timing_granularity
7
+ constraints constraints_file shard_weights suite paths_build shard_processes
8
+ shard_workers workers_per_shard
9
+ ].freeze
10
+
11
+ TOP_MERGE_KEYS = %w[workers prepare start hooks reporting coverage databases].freeze
12
+
13
+ module_function
14
+
15
+ def merge(raw, suite_name)
16
+ name = suite_name.to_s.strip
17
+ base = deep_stringify(raw.is_a?(Hash) ? raw : {})
18
+ return [base, nil] if name.empty?
19
+
20
+ suites = base["suites"]
21
+ unless suites.is_a?(Hash)
22
+ raise Polyrun::Error, "polyrun: suites: map is required when selecting suite #{name.inspect}"
23
+ end
24
+
25
+ overlay = suites[name]
26
+ unless overlay.is_a?(Hash)
27
+ available = suites.keys.map(&:to_s).sort.join(", ")
28
+ raise Polyrun::Error, "polyrun: unknown suite #{name.inspect} (known: #{available})"
29
+ end
30
+
31
+ apply_overlay!(base, deep_stringify(overlay))
32
+ [base, name]
33
+ end
34
+
35
+ def apply_overlay!(base, overlay)
36
+ merge_partition_block!(base, overlay)
37
+ merge_partition_shortcuts!(base, overlay)
38
+ merge_top_keys!(base, overlay)
39
+ end
40
+ private_class_method :apply_overlay!
41
+
42
+ def merge_partition_block!(base, overlay)
43
+ return unless overlay["partition"].is_a?(Hash)
44
+
45
+ base["partition"] = deep_merge_hashes(base["partition"] || {}, overlay["partition"])
46
+ end
47
+ private_class_method :merge_partition_block!
48
+
49
+ def merge_partition_shortcuts!(base, overlay)
50
+ PARTITION_KEYS.each do |key|
51
+ next unless overlay.key?(key)
52
+
53
+ base["partition"] ||= {}
54
+ base["partition"][key] =
55
+ if key == "paths_build" && base["partition"][key].is_a?(Hash) && overlay[key].is_a?(Hash)
56
+ deep_merge_hashes(base["partition"][key], overlay[key])
57
+ else
58
+ overlay[key]
59
+ end
60
+ end
61
+ end
62
+ private_class_method :merge_partition_shortcuts!
63
+
64
+ def merge_top_keys!(base, overlay)
65
+ TOP_MERGE_KEYS.each do |key|
66
+ next unless overlay.key?(key)
67
+
68
+ base[key] =
69
+ if base[key].is_a?(Hash) && overlay[key].is_a?(Hash)
70
+ deep_merge_hashes(base[key], overlay[key])
71
+ else
72
+ overlay[key]
73
+ end
74
+ end
75
+ end
76
+ private_class_method :merge_top_keys!
77
+
78
+ def deep_stringify(obj)
79
+ case obj
80
+ when Hash
81
+ obj.each_with_object({}) { |(k, v), out| out[k.to_s] = deep_stringify(v) }
82
+ when Array
83
+ obj.map { |e| deep_stringify(e) }
84
+ else
85
+ obj
86
+ end
87
+ end
88
+ private_class_method :deep_stringify
89
+
90
+ def deep_merge_hashes(base, overlay)
91
+ a = base.dup
92
+ overlay.each do |k, v|
93
+ a[k] = if a[k].is_a?(Hash) && v.is_a?(Hash)
94
+ deep_merge_hashes(a[k], v)
95
+ else
96
+ v
97
+ end
98
+ end
99
+ a
100
+ end
101
+ private_class_method :deep_merge_hashes
102
+ end
103
+ end
104
+ end
@@ -9,9 +9,9 @@ module Polyrun
9
9
  DEFAULT_PARALLEL_WORKERS = 5
10
10
  MAX_PARALLEL_WORKERS = 10
11
11
 
12
- attr_reader :path, :raw
12
+ attr_reader :path, :raw, :suite
13
13
 
14
- def self.load(path: nil)
14
+ def self.load(path: nil, suite: nil)
15
15
  explicit = path && !path.to_s.strip.empty?
16
16
  resolved = resolve_path(path)
17
17
  if explicit && !(resolved && File.file?(resolved))
@@ -20,11 +20,16 @@ module Polyrun
20
20
 
21
21
  raw =
22
22
  if resolved && File.file?(resolved)
23
- YAML.safe_load_file(resolved, permitted_classes: [Symbol], aliases: true) || {}
23
+ YAML.safe_load_file(resolved, permitted_classes: [Symbol], aliases: true)
24
24
  else
25
25
  {}
26
26
  end
27
- new(path: resolved, raw: raw)
27
+ raw = {} if raw.nil?
28
+ raise Polyrun::Error, "polyrun config root must be a mapping" unless raw.is_a?(Hash)
29
+
30
+ suite_name = (suite.nil? || suite.to_s.strip.empty?) ? ENV["POLYRUN_SUITE"] : suite
31
+ merged, applied = Suite.merge(raw, suite_name)
32
+ new(path: resolved, raw: merged, suite: applied)
28
33
  end
29
34
 
30
35
  def self.resolve_path(explicit)
@@ -37,8 +42,9 @@ module Polyrun
37
42
  nil
38
43
  end
39
44
 
40
- def initialize(path:, raw:)
45
+ def initialize(path:, raw:, suite: nil)
41
46
  @path = path
47
+ @suite = suite
42
48
  @raw = raw.freeze
43
49
  end
44
50
 
@@ -76,8 +82,14 @@ module Polyrun
76
82
  def reporting
77
83
  raw["reporting"] || raw[:reporting] || {}
78
84
  end
85
+
86
+ def suites
87
+ raw["suites"] || raw[:suites] || {}
88
+ end
79
89
  end
80
90
  end
81
91
 
92
+ require_relative "config/suite"
82
93
  require_relative "config/resolver"
83
94
  require_relative "config/effective"
95
+ require_relative "config/schema"
@@ -0,0 +1,131 @@
1
+ require "yaml"
2
+ require_relative "../config/suite"
3
+
4
+ module Polyrun
5
+ module Coverage
6
+ # Resolves coverage settings for {Rails.start!} and CLI gates.
7
+ #
8
+ # Order: explicit +config_path+ when that file exists; else +config/polyrun_coverage.yml+
9
+ # when present; else inline +coverage:+ from +polyrun.yml+ (or +POLYRUN_CONFIG+).
10
+ # Aliases: +formats+ -> +report_formats+; directory-like +output_path+ -> +report_output_dir+.
11
+ module ConfigResolve
12
+ module_function
13
+
14
+ DEFAULT_FILE = File.join("config", "polyrun_coverage.yml").freeze
15
+ POLYRUN_YML_NAMES = %w[polyrun.yml config/polyrun.yml].freeze
16
+ META_KEYS = %w[coverage_config_source].freeze
17
+
18
+ def load(root:, config_path: nil, polyrun_config_path: nil)
19
+ root = File.expand_path(root)
20
+ file_path, file_hash = load_file_coverage(root, config_path)
21
+ if file_path
22
+ chosen = file_hash
23
+ source = relative_or_basename(file_path)
24
+ else
25
+ inline_path = resolve_polyrun_yml(root, polyrun_config_path)
26
+ chosen = load_inline_coverage(inline_path)
27
+ source = chosen.empty? ? "none" : relative_or_basename(inline_path)
28
+ end
29
+ normalize_aliases!(chosen, root)
30
+ chosen["coverage_config_source"] = source
31
+ chosen
32
+ end
33
+
34
+ def minimum_line_gate(root: Dir.pwd, config_path: nil, polyrun_config_path: nil)
35
+ data = load(root: root, config_path: config_path, polyrun_config_path: polyrun_config_path)
36
+ min = data["minimum_line_percent"]
37
+ return nil if min.nil?
38
+
39
+ strict = data.key?("strict") ? data["strict"] != false : true
40
+ {minimum: min.to_f, strict: strict}
41
+ end
42
+
43
+ def load_inline_coverage(path)
44
+ return {} unless path
45
+
46
+ raw = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: true)
47
+ return {} unless raw.is_a?(Hash)
48
+
49
+ raw, = Polyrun::Config::Suite.merge(raw, ENV["POLYRUN_SUITE"])
50
+ cov = raw["coverage"] || raw[:coverage]
51
+ cov.is_a?(Hash) ? stringify_keys(cov) : {}
52
+ rescue Psych::SyntaxError, ArgumentError, TypeError
53
+ {}
54
+ end
55
+ private_class_method :load_inline_coverage
56
+
57
+ def load_file_coverage(root, config_path)
58
+ path =
59
+ if config_path && !config_path.to_s.strip.empty?
60
+ File.expand_path(config_path.to_s)
61
+ else
62
+ File.join(root, DEFAULT_FILE)
63
+ end
64
+ return [nil, {}] unless File.file?(path)
65
+
66
+ data = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: true)
67
+ hash = data.is_a?(Hash) ? stringify_keys(data) : {}
68
+ [path, hash]
69
+ rescue Psych::SyntaxError, ArgumentError, TypeError
70
+ [path, {}]
71
+ end
72
+ private_class_method :load_file_coverage
73
+
74
+ def resolve_polyrun_yml(root, explicit)
75
+ if explicit && !explicit.to_s.strip.empty?
76
+ p = File.expand_path(explicit.to_s)
77
+ return p if File.file?(p)
78
+
79
+ return nil
80
+ end
81
+
82
+ env = ENV["POLYRUN_CONFIG"]
83
+ if env && !env.empty?
84
+ p = File.expand_path(env)
85
+ return p if File.file?(p)
86
+ end
87
+
88
+ POLYRUN_YML_NAMES.each do |name|
89
+ full = File.expand_path(name, root)
90
+ return full if File.file?(full)
91
+ end
92
+ nil
93
+ end
94
+ private_class_method :resolve_polyrun_yml
95
+
96
+ def normalize_aliases!(h, root)
97
+ if !h.key?("report_formats") && h.key?("formats")
98
+ h["report_formats"] = Array(h.delete("formats"))
99
+ end
100
+ return if h.key?("report_output_dir")
101
+ return unless h.key?("output_path")
102
+
103
+ out = h["output_path"].to_s
104
+ return if out.empty? || out.end_with?(".json")
105
+
106
+ h["report_output_dir"] = File.expand_path(out, root)
107
+ h.delete("output_path")
108
+ end
109
+ private_class_method :normalize_aliases!
110
+
111
+ def relative_or_basename(path)
112
+ return DEFAULT_FILE if path.end_with?(DEFAULT_FILE)
113
+
114
+ File.basename(path)
115
+ end
116
+ private_class_method :relative_or_basename
117
+
118
+ def stringify_keys(obj)
119
+ case obj
120
+ when Hash
121
+ obj.each_with_object({}) { |(k, v), out| out[k.to_s] = stringify_keys(v) }
122
+ when Array
123
+ obj.map { |e| stringify_keys(e) }
124
+ else
125
+ obj
126
+ end
127
+ end
128
+ private_class_method :stringify_keys
129
+ end
130
+ end
131
+ end
@@ -2,13 +2,15 @@ require "yaml"
2
2
 
3
3
  require_relative "collector"
4
4
  require_relative "formatter"
5
+ require_relative "config_resolve"
5
6
 
6
7
  module Polyrun
7
8
  module Coverage
8
- # Rails integration entry point for {Collector.start!}: optional +config/polyrun_coverage.yml+ under the project
9
- # root, root inference from +spec_helper.rb+ / +rails_helper.rb+ / +test_helper.rb+, and +report_formats+ for
10
- # {Formatter.multi}. Call at the **top** of +spec/spec_helper.rb+ (before +require "rails"+) so stdlib
11
- # +Coverage+ sees application code.
9
+ # Rails integration entry point for {Collector.start!}: coverage settings from
10
+ # +config/polyrun_coverage.yml+ and/or inline +coverage:+ in +polyrun.yml+ (see {ConfigResolve}),
11
+ # root inference from +spec_helper.rb+ / +rails_helper.rb+ / +test_helper.rb+, and +report_formats+
12
+ # for {Formatter.multi}. Call at the **top** of +spec/spec_helper.rb+ (before +require "rails"+) so
13
+ # stdlib +Coverage+ sees application code.
12
14
  #
13
15
  # require "polyrun/coverage/rails"
14
16
  # Polyrun::Coverage::Rails.start!
@@ -16,10 +18,10 @@ module Polyrun
16
18
  module Rails
17
19
  module_function
18
20
 
19
- DEFAULT_CONFIG_RELATIVE = File.join("config", "polyrun_coverage.yml").freeze
21
+ DEFAULT_CONFIG_RELATIVE = ConfigResolve::DEFAULT_FILE
20
22
 
21
23
  # @param root [String, nil] project root (default: infer from caller, or +Rails.root+ when already loaded)
22
- # @param config_path [String, nil] YAML path (default: +<root>/config/polyrun_coverage.yml+ if present)
24
+ # @param config_path [String, nil] YAML path (default: resolve via {ConfigResolve})
23
25
  # @param overrides [Hash] merged over YAML; keys match {Collector.start!} (+report_formats+ builds +formatter+)
24
26
  def start!(root: nil, config_path: nil, **overrides)
25
27
  return if Collector.disabled?
@@ -29,6 +31,7 @@ module Polyrun
29
31
  cfg = load_config(root, config_path)
30
32
  merged = deep_merge_hashes(cfg, stringify_keys(overrides))
31
33
  merged["root"] = root
34
+ ConfigResolve::META_KEYS.each { |k| merged.delete(k) }
32
35
 
33
36
  report_formats = merged.delete("report_formats")
34
37
  unless merged.key?("formatter") || report_formats.nil?
@@ -63,12 +66,7 @@ module Polyrun
63
66
  end
64
67
 
65
68
  def load_config(root, config_path)
66
- path = config_path || File.join(root, DEFAULT_CONFIG_RELATIVE)
67
- path = File.expand_path(path)
68
- return {} unless File.file?(path)
69
-
70
- data = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: true)
71
- data.is_a?(Hash) ? stringify_keys(data) : {}
69
+ ConfigResolve.load(root: root, config_path: config_path)
72
70
  end
73
71
 
74
72
  def build_formatter(formats, root, merged)
@@ -3,6 +3,8 @@
3
3
  # Gate per-worker duplicate work in application code when POLYRUN_SHARD_TOTAL > 1.
4
4
  # See: docs/SETUP_PROFILE.md
5
5
 
6
+ version: 1
7
+
6
8
  partition:
7
9
  paths_file: spec/spec_paths.txt
8
10
  shard_total: 5
@@ -17,8 +19,19 @@ prepare:
17
19
  rails_root: .
18
20
  command: bundle exec ruby bin/test_prepare.rb
19
21
 
22
+ start:
23
+ prepare: true
24
+ databases: true
25
+
26
+ # Optional suite hooks (uncomment and set commands):
27
+ # hooks:
28
+ # before_suite: bundle exec ruby script/polyrun_before_suite.rb
29
+ # after_suite: bundle exec ruby script/polyrun_after_suite.rb
30
+ hooks: {}
31
+
20
32
  # Optional reporting (see docs/SPEC_QUALITY.md):
21
33
  # reporting:
34
+ # merge_failures: true
22
35
  # merge_spec_quality: true
23
36
  # merge_spec_quality_output: coverage/polyrun-spec-quality.json
24
37
 
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generated by: polyrun init --profile rails-prepare-task
4
+ # Runs once before parallel workers. Skip: POLYRUN_SKIP_PREPARE=1 (legacy: POLYRUN_START_SKIP_PREPARE=1).
5
+
6
+ namespace :polyrun do
7
+ desc "One-shot Polyrun prepare (delegates to `polyrun prepare`)"
8
+ task prepare: :environment do
9
+ skip = %w[1 true yes].include?(ENV["POLYRUN_SKIP_PREPARE"].to_s.downcase) ||
10
+ %w[1 true yes].include?(ENV["POLYRUN_START_SKIP_PREPARE"].to_s.downcase)
11
+ next if skip
12
+
13
+ ok = system({"RUBYOPT" => nil}, "bundle", "exec", "polyrun", "prepare")
14
+ abort "polyrun prepare failed" unless ok
15
+ end
16
+ end
@@ -1,3 +1,3 @@
1
1
  module Polyrun
2
- VERSION = "2.2.7"
2
+ VERSION = "2.2.8"
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: 2.2.7
4
+ version: 2.2.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -222,6 +222,7 @@ files:
222
222
  - lib/polyrun/cli/coverage_merge_io.rb
223
223
  - lib/polyrun/cli/database_commands.rb
224
224
  - lib/polyrun/cli/default_run.rb
225
+ - lib/polyrun/cli/doctor_command.rb
225
226
  - lib/polyrun/cli/env_commands.rb
226
227
  - lib/polyrun/cli/failure_commands.rb
227
228
  - lib/polyrun/cli/help.rb
@@ -244,17 +245,22 @@ files:
244
245
  - lib/polyrun/cli/run_shards_planning.rb
245
246
  - lib/polyrun/cli/run_shards_run.rb
246
247
  - lib/polyrun/cli/run_shards_worker_interrupt.rb
248
+ - lib/polyrun/cli/skip_env.rb
247
249
  - lib/polyrun/cli/spec_quality_commands.rb
248
250
  - lib/polyrun/cli/start_bootstrap.rb
249
251
  - lib/polyrun/cli/timing_command.rb
252
+ - lib/polyrun/cli/validate_command.rb
250
253
  - lib/polyrun/config.rb
251
254
  - lib/polyrun/config/dotted_path.rb
252
255
  - lib/polyrun/config/effective.rb
253
256
  - lib/polyrun/config/resolver.rb
257
+ - lib/polyrun/config/schema.rb
258
+ - lib/polyrun/config/suite.rb
254
259
  - lib/polyrun/coverage/cobertura_zero_lines.rb
255
260
  - lib/polyrun/coverage/collector.rb
256
261
  - lib/polyrun/coverage/collector_finish.rb
257
262
  - lib/polyrun/coverage/collector_fragment_meta.rb
263
+ - lib/polyrun/coverage/config_resolve.rb
258
264
  - lib/polyrun/coverage/example_diff.rb
259
265
  - lib/polyrun/coverage/example_diff_snapshot.rb
260
266
  - lib/polyrun/coverage/example_diff_track_filter.rb
@@ -360,6 +366,7 @@ files:
360
366
  - lib/polyrun/templates/polyrun_hooks_spec_quality.rb
361
367
  - lib/polyrun/templates/polyrun_spec_quality.yml
362
368
  - lib/polyrun/templates/rails_prepare.polyrun.yml
369
+ - lib/polyrun/templates/rails_prepare_task.rake
363
370
  - lib/polyrun/timing/merge.rb
364
371
  - lib/polyrun/timing/rspec_example_formatter.rb
365
372
  - lib/polyrun/timing/stats.rb