coverband 4.2.0 → 4.2.2

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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +29 -9
  3. data/.travis.yml +10 -8
  4. data/CODE_OF_CONDUCT.md +76 -0
  5. data/Gemfile +3 -1
  6. data/Gemfile.rails4 +1 -0
  7. data/README.md +72 -43
  8. data/Rakefile +14 -3
  9. data/changes.md +66 -11
  10. data/coverband.gemspec +2 -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 +182 -0
  14. data/lib/coverband/adapters/redis_store.rb +33 -11
  15. data/lib/coverband/at_exit.rb +2 -11
  16. data/lib/coverband/collectors/coverage.rb +34 -40
  17. data/lib/coverband/collectors/delta.rb +34 -28
  18. data/lib/coverband/collectors/view_tracker.rb +124 -0
  19. data/lib/coverband/configuration.rb +96 -32
  20. data/lib/coverband/integrations/background.rb +9 -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 +21 -27
  28. data/lib/coverband/reporters/web.rb +19 -21
  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 +37 -8
  33. data/lib/coverband/utils/lines_classifier.rb +5 -0
  34. data/lib/coverband/utils/railtie.rb +20 -9
  35. data/lib/coverband/utils/relative_file_converter.rb +41 -0
  36. data/lib/coverband/utils/result.rb +1 -37
  37. data/lib/coverband/utils/results.rb +51 -0
  38. data/lib/coverband/utils/s3_report.rb +2 -2
  39. data/lib/coverband/utils/source_file.rb +31 -8
  40. data/lib/coverband/utils/tasks.rb +10 -10
  41. data/lib/coverband/version.rb +5 -1
  42. data/lib/coverband.rb +39 -22
  43. data/public/application.css +6 -6
  44. data/public/application.js +27 -0
  45. data/test/benchmarks/benchmark.rake +167 -73
  46. data/test/benchmarks/init_rails.rake +10 -0
  47. data/test/coverband/adapters/base_test.rb +73 -42
  48. data/test/coverband/adapters/file_store_test.rb +48 -37
  49. data/test/coverband/adapters/hash_redis_store_test.rb +190 -0
  50. data/test/coverband/adapters/redis_store_test.rb +92 -59
  51. data/test/coverband/at_exit_test.rb +0 -2
  52. data/test/coverband/collectors/coverage_test.rb +62 -10
  53. data/test/coverband/collectors/delta_test.rb +36 -6
  54. data/test/coverband/collectors/view_tracker_test.rb +77 -0
  55. data/test/coverband/configuration_test.rb +47 -7
  56. data/test/coverband/coverband_test.rb +14 -2
  57. data/test/coverband/integrations/background_middleware_test.rb +44 -0
  58. data/test/coverband/integrations/background_test.rb +1 -3
  59. data/test/coverband/integrations/report_middleware_test.rb +44 -0
  60. data/test/coverband/integrations/resque_worker_test.rb +6 -6
  61. data/test/coverband/integrations/test_resque_job.rb +3 -1
  62. data/test/coverband/reporters/base_test.rb +2 -79
  63. data/test/coverband/reporters/console_test.rb +2 -6
  64. data/test/coverband/reporters/html_test.rb +58 -19
  65. data/test/coverband/reporters/web_test.rb +0 -10
  66. data/test/coverband/utils/absolute_file_converter_test.rb +56 -0
  67. data/test/coverband/utils/file_groups_test.rb +11 -5
  68. data/test/coverband/utils/file_hasher_test.rb +29 -0
  69. data/test/coverband/utils/file_list_test.rb +5 -5
  70. data/test/coverband/utils/html_formatter_test.rb +43 -0
  71. data/test/coverband/utils/relative_file_converter_test.rb +39 -0
  72. data/test/coverband/utils/result_test.rb +6 -47
  73. data/test/coverband/utils/results_test.rb +54 -0
  74. data/test/coverband/utils/s3_report_test.rb +2 -0
  75. data/test/coverband/utils/source_file_test.rb +50 -0
  76. data/test/dog.rb.erb +12 -0
  77. data/test/forked/rails_full_stack_test.rb +106 -0
  78. data/test/forked/rails_rake_full_stack_test.rb +48 -0
  79. data/test/integration/full_stack_test.rb +17 -16
  80. data/test/rails4_dummy/Rakefile +6 -0
  81. data/test/rails4_dummy/config/application.rb +8 -9
  82. data/test/rails4_dummy/config/coverband.rb +5 -3
  83. data/test/rails5_dummy/Rakefile +6 -0
  84. data/test/rails5_dummy/config/application.rb +6 -10
  85. data/test/rails5_dummy/config/coverband.rb +4 -2
  86. data/test/rails_test_helper.rb +22 -5
  87. data/test/test_helper.rb +45 -8
  88. data/test/unique_files.rb +17 -9
  89. data/views/file_list.erb +2 -2
  90. data/views/gem_list.erb +10 -1
  91. data/views/layout.erb +6 -18
  92. data/views/nav.erb +35 -0
  93. data/views/source_file.erb +13 -4
  94. data/views/source_file_loader.erb +1 -1
  95. data/views/view_tracker.erb +37 -0
  96. metadata +59 -25
  97. data/lib/coverband/utils/file_path_helper.rb +0 -57
  98. data/test/coverband/integrations/middleware_test.rb +0 -96
  99. data/test/integration/rails_full_stack_test.rb +0 -95
@@ -3,45 +3,48 @@
3
3
  module Coverband
4
4
  module Adapters
5
5
  class Base
6
- include Coverband::Utils::FilePathHelper
6
+ DATA_KEY = 'data'
7
+ FIRST_UPDATED_KEY = 'first_updated_at'
8
+ LAST_UPDATED_KEY = 'last_updated_at'
9
+ FILE_HASH = 'file_hash'
10
+ ABSTRACT_KEY = 'abstract'
11
+
7
12
  attr_accessor :type
8
13
 
9
14
  def initialize
10
- @file_hash_cache = {}
15
+ @type = Coverband::RUNTIME_TYPE
11
16
  end
12
17
 
13
18
  def clear!
14
- raise 'abstract'
19
+ raise ABSTRACT_KEY
15
20
  end
16
21
 
17
- def clear_file!(_file)
18
- raise 'abstract'
22
+ def clear_file!(_file = nil)
23
+ raise ABSTRACT_KEY
19
24
  end
20
25
 
21
26
  def migrate!
22
- raise 'abstract'
27
+ raise ABSTRACT_KEY
23
28
  end
24
29
 
25
30
  def size
26
- raise 'abstract'
31
+ raise ABSTRACT_KEY
27
32
  end
28
33
 
29
- def size_in_mib
30
- format('%.2f', (size.to_f / 2**20))
34
+ def save_coverage
35
+ raise ABSTRACT_KEY
36
+ end
37
+
38
+ def coverage(_local_type = nil)
39
+ raise ABSTRACT_KEY
31
40
  end
32
41
 
33
- # Note: This could lead to slight race on redis
34
- # where multiple processes pull the old coverage and add to it then push
35
- # the Coverband 2 had the same issue,
36
- # and the tradeoff has always been acceptable
37
- def save_report(report)
38
- data = report.dup
39
- data = merge_reports(data, get_report)
40
- save_coverage(data)
42
+ def size_in_mib
43
+ format('%.2f', (size.to_f / 2**20))
41
44
  end
42
45
 
43
- def coverage
44
- get_report
46
+ def save_report(_report)
47
+ raise 'abstract'
45
48
  end
46
49
 
47
50
  def get_coverage_report
@@ -53,60 +56,69 @@ module Coverband
53
56
  coverage.keys || []
54
57
  end
55
58
 
56
- # TODO: deprecate / remove?
57
- def covered_lines_for_file(file)
58
- Array(coverage.dig(file, 'data'))
59
+ def raw_store
60
+ raise ABSTRACT_KEY
59
61
  end
60
62
 
61
63
  protected
62
64
 
63
65
  def split_coverage(types)
64
66
  types.reduce({}) do |data, type|
65
- data.update(type => get_report(type))
67
+ if type == Coverband::RUNTIME_TYPE && Coverband.configuration.simulate_oneshot_lines_coverage
68
+ data.update(type => simulated_runtime_coverage)
69
+ else
70
+ data.update(type => coverage(type))
71
+ end
66
72
  end
67
73
  end
68
74
 
69
- def merged_coverage(types)
70
- types.reduce({}) do |data, type|
71
- merge_reports(data, get_report(type), skip_expansion: true)
75
+ def simulated_runtime_coverage
76
+ runtime_data = coverage(Coverband::RUNTIME_TYPE)
77
+ eager_data = coverage(Coverband::EAGER_TYPE)
78
+ eager_data.values do |vals|
79
+ vals['data'].map! { |line_coverage| line_coverage ? (0 - line_coverage) : line_coverage }
72
80
  end
81
+ merge_reports(runtime_data, eager_data, skip_expansion: true)
73
82
  end
74
83
 
75
- def save_coverage
76
- raise 'abstract'
77
- end
78
-
79
- def get_report
80
- raise 'abstract'
84
+ def merged_coverage(types)
85
+ types.reduce({}) do |data, type|
86
+ merge_reports(data, coverage(type), skip_expansion: true)
87
+ end
81
88
  end
82
89
 
83
90
  def file_hash(file)
84
- @file_hash_cache[file] ||= Digest::MD5.file(file).hexdigest
91
+ Coverband::Utils::FileHasher.hash(file)
85
92
  end
86
93
 
94
+ # TODO: modify to extend report inline?
87
95
  def expand_report(report)
88
96
  expanded = {}
89
97
  report_time = Time.now.to_i
90
- updated_time = self.type == Coverband::EAGER_TYPE ? nil : report_time
98
+ updated_time = type == Coverband::EAGER_TYPE ? nil : report_time
91
99
  report.each_pair do |key, line_data|
92
100
  extended_data = {
93
- 'first_updated_at' => report_time,
94
- 'last_updated_at' => updated_time,
95
- 'file_hash' => file_hash(key),
96
- 'data' => line_data
101
+ FIRST_UPDATED_KEY => report_time,
102
+ LAST_UPDATED_KEY => updated_time,
103
+ FILE_HASH => file_hash(key),
104
+ DATA_KEY => line_data
97
105
  }
98
- expanded[full_path_to_relative(key)] = extended_data
106
+ expanded[Utils::RelativeFileConverter.convert(key)] = extended_data
99
107
  end
100
108
  expanded
101
109
  end
102
110
 
103
111
  def merge_reports(new_report, old_report, options = {})
112
+ # transparently update from RUNTIME_TYPE = nil to RUNTIME_TYPE = :runtime
113
+ # transparent update for format coveband_3_2
114
+ old_report = coverage(nil, override_type: nil) if old_report.nil? && type == Coverband::RUNTIME_TYPE
115
+
104
116
  new_report = expand_report(new_report) unless options[:skip_expansion]
105
117
  keys = (new_report.keys + old_report.keys).uniq
106
118
  keys.each do |file|
107
119
  new_report[file] = if new_report[file] &&
108
120
  old_report[file] &&
109
- new_report[file]['file_hash'] == old_report[file]['file_hash']
121
+ new_report[file][FILE_HASH] == old_report[file][FILE_HASH]
110
122
  merge_expanded_data(new_report[file], old_report[file])
111
123
  elsif new_report[file]
112
124
  new_report[file]
@@ -119,15 +131,22 @@ module Coverband
119
131
 
120
132
  def merge_expanded_data(new_expanded, old_expanded)
121
133
  {
122
- 'first_updated_at' => old_expanded['first_updated_at'],
123
- 'last_updated_at' => new_expanded['last_updated_at'],
124
- 'file_hash' => new_expanded['file_hash'],
125
- 'data' => array_add(new_expanded['data'], old_expanded['data'])
134
+ FIRST_UPDATED_KEY => old_expanded[FIRST_UPDATED_KEY],
135
+ LAST_UPDATED_KEY => new_expanded[LAST_UPDATED_KEY],
136
+ FILE_HASH => new_expanded[FILE_HASH],
137
+ DATA_KEY => array_add(new_expanded[DATA_KEY], old_expanded[DATA_KEY])
126
138
  }
127
139
  end
128
140
 
141
+ # TODO: This should only be 2 cases get our dup / not dups aligned
129
142
  def array_add(latest, original)
130
- latest.map.with_index { |v, i| (v && original[i]) ? v + original[i] : nil }
143
+ if Coverband.configuration.use_oneshot_lines_coverage
144
+ latest.map!.with_index { |v, i| (v + original[i] >= 1 ? 1 : 0) if v && original[i] }
145
+ elsif Coverband.configuration.simulate_oneshot_lines_coverage
146
+ latest.map.with_index { |v, i| (v + original[i] >= 1 ? 1 : 0) if v && original[i] }
147
+ else
148
+ latest.map.with_index { |v, i| (v && original[i]) ? v + original[i] : nil }
149
+ end
131
150
  end
132
151
  end
133
152
  end
@@ -28,21 +28,27 @@ module Coverband
28
28
  raise NotImplementedError, "FileStore doesn't support migrations"
29
29
  end
30
30
 
31
- private
32
-
33
- attr_accessor :path
34
-
35
- def save_coverage(report)
36
- File.open(path, 'w') { |f| f.write(report.to_json) }
37
- end
38
-
39
- def get_report
31
+ def coverage(_local_type = nil)
40
32
  if File.exist?(path)
41
33
  JSON.parse(File.read(path))
42
34
  else
43
35
  {}
44
36
  end
45
37
  end
38
+
39
+ def save_report(report)
40
+ data = report.dup
41
+ data = merge_reports(data, coverage)
42
+ File.open(path, 'w') { |f| f.write(data.to_json) }
43
+ end
44
+
45
+ def raw_store
46
+ raise NotImplementedError, "FileStore doesn't support raw_store"
47
+ end
48
+
49
+ private
50
+
51
+ attr_accessor :path
46
52
  end
47
53
  end
48
54
  end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coverband
4
+ module Adapters
5
+ class HashRedisStore < Base
6
+ FILE_KEY = 'file'
7
+ FILE_LENGTH_KEY = 'file_length'
8
+ META_DATA_KEYS = [DATA_KEY, FIRST_UPDATED_KEY, LAST_UPDATED_KEY].freeze
9
+ ###
10
+ # This key isn't related to the coverband version, but to the interal format
11
+ # used to store data to redis. It is changed only when breaking changes to our
12
+ # redis format are required.
13
+ ###
14
+ REDIS_STORAGE_FORMAT_VERSION = 'coverband_hash_3_3'
15
+
16
+ attr_reader :redis_namespace
17
+
18
+ def initialize(redis, opts = {})
19
+ super()
20
+ @redis_namespace = opts[:redis_namespace]
21
+ @format_version = REDIS_STORAGE_FORMAT_VERSION
22
+ @redis = redis
23
+ raise 'HashRedisStore requires redis >= 2.6.0' unless supported?
24
+
25
+ @ttl = opts[:ttl] || -1
26
+ @relative_file_converter = opts[:relative_file_converter] || Utils::RelativeFileConverter
27
+ end
28
+
29
+ def supported?
30
+ Gem::Version.new(@redis.info['redis_version']) >= Gem::Version.new('2.6.0')
31
+ end
32
+
33
+ def clear!
34
+ old_type = type
35
+ Coverband::TYPES.each do |type|
36
+ self.type = type
37
+ file_keys = files_set
38
+ @redis.del(*file_keys) if file_keys.any?
39
+ @redis.del(files_key)
40
+ end
41
+ self.type = old_type
42
+ end
43
+
44
+ def clear_file!(file)
45
+ file_hash = file_hash(file)
46
+ relative_path_file = @relative_file_converter.convert(file)
47
+ Coverband::TYPES.each do |type|
48
+ @redis.del(key(relative_path_file, type, file_hash: file_hash))
49
+ end
50
+ @redis.srem(files_key, relative_path_file)
51
+ end
52
+
53
+ def save_report(report)
54
+ report_time = Time.now.to_i
55
+ updated_time = type == Coverband::EAGER_TYPE ? nil : report_time
56
+ script_id = hash_incr_script
57
+ @redis.pipelined do
58
+ keys = report.map do |file, data|
59
+ relative_file = @relative_file_converter.convert(file)
60
+ file_hash = file_hash(relative_file)
61
+ key = key(relative_file, file_hash: file_hash)
62
+ script_input = save_report_script_input(
63
+ key: key,
64
+ file: relative_file,
65
+ file_hash: file_hash,
66
+ data: data,
67
+ report_time: report_time,
68
+ updated_time: updated_time
69
+ )
70
+ @redis.evalsha(script_id, script_input[:keys], script_input[:args])
71
+ key
72
+ end
73
+ @redis.sadd(files_key, keys) if keys.any?
74
+ end
75
+ end
76
+
77
+ def coverage(local_type = nil)
78
+ files_set(local_type).each_with_object({}) do |key, hash|
79
+ add_coverage_for_file(key, hash)
80
+ end
81
+ end
82
+
83
+ def raw_store
84
+ @redis
85
+ end
86
+
87
+ def size
88
+ 'not available'
89
+ end
90
+
91
+ def size_in_mib
92
+ 'not available'
93
+ end
94
+
95
+ private
96
+
97
+ def add_coverage_for_file(key, hash)
98
+ data_from_redis = @redis.hgetall(key)
99
+
100
+ return if data_from_redis.empty?
101
+
102
+ file = data_from_redis[FILE_KEY]
103
+ return unless file_hash(file) == data_from_redis[FILE_HASH]
104
+
105
+ data = coverage_data_from_redis(data_from_redis)
106
+ hash[file] = data_from_redis.select { |meta_data_key, _value| META_DATA_KEYS.include?(meta_data_key) }.merge!('data' => data)
107
+ hash[file].merge!(LAST_UPDATED_KEY => hash[file][LAST_UPDATED_KEY].to_i, FIRST_UPDATED_KEY => hash[file][FIRST_UPDATED_KEY].to_i)
108
+ end
109
+
110
+ def coverage_data_from_redis(data_from_redis)
111
+ max = data_from_redis[FILE_LENGTH_KEY].to_i - 1
112
+ Array.new(max + 1) do |index|
113
+ line_coverage = data_from_redis[index.to_s]
114
+ line_coverage.nil? ? nil : line_coverage.to_i
115
+ end
116
+ end
117
+
118
+ def save_report_script_input(key:, file:, file_hash:, data:, report_time:, updated_time:)
119
+ data.each_with_index
120
+ .each_with_object(keys: [key], args: [report_time, updated_time, file, file_hash, @ttl, data.length]) do |(coverage, index), hash|
121
+ if coverage
122
+ hash[:keys] << index
123
+ hash[:args] << coverage
124
+ end
125
+ end
126
+ end
127
+
128
+ def hash_incr_script
129
+ @hash_incr_script ||= @redis.script(:load, <<~LUA)
130
+ local first_updated_at = table.remove(ARGV, 1)
131
+ local last_updated_at = table.remove(ARGV, 1)
132
+ local file = table.remove(ARGV, 1)
133
+ local file_hash = table.remove(ARGV, 1)
134
+ local ttl = table.remove(ARGV, 1)
135
+ local file_length = table.remove(ARGV, 1)
136
+ local hash_key = table.remove(KEYS, 1)
137
+ redis.call('HMSET', hash_key, '#{LAST_UPDATED_KEY}', last_updated_at, '#{FILE_KEY}', file, '#{FILE_HASH}', file_hash, '#{FILE_LENGTH_KEY}', file_length)
138
+ redis.call('HSETNX', hash_key, '#{FIRST_UPDATED_KEY}', first_updated_at)
139
+ for i, key in ipairs(KEYS) do
140
+ if ARGV[i] == '-1' then
141
+ redis.call("HSET", hash_key, key, ARGV[i])
142
+ else
143
+ redis.call("HINCRBY", hash_key, key, ARGV[i])
144
+ end
145
+ end
146
+ if ttl ~= '-1' then
147
+ redis.call("EXPIRE", hash_key, ttl)
148
+ end
149
+ LUA
150
+ end
151
+
152
+ def values_from_redis(local_type, files)
153
+ return files if files.empty?
154
+
155
+ @redis.mget(*files.map { |file| key(file, local_type) }).map do |value|
156
+ value.nil? ? {} : JSON.parse(value)
157
+ end
158
+ end
159
+
160
+ def relative_paths(files)
161
+ files&.map! { |file| full_path_to_relative(file) }
162
+ end
163
+
164
+ def files_set(local_type = nil)
165
+ @redis.smembers(files_key(local_type))
166
+ end
167
+
168
+ def files_key(local_type = nil)
169
+ "#{key_prefix(local_type)}.files"
170
+ end
171
+
172
+ def key(file, local_type = nil, file_hash:)
173
+ [key_prefix(local_type), file, file_hash].join('.')
174
+ end
175
+
176
+ def key_prefix(local_type = nil)
177
+ local_type ||= type
178
+ [@format_version, @redis_namespace, local_type].compact.join('.')
179
+ end
180
+ end
181
+ end
182
+ end
@@ -13,23 +13,31 @@ module Coverband
13
13
  ###
14
14
  REDIS_STORAGE_FORMAT_VERSION = 'coverband_3_2'
15
15
 
16
+ attr_reader :redis_namespace
17
+
16
18
  def initialize(redis, opts = {})
17
19
  super()
18
20
  @redis = redis
19
21
  @ttl = opts[:ttl]
20
22
  @redis_namespace = opts[:redis_namespace]
21
23
  @format_version = REDIS_STORAGE_FORMAT_VERSION
24
+ @keys = {}
25
+ Coverband::TYPES.each do |type|
26
+ @keys[type] = [@format_version, @redis_namespace, type].compact.join('.')
27
+ end
22
28
  end
23
29
 
24
30
  def clear!
25
31
  Coverband::TYPES.each do |type|
26
32
  @redis.del(type_base_key(type))
27
33
  end
34
+ # temporarily clear the old namespace of coverband_3_2
35
+ @redis.del(type_base_key(nil))
28
36
  end
29
37
 
30
38
  def clear_file!(filename)
31
39
  Coverband::TYPES.each do |type|
32
- data = get_report(type)
40
+ data = coverage(type)
33
41
  data.delete(filename)
34
42
  save_coverage(data, type)
35
43
  end
@@ -47,18 +55,18 @@ module Coverband
47
55
  def migrate!
48
56
  reset_base_key
49
57
  @format_version = 'coverband3_1'
50
- previous_data = get_report
58
+ previous_data = coverage
51
59
  if previous_data.empty?
52
60
  puts 'no previous data to migrate found'
53
61
  exit 0
54
62
  end
55
63
  relative_path_report = previous_data.each_with_object({}) do |(key, vals), fixed_report|
56
- fixed_report[full_path_to_relative(key)] = vals
64
+ fixed_report[Utils::RelativeFileConverter.convert(key)] = vals
57
65
  end
58
66
  clear!
59
67
  reset_base_key
60
68
  @format_version = REDIS_STORAGE_FORMAT_VERSION
61
- save_coverage(merge_reports(get_report, relative_path_report, skip_expansion: true))
69
+ save_coverage(merge_reports(coverage, relative_path_report, skip_expansion: true))
62
70
  end
63
71
 
64
72
  def type=(type)
@@ -66,6 +74,26 @@ module Coverband
66
74
  reset_base_key
67
75
  end
68
76
 
77
+ def coverage(local_type = nil, opts = {})
78
+ local_type ||= opts.key?(:override_type) ? opts[:override_type] : type
79
+ data = redis.get type_base_key(local_type)
80
+ data ? JSON.parse(data) : {}
81
+ end
82
+
83
+ # Note: This could lead to slight race on redis
84
+ # where multiple processes pull the old coverage and add to it then push
85
+ # the Coverband 2 had the same issue,
86
+ # and the tradeoff has always been acceptable
87
+ def save_report(report)
88
+ data = report.dup
89
+ data = merge_reports(data, coverage)
90
+ save_coverage(data)
91
+ end
92
+
93
+ def raw_store
94
+ @redis
95
+ end
96
+
69
97
  private
70
98
 
71
99
  attr_reader :redis
@@ -79,7 +107,7 @@ module Coverband
79
107
  end
80
108
 
81
109
  def type_base_key(local_type)
82
- [@format_version, @redis_namespace, local_type].compact.join('.')
110
+ @keys[local_type]
83
111
  end
84
112
 
85
113
  def save_coverage(data, local_type = nil)
@@ -87,12 +115,6 @@ module Coverband
87
115
  redis.set type_base_key(local_type), data.to_json
88
116
  redis.expire(type_base_key(local_type), @ttl) if @ttl
89
117
  end
90
-
91
- def get_report(local_type = nil)
92
- local_type ||= type
93
- data = redis.get type_base_key(local_type)
94
- data ? JSON.parse(data) : {}
95
- end
96
118
  end
97
119
  end
98
120
  end
@@ -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
@@ -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