polyrun 2.2.4 → 2.2.5

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: de79eaae07044fa991b839a44011f7648df5ba16d0586a1ea2708c1d7160cf5e
4
- data.tar.gz: f3cbab3bf642164ffa507aa75be1acaaf7a232057b1b1c5f3fcdb0f804c2a0a7
3
+ metadata.gz: 6364a918386209ff30b2c12280f3a37a0cb70de54a83f1f07606c2c526ac773c
4
+ data.tar.gz: 8d75d162af33d721f864cf8a4002d98f57e53d24db55dc607dad82e94f5416f7
5
5
  SHA512:
6
- metadata.gz: f62f0cbc1760e3fc42a4711b7e55eace3b8b5bb038836abcae91eba876cfabf9b72832e2f04a284744fdf09247288bff6b60c7a3b1a38d00ee140d37634702df
7
- data.tar.gz: 4f9b6d94992745419d35a5bbb40041094569f725470904f546566bfdba7a3e7eeca899f5b80844d9908ca36e744870fef3f8ecef6e20d589fa090d0363859147
6
+ metadata.gz: e92439850018abacae0f1041315b213f4624abc78e49b3d4ca53d53129428790961b1d77b8a923d882e0fe0c9157c14c125ab13bbdf34db52c86c82082917104
7
+ data.tar.gz: 6d64c292b054ffd8cf95a47bda1fa3253284af76e98d8d9fc32631b389e51e9acbf2b3ad6c0e64e19f471847fba17b00b706ef1d4d8c549414abea9be4a9806a
data/CHANGELOG.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # CHANGELOG
2
2
 
3
- ## Unreleased
3
+ ## 2.2.5 (2026-09-24)
4
+
5
+ - Fix bare `polyrun` suite selection when both `spec/` and `test/` exist: honor `partition.suite`, else infer from `partition.paths_file`, else filesystem auto-detect (RSpec first).
6
+ - Exit 2 when `partition.suite` disagrees with `paths_file`, or when `run-shards` paths infer a different suite than a recognizable command after `--`.
7
+ - Add `parallel-minitest` and `parallel-quick` CLI subcommands (same shape as `parallel-rspec`); they do not run `start` prepare or database bootstrap.
8
+ - Recognize `bin/rails test` and compact `ruby -Itest` in the suite/command mismatch guard; strip `path:line` locators before classifying paths.
9
+ - Skip legacy `spec/spec_paths.txt` when `partition.suite` is `minitest` or `quick`; allow mixed path lists only with suite-agnostic custom runners.
4
10
 
5
11
  ## 2.2.4 (2026-09-04)
6
12
 
data/README.md CHANGED
@@ -10,7 +10,7 @@ Running tests in parallel across processes still requires a single merged covera
10
10
 
11
11
  Polyrun provides:
12
12
 
13
- - Orchestration: `plan`, `run-shards`, and `parallel-rspec` (run-shards plus merge-coverage), with an optional on-disk queue and constraints for file lists and load balancing. For **GitHub Actions-style matrix sharding** (one job per global shard), use `ci-shard-run -- …` (any test runner) or `ci-shard-rspec`—not `run-shards` / `parallel-rspec`, which fan out N workers on one machine.
13
+ - Orchestration: `plan`, `run-shards`, and `parallel-rspec` / `parallel-minitest` / `parallel-quick` (run-shards plus merge-coverage), with an optional on-disk queue and constraints for file lists and load balancing. Bare `polyrun` (no subcommand) picks the suite from `partition.suite`, else `partition.paths_file`, else filesystem auto-detect. For **GitHub Actions-style matrix sharding** (one job per global shard), use `ci-shard-run -- …` (any test runner) or `ci-shard-rspec`—not `run-shards` / `parallel-*`, which fan out N workers on one machine.
14
14
  - Coverage: merge SimpleCov-compatible JSON fragments; emit JSON, LCOV, Cobertura, or console summaries (you can drop separate SimpleCov merge plugins for this path).
15
15
  - CI reporting: JUnit XML from RSpec JSON; slow-file reports from merged timing JSON.
16
16
  - Parallel hygiene: asset digest markers, SQL snapshots, YAML fixture batches, and DB URL or shard helpers aligned with `POLYRUN_SHARD_*`.
@@ -92,6 +92,8 @@ bin/polyrun build-paths # write spec/spec_paths.txt from partition.paths_build
92
92
  bin/polyrun ci-shard-run -- bundle exec rspec # CI matrix: shard plan + append paths to the command after --
93
93
  bin/polyrun ci-shard-rspec # same as ci-shard-run -- bundle exec rspec
94
94
  bin/polyrun parallel-rspec --workers 5 # run-shards + merge-coverage (default: bundle exec rspec)
95
+ bin/polyrun parallel-minitest --workers 5 # same; default rails test or ruby -I test
96
+ bin/polyrun parallel-quick --workers 5 # same; default bundle exec polyrun quick
95
97
  bin/polyrun run-shards --workers 5 --merge-coverage -- bundle exec rspec
96
98
  bin/polyrun merge-coverage -i cov1.json -i cov2.json -o merged.json --format json,lcov,cobertura,console
97
99
  bin/polyrun env --shard 0 --total 4 # print DATABASE_URL exports from polyrun.yml in cwd
@@ -7,20 +7,32 @@ module Polyrun
7
7
  private
8
8
 
9
9
  def dispatch_default_parallel!(config_path)
10
- suite = Polyrun::Partition::Paths.detect_auto_suite(Dir.pwd)
11
- unless suite
12
- Polyrun::Log.warn "polyrun: no tests found (spec/**/*_spec.rb, test/**/*_test.rb, or Polyrun quick files). See polyrun help."
10
+ cfg = Polyrun::Config.load(path: config_path || ENV["POLYRUN_CONFIG"])
11
+ pc = cfg.partition
12
+ code = Polyrun::Partition::PathsBuild.apply!(partition: pc, cwd: Dir.pwd)
13
+ return code if code != 0
14
+
15
+ resolved = Polyrun::Partition::Suite.resolve_default(cwd: Dir.pwd, partition: pc)
16
+ if resolved[:error]
17
+ Polyrun::Log.warn "polyrun: #{resolved[:error]}"
13
18
  return 2
14
19
  end
15
20
 
21
+ suite = resolved[:suite]
16
22
  Polyrun::Log.warn "polyrun: default → parallel #{suite} (use `polyrun help` for subcommands)" if @verbose
17
23
 
18
24
  case suite
19
25
  when :rspec
20
26
  cmd_start([], config_path)
21
27
  when :minitest
28
+ code = start_bootstrap!(cfg, [], config_path)
29
+ return code if code != 0
30
+
22
31
  cmd_parallel_minitest([], config_path)
23
32
  when :quick
33
+ code = start_bootstrap!(cfg, [], config_path)
34
+ return code if code != 0
35
+
24
36
  cmd_parallel_quick([], config_path)
25
37
  else
26
38
  2
@@ -5,7 +5,7 @@ module Polyrun
5
5
  Polyrun::Log.puts <<~HELP
6
6
  usage: polyrun [global options] [<command> | <paths...>]
7
7
 
8
- With no command, runs parallel tests for the detected suite: RSpec under spec/, Minitest under test/, or Polyrun Quick (same discovery as polyrun quick). If the first argument is a known subcommand name, it is dispatched. Otherwise, path-like tokens (optionally with run-shards flags such as --workers) shard those files in parallel; see commands below.
8
+ With no command, runs parallel tests for the resolved suite: partition.suite when set; else suite inferred from partition.paths_file; else filesystem auto-detect (RSpec under spec/ before Minitest under test/, then Polyrun Quick). If the first argument is a known subcommand name, it is dispatched. Otherwise, path-like tokens (optionally with run-shards flags such as --workers) shard those files in parallel; see commands below.
9
9
 
10
10
  global:
11
11
  -c, --config PATH polyrun.yml path (or POLYRUN_CONFIG)
@@ -40,7 +40,9 @@ module Polyrun
40
40
  merge-coverage merge SimpleCov JSON fragments (json/lcov/cobertura/console/html/csv/markdown)
41
41
  merge-failures merge per-shard failure JSONL fragments or RSpec JSON files (jsonl/json/csv/markdown)
42
42
  run-shards fan out N parallel OS processes (POLYRUN_SHARD_*; not Ruby threads); optional --merge-coverage / --merge-failures / --merge-spec-quality
43
- parallel-rspec run-shards + merge-coverage (defaults to: bundle exec rspec after --)
43
+ parallel-rspec run-shards + merge-coverage (defaults to: bundle exec rspec after --); no start prepare/DB bootstrap
44
+ parallel-minitest run-shards + merge-coverage (defaults to: bundle exec rails test or ruby -I test after --); no start prepare/DB bootstrap
45
+ parallel-quick run-shards + merge-coverage (defaults to: bundle exec polyrun quick after --); no start prepare/DB bootstrap
44
46
  start parallel-rspec; auto-runs prepare (shell/assets) and db:setup-* when polyrun.yml configures them; legacy script/build_spec_paths.rb if paths_build absent
45
47
  ci-shard-run CI matrix: build-paths + plan for POLYRUN_SHARD_INDEX / POLYRUN_SHARD_TOTAL (or config), then run your command with that shard's paths after --; optional --shard-processes M or --workers M (POLYRUN_SHARD_PROCESSES; not POLYRUN_WORKERS) for N×M jobs × processes on this host
46
48
  ci-shard-rspec same as ci-shard-run -- bundle exec rspec; optional --shard-processes / --workers / -- [rspec-only flags]
@@ -40,11 +40,8 @@ module Polyrun
40
40
  end
41
41
 
42
42
  # Same as parallel-rspec but runs +bundle exec rails test+ or +bundle exec ruby -I test+ after +--+.
43
+ # Does not run +start+ prepare/database bootstrap (use +polyrun start+ or bare +polyrun+ for that).
43
44
  def cmd_parallel_minitest(argv, config_path)
44
- cfg = Polyrun::Config.load(path: config_path || ENV["POLYRUN_CONFIG"])
45
- code = start_bootstrap!(cfg, argv, config_path)
46
- return code if code != 0
47
-
48
45
  sep = argv.index("--")
49
46
  combined =
50
47
  if sep
@@ -60,11 +57,8 @@ module Polyrun
60
57
 
61
58
  # Same as parallel-rspec but runs +bundle exec polyrun quick+ after +--+ (one Quick process per shard).
62
59
  # Run from the app root with +bundle exec+ so workers resolve the same gem as the parent (same concern as +bundle exec rspec+).
60
+ # Does not run +start+ prepare/database bootstrap (use +polyrun start+ or bare +polyrun+ for that).
63
61
  def cmd_parallel_quick(argv, config_path)
64
- cfg = Polyrun::Config.load(path: config_path || ENV["POLYRUN_CONFIG"])
65
- code = start_bootstrap!(cfg, argv, config_path)
66
- return code if code != 0
67
-
68
62
  sep = argv.index("--")
69
63
  combined =
70
64
  if sep
@@ -33,6 +33,9 @@ module Polyrun
33
33
  items, paths_source, err = run_shards_resolve_items(o[:paths_file], pc)
34
34
  return [err, nil] if err
35
35
 
36
+ err = run_shards_validate_suite_vs_command!(items, cmd)
37
+ return [err, nil] if err
38
+
36
39
  costs, strategy, err = run_shards_resolve_costs(
37
40
  o[:timing_path],
38
41
  o[:strategy],
@@ -64,9 +64,18 @@ module Polyrun
64
64
  Polyrun::Log.warn "polyrun run-shards: no paths (empty paths file or list)"
65
65
  return [nil, nil, 2]
66
66
  end
67
+
67
68
  [items, paths_source, nil]
68
69
  end
69
70
 
71
+ def run_shards_validate_suite_vs_command!(items, cmd)
72
+ msg = Polyrun::Partition::Suite.command_mismatch_message(items, cmd)
73
+ return nil unless msg
74
+
75
+ Polyrun::Log.warn "polyrun run-shards: #{msg}"
76
+ 2
77
+ end
78
+
70
79
  def run_shards_resolve_costs(timing_path, strategy, timing_granularity, strategy_explicit: false)
71
80
  strategy = strategy.to_s
72
81
  if timing_path
data/lib/polyrun/cli.rb CHANGED
@@ -35,7 +35,7 @@ module Polyrun
35
35
  DISPATCH_SUBCOMMAND_NAMES = %w[
36
36
  plan prepare merge-coverage merge-failures merge-spec-quality report-coverage report-junit report-timing report-spec-quality report-benchmark bench
37
37
  env config merge-timing db:setup-template db:setup-shard db:clone-shards
38
- run-shards parallel-rspec start build-paths init queue run-queue quick hook
38
+ run-shards parallel-rspec parallel-minitest parallel-quick start build-paths init queue run-queue quick hook
39
39
  ].freeze
40
40
 
41
41
  # First argv token that is a normal subcommand (not a path); if argv[0] is not here but looks like paths, run implicit parallel.
@@ -187,6 +187,10 @@ module Polyrun
187
187
  cmd_run_shards(argv, config_path)
188
188
  when "parallel-rspec"
189
189
  cmd_parallel_rspec(argv, config_path)
190
+ when "parallel-minitest"
191
+ cmd_parallel_minitest(argv, config_path)
192
+ when "parallel-quick"
193
+ cmd_parallel_quick(argv, config_path)
190
194
  when "start"
191
195
  cmd_start(argv, config_path)
192
196
  when "build-paths"
@@ -23,9 +23,10 @@ module Polyrun
23
23
  end
24
24
 
25
25
  # Infer parallel suite from explicit paths (+_spec.rb+ vs +_test.rb+ vs Polyrun quick-style +.rb+).
26
+ # Recognizes RSpec-style +path:line+ locators (strips a trailing numeric locator before classifying).
26
27
  # Returns +:rspec+, +:minitest+, +:quick+, +:invalid+ (mixed spec and test), or +nil+ (empty).
27
28
  def infer_suite_from_paths(paths)
28
- paths = paths.map { |p| File.expand_path(p) }
29
+ paths = Array(paths).map { |p| suite_classify_path(p) }
29
30
  return nil if paths.empty?
30
31
 
31
32
  specs = paths.count { |p| File.basename(p).end_with?("_spec.rb") }
@@ -44,8 +45,10 @@ module Polyrun
44
45
  # When +paths_file+ is set but missing, returns +{ error: "..." }+.
45
46
  # Otherwise returns +{ items:, source: }+ (human-readable source label).
46
47
  #
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+).
48
+ # +partition.suite+ (optional): +auto+ (default), +rspec+, +minitest+, +quick+ — used when resolving
49
+ # from globs. Legacy +spec/spec_paths.txt+ is used only for +auto+/+rspec+ (RSpec-oriented); an
50
+ # explicit +minitest+/+quick+ suite skips it and globs instead. Bare +polyrun+ suite selection is
51
+ # {Suite.resolve_default} (explicit suite → paths_file inference → {detect_auto_suite}).
49
52
  def resolve_run_shard_items(paths_file: nil, cwd: Dir.pwd, partition: {})
50
53
  if paths_file
51
54
  abs = File.expand_path(paths_file.to_s, cwd)
@@ -53,7 +56,7 @@ module Polyrun
53
56
  return {error: "paths file not found: #{abs}"}
54
57
  end
55
58
  {items: read_lines(abs), source: paths_file.to_s}
56
- elsif File.file?(File.join(cwd, "spec", "spec_paths.txt"))
59
+ elsif use_legacy_spec_paths?(cwd, partition)
57
60
  {items: read_lines(File.join(cwd, "spec", "spec_paths.txt")), source: "spec/spec_paths.txt"}
58
61
  else
59
62
  resolve_run_shard_items_glob(cwd: cwd, partition: partition)
@@ -104,6 +107,22 @@ module Polyrun
104
107
  require_relative "../quick/runner"
105
108
  Polyrun::Quick::Runner.parallel_default_paths(base)
106
109
  end
110
+
111
+ # Strip trailing numeric locators (+path:line+ or +path:line:column+), then expand.
112
+ def suite_classify_path(path)
113
+ raw = path.to_s.sub(/(?::\d+)+\z/, "")
114
+ File.expand_path(raw)
115
+ end
116
+ private_class_method :suite_classify_path
117
+
118
+ def use_legacy_spec_paths?(cwd, partition)
119
+ return false unless File.file?(File.join(cwd, "spec", "spec_paths.txt"))
120
+
121
+ suite = (partition["suite"] || partition[:suite] || "auto").to_s.downcase
122
+ suite = "auto" if suite.empty?
123
+ %w[auto rspec].include?(suite)
124
+ end
125
+ private_class_method :use_legacy_spec_paths?
107
126
  end
108
127
  end
109
128
  end
@@ -0,0 +1,116 @@
1
+ module Polyrun
2
+ module Partition
3
+ # Chooses the parallel runner suite for bare +polyrun+ and validates command vs path lists.
4
+ module Suite
5
+ module_function
6
+
7
+ # Resolve suite for default CLI dispatch.
8
+ # Order: explicit +partition.suite+ (not auto) → infer from +paths_file+ → +detect_auto_suite+.
9
+ # Returns +{ suite: :rspec|:minitest|:quick }+ or +{ error: "..." }+.
10
+ def resolve_default(cwd: Dir.pwd, partition: {})
11
+ configured = configured_suite(partition)
12
+ return {error: "unknown partition.suite: #{configured.inspect} (expected auto, rspec, minitest, quick)"} unless
13
+ %w[auto rspec minitest quick].include?(configured)
14
+
15
+ inferred = infer_from_paths_file(cwd, partition)
16
+ return inferred if inferred.is_a?(Hash) && inferred[:error]
17
+
18
+ if configured != "auto"
19
+ suite = configured.to_sym
20
+ if inferred && inferred != suite
21
+ return {
22
+ error: "partition.suite is #{suite} but paths_file infers #{inferred} " \
23
+ "(fix partition.suite or the paths file)"
24
+ }
25
+ end
26
+ return {suite: suite}
27
+ end
28
+
29
+ return {suite: inferred} if inferred
30
+
31
+ auto = Paths.detect_auto_suite(cwd)
32
+ return {suite: auto} if auto
33
+
34
+ {
35
+ error: "no tests found (spec/**/*_spec.rb, test/**/*_test.rb, or Polyrun quick files). See polyrun help."
36
+ }
37
+ end
38
+
39
+ def infer_from_command(cmd)
40
+ tokens = Array(cmd).map(&:to_s)
41
+ return nil if tokens.empty?
42
+
43
+ return :rspec if tokens.any? { |t| File.basename(t) == "rspec" }
44
+ return :quick if tokens.include?("quick") && tokens.any? { |t| File.basename(t).include?("polyrun") }
45
+ return :minitest if minitest_command?(tokens)
46
+
47
+ nil
48
+ end
49
+
50
+ def command_mismatch_message(items, cmd)
51
+ path_suite = Paths.infer_suite_from_paths(Array(items))
52
+ cmd_suite = infer_from_command(cmd)
53
+
54
+ if path_suite == :invalid
55
+ return nil unless cmd_suite
56
+
57
+ return "mixing _spec.rb and _test.rb paths is not supported with a #{cmd_suite} command " \
58
+ "(use a suite-agnostic custom command, or split the path list)"
59
+ end
60
+
61
+ return nil if path_suite.nil?
62
+ return nil if cmd_suite.nil?
63
+ return nil if path_suite == cmd_suite
64
+
65
+ "paths infer #{path_suite} but command looks like #{cmd_suite} " \
66
+ "(use parallel-#{path_suite}, fix partition.paths_file, or pass a matching command after --)"
67
+ end
68
+
69
+ def configured_suite(partition)
70
+ suite = (partition["suite"] || partition[:suite] || "auto").to_s.downcase
71
+ suite.empty? ? "auto" : suite
72
+ end
73
+ private_class_method :configured_suite
74
+
75
+ def infer_from_paths_file(cwd, partition)
76
+ paths_file = partition["paths_file"] || partition[:paths_file]
77
+ return nil unless paths_file
78
+
79
+ abs = File.expand_path(paths_file.to_s, cwd)
80
+ return nil unless File.file?(abs)
81
+
82
+ items = Paths.read_lines(abs)
83
+ inferred = Paths.infer_suite_from_paths(items)
84
+ return {error: "paths_file mixes _spec.rb and _test.rb (not supported in one run)"} if inferred == :invalid
85
+
86
+ inferred
87
+ end
88
+ private_class_method :infer_from_paths_file
89
+
90
+ def minitest_command?(tokens)
91
+ return true if tokens.any? { |t| File.basename(t) == "rails" } && tokens.include?("test")
92
+ return true if ruby_with_test_load_path?(tokens)
93
+
94
+ false
95
+ end
96
+ private_class_method :minitest_command?
97
+
98
+ def ruby_with_test_load_path?(tokens)
99
+ tokens.each_with_index do |tok, i|
100
+ next unless File.basename(tok) == "ruby"
101
+
102
+ rest = tokens[(i + 1)..] || []
103
+ rest.each_with_index do |arg, j|
104
+ return true if arg == "-Itest"
105
+ return true if arg == "-I" && rest[j + 1] == "test"
106
+ next unless arg.start_with?("-I") && arg.length > 2
107
+
108
+ return true if arg[2..].split(/[=:]/).include?("test")
109
+ end
110
+ end
111
+ false
112
+ end
113
+ private_class_method :ruby_with_test_load_path?
114
+ end
115
+ end
116
+ end
@@ -1,3 +1,3 @@
1
1
  module Polyrun
2
- VERSION = "2.2.4"
2
+ VERSION = "2.2.5"
3
3
  end
data/lib/polyrun.rb CHANGED
@@ -11,6 +11,7 @@ require_relative "polyrun/coverage/reporting"
11
11
  require_relative "polyrun/coverage/rails"
12
12
  require_relative "polyrun/partition/plan"
13
13
  require_relative "polyrun/partition/paths"
14
+ require_relative "polyrun/partition/suite"
14
15
  require_relative "polyrun/partition/paths_build"
15
16
  require_relative "polyrun/queue/file_store"
16
17
  require_relative "polyrun/data/fixtures"
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.4
4
+ version: 2.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -313,6 +313,7 @@ files:
313
313
  - lib/polyrun/partition/plan_sharding.rb
314
314
  - lib/polyrun/partition/reports.rb
315
315
  - lib/polyrun/partition/stable_shuffle.rb
316
+ - lib/polyrun/partition/suite.rb
316
317
  - lib/polyrun/partition/timing_diagnostics.rb
317
318
  - lib/polyrun/partition/timing_keys.rb
318
319
  - lib/polyrun/prepare/artifacts.rb