coverband 6.2.2 → 7.0.0.rc.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/.github/workflows/diagram.yml +2 -2
- data/.github/workflows/main.yml +64 -7
- data/Gemfile +16 -0
- data/Gemfile.rails7.0 +12 -0
- data/Gemfile.rails7.1 +6 -1
- data/Gemfile.rails7.2 +5 -0
- data/Gemfile.rails8.0 +15 -0
- data/README.md +218 -12
- data/Rakefile +16 -5
- data/changes.md +60 -0
- data/coverband.gemspec +15 -13
- data/lib/coverband/adapters/active_support_cache_store.rb +54 -0
- data/lib/coverband/adapters/base.rb +42 -10
- data/lib/coverband/adapters/file_store.rb +2 -2
- data/lib/coverband/adapters/hash_redis_store.rb +22 -3
- data/lib/coverband/adapters/null_store.rb +0 -4
- data/lib/coverband/adapters/redis_cleanup.rb +101 -0
- data/lib/coverband/adapters/redis_store.rb +32 -65
- data/lib/coverband/adapters/session_coverage.rb +172 -0
- data/lib/coverband/adapters/stdout_store.rb +0 -4
- data/lib/coverband/adapters/tracker_storage/base.rb +79 -0
- data/lib/coverband/adapters/tracker_storage/document_repository.rb +49 -0
- data/lib/coverband/adapters/tracker_storage/factory.rb +57 -0
- data/lib/coverband/adapters/tracker_storage/redis_hash_repository.rb +127 -0
- data/lib/coverband/collectors/abstract_tracker.rb +135 -51
- data/lib/coverband/collectors/query_burst_tracker.rb +72 -19
- data/lib/coverband/collectors/route_tracker.rb +6 -0
- data/lib/coverband/collectors/tracker_registry.rb +63 -0
- data/lib/coverband/collectors/translation_tracker.rb +6 -0
- data/lib/coverband/collectors/view_tracker.rb +11 -5
- data/lib/coverband/configuration.rb +95 -131
- data/lib/coverband/mcp/http_handler.rb +1 -1
- data/lib/coverband/mcp/server.rb +4 -1
- data/lib/coverband/mcp/tools/get_storage_health.rb +28 -0
- data/lib/coverband/reporters/base.rb +0 -2
- data/lib/coverband/reporters/html_report.rb +2 -3
- data/lib/coverband/reporters/json_report.rb +2 -2
- data/lib/coverband/reporters/web.rb +72 -15
- data/lib/coverband/storage/document.rb +173 -0
- data/lib/coverband/storage/generation.rb +165 -0
- data/lib/coverband/storage/generation_change.rb +53 -0
- data/lib/coverband/storage/generation_coordinator.rb +145 -0
- data/lib/coverband/storage/io_guard.rb +29 -0
- data/lib/coverband/storage/read_fallback.rb +30 -0
- data/lib/coverband/storage/redis_target.rb +74 -0
- data/lib/coverband/storage/session.rb +527 -0
- data/lib/coverband/storage/target.rb +117 -0
- data/lib/coverband/storage/writer.rb +194 -0
- data/lib/coverband/storage_health.rb +90 -0
- data/lib/coverband/utils/configuration_template.rb +14 -1
- data/lib/coverband/utils/railtie.rb +2 -2
- data/lib/coverband/utils/result.rb +5 -5
- data/lib/coverband/utils/results.rb +32 -16
- data/lib/coverband/utils/source_file.rb +17 -27
- data/lib/coverband/utils/tasks.rb +71 -6
- data/lib/coverband/version.rb +1 -1
- data/lib/coverband.rb +98 -20
- data/public/application.css +540 -146
- data/test/benchmarks/benchmark.rake +13 -0
- data/test/coverband/adapters/active_support_cache_store_test.rb +613 -0
- data/test/coverband/adapters/capabilities_test.rb +80 -0
- data/test/coverband/adapters/redis_store_test.rb +63 -2
- data/test/coverband/adapters/solid_cache_store_test.rb +181 -0
- data/test/coverband/adapters/tracker_storage_test.rb +170 -0
- data/test/coverband/collectors/query_burst_tracker_test.rb +155 -10
- data/test/coverband/collectors/route_tracker_test.rb +2 -13
- data/test/coverband/collectors/tracker_registry_test.rb +218 -0
- data/test/coverband/collectors/translation_tracker_test.rb +76 -16
- data/test/coverband/collectors/view_tracker_test.rb +49 -12
- data/test/coverband/configuration_test.rb +36 -38
- data/test/coverband/mcp/server_test.rb +2 -1
- data/test/coverband/mcp/tools/get_storage_health_test.rb +41 -0
- data/test/coverband/reporters/html_test.rb +26 -0
- data/test/coverband/reporters/json_test.rb +53 -0
- data/test/coverband/reporters/web_test.rb +96 -0
- data/test/coverband/storage/generation_coordinator_test.rb +191 -0
- data/test/coverband/storage/io_guard_test.rb +127 -0
- data/test/coverband/storage/protocol_conformance_test.rb +725 -0
- data/test/coverband/storage_health_test.rb +99 -0
- data/test/coverband/track_key_test.rb +8 -0
- data/test/coverband/utils/cleanup_tasks_test.rb +188 -0
- data/test/coverband/utils/file_list_test.rb +2 -2
- data/test/coverband/utils/result_test.rb +4 -4
- data/test/coverband/utils/results_test.rb +100 -10
- data/test/coverband/utils/source_file_test.rb +40 -6
- data/test/forked/rails_cache_full_stack_test.rb +64 -0
- data/test/forked/rails_full_stack_test.rb +7 -0
- data/test/integration/full_stack_deferred_eager_test.rb +0 -1
- data/test/integration/full_stack_send_deferred_eager_test.rb +0 -1
- data/test/integration/full_stack_test.rb +0 -1
- data/test/integration/mcp_integration_test.rb +4 -2
- data/test/integration/standalone_json_report_test.rb +46 -0
- data/test/integration/without_redis_test.rb +76 -0
- data/test/rails7_dummy/config/application.rb +1 -0
- data/test/rails7_dummy/config/coverband_missing_redis.rb +0 -2
- data/test/rails7_dummy/config/coverband_rails_cache.rb +16 -0
- data/test/rails8_dummy/config/application.rb +1 -0
- data/test/rails8_dummy/config/coverband_rails_cache.rb +3 -0
- data/test/rails_test_helper.rb +2 -1
- data/test/test_helper.rb +5 -17
- data/views/file_list.erb +2 -2
- data/views/source_file.erb +4 -3
- metadata +48 -40
- data/lib/coverband/adapters/memcached_store.rb +0 -85
- data/lib/coverband/adapters/web_service_store.rb +0 -152
- data/lib/coverband/collectors/view_tracker_service.rb +0 -59
- data/test/coverband/adapters/memecached_store_test.rb +0 -26
- data/test/coverband/adapters/web_service_store_test.rb +0 -56
|
@@ -5,13 +5,23 @@ module Coverband
|
|
|
5
5
|
# Configuration parsing and options for the coverband gem.
|
|
6
6
|
###
|
|
7
7
|
class Configuration
|
|
8
|
+
def self.add_tracker_flag(name, default: false)
|
|
9
|
+
name = name.to_sym
|
|
10
|
+
attr_accessor name
|
|
11
|
+
dynamic_flag_defaults[name] = default
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.dynamic_flag_defaults
|
|
15
|
+
@dynamic_flag_defaults ||= {}
|
|
16
|
+
end
|
|
17
|
+
|
|
8
18
|
attr_accessor :root_paths, :root,
|
|
9
19
|
:verbose,
|
|
10
|
-
:
|
|
20
|
+
:redis_namespace, :redis_ttl,
|
|
11
21
|
:background_reporting_enabled,
|
|
12
|
-
:test_env, :web_enable_clear, :
|
|
13
|
-
:use_oneshot_lines_coverage,
|
|
14
|
-
:view_tracker, :defer_eager_loading_data,
|
|
22
|
+
:test_env, :web_enable_clear, :web_debug, :report_on_exit,
|
|
23
|
+
:use_oneshot_lines_coverage,
|
|
24
|
+
:track_views, :view_tracker, :defer_eager_loading_data,
|
|
15
25
|
:track_routes, :track_redirect_routes, :route_tracker,
|
|
16
26
|
:track_translations, :translations_tracker,
|
|
17
27
|
:track_query_bursts, :query_burst_tracker,
|
|
@@ -19,20 +29,42 @@ module Coverband
|
|
|
19
29
|
:trackers, :csp_policy, :hide_settings,
|
|
20
30
|
:mcp_enabled
|
|
21
31
|
|
|
22
|
-
attr_writer :logger, :
|
|
23
|
-
:s3_secret_access_key, :password, :api_key, :service_url, :coverband_timeout, :service_dev_mode,
|
|
24
|
-
:service_test_mode, :process_type, :track_views, :redis_url,
|
|
32
|
+
attr_writer :logger, :password, :redis_url,
|
|
25
33
|
:background_reporting_sleep_seconds, :reporting_wiggle,
|
|
26
34
|
:send_deferred_eager_loading_data, :paged_reporting, :mcp_allowed_environments, :mcp_password
|
|
27
35
|
|
|
28
|
-
attr_reader :
|
|
36
|
+
attr_reader :ignore
|
|
37
|
+
|
|
38
|
+
HOSTED_SERVICE_REMOVED = "coverband.io is no longer maintained and support for it was removed. " \
|
|
39
|
+
"Delete the setting and configure a store, see Upgrading to 7.0 in the README"
|
|
40
|
+
|
|
41
|
+
# Settings removed in 7.0. Assigning one logs how to upgrade rather than
|
|
42
|
+
# raising NoMethodError from the app's config file during boot.
|
|
43
|
+
REMOVED_SETTINGS = {
|
|
44
|
+
reporter: "it has had no effect since Coverband 5.0. Delete the setting",
|
|
45
|
+
api_key: HOSTED_SERVICE_REMOVED,
|
|
46
|
+
service_url: HOSTED_SERVICE_REMOVED,
|
|
47
|
+
coverband_timeout: HOSTED_SERVICE_REMOVED,
|
|
48
|
+
service_dev_mode: HOSTED_SERVICE_REMOVED,
|
|
49
|
+
service_test_mode: HOSTED_SERVICE_REMOVED,
|
|
50
|
+
process_type: HOSTED_SERVICE_REMOVED,
|
|
51
|
+
s3_region: "the S3 report upload was removed earlier and this setting did nothing. Delete the setting",
|
|
52
|
+
s3_bucket: "the S3 report upload was removed earlier and this setting did nothing. Delete the setting",
|
|
53
|
+
s3_access_key_id: "the S3 report upload was removed earlier and this setting did nothing. Delete the setting",
|
|
54
|
+
s3_secret_access_key: "the S3 report upload was removed earlier and this setting did nothing. Delete the setting",
|
|
55
|
+
track_gems: "gem tracking was removed earlier and this setting did nothing. Delete the setting",
|
|
56
|
+
gem_details: "gem tracking was removed earlier and this setting did nothing. Delete the setting",
|
|
57
|
+
simulate_oneshot_lines_coverage: "it had no production effect. Delete the setting"
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
REMOVED_SETTINGS.each do |name, fix|
|
|
61
|
+
define_method(:"#{name}=") do |_value|
|
|
62
|
+
logger.warn "Coverband: config.#{name} was removed in 7.0 and is ignored, #{fix}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
29
65
|
|
|
30
|
-
|
|
31
|
-
#
|
|
32
|
-
# actions polluting the 'runtime' metrics
|
|
33
|
-
#
|
|
34
|
-
# * Perhaps detect heroku deployment ENV var opposed to tasks?
|
|
35
|
-
#####
|
|
66
|
+
# Tasks that load application code for deployment or maintenance should not
|
|
67
|
+
# contribute to production runtime coverage.
|
|
36
68
|
IGNORE_TASKS = ["coverband:clear",
|
|
37
69
|
"coverband:coverage",
|
|
38
70
|
"coverband:coverage_server",
|
|
@@ -67,7 +99,6 @@ module Coverband
|
|
|
67
99
|
@ignore = IGNORE_DEFAULTS.map { |ignore_str| Regexp.new(ignore_str) }
|
|
68
100
|
@search_paths = TRACKED_DEFAULT_PATHS.dup
|
|
69
101
|
@verbose = false
|
|
70
|
-
@reporter = "scov"
|
|
71
102
|
@logger = nil
|
|
72
103
|
@store = nil
|
|
73
104
|
@background_reporting_enabled = true
|
|
@@ -90,7 +121,6 @@ module Coverband
|
|
|
90
121
|
@web_debug = false
|
|
91
122
|
@report_on_exit = true
|
|
92
123
|
@use_oneshot_lines_coverage = ENV["ONESHOT"] || false
|
|
93
|
-
@simulate_oneshot_lines_coverage = false # this is being deprecated
|
|
94
124
|
@current_root = nil
|
|
95
125
|
@all_root_paths = nil
|
|
96
126
|
@all_root_patterns = nil
|
|
@@ -103,56 +133,39 @@ module Coverband
|
|
|
103
133
|
@mcp_password = nil
|
|
104
134
|
@mcp_allowed_environments = %w[development test]
|
|
105
135
|
|
|
106
|
-
# coverband service settings
|
|
107
|
-
@api_key = nil
|
|
108
|
-
@service_url = nil
|
|
109
|
-
@coverband_timeout = nil
|
|
110
|
-
@service_dev_mode = nil
|
|
111
|
-
@service_test_mode = nil
|
|
112
|
-
@process_type = nil
|
|
113
|
-
|
|
114
136
|
@redis_url = nil
|
|
115
137
|
@redis_namespace = nil
|
|
116
138
|
@redis_ttl = 2_592_000 # in seconds. Default is 30 days.
|
|
117
139
|
@reporting_wiggle = nil
|
|
118
140
|
|
|
119
141
|
@trackers = []
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@s3_access_key_id = nil
|
|
125
|
-
@s3_secret_access_key = nil
|
|
126
|
-
@track_gems = false
|
|
127
|
-
@gem_details = false
|
|
142
|
+
@registered_trackers = {}
|
|
143
|
+
self.class.dynamic_flag_defaults.each do |name, default|
|
|
144
|
+
instance_variable_set(:"@#{name}", default)
|
|
145
|
+
end
|
|
128
146
|
end
|
|
129
147
|
|
|
130
148
|
def railtie!
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if Coverband.configuration.track_translations
|
|
137
|
-
Coverband.configuration.translations_tracker = Coverband::Collectors::TranslationTracker.new
|
|
138
|
-
trackers << Coverband.configuration.translations_tracker
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
if Coverband.configuration.track_views
|
|
142
|
-
Coverband.configuration.view_tracker = if Coverband.coverband_service?
|
|
143
|
-
Coverband::Collectors::ViewTrackerService.new
|
|
144
|
-
else
|
|
145
|
-
Coverband::Collectors::ViewTracker.new
|
|
149
|
+
unless store.supports_trackers?
|
|
150
|
+
if Coverband::Collectors::TrackerRegistry.registry.any? { |_name, entry| entry.enabled_proc.call(self) }
|
|
151
|
+
logger.info "Coverband: #{store.class.name} does not support tracking, " \
|
|
152
|
+
"view/route/translation/query burst trackers are disabled. " \
|
|
153
|
+
"Coverband::Adapters::ActiveSupportCacheStore supports tracking on any ActiveSupport::Cache store."
|
|
146
154
|
end
|
|
147
|
-
|
|
155
|
+
return
|
|
148
156
|
end
|
|
149
157
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
Coverband::Collectors::TrackerRegistry.each do |entry|
|
|
159
|
+
next unless entry.enabled_proc.call(self)
|
|
160
|
+
|
|
161
|
+
tracker = build_tracker(entry)
|
|
162
|
+
setter = :"#{entry.name}="
|
|
163
|
+
public_send(setter, tracker) if respond_to?(setter)
|
|
164
|
+
registered_trackers[entry.name] = tracker
|
|
165
|
+
trackers << tracker
|
|
153
166
|
end
|
|
154
167
|
trackers.each { |tracker| tracker.railtie! }
|
|
155
|
-
rescue
|
|
168
|
+
rescue *Coverband.redis_connection_errors => e
|
|
156
169
|
Coverband.configuration.logger.info "Redis is not available (#{e}), Coverband not configured"
|
|
157
170
|
Coverband.configuration.logger.info "If this is a setup task like assets:precompile feel free to ignore"
|
|
158
171
|
end
|
|
@@ -170,6 +183,13 @@ module Coverband
|
|
|
170
183
|
route_tracker
|
|
171
184
|
end
|
|
172
185
|
|
|
186
|
+
# Look up an initialized tracker by its registered name. Bundled trackers
|
|
187
|
+
# also have their own accessor, trackers registered by an app or gem only
|
|
188
|
+
# have this.
|
|
189
|
+
def tracker_for(name)
|
|
190
|
+
registered_trackers[name.to_sym]
|
|
191
|
+
end
|
|
192
|
+
|
|
173
193
|
def password
|
|
174
194
|
@password || ENV["COVERBAND_PASSWORD"]
|
|
175
195
|
end
|
|
@@ -195,13 +215,14 @@ module Coverband
|
|
|
195
215
|
@mcp_allowed_environments || %w[development test]
|
|
196
216
|
end
|
|
197
217
|
|
|
198
|
-
# The adjustments here
|
|
218
|
+
# The adjustments here protect the store from being overloaded
|
|
199
219
|
# the tradeoff being the delay in when reporting data is available
|
|
200
220
|
# if running your own redis increasing this number reduces load on the redis CPU
|
|
201
221
|
def background_reporting_sleep_seconds
|
|
202
|
-
@background_reporting_sleep_seconds ||= if
|
|
203
|
-
#
|
|
204
|
-
|
|
222
|
+
@background_reporting_sleep_seconds ||= if store.is_a?(Coverband::Adapters::ActiveSupportCacheStore)
|
|
223
|
+
# every report rewrites a whole document, and a cache backed store is
|
|
224
|
+
# the one most likely to be shared with the host application
|
|
225
|
+
600
|
|
205
226
|
elsif store.is_a?(Coverband::Adapters::HashRedisStore)
|
|
206
227
|
# Default to 5 minutes if using the hash redis store
|
|
207
228
|
300
|
|
@@ -215,20 +236,18 @@ module Coverband
|
|
|
215
236
|
end
|
|
216
237
|
|
|
217
238
|
def store
|
|
218
|
-
@store ||= if
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
require "coverband/adapters/web_service_store"
|
|
224
|
-
Coverband::Adapters::WebServiceStore.new(service_url)
|
|
239
|
+
@store ||= if !Coverband.redis_available?
|
|
240
|
+
logger.warn "Coverband: no store configured and the redis gem is not installed, so coverage is not being recorded"
|
|
241
|
+
logger.warn "Coverband: add `gem \"redis\"` to your Gemfile to use the default Redis store, or configure one: config.store = Coverband::Adapters::ActiveSupportCacheStore.new { Rails.cache }"
|
|
242
|
+
require "coverband/adapters/null_store"
|
|
243
|
+
Coverband::Adapters::NullStore.new
|
|
225
244
|
else
|
|
226
245
|
begin
|
|
227
246
|
# Redis gem automatically enables TLS when the scheme is 'rediss://'
|
|
228
247
|
# For serverless ElastiCache or other TLS-required Redis instances, use:
|
|
229
248
|
# REDIS_URL=rediss://your-endpoint:6379
|
|
230
249
|
Coverband::Adapters::RedisStore.new(Redis.new(url: redis_url), redis_store_options)
|
|
231
|
-
rescue
|
|
250
|
+
rescue *Coverband.redis_connection_errors => error
|
|
232
251
|
logger.info "Redis is not available (#{error}), defaulting to NullStore"
|
|
233
252
|
logger.info "If this is intended, please explicitly configure your store: config.store = Coverband::Adapters::FileStore.new('log/coverage')"
|
|
234
253
|
require "coverband/adapters/null_store"
|
|
@@ -239,24 +258,10 @@ module Coverband
|
|
|
239
258
|
|
|
240
259
|
def store=(store)
|
|
241
260
|
raise "Pass in an instance of Coverband::Adapters" unless store.is_a?(Coverband::Adapters::Base)
|
|
242
|
-
if api_key && store.class.to_s != "Coverband::Adapters::WebServiceStore"
|
|
243
|
-
raise "invalid configuration: only coverband service expects an API Key"
|
|
244
|
-
end
|
|
245
|
-
if ENV["COVERBAND_REDIS_URL"] &&
|
|
246
|
-
defined?(::Coverband::Adapters::WebServiceStore) &&
|
|
247
|
-
store.instance_of?(::Coverband::Adapters::WebServiceStore)
|
|
248
|
-
raise "invalid configuration: coverband service shouldn't have redis url set"
|
|
249
|
-
end
|
|
250
261
|
|
|
251
262
|
@store = store
|
|
252
263
|
end
|
|
253
264
|
|
|
254
|
-
def track_views
|
|
255
|
-
return false if service_disabled_dev_test_env?
|
|
256
|
-
|
|
257
|
-
@track_views
|
|
258
|
-
end
|
|
259
|
-
|
|
260
265
|
###
|
|
261
266
|
# Search Paths
|
|
262
267
|
###
|
|
@@ -295,7 +300,7 @@ module Coverband
|
|
|
295
300
|
@all_root_patterns ||= all_root_paths.map { |path| /^#{path}/ }.freeze
|
|
296
301
|
end
|
|
297
302
|
|
|
298
|
-
SKIPPED_SETTINGS = %w[@
|
|
303
|
+
SKIPPED_SETTINGS = %w[@store @password @mcp_password]
|
|
299
304
|
def to_h
|
|
300
305
|
instance_variables
|
|
301
306
|
.each_with_object({}) do |var, hash|
|
|
@@ -307,38 +312,10 @@ module Coverband
|
|
|
307
312
|
@redis_url ||= ENV["COVERBAND_REDIS_URL"] || ENV["REDIS_URL"]
|
|
308
313
|
end
|
|
309
314
|
|
|
310
|
-
def api_key
|
|
311
|
-
@api_key ||= ENV["COVERBAND_API_KEY"]
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
def service_url
|
|
315
|
-
@service_url ||= ENV["COVERBAND_URL"] || "https://coverband.io"
|
|
316
|
-
end
|
|
317
|
-
|
|
318
315
|
def coverband_env
|
|
319
316
|
ENV["RACK_ENV"] || ENV["RAILS_ENV"] || ((defined?(Rails) && Rails.respond_to?(:env)) ? Rails.env : "unknown")
|
|
320
317
|
end
|
|
321
318
|
|
|
322
|
-
def coverband_timeout
|
|
323
|
-
@coverband_timeout ||= (coverband_env == "development") ? 5 : 2
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
def service_dev_mode
|
|
327
|
-
@service_dev_mode ||= ENV["COVERBAND_ENABLE_DEV_MODE"] || false
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
def service_test_mode
|
|
331
|
-
@service_test_mode ||= ENV["COVERBAND_ENABLE_TEST_MODE"] || false
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
def process_type
|
|
335
|
-
@process_type ||= ENV["PROCESS_TYPE"] || "unknown"
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
def service?
|
|
339
|
-
Coverband.coverband_service? || !api_key.nil?
|
|
340
|
-
end
|
|
341
|
-
|
|
342
319
|
def defer_eager_loading_data?
|
|
343
320
|
@defer_eager_loading_data
|
|
344
321
|
end
|
|
@@ -351,35 +328,22 @@ module Coverband
|
|
|
351
328
|
!!@paged_reporting
|
|
352
329
|
end
|
|
353
330
|
|
|
354
|
-
|
|
355
|
-
return false unless service?
|
|
356
|
-
|
|
357
|
-
(coverband_env == "test" && !Coverband.configuration.service_test_mode) ||
|
|
358
|
-
(coverband_env == "development" && !Coverband.configuration.service_dev_mode)
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
def s3_bucket
|
|
362
|
-
puts "deprecated, s3 is no longer support"
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
def s3_region
|
|
366
|
-
puts "deprecated, s3 is no longer support"
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
def s3_access_key_id
|
|
370
|
-
puts "deprecated, s3 is no longer support"
|
|
371
|
-
end
|
|
331
|
+
private
|
|
372
332
|
|
|
373
|
-
def
|
|
374
|
-
|
|
333
|
+
def registered_trackers
|
|
334
|
+
@registered_trackers ||= {}
|
|
375
335
|
end
|
|
376
336
|
|
|
377
|
-
def
|
|
378
|
-
|
|
337
|
+
def build_tracker(entry)
|
|
338
|
+
tracker = if entry.tracker_class.respond_to?(:call)
|
|
339
|
+
entry.tracker_class.call
|
|
340
|
+
else
|
|
341
|
+
entry.tracker_class.new
|
|
342
|
+
end
|
|
343
|
+
Coverband::Collectors::TrackerRegistry.validate_report_route!(entry.name, tracker.class)
|
|
344
|
+
tracker
|
|
379
345
|
end
|
|
380
346
|
|
|
381
|
-
private
|
|
382
|
-
|
|
383
347
|
def redis_store_options
|
|
384
348
|
{ttl: Coverband.configuration.redis_ttl,
|
|
385
349
|
redis_namespace: Coverband.configuration.redis_namespace}
|
data/lib/coverband/mcp/server.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "tools/get_dead_methods"
|
|
|
7
7
|
require_relative "tools/get_view_tracker_data"
|
|
8
8
|
require_relative "tools/get_route_tracker_data"
|
|
9
9
|
require_relative "tools/get_translation_tracker_data"
|
|
10
|
+
require_relative "tools/get_storage_health"
|
|
10
11
|
|
|
11
12
|
module Coverband
|
|
12
13
|
module MCP
|
|
@@ -66,6 +67,7 @@ module Coverband
|
|
|
66
67
|
- get_view_tracker_data
|
|
67
68
|
- get_route_tracker_data
|
|
68
69
|
- get_translation_tracker_data
|
|
70
|
+
- get_storage_health
|
|
69
71
|
|
|
70
72
|
For Claude Desktop, configure with:
|
|
71
73
|
{
|
|
@@ -108,7 +110,8 @@ module Coverband
|
|
|
108
110
|
Tools::GetDeadMethods,
|
|
109
111
|
Tools::GetViewTrackerData,
|
|
110
112
|
Tools::GetRouteTrackerData,
|
|
111
|
-
Tools::GetTranslationTrackerData
|
|
113
|
+
Tools::GetTranslationTrackerData,
|
|
114
|
+
Tools::GetStorageHealth
|
|
112
115
|
]
|
|
113
116
|
end
|
|
114
117
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coverband
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class GetStorageHealth < ::MCP::Tool
|
|
7
|
+
description "Get Coverband's observed coverage and tracker storage protocol state, " \
|
|
8
|
+
"including stalled writes and recorded data loss. This is not an active backend probe."
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {}
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def self.call(server_context:, **)
|
|
15
|
+
::MCP::Tool::Response.new([{
|
|
16
|
+
type: "text",
|
|
17
|
+
text: JSON.pretty_generate(Coverband.storage_health)
|
|
18
|
+
}])
|
|
19
|
+
rescue => e
|
|
20
|
+
::MCP::Tool::Response.new([{
|
|
21
|
+
type: "text",
|
|
22
|
+
text: "Error getting storage health: #{e.message}"
|
|
23
|
+
}])
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -84,8 +84,6 @@ module Coverband
|
|
|
84
84
|
# basically because paths on machines or deployed hosts could be different, so
|
|
85
85
|
# two different keys could point to the same filename or `line_key`
|
|
86
86
|
# this happens when deployment has a dynamic path or the path change during deployment (hot code reload)
|
|
87
|
-
# TODO: think we are filtering based on ignore while sending to the store
|
|
88
|
-
# and as we also pull it out here
|
|
89
87
|
###
|
|
90
88
|
def get_current_scov_data_imp(store, roots, options = {})
|
|
91
89
|
scov_style_report = {}
|
|
@@ -9,14 +9,13 @@ module Coverband
|
|
|
9
9
|
def initialize(store, options = {})
|
|
10
10
|
self.page = options.fetch(:page) { nil }
|
|
11
11
|
self.open_report = options.fetch(:open_report) { true }
|
|
12
|
-
# TODO: refactor notice out to top level of web only
|
|
13
12
|
self.notice = options.fetch(:notice) { nil }
|
|
14
13
|
self.base_path = options.fetch(:base_path) { "./" }
|
|
15
14
|
self.filename = options.fetch(:filename) { nil }
|
|
16
15
|
|
|
17
16
|
coverband_reports = Coverband::Reporters::Base.report(store, options)
|
|
18
|
-
#
|
|
19
|
-
self.filtered_report_files = if (page || filename) && store.
|
|
17
|
+
# paging and filename optimization only exists on stores that declare it
|
|
18
|
+
self.filtered_report_files = if (page || filename) && store.supports_paged_reports?
|
|
20
19
|
coverband_reports
|
|
21
20
|
else
|
|
22
21
|
self.class.fix_reports(coverband_reports)
|
|
@@ -85,7 +85,8 @@ module Coverband
|
|
|
85
85
|
|
|
86
86
|
data = {
|
|
87
87
|
**coverage_totals(source_files),
|
|
88
|
-
files: coverage_files(result, source_files)
|
|
88
|
+
files: coverage_files(result, source_files),
|
|
89
|
+
storage_health: Coverband.storage_health(coverage: store)
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
if as_report
|
|
@@ -114,7 +115,6 @@ module Coverband
|
|
|
114
115
|
data["iTotalDisplayRecords"] = filesreported
|
|
115
116
|
data["aaData"] = row_data
|
|
116
117
|
data.delete(:files)
|
|
117
|
-
data = data.as_json
|
|
118
118
|
end
|
|
119
119
|
data.to_json
|
|
120
120
|
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "base64"
|
|
4
3
|
require "coverband"
|
|
5
4
|
|
|
6
5
|
begin
|
|
@@ -42,11 +41,11 @@ module Coverband
|
|
|
42
41
|
def check_auth
|
|
43
42
|
return true unless Coverband.configuration.password
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return unless auth_header
|
|
44
|
+
auth = Rack::Auth::Basic::Request.new(request.env)
|
|
45
|
+
return false unless auth.provided? && auth.basic? && auth.credentials
|
|
48
46
|
|
|
49
|
-
|
|
47
|
+
# credentials splits on the first colon only, so passwords may contain one
|
|
48
|
+
Rack::Utils.secure_compare(Coverband.configuration.password.to_s, auth.credentials.last.to_s)
|
|
50
49
|
end
|
|
51
50
|
|
|
52
51
|
def self.call(env)
|
|
@@ -114,6 +113,7 @@ module Coverband
|
|
|
114
113
|
def index
|
|
115
114
|
notice = "<strong>Notice:</strong> #{Rack::Utils.escape_html(request.params["notice"])}<br/>"
|
|
116
115
|
notice = request.params["notice"] ? notice : ""
|
|
116
|
+
notice += storage_notices(Coverband.configuration.store, subject: "coverage")
|
|
117
117
|
page = (request.params["page"] || 1).to_i
|
|
118
118
|
options = {
|
|
119
119
|
static: false,
|
|
@@ -153,6 +153,7 @@ module Coverband
|
|
|
153
153
|
def display_abstract_tracker(tracker)
|
|
154
154
|
notice = "<strong>Notice:</strong> #{Rack::Utils.escape_html(request.params["notice"])}<br/>"
|
|
155
155
|
notice = request.params["notice"] ? notice : ""
|
|
156
|
+
notice += storage_notices(tracker, subject: "tracker")
|
|
156
157
|
used_keys_sort = tracker_used_keys_sort_mode(tracker)
|
|
157
158
|
options = {
|
|
158
159
|
tracker: tracker,
|
|
@@ -189,11 +190,14 @@ module Coverband
|
|
|
189
190
|
end
|
|
190
191
|
|
|
191
192
|
def clear
|
|
192
|
-
if Coverband.configuration.web_enable_clear
|
|
193
|
-
Coverband.configuration.store.clear!
|
|
194
|
-
|
|
193
|
+
notice = if Coverband.configuration.web_enable_clear
|
|
194
|
+
if Coverband.configuration.store.clear! == false
|
|
195
|
+
"coverage clear failed, the store did not accept the write"
|
|
196
|
+
else
|
|
197
|
+
"coverband coverage cleared"
|
|
198
|
+
end
|
|
195
199
|
else
|
|
196
|
-
|
|
200
|
+
"web_enable_clear isn't enabled in your configuration"
|
|
197
201
|
end
|
|
198
202
|
[302, {"Location" => "#{base_path}?notice=#{notice}"}, []]
|
|
199
203
|
end
|
|
@@ -202,19 +206,72 @@ module Coverband
|
|
|
202
206
|
if Coverband.configuration.web_enable_clear
|
|
203
207
|
filename = request.params["filename"]
|
|
204
208
|
Coverband.configuration.store.clear_file!(filename)
|
|
205
|
-
notice = "coverage for file #{filename}
|
|
209
|
+
notice = "coverage clear submitted for file #{filename}, applied within a reporting cycle"
|
|
206
210
|
else
|
|
207
211
|
notice = "web_enable_clear isn't enabled in your configuration"
|
|
208
212
|
end
|
|
209
213
|
[302, {"Location" => "#{base_path}?notice=#{notice}"}, []]
|
|
210
214
|
end
|
|
211
215
|
|
|
216
|
+
###
|
|
217
|
+
# A cache can drop what we wrote, and a conflicted delta can outlive its
|
|
218
|
+
# retention caps. Both are repaired as far as they can be, but the numbers
|
|
219
|
+
# on screen are partial after either, so say so rather than implying the
|
|
220
|
+
# report is complete.
|
|
221
|
+
###
|
|
222
|
+
###
|
|
223
|
+
# Nothing is lost, but nothing is arriving either, and an empty tab reads
|
|
224
|
+
# exactly like an app that used nothing. Process-local, unlike a recorded
|
|
225
|
+
# loss -- when the failure is that nothing can be stored, what this process
|
|
226
|
+
# is holding is the only evidence there is.
|
|
227
|
+
###
|
|
228
|
+
def storage_notices(source, subject:)
|
|
229
|
+
health = Coverband::StorageHealth.for_source(source)
|
|
230
|
+
data_loss_notice(health, subject: subject) + unwritten_notice(health, subject: subject)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def unwritten_notice(health, subject:)
|
|
234
|
+
held = health[:unwritten]
|
|
235
|
+
return "" unless held
|
|
236
|
+
|
|
237
|
+
deltas = held[:deltas]
|
|
238
|
+
"<strong>Notice:</strong> #{deltas} #{subject} #{(deltas == 1) ? "report" : "reports"} " \
|
|
239
|
+
"could not be stored, the oldest since #{held[:since]}. What is shown is missing them, " \
|
|
240
|
+
"and the log says why each write was refused.<br/>"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def data_loss_notice(health, subject:)
|
|
244
|
+
loss = health[:data_loss]
|
|
245
|
+
return "" unless loss
|
|
246
|
+
|
|
247
|
+
detail = Rack::Utils.escape_html(loss[:detail].to_s)
|
|
248
|
+
kind = Rack::Utils.escape_html(loss[:kind])
|
|
249
|
+
lead, consequence = data_loss_wording(kind, subject)
|
|
250
|
+
"<strong>Notice:</strong> #{lead} at #{loss[:at]} " \
|
|
251
|
+
"(#{kind}): #{detail}. #{consequence}<br/>"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# a forfeited retry is not lost data -- that work is in the document unless
|
|
255
|
+
# another writer clobbered the write it went out in -- so it must not read
|
|
256
|
+
# like a real eviction
|
|
257
|
+
def data_loss_wording(kind, subject)
|
|
258
|
+
if kind == "unconfirmed_dropped"
|
|
259
|
+
["some #{subject} data may be undercounted",
|
|
260
|
+
"Affected only if another process overwrote that write."]
|
|
261
|
+
else
|
|
262
|
+
["#{subject} data was lost", "Results before that point are unavailable."]
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
212
266
|
def clear_abstract_tracking(tracker)
|
|
213
|
-
if Coverband.configuration.web_enable_clear
|
|
214
|
-
|
|
215
|
-
|
|
267
|
+
notice = if !Coverband.configuration.web_enable_clear
|
|
268
|
+
"web_enable_clear isn't enabled in your configuration"
|
|
269
|
+
elsif tracker.reset_recordings == false
|
|
270
|
+
# a reset whose pointer write didn't land hasn't happened, and saying
|
|
271
|
+
# otherwise leaves the operator believing data is gone when it is not
|
|
272
|
+
"#{tracker.title} tracking reset failed, the store did not accept the write"
|
|
216
273
|
else
|
|
217
|
-
|
|
274
|
+
"#{tracker.title} tracking reset"
|
|
218
275
|
end
|
|
219
276
|
[302, {"Location" => "#{base_path}/#{tracker.route}?notice=#{notice}"}, []]
|
|
220
277
|
end
|
|
@@ -223,7 +280,7 @@ module Coverband
|
|
|
223
280
|
if Coverband.configuration.web_enable_clear
|
|
224
281
|
key = request.params["key"]
|
|
225
282
|
tracker.clear_key!(key)
|
|
226
|
-
notice = "coverage for #{tracker.title} #{key}
|
|
283
|
+
notice = "coverage for #{tracker.title} #{key} clear submitted, applied within a reporting cycle"
|
|
227
284
|
else
|
|
228
285
|
notice = "web_enable_clear isn't enabled in your configuration"
|
|
229
286
|
end
|