coverband 4.2.0 → 4.2.1.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +29 -9
  3. data/.travis.yml +9 -1
  4. data/Gemfile +2 -1
  5. data/Gemfile.rails4 +1 -0
  6. data/README.md +34 -13
  7. data/Rakefile +10 -2
  8. data/changes.md +25 -10
  9. data/coverband.gemspec +5 -1
  10. data/lib/coverband.rb +30 -18
  11. data/lib/coverband/adapters/base.rb +2 -7
  12. data/lib/coverband/adapters/file_store.rb +1 -1
  13. data/lib/coverband/at_exit.rb +2 -11
  14. data/lib/coverband/collectors/coverage.rb +29 -32
  15. data/lib/coverband/collectors/delta.rb +20 -24
  16. data/lib/coverband/configuration.rb +49 -19
  17. data/lib/coverband/integrations/background.rb +6 -4
  18. data/lib/coverband/integrations/{middleware.rb → background_middleware.rb} +2 -6
  19. data/lib/coverband/integrations/bundler.rb +8 -0
  20. data/lib/coverband/integrations/report_middleware.rb +15 -0
  21. data/lib/coverband/integrations/resque.rb +3 -5
  22. data/lib/coverband/reporters/base.rb +39 -13
  23. data/lib/coverband/reporters/html_report.rb +21 -27
  24. data/lib/coverband/reporters/web.rb +2 -21
  25. data/lib/coverband/utils/file_list.rb +16 -5
  26. data/lib/coverband/utils/file_path_helper.rb +2 -0
  27. data/lib/coverband/utils/html_formatter.rb +25 -8
  28. data/lib/coverband/utils/lines_classifier.rb +5 -0
  29. data/lib/coverband/utils/railtie.rb +11 -12
  30. data/lib/coverband/utils/result.rb +5 -44
  31. data/lib/coverband/utils/results.rb +51 -0
  32. data/lib/coverband/utils/source_file.rb +29 -5
  33. data/lib/coverband/utils/tasks.rb +11 -2
  34. data/lib/coverband/version.rb +1 -1
  35. data/public/application.js +27 -0
  36. data/test/benchmarks/benchmark.rake +10 -20
  37. data/test/coverband/adapters/file_store_test.rb +5 -5
  38. data/test/coverband/adapters/redis_store_test.rb +8 -7
  39. data/test/coverband/at_exit_test.rb +0 -2
  40. data/test/coverband/collectors/coverage_test.rb +57 -9
  41. data/test/coverband/collectors/delta_test.rb +27 -6
  42. data/test/coverband/configuration_test.rb +47 -7
  43. data/test/coverband/coverband_test.rb +0 -1
  44. data/test/coverband/integrations/background_middleware_test.rb +44 -0
  45. data/test/coverband/integrations/background_test.rb +1 -3
  46. data/test/coverband/integrations/report_middleware_test.rb +44 -0
  47. data/test/coverband/integrations/resque_worker_test.rb +4 -3
  48. data/test/coverband/integrations/test_resque_job.rb +3 -1
  49. data/test/coverband/reporters/base_test.rb +4 -4
  50. data/test/coverband/reporters/console_test.rb +1 -2
  51. data/test/coverband/reporters/html_test.rb +79 -16
  52. data/test/coverband/reporters/web_test.rb +0 -10
  53. data/test/coverband/utils/file_groups_test.rb +1 -1
  54. data/test/coverband/utils/file_list_test.rb +5 -5
  55. data/test/coverband/utils/html_formatter_test.rb +45 -0
  56. data/test/coverband/utils/result_test.rb +27 -47
  57. data/test/coverband/utils/results_test.rb +54 -0
  58. data/test/coverband/utils/s3_report_test.rb +2 -0
  59. data/test/coverband/utils/source_file_test.rb +50 -0
  60. data/test/forked/rails_full_stack_test.rb +101 -0
  61. data/test/forked/rails_rake_full_stack_test.rb +32 -0
  62. data/test/integration/full_stack_test.rb +17 -15
  63. data/test/rails4_dummy/Rakefile +6 -0
  64. data/test/rails4_dummy/config/application.rb +8 -9
  65. data/test/rails4_dummy/config/coverband.rb +3 -3
  66. data/test/rails5_dummy/Rakefile +6 -0
  67. data/test/rails5_dummy/config/application.rb +6 -10
  68. data/test/rails5_dummy/config/coverband.rb +2 -2
  69. data/test/rails_test_helper.rb +23 -4
  70. data/test/test_helper.rb +26 -1
  71. data/test/unique_files.rb +6 -5
  72. data/views/file_list.erb +2 -2
  73. data/views/gem_list.erb +10 -1
  74. data/views/layout.erb +10 -3
  75. data/views/source_file.erb +13 -4
  76. data/views/source_file_loader.erb +1 -1
  77. metadata +79 -9
  78. data/test/coverband/integrations/middleware_test.rb +0 -96
  79. data/test/integration/rails_full_stack_test.rb +0 -95
@@ -53,11 +53,6 @@ module Coverband
53
53
  coverage.keys || []
54
54
  end
55
55
 
56
- # TODO: deprecate / remove?
57
- def covered_lines_for_file(file)
58
- Array(coverage.dig(file, 'data'))
59
- end
60
-
61
56
  protected
62
57
 
63
58
  def split_coverage(types)
@@ -76,7 +71,7 @@ module Coverband
76
71
  raise 'abstract'
77
72
  end
78
73
 
79
- def get_report
74
+ def get_report(_type = nil)
80
75
  raise 'abstract'
81
76
  end
82
77
 
@@ -87,7 +82,7 @@ module Coverband
87
82
  def expand_report(report)
88
83
  expanded = {}
89
84
  report_time = Time.now.to_i
90
- updated_time = self.type == Coverband::EAGER_TYPE ? nil : report_time
85
+ updated_time = type == Coverband::EAGER_TYPE ? nil : report_time
91
86
  report.each_pair do |key, line_data|
92
87
  extended_data = {
93
88
  'first_updated_at' => report_time,
@@ -36,7 +36,7 @@ module Coverband
36
36
  File.open(path, 'w') { |f| f.write(report.to_json) }
37
37
  end
38
38
 
39
- def get_report
39
+ def get_report(_local_type = nil)
40
40
  if File.exist?(path)
41
41
  JSON.parse(File.read(path))
42
42
  else
@@ -16,19 +16,10 @@ module Coverband
16
16
  at_exit do
17
17
  ::Coverband::Background.stop
18
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) }
19
+ if !Coverband.configuration.report_on_exit
28
20
  # skip reporting
29
21
  else
30
- Coverband.report_coverage(true)
31
- #Coverband.configuration.logger&.debug('Coverband: Reported coverage before exit')
22
+ Coverband.report_coverage
32
23
  end
33
24
  end
34
25
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'singleton'
3
4
  require_relative 'delta'
4
5
 
@@ -13,10 +14,13 @@ module Coverband
13
14
  class Coverage
14
15
  include Singleton
15
16
 
17
+ def self.ruby_version_greater_than_or_equal_to?(version)
18
+ Gem::Version.new(RUBY_VERSION) >= Gem::Version.new(version)
19
+ end
20
+
16
21
  def reset_instance
17
22
  @project_directory = File.expand_path(Coverband.configuration.root)
18
- @ignore_patterns = Coverband.configuration.ignore + ['internal:prelude', 'schema.rb']
19
- @reporting_frequency = Coverband.configuration.reporting_frequency
23
+ @ignore_patterns = Coverband.configuration.ignore
20
24
  @store = Coverband.configuration.store
21
25
  @verbose = Coverband.configuration.verbose
22
26
  @logger = Coverband.configuration.logger
@@ -34,29 +38,29 @@ module Coverband
34
38
  @store.type = Coverband::EAGER_TYPE
35
39
  end
36
40
 
37
- def report_coverage(force_report = false)
38
- return if !ready_to_report? && !force_report
39
- raise 'no Coverband store set' unless @store
41
+ def eager_loading
42
+ old_coverage_type = @store.type
43
+ eager_loading!
44
+ yield
45
+ ensure
46
+ report_coverage
47
+ @store.type = old_coverage_type
48
+ end
40
49
 
41
- original_previous_set = Delta.previous_results
42
- files_with_line_usage = filtered_files(Delta.results)
50
+ def report_coverage
51
+ @semaphore.synchronize do
52
+ raise 'no Coverband store set' unless @store
43
53
 
44
- ###
45
- # Hack to prevent processes and threads from reporting first Coverage hit
46
- # when we are in runtime collection mode, which do not have a cache of previous
47
- # coverage to remove the initial stdlib Coverage loading data
48
- ###
49
- if ((original_previous_set.nil? && @store.type == Coverband::EAGER_TYPE) ||
50
- (original_previous_set && @store.type != Coverband::EAGER_TYPE))
54
+ files_with_line_usage = filtered_files(Delta.results)
51
55
  @store.save_report(files_with_line_usage)
52
56
  end
53
- rescue StandardError => err
57
+ rescue StandardError => e
54
58
  if @verbose
55
- @logger&.error 'coverage failed to store'
56
- @logger&.error "error: #{err.inspect} #{err.message}"
57
- @logger&.error err.backtrace
59
+ @logger.error 'coverage failed to store'
60
+ @logger.error "error: #{e.inspect} #{e.message}"
61
+ @logger.error e.backtrace
58
62
  end
59
- raise err if @test_env
63
+ raise e if @test_env
60
64
  end
61
65
 
62
66
  protected
@@ -83,25 +87,18 @@ module Coverband
83
87
  end.select { |_file_name, coverage| coverage.any? { |value| value&.nonzero? } }
84
88
  end
85
89
 
86
- def ready_to_report?
87
- (rand * 100.0) >= (100.0 - @reporting_frequency)
88
- end
89
-
90
90
  def initialize
91
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3.0')
92
- raise NotImplementedError, 'not supported until Ruby 2.3.0 and later'
93
- end
91
+ @semaphore = Mutex.new
92
+ raise NotImplementedError, 'Coverage needs Ruby > 2.3.0' if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3.0')
93
+
94
94
  require 'coverage'
95
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.5.0')
95
+ if Coverage.ruby_version_greater_than_or_equal_to?('2.6.0')
96
+ ::Coverage.start(oneshot_lines: Coverband.configuration.use_oneshot_lines_coverage) unless ::Coverage.running?
97
+ elsif Coverage.ruby_version_greater_than_or_equal_to?('2.5.0')
96
98
  ::Coverage.start unless ::Coverage.running?
97
99
  else
98
100
  ::Coverage.start
99
101
  end
100
- if Coverband.configuration.safe_reload_files
101
- Coverband.configuration.safe_reload_files.each do |safe_file|
102
- load safe_file
103
- end
104
- end
105
102
  reset_instance
106
103
  end
107
104
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Coverband
3
4
  module Collectors
4
5
  class Delta
5
- @semaphore = Mutex.new
6
- @@previous_coverage = nil
6
+ @@previous_coverage = {}
7
7
  attr_reader :current_coverage
8
8
 
9
9
  def initialize(current_coverage)
@@ -17,18 +17,9 @@ module Coverband
17
17
  end
18
18
 
19
19
  def self.results(process_coverage = RubyCoverage)
20
- @semaphore.synchronize do
21
- set_default_results
22
- new(process_coverage.results).results
23
- end
24
- end
25
-
26
- def self.previous_results
27
- @@previous_coverage
28
- end
29
-
30
- def self.set_default_results
31
- @@previous_coverage ||= {}
20
+ coverage_results = process_coverage.results
21
+ coverage_results = transform_oneshot_lines_results(coverage_results) if Coverband.configuration.use_oneshot_lines_coverage
22
+ new(coverage_results).results
32
23
  end
33
24
 
34
25
  def results
@@ -38,28 +29,33 @@ module Coverband
38
29
  end
39
30
 
40
31
  def self.reset
41
- @@previous_coverage = nil
32
+ @@previous_coverage = {}
42
33
  end
43
34
 
44
35
  private
45
36
 
46
37
  def generate
47
38
  current_coverage.each_with_object({}) do |(file, line_counts), new_results|
48
- if @@previous_coverage[file]
49
- new_results[file] = array_diff(line_counts, @@previous_coverage[file])
50
- else
51
- new_results[file] = line_counts
52
- end
39
+ new_results[file] = if @@previous_coverage[file]
40
+ array_diff(line_counts, @@previous_coverage[file])
41
+ else
42
+ line_counts
43
+ end
53
44
  end
54
45
  end
55
46
 
56
47
  def array_diff(latest, original)
57
48
  latest.map.with_index do |v, i|
58
- if (v && original[i])
59
- [0, v - original[i]].max
60
- else
61
- nil
49
+ [0, v - original[i]].max if v && original[i]
50
+ end
51
+ end
52
+
53
+ private_class_method def self.transform_oneshot_lines_results(results)
54
+ 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|
56
+ line_counts[line_number - 1] = 1
62
57
  end
58
+ new_results[file] = transformed_line_counts
63
59
  end
64
60
  end
65
61
  end
@@ -3,15 +3,31 @@
3
3
  module Coverband
4
4
  class Configuration
5
5
  attr_accessor :root_paths, :root,
6
- :ignore, :additional_files, :verbose,
7
- :reporter, :reporting_frequency,
8
- :redis_namespace, :redis_ttl,
9
- :safe_reload_files, :background_reporting_enabled,
6
+ :additional_files, :verbose,
7
+ :reporter, :redis_namespace, :redis_ttl,
8
+ :background_reporting_enabled,
10
9
  :background_reporting_sleep_seconds, :test_env,
11
- :web_enable_clear, :gem_details, :web_debug
10
+ :web_enable_clear, :gem_details, :web_debug, :report_on_exit
12
11
 
13
12
  attr_writer :logger, :s3_region, :s3_bucket, :s3_access_key_id, :s3_secret_access_key
14
- attr_reader :track_gems
13
+ attr_reader :track_gems, :ignore, :use_oneshot_lines_coverage
14
+
15
+ #####
16
+ # TODO: This is is brittle and not a great solution to avoid deploy time
17
+ # actions polluting the 'runtime' metrics
18
+ #
19
+ # * should we skip /bin/rails webpacker:compile ?
20
+ # * Perhaps detect heroku deployment ENV var opposed to tasks?
21
+ #####
22
+ IGNORE_TASKS = ['coverband:clear',
23
+ 'coverband:coverage',
24
+ 'coverband:coverage_server',
25
+ 'coverband:migrate']
26
+
27
+ # Heroku when building assets runs code from a dynamic directory
28
+ # /tmp was added to avoid coverage from /tmp/build directories during
29
+ # heroku asset compilation
30
+ IGNORE_DEFAULTS = %w[vendor .erb$ .slim$ /tmp internal:prelude schema.rb]
15
31
 
16
32
  def initialize
17
33
  reset
@@ -20,12 +36,8 @@ module Coverband
20
36
  def reset
21
37
  @root = Dir.pwd
22
38
  @root_paths = []
23
- # Heroku when building assets runs code from a dynamic directory
24
- # /tmp was added to avoid coverage from /tmp/build directories during
25
- # heroku asset compilation
26
- @ignore = %w[vendor .erb$ .slim$ /tmp]
39
+ @ignore = IGNORE_DEFAULTS.dup
27
40
  @additional_files = []
28
- @reporting_frequency = 0.0
29
41
  @verbose = false
30
42
  @reporter = 'scov'
31
43
  @logger = nil
@@ -38,6 +50,8 @@ module Coverband
38
50
  @gem_details = false
39
51
  @groups = {}
40
52
  @web_debug = false
53
+ @report_on_exit = true
54
+ @use_oneshot_lines_coverage = false
41
55
 
42
56
  # TODO: should we push these to adapter configs
43
57
  @s3_region = nil
@@ -49,7 +63,7 @@ module Coverband
49
63
  end
50
64
 
51
65
  def logger
52
- @logger ||= if defined?(Rails.logger)
66
+ @logger ||= if defined?(Rails.logger) && Rails.logger
53
67
  Rails.logger
54
68
  else
55
69
  Logger.new(STDOUT)
@@ -77,21 +91,27 @@ module Coverband
77
91
  end
78
92
 
79
93
  def store=(store)
80
- if store.is_a?(Coverband::Adapters::Base)
81
- @store = store
82
- else
83
- raise 'please pass in an subclass of Coverband::Adapters for supported stores'
84
- end
94
+ raise 'Pass in an instance of Coverband::Adapters' unless store.is_a?(Coverband::Adapters::Base)
95
+
96
+ @store = store
97
+ end
98
+
99
+ ###
100
+ # Don't allow the ignore to override things like gem tracking
101
+ ###
102
+ def ignore=(ignored_array)
103
+ @ignore = (@ignore + ignored_array).uniq
85
104
  end
86
105
 
87
106
  def track_gems=(value)
88
107
  @track_gems = value
89
108
  return unless @track_gems
109
+
90
110
  # by default we ignore vendor where many deployments put gems
91
111
  # we will remove this default if track_gems is set
92
112
  @ignore.delete('vendor')
93
113
  # while we want to allow vendored gems we don't want to track vendored ruby STDLIB
94
- @ignore << 'vendor/ruby-*'
114
+ @ignore << 'vendor/ruby-*' unless @ignore.include?('vendor/ruby-*')
95
115
  add_group('App', root)
96
116
  # TODO: rework support for multiple gem paths
97
117
  # currently this supports GEM_HOME (which should be first path)
@@ -135,7 +155,7 @@ module Coverband
135
155
  roots
136
156
  end
137
157
 
138
- SKIPPED_SETTINGS = %w(@s3_secret_access_key @store)
158
+ SKIPPED_SETTINGS = %w[@s3_secret_access_key @store]
139
159
  def to_h
140
160
  instance_variables
141
161
  .each_with_object('gem_paths': gem_paths) do |var, hash|
@@ -143,6 +163,16 @@ module Coverband
143
163
  end
144
164
  end
145
165
 
166
+ def use_oneshot_lines_coverage=(value)
167
+ raise(Exception, 'One shot line coverage is only available in ruby >= 2.6') unless one_shot_coverage_implemented_in_ruby_version? || !value
168
+
169
+ @use_oneshot_lines_coverage = value
170
+ end
171
+
172
+ def one_shot_coverage_implemented_in_ruby_version?
173
+ Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.6.0')
174
+ end
175
+
146
176
  private
147
177
 
148
178
  def redis_url
@@ -7,6 +7,7 @@ module Coverband
7
7
 
8
8
  def self.stop
9
9
  return unless @thread
10
+
10
11
  @semaphore.synchronize do
11
12
  if @thread
12
13
  @thread.exit
@@ -16,7 +17,7 @@ module Coverband
16
17
  end
17
18
 
18
19
  def self.running?
19
- @thread && @thread.alive?
20
+ @thread&.alive?
20
21
  end
21
22
 
22
23
  def self.start
@@ -25,12 +26,13 @@ module Coverband
25
26
  logger = Coverband.configuration.logger
26
27
  @semaphore.synchronize do
27
28
  return if running?
28
- logger&.debug('Coverband: Starting background reporting')
29
+
30
+ logger.debug('Coverband: Starting background reporting')
29
31
  sleep_seconds = Coverband.configuration.background_reporting_sleep_seconds
30
32
  @thread = Thread.new do
31
33
  loop do
32
- Coverband.report_coverage(true)
33
- logger&.debug("Coverband: Reported coverage via thread. Sleeping #{sleep_seconds}s") if Coverband.configuration.verbose
34
+ Coverband.report_coverage
35
+ logger.debug("Coverband: Reported coverage via thread. Sleeping #{sleep_seconds}s") if Coverband.configuration.verbose
34
36
  sleep(sleep_seconds)
35
37
  end
36
38
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coverband
4
- class Middleware
4
+ class BackgroundMiddleware
5
5
  def initialize(app)
6
6
  @app = app
7
7
  end
@@ -10,11 +10,7 @@ module Coverband
10
10
  @app.call(env)
11
11
  ensure
12
12
  AtExit.register
13
- if Coverband.configuration.background_reporting_enabled
14
- Background.start
15
- else
16
- Collectors::Coverage.instance.report_coverage
17
- end
13
+ Background.start if Coverband.configuration.background_reporting_enabled
18
14
  end
19
15
  end
20
16
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundlerEagerLoad
4
+ def require(*groups)
5
+ Coverband.eager_loading_coverage { super }
6
+ end
7
+ end
8
+ Bundler.singleton_class.prepend BundlerEagerLoad
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coverband
4
+ class ReportMiddleware
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ @app.call(env)
11
+ ensure
12
+ Collectors::Coverage.instance.report_coverage
13
+ end
14
+ end
15
+ end