coverband 6.2.1 → 6.2.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.
@@ -10,6 +10,39 @@ module Coverband
10
10
  class WebTest < Minitest::Test
11
11
  include Rack::Test::Methods
12
12
 
13
+ class FakeViewsTracker
14
+ REPORT_ROUTE = "views_tracker"
15
+ TITLE = "Views"
16
+
17
+ attr_reader :used_keys
18
+
19
+ def initialize(used_keys:)
20
+ @used_keys = used_keys
21
+ end
22
+
23
+ def route
24
+ REPORT_ROUTE
25
+ end
26
+
27
+ def title
28
+ TITLE
29
+ end
30
+
31
+ def unused_keys
32
+ []
33
+ end
34
+
35
+ def tracking_since
36
+ "N/A"
37
+ end
38
+
39
+ def clear_key!(_key)
40
+ end
41
+
42
+ def reset_recordings
43
+ end
44
+ end
45
+
13
46
  def app
14
47
  Coverband::Reporters::Web.new
15
48
  end
@@ -55,6 +88,40 @@ module Coverband
55
88
  get "/unknown.js"
56
89
  assert last_response.not_found?
57
90
  end
91
+
92
+ test "views tracker defaults to sorting used keys by last activity" do
93
+ tracker = FakeViewsTracker.new(
94
+ used_keys: {
95
+ "app/views/a_first.html.erb" => "100",
96
+ "app/views/z_latest.html.erb" => "200"
97
+ }
98
+ )
99
+
100
+ Coverband.configuration.trackers = [tracker]
101
+ get "/views_tracker"
102
+
103
+ assert last_response.ok?
104
+ first_index = last_response.body.index("app/views/a_first.html.erb")
105
+ latest_index = last_response.body.index("app/views/z_latest.html.erb")
106
+ assert latest_index < first_index
107
+ end
108
+
109
+ test "views tracker supports alpha sort toggle" do
110
+ tracker = FakeViewsTracker.new(
111
+ used_keys: {
112
+ "app/views/z_latest.html.erb" => "200",
113
+ "app/views/a_first.html.erb" => "100"
114
+ }
115
+ )
116
+
117
+ Coverband.configuration.trackers = [tracker]
118
+ get "/views_tracker?used_sort=alpha"
119
+
120
+ assert last_response.ok?
121
+ first_index = last_response.body.index("app/views/a_first.html.erb")
122
+ latest_index = last_response.body.index("app/views/z_latest.html.erb")
123
+ assert first_index < latest_index
124
+ end
58
125
  end
59
126
  end
60
127
 
@@ -51,6 +51,15 @@ class TrackKeyTest < Minitest::Test
51
51
  assert Coverband.track_key(:routes_tracker, "index#show")
52
52
  end
53
53
 
54
+ test "track_key with query_burst_tracker" do
55
+ mock_query_burst_tracker = mock
56
+ payload = {key: "controller:users#index", queries: 42, sql_time_ms: 155.2}
57
+ mock_query_burst_tracker.expects(:track_key).with(payload).returns(true)
58
+ Coverband.configuration.expects(:query_burst_tracker).returns(mock_query_burst_tracker)
59
+
60
+ assert Coverband.track_key(:query_burst_tracker, payload)
61
+ end
62
+
54
63
  test "track_key logs error when tracking fails" do
55
64
  mock_logger = mock
56
65
  mock_logger.expects(:error).with(regexp_matches(/Failed to track key/))
@@ -38,7 +38,7 @@ class RailsRoutesWithoutConfigStackTest < Minitest::Test
38
38
  def shutdown_server
39
39
  if File.exist?("/tmp/testrack.pid")
40
40
  pid = `cat /tmp/testrack.pid`&.strip&.to_i
41
- Process.kill("HUP", pid)
41
+ Process.kill("TERM", pid)
42
42
  sleep 1
43
43
  end
44
44
  end
@@ -37,7 +37,7 @@ class RailsWithoutConfigStackTest < Minitest::Test
37
37
  def shutdown_server
38
38
  if File.exist?("/tmp/testrack.pid")
39
39
  pid = `cat /tmp/testrack.pid`&.strip&.to_i
40
- Process.kill("HUP", pid)
40
+ Process.kill("TERM", pid)
41
41
  sleep 1
42
42
  end
43
43
  end
@@ -0,0 +1,123 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createRequire } from "node:module";
4
+ const require = createRequire(import.meta.url);
5
+ const D = require("../../public/dead_code_session.js");
6
+
7
+ test("parseState returns empty state on garbage", () => {
8
+ assert.deepEqual(D.parseState(null), D.emptyState());
9
+ assert.deepEqual(D.parseState("{not json"), D.emptyState());
10
+ assert.deepEqual(D.parseState('{"hiddenPaths": "nope"}').hiddenPaths, []);
11
+ });
12
+
13
+ test("parseState keeps valid entries and normalizes marks", () => {
14
+ const s = D.parseState(JSON.stringify({
15
+ hiddenPaths: ["app/", 5, "lib/foo.rb"],
16
+ markedPaths: [{ path: "old/" }, { nope: true }],
17
+ hideRuntimeUsed: true
18
+ }));
19
+ assert.deepEqual(s.hiddenPaths, ["app/", "lib/foo.rb"]);
20
+ assert.deepEqual(s.markedPaths, [{ path: "old/", comment: "", markedAt: "" }]);
21
+ assert.equal(s.hideRuntimeUsed, true);
22
+ });
23
+
24
+ test("pathMatches: folder prefix vs exact file", () => {
25
+ assert.ok(D.pathMatches("app/models/", "app/models/foo.rb"));
26
+ assert.ok(!D.pathMatches("app/models/", "app/models2/foo.rb"));
27
+ assert.ok(D.pathMatches("app/foo.rb", "app/foo.rb"));
28
+ assert.ok(!D.pathMatches("app/foo.rb", "app/foo.rb.bak"));
29
+ });
30
+
31
+ test("isExcluded combines hides, marks, and runtime rule", () => {
32
+ const s = D.emptyState();
33
+ D.addHidden(s, "app/hidden/");
34
+ D.addMark(s, "lib/dead.rb", "bye", "2026-08-07T00:00:00Z");
35
+ assert.ok(D.isExcluded(s, "app/hidden/x.rb", 0));
36
+ assert.ok(D.isExcluded(s, "lib/dead.rb", 0));
37
+ assert.ok(!D.isExcluded(s, "app/live.rb", 12.5));
38
+ s.hideRuntimeUsed = true;
39
+ assert.ok(D.isExcluded(s, "app/live.rb", 12.5));
40
+ assert.ok(!D.isExcluded(s, "app/unused.rb", 0));
41
+ assert.ok(!D.isExcluded(s, "app/unknown.rb", null));
42
+ });
43
+
44
+ test("addHidden dedupes; removeHidden removes; clearHides resets toggle", () => {
45
+ const s = D.emptyState();
46
+ D.addHidden(s, "app/");
47
+ D.addHidden(s, "app/");
48
+ assert.deepEqual(s.hiddenPaths, ["app/"]);
49
+ D.removeHidden(s, "app/");
50
+ assert.deepEqual(s.hiddenPaths, []);
51
+ s.hideRuntimeUsed = true;
52
+ D.addHidden(s, "x.rb");
53
+ D.clearHides(s);
54
+ assert.deepEqual(s.hiddenPaths, []);
55
+ assert.equal(s.hideRuntimeUsed, false);
56
+ });
57
+
58
+ test("addMark replaces same path; updateComment; removeMark; clearMarks", () => {
59
+ const s = D.emptyState();
60
+ D.addMark(s, "a.rb", "one", "t1");
61
+ D.addMark(s, "a.rb", "two", "t2");
62
+ assert.deepEqual(s.markedPaths, [{ path: "a.rb", comment: "two", markedAt: "t2" }]);
63
+ D.updateComment(s, "a.rb", "three");
64
+ assert.equal(s.markedPaths[0].comment, "three");
65
+ D.removeMark(s, "a.rb");
66
+ assert.deepEqual(s.markedPaths, []);
67
+ D.addMark(s, "b.rb", "", "t");
68
+ D.clearMarks(s);
69
+ assert.deepEqual(s.markedPaths, []);
70
+ });
71
+
72
+ test("toCSV escapes per RFC 4180", () => {
73
+ const csv = D.toCSV([
74
+ { path: "app/a.rb", comment: 'has "quotes", commas\nand newline', markedAt: "2026-08-07T01:02:03Z" },
75
+ { path: "lib/", comment: "", markedAt: "" }
76
+ ]);
77
+ assert.equal(csv,
78
+ 'path,comment,marked_at\r\n' +
79
+ 'app/a.rb,"has ""quotes"", commas\nand newline",2026-08-07T01:02:03Z\r\n' +
80
+ 'lib/,,\r\n');
81
+ });
82
+
83
+ test("segmentPrefixes splits path into cumulative prefixes", () => {
84
+ assert.deepEqual(D.segmentPrefixes("app/models/foo.rb"), [
85
+ { label: "app", prefix: "app/", isFile: false },
86
+ { label: "models", prefix: "app/models/", isFile: false },
87
+ { label: "foo.rb", prefix: "app/models/foo.rb", isFile: true }
88
+ ]);
89
+ assert.deepEqual(D.segmentPrefixes("foo.rb"), [{ label: "foo.rb", prefix: "foo.rb", isFile: true }]);
90
+ });
91
+
92
+ test("isExcludedIgnoringMarks applies hides and runtime rule but never marks", () => {
93
+ const s = D.emptyState();
94
+ D.addHidden(s, "app/hidden/");
95
+ D.addMark(s, "lib/dead.rb", "bye", "2026-08-07T00:00:00Z");
96
+ assert.ok(D.isExcludedIgnoringMarks(s, "app/hidden/x.rb", 0));
97
+ assert.ok(!D.isExcludedIgnoringMarks(s, "lib/dead.rb", 0), "marks alone must not exclude");
98
+ s.hideRuntimeUsed = true;
99
+ assert.ok(D.isExcludedIgnoringMarks(s, "app/live.rb", 12.5));
100
+ assert.ok(!D.isExcludedIgnoringMarks(s, "app/unused.rb", 0));
101
+ });
102
+
103
+ test("extractPath pulls the title attribute out of a rendered cell", () => {
104
+ assert.equal(D.extractPath('<a class="src_link" title="app/models/foo.rb">foo.rb</a>'), "app/models/foo.rb");
105
+ assert.equal(D.extractPath("<span>no title here</span>"), "");
106
+ assert.equal(D.extractPath(42), "");
107
+ });
108
+
109
+ test("rowRuntime parses the runtime column, null when not numeric", () => {
110
+ assert.equal(D.rowRuntime(["path", "link", "12.5"]), 12.5);
111
+ assert.equal(D.rowRuntime(["path", "link", "0"]), 0);
112
+ assert.equal(D.rowRuntime(["path", "link", "-"]), null);
113
+ assert.equal(D.rowRuntime(["path", "link", undefined]), null);
114
+ });
115
+
116
+ test("escapeHtml escapes the five HTML-significant characters", () => {
117
+ assert.equal(D.escapeHtml('<b>"Tom" & Jerry</b>'), "&lt;b&gt;&quot;Tom&quot; &amp; Jerry&lt;/b&gt;");
118
+ assert.equal(D.escapeHtml("plain"), "plain");
119
+ });
120
+
121
+ test("csvFilename embeds the ISO date", () => {
122
+ assert.equal(D.csvFilename(new Date("2026-08-07T15:04:05Z")), "coverband-dead-code-2026-08-07.csv");
123
+ });
@@ -27,9 +27,23 @@
27
27
  </ul>
28
28
 
29
29
  <h2>Used <%= tracker.title %>: (<%= tracker.used_keys.length %>)</h2>
30
- <p>These <%= tracker.title %> have been rendered at least once</p>
30
+ <p>
31
+ These <%= tracker.title %> have been rendered at least once.
32
+ Sort:
33
+ <% if used_keys_sort == "last_active" %>
34
+ <strong>Last Active</strong>
35
+ <% else %>
36
+ <a href='<%= "#{base_path}/#{tracker.route}?used_sort=last_active" %>'>Last Active</a>
37
+ <% end %>
38
+ |
39
+ <% if used_keys_sort == "alpha" %>
40
+ <strong>Alphabetical</strong>
41
+ <% else %>
42
+ <a href='<%= "#{base_path}/#{tracker.route}?used_sort=alpha" %>'>Alphabetical</a>
43
+ <% end %>
44
+ </p>
31
45
  <ul>
32
- <% tracker.used_keys.each_pair do |key, time_at| %>
46
+ <% (sorted_used_keys || tracker.used_keys).each_pair do |key, time_at| %>
33
47
  <li class="used-keys">
34
48
  <%= key %>
35
49
  <span class="last_seen_at">last activity recorded <%= Time.at(time_at.to_i)%></span>
@@ -39,6 +53,39 @@
39
53
  </li>
40
54
  <% end %>
41
55
  </ul>
56
+
57
+ <% if tracker.respond_to?(:used_key_stats) %>
58
+ <% key_stats = tracker.used_key_stats %>
59
+ <% if key_stats.any? %>
60
+ <h3><%= tracker.title %> Stats</h3>
61
+ <table class="file_list sorted">
62
+ <thead>
63
+ <tr>
64
+ <th>Key</th>
65
+ <th>Requests</th>
66
+ <th>Total SQL Calls</th>
67
+ <th>Total SQL ms</th>
68
+ <th>Max SQL Calls</th>
69
+ <th>Max SQL ms</th>
70
+ <th>Threshold Hits</th>
71
+ </tr>
72
+ </thead>
73
+ <tbody>
74
+ <% key_stats.each_pair do |key, stats| %>
75
+ <tr>
76
+ <td><%= key %></td>
77
+ <td><%= stats["requests"].to_i %></td>
78
+ <td><%= stats["total_queries"].to_i %></td>
79
+ <td><%= stats["total_sql_time_ms"].to_f.round(2) %></td>
80
+ <td><%= stats["max_queries"].to_i %></td>
81
+ <td><%= stats["max_sql_time_ms"].to_f.round(2) %></td>
82
+ <td><%= stats["threshold_hits"].to_i %></td>
83
+ </tr>
84
+ <% end %>
85
+ </tbody>
86
+ </table>
87
+ <% end %>
88
+ <% end %>
42
89
  </div>
43
90
  <div id="footer">
44
91
  Generated by <a href="http://github.com/danmayer/coverband">Coverband</a> v<%= Coverband::VERSION %>
data/views/layout.erb CHANGED
@@ -4,6 +4,7 @@
4
4
  <title>Coverband: <%= Coverband::VERSION %></title>
5
5
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
6
6
  <script src='<%= assets_path('dependencies.js') %>' type='text/javascript'></script>
7
+ <script src='<%= assets_path('dead_code_session.js') %>' type='text/javascript'></script>
7
8
  <script src='<%= assets_path('application.js') %>' type='text/javascript'></script>
8
9
  <link href='<%= assets_path('application.css') %>' media='screen, projection, print' rel='stylesheet' type='text/css'>
9
10
  <link rel="shortcut icon" type="image/png" href="<%= assets_path("favicon_#{coverage_css_class(result.source_files.covered_percent)}.png") %>" />
data/views/nav.erb CHANGED
@@ -37,6 +37,11 @@
37
37
  <li class='<%= nav_options[:active_link] == 'routes_tracker' ? 'active' : '' %>'>
38
38
  <a href='<%= base_path %>routes_tracker'>Route Tracker</a>
39
39
  </li>
40
+ <% end %>
41
+ <% if Coverband.configuration.track_query_bursts %>
42
+ <li class='<%= nav_options[:active_link] == 'query_bursts_tracker' ? 'active' : '' %>'>
43
+ <a href='<%= base_path %>query_bursts_tracker'>Query Burst Tracker</a>
44
+ </li>
40
45
  <% end %>
41
46
  <% if Coverband.configuration.track_translations %>
42
47
  <li class='<%= nav_options[:active_link] == 'translations_tracker' ? 'active' : '' %>'>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coverband
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.2.1
4
+ version: 6.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Mayer
@@ -365,6 +365,7 @@ files:
365
365
  - lib/coverband/collectors/abstract_tracker.rb
366
366
  - lib/coverband/collectors/coverage.rb
367
367
  - lib/coverband/collectors/delta.rb
368
+ - lib/coverband/collectors/query_burst_tracker.rb
368
369
  - lib/coverband/collectors/route_tracker.rb
369
370
  - lib/coverband/collectors/translation_tracker.rb
370
371
  - lib/coverband/collectors/view_tracker.rb
@@ -421,6 +422,7 @@ files:
421
422
  - public/colorbox/controls.png
422
423
  - public/colorbox/loading.gif
423
424
  - public/colorbox/loading_background.png
425
+ - public/dead_code_session.js
424
426
  - public/dependencies.js
425
427
  - public/favicon.png
426
428
  - public/favicon_green.png
@@ -461,8 +463,10 @@ files:
461
463
  - test/coverband/adapters/redis_store_test.rb
462
464
  - test/coverband/adapters/web_service_store_test.rb
463
465
  - test/coverband/at_exit_test.rb
466
+ - test/coverband/benchmarks/benchmark_memory_leak_check_test.rb
464
467
  - test/coverband/collectors/coverage_test.rb
465
468
  - test/coverband/collectors/delta_test.rb
469
+ - test/coverband/collectors/query_burst_tracker_test.rb
466
470
  - test/coverband/collectors/route_tracker_test.rb
467
471
  - test/coverband/collectors/translation_tracker_test.rb
468
472
  - test/coverband/collectors/view_tracker_test.rb
@@ -528,6 +532,7 @@ files:
528
532
  - test/integration/full_stack_send_deferred_eager_test.rb
529
533
  - test/integration/full_stack_test.rb
530
534
  - test/integration/mcp_integration_test.rb
535
+ - test/javascript/dead_code_session.test.mjs
531
536
  - test/jruby_check.rb
532
537
  - test/rails7_dummy/Rakefile
533
538
  - test/rails7_dummy/app/controllers/dummy_controller.rb