coverband 6.1.8 → 6.2.1
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/README.md +68 -8
- data/changes.md +17 -0
- data/lib/coverband/collectors/abstract_tracker.rb +17 -1
- data/lib/coverband/collectors/view_tracker.rb +11 -1
- data/lib/coverband/configuration.rb +3 -0
- data/lib/coverband/mcp/http_handler.rb +27 -26
- data/lib/coverband/mcp/tools/get_dead_methods.rb +5 -5
- data/lib/coverband/reporters/web.rb +6 -4
- data/lib/coverband/utils/configuration_template.rb +4 -0
- data/lib/coverband/utils/dead_methods.rb +4 -0
- data/lib/coverband/utils/file_list.rb +1 -0
- data/lib/coverband/utils/html_formatter.rb +4 -1
- data/lib/coverband/utils/tasks.rb +18 -0
- data/lib/coverband/version.rb +1 -1
- data/test/benchmarks/benchmark_file_list_covered_lines.rb +54 -0
- data/test/coverband/collectors/view_tracker_test.rb +27 -0
- data/test/coverband/configuration_test.rb +12 -0
- data/test/coverband/mcp/http_handler_test.rb +58 -46
- data/test/coverband/mcp/tools/get_dead_methods_test.rb +20 -10
- data/test/coverband/reporters/web_test.rb +10 -0
- data/test/coverband/utils/dead_methods_test.rb +20 -0
- data/test/integration/mcp_integration_test.rb +3 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c47e38934426a369c81dd7d08e5474ad0d8a784b541975c1aa7ee50fc9e2d1ce
|
|
4
|
+
data.tar.gz: 346c8263e9c961631bcf4af3019d8473177088c5b9ae3cb3844dc7d86bcd7497
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 907a280ce513b23922b81c44674c135ab067e629ae9f8aaec6eb6a667946a52a85fd1e7a0fe17ee9b24c0a6b81b8b22bcb1e1c53e7ce75ccc7a9c3507b6dd183
|
|
7
|
+
data.tar.gz: 60198f0536094c5505ea532b238d9d38359f936edb6ea21854c946d3a5f63e7e02b66f1240596b787cad898546a3d48d41595d5d4cab28bb09face2a4f405499
|
data/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
<img src="https://raw.github.com/danmayer/coverband/
|
|
1
|
+
<img src="https://raw.github.com/danmayer/coverband/main/docs/assets/logo/heads.svg?sanitize=true" width='300'>
|
|
2
2
|
|
|
3
3
|
# Coverband
|
|
4
4
|
|
|
5
5
|
[](https://github.com/danmayer/coverband/actions)
|
|
6
|
-
[](https://coveralls.io/github/danmayer/coverband?branch=main)
|
|
7
7
|
[](https://codeclimate.com/github/danmayer/coverband/maintainability)
|
|
8
8
|
[](https://discord.gg/KAH38EV)
|
|
9
9
|
|
|
@@ -45,6 +45,28 @@ Coverband stores coverage data in Redis. The Redis endpoint is looked for in thi
|
|
|
45
45
|
|
|
46
46
|
The redis store can also be explicitly defined within the `config/coverband.rb`. See [advanced config](#advanced-config).
|
|
47
47
|
|
|
48
|
+
### Redis with TLS
|
|
49
|
+
|
|
50
|
+
For Redis servers that require TLS (such as AWS serverless ElastiCache), use the `rediss://` URL scheme instead of `redis://`. The Redis gem automatically enables TLS when it detects this scheme:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Example with AWS serverless ElastiCache
|
|
54
|
+
REDIS_URL=rediss://my-elasticache.abcdef.cache.amazonaws.com:6379
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or in `config/coverband.rb`:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
config.store = Coverband::Adapters::RedisStore.new(
|
|
61
|
+
Redis.new(url: "rediss://my-elasticache-endpoint:6379")
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The `rediss://` scheme works with:
|
|
66
|
+
- AWS ElastiCache (serverless or provisioned with required TLS)
|
|
67
|
+
- Any self-hosted Redis with TLS enabled
|
|
68
|
+
- Other managed Redis services that require TLS
|
|
69
|
+
|
|
48
70
|
## Gem Installation
|
|
49
71
|
|
|
50
72
|
Add this line to your application's `Gemfile`, remember to `bundle install` after updating:
|
|
@@ -57,7 +79,7 @@ gem 'coverband'
|
|
|
57
79
|
|
|
58
80
|
With older versions of Coverband, projects would report to redis using rack or sidekiq middleware. After Coverband 4.0, this should no longer be required and could cause performance issues. Reporting to redis is now automatically done within a background thread with no custom code needed.
|
|
59
81
|
|
|
60
|
-
See [changelog](https://github.com/danmayer/coverband/blob/
|
|
82
|
+
See [changelog](https://github.com/danmayer/coverband/blob/main/changes.md).
|
|
61
83
|
|
|
62
84
|
## Rails
|
|
63
85
|
|
|
@@ -77,10 +99,12 @@ run ActionController::Dispatcher.new
|
|
|
77
99
|
|
|
78
100
|
## Coverband Web UI
|
|
79
101
|
|
|
80
|
-

|
|
81
103
|
|
|
82
104
|
> You can check it out locally by running the [Coverband Demo App](https://github.com/danmayer/coverband_rails_example).
|
|
83
105
|
|
|
106
|
+
- View a shared demo at [https://coverband-rails-example.onrender.com/](https://coverband-rails-example.onrender.com/)
|
|
107
|
+
|
|
84
108
|
- View overall coverage information
|
|
85
109
|
|
|
86
110
|
- Drill into individual file coverage
|
|
@@ -208,7 +232,7 @@ Take Coverband for a spin on the live Heroku deployed [Coverband Demo](https://c
|
|
|
208
232
|
|
|
209
233
|
If you need to configure Coverband, this can be done by creating a `config/coverband.rb` file relative to your project root.
|
|
210
234
|
|
|
211
|
-
- See [lib/coverband/configuration.rb](https://github.com/danmayer/coverband/blob/
|
|
235
|
+
- See [lib/coverband/configuration.rb](https://github.com/danmayer/coverband/blob/main/lib/coverband/configuration.rb) for all options
|
|
212
236
|
- By default Coverband will try to store data to Redis \* Redis endpoint is looked for in this order: `ENV['COVERBAND_REDIS_URL']`, `ENV['REDIS_URL']`, or `localhost`
|
|
213
237
|
|
|
214
238
|
Below is an example config file for a Rails 5 app:
|
|
@@ -269,7 +293,21 @@ This feature is enabled by default. To stop this feature, disable the feature in
|
|
|
269
293
|
|
|
270
294
|
`config.track_views = false`
|
|
271
295
|
|
|
272
|
-
|
|
296
|
+
#### ViewComponent Support
|
|
297
|
+
|
|
298
|
+
Coverband can also track [ViewComponent](https://viewcomponent.org/) renders. This requires:
|
|
299
|
+
|
|
300
|
+
- ViewComponent **>= 4.6.0**
|
|
301
|
+
- Instrumentation enabled in your app config:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
# config/application.rb (or environment-specific config)
|
|
305
|
+
config.view_component.instrumentation_enabled = true
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
When enabled, Coverband subscribes to the `render.view_component` notification and tracks which component templates are rendered, just like standard Rails views.
|
|
309
|
+
|
|
310
|
+

|
|
273
311
|
|
|
274
312
|
### Hiding settings
|
|
275
313
|
|
|
@@ -312,6 +350,8 @@ Add a wiggle (in seconds) to the background thread to avoid all your servers rep
|
|
|
312
350
|
|
|
313
351
|
`config.reporting_wiggle = 30`
|
|
314
352
|
|
|
353
|
+
The default 30-second wiggle is suitable for most deployments. For larger fleets you may want to increase the wiggle proportionally (e.g. 1 second per server). The tradeoff is a small delay in coverage reporting freshness. The `reporting_wiggle` and `background_reporting_sleep_seconds` options work together — consider configuring both when tuning background reporting for your environment.
|
|
354
|
+
|
|
315
355
|
Another way to avoid cache stampede is to omit some reporting on starting servers. Coverband stores the results of eager_loading to Redis at server startup. The eager_loading results are the same for all servers, so there is no need to save all results. By configuring the eager_loading results of some servers to be stored in Redis, we can reduce the load on Redis during deployment.
|
|
316
356
|
|
|
317
357
|
```ruby
|
|
@@ -323,11 +363,21 @@ config.send_deferred_eager_loading_data = rand(100) < 5
|
|
|
323
363
|
config.send_deferred_eager_loading_data = ENV.fetch('ENABLE_EAGER_LOADING_COVERAGE', false)
|
|
324
364
|
```
|
|
325
365
|
|
|
366
|
+
Note: `defer_eager_loading_data = true` is required in order to use `send_deferred_eager_loading_data`. With both set, eager_loading data is deferred at startup and only sent by the subset of servers where `send_deferred_eager_loading_data` evaluates to `true`.
|
|
367
|
+
|
|
368
|
+
### Oneshot Mode
|
|
369
|
+
|
|
370
|
+
Enabling [oneshot mode](https://docs.ruby-lang.org/en/master/Coverage.html#module-coverage-oneshot-lines-coverage) reduces the Ruby CPU overhead. The tradeoff is that you no longer get frequency data — you will only know whether a line was executed at least once, not how many times.
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
config.use_oneshot_lines_coverage = true
|
|
374
|
+
```
|
|
375
|
+
|
|
326
376
|
### Redis Hash Store
|
|
327
377
|
|
|
328
378
|
Coverband on very high volume sites with many server processes reporting can have a race condition which can cause hit counts to be inaccurate. To resolve the race condition and reduce Ruby memory overhead we have introduced a new Redis storage option. This moves the some of the work from the Ruby processes to Redis. It is worth noting because of this, it has larger demands on the Redis server. So adjust your Redis instance accordingly. To help reduce the extra redis load you can also change the background reporting frequency.
|
|
329
379
|
|
|
330
|
-
-
|
|
380
|
+
- To use Redis Hash Store: `config.store = Coverband::Adapters::HashRedisStore.new(Redis.new(url: redis_url))`
|
|
331
381
|
- Adjust from default 30s reporting `config.background_reporting_sleep_seconds = 120`
|
|
332
382
|
|
|
333
383
|
See more discussion [here](https://github.com/danmayer/coverband/issues/384).
|
|
@@ -400,10 +450,20 @@ gem 'coverband', require: ['alternative_coverband_patch']
|
|
|
400
450
|
|
|
401
451
|
This conflict happens when a ruby method is patched twice, once using module prepend, and once using method aliasing. See this ruby issue for details. The fix is to apply all patches the same way. By default, Coverband will apply its patch using prepend, but you can change that to method aliasing by adding require: ['alternative_coverband_patch'] to the gem line as shown above.
|
|
402
452
|
|
|
403
|
-
### Redis Sizing Info
|
|
453
|
+
### Redis Sizing & Configuration Info
|
|
404
454
|
|
|
405
455
|
A few folks have asked about what size of Redis is needed to run Coverband. I have some of our largest services with hundreds of servers on cache.m3.medium with plenty of room to spare. I run most apps on the smallest AWS Redis instances available and bump up only if needed or if I am forced to be on a shared Redis instance, which I try to avoid. On Heroku, I have used it with most of the 3rd party and also been fine on the smallest Redis instances, if you have hundreds of dynos you would likely need to scale up. Also note there is a tradeoff one can make, `Coverband::Adapters::HashRedisStore` will use LUA on Redis and increase the Redis load, while being nicer to your app servers and avoid potential lost data during race conditions. While the `Coverband::Adapters::RedisStore` uses in app memory and merging and has lower load on Redis.
|
|
406
456
|
|
|
457
|
+
For a dedicated Coverband Redis instance, `allkeys-lfu` is a good choice for `maxmemory-policy` as it evicts the least-frequently-used keys first, meaning rarely-hit files are evicted before frequently-reported ones. On a shared Redis instance, be cautious with eviction policies that could interfere with other data. See [issue #595](https://github.com/danmayer/coverband/issues/595) for more discussion.
|
|
458
|
+
|
|
459
|
+
### Ruby Overhead Reduction Checklist
|
|
460
|
+
|
|
461
|
+
If Coverband is adding meaningful latency to your application, work through this checklist:
|
|
462
|
+
|
|
463
|
+
* Enable oneshot mode (`config.use_oneshot_lines_coverage = true`) — reduces CPU overhead by only tracking whether a line ran, not how many times
|
|
464
|
+
* Enable Redis Hash Store — moves merging work from Ruby processes to Redis, reducing per-process memory overhead
|
|
465
|
+
* Only enable Coverband on a subset of server instances — requests routed to those servers will be slightly slower, but coverage data will still be gathered. This is most effective with a Least Outstanding Requests load balancing algorithm. Avoid enabling on all servers but only reporting on a subset of requests — loading the coverage module itself has a performance impact regardless of reporting.
|
|
466
|
+
|
|
407
467
|
# Newer Features
|
|
408
468
|
|
|
409
469
|
### MCP Server for AI Assistants
|
data/changes.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
### 6.2.1
|
|
2
|
+
|
|
3
|
+
* Fix: Search now includes full filenames hidden by truncation in the web UI; users can search for complete file paths even when displaying truncated versions (#569)
|
|
4
|
+
* Feature: Added support for TLS Redis connections via `rediss://` URL scheme for use with serverless ElastiCache and other TLS-required Redis services (#580)
|
|
5
|
+
* Fix: MCP HTTP handler now properly uses `StreamableHTTPTransport` for full HTTP protocol support including GET, DELETE, and OPTIONS methods (#604)
|
|
6
|
+
* Fix: Suppressed Rails logger stdout pollution in stdio MCP mode to ensure clean JSON-RPC stream for Claude Desktop and other AI clients (#625)
|
|
7
|
+
* Fix: Tracker persistence now gracefully handles non-Redis stores (FileStore, NullStore) without raising errors at process exit (#637)
|
|
8
|
+
* Fix: Resolved Rack 1.6 compatibility issue in web reporter by detecting `Rack::Files` vs `Rack::File` class (#639)
|
|
9
|
+
* Docs: Added TLS Redis setup documentation to README for serverless ElastiCache configurations
|
|
10
|
+
|
|
11
|
+
### 6.2.0
|
|
12
|
+
|
|
13
|
+
* Fix: Correct `Rack::Static` URL handling in the web reporter by using an empty string URL prefix; thanks @alpaca-tc (#635)
|
|
14
|
+
* Fix: Skip missing files during dead method scans to avoid errors in dynamic or removed-file environments; thanks @doug-hall (#632)
|
|
15
|
+
* Fix: Improve MCP dead method formatting for clearer tool output; thanks @doug-hall (#633)
|
|
16
|
+
* Docs: README updates (including `master` → `main` wording updates); thanks @jjb (#627)
|
|
17
|
+
|
|
1
18
|
### 6.1.8
|
|
2
19
|
|
|
3
20
|
* Feature: Added temporal data to GetFileCoverage tool for MCP by @fabienpiette (#626)
|
|
@@ -53,6 +53,8 @@ module Coverband
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def used_keys
|
|
56
|
+
return {} unless redis_store
|
|
57
|
+
|
|
56
58
|
redis_store.hgetall(tracker_key)
|
|
57
59
|
end
|
|
58
60
|
|
|
@@ -74,6 +76,8 @@ module Coverband
|
|
|
74
76
|
end
|
|
75
77
|
|
|
76
78
|
def tracking_since
|
|
79
|
+
return "N/A" unless redis_store
|
|
80
|
+
|
|
77
81
|
if (tracking_time = redis_store.get(tracker_time_key))
|
|
78
82
|
Time.at(tracking_time.to_i).iso8601
|
|
79
83
|
else
|
|
@@ -82,18 +86,24 @@ module Coverband
|
|
|
82
86
|
end
|
|
83
87
|
|
|
84
88
|
def reset_recordings
|
|
89
|
+
return unless redis_store
|
|
90
|
+
|
|
85
91
|
redis_store.del(tracker_key)
|
|
86
92
|
redis_store.del(tracker_time_key)
|
|
87
93
|
end
|
|
88
94
|
|
|
89
95
|
def clear_key!(key)
|
|
90
96
|
return unless key
|
|
97
|
+
return unless redis_store
|
|
98
|
+
|
|
91
99
|
puts "#{tracker_key} key #{key}"
|
|
92
100
|
redis_store.hdel(tracker_key, key)
|
|
93
101
|
@logged_keys.delete(key)
|
|
94
102
|
end
|
|
95
103
|
|
|
96
104
|
def save_report
|
|
105
|
+
return unless redis_store
|
|
106
|
+
|
|
97
107
|
redis_store.set(tracker_time_key, Time.now.to_i) unless @one_time_timestamp || tracker_time_key_exists?
|
|
98
108
|
@one_time_timestamp = true
|
|
99
109
|
reported_time = Time.now.to_i
|
|
@@ -138,10 +148,16 @@ module Coverband
|
|
|
138
148
|
end
|
|
139
149
|
|
|
140
150
|
def redis_store
|
|
141
|
-
|
|
151
|
+
@redis_store ||= begin
|
|
152
|
+
store.raw_store
|
|
153
|
+
rescue NotImplementedError
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
142
156
|
end
|
|
143
157
|
|
|
144
158
|
def tracker_time_key_exists?
|
|
159
|
+
return false unless redis_store
|
|
160
|
+
|
|
145
161
|
if defined?(redis_store.exists?)
|
|
146
162
|
redis_store.exists?(tracker_time_key)
|
|
147
163
|
else
|
|
@@ -33,6 +33,13 @@ module Coverband
|
|
|
33
33
|
ActiveSupport::Notifications.subscribe(/render_(template|partial|collection).action_view/) do |name, start, finish, id, payload|
|
|
34
34
|
Coverband.configuration.view_tracker.track_key(payload) unless name.include?("!")
|
|
35
35
|
end
|
|
36
|
+
|
|
37
|
+
# ViewComponent >= 4.6.0 emits render.view_component with a view_identifier key
|
|
38
|
+
# containing the template path. Requires config.view_component.instrumentation_enabled = true
|
|
39
|
+
# in the host app.
|
|
40
|
+
ActiveSupport::Notifications.subscribe("render.view_component") do |name, start, finish, id, payload|
|
|
41
|
+
Coverband.configuration.view_tracker.track_key(identifier: payload[:view_identifier]) unless name.include?("!")
|
|
42
|
+
end
|
|
36
43
|
end
|
|
37
44
|
|
|
38
45
|
###
|
|
@@ -61,6 +68,8 @@ module Coverband
|
|
|
61
68
|
end
|
|
62
69
|
|
|
63
70
|
def used_keys
|
|
71
|
+
return {} unless redis_store
|
|
72
|
+
|
|
64
73
|
views = redis_store.hgetall(tracker_key)
|
|
65
74
|
normalized_views = {}
|
|
66
75
|
views.each_pair do |view, time|
|
|
@@ -93,6 +102,7 @@ module Coverband
|
|
|
93
102
|
|
|
94
103
|
def clear_key!(filename)
|
|
95
104
|
return unless filename
|
|
105
|
+
return unless redis_store
|
|
96
106
|
|
|
97
107
|
filename = "#{@project_directory}/#{filename}"
|
|
98
108
|
redis_store.hdel(tracker_key, filename)
|
|
@@ -126,7 +136,7 @@ module Coverband
|
|
|
126
136
|
|
|
127
137
|
def concrete_target
|
|
128
138
|
if defined?(Rails.application)
|
|
129
|
-
Dir.glob("#{@project_directory}/{,packs
|
|
139
|
+
Dir.glob("#{@project_directory}/{,packs/*,engines/*}/app/{views,components}/**/*.html.{erb,haml,slim}")
|
|
130
140
|
else
|
|
131
141
|
[]
|
|
132
142
|
end
|
|
@@ -213,6 +213,9 @@ module Coverband
|
|
|
213
213
|
Coverband::Adapters::WebServiceStore.new(service_url)
|
|
214
214
|
else
|
|
215
215
|
begin
|
|
216
|
+
# Redis gem automatically enables TLS when the scheme is 'rediss://'
|
|
217
|
+
# For serverless ElastiCache or other TLS-required Redis instances, use:
|
|
218
|
+
# REDIS_URL=rediss://your-endpoint:6379
|
|
216
219
|
Coverband::Adapters::RedisStore.new(Redis.new(url: redis_url), redis_store_options)
|
|
217
220
|
rescue Redis::CannotConnectError => error
|
|
218
221
|
logger.info "Redis is not available (#{error}), defaulting to NullStore"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Coverband
|
|
4
4
|
module MCP
|
|
5
|
-
# Rack middleware that adds MCP HTTP endpoint support.
|
|
5
|
+
# Rack middleware that adds MCP HTTP endpoint support using StreamableHTTPTransport.
|
|
6
6
|
# Can be used to wrap the existing Coverband::Reporters::Web app
|
|
7
7
|
# or mounted standalone.
|
|
8
8
|
#
|
|
@@ -10,7 +10,7 @@ module Coverband
|
|
|
10
10
|
# map "/coverage" do
|
|
11
11
|
# run Coverband::MCP::HttpHandler.new(Coverband::Reporters::Web.new)
|
|
12
12
|
# end
|
|
13
|
-
# # MCP endpoint available at
|
|
13
|
+
# # MCP endpoint available at /coverage/mcp with full Streamable HTTP transport support
|
|
14
14
|
#
|
|
15
15
|
# Usage standalone:
|
|
16
16
|
# map "/mcp" do
|
|
@@ -23,6 +23,7 @@ module Coverband
|
|
|
23
23
|
def initialize(app = nil)
|
|
24
24
|
@app = app
|
|
25
25
|
@server = nil
|
|
26
|
+
@transport = nil
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def call(env)
|
|
@@ -40,17 +41,22 @@ module Coverband
|
|
|
40
41
|
private
|
|
41
42
|
|
|
42
43
|
def mcp_request?(request)
|
|
43
|
-
|
|
44
|
+
# Accept GET, POST, DELETE, OPTIONS for StreamableHTTPTransport protocol
|
|
45
|
+
%w[GET POST DELETE OPTIONS].include?(request.request_method) &&
|
|
46
|
+
request.path_info.end_with?(MCP_PATH)
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
def handle_mcp_request(request)
|
|
50
|
+
# Handle CORS preflight
|
|
51
|
+
return cors_preflight_response if request.request_method == "OPTIONS"
|
|
52
|
+
|
|
47
53
|
# Check authentication if MCP password is configured
|
|
48
54
|
unless authenticate_mcp_request(request)
|
|
49
55
|
return [401, {
|
|
50
56
|
"Content-Type" => "application/json",
|
|
51
57
|
"Access-Control-Allow-Origin" => "*",
|
|
52
|
-
"Access-Control-Allow-Methods" => "POST, OPTIONS",
|
|
53
|
-
"Access-Control-Allow-Headers" => "Content-Type, Authorization",
|
|
58
|
+
"Access-Control-Allow-Methods" => "GET, POST, DELETE, OPTIONS",
|
|
59
|
+
"Access-Control-Allow-Headers" => "Content-Type, Authorization, Accept, Mcp-Session-Id",
|
|
54
60
|
"WWW-Authenticate" => 'Bearer realm="Coverband MCP"'
|
|
55
61
|
}, [JSON.generate({
|
|
56
62
|
"error" => "Authentication required",
|
|
@@ -58,29 +64,21 @@ module Coverband
|
|
|
58
64
|
})]]
|
|
59
65
|
end
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# response might already be a JSON string, so check before converting
|
|
66
|
-
response_body = response.is_a?(String) ? response : response.to_json
|
|
67
|
-
|
|
68
|
-
[
|
|
69
|
-
200,
|
|
70
|
-
{
|
|
71
|
-
"content-type" => "application/json",
|
|
72
|
-
"access-control-allow-origin" => "*",
|
|
73
|
-
"access-control-allow-methods" => "POST, OPTIONS",
|
|
74
|
-
"access-control-allow-headers" => "Content-Type"
|
|
75
|
-
},
|
|
76
|
-
[response_body]
|
|
77
|
-
]
|
|
78
|
-
rescue JSON::ParserError => e
|
|
79
|
-
error_response(400, "Invalid JSON: #{e.message}")
|
|
67
|
+
# Delegate to StreamableHTTPTransport which handles the full MCP HTTP protocol
|
|
68
|
+
# (GET for SSE streams, POST for requests/responses, DELETE for cleanup, etc.)
|
|
69
|
+
transport.handle_request(request)
|
|
80
70
|
rescue => e
|
|
81
71
|
error_response(500, "Server error: #{e.message}")
|
|
82
72
|
end
|
|
83
73
|
|
|
74
|
+
def cors_preflight_response
|
|
75
|
+
[204, {
|
|
76
|
+
"Access-Control-Allow-Origin" => "*",
|
|
77
|
+
"Access-Control-Allow-Methods" => "GET, POST, DELETE, OPTIONS",
|
|
78
|
+
"Access-Control-Allow-Headers" => "Content-Type, Authorization, Accept, Mcp-Session-Id"
|
|
79
|
+
}, []]
|
|
80
|
+
end
|
|
81
|
+
|
|
84
82
|
def authenticate_mcp_request(request)
|
|
85
83
|
# If no MCP password is configured, allow access
|
|
86
84
|
mcp_password = Coverband.configuration.mcp_password
|
|
@@ -98,8 +96,11 @@ module Coverband
|
|
|
98
96
|
token == mcp_password
|
|
99
97
|
end
|
|
100
98
|
|
|
101
|
-
def
|
|
102
|
-
@
|
|
99
|
+
def transport
|
|
100
|
+
@transport ||= begin
|
|
101
|
+
server = ::Coverband::MCP::Server.new
|
|
102
|
+
::MCP::Server::Transports::StreamableHTTPTransport.new(server.mcp_server)
|
|
103
|
+
end
|
|
103
104
|
end
|
|
104
105
|
|
|
105
106
|
def error_response(status, message)
|
|
@@ -28,21 +28,21 @@ module Coverband
|
|
|
28
28
|
|
|
29
29
|
if file_pattern
|
|
30
30
|
dead_methods = dead_methods.select do |method|
|
|
31
|
-
File.fnmatch(file_pattern, method
|
|
31
|
+
File.fnmatch(file_pattern, method.file_path, File::FNM_PATHNAME)
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Group by file for easier reading
|
|
36
|
-
grouped = dead_methods.group_by
|
|
36
|
+
grouped = dead_methods.group_by(&:file_path)
|
|
37
37
|
|
|
38
38
|
result = grouped.map do |file_path, methods|
|
|
39
39
|
{
|
|
40
40
|
file: file_path,
|
|
41
41
|
dead_methods: methods.map do |m|
|
|
42
42
|
{
|
|
43
|
-
class_name: m
|
|
44
|
-
method_name: m
|
|
45
|
-
line_number: m
|
|
43
|
+
class_name: m.class_name,
|
|
44
|
+
method_name: m.name,
|
|
45
|
+
line_number: m.first_line_number
|
|
46
46
|
}
|
|
47
47
|
end
|
|
48
48
|
}
|
|
@@ -32,9 +32,11 @@ module Coverband
|
|
|
32
32
|
|
|
33
33
|
def init_web
|
|
34
34
|
full_path = Gem::Specification.find_by_name("coverband").full_gem_path
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
# Rack::Files was introduced in Rack 2.0; Rack 1.x uses Rack::File
|
|
36
|
+
rack_file_server = defined?(Rack::Files) ? Rack::Files : Rack::File
|
|
37
|
+
@file_server = rack_file_server.new(
|
|
38
|
+
File.expand_path("public", full_path)
|
|
39
|
+
)
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
def check_auth
|
|
@@ -85,7 +87,7 @@ module Coverband
|
|
|
85
87
|
else
|
|
86
88
|
case request_path_info
|
|
87
89
|
when /.*\.(css|js|gif|png)/
|
|
88
|
-
@
|
|
90
|
+
@file_server.get(env)
|
|
89
91
|
when %r{/settings}
|
|
90
92
|
[200, coverband_headers, [settings]]
|
|
91
93
|
when %r{/view_tracker_data}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
# Coverband.configure do |config|
|
|
8
8
|
####
|
|
9
9
|
# set a redis URL and set it with with some reasonable timeouts
|
|
10
|
+
# For TLS connections (e.g. AWS serverless ElastiCache), use rediss:// scheme
|
|
10
11
|
####
|
|
11
12
|
# redis_url = ENV["COVERBAND_REDIS"] || ENV["REDIS_URL"] || "redis://localhost:6379"
|
|
12
13
|
# config.store = Coverband::Adapters::RedisStore.new(
|
|
@@ -18,6 +19,9 @@
|
|
|
18
19
|
# reconnect_delay_max: ENV.fetch("REDIS_RECONNECT_DELAY_MAX", 2.5)
|
|
19
20
|
# )
|
|
20
21
|
# )
|
|
22
|
+
#
|
|
23
|
+
# Example with TLS (rediss:// scheme automatically enables TLS):
|
|
24
|
+
# redis_url = "rediss://my-elasticache.abcdef.cache.amazonaws.com:6379"
|
|
21
25
|
|
|
22
26
|
# Allow folks to reset the coverband data via the web UI
|
|
23
27
|
# config.web_enable_clear = true
|
|
@@ -43,7 +43,11 @@ module Coverband
|
|
|
43
43
|
# and runtime phases.
|
|
44
44
|
coverage = Coverband.configuration.store.get_coverage_report[Coverband::MERGED_TYPE]
|
|
45
45
|
coverage.flat_map do |file_path, coverage|
|
|
46
|
+
next [] unless File.exist?(file_path)
|
|
47
|
+
|
|
46
48
|
scan(file_path: file_path, coverage: coverage["data"])
|
|
49
|
+
rescue Errno::ENOENT
|
|
50
|
+
[]
|
|
47
51
|
end
|
|
48
52
|
end
|
|
49
53
|
|
|
@@ -180,7 +180,10 @@ module Coverband
|
|
|
180
180
|
|
|
181
181
|
def link_to_source_file(source_file)
|
|
182
182
|
data_loader_url = "#{base_path}load_file_details?filename=#{source_file.filename}"
|
|
183
|
-
|
|
183
|
+
full_filename = shortened_filename(source_file)
|
|
184
|
+
truncated_filename = truncate(full_filename)
|
|
185
|
+
# Include full filename in a hidden span so DataTables search can find it even when truncated
|
|
186
|
+
%(<a href="##{id source_file}" class="src_link" title="#{full_filename}" data-loader-url="#{data_loader_url}" onclick="src_link_click(this)">#{truncated_filename}<span style="display:none;">#{full_filename}</span></a>)
|
|
184
187
|
end
|
|
185
188
|
|
|
186
189
|
def truncate(text, length: 50)
|
|
@@ -128,10 +128,28 @@ namespace :coverband do
|
|
|
128
128
|
|
|
129
129
|
desc "Start MCP server for AI assistant integration (set COVERBAND_MCP_HTTP=true for HTTP mode)"
|
|
130
130
|
task :mcp do
|
|
131
|
+
# In stdio mode, we must suppress all non-JSON-RPC output to stdout to comply with
|
|
132
|
+
# the MCP stdio transport spec. Otherwise, Rails logger and gem output will pollute
|
|
133
|
+
# the JSON-RPC stream and break clients like Claude Desktop.
|
|
134
|
+
# See https://github.com/danmayer/coverband/issues/625
|
|
135
|
+
use_stdio_mode = !ENV["COVERBAND_MCP_HTTP"]
|
|
136
|
+
|
|
137
|
+
original_stdout = nil
|
|
138
|
+
if use_stdio_mode
|
|
139
|
+
# Save original stdout and redirect stdout to stderr temporarily
|
|
140
|
+
original_stdout = $stdout
|
|
141
|
+
$stdout = $stderr
|
|
142
|
+
end
|
|
143
|
+
|
|
131
144
|
if Rake::Task.task_defined?("environment")
|
|
132
145
|
Rake.application["environment"].invoke
|
|
133
146
|
end
|
|
134
147
|
|
|
148
|
+
if use_stdio_mode
|
|
149
|
+
# Restore stdout for JSON-RPC communication over stdio
|
|
150
|
+
$stdout = original_stdout
|
|
151
|
+
end
|
|
152
|
+
|
|
135
153
|
begin
|
|
136
154
|
require "coverband/mcp"
|
|
137
155
|
rescue LoadError
|
data/lib/coverband/version.rb
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "benchmark/ips"
|
|
4
|
+
require "memory_profiler"
|
|
5
|
+
|
|
6
|
+
# Mock SourceFile to simulate the overhead of creating line objects
|
|
7
|
+
class MockSourceFile
|
|
8
|
+
attr_reader :covered_lines_count
|
|
9
|
+
|
|
10
|
+
def initialize(count)
|
|
11
|
+
@covered_lines_count = count
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def covered_lines
|
|
15
|
+
# Simulate allocating an array of objects
|
|
16
|
+
Array.new(@covered_lines_count) { Object.new }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class FileList < Array
|
|
21
|
+
def covered_lines_original
|
|
22
|
+
return 0.0 if empty?
|
|
23
|
+
map { |f| f.covered_lines.count }.inject(:+)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def covered_lines_optimized
|
|
27
|
+
return 0.0 if empty?
|
|
28
|
+
sum(&:covered_lines_count)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Generate data: 10,000 files with varying coverage
|
|
33
|
+
files = Array.new(10000) { |i| MockSourceFile.new(i % 100) }
|
|
34
|
+
file_list = FileList.new(files)
|
|
35
|
+
|
|
36
|
+
puts "---------------------------------------------------"
|
|
37
|
+
puts "Memory Profiling: FileList#covered_lines"
|
|
38
|
+
|
|
39
|
+
puts "\nOriginal (map.inject):"
|
|
40
|
+
report = MemoryProfiler.report { file_list.covered_lines_original }
|
|
41
|
+
puts " Total allocated: #{report.total_allocated_memsize} bytes (#{report.total_allocated} objects)"
|
|
42
|
+
|
|
43
|
+
puts "\nOptimized (sum):"
|
|
44
|
+
report = MemoryProfiler.report { file_list.covered_lines_optimized }
|
|
45
|
+
puts " Total allocated: #{report.total_allocated_memsize} bytes (#{report.total_allocated} objects)"
|
|
46
|
+
|
|
47
|
+
puts "\n---------------------------------------------------"
|
|
48
|
+
puts "Benchmark: FileList#covered_lines"
|
|
49
|
+
|
|
50
|
+
Benchmark.ips do |x|
|
|
51
|
+
x.report("map.inject") { file_list.covered_lines_original }
|
|
52
|
+
x.report("sum") { file_list.covered_lines_optimized }
|
|
53
|
+
x.compare!
|
|
54
|
+
end
|
|
@@ -107,6 +107,19 @@ class ViewTrackerTest < Minitest::Test
|
|
|
107
107
|
assert_equal [], tracker.used_keys.keys
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
+
test "track view_component renders via railtie" do
|
|
111
|
+
Coverband::Collectors::ViewTracker.expects(:supported_version?).returns(true)
|
|
112
|
+
store = fake_store
|
|
113
|
+
file_path = "#{File.expand_path(Coverband.configuration.root)}/app/components/example_component.html.erb"
|
|
114
|
+
tracker = Coverband::Collectors::ViewTracker.new(store: store, roots: "dir")
|
|
115
|
+
Coverband.configuration.expects(:view_tracker).returns(tracker).at_least_once
|
|
116
|
+
tracker.railtie!
|
|
117
|
+
|
|
118
|
+
ActiveSupport::Notifications.instrument("render.view_component", view_identifier: file_path)
|
|
119
|
+
|
|
120
|
+
assert_includes tracker.logged_keys, file_path
|
|
121
|
+
end
|
|
122
|
+
|
|
110
123
|
test "reset store" do
|
|
111
124
|
Coverband::Collectors::ViewTracker.expects(:supported_version?).returns(true)
|
|
112
125
|
store = fake_store
|
|
@@ -128,6 +141,20 @@ class ViewTrackerTest < Minitest::Test
|
|
|
128
141
|
assert_equal [], tracker.logged_keys
|
|
129
142
|
end
|
|
130
143
|
|
|
144
|
+
test "no-op tracker operations with non-redis stores" do
|
|
145
|
+
Coverband::Collectors::ViewTracker.expects(:supported_version?).returns(true)
|
|
146
|
+
store = Coverband::Adapters::NullStore.new
|
|
147
|
+
tracker = Coverband::Collectors::ViewTracker.new(store: store, roots: "dir")
|
|
148
|
+
|
|
149
|
+
tracker.track_key(identifier: "file")
|
|
150
|
+
tracker.save_report
|
|
151
|
+
|
|
152
|
+
assert_equal({}, tracker.used_keys)
|
|
153
|
+
assert_equal "N/A", tracker.tracking_since
|
|
154
|
+
assert_nil tracker.reset_recordings
|
|
155
|
+
assert_nil tracker.clear_key!("file")
|
|
156
|
+
end
|
|
157
|
+
|
|
131
158
|
protected
|
|
132
159
|
|
|
133
160
|
def fake_store
|
|
@@ -112,4 +112,16 @@ class BaseTest < Minitest::Test
|
|
|
112
112
|
config.redis_url = "redis://localhost:3333"
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
|
+
|
|
116
|
+
test "redis_url with rediss:// scheme for TLS support" do
|
|
117
|
+
Coverband::Collectors::Coverage.instance.reset_instance
|
|
118
|
+
Coverband.configuration.reset
|
|
119
|
+
# Verify that rediss:// URLs (TLS) can be configured
|
|
120
|
+
# The Redis gem will automatically enable TLS when it sees the rediss:// scheme
|
|
121
|
+
Coverband.configure do |config|
|
|
122
|
+
config.redis_url = "rediss://my-elasticache.example.com:6379"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
assert_equal "rediss://my-elasticache.example.com:6379", Coverband.configuration.redis_url
|
|
126
|
+
end
|
|
115
127
|
end
|
|
@@ -37,13 +37,13 @@ if defined?(Coverband::MCP)
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
test "handles MCP requests at /mcp endpoint" do
|
|
41
|
-
# Mock the
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
test "handles MCP POST requests at /mcp endpoint via StreamableHTTPTransport" do
|
|
41
|
+
# Mock the transport to verify it's called
|
|
42
|
+
transport_mock = mock("transport")
|
|
43
|
+
transport_mock.expects(:handle_request).returns([200, {"Content-Type" => "application/json"}, ["{}"]])
|
|
44
44
|
|
|
45
45
|
handler = Coverband::MCP::HttpHandler.new
|
|
46
|
-
handler.expects(:
|
|
46
|
+
handler.expects(:transport).returns(transport_mock)
|
|
47
47
|
|
|
48
48
|
@app = handler
|
|
49
49
|
|
|
@@ -56,12 +56,21 @@ if defined?(Coverband::MCP)
|
|
|
56
56
|
post "/mcp", json_request, {"CONTENT_TYPE" => "application/json"}
|
|
57
57
|
|
|
58
58
|
assert_equal 200, last_response.status
|
|
59
|
-
|
|
59
|
+
end
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
test "handles MCP GET requests at /mcp endpoint via StreamableHTTPTransport" do
|
|
62
|
+
# Mock the transport to verify it's called for GET
|
|
63
|
+
transport_mock = mock("transport")
|
|
64
|
+
transport_mock.expects(:handle_request).returns([200, {"Content-Type" => "text/event-stream"}, []])
|
|
65
|
+
|
|
66
|
+
handler = Coverband::MCP::HttpHandler.new
|
|
67
|
+
handler.expects(:transport).returns(transport_mock)
|
|
68
|
+
|
|
69
|
+
@app = handler
|
|
70
|
+
|
|
71
|
+
get "/mcp", {}, {"ACCEPT" => "text/event-stream"}
|
|
72
|
+
|
|
73
|
+
assert_equal 200, last_response.status
|
|
65
74
|
end
|
|
66
75
|
|
|
67
76
|
test "returns 404 for non-MCP requests when no wrapped app" do
|
|
@@ -81,51 +90,49 @@ if defined?(Coverband::MCP)
|
|
|
81
90
|
assert_equal "wrapped app response", last_response.body
|
|
82
91
|
end
|
|
83
92
|
|
|
84
|
-
test "
|
|
85
|
-
|
|
93
|
+
test "responds to GET, POST, DELETE, OPTIONS for MCP endpoint" do
|
|
94
|
+
transport_mock = mock("transport")
|
|
95
|
+
transport_mock.expects(:handle_request).at_least(3).returns([200, {"Content-Type" => "application/json"}, ["{}"]])
|
|
86
96
|
|
|
87
|
-
|
|
88
|
-
|
|
97
|
+
handler = Coverband::MCP::HttpHandler.new
|
|
98
|
+
handler.expects(:transport).at_least(3).returns(transport_mock)
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
post "/mcp", "invalid json", {"CONTENT_TYPE" => "application/json"}
|
|
100
|
+
@app = handler
|
|
92
101
|
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
# POST request
|
|
103
|
+
post "/mcp", "{}", {"CONTENT_TYPE" => "application/json"}
|
|
104
|
+
assert_equal 200, last_response.status
|
|
95
105
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
# GET request
|
|
107
|
+
get "/mcp", {}, {"ACCEPT" => "text/event-stream"}
|
|
108
|
+
assert_equal 200, last_response.status
|
|
99
109
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
# DELETE request
|
|
111
|
+
delete "/mcp"
|
|
112
|
+
assert_equal 200, last_response.status
|
|
113
|
+
end
|
|
104
114
|
|
|
115
|
+
test "handles CORS preflight OPTIONS request" do
|
|
105
116
|
handler = Coverband::MCP::HttpHandler.new
|
|
106
|
-
handler.expects(:mcp_server).returns(server_mock)
|
|
107
|
-
|
|
108
117
|
@app = handler
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
post "/mcp", json_request, {"CONTENT_TYPE" => "application/json"}
|
|
112
|
-
|
|
113
|
-
assert_equal 500, last_response.status
|
|
119
|
+
options "/mcp"
|
|
114
120
|
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
assert_equal 204, last_response.status
|
|
122
|
+
assert_equal "*", last_response.headers["Access-Control-Allow-Origin"]
|
|
123
|
+
assert_equal "GET, POST, DELETE, OPTIONS", last_response.headers["Access-Control-Allow-Methods"]
|
|
117
124
|
end
|
|
118
125
|
|
|
119
|
-
test "
|
|
120
|
-
|
|
126
|
+
test "delegates non-MCP requests to wrapped app for non-POST" do
|
|
127
|
+
@app = app_with_wrapped_handler
|
|
121
128
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
get "/other-path"
|
|
130
|
+
assert_equal 200, last_response.status
|
|
131
|
+
assert_equal "wrapped app response", last_response.body
|
|
125
132
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
delete "/other-path"
|
|
134
|
+
assert_equal 200, last_response.status
|
|
135
|
+
assert_equal "wrapped app response", last_response.body
|
|
129
136
|
end
|
|
130
137
|
|
|
131
138
|
test "mcp_request? correctly identifies MCP requests" do
|
|
@@ -136,15 +143,20 @@ if defined?(Coverband::MCP)
|
|
|
136
143
|
request = Rack::Request.new(env)
|
|
137
144
|
assert handler.send(:mcp_request?, request)
|
|
138
145
|
|
|
139
|
-
#
|
|
140
|
-
env = Rack::MockRequest.env_for("/
|
|
146
|
+
# GET request to /mcp
|
|
147
|
+
env = Rack::MockRequest.env_for("/mcp", method: "GET")
|
|
141
148
|
request = Rack::Request.new(env)
|
|
142
149
|
assert handler.send(:mcp_request?, request)
|
|
143
150
|
|
|
144
|
-
#
|
|
145
|
-
env = Rack::MockRequest.env_for("/mcp", method: "
|
|
151
|
+
# DELETE request to /mcp
|
|
152
|
+
env = Rack::MockRequest.env_for("/mcp", method: "DELETE")
|
|
146
153
|
request = Rack::Request.new(env)
|
|
147
|
-
|
|
154
|
+
assert handler.send(:mcp_request?, request)
|
|
155
|
+
|
|
156
|
+
# OPTIONS request to /mcp
|
|
157
|
+
env = Rack::MockRequest.env_for("/mcp", method: "OPTIONS")
|
|
158
|
+
request = Rack::Request.new(env)
|
|
159
|
+
assert handler.send(:mcp_request?, request)
|
|
148
160
|
|
|
149
161
|
# POST request to /other-path
|
|
150
162
|
env = Rack::MockRequest.env_for("/other-path", method: "POST")
|
|
@@ -10,6 +10,16 @@ end
|
|
|
10
10
|
|
|
11
11
|
if defined?(Coverband::MCP)
|
|
12
12
|
class GetDeadMethodsTest < Minitest::Test
|
|
13
|
+
def build_dead_method(file_path:, class_name:, method_name:, line_number:)
|
|
14
|
+
Coverband::Utils::MethodDefinitionScanner::MethodDefinition.new(
|
|
15
|
+
first_line_number: line_number,
|
|
16
|
+
last_line_number: line_number,
|
|
17
|
+
name: method_name,
|
|
18
|
+
class_name: class_name,
|
|
19
|
+
file_path: file_path
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
13
23
|
def setup
|
|
14
24
|
super
|
|
15
25
|
Coverband.configure do |config|
|
|
@@ -35,24 +45,24 @@ if defined?(Coverband::MCP)
|
|
|
35
45
|
if defined?(RubyVM::AbstractSyntaxTree)
|
|
36
46
|
test "call returns dead methods when AST support available" do
|
|
37
47
|
mock_dead_methods = [
|
|
38
|
-
|
|
48
|
+
build_dead_method(
|
|
39
49
|
file_path: "/app/models/user.rb",
|
|
40
50
|
class_name: "User",
|
|
41
51
|
method_name: "unused_method",
|
|
42
52
|
line_number: 10
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
),
|
|
54
|
+
build_dead_method(
|
|
45
55
|
file_path: "/app/models/user.rb",
|
|
46
56
|
class_name: "User",
|
|
47
57
|
method_name: "another_unused",
|
|
48
58
|
line_number: 15
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
),
|
|
60
|
+
build_dead_method(
|
|
51
61
|
file_path: "/app/models/order.rb",
|
|
52
62
|
class_name: "Order",
|
|
53
63
|
method_name: "dead_method",
|
|
54
64
|
line_number: 20
|
|
55
|
-
|
|
65
|
+
)
|
|
56
66
|
]
|
|
57
67
|
|
|
58
68
|
Coverband::Utils::DeadMethods.expects(:scan_all).returns(mock_dead_methods)
|
|
@@ -83,18 +93,18 @@ if defined?(Coverband::MCP)
|
|
|
83
93
|
|
|
84
94
|
test "call filters by file_pattern when provided" do
|
|
85
95
|
mock_dead_methods = [
|
|
86
|
-
|
|
96
|
+
build_dead_method(
|
|
87
97
|
file_path: "/app/models/user.rb",
|
|
88
98
|
class_name: "User",
|
|
89
99
|
method_name: "unused_method",
|
|
90
100
|
line_number: 10
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
),
|
|
102
|
+
build_dead_method(
|
|
93
103
|
file_path: "/app/helpers/user_helper.rb",
|
|
94
104
|
class_name: "UserHelper",
|
|
95
105
|
method_name: "dead_helper",
|
|
96
106
|
line_number: 5
|
|
97
|
-
|
|
107
|
+
)
|
|
98
108
|
]
|
|
99
109
|
|
|
100
110
|
Coverband::Utils::DeadMethods.expects(:scan_all).returns(mock_dead_methods)
|
|
@@ -45,6 +45,16 @@ module Coverband
|
|
|
45
45
|
get "/json?line_coverage=true"
|
|
46
46
|
assert last_response.ok?
|
|
47
47
|
end
|
|
48
|
+
|
|
49
|
+
test "renders static files" do
|
|
50
|
+
get "/application.js"
|
|
51
|
+
assert last_response.ok?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
test "renders 404 if static file doesn't exist" do
|
|
55
|
+
get "/unknown.js"
|
|
56
|
+
assert last_response.not_found?
|
|
57
|
+
end
|
|
48
58
|
end
|
|
49
59
|
end
|
|
50
60
|
|
|
@@ -48,6 +48,26 @@ if defined?(RubyVM::AbstractSyntaxTree)
|
|
|
48
48
|
assert_equal(6, dead_method.last_line_number)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
def test_scan_all_skips_missing_files
|
|
52
|
+
missing_file = "./test/fixtures/missing_file.rb"
|
|
53
|
+
existing_file = "./test/dog.rb"
|
|
54
|
+
coverage_report = {
|
|
55
|
+
Coverband::MERGED_TYPE => {
|
|
56
|
+
missing_file => {"data" => []},
|
|
57
|
+
existing_file => {"data" => []}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
dead_method = Object.new
|
|
61
|
+
|
|
62
|
+
Coverband.configuration.stubs(:store).returns(
|
|
63
|
+
stub(get_coverage_report: coverage_report)
|
|
64
|
+
)
|
|
65
|
+
DeadMethods.expects(:scan).with(file_path: existing_file, coverage: []).returns([dead_method])
|
|
66
|
+
DeadMethods.expects(:scan).with(file_path: missing_file, coverage: []).never
|
|
67
|
+
|
|
68
|
+
assert_equal [dead_method], DeadMethods.scan_all
|
|
69
|
+
end
|
|
70
|
+
|
|
51
71
|
def test_output_all
|
|
52
72
|
require_unique_file
|
|
53
73
|
@coverband.report_coverage
|
|
@@ -84,9 +84,9 @@ if defined?(Coverband::MCP)
|
|
|
84
84
|
test "HTTP handler integrates with MCP server" do
|
|
85
85
|
handler = Coverband::MCP::HttpHandler.new
|
|
86
86
|
|
|
87
|
-
# Verify handler can create and use MCP server
|
|
88
|
-
|
|
89
|
-
assert_instance_of
|
|
87
|
+
# Verify handler can create and use transport with MCP server
|
|
88
|
+
transport = handler.send(:transport)
|
|
89
|
+
assert_instance_of ::MCP::Server::Transports::StreamableHTTPTransport, transport
|
|
90
90
|
|
|
91
91
|
# Verify handler responds to rack interface
|
|
92
92
|
assert_respond_to handler, :call
|
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.1
|
|
4
|
+
version: 6.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dan Mayer
|
|
@@ -446,6 +446,7 @@ files:
|
|
|
446
446
|
- test/benchmarks/benchmark.rake
|
|
447
447
|
- test/benchmarks/benchmark_delta.rb
|
|
448
448
|
- test/benchmarks/benchmark_file_list.rb
|
|
449
|
+
- test/benchmarks/benchmark_file_list_covered_lines.rb
|
|
449
450
|
- test/benchmarks/benchmark_unused_keys.rb
|
|
450
451
|
- test/benchmarks/coverage_fork.sh
|
|
451
452
|
- test/benchmarks/dog.rb
|