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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +1 -1
- data/lib/polyrun/cli/ci_shard_run_command.rb +97 -2
- data/lib/polyrun/cli/ci_shard_run_parse.rb +68 -0
- data/lib/polyrun/cli/config_command.rb +42 -0
- data/lib/polyrun/cli/default_run.rb +115 -0
- data/lib/polyrun/cli/help.rb +54 -0
- data/lib/polyrun/cli/helpers.rb +4 -31
- data/lib/polyrun/cli/plan_command.rb +22 -12
- data/lib/polyrun/cli/prepare_command.rb +2 -2
- data/lib/polyrun/cli/queue_command.rb +46 -19
- data/lib/polyrun/cli/run_shards_command.rb +62 -5
- data/lib/polyrun/cli/run_shards_plan_boot_phases.rb +1 -1
- data/lib/polyrun/cli/run_shards_plan_options.rb +1 -1
- data/lib/polyrun/cli/run_shards_planning.rb +8 -8
- data/lib/polyrun/cli/run_shards_run.rb +4 -2
- data/lib/polyrun/cli/start_bootstrap.rb +2 -6
- data/lib/polyrun/cli.rb +46 -50
- data/lib/polyrun/config/dotted_path.rb +21 -0
- data/lib/polyrun/config/effective.rb +72 -0
- data/lib/polyrun/config/resolver.rb +78 -0
- data/lib/polyrun/config.rb +7 -0
- data/lib/polyrun/coverage/collector.rb +15 -9
- data/lib/polyrun/coverage/collector_finish.rb +2 -0
- data/lib/polyrun/coverage/collector_fragment_meta.rb +57 -0
- data/lib/polyrun/partition/paths.rb +83 -2
- data/lib/polyrun/quick/runner.rb +26 -17
- data/lib/polyrun/templates/ci_matrix.polyrun.yml +3 -2
- data/lib/polyrun/version.rb +1 -1
- metadata +9 -1
|
@@ -7,11 +7,18 @@ module Polyrun
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
# File-backed queue (spec_queue.md): init → claim batches → ack (ledger append-only).
|
|
10
|
+
#
|
|
11
|
+
# *N×M and load balancing*: +queue init+ uses the same +Partition::Plan+ slice as +polyrun plan+
|
|
12
|
+
# when +--shard+ / +--total+ (or +partition.shard_index+ / +shard_total+ / CI env) define a matrix.
|
|
13
|
+
# Each CI job runs +init+ once for its shard; the queue holds **only** that shard's paths (no duplicate
|
|
14
|
+
# work across matrix jobs). **M** local workers use +queue claim+ on a **shared** queue directory
|
|
15
|
+
# (NFS or the runner disk): claims are mutually exclusive — **dynamic** balance (fast workers pull more
|
|
16
|
+
# batches), **not** the same static per-worker wall time as +cost_binpack+ on the full suite.
|
|
17
|
+
# +--timing+ on init sorts dequeue order (heavy items first when weights are known); it does **not**
|
|
18
|
+
# replace binpack across workers — for that, use static +plan+ / +run-shards+ with +cost_binpack+.
|
|
10
19
|
def cmd_queue(argv)
|
|
11
20
|
dir = ".polyrun-queue"
|
|
12
21
|
paths_file = nil
|
|
13
|
-
timing_path = nil
|
|
14
|
-
timing_granularity = nil
|
|
15
22
|
worker = ENV["USER"] || "worker"
|
|
16
23
|
batch = 5
|
|
17
24
|
lease_id = nil
|
|
@@ -20,7 +27,7 @@ module Polyrun
|
|
|
20
27
|
Polyrun::Debug.log("queue: subcommand=#{sub.inspect}")
|
|
21
28
|
case sub
|
|
22
29
|
when "init"
|
|
23
|
-
queue_cmd_init(argv, dir
|
|
30
|
+
queue_cmd_init(argv, dir)
|
|
24
31
|
when "claim"
|
|
25
32
|
queue_cmd_claim(argv, dir, worker, batch)
|
|
26
33
|
when "ack"
|
|
@@ -33,34 +40,54 @@ module Polyrun
|
|
|
33
40
|
end
|
|
34
41
|
end
|
|
35
42
|
|
|
36
|
-
def queue_cmd_init(argv, dir
|
|
43
|
+
def queue_cmd_init(argv, dir)
|
|
44
|
+
cfg = Polyrun::Config.load(path: ENV["POLYRUN_CONFIG"])
|
|
45
|
+
pc = cfg.partition
|
|
46
|
+
ctx = plan_command_initial_context(pc)
|
|
47
|
+
paths_file = nil
|
|
37
48
|
OptionParser.new do |opts|
|
|
38
|
-
opts.banner = "usage: polyrun queue init --paths-file P [--
|
|
49
|
+
opts.banner = "usage: polyrun queue init --paths-file P [--dir DIR] [same partition options as polyrun plan]"
|
|
39
50
|
opts.on("--dir PATH") { |v| dir = v }
|
|
40
|
-
opts.on("--paths-file PATH") { |v| paths_file = v }
|
|
41
|
-
opts
|
|
42
|
-
opts.on("--timing-granularity VAL") { |v| timing_granularity = v }
|
|
51
|
+
opts.on("--paths-file PATH", String) { |v| paths_file = v }
|
|
52
|
+
plan_command_register_partition_options!(opts, ctx)
|
|
43
53
|
end.parse!(argv)
|
|
44
54
|
unless paths_file
|
|
45
55
|
Polyrun::Log.warn "queue init: need --paths-file"
|
|
46
56
|
return 2
|
|
47
57
|
end
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
File.expand_path(timing_path, Dir.pwd),
|
|
55
|
-
granularity: g
|
|
56
|
-
)
|
|
57
|
-
end
|
|
58
|
-
ordered = queue_init_ordered_items(items, costs, g)
|
|
58
|
+
code = Polyrun::Partition::PathsBuild.apply!(partition: pc, cwd: Dir.pwd)
|
|
59
|
+
return code if code != 0
|
|
60
|
+
|
|
61
|
+
ordered, code = queue_partition_manifest_and_ordered_paths(cfg, pc, ctx, paths_file)
|
|
62
|
+
return code if code != 0
|
|
63
|
+
|
|
59
64
|
Polyrun::Queue::FileStore.new(dir).init!(ordered)
|
|
60
65
|
Polyrun::Log.puts JSON.generate({"dir" => File.expand_path(dir), "count" => ordered.size})
|
|
61
66
|
0
|
|
62
67
|
end
|
|
63
68
|
|
|
69
|
+
def queue_partition_manifest_and_ordered_paths(cfg, pc, ctx, paths_file)
|
|
70
|
+
Polyrun::Log.warn "polyrun queue init: using #{cfg.path}" if @verbose && cfg.path
|
|
71
|
+
|
|
72
|
+
manifest, code = plan_command_manifest_from_paths(cfg, pc, [], ctx, paths_file)
|
|
73
|
+
return [nil, code] if code != 0
|
|
74
|
+
|
|
75
|
+
paths = manifest["paths"] || []
|
|
76
|
+
g = resolve_partition_timing_granularity(pc, ctx[:timing_granularity])
|
|
77
|
+
timing_for_sort = plan_resolve_timing_path(pc, ctx[:timing_path], ctx[:strategy])
|
|
78
|
+
costs = queue_init_timing_costs(timing_for_sort, g)
|
|
79
|
+
[queue_init_ordered_items(paths, costs, g), 0]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def queue_init_timing_costs(timing_for_sort, g)
|
|
83
|
+
return nil unless timing_for_sort
|
|
84
|
+
|
|
85
|
+
Polyrun::Partition::Plan.load_timing_costs(
|
|
86
|
+
File.expand_path(timing_for_sort.to_s, Dir.pwd),
|
|
87
|
+
granularity: g
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
64
91
|
def queue_init_ordered_items(items, costs, granularity = :file)
|
|
65
92
|
if costs && !costs.empty?
|
|
66
93
|
dw = costs.values.sum / costs.size.to_f
|
|
@@ -12,12 +12,10 @@ module Polyrun
|
|
|
12
12
|
|
|
13
13
|
private
|
|
14
14
|
|
|
15
|
-
# Default and upper bound for parallel OS processes (POLYRUN_WORKERS / --workers).
|
|
16
|
-
DEFAULT_PARALLEL_WORKERS = 5
|
|
17
|
-
MAX_PARALLEL_WORKERS = 10
|
|
15
|
+
# Default and upper bound for parallel OS processes (POLYRUN_WORKERS / --workers); see {Polyrun::Config}.
|
|
18
16
|
|
|
19
17
|
# Spawns N OS processes (not Ruby threads) with POLYRUN_SHARD_INDEX / POLYRUN_SHARD_TOTAL so
|
|
20
|
-
# {Coverage::Collector} writes coverage/polyrun-fragment
|
|
18
|
+
# {Coverage::Collector} writes coverage/polyrun-fragment-worker<N>.json (or shard<S>-worker<W>.json in N×M CI). Merge with merge-coverage.
|
|
21
19
|
def cmd_run_shards(argv, config_path)
|
|
22
20
|
run_shards_run!(argv, config_path)
|
|
23
21
|
end
|
|
@@ -37,6 +35,54 @@ module Polyrun
|
|
|
37
35
|
cmd_run_shards(combined, config_path)
|
|
38
36
|
end
|
|
39
37
|
|
|
38
|
+
# Same as parallel-rspec but runs +bundle exec rails test+ or +bundle exec ruby -I test+ after +--+.
|
|
39
|
+
def cmd_parallel_minitest(argv, config_path)
|
|
40
|
+
cfg = Polyrun::Config.load(path: config_path || ENV["POLYRUN_CONFIG"])
|
|
41
|
+
code = start_bootstrap!(cfg, argv, config_path)
|
|
42
|
+
return code if code != 0
|
|
43
|
+
|
|
44
|
+
sep = argv.index("--")
|
|
45
|
+
combined =
|
|
46
|
+
if sep
|
|
47
|
+
head = argv[0...sep]
|
|
48
|
+
tail = argv[sep..]
|
|
49
|
+
head + ["--merge-coverage"] + tail
|
|
50
|
+
else
|
|
51
|
+
argv + ["--merge-coverage", "--"] + minitest_parallel_cmd
|
|
52
|
+
end
|
|
53
|
+
Polyrun::Debug.log_kv(parallel_minitest: "combined argv", argv: combined)
|
|
54
|
+
cmd_run_shards(combined, config_path)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Same as parallel-rspec but runs +bundle exec polyrun quick+ after +--+ (one Quick process per shard).
|
|
58
|
+
# Run from the app root with +bundle exec+ so workers resolve the same gem as the parent (same concern as +bundle exec rspec+).
|
|
59
|
+
def cmd_parallel_quick(argv, config_path)
|
|
60
|
+
cfg = Polyrun::Config.load(path: config_path || ENV["POLYRUN_CONFIG"])
|
|
61
|
+
code = start_bootstrap!(cfg, argv, config_path)
|
|
62
|
+
return code if code != 0
|
|
63
|
+
|
|
64
|
+
sep = argv.index("--")
|
|
65
|
+
combined =
|
|
66
|
+
if sep
|
|
67
|
+
head = argv[0...sep]
|
|
68
|
+
tail = argv[sep..]
|
|
69
|
+
head + ["--merge-coverage"] + tail
|
|
70
|
+
else
|
|
71
|
+
argv + ["--merge-coverage", "--", "bundle", "exec", "polyrun", "quick"]
|
|
72
|
+
end
|
|
73
|
+
Polyrun::Debug.log_kv(parallel_quick: "combined argv", argv: combined)
|
|
74
|
+
cmd_run_shards(combined, config_path)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def minitest_parallel_cmd
|
|
78
|
+
rails_bin = File.expand_path("bin/rails", Dir.pwd)
|
|
79
|
+
if File.file?(rails_bin)
|
|
80
|
+
["bundle", "exec", "rails", "test"]
|
|
81
|
+
else
|
|
82
|
+
["bundle", "exec", "ruby", "-I", "test"]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
40
86
|
# Convenience alias: optional legacy script/build_spec_paths.rb (if present and partition.paths_build unset), then parallel-rspec.
|
|
41
87
|
def cmd_start(argv, config_path)
|
|
42
88
|
cfg = Polyrun::Config.load(path: config_path || ENV["POLYRUN_CONFIG"])
|
|
@@ -66,10 +112,21 @@ module Polyrun
|
|
|
66
112
|
end
|
|
67
113
|
|
|
68
114
|
# ENV for a worker process: POLYRUN_SHARD_* plus per-shard database URLs from polyrun.yml or DATABASE_URL.
|
|
69
|
-
|
|
115
|
+
# When +matrix_total+ > 1 with multiple local workers, sets +POLYRUN_SHARD_MATRIX_INDEX+ / +POLYRUN_SHARD_MATRIX_TOTAL+
|
|
116
|
+
# so {Coverage::Collector} can name fragments uniquely across CI matrix jobs (NxM sharding).
|
|
117
|
+
def shard_child_env(cfg:, workers:, shard:, matrix_index: nil, matrix_total: nil)
|
|
70
118
|
child_env = ENV.to_h.merge(
|
|
71
119
|
Polyrun::Database::Shard.env_map(shard_index: shard, shard_total: workers)
|
|
72
120
|
)
|
|
121
|
+
mt = matrix_total.nil? ? 0 : Integer(matrix_total)
|
|
122
|
+
if mt > 1
|
|
123
|
+
if matrix_index.nil?
|
|
124
|
+
Polyrun::Log.warn "polyrun run-shards: matrix_total=#{mt} but matrix_index is nil; omit POLYRUN_SHARD_MATRIX_*"
|
|
125
|
+
else
|
|
126
|
+
child_env["POLYRUN_SHARD_MATRIX_INDEX"] = Integer(matrix_index).to_s
|
|
127
|
+
child_env["POLYRUN_SHARD_MATRIX_TOTAL"] = mt.to_s
|
|
128
|
+
end
|
|
129
|
+
end
|
|
73
130
|
dh = cfg.databases
|
|
74
131
|
if dh.is_a?(Hash) && !dh.empty?
|
|
75
132
|
child_env.merge!(Polyrun::Database::UrlBuilder.env_exports_for_databases(dh, shard_index: shard))
|
|
@@ -25,7 +25,7 @@ module Polyrun
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def run_shards_plan_phase_b(o, cmd, cfg, pc, run_t0, config_path)
|
|
28
|
-
items, paths_source, err = run_shards_resolve_items(o[:paths_file])
|
|
28
|
+
items, paths_source, err = run_shards_resolve_items(o[:paths_file], pc)
|
|
29
29
|
return [err, nil] if err
|
|
30
30
|
|
|
31
31
|
costs, strategy, err = run_shards_resolve_costs(o[:timing_path], o[:strategy], o[:timing_granularity])
|
|
@@ -15,7 +15,7 @@ module Polyrun
|
|
|
15
15
|
|
|
16
16
|
def run_shards_plan_options_state(pc)
|
|
17
17
|
{
|
|
18
|
-
workers: env_int("POLYRUN_WORKERS",
|
|
18
|
+
workers: env_int("POLYRUN_WORKERS", Polyrun::Config::DEFAULT_PARALLEL_WORKERS),
|
|
19
19
|
paths_file: nil,
|
|
20
20
|
strategy: (pc["strategy"] || pc[:strategy] || "round_robin").to_s,
|
|
21
21
|
seed: pc["seed"] || pc[:seed],
|
|
@@ -38,9 +38,9 @@ module Polyrun
|
|
|
38
38
|
Polyrun::Log.warn "polyrun run-shards: --workers must be >= 1"
|
|
39
39
|
return 2
|
|
40
40
|
end
|
|
41
|
-
if w >
|
|
42
|
-
Polyrun::Log.warn "polyrun run-shards: capping --workers / POLYRUN_WORKERS from #{w} to #{
|
|
43
|
-
o[:workers] =
|
|
41
|
+
if w > Polyrun::Config::MAX_PARALLEL_WORKERS
|
|
42
|
+
Polyrun::Log.warn "polyrun run-shards: capping --workers / POLYRUN_WORKERS from #{w} to #{Polyrun::Config::MAX_PARALLEL_WORKERS}"
|
|
43
|
+
o[:workers] = Polyrun::Config::MAX_PARALLEL_WORKERS
|
|
44
44
|
end
|
|
45
45
|
nil
|
|
46
46
|
end
|
|
@@ -53,18 +53,18 @@ module Polyrun
|
|
|
53
53
|
nil
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
def run_shards_resolve_items(paths_file)
|
|
57
|
-
resolved = Polyrun::Partition::Paths.resolve_run_shard_items(paths_file: paths_file)
|
|
56
|
+
def run_shards_resolve_items(paths_file, partition)
|
|
57
|
+
resolved = Polyrun::Partition::Paths.resolve_run_shard_items(paths_file: paths_file, partition: partition)
|
|
58
58
|
if resolved[:error]
|
|
59
59
|
Polyrun::Log.warn "polyrun run-shards: #{resolved[:error]}"
|
|
60
60
|
return [nil, nil, 2]
|
|
61
61
|
end
|
|
62
62
|
items = resolved[:items]
|
|
63
63
|
paths_source = resolved[:source]
|
|
64
|
-
Polyrun::Log.warn "polyrun run-shards: #{items.size}
|
|
64
|
+
Polyrun::Log.warn "polyrun run-shards: #{items.size} path(s) from #{paths_source}"
|
|
65
65
|
|
|
66
66
|
if items.empty?
|
|
67
|
-
Polyrun::Log.warn "polyrun run-shards: no
|
|
67
|
+
Polyrun::Log.warn "polyrun run-shards: no paths (empty paths file or list)"
|
|
68
68
|
return [nil, nil, 2]
|
|
69
69
|
end
|
|
70
70
|
[items, paths_source, nil]
|
|
@@ -119,7 +119,7 @@ module Polyrun
|
|
|
119
119
|
|
|
120
120
|
def run_shards_warn_parallel_banner(item_count, workers, strategy)
|
|
121
121
|
Polyrun::Log.warn <<~MSG
|
|
122
|
-
polyrun run-shards: #{item_count}
|
|
122
|
+
polyrun run-shards: #{item_count} path(s) -> #{workers} parallel worker processes (not Ruby threads); strategy=#{strategy}
|
|
123
123
|
(plain `bundle exec rspec` is one process; this command fans out.)
|
|
124
124
|
MSG
|
|
125
125
|
end
|
|
@@ -50,6 +50,8 @@ module Polyrun
|
|
|
50
50
|
cfg = ctx[:cfg]
|
|
51
51
|
plan = ctx[:plan]
|
|
52
52
|
parallel = ctx[:parallel]
|
|
53
|
+
mx = ctx[:matrix_shard_index]
|
|
54
|
+
mt = ctx[:matrix_shard_total]
|
|
53
55
|
|
|
54
56
|
pids = []
|
|
55
57
|
workers.times do |shard|
|
|
@@ -59,7 +61,7 @@ module Polyrun
|
|
|
59
61
|
next
|
|
60
62
|
end
|
|
61
63
|
|
|
62
|
-
child_env = shard_child_env(cfg: cfg, workers: workers, shard: shard)
|
|
64
|
+
child_env = shard_child_env(cfg: cfg, workers: workers, shard: shard, matrix_index: mx, matrix_total: mt)
|
|
63
65
|
|
|
64
66
|
Polyrun::Log.warn "polyrun run-shards: shard #{shard} → #{paths.size} file(s)" if @verbose
|
|
65
67
|
pid = Process.spawn(child_env, *cmd, *paths)
|
|
@@ -140,7 +142,7 @@ module Polyrun
|
|
|
140
142
|
|
|
141
143
|
if ctx[:parallel]
|
|
142
144
|
Polyrun::Log.warn <<~MSG
|
|
143
|
-
polyrun run-shards: coverage — each worker writes coverage/polyrun-fragment
|
|
145
|
+
polyrun run-shards: coverage — each worker writes coverage/polyrun-fragment-worker<N>.json when Polyrun coverage is enabled (POLYRUN_SHARD_INDEX per process).
|
|
144
146
|
polyrun run-shards: next step — merge with: polyrun merge-coverage -i 'coverage/polyrun-fragment-*.json' -o coverage/merged.json --format json,cobertura,console
|
|
145
147
|
MSG
|
|
146
148
|
end
|
|
@@ -4,10 +4,6 @@ module Polyrun
|
|
|
4
4
|
module StartBootstrap
|
|
5
5
|
private
|
|
6
6
|
|
|
7
|
-
# Keep in sync with {RunShardsCommand} worker defaults.
|
|
8
|
-
START_ARG_WORKERS_DEFAULT = 5
|
|
9
|
-
START_ARG_WORKERS_MAX = 10
|
|
10
|
-
|
|
11
7
|
def start_bootstrap!(cfg, argv, config_path)
|
|
12
8
|
if start_run_prepare?(cfg) && !truthy_env?("POLYRUN_START_SKIP_PREPARE")
|
|
13
9
|
recipe = cfg.prepare["recipe"] || cfg.prepare[:recipe] || "default"
|
|
@@ -76,7 +72,7 @@ module Polyrun
|
|
|
76
72
|
def parse_workers_from_start_argv(argv)
|
|
77
73
|
sep = argv.index("--")
|
|
78
74
|
head = sep ? argv[0...sep] : argv
|
|
79
|
-
workers = env_int("POLYRUN_WORKERS",
|
|
75
|
+
workers = env_int("POLYRUN_WORKERS", Polyrun::Config::DEFAULT_PARALLEL_WORKERS)
|
|
80
76
|
i = 0
|
|
81
77
|
while i < head.size
|
|
82
78
|
if head[i] == "--workers" && head[i + 1]
|
|
@@ -87,7 +83,7 @@ module Polyrun
|
|
|
87
83
|
i += 1
|
|
88
84
|
end
|
|
89
85
|
end
|
|
90
|
-
workers.clamp(1,
|
|
86
|
+
workers.clamp(1, Polyrun::Config::MAX_PARALLEL_WORKERS)
|
|
91
87
|
end
|
|
92
88
|
|
|
93
89
|
def truthy_env?(name)
|
data/lib/polyrun/cli.rb
CHANGED
|
@@ -12,7 +12,11 @@ require_relative "cli/queue_command"
|
|
|
12
12
|
require_relative "cli/timing_command"
|
|
13
13
|
require_relative "cli/init_command"
|
|
14
14
|
require_relative "cli/quick_command"
|
|
15
|
+
require_relative "cli/ci_shard_run_parse"
|
|
15
16
|
require_relative "cli/ci_shard_run_command"
|
|
17
|
+
require_relative "cli/config_command"
|
|
18
|
+
require_relative "cli/default_run"
|
|
19
|
+
require_relative "cli/help"
|
|
16
20
|
|
|
17
21
|
module Polyrun
|
|
18
22
|
class CLI
|
|
@@ -21,6 +25,18 @@ module Polyrun
|
|
|
21
25
|
"ci-shard-rspec" => :cmd_ci_shard_rspec
|
|
22
26
|
}.freeze
|
|
23
27
|
|
|
28
|
+
# Keep in sync with +dispatch_cli_command_subcommands+ (+when+ branches). Used for implicit path routing.
|
|
29
|
+
DISPATCH_SUBCOMMAND_NAMES = %w[
|
|
30
|
+
plan prepare merge-coverage report-coverage report-junit report-timing
|
|
31
|
+
env config merge-timing db:setup-template db:setup-shard db:clone-shards
|
|
32
|
+
run-shards parallel-rspec start build-paths init queue quick
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
# First argv token that is a normal subcommand (not a path); if argv[0] is not here but looks like paths, run implicit parallel.
|
|
36
|
+
IMPLICIT_PATH_EXCLUSION_TOKENS = (
|
|
37
|
+
DISPATCH_SUBCOMMAND_NAMES + CI_SHARD_COMMANDS.keys + %w[help version]
|
|
38
|
+
).freeze
|
|
39
|
+
|
|
24
40
|
include Helpers
|
|
25
41
|
include PlanCommand
|
|
26
42
|
include PrepareCommand
|
|
@@ -33,7 +49,11 @@ module Polyrun
|
|
|
33
49
|
include TimingCommand
|
|
34
50
|
include InitCommand
|
|
35
51
|
include QuickCommand
|
|
52
|
+
include CiShardRunParse
|
|
36
53
|
include CiShardRunCommand
|
|
54
|
+
include ConfigCommand
|
|
55
|
+
include DefaultRun
|
|
56
|
+
include Help
|
|
37
57
|
|
|
38
58
|
def self.run(argv = ARGV)
|
|
39
59
|
new.run(argv)
|
|
@@ -44,12 +64,30 @@ module Polyrun
|
|
|
44
64
|
config_path = parse_global_cli!(argv)
|
|
45
65
|
return config_path if config_path.is_a?(Integer)
|
|
46
66
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
67
|
+
if argv.empty?
|
|
68
|
+
Polyrun::Debug.log_kv(
|
|
69
|
+
command: "(default)",
|
|
70
|
+
cwd: Dir.pwd,
|
|
71
|
+
polyrun_config: config_path,
|
|
72
|
+
argv_rest: [],
|
|
73
|
+
verbose: @verbose
|
|
74
|
+
)
|
|
75
|
+
return dispatch_default_parallel!(config_path)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if implicit_parallel_run?(argv)
|
|
79
|
+
Polyrun::Debug.log_kv(
|
|
80
|
+
command: "(paths)",
|
|
81
|
+
cwd: Dir.pwd,
|
|
82
|
+
polyrun_config: config_path,
|
|
83
|
+
argv_rest: argv.dup,
|
|
84
|
+
verbose: @verbose
|
|
85
|
+
)
|
|
86
|
+
return dispatch_implicit_parallel_targets!(argv, config_path)
|
|
51
87
|
end
|
|
52
88
|
|
|
89
|
+
command = argv.shift
|
|
90
|
+
|
|
53
91
|
Polyrun::Debug.log_kv(
|
|
54
92
|
command: command,
|
|
55
93
|
cwd: Dir.pwd,
|
|
@@ -96,6 +134,7 @@ module Polyrun
|
|
|
96
134
|
end
|
|
97
135
|
end
|
|
98
136
|
|
|
137
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity -- explicit dispatch table
|
|
99
138
|
def dispatch_cli_command_subcommands(command, argv, config_path)
|
|
100
139
|
case command
|
|
101
140
|
when "plan"
|
|
@@ -112,6 +151,8 @@ module Polyrun
|
|
|
112
151
|
cmd_report_timing(argv)
|
|
113
152
|
when "env"
|
|
114
153
|
cmd_env(argv, config_path)
|
|
154
|
+
when "config"
|
|
155
|
+
cmd_config(argv, config_path)
|
|
115
156
|
when "merge-timing"
|
|
116
157
|
cmd_merge_timing(argv)
|
|
117
158
|
when "db:setup-template"
|
|
@@ -141,52 +182,7 @@ module Polyrun
|
|
|
141
182
|
2
|
|
142
183
|
end
|
|
143
184
|
end
|
|
144
|
-
|
|
145
|
-
def print_help
|
|
146
|
-
Polyrun::Log.puts <<~HELP
|
|
147
|
-
usage: polyrun [global options] <command> [options]
|
|
148
|
-
|
|
149
|
-
global:
|
|
150
|
-
-c, --config PATH polyrun.yml path (or POLYRUN_CONFIG)
|
|
151
|
-
-v, --verbose
|
|
152
|
-
-h, --help
|
|
153
|
-
|
|
154
|
-
Trace timing (stderr): DEBUG=1 or POLYRUN_DEBUG=1
|
|
155
|
-
Branch coverage in JSON fragments: POLYRUN_COVERAGE_BRANCHES=1 (stdlib Coverage; merge-coverage merges branches)
|
|
156
|
-
polyrun quick coverage: POLYRUN_COVERAGE=1 or (config/polyrun_coverage.yml + POLYRUN_QUICK_COVERAGE=1); POLYRUN_COVERAGE_DISABLE=1 skips
|
|
157
|
-
Merge wall time (stderr): POLYRUN_PROFILE_MERGE=1 (or verbose / DEBUG)
|
|
158
|
-
Post-merge formats (run-shards): POLYRUN_MERGE_FORMATS (default: json,lcov,cobertura,console,html)
|
|
159
|
-
Skip optional script/build_spec_paths.rb before start: POLYRUN_SKIP_BUILD_SPEC_PATHS=1
|
|
160
|
-
Skip start auto-prepare / auto DB provision: POLYRUN_START_SKIP_PREPARE=1, POLYRUN_START_SKIP_DATABASES=1
|
|
161
|
-
Skip writing paths_file from partition.paths_build: POLYRUN_SKIP_PATHS_BUILD=1
|
|
162
|
-
Warn if merge-coverage wall time exceeds N seconds (default 10): POLYRUN_MERGE_SLOW_WARN_SECONDS (0 disables)
|
|
163
|
-
Parallel RSpec workers: POLYRUN_WORKERS default 5, max 10 (run-shards / parallel-rspec / start)
|
|
164
|
-
Partition timing granularity (default file): POLYRUN_TIMING_GRANULARITY=file|example (experimental per-example; see partition.timing_granularity)
|
|
165
|
-
|
|
166
|
-
commands:
|
|
167
|
-
version print version
|
|
168
|
-
plan emit partition manifest JSON
|
|
169
|
-
prepare run prepare recipe: default | assets (optional prepare.command overrides bin/rails assets:precompile) | shell (prepare.command required)
|
|
170
|
-
merge-coverage merge SimpleCov JSON fragments (json/lcov/cobertura/console)
|
|
171
|
-
run-shards fan out N parallel OS processes (POLYRUN_SHARD_*; not Ruby threads); optional --merge-coverage
|
|
172
|
-
parallel-rspec run-shards + merge-coverage (defaults to: bundle exec rspec after --)
|
|
173
|
-
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
|
|
174
|
-
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 -- (like run-shards; not multi-worker)
|
|
175
|
-
ci-shard-rspec same as ci-shard-run -- bundle exec rspec; optional -- [rspec-only flags]
|
|
176
|
-
build-paths write partition.paths_file from partition.paths_build (same as auto step before plan/run-shards)
|
|
177
|
-
init write a starter polyrun.yml or POLYRUN.md from built-in templates (see docs/SETUP_PROFILE.md)
|
|
178
|
-
queue file-backed batch queue (init / claim / ack / status)
|
|
179
|
-
quick run Polyrun::Quick (describe/it, before/after, let, expect…to, assert_*; optional capybara!)
|
|
180
|
-
report-coverage write all coverage formats from one JSON file
|
|
181
|
-
report-junit RSpec JSON or Polyrun testcase JSON → JUnit XML (CI)
|
|
182
|
-
report-timing print slow-file summary from merged timing JSON
|
|
183
|
-
merge-timing merge polyrun_timing_*.json shards
|
|
184
|
-
env print shard + database env (see polyrun.yml databases)
|
|
185
|
-
db:setup-template migrate template DB (PostgreSQL)
|
|
186
|
-
db:setup-shard CREATE DATABASE shard FROM template (one POLYRUN_SHARD_INDEX)
|
|
187
|
-
db:clone-shards migrate templates + DROP/CREATE all shard DBs (replaces clone_shard shell scripts)
|
|
188
|
-
HELP
|
|
189
|
-
end
|
|
185
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
190
186
|
|
|
191
187
|
def cmd_version
|
|
192
188
|
Polyrun::Log.puts "polyrun #{Polyrun::VERSION}"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Polyrun
|
|
2
|
+
class Config
|
|
3
|
+
# Read nested keys from loaded YAML (+String+ / +Symbol+ indifferent at each step).
|
|
4
|
+
module DottedPath
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def dig(raw, dotted)
|
|
8
|
+
segments = dotted.split(".")
|
|
9
|
+
return nil if segments.empty?
|
|
10
|
+
return nil if segments.any?(&:empty?)
|
|
11
|
+
|
|
12
|
+
segments.reduce(raw) do |m, seg|
|
|
13
|
+
break nil if m.nil?
|
|
14
|
+
break nil unless m.is_a?(Hash)
|
|
15
|
+
|
|
16
|
+
m[seg] || m[seg.to_sym]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require_relative "dotted_path"
|
|
2
|
+
require_relative "resolver"
|
|
3
|
+
|
|
4
|
+
module Polyrun
|
|
5
|
+
class Config
|
|
6
|
+
# Nested hash of values Polyrun uses: loaded YAML (string keys) with overlays for
|
|
7
|
+
# merged +prepare.env+, resolved +partition.shard_index+ / +shard_total+ / +shard_processes+ / +timing_granularity+,
|
|
8
|
+
# and top-level +workers+ (+POLYRUN_WORKERS+ default).
|
|
9
|
+
#
|
|
10
|
+
# +build+ memoizes the last (cfg, env) in-process so repeated +dig+ calls on the same load do not
|
|
11
|
+
# rebuild the tree (single-threaded CLI).
|
|
12
|
+
module Effective
|
|
13
|
+
class << self
|
|
14
|
+
# Per-thread cache avoids rebuilding the effective tree on repeated +dig+; no class ivars (RuboCop ThreadSafety).
|
|
15
|
+
def build(cfg, env: ENV)
|
|
16
|
+
key = cache_key(cfg, env)
|
|
17
|
+
per_thread = (Thread.current[:polyrun_effective_build] ||= {})
|
|
18
|
+
per_thread[key] ||= build_uncached(cfg, env: env)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def dig(cfg, dotted_path, env: ENV)
|
|
22
|
+
Polyrun::Config::DottedPath.dig(build(cfg, env: env), dotted_path)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def cache_key(cfg, env)
|
|
28
|
+
[cfg.path, cfg.object_id, env_fingerprint(env)]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def env_fingerprint(env)
|
|
32
|
+
env.to_h.keys.sort.map { |k| [k, env[k]] }.hash
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def build_uncached(cfg, env:)
|
|
36
|
+
r = Polyrun::Config::Resolver
|
|
37
|
+
base = deep_stringify_keys(cfg.raw)
|
|
38
|
+
|
|
39
|
+
prep = cfg.prepare
|
|
40
|
+
base["prepare"] = deep_stringify_keys(prep)
|
|
41
|
+
base["prepare"]["env"] = r.merged_prepare_env(prep, env)
|
|
42
|
+
|
|
43
|
+
pc = cfg.partition
|
|
44
|
+
part = deep_stringify_keys(pc).merge(
|
|
45
|
+
"shard_index" => r.resolve_shard_index(pc, env),
|
|
46
|
+
"shard_total" => r.resolve_shard_total(pc, env),
|
|
47
|
+
"shard_processes" => r.resolve_shard_processes(pc, env),
|
|
48
|
+
"timing_granularity" => r.resolve_partition_timing_granularity(pc, nil, env).to_s
|
|
49
|
+
)
|
|
50
|
+
base["partition"] = part
|
|
51
|
+
|
|
52
|
+
base["workers"] = r.parallel_worker_count_default(env)
|
|
53
|
+
|
|
54
|
+
base
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def deep_stringify_keys(obj)
|
|
58
|
+
case obj
|
|
59
|
+
when Hash
|
|
60
|
+
obj.each_with_object({}) do |(k, v), m|
|
|
61
|
+
m[k.to_s] = deep_stringify_keys(v)
|
|
62
|
+
end
|
|
63
|
+
when Array
|
|
64
|
+
obj.map { |e| deep_stringify_keys(e) }
|
|
65
|
+
else
|
|
66
|
+
obj
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require_relative "../env/ci"
|
|
2
|
+
require_relative "../partition/timing_keys"
|
|
3
|
+
|
|
4
|
+
module Polyrun
|
|
5
|
+
class Config
|
|
6
|
+
# Single source for values derived from +polyrun.yml+, +ENV+, and CI detection.
|
|
7
|
+
# Used by {Effective}, CLI helpers, and prepare.
|
|
8
|
+
module Resolver
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def env_int(name, fallback, env = ENV)
|
|
12
|
+
s = env[name]
|
|
13
|
+
return fallback if s.nil? || s.empty?
|
|
14
|
+
|
|
15
|
+
Integer(s, exception: false) || fallback
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def prepare_env_yaml_string_map(prep)
|
|
19
|
+
(prep["env"] || prep[:env] || {}).transform_keys(&:to_s).transform_values(&:to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Same merge order as +polyrun prepare+: YAML +prepare.env+ overrides process +ENV+ for overlapping keys.
|
|
23
|
+
def merged_prepare_env(prep, env = ENV)
|
|
24
|
+
prep_env = prepare_env_yaml_string_map(prep)
|
|
25
|
+
env.to_h.merge(prep_env)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def partition_int(pc, keys, default)
|
|
29
|
+
keys.each do |k|
|
|
30
|
+
v = pc[k] || pc[k.to_sym]
|
|
31
|
+
next if v.nil? || v.to_s.empty?
|
|
32
|
+
|
|
33
|
+
i = Integer(v, exception: false)
|
|
34
|
+
return i unless i.nil?
|
|
35
|
+
end
|
|
36
|
+
default
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def resolve_shard_index(pc, env = ENV)
|
|
40
|
+
return Integer(env["POLYRUN_SHARD_INDEX"]) if env["POLYRUN_SHARD_INDEX"] && !env["POLYRUN_SHARD_INDEX"].empty?
|
|
41
|
+
|
|
42
|
+
ci = Polyrun::Env::Ci.detect_shard_index
|
|
43
|
+
return ci unless ci.nil?
|
|
44
|
+
|
|
45
|
+
partition_int(pc, %w[shard_index shard], 0)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def resolve_shard_total(pc, env = ENV)
|
|
49
|
+
return Integer(env["POLYRUN_SHARD_TOTAL"]) if env["POLYRUN_SHARD_TOTAL"] && !env["POLYRUN_SHARD_TOTAL"].empty?
|
|
50
|
+
|
|
51
|
+
ci = Polyrun::Env::Ci.detect_shard_total
|
|
52
|
+
return ci unless ci.nil?
|
|
53
|
+
|
|
54
|
+
partition_int(pc, %w[shard_total total], 1)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Processes per CI matrix job for +ci-shard-run+ / +ci-shard-rspec+ (NxM: N jobs × M processes).
|
|
58
|
+
# +POLYRUN_SHARD_PROCESSES+ or +partition.shard_processes+; CLI +--shard-processes+ / +--workers+ overrides.
|
|
59
|
+
def resolve_shard_processes(pc, env = ENV)
|
|
60
|
+
return Integer(env["POLYRUN_SHARD_PROCESSES"]) if env["POLYRUN_SHARD_PROCESSES"] && !env["POLYRUN_SHARD_PROCESSES"].empty?
|
|
61
|
+
|
|
62
|
+
partition_int(pc, %w[shard_processes shard_workers workers_per_shard], 1)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# +cli_val+ is an override (e.g. +run-shards --timing-granularity+); +nil+ uses YAML then +POLYRUN_TIMING_GRANULARITY+.
|
|
66
|
+
def resolve_partition_timing_granularity(pc, cli_val, env = ENV)
|
|
67
|
+
raw = cli_val
|
|
68
|
+
raw ||= pc && (pc["timing_granularity"] || pc[:timing_granularity])
|
|
69
|
+
raw ||= env["POLYRUN_TIMING_GRANULARITY"]
|
|
70
|
+
Polyrun::Partition::TimingKeys.normalize_granularity(raw || "file")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parallel_worker_count_default(env = ENV)
|
|
74
|
+
env_int("POLYRUN_WORKERS", Polyrun::Config::DEFAULT_PARALLEL_WORKERS, env)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/polyrun/config.rb
CHANGED
|
@@ -5,6 +5,10 @@ module Polyrun
|
|
|
5
5
|
class Config
|
|
6
6
|
DEFAULT_FILENAMES = %w[polyrun.yml config/polyrun.yml].freeze
|
|
7
7
|
|
|
8
|
+
# Parallel worker defaults (+run-shards+, +POLYRUN_WORKERS+); single source with {Resolver} and {Effective}.
|
|
9
|
+
DEFAULT_PARALLEL_WORKERS = 5
|
|
10
|
+
MAX_PARALLEL_WORKERS = 10
|
|
11
|
+
|
|
8
12
|
attr_reader :path, :raw
|
|
9
13
|
|
|
10
14
|
def self.load(path: nil)
|
|
@@ -59,3 +63,6 @@ module Polyrun
|
|
|
59
63
|
end
|
|
60
64
|
end
|
|
61
65
|
end
|
|
66
|
+
|
|
67
|
+
require_relative "config/resolver"
|
|
68
|
+
require_relative "config/effective"
|