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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -1
  3. data/CONTRIBUTING.md +22 -0
  4. data/docs/SETUP_PROFILE.md +1 -1
  5. data/ext/polyrun_coverage_merge/coverage_merge.c +492 -0
  6. data/ext/polyrun_coverage_merge/extconf.rb +3 -0
  7. data/lib/polyrun/benchmark/profile.rb +191 -0
  8. data/lib/polyrun/benchmark/report.rb +87 -0
  9. data/lib/polyrun/cli/benchmark_commands.rb +66 -0
  10. data/lib/polyrun/cli/coverage_commands.rb +4 -4
  11. data/lib/polyrun/cli/coverage_merge_io.rb +30 -4
  12. data/lib/polyrun/cli/failure_commands.rb +10 -3
  13. data/lib/polyrun/cli/help.rb +12 -8
  14. data/lib/polyrun/cli/report_commands.rb +25 -11
  15. data/lib/polyrun/cli/run_shards_parallel_children.rb +1 -1
  16. data/lib/polyrun/cli/run_shards_plan_options.rb +1 -1
  17. data/lib/polyrun/cli/spec_quality_commands.rb +16 -7
  18. data/lib/polyrun/cli.rb +8 -1
  19. data/lib/polyrun/coverage/example_diff.rb +127 -60
  20. data/lib/polyrun/coverage/example_diff_snapshot.rb +44 -0
  21. data/lib/polyrun/coverage/example_diff_track_filter.rb +51 -0
  22. data/lib/polyrun/coverage/file_stats_report.rb +36 -0
  23. data/lib/polyrun/coverage/formatter.rb +22 -1
  24. data/lib/polyrun/coverage/merge/formatters.rb +11 -3
  25. data/lib/polyrun/coverage/merge_merge_two.rb +112 -63
  26. data/lib/polyrun/coverage/merge_native.rb +37 -0
  27. data/lib/polyrun/coverage/reporting.rb +1 -1
  28. data/lib/polyrun/export/csv.rb +25 -0
  29. data/lib/polyrun/export/markdown.rb +47 -0
  30. data/lib/polyrun/hooks/shell_runner.rb +33 -0
  31. data/lib/polyrun/hooks.rb +4 -7
  32. data/lib/polyrun/reporting/failure_merge.rb +48 -12
  33. data/lib/polyrun/reporting/junit.rb +8 -7
  34. data/lib/polyrun/reporting/junit_report.rb +87 -0
  35. data/lib/polyrun/rspec/example_debug.rb +1 -1
  36. data/lib/polyrun/rspec/sharded_formatter_compat.rb +30 -7
  37. data/lib/polyrun/spec_quality/profile.rb +24 -12
  38. data/lib/polyrun/spec_quality/report.rb +93 -0
  39. data/lib/polyrun/spec_quality.rb +19 -11
  40. data/lib/polyrun/timing/summary.rb +55 -5
  41. data/lib/polyrun/version.rb +1 -1
  42. data/polyrun.gemspec +8 -1
  43. metadata +58 -2
@@ -4,114 +4,163 @@ module Polyrun
4
4
  module_function
5
5
 
6
6
  def merge_two(a, b)
7
- keys = a.keys | b.keys
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
- keys.each do |path|
10
- out[path] = merge_file_entry(a[path], b[path])
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 normalize_file_entry(v)
16
- return nil if v.nil?
17
- return {"lines" => v} if v.is_a?(Array)
35
+ def native_merge_line_arrays?
36
+ native_acceleration?
37
+ end
18
38
 
19
- v
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
- h = normalize_file_entry(file)
24
- return nil unless h.is_a?(Hash)
51
+ hash = normalize_file_entry(file)
52
+ return nil unless hash.is_a?(Hash)
25
53
 
26
- h["lines"] || h[:lines]
54
+ hash["lines"] || hash[:lines]
27
55
  end
28
56
 
29
- def merge_file_entry(x, y)
30
- x = normalize_file_entry(x)
31
- y = normalize_file_entry(y)
32
- return y if x.nil?
33
- return x if y.nil?
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(x["lines"] || x[:lines], y["lines"] || y[:lines])
63
+ lines = merge_line_arrays(left["lines"] || left[:lines], right["lines"] || right[:lines])
36
64
  entry = {"lines" => lines}
37
- bx = x["branches"] || x[:branches]
38
- by = y["branches"] || y[:branches]
39
- br = merge_branch_arrays(bx, by)
40
- entry["branches"] = br if br
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(a, b)
45
- a ||= []
46
- b ||= []
47
- na = a.size
48
- nb = b.size
49
- max_len = (na > nb) ? na : nb
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
- i = 0
52
- while i < max_len
53
- out[i] = merge_line_hits(a[i], b[i])
54
- i += 1
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(x, y)
60
- return y if x.nil?
61
- return x if y.nil?
62
- return "ignored" if x == "ignored" || y == "ignored"
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
- xi = line_hit_to_i(x)
65
- yi = line_hit_to_i(y)
66
- return xi + yi if xi && yi
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 yi if xi.nil? && yi
69
- return xi if yi.nil? && xi
104
+ return right_i if left_i.nil? && right_i
105
+ return left_i if right_i.nil? && left_i
70
106
 
71
- x
107
+ left
72
108
  end
73
109
 
74
- def line_hit_to_i(v)
75
- case v
76
- when Integer then v
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(v, exception: false)
115
+ Integer(value, exception: false)
80
116
  end
81
117
  end
82
118
 
83
- def merge_branch_arrays(a, b)
84
- return nil if a.nil? && b.nil?
85
- return (a || b).dup if a.nil? || b.nil?
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
- [a, b].each do |arr|
89
- arr.each do |br|
90
- k = branch_key(br)
91
- existing = index[k]
92
- index[k] =
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, br)
132
+ merge_branch_entries(existing, branch)
95
133
  else
96
- br.dup
134
+ branch.dup
97
135
  end
98
136
  end
99
137
  end
100
- index.values.sort_by { |br| branch_key(br) }
138
+ index.values.sort_by { |branch| branch_key(branch) }
101
139
  end
102
140
 
103
- def branch_key(br)
104
- h = br.is_a?(Hash) ? br : {}
105
- [h["type"] || h[:type], h["start_line"] || h[:start_line], h["end_line"] || h[:end_line]]
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 merge_branch_entries(x, y)
109
- out = x.is_a?(Hash) ? x.dup : {}
110
- xc = (x["coverage"] || x[:coverage]).to_i
111
- yc = (y["coverage"] || y[:coverage]).to_i
112
- out["coverage"] = xc + yc
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 = begin
132
- system(merged, "sh", "-c", cmd)
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 "json"
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
- doc = {
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
- File.write(out_abs, rows.map { |h| JSON.generate(h) }.join("\n") + (rows.empty? ? "" : "\n"))
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 json)"
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
- xml = emit_xml(doc)
36
- File.write(output_path, xml)
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)