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.
- checksums.yaml +4 -4
- data/.rubocop.yml +29 -10
- data/.travis.yml +16 -8
- data/CODE_OF_CONDUCT.md +76 -0
- data/Gemfile +3 -1
- data/Gemfile.rails4 +1 -0
- data/README.md +108 -59
- data/Rakefile +19 -3
- data/changes.md +85 -10
- data/coverband.gemspec +3 -5
- data/lib/coverband/adapters/base.rb +64 -45
- data/lib/coverband/adapters/file_store.rb +15 -9
- data/lib/coverband/adapters/hash_redis_store.rb +190 -0
- data/lib/coverband/adapters/redis_store.rb +35 -11
- data/lib/coverband/at_exit.rb +2 -11
- data/lib/coverband/collectors/coverage.rb +35 -41
- data/lib/coverband/collectors/delta.rb +34 -28
- data/lib/coverband/collectors/view_tracker.rb +157 -0
- data/lib/coverband/configuration.rb +115 -33
- data/lib/coverband/integrations/background.rb +12 -4
- data/lib/coverband/integrations/{middleware.rb → background_middleware.rb} +2 -6
- data/lib/coverband/integrations/bundler.rb +8 -0
- data/lib/coverband/integrations/report_middleware.rb +15 -0
- data/lib/coverband/integrations/resque.rb +3 -5
- data/lib/coverband/reporters/base.rb +40 -16
- data/lib/coverband/reporters/console_report.rb +1 -0
- data/lib/coverband/reporters/html_report.rb +31 -27
- data/lib/coverband/reporters/web.rb +63 -18
- data/lib/coverband/utils/absolute_file_converter.rb +47 -0
- data/lib/coverband/utils/file_hasher.rb +16 -0
- data/lib/coverband/utils/file_list.rb +16 -5
- data/lib/coverband/utils/html_formatter.rb +45 -8
- data/lib/coverband/utils/lines_classifier.rb +5 -0
- data/lib/coverband/utils/railtie.rb +28 -10
- data/lib/coverband/utils/relative_file_converter.rb +41 -0
- data/lib/coverband/utils/result.rb +6 -39
- data/lib/coverband/utils/results.rb +51 -0
- data/lib/coverband/utils/s3_report.rb +2 -3
- data/lib/coverband/utils/source_file.rb +34 -8
- data/lib/coverband/utils/tasks.rb +10 -10
- data/lib/coverband/version.rb +5 -1
- data/lib/coverband.rb +49 -27
- data/lua/install.sh +15 -0
- data/lua/lib/persist-coverage.lua +28 -0
- data/lua/test/bootstrap.lua +5 -0
- data/lua/test/harness.lua +19 -0
- data/lua/test/redis-call.lua +55 -0
- data/lua/test/test-persist-coverage.lua +132 -0
- data/public/application.css +14 -6
- data/public/application.js +27 -0
- data/test/benchmarks/benchmark.rake +167 -73
- data/test/benchmarks/init_rails.rake +10 -0
- data/test/coverband/adapters/base_test.rb +73 -42
- data/test/coverband/adapters/file_store_test.rb +48 -37
- data/test/coverband/adapters/hash_redis_store_test.rb +195 -0
- data/test/coverband/adapters/redis_store_test.rb +102 -59
- data/test/coverband/at_exit_test.rb +0 -2
- data/test/coverband/collectors/coverage_test.rb +71 -13
- data/test/coverband/collectors/delta_test.rb +36 -6
- data/test/coverband/collectors/view_tracker_test.rb +113 -0
- data/test/coverband/configuration_test.rb +47 -7
- data/test/coverband/coverband_test.rb +14 -2
- data/test/coverband/integrations/background_middleware_test.rb +44 -0
- data/test/coverband/integrations/background_test.rb +11 -3
- data/test/coverband/integrations/report_middleware_test.rb +44 -0
- data/test/coverband/integrations/resque_worker_test.rb +5 -6
- data/test/coverband/integrations/test_resque_job.rb +3 -1
- data/test/coverband/reporters/base_test.rb +5 -81
- data/test/coverband/reporters/console_test.rb +3 -10
- data/test/coverband/reporters/html_test.rb +71 -21
- data/test/coverband/reporters/web_test.rb +0 -10
- data/test/coverband/utils/absolute_file_converter_test.rb +56 -0
- data/test/coverband/utils/file_groups_test.rb +11 -5
- data/test/coverband/utils/file_hasher_test.rb +29 -0
- data/test/coverband/utils/file_list_test.rb +5 -5
- data/test/coverband/utils/html_formatter_test.rb +41 -0
- data/test/coverband/utils/relative_file_converter_test.rb +39 -0
- data/test/coverband/utils/result_test.rb +6 -47
- data/test/coverband/utils/results_test.rb +54 -0
- data/test/coverband/utils/s3_report_test.rb +2 -0
- data/test/coverband/utils/source_file_test.rb +54 -0
- data/test/dog.rb.erb +12 -0
- data/test/forked/rails_full_stack_test.rb +106 -0
- data/test/forked/rails_rake_full_stack_test.rb +58 -0
- data/test/integration/full_stack_test.rb +15 -16
- data/test/rails4_dummy/Rakefile +6 -0
- data/test/rails4_dummy/config/application.rb +8 -9
- data/test/rails4_dummy/config/coverband.rb +3 -11
- data/test/rails4_dummy/config/coverband_missing_redis.rb +3 -0
- data/test/rails5_dummy/Rakefile +6 -0
- data/test/rails5_dummy/config/application.rb +6 -10
- data/test/rails5_dummy/config/coverband.rb +7 -3
- data/test/rails5_dummy/config/coverband_missing_redis.rb +15 -0
- data/test/rails_test_helper.rb +22 -5
- data/test/test_helper.rb +52 -11
- data/test/unique_files.rb +17 -9
- data/views/data.erb +1 -0
- data/views/file_list.erb +13 -12
- data/views/gem_list.erb +10 -1
- data/views/layout.erb +6 -18
- data/views/nav.erb +36 -0
- data/views/source_file.erb +20 -4
- data/views/source_file_loader.erb +1 -1
- data/views/view_tracker.erb +52 -0
- metadata +71 -12
- data/lib/coverband/utils/file_path_helper.rb +0 -57
- data/test/coverband/integrations/middleware_test.rb +0 -96
- data/test/integration/rails_full_stack_test.rb +0 -95
data/lib/coverband/at_exit.rb
CHANGED
|
@@ -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
|
|
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
|
|
19
|
-
@reporting_frequency = Coverband.configuration.reporting_frequency
|
|
23
|
+
@ignore_patterns = Coverband.configuration.ignore
|
|
20
24
|
@store = Coverband.configuration.store
|
|
21
|
-
@verbose
|
|
22
|
-
@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 =
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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 =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
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.
|
|
82
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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
|
-
|
|
6
|
-
@@
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 =
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
:
|
|
7
|
-
:reporter, :
|
|
8
|
-
:
|
|
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,
|
|
14
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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 =
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
33
|
-
|
|
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
|