coverband 6.0.3.rc.4 → 6.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf0b6bab823d8155a5e15d361c3acbbef271f84e79947b9818a650e640a61fc7
4
- data.tar.gz: 25cd0843b3c1e852794bcc008be165bfcbf7e476f373ab681cbff7861f050904
3
+ metadata.gz: ed2b063179cbc773c9b3b8fab3403820f9855c32a9de007c347b58873eecf13a
4
+ data.tar.gz: 7968b26ac22e3209b0bf2a9baabdfe107ed9029a0d4557eea5b80c22439e3278
5
5
  SHA512:
6
- metadata.gz: d2ae2023667369ac1e7be83ec41898c6e77662545f6226c02417ef9a43e75a4e2950ad0d775e18c96d800270d81da104162d4414b13768ac98c14bcff431acd1
7
- data.tar.gz: 64ddff4e59042a6ee22cd4839e6e7ac57dff4cf289b02a8f6801c144aafea7edd417ed918c38309b61173176cb177ce548dbf6302963d4b53464d8901cb05d30
6
+ metadata.gz: 3bccf2dbc8e92d5980365a99b5ae2852fd8e04d224202a58e3a8490991b88eb88a881fbf620e4ea60d66cd4714a70f11a8015ebd96aa772a65a35e1fb642ea24
7
+ data.tar.gz: 784f5e0905989f865199261270072c397e520b2269ed387f0ecd1276ef5a7395fc5d4de7ec9990aceeda1cc883778bcfcf4c7c2260c792e81557914298778d87
@@ -28,11 +28,5 @@ If applicable, add screenshots to help explain your problem.
28
28
  - Browser [e.g. chrome, safari]
29
29
  - Version [e.g. 22]
30
30
 
31
- **Smartphone (please complete the following information):**
32
- - Device: [e.g. iPhone6]
33
- - OS: [e.g. iOS8.1]
34
- - Browser [e.g. stock browser, safari]
35
- - Version [e.g. 22]
36
-
37
31
  **Additional context**
38
32
  Add any other context about the problem here.
@@ -26,7 +26,7 @@ jobs:
26
26
  runs-on: ${{ matrix.os }}-latest
27
27
  steps:
28
28
  - uses: actions/checkout@v4
29
- - uses: supercharge/redis-github-action@1.2.0
29
+ - uses: supercharge/redis-github-action@1.8.0
30
30
  with:
31
31
  redis-version: ${{ matrix.redis-version }}
32
32
  - uses: ruby/setup-ruby@v1
data/README.md CHANGED
@@ -101,6 +101,22 @@ run ActionController::Dispatcher.new
101
101
 
102
102
  This triggers coverage collection on the current webserver process. Useful in development but confusing in production environments where many ruby processes are usually running.
103
103
 
104
+ #### Interpreting results
105
+
106
+ The columns in the web UI are as follows:
107
+
108
+ - **% covered** - Percentage of total relevant lines covered
109
+ - **% runtime** - Percentage of the runtime lines covered where runtime lines are lines that are hit after the application has been eagerly loaded
110
+ - **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.
111
+ - **Relevant lines** - Lines that are coverable, i.e. not empty
112
+ - **Lines runtime** - Total lines minus uncoverable lines minus the lines that are only hit during eager loading of application
113
+ - **Lines missed** - Relevant lines not covered
114
+ - **Avg hits/line** - Total of coverage to the file divided by relevant lines.
115
+
116
+ 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.
117
+
118
+ ![example of a file with lines not hit at runtime](https://user-images.githubusercontent.com/96786/63541229-aa98a580-c4eb-11e9-8eb8-c004fe1369db.png)
119
+
104
120
  ### Mounting as a Rack App
105
121
 
106
122
  Coverband comes with a mountable rack app for viewing reports. For Rails this can be done in `config/routes.rb` with:
@@ -191,7 +207,7 @@ Do you use figaro, mc-settings, dotenv or something else to inject environment v
191
207
  For example if you use dotenv, you need to do this, see https://github.com/bkeepers/dotenv#note-on-load-order
192
208
 
193
209
  ```
194
- gem 'dotenv-rails', require: 'dotenv/rails-now'
210
+ gem 'dotenv-rails', require: 'dotenv/load'
195
211
  gem 'coverband'
196
212
  gem 'other-gem-that-requires-env-variables'
197
213
  ```
@@ -222,6 +238,14 @@ This feature is enabled by default. To stop this feature, disable the feature in
222
238
 
223
239
  ![image](https://raw.github.com/danmayer/coverband/master/docs/coverband_view_tracker.png)
224
240
 
241
+ ### Hiding settings
242
+
243
+ Coverband provides a view of all of its current settings. Sometimes you might want to hide this view,
244
+ such as when sharing coverband data with a large number of developers of varying trust levels.
245
+ You can disable the settings view like so:
246
+
247
+ `config.hide_settings = false`
248
+
225
249
  ### Fixing Coverage Only Shows Loading Hits
226
250
 
227
251
  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`.
data/changes.md CHANGED
@@ -1,3 +1,25 @@
1
+ ### Coverband 6.1.1
2
+
3
+ * Performance fix making paged report loading 10X faster
4
+
5
+ ### Coverband 6.1.0
6
+
7
+ This release has a number of smaller fixes and improvements. It includes a sizable refactoring around the UI which should simplify improvements going forward. This release is mostly targetting large projects with 6K+ ruby files, use the new `config.paged_reporting = true` option with the HashRedisStore to enable paged reporting for large projects. The HashRedisStore now also includes the last time a line in a file was executed.
8
+
9
+ * Thanks to @FeLvi-zzz for the last time accessed support for the Hash Redis Store
10
+ * Thanks to @alpaca-tc for the improvements on the route tracker
11
+ * Thanks to @ydah for typo fixes, doc updates, adding ruby 3.3 to build matrix, improvements on CI, standardrb fixes
12
+ * Thanks to @trivett, @khaled-badenjki, @IsabelleLePivain for improved docs
13
+ * Thanks to @prastamaha for the memcached adapter
14
+ * Thanks to @ursm for a yaml fix
15
+ * Thanks to @Drowze for a layered cache approach for perf improvements
16
+ * Thanks to @vs37559 for a sinatra pandrino fix
17
+ * This release addresses large projects and adds in paged reporting
18
+ * to ensure even on projects with 10K+ files it can load on heroku under the 30s timeout
19
+ * only supports HashRedis store
20
+ * faster UI for web UI in general which should be noticable on non paged reports
21
+ * reduce redis calls
22
+
1
23
  ### Coverband 6.0.2
2
24
 
3
25
  * thanks makicamel for improved deferred eager loading
@@ -11,16 +33,12 @@
11
33
 
12
34
  ### Coverband 6.0.0
13
35
 
14
- __NOTE: I ended up having 5.2.6 in various RCs for a long time, mostly because I had some breaking changes that were related to dropping support for old versions of Ruby and Rails__
15
-
16
36
  * The 6.0.0 release is all that was the 6 different RC releases of 5.2.6
17
37
  * Added Rails test matrix to github actions to test on all the supported versions
18
38
  - Rails: 6.0.x, 6.1.x, 7.0.x, 7.1.x
19
39
 
20
40
  ### Coverband 5.2.6
21
41
 
22
- __NOTE: the current RCs include below, but this might turn into coverband 6.0__
23
-
24
42
  - add support for translation keys
25
43
  - refactor non Coverage.so based trackers
26
44
  - adds CSP report support (thanks @jwg2s)
data/coverband.gemspec CHANGED
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
36
36
  spec.add_development_dependency "memory_profiler"
37
37
  # breaking change in minitest and mocha...
38
38
  # note: we are also adding 'spy' as mocha doesn't want us to spy on redis calls...
39
+ spec.add_development_dependency "spy"
39
40
  # ^^^ probably need a large test cleanup refactor
40
41
  spec.add_development_dependency "minitest", "= 5.18.1"
41
42
  spec.add_development_dependency "minitest-fork_executor"
@@ -45,8 +46,9 @@ Gem::Specification.new do |spec|
45
46
  spec.add_development_dependency "rack-test"
46
47
  spec.add_development_dependency "rake"
47
48
  spec.add_development_dependency "resque"
48
- spec.add_development_dependency "standard", "~> 1.34.0"
49
- spec.add_development_dependency "standardrb"
49
+ spec.add_development_dependency "standard", "= 1.34.0"
50
+ # breaking changes in various rubocop versions
51
+ spec.add_development_dependency "rubocop", "= 1.60.0"
50
52
 
51
53
  spec.add_development_dependency "coveralls"
52
54
  # minitest-profile is not compatible with Rails 7.1.0 setup... dropping it for now
@@ -116,8 +116,8 @@ module Coverband
116
116
 
117
117
  def supported?
118
118
  Gem::Version.new(@redis.info["redis_version"]) >= Gem::Version.new("2.6.0")
119
- rescue Redis::CannotConnectError => error
120
- Coverband.configuration.logger.info "Redis is not available (#{error}), Coverband not configured"
119
+ rescue Redis::CannotConnectError => e
120
+ Coverband.configuration.logger.info "Redis is not available (#{e}), Coverband not configured"
121
121
  Coverband.configuration.logger.info "If this is a setup task like assets:precompile feel free to ignore"
122
122
  end
123
123
 
@@ -149,7 +149,7 @@ module Coverband
149
149
  updated_time = (type == Coverband::EAGER_TYPE) ? nil : report_time
150
150
  keys = []
151
151
  report.each_slice(@save_report_batch_size) do |slice|
152
- files_data = slice.map { |(file, data)|
152
+ files_data = slice.map do |(file, data)|
153
153
  relative_file = @relative_file_converter.convert(file)
154
154
  file_hash = file_hash(relative_file)
155
155
  key = key(relative_file, file_hash: file_hash)
@@ -162,12 +162,12 @@ module Coverband
162
162
  report_time: report_time,
163
163
  updated_time: updated_time
164
164
  )
165
- }
165
+ end
166
166
  next unless files_data.any?
167
167
 
168
168
  arguments_key = [@redis_namespace, SecureRandom.uuid].compact.join(".")
169
169
  @redis.set(arguments_key, {ttl: @ttl, files_data: files_data}.to_json, ex: JSON_PAYLOAD_EXPIRATION)
170
- @redis.evalsha(hash_incr_script, [arguments_key])
170
+ @redis.evalsha(hash_incr_script, [arguments_key], [report_time])
171
171
  end
172
172
  @redis.sadd(files_key, keys) if keys.any?
173
173
  end
@@ -182,7 +182,9 @@ module Coverband
182
182
  elsif opts[:filename]
183
183
  type_key_prefix = key_prefix(local_type)
184
184
  # NOTE: a better way to extract filename from key would be better
185
- files_set(local_type).select { |cache_key| cache_key.sub(type_key_prefix, "").match(short_name(opts[:filename])) } || {}
185
+ files_set(local_type).select do |cache_key|
186
+ cache_key.sub(type_key_prefix, "").match(short_name(opts[:filename]))
187
+ end || {}
186
188
  else
187
189
  files_set(local_type)
188
190
  end
@@ -213,13 +215,13 @@ module Coverband
213
215
  end
214
216
  end
215
217
 
216
- def coverage_for_types(types, opts = {})
218
+ def coverage_for_types(_types, opts = {})
217
219
  page_size = opts[:page_size] || 250
218
-
219
- local_type = Coverband::RUNTIME_TYPE
220
220
  hash_data = {}
221
221
 
222
- runtime_file_set = files_set(local_type).each_slice(page_size).to_a[opts[:page] - 1] || []
222
+ runtime_file_set = files_set(Coverband::RUNTIME_TYPE)
223
+ @cached_file_count = runtime_file_set.length
224
+ runtime_file_set = runtime_file_set.each_slice(page_size).to_a[opts[:page] - 1] || []
223
225
 
224
226
  hash_data[Coverband::RUNTIME_TYPE] = runtime_file_set.each_slice(page_size).flat_map do |key_batch|
225
227
  @redis.pipelined do |pipeline|
@@ -229,14 +231,14 @@ module Coverband
229
231
  end
230
232
  end
231
233
 
234
+ # NOTE: This is kind of hacky, we find all the matching eager loading data
235
+ # for current page of runtime data.
232
236
  eager_key_pre = key_prefix(Coverband::EAGER_TYPE)
233
237
  runtime_key_pre = key_prefix(Coverband::RUNTIME_TYPE)
234
- matched_file_set = files_set(Coverband::EAGER_TYPE)
235
- .select { |eager_key, val|
236
- runtime_file_set.any? { |runtime_key|
237
- (eager_key.sub(eager_key_pre, "") == runtime_key.sub(runtime_key_pre, ""))
238
- }
239
- } || []
238
+ matched_file_set = runtime_file_set.map do |runtime_key|
239
+ runtime_key.sub(runtime_key_pre, eager_key_pre)
240
+ end
241
+
240
242
  hash_data[Coverband::EAGER_TYPE] = matched_file_set.each_slice(page_size).flat_map do |key_batch|
241
243
  @redis.pipelined do |pipeline|
242
244
  key_batch.each do |key|
@@ -244,6 +246,7 @@ module Coverband
244
246
  end
245
247
  end
246
248
  end
249
+
247
250
  hash_data[Coverband::RUNTIME_TYPE] = hash_data[Coverband::RUNTIME_TYPE].each_with_object({}) do |data_from_redis, hash|
248
251
  add_coverage_for_file(data_from_redis, hash)
249
252
  end
@@ -255,13 +258,17 @@ module Coverband
255
258
 
256
259
  def short_name(filename)
257
260
  filename.sub(/^#{Coverband.configuration.root}/, ".")
258
- .gsub(%r{^\.\/}, "")
261
+ .gsub(%r{^\./}, "")
259
262
  end
260
263
 
261
264
  def file_count(local_type = nil)
262
265
  files_set(local_type).count { |filename| !Coverband.configuration.ignore.any? { |i| filename.match(i) } }
263
266
  end
264
267
 
268
+ def cached_file_count
269
+ @cached_file_count ||= file_count(Coverband::RUNTIME_TYPE)
270
+ end
271
+
265
272
  def raw_store
266
273
  @redis
267
274
  end
@@ -283,9 +290,14 @@ module Coverband
283
290
  return unless file_hash(file) == data_from_redis[FILE_HASH]
284
291
 
285
292
  data = coverage_data_from_redis(data_from_redis)
286
- hash[file] = data_from_redis.select { |meta_data_key, _value| META_DATA_KEYS.include?(meta_data_key) }.merge!("data" => data)
287
- hash[file][LAST_UPDATED_KEY] = (hash[file][LAST_UPDATED_KEY].nil? || hash[file][LAST_UPDATED_KEY] == "") ? nil : hash[file][LAST_UPDATED_KEY].to_i
288
- hash[file].merge!(LAST_UPDATED_KEY => hash[file][LAST_UPDATED_KEY], FIRST_UPDATED_KEY => hash[file][FIRST_UPDATED_KEY].to_i)
293
+ timedata = coverage_time_data_from_redis(data_from_redis)
294
+ hash[file] = data_from_redis.select do |meta_data_key, _value|
295
+ META_DATA_KEYS.include?(meta_data_key)
296
+ end.merge!("data" => data, "timedata" => timedata)
297
+ hash[file][LAST_UPDATED_KEY] =
298
+ (hash[file][LAST_UPDATED_KEY].nil? || hash[file][LAST_UPDATED_KEY] == "") ? nil : hash[file][LAST_UPDATED_KEY].to_i
299
+ hash[file].merge!(LAST_UPDATED_KEY => hash[file][LAST_UPDATED_KEY],
300
+ FIRST_UPDATED_KEY => hash[file][FIRST_UPDATED_KEY].to_i)
289
301
  end
290
302
 
291
303
  def coverage_data_from_redis(data_from_redis)
@@ -296,10 +308,18 @@ module Coverband
296
308
  end
297
309
  end
298
310
 
311
+ def coverage_time_data_from_redis(data_from_redis)
312
+ max = data_from_redis[FILE_LENGTH_KEY].to_i - 1
313
+ Array.new(max + 1) do |index|
314
+ unixtime = data_from_redis["#{index}_last_posted"]
315
+ unixtime.nil? ? nil : Time.at(unixtime.to_i)
316
+ end
317
+ end
318
+
299
319
  def script_input(key:, file:, file_hash:, data:, report_time:, updated_time:)
300
- coverage_data = data.each_with_index.each_with_object({}) { |(coverage, index), hash|
320
+ coverage_data = data.each_with_index.each_with_object({}) do |(coverage, index), hash|
301
321
  hash[index] = coverage if coverage
302
- }
322
+ end
303
323
  meta = {
304
324
  first_updated_at: report_time,
305
325
  file: file,
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coverband
4
+ module Adapters
5
+ class MemcachedStore < Base
6
+ STORAGE_FORMAT_VERSION = "coverband_3_2"
7
+
8
+ attr_reader :memcached_namespace
9
+
10
+ def initialize(memcached, opts = {})
11
+ super()
12
+ @memcached = memcached
13
+ @memcached_namespace = opts[:memcached_namespace]
14
+ @format_version = STORAGE_FORMAT_VERSION
15
+ @keys = {}
16
+ Coverband::TYPES.each do |type|
17
+ @keys[type] = [@format_version, @memcached_namespace, type].compact.join(".")
18
+ end
19
+ end
20
+
21
+ def clear!
22
+ Coverband::TYPES.each do |type|
23
+ @memcached.delete(type_base_key(type))
24
+ end
25
+ end
26
+
27
+ def clear_file!(filename)
28
+ Coverband::TYPES.each do |type|
29
+ data = coverage(type)
30
+ data.delete(filename)
31
+ save_coverage(data, type)
32
+ end
33
+ end
34
+
35
+ def size
36
+ @memcached.read(base_key) ? @memcached.read(base_key).bytesize : "N/A"
37
+ end
38
+
39
+ def migrate!
40
+ raise NotImplementedError, "MemcachedStore doesn't support migrations"
41
+ end
42
+
43
+ def type=(type)
44
+ super
45
+ reset_base_key
46
+ end
47
+
48
+ def coverage(local_type = nil, opts = {})
49
+ local_type ||= opts.key?(:override_type) ? opts[:override_type] : type
50
+ data = memcached.read(type_base_key(local_type))
51
+ data = data ? JSON.parse(data) : {}
52
+ data.delete_if { |file_path, file_data| file_hash(file_path) != file_data["file_hash"] } unless opts[:skip_hash_check]
53
+ data
54
+ end
55
+
56
+ def save_report(report)
57
+ data = report.dup
58
+ data = merge_reports(data, coverage(nil, skip_hash_check: true))
59
+ save_coverage(data)
60
+ end
61
+
62
+ def raw_store
63
+ raise NotImplementedError, "MemcachedStore doesn't support raw_store"
64
+ end
65
+
66
+ attr_reader :memcached
67
+
68
+ private
69
+
70
+ def reset_base_key
71
+ @base_key = nil
72
+ end
73
+
74
+ def base_key
75
+ @base_key ||= [@format_version, @memcached_namespace, type].compact.join(".")
76
+ end
77
+
78
+ def type_base_key(local_type)
79
+ @keys[local_type]
80
+ end
81
+
82
+ def save_coverage(data, local_type = nil)
83
+ local_type ||= type
84
+ key = type_base_key(local_type)
85
+ memcached.write(key, data.to_json)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -25,7 +25,10 @@ module Coverband
25
25
  # and ensure high performance
26
26
  ###
27
27
  def track_key(payload)
28
- route = if payload[:request]
28
+ route = if payload.key?(:location)
29
+ # For redirect.action_dispatch
30
+ return unless Coverband.configuration.track_redirect_routes
31
+
29
32
  {
30
33
  controller: nil,
31
34
  action: nil,
@@ -33,6 +36,7 @@ module Coverband
33
36
  verb: payload[:request].method
34
37
  }
35
38
  else
39
+ # For start_processing.action_controller
36
40
  {
37
41
  controller: payload[:params]["controller"],
38
42
  action: payload[:action],
@@ -40,11 +44,10 @@ module Coverband
40
44
  verb: payload[:method]
41
45
  }
42
46
  end
43
- if route
44
- if newly_seen_key?(route)
45
- @logged_keys << route
46
- @keys_to_record << route if track_key?(route)
47
- end
47
+
48
+ if newly_seen_key?(route)
49
+ @logged_keys << route
50
+ @keys_to_record << route if track_key?(route)
48
51
  end
49
52
  end
50
53
 
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coverband
4
+ ###
5
+ # Configuration parsing and options for the coverband gem.
6
+ ###
4
7
  class Configuration
5
8
  attr_accessor :root_paths, :root,
6
9
  :verbose,
@@ -9,9 +12,9 @@ module Coverband
9
12
  :test_env, :web_enable_clear, :gem_details, :web_debug, :report_on_exit,
10
13
  :simulate_oneshot_lines_coverage,
11
14
  :view_tracker, :defer_eager_loading_data,
12
- :track_routes, :route_tracker,
15
+ :track_routes, :track_redirect_routes, :route_tracker,
13
16
  :track_translations, :translations_tracker,
14
- :trackers, :csp_policy
17
+ :trackers, :csp_policy, :hide_settings
15
18
 
16
19
  attr_writer :logger, :s3_region, :s3_bucket, :s3_access_key_id,
17
20
  :s3_secret_access_key, :password, :api_key, :service_url, :coverband_timeout, :service_dev_mode,
@@ -74,6 +77,7 @@ module Coverband
74
77
  @track_views = true
75
78
  @view_tracker = nil
76
79
  @track_routes = false
80
+ @track_redirect_routes = true
77
81
  @route_tracker = nil
78
82
  @track_translations = false
79
83
  @translations_tracker = nil
@@ -86,6 +90,7 @@ module Coverband
86
90
  @all_root_patterns = nil
87
91
  @password = nil
88
92
  @csp_policy = false
93
+ @hide_settings = false
89
94
 
90
95
  # coverband service settings
91
96
  @api_key = nil
@@ -131,8 +136,8 @@ module Coverband
131
136
  trackers << Coverband.configuration.view_tracker
132
137
  end
133
138
  trackers.each { |tracker| tracker.railtie! }
134
- rescue Redis::CannotConnectError => error
135
- Coverband.configuration.logger.info "Redis is not available (#{error}), Coverband not configured"
139
+ rescue Redis::CannotConnectError => e
140
+ Coverband.configuration.logger.info "Redis is not available (#{e}), Coverband not configured"
136
141
  Coverband.configuration.logger.info "If this is a setup task like assets:precompile feel free to ignore"
137
142
  end
138
143
 
@@ -169,7 +174,10 @@ module Coverband
169
174
 
170
175
  def store
171
176
  @store ||= if service?
172
- raise "invalid configuration: unclear default store coverband expects either api_key or redis_url" if ENV["COVERBAND_REDIS_URL"]
177
+ if ENV["COVERBAND_REDIS_URL"]
178
+ raise "invalid configuration: unclear default store coverband expects either api_key or redis_url"
179
+ end
180
+
173
181
  require "coverband/adapters/web_service_store"
174
182
  Coverband::Adapters::WebServiceStore.new(service_url)
175
183
  else
@@ -179,8 +187,14 @@ module Coverband
179
187
 
180
188
  def store=(store)
181
189
  raise "Pass in an instance of Coverband::Adapters" unless store.is_a?(Coverband::Adapters::Base)
182
- raise "invalid configuration: only coverband service expects an API Key" if api_key && store.class.to_s != "Coverband::Adapters::WebServiceStore"
183
- raise "invalid configuration: coverband service shouldn't have redis url set" if ENV["COVERBAND_REDIS_URL"] && store.instance_of?(::Coverband::Adapters::WebServiceStore)
190
+ if api_key && store.class.to_s != "Coverband::Adapters::WebServiceStore"
191
+ raise "invalid configuration: only coverband service expects an API Key"
192
+ end
193
+ if ENV["COVERBAND_REDIS_URL"] &&
194
+ defined?(::Coverband::Adapters::WebServiceStore) &&
195
+ store.instance_of?(::Coverband::Adapters::WebServiceStore)
196
+ raise "invalid configuration: coverband service shouldn't have redis url set"
197
+ end
184
198
 
185
199
  @store = store
186
200
  end
@@ -240,7 +254,10 @@ module Coverband
240
254
  end
241
255
 
242
256
  def use_oneshot_lines_coverage=(value)
243
- raise(StandardError, "One shot line coverage is only available in ruby >= 2.6") unless one_shot_coverage_implemented_in_ruby_version? || !value
257
+ unless one_shot_coverage_implemented_in_ruby_version? || !value
258
+ raise(StandardError,
259
+ "One shot line coverage is only available in ruby >= 2.6")
260
+ end
244
261
 
245
262
  @use_oneshot_lines_coverage = value
246
263
  end
@@ -321,7 +338,7 @@ module Coverband
321
338
  end
322
339
 
323
340
  def track_gems=(_value)
324
- puts "gem tracking is deprecated, setting this will be ignored"
341
+ puts "gem tracking is deprecated, setting this will be ignored & eventually removed"
325
342
  end
326
343
 
327
344
  private
@@ -72,7 +72,7 @@ module Coverband
72
72
  data[:covered_strength].to_s
73
73
  ]
74
74
  end
75
- filesreported = store.file_count(:runtime)
75
+ filesreported = store.cached_file_count
76
76
  data["iTotalRecords"] = filesreported
77
77
  data["iTotalDisplayRecords"] = filesreported
78
78
  data["aaData"] = row_data
@@ -141,6 +141,7 @@ module Coverband
141
141
  end
142
142
 
143
143
  def settings
144
+ return "" if Coverband.configuration.hide_settings
144
145
  Coverband::Utils::HTMLFormatter.new(nil, base_path: base_path).format_settings!
145
146
  end
146
147
 
@@ -155,11 +155,11 @@ module Coverband
155
155
  Digest::SHA1.hexdigest(source_file.filename)
156
156
  end
157
157
 
158
- def timeago(time)
159
- if time
158
+ def timeago(time, err_msg = "Not Available")
159
+ if time.respond_to?(:iso8601)
160
160
  "<abbr class=\"timeago\" title=\"#{time.iso8601}\">#{time.iso8601}</abbr>"
161
161
  else
162
- "Not Available"
162
+ err_msg
163
163
  end
164
164
  end
165
165
 
@@ -25,13 +25,15 @@ module Coverband
25
25
  attr_reader :coverage
26
26
  # Whether this line was skipped
27
27
  attr_reader :skipped
28
+ # The coverage data posted time for this line: either nil (never), nil (missed) or Time instance (last posted)
29
+ attr_reader :coverage_posted
28
30
 
29
31
  # Lets grab some fancy aliases, shall we?
30
32
  alias source src
31
33
  alias line line_number
32
34
  alias number line_number
33
35
 
34
- def initialize(src, line_number, coverage)
36
+ def initialize(src, line_number, coverage, coverage_posted = nil)
35
37
  raise ArgumentError, "Only String accepted for source" unless src.is_a?(String)
36
38
  raise ArgumentError, "Only Integer accepted for line_number" unless line_number.is_a?(Integer)
37
39
  raise ArgumentError, "Only Integer and nil accepted for coverage" unless coverage.is_a?(Integer) || coverage.nil?
@@ -40,6 +42,7 @@ module Coverband
40
42
  @line_number = line_number
41
43
  @coverage = coverage
42
44
  @skipped = false
45
+ @coverage_posted = coverage_posted
43
46
  end
44
47
 
45
48
  # Returns true if this is a line that should have been covered, but was not
@@ -82,6 +85,8 @@ module Coverband
82
85
  attr_reader :filename
83
86
  # The array of coverage data received from the Coverage.result
84
87
  attr_reader :coverage
88
+ # The array of coverage timedata received from the Coverage.result
89
+ attr_reader :coverage_posted
85
90
 
86
91
  # the date this version of the file first started to record coverage
87
92
  attr_reader :first_updated_at
@@ -96,6 +101,7 @@ module Coverband
96
101
  @runtime_relavant_lines = nil
97
102
  if file_data.is_a?(Hash)
98
103
  @coverage = file_data["data"]
104
+ @coverage_posted = file_data["timedata"] || [] # NOTE: only implement timedata for HashRedisStore
99
105
  @first_updated_at = @last_updated_at = NOT_AVAILABLE
100
106
  @first_updated_at = Time.at(file_data["first_updated_at"]) if file_data["first_updated_at"]
101
107
  @last_updated_at = Time.at(file_data["last_updated_at"]) if file_data["last_updated_at"]
@@ -139,7 +145,12 @@ module Coverband
139
145
  coverage_exceeding_source_warn if coverage.size > src.size
140
146
 
141
147
  lines = src.map.with_index(1) { |src, i|
142
- Coverband::Utils::SourceFile::Line.new(src, i, never_loaded ? 0 : coverage[i - 1])
148
+ Coverband::Utils::SourceFile::Line.new(
149
+ src,
150
+ i,
151
+ never_loaded ? 0 : coverage[i - 1],
152
+ (never_loaded || !coverage_posted.is_a?(Array)) ? nil : coverage_posted[i - 1]
153
+ )
143
154
  }
144
155
 
145
156
  process_skipped_lines(lines)
@@ -200,6 +211,10 @@ module Coverband
200
211
  lines[index]&.coverage
201
212
  end
202
213
 
214
+ def line_coverage_posted(index)
215
+ lines[index]&.coverage_posted
216
+ end
217
+
203
218
  # Returns all lines that should have been, but were not covered
204
219
  # as instances of SimpleCov::SourceFile::Line
205
220
  def missed_lines
@@ -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.0.3.rc.4"
8
+ VERSION = "6.1.1"
9
9
  end
@@ -21,6 +21,9 @@ for _, file_data in ipairs(files_data) do
21
21
  redis.call('HSETNX', hash_key, 'first_updated_at', first_updated_at)
22
22
  for line, coverage in pairs(file_data.coverage) do
23
23
  redis.call("HINCRBY", hash_key, line, coverage)
24
+ if coverage > 0 then
25
+ redis.call("HSET", hash_key, line .. "_last_posted", ARGV[1])
26
+ end
24
27
  end
25
28
  if ttl and ttl ~= cjson.null then
26
29
  redis.call("EXPIRE", hash_key, ttl)
@@ -39,15 +39,16 @@ $(document).ready(function() {
39
39
  // best docs on our version of datatables 1.7 https://datatables.net/beta/1.7/examples/server_side/server_side.html
40
40
  if ($(".file_list.unsorted").length == 1) {
41
41
  $(".dataTables_empty").html("loading...");
42
- var current_rows = 0;
43
42
  var total_rows = 0;
44
43
  var page = 1;
44
+ var all_data = [];
45
45
 
46
46
  // load and render page content before we start the loop
47
47
  // perhaps move this into a datatable ready event
48
+ $(".dataTables_empty").html("loading...");
48
49
  setTimeout(() => {
49
50
  get_page(page);
50
- }, 1250);
51
+ }, 1200);
51
52
 
52
53
  function get_page(page) {
53
54
  $.ajax({
@@ -56,23 +57,23 @@ $(document).ready(function() {
56
57
  dataType: 'json',
57
58
  success: function(data) {
58
59
  total_rows = data["iTotalRecords"];
59
- // NOTE: we request 250 at a time, but we seem to have some files that we have as a list but 0 coverage,
60
- // so we don't get back 250 per page... to ensure we we need to account for filtered out and empty files
61
- // this 250 at the moment is synced to the 250 in the hash redis store
62
- current_rows += 250; //data["aaData"].length;
63
- $(".file_list.unsorted").dataTable().fnAddData(data["aaData"]);
60
+ all_data = all_data.concat(data["aaData"]);
61
+ $(".dataTables_empty").html("loading... on " + all_data.length + " of " + total_rows + " files");
64
62
  page += 1;
65
- // allow rendering to complete before we click the anchor
66
- setTimeout(() => {
67
- if (window.auto_click_anchor && $(window.auto_click_anchor).length > 0) {
68
- $(window.auto_click_anchor).click();
69
- }
70
- }, 50);
63
+ ;
71
64
  // the page less than 100 is to stop infinite loop in case of folks never clearing out old coverage reports
72
- if (page < 100 && current_rows < total_rows) {
65
+ if (page < 50 && all_data.length < total_rows) {
73
66
  setTimeout(() => {
74
67
  get_page(page);
75
- }, 200);
68
+ }, 10);
69
+ } else {
70
+ $(".file_list.unsorted").dataTable().fnAddData(all_data);
71
+ // allow rendering to complete before we click the anchor
72
+ setTimeout(() => {
73
+ if (window.auto_click_anchor && $(window.auto_click_anchor).length > 0) {
74
+ $(window.auto_click_anchor).click();
75
+ }
76
+ }, 50)
76
77
  }
77
78
  }
78
79
  });
@@ -88,7 +88,7 @@ var hljs=new function(){function l(o){return o.replace(/&/gm,"&amp;").replace(/<
88
88
  xhrError: "This content failed to load.",
89
89
  imgError: "This image failed to load.",
90
90
 
91
- // accessbility
91
+ // accessibility
92
92
  returnFocus: true,
93
93
  trapFocus: true,
94
94
 
@@ -103,7 +103,7 @@ var hljs=new function(){function l(o){return o.replace(/&/gm,"&amp;").replace(/<
103
103
  return this.rel;
104
104
  },
105
105
  href: function() {
106
- // using this.href would give the absolute url, when the href may have been intedned as a selector (e.g. '#container')
106
+ // using this.href would give the absolute url, when the href may have been intended as a selector (e.g. '#container')
107
107
  return $(this).attr('href');
108
108
  },
109
109
  title: function() {
@@ -145,7 +145,7 @@ var hljs=new function(){function l(o){return o.replace(/&/gm,"&amp;").replace(/<
145
145
  $prev,
146
146
  $close,
147
147
  $groupControls,
148
- $events = $('<a/>'), // $({}) would be prefered, but there is an issue with jQuery 1.4.2
148
+ $events = $('<a/>'), // $({}) would be preferred, but there is an issue with jQuery 1.4.2
149
149
 
150
150
  // Variables for cached values or use across multiple functions
151
151
  settings,
@@ -57,7 +57,8 @@ class HashRedisStoreTest < Minitest::Test
57
57
  "first_updated_at" => yesterday.to_i,
58
58
  "last_updated_at" => yesterday.to_i,
59
59
  "file_hash" => "abcd",
60
- "data" => [0, 1, 2]
60
+ "data" => [0, 1, 2],
61
+ "timedata" => [nil, Time.at(yesterday.to_i), Time.at(yesterday.to_i)]
61
62
  },
62
63
  @store.coverage["./dog.rb"]
63
64
  )
@@ -65,15 +66,12 @@ class HashRedisStoreTest < Minitest::Test
65
66
  @store.save_report(
66
67
  "app_path/dog.rb" => [1, 1, 0]
67
68
  )
68
- assert_equal(
69
- {
70
- "first_updated_at" => yesterday.to_i,
71
- "last_updated_at" => today.to_i,
72
- "file_hash" => "abcd",
73
- "data" => [1, 2, 2]
74
- },
75
- @store.coverage["./dog.rb"]
76
- )
69
+
70
+ assert_equal("abcd", @store.coverage["./dog.rb"]["file_hash"])
71
+ assert_equal(today.to_i, @store.coverage["./dog.rb"]["last_updated_at"])
72
+ assert_equal(yesterday.to_i, @store.coverage["./dog.rb"]["first_updated_at"])
73
+ assert_equal([1, 2, 2], @store.coverage["./dog.rb"]["data"])
74
+ assert_equal([Time.at(today.to_i), Time.at(today.to_i), Time.at(yesterday.to_i)], @store.coverage["./dog.rb"]["timedata"])
77
75
  end
78
76
 
79
77
  def test_ttl_set
@@ -109,7 +107,8 @@ class HashRedisStoreTest < Minitest::Test
109
107
  "first_updated_at" => current_time.to_i,
110
108
  "last_updated_at" => current_time.to_i,
111
109
  "file_hash" => "abcd",
112
- "data" => [0, nil, 1, 2]
110
+ "data" => [0, nil, 1, 2],
111
+ "timedata" => [nil, nil, Time.at(current_time.to_i), Time.at(current_time.to_i)]
113
112
  }, @store.coverage["./dog.rb"]
114
113
  )
115
114
  assert_equal [1, 2, 0, 1, 5], @store.coverage["./cat.rb"]["data"]
@@ -213,7 +212,8 @@ class HashRedisStoreTest < Minitest::Test
213
212
  "first_updated_at" => yesterday.to_i,
214
213
  "last_updated_at" => yesterday.to_i,
215
214
  "file_hash" => "abcd",
216
- "data" => [0, 1, 2]
215
+ "data" => [0, 1, 2],
216
+ "timedata" => [nil, Time.at(yesterday.to_i), Time.at(yesterday.to_i)]
217
217
  },
218
218
  @store.coverage["./dog.rb"]
219
219
  )
@@ -225,7 +225,8 @@ class HashRedisStoreTest < Minitest::Test
225
225
  "first_updated_at" => yesterday.to_i,
226
226
  "last_updated_at" => yesterday.to_i,
227
227
  "file_hash" => "abcd",
228
- "data" => [0, 1, 2]
228
+ "data" => [0, 1, 2],
229
+ "timedata" => [nil, Time.at(yesterday.to_i), Time.at(yesterday.to_i)]
229
230
  },
230
231
  @store.coverage["./dog.rb"]
231
232
  )
@@ -235,9 +236,58 @@ class HashRedisStoreTest < Minitest::Test
235
236
  "first_updated_at" => yesterday.to_i,
236
237
  "last_updated_at" => yesterday.to_i,
237
238
  "file_hash" => "abcd",
238
- "data" => [0, 2, 4]
239
+ "data" => [0, 2, 4],
240
+ "timedata" => [nil, Time.at(yesterday.to_i), Time.at(yesterday.to_i)]
239
241
  },
240
242
  @store.coverage["./dog.rb"]
241
243
  )
242
244
  end
245
+
246
+ def test_split_coverage
247
+ @store = Coverband::Adapters::HashRedisStore.new(
248
+ @redis,
249
+ redis_namespace: "coverband_test",
250
+ relative_file_converter: MockRelativeFileConverter
251
+ )
252
+
253
+ mock_file_hash
254
+ yesterday = DateTime.now.prev_day.to_time
255
+ mock_time(yesterday)
256
+
257
+ @store.type = :eager_loading
258
+ data = {
259
+ "app_path/dog.rb" => [0, nil, 1]
260
+ }
261
+ @store.save_report(data)
262
+
263
+ @store.type = :runtime
264
+ @store.save_report(
265
+ "app_path/dog.rb" => [0, 1, 2]
266
+ )
267
+ redis_pipelined = Spy.on(@redis, :pipelined).and_call_through
268
+ assert_equal(
269
+ {
270
+ runtime: {
271
+ "./dog.rb" => {
272
+ "first_updated_at" => yesterday.to_i,
273
+ "last_updated_at" => yesterday.to_i,
274
+ "file_hash" => "abcd",
275
+ "data" => [0, 1, 2],
276
+ "timedata" => [nil, Time.at(yesterday.to_i), Time.at(yesterday.to_i)]
277
+ }
278
+ },
279
+ eager_loading: {
280
+ "./dog.rb" => {
281
+ "first_updated_at" => yesterday.to_i,
282
+ "last_updated_at" => nil,
283
+ "file_hash" => "abcd",
284
+ "data" => [0, nil, 1],
285
+ "timedata" => [nil, nil, Time.at(yesterday.to_i)]
286
+ }
287
+ }
288
+ },
289
+ @store.split_coverage([Coverband::RUNTIME_TYPE, Coverband::EAGER_TYPE], {}, {page: 1})
290
+ )
291
+ assert_equal 2, redis_pipelined.calls.count
292
+ end
243
293
  end
@@ -33,14 +33,32 @@ class RouterTrackerTest < Minitest::Test
33
33
  tracker = Coverband::Collectors::RouteTracker.new(store: store, roots: "dir")
34
34
 
35
35
  payload = {
36
- request: Payload.new("path", "GET")
36
+ request: Payload.new("path", "GET"),
37
+ status: 302,
38
+ location: "https://coverband.dev/"
37
39
  }
38
40
  tracker.track_key(payload)
39
41
  tracker.save_report
40
42
  assert_equal [route_hash], tracker.logged_keys
41
43
  end
42
44
 
43
- test "track controller routes" do
45
+ test "track redirect routes when track_redirect_routes is false" do
46
+ Coverband.configuration.track_redirect_routes = false
47
+
48
+ store = fake_store
49
+ tracker = Coverband::Collectors::RouteTracker.new(store: store, roots: "dir")
50
+
51
+ payload = {
52
+ request: Payload.new("path", "GET"),
53
+ status: 302,
54
+ location: "https://coverband.dev/"
55
+ }
56
+ tracker.track_key(payload)
57
+ tracker.save_report
58
+ assert_equal [], tracker.logged_keys
59
+ end
60
+
61
+ test "track controller routes in Rails < 6.1" do
44
62
  store = fake_store
45
63
  route_hash = {controller: "some/controller", action: "index", url_path: nil, verb: "GET"}
46
64
  store.raw_store.expects(:hset).with(tracker_key, route_hash.to_s, anything)
@@ -57,6 +75,27 @@ class RouterTrackerTest < Minitest::Test
57
75
  assert_equal [route_hash], tracker.logged_keys
58
76
  end
59
77
 
78
+ test "track controller routes in Rails >= 6.1" do
79
+ store = fake_store
80
+ route_hash = {controller: "some/controller", action: "index", url_path: nil, verb: "GET"}
81
+ store.raw_store.expects(:hset).with(tracker_key, route_hash.to_s, anything)
82
+ tracker = Coverband::Collectors::RouteTracker.new(store: store, roots: "dir")
83
+ payload = {
84
+ params: {
85
+ "controller" => "some/controller",
86
+ "action" => "index"
87
+ },
88
+ controller: "SomeController",
89
+ action: "index",
90
+ path: "path",
91
+ method: "GET",
92
+ request: Payload.new("path", "GET")
93
+ }
94
+ tracker.track_key(payload)
95
+ tracker.save_report
96
+ assert_equal [route_hash], tracker.logged_keys
97
+ end
98
+
60
99
  test "report used routes" do
61
100
  store = fake_store
62
101
  route_hash = {controller: "some/controller", action: "index", url_path: nil, verb: "GET"}
data/test/test_helper.rb CHANGED
@@ -35,6 +35,7 @@ require "coverband/utils/results"
35
35
  require "coverband/reporters/html_report"
36
36
  require "coverband/reporters/json_report"
37
37
  require "webmock/minitest"
38
+ require "spy/integration"
38
39
 
39
40
  require_relative "unique_files"
40
41
  $VERBOSE = original_verbosity
@@ -45,6 +45,7 @@
45
45
  runtime:
46
46
  <%= result.file_with_type(source_file, Coverband::RUNTIME_TYPE)&.line_coverage(index) || 0 %>
47
47
  all: <%= line.coverage %>
48
+ last posted: <%= timeago(result.file_with_type(source_file, Coverband::RUNTIME_TYPE)&.line_coverage_posted(index), "-") %>
48
49
  </span><% end %>
49
50
  <% if line.skipped? %><span class="hits">skipped</span><% end %>
50
51
  <code class="ruby"><%= CGI.escapeHTML(line.src.chomp) %></code>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coverband
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.3.rc.4
4
+ version: 6.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Mayer
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-04-09 00:00:00.000000000 Z
12
+ date: 2024-04-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: benchmark-ips
@@ -67,6 +67,20 @@ dependencies:
67
67
  - - ">="
68
68
  - !ruby/object:Gem::Version
69
69
  version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: spy
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
70
84
  - !ruby/object:Gem::Dependency
71
85
  name: minitest
72
86
  requirement: !ruby/object:Gem::Requirement
@@ -183,30 +197,30 @@ dependencies:
183
197
  name: standard
184
198
  requirement: !ruby/object:Gem::Requirement
185
199
  requirements:
186
- - - "~>"
200
+ - - '='
187
201
  - !ruby/object:Gem::Version
188
202
  version: 1.34.0
189
203
  type: :development
190
204
  prerelease: false
191
205
  version_requirements: !ruby/object:Gem::Requirement
192
206
  requirements:
193
- - - "~>"
207
+ - - '='
194
208
  - !ruby/object:Gem::Version
195
209
  version: 1.34.0
196
210
  - !ruby/object:Gem::Dependency
197
- name: standardrb
211
+ name: rubocop
198
212
  requirement: !ruby/object:Gem::Requirement
199
213
  requirements:
200
- - - ">="
214
+ - - '='
201
215
  - !ruby/object:Gem::Version
202
- version: '0'
216
+ version: 1.60.0
203
217
  type: :development
204
218
  prerelease: false
205
219
  version_requirements: !ruby/object:Gem::Requirement
206
220
  requirements:
207
- - - ">="
221
+ - - '='
208
222
  - !ruby/object:Gem::Version
209
- version: '0'
223
+ version: 1.60.0
210
224
  - !ruby/object:Gem::Dependency
211
225
  name: coveralls
212
226
  requirement: !ruby/object:Gem::Requirement
@@ -284,6 +298,7 @@ files:
284
298
  - lib/coverband/adapters/base.rb
285
299
  - lib/coverband/adapters/file_store.rb
286
300
  - lib/coverband/adapters/hash_redis_store.rb
301
+ - lib/coverband/adapters/memcached_store.rb
287
302
  - lib/coverband/adapters/null_store.rb
288
303
  - lib/coverband/adapters/redis_store.rb
289
304
  - lib/coverband/adapters/stdout_store.rb
@@ -509,9 +524,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
509
524
  version: '2.7'
510
525
  required_rubygems_version: !ruby/object:Gem::Requirement
511
526
  requirements:
512
- - - ">"
527
+ - - ">="
513
528
  - !ruby/object:Gem::Version
514
- version: 1.3.1
529
+ version: '0'
515
530
  requirements: []
516
531
  rubygems_version: 3.4.10
517
532
  signing_key: