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