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
|
@@ -43,10 +43,11 @@ module Polyrun
|
|
|
43
43
|
config_path = nil
|
|
44
44
|
strict = false
|
|
45
45
|
json_out = false
|
|
46
|
+
report_format = nil
|
|
46
47
|
plan_paths = []
|
|
47
48
|
|
|
48
49
|
OptionParser.new do |opts|
|
|
49
|
-
opts.banner = "usage: polyrun report-spec-quality -i FILE [-o PATH] [--top N] [--profile LIST] [--strict] [--json]"
|
|
50
|
+
opts.banner = "usage: polyrun report-spec-quality -i FILE [-o PATH] [--top N] [--profile LIST] [--strict] [--format text|json|csv|markdown] [--json]"
|
|
50
51
|
opts.on("-i", "--input PATH", "Merged polyrun-spec-quality.json") { |v| input = v }
|
|
51
52
|
opts.on("-o", "--output PATH", "Write report to file instead of stdout") { |v| out_file = v }
|
|
52
53
|
opts.on("--top N", Integer) { |v| top = v }
|
|
@@ -54,6 +55,7 @@ module Polyrun
|
|
|
54
55
|
opts.on("-c", "--config PATH", "polyrun_spec_quality.yml path") { |v| config_path = v }
|
|
55
56
|
opts.on("--plan PATH", "Partition plan JSON (repeatable; polyrun plan output per shard)") { |v| plan_paths << v }
|
|
56
57
|
opts.on("--strict", "Exit 1 when gate thresholds fail") { strict = true }
|
|
58
|
+
opts.on("--format VAL", "text (default), json, csv, or markdown") { |v| report_format = v }
|
|
57
59
|
opts.on("--json", "Write analysis JSON instead of text report") { json_out = true }
|
|
58
60
|
end.parse!(argv)
|
|
59
61
|
input ||= argv.first
|
|
@@ -67,13 +69,20 @@ module Polyrun
|
|
|
67
69
|
cfg = load_spec_quality_config(config_path)
|
|
68
70
|
strict = true if cfg["strict"] || strict
|
|
69
71
|
plan_shards = Polyrun::SpecQuality::PlanLoader.load_shards(plan_paths)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
resolved_format = report_format || (json_out ? "json" : "text")
|
|
73
|
+
|
|
74
|
+
begin
|
|
75
|
+
text = Polyrun::SpecQuality::Report.render(
|
|
76
|
+
merged,
|
|
77
|
+
format: resolved_format,
|
|
78
|
+
cfg: cfg,
|
|
79
|
+
top: top,
|
|
80
|
+
profile: profile,
|
|
81
|
+
plan_shards: plan_shards
|
|
76
82
|
)
|
|
83
|
+
rescue Polyrun::Error => e
|
|
84
|
+
Polyrun::Log.warn e.message.to_s
|
|
85
|
+
return 2
|
|
77
86
|
end
|
|
78
87
|
|
|
79
88
|
if out_file
|
data/lib/polyrun/cli.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "cli/run_queue_command"
|
|
|
14
14
|
require_relative "cli/queue_command"
|
|
15
15
|
require_relative "cli/timing_command"
|
|
16
16
|
require_relative "cli/spec_quality_commands"
|
|
17
|
+
require_relative "cli/benchmark_commands"
|
|
17
18
|
require_relative "cli/init_command"
|
|
18
19
|
require_relative "cli/quick_command"
|
|
19
20
|
require_relative "cli/ci_shard_run_parse"
|
|
@@ -32,7 +33,7 @@ module Polyrun
|
|
|
32
33
|
|
|
33
34
|
# Keep in sync with +dispatch_cli_command_subcommands+ (+when+ branches). Used for implicit path routing.
|
|
34
35
|
DISPATCH_SUBCOMMAND_NAMES = %w[
|
|
35
|
-
plan prepare merge-coverage merge-failures merge-spec-quality report-coverage report-junit report-timing report-spec-quality
|
|
36
|
+
plan prepare merge-coverage merge-failures merge-spec-quality report-coverage report-junit report-timing report-spec-quality report-benchmark bench
|
|
36
37
|
env config merge-timing db:setup-template db:setup-shard db:clone-shards
|
|
37
38
|
run-shards parallel-rspec start build-paths init queue run-queue quick hook
|
|
38
39
|
].freeze
|
|
@@ -55,6 +56,7 @@ module Polyrun
|
|
|
55
56
|
include QueueCommand
|
|
56
57
|
include TimingCommand
|
|
57
58
|
include SpecQualityCommands
|
|
59
|
+
include BenchmarkCommands
|
|
58
60
|
include InitCommand
|
|
59
61
|
include QuickCommand
|
|
60
62
|
include CiShardRunParse
|
|
@@ -120,6 +122,7 @@ module Polyrun
|
|
|
120
122
|
config_path = argv.shift or break
|
|
121
123
|
when "-v", "--verbose"
|
|
122
124
|
@verbose = true
|
|
125
|
+
ENV["POLYRUN_VERBOSE"] = "1"
|
|
123
126
|
argv.shift
|
|
124
127
|
when "-h", "--help"
|
|
125
128
|
print_help
|
|
@@ -170,6 +173,10 @@ module Polyrun
|
|
|
170
173
|
cmd_merge_spec_quality(argv)
|
|
171
174
|
when "report-spec-quality"
|
|
172
175
|
cmd_report_spec_quality(argv)
|
|
176
|
+
when "report-benchmark"
|
|
177
|
+
cmd_report_benchmark(argv)
|
|
178
|
+
when "bench"
|
|
179
|
+
cmd_bench(argv)
|
|
173
180
|
when "db:setup-template"
|
|
174
181
|
cmd_db_setup_template(argv, config_path)
|
|
175
182
|
when "db:setup-shard"
|
|
@@ -1,63 +1,124 @@
|
|
|
1
|
+
require_relative "example_diff_track_filter"
|
|
2
|
+
require_relative "example_diff_snapshot"
|
|
3
|
+
|
|
1
4
|
module Polyrun
|
|
2
5
|
module Coverage
|
|
3
6
|
# Per-example line hit deltas from stdlib +Coverage.peek_result+ snapshots.
|
|
4
7
|
module ExampleDiff
|
|
5
8
|
module_function
|
|
6
9
|
|
|
7
|
-
# @return [Hash{String=>Hash}] path =>
|
|
8
|
-
def peek_blob
|
|
10
|
+
# @return [Hash{String=>Hash}] path => sparse line snapshot
|
|
11
|
+
def peek_blob(root: nil, track_under: nil, ignore_paths: nil)
|
|
9
12
|
return {} unless coverage_active?
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
snapshot_peek(
|
|
15
|
+
::Coverage.peek_result,
|
|
16
|
+
root: root,
|
|
17
|
+
track_under: track_under,
|
|
18
|
+
ignore_paths: ignore_paths
|
|
19
|
+
)
|
|
12
20
|
end
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
# Frozen snapshot for storage between peeks (stdlib mutates line arrays in place).
|
|
23
|
+
def snapshot_peek(raw, root: nil, track_under: nil, ignore_paths: nil)
|
|
24
|
+
return {} if raw.nil? || raw.empty?
|
|
17
25
|
|
|
18
|
-
|
|
26
|
+
filter = track_filter_for(root: root, track_under: track_under, ignore_paths: ignore_paths)
|
|
19
27
|
out = {}
|
|
20
|
-
raw.each do |path,
|
|
21
|
-
next unless
|
|
28
|
+
raw.each do |path, coverage_entry|
|
|
29
|
+
next unless coverage_entry.is_a?(Hash)
|
|
30
|
+
next if filter && !filter.include_path?(path.to_s)
|
|
22
31
|
|
|
23
|
-
lines =
|
|
32
|
+
lines = coverage_entry[:lines] || coverage_entry["lines"]
|
|
24
33
|
next unless lines.is_a?(Array)
|
|
25
34
|
|
|
26
|
-
out[path.to_s] =
|
|
35
|
+
out[path.to_s] = Snapshot.sparse_snapshot_lines(lines)
|
|
27
36
|
end
|
|
28
37
|
out
|
|
29
38
|
end
|
|
30
39
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
# +after_source+ may be a normalized blob or a raw +Coverage.peek_result+ hash.
|
|
41
|
+
def diff(before_blob, after_source, root: nil, track_under: nil, ignore_paths: nil)
|
|
42
|
+
filter = track_filter_for(root: root, track_under: track_under, ignore_paths: ignore_paths)
|
|
43
|
+
if raw_peek_result?(after_source)
|
|
44
|
+
diff_against_peek(before_blob, after_source, filter: filter)
|
|
45
|
+
else
|
|
46
|
+
diff_blobs(before_blob, after_source, filter: filter)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def coverage_active?
|
|
51
|
+
defined?(::Coverage) && ::Coverage.respond_to?(:running?) && ::Coverage.running?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def diff_blobs(before_blob, after_blob, filter: nil)
|
|
34
55
|
before_blob ||= {}
|
|
35
56
|
after_blob ||= {}
|
|
36
|
-
|
|
57
|
+
paths = collect_diff_paths(before_blob, after_blob, filter: filter)
|
|
58
|
+
diff_paths(paths, before_blob, after_blob, raw_after: false)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def diff_against_peek(before_blob, after_raw, filter: nil)
|
|
62
|
+
before_blob ||= {}
|
|
63
|
+
after_raw ||= {}
|
|
64
|
+
paths = collect_diff_paths(before_blob, after_raw, filter: filter)
|
|
65
|
+
diff_paths(paths, before_blob, after_raw, raw_after: true)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def filter_lines(lines, root:, track_under:, ignore_paths: [])
|
|
69
|
+
filter = TrackFilter.new(root: root, track_under: track_under, ignore_paths: ignore_paths)
|
|
70
|
+
lines.select { |path, _line, _delta| filter.include_path?(path) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def apply_track_under(delta, root:, track_under:, ignore_paths: [])
|
|
74
|
+
filtered = filter_lines(delta[:lines] || [], root: root, track_under: track_under, ignore_paths: ignore_paths)
|
|
75
|
+
unique = filtered.size
|
|
76
|
+
churn = filtered.sum { |(_path, _line, hit_delta)| hit_delta }
|
|
77
|
+
max_churn = filtered.map { |(_path, _line, hit_delta)| hit_delta }.max || 0
|
|
78
|
+
{
|
|
79
|
+
unique_lines: unique,
|
|
80
|
+
line_churn: churn,
|
|
81
|
+
max_line_churn: max_churn,
|
|
82
|
+
lines: filtered
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def line_array(entry)
|
|
87
|
+
return [] unless entry.is_a?(Hash)
|
|
88
|
+
return [] if entry["sparse"]
|
|
37
89
|
|
|
90
|
+
arr = entry["lines"] || entry[:lines]
|
|
91
|
+
arr.is_a?(Array) ? arr : []
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def raw_peek_result?(source)
|
|
95
|
+
return false unless source.is_a?(Hash)
|
|
96
|
+
return false if source.empty?
|
|
97
|
+
|
|
98
|
+
entry = source.values.first
|
|
99
|
+
entry.is_a?(Hash) && (entry.key?(:lines) || entry.key?(:branches))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def diff_paths(paths, before_blob, after_blob, raw_after:)
|
|
38
103
|
line_entries = []
|
|
39
104
|
unique = 0
|
|
40
105
|
churn = 0
|
|
41
106
|
max_churn = 0
|
|
42
107
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
next if a.nil? && b.nil?
|
|
108
|
+
paths.each do |path|
|
|
109
|
+
before_entry = before_blob[path]
|
|
110
|
+
after_lines =
|
|
111
|
+
if raw_after
|
|
112
|
+
lines_from_peek_entry(path, after_blob)
|
|
113
|
+
else
|
|
114
|
+
line_array(after_blob[path])
|
|
115
|
+
end
|
|
52
116
|
|
|
53
|
-
|
|
54
|
-
next unless delta.positive?
|
|
55
|
-
|
|
56
|
-
line_no = i + 1
|
|
57
|
-
line_entries << [path, line_no, delta]
|
|
117
|
+
accumulate_line_delta(before_entry, after_lines) do |delta, line_no|
|
|
58
118
|
unique += 1
|
|
59
119
|
churn += delta
|
|
60
120
|
max_churn = delta if delta > max_churn
|
|
121
|
+
line_entries << [path, line_no, delta]
|
|
61
122
|
end
|
|
62
123
|
end
|
|
63
124
|
|
|
@@ -68,55 +129,61 @@ module Polyrun
|
|
|
68
129
|
lines: line_entries
|
|
69
130
|
}
|
|
70
131
|
end
|
|
71
|
-
# rubocop:enable Metrics/AbcSize
|
|
72
|
-
|
|
73
|
-
def filter_lines(lines, root:, track_under:, ignore_paths: [])
|
|
74
|
-
root = File.expand_path(root)
|
|
75
|
-
prefixes = Array(track_under).map { |d| File.join(root, d.to_s) }
|
|
76
|
-
ignore = Array(ignore_paths).map(&:to_s).reject(&:empty?)
|
|
77
|
-
|
|
78
|
-
lines.select do |path, _line, _delta|
|
|
79
|
-
p = File.expand_path(path.to_s, root)
|
|
80
|
-
next false if ignore.any? { |pat| path_matches_ignore?(p, pat) }
|
|
81
132
|
|
|
82
|
-
|
|
133
|
+
def collect_diff_paths(before_blob, after_blob, filter: nil)
|
|
134
|
+
keys = before_blob.keys.dup
|
|
135
|
+
after_blob.each_key do |path|
|
|
136
|
+
string_path = path.to_s
|
|
137
|
+
keys << string_path unless keys.include?(string_path)
|
|
83
138
|
end
|
|
84
|
-
|
|
139
|
+
return keys unless filter
|
|
85
140
|
|
|
86
|
-
|
|
87
|
-
filtered = filter_lines(delta[:lines] || [], root: root, track_under: track_under, ignore_paths: ignore_paths)
|
|
88
|
-
unique = filtered.size
|
|
89
|
-
churn = filtered.sum { |(_p, _l, d)| d }
|
|
90
|
-
max_churn = filtered.map { |(_p, _l, d)| d }.max || 0
|
|
91
|
-
{
|
|
92
|
-
unique_lines: unique,
|
|
93
|
-
line_churn: churn,
|
|
94
|
-
max_line_churn: max_churn,
|
|
95
|
-
lines: filtered
|
|
96
|
-
}
|
|
141
|
+
keys.select { |path| filter.include_path?(path) }
|
|
97
142
|
end
|
|
98
143
|
|
|
99
|
-
def
|
|
100
|
-
|
|
144
|
+
def accumulate_line_delta(before_entry, after_lines)
|
|
145
|
+
before_hits = Snapshot.hits_map(before_entry)
|
|
146
|
+
before_max = before_hits.empty? ? 0 : before_hits.keys.max + 1
|
|
147
|
+
max_len = [after_lines.size, before_max].max
|
|
101
148
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
149
|
+
(0...max_len).each do |index|
|
|
150
|
+
before_hit = before_hits[index]
|
|
151
|
+
after_hit = after_lines[index]
|
|
152
|
+
next if after_hit.nil? && before_hit.nil?
|
|
153
|
+
|
|
154
|
+
delta = integer_hit(after_hit) - integer_hit(before_hit)
|
|
155
|
+
next unless delta.positive?
|
|
156
|
+
|
|
157
|
+
yield(delta, index + 1)
|
|
158
|
+
end
|
|
105
159
|
end
|
|
106
160
|
|
|
107
|
-
def
|
|
161
|
+
def lines_from_peek_entry(path, after_raw)
|
|
162
|
+
entry = after_raw[path] || after_raw[path.to_s]
|
|
108
163
|
return [] unless entry.is_a?(Hash)
|
|
109
164
|
|
|
110
|
-
arr = entry[
|
|
165
|
+
arr = entry[:lines] || entry["lines"]
|
|
111
166
|
arr.is_a?(Array) ? arr : []
|
|
112
167
|
end
|
|
113
168
|
|
|
169
|
+
def track_filter_for(root:, track_under:, ignore_paths:)
|
|
170
|
+
return nil if track_under.nil? && ignore_paths.nil?
|
|
171
|
+
|
|
172
|
+
TrackFilter.new(
|
|
173
|
+
root: root || Dir.pwd,
|
|
174
|
+
track_under: track_under,
|
|
175
|
+
ignore_paths: ignore_paths || []
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
114
179
|
def integer_hit(value)
|
|
115
180
|
return 0 if value.nil?
|
|
181
|
+
return 0 if value == "ignored"
|
|
116
182
|
|
|
117
183
|
value.is_a?(Integer) ? value : value.to_i
|
|
118
184
|
end
|
|
119
|
-
private_class_method :
|
|
185
|
+
private_class_method :diff_paths, :collect_diff_paths, :accumulate_line_delta,
|
|
186
|
+
:lines_from_peek_entry, :track_filter_for, :integer_hit, :raw_peek_result?
|
|
120
187
|
end
|
|
121
188
|
end
|
|
122
189
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Polyrun
|
|
2
|
+
module Coverage
|
|
3
|
+
module ExampleDiff
|
|
4
|
+
# Sparse line-hit snapshots for per-example diff storage.
|
|
5
|
+
module Snapshot
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def sparse_snapshot_lines(lines)
|
|
9
|
+
{"sparse" => true, "hits" => dense_hits_map(lines)}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def hits_map(entry)
|
|
13
|
+
return {} if entry.nil?
|
|
14
|
+
|
|
15
|
+
if entry.is_a?(Hash) && entry["sparse"]
|
|
16
|
+
entry["hits"] || {}
|
|
17
|
+
else
|
|
18
|
+
dense_hits_map(ExampleDiff.line_array(entry))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def dense_hits_map(lines)
|
|
23
|
+
hits = {}
|
|
24
|
+
lines.each_with_index do |value, index|
|
|
25
|
+
stored = snapshot_line_value(value)
|
|
26
|
+
hits[index] = stored unless stored.nil?
|
|
27
|
+
end
|
|
28
|
+
hits
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def snapshot_line_value(value)
|
|
32
|
+
case value
|
|
33
|
+
when nil then nil
|
|
34
|
+
when "ignored" then "ignored"
|
|
35
|
+
when Integer then value
|
|
36
|
+
else
|
|
37
|
+
parsed = Integer(value, exception: false)
|
|
38
|
+
parsed.nil? ? value : parsed
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Polyrun
|
|
2
|
+
module Coverage
|
|
3
|
+
module ExampleDiff
|
|
4
|
+
# Path inclusion for scoped snapshots and track-aware diffs.
|
|
5
|
+
class TrackFilter
|
|
6
|
+
def initialize(root:, track_under: nil, ignore_paths: [])
|
|
7
|
+
@root = root ? File.expand_path(root) : nil
|
|
8
|
+
@track_under = Array(track_under).map(&:to_s).reject(&:empty?)
|
|
9
|
+
@ignore = Array(ignore_paths).map(&:to_s).reject(&:empty?)
|
|
10
|
+
@prefixes =
|
|
11
|
+
if @root && !@track_under.empty?
|
|
12
|
+
@track_under.map { |directory| File.join(@root, directory) }
|
|
13
|
+
else
|
|
14
|
+
[]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def include_path?(path)
|
|
19
|
+
absolute = absolute_path(path)
|
|
20
|
+
return false if ignored?(absolute)
|
|
21
|
+
|
|
22
|
+
return true if @prefixes.empty?
|
|
23
|
+
|
|
24
|
+
@prefixes.any? { |prefix| absolute == prefix || absolute.start_with?(prefix + "/") }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def absolute_path(path)
|
|
30
|
+
if @root
|
|
31
|
+
File.expand_path(path.to_s, @root)
|
|
32
|
+
else
|
|
33
|
+
File.expand_path(path.to_s)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def ignored?(absolute)
|
|
38
|
+
@ignore.any? { |pattern| path_matches_ignore?(absolute, pattern) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def path_matches_ignore?(path, pattern)
|
|
42
|
+
return path.include?(pattern) if !pattern.start_with?("/")
|
|
43
|
+
|
|
44
|
+
path.match?(Regexp.new(pattern))
|
|
45
|
+
rescue RegexpError
|
|
46
|
+
path.include?(pattern)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require_relative "../export/csv"
|
|
2
|
+
require_relative "../export/markdown"
|
|
3
|
+
|
|
4
|
+
module Polyrun
|
|
5
|
+
module Coverage
|
|
6
|
+
# Per-file line coverage tables for CSV and Markdown exports.
|
|
7
|
+
module FileStatsReport
|
|
8
|
+
HEADERS = %w[path line_percent lines_covered lines_relevant].freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def file_rows(coverage_blob)
|
|
13
|
+
coverage_blob.map do |path, file_entry|
|
|
14
|
+
line_percent, lines_relevant, lines_covered = Merge.file_line_stats(file_entry)
|
|
15
|
+
[path, format("%.2f", line_percent), lines_covered, lines_relevant]
|
|
16
|
+
end.sort_by { |row| row[1].to_f }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def emit_csv(coverage_blob)
|
|
20
|
+
Export::Csv.generate(HEADERS, file_rows(coverage_blob))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def emit_markdown(coverage_blob, title: "Polyrun coverage report")
|
|
24
|
+
summary = Merge.console_summary(coverage_blob)
|
|
25
|
+
summary_line = Merge.format_console_summary(summary).strip
|
|
26
|
+
Export::Markdown.document(
|
|
27
|
+
title,
|
|
28
|
+
[
|
|
29
|
+
{body: summary_line},
|
|
30
|
+
{heading: "Per-file coverage", headers: HEADERS, rows: file_rows(coverage_blob)}
|
|
31
|
+
]
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -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
|