polyrun 1.4.1 → 1.4.2
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 +9 -0
- data/lib/polyrun/coverage/collector_finish.rb +3 -2
- data/lib/polyrun/coverage/formatter.rb +2 -1
- data/lib/polyrun/coverage/merge/formatters_html.rb +187 -43
- data/lib/polyrun/coverage/merge/html/_file_list.html.erb +21 -0
- data/lib/polyrun/coverage/merge/html/_file_section.html.erb +26 -0
- data/lib/polyrun/coverage/merge/html/_groups_table.html.erb +18 -0
- data/lib/polyrun/coverage/merge/html/_overview.html.erb +47 -0
- data/lib/polyrun/coverage/merge/html/report.css +147 -0
- data/lib/polyrun/coverage/merge/html/report.js +48 -0
- data/lib/polyrun/coverage/merge/html/template.html.erb +30 -0
- data/lib/polyrun/coverage/track_files.rb +9 -0
- data/lib/polyrun/version.rb +1 -1
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f37969f7a73313a98aa0fd343ae1ed871a32b6dc504da56dd5cb51f42ef2969a
|
|
4
|
+
data.tar.gz: 3e17734a4419ea15b243ad01261701106d99e3470e4408d2b545deb5876ddcf3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68b10554a23d62042db0d6d5307933ffdbb0fb58cc2452b78e1fca2e1a9b42095bc25e7e98b6b104f30a8be6cb0c4aad09212a57b1f4bbdbd2e2efad3501e3fc
|
|
7
|
+
data.tar.gz: 70594cb4f7cd5fb32be05aa0da60058e0fc9b50c9ca3ae4223e88a39293a5199e078eda52a2cb461f6782011aa22f05e3ca5efdc7b52cacc184f67a7326c13db
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 1.4.2 (2026-04-24)
|
|
6
|
+
|
|
7
|
+
- Add richer HTML coverage reports: summary cards, group coverage, sortable file tables, project-relative paths, and per-file source detail.
|
|
8
|
+
- Refactor HTML coverage rendering into stdlib `ERB` templates with `_*.html.erb` partials and isolated `report.css` / `report.js` assets; inline assets into final standalone report.
|
|
9
|
+
- Fix `track_files` coverage scope in `Collector.finish`: keep only files matched by tracked globs, drop unrelated loaded runtime files, and add unloaded tracked files only for non-sharded runs.
|
|
10
|
+
- Add coverage specs for divergent `track_under` / `track_files` configs in serial and sharded finish paths; add `TrackFiles.keep_tracked_files`.
|
|
11
|
+
|
|
3
12
|
## 1.4.1 (2026-04-16)
|
|
4
13
|
|
|
5
14
|
- Add `polyrun merge-failures` and `run-shards --merge-failures` / `--merge-failures-output` / `--merge-failures-format`; merge per-worker JSONL under `tmp/polyrun_failures/polyrun-failure-fragment-*.jsonl` (or RSpec JSON via `-i`). Run merge after all workers exit, including when a shard failed (`--merge-coverage` still runs only after all shards succeed).
|
|
@@ -41,9 +41,10 @@ module Polyrun
|
|
|
41
41
|
def self.track_blob_for_finish(cfg, blob)
|
|
42
42
|
sharded = ENV["POLYRUN_SHARD_TOTAL"].to_i > 1
|
|
43
43
|
if cfg[:track_files]
|
|
44
|
-
|
|
44
|
+
filtered = TrackFiles.keep_tracked_files(blob, cfg[:root], cfg[:track_files])
|
|
45
|
+
return filtered if sharded
|
|
45
46
|
|
|
46
|
-
TrackFiles.merge_untracked_into_blob(
|
|
47
|
+
TrackFiles.merge_untracked_into_blob(filtered, cfg[:root], cfg[:track_files])
|
|
47
48
|
else
|
|
48
49
|
Collector.keep_under_root(blob, cfg[:root], cfg[:track_under])
|
|
49
50
|
end
|
|
@@ -106,7 +106,8 @@ module Polyrun
|
|
|
106
106
|
def write_files(result, output_dir, basename)
|
|
107
107
|
path = File.join(output_dir, "#{basename}.html")
|
|
108
108
|
title = (result.meta && result.meta["title"]) || (result.meta && result.meta[:title]) || "Polyrun coverage"
|
|
109
|
-
|
|
109
|
+
root = result.meta && (result.meta["polyrun_coverage_root"] || result.meta[:polyrun_coverage_root])
|
|
110
|
+
File.write(path, Merge.emit_html(result.coverage_blob, title: title, root: root, groups: result.groups))
|
|
110
111
|
{html: path}
|
|
111
112
|
end
|
|
112
113
|
end
|
|
@@ -1,54 +1,198 @@
|
|
|
1
1
|
require "cgi"
|
|
2
|
+
require "digest/sha1"
|
|
3
|
+
require "erb"
|
|
4
|
+
require "pathname"
|
|
2
5
|
|
|
3
6
|
module Polyrun
|
|
4
7
|
module Coverage
|
|
5
8
|
module Merge
|
|
6
9
|
module_function
|
|
7
10
|
|
|
8
|
-
#
|
|
9
|
-
def emit_html(coverage_blob, title: "Polyrun coverage")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
# Standalone HTML report with summary, file table, and per-file source details.
|
|
12
|
+
def emit_html(coverage_blob, title: "Polyrun coverage", root: nil, groups: nil, generated_at: Time.now)
|
|
13
|
+
files = coverage_blob.keys.sort.map { |path| html_file_payload(path, coverage_blob[path], root) }
|
|
14
|
+
summary = html_summary(files)
|
|
15
|
+
groups_html = render_html_partial("groups_table", group_rows_html: html_group_rows(groups).join("\n"))
|
|
16
|
+
overview_html = render_html_partial(
|
|
17
|
+
"overview",
|
|
18
|
+
summary: summary,
|
|
19
|
+
summary_badge_class: html_badge_class(summary[:line_percent]),
|
|
20
|
+
groups_html: groups_html
|
|
21
|
+
)
|
|
22
|
+
file_list_html = render_html_partial("file_list", file_rows_html: files.map { |file| html_file_list_row(file) }.join("\n"))
|
|
23
|
+
file_sections_html = files.map { |file| render_html_partial("file_section", file: file) }.join("\n")
|
|
24
|
+
ERB.new(File.read(html_template_path), trim_mode: "-").result_with_hash(
|
|
25
|
+
title: CGI.escapeHTML(title.to_s),
|
|
26
|
+
generated_label: html_generated_label(generated_at),
|
|
27
|
+
overview_html: overview_html,
|
|
28
|
+
file_list_html: file_list_html,
|
|
29
|
+
file_sections_html: file_sections_html,
|
|
30
|
+
stylesheet: File.read(html_stylesheet_path),
|
|
31
|
+
javascript: File.read(html_javascript_path)
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def html_asset_dir
|
|
36
|
+
File.join(__dir__, "html")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def html_template_path
|
|
40
|
+
File.join(html_asset_dir, "template.html.erb")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def html_stylesheet_path
|
|
44
|
+
File.join(html_asset_dir, "report.css")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def html_javascript_path
|
|
48
|
+
File.join(html_asset_dir, "report.js")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def html_partial_path(name)
|
|
52
|
+
File.join(html_asset_dir, "_#{name}.html.erb")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render_html_partial(name, locals = {})
|
|
56
|
+
ERB.new(File.read(html_partial_path(name)), trim_mode: "-").result_with_hash(locals)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def html_file_payload(path, file, root)
|
|
60
|
+
line_arr = line_array_from_file_entry(file) || []
|
|
61
|
+
source_lines = html_source_lines(path, line_arr.length)
|
|
62
|
+
counts = line_counts(file)
|
|
63
|
+
relevant = counts[:relevant]
|
|
64
|
+
covered = counts[:covered]
|
|
65
|
+
total_hits = line_arr.sum { |hit| html_numeric_hit(hit) || 0 }
|
|
66
|
+
pct = relevant.positive? ? (100.0 * covered / relevant) : 0.0
|
|
67
|
+
{
|
|
68
|
+
id: "file-#{Digest::SHA1.hexdigest(path.to_s)}",
|
|
69
|
+
path: path.to_s,
|
|
70
|
+
display_path: html_display_path(path, root),
|
|
71
|
+
badge_class: html_badge_class(pct),
|
|
72
|
+
total_lines: [source_lines.length, line_arr.length].max,
|
|
73
|
+
relevant: relevant,
|
|
74
|
+
covered: covered,
|
|
75
|
+
missed: relevant - covered,
|
|
76
|
+
avg_hits: relevant.positive? ? (total_hits.to_f / relevant) : 0.0,
|
|
77
|
+
line_percent: pct,
|
|
78
|
+
source_rows: html_source_rows(source_lines, line_arr)
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def html_summary(files)
|
|
83
|
+
relevant = files.sum { |file| file[:relevant] }
|
|
84
|
+
covered = files.sum { |file| file[:covered] }
|
|
85
|
+
total_hits = files.sum { |file| file[:avg_hits] * file[:relevant] }
|
|
86
|
+
{
|
|
87
|
+
files: files.length,
|
|
88
|
+
lines_relevant: relevant,
|
|
89
|
+
lines_covered: covered,
|
|
90
|
+
lines_missed: relevant - covered,
|
|
91
|
+
line_percent: relevant.positive? ? (100.0 * covered / relevant) : 0.0,
|
|
92
|
+
avg_hits: relevant.positive? ? (total_hits.to_f / relevant) : 0.0
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def html_group_rows(groups)
|
|
97
|
+
return [] unless groups.is_a?(Hash) && !groups.empty?
|
|
98
|
+
|
|
99
|
+
groups.map do |name, data|
|
|
100
|
+
pct = html_group_percent(data)
|
|
101
|
+
<<~ROW.strip
|
|
102
|
+
<tr>
|
|
103
|
+
<td>#{CGI.escapeHTML(name.to_s)}</td>
|
|
104
|
+
<td class="cell--number"><span class="badge #{html_badge_class(pct)}">#{format("%.2f", pct)}%</span></td>
|
|
105
|
+
</tr>
|
|
106
|
+
ROW
|
|
17
107
|
end
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
</
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def html_group_percent(data)
|
|
111
|
+
return 0.0 unless data.is_a?(Hash)
|
|
112
|
+
|
|
113
|
+
lines = data["lines"] || data[:lines]
|
|
114
|
+
pct = lines.is_a?(Hash) ? (lines["covered_percent"] || lines[:covered_percent]) : nil
|
|
115
|
+
pct.to_f
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def html_file_list_row(file)
|
|
119
|
+
<<~ROW.strip
|
|
120
|
+
<tr class="t-file">
|
|
121
|
+
<td class="strong t-file__name"><a href="##{file[:id]}" class="src_link" title="#{CGI.escapeHTML(file[:display_path])}">#{CGI.escapeHTML(file[:display_path])}</a></td>
|
|
122
|
+
<td class="cell--number"><span class="badge #{html_badge_class(file[:line_percent])}">#{format("%.2f", file[:line_percent])}%</span></td>
|
|
123
|
+
<td class="cell--number">#{file[:total_lines]}</td>
|
|
124
|
+
<td class="cell--number">#{file[:relevant]}</td>
|
|
125
|
+
<td class="cell--number">#{file[:covered]}</td>
|
|
126
|
+
<td class="cell--number">#{file[:missed]}</td>
|
|
127
|
+
<td class="cell--number">#{format("%.2f", file[:avg_hits])}</td>
|
|
128
|
+
</tr>
|
|
129
|
+
ROW
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def html_source_rows(source_lines, line_arr)
|
|
133
|
+
max_len = [source_lines.length, line_arr.length].max
|
|
134
|
+
Array.new(max_len) do |idx|
|
|
135
|
+
source = source_lines[idx] || ""
|
|
136
|
+
hit = line_arr[idx]
|
|
137
|
+
css = html_line_class(hit)
|
|
138
|
+
hits_label = html_hit_label(hit)
|
|
139
|
+
<<~ROW.strip
|
|
140
|
+
<tr class="#{css}">
|
|
141
|
+
<td class="line-num">#{idx + 1}</td>
|
|
142
|
+
<td class="line-hits">#{hits_label}</td>
|
|
143
|
+
<td class="line-source"><code>#{source.empty? ? " " : CGI.escapeHTML(source)}</code></td>
|
|
144
|
+
</tr>
|
|
145
|
+
ROW
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def html_source_lines(path, fallback_length)
|
|
150
|
+
return Array.new(fallback_length, "") unless File.file?(path.to_s)
|
|
151
|
+
|
|
152
|
+
File.readlines(path.to_s, chomp: true)
|
|
153
|
+
rescue Errno::ENOENT, Errno::EACCES, ArgumentError
|
|
154
|
+
Array.new(fallback_length, "")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def html_display_path(path, root)
|
|
158
|
+
p = File.expand_path(path.to_s)
|
|
159
|
+
return p if root.nil? || root.to_s.empty?
|
|
160
|
+
|
|
161
|
+
Pathname.new(p).relative_path_from(Pathname.new(File.expand_path(root.to_s))).to_s
|
|
162
|
+
rescue ArgumentError
|
|
163
|
+
p
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def html_generated_label(generated_at)
|
|
167
|
+
t = generated_at.is_a?(Time) ? generated_at : Time.now
|
|
168
|
+
t.utc.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def html_badge_class(percent)
|
|
172
|
+
return "green" if percent >= 90.0
|
|
173
|
+
return "yellow" if percent >= 80.0
|
|
174
|
+
|
|
175
|
+
"red"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def html_numeric_hit(hit)
|
|
179
|
+
return nil if hit.nil? || hit == "ignored"
|
|
180
|
+
|
|
181
|
+
hit.to_i
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def html_hit_label(hit)
|
|
185
|
+
return "" if hit.nil?
|
|
186
|
+
return "ignored" if hit == "ignored"
|
|
187
|
+
|
|
188
|
+
hit.to_i.to_s
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def html_line_class(hit)
|
|
192
|
+
return "line-none" if hit.nil?
|
|
193
|
+
return "line-ignored" if hit == "ignored"
|
|
194
|
+
|
|
195
|
+
hit.to_i.positive? ? "line-covered" : "line-missed"
|
|
52
196
|
end
|
|
53
197
|
end
|
|
54
198
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<div class="panel file_list_container" id="file-list">
|
|
2
|
+
<h2>Files</h2>
|
|
3
|
+
<div class="file_list--responsive">
|
|
4
|
+
<table class="file_list" data-sortable="true">
|
|
5
|
+
<thead>
|
|
6
|
+
<tr>
|
|
7
|
+
<th>File</th>
|
|
8
|
+
<th class="cell--number">% covered</th>
|
|
9
|
+
<th class="cell--number">Lines</th>
|
|
10
|
+
<th class="cell--number">Relevant Lines</th>
|
|
11
|
+
<th class="cell--number">Lines covered</th>
|
|
12
|
+
<th class="cell--number">Lines missed</th>
|
|
13
|
+
<th class="cell--number">Avg. Hits / Line</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody>
|
|
17
|
+
<%= file_rows_html %>
|
|
18
|
+
</tbody>
|
|
19
|
+
</table>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div class="panel file-section source_file" id="<%= file[:id] %>">
|
|
2
|
+
<div class="file-heading">
|
|
3
|
+
<h2><%= CGI.escapeHTML(file[:display_path]) %></h2>
|
|
4
|
+
<a href="#overview">Back to summary</a>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="file-meta">
|
|
7
|
+
<span class="badge <%= file[:badge_class] %>"><%= format("%.2f", file[:line_percent]) %>%</span>
|
|
8
|
+
<%= file[:covered] %> / <%= file[:relevant] %> relevant lines covered,
|
|
9
|
+
<%= file[:missed] %> missed,
|
|
10
|
+
<%= format("%.2f", file[:avg_hits]) %> avg. hits / line
|
|
11
|
+
</div>
|
|
12
|
+
<div class="file_list--responsive">
|
|
13
|
+
<table class="source_table">
|
|
14
|
+
<thead>
|
|
15
|
+
<tr>
|
|
16
|
+
<th class="line-num">Line</th>
|
|
17
|
+
<th class="line-hits">Hits</th>
|
|
18
|
+
<th class="line-source">Source</th>
|
|
19
|
+
</tr>
|
|
20
|
+
</thead>
|
|
21
|
+
<tbody>
|
|
22
|
+
<%= file[:source_rows].join("\n") %>
|
|
23
|
+
</tbody>
|
|
24
|
+
</table>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<% unless group_rows_html.empty? %>
|
|
2
|
+
<div class="group-summary">
|
|
3
|
+
<h3>Groups</h3>
|
|
4
|
+
<div class="file_list--responsive">
|
|
5
|
+
<table class="group-table" data-sortable="true">
|
|
6
|
+
<thead>
|
|
7
|
+
<tr>
|
|
8
|
+
<th>Group</th>
|
|
9
|
+
<th class="cell--number">% covered</th>
|
|
10
|
+
</tr>
|
|
11
|
+
</thead>
|
|
12
|
+
<tbody>
|
|
13
|
+
<%= group_rows_html %>
|
|
14
|
+
</tbody>
|
|
15
|
+
</table>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
<% end %>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<div class="panel file_list_container" id="overview">
|
|
2
|
+
<h2>
|
|
3
|
+
<span class="group_name">All Files</span>
|
|
4
|
+
(<span class="covered_percent"><span class="<%= summary_badge_class %>"><%= format("%.2f", summary[:line_percent]) %>%</span></span>
|
|
5
|
+
covered at
|
|
6
|
+
<span class="covered_strength"><span class="<%= summary_badge_class %>"><%= format("%.2f", summary[:avg_hits]) %></span></span>
|
|
7
|
+
hits/line)
|
|
8
|
+
</h2>
|
|
9
|
+
|
|
10
|
+
<div><b><%= summary[:files] %></b> files in total.</div>
|
|
11
|
+
<div class="t-line-summary">
|
|
12
|
+
<b><%= summary[:lines_relevant] %></b> relevant lines,
|
|
13
|
+
<span class="green"><b><%= summary[:lines_covered] %></b> lines covered</span> and
|
|
14
|
+
<span class="red"><b><%= summary[:lines_missed] %></b> lines missed</span>
|
|
15
|
+
(<span class="<%= summary_badge_class %>"><%= format("%.2f", summary[:line_percent]) %>%</span>)
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="metrics">
|
|
19
|
+
<div class="metric">
|
|
20
|
+
<div class="metric-label">Coverage</div>
|
|
21
|
+
<div class="metric-value"><%= format("%.2f", summary[:line_percent]) %>%</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="metric">
|
|
24
|
+
<div class="metric-label">Relevant Lines</div>
|
|
25
|
+
<div class="metric-value"><%= summary[:lines_relevant] %></div>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="metric">
|
|
28
|
+
<div class="metric-label">Lines Covered</div>
|
|
29
|
+
<div class="metric-value"><%= summary[:lines_covered] %></div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="metric">
|
|
32
|
+
<div class="metric-label">Lines Missed</div>
|
|
33
|
+
<div class="metric-value"><%= summary[:lines_missed] %></div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="metric">
|
|
36
|
+
<div class="metric-label">Avg. Hits / Line</div>
|
|
37
|
+
<div class="metric-value"><%= format("%.2f", summary[:avg_hits]) %></div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<%= groups_html %>
|
|
42
|
+
|
|
43
|
+
<div class="nav-links">
|
|
44
|
+
<a href="#file-list">File List</a>
|
|
45
|
+
<a href="#source-files">Source Files</a>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
--bg: #f5f7fb;
|
|
4
|
+
--surface: #ffffff;
|
|
5
|
+
--border: #d7ddea;
|
|
6
|
+
--text: #1f2937;
|
|
7
|
+
--muted: #6b7280;
|
|
8
|
+
--green: #0f766e;
|
|
9
|
+
--green-bg: #ecfdf5;
|
|
10
|
+
--yellow: #b45309;
|
|
11
|
+
--yellow-bg: #fffbeb;
|
|
12
|
+
--red: #b91c1c;
|
|
13
|
+
--red-bg: #fef2f2;
|
|
14
|
+
--gray-bg: #f8fafc;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
* { box-sizing: border-box; }
|
|
18
|
+
|
|
19
|
+
html {
|
|
20
|
+
scroll-behavior: smooth;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
body {
|
|
24
|
+
margin: 0;
|
|
25
|
+
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
26
|
+
background: var(--bg);
|
|
27
|
+
color: var(--text);
|
|
28
|
+
line-height: 1.45;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
a { color: #1d4ed8; text-decoration: none; }
|
|
32
|
+
a:hover { text-decoration: underline; }
|
|
33
|
+
|
|
34
|
+
#wrapper { max-width: 1200px; margin: 0 auto; padding: 1.5rem; }
|
|
35
|
+
|
|
36
|
+
.topbar {
|
|
37
|
+
display: flex;
|
|
38
|
+
justify-content: space-between;
|
|
39
|
+
gap: 1rem;
|
|
40
|
+
align-items: baseline;
|
|
41
|
+
margin-bottom: 1rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.timestamp { color: var(--muted); font-size: 0.9rem; }
|
|
45
|
+
|
|
46
|
+
.panel {
|
|
47
|
+
background: var(--surface);
|
|
48
|
+
border: 1px solid var(--border);
|
|
49
|
+
border-radius: 12px;
|
|
50
|
+
padding: 1rem 1.25rem;
|
|
51
|
+
margin-bottom: 1rem;
|
|
52
|
+
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.metrics {
|
|
56
|
+
display: grid;
|
|
57
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
58
|
+
gap: 0.75rem;
|
|
59
|
+
margin-top: 1rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.metric {
|
|
63
|
+
border: 1px solid var(--border);
|
|
64
|
+
border-radius: 10px;
|
|
65
|
+
padding: 0.85rem 1rem;
|
|
66
|
+
background: var(--gray-bg);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.metric-label { color: var(--muted); font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.03em; }
|
|
70
|
+
.metric-value { font-size: 1.4rem; font-weight: 700; margin-top: 0.2rem; }
|
|
71
|
+
.summary-line, .t-line-summary { margin-top: 0.75rem; color: var(--muted); }
|
|
72
|
+
.group-summary { margin-top: 1rem; }
|
|
73
|
+
|
|
74
|
+
table { width: 100%; border-collapse: collapse; }
|
|
75
|
+
|
|
76
|
+
th, td { padding: 0.55rem 0.7rem; border-top: 1px solid var(--border); vertical-align: top; }
|
|
77
|
+
|
|
78
|
+
thead th {
|
|
79
|
+
background: var(--gray-bg);
|
|
80
|
+
color: var(--muted);
|
|
81
|
+
font-size: 0.82rem;
|
|
82
|
+
text-transform: uppercase;
|
|
83
|
+
letter-spacing: 0.03em;
|
|
84
|
+
border-top: none;
|
|
85
|
+
position: sticky;
|
|
86
|
+
top: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
th[data-sortable-column="true"] {
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
user-select: none;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
th[data-sort-direction="asc"]::after { content: " ↑"; }
|
|
95
|
+
th[data-sort-direction="desc"]::after { content: " ↓"; }
|
|
96
|
+
|
|
97
|
+
.cell--number { text-align: right; white-space: nowrap; }
|
|
98
|
+
|
|
99
|
+
.badge {
|
|
100
|
+
display: inline-block;
|
|
101
|
+
min-width: 72px;
|
|
102
|
+
text-align: center;
|
|
103
|
+
border-radius: 999px;
|
|
104
|
+
padding: 0.15rem 0.55rem;
|
|
105
|
+
font-weight: 700;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.green { color: var(--green); }
|
|
109
|
+
.yellow { color: var(--yellow); }
|
|
110
|
+
.red { color: var(--red); }
|
|
111
|
+
.badge.green { background: var(--green-bg); }
|
|
112
|
+
.badge.yellow { background: var(--yellow-bg); }
|
|
113
|
+
.badge.red { background: var(--red-bg); }
|
|
114
|
+
|
|
115
|
+
.file_list--responsive { overflow-x: auto; }
|
|
116
|
+
.file-section { margin-top: 1rem; }
|
|
117
|
+
|
|
118
|
+
.file-heading {
|
|
119
|
+
display: flex;
|
|
120
|
+
justify-content: space-between;
|
|
121
|
+
gap: 1rem;
|
|
122
|
+
align-items: baseline;
|
|
123
|
+
flex-wrap: wrap;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.file-meta { color: var(--muted); font-size: 0.95rem; }
|
|
127
|
+
|
|
128
|
+
.source_table td {
|
|
129
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
130
|
+
font-size: 0.83rem;
|
|
131
|
+
white-space: pre-wrap;
|
|
132
|
+
word-break: break-word;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.line-num, .line-hits {
|
|
136
|
+
width: 5rem;
|
|
137
|
+
color: var(--muted);
|
|
138
|
+
text-align: right;
|
|
139
|
+
white-space: nowrap;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.line-source { width: auto; }
|
|
143
|
+
.line-covered { background: rgba(16, 185, 129, 0.10); }
|
|
144
|
+
.line-missed { background: rgba(239, 68, 68, 0.10); }
|
|
145
|
+
.line-ignored, .line-none { background: transparent; }
|
|
146
|
+
.nav-links { display: flex; gap: 0.75rem; flex-wrap: wrap; margin-top: 0.75rem; }
|
|
147
|
+
.group-table td:first-child, .file_list td:first-child { width: 100%; }
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
function cellValue(row, index) {
|
|
3
|
+
const cell = row.children[index];
|
|
4
|
+
return cell ? cell.textContent.trim() : "";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function sortableValue(text) {
|
|
8
|
+
const numeric = Number.parseFloat(text.replace(/[% ,]/g, ""));
|
|
9
|
+
return Number.isNaN(numeric) ? text.toLowerCase() : numeric;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sortTable(table, index, direction) {
|
|
13
|
+
const body = table.tBodies[0];
|
|
14
|
+
if (!body) return;
|
|
15
|
+
|
|
16
|
+
const rows = Array.from(body.rows);
|
|
17
|
+
rows.sort((left, right) => {
|
|
18
|
+
const a = sortableValue(cellValue(left, index));
|
|
19
|
+
const b = sortableValue(cellValue(right, index));
|
|
20
|
+
if (a < b) return direction === "asc" ? -1 : 1;
|
|
21
|
+
if (a > b) return direction === "asc" ? 1 : -1;
|
|
22
|
+
return 0;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
rows.forEach((row) => body.appendChild(row));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function installSortableTables() {
|
|
29
|
+
document.querySelectorAll("table[data-sortable='true']").forEach((table) => {
|
|
30
|
+
const headers = Array.from(table.tHead ? table.tHead.rows[0].cells : []);
|
|
31
|
+
headers.forEach((header, index) => {
|
|
32
|
+
header.dataset.sortableColumn = "true";
|
|
33
|
+
header.addEventListener("click", () => {
|
|
34
|
+
const nextDirection = header.dataset.sortDirection === "asc" ? "desc" : "asc";
|
|
35
|
+
headers.forEach((cell) => delete cell.dataset.sortDirection);
|
|
36
|
+
header.dataset.sortDirection = nextDirection;
|
|
37
|
+
sortTable(table, index, nextDirection);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (document.readyState === "loading") {
|
|
44
|
+
document.addEventListener("DOMContentLoaded", installSortableTables);
|
|
45
|
+
} else {
|
|
46
|
+
installSortableTables();
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8"/>
|
|
5
|
+
<title><%= title %></title>
|
|
6
|
+
<style>
|
|
7
|
+
<%= stylesheet %>
|
|
8
|
+
</style>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="wrapper">
|
|
12
|
+
<div class="topbar">
|
|
13
|
+
<h1><%= title %></h1>
|
|
14
|
+
<div class="timestamp">Generated <%= generated_label %></div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<%= overview_html %>
|
|
18
|
+
|
|
19
|
+
<%= file_list_html %>
|
|
20
|
+
|
|
21
|
+
<div id="source-files">
|
|
22
|
+
<%= file_sections_html %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<script>
|
|
27
|
+
<%= javascript %>
|
|
28
|
+
</script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -22,6 +22,15 @@ module Polyrun
|
|
|
22
22
|
end.map { |rel| File.expand_path(rel, root) }.uniq
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Keeps only loaded files that match configured +track_files+ globs.
|
|
26
|
+
def keep_tracked_files(blob, root, track_files)
|
|
27
|
+
tracked = expand_globs(root, track_files).each_with_object({}) { |abs, acc| acc[abs] = true }
|
|
28
|
+
blob.each_with_object({}) do |(path, entry), out|
|
|
29
|
+
abs = File.expand_path(path.to_s)
|
|
30
|
+
out[abs] = entry if tracked[abs]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
25
34
|
# Adds tracked files that were never required, with simulated line arrays (blank/comment => nil, else 0).
|
|
26
35
|
# Matches SimpleCov +add_not_loaded_files+ behavior for coverage completeness.
|
|
27
36
|
def merge_untracked_into_blob(blob, root, track_files)
|
data/lib/polyrun/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: polyrun
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.4.
|
|
4
|
+
version: 1.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -207,6 +207,13 @@ files:
|
|
|
207
207
|
- lib/polyrun/coverage/merge.rb
|
|
208
208
|
- lib/polyrun/coverage/merge/formatters.rb
|
|
209
209
|
- lib/polyrun/coverage/merge/formatters_html.rb
|
|
210
|
+
- lib/polyrun/coverage/merge/html/_file_list.html.erb
|
|
211
|
+
- lib/polyrun/coverage/merge/html/_file_section.html.erb
|
|
212
|
+
- lib/polyrun/coverage/merge/html/_groups_table.html.erb
|
|
213
|
+
- lib/polyrun/coverage/merge/html/_overview.html.erb
|
|
214
|
+
- lib/polyrun/coverage/merge/html/report.css
|
|
215
|
+
- lib/polyrun/coverage/merge/html/report.js
|
|
216
|
+
- lib/polyrun/coverage/merge/html/template.html.erb
|
|
210
217
|
- lib/polyrun/coverage/merge_fragment_meta.rb
|
|
211
218
|
- lib/polyrun/coverage/merge_merge_two.rb
|
|
212
219
|
- lib/polyrun/coverage/rails.rb
|