polyrun 2.1.3 → 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 +10 -1
- data/CONTRIBUTING.md +22 -0
- data/docs/SETUP_PROFILE.md +1 -1
- 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 +12 -8
- data/lib/polyrun/cli/report_commands.rb +25 -11
- data/lib/polyrun/cli/run_shards_parallel_children.rb +1 -1
- data/lib/polyrun/cli/run_shards_plan_options.rb +1 -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/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 +1 -1
- data/lib/polyrun/rspec/sharded_formatter_compat.rb +30 -7
- 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/polyrun.gemspec +8 -1
- metadata +58 -2
|
@@ -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
|
|
@@ -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|
|
|
@@ -14,26 +14,26 @@ module Polyrun
|
|
|
14
14
|
module Junit
|
|
15
15
|
module_function
|
|
16
16
|
|
|
17
|
-
def write_from_json_file(json_path, output_path:)
|
|
17
|
+
def write_from_json_file(json_path, output_path:, format: "xml")
|
|
18
18
|
data = JSON.parse(File.read(json_path))
|
|
19
|
-
write_from_hash(data, output_path: output_path)
|
|
19
|
+
write_from_hash(data, output_path: output_path, format: format)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# Merge several RSpec JSON outputs (parallel shards) by concatenating +examples+.
|
|
23
|
-
def merge_rspec_json_files(paths, output_path:)
|
|
23
|
+
def merge_rspec_json_files(paths, output_path:, format: "xml")
|
|
24
24
|
merged = {"examples" => []}
|
|
25
25
|
paths.each do |p|
|
|
26
26
|
data = JSON.parse(File.read(p))
|
|
27
27
|
merged["examples"].concat(data["examples"] || [])
|
|
28
28
|
end
|
|
29
29
|
merged["summary"] = {"summary_line" => "merged #{paths.size} RSpec JSON file(s)"}
|
|
30
|
-
write_from_hash(merged, output_path: output_path)
|
|
30
|
+
write_from_hash(merged, output_path: output_path, format: format)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def write_from_hash(data, output_path:)
|
|
33
|
+
def write_from_hash(data, output_path:, format: "xml")
|
|
34
34
|
doc = parse_input(data)
|
|
35
|
-
|
|
36
|
-
File.write(output_path,
|
|
35
|
+
body = render(doc, format: format)
|
|
36
|
+
File.write(output_path, body)
|
|
37
37
|
output_path
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -123,3 +123,4 @@ module Polyrun
|
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
require_relative "junit_emit"
|
|
126
|
+
require_relative "junit_report"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
require_relative "../export/csv"
|
|
4
|
+
require_relative "../export/markdown"
|
|
5
|
+
|
|
6
|
+
module Polyrun
|
|
7
|
+
module Reporting
|
|
8
|
+
module Junit
|
|
9
|
+
CSV_HEADERS = %w[classname name status time_seconds failure_message].freeze
|
|
10
|
+
MARKDOWN_HEADERS = %w[classname name status time_seconds].freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def render(doc, format: "xml")
|
|
15
|
+
case format.to_s.downcase
|
|
16
|
+
when "xml" then emit_xml(doc)
|
|
17
|
+
when "csv" then emit_csv(doc)
|
|
18
|
+
when "markdown", "md" then emit_markdown(doc)
|
|
19
|
+
else
|
|
20
|
+
raise Polyrun::Error, "report-junit: unknown format #{format.inspect} (use xml, csv, or markdown)"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def emit_csv(doc)
|
|
25
|
+
rows = Array(doc["testcases"]).map { |testcase| csv_row(testcase) }
|
|
26
|
+
Export::Csv.generate(CSV_HEADERS, rows)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def csv_row(testcase)
|
|
30
|
+
testcase = testcase.transform_keys(&:to_s)
|
|
31
|
+
failure = testcase["failure"] || {}
|
|
32
|
+
failure = failure.transform_keys(&:to_s) if failure.is_a?(Hash)
|
|
33
|
+
[
|
|
34
|
+
testcase["classname"],
|
|
35
|
+
testcase["name"],
|
|
36
|
+
status_of(testcase),
|
|
37
|
+
format_float(testcase["time"] || 0),
|
|
38
|
+
failure["message"]
|
|
39
|
+
]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def emit_markdown(doc)
|
|
43
|
+
cases = Array(doc["testcases"])
|
|
44
|
+
sections = [
|
|
45
|
+
markdown_summary_section(doc, cases),
|
|
46
|
+
markdown_testcases_section(cases)
|
|
47
|
+
]
|
|
48
|
+
failure_section = markdown_failures_section(cases)
|
|
49
|
+
sections << failure_section if failure_section
|
|
50
|
+
Export::Markdown.document(doc["name"].to_s, sections)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def markdown_summary_section(doc, cases)
|
|
54
|
+
total_time = cases.sum { |testcase| (testcase["time"] || testcase[:time] || 0).to_f }
|
|
55
|
+
summary_rows = [
|
|
56
|
+
["tests", cases.size],
|
|
57
|
+
["failures", cases.count { |testcase| status_of(testcase) == "failed" }],
|
|
58
|
+
["errors", cases.count { |testcase| status_of(testcase) == "error" }],
|
|
59
|
+
["skipped", cases.count { |testcase| %w[pending skipped].include?(status_of(testcase)) }],
|
|
60
|
+
["time_seconds", format_float(total_time)],
|
|
61
|
+
["hostname", doc["hostname"]]
|
|
62
|
+
]
|
|
63
|
+
{heading: "Summary", headers: %w[metric value], rows: summary_rows}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def markdown_testcases_section(cases)
|
|
67
|
+
testcase_rows = cases.map do |testcase|
|
|
68
|
+
testcase = testcase.transform_keys(&:to_s)
|
|
69
|
+
[testcase["classname"], testcase["name"], status_of(testcase), format_float(testcase["time"] || 0)]
|
|
70
|
+
end
|
|
71
|
+
{heading: "Test cases", headers: MARKDOWN_HEADERS, rows: testcase_rows}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def markdown_failures_section(cases)
|
|
75
|
+
failed = cases.select { |testcase| %w[failed error].include?(status_of(testcase)) }
|
|
76
|
+
return if failed.empty?
|
|
77
|
+
|
|
78
|
+
failure_rows = failed.map do |testcase|
|
|
79
|
+
testcase = testcase.transform_keys(&:to_s)
|
|
80
|
+
failure = (testcase["failure"] || {}).transform_keys(&:to_s)
|
|
81
|
+
[testcase["name"], status_of(testcase), failure["message"], failure["body"]]
|
|
82
|
+
end
|
|
83
|
+
{heading: "Failures", headers: %w[name status message body], rows: failure_rows}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -82,7 +82,7 @@ module Polyrun
|
|
|
82
82
|
group = example.metadata[:example_group]
|
|
83
83
|
Polyrun::Log.puts "\n\nRunning #{group[:file_path]}:#{group[:line_number]}"
|
|
84
84
|
|
|
85
|
-
level = log_level
|
|
85
|
+
level = ExampleDebug.log_level
|
|
86
86
|
Rails.logger.level = level
|
|
87
87
|
if defined?(ActiveRecord::Base)
|
|
88
88
|
ar_logger = Logger.new(Polyrun::Log.stdout)
|