polyrun 1.0.0 → 1.2.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 +35 -0
- data/README.md +23 -3
- data/docs/SETUP_PROFILE.md +1 -1
- data/lib/polyrun/cli/ci_shard_run_command.rb +65 -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 +19 -30
- data/lib/polyrun/cli/plan_command.rb +47 -17
- data/lib/polyrun/cli/prepare_command.rb +2 -3
- data/lib/polyrun/cli/prepare_recipe.rb +12 -7
- data/lib/polyrun/cli/queue_command.rb +17 -7
- data/lib/polyrun/cli/run_shards_command.rb +49 -3
- data/lib/polyrun/cli/run_shards_plan_boot_phases.rb +3 -3
- data/lib/polyrun/cli/run_shards_plan_options.rb +18 -11
- data/lib/polyrun/cli/run_shards_planning.rb +16 -12
- data/lib/polyrun/cli/start_bootstrap.rb +2 -6
- data/lib/polyrun/cli.rb +53 -47
- data/lib/polyrun/config/dotted_path.rb +21 -0
- data/lib/polyrun/config/effective.rb +71 -0
- data/lib/polyrun/config/resolver.rb +70 -0
- data/lib/polyrun/config.rb +7 -0
- data/lib/polyrun/database/provision.rb +12 -7
- data/lib/polyrun/partition/constraints.rb +15 -4
- data/lib/polyrun/partition/paths.rb +83 -2
- data/lib/polyrun/partition/plan.rb +38 -28
- data/lib/polyrun/partition/timing_keys.rb +85 -0
- data/lib/polyrun/prepare/assets.rb +12 -5
- data/lib/polyrun/process_stdio.rb +91 -0
- data/lib/polyrun/quick/runner.rb +26 -17
- data/lib/polyrun/rspec.rb +19 -0
- data/lib/polyrun/templates/POLYRUN.md +1 -1
- data/lib/polyrun/templates/ci_matrix.polyrun.yml +4 -1
- data/lib/polyrun/timing/merge.rb +2 -1
- data/lib/polyrun/timing/rspec_example_formatter.rb +53 -0
- data/lib/polyrun/version.rb +1 -1
- data/polyrun.gemspec +1 -1
- data/sig/polyrun/rspec.rbs +2 -0
- metadata +12 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
require_relative "../process_stdio"
|
|
2
2
|
|
|
3
3
|
module Polyrun
|
|
4
4
|
class CLI
|
|
@@ -22,8 +22,7 @@ module Polyrun
|
|
|
22
22
|
return [manifest, nil]
|
|
23
23
|
end
|
|
24
24
|
if custom && !custom.to_s.strip.empty?
|
|
25
|
-
|
|
26
|
-
prepare_log_stderr(err)
|
|
25
|
+
st = prepare_run_shell_inherit_stdio(child_env, custom.to_s, rails_root, silent: !@verbose)
|
|
27
26
|
unless st.success?
|
|
28
27
|
Polyrun::Log.warn "polyrun prepare: assets custom command failed (exit #{st.exitstatus})"
|
|
29
28
|
return [manifest, 1]
|
|
@@ -59,8 +58,7 @@ module Polyrun
|
|
|
59
58
|
return [manifest, nil]
|
|
60
59
|
end
|
|
61
60
|
lines.each_with_index do |line, i|
|
|
62
|
-
|
|
63
|
-
prepare_log_stderr(err)
|
|
61
|
+
st = prepare_run_shell_inherit_stdio(child_env, line, rails_root, silent: !@verbose)
|
|
64
62
|
unless st.success?
|
|
65
63
|
Polyrun::Log.warn "polyrun prepare: shell step #{i + 1} failed (exit #{st.exitstatus})"
|
|
66
64
|
return [manifest, 1]
|
|
@@ -69,8 +67,15 @@ module Polyrun
|
|
|
69
67
|
[manifest, nil]
|
|
70
68
|
end
|
|
71
69
|
|
|
72
|
-
def
|
|
73
|
-
Polyrun::
|
|
70
|
+
def prepare_run_shell_inherit_stdio(child_env, script, rails_root, silent: false)
|
|
71
|
+
Polyrun::ProcessStdio.inherit_stdio_spawn_wait(
|
|
72
|
+
child_env,
|
|
73
|
+
"sh",
|
|
74
|
+
"-c",
|
|
75
|
+
script.to_s,
|
|
76
|
+
chdir: rails_root,
|
|
77
|
+
silent: silent
|
|
78
|
+
)
|
|
74
79
|
end
|
|
75
80
|
end
|
|
76
81
|
end
|
|
@@ -11,6 +11,7 @@ module Polyrun
|
|
|
11
11
|
dir = ".polyrun-queue"
|
|
12
12
|
paths_file = nil
|
|
13
13
|
timing_path = nil
|
|
14
|
+
timing_granularity = nil
|
|
14
15
|
worker = ENV["USER"] || "worker"
|
|
15
16
|
batch = 5
|
|
16
17
|
lease_id = nil
|
|
@@ -19,7 +20,7 @@ module Polyrun
|
|
|
19
20
|
Polyrun::Debug.log("queue: subcommand=#{sub.inspect}")
|
|
20
21
|
case sub
|
|
21
22
|
when "init"
|
|
22
|
-
queue_cmd_init(argv, dir, paths_file, timing_path)
|
|
23
|
+
queue_cmd_init(argv, dir, paths_file, timing_path, timing_granularity)
|
|
23
24
|
when "claim"
|
|
24
25
|
queue_cmd_claim(argv, dir, worker, batch)
|
|
25
26
|
when "ack"
|
|
@@ -32,29 +33,38 @@ module Polyrun
|
|
|
32
33
|
end
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
def queue_cmd_init(argv, dir, paths_file, timing_path)
|
|
36
|
+
def queue_cmd_init(argv, dir, paths_file, timing_path, timing_granularity)
|
|
36
37
|
OptionParser.new do |opts|
|
|
37
|
-
opts.banner = "usage: polyrun queue init --paths-file P [--timing PATH] [--dir DIR]"
|
|
38
|
+
opts.banner = "usage: polyrun queue init --paths-file P [--timing PATH] [--timing-granularity VAL] [--dir DIR]"
|
|
38
39
|
opts.on("--dir PATH") { |v| dir = v }
|
|
39
40
|
opts.on("--paths-file PATH") { |v| paths_file = v }
|
|
40
41
|
opts.on("--timing PATH") { |v| timing_path = v }
|
|
42
|
+
opts.on("--timing-granularity VAL") { |v| timing_granularity = v }
|
|
41
43
|
end.parse!(argv)
|
|
42
44
|
unless paths_file
|
|
43
45
|
Polyrun::Log.warn "queue init: need --paths-file"
|
|
44
46
|
return 2
|
|
45
47
|
end
|
|
48
|
+
cfg = Polyrun::Config.load(path: ENV["POLYRUN_CONFIG"])
|
|
49
|
+
g = resolve_partition_timing_granularity(cfg.partition, timing_granularity)
|
|
46
50
|
items = Polyrun::Partition::Paths.read_lines(paths_file)
|
|
47
|
-
costs =
|
|
48
|
-
|
|
51
|
+
costs =
|
|
52
|
+
if timing_path
|
|
53
|
+
Polyrun::Partition::Plan.load_timing_costs(
|
|
54
|
+
File.expand_path(timing_path, Dir.pwd),
|
|
55
|
+
granularity: g
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
ordered = queue_init_ordered_items(items, costs, g)
|
|
49
59
|
Polyrun::Queue::FileStore.new(dir).init!(ordered)
|
|
50
60
|
Polyrun::Log.puts JSON.generate({"dir" => File.expand_path(dir), "count" => ordered.size})
|
|
51
61
|
0
|
|
52
62
|
end
|
|
53
63
|
|
|
54
|
-
def queue_init_ordered_items(items, costs)
|
|
64
|
+
def queue_init_ordered_items(items, costs, granularity = :file)
|
|
55
65
|
if costs && !costs.empty?
|
|
56
66
|
dw = costs.values.sum / costs.size.to_f
|
|
57
|
-
items.sort_by { |p| [-queue_weight_for(p, costs, dw), p] }
|
|
67
|
+
items.sort_by { |p| [-queue_weight_for(p, costs, dw, granularity: granularity), p] }
|
|
58
68
|
else
|
|
59
69
|
items.sort
|
|
60
70
|
end
|
|
@@ -12,9 +12,7 @@ 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
18
|
# {Coverage::Collector} writes coverage/polyrun-fragment-<shard>.json. Merge with merge-coverage.
|
|
@@ -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"])
|
|
@@ -25,16 +25,16 @@ 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
|
-
costs, strategy, err = run_shards_resolve_costs(o[:timing_path], o[:strategy])
|
|
31
|
+
costs, strategy, err = run_shards_resolve_costs(o[:timing_path], o[:strategy], o[:timing_granularity])
|
|
32
32
|
return [err, nil] if err
|
|
33
33
|
|
|
34
34
|
run_shards_plan_ready_log(o, strategy, cmd, paths_source, items.size)
|
|
35
35
|
|
|
36
36
|
constraints = load_partition_constraints(pc, o[:constraints_path])
|
|
37
|
-
plan = run_shards_make_plan(items, o[:workers], strategy, o[:seed], costs, constraints)
|
|
37
|
+
plan = run_shards_make_plan(items, o[:workers], strategy, o[:seed], costs, constraints, o[:timing_granularity])
|
|
38
38
|
|
|
39
39
|
run_shards_debug_shard_sizes(plan, o[:workers])
|
|
40
40
|
Polyrun::Log.warn "polyrun run-shards: #{items.size} paths → #{o[:workers]} workers (#{strategy})" if @verbose
|
|
@@ -9,17 +9,19 @@ module Polyrun
|
|
|
9
9
|
st = run_shards_plan_options_state(pc)
|
|
10
10
|
run_shards_plan_options_parse!(head, st)
|
|
11
11
|
st[:paths_file] ||= pc["paths_file"] || pc[:paths_file]
|
|
12
|
+
st[:timing_granularity] = resolve_partition_timing_granularity(pc, st[:timing_granularity])
|
|
12
13
|
st
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def run_shards_plan_options_state(pc)
|
|
16
17
|
{
|
|
17
|
-
workers: env_int("POLYRUN_WORKERS",
|
|
18
|
+
workers: env_int("POLYRUN_WORKERS", Polyrun::Config::DEFAULT_PARALLEL_WORKERS),
|
|
18
19
|
paths_file: nil,
|
|
19
20
|
strategy: (pc["strategy"] || pc[:strategy] || "round_robin").to_s,
|
|
20
21
|
seed: pc["seed"] || pc[:seed],
|
|
21
22
|
timing_path: nil,
|
|
22
23
|
constraints_path: nil,
|
|
24
|
+
timing_granularity: nil,
|
|
23
25
|
merge_coverage: false,
|
|
24
26
|
merge_output: nil,
|
|
25
27
|
merge_format: nil
|
|
@@ -28,18 +30,23 @@ module Polyrun
|
|
|
28
30
|
|
|
29
31
|
def run_shards_plan_options_parse!(head, st)
|
|
30
32
|
OptionParser.new do |opts|
|
|
31
|
-
opts
|
|
32
|
-
opts.on("--workers N", Integer) { |v| st[:workers] = v }
|
|
33
|
-
opts.on("--strategy NAME", String) { |v| st[:strategy] = v }
|
|
34
|
-
opts.on("--seed VAL") { |v| st[:seed] = v }
|
|
35
|
-
opts.on("--paths-file PATH", String) { |v| st[:paths_file] = v }
|
|
36
|
-
opts.on("--constraints PATH", String) { |v| st[:constraints_path] = v }
|
|
37
|
-
opts.on("--timing PATH", "merged polyrun_timing.json; implies cost_binpack unless hrw/cost") { |v| st[:timing_path] = v }
|
|
38
|
-
opts.on("--merge-coverage", "After success, merge coverage/polyrun-fragment-*.json (Polyrun coverage must be enabled)") { st[:merge_coverage] = true }
|
|
39
|
-
opts.on("--merge-output PATH", String) { |v| st[:merge_output] = v }
|
|
40
|
-
opts.on("--merge-format LIST", String) { |v| st[:merge_format] = v }
|
|
33
|
+
run_shards_plan_options_register!(opts, st)
|
|
41
34
|
end.parse!(head)
|
|
42
35
|
end
|
|
36
|
+
|
|
37
|
+
def run_shards_plan_options_register!(opts, st)
|
|
38
|
+
opts.banner = "usage: polyrun run-shards [--workers N] [--strategy NAME] [--paths-file P] [--timing P] [--timing-granularity VAL] [--constraints P] [--seed S] [--merge-coverage] [--merge-output P] [--merge-format LIST] [--] <command> [args...]"
|
|
39
|
+
opts.on("--workers N", Integer) { |v| st[:workers] = v }
|
|
40
|
+
opts.on("--strategy NAME", String) { |v| st[:strategy] = v }
|
|
41
|
+
opts.on("--seed VAL") { |v| st[:seed] = v }
|
|
42
|
+
opts.on("--paths-file PATH", String) { |v| st[:paths_file] = v }
|
|
43
|
+
opts.on("--constraints PATH", String) { |v| st[:constraints_path] = v }
|
|
44
|
+
opts.on("--timing PATH", "merged polyrun_timing.json; implies cost_binpack unless hrw/cost") { |v| st[:timing_path] = v }
|
|
45
|
+
opts.on("--timing-granularity VAL", "file (default) or example (experimental)") { |v| st[:timing_granularity] = v }
|
|
46
|
+
opts.on("--merge-coverage", "After success, merge coverage/polyrun-fragment-*.json (Polyrun coverage must be enabled)") { st[:merge_coverage] = true }
|
|
47
|
+
opts.on("--merge-output PATH", String) { |v| st[:merge_output] = v }
|
|
48
|
+
opts.on("--merge-format LIST", String) { |v| st[:merge_format] = v }
|
|
49
|
+
end
|
|
43
50
|
end
|
|
44
51
|
end
|
|
45
52
|
end
|
|
@@ -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,26 +53,29 @@ 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]
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
def run_shards_resolve_costs(timing_path, strategy)
|
|
73
|
+
def run_shards_resolve_costs(timing_path, strategy, timing_granularity)
|
|
74
74
|
if timing_path
|
|
75
|
-
costs = Polyrun::Partition::Plan.load_timing_costs(
|
|
75
|
+
costs = Polyrun::Partition::Plan.load_timing_costs(
|
|
76
|
+
File.expand_path(timing_path.to_s, Dir.pwd),
|
|
77
|
+
granularity: timing_granularity
|
|
78
|
+
)
|
|
76
79
|
if costs.empty?
|
|
77
80
|
Polyrun::Log.warn "polyrun run-shards: timing file missing or empty: #{timing_path}"
|
|
78
81
|
return [nil, nil, 2]
|
|
@@ -90,7 +93,7 @@ module Polyrun
|
|
|
90
93
|
end
|
|
91
94
|
end
|
|
92
95
|
|
|
93
|
-
def run_shards_make_plan(items, workers, strategy, seed, costs, constraints)
|
|
96
|
+
def run_shards_make_plan(items, workers, strategy, seed, costs, constraints, timing_granularity)
|
|
94
97
|
Polyrun::Debug.time("Partition::Plan.new (partition #{items.size} paths → #{workers} shards)") do
|
|
95
98
|
Polyrun::Partition::Plan.new(
|
|
96
99
|
items: items,
|
|
@@ -99,7 +102,8 @@ module Polyrun
|
|
|
99
102
|
seed: seed,
|
|
100
103
|
costs: costs,
|
|
101
104
|
constraints: constraints,
|
|
102
|
-
root: Dir.pwd
|
|
105
|
+
root: Dir.pwd,
|
|
106
|
+
timing_granularity: timing_granularity
|
|
103
107
|
)
|
|
104
108
|
end
|
|
105
109
|
end
|
|
@@ -115,7 +119,7 @@ module Polyrun
|
|
|
115
119
|
|
|
116
120
|
def run_shards_warn_parallel_banner(item_count, workers, strategy)
|
|
117
121
|
Polyrun::Log.warn <<~MSG
|
|
118
|
-
polyrun run-shards: #{item_count}
|
|
122
|
+
polyrun run-shards: #{item_count} path(s) -> #{workers} parallel worker processes (not Ruby threads); strategy=#{strategy}
|
|
119
123
|
(plain `bundle exec rspec` is one process; this command fans out.)
|
|
120
124
|
MSG
|
|
121
125
|
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,9 +12,30 @@ 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_command"
|
|
16
|
+
require_relative "cli/config_command"
|
|
17
|
+
require_relative "cli/default_run"
|
|
18
|
+
require_relative "cli/help"
|
|
15
19
|
|
|
16
20
|
module Polyrun
|
|
17
21
|
class CLI
|
|
22
|
+
CI_SHARD_COMMANDS = {
|
|
23
|
+
"ci-shard-run" => :cmd_ci_shard_run,
|
|
24
|
+
"ci-shard-rspec" => :cmd_ci_shard_rspec
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# Keep in sync with +dispatch_cli_command_subcommands+ (+when+ branches). Used for implicit path routing.
|
|
28
|
+
DISPATCH_SUBCOMMAND_NAMES = %w[
|
|
29
|
+
plan prepare merge-coverage report-coverage report-junit report-timing
|
|
30
|
+
env config merge-timing db:setup-template db:setup-shard db:clone-shards
|
|
31
|
+
run-shards parallel-rspec start build-paths init queue quick
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
# First argv token that is a normal subcommand (not a path); if argv[0] is not here but looks like paths, run implicit parallel.
|
|
35
|
+
IMPLICIT_PATH_EXCLUSION_TOKENS = (
|
|
36
|
+
DISPATCH_SUBCOMMAND_NAMES + CI_SHARD_COMMANDS.keys + %w[help version]
|
|
37
|
+
).freeze
|
|
38
|
+
|
|
18
39
|
include Helpers
|
|
19
40
|
include PlanCommand
|
|
20
41
|
include PrepareCommand
|
|
@@ -27,6 +48,10 @@ module Polyrun
|
|
|
27
48
|
include TimingCommand
|
|
28
49
|
include InitCommand
|
|
29
50
|
include QuickCommand
|
|
51
|
+
include CiShardRunCommand
|
|
52
|
+
include ConfigCommand
|
|
53
|
+
include DefaultRun
|
|
54
|
+
include Help
|
|
30
55
|
|
|
31
56
|
def self.run(argv = ARGV)
|
|
32
57
|
new.run(argv)
|
|
@@ -37,12 +62,30 @@ module Polyrun
|
|
|
37
62
|
config_path = parse_global_cli!(argv)
|
|
38
63
|
return config_path if config_path.is_a?(Integer)
|
|
39
64
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
65
|
+
if argv.empty?
|
|
66
|
+
Polyrun::Debug.log_kv(
|
|
67
|
+
command: "(default)",
|
|
68
|
+
cwd: Dir.pwd,
|
|
69
|
+
polyrun_config: config_path,
|
|
70
|
+
argv_rest: [],
|
|
71
|
+
verbose: @verbose
|
|
72
|
+
)
|
|
73
|
+
return dispatch_default_parallel!(config_path)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if implicit_parallel_run?(argv)
|
|
77
|
+
Polyrun::Debug.log_kv(
|
|
78
|
+
command: "(paths)",
|
|
79
|
+
cwd: Dir.pwd,
|
|
80
|
+
polyrun_config: config_path,
|
|
81
|
+
argv_rest: argv.dup,
|
|
82
|
+
verbose: @verbose
|
|
83
|
+
)
|
|
84
|
+
return dispatch_implicit_parallel_targets!(argv, config_path)
|
|
44
85
|
end
|
|
45
86
|
|
|
87
|
+
command = argv.shift
|
|
88
|
+
|
|
46
89
|
Polyrun::Debug.log_kv(
|
|
47
90
|
command: command,
|
|
48
91
|
cwd: Dir.pwd,
|
|
@@ -89,6 +132,7 @@ module Polyrun
|
|
|
89
132
|
end
|
|
90
133
|
end
|
|
91
134
|
|
|
135
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity -- explicit dispatch table
|
|
92
136
|
def dispatch_cli_command_subcommands(command, argv, config_path)
|
|
93
137
|
case command
|
|
94
138
|
when "plan"
|
|
@@ -105,6 +149,8 @@ module Polyrun
|
|
|
105
149
|
cmd_report_timing(argv)
|
|
106
150
|
when "env"
|
|
107
151
|
cmd_env(argv, config_path)
|
|
152
|
+
when "config"
|
|
153
|
+
cmd_config(argv, config_path)
|
|
108
154
|
when "merge-timing"
|
|
109
155
|
cmd_merge_timing(argv)
|
|
110
156
|
when "db:setup-template"
|
|
@@ -121,6 +167,8 @@ module Polyrun
|
|
|
121
167
|
cmd_start(argv, config_path)
|
|
122
168
|
when "build-paths"
|
|
123
169
|
cmd_build_paths(config_path)
|
|
170
|
+
when *CI_SHARD_COMMANDS.keys
|
|
171
|
+
send(CI_SHARD_COMMANDS.fetch(command), argv, config_path)
|
|
124
172
|
when "init"
|
|
125
173
|
cmd_init(argv, config_path)
|
|
126
174
|
when "queue"
|
|
@@ -132,49 +180,7 @@ module Polyrun
|
|
|
132
180
|
2
|
|
133
181
|
end
|
|
134
182
|
end
|
|
135
|
-
|
|
136
|
-
def print_help
|
|
137
|
-
Polyrun::Log.puts <<~HELP
|
|
138
|
-
usage: polyrun [global options] <command> [options]
|
|
139
|
-
|
|
140
|
-
global:
|
|
141
|
-
-c, --config PATH polyrun.yml path (or POLYRUN_CONFIG)
|
|
142
|
-
-v, --verbose
|
|
143
|
-
-h, --help
|
|
144
|
-
|
|
145
|
-
Trace timing (stderr): DEBUG=1 or POLYRUN_DEBUG=1
|
|
146
|
-
Branch coverage in JSON fragments: POLYRUN_COVERAGE_BRANCHES=1 (stdlib Coverage; merge-coverage merges branches)
|
|
147
|
-
polyrun quick coverage: POLYRUN_COVERAGE=1 or (config/polyrun_coverage.yml + POLYRUN_QUICK_COVERAGE=1); POLYRUN_COVERAGE_DISABLE=1 skips
|
|
148
|
-
Merge wall time (stderr): POLYRUN_PROFILE_MERGE=1 (or verbose / DEBUG)
|
|
149
|
-
Post-merge formats (run-shards): POLYRUN_MERGE_FORMATS (default: json,lcov,cobertura,console,html)
|
|
150
|
-
Skip optional script/build_spec_paths.rb before start: POLYRUN_SKIP_BUILD_SPEC_PATHS=1
|
|
151
|
-
Skip start auto-prepare / auto DB provision: POLYRUN_START_SKIP_PREPARE=1, POLYRUN_START_SKIP_DATABASES=1
|
|
152
|
-
Skip writing paths_file from partition.paths_build: POLYRUN_SKIP_PATHS_BUILD=1
|
|
153
|
-
Warn if merge-coverage wall time exceeds N seconds (default 10): POLYRUN_MERGE_SLOW_WARN_SECONDS (0 disables)
|
|
154
|
-
Parallel RSpec workers: POLYRUN_WORKERS default 5, max 10 (run-shards / parallel-rspec / start)
|
|
155
|
-
|
|
156
|
-
commands:
|
|
157
|
-
version print version
|
|
158
|
-
plan emit partition manifest JSON
|
|
159
|
-
prepare run prepare recipe: default | assets (optional prepare.command overrides bin/rails assets:precompile) | shell (prepare.command required)
|
|
160
|
-
merge-coverage merge SimpleCov JSON fragments (json/lcov/cobertura/console)
|
|
161
|
-
run-shards fan out N parallel OS processes (POLYRUN_SHARD_*; not Ruby threads); optional --merge-coverage
|
|
162
|
-
parallel-rspec run-shards + merge-coverage (defaults to: bundle exec rspec after --)
|
|
163
|
-
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
|
|
164
|
-
build-paths write partition.paths_file from partition.paths_build (same as auto step before plan/run-shards)
|
|
165
|
-
init write a starter polyrun.yml or POLYRUN.md from built-in templates (see docs/SETUP_PROFILE.md)
|
|
166
|
-
queue file-backed batch queue (init / claim / ack / status)
|
|
167
|
-
quick run Polyrun::Quick (describe/it, before/after, let, expect…to, assert_*; optional capybara!)
|
|
168
|
-
report-coverage write all coverage formats from one JSON file
|
|
169
|
-
report-junit RSpec JSON or Polyrun testcase JSON → JUnit XML (CI)
|
|
170
|
-
report-timing print slow-file summary from merged timing JSON
|
|
171
|
-
merge-timing merge polyrun_timing_*.json shards
|
|
172
|
-
env print shard + database env (see polyrun.yml databases)
|
|
173
|
-
db:setup-template migrate template DB (PostgreSQL)
|
|
174
|
-
db:setup-shard CREATE DATABASE shard FROM template (one POLYRUN_SHARD_INDEX)
|
|
175
|
-
db:clone-shards migrate templates + DROP/CREATE all shard DBs (replaces clone_shard shell scripts)
|
|
176
|
-
HELP
|
|
177
|
-
end
|
|
183
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
178
184
|
|
|
179
185
|
def cmd_version
|
|
180
186
|
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,71 @@
|
|
|
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+ / +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
|
+
"timing_granularity" => r.resolve_partition_timing_granularity(pc, nil, env).to_s
|
|
48
|
+
)
|
|
49
|
+
base["partition"] = part
|
|
50
|
+
|
|
51
|
+
base["workers"] = r.parallel_worker_count_default(env)
|
|
52
|
+
|
|
53
|
+
base
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def deep_stringify_keys(obj)
|
|
57
|
+
case obj
|
|
58
|
+
when Hash
|
|
59
|
+
obj.each_with_object({}) do |(k, v), m|
|
|
60
|
+
m[k.to_s] = deep_stringify_keys(v)
|
|
61
|
+
end
|
|
62
|
+
when Array
|
|
63
|
+
obj.map { |e| deep_stringify_keys(e) }
|
|
64
|
+
else
|
|
65
|
+
obj
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
# +cli_val+ is an override (e.g. +run-shards --timing-granularity+); +nil+ uses YAML then +POLYRUN_TIMING_GRANULARITY+.
|
|
58
|
+
def resolve_partition_timing_granularity(pc, cli_val, env = ENV)
|
|
59
|
+
raw = cli_val
|
|
60
|
+
raw ||= pc && (pc["timing_granularity"] || pc[:timing_granularity])
|
|
61
|
+
raw ||= env["POLYRUN_TIMING_GRANULARITY"]
|
|
62
|
+
Polyrun::Partition::TimingKeys.normalize_granularity(raw || "file")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parallel_worker_count_default(env = ENV)
|
|
66
|
+
env_int("POLYRUN_WORKERS", Polyrun::Config::DEFAULT_PARALLEL_WORKERS, env)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|