coverband 4.2.0.rc1 → 4.2.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.travis.yml +4 -2
  4. data/README.md +48 -43
  5. data/Rakefile +1 -1
  6. data/changes.md +40 -24
  7. data/coverband.gemspec +2 -0
  8. data/lib/coverband.rb +23 -3
  9. data/lib/coverband/adapters/base.rb +24 -1
  10. data/lib/coverband/adapters/redis_store.rb +28 -7
  11. data/lib/coverband/at_exit.rb +19 -2
  12. data/lib/coverband/collectors/coverage.rb +36 -82
  13. data/lib/coverband/collectors/delta.rb +63 -0
  14. data/lib/coverband/integrations/background.rb +4 -3
  15. data/lib/coverband/integrations/rack_server_check.rb +4 -1
  16. data/lib/coverband/reporters/base.rb +21 -14
  17. data/lib/coverband/reporters/html_report.rb +40 -15
  18. data/lib/coverband/reporters/web.rb +29 -6
  19. data/lib/coverband/utils/html_formatter.rb +20 -4
  20. data/lib/coverband/utils/railtie.rb +8 -0
  21. data/lib/coverband/utils/result.rb +3 -2
  22. data/lib/coverband/utils/results.rb +63 -0
  23. data/lib/coverband/utils/source_file.rb +5 -0
  24. data/lib/coverband/version.rb +1 -1
  25. data/public/application.css +5 -0
  26. data/public/application.js +108 -1644
  27. data/public/dependencies.js +1581 -0
  28. data/public/favicon.png +0 -0
  29. data/test/coverband/adapters/redis_store_test.rb +26 -9
  30. data/test/coverband/collectors/coverage_test.rb +56 -45
  31. data/test/coverband/collectors/delta_test.rb +52 -0
  32. data/test/coverband/coverband_test.rb +7 -0
  33. data/test/coverband/integrations/background_test.rb +14 -11
  34. data/test/coverband/integrations/middleware_test.rb +1 -0
  35. data/test/coverband/reporters/base_test.rb +4 -4
  36. data/test/coverband/reporters/console_test.rb +1 -1
  37. data/test/coverband/reporters/html_test.rb +6 -7
  38. data/test/integration/full_stack_test.rb +10 -8
  39. data/test/integration/rails_full_stack_test.rb +23 -4
  40. data/test/rails4_dummy/config/application.rb +1 -1
  41. data/test/rails4_dummy/config/coverband.rb +4 -1
  42. data/test/rails4_dummy/tmp/.keep +0 -0
  43. data/test/rails5_dummy/config/application.rb +1 -1
  44. data/test/rails5_dummy/config/coverband.rb +3 -1
  45. data/test/rails_test_helper.rb +13 -8
  46. data/test/test_helper.rb +9 -13
  47. data/test/unique_files.rb +23 -0
  48. data/views/file_list.erb +9 -0
  49. data/views/gem_list.erb +1 -1
  50. data/views/layout.erb +4 -3
  51. data/views/source_file.erb +16 -4
  52. data/views/source_file_loader.erb +7 -0
  53. metadata +29 -4
  54. data/test/integration/rails_gems_full_stack_test.rb +0 -36
@@ -22,7 +22,17 @@ module Coverband
22
22
  end
23
23
 
24
24
  def clear!
25
- @redis.del(base_key)
25
+ Coverband::TYPES.each do |type|
26
+ @redis.del(type_base_key(type))
27
+ end
28
+ end
29
+
30
+ def clear_file!(filename)
31
+ Coverband::TYPES.each do |type|
32
+ data = get_report(type)
33
+ data.delete(filename)
34
+ save_coverage(data, type)
35
+ end
26
36
  end
27
37
 
28
38
  def size
@@ -51,6 +61,11 @@ module Coverband
51
61
  save_coverage(merge_reports(get_report, relative_path_report, skip_expansion: true))
52
62
  end
53
63
 
64
+ def type=(type)
65
+ super
66
+ reset_base_key
67
+ end
68
+
54
69
  private
55
70
 
56
71
  attr_reader :redis
@@ -60,16 +75,22 @@ module Coverband
60
75
  end
61
76
 
62
77
  def base_key
63
- @base_key ||= [@format_version, @redis_namespace].compact.join('.')
78
+ @base_key ||= [@format_version, @redis_namespace, type].compact.join('.')
79
+ end
80
+
81
+ def type_base_key(local_type)
82
+ [@format_version, @redis_namespace, local_type].compact.join('.')
64
83
  end
65
84
 
66
- def save_coverage(data)
67
- redis.set base_key, data.to_json
68
- redis.expire(base_key, @ttl) if @ttl
85
+ def save_coverage(data, local_type = nil)
86
+ local_type ||= type
87
+ redis.set type_base_key(local_type), data.to_json
88
+ redis.expire(type_base_key(local_type), @ttl) if @ttl
69
89
  end
70
90
 
71
- def get_report
72
- data = redis.get base_key
91
+ def get_report(local_type = nil)
92
+ local_type ||= type
93
+ data = redis.get type_base_key(local_type)
73
94
  data ? JSON.parse(data) : {}
74
95
  end
75
96
  end
@@ -4,15 +4,32 @@ module Coverband
4
4
  class AtExit
5
5
  @semaphore = Mutex.new
6
6
 
7
+ @at_exit_registered = nil
7
8
  def self.register
9
+ return if ENV['COVERBAND_DISABLE_AT_EXIT']
8
10
  return if @at_exit_registered
11
+
9
12
  @semaphore.synchronize do
10
13
  return if @at_exit_registered
14
+
11
15
  @at_exit_registered = true
12
16
  at_exit do
13
17
  ::Coverband::Background.stop
14
- Coverband.report_coverage(true)
15
- Coverband.configuration.logger&.debug('Coverband: Reported coverage before exit')
18
+
19
+ #####
20
+ # TODO: This is is brittle and not a great solution to avoid deploy time
21
+ # actions polluting the 'runtime' metrics
22
+ #
23
+ # * should we skip /bin/rails webpacker:compile ?
24
+ # * Perhaps detect heroku deployment ENV var opposed to tasks?
25
+ #####
26
+ default_heroku_tasks = ['assets:clean', 'assets:precompile']
27
+ if defined?(Rake) && Rake.respond_to?(:application) && (Rake&.application&.top_level_tasks || []).any? { |task| default_heroku_tasks.include?(task) }
28
+ # skip reporting
29
+ else
30
+ Coverband.report_coverage(true)
31
+ #Coverband.configuration.logger&.debug('Coverband: Reported coverage before exit')
32
+ end
16
33
  end
17
34
  end
18
35
  end
@@ -1,48 +1,62 @@
1
1
  # frozen_string_literal: true
2
+ require 'singleton'
3
+ require_relative 'delta'
2
4
 
3
5
  module Coverband
4
6
  module Collectors
5
7
  ###
6
8
  # TODO: look at alternatives to semaphore
7
- # StandardError seems line be better option
9
+ # StandardError seems like be better option
8
10
  # coverband previously had RuntimeError here
9
11
  # but runtime error can let a large number of error crash this method
10
12
  # and this method is currently in a ensure block in middleware and threads
11
13
  ###
12
14
  class Coverage
13
- def self.instance
14
- Thread.current[:coverband_instance] ||= Coverband::Collectors::Coverage.new
15
- end
15
+ include Singleton
16
+ extend Forwardable
16
17
 
17
18
  def reset_instance
18
19
  @project_directory = File.expand_path(Coverband.configuration.root)
19
- @file_line_usage = {}
20
20
  @ignore_patterns = Coverband.configuration.ignore + ['internal:prelude', 'schema.rb']
21
21
  @reporting_frequency = Coverband.configuration.reporting_frequency
22
22
  @store = Coverband.configuration.store
23
23
  @verbose = Coverband.configuration.verbose
24
24
  @logger = Coverband.configuration.logger
25
- @current_thread = Thread.current
26
25
  @test_env = Coverband.configuration.test_env
27
26
  @track_gems = Coverband.configuration.track_gems
28
- @@previous_results = nil
29
- Thread.current[:coverband_instance] = nil
27
+ Delta.reset
30
28
  self
31
29
  end
32
30
 
31
+ def runtime!
32
+ @store.type = nil
33
+ end
34
+
35
+ def eager_loading!
36
+ @store.type = Coverband::EAGER_TYPE
37
+ end
38
+
33
39
  def report_coverage(force_report = false)
34
40
  return if !ready_to_report? && !force_report
35
41
  raise 'no Coverband store set' unless @store
36
42
 
37
- new_results = get_new_coverage_results
38
- add_filtered_files(new_results)
39
- @store.save_report(files_with_line_usage)
40
- @file_line_usage.clear
43
+ original_previous_set = Delta.previous_results
44
+ files_with_line_usage = filtered_files(Delta.results)
45
+
46
+ ###
47
+ # Hack to prevent processes and threads from reporting first Coverage hit
48
+ # when we are in runtime collection mode, which do not have a cache of previous
49
+ # coverage to remove the initial stdlib Coverage loading data
50
+ ###
51
+ if ((original_previous_set.nil? && @store.type == Coverband::EAGER_TYPE) ||
52
+ (original_previous_set && @store.type != Coverband::EAGER_TYPE))
53
+ @store.save_report(files_with_line_usage)
54
+ end
41
55
  rescue StandardError => err
42
56
  if @verbose
43
- @logger.error 'coverage failed to store'
44
- @logger.error "error: #{err.inspect} #{err.message}"
45
- @logger.error err.backtrace
57
+ @logger&.error 'coverage failed to store'
58
+ @logger&.error "error: #{err.inspect} #{err.message}"
59
+ @logger&.error err.backtrace
46
60
  end
47
61
  raise err if @test_env
48
62
  end
@@ -59,85 +73,27 @@ module Coverband
59
73
  @ignore_patterns.none? do |pattern|
60
74
  file.include?(pattern)
61
75
  end && (file.start_with?(@project_directory) ||
62
- (@track_gems &&
63
- Coverband.configuration.gem_paths.any? { |path| file.start_with?(path) }))
76
+ (@track_gems &&
77
+ Coverband.configuration.gem_paths.any? { |path| file.start_with?(path) }))
64
78
  end
65
79
 
66
80
  private
67
81
 
68
- def add_filtered_files(new_results)
69
- new_results.each_pair do |file, line_counts|
70
- next unless track_file?(file)
71
- add_file(file, line_counts)
72
- end
82
+ def filtered_files(new_results)
83
+ new_results.each_with_object({}) do |(file, line_counts), file_line_usage|
84
+ file_line_usage[file] = line_counts if track_file?(file)
85
+ end.select { |_file_name, coverage| coverage.any? { |value| value&.nonzero? } }
73
86
  end
74
87
 
75
88
  def ready_to_report?
76
89
  (rand * 100.0) >= (100.0 - @reporting_frequency)
77
90
  end
78
91
 
79
- def get_new_coverage_results
80
- @semaphore.synchronize { new_coverage(::Coverage.peek_result.dup) }
81
- end
82
-
83
- def files_with_line_usage
84
- @file_line_usage.select do |_file_name, coverage|
85
- coverage.any? { |value| value&.nonzero? }
86
- end
87
- end
88
-
89
- def array_diff(latest, original)
90
- latest.map.with_index do |v, i|
91
- if (v && original[i])
92
- if v >= original[i]
93
- v - original[i]
94
- else
95
- 0
96
- end
97
- else
98
- nil
99
- end
100
- end
101
- end
102
-
103
- def previous_results
104
- @@previous_results
105
- end
106
-
107
- def add_previous_results(val)
108
- @@previous_results = val
109
- end
110
-
111
- def new_coverage(current_coverage)
112
- if previous_results
113
- new_results = {}
114
- current_coverage.each_pair do |file, line_counts|
115
- if previous_results[file]
116
- new_results[file] = array_diff(line_counts, previous_results[file])
117
- else
118
- new_results[file] = line_counts
119
- end
120
- end
121
- else
122
- new_results = current_coverage
123
- end
124
-
125
- add_previous_results(current_coverage)
126
- new_results.dup
127
- end
128
-
129
- def add_file(file, line_counts)
130
- @file_line_usage[file] = line_counts
131
- end
132
-
133
92
  def initialize
134
93
  if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3.0')
135
94
  raise NotImplementedError, 'not supported until Ruby 2.3.0 and later'
136
95
  end
137
- unless defined?(::Coverage)
138
- # puts 'loading coverage'
139
- require 'coverage'
140
- end
96
+ require 'coverage'
141
97
  if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.5.0')
142
98
  ::Coverage.start unless ::Coverage.running?
143
99
  else
@@ -148,8 +104,6 @@ module Coverband
148
104
  load safe_file
149
105
  end
150
106
  end
151
- @semaphore = Mutex.new
152
- @@previous_results = nil
153
107
  reset_instance
154
108
  end
155
109
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+ module Coverband
3
+ module Collectors
4
+ class Delta
5
+ @semaphore = Mutex.new
6
+ @@previous_coverage = nil
7
+ attr_reader :current_coverage
8
+
9
+ def initialize(current_coverage)
10
+ @current_coverage = current_coverage
11
+ end
12
+
13
+ class RubyCoverage
14
+ def self.results
15
+ ::Coverage.peek_result.dup
16
+ end
17
+ end
18
+
19
+ def self.results(process_coverage = RubyCoverage)
20
+ @semaphore.synchronize do
21
+ @@previous_coverage ||= {}
22
+ new(process_coverage.results).results
23
+ end
24
+ end
25
+
26
+ def self.previous_results
27
+ @@previous_coverage
28
+ end
29
+
30
+ def results
31
+ new_results = generate
32
+ @@previous_coverage = current_coverage
33
+ new_results
34
+ end
35
+
36
+ def self.reset
37
+ @@previous_coverage = nil
38
+ end
39
+
40
+ private
41
+
42
+ def generate
43
+ current_coverage.each_with_object({}) do |(file, line_counts), new_results|
44
+ if @@previous_coverage[file]
45
+ new_results[file] = array_diff(line_counts, @@previous_coverage[file])
46
+ else
47
+ new_results[file] = line_counts
48
+ end
49
+ end
50
+ end
51
+
52
+ def array_diff(latest, original)
53
+ latest.map.with_index do |v, i|
54
+ if (v && original[i])
55
+ [0, v - original[i]].max
56
+ else
57
+ nil
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -3,6 +3,7 @@
3
3
  module Coverband
4
4
  class Background
5
5
  @semaphore = Mutex.new
6
+ @thread = nil
6
7
 
7
8
  def self.stop
8
9
  return unless @thread
@@ -15,15 +16,15 @@ module Coverband
15
16
  end
16
17
 
17
18
  def self.running?
18
- !!@thread
19
+ @thread && @thread.alive?
19
20
  end
20
21
 
21
22
  def self.start
22
- return if @thread
23
+ return if running?
23
24
 
24
25
  logger = Coverband.configuration.logger
25
26
  @semaphore.synchronize do
26
- return if @thread
27
+ return if running?
27
28
  logger&.debug('Coverband: Starting background reporting')
28
29
  sleep_seconds = Coverband.configuration.background_reporting_sleep_seconds
29
30
  @thread = Thread.new do
@@ -20,7 +20,10 @@ module Coverband
20
20
 
21
21
  def rails_server?
22
22
  @stack.any? do |location|
23
- location.path.include?('rails/commands/commands_tasks.rb') && location.label == 'server'
23
+ (
24
+ location.path.include?('rails/commands/commands_tasks.rb') && location.label == 'server' ||
25
+ location.path.include?('rails/commands/server/server_command.rb') && location.label == 'perform'
26
+ )
24
27
  end
25
28
  end
26
29
  end
@@ -15,7 +15,7 @@ module Coverband
15
15
 
16
16
  if Coverband.configuration.verbose
17
17
  msg = "report:\n #{scov_style_report.inspect}"
18
- Coverband.configuration.logger.debug msg
18
+ # Coverband.configuration.logger.debug msg
19
19
  end
20
20
  scov_style_report
21
21
  end
@@ -28,15 +28,18 @@ module Coverband
28
28
  end
29
29
 
30
30
  # normalize names across servers
31
- report_hash.each_with_object({}) do |(key, vals), fixed_report|
32
- filename = relative_path_to_full(key, roots)
33
- fixed_report[filename] = if fixed_report.key?(filename) && fixed_report[filename]['data'] && vals['data']
34
- merged_data = merge_arrays(fixed_report[filename]['data'], vals['data'])
35
- vals['data'] = merged_data
36
- vals
37
- else
38
- vals
39
- end
31
+ report_hash.each_with_object({}) do |(name, report), fixed_report|
32
+ fixed_report[name] = {}
33
+ report.each_pair do |key, vals|
34
+ filename = relative_path_to_full(key, roots)
35
+ fixed_report[name][filename] = if fixed_report[name].key?(filename) && fixed_report[name][filename]['data'] && vals['data']
36
+ merged_data = merge_arrays(fixed_report[name][filename]['data'], vals['data'])
37
+ vals['data'] = merged_data
38
+ vals
39
+ else
40
+ vals
41
+ end
42
+ end
40
43
  end
41
44
  end
42
45
 
@@ -64,10 +67,14 @@ module Coverband
64
67
  ###
65
68
  def get_current_scov_data_imp(store, roots)
66
69
  scov_style_report = {}
67
- store.coverage.each_pair do |key, line_data|
68
- next if Coverband.configuration.ignore.any? { |i| key.match(i) }
69
- next unless line_data
70
- scov_style_report[key] = line_data
70
+ store.get_coverage_report.each_pair do |name, data|
71
+ data.each_pair do |key, line_data|
72
+ next if Coverband.configuration.ignore.any? { |i| key.match(i) }
73
+ next unless line_data
74
+
75
+ scov_style_report[name] = {} unless scov_style_report.key?(name)
76
+ scov_style_report[name][key] = line_data
77
+ end
71
78
  end
72
79
 
73
80
  scov_style_report = fix_file_names(scov_style_report, roots)
@@ -3,23 +3,28 @@
3
3
  module Coverband
4
4
  module Reporters
5
5
  class HTMLReport < Base
6
- def self.report(store, options = {})
7
- scov_style_report = super(store, options)
8
- open_report = options.fetch(:open_report) { true }
9
- html = options.fetch(:html) { false }
10
- notice = options.fetch(:notice) { nil }
11
- base_path = options.fetch(:base_path) { nil }
6
+ attr_accessor :filtered_report_files, :open_report, :html, :notice,
7
+ :base_path, :filename
12
8
 
13
- # list all files, even if not tracked by Coverband (0% coverage)
14
- tracked_glob = "#{Coverband.configuration.current_root}/{app,lib,config}/**/*.{rb}"
15
- report_files = Coverband::Utils::Result.add_not_loaded_files(scov_style_report, tracked_glob)
16
- # apply coverband filters
17
- filtered_report_files = {}
18
- report_files.each_pair do |file, data|
19
- next if Coverband.configuration.ignore.any? { |i| file.match(i) }
20
- filtered_report_files[file] = data
21
- end
9
+ def initialize(store, options = {})
10
+ coverband_reports = Coverband::Reporters::Base.report(store, options)
11
+ self.open_report = options.fetch(:open_report) { true }
12
+ self.html = options.fetch(:html) { false }
13
+ # TODO: refactor notice out to top level of web only
14
+ self.notice = options.fetch(:notice) { nil }
15
+ self.base_path = options.fetch(:base_path) { nil }
16
+ self.filename = options.fetch(:filename) { nil }
17
+
18
+ self.filtered_report_files = self.class.fix_reports(coverband_reports)
19
+ end
20
+
21
+ def file_details
22
+ Coverband::Utils::HTMLFormatter.new(filtered_report_files,
23
+ base_path: base_path,
24
+ notice: notice).format_source_file!(filename)
25
+ end
22
26
 
27
+ def report
23
28
  if html
24
29
  Coverband::Utils::HTMLFormatter.new(filtered_report_files,
25
30
  base_path: base_path,
@@ -35,6 +40,26 @@ module Coverband
35
40
  Coverband::Utils::S3Report.instance.persist! if Coverband.configuration.s3_bucket
36
41
  end
37
42
  end
43
+
44
+ private
45
+
46
+ def self.fix_reports(reports)
47
+ # list all files, even if not tracked by Coverband (0% coverage)
48
+ tracked_glob = "#{Coverband.configuration.current_root}/{app,lib,config}/**/*.{rb}"
49
+ filtered_report_files = {}
50
+
51
+ reports.each_pair do |report_name, report_data|
52
+ filtered_report_files[report_name] = {}
53
+ report_files = Coverband::Utils::Result.add_not_loaded_files(report_data, tracked_glob)
54
+
55
+ # apply coverband filters
56
+ report_files.each_pair do |file, data|
57
+ next if Coverband.configuration.ignore.any? { |i| file.match(i) }
58
+
59
+ filtered_report_files[report_name][file] = data
60
+ end
61
+ end
62
+ end
38
63
  end
39
64
  end
40
65
  end