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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7666af9186562083f29dc56e6c867e48b877acdff6ad28ff8c351e8d3c308582
4
- data.tar.gz: 503f5435deb22112044f7841a82728e6782a770eb656859419e8412d623dcff0
3
+ metadata.gz: f37969f7a73313a98aa0fd343ae1ed871a32b6dc504da56dd5cb51f42ef2969a
4
+ data.tar.gz: 3e17734a4419ea15b243ad01261701106d99e3470e4408d2b545deb5876ddcf3
5
5
  SHA512:
6
- metadata.gz: d0d5d248f1e072c446049bafff111db6e29d784a4a0992528214a6e29cca7b156b67144e6b9c22fc71fa9143f110ac443cda2953219aa816c5562d3247b5e02b
7
- data.tar.gz: d0be776ce5d4a7a5acacfe237a4d07a47c078562aee6a054fb5b63a31a78e199bc2b5ee276b0a8e77a0f0cacd3e4f03cb72d8fd5037baf782d0efb8a1cdf1b04
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
- return Collector.keep_under_root(blob, cfg[:root], cfg[:track_under]) if sharded
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(blob, cfg[:root], cfg[:track_files])
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
- File.write(path, Merge.emit_html(result.coverage_blob, title: title))
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
- # Minimal standalone HTML report (no extra gems), index listing similar to SimpleCov.
9
- def emit_html(coverage_blob, title: "Polyrun coverage")
10
- summary = console_summary(coverage_blob)
11
- rows = []
12
- coverage_blob.keys.sort.each do |path|
13
- file = coverage_blob[path]
14
- pct, rel, cov = file_line_stats(file)
15
- esc = CGI.escapeHTML(path.to_s)
16
- rows << "<tr><td class=\"path\">#{esc}</td><td class=\"pct\">#{format("%.2f", pct)}</td><td class=\"hits\">#{cov} / #{rel}</td></tr>"
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
- esc_title = CGI.escapeHTML(title.to_s)
19
- <<~HTML
20
- <!DOCTYPE html>
21
- <html lang="en">
22
- <head>
23
- <meta charset="utf-8"/>
24
- <title>#{esc_title}</title>
25
- <style>
26
- body { font-family: system-ui, sans-serif; margin: 1.5rem; color: #1a1a1a; }
27
- h1 { font-size: 1.25rem; }
28
- .summary { margin: 1rem 0; }
29
- table { border-collapse: collapse; width: 100%; max-width: 56rem; }
30
- th, td { border: 1px solid #ccc; padding: 0.35rem 0.5rem; text-align: left; }
31
- th { background: #f4f4f4; }
32
- tr:nth-child(even) { background: #fafafa; }
33
- td.path { word-break: break-all; font-size: 0.9rem; }
34
- td.pct { white-space: nowrap; }
35
- </style>
36
- </head>
37
- <body>
38
- <h1>#{esc_title}</h1>
39
- <p class="summary">
40
- <strong>#{format("%.2f", summary[:line_percent])}%</strong> lines
41
- (#{summary[:lines_covered]} / #{summary[:lines_relevant]}) across #{summary[:files]} files
42
- </p>
43
- <table>
44
- <thead><tr><th>File</th><th>Coverage</th><th>Lines (covered / relevant)</th></tr></thead>
45
- <tbody>
46
- #{rows.join("\n")}
47
- </tbody>
48
- </table>
49
- </body>
50
- </html>
51
- HTML
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? ? "&nbsp;" : 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)
@@ -1,3 +1,3 @@
1
1
  module Polyrun
2
- VERSION = "1.4.1"
2
+ VERSION = "1.4.2"
3
3
  end
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.1
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