coverband 4.2.0 → 4.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +29 -10
  3. data/.travis.yml +16 -8
  4. data/CODE_OF_CONDUCT.md +76 -0
  5. data/Gemfile +3 -1
  6. data/Gemfile.rails4 +1 -0
  7. data/README.md +108 -59
  8. data/Rakefile +19 -3
  9. data/changes.md +85 -10
  10. data/coverband.gemspec +3 -5
  11. data/lib/coverband/adapters/base.rb +64 -45
  12. data/lib/coverband/adapters/file_store.rb +15 -9
  13. data/lib/coverband/adapters/hash_redis_store.rb +190 -0
  14. data/lib/coverband/adapters/redis_store.rb +35 -11
  15. data/lib/coverband/at_exit.rb +2 -11
  16. data/lib/coverband/collectors/coverage.rb +35 -41
  17. data/lib/coverband/collectors/delta.rb +34 -28
  18. data/lib/coverband/collectors/view_tracker.rb +157 -0
  19. data/lib/coverband/configuration.rb +115 -33
  20. data/lib/coverband/integrations/background.rb +12 -4
  21. data/lib/coverband/integrations/{middleware.rb → background_middleware.rb} +2 -6
  22. data/lib/coverband/integrations/bundler.rb +8 -0
  23. data/lib/coverband/integrations/report_middleware.rb +15 -0
  24. data/lib/coverband/integrations/resque.rb +3 -5
  25. data/lib/coverband/reporters/base.rb +40 -16
  26. data/lib/coverband/reporters/console_report.rb +1 -0
  27. data/lib/coverband/reporters/html_report.rb +31 -27
  28. data/lib/coverband/reporters/web.rb +63 -18
  29. data/lib/coverband/utils/absolute_file_converter.rb +47 -0
  30. data/lib/coverband/utils/file_hasher.rb +16 -0
  31. data/lib/coverband/utils/file_list.rb +16 -5
  32. data/lib/coverband/utils/html_formatter.rb +45 -8
  33. data/lib/coverband/utils/lines_classifier.rb +5 -0
  34. data/lib/coverband/utils/railtie.rb +28 -10
  35. data/lib/coverband/utils/relative_file_converter.rb +41 -0
  36. data/lib/coverband/utils/result.rb +6 -39
  37. data/lib/coverband/utils/results.rb +51 -0
  38. data/lib/coverband/utils/s3_report.rb +2 -3
  39. data/lib/coverband/utils/source_file.rb +34 -8
  40. data/lib/coverband/utils/tasks.rb +10 -10
  41. data/lib/coverband/version.rb +5 -1
  42. data/lib/coverband.rb +49 -27
  43. data/lua/install.sh +15 -0
  44. data/lua/lib/persist-coverage.lua +28 -0
  45. data/lua/test/bootstrap.lua +5 -0
  46. data/lua/test/harness.lua +19 -0
  47. data/lua/test/redis-call.lua +55 -0
  48. data/lua/test/test-persist-coverage.lua +132 -0
  49. data/public/application.css +14 -6
  50. data/public/application.js +27 -0
  51. data/test/benchmarks/benchmark.rake +167 -73
  52. data/test/benchmarks/init_rails.rake +10 -0
  53. data/test/coverband/adapters/base_test.rb +73 -42
  54. data/test/coverband/adapters/file_store_test.rb +48 -37
  55. data/test/coverband/adapters/hash_redis_store_test.rb +195 -0
  56. data/test/coverband/adapters/redis_store_test.rb +102 -59
  57. data/test/coverband/at_exit_test.rb +0 -2
  58. data/test/coverband/collectors/coverage_test.rb +71 -13
  59. data/test/coverband/collectors/delta_test.rb +36 -6
  60. data/test/coverband/collectors/view_tracker_test.rb +113 -0
  61. data/test/coverband/configuration_test.rb +47 -7
  62. data/test/coverband/coverband_test.rb +14 -2
  63. data/test/coverband/integrations/background_middleware_test.rb +44 -0
  64. data/test/coverband/integrations/background_test.rb +11 -3
  65. data/test/coverband/integrations/report_middleware_test.rb +44 -0
  66. data/test/coverband/integrations/resque_worker_test.rb +5 -6
  67. data/test/coverband/integrations/test_resque_job.rb +3 -1
  68. data/test/coverband/reporters/base_test.rb +5 -81
  69. data/test/coverband/reporters/console_test.rb +3 -10
  70. data/test/coverband/reporters/html_test.rb +71 -21
  71. data/test/coverband/reporters/web_test.rb +0 -10
  72. data/test/coverband/utils/absolute_file_converter_test.rb +56 -0
  73. data/test/coverband/utils/file_groups_test.rb +11 -5
  74. data/test/coverband/utils/file_hasher_test.rb +29 -0
  75. data/test/coverband/utils/file_list_test.rb +5 -5
  76. data/test/coverband/utils/html_formatter_test.rb +41 -0
  77. data/test/coverband/utils/relative_file_converter_test.rb +39 -0
  78. data/test/coverband/utils/result_test.rb +6 -47
  79. data/test/coverband/utils/results_test.rb +54 -0
  80. data/test/coverband/utils/s3_report_test.rb +2 -0
  81. data/test/coverband/utils/source_file_test.rb +54 -0
  82. data/test/dog.rb.erb +12 -0
  83. data/test/forked/rails_full_stack_test.rb +106 -0
  84. data/test/forked/rails_rake_full_stack_test.rb +58 -0
  85. data/test/integration/full_stack_test.rb +15 -16
  86. data/test/rails4_dummy/Rakefile +6 -0
  87. data/test/rails4_dummy/config/application.rb +8 -9
  88. data/test/rails4_dummy/config/coverband.rb +3 -11
  89. data/test/rails4_dummy/config/coverband_missing_redis.rb +3 -0
  90. data/test/rails5_dummy/Rakefile +6 -0
  91. data/test/rails5_dummy/config/application.rb +6 -10
  92. data/test/rails5_dummy/config/coverband.rb +7 -3
  93. data/test/rails5_dummy/config/coverband_missing_redis.rb +15 -0
  94. data/test/rails_test_helper.rb +22 -5
  95. data/test/test_helper.rb +52 -11
  96. data/test/unique_files.rb +17 -9
  97. data/views/data.erb +1 -0
  98. data/views/file_list.erb +13 -12
  99. data/views/gem_list.erb +10 -1
  100. data/views/layout.erb +6 -18
  101. data/views/nav.erb +36 -0
  102. data/views/source_file.erb +20 -4
  103. data/views/source_file_loader.erb +1 -1
  104. data/views/view_tracker.erb +52 -0
  105. metadata +71 -12
  106. data/lib/coverband/utils/file_path_helper.rb +0 -57
  107. data/test/coverband/integrations/middleware_test.rb +0 -96
  108. data/test/integration/rails_full_stack_test.rb +0 -95
@@ -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,13 +14,16 @@ 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
- @verbose = Coverband.configuration.verbose
22
- @logger = Coverband.configuration.logger
25
+ @verbose = Coverband.configuration.verbose
26
+ @logger = Coverband.configuration.logger
23
27
  @test_env = Coverband.configuration.test_env
24
28
  @track_gems = Coverband.configuration.track_gems
25
29
  Delta.reset
@@ -27,36 +31,34 @@ module Coverband
27
31
  end
28
32
 
29
33
  def runtime!
30
- @store.type = nil
34
+ @store.type = Coverband::RUNTIME_TYPE
31
35
  end
32
36
 
33
37
  def eager_loading!
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
54
- if @verbose
55
- @logger&.error 'coverage failed to store'
56
- @logger&.error "error: #{err.inspect} #{err.message}"
57
- @logger&.error err.backtrace
58
- end
59
- raise err if @test_env
57
+ rescue StandardError => e
58
+ @logger&.error 'coverage failed to store'
59
+ @logger&.error "Coverband Error: #{e.inspect} #{e.message}"
60
+ e.backtrace.each { |line| @logger&.error line } if @verbose
61
+ raise e if @test_env
60
62
  end
61
63
 
62
64
  protected
@@ -69,7 +71,7 @@ module Coverband
69
71
  ###
70
72
  def track_file?(file)
71
73
  @ignore_patterns.none? do |pattern|
72
- file.include?(pattern)
74
+ file.match(pattern)
73
75
  end && (file.start_with?(@project_directory) ||
74
76
  (@track_gems &&
75
77
  Coverband.configuration.gem_paths.any? { |path| file.start_with?(path) }))
@@ -78,30 +80,22 @@ module Coverband
78
80
  private
79
81
 
80
82
  def filtered_files(new_results)
81
- new_results.each_with_object({}) do |(file, line_counts), file_line_usage|
82
- file_line_usage[file] = line_counts if track_file?(file)
83
- end.select { |_file_name, coverage| coverage.any? { |value| value&.nonzero? } }
84
- end
85
-
86
- def ready_to_report?
87
- (rand * 100.0) >= (100.0 - @reporting_frequency)
83
+ new_results.select! { |file, coverage| track_file?(file) && coverage.any? { |value| value&.nonzero? } }
84
+ new_results
88
85
  end
89
86
 
90
87
  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
88
+ @semaphore = Mutex.new
89
+ raise NotImplementedError, 'Coverage needs Ruby > 2.3.0' if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3.0')
90
+
94
91
  require 'coverage'
95
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.5.0')
92
+ if Coverage.ruby_version_greater_than_or_equal_to?('2.6.0')
93
+ ::Coverage.start(oneshot_lines: Coverband.configuration.use_oneshot_lines_coverage) unless ::Coverage.running?
94
+ elsif Coverage.ruby_version_greater_than_or_equal_to?('2.5.0')
96
95
  ::Coverage.start unless ::Coverage.running?
97
96
  else
98
97
  ::Coverage.start
99
98
  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
99
  reset_instance
106
100
  end
107
101
  end
@@ -1,9 +1,11 @@
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
+ @@stubs = {}
8
+
7
9
  attr_reader :current_coverage
8
10
 
9
11
  def initialize(current_coverage)
@@ -12,54 +14,58 @@ 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
- @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 ||= {}
26
+ coverage_results = process_coverage.results
27
+ new(coverage_results).results
32
28
  end
33
29
 
34
30
  def results
35
- new_results = generate
36
- @@previous_coverage = current_coverage
37
- 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
38
38
  end
39
39
 
40
40
  def self.reset
41
- @@previous_coverage = nil
41
+ @@previous_coverage = {}
42
42
  end
43
43
 
44
44
  private
45
45
 
46
46
  def generate
47
47
  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
48
+ new_results[file] = if @@previous_coverage && @@previous_coverage[file]
49
+ array_diff(line_counts, @@previous_coverage[file])
50
+ else
51
+ line_counts
52
+ end
53
53
  end
54
54
  end
55
55
 
56
56
  def array_diff(latest, original)
57
57
  latest.map.with_index do |v, i|
58
- if (v && original[i])
59
- [0, v - original[i]].max
60
- else
61
- nil
58
+ [0, v - original[i]].max if v && original[i]
59
+ end
60
+ end
61
+
62
+ def transform_oneshot_lines_results(results)
63
+ results.each_with_object({}) do |(file, coverage), new_results|
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|
66
+ line_counts[line_number - 1] = 1
62
67
  end
68
+ new_results[file] = transformed_line_counts
63
69
  end
64
70
  end
65
71
  end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Coverband
6
+ module Collectors
7
+ ###
8
+ # This class tracks view file usage via ActiveSupport::Notifications
9
+ #
10
+ # This is a port of Flatfoot, a project I open sourced years ago,
11
+ # but am now rolling into Coverband
12
+ # https://github.com/livingsocial/flatfoot
13
+ #
14
+ # TODO: test and ensure slim, haml, and other support
15
+ ###
16
+ class ViewTracker
17
+ DEFAULT_TARGET = Dir.glob('app/views/**/*.html.erb').reject { |file| file.match(/(_mailer)/) }
18
+ attr_accessor :target, :logged_views, :views_to_record
19
+ attr_reader :logger, :roots, :store, :ignore_patterns
20
+
21
+ def initialize(options = {})
22
+ raise NotImplementedError, 'View Tracker requires Rails 4 or greater' unless self.class.supported_version?
23
+
24
+ @project_directory = File.expand_path(Coverband.configuration.root)
25
+ @ignore_patterns = Coverband.configuration.ignore
26
+ @store = options.fetch(:store) { Coverband.configuration.store }
27
+ @logger = options.fetch(:logger) { Coverband.configuration.logger }
28
+ @target = options.fetch(:target) { DEFAULT_TARGET }
29
+
30
+ @roots = options.fetch(:roots) { Coverband.configuration.all_root_patterns }
31
+ @roots = @roots.split(',') if @roots.is_a?(String)
32
+ @one_time_timestamp = false
33
+
34
+ @logged_views = []
35
+ @views_to_record = []
36
+ end
37
+
38
+ ###
39
+ # This method is called on every render call, so we try to reduce method calls
40
+ # and ensure high performance
41
+ ###
42
+ def track_views(_name, _start, _finish, _id, payload)
43
+ if (file = payload[:identifier])
44
+ if newly_seen_file?(file)
45
+ logged_views << file
46
+ views_to_record << file if track_file?(file)
47
+ end
48
+ end
49
+
50
+ ###
51
+ # Annoyingly while you get full path for templates
52
+ # notifications only pass part of the path for layouts dropping any format info
53
+ # such as .html.erb or .js.erb
54
+ # http://edgeguides.rubyonrails.org/active_support_instrumentation.html#render_partial-action_view
55
+ ###
56
+ return unless (layout_file = payload[:layout])
57
+ return unless newly_seen_file?(layout_file)
58
+
59
+ logged_views << layout_file
60
+ views_to_record << layout_file if track_file?(layout_file, layout: true)
61
+ end
62
+
63
+ def used_views
64
+ views = redis_store.hgetall(tracker_key)
65
+ normalized_views = {}
66
+ views.each_pair do |view, time|
67
+ roots.each do |root|
68
+ view = view.gsub(/#{root}/, '')
69
+ end
70
+ normalized_views[view] = time
71
+ end
72
+ normalized_views
73
+ end
74
+
75
+ def unused_views
76
+ recently_used_views = used_views.keys
77
+ all_views = target.reject { |view| recently_used_views.include?(view) }
78
+ # since layouts don't include format we count them used if they match with ANY formats
79
+ all_views.reject { |view| view.match(/\/layouts\//) && recently_used_views.any? { |used_view| view.include?(used_view) } }
80
+ end
81
+
82
+ def as_json
83
+ {
84
+ unused_views: unused_views,
85
+ used_views: used_views
86
+ }.to_json
87
+ end
88
+
89
+ def tracking_since
90
+ if (tracking_time = redis_store.get(tracker_time_key))
91
+ Time.at(tracking_time.to_i).iso8601
92
+ else
93
+ 'N/A'
94
+ end
95
+ end
96
+
97
+ def reset_recordings
98
+ redis_store.del(tracker_key)
99
+ redis_store.del(tracker_time_key)
100
+ end
101
+
102
+ def clear_file!(filename)
103
+ return unless filename
104
+
105
+ filename = "#{@project_directory}/#{filename}"
106
+ redis_store.hdel(tracker_key, filename)
107
+ logged_views.delete(filename)
108
+ end
109
+
110
+ def report_views_tracked
111
+ redis_store.set(tracker_time_key, Time.now.to_i) unless @one_time_timestamp || redis_store.exists(tracker_time_key)
112
+ @one_time_timestamp = true
113
+ reported_time = Time.now.to_i
114
+ views_to_record.each do |file|
115
+ redis_store.hset(tracker_key, file, reported_time)
116
+ end
117
+ self.views_to_record = []
118
+ rescue StandardError => e
119
+ # we don't want to raise errors if Coverband can't reach redis.
120
+ # This is a nice to have not a bring the system down
121
+ logger&.error "Coverband: view_tracker failed to store, error #{e.class.name}"
122
+ end
123
+
124
+ def self.supported_version?
125
+ defined?(Rails) && defined?(Rails::VERSION) && Rails::VERSION::STRING.split('.').first.to_i >= 4
126
+ end
127
+
128
+ protected
129
+
130
+ def newly_seen_file?(file)
131
+ return false if logged_views.include?(file)
132
+
133
+ true
134
+ end
135
+
136
+ def track_file?(file, options = {})
137
+ @ignore_patterns.none? do |pattern|
138
+ file.include?(pattern)
139
+ end && (file.start_with?(@project_directory) || options[:layout])
140
+ end
141
+
142
+ private
143
+
144
+ def redis_store
145
+ store.raw_store
146
+ end
147
+
148
+ def tracker_key
149
+ 'render_tracker_2'
150
+ end
151
+
152
+ def tracker_time_key
153
+ 'render_tracker_time'
154
+ end
155
+ end
156
+ end
157
+ end
@@ -3,15 +3,48 @@
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,
11
+ :simulate_oneshot_lines_coverage, :track_views, :view_tracker,
12
+ :reporting_wiggle
12
13
 
13
- attr_writer :logger, :s3_region, :s3_bucket, :s3_access_key_id, :s3_secret_access_key
14
- attr_reader :track_gems
14
+ attr_writer :logger, :s3_region, :s3_bucket, :s3_access_key_id,
15
+ :s3_secret_access_key, :password
16
+ attr_reader :track_gems, :ignore, :use_oneshot_lines_coverage
17
+
18
+ #####
19
+ # TODO: This is is brittle and not a great solution to avoid deploy time
20
+ # actions polluting the 'runtime' metrics
21
+ #
22
+ # * should we skip /bin/rails webpacker:compile ?
23
+ # * Perhaps detect heroku deployment ENV var opposed to tasks?
24
+ #####
25
+ IGNORE_TASKS = ['coverband:clear',
26
+ 'coverband:coverage',
27
+ 'coverband:coverage_server',
28
+ 'coverband:migrate',
29
+ 'assets:precompile',
30
+ 'db:version',
31
+ 'db:create',
32
+ 'db:drop',
33
+ 'db:seed',
34
+ 'db:setup',
35
+ 'db:test:prepare',
36
+ 'db:structure:dump',
37
+ 'db:structure:load',
38
+ 'db:version']
39
+
40
+ # Heroku when building assets runs code from a dynamic directory
41
+ # /tmp was added to avoid coverage from /tmp/build directories during
42
+ # heroku asset compilation
43
+ IGNORE_DEFAULTS = %w[vendor/ .erb$ .slim$ /tmp internal:prelude schema.rb]
44
+
45
+ # Add in missing files which were never loaded
46
+ # we need to know what all paths to check for unloaded files
47
+ TRACKED_DEFAULT_PATHS = %w[app lib config]
15
48
 
16
49
  def initialize
17
50
  reset
@@ -20,12 +53,9 @@ module Coverband
20
53
  def reset
21
54
  @root = Dir.pwd
22
55
  @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]
56
+ @ignore = IGNORE_DEFAULTS.dup
57
+ @seach_paths = TRACKED_DEFAULT_PATHS.dup
27
58
  @additional_files = []
28
- @reporting_frequency = 0.0
29
59
  @verbose = false
30
60
  @reporter = 'scov'
31
61
  @logger = nil
@@ -36,8 +66,16 @@ module Coverband
36
66
  @web_enable_clear = false
37
67
  @track_gems = false
38
68
  @gem_details = false
69
+ @track_views = false
39
70
  @groups = {}
40
71
  @web_debug = false
72
+ @report_on_exit = true
73
+ @use_oneshot_lines_coverage = ENV['ONESHOT'] || false
74
+ @simulate_oneshot_lines_coverage = ENV['SIMULATE_ONESHOT'] || false
75
+ @current_root = nil
76
+ @all_root_paths = nil
77
+ @all_root_patterns = nil
78
+ @password = nil
41
79
 
42
80
  # TODO: should we push these to adapter configs
43
81
  @s3_region = nil
@@ -45,17 +83,22 @@ module Coverband
45
83
  @s3_access_key_id = nil
46
84
  @s3_secret_access_key = nil
47
85
  @redis_namespace = nil
48
- @redis_ttl = nil
86
+ @redis_ttl = 2_592_000 # in seconds. Default is 30 days.
87
+ @reporting_wiggle = nil
49
88
  end
50
89
 
51
90
  def logger
52
- @logger ||= if defined?(Rails.logger)
91
+ @logger ||= if defined?(Rails.logger) && Rails.logger
53
92
  Rails.logger
54
93
  else
55
94
  Logger.new(STDOUT)
56
95
  end
57
96
  end
58
97
 
98
+ def password
99
+ @password || ENV['COVERBAND_PASSWORD']
100
+ end
101
+
59
102
  def s3_bucket
60
103
  @s3_bucket || ENV['AWS_BUCKET']
61
104
  end
@@ -77,29 +120,52 @@ module Coverband
77
120
  end
78
121
 
79
122
  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
123
+ raise 'Pass in an instance of Coverband::Adapters' unless store.is_a?(Coverband::Adapters::Base)
124
+
125
+ # Default to 5 minutes if using the hash redis store
126
+ # This is a safer default for the high server volumes that need the hash store
127
+ # it should avoid overloading the redis with lots of load
128
+ @background_reporting_sleep_seconds = 300 if store.is_a?(Coverband::Adapters::HashRedisStore)
129
+
130
+ @store = store
131
+ end
132
+
133
+ ###
134
+ # Search Paths
135
+ ###
136
+ def tracked_search_paths
137
+ "#{Coverband.configuration.current_root}/{#{@seach_paths.join(',')}}/**/*.{rb}"
138
+ end
139
+
140
+ ###
141
+ # Don't allow the to override defaults
142
+ ###
143
+ def search_paths=(path_array)
144
+ @seach_paths = (@seach_paths + path_array).uniq
145
+ end
146
+
147
+ ###
148
+ # Don't allow the ignore to override things like gem tracking
149
+ ###
150
+ def ignore=(ignored_array)
151
+ @ignore = (@ignore + ignored_array).uniq
85
152
  end
86
153
 
87
154
  def track_gems=(value)
88
155
  @track_gems = value
89
156
  return unless @track_gems
157
+
90
158
  # by default we ignore vendor where many deployments put gems
91
159
  # we will remove this default if track_gems is set
92
- @ignore.delete('vendor')
160
+ @ignore.delete('vendor/')
93
161
  # while we want to allow vendored gems we don't want to track vendored ruby STDLIB
94
- @ignore << 'vendor/ruby-*'
162
+ @ignore << 'vendor/ruby-*' unless @ignore.include?('vendor/ruby-*')
95
163
  add_group('App', root)
96
164
  # TODO: rework support for multiple gem paths
97
- # currently this supports GEM_HOME (which should be first path)
98
- # but various gem managers setup multiple gem paths
99
- # gem_paths.each_with_index do |path, index|
100
- # add_group("gems_#{index}", path)
101
- # end
102
- add_group('Gems', gem_paths.first)
165
+ # this works but seems hacky and error prone
166
+ # basically since it is converted to a regex we join all the paths
167
+ # with a regex 'OR' using '|'
168
+ add_group('Gems', gem_paths.join('|'))
103
169
  end
104
170
 
105
171
  #
@@ -125,17 +191,23 @@ module Coverband
125
191
  end
126
192
 
127
193
  def current_root
128
- File.expand_path(Coverband.configuration.root)
194
+ @current_root ||= File.expand_path(Coverband.configuration.root).freeze
129
195
  end
130
196
 
131
197
  def all_root_paths
132
- roots = Coverband.configuration.root_paths.dup
133
- roots += Coverband.configuration.gem_paths.dup if Coverband.configuration.track_gems
134
- roots << "#{Coverband.configuration.current_root}/"
135
- roots
198
+ return @all_root_paths if @all_root_paths
199
+
200
+ @all_root_paths = Coverband.configuration.root_paths.dup
201
+ @all_root_paths += Coverband.configuration.gem_paths.dup if Coverband.configuration.track_gems
202
+ @all_root_paths << "#{Coverband.configuration.current_root}/"
203
+ @all_root_paths
204
+ end
205
+
206
+ def all_root_patterns
207
+ @all_root_patterns ||= all_root_paths.map { |path| /^#{path}/ }.freeze
136
208
  end
137
209
 
138
- SKIPPED_SETTINGS = %w(@s3_secret_access_key @store)
210
+ SKIPPED_SETTINGS = %w[@s3_secret_access_key @store]
139
211
  def to_h
140
212
  instance_variables
141
213
  .each_with_object('gem_paths': gem_paths) do |var, hash|
@@ -143,6 +215,16 @@ module Coverband
143
215
  end
144
216
  end
145
217
 
218
+ def use_oneshot_lines_coverage=(value)
219
+ raise(Exception, 'One shot line coverage is only available in ruby >= 2.6') unless one_shot_coverage_implemented_in_ruby_version? || !value
220
+
221
+ @use_oneshot_lines_coverage = value
222
+ end
223
+
224
+ def one_shot_coverage_implemented_in_ruby_version?
225
+ Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.6.0')
226
+ end
227
+
146
228
  private
147
229
 
148
230
  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,19 @@ 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') if Coverband.configuration.verbose
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
+ Coverband.configuration.view_tracker&.report_views_tracked
36
+ if Coverband.configuration.reporting_wiggle
37
+ sleep_seconds = Coverband.configuration.background_reporting_sleep_seconds + rand(Coverband.configuration.reporting_wiggle.to_i)
38
+ end
39
+ if Coverband.configuration.verbose
40
+ logger.debug("Coverband: background reporting coverage (#{Coverband.configuration.store.type}). Sleeping #{sleep_seconds}s")
41
+ end
34
42
  sleep(sleep_seconds)
35
43
  end
36
44
  end