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
data/changes.md
CHANGED
|
@@ -1,3 +1,63 @@
|
|
|
1
|
+
### 7.0.0
|
|
2
|
+
|
|
3
|
+
**Breaking changes**
|
|
4
|
+
|
|
5
|
+
* **Upgrading from 6.x? Follow [Upgrading to 7.0](https://github.com/danmayer/coverband#upgrading-to-70) in the README**, which lists what to change for each item below. Removed APIs are removed outright rather than deprecated first.
|
|
6
|
+
* **The `redis` gem is no longer a dependency of Coverband. If you use Redis (the default), add `gem "redis"` to your Gemfile when you upgrade.** Redis is still the default store and still needs no other configuration: whenever the `redis` gem is installed Coverband loads it and stores coverage there exactly as before. Apps that only got `redis` through Coverband will otherwise boot with a warning telling them to add it, and record nothing until they do. Apps that store coverage elsewhere (`ActiveSupportCacheStore` with Solid Cache, memcached, or files) no longer need Redis installed at all
|
|
7
|
+
* **Coverage and tracker history do not survive this upgrade.** Stored documents now carry the metadata the new merge protocol needs, and keys are scoped by a generation token, so the storage format version changed for both `RedisStore` and the cache adapters. Old keys are ignored rather than migrated, and coverage accumulates fresh from the upgrade. Delete the old `coverband_3_2.*` keys once you are happy with the new data.
|
|
8
|
+
* **Reporting to coverband.io (`WebServiceStore`) has been removed, as the hosted service is no longer maintained.** This also removes `ViewTrackerService`, loading `config/coverband_service.rb`, the `api_key`, `service_url`, `coverband_timeout`, `service_dev_mode`, `service_test_mode`, and `process_type` settings, and the `COVERBAND_API_KEY`, `COVERBAND_URL`, `COVERBAND_ENABLE_DEV_MODE`, `COVERBAND_ENABLE_TEST_MODE`, and `PROCESS_TYPE` environment variables. Setting `COVERBAND_API_KEY` no longer switches Coverband into service mode. Move settings into `config/coverband.rb` and configure Redis or an `ActiveSupportCacheStore`
|
|
9
|
+
* Removed configuration settings that no longer had production consumers: `reporter` (no effect since 5.0), `s3_region`, `s3_bucket`, `s3_access_key_id`, `s3_secret_access_key`, `track_gems`, `gem_details`, and `simulate_oneshot_lines_coverage`, plus the coverband.io settings above. Assigning any of them logs a warning naming the setting and how to fix it, and is otherwise ignored, so an old `config/coverband.rb` still boots.
|
|
10
|
+
* `Coverband::Utils::SourceFile` and `Coverband::Utils::Result` now accept only enriched file records from stored Coverband reports. Passing the legacy bare line-coverage Array is no longer supported.
|
|
11
|
+
* **`Coverband::Adapters::MemcachedStore` has been removed.** Use `ActiveSupportCacheStore.new(cache, cache_namespace: "...")` with the same memcached cache; it also supports the trackers. `ActiveSupportCacheStore` no longer accepts `memcached_namespace`
|
|
12
|
+
* **`background_reporting_sleep_seconds` now defaults to 600** for the new cache adapter (`HashRedisStore` still defaults to 300, everything else to 60), because each report rewrites a whole document.
|
|
13
|
+
* `Adapters::Base#size` now returns bytes as an Integer or `nil`; `size_in_mib` owns the "N/A" presentation. A store returning the string `"N/A"` previously reported `0.00` MiB.
|
|
14
|
+
* `Adapters::Base#get_coverage_report` now reports the receiver's coverage instead of routing through `Coverband.configuration.store`, which quietly returned another store's data when the receiver was not the configured one.
|
|
15
|
+
* Removed `AbstractTracker#redis_store`; custom trackers use `#storage`. Trackers no longer reach through the store to raw Redis commands.
|
|
16
|
+
* Removed the misspelled `Coverband::Utils::Results#runtime_relavent_lines`; use `#runtime_relevant_lines` instead.
|
|
17
|
+
* Removed `Adapters::Base#raw_store` and the Redis adapters' raw-client escape hatch. Redis maintenance now uses the narrow `store.cleanup` capability (`clear_legacy!` and `clear_orphans!`); adapters that cannot enumerate keys return `nil`.
|
|
18
|
+
* Work is retained when generation resolution fails, so a backend blip during a reporting cycle no longer discards that cycle's coverage. Previously only deltas that had reached the pending queue were protected, and the generation is resolved before the payload is enqueued. The retention caps apply to that held work too, so a sustained outage cannot grow the queue without limit
|
|
19
|
+
* A cache store that is not resolvable yet (`ActiveSupportCacheStore.new { Rails.cache }` running before Rails assigns `Rails.cache`) is no longer a `NoMethodError` on a nil target. It holds the cycle's work, logs, and resolves on a later cycle. A resolver returning something that is not a cache store now names the problem instead of failing deep in the storage layer
|
|
20
|
+
* Rake tasks that reach the store (`coverband:clear`, `clear_coverage`, `clear_tracker`, `clear_legacy`, `clear_orphans`, `coverage`, `coverage_json`, `coverage_html`) load the Rails environment first. With a lazily configured store they previously failed with an unavailable cache unless `rake environment` was named explicitly
|
|
21
|
+
* Coverage data loss (eviction, dropped pending deltas) is now surfaced on the web report index. It was logged and shown on the tracker tabs, but the coverage index reported partial numbers as though they were complete
|
|
22
|
+
* `Adapters::SessionCoverage#save_report` returns the protocol state from `Session#record` rather than `nil`, so a caller can tell a write that landed from one that was deferred or refused
|
|
23
|
+
* Coverage now absorbs the two-cycle outage the design commits to. The cap bounds the queue, and the cycle that recovers takes a slot in it, so a cap of 2 only ever absorbed one; it is now 3
|
|
24
|
+
* Data loss from dropping a delta that was already written and merely awaiting confirmation is reported as `unconfirmed_dropped` rather than `pending_dropped`, and the web report says the numbers may be undercounted instead of claiming earlier results are unavailable. Only the second kind is lost coverage
|
|
25
|
+
* Trackers keep their own keys whenever a report is not stored (`:failed`, `:unavailable`, or an error), so an unreachable backend is no longer lossier for them than one that raises. `TrackerStorage::Base#retains_pending?` is removed; the decision comes from what `record` returns
|
|
26
|
+
* A write refused or errored *after* the delta was enqueued now reports `:retained` rather than `:failed`, since the work is in the storage queue and a caller holding a second copy would replay it. This is the documented memcached >1MB path, where it double counted `QueryBurstTracker`
|
|
27
|
+
* `QueryBurstTracker` reports on a quiet cycle when storage is holding work it could not write, instead of waiting for the next burst to flush it
|
|
28
|
+
* A document that cannot be written is now reported while it is happening, rather than an hour later. Its retention caps never fire on a quiet tracker -- nothing new arrives for them to drop -- so only the absolute age cap eventually turned a permanently refused write (a tracker document past memcached's 1MB limit, for instance) into a loss event, and until then the tab read "0 used" exactly like an app that used none. The web report now says how many reports could not be stored and since when
|
|
29
|
+
* Presence trackers (views, routes, translations) re-supply their known keys when storage reports work dropped at its retention caps, so a backend outage longer than the queue no longer leaves a used view reading as unused. Their merge is idempotent, so this cannot double count; `QueryBurstTracker` sums, so its loss stays irreducible and reported
|
|
30
|
+
* `save_report` no longer mutates the report it is given. The merge used to sum stored counts back into the caller's line arrays, so a caller that reused its report object re-submitted counts that were already recorded.
|
|
31
|
+
|
|
32
|
+
**Features**
|
|
33
|
+
|
|
34
|
+
* Replaced the generation lifecycle mixin and its implicit instance-variable/hook contract with a state-owning coordinator and typed generation-change events (#656)
|
|
35
|
+
* Added structured storage health through `Coverband.storage_health`, normal JSON reports, and the MCP `get_storage_health` tool, so monitoring can distinguish healthy, stalled, data-loss, and unsupported diagnostic states (#658)
|
|
36
|
+
* Added `Coverband::Adapters::ActiveSupportCacheStore`, which stores coverage in any `ActiveSupport::Cache::Store` — Redis, Memcached, files, or Solid Cache for Postgres/MySQL/SQLite. The cache target can be passed lazily (`ActiveSupportCacheStore.new { Rails.cache }`) so it works from `config/coverband.rb`, where `Rails.cache` does not exist yet (#533)
|
|
37
|
+
* Trackers (views, routes, translations, query bursts) now work on non-Redis stores. They previously reached through the store to raw Redis commands, so file and memcached users got trackers that silently collected nothing and empty web UI tabs
|
|
38
|
+
* Fixed the read-modify-write conflict Coverband has always had on `RedisStore`: concurrent reports could silently drop one process's contribution. Reports are now applied under a per-writer sequence, so a conflicting write is detected and repaired on the next cycle without double counting. `QueryBurstTracker` had the same conflict on its cumulative counters and is fixed the same way
|
|
39
|
+
* Resets are now strong: `clear!` and `reset_recordings` retire a whole generation key, so a straggling write from before the reset cannot undo it
|
|
40
|
+
* Coverband now detects when a backend has dropped its data (cache eviction, `Rails.cache.clear`) and reports it in the web UI rather than silently showing partial numbers
|
|
41
|
+
* Trackers are skipped with an explanatory log line when the configured store cannot support them, instead of collecting nothing
|
|
42
|
+
* Query bursts no longer count Coverband's own storage queries against the surrounding request, which would otherwise inflate query counts on a database backed cache
|
|
43
|
+
* Added `persistent_coverage?`, `supports_trackers?`, and `supports_paged_reports?` capability predicates on adapters, plus `file_count` and `cached_file_count` defaults so non-Redis stores no longer raise from the web UI's datatables path
|
|
44
|
+
* `redis_ttl` applies to stored coverage documents again; generation pointers deliberately never expire, since a pointer that outlived its document would leave live coverage unreachable
|
|
45
|
+
* Storage reads degrade to empty and log when the backend is unavailable (an unreachable cache, or a Solid Cache table that has not been created yet) rather than raising into the request rendering the report; write failures still reach the reporting paths that log them
|
|
46
|
+
* Added `rake test:solid_cache`, an ENV gated SQLite backed Solid Cache suite covering atomic pointer creation, batched pointer reads, ActiveRecord being unavailable, a missing Solid Cache schema, and Coverband's own SQL not being attributed to the request that triggered the report
|
|
47
|
+
* Added `rake coverband:clear_orphans` to reclaim generation keys no longer referenced by a pointer, and scoped `coverband:clear_legacy` to Coverband's own namespaces and tracker names so it cannot delete unrelated application keys
|
|
48
|
+
* CI now runs a memcached service, so the memcached backed adapter is actually exercised
|
|
49
|
+
* Coverage adapters share one implementation of the merge protocol, and the generation lifecycle is shared between the session and the Redis hash repository, rather than each reimplementing the same bookkeeping
|
|
50
|
+
* The web report has a dark theme. It follows the operating system through `prefers-color-scheme`, so there is nothing to configure and no toggle; every color in `public/application.css` now resolves through a custom property on `:root`, which makes the report restylable from one place. Coverage row colors are picked for contrast on the dark surface rather than darkened mechanically, so covered / missed / never / skipped stay as easy to tell apart; thanks @naofumi-fujii (#665, #666, #667)
|
|
51
|
+
* Added `Coverband::Collectors::TrackerRegistry` so applications and gems can register their own trackers, along with `Coverband::Configuration.add_tracker_flag` for their enablement flags and `Coverband.configuration.tracker_for` / `Coverband.track_key` support for trackers without a dedicated accessor; bundled trackers now self-register and are initialized generically at Rails boot (#651)
|
|
52
|
+
|
|
53
|
+
**Bug fixes**
|
|
54
|
+
|
|
55
|
+
* Coverband's development dependencies no longer globally constrain JSON to 2.x. Legacy Rails test bundles retain their required JSON 2 constraint, while the latest-Rails suite explicitly validates Coverband against JSON 3. This constraint was development-only and never affected applications installing Coverband.
|
|
56
|
+
* The web report's `/report_json` route and `JSONReport.new(store, as_report: true)` no longer raise `NoMethodError` outside Rails, where ActiveSupport's `as_json` is not loaded; thanks @dajiaohuang (#669)
|
|
57
|
+
* The web report's password check accepts passwords containing `:` (it split the credentials on every colon) and compares in constant time, as does the MCP endpoint's bearer token check. This also drops the runtime dependency on the `base64` gem
|
|
58
|
+
* `Coverband::Utils::Results#respond_to?(name, true)` no longer raises `ArgumentError`
|
|
59
|
+
* The runtime % no longer reads 100% for files with runtime relevant lines that were never hit, and no longer divides by every line of a file that was never eager loaded. It only counts runtime hits on lines that eager loading did not cover, and takes line relevance from real coverage data rather than the never loaded placeholder, which marked blank and comment lines as relevant. Runtime relevant lines on the file page use the same calculation, and the runtime % there is colored by its own value rather than by % covered (#668)
|
|
60
|
+
|
|
1
61
|
### 6.2.2
|
|
2
62
|
|
|
3
63
|
* Feature: Added a dead-code review session to the web report — hide files/folders by hovering any path segment, mark files for deletion with an optional comment, and export marked entries to CSV to drive an out-of-band code-removal process; session state lives in browser localStorage; thanks @obie (#647, #649)
|
data/coverband.gemspec
CHANGED
|
@@ -48,21 +48,23 @@ Gem::Specification.new do |spec|
|
|
|
48
48
|
spec.add_development_dependency "rake"
|
|
49
49
|
spec.add_development_dependency "resque"
|
|
50
50
|
spec.add_development_dependency "standard", ">= 1.35.1"
|
|
51
|
-
# breaking changes in various rubocop versions
|
|
52
|
-
spec.add_development_dependency "rubocop"
|
|
53
|
-
|
|
54
|
-
spec.add_development_dependency "coveralls"
|
|
55
51
|
# minitest-profile is not compatible with Rails 7.1.0 setup... dropping it for now
|
|
56
52
|
# spec.add_development_dependency "minitest-profile"
|
|
57
53
|
spec.add_development_dependency "webmock"
|
|
58
|
-
spec.add_development_dependency "dalli" #
|
|
59
|
-
|
|
54
|
+
spec.add_development_dependency "dalli" # memcached client for the cache adapter tests
|
|
55
|
+
# the cache adapter duck types the ActiveSupport::Cache::Store interface and
|
|
56
|
+
# never requires it, but the tests need real cache stores to run against
|
|
57
|
+
spec.add_development_dependency "activesupport"
|
|
58
|
+
spec.add_development_dependency "mcp", ">= 0.23" # For MCP server support testing
|
|
59
|
+
|
|
60
|
+
# Redis is optional since 7.0: it stays the default store whenever the redis gem
|
|
61
|
+
# is installed, and the ActiveSupport cache adapter covers everything else.
|
|
62
|
+
spec.add_development_dependency "redis", ">= 3.0"
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
spec.add_runtime_dependency "base64"
|
|
64
|
+
spec.post_install_message = <<~MSG
|
|
65
|
+
Coverband 7.0 no longer depends on the redis gem. Redis is still the default
|
|
66
|
+
store, so if you use it add `gem "redis"` to your Gemfile. To store coverage
|
|
67
|
+
elsewhere (Solid Cache, memcached, files) see config.store in the README.
|
|
68
|
+
Coverage stored by Coverband 6.x is not carried over; see changes.md.
|
|
69
|
+
MSG
|
|
68
70
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../storage/target"
|
|
5
|
+
require_relative "../storage/session"
|
|
6
|
+
require_relative "tracker_storage/factory"
|
|
7
|
+
require_relative "session_coverage"
|
|
8
|
+
|
|
9
|
+
module Coverband
|
|
10
|
+
module Adapters
|
|
11
|
+
###
|
|
12
|
+
# Stores coverage in any ActiveSupport::Cache::Store: Redis, Memcached,
|
|
13
|
+
# files, and through Solid Cache, Postgres, MySQL, and SQLite -- one adapter
|
|
14
|
+
# instead of one per backend. See docs/active_support_cache_adapter_plan.md.
|
|
15
|
+
#
|
|
16
|
+
# The cache target may be given lazily, because Rails.cache does not exist
|
|
17
|
+
# while config/coverband.rb is loading:
|
|
18
|
+
#
|
|
19
|
+
# config.store = Coverband::Adapters::ActiveSupportCacheStore.new { Rails.cache }
|
|
20
|
+
###
|
|
21
|
+
class ActiveSupportCacheStore < Base
|
|
22
|
+
include SessionCoverage
|
|
23
|
+
|
|
24
|
+
# bumped from coverband_3_2: documents carry their metadata and keys are
|
|
25
|
+
# generation scoped, so old keys are ignored rather than migrated
|
|
26
|
+
STORAGE_FORMAT_VERSION = "coverband_cache_4_0"
|
|
27
|
+
|
|
28
|
+
attr_reader :cache_namespace
|
|
29
|
+
|
|
30
|
+
def initialize(cache = nil, opts = {}, &block)
|
|
31
|
+
super()
|
|
32
|
+
@target = Storage::Target.new(cache, &block)
|
|
33
|
+
@cache_namespace = opts[:cache_namespace] || opts[:redis_namespace] || Coverband.configuration.redis_namespace
|
|
34
|
+
@format_version = self.class::STORAGE_FORMAT_VERSION
|
|
35
|
+
@ttl = opts[:ttl]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def tracker_storage
|
|
39
|
+
@tracker_storage ||= TrackerStorage::Factory.cache(target: @target, namespace: @cache_namespace,
|
|
40
|
+
format_version: @format_version)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def storage_target
|
|
46
|
+
@target
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def coverage_key_base(local_type)
|
|
50
|
+
[@format_version, @cache_namespace, "coverage", local_type].compact.join(".")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -35,30 +35,63 @@ module Coverband
|
|
|
35
35
|
raise ABSTRACT_KEY
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# adapters return bytes or nil; presentation lives here, so a store
|
|
39
|
+
# returning the string "N/A" can't be truthy and reported as 0.00 MiB
|
|
38
40
|
def size_in_mib
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
bytes = size
|
|
42
|
+
return "N/A" unless bytes.is_a?(Numeric)
|
|
43
|
+
|
|
44
|
+
format("%<size>.2f", size: (bytes.to_f / 2**20))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# tracker storage, or nil when this store can't support it; trackers are
|
|
48
|
+
# then skipped rather than left silently collecting nothing
|
|
49
|
+
def tracker_storage
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def supports_trackers?
|
|
54
|
+
!tracker_storage.nil?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# paginated / single file report loading, HashRedisStore only
|
|
58
|
+
def supports_paged_reports?
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# eligible for the core coverage contract: round trips and can be counted
|
|
63
|
+
def persistent_coverage?
|
|
64
|
+
false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def file_count
|
|
68
|
+
coverage(Coverband::RUNTIME_TYPE).keys.length
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def cached_file_count
|
|
72
|
+
@cached_file_count ||= file_count
|
|
44
73
|
end
|
|
45
74
|
|
|
46
75
|
def save_report(_report)
|
|
47
76
|
raise "abstract"
|
|
48
77
|
end
|
|
49
78
|
|
|
79
|
+
# this adapter's coverage: it used to route through configuration.store,
|
|
80
|
+
# quietly reporting someone else's data when the receiver was not it
|
|
50
81
|
def get_coverage_report(options = {})
|
|
51
82
|
coverage_cache = {}
|
|
52
|
-
data =
|
|
53
|
-
data.merge(Coverband::MERGED_TYPE =>
|
|
83
|
+
data = split_coverage(Coverband::TYPES, coverage_cache, options)
|
|
84
|
+
data.merge(Coverband::MERGED_TYPE => merged_coverage(Coverband::TYPES, coverage_cache))
|
|
54
85
|
end
|
|
55
86
|
|
|
56
87
|
def covered_files
|
|
57
88
|
coverage.keys || []
|
|
58
89
|
end
|
|
59
90
|
|
|
60
|
-
|
|
61
|
-
|
|
91
|
+
# Optional maintenance capability. Redis-backed adapters return an object
|
|
92
|
+
# that can enumerate and safely clean Coverband-owned keys.
|
|
93
|
+
def cleanup
|
|
94
|
+
nil
|
|
62
95
|
end
|
|
63
96
|
|
|
64
97
|
protected
|
|
@@ -79,7 +112,6 @@ module Coverband
|
|
|
79
112
|
Coverband::Utils::FileHasher.hash_file(file)
|
|
80
113
|
end
|
|
81
114
|
|
|
82
|
-
# TODO: modify to extend report inline?
|
|
83
115
|
def expand_report(report)
|
|
84
116
|
expanded = {}
|
|
85
117
|
report_time = Time.now.to_i
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
|
+
require_relative "tracker_storage/factory"
|
|
4
5
|
|
|
5
6
|
module Coverband
|
|
6
7
|
module Adapters
|
|
@@ -34,7 +35,7 @@ module Coverband
|
|
|
34
35
|
|
|
35
36
|
def supported?
|
|
36
37
|
Gem::Version.new(@redis.info["redis_version"]) >= Gem::Version.new("2.6.0")
|
|
37
|
-
rescue
|
|
38
|
+
rescue *Coverband.redis_connection_errors => e
|
|
38
39
|
Coverband.configuration.logger.info "Redis is not available (#{e}), Coverband not configured"
|
|
39
40
|
Coverband.configuration.logger.info "If this is a setup task like assets:precompile feel free to ignore"
|
|
40
41
|
end
|
|
@@ -184,8 +185,26 @@ module Coverband
|
|
|
184
185
|
@cached_file_count ||= file_count(Coverband::RUNTIME_TYPE)
|
|
185
186
|
end
|
|
186
187
|
|
|
187
|
-
def
|
|
188
|
-
@redis
|
|
188
|
+
def cleanup
|
|
189
|
+
RedisCleanup.new(@redis, namespace: @redis_namespace)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def persistent_coverage?
|
|
193
|
+
true
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
###
|
|
197
|
+
# Coverage storage here is unchanged: LUA scripts and per file keys avoid
|
|
198
|
+
# the merge conflict entirely. Tracker storage is shared across adapters,
|
|
199
|
+
# so this store picks it up like any other.
|
|
200
|
+
###
|
|
201
|
+
def tracker_storage
|
|
202
|
+
@tracker_storage ||= TrackerStorage::Factory.redis(redis: @redis, namespace: @redis_namespace,
|
|
203
|
+
format_version: REDIS_STORAGE_FORMAT_VERSION)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def supports_paged_reports?
|
|
207
|
+
true
|
|
189
208
|
end
|
|
190
209
|
|
|
191
210
|
def size
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coverband
|
|
4
|
+
module Adapters
|
|
5
|
+
###
|
|
6
|
+
# The narrow maintenance interface shared by Redis-backed adapters. Key
|
|
7
|
+
# discovery and deletion stay here so callers never need the raw client or
|
|
8
|
+
# knowledge of Coverband's Redis formats.
|
|
9
|
+
###
|
|
10
|
+
class RedisCleanup
|
|
11
|
+
GRACE_SECONDS = 3600
|
|
12
|
+
TRACKERS = %w[ViewTracker RouteTracker TranslationTracker QueryBurstTracker].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(redis, namespace: nil)
|
|
15
|
+
@redis = redis
|
|
16
|
+
@namespace = namespace
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def clear_legacy!
|
|
20
|
+
patterns = legacy_formats.map { |format| "#{format}*" }
|
|
21
|
+
[@namespace, nil].uniq.each do |namespace|
|
|
22
|
+
TRACKERS.each do |tracker|
|
|
23
|
+
prefix = namespace ? "#{namespace}_#{tracker}" : tracker
|
|
24
|
+
patterns << "#{prefix}_tracker"
|
|
25
|
+
patterns << "#{prefix}_tracker_time"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
delete_matching(patterns)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def clear_orphans!
|
|
33
|
+
orphan_formats.sum { |format| remove_orphans(format) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
###
|
|
39
|
+
# A generation is garbage only while its pointer names something else.
|
|
40
|
+
# Rechecking immediately before deletion protects a generation made live
|
|
41
|
+
# by a concurrent reset. Young or uncertain generations are retained.
|
|
42
|
+
###
|
|
43
|
+
def remove_orphans(format)
|
|
44
|
+
removed = 0
|
|
45
|
+
|
|
46
|
+
@redis.scan_each(match: "#{format}*.g*").to_a.uniq.each do |key|
|
|
47
|
+
base = key[/\A(.*)\.g[^.]*\z/, 1]
|
|
48
|
+
token = key[/\.g([^.]*)\z/, 1]
|
|
49
|
+
next unless base && token
|
|
50
|
+
|
|
51
|
+
pointer = read_pointer("#{base}.pointer")
|
|
52
|
+
next if pointer && pointer["token"] == token
|
|
53
|
+
next if recently_written?(key)
|
|
54
|
+
|
|
55
|
+
removed += @redis.del(key)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
removed
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def read_pointer(key)
|
|
62
|
+
raw = @redis.get(key)
|
|
63
|
+
raw ? JSON.parse(raw) : nil
|
|
64
|
+
rescue JSON::ParserError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def recently_written?(key)
|
|
69
|
+
idle = @redis.object("idletime", key)
|
|
70
|
+
idle ? idle < GRACE_SECONDS : true
|
|
71
|
+
rescue
|
|
72
|
+
# If Redis cannot establish the age, retaining the key is safest.
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def legacy_formats
|
|
77
|
+
%w[coverband_3_2 coverband_hash_3_2 coverband_hash_4_0] - current_formats
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def current_formats
|
|
81
|
+
[
|
|
82
|
+
RedisStore::REDIS_STORAGE_FORMAT_VERSION,
|
|
83
|
+
HashRedisStore::REDIS_STORAGE_FORMAT_VERSION,
|
|
84
|
+
ActiveSupportCacheStore::STORAGE_FORMAT_VERSION
|
|
85
|
+
].uniq
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def orphan_formats
|
|
89
|
+
[
|
|
90
|
+
RedisStore::REDIS_STORAGE_FORMAT_VERSION,
|
|
91
|
+
HashRedisStore::REDIS_STORAGE_FORMAT_VERSION
|
|
92
|
+
].uniq
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def delete_matching(patterns)
|
|
96
|
+
keys = patterns.flat_map { |pattern| @redis.scan_each(match: pattern).to_a }.uniq
|
|
97
|
+
keys.any? ? @redis.del(*keys) : 0
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../storage/redis_target"
|
|
4
|
+
require_relative "../storage/session"
|
|
5
|
+
require_relative "tracker_storage/factory"
|
|
6
|
+
require_relative "session_coverage"
|
|
7
|
+
|
|
3
8
|
module Coverband
|
|
4
9
|
module Adapters
|
|
5
10
|
###
|
|
6
11
|
# RedisStore store a merged coverage file to redis
|
|
7
12
|
###
|
|
8
13
|
class RedisStore < Base
|
|
14
|
+
include SessionCoverage
|
|
15
|
+
|
|
9
16
|
###
|
|
10
17
|
# This key isn't related to the coverband version, but to the internal format
|
|
11
18
|
# used to store data to redis. It is changed only when breaking changes to our
|
|
12
19
|
# redis format are required.
|
|
20
|
+
#
|
|
21
|
+
# Bumped from coverband_3_2 for 7.0: coverage documents now carry the
|
|
22
|
+
# metadata the merge protocol needs, and keys are generation scoped. There
|
|
23
|
+
# is no migration; old keys are ignored.
|
|
13
24
|
###
|
|
14
|
-
REDIS_STORAGE_FORMAT_VERSION = "
|
|
25
|
+
REDIS_STORAGE_FORMAT_VERSION = "coverband_5_0"
|
|
15
26
|
|
|
16
27
|
attr_reader :redis_namespace
|
|
17
28
|
|
|
@@ -21,85 +32,41 @@ module Coverband
|
|
|
21
32
|
@ttl = opts[:ttl]
|
|
22
33
|
@redis_namespace = opts[:redis_namespace]
|
|
23
34
|
@format_version = REDIS_STORAGE_FORMAT_VERSION
|
|
24
|
-
@keys = {}
|
|
25
|
-
Coverband::TYPES.each do |type|
|
|
26
|
-
@keys[type] = [@format_version, @redis_namespace, type].compact.join(".")
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def clear!
|
|
31
|
-
Coverband::TYPES.each do |type|
|
|
32
|
-
@redis.del(type_base_key(type))
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def clear_file!(filename)
|
|
37
|
-
Coverband::TYPES.each do |type|
|
|
38
|
-
data = coverage(type)
|
|
39
|
-
data.delete(filename)
|
|
40
|
-
save_coverage(data, type)
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def size
|
|
45
|
-
@redis.get(base_key) ? @redis.get(base_key).bytesize : "N/A"
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def type=(type)
|
|
49
|
-
super
|
|
50
|
-
reset_base_key
|
|
51
35
|
end
|
|
52
36
|
|
|
53
|
-
def
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
data.delete_if { |file_path, file_data| file_hash(file_path) != file_data["file_hash"] } unless opts[:skip_hash_check]
|
|
58
|
-
data
|
|
37
|
+
def tracker_storage
|
|
38
|
+
sync_target
|
|
39
|
+
@tracker_storage ||= TrackerStorage::Factory.redis(redis: @redis, namespace: @redis_namespace,
|
|
40
|
+
format_version: @format_version)
|
|
59
41
|
end
|
|
60
42
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# the Coverband 2 had the same issue,
|
|
64
|
-
# and the tradeoff has always been acceptable
|
|
65
|
-
def save_report(report)
|
|
66
|
-
data = merge_reports(report, coverage(nil, skip_hash_check: true))
|
|
67
|
-
save_coverage(data)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def raw_store
|
|
71
|
-
@redis
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def file_count
|
|
75
|
-
data = redis.get type_base_key(Coverband::RUNTIME_TYPE)
|
|
76
|
-
JSON.parse(data).keys.length
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def cached_file_count
|
|
80
|
-
@cached_file_count ||= file_count
|
|
43
|
+
def cleanup
|
|
44
|
+
RedisCleanup.new(@redis, namespace: @redis_namespace)
|
|
81
45
|
end
|
|
82
46
|
|
|
83
47
|
private
|
|
84
48
|
|
|
85
49
|
attr_reader :redis
|
|
86
50
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
51
|
+
###
|
|
52
|
+
# The client can be swapped after construction, and a different client is
|
|
53
|
+
# a different database: cached generation tokens and unconfirmed deltas
|
|
54
|
+
# from the old one would be meaningless against the new one.
|
|
55
|
+
###
|
|
56
|
+
def sync_target
|
|
57
|
+
return @target if @target && @target.redis.equal?(@redis)
|
|
90
58
|
|
|
91
|
-
|
|
92
|
-
@
|
|
59
|
+
sessions.clear
|
|
60
|
+
@tracker_storage = nil
|
|
61
|
+
@target = Storage::RedisTarget.new(@redis, ttl: @ttl)
|
|
93
62
|
end
|
|
94
63
|
|
|
95
|
-
def
|
|
96
|
-
|
|
64
|
+
def storage_target
|
|
65
|
+
sync_target
|
|
97
66
|
end
|
|
98
67
|
|
|
99
|
-
def
|
|
100
|
-
|
|
101
|
-
redis.set type_base_key(local_type), data.to_json
|
|
102
|
-
redis.expire(type_base_key(local_type), @ttl) if @ttl
|
|
68
|
+
def coverage_key_base(local_type)
|
|
69
|
+
[@format_version, @redis_namespace, "coverage", local_type].compact.join(".")
|
|
103
70
|
end
|
|
104
71
|
end
|
|
105
72
|
end
|