polyrun 1.3.0 → 1.4.1
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 +16 -0
- data/README.md +58 -0
- data/lib/polyrun/cli/ci_shard_hooks.rb +121 -0
- data/lib/polyrun/cli/ci_shard_run_command.rb +6 -22
- data/lib/polyrun/cli/failure_commands.rb +92 -0
- data/lib/polyrun/cli/help.rb +4 -1
- data/lib/polyrun/cli/hooks_command.rb +97 -0
- data/lib/polyrun/cli/run_shards_command.rb +4 -1
- data/lib/polyrun/cli/run_shards_parallel_children.rb +99 -0
- data/lib/polyrun/cli/run_shards_plan_boot_phases.rb +37 -2
- data/lib/polyrun/cli/run_shards_plan_options.rb +10 -2
- data/lib/polyrun/cli/run_shards_run.rb +73 -64
- data/lib/polyrun/cli.rb +8 -2
- data/lib/polyrun/config.rb +10 -0
- data/lib/polyrun/hooks/dsl.rb +128 -0
- data/lib/polyrun/hooks/worker_runner.rb +27 -0
- data/lib/polyrun/hooks/worker_shell.rb +50 -0
- data/lib/polyrun/hooks.rb +185 -0
- data/lib/polyrun/reporting/failure_merge.rb +135 -0
- data/lib/polyrun/reporting/rspec_failure_fragment_formatter.rb +95 -0
- data/lib/polyrun/rspec.rb +14 -0
- data/lib/polyrun/version.rb +1 -1
- data/lib/polyrun.rb +1 -0
- metadata +11 -1
|
@@ -31,7 +31,7 @@ module Polyrun
|
|
|
31
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
|
-
run_shards_plan_ready_log(o, strategy, cmd, paths_source, items.size)
|
|
34
|
+
run_shards_plan_ready_log(o, cfg, strategy, cmd, paths_source, items.size)
|
|
35
35
|
|
|
36
36
|
constraints = load_partition_constraints(pc, o[:constraints_path])
|
|
37
37
|
plan = run_shards_make_plan(items, o[:workers], strategy, o[:seed], costs, constraints, o[:timing_granularity])
|
|
@@ -59,12 +59,13 @@ module Polyrun
|
|
|
59
59
|
[run_t0, head, cmd, cfg, cfg.partition]
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
def run_shards_plan_ready_log(o, strategy, cmd, paths_source, item_count)
|
|
62
|
+
def run_shards_plan_ready_log(o, cfg, strategy, cmd, paths_source, item_count)
|
|
63
63
|
Polyrun::Debug.log_kv(
|
|
64
64
|
run_shards: "ready to partition",
|
|
65
65
|
workers: o[:workers],
|
|
66
66
|
strategy: strategy,
|
|
67
67
|
merge_coverage: o[:merge_coverage],
|
|
68
|
+
merge_failures: run_shards_merge_failures_flag(o, cfg),
|
|
68
69
|
command: cmd,
|
|
69
70
|
timing_path: o[:timing_path],
|
|
70
71
|
paths_source: paths_source,
|
|
@@ -72,6 +73,37 @@ module Polyrun
|
|
|
72
73
|
)
|
|
73
74
|
end
|
|
74
75
|
|
|
76
|
+
def run_shards_merge_failures_flag(o, cfg)
|
|
77
|
+
return true if o[:merge_failures]
|
|
78
|
+
return true if %w[1 true yes].include?(ENV["POLYRUN_MERGE_FAILURES"].to_s.downcase)
|
|
79
|
+
|
|
80
|
+
rep = cfg.reporting
|
|
81
|
+
v = rep["merge_failures"] || rep[:merge_failures]
|
|
82
|
+
v == true || %w[1 true yes].include?(v.to_s.downcase)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def run_shards_merge_failures_output_opt(o, cfg)
|
|
86
|
+
x = o[:merge_failures_output]
|
|
87
|
+
return x if x && !x.to_s.strip.empty?
|
|
88
|
+
|
|
89
|
+
x = ENV["POLYRUN_MERGED_FAILURES_OUT"]
|
|
90
|
+
return x if x && !x.to_s.strip.empty?
|
|
91
|
+
|
|
92
|
+
rep = cfg.reporting
|
|
93
|
+
rep["merge_failures_output"] || rep[:merge_failures_output]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def run_shards_merge_failures_format_opt(o, cfg)
|
|
97
|
+
x = o[:merge_failures_format]
|
|
98
|
+
return x if x && !x.to_s.strip.empty?
|
|
99
|
+
|
|
100
|
+
x = ENV["POLYRUN_MERGED_FAILURES_FORMAT"]
|
|
101
|
+
return x if x && !x.to_s.strip.empty?
|
|
102
|
+
|
|
103
|
+
rep = cfg.reporting
|
|
104
|
+
rep["merge_failures_format"] || rep[:merge_failures_format]
|
|
105
|
+
end
|
|
106
|
+
|
|
75
107
|
def run_shards_plan_context_hash(o, cmd, cfg, plan, run_t0, parallel, config_path)
|
|
76
108
|
{
|
|
77
109
|
workers: o[:workers],
|
|
@@ -83,6 +115,9 @@ module Polyrun
|
|
|
83
115
|
merge_coverage: o[:merge_coverage],
|
|
84
116
|
merge_output: o[:merge_output],
|
|
85
117
|
merge_format: o[:merge_format],
|
|
118
|
+
merge_failures: run_shards_merge_failures_flag(o, cfg),
|
|
119
|
+
merge_failures_output: run_shards_merge_failures_output_opt(o, cfg),
|
|
120
|
+
merge_failures_format: run_shards_merge_failures_format_opt(o, cfg),
|
|
86
121
|
config_path: config_path
|
|
87
122
|
}
|
|
88
123
|
end
|
|
@@ -24,7 +24,10 @@ module Polyrun
|
|
|
24
24
|
timing_granularity: nil,
|
|
25
25
|
merge_coverage: false,
|
|
26
26
|
merge_output: nil,
|
|
27
|
-
merge_format: nil
|
|
27
|
+
merge_format: nil,
|
|
28
|
+
merge_failures: false,
|
|
29
|
+
merge_failures_output: nil,
|
|
30
|
+
merge_failures_format: nil
|
|
28
31
|
}
|
|
29
32
|
end
|
|
30
33
|
|
|
@@ -34,8 +37,9 @@ module Polyrun
|
|
|
34
37
|
end.parse!(head)
|
|
35
38
|
end
|
|
36
39
|
|
|
40
|
+
# rubocop:disable Metrics/AbcSize -- one argv block for run-shards
|
|
37
41
|
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...]"
|
|
42
|
+
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] [--merge-failures] [--merge-failures-output P] [--merge-failures-format jsonl|json] [--] <command> [args...]"
|
|
39
43
|
opts.on("--workers N", Integer) { |v| st[:workers] = v }
|
|
40
44
|
opts.on("--strategy NAME", String) { |v| st[:strategy] = v }
|
|
41
45
|
opts.on("--seed VAL") { |v| st[:seed] = v }
|
|
@@ -46,7 +50,11 @@ module Polyrun
|
|
|
46
50
|
opts.on("--merge-coverage", "After success, merge coverage/polyrun-fragment-*.json (Polyrun coverage must be enabled)") { st[:merge_coverage] = true }
|
|
47
51
|
opts.on("--merge-output PATH", String) { |v| st[:merge_output] = v }
|
|
48
52
|
opts.on("--merge-format LIST", String) { |v| st[:merge_format] = v }
|
|
53
|
+
opts.on("--merge-failures", "After all workers exit, merge tmp/polyrun_failures/polyrun-failure-fragment-*.jsonl (use Polyrun::RSpec.install_failure_fragments!)") { st[:merge_failures] = true }
|
|
54
|
+
opts.on("--merge-failures-output PATH", String) { |v| st[:merge_failures_output] = v }
|
|
55
|
+
opts.on("--merge-failures-format VAL", "jsonl (default) or json") { |v| st[:merge_failures_format] = v }
|
|
49
56
|
end
|
|
57
|
+
# rubocop:enable Metrics/AbcSize
|
|
50
58
|
end
|
|
51
59
|
end
|
|
52
60
|
end
|
|
@@ -2,12 +2,14 @@ require "shellwords"
|
|
|
2
2
|
require "rbconfig"
|
|
3
3
|
|
|
4
4
|
require_relative "run_shards_planning"
|
|
5
|
+
require_relative "run_shards_parallel_children"
|
|
5
6
|
|
|
6
7
|
module Polyrun
|
|
7
8
|
class CLI
|
|
8
9
|
# Partition + spawn workers for `polyrun run-shards` (keeps {RunShardsCommand} file small).
|
|
9
10
|
module RunShardsRun
|
|
10
11
|
include RunShardsPlanning
|
|
12
|
+
include RunShardsParallelChildren
|
|
11
13
|
|
|
12
14
|
private
|
|
13
15
|
|
|
@@ -18,59 +20,84 @@ module Polyrun
|
|
|
18
20
|
run_shards_workers_and_merge(ctx)
|
|
19
21
|
end
|
|
20
22
|
|
|
23
|
+
# rubocop:disable Metrics/AbcSize -- orchestration: hooks, merge, worker failures
|
|
21
24
|
def run_shards_workers_and_merge(ctx)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
hook_cfg = Polyrun::Hooks.from_config(ctx[:cfg])
|
|
26
|
+
suite_started = false
|
|
27
|
+
exit_code = 1
|
|
28
|
+
merged_failures_path = nil
|
|
29
|
+
merge_failures_errored = false
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
env_suite = ENV.to_h.merge(
|
|
33
|
+
"POLYRUN_HOOK_ORCHESTRATOR" => "1",
|
|
34
|
+
"POLYRUN_SHARD_TOTAL" => ctx[:workers].to_s
|
|
35
|
+
)
|
|
36
|
+
code = hook_cfg.run_phase_if_enabled(:before_suite, env_suite)
|
|
37
|
+
return code if code != 0
|
|
38
|
+
|
|
39
|
+
suite_started = true
|
|
40
|
+
|
|
41
|
+
pids, spawn_err = run_shards_spawn_workers(ctx, hook_cfg)
|
|
42
|
+
if spawn_err
|
|
43
|
+
exit_code = spawn_err
|
|
44
|
+
return spawn_err
|
|
45
|
+
end
|
|
46
|
+
if pids.empty?
|
|
47
|
+
exit_code = 1
|
|
48
|
+
return 1
|
|
49
|
+
end
|
|
26
50
|
|
|
27
|
-
|
|
28
|
-
failed = shard_results.reject { |r| r[:success] }.map { |r| r[:shard] }
|
|
51
|
+
run_shards_warn_interleaved(ctx[:parallel], pids.size)
|
|
29
52
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC) - ctx[:run_t0]
|
|
33
|
-
))
|
|
53
|
+
shard_results, wait_hook_err = run_shards_wait_all_children(pids, hook_cfg, ctx)
|
|
54
|
+
failed = shard_results.reject { |r| r[:success] }.map { |r| r[:shard] }
|
|
34
55
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
Polyrun::Debug.log(format(
|
|
57
|
+
"run-shards: workers wall time since start: %.3fs",
|
|
58
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - ctx[:run_t0]
|
|
59
|
+
))
|
|
38
60
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
run_shards_merge_or_hint_coverage(ctx)
|
|
45
|
-
end
|
|
61
|
+
if ctx[:parallel]
|
|
62
|
+
Polyrun::Log.warn "polyrun run-shards: finished #{pids.size} worker(s)" + (failed.any? ? " (some failed)" : " (exit 0)")
|
|
63
|
+
end
|
|
46
64
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
mt = ctx[:matrix_shard_total]
|
|
55
|
-
|
|
56
|
-
pids = []
|
|
57
|
-
workers.times do |shard|
|
|
58
|
-
paths = plan.shard(shard)
|
|
59
|
-
if paths.empty?
|
|
60
|
-
Polyrun::Log.warn "polyrun run-shards: shard #{shard} skipped (no paths)" if @verbose || parallel
|
|
61
|
-
next
|
|
65
|
+
if ctx[:merge_failures]
|
|
66
|
+
begin
|
|
67
|
+
merged_failures_path = merge_failures_after_shards(ctx)
|
|
68
|
+
rescue Polyrun::Error => e
|
|
69
|
+
Polyrun::Log.warn e.message.to_s
|
|
70
|
+
merge_failures_errored = true
|
|
71
|
+
end
|
|
62
72
|
end
|
|
63
73
|
|
|
64
|
-
|
|
74
|
+
if failed.any?
|
|
75
|
+
run_shards_log_failed_reruns(
|
|
76
|
+
failed, shard_results, ctx[:plan], ctx[:parallel], ctx[:workers], ctx[:cmd],
|
|
77
|
+
merge_failures: ctx[:merge_failures]
|
|
78
|
+
)
|
|
79
|
+
exit_code = 1
|
|
80
|
+
exit_code = 1 if wait_hook_err != 0
|
|
81
|
+
return exit_code
|
|
82
|
+
end
|
|
65
83
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
exit_code = run_shards_merge_or_hint_coverage(ctx)
|
|
85
|
+
exit_code = 1 if wait_hook_err != 0 && exit_code == 0
|
|
86
|
+
exit_code = 1 if merge_failures_errored && exit_code == 0
|
|
87
|
+
exit_code
|
|
88
|
+
ensure
|
|
89
|
+
if suite_started
|
|
90
|
+
env_after = ENV.to_h.merge(
|
|
91
|
+
"POLYRUN_HOOK_ORCHESTRATOR" => "1",
|
|
92
|
+
"POLYRUN_SHARD_TOTAL" => ctx[:workers].to_s,
|
|
93
|
+
"POLYRUN_SUITE_EXIT_STATUS" => exit_code.to_s,
|
|
94
|
+
"POLYRUN_MERGED_FAILURES_PATH" => merged_failures_path.to_s
|
|
95
|
+
)
|
|
96
|
+
hook_cfg.run_phase_if_enabled(:after_suite, env_after)
|
|
97
|
+
end
|
|
71
98
|
end
|
|
72
|
-
pids
|
|
73
99
|
end
|
|
100
|
+
# rubocop:enable Metrics/AbcSize
|
|
74
101
|
|
|
75
102
|
def run_shards_warn_interleaved(parallel, pid_count)
|
|
76
103
|
return unless parallel && pid_count > 1
|
|
@@ -79,27 +106,6 @@ module Polyrun
|
|
|
79
106
|
Polyrun::Log.warn "polyrun run-shards: each worker prints its own summary line; the last \"N examples\" line is not a total across shards."
|
|
80
107
|
end
|
|
81
108
|
|
|
82
|
-
def run_shards_wait_all_children(pids)
|
|
83
|
-
shard_results = []
|
|
84
|
-
Polyrun::Debug.time("Process.wait (#{pids.size} worker process(es))") do
|
|
85
|
-
pids.each do |h|
|
|
86
|
-
Process.wait(h[:pid])
|
|
87
|
-
exitstatus = $?.exitstatus
|
|
88
|
-
ok = $?.success?
|
|
89
|
-
Polyrun::Debug.log("[parent pid=#{$$}] run-shards: Process.wait child_pid=#{h[:pid]} shard=#{h[:shard]} exit=#{exitstatus} success=#{ok}")
|
|
90
|
-
shard_results << {shard: h[:shard], exitstatus: exitstatus, success: ok}
|
|
91
|
-
end
|
|
92
|
-
rescue Interrupt
|
|
93
|
-
# Do not trap SIGINT: Process.wait raises Interrupt; a trap races and prints Interrupt + SystemExit traces.
|
|
94
|
-
run_shards_shutdown_on_signal!(pids, 130)
|
|
95
|
-
rescue SignalException => e
|
|
96
|
-
raise unless e.signm == "SIGTERM"
|
|
97
|
-
|
|
98
|
-
run_shards_shutdown_on_signal!(pids, 143)
|
|
99
|
-
end
|
|
100
|
-
shard_results
|
|
101
|
-
end
|
|
102
|
-
|
|
103
109
|
# Best-effort worker teardown then exit. Does not return.
|
|
104
110
|
def run_shards_shutdown_on_signal!(pids, code)
|
|
105
111
|
run_shards_terminate_children!(pids)
|
|
@@ -149,7 +155,7 @@ module Polyrun
|
|
|
149
155
|
0
|
|
150
156
|
end
|
|
151
157
|
|
|
152
|
-
def run_shards_log_failed_reruns(failed, shard_results, plan, parallel, workers, cmd)
|
|
158
|
+
def run_shards_log_failed_reruns(failed, shard_results, plan, parallel, workers, cmd, merge_failures: false)
|
|
153
159
|
exit_by_shard = shard_results.each_with_object({}) { |r, h| h[r[:shard]] = r[:exitstatus] }
|
|
154
160
|
failed_detail = failed.sort.map { |s| "#{s} (exit #{exit_by_shard[s]})" }.join(", ")
|
|
155
161
|
Polyrun::Log.warn "polyrun run-shards: failed shard(s): #{failed_detail}"
|
|
@@ -164,6 +170,9 @@ module Polyrun
|
|
|
164
170
|
rerun << Shellwords.join(cmd + paths)
|
|
165
171
|
Polyrun::Log.warn "polyrun run-shards: shard #{s} re-run (same spec list, no interleave): #{rerun}"
|
|
166
172
|
end
|
|
173
|
+
unless merge_failures
|
|
174
|
+
Polyrun::Log.warn "polyrun run-shards: one merged failure report — use run-shards --merge-failures with Polyrun::RSpec.install_failure_fragments!; POLYRUN_MERGED_FAILURES_PATH is set on after_suite when merge runs."
|
|
175
|
+
end
|
|
167
176
|
end
|
|
168
177
|
end
|
|
169
178
|
end
|
data/lib/polyrun/cli.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative "cli/ci_shard_run_parse"
|
|
|
16
16
|
require_relative "cli/ci_shard_run_command"
|
|
17
17
|
require_relative "cli/config_command"
|
|
18
18
|
require_relative "cli/default_run"
|
|
19
|
+
require_relative "cli/hooks_command"
|
|
19
20
|
require_relative "cli/help"
|
|
20
21
|
|
|
21
22
|
module Polyrun
|
|
@@ -27,9 +28,9 @@ module Polyrun
|
|
|
27
28
|
|
|
28
29
|
# Keep in sync with +dispatch_cli_command_subcommands+ (+when+ branches). Used for implicit path routing.
|
|
29
30
|
DISPATCH_SUBCOMMAND_NAMES = %w[
|
|
30
|
-
plan prepare merge-coverage report-coverage report-junit report-timing
|
|
31
|
+
plan prepare merge-coverage merge-failures report-coverage report-junit report-timing
|
|
31
32
|
env config merge-timing db:setup-template db:setup-shard db:clone-shards
|
|
32
|
-
run-shards parallel-rspec start build-paths init queue quick
|
|
33
|
+
run-shards parallel-rspec start build-paths init queue quick hook
|
|
33
34
|
].freeze
|
|
34
35
|
|
|
35
36
|
# First argv token that is a normal subcommand (not a path); if argv[0] is not here but looks like paths, run implicit parallel.
|
|
@@ -53,6 +54,7 @@ module Polyrun
|
|
|
53
54
|
include CiShardRunCommand
|
|
54
55
|
include ConfigCommand
|
|
55
56
|
include DefaultRun
|
|
57
|
+
include HooksCommand
|
|
56
58
|
include Help
|
|
57
59
|
|
|
58
60
|
def self.run(argv = ARGV)
|
|
@@ -143,6 +145,8 @@ module Polyrun
|
|
|
143
145
|
cmd_prepare(argv, config_path)
|
|
144
146
|
when "merge-coverage"
|
|
145
147
|
cmd_merge_coverage(argv, config_path)
|
|
148
|
+
when "merge-failures"
|
|
149
|
+
cmd_merge_failures(argv, config_path)
|
|
146
150
|
when "report-coverage"
|
|
147
151
|
cmd_report_coverage(argv)
|
|
148
152
|
when "report-junit"
|
|
@@ -177,6 +181,8 @@ module Polyrun
|
|
|
177
181
|
cmd_queue(argv)
|
|
178
182
|
when "quick"
|
|
179
183
|
cmd_quick(argv)
|
|
184
|
+
when "hook"
|
|
185
|
+
cmd_hook(argv, config_path)
|
|
180
186
|
else
|
|
181
187
|
Polyrun::Log.warn "unknown command: #{command}"
|
|
182
188
|
2
|
data/lib/polyrun/config.rb
CHANGED
|
@@ -61,6 +61,16 @@ module Polyrun
|
|
|
61
61
|
def version
|
|
62
62
|
raw["version"] || raw[:version]
|
|
63
63
|
end
|
|
64
|
+
|
|
65
|
+
# Optional +hooks:+ block for +run-shards+ / +parallel-rspec+ / +ci-shard-*+ (see {Hooks}).
|
|
66
|
+
def hooks
|
|
67
|
+
raw["hooks"] || raw[:hooks] || {}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Optional +reporting:+ block (merge-failures output paths, etc.).
|
|
71
|
+
def reporting
|
|
72
|
+
raw["reporting"] || raw[:reporting] || {}
|
|
73
|
+
end
|
|
64
74
|
end
|
|
65
75
|
end
|
|
66
76
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
require "monitor"
|
|
2
|
+
|
|
3
|
+
require_relative "../log"
|
|
4
|
+
|
|
5
|
+
module Polyrun
|
|
6
|
+
class Hooks
|
|
7
|
+
# Ruby DSL for +hooks.ruby+ / +hooks.ruby_file+ in +polyrun.yml+ (see README).
|
|
8
|
+
#
|
|
9
|
+
# Example file (+config/polyrun_hooks.rb+):
|
|
10
|
+
#
|
|
11
|
+
# before(:suite) { |env| puts env["POLYRUN_HOOK_PHASE"] }
|
|
12
|
+
# after(:each) { |env| }
|
|
13
|
+
#
|
|
14
|
+
# Blocks receive a hash with string keys (same env as shell hooks).
|
|
15
|
+
module Dsl
|
|
16
|
+
class Registry
|
|
17
|
+
def initialize
|
|
18
|
+
@phases = Hash.new { |h, k| h[k] = [] }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def add(phase, proc)
|
|
22
|
+
@phases[phase.to_sym] << proc
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param env [Hash] string-keyed env
|
|
26
|
+
def run(phase, env)
|
|
27
|
+
@phases[phase.to_sym].each { |pr| pr.call(env) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def any?(phase)
|
|
31
|
+
@phases[phase.to_sym].any?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def empty?
|
|
35
|
+
@phases.values.all?(&:empty?)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def worker_hooks?
|
|
39
|
+
any?(:before_worker) || any?(:after_worker)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Evaluates a hook file with +before+ / +after+ (+before(:suite)+, etc.).
|
|
44
|
+
class FileContext
|
|
45
|
+
def initialize(path)
|
|
46
|
+
@path = path
|
|
47
|
+
@registry = Registry.new
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
attr_reader :registry
|
|
51
|
+
|
|
52
|
+
def load_file
|
|
53
|
+
instance_eval(File.read(@path), @path)
|
|
54
|
+
@registry
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def before(sym, &block)
|
|
58
|
+
@registry.add(map_before(sym), block)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def after(sym, &block)
|
|
62
|
+
@registry.add(map_after(sym), block)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def map_before(sym)
|
|
68
|
+
case sym.to_sym
|
|
69
|
+
when :suite then :before_suite
|
|
70
|
+
when :all then :before_shard
|
|
71
|
+
when :each then :before_worker
|
|
72
|
+
else
|
|
73
|
+
raise ArgumentError, "hooks DSL: before(#{sym.inspect}) — use :suite, :all, or :each"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def map_after(sym)
|
|
78
|
+
case sym.to_sym
|
|
79
|
+
when :suite then :after_suite
|
|
80
|
+
when :all then :after_shard
|
|
81
|
+
when :each then :after_worker
|
|
82
|
+
else
|
|
83
|
+
raise ArgumentError, "hooks DSL: after(#{sym.inspect}) — use :suite, :all, or :each"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class << self
|
|
89
|
+
# @return [Registry, nil]
|
|
90
|
+
def load_registry(path)
|
|
91
|
+
return nil if path.nil? || path.to_s.strip.empty?
|
|
92
|
+
|
|
93
|
+
full = File.expand_path(path.to_s, Dir.pwd)
|
|
94
|
+
return nil unless File.file?(full)
|
|
95
|
+
|
|
96
|
+
mtime = File.mtime(full)
|
|
97
|
+
cache_mu.synchronize do
|
|
98
|
+
hit = registry_cache[full]
|
|
99
|
+
return hit[:registry] if hit && hit[:mtime] == mtime
|
|
100
|
+
|
|
101
|
+
reg = FileContext.new(full).load_file
|
|
102
|
+
registry_cache[full] = {mtime: mtime, registry: reg}
|
|
103
|
+
reg
|
|
104
|
+
end
|
|
105
|
+
rescue => e
|
|
106
|
+
Polyrun::Log.warn "polyrun hooks: failed to load #{full}: #{e.class}: #{e.message}"
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def clear_cache!
|
|
111
|
+
cache_mu.synchronize { registry_cache.clear }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable -- single-threaded hook load; Monitor protects cache
|
|
117
|
+
def cache_mu
|
|
118
|
+
@cache_mu ||= Monitor.new
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def registry_cache
|
|
122
|
+
@registry_cache ||= {}
|
|
123
|
+
end
|
|
124
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require_relative "../log"
|
|
2
|
+
|
|
3
|
+
module Polyrun
|
|
4
|
+
class Hooks
|
|
5
|
+
# Invoked by worker shell wrapper (+ruby -e+). Requires +POLYRUN_HOOKS_RUBY_FILE+.
|
|
6
|
+
module WorkerRunner
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# @return [Integer] exit code (0 on success)
|
|
10
|
+
def run!(phase)
|
|
11
|
+
phase = phase.to_sym
|
|
12
|
+
path = ENV["POLYRUN_HOOKS_RUBY_FILE"]
|
|
13
|
+
return 0 if path.nil? || path.empty?
|
|
14
|
+
|
|
15
|
+
registry = Dsl.load_registry(path)
|
|
16
|
+
return 0 if registry.nil? || !registry.any?(phase)
|
|
17
|
+
|
|
18
|
+
env = ENV.to_h
|
|
19
|
+
registry.run(phase, env)
|
|
20
|
+
0
|
|
21
|
+
rescue => e
|
|
22
|
+
Polyrun::Log.warn "polyrun hooks worker #{phase}: #{e.class}: #{e.message}"
|
|
23
|
+
1
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require "shellwords"
|
|
2
|
+
require "rbconfig"
|
|
3
|
+
|
|
4
|
+
module Polyrun
|
|
5
|
+
class Hooks
|
|
6
|
+
# Builds +sh -c+ script for worker processes (shell + Ruby +before_worker+ / +after_worker+).
|
|
7
|
+
module WorkerShell
|
|
8
|
+
# @param cmd [Array<String>] argv before paths
|
|
9
|
+
# @param paths [Array<String>]
|
|
10
|
+
# @return [String] shell script body for +sh -c+ (worker process)
|
|
11
|
+
# rubocop:disable Metrics/AbcSize -- shell + ruby worker hook branches
|
|
12
|
+
def build_worker_shell_script(cmd, paths)
|
|
13
|
+
main = Shellwords.join(cmd + paths)
|
|
14
|
+
rb = RbConfig.ruby
|
|
15
|
+
bw_shell = commands_for(:before_worker)
|
|
16
|
+
aw_shell = commands_for(:after_worker)
|
|
17
|
+
bw_ruby = ruby_registry&.any?(:before_worker)
|
|
18
|
+
aw_ruby = ruby_registry&.any?(:after_worker)
|
|
19
|
+
|
|
20
|
+
lines = []
|
|
21
|
+
lines << "export POLYRUN_HOOK_PHASE=before_worker"
|
|
22
|
+
if bw_ruby || bw_shell.any?
|
|
23
|
+
lines << "set -e"
|
|
24
|
+
lines << worker_ruby_line(rb, :before_worker) if bw_ruby
|
|
25
|
+
bw_shell.each { |c| lines << c }
|
|
26
|
+
end
|
|
27
|
+
lines << "set +e"
|
|
28
|
+
lines << main
|
|
29
|
+
lines << "ec=$?"
|
|
30
|
+
lines << "export POLYRUN_HOOK_PHASE=after_worker"
|
|
31
|
+
if aw_ruby || aw_shell.any?
|
|
32
|
+
lines << "set +e"
|
|
33
|
+
aw_shell.each { |c| lines << "( #{c} ) || true" }
|
|
34
|
+
lines << worker_ruby_line(rb, :after_worker, wrap_allow_fail: true) if aw_ruby
|
|
35
|
+
end
|
|
36
|
+
lines << "exit $ec"
|
|
37
|
+
lines.join("\n")
|
|
38
|
+
end
|
|
39
|
+
# rubocop:enable Metrics/AbcSize
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def worker_ruby_line(rb_exe, phase, wrap_allow_fail: false)
|
|
44
|
+
code = %(require "polyrun"; Polyrun::Hooks::WorkerRunner.run!(:#{phase}))
|
|
45
|
+
line = "#{rb_exe} -e #{Shellwords.escape(code)}"
|
|
46
|
+
wrap_allow_fail ? "( #{line} ) || true" : line
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|