coverband 6.1.5 → 6.1.7

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +4 -4
  3. data/README.md +123 -0
  4. data/agents.md +217 -0
  5. data/bin/coverband-mcp +42 -0
  6. data/changes.md +23 -0
  7. data/coverband/log.272267 +1 -0
  8. data/coverband.gemspec +3 -1
  9. data/lib/coverband/adapters/hash_redis_store.rb +1 -3
  10. data/lib/coverband/collectors/route_tracker.rb +1 -1
  11. data/lib/coverband/collectors/view_tracker.rb +21 -13
  12. data/lib/coverband/configuration.rb +57 -18
  13. data/lib/coverband/integrations/resque.rb +2 -2
  14. data/lib/coverband/mcp/http_handler.rb +118 -0
  15. data/lib/coverband/mcp/server.rb +116 -0
  16. data/lib/coverband/mcp/tools/get_coverage_summary.rb +41 -0
  17. data/lib/coverband/mcp/tools/get_dead_methods.rb +69 -0
  18. data/lib/coverband/mcp/tools/get_file_coverage.rb +72 -0
  19. data/lib/coverband/mcp/tools/get_route_tracker_data.rb +60 -0
  20. data/lib/coverband/mcp/tools/get_translation_tracker_data.rb +60 -0
  21. data/lib/coverband/mcp/tools/get_uncovered_files.rb +73 -0
  22. data/lib/coverband/mcp/tools/get_view_tracker_data.rb +60 -0
  23. data/lib/coverband/mcp.rb +27 -0
  24. data/lib/coverband/reporters/base.rb +2 -4
  25. data/lib/coverband/reporters/web.rb +17 -14
  26. data/lib/coverband/utils/lines_classifier.rb +1 -1
  27. data/lib/coverband/utils/railtie.rb +1 -1
  28. data/lib/coverband/utils/result.rb +2 -1
  29. data/lib/coverband/utils/source_file.rb +5 -5
  30. data/lib/coverband/utils/tasks.rb +31 -0
  31. data/lib/coverband/version.rb +1 -1
  32. data/lib/coverband.rb +7 -7
  33. data/test/benchmarks/benchmark.rake +7 -15
  34. data/test/coverband/file_store_integration_test.rb +72 -0
  35. data/test/coverband/file_store_redis_error_test.rb +56 -0
  36. data/test/coverband/github_issue_586_test.rb +46 -0
  37. data/test/coverband/initialization_timing_test.rb +71 -0
  38. data/test/coverband/mcp/http_handler_test.rb +159 -0
  39. data/test/coverband/mcp/security_test.rb +145 -0
  40. data/test/coverband/mcp/server_test.rb +125 -0
  41. data/test/coverband/mcp/tools/get_coverage_summary_test.rb +75 -0
  42. data/test/coverband/mcp/tools/get_dead_methods_test.rb +162 -0
  43. data/test/coverband/mcp/tools/get_file_coverage_test.rb +159 -0
  44. data/test/coverband/mcp/tools/get_route_tracker_data_test.rb +122 -0
  45. data/test/coverband/mcp/tools/get_translation_tracker_data_test.rb +122 -0
  46. data/test/coverband/mcp/tools/get_uncovered_files_test.rb +177 -0
  47. data/test/coverband/mcp/tools/get_view_tracker_data_test.rb +122 -0
  48. data/test/coverband/reporters/web_test.rb +5 -0
  49. data/test/coverband/track_key_test.rb +9 -9
  50. data/test/coverband/tracker_initialization_test.rb +75 -0
  51. data/test/coverband/user_environment_simulation_test.rb +75 -0
  52. data/test/coverband/utils/lines_classifier_test.rb +1 -1
  53. data/test/integration/mcp_integration_test.rb +175 -0
  54. data/test/test_helper.rb +4 -5
  55. metadata +67 -11
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coverband
4
+ module MCP
5
+ module Tools
6
+ class GetUncoveredFiles < ::MCP::Tool
7
+ description "Get files with coverage below a specified threshold. " \
8
+ "Useful for finding code that may need more production testing or could be dead code."
9
+
10
+ input_schema(
11
+ properties: {
12
+ threshold: {
13
+ type: "number",
14
+ description: "Coverage percentage threshold (default: 50). Files below this are returned."
15
+ },
16
+ include_never_loaded: {
17
+ type: "boolean",
18
+ description: "Include files that were never loaded in production (default: true)"
19
+ }
20
+ }
21
+ )
22
+
23
+ def self.call(server_context:, threshold: 50, include_never_loaded: true, **)
24
+ store = Coverband.configuration.store
25
+ report = Coverband::Reporters::JSONReport.new(store, line_coverage: false)
26
+ data = JSON.parse(report.report)
27
+
28
+ files = data["files"] || {}
29
+
30
+ uncovered = files.select do |_path, file_data|
31
+ percent = file_data["covered_percent"] || 0
32
+ never_loaded = file_data["never_loaded"]
33
+
34
+ if include_never_loaded
35
+ percent < threshold || never_loaded
36
+ else
37
+ percent < threshold && !never_loaded
38
+ end
39
+ end
40
+
41
+ # Sort by coverage percentage ascending (least covered first)
42
+ sorted = uncovered.sort_by { |_path, data| data["covered_percent"] || 0 }
43
+
44
+ result = sorted.map do |path, file_data|
45
+ {
46
+ file: path,
47
+ covered_percent: file_data["covered_percent"],
48
+ lines_of_code: file_data["lines_of_code"],
49
+ lines_covered: file_data["lines_covered"],
50
+ lines_missed: file_data["lines_missed"],
51
+ never_loaded: file_data["never_loaded"]
52
+ }
53
+ end
54
+
55
+ ::MCP::Tool::Response.new([{
56
+ type: "text",
57
+ text: JSON.pretty_generate({
58
+ threshold: threshold,
59
+ include_never_loaded: include_never_loaded,
60
+ total_uncovered_files: result.length,
61
+ files: result
62
+ })
63
+ }])
64
+ rescue => e
65
+ ::MCP::Tool::Response.new([{
66
+ type: "text",
67
+ text: "Error getting uncovered files: #{e.message}"
68
+ }])
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coverband
4
+ module MCP
5
+ module Tools
6
+ class GetViewTrackerData < ::MCP::Tool
7
+ description "Get Rails view template usage tracking data. Shows which view templates " \
8
+ "have been rendered in production and which have never been accessed."
9
+
10
+ input_schema(
11
+ properties: {
12
+ show_unused_only: {
13
+ type: "boolean",
14
+ description: "Only return unused views (default: false)"
15
+ }
16
+ }
17
+ )
18
+
19
+ def self.call(server_context:, show_unused_only: false, **)
20
+ tracker = Coverband.configuration.view_tracker
21
+
22
+ unless tracker
23
+ return ::MCP::Tool::Response.new([{
24
+ type: "text",
25
+ text: "View tracking is not enabled. Enable it with `config.track_views = true` in your coverband configuration."
26
+ }])
27
+ end
28
+
29
+ data = JSON.parse(tracker.as_json)
30
+
31
+ result = if show_unused_only
32
+ {
33
+ tracking_since: tracker.tracking_since,
34
+ unused_views: data["unused_keys"] || [],
35
+ total_unused: data["unused_keys"]&.length || 0
36
+ }
37
+ else
38
+ {
39
+ tracking_since: tracker.tracking_since,
40
+ used_views: data["used_keys"] || [],
41
+ unused_views: data["unused_keys"] || [],
42
+ total_used: data["used_keys"]&.length || 0,
43
+ total_unused: data["unused_keys"]&.length || 0
44
+ }
45
+ end
46
+
47
+ ::MCP::Tool::Response.new([{
48
+ type: "text",
49
+ text: JSON.pretty_generate(result)
50
+ }])
51
+ rescue => e
52
+ ::MCP::Tool::Response.new([{
53
+ type: "text",
54
+ text: "Error getting view tracker data: #{e.message}"
55
+ }])
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "mcp"
5
+ rescue LoadError
6
+ raise LoadError, <<~MSG
7
+ The 'mcp' gem is required for MCP server support.
8
+ Add `gem 'mcp'` to your Gemfile and run `bundle install`.
9
+ MSG
10
+ end
11
+
12
+ require "coverband"
13
+ require "coverband/utils/html_formatter"
14
+ require "coverband/utils/result"
15
+ require "coverband/utils/file_list"
16
+ require "coverband/utils/source_file"
17
+ require "coverband/utils/lines_classifier"
18
+ require "coverband/utils/results"
19
+ require "coverband/reporters/json_report"
20
+
21
+ # Load dead methods support if available
22
+ if defined?(RubyVM::AbstractSyntaxTree)
23
+ require "coverband/utils/dead_methods"
24
+ end
25
+
26
+ require_relative "mcp/server"
27
+ require_relative "mcp/http_handler"
@@ -55,13 +55,11 @@ module Coverband
55
55
  fixed_report[name] = {}
56
56
  report.each_pair do |key, vals|
57
57
  filename = Coverband::Utils::AbsoluteFileConverter.convert(key)
58
- fixed_report[name][filename] = if fixed_report[name].key?(filename) && fixed_report[name][filename][DATA_KEY] && vals[DATA_KEY]
58
+ if fixed_report[name].key?(filename) && fixed_report[name][filename][DATA_KEY] && vals[DATA_KEY]
59
59
  merged_data = merge_arrays(fixed_report[name][filename][DATA_KEY], vals[DATA_KEY])
60
60
  vals[DATA_KEY] = merged_data
61
- vals
62
- else
63
- vals
64
61
  end
62
+ fixed_report[name][filename] = vals
65
63
  end
66
64
  end
67
65
  end
@@ -62,9 +62,9 @@ module Coverband
62
62
  Coverband.configuration.trackers.each do |tracker|
63
63
  if request_path_info.match(tracker.class::REPORT_ROUTE)
64
64
  tracker_route = true
65
- if request_path_info =~ %r{\/clear_.*_key}
65
+ if request_path_info =~ %r{/clear_.*_key}
66
66
  return clear_abstract_tracking_key(tracker)
67
- elsif request_path_info =~ %r{\/clear_.*}
67
+ elsif request_path_info =~ %r{/clear_.*}
68
68
  return clear_abstract_tracking(tracker)
69
69
  else
70
70
  return [200, {"content-type" => "text/html"}, [display_abstract_tracker(tracker)]]
@@ -75,9 +75,9 @@ module Coverband
75
75
  unless tracker_route
76
76
  if request.post?
77
77
  case request_path_info
78
- when %r{\/clear_file}
78
+ when %r{/clear_file}
79
79
  clear_file
80
- when %r{\/clear}
80
+ when %r{/clear}
81
81
  clear
82
82
  else
83
83
  [404, coverband_headers, ["404 error!"]]
@@ -86,21 +86,21 @@ module Coverband
86
86
  case request_path_info
87
87
  when /.*\.(css|js|gif|png)/
88
88
  @static.call(env)
89
- when %r{\/settings}
89
+ when %r{/settings}
90
90
  [200, coverband_headers, [settings]]
91
- when %r{\/view_tracker_data}
91
+ when %r{/view_tracker_data}
92
92
  [200, coverband_headers(content_type: "text/json"), [view_tracker_data]]
93
- when %r{\/enriched_debug_data}
93
+ when %r{/enriched_debug_data}
94
94
  [200, coverband_headers(content_type: "text/json"), [enriched_debug_data]]
95
- when %r{\/debug_data}
95
+ when %r{/debug_data}
96
96
  [200, coverband_headers(content_type: "text/json"), [debug_data]]
97
- when %r{\/load_file_details}
97
+ when %r{/load_file_details}
98
98
  [200, coverband_headers(content_type: "text/json"), [load_file_details]]
99
- when %r{\/json}
99
+ when %r{/json}
100
100
  [200, coverband_headers(content_type: "text/json"), [json]]
101
- when %r{\/report_json}
101
+ when %r{/report_json}
102
102
  [200, coverband_headers(content_type: "text/json"), [report_json]]
103
- when %r{\/$}
103
+ when %r{/$}
104
104
  [200, coverband_headers, [index]]
105
105
  else
106
106
  [404, coverband_headers, ["404 error!"]]
@@ -125,7 +125,10 @@ module Coverband
125
125
  end
126
126
 
127
127
  def json
128
- Coverband::Reporters::JSONReport.new(Coverband.configuration.store).report
128
+ Coverband::Reporters::JSONReport.new(
129
+ Coverband.configuration.store,
130
+ line_coverage: request.params["line_coverage"] == "true"
131
+ ).report
129
132
  end
130
133
 
131
134
  def report_json
@@ -241,7 +244,7 @@ module Coverband
241
244
  # %r{\/.*\/}.match?(request.path) ? request.path.match("\/.*\/")[0] : "/"
242
245
  # ^^ the above is NOT valid Ruby 2.3/2.4 even though rubocop / standard think it is
243
246
  def base_path
244
- (request.path =~ %r{\/.*\/}) ? request.path.match("/.*/")[0] : "/"
247
+ (request.path =~ %r{/.*/}) ? request.path.match("/.*/")[0] : "/"
245
248
  end
246
249
  end
247
250
  end
@@ -23,7 +23,7 @@ module Coverband
23
23
  WHITESPACE_OR_COMMENT_LINE = Regexp.union(WHITESPACE_LINE, COMMENT_LINE)
24
24
 
25
25
  def self.no_cov_line
26
- /^(\s*)#(\s*)(\:nocov\:)/o
26
+ /^(\s*)#(\s*)(:nocov:)/o
27
27
  end
28
28
 
29
29
  def self.no_cov_line?(line)
@@ -7,7 +7,7 @@ module Coverband
7
7
  super
8
8
  end
9
9
  end
10
- Rails::Engine.prepend(RailsEagerLoad)
10
+ Rails::Engine.prepend(RailsEagerLoad) if defined? ::Rails::Engine
11
11
 
12
12
  class Railtie < Rails::Railtie
13
13
  initializer "coverband.configure" do |app|
@@ -14,12 +14,13 @@ module Coverband
14
14
  module Utils
15
15
  class Result
16
16
  extend Forwardable
17
+
17
18
  # Returns the original Coverage.result used for this instance of Coverband::Result
18
19
  attr_reader :original_result
19
20
  # Returns all files that are applicable to this result (sans filters!)
20
21
  # as instances of Coverband::SourceFile. Aliased as :source_files
21
22
  attr_reader :files
22
- alias source_files files
23
+ alias_method :source_files, :files
23
24
  # Explicitly set the Time this result has been created
24
25
  attr_writer :created_at
25
26
 
@@ -29,9 +29,9 @@ module Coverband
29
29
  attr_reader :coverage_posted
30
30
 
31
31
  # Lets grab some fancy aliases, shall we?
32
- alias source src
33
- alias line line_number
34
- alias number line_number
32
+ alias_method :source, :src
33
+ alias_method :line, :line_number
34
+ alias_method :number, :line_number
35
35
 
36
36
  def initialize(src, line_number, coverage, coverage_posted = nil)
37
37
  raise ArgumentError, "Only String accepted for source" unless src.is_a?(String)
@@ -132,14 +132,14 @@ module Coverband
132
132
  # suppress reading unused source code.
133
133
  @src ||= File.open(filename, "rb", &:readlines)
134
134
  end
135
- alias source src
135
+ alias_method :source, :src
136
136
 
137
137
  # Returns all source lines for this file as instances of SimpleCov::SourceFile::Line,
138
138
  # and thus including coverage data. Aliased as :source_lines
139
139
  def lines
140
140
  @lines ||= build_lines
141
141
  end
142
- alias source_lines lines
142
+ alias_method :source_lines, :lines
143
143
 
144
144
  def build_lines
145
145
  coverage_exceeding_source_warn if coverage.size > src.size
@@ -126,6 +126,37 @@ namespace :coverband do
126
126
  Port: ENV.fetch("COVERBAND_COVERAGE_PORT", 9022).to_i
127
127
  end
128
128
 
129
+ desc "Start MCP server for AI assistant integration (set COVERBAND_MCP_HTTP=true for HTTP mode)"
130
+ task :mcp do
131
+ if Rake::Task.task_defined?("environment")
132
+ Rake.application["environment"].invoke
133
+ end
134
+
135
+ begin
136
+ require "coverband/mcp"
137
+ rescue LoadError
138
+ abort "The 'mcp' gem is required for MCP server support. Add `gem 'mcp'` to your Gemfile."
139
+ end
140
+
141
+ server = Coverband::MCP::Server.new
142
+
143
+ if ENV["COVERBAND_MCP_HTTP"]
144
+ # HTTP mode with Streamable HTTP transport (SSE)
145
+ begin
146
+ require "rackup"
147
+ rescue LoadError
148
+ abort "The 'rackup' gem is required for HTTP mode. Add `gem 'rackup'` to your Gemfile."
149
+ end
150
+
151
+ port = ENV.fetch("COVERBAND_MCP_PORT", 9023).to_i
152
+ host = ENV.fetch("COVERBAND_MCP_HOST", "localhost")
153
+ server.run_http(port: port, host: host)
154
+ else
155
+ # Default stdio mode
156
+ server.run_stdio
157
+ end
158
+ end
159
+
129
160
  # experimental dead method detection using RubyVM::AbstractSyntaxTree
130
161
  # combined with the coverband coverage.
131
162
  if defined?(RubyVM::AbstractSyntaxTree)
@@ -5,5 +5,5 @@
5
5
  # use format "4.2.1.rc.1" ~> 4.2.1.rc to prerelease versions like v4.2.1.rc.2 and v4.2.1.rc.3
6
6
  ###
7
7
  module Coverband
8
- VERSION = "6.1.5"
8
+ VERSION = "6.1.7"
9
9
  end
data/lib/coverband.rb CHANGED
@@ -51,8 +51,8 @@ module Coverband
51
51
  yield(configuration)
52
52
  elsif File.exist?(configuration_file)
53
53
  load configuration_file
54
- else
55
- configuration.logger.debug("using default configuration") if Coverband.configuration.verbose
54
+ elsif Coverband.configuration.verbose
55
+ configuration.logger.debug("using default configuration")
56
56
  end
57
57
  @@configured = true
58
58
  coverage_instance.reset_instance
@@ -107,16 +107,16 @@ module Coverband
107
107
  # @raise [ArgumentError] If the tracker_type is not supported
108
108
  def self.track_key(tracker_type, key)
109
109
  return false unless key
110
-
110
+
111
111
  supported_trackers = [:view_tracker, :translations_tracker, :routes_tracker]
112
-
112
+
113
113
  unless supported_trackers.include?(tracker_type)
114
- raise ArgumentError, "Unsupported tracker type: #{tracker_type}. Must be one of: #{supported_trackers.join(', ')}"
114
+ raise ArgumentError, "Unsupported tracker type: #{tracker_type}. Must be one of: #{supported_trackers.join(", ")}"
115
115
  end
116
-
116
+
117
117
  begin
118
118
  tracker = configuration.send(tracker_type)
119
- return false unless tracker && tracker.respond_to?(:track_key)
119
+ return false unless tracker&.respond_to?(:track_key)
120
120
 
121
121
  tracker.track_key(key)
122
122
  true
@@ -102,7 +102,7 @@ namespace :benchmarks do
102
102
  lines = 45
103
103
  non_nil_lines = 18
104
104
  lines.times.map do |line|
105
- coverage = (line < non_nil_lines) ? rand(5) : nil
105
+ (line < non_nil_lines) ? rand(5) : nil
106
106
  end
107
107
  end
108
108
 
@@ -146,7 +146,7 @@ namespace :benchmarks do
146
146
  x.report("store_reports_all") { store.save_report(report) }
147
147
  end
148
148
  keys_subset = report.keys.first(100)
149
- report_subset = report.select { |key, _value| keys_subset.include?(key) }
149
+ report_subset = report.slice(*keys_subset)
150
150
  Benchmark.ips do |x|
151
151
  x.config(time: 20, warmup: 5)
152
152
  x.report("store_reports_subset") { store.save_report(report_subset) }
@@ -183,7 +183,7 @@ namespace :benchmarks do
183
183
 
184
184
  def measure_memory_report_coverage
185
185
  require "memory_profiler"
186
- report = fake_report
186
+ fake_report
187
187
  store = benchmark_redis_store
188
188
  store.clear!
189
189
  mock_files(store)
@@ -258,37 +258,29 @@ namespace :benchmarks do
258
258
  # about 2mb
259
259
  puts(ObjectSpace.memsize_of(data) / 2**20)
260
260
 
261
- json_data = JSON.parse(data)
261
+ JSON.parse(data)
262
262
  # this seems to just show the value of the pointer
263
263
  # puts(ObjectSpace.memsize_of(json_data) / 2**20)
264
264
  # implies json takes 10-12 mb
265
265
  puts(ObjectSpace.memsize_of_all / 2**20)
266
-
267
- json_data = nil
268
266
  GC.start
269
- json_data = JSON.parse(data)
267
+ JSON.parse(data)
270
268
  # this seems to just show the value of the pointer
271
269
  # puts(ObjectSpace.memsize_of(json_data) / 2**20)
272
270
  # implies json takes 10-12 mb
273
271
  puts(ObjectSpace.memsize_of_all / 2**20)
274
-
275
- json_data = nil
276
272
  GC.start
277
- json_data = JSON.parse(data)
273
+ JSON.parse(data)
278
274
  # this seems to just show the value of the pointer
279
275
  # puts(ObjectSpace.memsize_of(json_data) / 2**20)
280
276
  # implies json takes 10-12 mb
281
277
  puts(ObjectSpace.memsize_of_all / 2**20)
282
-
283
- json_data = nil
284
278
  GC.start
285
- json_data = JSON.parse(data)
279
+ JSON.parse(data)
286
280
  # this seems to just show the value of the pointer
287
281
  # puts(ObjectSpace.memsize_of(json_data) / 2**20)
288
282
  # implies json takes 10-12 mb
289
283
  puts(ObjectSpace.memsize_of_all / 2**20)
290
-
291
- json_data = nil
292
284
  GC.start
293
285
  puts(ObjectSpace.memsize_of_all / 2**20)
294
286
 
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path("../test_helper", File.dirname(__FILE__))
4
+
5
+ class FileStoreIntegrationTest < Minitest::Test
6
+ def setup
7
+ super
8
+ # Reset state
9
+ Thread.current[:coverband_instance] = nil
10
+ Coverband.class_variable_set(:@@configured, false)
11
+ end
12
+
13
+ def teardown
14
+ super
15
+ Thread.current[:coverband_instance] = nil
16
+ Coverband.configure do |config|
17
+ # Reset to default
18
+ end
19
+ Coverband.class_variable_set(:@@configured, false)
20
+ end
21
+
22
+ test "auto-start with FileStore configuration does not try Redis" do
23
+ # Clear any existing store to simulate fresh start
24
+ Coverband.configuration.instance_variable_set(:@store, nil)
25
+
26
+ # This simulates the typical user setup
27
+ Coverband.configure do |config|
28
+ config.root = Dir.pwd
29
+ config.background_reporting_enabled = false
30
+ config.store = Coverband::Adapters::FileStore.new("tmp/integration_test_coverage")
31
+ config.logger = Logger.new($stdout)
32
+ config.verbose = false
33
+ end
34
+
35
+ # Start coverband - this should work without Redis errors
36
+ begin
37
+ Coverband.start
38
+ Coverband.report_coverage
39
+ rescue Redis::CannotConnectError => e
40
+ flunk "Should not try to connect to Redis when FileStore is configured: #{e.message}"
41
+ rescue => e
42
+ flunk "Unexpected error: #{e.class}: #{e.message}"
43
+ end
44
+
45
+ # Verify we're using the correct store
46
+ assert_instance_of Coverband::Adapters::FileStore, Coverband.configuration.store
47
+ end
48
+
49
+ test "store fallback works even in auto-start scenario" do
50
+ # Backup original ENV to avoid side effects
51
+ original_disable = ENV["COVERBAND_DISABLE_AUTO_START"]
52
+
53
+ begin
54
+ # Enable auto-start
55
+ ENV.delete("COVERBAND_DISABLE_AUTO_START")
56
+
57
+ # Clear configuration state
58
+ Coverband.configuration.instance_variable_set(:@store, nil)
59
+ Coverband.class_variable_set(:@@configured, false)
60
+
61
+ # Mock Redis to fail
62
+ Redis.expects(:new).raises(Redis::CannotConnectError.new("Connection refused")).at_least_once
63
+
64
+ # This should not raise an error even with Redis unavailable
65
+ store = Coverband.configuration.store
66
+
67
+ assert_instance_of Coverband::Adapters::NullStore, store
68
+ ensure
69
+ ENV["COVERBAND_DISABLE_AUTO_START"] = original_disable
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path("../test_helper", File.dirname(__FILE__))
4
+
5
+ class FileStoreRedisErrorTest < Minitest::Test
6
+ def setup
7
+ super
8
+ # Reset the singleton instance to ensure clean state
9
+ Coverband::Collectors::Coverage.instance.reset_instance
10
+ end
11
+
12
+ def teardown
13
+ super
14
+ Thread.current[:coverband_instance] = nil
15
+ Coverband.configure do |config|
16
+ # Reset to default redis store
17
+ end
18
+ Coverband::Collectors::Coverage.instance.reset_instance
19
+ end
20
+
21
+ test "Default store gracefully handles Redis connection failure" do
22
+ # Clear any existing store configuration
23
+ Coverband.configuration.instance_variable_set(:@store, nil)
24
+
25
+ # Mock Redis to simulate connection failure
26
+ Redis.expects(:new).raises(Redis::CannotConnectError.new("Connection refused"))
27
+
28
+ # This should not raise an error, instead it should fall back to NullStore
29
+ store = Coverband.configuration.store
30
+
31
+ assert_instance_of Coverband::Adapters::NullStore, store
32
+ end
33
+
34
+ test "FileStore configuration overrides default store" do
35
+ # Clear any existing store
36
+ Coverband.configuration.instance_variable_set(:@store, nil)
37
+
38
+ file_store = Coverband::Adapters::FileStore.new("tmp/test_coverage")
39
+ Coverband.configure do |config|
40
+ config.store = file_store
41
+ end
42
+
43
+ assert_same file_store, Coverband.configuration.store
44
+ assert_instance_of Coverband::Adapters::FileStore, Coverband.configuration.store
45
+ end
46
+
47
+ test "FileStore is properly set after configuration" do
48
+ store_path = "tmp/test_coverage"
49
+ Coverband.configure do |config|
50
+ config.store = Coverband::Adapters::FileStore.new(store_path)
51
+ end
52
+
53
+ assert_instance_of Coverband::Adapters::FileStore, Coverband.configuration.store
54
+ refute Coverband.configuration.store.is_a?(Coverband::Adapters::RedisStore)
55
+ end
56
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This test demonstrates the fix for GitHub issue #586
4
+ # https://github.com/danmayer/coverband/issues/586
5
+
6
+ require File.expand_path("../test_helper", File.dirname(__FILE__))
7
+
8
+ class GitHubIssue586Test < Minitest::Test
9
+ def setup
10
+ super
11
+ # Reset the singleton instance to ensure clean state
12
+ Coverband::Collectors::Coverage.instance.reset_instance
13
+ end
14
+
15
+ def teardown
16
+ super
17
+ Thread.current[:coverband_instance] = nil
18
+ Coverband.configure do |config|
19
+ # Reset to default
20
+ end
21
+ Coverband::Collectors::Coverage.instance.reset_instance
22
+ end
23
+
24
+ test "GitHub issue #586: FileStore should not cause Redis connection errors" do
25
+ # This reproduces the exact configuration from the GitHub issue
26
+ Coverband.configure do |config|
27
+ config.root = Dir.pwd
28
+ config.background_reporting_enabled = false
29
+ config.store = Coverband::Adapters::FileStore.new("tmp/coverband_log")
30
+ config.logger = Logger.new($stdout)
31
+ config.verbose = false
32
+ end
33
+
34
+ # This should not raise Redis connection errors
35
+ begin
36
+ # Simulate running ruby code to analyze
37
+ Coverband.start
38
+ Coverband.report_coverage
39
+
40
+ # Verify the store is what the user configured
41
+ assert_instance_of Coverband::Adapters::FileStore, Coverband.configuration.store
42
+ rescue Redis::CannotConnectError => e
43
+ flunk "Should not get Redis connection error when using FileStore: #{e.message}"
44
+ end
45
+ end
46
+ end