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.
- checksums.yaml +4 -4
- data/.rubocop.yml +29 -9
- data/.travis.yml +10 -8
- data/CODE_OF_CONDUCT.md +76 -0
- data/Gemfile +3 -1
- data/Gemfile.rails4 +1 -0
- data/README.md +72 -43
- data/Rakefile +14 -3
- data/changes.md +66 -11
- data/coverband.gemspec +2 -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 +182 -0
- data/lib/coverband/adapters/redis_store.rb +33 -11
- data/lib/coverband/at_exit.rb +2 -11
- data/lib/coverband/collectors/coverage.rb +34 -40
- data/lib/coverband/collectors/delta.rb +34 -28
- data/lib/coverband/collectors/view_tracker.rb +124 -0
- data/lib/coverband/configuration.rb +96 -32
- data/lib/coverband/integrations/background.rb +9 -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 +21 -27
- data/lib/coverband/reporters/web.rb +19 -21
- 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 +37 -8
- data/lib/coverband/utils/lines_classifier.rb +5 -0
- data/lib/coverband/utils/railtie.rb +20 -9
- data/lib/coverband/utils/relative_file_converter.rb +41 -0
- data/lib/coverband/utils/result.rb +1 -37
- data/lib/coverband/utils/results.rb +51 -0
- data/lib/coverband/utils/s3_report.rb +2 -2
- data/lib/coverband/utils/source_file.rb +31 -8
- data/lib/coverband/utils/tasks.rb +10 -10
- data/lib/coverband/version.rb +5 -1
- data/lib/coverband.rb +39 -22
- data/public/application.css +6 -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 +190 -0
- data/test/coverband/adapters/redis_store_test.rb +92 -59
- data/test/coverband/at_exit_test.rb +0 -2
- data/test/coverband/collectors/coverage_test.rb +62 -10
- data/test/coverband/collectors/delta_test.rb +36 -6
- data/test/coverband/collectors/view_tracker_test.rb +77 -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 +1 -3
- data/test/coverband/integrations/report_middleware_test.rb +44 -0
- data/test/coverband/integrations/resque_worker_test.rb +6 -6
- data/test/coverband/integrations/test_resque_job.rb +3 -1
- data/test/coverband/reporters/base_test.rb +2 -79
- data/test/coverband/reporters/console_test.rb +2 -6
- data/test/coverband/reporters/html_test.rb +58 -19
- 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 +43 -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 +50 -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 +48 -0
- data/test/integration/full_stack_test.rb +17 -16
- data/test/rails4_dummy/Rakefile +6 -0
- data/test/rails4_dummy/config/application.rb +8 -9
- data/test/rails4_dummy/config/coverband.rb +5 -3
- data/test/rails5_dummy/Rakefile +6 -0
- data/test/rails5_dummy/config/application.rb +6 -10
- data/test/rails5_dummy/config/coverband.rb +4 -2
- data/test/rails_test_helper.rb +22 -5
- data/test/test_helper.rb +45 -8
- data/test/unique_files.rb +17 -9
- data/views/file_list.erb +2 -2
- data/views/gem_list.erb +10 -1
- data/views/layout.erb +6 -18
- data/views/nav.erb +35 -0
- data/views/source_file.erb +13 -4
- data/views/source_file_loader.erb +1 -1
- data/views/view_tracker.erb +37 -0
- metadata +59 -25
- 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
|
@@ -3,45 +3,48 @@
|
|
|
3
3
|
module Coverband
|
|
4
4
|
module Adapters
|
|
5
5
|
class Base
|
|
6
|
-
|
|
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
|
-
@
|
|
15
|
+
@type = Coverband::RUNTIME_TYPE
|
|
11
16
|
end
|
|
12
17
|
|
|
13
18
|
def clear!
|
|
14
|
-
raise
|
|
19
|
+
raise ABSTRACT_KEY
|
|
15
20
|
end
|
|
16
21
|
|
|
17
|
-
def clear_file!(_file)
|
|
18
|
-
raise
|
|
22
|
+
def clear_file!(_file = nil)
|
|
23
|
+
raise ABSTRACT_KEY
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
def migrate!
|
|
22
|
-
raise
|
|
27
|
+
raise ABSTRACT_KEY
|
|
23
28
|
end
|
|
24
29
|
|
|
25
30
|
def size
|
|
26
|
-
raise
|
|
31
|
+
raise ABSTRACT_KEY
|
|
27
32
|
end
|
|
28
33
|
|
|
29
|
-
def
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
44
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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 =
|
|
98
|
+
updated_time = type == Coverband::EAGER_TYPE ? nil : report_time
|
|
91
99
|
report.each_pair do |key, line_data|
|
|
92
100
|
extended_data = {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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[
|
|
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][
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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[
|
|
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(
|
|
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
|
-
[
|
|
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
|
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
|
|
@@ -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
|