coverband 1.5.0 → 2.0.0
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 +5 -5
- data/.gitignore +3 -0
- data/.travis.yml +3 -0
- data/Gemfile +2 -1
- data/README.md +286 -156
- data/Rakefile +10 -3
- data/changes.md +33 -2
- data/coverband.gemspec +30 -23
- data/docs/coverband_install.gif +0 -0
- data/lib/coverband/adapters/base.rb +31 -0
- data/lib/coverband/adapters/file_store.rb +7 -8
- data/lib/coverband/adapters/memory_cache_store.rb +13 -6
- data/lib/coverband/adapters/redis_store.rb +12 -52
- data/lib/coverband/baseline.rb +14 -5
- data/lib/coverband/collectors/base.rb +126 -0
- data/lib/coverband/collectors/coverage.rb +125 -0
- data/lib/coverband/collectors/trace.rb +112 -0
- data/lib/coverband/configuration.rb +22 -16
- data/lib/coverband/middleware.rb +5 -5
- data/lib/coverband/reporters/base.rb +26 -31
- data/lib/coverband/reporters/console_report.rb +2 -3
- data/lib/coverband/reporters/simple_cov_report.rb +5 -6
- data/lib/coverband/s3_report_writer.rb +7 -4
- data/lib/coverband/s3_web.rb +10 -6
- data/lib/coverband/tasks.rb +51 -46
- data/lib/coverband/version.rb +3 -1
- data/lib/coverband.rb +10 -15
- data/test/benchmarks/benchmark.rake +115 -71
- data/test/benchmarks/dog.rb +3 -3
- data/test/fake_app/basic_rack.rb +4 -2
- data/test/test_helper.rb +20 -12
- data/test/unit/adapters_file_store_test.rb +12 -11
- data/test/unit/adapters_memory_cache_store_test.rb +3 -4
- data/test/unit/adapters_redis_store_test.rb +42 -118
- data/test/unit/baseline_test.rb +30 -21
- data/test/unit/collectors_base_test.rb +102 -0
- data/test/unit/collectors_coverage_test.rb +137 -0
- data/test/unit/collectors_trace_test.rb +104 -0
- data/test/unit/configuration_test.rb +7 -11
- data/test/unit/dog.rb +3 -1
- data/test/unit/middleware_test.rb +56 -77
- data/test/unit/reports_base_test.rb +87 -61
- data/test/unit/reports_console_test.rb +18 -21
- data/test/unit/reports_simple_cov_test.rb +23 -26
- data/test/unit/s3_report_writer_test.rb +6 -4
- data/test/unit/s3_web_test.rb +3 -1
- metadata +43 -21
- data/lib/coverband/base.rb +0 -180
- data/test/unit/base_test.rb +0 -96
data/changes.md
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
|
+
# Future Roadmap
|
|
2
|
+
|
|
3
|
+
### Coverband 3.0
|
|
4
|
+
|
|
5
|
+
Will be the fully modern release that drops maintenance legacy support in favor of increased performance, ease of use, and maintainability.
|
|
6
|
+
|
|
7
|
+
* expects to drop Tracepoint collection engine
|
|
8
|
+
* expects to drop anything below Ruby 2.3
|
|
9
|
+
* Release will be aimed as significantly simplifying ease of use
|
|
10
|
+
* expects to drop the concept of baseline recordings
|
|
11
|
+
* improve support for eager-loading
|
|
12
|
+
* add built-in support for easy loading via Railties
|
|
13
|
+
* expects to add safe list support to force reload files one wants coverage on that may happen outside of the standard load order
|
|
14
|
+
* built in support for activejob, sidekiq, and other common frameworks
|
|
15
|
+
|
|
16
|
+
# Released
|
|
17
|
+
|
|
18
|
+
### 2.0.0
|
|
19
|
+
|
|
20
|
+
Major release with various backwards compatibility breaking changes (generally related to the configuration). The 2.0 lifecycle will act as a mostly easy upgrade that supports past users looking to move to the much faster new Coverage Adapter.
|
|
21
|
+
|
|
22
|
+
* Continues to support Ruby 2.0 and up
|
|
23
|
+
* supports multiple collect engines, introducing the concept of multiple collector adapters
|
|
24
|
+
* extends the concepts of multiple storage adapters, enabling additional authors to help support Kafka, graphite, other adapters
|
|
25
|
+
* old require based loading, but working towards deprecating the entire baseline concept
|
|
26
|
+
* Introduces massive performance enhancements by moving to Ruby `Coverage` based collection
|
|
27
|
+
* Opposed to sampling this is now a reporting frequency, when using `Coverage` collector
|
|
28
|
+
* Reduced configuration complexity
|
|
29
|
+
* Refactoring the code preparing for more varied storage and reporting options
|
|
30
|
+
* Drop Redis as a gem runtime_dependency
|
|
31
|
+
|
|
1
32
|
### 1.5.0
|
|
2
33
|
|
|
3
|
-
This is a
|
|
34
|
+
This is a significant release with significant refactoring a stepping stone for a 2.0 release.
|
|
4
35
|
|
|
5
36
|
* staging a changes.md document!
|
|
6
37
|
* refactored out full abstraction for stores
|
|
@@ -20,4 +51,4 @@ This is a major release with significant refactoring a stepping stone for a 2.0
|
|
|
20
51
|
|
|
21
52
|
* This was a small fix release addressing some issues
|
|
22
53
|
* mostly readme updates
|
|
23
|
-
* last release prior to having a changes document!
|
|
54
|
+
* last release prior to having a changes document!
|
data/coverband.gemspec
CHANGED
|
@@ -1,34 +1,41 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
5
|
require 'coverband/version'
|
|
5
6
|
|
|
6
7
|
Gem::Specification.new do |spec|
|
|
7
|
-
spec.name =
|
|
8
|
+
spec.name = 'coverband'
|
|
8
9
|
spec.version = Coverband::VERSION
|
|
9
|
-
spec.authors = [
|
|
10
|
-
spec.email = [
|
|
11
|
-
spec.description =
|
|
12
|
-
spec.summary =
|
|
13
|
-
spec.homepage =
|
|
14
|
-
spec.license =
|
|
10
|
+
spec.authors = ['Dan Mayer']
|
|
11
|
+
spec.email = ['dan@mayerdan.com']
|
|
12
|
+
spec.description = 'Rack middleware to help measure production code usage (LOC runtime usage)'
|
|
13
|
+
spec.summary = 'Rack middleware to help measure production code usage (LOC runtime usage)'
|
|
14
|
+
spec.homepage = 'https://github.com/danmayer/coverband'
|
|
15
|
+
spec.license = 'MIT'
|
|
15
16
|
|
|
16
|
-
spec.files = `git ls-files`.split(
|
|
17
|
+
spec.files = `git ls-files`.split("\n")
|
|
17
18
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
|
-
spec.require_paths = [
|
|
20
|
+
spec.require_paths = ['lib']
|
|
20
21
|
|
|
21
|
-
spec.add_development_dependency "bundler", "~> 1.3"
|
|
22
|
-
spec.add_development_dependency "rake"
|
|
23
|
-
spec.add_development_dependency "mocha", "~> 0.14.0"
|
|
24
|
-
spec.add_development_dependency "rack"
|
|
25
|
-
spec.add_development_dependency "rack-test"
|
|
26
|
-
spec.add_development_dependency "test-unit"
|
|
27
|
-
spec.add_development_dependency 'sinatra'
|
|
28
|
-
spec.add_development_dependency 'classifier-reborn'
|
|
29
22
|
spec.add_development_dependency 'aws-sdk', '~> 2'
|
|
30
|
-
spec.
|
|
31
|
-
spec.
|
|
32
|
-
|
|
33
|
-
spec.
|
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
|
24
|
+
spec.add_development_dependency 'classifier-reborn'
|
|
25
|
+
spec.add_development_dependency 'mocha', '~> 0.14.0'
|
|
26
|
+
spec.add_development_dependency 'rack'
|
|
27
|
+
spec.add_development_dependency 'rack-test'
|
|
28
|
+
spec.add_development_dependency 'rake'
|
|
29
|
+
spec.add_development_dependency 'sinatra'
|
|
30
|
+
spec.add_development_dependency 'test-unit'
|
|
31
|
+
spec.add_development_dependency 'redis'
|
|
32
|
+
spec.add_development_dependency 'benchmark-ips'
|
|
33
|
+
# add when debugging
|
|
34
|
+
# require 'byebug'; byebug
|
|
35
|
+
#spec.add_development_dependency 'byebug'
|
|
36
|
+
# deprecate when dropping support for older ruby
|
|
37
|
+
spec.add_runtime_dependency 'json'
|
|
38
|
+
# todo make an optional dependency for simplecov reports
|
|
39
|
+
# also likely should just require simplecov-html not the whole lib
|
|
40
|
+
spec.add_runtime_dependency 'simplecov', '> 0.11.1'
|
|
34
41
|
end
|
|
Binary file
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coverband
|
|
4
|
+
module Adapters
|
|
5
|
+
class Base
|
|
6
|
+
def initialize
|
|
7
|
+
raise 'abstract'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def clear!
|
|
11
|
+
raise 'abstract'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def save_report(report)
|
|
15
|
+
raise 'abstract'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def coverage
|
|
19
|
+
raise 'abstract'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def covered_files
|
|
23
|
+
raise 'abstract'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def covered_lines_for_file(file)
|
|
27
|
+
raise 'abstract'
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Coverband
|
|
2
4
|
module Adapters
|
|
3
|
-
class FileStore
|
|
5
|
+
class FileStore < Base
|
|
4
6
|
attr_accessor :path
|
|
5
7
|
|
|
6
|
-
def initialize(path,
|
|
8
|
+
def initialize(path, _opts = {})
|
|
7
9
|
@path = path
|
|
8
10
|
|
|
9
11
|
config_dir = File.dirname(@path)
|
|
@@ -11,18 +13,16 @@ module Coverband
|
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def clear!
|
|
14
|
-
if File.exist?(path)
|
|
15
|
-
File.delete(path)
|
|
16
|
-
end
|
|
16
|
+
File.delete(path) if File.exist?(path)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def save_report(report)
|
|
20
20
|
results = existing_data(path)
|
|
21
21
|
report.each_pair do |file, values|
|
|
22
|
-
if results.
|
|
22
|
+
if results.key?(file)
|
|
23
23
|
# convert the keys to "3" opposed to 3
|
|
24
24
|
values = JSON.parse(values.to_json)
|
|
25
|
-
results[file].merge!(
|
|
25
|
+
results[file].merge!(values) { |_k, old_v, new_v| old_v.to_i + new_v.to_i }
|
|
26
26
|
else
|
|
27
27
|
results[file] = values
|
|
28
28
|
end
|
|
@@ -53,7 +53,6 @@ module Coverband
|
|
|
53
53
|
{}
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
|
-
|
|
57
56
|
end
|
|
58
57
|
end
|
|
59
58
|
end
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
###
|
|
4
|
+
# TODO current benchmarks aren't showing much advantage from this wrapped cache approach
|
|
5
|
+
# re-evaluate before 2.0.0 release
|
|
6
|
+
###
|
|
1
7
|
module Coverband
|
|
2
8
|
module Adapters
|
|
3
|
-
class MemoryCacheStore
|
|
9
|
+
class MemoryCacheStore < Base
|
|
4
10
|
attr_accessor :store
|
|
5
11
|
|
|
6
12
|
def initialize(store)
|
|
@@ -20,10 +26,11 @@ module Coverband
|
|
|
20
26
|
store.save_report(filtered_files) if filtered_files.any?
|
|
21
27
|
end
|
|
22
28
|
|
|
29
|
+
# rubocop:disable Lint/IneffectiveAccessModifier
|
|
23
30
|
private
|
|
24
31
|
|
|
25
32
|
def self.files_cache
|
|
26
|
-
@files_cache ||=
|
|
33
|
+
@files_cache ||= {}
|
|
27
34
|
end
|
|
28
35
|
|
|
29
36
|
def files_cache
|
|
@@ -31,16 +38,16 @@ module Coverband
|
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
def filter(files)
|
|
34
|
-
files.each_with_object(
|
|
35
|
-
#first time we see a file, we pre-init the in memory cache to whatever is in store(redis)
|
|
41
|
+
files.each_with_object({}) do |(file, lines), filtered_file_hash|
|
|
42
|
+
# first time we see a file, we pre-init the in memory cache to whatever is in store(redis)
|
|
36
43
|
line_cache = files_cache[file] ||= Set.new(store.covered_lines_for_file(file))
|
|
37
44
|
lines.reject! do |line|
|
|
38
|
-
line_cache.include?(line) ? true : (line_cache << line
|
|
45
|
+
line_cache.include?(line) ? true : (line_cache << line && false)
|
|
39
46
|
end
|
|
40
47
|
filtered_file_hash[file] = lines if lines.any?
|
|
41
48
|
end
|
|
42
49
|
end
|
|
43
|
-
|
|
50
|
+
# rubocop:enable Lint/IneffectiveAccessModifier
|
|
44
51
|
end
|
|
45
52
|
end
|
|
46
53
|
end
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Coverband
|
|
2
4
|
module Adapters
|
|
3
|
-
class RedisStore
|
|
4
|
-
BASE_KEY = '
|
|
5
|
+
class RedisStore < Base
|
|
6
|
+
BASE_KEY = 'coverband2'
|
|
5
7
|
|
|
6
8
|
def initialize(redis, opts = {})
|
|
7
9
|
@redis = redis
|
|
8
|
-
#remove check for coverband 2.0
|
|
9
|
-
@_sadd_supports_array = recent_gem_version? && recent_server_version?
|
|
10
|
-
#possibly drop array storage for 2.0
|
|
11
|
-
@store_as_array = opts.fetch(:array){ false }
|
|
12
10
|
end
|
|
13
11
|
|
|
14
12
|
def clear!
|
|
@@ -17,20 +15,10 @@ module Coverband
|
|
|
17
15
|
end
|
|
18
16
|
|
|
19
17
|
def save_report(report)
|
|
20
|
-
|
|
21
|
-
redis.pipelined do
|
|
22
|
-
store_array(BASE_KEY, report.keys)
|
|
23
|
-
|
|
24
|
-
report.each do |file, lines|
|
|
25
|
-
store_array("#{BASE_KEY}.#{file}", lines.keys)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
else
|
|
29
|
-
store_array(BASE_KEY, report.keys)
|
|
18
|
+
store_array(BASE_KEY, report.keys)
|
|
30
19
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
20
|
+
report.each do |file, lines|
|
|
21
|
+
store_map("#{BASE_KEY}.#{file}", lines)
|
|
34
22
|
end
|
|
35
23
|
end
|
|
36
24
|
|
|
@@ -47,55 +35,27 @@ module Coverband
|
|
|
47
35
|
end
|
|
48
36
|
|
|
49
37
|
def covered_lines_for_file(file)
|
|
50
|
-
|
|
51
|
-
@redis.smembers("#{BASE_KEY}.#{file}").map(&:to_i)
|
|
52
|
-
else
|
|
53
|
-
@redis.hgetall("#{BASE_KEY}.#{file}")
|
|
54
|
-
end
|
|
38
|
+
@redis.hgetall("#{BASE_KEY}.#{file}")
|
|
55
39
|
end
|
|
56
40
|
|
|
57
41
|
private
|
|
58
42
|
|
|
59
43
|
attr_reader :redis
|
|
60
44
|
|
|
61
|
-
def sadd_supports_array?
|
|
62
|
-
@_sadd_supports_array
|
|
63
|
-
end
|
|
64
|
-
|
|
65
45
|
def store_map(key, values)
|
|
66
46
|
unless values.empty?
|
|
67
47
|
existing = redis.hgetall(key)
|
|
68
|
-
#in redis all keys are strings
|
|
69
|
-
values = Hash[values.map{|k,val| [k.to_s,val] }
|
|
70
|
-
values.merge!(
|
|
48
|
+
# in redis all keys are strings
|
|
49
|
+
values = Hash[values.map { |k, val| [k.to_s, val] }]
|
|
50
|
+
values.merge!(existing) { |_k, old_v, new_v| old_v.to_i + new_v.to_i }
|
|
71
51
|
redis.mapped_hmset(key, values)
|
|
72
52
|
end
|
|
73
53
|
end
|
|
74
54
|
|
|
75
55
|
def store_array(key, values)
|
|
76
|
-
|
|
77
|
-
redis.sadd(key, values) if (values.length > 0)
|
|
78
|
-
else
|
|
79
|
-
values.each do |value|
|
|
80
|
-
redis.sadd(key, value)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
56
|
+
redis.sadd(key, values) unless values.empty?
|
|
83
57
|
values
|
|
84
58
|
end
|
|
85
|
-
|
|
86
|
-
def recent_server_version?
|
|
87
|
-
info_data = redis.info
|
|
88
|
-
if info_data.is_a?(Hash)
|
|
89
|
-
Gem::Version.new(info_data['redis_version']) >= Gem::Version.new('2.4')
|
|
90
|
-
else
|
|
91
|
-
#guess supported
|
|
92
|
-
true
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def recent_gem_version?
|
|
97
|
-
Gem::Version.new(Redis::VERSION) >= Gem::Version.new('3.0')
|
|
98
|
-
end
|
|
99
59
|
end
|
|
100
60
|
end
|
|
101
61
|
end
|
data/lib/coverband/baseline.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Coverband
|
|
2
4
|
class Baseline
|
|
3
|
-
|
|
4
5
|
def self.record
|
|
5
6
|
require 'coverage'
|
|
6
7
|
Coverage.start
|
|
@@ -8,15 +9,24 @@ module Coverband
|
|
|
8
9
|
|
|
9
10
|
project_directory = File.expand_path(Coverband.configuration.root)
|
|
10
11
|
results = Coverage.result
|
|
11
|
-
results = results.reject { |key,
|
|
12
|
+
results = results.reject { |key, _val| !key.match(project_directory) || Coverband.configuration.ignore.any? { |pattern| key.match(/#{pattern}/) } }
|
|
12
13
|
|
|
13
14
|
Coverband.configuration.store.save_report(convert_coverage_format(results))
|
|
14
15
|
end
|
|
15
16
|
|
|
16
|
-
def self.parse_baseline(
|
|
17
|
+
def self.parse_baseline(_back_compat = nil)
|
|
17
18
|
Coverband.configuration.store.coverage
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
def self.exclude_files(files)
|
|
22
|
+
Coverband.configuration.ignore.each do |ignore|
|
|
23
|
+
path = Coverband.configuration.root + "/#{ignore}"
|
|
24
|
+
excludes = File.directory?(path) ? Dir.glob("#{path}/**/*") : [path]
|
|
25
|
+
files -= excludes
|
|
26
|
+
end
|
|
27
|
+
files
|
|
28
|
+
end
|
|
29
|
+
|
|
20
30
|
private
|
|
21
31
|
|
|
22
32
|
def self.convert_coverage_format(results)
|
|
@@ -24,12 +34,11 @@ module Coverband
|
|
|
24
34
|
results.each_pair do |file, data|
|
|
25
35
|
lines_map = {}
|
|
26
36
|
data.each_with_index do |hits, index|
|
|
27
|
-
lines_map[(index+1)] = hits unless hits.nil?
|
|
37
|
+
lines_map[(index + 1)] = hits unless hits.nil?
|
|
28
38
|
end
|
|
29
39
|
file_map[file] = lines_map
|
|
30
40
|
end
|
|
31
41
|
file_map
|
|
32
42
|
end
|
|
33
|
-
|
|
34
43
|
end
|
|
35
44
|
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
####
|
|
4
|
+
# TODO refactor this along with the coverage and trace collector
|
|
5
|
+
####
|
|
6
|
+
module Coverband
|
|
7
|
+
module Collectors
|
|
8
|
+
class Base
|
|
9
|
+
def self.instance
|
|
10
|
+
if Coverband.configuration.collector == 'trace'
|
|
11
|
+
Thread.current[:coverband_instance] ||= Coverband::Collectors::Trace.new
|
|
12
|
+
elsif Coverband.configuration.collector == 'coverage'
|
|
13
|
+
Thread.current[:coverband_instance] ||= Coverband::Collectors::Coverage.new
|
|
14
|
+
else
|
|
15
|
+
raise 'select valid collector [trace, coverage]'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def start
|
|
20
|
+
@enabled = true
|
|
21
|
+
record_coverage
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stop
|
|
25
|
+
@enabled = false
|
|
26
|
+
stop_coverage
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sample
|
|
30
|
+
configure_sampling
|
|
31
|
+
record_coverage
|
|
32
|
+
result = yield
|
|
33
|
+
report_coverage
|
|
34
|
+
result
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def save
|
|
38
|
+
@enabled = true
|
|
39
|
+
report_coverage
|
|
40
|
+
@enabled = false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reset_instance
|
|
44
|
+
@project_directory = File.expand_path(Coverband.configuration.root)
|
|
45
|
+
@enabled = false
|
|
46
|
+
@disable_on_failure_for = Coverband.configuration.disable_on_failure_for
|
|
47
|
+
@file_line_usage = {}
|
|
48
|
+
@ignored_files = Set.new
|
|
49
|
+
@startup_delay = Coverband.configuration.startup_delay
|
|
50
|
+
@ignore_patterns = Coverband.configuration.ignore + ['internal:prelude', 'schema.rb']
|
|
51
|
+
@ignore_patterns += ['gems'] unless Coverband.configuration.include_gems
|
|
52
|
+
@sample_percentage = Coverband.configuration.percentage
|
|
53
|
+
@store = Coverband.configuration.store
|
|
54
|
+
@store = Coverband::Adapters::MemoryCacheStore.new(@store) if Coverband.configuration.memory_caching
|
|
55
|
+
@verbose = Coverband.configuration.verbose
|
|
56
|
+
@logger = Coverband.configuration.logger
|
|
57
|
+
@current_thread = Thread.current
|
|
58
|
+
Thread.current[:coverband_instance] = nil
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def configure_sampling
|
|
63
|
+
if @startup_delay != 0 || (rand * 100.0) > @sample_percentage
|
|
64
|
+
@startup_delay -= 1 if @startup_delay > 0
|
|
65
|
+
@enabled = false
|
|
66
|
+
else
|
|
67
|
+
@enabled = true
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def record_coverage
|
|
72
|
+
raise 'abstract'
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def stop_coverage
|
|
76
|
+
raise 'abstract'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def report_coverage
|
|
80
|
+
raise 'abstract'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
protected
|
|
84
|
+
|
|
85
|
+
def track_file?(file)
|
|
86
|
+
@ignore_patterns.none? { |pattern| file.include?(pattern) } && file.start_with?(@project_directory)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def output_file_line_usage
|
|
90
|
+
@logger.info 'coverband debug coverband file:line usage:'
|
|
91
|
+
@file_line_usage.sort_by { |_key, value| value.length }.each do |pair|
|
|
92
|
+
file = pair.first
|
|
93
|
+
lines = pair.last
|
|
94
|
+
@logger.info "file: #{file} => #{lines.sort_by { |_key, value| value }}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def failed_at_thread_key
|
|
101
|
+
"__#{self.class.name}__failed_at"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def failed_at
|
|
105
|
+
Thread.current[failed_at_thread_key]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def failed_at=(time)
|
|
109
|
+
Thread.current[failed_at_thread_key] = time
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def failed!
|
|
113
|
+
self.failed_at = Time.now
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def failed_recently?
|
|
117
|
+
return false unless @disable_on_failure_for && failed_at
|
|
118
|
+
failed_at + @disable_on_failure_for > Time.now
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def initialize
|
|
122
|
+
reset_instance
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coverband
|
|
4
|
+
module Collectors
|
|
5
|
+
class Coverage < Base
|
|
6
|
+
def record_coverage
|
|
7
|
+
# noop
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def stop_coverage
|
|
11
|
+
# noop
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def report_coverage
|
|
15
|
+
unless @enabled
|
|
16
|
+
@logger.info 'coverage disabled' if @verbose
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if failed_recently?
|
|
21
|
+
@logger.info 'coverage reporting standing-by because of recent failure' if @verbose
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
new_results = nil
|
|
26
|
+
@semaphore.synchronize { new_results = new_coverage(::Coverage.peek_result.dup) }
|
|
27
|
+
new_results.each_pair do |file, line_counts|
|
|
28
|
+
next if @ignored_files.include?(file)
|
|
29
|
+
next unless track_file?(file)
|
|
30
|
+
add_file(file, line_counts)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if @verbose
|
|
34
|
+
@logger.info "coverband file usage: #{file_usage.inspect}"
|
|
35
|
+
output_file_line_usage if @verbose == 'debug'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if @store
|
|
39
|
+
@store.save_report(@file_line_usage)
|
|
40
|
+
@file_line_usage.clear
|
|
41
|
+
elsif @verbose
|
|
42
|
+
@logger.info 'coverage report: '
|
|
43
|
+
@logger.info @file_line_usage.inspect
|
|
44
|
+
end
|
|
45
|
+
# StandardError might be better option
|
|
46
|
+
# coverband previously had RuntimeError here
|
|
47
|
+
# but runtime error can let a large number of error crash this method
|
|
48
|
+
# and this method is currently in a ensure block in middleware
|
|
49
|
+
rescue StandardError => err
|
|
50
|
+
failed!
|
|
51
|
+
if @verbose
|
|
52
|
+
@logger.info 'coverage missing'
|
|
53
|
+
@logger.info "error: #{err.inspect} #{err.message}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def array_diff(latest, original)
|
|
60
|
+
latest.map.with_index { |v, i| v ? v - original[i] : nil }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def previous_results
|
|
64
|
+
@@previous_results
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def add_previous_results(val)
|
|
68
|
+
@@previous_results = val
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def new_coverage(current_coverage)
|
|
72
|
+
if previous_results
|
|
73
|
+
new_results = {}
|
|
74
|
+
current_coverage.each_pair do |file, line_counts|
|
|
75
|
+
if previous_results[file]
|
|
76
|
+
new_results[file] = array_diff(line_counts, previous_results[file])
|
|
77
|
+
else
|
|
78
|
+
new_results[file] = line_counts
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
new_results = current_coverage
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
add_previous_results(current_coverage)
|
|
86
|
+
new_results.dup
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# TODO this seems like a dumb conversion for the already good coverage format
|
|
90
|
+
# coverage is 0 based other implementation matches line number
|
|
91
|
+
def add_file(file, line_counts)
|
|
92
|
+
@file_line_usage[file] = Hash.new(0) unless @file_line_usage.include?(file)
|
|
93
|
+
line_counts.each_with_index do |line_count, index|
|
|
94
|
+
@file_line_usage[file][(index + 1)] = line_count if line_count
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def file_usage
|
|
99
|
+
hash = {}
|
|
100
|
+
@file_line_usage.each do |file, lines|
|
|
101
|
+
hash[file] = lines.values.compact.inject(0, :+)
|
|
102
|
+
end
|
|
103
|
+
hash.sort_by { |_key, value| value }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def initialize
|
|
107
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3.0')
|
|
108
|
+
raise NotImplementedError, 'not supported until Ruby 2.3.0 and later'
|
|
109
|
+
end
|
|
110
|
+
unless defined?(::Coverage)
|
|
111
|
+
# puts 'loading coverage'
|
|
112
|
+
require 'coverage'
|
|
113
|
+
end
|
|
114
|
+
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.5.0')
|
|
115
|
+
::Coverage.start unless ::Coverage.running?
|
|
116
|
+
else
|
|
117
|
+
::Coverage.start
|
|
118
|
+
end
|
|
119
|
+
@semaphore = Mutex.new
|
|
120
|
+
@@previous_results = nil
|
|
121
|
+
reset_instance
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|