polyrun 1.1.0 → 1.3.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.
@@ -8,6 +8,7 @@ require_relative "formatter"
8
8
  require_relative "merge"
9
9
  require_relative "result"
10
10
  require_relative "track_files"
11
+ require_relative "collector_fragment_meta"
11
12
  require_relative "../debug"
12
13
 
13
14
  module Polyrun
@@ -24,7 +25,7 @@ module Polyrun
24
25
 
25
26
  # @param root [String] project root (absolute or relative)
26
27
  # @param reject_patterns [Array<String>] path substrings to drop (like SimpleCov add_filter)
27
- # @param output_path [String, nil] default coverage/polyrun-fragment-<shard>.json
28
+ # @param output_path [String, nil] default see {.fragment_default_basename_from_env}
28
29
  # @param minimum_line_percent [Float, nil] exit 1 if below (when strict)
29
30
  # @param strict [Boolean] whether to exit non-zero on threshold failure (default true when minimum set)
30
31
  # @param track_under [Array<String>] when +track_files+ is nil, only keep coverage keys under these dirs relative to +root+. Default +["lib"]+.
@@ -34,18 +35,25 @@ module Polyrun
34
35
  # @param formatter [Object, nil] Object responding to +format(result, output_dir:, basename:)+ like SimpleCov formatters (e.g. {Formatter.multi} or {Formatter::MultiFormatter})
35
36
  # @param report_output_dir [String, nil] directory for +formatter+ outputs (default +coverage/+ under +root+)
36
37
  # @param report_basename [String] file prefix for formatter outputs (default +polyrun-coverage+)
38
+ # See {CollectorFragmentMeta.fragment_default_basename_from_env}.
39
+ def self.fragment_default_basename_from_env(env = ENV)
40
+ CollectorFragmentMeta.fragment_default_basename_from_env(env)
41
+ end
42
+
37
43
  def start!(root:, reject_patterns: [], track_under: ["lib"], track_files: nil, groups: nil, output_path: nil, minimum_line_percent: nil, strict: nil, meta: {}, formatter: nil, report_output_dir: nil, report_basename: "polyrun-coverage")
38
44
  return if disabled?
39
45
 
40
46
  root = File.expand_path(root)
41
- shard = ENV.fetch("POLYRUN_SHARD_INDEX", "0")
42
- output_path ||= File.join(root, "coverage", "polyrun-fragment-#{shard}.json")
47
+ basename = fragment_default_basename_from_env
48
+ output_path ||= File.join(root, "coverage", "polyrun-fragment-#{basename}.json")
43
49
  strict = if minimum_line_percent.nil?
44
50
  false
45
51
  else
46
52
  strict.nil? || strict
47
53
  end
48
54
 
55
+ fragment_meta = CollectorFragmentMeta.fragment_meta_from_env(basename)
56
+
49
57
  @config = {
50
58
  root: root,
51
59
  track_under: Array(track_under).map(&:to_s),
@@ -59,7 +67,8 @@ module Polyrun
59
67
  formatter: formatter,
60
68
  report_output_dir: report_output_dir,
61
69
  report_basename: report_basename,
62
- shard_total_at_start: ENV["POLYRUN_SHARD_TOTAL"].to_i
70
+ shard_total_at_start: ENV["POLYRUN_SHARD_TOTAL"].to_i,
71
+ fragment_meta: fragment_meta
63
72
  }
64
73
 
65
74
  unless ::Coverage.running?
@@ -110,11 +119,7 @@ module Polyrun
110
119
  end
111
120
 
112
121
  def self.finish_debug_time_label
113
- if ENV["POLYRUN_SHARD_TOTAL"].to_i > 1
114
- "worker pid=#{$$} shard=#{ENV.fetch("POLYRUN_SHARD_INDEX", "?")} Coverage::Collector.finish (write fragment)"
115
- else
116
- "Coverage::Collector.finish (write fragment)"
117
- end
122
+ CollectorFragmentMeta.finish_debug_time_label
118
123
  end
119
124
 
120
125
  def build_meta(cfg)
@@ -123,6 +128,7 @@ module Polyrun
123
128
  m["timestamp"] ||= Time.now.to_i
124
129
  m["command_name"] ||= "rspec"
125
130
  m["polyrun_coverage_root"] = cfg[:root].to_s
131
+ CollectorFragmentMeta.merge_fragment_meta!(m, cfg[:fragment_meta])
126
132
  if cfg[:groups]
127
133
  m["polyrun_coverage_groups"] = cfg[:groups].transform_keys(&:to_s).transform_values(&:to_s)
128
134
  end
@@ -12,6 +12,8 @@ module Polyrun
12
12
  collector_finish: "start",
13
13
  polyrun_shard_index: ENV["POLYRUN_SHARD_INDEX"],
14
14
  polyrun_shard_total: ENV["POLYRUN_SHARD_TOTAL"],
15
+ polyrun_shard_matrix_index: ENV["POLYRUN_SHARD_MATRIX_INDEX"],
16
+ polyrun_shard_matrix_total: ENV["POLYRUN_SHARD_MATRIX_TOTAL"],
15
17
  output_path: cfg[:output_path]
16
18
  )
17
19
  Polyrun::Debug.time(Collector.finish_debug_time_label) do
@@ -0,0 +1,57 @@
1
+ module Polyrun
2
+ module Coverage
3
+ # Shard / worker naming for coverage JSON fragments (N×M CI vs run-shards).
4
+ module CollectorFragmentMeta
5
+ module_function
6
+
7
+ # Default fragment basename (no extension) for +coverage/polyrun-fragment-<basename>.json+.
8
+ def fragment_default_basename_from_env(env = ENV)
9
+ local = env.fetch("POLYRUN_SHARD_INDEX", "0")
10
+ mt = env["POLYRUN_SHARD_MATRIX_TOTAL"].to_i
11
+ if mt > 1
12
+ mi = env.fetch("POLYRUN_SHARD_MATRIX_INDEX", "0")
13
+ "shard#{mi}-worker#{local}"
14
+ elsif env["POLYRUN_SHARD_TOTAL"].to_i > 1
15
+ "worker#{local}"
16
+ else
17
+ local
18
+ end
19
+ end
20
+
21
+ def finish_debug_time_label
22
+ mt = ENV["POLYRUN_SHARD_MATRIX_TOTAL"].to_i
23
+ if mt > 1
24
+ "worker pid=#{$$} shard(matrix)=#{ENV.fetch("POLYRUN_SHARD_MATRIX_INDEX", "?")} worker(local)=#{ENV.fetch("POLYRUN_SHARD_INDEX", "?")} Coverage::Collector.finish (write fragment)"
25
+ elsif ENV["POLYRUN_SHARD_TOTAL"].to_i > 1
26
+ "worker pid=#{$$} worker=#{ENV.fetch("POLYRUN_SHARD_INDEX", "?")} Coverage::Collector.finish (write fragment)"
27
+ else
28
+ "Coverage::Collector.finish (write fragment)"
29
+ end
30
+ end
31
+
32
+ def fragment_meta_from_env(basename)
33
+ mt = ENV["POLYRUN_SHARD_MATRIX_TOTAL"].to_i
34
+ {
35
+ basename: basename,
36
+ worker_index: ENV.fetch("POLYRUN_SHARD_INDEX", "0"),
37
+ shard_matrix_index: shard_matrix_index_value(mt)
38
+ }
39
+ end
40
+
41
+ def shard_matrix_index_value(matrix_total)
42
+ return nil if matrix_total <= 1
43
+
44
+ ENV.fetch("POLYRUN_SHARD_MATRIX_INDEX", "0")
45
+ end
46
+
47
+ def merge_fragment_meta!(m, fm)
48
+ return m if fm.nil?
49
+
50
+ m["polyrun_fragment_basename"] = fm[:basename].to_s if fm[:basename]
51
+ m["polyrun_worker_index"] = fm[:worker_index].to_s if fm[:worker_index]
52
+ m["polyrun_shard_matrix_index"] = fm[:shard_matrix_index].to_s if fm[:shard_matrix_index]
53
+ m
54
+ end
55
+ end
56
+ end
57
+ end
@@ -8,9 +8,45 @@ module Polyrun
8
8
  File.read(File.expand_path(path.to_s, Dir.pwd)).split("\n").map(&:strip).reject(&:empty?)
9
9
  end
10
10
 
11
+ # Prefer +spec/+ RSpec files, then +test/+ Minitest, then Polyrun Quick files (same globs as +polyrun quick+).
12
+ # Order avoids running the broader Quick glob when RSpec or Minitest files already exist.
13
+ def detect_auto_suite(cwd = Dir.pwd)
14
+ base = File.expand_path(cwd)
15
+ return :rspec if Dir.glob(File.join(base, "spec/**/*_spec.rb")).any?
16
+
17
+ return :minitest if Dir.glob(File.join(base, "test/**/*_test.rb")).any?
18
+
19
+ quick = quick_parallel_default_paths(base)
20
+ return :quick if quick.any?
21
+
22
+ nil
23
+ end
24
+
25
+ # Infer parallel suite from explicit paths (+_spec.rb+ vs +_test.rb+ vs Polyrun quick-style +.rb+).
26
+ # Returns +:rspec+, +:minitest+, +:quick+, +:invalid+ (mixed spec and test), or +nil+ (empty).
27
+ def infer_suite_from_paths(paths)
28
+ paths = paths.map { |p| File.expand_path(p) }
29
+ return nil if paths.empty?
30
+
31
+ specs = paths.count { |p| File.basename(p).end_with?("_spec.rb") }
32
+ tests = paths.count { |p| File.basename(p).end_with?("_test.rb") }
33
+ return :invalid if specs.positive? && tests.positive?
34
+
35
+ return :rspec if specs.positive?
36
+ return :minitest if tests.positive?
37
+
38
+ others = paths.size - specs - tests
39
+ return :quick if others.positive?
40
+
41
+ nil
42
+ end
43
+
11
44
  # When +paths_file+ is set but missing, returns +{ error: "..." }+.
12
45
  # Otherwise returns +{ items:, source: }+ (human-readable source label).
13
- def resolve_run_shard_items(paths_file: nil, cwd: Dir.pwd)
46
+ #
47
+ # +partition.suite+ (optional): +auto+ (default), +rspec+, +minitest+, +quick+ — used only when resolving
48
+ # from globs (no explicit +paths_file+ and no +spec/spec_paths.txt+).
49
+ def resolve_run_shard_items(paths_file: nil, cwd: Dir.pwd, partition: {})
14
50
  if paths_file
15
51
  abs = File.expand_path(paths_file.to_s, cwd)
16
52
  unless File.file?(abs)
@@ -20,9 +56,54 @@ module Polyrun
20
56
  elsif File.file?(File.join(cwd, "spec", "spec_paths.txt"))
21
57
  {items: read_lines(File.join(cwd, "spec", "spec_paths.txt")), source: "spec/spec_paths.txt"}
22
58
  else
23
- {items: Dir.glob(File.join(cwd, "spec/**/*_spec.rb")).sort, source: "spec/**/*_spec.rb glob"}
59
+ resolve_run_shard_items_glob(cwd: cwd, partition: partition)
24
60
  end
25
61
  end
62
+
63
+ def resolve_run_shard_items_glob(cwd:, partition: {})
64
+ suite = (partition["suite"] || partition[:suite] || "auto").to_s.downcase
65
+ suite = "auto" if suite.empty?
66
+
67
+ base = File.expand_path(cwd)
68
+ spec = Dir.glob(File.join(base, "spec/**/*_spec.rb")).sort
69
+ test = Dir.glob(File.join(base, "test/**/*_test.rb")).sort
70
+ quick = quick_parallel_default_paths(base)
71
+
72
+ case suite
73
+ when "rspec"
74
+ return {error: "partition.suite is rspec but no spec/**/*_spec.rb files"} if spec.empty?
75
+
76
+ {items: spec, source: "spec/**/*_spec.rb glob"}
77
+ when "minitest"
78
+ return {error: "partition.suite is minitest but no test/**/*_test.rb files"} if test.empty?
79
+
80
+ {items: test, source: "test/**/*_test.rb glob"}
81
+ when "quick"
82
+ return {error: "partition.suite is quick but no Polyrun quick files under spec/ or test/"} if quick.empty?
83
+
84
+ {items: quick, source: "Polyrun quick glob"}
85
+ when "auto"
86
+ if spec.any?
87
+ {items: spec, source: "spec/**/*_spec.rb glob"}
88
+ elsif test.any?
89
+ {items: test, source: "test/**/*_test.rb glob"}
90
+ elsif quick.any?
91
+ {items: quick, source: "Polyrun quick glob"}
92
+ else
93
+ {
94
+ error: "no spec paths (spec/spec_paths.txt, partition.paths_file, or spec/**/*_spec.rb); " \
95
+ "no test/**/*_test.rb; no Polyrun quick files"
96
+ }
97
+ end
98
+ else
99
+ {error: "unknown partition.suite: #{suite.inspect} (expected auto, rspec, minitest, quick)"}
100
+ end
101
+ end
102
+
103
+ def quick_parallel_default_paths(base)
104
+ require_relative "../quick/runner"
105
+ Polyrun::Quick::Runner.parallel_default_paths(base)
106
+ end
26
107
  end
27
108
  end
28
109
  end
@@ -62,6 +62,30 @@ module Polyrun
62
62
  new(out: out, err: err, verbose: verbose).run(paths)
63
63
  end
64
64
 
65
+ # Files Polyrun::Quick would run with no explicit paths (excludes normal RSpec/Minitest files).
66
+ def self.parallel_default_paths(cwd = Dir.pwd)
67
+ base = File.expand_path(cwd)
68
+ globs = [
69
+ File.join(base, "spec", "polyrun_quick", "**", "*.rb"),
70
+ File.join(base, "test", "polyrun_quick", "**", "*.rb"),
71
+ File.join(base, "spec", "**", "*.rb"),
72
+ File.join(base, "test", "**", "*.rb")
73
+ ]
74
+ globs.flat_map { |g| Dir.glob(g) }.uniq.reject { |p| quick_path_excluded?(p, base) }.sort
75
+ end
76
+
77
+ def self.quick_path_excluded?(path, base)
78
+ rel = Pathname.new(path).relative_path_from(Pathname.new(base)).to_s
79
+ parts = rel.split(File::SEPARATOR)
80
+ bn = File.basename(path)
81
+ return true if bn.end_with?("_spec.rb", "_test.rb")
82
+ return true if %w[spec_helper.rb rails_helper.rb test_helper.rb].include?(bn)
83
+ return true if parts[0] == "spec" && %w[support fixtures factories].include?(parts[1])
84
+ return true if parts[0] == "test" && %w[support fixtures].include?(parts[1])
85
+
86
+ false
87
+ end
88
+
65
89
  def initialize(out: $stdout, err: $stderr, verbose: false)
66
90
  @out = out
67
91
  @err = err
@@ -153,27 +177,12 @@ module Polyrun
153
177
  end
154
178
 
155
179
  def default_globs
156
- base = File.expand_path(Dir.pwd)
157
- globs = [
158
- File.join(base, "spec", "polyrun_quick", "**", "*.rb"),
159
- File.join(base, "test", "polyrun_quick", "**", "*.rb"),
160
- File.join(base, "spec", "**", "*.rb"),
161
- File.join(base, "test", "**", "*.rb")
162
- ]
163
- globs.flat_map { |g| Dir.glob(g) }.uniq.reject { |p| default_quick_exclude?(p, base) }.sort
180
+ Runner.parallel_default_paths(Dir.pwd)
164
181
  end
165
182
 
166
183
  # Omit RSpec/Minitest files and common helpers so +polyrun quick+ with no args does not load normal suites.
167
184
  def default_quick_exclude?(path, base)
168
- rel = Pathname.new(path).relative_path_from(Pathname.new(base)).to_s
169
- parts = rel.split(File::SEPARATOR)
170
- bn = File.basename(path)
171
- return true if bn.end_with?("_spec.rb", "_test.rb")
172
- return true if %w[spec_helper.rb rails_helper.rb test_helper.rb].include?(bn)
173
- return true if parts[0] == "spec" && %w[support fixtures factories].include?(parts[1])
174
- return true if parts[0] == "test" && %w[support fixtures].include?(parts[1])
175
-
176
- false
185
+ Runner.quick_path_excluded?(path, base)
177
186
  end
178
187
  end
179
188
  end
@@ -3,8 +3,9 @@
3
3
  # bundle exec polyrun -c polyrun.yml ci-shard-run -- bundle exec rspec
4
4
  # (or ci-shard-rspec; or e.g. ci-shard-run -- bundle exec polyrun quick).
5
5
  # Equivalent to build-paths, plan --shard/--total, then run that command with this slice's paths.
6
- # A separate CI job downloads coverage/polyrun-fragment-*.json and runs merge-coverage.
7
- # Do not use parallel-rspec with multiple workers inside the same matrix row unless you intend nested parallelism.
6
+ # A separate CI job downloads coverage/polyrun-fragment-*.json (e.g. shard<S>-worker<W>.json per N×M process) and runs merge-coverage.
7
+ # For N×M (N matrix jobs × M processes per job): set shard_processes: M or POLYRUN_SHARD_PROCESSES,
8
+ # or pass --shard-processes M to ci-shard-run / ci-shard-rspec (local split is round-robin).
8
9
  # See: docs/SETUP_PROFILE.md
9
10
 
10
11
  partition:
@@ -1,3 +1,3 @@
1
1
  module Polyrun
2
- VERSION = "1.1.0"
2
+ VERSION = "1.3.0"
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.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -167,10 +167,14 @@ files:
167
167
  - lib/polyrun.rb
168
168
  - lib/polyrun/cli.rb
169
169
  - lib/polyrun/cli/ci_shard_run_command.rb
170
+ - lib/polyrun/cli/ci_shard_run_parse.rb
171
+ - lib/polyrun/cli/config_command.rb
170
172
  - lib/polyrun/cli/coverage_commands.rb
171
173
  - lib/polyrun/cli/coverage_merge_io.rb
172
174
  - lib/polyrun/cli/database_commands.rb
175
+ - lib/polyrun/cli/default_run.rb
173
176
  - lib/polyrun/cli/env_commands.rb
177
+ - lib/polyrun/cli/help.rb
174
178
  - lib/polyrun/cli/helpers.rb
175
179
  - lib/polyrun/cli/init_command.rb
176
180
  - lib/polyrun/cli/plan_command.rb
@@ -187,9 +191,13 @@ files:
187
191
  - lib/polyrun/cli/start_bootstrap.rb
188
192
  - lib/polyrun/cli/timing_command.rb
189
193
  - lib/polyrun/config.rb
194
+ - lib/polyrun/config/dotted_path.rb
195
+ - lib/polyrun/config/effective.rb
196
+ - lib/polyrun/config/resolver.rb
190
197
  - lib/polyrun/coverage/cobertura_zero_lines.rb
191
198
  - lib/polyrun/coverage/collector.rb
192
199
  - lib/polyrun/coverage/collector_finish.rb
200
+ - lib/polyrun/coverage/collector_fragment_meta.rb
193
201
  - lib/polyrun/coverage/filter.rb
194
202
  - lib/polyrun/coverage/formatter.rb
195
203
  - lib/polyrun/coverage/merge.rb