deep-cover 0.5.2 → 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.deep_cover.rb +8 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +15 -1
- data/.travis.yml +1 -0
- data/README.md +30 -1
- data/Rakefile +10 -1
- data/bin/cov +1 -1
- data/deep_cover.gemspec +4 -5
- data/exe/deep-cover +5 -3
- data/lib/deep_cover.rb +1 -1
- data/lib/deep_cover/analyser/node.rb +1 -1
- data/lib/deep_cover/analyser/ruby25_like_branch.rb +209 -0
- data/lib/deep_cover/auto_run.rb +19 -19
- data/lib/deep_cover/autoload_tracker.rb +181 -44
- data/lib/deep_cover/backports.rb +3 -1
- data/lib/deep_cover/base.rb +13 -8
- data/lib/deep_cover/basics.rb +1 -1
- data/lib/deep_cover/cli/debugger.rb +2 -2
- data/lib/deep_cover/cli/instrumented_clone_reporter.rb +21 -8
- data/lib/deep_cover/cli/runner.rb +126 -0
- data/lib/deep_cover/config_setter.rb +1 -0
- data/lib/deep_cover/core_ext/autoload_overrides.rb +82 -14
- data/lib/deep_cover/core_ext/coverage_replacement.rb +34 -5
- data/lib/deep_cover/core_ext/exec_callbacks.rb +27 -0
- data/lib/deep_cover/core_ext/load_overrides.rb +4 -6
- data/lib/deep_cover/core_ext/require_overrides.rb +1 -3
- data/lib/deep_cover/coverage.rb +105 -2
- data/lib/deep_cover/coverage/analysis.rb +30 -28
- data/lib/deep_cover/coverage/persistence.rb +60 -70
- data/lib/deep_cover/covered_code.rb +16 -49
- data/lib/deep_cover/custom_requirer.rb +112 -51
- data/lib/deep_cover/load.rb +10 -6
- data/lib/deep_cover/memoize.rb +1 -3
- data/lib/deep_cover/module_override.rb +7 -0
- data/lib/deep_cover/node/assignments.rb +2 -1
- data/lib/deep_cover/node/base.rb +6 -6
- data/lib/deep_cover/node/block.rb +10 -8
- data/lib/deep_cover/node/case.rb +3 -3
- data/lib/deep_cover/node/collections.rb +8 -0
- data/lib/deep_cover/node/if.rb +19 -3
- data/lib/deep_cover/node/literals.rb +28 -7
- data/lib/deep_cover/node/mixin/can_augment_children.rb +4 -4
- data/lib/deep_cover/node/mixin/child_can_be_empty.rb +1 -1
- data/lib/deep_cover/node/mixin/filters.rb +6 -2
- data/lib/deep_cover/node/mixin/has_child.rb +8 -8
- data/lib/deep_cover/node/mixin/has_child_handler.rb +3 -3
- data/lib/deep_cover/node/mixin/has_tracker.rb +7 -3
- data/lib/deep_cover/node/root.rb +1 -1
- data/lib/deep_cover/node/send.rb +53 -7
- data/lib/deep_cover/node/short_circuit.rb +11 -3
- data/lib/deep_cover/parser_ext/range.rb +11 -27
- data/lib/deep_cover/problem_with_diagnostic.rb +1 -1
- data/lib/deep_cover/reporter.rb +0 -1
- data/lib/deep_cover/reporter/base.rb +68 -0
- data/lib/deep_cover/reporter/html.rb +1 -1
- data/lib/deep_cover/reporter/html/index.rb +4 -8
- data/lib/deep_cover/reporter/html/site.rb +10 -18
- data/lib/deep_cover/reporter/html/source.rb +3 -3
- data/lib/deep_cover/reporter/html/template/source.html.erb +1 -1
- data/lib/deep_cover/reporter/istanbul.rb +86 -56
- data/lib/deep_cover/reporter/text.rb +5 -13
- data/lib/deep_cover/reporter/{util/tree.rb → tree/util.rb} +19 -21
- data/lib/deep_cover/tools/blank.rb +25 -0
- data/lib/deep_cover/tools/builtin_coverage.rb +8 -8
- data/lib/deep_cover/tools/dump_covered_code.rb +2 -9
- data/lib/deep_cover/tools/execute_sample.rb +17 -6
- data/lib/deep_cover/tools/format_generated_code.rb +1 -1
- data/lib/deep_cover/tools/indent_string.rb +26 -0
- data/lib/deep_cover/tools/our_coverage.rb +2 -2
- data/lib/deep_cover/tools/strip_heredoc.rb +18 -0
- data/lib/deep_cover/tracker_bucket.rb +50 -0
- data/lib/deep_cover/tracker_hits_per_path.rb +35 -0
- data/lib/deep_cover/tracker_storage.rb +76 -0
- data/lib/deep_cover/tracker_storage_per_path.rb +34 -0
- data/lib/deep_cover/version.rb +1 -1
- data/lib/deep_cover_entry.rb +3 -0
- metadata +30 -37
- data/bin/gemcov +0 -8
- data/bin/selfcov +0 -21
- data/lib/deep_cover/cli/deep_cover.rb +0 -126
- data/lib/deep_cover/coverage/base.rb +0 -81
- data/lib/deep_cover/coverage/istanbul.rb +0 -34
- data/lib/deep_cover/tools/transform_keys.rb +0 -9
@@ -1,25 +1,93 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
4
|
-
# that we can't reuse, hence we need to do workarounds.
|
3
|
+
# Autoload is quite difficult to hook into to do what we need to do.
|
5
4
|
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
5
|
+
# We create a temporary file that gets set as path for autoloads. The file then does a require of the
|
6
|
+
# autoloaded file. We also keep track of autoloaded constants and files and change the autoload's target when
|
7
|
+
# those files get required (or when we guess they are being).
|
8
|
+
#
|
9
|
+
# Doing it this way solves:
|
10
|
+
#
|
11
|
+
# * When autoload is triggered, it doesn't always call `require`. In Ruby 2.1 and 2.2, it just loads the
|
12
|
+
# file somehow without using require, this means we can't instrument autoloaded files. Using an intercept
|
13
|
+
# file means that the intercept is not covered (we don't care) and then the manual require will
|
14
|
+
# allow us to instrument the real target.
|
15
|
+
#
|
16
|
+
# * When autoload is triggered, there are special states setup internal to ruby to allow constants to
|
17
|
+
# be used conditionally. If the target file is not actually required (such as when using our custom requirer),
|
18
|
+
# then the state is not correct and we can't do simple things such as `A ||= 1` or `module A; ...; end`.
|
19
|
+
# This problem was happening when all we did was use the custom require that does get called for Ruby 2.3+.
|
20
|
+
#
|
21
|
+
# To solve the issues, we keep track of all the autoloads, and when we detect that a file is being autoloaded,
|
22
|
+
# we change the state so that ruby thinks the file was already loaded.
|
23
|
+
#
|
24
|
+
# * An issue with the interceptor files is that if some code manually requires a file that is the target of
|
25
|
+
# autoloading, ruby would not disable the autoload behavior, and would end up trying to autoload once the constant
|
26
|
+
# is reached.
|
27
|
+
#
|
28
|
+
# To solve this, every require, we check if it is for a file that is is meant to autoload a constant, and if so,
|
29
|
+
# we remove that autoload.
|
30
|
+
#
|
31
|
+
# * To make matter more complicated, when loading gems without bundle's setup, the $LOAD_PATH is filled by RubyGems
|
32
|
+
# as part of a monkey-patch of Kernel#require. Because of this, we can't always find the file that will be required,
|
33
|
+
# even though a file will indeed be required.
|
34
|
+
#
|
35
|
+
# However, if nothing is done, the autoload can cause problems on that built-in require (as explained in a previous
|
36
|
+
# point). So when we don't find the target, we check if it's a require that will go through the $LOAD_PATH,
|
37
|
+
# meaning that require's parameter is not an absolute path, and isn't relative to the current directory (so doesn't
|
38
|
+
# start with './' or '../').
|
39
|
+
#
|
40
|
+
# When a require does go through $LOAD_PATH, we do a best effort to deactivate the autoloads that seem to match
|
41
|
+
# to match this require by comparing the required path (as it is received) to all the autoload path we have, and
|
42
|
+
# deactivating those that match. This is only an issue when there is an autoload made which will load a different
|
43
|
+
# gem. This is pretty rare (but deep-cover does it...)
|
44
|
+
#
|
45
|
+
# * All of this changing autoloads means that for modules/classes that are frozen, we can't handle the situation, since
|
46
|
+
# we can't change the autoload stuff.
|
47
|
+
#
|
48
|
+
# We don't resolve this problem. However, we could work around it by always calling the real require for these
|
49
|
+
# files (which means we wouldn't cover them), and by not creating interceptor files for the autoloads. Since we
|
50
|
+
# can't know when the #autoload call is made, if the constant will be frozen later on, we would have to instead
|
51
|
+
# monkey-patch #freeze on modules and classes to remove the interceptor file before things get frozen.
|
52
|
+
#
|
53
|
+
# * Kernel#autoload uses the caller's `Module.nesting` instead of using self.
|
54
|
+
# This means that if we intercept the autoload, then we cannot call the original Kernel#autoload because it will
|
55
|
+
# now use our Module instead if our caller's. The only reliable solution we've found to this is to use binding_of_caller
|
56
|
+
# to get the correct object to call autoload on.
|
57
|
+
#
|
58
|
+
# (This is not a problem with Module#autoload and Module.autoload)
|
59
|
+
#
|
60
|
+
# A possible solution to investigate is to make a simple C extension, to do the work of our monkey-patch, this way,
|
61
|
+
# the check for the caller doesn't find our callstack
|
62
|
+
#
|
63
|
+
# Some situations where Module.nesting of the caller is different from self in Kernel#autoload:
|
64
|
+
# * When in the top-level: (self: main) vs (Module.nesting: nil, which we default to Object)
|
65
|
+
# * When called from a method defined on a module that is included:
|
66
|
+
# module A
|
67
|
+
# def begin_autoload
|
68
|
+
# # `Kernel.autoload` would have the same result
|
69
|
+
# autoload :A1, 'hello'
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
# class B
|
73
|
+
# include A
|
74
|
+
# end
|
75
|
+
# Calling `B.new.begin_autoload` is equivalent to:
|
76
|
+
# A.autoload :A1, 'hello'
|
77
|
+
# NOT this:
|
78
|
+
# B.autoload :A1, 'hello'
|
11
79
|
#
|
12
|
-
# Our solution is to track autoloads ourself, and when requiring a path that has autoloads,
|
13
|
-
# we remove the autoloads from the constants first.
|
14
|
-
|
15
80
|
require 'binding_of_caller'
|
81
|
+
require 'tempfile'
|
16
82
|
|
17
83
|
module DeepCover
|
84
|
+
load_all
|
85
|
+
|
18
86
|
module KernelAutoloadOverride
|
19
87
|
def autoload(name, path)
|
20
88
|
mod = binding.of_caller(1).eval('Module.nesting').first || Object
|
21
|
-
DeepCover.autoload_tracker.
|
22
|
-
mod.autoload_without_deep_cover(name,
|
89
|
+
autoload_path = DeepCover.autoload_tracker.autoload_path_for(mod, name, path)
|
90
|
+
mod.autoload_without_deep_cover(name, autoload_path)
|
23
91
|
end
|
24
92
|
|
25
93
|
extend ModuleOverride
|
@@ -28,8 +96,8 @@ module DeepCover
|
|
28
96
|
|
29
97
|
module ModuleAutoloadOverride
|
30
98
|
def autoload(name, path)
|
31
|
-
DeepCover.autoload_tracker.
|
32
|
-
autoload_without_deep_cover(name,
|
99
|
+
autoload_path = DeepCover.autoload_tracker.autoload_path_for(self, name, path)
|
100
|
+
autoload_without_deep_cover(name, autoload_path)
|
33
101
|
end
|
34
102
|
|
35
103
|
extend ModuleOverride
|
@@ -4,13 +4,32 @@
|
|
4
4
|
|
5
5
|
module DeepCover
|
6
6
|
module CoverageReplacement
|
7
|
+
OLD_COVERAGE_SENTINEL = Object.new
|
8
|
+
ALL_COVERAGES = {lines: true, branches: true, methods: true}.freeze
|
9
|
+
|
7
10
|
class << self
|
8
11
|
def running?
|
9
12
|
DeepCover.running?
|
10
13
|
end
|
11
14
|
|
12
|
-
def start
|
13
|
-
|
15
|
+
def start(targets = OLD_COVERAGE_SENTINEL)
|
16
|
+
if targets == OLD_COVERAGE_SENTINEL
|
17
|
+
# Do nothing
|
18
|
+
elsif targets == :all
|
19
|
+
targets = ALL_COVERAGES
|
20
|
+
else
|
21
|
+
targets = targets.to_hash.slice(*ALL_COVERAGES.keys).select { |_, v| v }
|
22
|
+
targets = targets.map { |k, v| [k, !!v] }.to_h
|
23
|
+
raise 'no measuring target is specified' if targets.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
if DeepCover.running?
|
27
|
+
raise 'cannot change the measuring target during coverage measurement' if @started_args != targets
|
28
|
+
return
|
29
|
+
end
|
30
|
+
|
31
|
+
@started_args = targets
|
32
|
+
|
14
33
|
DeepCover.start
|
15
34
|
nil
|
16
35
|
end
|
@@ -23,9 +42,19 @@ module DeepCover
|
|
23
42
|
|
24
43
|
def peek_result
|
25
44
|
raise 'coverage measurement is not enabled' unless running?
|
26
|
-
|
27
|
-
|
28
|
-
|
45
|
+
if @started_args == OLD_COVERAGE_SENTINEL
|
46
|
+
DeepCover.coverage.covered_codes.map do |covered_code|
|
47
|
+
[covered_code.path.to_s, covered_code.line_coverage(allow_partial: false)]
|
48
|
+
end.to_h
|
49
|
+
else
|
50
|
+
DeepCover.coverage.covered_codes.map do |covered_code|
|
51
|
+
cov = {}
|
52
|
+
cov[:branches] = DeepCover::Analyser::Ruby25LikeBranch.new(covered_code).results if @started_args[:branches]
|
53
|
+
cov[:lines] = covered_code.line_coverage(allow_partial: false) if @started_args[:lines]
|
54
|
+
cov[:methods] = {} if @started_args[:methods]
|
55
|
+
[covered_code.path.to_s, cov]
|
56
|
+
end.to_h
|
57
|
+
end
|
29
58
|
end
|
30
59
|
end
|
31
60
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../module_override'
|
4
|
+
|
5
|
+
# Adds a functionality to add callbacks before an `exec`
|
6
|
+
|
7
|
+
module DeepCover
|
8
|
+
module ExecCallbacks
|
9
|
+
class << self
|
10
|
+
attr_reader :callbacks
|
11
|
+
|
12
|
+
def before_exec(&block)
|
13
|
+
self.active = true
|
14
|
+
(@callbacks ||= []) << block
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def exec(*args)
|
19
|
+
ExecCallbacks.callbacks.each(&:call)
|
20
|
+
exec_without_deep_cover(*args)
|
21
|
+
end
|
22
|
+
|
23
|
+
extend ModuleOverride
|
24
|
+
override ::Kernel, ::Kernel.singleton_class
|
25
|
+
self.active = true
|
26
|
+
end
|
27
|
+
end
|
@@ -10,12 +10,10 @@ module DeepCover
|
|
10
10
|
def load(path, wrap = false)
|
11
11
|
return load_without_deep_cover(path, wrap) if wrap
|
12
12
|
|
13
|
-
|
14
|
-
result = load_without_deep_cover(path) if result.is_a? Symbol
|
15
|
-
result
|
13
|
+
DeepCover.custom_requirer.load(path) { load_without_deep_cover(path) }
|
16
14
|
end
|
17
|
-
end
|
18
15
|
|
19
|
-
|
20
|
-
|
16
|
+
extend ModuleOverride
|
17
|
+
override ::Kernel, ::Kernel.singleton_class
|
18
|
+
end
|
21
19
|
end
|
@@ -11,9 +11,7 @@ module DeepCover
|
|
11
11
|
|
12
12
|
module RequireOverride
|
13
13
|
def require(path)
|
14
|
-
|
15
|
-
result = require_without_deep_cover(path) if result.is_a? Symbol
|
16
|
-
result
|
14
|
+
DeepCover.custom_requirer.require(path) { require_without_deep_cover(path) }
|
17
15
|
end
|
18
16
|
|
19
17
|
def require_relative(path)
|
data/lib/deep_cover/coverage.rb
CHANGED
@@ -3,8 +3,111 @@
|
|
3
3
|
module DeepCover
|
4
4
|
bootstrap
|
5
5
|
|
6
|
+
require_relative_dir 'coverage'
|
7
|
+
|
8
|
+
# A collection of CoveredCode
|
6
9
|
class Coverage
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
attr_reader :tracker_storage_per_path
|
13
|
+
|
14
|
+
def initialize(**options)
|
15
|
+
@covered_code_index = {}
|
16
|
+
@options = options
|
17
|
+
@tracker_storage_per_path = TrackerStoragePerPath.new(TrackerBucket[tracker_global])
|
18
|
+
end
|
19
|
+
|
20
|
+
def covered_codes
|
21
|
+
each.to_a
|
22
|
+
end
|
23
|
+
|
24
|
+
def line_coverage(filename, **options)
|
25
|
+
covered_code(filename).line_coverage(**options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def covered_code?(path)
|
29
|
+
@covered_code_index.include?(path)
|
30
|
+
end
|
31
|
+
|
32
|
+
def covered_code(path, **options)
|
33
|
+
raise 'path must be an absolute path' unless Pathname.new(path).absolute?
|
34
|
+
@covered_code_index[path] ||= CoveredCode.new(path: path,
|
35
|
+
tracker_storage: @tracker_storage_per_path[path],
|
36
|
+
**options,
|
37
|
+
**@options)
|
38
|
+
end
|
39
|
+
|
40
|
+
def each
|
41
|
+
return to_enum unless block_given?
|
42
|
+
@tracker_storage_per_path.each_key do |path|
|
43
|
+
begin
|
44
|
+
cov_code = covered_code(path)
|
45
|
+
rescue Parser::SyntaxError
|
46
|
+
next
|
47
|
+
end
|
48
|
+
yield cov_code
|
49
|
+
end
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def report(**options)
|
54
|
+
case (reporter = options.fetch(:reporter, DEFAULTS[:reporter]).to_sym)
|
55
|
+
when :html
|
56
|
+
msg = if (output = options.fetch(:output, DEFAULTS[:output]))
|
57
|
+
Reporter::HTML.report(self, **options)
|
58
|
+
"HTML generated: open #{output}/index.html"
|
59
|
+
else
|
60
|
+
'No HTML generated'
|
61
|
+
end
|
62
|
+
Reporter::Text.report(self, **options) + "\n\n" + msg
|
63
|
+
when :istanbul
|
64
|
+
if Reporter::Istanbul.available?
|
65
|
+
Reporter::Istanbul.report(self, **options)
|
66
|
+
else
|
67
|
+
warn 'nyc not available. Please install `nyc` using `yarn global add nyc` or `npm i nyc -g`'
|
68
|
+
end
|
69
|
+
when :text
|
70
|
+
Reporter::Text.report(self, **options)
|
71
|
+
else
|
72
|
+
raise ArgumentError, "Unknown reporter: #{reporter}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.load(dest_path, dirname = 'deep_cover', with_trackers: true)
|
77
|
+
Persistence.new(dest_path, dirname).load(with_trackers: with_trackers)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.saved?(dest_path, dirname = 'deep_cover')
|
81
|
+
Persistence.new(dest_path, dirname).saved?
|
82
|
+
end
|
83
|
+
|
84
|
+
def save(dest_path, dirname = 'deep_cover')
|
85
|
+
Persistence.new(dest_path, dirname).save(self)
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
def save_trackers(dest_path, dirname = 'deep_cover')
|
90
|
+
Persistence.new(dest_path, dirname).save_trackers(@tracker_storage_per_path.tracker_hits_per_path)
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
def tracker_global
|
95
|
+
@options.fetch(:tracker_global, DEFAULTS[:tracker_global])
|
96
|
+
end
|
97
|
+
|
98
|
+
def analysis(**options)
|
99
|
+
Analysis.new(covered_codes, options)
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def marshal_dump
|
105
|
+
{options: @options, tracker_storage_per_path: @tracker_storage_per_path}
|
106
|
+
end
|
107
|
+
|
108
|
+
def marshal_load(options:, tracker_storage_per_path:)
|
109
|
+
initialize(**options)
|
110
|
+
@tracker_storage_per_path = tracker_storage_per_path
|
111
|
+
end
|
7
112
|
end
|
8
|
-
require_relative_dir 'coverage'
|
9
|
-
Coverage.include Coverage::Istanbul
|
10
113
|
end
|
@@ -1,40 +1,42 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module DeepCover
|
4
|
-
class Coverage
|
5
|
-
|
6
|
-
|
4
|
+
class Coverage
|
5
|
+
class Analysis < Struct.new(:covered_codes, :options)
|
6
|
+
include Memoize
|
7
|
+
memoize :analyser_map, :stat_map
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
def analyser_map
|
10
|
+
covered_codes.map do |covered_code|
|
11
|
+
[covered_code, compute_analysers(covered_code)]
|
12
|
+
end.to_h
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
def stat_map
|
16
|
+
analyser_map.transform_values { |a| a.transform_values(&:stats) }
|
17
|
+
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
def overall
|
20
|
+
return 100 if stat_map.empty?
|
21
|
+
node, branch = Tools.merge(*stat_map.values, :+).values_at(:node, :branch)
|
22
|
+
(node + branch).percent_covered
|
23
|
+
end
|
21
24
|
|
22
|
-
|
25
|
+
def self.template
|
26
|
+
{node: Analyser::Node, per_char: Analyser::PerChar, branch: Analyser::Branch}
|
27
|
+
end
|
23
28
|
|
24
|
-
|
25
|
-
base = Analyser::Node.new(covered_code, **options)
|
26
|
-
{node: base}.merge!(
|
27
|
-
{
|
28
|
-
per_char: Analyser::PerChar,
|
29
|
-
branch: Analyser::Branch,
|
30
|
-
}.transform_values { |klass| klass.new(base, **options) }
|
31
|
-
)
|
32
|
-
end
|
33
|
-
end
|
29
|
+
private
|
34
30
|
|
35
|
-
|
36
|
-
|
37
|
-
|
31
|
+
def compute_analysers(covered_code)
|
32
|
+
base = Analyser::Node.new(covered_code, **options)
|
33
|
+
{node: base}.merge!(
|
34
|
+
{
|
35
|
+
per_char: Analyser::PerChar,
|
36
|
+
branch: Analyser::Branch,
|
37
|
+
}.transform_values { |klass| klass.new(base, **options) }
|
38
|
+
)
|
39
|
+
end
|
38
40
|
end
|
39
41
|
end
|
40
42
|
end
|
@@ -2,93 +2,83 @@
|
|
2
2
|
|
3
3
|
module DeepCover
|
4
4
|
require 'securerandom'
|
5
|
-
class Coverage
|
6
|
-
|
7
|
-
|
5
|
+
class Coverage
|
6
|
+
class Persistence
|
7
|
+
BASENAME = 'coverage.dc'
|
8
|
+
TRACKER_TEMPLATE = 'trackers%{unique}.dct'
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
attr_reader :dir_path
|
11
|
+
def initialize(dest_path, dirname)
|
12
|
+
@dir_path = Pathname(dest_path).join(dirname).expand_path
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
def load(with_trackers: true)
|
16
|
+
saved?
|
17
|
+
cov = load_coverage
|
18
|
+
cov.tracker_storage_per_path.tracker_hits_per_path = load_trackers if with_trackers
|
19
|
+
cov
|
20
|
+
end
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
def save(coverage)
|
23
|
+
create_if_needed
|
24
|
+
delete_trackers
|
25
|
+
save_coverage(coverage)
|
26
|
+
end
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
version: DeepCover::VERSION,
|
35
|
-
global: global,
|
36
|
-
trackers: trackers,
|
37
|
-
))
|
38
|
-
end
|
28
|
+
def save_trackers(tracker_hits_per_path)
|
29
|
+
saved?
|
30
|
+
basename = format(TRACKER_TEMPLATE, unique: SecureRandom.urlsafe_base64)
|
31
|
+
dir_path.join(basename).binwrite(Marshal.dump(
|
32
|
+
version: DeepCover::VERSION,
|
33
|
+
tracker_hits_per_path: tracker_hits_per_path,
|
34
|
+
))
|
35
|
+
end
|
39
36
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
37
|
+
def saved?
|
38
|
+
raise "Can't find folder '#{dir_path}'" unless dir_path.exist?
|
39
|
+
self
|
40
|
+
end
|
44
41
|
|
45
|
-
|
42
|
+
private
|
46
43
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
def save_coverage(coverage)
|
52
|
-
dir_path.join(BASENAME).binwrite(Marshal.dump(
|
53
|
-
version: DeepCover::VERSION,
|
54
|
-
coverage: coverage,
|
55
|
-
))
|
56
|
-
end
|
44
|
+
def create_if_needed
|
45
|
+
dir_path.mkpath
|
46
|
+
end
|
57
47
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
48
|
+
def save_coverage(coverage)
|
49
|
+
dir_path.join(BASENAME).binwrite(Marshal.dump(
|
50
|
+
version: DeepCover::VERSION,
|
51
|
+
coverage: coverage,
|
52
|
+
))
|
63
53
|
end
|
64
|
-
end
|
65
54
|
|
66
|
-
|
67
|
-
|
68
|
-
Marshal.load(
|
55
|
+
# rubocop:disable Security/MarshalLoad
|
56
|
+
def load_coverage
|
57
|
+
Marshal.load(dir_path.join(BASENAME).binread).tap do |version:, coverage:|
|
69
58
|
raise "dump version mismatch: #{version}, currently #{DeepCover::VERSION}" unless version == DeepCover::VERSION
|
70
|
-
|
59
|
+
return coverage
|
71
60
|
end
|
72
61
|
end
|
73
|
-
end
|
74
|
-
# rubocop:enable Security/MarshalLoad
|
75
62
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
63
|
+
# returns a TrackerHitsPerPath
|
64
|
+
def load_trackers
|
65
|
+
tracker_files.map do |full_path|
|
66
|
+
Marshal.load(full_path.binread).yield_self do |version:, tracker_hits_per_path:|
|
67
|
+
raise "dump version mismatch: #{version}, currently #{DeepCover::VERSION}" unless version == DeepCover::VERSION
|
68
|
+
tracker_hits_per_path
|
69
|
+
end
|
70
|
+
end.inject(:merge!)
|
82
71
|
end
|
83
|
-
|
72
|
+
# rubocop:enable Security/MarshalLoad
|
84
73
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
74
|
+
def tracker_files
|
75
|
+
basename = format(TRACKER_TEMPLATE, unique: '*')
|
76
|
+
Pathname.glob(dir_path.join(basename))
|
77
|
+
end
|
89
78
|
|
90
|
-
|
91
|
-
|
79
|
+
def delete_trackers
|
80
|
+
tracker_files.each(&:delete)
|
81
|
+
end
|
92
82
|
end
|
93
83
|
end
|
94
84
|
end
|