polyrun 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -1
- data/CONTRIBUTING.md +23 -0
- data/docs/SETUP_PROFILE.md +29 -0
- data/ext/polyrun_coverage_merge/coverage_merge.c +492 -0
- data/ext/polyrun_coverage_merge/extconf.rb +3 -0
- data/lib/polyrun/benchmark/profile.rb +191 -0
- data/lib/polyrun/benchmark/report.rb +87 -0
- data/lib/polyrun/cli/benchmark_commands.rb +66 -0
- data/lib/polyrun/cli/coverage_commands.rb +4 -4
- data/lib/polyrun/cli/coverage_merge_io.rb +30 -4
- data/lib/polyrun/cli/failure_commands.rb +10 -3
- data/lib/polyrun/cli/help.rb +14 -7
- data/lib/polyrun/cli/report_commands.rb +25 -11
- data/lib/polyrun/cli/run_queue_command.rb +5 -32
- data/lib/polyrun/cli/run_shards_parallel_children.rb +7 -0
- data/lib/polyrun/cli/run_shards_parallel_wait.rb +3 -0
- data/lib/polyrun/cli/run_shards_plan_options.rb +1 -1
- data/lib/polyrun/cli/run_shards_run.rb +1 -0
- data/lib/polyrun/cli/run_shards_worker_interrupt.rb +9 -1
- data/lib/polyrun/cli/spec_quality_commands.rb +16 -7
- data/lib/polyrun/cli.rb +8 -1
- data/lib/polyrun/coverage/example_diff.rb +127 -60
- data/lib/polyrun/coverage/example_diff_snapshot.rb +44 -0
- data/lib/polyrun/coverage/example_diff_track_filter.rb +51 -0
- data/lib/polyrun/coverage/file_stats_report.rb +36 -0
- data/lib/polyrun/coverage/formatter.rb +22 -1
- data/lib/polyrun/coverage/merge/formatters.rb +11 -3
- data/lib/polyrun/coverage/merge_merge_two.rb +112 -63
- data/lib/polyrun/coverage/merge_native.rb +37 -0
- data/lib/polyrun/coverage/reporting.rb +1 -1
- data/lib/polyrun/export/csv.rb +25 -0
- data/lib/polyrun/export/markdown.rb +47 -0
- data/lib/polyrun/hooks/shell_runner.rb +33 -0
- data/lib/polyrun/hooks.rb +4 -7
- data/lib/polyrun/partition/paths_build.rb +8 -0
- data/lib/polyrun/queue/worker_loop.rb +39 -0
- data/lib/polyrun/reporting/failure_merge.rb +48 -12
- data/lib/polyrun/reporting/junit.rb +8 -7
- data/lib/polyrun/reporting/junit_report.rb +87 -0
- data/lib/polyrun/rspec/example_debug.rb +158 -0
- data/lib/polyrun/rspec/example_debug_instrumentation.rb +73 -0
- data/lib/polyrun/rspec/sharded_formatter_compat.rb +57 -0
- data/lib/polyrun/rspec.rb +34 -0
- data/lib/polyrun/spec_quality/profile.rb +24 -12
- data/lib/polyrun/spec_quality/report.rb +93 -0
- data/lib/polyrun/spec_quality.rb +19 -11
- data/lib/polyrun/timing/summary.rb +55 -5
- data/lib/polyrun/version.rb +1 -1
- data/lib/polyrun/worker_output.rb +159 -0
- data/lib/polyrun/worker_output_forwarders.rb +90 -0
- data/polyrun.gemspec +8 -1
- data/sig/polyrun/rspec.rbs +10 -0
- data/sig/polyrun/worker_output.rbs +11 -0
- metadata +65 -2
|
@@ -3,6 +3,7 @@ require "json"
|
|
|
3
3
|
|
|
4
4
|
require_relative "merge"
|
|
5
5
|
require_relative "result"
|
|
6
|
+
require_relative "file_stats_report"
|
|
6
7
|
|
|
7
8
|
module Polyrun
|
|
8
9
|
module Coverage
|
|
@@ -26,8 +27,11 @@ module Polyrun
|
|
|
26
27
|
when :cobertura then CoberturaFormatter
|
|
27
28
|
when :console then ConsoleFormatter
|
|
28
29
|
when :html then HtmlFormatter
|
|
30
|
+
when :csv then CsvFormatter
|
|
31
|
+
when :markdown then MarkdownFormatter
|
|
29
32
|
else
|
|
30
|
-
raise ArgumentError,
|
|
33
|
+
raise ArgumentError,
|
|
34
|
+
"unknown coverage format: #{name.inspect} (expected :json, :lcov, :cobertura, :console, :html, :csv, :markdown)"
|
|
31
35
|
end
|
|
32
36
|
end
|
|
33
37
|
|
|
@@ -111,6 +115,23 @@ module Polyrun
|
|
|
111
115
|
{html: path}
|
|
112
116
|
end
|
|
113
117
|
end
|
|
118
|
+
|
|
119
|
+
class CsvFormatter < Base
|
|
120
|
+
def write_files(result, output_dir, basename)
|
|
121
|
+
path = File.join(output_dir, "#{basename}.csv")
|
|
122
|
+
File.write(path, FileStatsReport.emit_csv(result.coverage_blob))
|
|
123
|
+
{csv: path}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class MarkdownFormatter < Base
|
|
128
|
+
def write_files(result, output_dir, basename)
|
|
129
|
+
path = File.join(output_dir, "#{basename}.md")
|
|
130
|
+
title = (result.meta && result.meta["title"]) || (result.meta && result.meta[:title]) || "Polyrun coverage report"
|
|
131
|
+
File.write(path, FileStatsReport.emit_markdown(result.coverage_blob, title: title))
|
|
132
|
+
{markdown: path}
|
|
133
|
+
end
|
|
134
|
+
end
|
|
114
135
|
end
|
|
115
136
|
end
|
|
116
137
|
end
|
|
@@ -153,16 +153,24 @@ module Polyrun
|
|
|
153
153
|
# Per-file line stats for HTML and other formatters.
|
|
154
154
|
# Integer line counts for one file entry (for O(files x groups) group aggregation).
|
|
155
155
|
def line_counts(file_entry)
|
|
156
|
+
if native_acceleration?
|
|
157
|
+
MergeNative.line_counts(file_entry)
|
|
158
|
+
else
|
|
159
|
+
line_counts_ruby(file_entry)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def line_counts_ruby(file_entry)
|
|
156
164
|
line_arr = line_array_from_file_entry(file_entry)
|
|
157
165
|
return {relevant: 0, covered: 0} unless line_arr.is_a?(Array)
|
|
158
166
|
|
|
159
167
|
relevant = 0
|
|
160
168
|
covered = 0
|
|
161
|
-
line_arr.each do |
|
|
162
|
-
next if
|
|
169
|
+
line_arr.each do |hit|
|
|
170
|
+
next if hit.nil? || hit == "ignored"
|
|
163
171
|
|
|
164
172
|
relevant += 1
|
|
165
|
-
covered += 1 if
|
|
173
|
+
covered += 1 if hit.to_i > 0
|
|
166
174
|
end
|
|
167
175
|
{relevant: relevant, covered: covered}
|
|
168
176
|
end
|
|
@@ -4,114 +4,163 @@ module Polyrun
|
|
|
4
4
|
module_function
|
|
5
5
|
|
|
6
6
|
def merge_two(a, b)
|
|
7
|
-
|
|
7
|
+
if native_acceleration?
|
|
8
|
+
MergeNative.merge_two(a, b)
|
|
9
|
+
else
|
|
10
|
+
merge_two_ruby(a, b)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def merge_two_ruby(a, b)
|
|
15
|
+
a = {} if a.nil?
|
|
16
|
+
b = {} if b.nil?
|
|
17
|
+
if a.size >= b.size
|
|
18
|
+
merge_two_by_keys(a, b)
|
|
19
|
+
else
|
|
20
|
+
merge_two_by_keys(b, a)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def merge_two_by_keys(primary, secondary)
|
|
8
25
|
out = {}
|
|
9
|
-
|
|
10
|
-
out[path] = merge_file_entry(
|
|
26
|
+
primary.each do |path, entry|
|
|
27
|
+
out[path] = merge_file_entry(entry, secondary[path])
|
|
28
|
+
end
|
|
29
|
+
secondary.each do |path, entry|
|
|
30
|
+
out[path] = entry unless out.key?(path)
|
|
11
31
|
end
|
|
12
32
|
out
|
|
13
33
|
end
|
|
14
34
|
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
|
|
35
|
+
def native_merge_line_arrays?
|
|
36
|
+
native_acceleration?
|
|
37
|
+
end
|
|
18
38
|
|
|
19
|
-
|
|
39
|
+
def native_acceleration?
|
|
40
|
+
MergeNative.available?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def normalize_file_entry(value)
|
|
44
|
+
return nil if value.nil?
|
|
45
|
+
return {"lines" => value} if value.is_a?(Array)
|
|
46
|
+
|
|
47
|
+
value
|
|
20
48
|
end
|
|
21
49
|
|
|
22
50
|
def line_array_from_file_entry(file)
|
|
23
|
-
|
|
24
|
-
return nil unless
|
|
51
|
+
hash = normalize_file_entry(file)
|
|
52
|
+
return nil unless hash.is_a?(Hash)
|
|
25
53
|
|
|
26
|
-
|
|
54
|
+
hash["lines"] || hash[:lines]
|
|
27
55
|
end
|
|
28
56
|
|
|
29
|
-
def merge_file_entry(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return
|
|
33
|
-
return
|
|
57
|
+
def merge_file_entry(left, right)
|
|
58
|
+
left = normalize_file_entry(left)
|
|
59
|
+
right = normalize_file_entry(right)
|
|
60
|
+
return right if left.nil?
|
|
61
|
+
return left if right.nil?
|
|
34
62
|
|
|
35
|
-
lines = merge_line_arrays(
|
|
63
|
+
lines = merge_line_arrays(left["lines"] || left[:lines], right["lines"] || right[:lines])
|
|
36
64
|
entry = {"lines" => lines}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
entry["branches"] =
|
|
65
|
+
left_branches = left["branches"] || left[:branches]
|
|
66
|
+
right_branches = right["branches"] || right[:branches]
|
|
67
|
+
branches = merge_branch_arrays(left_branches, right_branches)
|
|
68
|
+
entry["branches"] = branches if branches
|
|
41
69
|
entry
|
|
42
70
|
end
|
|
43
71
|
|
|
44
|
-
def merge_line_arrays(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
def merge_line_arrays(left, right)
|
|
73
|
+
if native_acceleration?
|
|
74
|
+
MergeNative.merge_line_arrays(left, right)
|
|
75
|
+
else
|
|
76
|
+
merge_line_arrays_ruby(left, right)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def merge_line_arrays_ruby(left, right)
|
|
81
|
+
left ||= []
|
|
82
|
+
right ||= []
|
|
83
|
+
left_size = left.size
|
|
84
|
+
right_size = right.size
|
|
85
|
+
max_len = (left_size > right_size) ? left_size : right_size
|
|
50
86
|
out = Array.new(max_len)
|
|
51
|
-
|
|
52
|
-
while
|
|
53
|
-
out[
|
|
54
|
-
|
|
87
|
+
index = 0
|
|
88
|
+
while index < max_len
|
|
89
|
+
out[index] = merge_line_hits(left[index], right[index])
|
|
90
|
+
index += 1
|
|
55
91
|
end
|
|
56
92
|
out
|
|
57
93
|
end
|
|
58
94
|
|
|
59
|
-
def merge_line_hits(
|
|
60
|
-
return
|
|
61
|
-
return
|
|
62
|
-
return "ignored" if
|
|
95
|
+
def merge_line_hits(left, right)
|
|
96
|
+
return right if left.nil?
|
|
97
|
+
return left if right.nil?
|
|
98
|
+
return "ignored" if left == "ignored" || right == "ignored"
|
|
63
99
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return
|
|
100
|
+
left_i = line_hit_to_i(left)
|
|
101
|
+
right_i = line_hit_to_i(right)
|
|
102
|
+
return left_i + right_i if left_i && right_i
|
|
67
103
|
|
|
68
|
-
return
|
|
69
|
-
return
|
|
104
|
+
return right_i if left_i.nil? && right_i
|
|
105
|
+
return left_i if right_i.nil? && left_i
|
|
70
106
|
|
|
71
|
-
|
|
107
|
+
left
|
|
72
108
|
end
|
|
73
109
|
|
|
74
|
-
def line_hit_to_i(
|
|
75
|
-
case
|
|
76
|
-
when Integer then
|
|
110
|
+
def line_hit_to_i(value)
|
|
111
|
+
case value
|
|
112
|
+
when Integer then value
|
|
77
113
|
when nil then nil
|
|
78
114
|
else
|
|
79
|
-
Integer(
|
|
115
|
+
Integer(value, exception: false)
|
|
80
116
|
end
|
|
81
117
|
end
|
|
82
118
|
|
|
83
|
-
def merge_branch_arrays(
|
|
84
|
-
return nil if
|
|
85
|
-
return (
|
|
119
|
+
def merge_branch_arrays(left, right)
|
|
120
|
+
return nil if left.nil? && right.nil?
|
|
121
|
+
return (left || right).dup if left.nil? || right.nil?
|
|
86
122
|
|
|
87
123
|
index = {}
|
|
88
|
-
[
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
124
|
+
[left, right].each do |array|
|
|
125
|
+
array.each do |branch|
|
|
126
|
+
next unless branch.is_a?(Hash)
|
|
127
|
+
|
|
128
|
+
key = branch_key(branch)
|
|
129
|
+
existing = index[key]
|
|
130
|
+
index[key] =
|
|
93
131
|
if existing
|
|
94
|
-
merge_branch_entries(existing,
|
|
132
|
+
merge_branch_entries(existing, branch)
|
|
95
133
|
else
|
|
96
|
-
|
|
134
|
+
branch.dup
|
|
97
135
|
end
|
|
98
136
|
end
|
|
99
137
|
end
|
|
100
|
-
index.values.sort_by { |
|
|
138
|
+
index.values.sort_by { |branch| branch_key(branch) }
|
|
101
139
|
end
|
|
102
140
|
|
|
103
|
-
def branch_key(
|
|
104
|
-
|
|
105
|
-
[
|
|
141
|
+
def branch_key(branch)
|
|
142
|
+
hash = branch.is_a?(Hash) ? branch : {}
|
|
143
|
+
[hash["type"] || hash[:type], hash["start_line"] || hash[:start_line], hash["end_line"] || hash[:end_line]]
|
|
106
144
|
end
|
|
107
145
|
|
|
108
|
-
def
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
146
|
+
def merge_branch_arrays_for_native(left, right)
|
|
147
|
+
merge_branch_arrays(left, right)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def sort_branches_for_native(branches)
|
|
151
|
+
branches.sort_by { |branch| branch_key(branch) }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def merge_branch_entries(left, right)
|
|
155
|
+
out = left.is_a?(Hash) ? left.dup : {}
|
|
156
|
+
left_count = (left["coverage"] || left[:coverage]).to_i
|
|
157
|
+
right_count = (right["coverage"] || right[:coverage]).to_i
|
|
158
|
+
out["coverage"] = left_count + right_count
|
|
113
159
|
out
|
|
114
160
|
end
|
|
115
161
|
end
|
|
116
162
|
end
|
|
117
163
|
end
|
|
164
|
+
|
|
165
|
+
require_relative "merge_native"
|
|
166
|
+
Polyrun::Coverage::MergeNative.load!
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Polyrun
|
|
2
|
+
module Coverage
|
|
3
|
+
module MergeNative
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def load!
|
|
7
|
+
return true if available?
|
|
8
|
+
|
|
9
|
+
extension_dir = File.expand_path("../../../ext/polyrun_coverage_merge", __dir__)
|
|
10
|
+
if File.directory?(extension_dir)
|
|
11
|
+
$LOAD_PATH.unshift(extension_dir) unless $LOAD_PATH.include?(extension_dir)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require "polyrun_coverage_merge"
|
|
15
|
+
true
|
|
16
|
+
rescue LoadError
|
|
17
|
+
false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def available?
|
|
21
|
+
::Object.const_defined?(:PolyrunCoverageMerge)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def merge_line_arrays(left, right)
|
|
25
|
+
::PolyrunCoverageMerge.merge_line_arrays(left, right)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def merge_two(left, right)
|
|
29
|
+
::PolyrunCoverageMerge.merge_two(left, right)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def line_counts(file_entry)
|
|
33
|
+
::PolyrunCoverageMerge.line_counts(file_entry)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -9,7 +9,7 @@ module Polyrun
|
|
|
9
9
|
# Ready-to-use multi-format output (SimpleCov-compatible result blob), no extra gems.
|
|
10
10
|
# Pass +formatter:+ for multi-formatter composition ({Formatter::MultiFormatter}, custom classes).
|
|
11
11
|
module Reporting
|
|
12
|
-
DEFAULT_FORMATS = %w[json lcov cobertura console html].freeze
|
|
12
|
+
DEFAULT_FORMATS = %w[json lcov cobertura console html csv markdown].freeze
|
|
13
13
|
|
|
14
14
|
# Comma list for +merge-coverage+ / +run-shards --merge-coverage+ defaults (Codecov, Jenkins, HTML, etc.).
|
|
15
15
|
DEFAULT_MERGE_FORMAT_LIST = DEFAULT_FORMATS.join(",").freeze
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Polyrun
|
|
2
|
+
module Export
|
|
3
|
+
# RFC-style CSV field escaping for report exports.
|
|
4
|
+
module Csv
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def escape_field(value)
|
|
8
|
+
string = value.nil? ? "" : value.to_s
|
|
9
|
+
if string.match?(/[",\r\n]/)
|
|
10
|
+
"\"#{string.gsub('"', '""')}\""
|
|
11
|
+
else
|
|
12
|
+
string
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def generate(headers, rows)
|
|
17
|
+
lines = [headers.map { |header| escape_field(header) }.join(",")]
|
|
18
|
+
rows.each do |row|
|
|
19
|
+
lines << Array(row).map { |cell| escape_field(cell) }.join(",")
|
|
20
|
+
end
|
|
21
|
+
lines.join("\n") + "\n"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Polyrun
|
|
2
|
+
module Export
|
|
3
|
+
# GitHub-flavored markdown tables for report exports.
|
|
4
|
+
module Markdown
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def escape_cell(value)
|
|
8
|
+
value.nil? ? "" : value.to_s.gsub("|", "\\|").tr("\n", " ")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def heading(level, text)
|
|
12
|
+
level = level.to_i.clamp(1, 6)
|
|
13
|
+
"#{"#" * level} #{text}\n"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def table(headers, rows)
|
|
17
|
+
return "" if headers.nil? || headers.empty?
|
|
18
|
+
|
|
19
|
+
header_line = "| #{headers.map { |header| escape_cell(header) }.join(" | ")} |"
|
|
20
|
+
separator_line = "| #{headers.map { "---" }.join(" | ")} |"
|
|
21
|
+
body_lines = rows.map do |row|
|
|
22
|
+
"| #{Array(row).map { |cell| escape_cell(cell) }.join(" | ")} |"
|
|
23
|
+
end
|
|
24
|
+
([header_line, separator_line] + body_lines).join("\n") + "\n"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def document(title, sections)
|
|
28
|
+
lines = [heading(1, title), ""]
|
|
29
|
+
sections.each do |section|
|
|
30
|
+
heading_text = section[:heading] || section["heading"]
|
|
31
|
+
lines << heading(2, heading_text) if heading_text && !heading_text.to_s.empty?
|
|
32
|
+
|
|
33
|
+
body = section[:body] || section["body"]
|
|
34
|
+
lines << body if body && !body.to_s.empty?
|
|
35
|
+
|
|
36
|
+
table_headers = section[:headers] || section["headers"]
|
|
37
|
+
table_rows = section[:rows] || section["rows"]
|
|
38
|
+
if table_headers && !table_headers.empty?
|
|
39
|
+
lines << table(table_headers, table_rows || [])
|
|
40
|
+
end
|
|
41
|
+
lines << ""
|
|
42
|
+
end
|
|
43
|
+
lines.join("\n")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
|
|
3
|
+
module Polyrun
|
|
4
|
+
class Hooks
|
|
5
|
+
# Runs shell hook commands; suppresses stdout/stderr on success unless verbose.
|
|
6
|
+
module ShellRunner
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def run_shell_hook(cmd, merged)
|
|
10
|
+
if hook_shell_output_verbose?
|
|
11
|
+
ok = system(merged, "sh", "-c", cmd)
|
|
12
|
+
return [ok, $?.exitstatus]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
stdout, stderr, status = Open3.capture3(merged, "sh", "-c", cmd)
|
|
16
|
+
unless status.success?
|
|
17
|
+
$stdout.print(stdout) unless stdout.empty?
|
|
18
|
+
$stderr.print(stderr) unless stderr.empty?
|
|
19
|
+
end
|
|
20
|
+
[status.success?, status.exitstatus]
|
|
21
|
+
rescue Interrupt
|
|
22
|
+
Polyrun::Log.warn "polyrun hooks: shell hook interrupted"
|
|
23
|
+
[false, 130]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def hook_shell_output_verbose?
|
|
27
|
+
Polyrun::Debug.enabled? ||
|
|
28
|
+
%w[1 true yes].include?(ENV["POLYRUN_VERBOSE"]&.to_s&.downcase) ||
|
|
29
|
+
%w[1 true yes].include?(ENV["POLYRUN_HOOKS_VERBOSE"]&.to_s&.downcase)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/polyrun/hooks.rb
CHANGED
|
@@ -2,6 +2,7 @@ require "shellwords"
|
|
|
2
2
|
|
|
3
3
|
require_relative "log"
|
|
4
4
|
require_relative "hooks/dsl"
|
|
5
|
+
require_relative "hooks/shell_runner"
|
|
5
6
|
require_relative "hooks/worker_runner"
|
|
6
7
|
require_relative "hooks/worker_shell"
|
|
7
8
|
|
|
@@ -17,6 +18,7 @@ module Polyrun
|
|
|
17
18
|
# Orchestration respects +POLYRUN_HOOKS_DISABLE=1+ (+run_phase_if_enabled+); +polyrun hook run+ always runs {#run_phase}.
|
|
18
19
|
class Hooks
|
|
19
20
|
include WorkerShell
|
|
21
|
+
include ShellRunner
|
|
20
22
|
|
|
21
23
|
PHASES = %i[
|
|
22
24
|
before_suite after_suite
|
|
@@ -128,13 +130,8 @@ module Polyrun
|
|
|
128
130
|
end
|
|
129
131
|
|
|
130
132
|
commands_for(phase).each do |cmd|
|
|
131
|
-
ok =
|
|
132
|
-
|
|
133
|
-
rescue Interrupt
|
|
134
|
-
Polyrun::Log.warn "polyrun hooks: #{phase} shell hook interrupted"
|
|
135
|
-
return 130
|
|
136
|
-
end
|
|
137
|
-
return $?.exitstatus unless ok
|
|
133
|
+
ok, exitstatus = run_shell_hook(cmd, merged)
|
|
134
|
+
return exitstatus unless ok
|
|
138
135
|
end
|
|
139
136
|
0
|
|
140
137
|
end
|
|
@@ -40,12 +40,20 @@ module Polyrun
|
|
|
40
40
|
|
|
41
41
|
pool = glob_under_cwd(all_glob, cwd)
|
|
42
42
|
pool.uniq!
|
|
43
|
+
pool = reject_exclude_prefixes(pool, pb)
|
|
43
44
|
stages = Array(pb["stages"])
|
|
44
45
|
return sort_paths(pool) if stages.empty?
|
|
45
46
|
|
|
46
47
|
apply_stages_to_pool(stages, pool, cwd)
|
|
47
48
|
end
|
|
48
49
|
|
|
50
|
+
def reject_exclude_prefixes(pool, pb)
|
|
51
|
+
prefixes = Array(pb["exclude_prefixes"]).map(&:to_s).reject(&:empty?)
|
|
52
|
+
return pool if prefixes.empty?
|
|
53
|
+
|
|
54
|
+
pool.reject { |path| prefixes.any? { |prefix| path.start_with?(prefix) } }
|
|
55
|
+
end
|
|
56
|
+
|
|
49
57
|
def apply_stages_to_pool(stages, pool, cwd)
|
|
50
58
|
remaining = Set.new(pool)
|
|
51
59
|
out = []
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Polyrun
|
|
2
|
+
module Queue
|
|
3
|
+
# Claim / run / ack loop for +polyrun run-queue+ workers (forked or direct).
|
|
4
|
+
module WorkerLoop
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def run(store:, worker_id:, batch:, cmd:, on_failure:)
|
|
8
|
+
batches_ok = 0
|
|
9
|
+
batches_fail = 0
|
|
10
|
+
loop do
|
|
11
|
+
claim = store.claim!(worker_id: worker_id, batch_size: batch)
|
|
12
|
+
paths = claim["paths"] || []
|
|
13
|
+
break if paths.empty?
|
|
14
|
+
|
|
15
|
+
code = run_batch(cmd, paths)
|
|
16
|
+
if code == 0
|
|
17
|
+
store.ack!(lease_id: claim["lease_id"], worker_id: worker_id)
|
|
18
|
+
batches_ok += 1
|
|
19
|
+
elsif on_failure.to_s == "requeue"
|
|
20
|
+
store.reclaim_lease!(claim["lease_id"])
|
|
21
|
+
batches_fail += 1
|
|
22
|
+
return {ok: batches_ok, fail: batches_fail, exit_code: 1}
|
|
23
|
+
else
|
|
24
|
+
batches_fail += 1
|
|
25
|
+
return {ok: batches_ok, fail: batches_fail, exit_code: code.zero? ? 1 : code}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
{ok: batches_ok, fail: batches_fail, exit_code: 0}
|
|
29
|
+
rescue Polyrun::Error => e
|
|
30
|
+
Polyrun::Log.warn "polyrun run-queue worker #{worker_id}: #{e.message}"
|
|
31
|
+
{ok: batches_ok, fail: batches_fail, exit_code: 2}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def run_batch(cmd, paths)
|
|
35
|
+
system(*cmd, *paths) ? 0 : ($?.exitstatus || 1)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
require "fileutils"
|
|
3
3
|
|
|
4
|
+
require_relative "../export/csv"
|
|
5
|
+
require_relative "../export/markdown"
|
|
6
|
+
|
|
4
7
|
module Polyrun
|
|
5
8
|
module Reporting
|
|
6
9
|
# Merge per-worker / per-shard failure fragments (JSONL or RSpec JSON) into one report.
|
|
@@ -8,6 +11,10 @@ module Polyrun
|
|
|
8
11
|
module FailureMerge
|
|
9
12
|
DEFAULT_FRAGMENT_DIR = "tmp/polyrun_failures".freeze
|
|
10
13
|
FRAGMENT_GLOB = "polyrun-failure-fragment-*.jsonl".freeze
|
|
14
|
+
CSV_HEADERS = %w[
|
|
15
|
+
id full_description location file_path line_number message exception_class
|
|
16
|
+
polyrun_shard_index polyrun_shard_total source
|
|
17
|
+
].freeze
|
|
11
18
|
|
|
12
19
|
module_function
|
|
13
20
|
|
|
@@ -24,7 +31,7 @@ module Polyrun
|
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
# @param paths [Array<String>] fragment paths (.jsonl and/or RSpec --format json outputs)
|
|
27
|
-
# @param format [String] "jsonl" or "
|
|
34
|
+
# @param format [String] "jsonl", "json", "csv", or "markdown"
|
|
28
35
|
# @param output [String] destination path
|
|
29
36
|
# @return [Integer] count of failure rows merged
|
|
30
37
|
def merge_files!(paths, output:, format: "jsonl")
|
|
@@ -34,23 +41,52 @@ module Polyrun
|
|
|
34
41
|
FileUtils.mkdir_p(File.dirname(out_abs))
|
|
35
42
|
case fmt
|
|
36
43
|
when "json"
|
|
37
|
-
|
|
38
|
-
"meta" => {
|
|
39
|
-
"polyrun_merge" => true,
|
|
40
|
-
"inputs" => paths.map { |p| File.expand_path(p) },
|
|
41
|
-
"failure_count" => rows.size
|
|
42
|
-
},
|
|
43
|
-
"failures" => rows
|
|
44
|
-
}
|
|
45
|
-
File.write(out_abs, JSON.generate(doc))
|
|
44
|
+
write_json_output!(out_abs, paths, rows)
|
|
46
45
|
when "jsonl"
|
|
47
|
-
|
|
46
|
+
write_jsonl_output!(out_abs, rows)
|
|
47
|
+
when "csv"
|
|
48
|
+
write_csv_output!(out_abs, rows)
|
|
49
|
+
when "markdown", "md"
|
|
50
|
+
write_markdown_output!(out_abs, rows)
|
|
48
51
|
else
|
|
49
|
-
raise Polyrun::Error, "merge-failures: unknown format #{fmt.inspect} (use jsonl or
|
|
52
|
+
raise Polyrun::Error, "merge-failures: unknown format #{fmt.inspect} (use jsonl, json, csv, or markdown)"
|
|
50
53
|
end
|
|
51
54
|
rows.size
|
|
52
55
|
end
|
|
53
56
|
|
|
57
|
+
def write_json_output!(out_abs, paths, rows)
|
|
58
|
+
doc = {
|
|
59
|
+
"meta" => {
|
|
60
|
+
"polyrun_merge" => true,
|
|
61
|
+
"inputs" => paths.map { |p| File.expand_path(p) },
|
|
62
|
+
"failure_count" => rows.size
|
|
63
|
+
},
|
|
64
|
+
"failures" => rows
|
|
65
|
+
}
|
|
66
|
+
File.write(out_abs, JSON.generate(doc))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def write_jsonl_output!(out_abs, rows)
|
|
70
|
+
File.write(out_abs, rows.map { |h| JSON.generate(h) }.join("\n") + (rows.empty? ? "" : "\n"))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def write_csv_output!(out_abs, rows)
|
|
74
|
+
csv_rows = rows.map { |row| CSV_HEADERS.map { |key| row[key] } }
|
|
75
|
+
File.write(out_abs, Export::Csv.generate(CSV_HEADERS, csv_rows))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def write_markdown_output!(out_abs, rows)
|
|
79
|
+
sections = rows.map.with_index(1) do |row, index|
|
|
80
|
+
{
|
|
81
|
+
heading: "Failure #{index}: #{row["full_description"] || row["location"] || row["id"]}",
|
|
82
|
+
headers: %w[field value],
|
|
83
|
+
rows: row.sort_by { |key, _| key.to_s }.map { |key, value| [key, value] }
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
sections = [{heading: "Failures", body: "(none)"}] if sections.empty?
|
|
87
|
+
File.write(out_abs, Export::Markdown.document("Polyrun failure report", sections))
|
|
88
|
+
end
|
|
89
|
+
|
|
54
90
|
def collect_rows(paths)
|
|
55
91
|
rows = []
|
|
56
92
|
paths.each do |p|
|