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.
Files changed (49) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +3 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +2 -1
  5. data/README.md +286 -156
  6. data/Rakefile +10 -3
  7. data/changes.md +33 -2
  8. data/coverband.gemspec +30 -23
  9. data/docs/coverband_install.gif +0 -0
  10. data/lib/coverband/adapters/base.rb +31 -0
  11. data/lib/coverband/adapters/file_store.rb +7 -8
  12. data/lib/coverband/adapters/memory_cache_store.rb +13 -6
  13. data/lib/coverband/adapters/redis_store.rb +12 -52
  14. data/lib/coverband/baseline.rb +14 -5
  15. data/lib/coverband/collectors/base.rb +126 -0
  16. data/lib/coverband/collectors/coverage.rb +125 -0
  17. data/lib/coverband/collectors/trace.rb +112 -0
  18. data/lib/coverband/configuration.rb +22 -16
  19. data/lib/coverband/middleware.rb +5 -5
  20. data/lib/coverband/reporters/base.rb +26 -31
  21. data/lib/coverband/reporters/console_report.rb +2 -3
  22. data/lib/coverband/reporters/simple_cov_report.rb +5 -6
  23. data/lib/coverband/s3_report_writer.rb +7 -4
  24. data/lib/coverband/s3_web.rb +10 -6
  25. data/lib/coverband/tasks.rb +51 -46
  26. data/lib/coverband/version.rb +3 -1
  27. data/lib/coverband.rb +10 -15
  28. data/test/benchmarks/benchmark.rake +115 -71
  29. data/test/benchmarks/dog.rb +3 -3
  30. data/test/fake_app/basic_rack.rb +4 -2
  31. data/test/test_helper.rb +20 -12
  32. data/test/unit/adapters_file_store_test.rb +12 -11
  33. data/test/unit/adapters_memory_cache_store_test.rb +3 -4
  34. data/test/unit/adapters_redis_store_test.rb +42 -118
  35. data/test/unit/baseline_test.rb +30 -21
  36. data/test/unit/collectors_base_test.rb +102 -0
  37. data/test/unit/collectors_coverage_test.rb +137 -0
  38. data/test/unit/collectors_trace_test.rb +104 -0
  39. data/test/unit/configuration_test.rb +7 -11
  40. data/test/unit/dog.rb +3 -1
  41. data/test/unit/middleware_test.rb +56 -77
  42. data/test/unit/reports_base_test.rb +87 -61
  43. data/test/unit/reports_console_test.rb +18 -21
  44. data/test/unit/reports_simple_cov_test.rb +23 -26
  45. data/test/unit/s3_report_writer_test.rb +6 -4
  46. data/test/unit/s3_web_test.rb +3 -1
  47. metadata +43 -21
  48. data/lib/coverband/base.rb +0 -180
  49. 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 major release with significant refactoring a stepping stone for a 2.0 release.
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
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
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 = "coverband"
8
+ spec.name = 'coverband'
8
9
  spec.version = Coverband::VERSION
9
- spec.authors = ["Dan Mayer"]
10
- spec.email = ["dan@mayerdan.com"]
11
- spec.description = %q{Rack middleware to help measure production code usage (LOC runtime usage)}
12
- spec.summary = %q{Rack middleware to help measure production code usage (LOC runtime usage)}
13
- spec.homepage = "https://github.com/danmayer/coverband"
14
- spec.license = "MIT"
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 = ["lib"]
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.add_runtime_dependency "simplecov"
31
- spec.add_runtime_dependency "json"
32
- # TODO make redis optional dependancy as we add additional adapters
33
- spec.add_runtime_dependency "redis"
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, opts = {})
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.has_key?(file)
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!( values ){|k, old_v, new_v| old_v.to_i + new_v.to_i}
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 ||= Hash.new
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(Hash.new) do |(file, lines), filtered_file_hash|
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 and false)
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 = 'coverband1'
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
- if @store_as_array
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
- report.each do |file, lines|
32
- store_map("#{BASE_KEY}.#{file}", lines)
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
- if @store_as_array
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!( existing ){|k, old_v, new_v| old_v.to_i + new_v.to_i}
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
- if sadd_supports_array?
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
@@ -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, val| !key.match(project_directory) || Coverband.configuration.ignore.any? { |pattern| key.match(/#{pattern}/) } }
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(back_compat = nil)
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