polyrun 1.2.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 +7 -0
- 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/help.rb +4 -4
- data/lib/polyrun/cli/plan_command.rb +22 -12
- data/lib/polyrun/cli/queue_command.rb +46 -19
- data/lib/polyrun/cli/run_shards_command.rb +13 -2
- data/lib/polyrun/cli/run_shards_run.rb +4 -2
- data/lib/polyrun/cli.rb +2 -0
- data/lib/polyrun/config/effective.rb +2 -1
- data/lib/polyrun/config/resolver.rb +8 -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/templates/ci_matrix.polyrun.yml +3 -2
- data/lib/polyrun/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3162ed760c231d4fa78ff396f55f708af13bda62808ba340f3c1494cf2dd97e
|
|
4
|
+
data.tar.gz: f401bd075462bafa14905c08a996fa46370025be9872f6a4d7aefbfdde251d2d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d0bf0e8ac88d3d0a007f53ce340c324bcd65c6397b4b67905d428d104ebff3078bc41220873b0b7d741e6cc65be06d5d103f8b8abd64e18ce0dc2d8f3e55bc1
|
|
7
|
+
data.tar.gz: 42906169eeeae14b3531d870359ce8fe9bd59261770553a6e7e6872e48e47b7b1013709eedea5c176a94ccee5c1e498551e8d6285ffc68b37c51d05169b184b1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 1.3.0 (2026-04-15)
|
|
4
|
+
|
|
5
|
+
- Add safe parsing for `ci-shard-run` / `ci-shard-rspec` `--shard-processes` and `--workers` (warn + exit 2 on missing or non-integer values).
|
|
6
|
+
- Fix `shard_child_env` when `matrix_total > 1` and `matrix_index` is nil: omit `POLYRUN_SHARD_MATRIX_*` and warn (avoid `Integer(nil)`).
|
|
7
|
+
- Document in `polyrun help` that `POLYRUN_SHARD_PROCESSES` and ci-shard `--workers` / `--shard-processes` are local processes per matrix job, distinct from `POLYRUN_WORKERS` / `run-shards`.
|
|
8
|
+
- BREAKING: Multi-worker shard runs may emit coverage JSON fragments whose basenames include `shard*` and `worker*` segments; `merge-coverage` still matches `polyrun-fragment-*.json`.
|
|
9
|
+
|
|
3
10
|
## 1.2.0 (2026-04-15)
|
|
4
11
|
|
|
5
12
|
- Add `polyrun config <dotted.path>` to print values from `Polyrun::Config::Effective` (same effective tree as runtime: arbitrary YAML paths, merged `prepare.env.<KEY>` as for `polyrun prepare`, resolved `partition.shard_index`, `partition.shard_total`, `partition.timing_granularity`, and `workers`).
|
|
@@ -6,6 +6,11 @@ module Polyrun
|
|
|
6
6
|
# workers on a single host. Runs +build-paths+, +plan+ for that shard, then +exec+ of a user command
|
|
7
7
|
# with that shard's paths appended (same argv pattern as +run-shards+ after +--+).
|
|
8
8
|
#
|
|
9
|
+
# With +--shard-processes M+ (or +partition.shard_processes+ / +POLYRUN_SHARD_PROCESSES+), fans out
|
|
10
|
+
# +M+ OS processes on this host, each running a subset of this shard's paths (NxM: +N+ matrix jobs × +M+
|
|
11
|
+
# processes). Child processes get local +POLYRUN_SHARD_INDEX+ / +POLYRUN_SHARD_TOTAL+ (+0..M-1+, +M+);
|
|
12
|
+
# when +N+ > 1, also +POLYRUN_SHARD_MATRIX_INDEX+ / +POLYRUN_SHARD_MATRIX_TOTAL+ for unique coverage fragments.
|
|
13
|
+
#
|
|
9
14
|
# After +--+, prefer **multiple argv tokens** (+bundle+, +exec+, +rspec+, …). A single token that
|
|
10
15
|
# contains spaces is split with +Shellwords+ (not a full shell); exotic quoting differs from +sh -c+.
|
|
11
16
|
module CiShardRunCommand
|
|
@@ -25,6 +30,60 @@ module Polyrun
|
|
|
25
30
|
[paths, 0]
|
|
26
31
|
end
|
|
27
32
|
|
|
33
|
+
def ci_shard_local_plan!(paths, workers)
|
|
34
|
+
Polyrun::Partition::Plan.new(
|
|
35
|
+
items: paths,
|
|
36
|
+
total_shards: workers,
|
|
37
|
+
strategy: "round_robin",
|
|
38
|
+
root: Dir.pwd
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# When +N+ > 1 and +M+ > 1, pass matrix index/total for coverage fragment names; else nil (see +shard_child_env+).
|
|
43
|
+
def ci_shard_matrix_context(pc, shard_processes)
|
|
44
|
+
n = resolve_shard_total(pc)
|
|
45
|
+
return [nil, nil] if n <= 1 || shard_processes <= 1
|
|
46
|
+
|
|
47
|
+
[resolve_shard_index(pc), n]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def ci_shard_run_fanout!(ctx)
|
|
51
|
+
pids = run_shards_spawn_workers(ctx)
|
|
52
|
+
return 1 if pids.empty?
|
|
53
|
+
|
|
54
|
+
run_shards_warn_interleaved(ctx[:parallel], pids.size)
|
|
55
|
+
shard_results = run_shards_wait_all_children(pids)
|
|
56
|
+
failed = shard_results.reject { |r| r[:success] }.map { |r| r[:shard] }
|
|
57
|
+
|
|
58
|
+
if failed.any?
|
|
59
|
+
Polyrun::Log.warn "polyrun ci-shard: finished #{pids.size} worker(s) (some failed)"
|
|
60
|
+
run_shards_log_failed_reruns(failed, shard_results, ctx[:plan], ctx[:parallel], ctx[:workers], ctx[:cmd])
|
|
61
|
+
return 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
Polyrun::Log.warn "polyrun ci-shard: finished #{pids.size} worker(s) (exit 0)"
|
|
65
|
+
0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ci_shard_fanout_context(cfg:, pc:, paths:, shard_processes:, cmd:, config_path:)
|
|
69
|
+
plan = ci_shard_local_plan!(paths, shard_processes)
|
|
70
|
+
mx, mt = ci_shard_matrix_context(pc, shard_processes)
|
|
71
|
+
{
|
|
72
|
+
workers: shard_processes,
|
|
73
|
+
cmd: cmd,
|
|
74
|
+
cfg: cfg,
|
|
75
|
+
plan: plan,
|
|
76
|
+
run_t0: Process.clock_gettime(Process::CLOCK_MONOTONIC),
|
|
77
|
+
parallel: true,
|
|
78
|
+
merge_coverage: false,
|
|
79
|
+
merge_output: nil,
|
|
80
|
+
merge_format: nil,
|
|
81
|
+
config_path: config_path,
|
|
82
|
+
matrix_shard_index: mx,
|
|
83
|
+
matrix_shard_total: mt
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
28
87
|
# Runner-agnostic matrix shard: +polyrun ci-shard-run [plan options] -- <command> [args...]+
|
|
29
88
|
# Paths for this shard are appended after the command (like +run-shards+).
|
|
30
89
|
def cmd_ci_shard_run(argv, config_path)
|
|
@@ -42,10 +101,27 @@ module Polyrun
|
|
|
42
101
|
end
|
|
43
102
|
cmd = Shellwords.split(cmd.first) if cmd.size == 1 && cmd.first.include?(" ")
|
|
44
103
|
|
|
104
|
+
cfg = Polyrun::Config.load(path: config_path || ENV["POLYRUN_CONFIG"])
|
|
105
|
+
pc = cfg.partition
|
|
106
|
+
shard_processes, perr = ci_shard_parse_shard_processes!(plan_argv, pc)
|
|
107
|
+
return perr if perr
|
|
108
|
+
|
|
109
|
+
shard_processes, err = ci_shard_normalize_shard_processes(shard_processes)
|
|
110
|
+
return err if err
|
|
111
|
+
|
|
45
112
|
paths, code = ci_shard_planned_paths!(plan_argv, config_path, command_label: "ci-shard-run")
|
|
46
113
|
return code if code != 0
|
|
47
114
|
|
|
48
|
-
|
|
115
|
+
if shard_processes <= 1
|
|
116
|
+
exec(*cmd, *paths)
|
|
117
|
+
return 0
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
ctx = ci_shard_fanout_context(
|
|
121
|
+
cfg: cfg, pc: pc, paths: paths, shard_processes: shard_processes, cmd: cmd, config_path: config_path
|
|
122
|
+
)
|
|
123
|
+
Polyrun::Log.warn "polyrun ci-shard-run: #{paths.size} path(s) → #{shard_processes} process(es) on this host (NxM: matrix jobs × local processes)"
|
|
124
|
+
ci_shard_run_fanout!(ctx)
|
|
49
125
|
end
|
|
50
126
|
|
|
51
127
|
# Same as +ci-shard-run -- bundle exec rspec+ with an optional second segment for RSpec-only flags:
|
|
@@ -55,10 +131,29 @@ module Polyrun
|
|
|
55
131
|
plan_argv = sep ? argv[0...sep] : argv
|
|
56
132
|
rspec_argv = sep ? argv[(sep + 1)..] : []
|
|
57
133
|
|
|
134
|
+
cfg = Polyrun::Config.load(path: config_path || ENV["POLYRUN_CONFIG"])
|
|
135
|
+
pc = cfg.partition
|
|
136
|
+
shard_processes, perr = ci_shard_parse_shard_processes!(plan_argv, pc)
|
|
137
|
+
return perr if perr
|
|
138
|
+
|
|
139
|
+
shard_processes, err = ci_shard_normalize_shard_processes(shard_processes)
|
|
140
|
+
return err if err
|
|
141
|
+
|
|
58
142
|
paths, code = ci_shard_planned_paths!(plan_argv, config_path, command_label: "ci-shard-rspec")
|
|
59
143
|
return code if code != 0
|
|
60
144
|
|
|
61
|
-
|
|
145
|
+
cmd = ["bundle", "exec", "rspec", *rspec_argv]
|
|
146
|
+
|
|
147
|
+
if shard_processes <= 1
|
|
148
|
+
exec(*cmd, *paths)
|
|
149
|
+
return 0
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
ctx = ci_shard_fanout_context(
|
|
153
|
+
cfg: cfg, pc: pc, paths: paths, shard_processes: shard_processes, cmd: cmd, config_path: config_path
|
|
154
|
+
)
|
|
155
|
+
Polyrun::Log.warn "polyrun ci-shard-rspec: #{paths.size} path(s) → #{shard_processes} process(es) on this host (NxM: matrix jobs × local processes)"
|
|
156
|
+
ci_shard_run_fanout!(ctx)
|
|
62
157
|
end
|
|
63
158
|
end
|
|
64
159
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Polyrun
|
|
2
|
+
class CLI
|
|
3
|
+
# Parsing for +ci-shard-run+ / +ci-shard-rspec+ plan argv (+--shard-processes+, +--workers+).
|
|
4
|
+
module CiShardRunParse
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
# Strips +--shard-processes+ / +--workers+ from +plan_argv+ and returns +[count, exit_code]+.
|
|
8
|
+
# +exit_code+ is +nil+ on success, +2+ on invalid or missing integer (no exception).
|
|
9
|
+
# Does not use +OptionParser+ so +plan+ flags (+--shard+, +--total+, …) pass through unchanged.
|
|
10
|
+
# Note: +--workers+ here means processes for this matrix job (+POLYRUN_SHARD_PROCESSES+), not +run-shards+ +POLYRUN_WORKERS+.
|
|
11
|
+
def ci_shard_parse_shard_processes!(plan_argv, pc)
|
|
12
|
+
workers = Polyrun::Config::Resolver.resolve_shard_processes(pc)
|
|
13
|
+
rest = []
|
|
14
|
+
i = 0
|
|
15
|
+
while i < plan_argv.size
|
|
16
|
+
case plan_argv[i]
|
|
17
|
+
when "--shard-processes"
|
|
18
|
+
n, err = ci_shard_parse_positive_int_flag!(plan_argv, i, "--shard-processes")
|
|
19
|
+
return [nil, err] if err
|
|
20
|
+
|
|
21
|
+
workers = n
|
|
22
|
+
i += 2
|
|
23
|
+
when "--workers"
|
|
24
|
+
n, err = ci_shard_parse_positive_int_flag!(plan_argv, i, "--workers")
|
|
25
|
+
return [nil, err] if err
|
|
26
|
+
|
|
27
|
+
workers = n
|
|
28
|
+
i += 2
|
|
29
|
+
else
|
|
30
|
+
rest << plan_argv[i]
|
|
31
|
+
i += 1
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
plan_argv.replace(rest)
|
|
35
|
+
[workers, nil]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Array(Integer or nil, Integer or nil)] +[value, exit_code]+ — +exit_code+ is +nil+ on success, +2+ on error
|
|
39
|
+
def ci_shard_parse_positive_int_flag!(argv, i, flag_name)
|
|
40
|
+
arg = argv[i + 1]
|
|
41
|
+
if arg.nil?
|
|
42
|
+
Polyrun::Log.warn "polyrun ci-shard: missing value for #{flag_name}"
|
|
43
|
+
return [nil, 2]
|
|
44
|
+
end
|
|
45
|
+
n = Integer(arg, exception: false)
|
|
46
|
+
if n.nil?
|
|
47
|
+
Polyrun::Log.warn "polyrun ci-shard: #{flag_name} must be an integer (got #{arg.inspect})"
|
|
48
|
+
return [nil, 2]
|
|
49
|
+
end
|
|
50
|
+
[n, nil]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Array(Integer, Integer, nil)] +[capped_workers, exit_code]+ — +exit_code+ is +nil+ when OK
|
|
54
|
+
def ci_shard_normalize_shard_processes(workers)
|
|
55
|
+
if workers < 1
|
|
56
|
+
Polyrun::Log.warn "polyrun ci-shard: --shard-processes / --workers must be >= 1"
|
|
57
|
+
return [workers, 2]
|
|
58
|
+
end
|
|
59
|
+
w = workers
|
|
60
|
+
if w > Polyrun::Config::MAX_PARALLEL_WORKERS
|
|
61
|
+
Polyrun::Log.warn "polyrun ci-shard: capping --shard-processes / --workers from #{w} to #{Polyrun::Config::MAX_PARALLEL_WORKERS}"
|
|
62
|
+
w = Polyrun::Config::MAX_PARALLEL_WORKERS
|
|
63
|
+
end
|
|
64
|
+
[w, nil]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/polyrun/cli/help.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Polyrun
|
|
|
21
21
|
Skip start auto-prepare / auto DB provision: POLYRUN_START_SKIP_PREPARE=1, POLYRUN_START_SKIP_DATABASES=1
|
|
22
22
|
Skip writing paths_file from partition.paths_build: POLYRUN_SKIP_PATHS_BUILD=1
|
|
23
23
|
Warn if merge-coverage wall time exceeds N seconds (default 10): POLYRUN_MERGE_SLOW_WARN_SECONDS (0 disables)
|
|
24
|
-
Parallel RSpec workers: POLYRUN_WORKERS default 5, max 10 (run-shards / parallel-rspec / start)
|
|
24
|
+
Parallel RSpec workers: POLYRUN_WORKERS default 5, max 10 (run-shards / parallel-rspec / start); distinct from POLYRUN_SHARD_PROCESSES / ci-shard --shard-processes (local processes per CI matrix job)
|
|
25
25
|
Partition timing granularity (default file): POLYRUN_TIMING_GRANULARITY=file|example (experimental per-example; see partition.timing_granularity)
|
|
26
26
|
|
|
27
27
|
commands:
|
|
@@ -32,11 +32,11 @@ module Polyrun
|
|
|
32
32
|
run-shards fan out N parallel OS processes (POLYRUN_SHARD_*; not Ruby threads); optional --merge-coverage
|
|
33
33
|
parallel-rspec run-shards + merge-coverage (defaults to: bundle exec rspec after --)
|
|
34
34
|
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
|
|
35
|
-
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 -- (
|
|
36
|
-
ci-shard-rspec same as ci-shard-run -- bundle exec rspec; optional -- [rspec-only flags]
|
|
35
|
+
ci-shard-run CI matrix: build-paths + plan for POLYRUN_SHARD_INDEX / POLYRUN_SHARD_TOTAL (or config), then run your command with that shard's paths after --; optional --shard-processes M or --workers M (POLYRUN_SHARD_PROCESSES; not POLYRUN_WORKERS) for N×M jobs × processes on this host
|
|
36
|
+
ci-shard-rspec same as ci-shard-run -- bundle exec rspec; optional --shard-processes / --workers / -- [rspec-only flags]
|
|
37
37
|
build-paths write partition.paths_file from partition.paths_build (same as auto step before plan/run-shards)
|
|
38
38
|
init write a starter polyrun.yml or POLYRUN.md from built-in templates (see docs/SETUP_PROFILE.md)
|
|
39
|
-
queue file-backed batch queue
|
|
39
|
+
queue file-backed batch queue: init (optional --shard/--total etc. as plan, then claim/ack); M workers share one dir; no duplicate paths across claims
|
|
40
40
|
quick run Polyrun::Quick (describe/it, before/after, let, expect…to, assert_*; optional capybara!)
|
|
41
41
|
report-coverage write all coverage formats from one JSON file
|
|
42
42
|
report-junit RSpec JSON or Polyrun testcase JSON → JUnit XML (CI)
|
|
@@ -76,21 +76,31 @@ module Polyrun
|
|
|
76
76
|
}
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
# Partition flags shared by +polyrun plan+ and +queue init+ (excluding +--paths-file+, which each command registers once).
|
|
80
|
+
def plan_command_register_partition_options!(opts, ctx)
|
|
81
|
+
opts.on("--shard INDEX", Integer) { |v| ctx[:shard] = v }
|
|
82
|
+
opts.on("--total N", Integer) { |v| ctx[:total] = v }
|
|
83
|
+
opts.on("--strategy NAME", String) { |v| ctx[:strategy] = v }
|
|
84
|
+
opts.on("--seed VAL") { |v| ctx[:seed] = v }
|
|
85
|
+
opts.on("--constraints PATH", "YAML: pin / serial_glob (see spec_queue.md)") { |v| ctx[:constraints_path] = v }
|
|
86
|
+
opts.on("--timing PATH", "path => seconds JSON; implies cost_binpack unless strategy is cost-based or hrw") do |v|
|
|
87
|
+
ctx[:timing_path] = v
|
|
88
|
+
end
|
|
89
|
+
opts.on("--timing-granularity VAL", "file (default) or example (experimental: path:line items)") do |v|
|
|
90
|
+
ctx[:timing_granularity] = v
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Shared by +polyrun plan+ and +queue init+ so partition flags match +Partition::Plan+ / +plan+ JSON.
|
|
95
|
+
def plan_command_register_options!(opts, ctx)
|
|
96
|
+
opts.on("--paths-file PATH", String) { |v| ctx[:paths_file] = v }
|
|
97
|
+
plan_command_register_partition_options!(opts, ctx)
|
|
98
|
+
end
|
|
99
|
+
|
|
79
100
|
def plan_command_parse_argv!(argv, ctx)
|
|
80
101
|
OptionParser.new do |opts|
|
|
81
102
|
opts.banner = "usage: polyrun plan [options] [--] [paths...]"
|
|
82
|
-
opts
|
|
83
|
-
opts.on("--total N", Integer) { |v| ctx[:total] = v }
|
|
84
|
-
opts.on("--strategy NAME", String) { |v| ctx[:strategy] = v }
|
|
85
|
-
opts.on("--seed VAL") { |v| ctx[:seed] = v }
|
|
86
|
-
opts.on("--paths-file PATH", String) { |v| ctx[:paths_file] = v }
|
|
87
|
-
opts.on("--constraints PATH", "YAML: pin / serial_glob (see spec_queue.md)") { |v| ctx[:constraints_path] = v }
|
|
88
|
-
opts.on("--timing PATH", "path => seconds JSON; implies cost_binpack unless strategy is cost-based or hrw") do |v|
|
|
89
|
-
ctx[:timing_path] = v
|
|
90
|
-
end
|
|
91
|
-
opts.on("--timing-granularity VAL", "file (default) or example (experimental: path:line items)") do |v|
|
|
92
|
-
ctx[:timing_granularity] = v
|
|
93
|
-
end
|
|
103
|
+
plan_command_register_options!(opts, ctx)
|
|
94
104
|
end.parse!(argv)
|
|
95
105
|
end
|
|
96
106
|
|
|
@@ -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
|
|
@@ -15,7 +15,7 @@ module Polyrun
|
|
|
15
15
|
# Default and upper bound for parallel OS processes (POLYRUN_WORKERS / --workers); see {Polyrun::Config}.
|
|
16
16
|
|
|
17
17
|
# Spawns N OS processes (not Ruby threads) with POLYRUN_SHARD_INDEX / POLYRUN_SHARD_TOTAL so
|
|
18
|
-
# {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.
|
|
19
19
|
def cmd_run_shards(argv, config_path)
|
|
20
20
|
run_shards_run!(argv, config_path)
|
|
21
21
|
end
|
|
@@ -112,10 +112,21 @@ module Polyrun
|
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
# ENV for a worker process: POLYRUN_SHARD_* plus per-shard database URLs from polyrun.yml or DATABASE_URL.
|
|
115
|
-
|
|
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)
|
|
116
118
|
child_env = ENV.to_h.merge(
|
|
117
119
|
Polyrun::Database::Shard.env_map(shard_index: shard, shard_total: workers)
|
|
118
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
|
|
119
130
|
dh = cfg.databases
|
|
120
131
|
if dh.is_a?(Hash) && !dh.empty?
|
|
121
132
|
child_env.merge!(Polyrun::Database::UrlBuilder.env_exports_for_databases(dh, shard_index: shard))
|
|
@@ -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
|
data/lib/polyrun/cli.rb
CHANGED
|
@@ -12,6 +12,7 @@ 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"
|
|
16
17
|
require_relative "cli/config_command"
|
|
17
18
|
require_relative "cli/default_run"
|
|
@@ -48,6 +49,7 @@ module Polyrun
|
|
|
48
49
|
include TimingCommand
|
|
49
50
|
include InitCommand
|
|
50
51
|
include QuickCommand
|
|
52
|
+
include CiShardRunParse
|
|
51
53
|
include CiShardRunCommand
|
|
52
54
|
include ConfigCommand
|
|
53
55
|
include DefaultRun
|
|
@@ -4,7 +4,7 @@ require_relative "resolver"
|
|
|
4
4
|
module Polyrun
|
|
5
5
|
class Config
|
|
6
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+,
|
|
7
|
+
# merged +prepare.env+, resolved +partition.shard_index+ / +shard_total+ / +shard_processes+ / +timing_granularity+,
|
|
8
8
|
# and top-level +workers+ (+POLYRUN_WORKERS+ default).
|
|
9
9
|
#
|
|
10
10
|
# +build+ memoizes the last (cfg, env) in-process so repeated +dig+ calls on the same load do not
|
|
@@ -44,6 +44,7 @@ module Polyrun
|
|
|
44
44
|
part = deep_stringify_keys(pc).merge(
|
|
45
45
|
"shard_index" => r.resolve_shard_index(pc, env),
|
|
46
46
|
"shard_total" => r.resolve_shard_total(pc, env),
|
|
47
|
+
"shard_processes" => r.resolve_shard_processes(pc, env),
|
|
47
48
|
"timing_granularity" => r.resolve_partition_timing_granularity(pc, nil, env).to_s
|
|
48
49
|
)
|
|
49
50
|
base["partition"] = part
|
|
@@ -54,6 +54,14 @@ module Polyrun
|
|
|
54
54
|
partition_int(pc, %w[shard_total total], 1)
|
|
55
55
|
end
|
|
56
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
|
+
|
|
57
65
|
# +cli_val+ is an override (e.g. +run-shards --timing-granularity+); +nil+ uses YAML then +POLYRUN_TIMING_GRANULARITY+.
|
|
58
66
|
def resolve_partition_timing_granularity(pc, cli_val, env = ENV)
|
|
59
67
|
raw = cli_val
|
|
@@ -8,6 +8,7 @@ require_relative "formatter"
|
|
|
8
8
|
require_relative "merge"
|
|
9
9
|
require_relative "result"
|
|
10
10
|
require_relative "track_files"
|
|
11
|
+
require_relative "collector_fragment_meta"
|
|
11
12
|
require_relative "../debug"
|
|
12
13
|
|
|
13
14
|
module Polyrun
|
|
@@ -24,7 +25,7 @@ module Polyrun
|
|
|
24
25
|
|
|
25
26
|
# @param root [String] project root (absolute or relative)
|
|
26
27
|
# @param reject_patterns [Array<String>] path substrings to drop (like SimpleCov add_filter)
|
|
27
|
-
# @param output_path [String, nil] default
|
|
28
|
+
# @param output_path [String, nil] default see {.fragment_default_basename_from_env}
|
|
28
29
|
# @param minimum_line_percent [Float, nil] exit 1 if below (when strict)
|
|
29
30
|
# @param strict [Boolean] whether to exit non-zero on threshold failure (default true when minimum set)
|
|
30
31
|
# @param track_under [Array<String>] when +track_files+ is nil, only keep coverage keys under these dirs relative to +root+. Default +["lib"]+.
|
|
@@ -34,18 +35,25 @@ module Polyrun
|
|
|
34
35
|
# @param formatter [Object, nil] Object responding to +format(result, output_dir:, basename:)+ like SimpleCov formatters (e.g. {Formatter.multi} or {Formatter::MultiFormatter})
|
|
35
36
|
# @param report_output_dir [String, nil] directory for +formatter+ outputs (default +coverage/+ under +root+)
|
|
36
37
|
# @param report_basename [String] file prefix for formatter outputs (default +polyrun-coverage+)
|
|
38
|
+
# See {CollectorFragmentMeta.fragment_default_basename_from_env}.
|
|
39
|
+
def self.fragment_default_basename_from_env(env = ENV)
|
|
40
|
+
CollectorFragmentMeta.fragment_default_basename_from_env(env)
|
|
41
|
+
end
|
|
42
|
+
|
|
37
43
|
def start!(root:, reject_patterns: [], track_under: ["lib"], track_files: nil, groups: nil, output_path: nil, minimum_line_percent: nil, strict: nil, meta: {}, formatter: nil, report_output_dir: nil, report_basename: "polyrun-coverage")
|
|
38
44
|
return if disabled?
|
|
39
45
|
|
|
40
46
|
root = File.expand_path(root)
|
|
41
|
-
|
|
42
|
-
output_path ||= File.join(root, "coverage", "polyrun-fragment-#{
|
|
47
|
+
basename = fragment_default_basename_from_env
|
|
48
|
+
output_path ||= File.join(root, "coverage", "polyrun-fragment-#{basename}.json")
|
|
43
49
|
strict = if minimum_line_percent.nil?
|
|
44
50
|
false
|
|
45
51
|
else
|
|
46
52
|
strict.nil? || strict
|
|
47
53
|
end
|
|
48
54
|
|
|
55
|
+
fragment_meta = CollectorFragmentMeta.fragment_meta_from_env(basename)
|
|
56
|
+
|
|
49
57
|
@config = {
|
|
50
58
|
root: root,
|
|
51
59
|
track_under: Array(track_under).map(&:to_s),
|
|
@@ -59,7 +67,8 @@ module Polyrun
|
|
|
59
67
|
formatter: formatter,
|
|
60
68
|
report_output_dir: report_output_dir,
|
|
61
69
|
report_basename: report_basename,
|
|
62
|
-
shard_total_at_start: ENV["POLYRUN_SHARD_TOTAL"].to_i
|
|
70
|
+
shard_total_at_start: ENV["POLYRUN_SHARD_TOTAL"].to_i,
|
|
71
|
+
fragment_meta: fragment_meta
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
unless ::Coverage.running?
|
|
@@ -110,11 +119,7 @@ module Polyrun
|
|
|
110
119
|
end
|
|
111
120
|
|
|
112
121
|
def self.finish_debug_time_label
|
|
113
|
-
|
|
114
|
-
"worker pid=#{$$} shard=#{ENV.fetch("POLYRUN_SHARD_INDEX", "?")} Coverage::Collector.finish (write fragment)"
|
|
115
|
-
else
|
|
116
|
-
"Coverage::Collector.finish (write fragment)"
|
|
117
|
-
end
|
|
122
|
+
CollectorFragmentMeta.finish_debug_time_label
|
|
118
123
|
end
|
|
119
124
|
|
|
120
125
|
def build_meta(cfg)
|
|
@@ -123,6 +128,7 @@ module Polyrun
|
|
|
123
128
|
m["timestamp"] ||= Time.now.to_i
|
|
124
129
|
m["command_name"] ||= "rspec"
|
|
125
130
|
m["polyrun_coverage_root"] = cfg[:root].to_s
|
|
131
|
+
CollectorFragmentMeta.merge_fragment_meta!(m, cfg[:fragment_meta])
|
|
126
132
|
if cfg[:groups]
|
|
127
133
|
m["polyrun_coverage_groups"] = cfg[:groups].transform_keys(&:to_s).transform_values(&:to_s)
|
|
128
134
|
end
|
|
@@ -12,6 +12,8 @@ module Polyrun
|
|
|
12
12
|
collector_finish: "start",
|
|
13
13
|
polyrun_shard_index: ENV["POLYRUN_SHARD_INDEX"],
|
|
14
14
|
polyrun_shard_total: ENV["POLYRUN_SHARD_TOTAL"],
|
|
15
|
+
polyrun_shard_matrix_index: ENV["POLYRUN_SHARD_MATRIX_INDEX"],
|
|
16
|
+
polyrun_shard_matrix_total: ENV["POLYRUN_SHARD_MATRIX_TOTAL"],
|
|
15
17
|
output_path: cfg[:output_path]
|
|
16
18
|
)
|
|
17
19
|
Polyrun::Debug.time(Collector.finish_debug_time_label) do
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Polyrun
|
|
2
|
+
module Coverage
|
|
3
|
+
# Shard / worker naming for coverage JSON fragments (N×M CI vs run-shards).
|
|
4
|
+
module CollectorFragmentMeta
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# Default fragment basename (no extension) for +coverage/polyrun-fragment-<basename>.json+.
|
|
8
|
+
def fragment_default_basename_from_env(env = ENV)
|
|
9
|
+
local = env.fetch("POLYRUN_SHARD_INDEX", "0")
|
|
10
|
+
mt = env["POLYRUN_SHARD_MATRIX_TOTAL"].to_i
|
|
11
|
+
if mt > 1
|
|
12
|
+
mi = env.fetch("POLYRUN_SHARD_MATRIX_INDEX", "0")
|
|
13
|
+
"shard#{mi}-worker#{local}"
|
|
14
|
+
elsif env["POLYRUN_SHARD_TOTAL"].to_i > 1
|
|
15
|
+
"worker#{local}"
|
|
16
|
+
else
|
|
17
|
+
local
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def finish_debug_time_label
|
|
22
|
+
mt = ENV["POLYRUN_SHARD_MATRIX_TOTAL"].to_i
|
|
23
|
+
if mt > 1
|
|
24
|
+
"worker pid=#{$$} shard(matrix)=#{ENV.fetch("POLYRUN_SHARD_MATRIX_INDEX", "?")} worker(local)=#{ENV.fetch("POLYRUN_SHARD_INDEX", "?")} Coverage::Collector.finish (write fragment)"
|
|
25
|
+
elsif ENV["POLYRUN_SHARD_TOTAL"].to_i > 1
|
|
26
|
+
"worker pid=#{$$} worker=#{ENV.fetch("POLYRUN_SHARD_INDEX", "?")} Coverage::Collector.finish (write fragment)"
|
|
27
|
+
else
|
|
28
|
+
"Coverage::Collector.finish (write fragment)"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fragment_meta_from_env(basename)
|
|
33
|
+
mt = ENV["POLYRUN_SHARD_MATRIX_TOTAL"].to_i
|
|
34
|
+
{
|
|
35
|
+
basename: basename,
|
|
36
|
+
worker_index: ENV.fetch("POLYRUN_SHARD_INDEX", "0"),
|
|
37
|
+
shard_matrix_index: shard_matrix_index_value(mt)
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def shard_matrix_index_value(matrix_total)
|
|
42
|
+
return nil if matrix_total <= 1
|
|
43
|
+
|
|
44
|
+
ENV.fetch("POLYRUN_SHARD_MATRIX_INDEX", "0")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def merge_fragment_meta!(m, fm)
|
|
48
|
+
return m if fm.nil?
|
|
49
|
+
|
|
50
|
+
m["polyrun_fragment_basename"] = fm[:basename].to_s if fm[:basename]
|
|
51
|
+
m["polyrun_worker_index"] = fm[:worker_index].to_s if fm[:worker_index]
|
|
52
|
+
m["polyrun_shard_matrix_index"] = fm[:shard_matrix_index].to_s if fm[:shard_matrix_index]
|
|
53
|
+
m
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
# bundle exec polyrun -c polyrun.yml ci-shard-run -- bundle exec rspec
|
|
4
4
|
# (or ci-shard-rspec; or e.g. ci-shard-run -- bundle exec polyrun quick).
|
|
5
5
|
# Equivalent to build-paths, plan --shard/--total, then run that command with this slice's paths.
|
|
6
|
-
# A separate CI job downloads coverage/polyrun-fragment-*.json and runs merge-coverage.
|
|
7
|
-
#
|
|
6
|
+
# A separate CI job downloads coverage/polyrun-fragment-*.json (e.g. shard<S>-worker<W>.json per N×M process) and runs merge-coverage.
|
|
7
|
+
# For N×M (N matrix jobs × M processes per job): set shard_processes: M or POLYRUN_SHARD_PROCESSES,
|
|
8
|
+
# or pass --shard-processes M to ci-shard-run / ci-shard-rspec (local split is round-robin).
|
|
8
9
|
# See: docs/SETUP_PROFILE.md
|
|
9
10
|
|
|
10
11
|
partition:
|
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: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -167,6 +167,7 @@ files:
|
|
|
167
167
|
- lib/polyrun.rb
|
|
168
168
|
- lib/polyrun/cli.rb
|
|
169
169
|
- lib/polyrun/cli/ci_shard_run_command.rb
|
|
170
|
+
- lib/polyrun/cli/ci_shard_run_parse.rb
|
|
170
171
|
- lib/polyrun/cli/config_command.rb
|
|
171
172
|
- lib/polyrun/cli/coverage_commands.rb
|
|
172
173
|
- lib/polyrun/cli/coverage_merge_io.rb
|
|
@@ -196,6 +197,7 @@ files:
|
|
|
196
197
|
- lib/polyrun/coverage/cobertura_zero_lines.rb
|
|
197
198
|
- lib/polyrun/coverage/collector.rb
|
|
198
199
|
- lib/polyrun/coverage/collector_finish.rb
|
|
200
|
+
- lib/polyrun/coverage/collector_fragment_meta.rb
|
|
199
201
|
- lib/polyrun/coverage/filter.rb
|
|
200
202
|
- lib/polyrun/coverage/formatter.rb
|
|
201
203
|
- lib/polyrun/coverage/merge.rb
|