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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/diagram.yml +2 -2
  3. data/.github/workflows/main.yml +64 -7
  4. data/Gemfile +16 -0
  5. data/Gemfile.rails7.0 +12 -0
  6. data/Gemfile.rails7.1 +6 -1
  7. data/Gemfile.rails7.2 +5 -0
  8. data/Gemfile.rails8.0 +15 -0
  9. data/README.md +218 -12
  10. data/Rakefile +16 -5
  11. data/changes.md +60 -0
  12. data/coverband.gemspec +15 -13
  13. data/lib/coverband/adapters/active_support_cache_store.rb +54 -0
  14. data/lib/coverband/adapters/base.rb +42 -10
  15. data/lib/coverband/adapters/file_store.rb +2 -2
  16. data/lib/coverband/adapters/hash_redis_store.rb +22 -3
  17. data/lib/coverband/adapters/null_store.rb +0 -4
  18. data/lib/coverband/adapters/redis_cleanup.rb +101 -0
  19. data/lib/coverband/adapters/redis_store.rb +32 -65
  20. data/lib/coverband/adapters/session_coverage.rb +172 -0
  21. data/lib/coverband/adapters/stdout_store.rb +0 -4
  22. data/lib/coverband/adapters/tracker_storage/base.rb +79 -0
  23. data/lib/coverband/adapters/tracker_storage/document_repository.rb +49 -0
  24. data/lib/coverband/adapters/tracker_storage/factory.rb +57 -0
  25. data/lib/coverband/adapters/tracker_storage/redis_hash_repository.rb +127 -0
  26. data/lib/coverband/collectors/abstract_tracker.rb +135 -51
  27. data/lib/coverband/collectors/query_burst_tracker.rb +72 -19
  28. data/lib/coverband/collectors/route_tracker.rb +6 -0
  29. data/lib/coverband/collectors/tracker_registry.rb +63 -0
  30. data/lib/coverband/collectors/translation_tracker.rb +6 -0
  31. data/lib/coverband/collectors/view_tracker.rb +11 -5
  32. data/lib/coverband/configuration.rb +95 -131
  33. data/lib/coverband/mcp/http_handler.rb +1 -1
  34. data/lib/coverband/mcp/server.rb +4 -1
  35. data/lib/coverband/mcp/tools/get_storage_health.rb +28 -0
  36. data/lib/coverband/reporters/base.rb +0 -2
  37. data/lib/coverband/reporters/html_report.rb +2 -3
  38. data/lib/coverband/reporters/json_report.rb +2 -2
  39. data/lib/coverband/reporters/web.rb +72 -15
  40. data/lib/coverband/storage/document.rb +173 -0
  41. data/lib/coverband/storage/generation.rb +165 -0
  42. data/lib/coverband/storage/generation_change.rb +53 -0
  43. data/lib/coverband/storage/generation_coordinator.rb +145 -0
  44. data/lib/coverband/storage/io_guard.rb +29 -0
  45. data/lib/coverband/storage/read_fallback.rb +30 -0
  46. data/lib/coverband/storage/redis_target.rb +74 -0
  47. data/lib/coverband/storage/session.rb +527 -0
  48. data/lib/coverband/storage/target.rb +117 -0
  49. data/lib/coverband/storage/writer.rb +194 -0
  50. data/lib/coverband/storage_health.rb +90 -0
  51. data/lib/coverband/utils/configuration_template.rb +14 -1
  52. data/lib/coverband/utils/railtie.rb +2 -2
  53. data/lib/coverband/utils/result.rb +5 -5
  54. data/lib/coverband/utils/results.rb +32 -16
  55. data/lib/coverband/utils/source_file.rb +17 -27
  56. data/lib/coverband/utils/tasks.rb +71 -6
  57. data/lib/coverband/version.rb +1 -1
  58. data/lib/coverband.rb +98 -20
  59. data/public/application.css +540 -146
  60. data/test/benchmarks/benchmark.rake +13 -0
  61. data/test/coverband/adapters/active_support_cache_store_test.rb +613 -0
  62. data/test/coverband/adapters/capabilities_test.rb +80 -0
  63. data/test/coverband/adapters/redis_store_test.rb +63 -2
  64. data/test/coverband/adapters/solid_cache_store_test.rb +181 -0
  65. data/test/coverband/adapters/tracker_storage_test.rb +170 -0
  66. data/test/coverband/collectors/query_burst_tracker_test.rb +155 -10
  67. data/test/coverband/collectors/route_tracker_test.rb +2 -13
  68. data/test/coverband/collectors/tracker_registry_test.rb +218 -0
  69. data/test/coverband/collectors/translation_tracker_test.rb +76 -16
  70. data/test/coverband/collectors/view_tracker_test.rb +49 -12
  71. data/test/coverband/configuration_test.rb +36 -38
  72. data/test/coverband/mcp/server_test.rb +2 -1
  73. data/test/coverband/mcp/tools/get_storage_health_test.rb +41 -0
  74. data/test/coverband/reporters/html_test.rb +26 -0
  75. data/test/coverband/reporters/json_test.rb +53 -0
  76. data/test/coverband/reporters/web_test.rb +96 -0
  77. data/test/coverband/storage/generation_coordinator_test.rb +191 -0
  78. data/test/coverband/storage/io_guard_test.rb +127 -0
  79. data/test/coverband/storage/protocol_conformance_test.rb +725 -0
  80. data/test/coverband/storage_health_test.rb +99 -0
  81. data/test/coverband/track_key_test.rb +8 -0
  82. data/test/coverband/utils/cleanup_tasks_test.rb +188 -0
  83. data/test/coverband/utils/file_list_test.rb +2 -2
  84. data/test/coverband/utils/result_test.rb +4 -4
  85. data/test/coverband/utils/results_test.rb +100 -10
  86. data/test/coverband/utils/source_file_test.rb +40 -6
  87. data/test/forked/rails_cache_full_stack_test.rb +64 -0
  88. data/test/forked/rails_full_stack_test.rb +7 -0
  89. data/test/integration/full_stack_deferred_eager_test.rb +0 -1
  90. data/test/integration/full_stack_send_deferred_eager_test.rb +0 -1
  91. data/test/integration/full_stack_test.rb +0 -1
  92. data/test/integration/mcp_integration_test.rb +4 -2
  93. data/test/integration/standalone_json_report_test.rb +46 -0
  94. data/test/integration/without_redis_test.rb +76 -0
  95. data/test/rails7_dummy/config/application.rb +1 -0
  96. data/test/rails7_dummy/config/coverband_missing_redis.rb +0 -2
  97. data/test/rails7_dummy/config/coverband_rails_cache.rb +16 -0
  98. data/test/rails8_dummy/config/application.rb +1 -0
  99. data/test/rails8_dummy/config/coverband_rails_cache.rb +3 -0
  100. data/test/rails_test_helper.rb +2 -1
  101. data/test/test_helper.rb +5 -17
  102. data/views/file_list.erb +2 -2
  103. data/views/source_file.erb +4 -3
  104. metadata +48 -40
  105. data/lib/coverband/adapters/memcached_store.rb +0 -85
  106. data/lib/coverband/adapters/web_service_store.rb +0 -152
  107. data/lib/coverband/collectors/view_tracker_service.rb +0 -59
  108. data/test/coverband/adapters/memecached_store_test.rb +0 -26
  109. data/test/coverband/adapters/web_service_store_test.rb +0 -56
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20188ba8fca24027b2a7bd367ac88586b7dace04a6eadfd9714bdf509849683e
4
- data.tar.gz: c2db00de6ffaa32256d083c7d51290a5936f5e2ee46c774eed901f91fa0ba545
3
+ metadata.gz: 79f6cb61a683aceb3212e907c217ec09292425cab8e0a1974815f81d2ece54dd
4
+ data.tar.gz: 510955093e4918001fe135149cfe81e4b95ac76a744996556f19cce3b4478bf1
5
5
  SHA512:
6
- metadata.gz: 22b5b1281136a3ed3172383d84526f1659568e6b204cc38b636d929c76b8ec84c9b70d20e61a54484cf97d8604e268de9da87cc596bbff2cbb81752c1078ca8b
7
- data.tar.gz: aa55b358d2a3a196fd5de29f850ad41a877274649ccf377395a114c6e940bc926424fccbe6a3e7642225eaa81432f73094443b5874c8e79ac75cccb39a6c3422
6
+ metadata.gz: b3ce502e37b7de70e3afecd45bf050dd0c8b0c46511d348b5dc3ae836c085cf2bcd51a8978cfb375c615a3e24238ffe34366139e8351c8c05b5d1b239857f572
7
+ data.tar.gz: 5fac050e3420d6bda53425f63b9e145b1688194a430cadcad3a2b634476f26a71c8237317b9a3237fade84835b9047fa09e5107084f8a6c6e32d782126386aff
@@ -6,7 +6,7 @@ on:
6
6
  - main
7
7
  jobs:
8
8
  get_data:
9
- runs-on: ubuntu-latest
9
+ runs-on: ubuntu-24.04
10
10
  steps:
11
11
  - name: Checkout code
12
12
  uses: actions/checkout@master
@@ -14,4 +14,4 @@ jobs:
14
14
  uses: githubocto/repo-visualizer@main
15
15
  with:
16
16
  excluded_paths: "ignore,.github"
17
- branch: diagram
17
+ branch: diagram
@@ -13,8 +13,8 @@ jobs:
13
13
  fail-fast: false
14
14
  matrix:
15
15
  # need to figure out how to use redis on macos github actions
16
- # os: [ubuntu, macos]
17
- os: [ubuntu]
16
+ # os: [ubuntu-24.04, macos-15]
17
+ os: [ubuntu-24.04]
18
18
  # remove until I sort out CI issues for truffle
19
19
  # truffleruby,
20
20
  # truffleruby-head,
@@ -25,9 +25,11 @@ jobs:
25
25
  redis-version: [4, 5, 6, 7]
26
26
  exclude:
27
27
  # Rails 8 requires ruby 3.2+.
28
- - gemfile: 'rails_8.0'
28
+ - gemfile: 'Gemfile.rails8.0'
29
29
  ruby: '3.1'
30
- runs-on: ${{ matrix.os }}-latest
30
+ runs-on: ${{ matrix.os }}
31
+ env:
32
+ BUNDLE_GEMFILE: ${{ matrix.gemfile }}
31
33
  services:
32
34
  redis:
33
35
  image: redis:${{ matrix.redis-version }}
@@ -38,6 +40,15 @@ jobs:
38
40
  --health-interval 10s
39
41
  --health-timeout 5s
40
42
  --health-retries 5
43
+ memcached:
44
+ image: memcached:1.6
45
+ ports:
46
+ - 11211:11211
47
+ options: >-
48
+ --health-cmd "timeout 2 bash -c '</dev/tcp/127.0.0.1/11211'"
49
+ --health-interval 10s
50
+ --health-timeout 5s
51
+ --health-retries 5
41
52
  steps:
42
53
  - uses: actions/checkout@v7
43
54
  - uses: ruby/setup-ruby@v1
@@ -45,9 +56,26 @@ jobs:
45
56
  ruby-version: ${{ matrix.ruby }}
46
57
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
47
58
  - run: bundle exec rake test:all
59
+ env:
60
+ # the memcached adapter has an ENV gate so local runs need no service,
61
+ # which also meant CI never exercised it
62
+ COVERBAND_MEMCACHED: "true"
48
63
  - run: "RUBYOPT='--enable=frozen-string-literal --debug=frozen-string-literal' bundle exec rake"
64
+ env:
65
+ COVERBAND_MEMCACHED: "true"
66
+ # Solid Cache needs Rails 7.2+, and its engine changes process wide state,
67
+ # so it runs on its own, only where it is available.
68
+ #
69
+ # Its gems sit in an optional bundler group, which bundler-cache does not
70
+ # install, so the group is installed here rather than assumed present.
71
+ - name: Solid Cache adapter tests
72
+ if: matrix.gemfile == 'Gemfile.rails8.0'
73
+ run: |
74
+ bundle config set --local with solid_cache
75
+ bundle install --jobs 4
76
+ bundle exec rake test:solid_cache
49
77
  starndardrb:
50
- runs-on: ubuntu-latest
78
+ runs-on: ubuntu-24.04
51
79
  steps:
52
80
  - uses: actions/checkout@v7
53
81
  - uses: ruby/setup-ruby@v1
@@ -56,10 +84,39 @@ jobs:
56
84
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
57
85
  - run: bundle exec standardrb --format github
58
86
  javascript:
59
- runs-on: ubuntu-latest
87
+ runs-on: ubuntu-24.04
60
88
  steps:
61
89
  - uses: actions/checkout@v7
62
- - uses: actions/setup-node@v4
90
+ - uses: actions/setup-node@v7
63
91
  with:
64
92
  node-version: "20"
65
93
  - run: node --test test/javascript/*.test.mjs
94
+ json3:
95
+ runs-on: ubuntu-24.04
96
+ env:
97
+ BUNDLE_GEMFILE: Gemfile
98
+ services:
99
+ redis:
100
+ image: redis:7
101
+ ports:
102
+ - 6379:6379
103
+ options: >-
104
+ --health-cmd "redis-cli ping"
105
+ --health-interval 10s
106
+ --health-timeout 5s
107
+ --health-retries 5
108
+ memcached:
109
+ image: memcached:1.6
110
+ ports:
111
+ - 11211:11211
112
+ steps:
113
+ - uses: actions/checkout@v7
114
+ - uses: ruby/setup-ruby@v1
115
+ with:
116
+ ruby-version: "3.4"
117
+ bundler-cache: true
118
+ - name: Verify JSON 3 resolution
119
+ run: bundle exec ruby -rjson -e 'abort "expected JSON 3" unless JSON::VERSION.start_with?("3.")'
120
+ - run: bundle exec rake test:all
121
+ env:
122
+ COVERBAND_MEMCACHED: "true"
data/Gemfile CHANGED
@@ -4,6 +4,7 @@ source "https://rubygems.org"
4
4
 
5
5
  # Specify your gem's dependencies in coverband.gemspec
6
6
  gemspec
7
+ gem "json", ">= 3", "< 4" # Exercise Coverband itself against JSON 3.
7
8
  gem "rails" # latest
8
9
  gem "haml"
9
10
  gem "slim"
@@ -11,3 +12,18 @@ gem "webrick"
11
12
 
12
13
  # Required for Ruby 3.4+ (extracted from stdlib)
13
14
  gem "cgi"
15
+
16
+ # ActiveSupport 8.1's MemCacheStore passes positional options to ConnectionPool,
17
+ # which connection_pool 3.x turned into keyword arguments. Pinned so the
18
+ # memcached backed cache adapter tests can actually run.
19
+ gem "connection_pool", "~> 2.5"
20
+
21
+ # Solid Cache is a central target for the ActiveSupport::Cache adapter: it is
22
+ # how Coverband reaches Postgres, MySQL, and SQLite. Needs Rails 7.2+.
23
+ #
24
+ # Kept out of the default group on purpose: the dummy Rails apps require every
25
+ # default gem, and Solid Cache's engine expects an ActiveJob they do not load.
26
+ group :solid_cache, optional: true do
27
+ gem "solid_cache"
28
+ gem "sqlite3"
29
+ end
data/Gemfile.rails7.0 CHANGED
@@ -4,10 +4,22 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in coverband.gemspec
6
6
  gemspec
7
+ gem "json", "< 3" # ActiveSupport 7.0 and Ruby 3.1-era multi_json reject JSON 3.
7
8
  gem 'rails', '~>7.0.0'
8
9
  gem "haml"
9
10
  gem "slim"
10
11
  gem "webrick"
11
12
 
13
+ # ActiveSupport's MemCacheStore passes positional options to ConnectionPool,
14
+ # which connection_pool 3.x turned into keyword arguments.
15
+ gem "connection_pool", "~> 2.5"
16
+
17
+ # Rack 2 requires ostruct, which Ruby 4 no longer ships as a default gem.
18
+ gem "ostruct"
19
+
12
20
  # Required for Ruby 3.4+ (extracted from stdlib)
13
21
  gem "cgi"
22
+
23
+ # railties 7.0 requires tsort without declaring it, and Ruby 4.1 no longer
24
+ # ships it as a default gem.
25
+ gem "tsort"
data/Gemfile.rails7.1 CHANGED
@@ -4,10 +4,15 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in coverband.gemspec
6
6
  gemspec
7
+ gem "json", "< 3" # ActiveSupport 7.1 passes options that JSON 3 rejects.
7
8
  gem 'rails', '~>7.1.0'
8
9
  gem "haml"
9
10
  gem "slim"
10
11
  gem "webrick"
11
12
 
13
+ # ActiveSupport's MemCacheStore passes positional options to ConnectionPool,
14
+ # which connection_pool 3.x turned into keyword arguments.
15
+ gem "connection_pool", "~> 2.5"
16
+
12
17
  # Required for Ruby 3.4+ (extracted from stdlib)
13
- gem "cgi"
18
+ gem "cgi"
data/Gemfile.rails7.2 CHANGED
@@ -4,10 +4,15 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in coverband.gemspec
6
6
  gemspec
7
+ gem "json", "< 3" # ActiveSupport 7.2 passes options that JSON 3 rejects.
7
8
  gem 'rails', '~>7.2.0'
8
9
  gem "haml"
9
10
  gem "slim"
10
11
  gem "webrick"
11
12
 
13
+ # ActiveSupport's MemCacheStore passes positional options to ConnectionPool,
14
+ # which connection_pool 3.x turned into keyword arguments.
15
+ gem "connection_pool", "~> 2.5"
16
+
12
17
  # Required for Ruby 3.4+ (extracted from stdlib)
13
18
  gem "cgi"
data/Gemfile.rails8.0 CHANGED
@@ -4,10 +4,25 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in coverband.gemspec
6
6
  gemspec
7
+ gem "json", "< 3" # ActiveSupport 8.0 passes options that JSON 3 rejects.
7
8
  gem 'rails', '~>8.0.0'
8
9
  gem "haml"
9
10
  gem "slim"
10
11
  gem "webrick"
11
12
 
13
+ # ActiveSupport's MemCacheStore passes positional options to ConnectionPool,
14
+ # which connection_pool 3.x turned into keyword arguments.
15
+ gem "connection_pool", "~> 2.5"
16
+
12
17
  # Required for Ruby 3.4+ (extracted from stdlib)
13
18
  gem "cgi"
19
+
20
+ # Solid Cache is a central target for the ActiveSupport::Cache adapter: it is
21
+ # how Coverband reaches Postgres, MySQL, and SQLite. Needs Rails 7.2+.
22
+ #
23
+ # Kept out of the default group on purpose: the dummy Rails apps require every
24
+ # default gem, and Solid Cache's engine expects an ActiveJob they do not load.
25
+ group :solid_cache, optional: true do
26
+ gem "solid_cache"
27
+ gem "sqlite3"
28
+ end
data/README.md CHANGED
@@ -3,7 +3,6 @@
3
3
  # Coverband
4
4
 
5
5
  [![GithubCI](https://github.com/danmayer/coverband/workflows/CI/badge.svg)](https://github.com/danmayer/coverband/actions)
6
- [![Coverage Status](https://coveralls.io/repos/github/danmayer/coverband/badge.svg?branch=main)](https://coveralls.io/github/danmayer/coverband?branch=main)
7
6
  [![Maintainability](https://api.codeclimate.com/v1/badges/1e6682f9540d75f26da7/maintainability)](https://codeclimate.com/github/danmayer/coverband/maintainability)
8
7
  [![Discord Shield](https://img.shields.io/discord/609509533999562753)](https://discord.gg/KAH38EV)
9
8
 
@@ -69,12 +68,15 @@ The `rediss://` scheme works with:
69
68
 
70
69
  ## Gem Installation
71
70
 
72
- Add this line to your application's `Gemfile`, remember to `bundle install` after updating:
71
+ Add these lines to your application's `Gemfile`, remember to `bundle install` after updating:
73
72
 
74
- ```bash
73
+ ```ruby
75
74
  gem 'coverband'
75
+ gem 'redis' # the default store
76
76
  ```
77
77
 
78
+ Redis is the default store and needs no configuration beyond a reachable Redis. Since Coverband 7.0 the `redis` gem is no longer installed for you, so add it yourself as above. If you would rather keep coverage somewhere else (Solid Cache, memcached, files), leave out `redis` and set `config.store`, see [ActiveSupport::Cache Store](#activesupportcache-store). Without the `redis` gem and without a configured store, Coverband logs a warning at boot and records nothing. Upgrading from 6.x? See [Upgrading to 7.0](#upgrading-to-70).
79
+
78
80
  ### No custom code or middleware required
79
81
 
80
82
  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.
@@ -97,6 +99,46 @@ use Coverband::BackgroundMiddleware
97
99
  run ActionController::Dispatcher.new
98
100
  ```
99
101
 
102
+ ## Upgrading to 7.0
103
+
104
+ 7.0 is a major release with breaking changes. Most apps only need the first two steps. The full list is in the [changelog](https://github.com/danmayer/coverband/blob/main/changes.md).
105
+
106
+ 1. **Add the `redis` gem if you use Redis.** Coverband no longer installs it for you. Redis is still the default store and still needs no other configuration once the gem is in your Gemfile:
107
+
108
+ ```ruby
109
+ gem 'coverband'
110
+ gem 'redis'
111
+ ```
112
+
113
+ Without it, and without a configured `config.store`, Coverband logs a warning at boot and records nothing.
114
+
115
+ 2. **Coverage starts fresh.** The storage format changed, so data recorded by 6.x is ignored rather than migrated. Once you are happy with the new data, delete the old keys with `rake coverband:clear_legacy` (Redis only).
116
+
117
+ 3. **Delete removed settings from `config/coverband.rb`.** These now do nothing. Assigning one logs a warning naming the setting, but the app still boots:
118
+
119
+ | Setting | What to do |
120
+ | --- | --- |
121
+ | `reporter` | Delete it. It has had no effect since Coverband 5.0. |
122
+ | `api_key`, `service_url`, `coverband_timeout`, `service_dev_mode`, `service_test_mode`, `process_type` | Delete them, see the coverband.io step below. |
123
+ | `s3_region`, `s3_bucket`, `s3_access_key_id`, `s3_secret_access_key`, `track_gems`, `gem_details`, `simulate_oneshot_lines_coverage` | Delete them. They were already no-ops. |
124
+
125
+ 4. **coverband.io users: configure a store.** coverband.io is no longer maintained, so reporting to it (`WebServiceStore`) has been removed, along with `config/coverband_service.rb` and the `COVERBAND_API_KEY`, `COVERBAND_URL`, `COVERBAND_ENABLE_DEV_MODE`, `COVERBAND_ENABLE_TEST_MODE`, and `PROCESS_TYPE` environment variables. Move any settings from `config/coverband_service.rb` into `config/coverband.rb` and use Redis (step 1) or an [ActiveSupport::Cache store](#activesupportcache-store).
126
+
127
+ 5. **`MemcachedStore` users: switch to `ActiveSupportCacheStore`.** `MemcachedStore` has been removed. The replacement takes the same cache and adds tracker support:
128
+
129
+ ```ruby
130
+ # before
131
+ config.store = Coverband::Adapters::MemcachedStore.new(cache, memcached_namespace: "myapp")
132
+ # after
133
+ config.store = Coverband::Adapters::ActiveSupportCacheStore.new(cache, cache_namespace: "myapp")
134
+ ```
135
+
136
+ 6. **Custom code against Coverband internals.** Only relevant if you wrote a tracker or call adapters directly:
137
+ - In a custom tracker, use `storage` instead of `redis_store`.
138
+ - Instead of `store.raw_store`, use `store.cleanup` (`clear_legacy!`, `clear_orphans!`), which returns `nil` for stores that cannot enumerate keys.
139
+ - Replace calls to the misspelled `Coverband::Utils::Results#runtime_relavent_lines` with `#runtime_relevant_lines`; the old spelling was removed in 7.0.
140
+ - `Coverband::Utils::SourceFile` and `Coverband::Utils::Result` take stored report records (`{"data" => [...]}`) rather than bare line arrays.
141
+
100
142
  ## Coverband Web UI
101
143
 
102
144
  ![image](https://raw.github.com/danmayer/coverband/main/docs/coverband_web_ui.png)
@@ -129,15 +171,20 @@ run ActionController::Dispatcher.new
129
171
 
130
172
  The columns in the web UI are as follows:
131
173
 
132
- - **% covered** - Percentage of total relevant lines covered
133
- - **% runtime** - Percentage of the runtime lines covered where runtime lines are lines that are hit after the application has been eagerly loaded
174
+ - **% covered** - Percentage of relevant lines covered, during either eager loading or runtime
175
+ - **% runtime** - Percentage of the runtime relevant lines that were hit at runtime. Runtime relevant lines are the relevant lines that were *not* hit while the application eager loaded, so this is the share of the code that only real usage can exercise (method bodies, branches, etc.) which real usage actually exercised
134
176
  - **Lines** - Total lines in the file including lines unreachable or uncover-able. An unreachable line would be an empty line with no code, comments, or `end` statements.
135
177
  - **Relevant lines** - Lines that are coverable, i.e. not empty
136
- - **Lines runtime** - Total lines minus uncoverable lines minus the lines that are only hit during eager loading of application
178
+ - **Lines covered** - Relevant lines hit during either eager loading or runtime
179
+ - **Lines runtime** - Relevant lines hit at least once at runtime
137
180
  - **Lines missed** - Relevant lines not covered
138
181
  - **Avg hits/line** - Total of coverage to the file divided by relevant lines.
139
182
 
140
- When viewing an individual file, a line of code such as a class or method definition may appear green because it is eager loaded by the application, but still not be hit at all in runtime by actual users.
183
+ The individual file view also lists the file's **runtime relevant lines**, the denominator of **% runtime**. A file only reports 100% runtime when every one of its runtime relevant lines was hit at runtime. If a file was never eager loaded (the default in development), every relevant line is runtime relevant, so **% runtime** matches **% covered**.
184
+
185
+ **Lines runtime** can include load time lines such as class and method definitions. Those lines are hit at runtime when a process loads the file lazily after Coverband switched to runtime coverage, e.g. a console, runner, or job process that does not eager load. Such lines still count as eager loaded for **% runtime** as long as any process loaded them during eager loading.
186
+
187
+ When viewing an individual file, a line of code such as a class or method definition may appear green because it is eager loaded by the application, but still not be hit at all in runtime by actual users. Each line's badge shows its `load` (eager loading) and `runtime` hit counts separately.
141
188
 
142
189
  ![example of a file with lines not hit at runtime](https://user-images.githubusercontent.com/96786/63541229-aa98a580-c4eb-11e9-8eb8-c004fe1369db.png)
143
190
  #### Coverage first seen and Last activity recorded
@@ -233,7 +280,7 @@ Take Coverband for a spin on the live Heroku deployed [Coverband Demo](https://c
233
280
  If you need to configure Coverband, this can be done by creating a `config/coverband.rb` file relative to your project root.
234
281
 
235
282
  - See [lib/coverband/configuration.rb](https://github.com/danmayer/coverband/blob/main/lib/coverband/configuration.rb) for all options
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`
283
+ - By default Coverband will try to store data to Redis (when the `redis` gem is installed) \* Redis endpoint is looked for in this order: `ENV['COVERBAND_REDIS_URL']`, `ENV['REDIS_URL']`, or `localhost`
237
284
 
238
285
  Below is an example config file for a Rails 5 app:
239
286
 
@@ -358,6 +405,61 @@ For each tracked key, the Query Burst Tracker reports:
358
405
 
359
406
  Tip: use this tracker in production-like traffic, where query patterns are most realistic.
360
407
 
408
+ ### Writing a Custom Tracker
409
+
410
+ Trackers can be shipped by an application or a separate gem without changing Coverband. A tracker subclasses `Coverband::Collectors::AbstractTracker`, registers itself once when its file is required, and is initialized at Rails boot when its enablement block returns true.
411
+
412
+ ```ruby
413
+ # lib/my_app/coverband/feature_flag_tracker.rb
414
+ class FeatureFlagTracker < Coverband::Collectors::AbstractTracker
415
+ # required: the path this tracker reports on inside the Coverband web report,
416
+ # and the label for its tab
417
+ REPORT_ROUTE = "feature_flag_tracker"
418
+ TITLE = "Feature Flags"
419
+
420
+ def railtie!
421
+ tracker = self
422
+ ActiveSupport::Notifications.subscribe("feature_flag.my_app") do |_name, _start, _finish, _id, payload|
423
+ tracker.track_key(payload.fetch(:key))
424
+ end
425
+ end
426
+
427
+ private
428
+
429
+ def concrete_target
430
+ MyApp::FeatureFlags.all.map(&:key)
431
+ end
432
+ end
433
+
434
+ Coverband::Configuration.add_tracker_flag(:track_feature_flags, default: false)
435
+ Coverband::Collectors::TrackerRegistry.register(
436
+ :feature_flag_tracker,
437
+ tracker_class: FeatureFlagTracker,
438
+ enabled: ->(config) { config.track_feature_flags }
439
+ )
440
+ ```
441
+
442
+ Require the tracker before enabling it in `config/coverband.rb`:
443
+
444
+ ```ruby
445
+ require "my_app/coverband/feature_flag_tracker"
446
+
447
+ Coverband.configure do |config|
448
+ config.track_feature_flags = true
449
+ end
450
+ ```
451
+
452
+ Every tracker must declare its own `REPORT_ROUTE`. `AbstractTracker`'s default of `"/"` matches every path in the web report, so a tracker that inherits it would answer all of Coverband's own report routes. Registering such a tracker raises `ArgumentError` rather than letting it take over the UI.
453
+
454
+ `tracker_class` may also be a callable that returns a tracker instance when initialization needs to choose an implementation. Registration and enablement are evaluated at boot, not on every request. Duplicate tracker names raise `ArgumentError` so load-order mistakes fail early. Coverband's Query Burst Tracker is a bundled example of the same registration pattern.
455
+
456
+ Trackers registered this way have no dedicated `Coverband.configuration` accessor, so reach them by their registered name:
457
+
458
+ ```ruby
459
+ Coverband.configuration.tracker_for(:feature_flag_tracker)
460
+ Coverband.track_key(:feature_flag_tracker, "flags.new_checkout")
461
+ ```
462
+
361
463
  ### Hiding settings
362
464
 
363
465
  Coverband provides a view of all of its current settings. Sometimes you might want to hide this view,
@@ -376,6 +478,14 @@ The web report includes a client-side "dead-code session" toolbar to help drive
376
478
 
377
479
  Session state lives in your browser's localStorage: "Reset session" clears the hidden set; "Start over" (in the modal) clears the marked list.
378
480
 
481
+ ### Dark mode
482
+
483
+ The web report follows your operating system's appearance setting through the CSS `prefers-color-scheme` media query. There is nothing to configure and no toggle in the UI: if your desktop is in dark mode, the report is dark, and it switches back when your desktop does. Printing always uses the light theme.
484
+
485
+ The coverage row colors (covered / missed / never / skipped) are chosen for contrast on the dark surface rather than darkened mechanically, so the states stay as easy to tell apart as in the light theme.
486
+
487
+ If you restyle the report, every color in `public/application.css` resolves through a custom property defined on `:root`, and the dark theme is a single `@media (prefers-color-scheme: dark)` block that redefines those properties. Overriding a property is enough to change both themes from one place.
488
+
379
489
  ### Fixing Coverage Only Shows Loading Hits
380
490
 
381
491
  If all your coverage is being counted as loading or eager_loading coverage, and nothing is showing as runtime Coverage the initialization hook failed for some reason. The most likely reason for this issue is manually calling `eager_load!` on some Plugin/Gem. If you or a plugin is altering the Rails initialization process, you can manually flip Coverband to runtime coverage by calling these two lines, in an `after_initialize` block, in `application.rb`.
@@ -441,6 +551,55 @@ Coverband on very high volume sites with many server processes reporting can hav
441
551
 
442
552
  See more discussion [here](https://github.com/danmayer/coverband/issues/384).
443
553
 
554
+ ### ActiveSupport::Cache Store
555
+
556
+ Coverband can store coverage in any `ActiveSupport::Cache::Store`, which covers
557
+ Redis, Memcached, files, and — through [Solid Cache](https://github.com/rails/solid_cache) —
558
+ Postgres, MySQL, and SQLite.
559
+
560
+ ```ruby
561
+ # Rails.cache does not exist yet when config/coverband.rb loads, so pass a block
562
+ config.store = Coverband::Adapters::ActiveSupportCacheStore.new { Rails.cache }
563
+
564
+ # or hand it a store directly
565
+ config.store = Coverband::Adapters::ActiveSupportCacheStore.new(
566
+ ActiveSupport::Cache::MemCacheStore.new("localhost:11211")
567
+ )
568
+ ```
569
+
570
+ The block form is the one to use with Solid Cache: its models come from a Rails
571
+ engine that has not loaded when `config/coverband.rb` runs, so building a
572
+ `SolidCache::Store` there raises `uninitialized constant
573
+ SolidCache::Store::Entries::Entry`.
574
+
575
+ Coverband starts from `before_configuration`, which is earlier than Rails
576
+ assigns `Rails.cache`, so the first reporting cycle can run before the block can
577
+ resolve. That cycle's coverage is held and written once the cache appears; it is
578
+ not lost, and it does not raise into the booting app.
579
+
580
+ This adapter supports the view, route, translation, and query burst trackers.
581
+ It replaces `MemcachedStore`, which was removed in 7.0 (see
582
+ [Upgrading to 7.0](#upgrading-to-70)).
583
+
584
+ Things to know before pointing it at a cache:
585
+
586
+ - **A cache may evict what Coverband writes.** Use a store that will not expire
587
+ the data: a dedicated Solid Cache database with `max_age: nil` plus a size
588
+ limit, or a Redis instance with no eviction policy. Coverband detects the loss
589
+ and reports it in the web UI when it can, but it cannot prevent it.
590
+ - **Memcached's 1MB value limit applies to every document**, trackers included.
591
+ A large translation or view tracker can exceed it, in which case the write is
592
+ refused and Coverband logs what could not be stored. Raise the slab limit
593
+ (`-I`) or use Redis/Solid Cache for large apps.
594
+ - **`ActiveSupport::Cache::MemoryStore` is per process**, so it is only useful
595
+ for development and tests.
596
+ - **Solid Cache** writes go through ActiveRecord, so size your connection pool
597
+ for one extra connection per process, and prefer a separate cache database so
598
+ Coverband's writes do not evict your application's own cached data.
599
+ - For very large multi-process deployments `Coverband::Adapters::HashRedisStore`
600
+ is still the best option: it avoids write conflicts entirely rather than
601
+ repairing them.
602
+
444
603
  ### Clear Coverage
445
604
 
446
605
  Now that Coverband uses MD5 hashes there should be no reason to manually clear coverage unless one is testing, changing versions, or possibly debugging Coverband itself.
@@ -452,6 +611,28 @@ This will clear both the coverage and trackers data. It can also be done through
452
611
  To clear only coverage data, run `rake coverband:clear_coverage`.
453
612
  To clear only trackers data, run `rake coverband:clear_tracker`.
454
613
 
614
+ Redis-backed stores also support two maintenance tasks:
615
+
616
+ - `rake coverband:clear_legacy` removes Coverband-owned keys from storage
617
+ formats retired by the 7.0 upgrade.
618
+ - `rake coverband:clear_orphans` removes old generation documents that no
619
+ pointer references, after rechecking that each candidate is not live or too
620
+ young to delete safely.
621
+
622
+ In Coverband 7.0, adapters no longer expose their backend through `raw_store`.
623
+ Code that needs these supported maintenance operations can request the narrow
624
+ capability instead:
625
+
626
+ ```ruby
627
+ cleanup = Coverband.configuration.store.cleanup
628
+ cleanup&.clear_legacy!
629
+ cleanup&.clear_orphans!
630
+ ```
631
+
632
+ `RedisStore` and `HashRedisStore` provide this capability. Adapters that cannot
633
+ enumerate their keys return `nil`; clear the backing cache or rely on its expiry
634
+ policy for those stores.
635
+
455
636
  ### Adding Rake Tasks outside of Rails
456
637
 
457
638
  Rails apps should automatically include the tasks via the Railtie.
@@ -550,6 +731,28 @@ The MCP server provides the following tools:
550
731
  | `get_view_tracker_data` | Get view/template usage data |
551
732
  | `get_route_tracker_data` | Get route usage statistics |
552
733
  | `get_translation_tracker_data` | Get translation key usage data |
734
+ | `get_storage_health` | Get observed coverage and tracker storage protocol state |
735
+
736
+ #### Storage health
737
+
738
+ `Coverband.storage_health` returns normalized diagnostics for coverage and every initialized
739
+ tracker. The normal JSON report also includes this value as its top-level `storage_health`
740
+ field, and MCP exposes the same representation through `get_storage_health`:
741
+
742
+ ```ruby
743
+ Coverband.storage_health
744
+ # => {
745
+ # coverage: {status: "ok", data_loss: nil, unwritten: nil},
746
+ # trackers: {view_tracker: {status: "stalled", data_loss: nil,
747
+ # unwritten: {deltas: 3, since: "2026-08-19T12:01:00Z"}}}
748
+ # }
749
+ ```
750
+
751
+ Statuses are `ok`, `stalled`, `data_loss`, or `unsupported`; `data_loss` takes precedence
752
+ when both loss and unwritten work are present. These diagnostics describe state Coverband's
753
+ storage protocol has already observed. They are not an active backend availability or
754
+ connectivity probe. SimpleCov-compatible JSON generated with `for_merged_report` does not
755
+ include this metadata.
553
756
 
554
757
  #### Running the MCP Server
555
758
 
@@ -673,7 +876,7 @@ Outputs:
673
876
  # Prerequisites
674
877
 
675
878
  - Ruby 3.1+
676
- - Coverband currently requires Redis for production usage
879
+ - A production store: Redis by default (add the `redis` gem), or any `ActiveSupport::Cache` store such as Solid Cache or memcached
677
880
 
678
881
  ### Ruby and Rails Version Support
679
882
 
@@ -721,13 +924,16 @@ If you submit a change please make sure the tests and benchmarks are passing.
721
924
  - **total fail** on front end code, for line for line coverage, because of the precompiled template step basically coverage doesn't work well for `erb`, `slim`, and the like.
722
925
  - related it will try to report something, but the line numbers reported for `ERB` files are often off and aren't considered useful. I recommend filtering out .erb using the `config.ignore` option. The default configuration excludes these files
723
926
  - **NOTE:** We now have file level coverage for view files, but don't support line level detail
724
- - **Coverage does NOT work when used alongside Scout APM Auto Instrumentation**
725
- - In an environment that uses Scout's `AUTO_INSTRUMENT=true` (usually production or staging) it stops reporting any coverage, it will show one or two files that have been loaded at the start but everything else will show up as having 0% coverage
927
+ - **Scout APM Auto Instrumentation** — fixed in Ruby 3.4.10 / 4.0.4, broken on earlier Rubies
928
+ - **Symptom:** in an environment that uses Scout's auto instruments (usually production or staging) Coverband stops reporting coverage. You will see one or two files loaded at the start, and everything else shows up as 0% / never loaded. This is dangerous: live code, particularly your controllers, gets reported as dead code.
929
+ - **Cause:** this is a Ruby bug, not a bug in Coverband or Scout. Scout's auto instrumentation installs a `RubyVM::InstructionSequence.load_iseq` hook and loads your files through `RubyVM::InstructionSequence.compile` / `.compile_file`. Until recently those methods did not register a file with the `Coverage` module the way a normal `require` does, so the file was never measured and never even appeared in `Coverage.result`. Note that this affected *every* file loaded while the hook was installed, not just the controllers Scout rewrites, which is why coverage appears to vanish entirely.
930
+ - **Fix:** upgrade to **Ruby >= 3.4.10 or >= 4.0.4**, which include the fix for [Ruby Bug #22018](https://bugs.ruby-lang.org/issues/22018). Coverband and Scout auto instruments then work together with no configuration changes and no minimum version of either gem. Both `lines` and `oneshot_lines` collection modes are unaffected.
931
+ - **Workaround for Ruby <= 3.3:** the fix was not backported to Ruby 3.1, 3.2, or 3.3, so on those versions you must disable auto instruments to collect coverage — set `auto_instruments: false` in `scout_apm.yml` or `SCOUT_AUTO_INSTRUMENTS=false` in the environment.
726
932
  - Bug tracked here: https://github.com/scoutapp/scout_apm_ruby/issues/343
727
933
  - **Coverband, [Elastic APM](https://github.com/elastic/apm-agent-ruby) and resque**
728
934
  - In an environment that uses the Elastic APM ruby agent, resque jobs will fail with `Transactions may not be nested. Already inside #<ElasticAPM::Transaction>` if the `elastic-apm` gem is loaded _before_ the `coverband` gem
729
935
  - Put `coverage` ahead of `elastic-apm` in your Gemfile
730
- - **Bootsnap**, The methods used by [bootsnap do not support having coverage enabled](https://github.com/Shopify/bootsnap/blob/main/lib/bootsnap/compile_cache/iseq.rb#L87), so you can either have Coverband or bootsnap, but not both.
936
+ - **Bootsnap**, you can run bootsnap alongside Coverband, but you lose part of the boot time speedup. [Bootsnap detects that coverage is enabled](https://github.com/Shopify/bootsnap/blob/main/lib/bootsnap/compile_cache/iseq.rb) and disables its `ISeq` compile cache, because a cached instruction sequence would not carry the `Coverage` instrumentation. Bootsnap's other caches (load path, YAML, JSON) are unaffected and keep working.
731
937
 
732
938
  ### Debugging Redis Store
733
939
 
data/Rakefile CHANGED
@@ -21,6 +21,22 @@ Rake::TestTask.new(:test) do |test|
21
21
  test.verbose = true
22
22
  end
23
23
 
24
+ ###
25
+ # Solid Cache brings a Rails engine with it, which changes how the rest of the
26
+ # suite behaves in the same process. It gets its own task and its own process.
27
+ ###
28
+ desc "run the Solid Cache backed adapter tests (SQLite, needs solid_cache)"
29
+ task :"test:solid_cache" do
30
+ # BUNDLE_WITH opts the optional group onto the load path for this run only.
31
+ # It has to already be installed: `bundle install --with solid_cache`.
32
+ sh({"COVERBAND_SOLID_CACHE" => "true", "BUNDLE_WITH" => "solid_cache"},
33
+ "bundle exec ruby -Ilib -Itest test/coverband/adapters/solid_cache_store_test.rb") do |ok, _res|
34
+ unless ok
35
+ abort "solid_cache tests failed. If the gems are missing, run: bundle install --with solid_cache"
36
+ end
37
+ end
38
+ end
39
+
24
40
  desc "run the web report's javascript unit tests (node --test)"
25
41
  task :"test:javascript" do
26
42
  # Pass expanded file paths rather than the bare directory: node's
@@ -51,8 +67,3 @@ desc "start webserver"
51
67
  task :server do
52
68
  exec "rackup -I lib"
53
69
  end
54
-
55
- desc "publish gem with 2 factor auth, reminder how"
56
- task :publish_gem do
57
- exec "gem push pkg/coverband-4.2.3.XXX.gem"
58
- end