coverband 4.2.1.rc3 → 4.2.1.rc4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +4 -0
  3. data/Gemfile +1 -0
  4. data/README.md +6 -4
  5. data/Rakefile +5 -2
  6. data/changes.md +32 -13
  7. data/lib/coverband.rb +3 -4
  8. data/lib/coverband/adapters/base.rb +59 -30
  9. data/lib/coverband/adapters/file_store.rb +8 -8
  10. data/lib/coverband/adapters/redis_store.rb +18 -10
  11. data/lib/coverband/collectors/coverage.rb +3 -4
  12. data/lib/coverband/collectors/delta.rb +18 -8
  13. data/lib/coverband/configuration.rb +25 -9
  14. data/lib/coverband/integrations/background.rb +3 -1
  15. data/lib/coverband/reporters/base.rb +6 -3
  16. data/lib/coverband/reporters/web.rb +11 -0
  17. data/lib/coverband/utils/file_path_helper.rb +12 -3
  18. data/lib/coverband/utils/source_file.rb +2 -1
  19. data/lib/coverband/utils/tasks.rb +1 -11
  20. data/lib/coverband/version.rb +1 -1
  21. data/test/benchmarks/benchmark.rake +135 -10
  22. data/test/coverband/adapters/base_test.rb +73 -42
  23. data/test/coverband/adapters/file_store_test.rb +48 -37
  24. data/test/coverband/adapters/redis_store_test.rb +35 -5
  25. data/test/coverband/collectors/coverage_test.rb +1 -1
  26. data/test/coverband/collectors/delta_test.rb +10 -1
  27. data/test/coverband/coverband_test.rb +14 -1
  28. data/test/coverband/utils/html_formatter_test.rb +0 -2
  29. data/test/dog.rb.erb +12 -0
  30. data/test/forked/rails_full_stack_test.rb +35 -25
  31. data/test/forked/rails_rake_full_stack_test.rb +12 -4
  32. data/test/rails4_dummy/config/coverband.rb +2 -0
  33. data/test/rails5_dummy/config/coverband.rb +2 -0
  34. data/test/rails_test_helper.rb +2 -1
  35. data/test/test_helper.rb +19 -6
  36. data/test/unique_files.rb +12 -5
  37. metadata +5 -4
@@ -31,7 +31,7 @@ module Coverband
31
31
  end
32
32
 
33
33
  def runtime!
34
- @store.type = nil
34
+ @store.type = Coverband::RUNTIME_TYPE
35
35
  end
36
36
 
37
37
  def eager_loading!
@@ -82,9 +82,8 @@ module Coverband
82
82
  private
83
83
 
84
84
  def filtered_files(new_results)
85
- new_results.each_with_object({}) do |(file, line_counts), file_line_usage|
86
- file_line_usage[file] = line_counts if track_file?(file)
87
- end.select { |_file_name, coverage| coverage.any? { |value| value&.nonzero? } }
85
+ new_results.select! { |file, coverage| track_file?(file) && coverage.any? { |value| value&.nonzero? } }
86
+ new_results
88
87
  end
89
88
 
90
89
  def initialize
@@ -4,6 +4,8 @@ module Coverband
4
4
  module Collectors
5
5
  class Delta
6
6
  @@previous_coverage = {}
7
+ @@stubs = {}
8
+
7
9
  attr_reader :current_coverage
8
10
 
9
11
  def initialize(current_coverage)
@@ -12,20 +14,27 @@ module Coverband
12
14
 
13
15
  class RubyCoverage
14
16
  def self.results
15
- ::Coverage.peek_result.dup
17
+ if Coverband.configuration.use_oneshot_lines_coverage
18
+ ::Coverage.result(clear: true, stop: false)
19
+ else
20
+ ::Coverage.peek_result
21
+ end
16
22
  end
17
23
  end
18
24
 
19
25
  def self.results(process_coverage = RubyCoverage)
20
26
  coverage_results = process_coverage.results
21
- coverage_results = transform_oneshot_lines_results(coverage_results) if Coverband.configuration.use_oneshot_lines_coverage
22
27
  new(coverage_results).results
23
28
  end
24
29
 
25
30
  def results
26
- new_results = generate
27
- @@previous_coverage = current_coverage
28
- new_results
31
+ if Coverband.configuration.use_oneshot_lines_coverage
32
+ transform_oneshot_lines_results(current_coverage)
33
+ else
34
+ new_results = generate
35
+ @@previous_coverage = current_coverage unless Coverband.configuration.simulate_oneshot_lines_coverage
36
+ new_results
37
+ end
29
38
  end
30
39
 
31
40
  def self.reset
@@ -36,7 +45,7 @@ module Coverband
36
45
 
37
46
  def generate
38
47
  current_coverage.each_with_object({}) do |(file, line_counts), new_results|
39
- new_results[file] = if @@previous_coverage[file]
48
+ new_results[file] = if @@previous_coverage && @@previous_coverage[file]
40
49
  array_diff(line_counts, @@previous_coverage[file])
41
50
  else
42
51
  line_counts
@@ -50,9 +59,10 @@ module Coverband
50
59
  end
51
60
  end
52
61
 
53
- private_class_method def self.transform_oneshot_lines_results(results)
62
+ def transform_oneshot_lines_results(results)
54
63
  results.each_with_object({}) do |(file, coverage), new_results|
55
- transformed_line_counts = coverage[:oneshot_lines].each_with_object(::Coverage.line_stub(file)) do |line_number, line_counts|
64
+ @@stubs[file] ||= ::Coverage.line_stub(file)
65
+ transformed_line_counts = coverage[:oneshot_lines].each_with_object(@@stubs[file].dup) do |line_number, line_counts|
56
66
  line_counts[line_number - 1] = 1
57
67
  end
58
68
  new_results[file] = transformed_line_counts
@@ -7,9 +7,10 @@ module Coverband
7
7
  :reporter, :redis_namespace, :redis_ttl,
8
8
  :background_reporting_enabled,
9
9
  :background_reporting_sleep_seconds, :test_env,
10
- :web_enable_clear, :gem_details, :web_debug, :report_on_exit
10
+ :web_enable_clear, :gem_details, :web_debug, :report_on_exit,
11
+ :simulate_oneshot_lines_coverage
11
12
 
12
- attr_writer :logger, :s3_region, :s3_bucket, :s3_access_key_id, :s3_secret_access_key
13
+ attr_writer :logger, :s3_region, :s3_bucket, :s3_access_key_id, :s3_secret_access_key, :password
13
14
  attr_reader :track_gems, :ignore, :use_oneshot_lines_coverage
14
15
 
15
16
  #####
@@ -51,7 +52,12 @@ module Coverband
51
52
  @groups = {}
52
53
  @web_debug = false
53
54
  @report_on_exit = true
54
- @use_oneshot_lines_coverage = false
55
+ @use_oneshot_lines_coverage = ENV['ONESHOT'] || false
56
+ @simulate_oneshot_lines_coverage = ENV['SIMULATE_ONESHOT'] || false
57
+ @current_root = nil
58
+ @all_root_paths = nil
59
+ @all_root_patterns = nil
60
+ @password = nil
55
61
 
56
62
  # TODO: should we push these to adapter configs
57
63
  @s3_region = nil
@@ -59,7 +65,7 @@ module Coverband
59
65
  @s3_access_key_id = nil
60
66
  @s3_secret_access_key = nil
61
67
  @redis_namespace = nil
62
- @redis_ttl = nil
68
+ @redis_ttl = 2_592_000 # in seconds. Default is 30 days.
63
69
  end
64
70
 
65
71
  def logger
@@ -70,6 +76,10 @@ module Coverband
70
76
  end
71
77
  end
72
78
 
79
+ def password
80
+ @password || ENV['COVERBAND_PASSWORD']
81
+ end
82
+
73
83
  def s3_bucket
74
84
  @s3_bucket || ENV['AWS_BUCKET']
75
85
  end
@@ -143,14 +153,20 @@ module Coverband
143
153
  end
144
154
 
145
155
  def current_root
146
- File.expand_path(Coverband.configuration.root)
156
+ @current_root ||= File.expand_path(Coverband.configuration.root).freeze
147
157
  end
148
158
 
149
159
  def all_root_paths
150
- roots = Coverband.configuration.root_paths.dup
151
- roots += Coverband.configuration.gem_paths.dup if Coverband.configuration.track_gems
152
- roots << "#{Coverband.configuration.current_root}/"
153
- roots
160
+ return @all_root_paths if @all_root_paths
161
+
162
+ @all_root_paths = Coverband.configuration.root_paths.dup
163
+ @all_root_paths += Coverband.configuration.gem_paths.dup if Coverband.configuration.track_gems
164
+ @all_root_paths << "#{Coverband.configuration.current_root}/"
165
+ @all_root_paths
166
+ end
167
+
168
+ def all_root_patterns
169
+ @all_root_patterns ||= all_root_paths.map { |path| /^#{path}/ }.freeze
154
170
  end
155
171
 
156
172
  SKIPPED_SETTINGS = %w[@s3_secret_access_key @store]
@@ -32,7 +32,9 @@ module Coverband
32
32
  @thread = Thread.new do
33
33
  loop do
34
34
  Coverband.report_coverage
35
- logger.debug("Coverband: Reported coverage via thread. Sleeping #{sleep_seconds}s") if Coverband.configuration.verbose
35
+ if Coverband.configuration.verbose
36
+ logger.debug("Coverband: background reporting coverage (#{Coverband.configuration.store.type}). Sleeping #{sleep_seconds}s")
37
+ end
36
38
  sleep(sleep_seconds)
37
39
  end
38
40
  end
@@ -9,6 +9,9 @@ module Coverband
9
9
  class Base
10
10
  class << self
11
11
  include Coverband::Utils::FilePathHelper
12
+
13
+ DATA_KEY = 'data'
14
+
12
15
  def report(store, _options = {})
13
16
  all_roots = Coverband.configuration.all_root_paths
14
17
  scov_style_report = get_current_scov_data_imp(store, all_roots)
@@ -54,9 +57,9 @@ module Coverband
54
57
  fixed_report[name] = {}
55
58
  report.each_pair do |key, vals|
56
59
  filename = relative_path_to_full(key, roots)
57
- fixed_report[name][filename] = if fixed_report[name].key?(filename) && fixed_report[name][filename]['data'] && vals['data']
58
- merged_data = merge_arrays(fixed_report[name][filename]['data'], vals['data'])
59
- vals['data'] = merged_data
60
+ fixed_report[name][filename] = if fixed_report[name].key?(filename) && fixed_report[name][filename][DATA_KEY] && vals[DATA_KEY]
61
+ merged_data = merge_arrays(fixed_report[name][filename][DATA_KEY], vals[DATA_KEY])
62
+ vals[DATA_KEY] = merged_data
60
63
  vals
61
64
  else
62
65
  vals
@@ -18,9 +18,20 @@ module Coverband
18
18
  urls: [/.*\.css/, /.*\.js/, /.*\.gif/, /.*\.png/])
19
19
  end
20
20
 
21
+ def check_auth
22
+ return true unless Coverband.configuration.password
23
+
24
+ auth_header = request.get_header('HTTP_AUTHORIZATION')
25
+ return unless auth_header
26
+
27
+ Coverband.configuration.password == Base64.decode64(auth_header.split[1]).split(':')[1]
28
+ end
29
+
21
30
  def call(env)
22
31
  @request = Rack::Request.new(env)
23
32
 
33
+ return [401, { 'www-authenticate' => 'Basic realm=""' }, ['']] unless check_auth
34
+
24
35
  if request.post?
25
36
  case request.path_info
26
37
  when %r{\/clear_file}
@@ -8,16 +8,23 @@ module Coverband
8
8
  module FilePathHelper
9
9
  module_function
10
10
 
11
+ @@path_cache = {}
12
+
11
13
  ###
12
14
  # Takes a full path and converts to a relative path
13
15
  ###
14
16
  def full_path_to_relative(full_path)
17
+ return @@path_cache[full_path] if @@path_cache.key?(full_path)
18
+
15
19
  relative_filename = full_path
16
- Coverband.configuration.all_root_paths.each do |root|
17
- relative_filename = relative_filename.gsub(/^#{root}/, './')
20
+ Coverband.configuration.all_root_patterns.each do |root|
21
+ relative_filename = relative_filename.sub(root, './')
18
22
  # once we have a relative path break out of the loop
19
23
  break if relative_filename.start_with? './'
20
24
  end
25
+
26
+ @@path_cache[full_path] = relative_filename
27
+
21
28
  relative_filename
22
29
  end
23
30
 
@@ -42,7 +49,9 @@ module Coverband
42
49
  relative_filename = relative_path
43
50
  local_filename = relative_filename
44
51
  roots.each do |root|
45
- relative_filename = relative_filename.gsub(/^#{root}/, './')
52
+ relative_filename = relative_filename.sub(/^#{root}/, './')
53
+ # once we have a relative path break out of the loop
54
+ break if relative_filename.start_with? './'
46
55
  end
47
56
  # the filename for our reports is expected to be a full path.
48
57
  # roots.last should be roots << current_root}/
@@ -160,7 +160,8 @@ module Coverband
160
160
 
161
161
  return 0.0 if relevant_lines.zero?
162
162
 
163
- Float(covered_lines.size * 100.0 / relevant_lines.to_f)
163
+ # handle edge case where runtime in dev can go over 100%
164
+ [Float(covered_lines.size * 100.0 / relevant_lines.to_f), 100.0].min
164
165
  end
165
166
 
166
167
  def formatted_covered_percent
@@ -4,15 +4,8 @@ namespace :coverband do
4
4
  # handles configuring in require => false and COVERBAND_DISABLE_AUTO_START cases
5
5
  Coverband.configure unless Coverband.configured?
6
6
 
7
- def environment
8
- Coverband.configuration.report_on_exit = false
9
- Coverband.configuration.background_reporting_enabled = false
10
- Rake.application['environment'].invoke if Rake::Task.task_defined?('environment')
11
- end
12
-
13
7
  desc 'report runtime Coverband code coverage'
14
8
  task :coverage do
15
- environment
16
9
  if Coverband.configuration.reporter == 'scov'
17
10
  Coverband::Reporters::HTMLReport.new(Coverband.configuration.store).report
18
11
  else
@@ -22,7 +15,6 @@ namespace :coverband do
22
15
 
23
16
  desc 'report runtime Coverband code coverage'
24
17
  task :coverage_server do
25
- environment
26
18
  Rack::Server.start app: Coverband::Reporters::Web.new, Port: ENV.fetch('COVERBAND_COVERAGE_PORT', 1022).to_i
27
19
  end
28
20
 
@@ -31,16 +23,14 @@ namespace :coverband do
31
23
  ###
32
24
  desc 'reset Coverband coverage data, helpful for development, debugging, etc'
33
25
  task :clear do
34
- environment
35
26
  Coverband.configuration.store.clear!
36
27
  end
37
28
 
38
29
  ###
39
- # clear data helpful for development or after configuration issues
30
+ # Updates the data in the coverband store from one format to another
40
31
  ###
41
32
  desc 'upgrade previous Coverband datastore to latest format'
42
33
  task :migrate do
43
- environment
44
34
  Coverband.configuration.store.migrate!
45
35
  end
46
36
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coverband
4
- VERSION = '4.2.1.rc3'
4
+ VERSION = '4.2.1.rc4'
5
5
  end
@@ -32,10 +32,7 @@ namespace :benchmarks do
32
32
  end
33
33
 
34
34
  def clone_classifier
35
- unless Dir.exist? classifier_dir
36
- system "git clone https://github.com/jekyll/classifier-reborn.git #{classifier_dir}"
37
- end
38
- # rubocop:enable Style/IfUnlessModifier
35
+ system "git clone https://github.com/jekyll/classifier-reborn.git #{classifier_dir}" unless Dir.exist? classifier_dir
39
36
  end
40
37
 
41
38
  # desc 'setup standard benchmark'
@@ -54,9 +51,9 @@ namespace :benchmarks do
54
51
  # moving from 5 second of time to 12 still shows slower based on when classifier is required
55
52
  # make sure to be plugged in while benchmarking ;) Otherwise you get very unreliable results
56
53
  require 'classifier-reborn'
57
- if ENV['COVERAGE']
54
+ if ENV['COVERAGE'] || ENV['ONESHOT']
58
55
  require 'coverage'
59
- ::Coverage.start
56
+ ::Coverage.start(oneshot_lines: !!ENV['ONESHOT'])
60
57
  end
61
58
  require 'redis'
62
59
  require 'coverband'
@@ -79,6 +76,8 @@ namespace :benchmarks do
79
76
  config.root = Dir.pwd
80
77
  config.logger = $stdout
81
78
  config.store = benchmark_redis_store
79
+ config.use_oneshot_lines_coverage = true if ENV['ONESHOT']
80
+ config.simulate_oneshot_lines_coverage = true if ENV['SIMULATE_ONESHOT']
82
81
  end
83
82
  end
84
83
 
@@ -207,11 +206,54 @@ namespace :benchmarks do
207
206
  end.pretty_print
208
207
  data = $stdout.string
209
208
  $stdout = previous_out
210
- raise 'leaking memory!!!' unless data.match('Total retained: 0 bytes')
209
+ unless data.match('Total retained: 0 bytes')
210
+ puts data
211
+ raise 'leaking memory!!!'
212
+ end
213
+ ensure
214
+ $stdout = previous_out
215
+ end
216
+
217
+ def measure_memory_report_coverage
218
+ require 'memory_profiler'
219
+ report = fake_report
220
+ store = benchmark_redis_store
221
+ store.clear!
222
+ mock_files(store)
223
+
224
+ # warmup
225
+ 3.times { Coverband.report_coverage }
226
+
227
+ previous_out = $stdout
228
+ capture = StringIO.new
229
+ $stdout = capture
230
+
231
+ MemoryProfiler.report do
232
+ 10.times do
233
+ Coverband.report_coverage
234
+ ###
235
+ # Set to nil not {} as it is easier to verify that no memory is retained when nil gets released
236
+ # don't use Coverband::Collectors::Delta.reset which sets to {}
237
+ # we clear this as this one variable is expected to retain memory and is a false positive
238
+ ###
239
+ Coverband::Collectors::Delta.class_variable_set(:@@previous_coverage, nil)
240
+ end
241
+ end.pretty_print
242
+ data = $stdout.string
243
+ $stdout = previous_out
244
+ unless data.match('Total retained: 0 bytes')
245
+ puts data
246
+ raise 'leaking memory!!!'
247
+ end
211
248
  ensure
212
249
  $stdout = previous_out
213
250
  end
214
251
 
252
+ ###
253
+ # TODO: This currently fails, as it holds a string in redis adapter
254
+ # but really Coverband shouldn't be configured multiple times and the leak is small
255
+ # not including in test suite but we can try to figure it out and fix.
256
+ ###
215
257
  def measure_configure_memory
216
258
  require 'memory_profiler'
217
259
  # warmup
@@ -231,18 +273,73 @@ namespace :benchmarks do
231
273
  end.pretty_print
232
274
  data = $stdout.string
233
275
  $stdout = previous_out
234
- puts data
235
- raise 'leaking memory!!!' unless data.match('Total retained: 0 bytes')
276
+ unless data.match('Total retained: 0 bytes')
277
+ puts data
278
+ raise 'leaking memory!!!'
279
+ end
236
280
  ensure
237
281
  $stdout = previous_out
238
282
  end
239
283
 
284
+ desc 'checks memory of collector'
285
+ task memory_check: [:setup] do
286
+ require 'pry-byebug'
287
+ require 'objspace'
288
+ puts 'memory load check'
289
+ puts(ObjectSpace.memsize_of_all / 2**20)
290
+ data = File.read('./tmp/debug_data.json')
291
+ # about 2mb
292
+ puts(ObjectSpace.memsize_of(data) / 2**20)
293
+
294
+ json_data = JSON.parse(data)
295
+ # this seems to just show the value of the pointer
296
+ # puts(ObjectSpace.memsize_of(json_data) / 2**20)
297
+ # implies json takes 10-12 mb
298
+ puts(ObjectSpace.memsize_of_all / 2**20)
299
+
300
+ json_data = nil
301
+ GC.start
302
+ json_data = JSON.parse(data)
303
+ # this seems to just show the value of the pointer
304
+ # puts(ObjectSpace.memsize_of(json_data) / 2**20)
305
+ # implies json takes 10-12 mb
306
+ puts(ObjectSpace.memsize_of_all / 2**20)
307
+
308
+ json_data = nil
309
+ GC.start
310
+ json_data = JSON.parse(data)
311
+ # this seems to just show the value of the pointer
312
+ # puts(ObjectSpace.memsize_of(json_data) / 2**20)
313
+ # implies json takes 10-12 mb
314
+ puts(ObjectSpace.memsize_of_all / 2**20)
315
+
316
+ json_data = nil
317
+ GC.start
318
+ json_data = JSON.parse(data)
319
+ # this seems to just show the value of the pointer
320
+ # puts(ObjectSpace.memsize_of(json_data) / 2**20)
321
+ # implies json takes 10-12 mb
322
+ puts(ObjectSpace.memsize_of_all / 2**20)
323
+
324
+ json_data = nil
325
+ GC.start
326
+ puts(ObjectSpace.memsize_of_all / 2**20)
327
+ debugger
328
+ puts 'done'
329
+ end
330
+
240
331
  desc 'runs memory reporting on Redis store'
241
332
  task memory_reporting: [:setup] do
242
333
  puts 'runs memory benchmarking to ensure we dont leak'
243
334
  measure_memory
244
335
  end
245
336
 
337
+ desc 'runs memory reporting on report_coverage'
338
+ task memory_reporting_report_coverage: [:setup] do
339
+ puts 'runs memory benchmarking on report_coverage to ensure we dont leak'
340
+ measure_memory_report_coverage
341
+ end
342
+
246
343
  desc 'runs memory reporting on configure'
247
344
  task memory_configure_reporting: [:setup] do
248
345
  puts 'runs memory benchmarking on configure to ensure we dont leak'
@@ -252,7 +349,12 @@ namespace :benchmarks do
252
349
  desc 'runs memory leak check via Rails tests'
253
350
  task memory_rails: [:setup] do
254
351
  puts 'runs memory rails test to ensure we dont leak'
255
- puts `COVERBAND_MEMORY_TEST=true bundle exec m test/unit/rails_full_stack_test.rb:22`
352
+ puts `COVERBAND_MEMORY_TEST=true bundle exec test/forked/rails_full_stack_test.rb`
353
+ end
354
+
355
+ desc 'runs memory leak checks'
356
+ task memory: %i[memory_reporting memory_reporting_report_coverage memory_rails] do
357
+ puts 'done'
256
358
  end
257
359
 
258
360
  desc 'runs benchmarks on reporting large sets of files to redis'
@@ -267,6 +369,29 @@ namespace :benchmarks do
267
369
  run_work(true)
268
370
  end
269
371
 
372
+ def run_big
373
+ require 'memory_profiler'
374
+ require './test/unique_files'
375
+
376
+ 4000.times { |index| require_unique_file('dog.rb.erb', dog_number: index) }
377
+ # warmup
378
+ 3.times { Coverband.report_coverage }
379
+ dogs = 400.times.map { |index| Object.const_get("Dog#{index}") }
380
+ MemoryProfiler.report do
381
+ 10.times do
382
+ dogs.each(&:bark)
383
+ Coverband.report_coverage
384
+ end
385
+ end.pretty_print
386
+ end
387
+
388
+ task run_big: %i[setup setup_redis] do
389
+ # ensure we cleared from last run
390
+ benchmark_redis_store.clear!
391
+
392
+ run_big
393
+ end
394
+
270
395
  # desc 'runs benchmarks file store'
271
396
  task run_file: %i[setup setup_file] do
272
397
  puts 'Coverband configured with file store'