deep-cover 0.5.2 → 0.5.3

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 (84) hide show
  1. checksums.yaml +5 -5
  2. data/.deep_cover.rb +8 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +15 -1
  5. data/.travis.yml +1 -0
  6. data/README.md +30 -1
  7. data/Rakefile +10 -1
  8. data/bin/cov +1 -1
  9. data/deep_cover.gemspec +4 -5
  10. data/exe/deep-cover +5 -3
  11. data/lib/deep_cover.rb +1 -1
  12. data/lib/deep_cover/analyser/node.rb +1 -1
  13. data/lib/deep_cover/analyser/ruby25_like_branch.rb +209 -0
  14. data/lib/deep_cover/auto_run.rb +19 -19
  15. data/lib/deep_cover/autoload_tracker.rb +181 -44
  16. data/lib/deep_cover/backports.rb +3 -1
  17. data/lib/deep_cover/base.rb +13 -8
  18. data/lib/deep_cover/basics.rb +1 -1
  19. data/lib/deep_cover/cli/debugger.rb +2 -2
  20. data/lib/deep_cover/cli/instrumented_clone_reporter.rb +21 -8
  21. data/lib/deep_cover/cli/runner.rb +126 -0
  22. data/lib/deep_cover/config_setter.rb +1 -0
  23. data/lib/deep_cover/core_ext/autoload_overrides.rb +82 -14
  24. data/lib/deep_cover/core_ext/coverage_replacement.rb +34 -5
  25. data/lib/deep_cover/core_ext/exec_callbacks.rb +27 -0
  26. data/lib/deep_cover/core_ext/load_overrides.rb +4 -6
  27. data/lib/deep_cover/core_ext/require_overrides.rb +1 -3
  28. data/lib/deep_cover/coverage.rb +105 -2
  29. data/lib/deep_cover/coverage/analysis.rb +30 -28
  30. data/lib/deep_cover/coverage/persistence.rb +60 -70
  31. data/lib/deep_cover/covered_code.rb +16 -49
  32. data/lib/deep_cover/custom_requirer.rb +112 -51
  33. data/lib/deep_cover/load.rb +10 -6
  34. data/lib/deep_cover/memoize.rb +1 -3
  35. data/lib/deep_cover/module_override.rb +7 -0
  36. data/lib/deep_cover/node/assignments.rb +2 -1
  37. data/lib/deep_cover/node/base.rb +6 -6
  38. data/lib/deep_cover/node/block.rb +10 -8
  39. data/lib/deep_cover/node/case.rb +3 -3
  40. data/lib/deep_cover/node/collections.rb +8 -0
  41. data/lib/deep_cover/node/if.rb +19 -3
  42. data/lib/deep_cover/node/literals.rb +28 -7
  43. data/lib/deep_cover/node/mixin/can_augment_children.rb +4 -4
  44. data/lib/deep_cover/node/mixin/child_can_be_empty.rb +1 -1
  45. data/lib/deep_cover/node/mixin/filters.rb +6 -2
  46. data/lib/deep_cover/node/mixin/has_child.rb +8 -8
  47. data/lib/deep_cover/node/mixin/has_child_handler.rb +3 -3
  48. data/lib/deep_cover/node/mixin/has_tracker.rb +7 -3
  49. data/lib/deep_cover/node/root.rb +1 -1
  50. data/lib/deep_cover/node/send.rb +53 -7
  51. data/lib/deep_cover/node/short_circuit.rb +11 -3
  52. data/lib/deep_cover/parser_ext/range.rb +11 -27
  53. data/lib/deep_cover/problem_with_diagnostic.rb +1 -1
  54. data/lib/deep_cover/reporter.rb +0 -1
  55. data/lib/deep_cover/reporter/base.rb +68 -0
  56. data/lib/deep_cover/reporter/html.rb +1 -1
  57. data/lib/deep_cover/reporter/html/index.rb +4 -8
  58. data/lib/deep_cover/reporter/html/site.rb +10 -18
  59. data/lib/deep_cover/reporter/html/source.rb +3 -3
  60. data/lib/deep_cover/reporter/html/template/source.html.erb +1 -1
  61. data/lib/deep_cover/reporter/istanbul.rb +86 -56
  62. data/lib/deep_cover/reporter/text.rb +5 -13
  63. data/lib/deep_cover/reporter/{util/tree.rb → tree/util.rb} +19 -21
  64. data/lib/deep_cover/tools/blank.rb +25 -0
  65. data/lib/deep_cover/tools/builtin_coverage.rb +8 -8
  66. data/lib/deep_cover/tools/dump_covered_code.rb +2 -9
  67. data/lib/deep_cover/tools/execute_sample.rb +17 -6
  68. data/lib/deep_cover/tools/format_generated_code.rb +1 -1
  69. data/lib/deep_cover/tools/indent_string.rb +26 -0
  70. data/lib/deep_cover/tools/our_coverage.rb +2 -2
  71. data/lib/deep_cover/tools/strip_heredoc.rb +18 -0
  72. data/lib/deep_cover/tracker_bucket.rb +50 -0
  73. data/lib/deep_cover/tracker_hits_per_path.rb +35 -0
  74. data/lib/deep_cover/tracker_storage.rb +76 -0
  75. data/lib/deep_cover/tracker_storage_per_path.rb +34 -0
  76. data/lib/deep_cover/version.rb +1 -1
  77. data/lib/deep_cover_entry.rb +3 -0
  78. metadata +30 -37
  79. data/bin/gemcov +0 -8
  80. data/bin/selfcov +0 -21
  81. data/lib/deep_cover/cli/deep_cover.rb +0 -126
  82. data/lib/deep_cover/coverage/base.rb +0 -81
  83. data/lib/deep_cover/coverage/istanbul.rb +0 -34
  84. data/lib/deep_cover/tools/transform_keys.rb +0 -9
@@ -7,6 +7,7 @@ module DeepCover
7
7
  end
8
8
 
9
9
  def config(notify = self)
10
+ raise ArgumentError, 'config does not accept an argument. Did you mean `configure`?' if block_given?
10
11
  @config ||= Config.new(notify)
11
12
  config_queue.each { |block| configure(&block) }
12
13
  config_queue.clear
@@ -1,25 +1,93 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # We need to override autoload, because MRI has special behaviors associated with it
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
- # Basically, when trying to use a constant set to be autoloaded in an optionnal way, like:
7
- # * module A; ...; end
8
- # * A ||= 1
9
- # When autoloading the file, the above won't work and will raise a "uninitialized constant A"
10
- # because ruby doesn't understand that custom require is currently requiring the correct file.
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.add(mod, name, path)
22
- mod.autoload_without_deep_cover(name, path)
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.add(self, name, path)
32
- autoload_without_deep_cover(name, path)
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
- return if running?
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
- DeepCover.coverage.covered_codes.map do |covered_code|
27
- [covered_code.path, covered_code.line_coverage(allow_partial: false)]
28
- end.to_h
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
- result = catch(:use_fallback) { DeepCover.custom_requirer.load(path) }
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
- extend ModuleOverride
20
- override ::Kernel, ::Kernel.singleton_class
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
- result = catch(:use_fallback) { DeepCover.custom_requirer.require(path) }
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)
@@ -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::Analysis < Struct.new(:covered_codes, :options)
5
- include Memoize
6
- memoize :analyser_map, :stat_map
4
+ class Coverage
5
+ class Analysis < Struct.new(:covered_codes, :options)
6
+ include Memoize
7
+ memoize :analyser_map, :stat_map
7
8
 
8
- def analyser_map
9
- covered_codes.map do |covered_code|
10
- [covered_code, compute_analysers(covered_code)]
11
- end.to_h
12
- end
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
- def stat_map
15
- analyser_map.transform_values { |a| a.transform_values(&:stats) }
16
- end
15
+ def stat_map
16
+ analyser_map.transform_values { |a| a.transform_values(&:stats) }
17
+ end
17
18
 
18
- def self.template
19
- {node: Analyser::Node, per_char: Analyser::PerChar, branch: Analyser::Branch}
20
- end
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
- private
25
+ def self.template
26
+ {node: Analyser::Node, per_char: Analyser::PerChar, branch: Analyser::Branch}
27
+ end
23
28
 
24
- def compute_analysers(covered_code)
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
- class Coverage
36
- def analysis(**options)
37
- Analysis.new(covered_codes, options)
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::Persistence
6
- BASENAME = 'coverage.dc'
7
- TRACKER_TEMPLATE = 'trackers%{unique}.dct'
5
+ class Coverage
6
+ class Persistence
7
+ BASENAME = 'coverage.dc'
8
+ TRACKER_TEMPLATE = 'trackers%{unique}.dct'
8
9
 
9
- attr_reader :dir_path
10
- def initialize(dest_path, dirname)
11
- @dir_path = Pathname(dest_path).join(dirname).expand_path
12
- end
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
- def load(with_trackers: true)
15
- saved?
16
- load_trackers if with_trackers
17
- load_coverage
18
- end
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
- def save(coverage)
21
- create_if_needed
22
- delete_trackers
23
- save_coverage(coverage)
24
- end
22
+ def save(coverage)
23
+ create_if_needed
24
+ delete_trackers
25
+ save_coverage(coverage)
26
+ end
25
27
 
26
- def save_trackers(global)
27
- saved?
28
- trackers = eval(global) # rubocop:disable Security/Eval
29
- # Some testing involves more than one process, some of which don't run any of our covered code.
30
- # Don't save anything if that's the case
31
- return if trackers.nil?
32
- basename = format(TRACKER_TEMPLATE, unique: SecureRandom.urlsafe_base64)
33
- dir_path.join(basename).binwrite(Marshal.dump(
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
- def saved?
41
- raise "Can't find folder '#{dir_path}'" unless dir_path.exist?
42
- self
43
- end
37
+ def saved?
38
+ raise "Can't find folder '#{dir_path}'" unless dir_path.exist?
39
+ self
40
+ end
44
41
 
45
- private
42
+ private
46
43
 
47
- def create_if_needed
48
- dir_path.mkpath
49
- end
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
- # rubocop:disable Security/MarshalLoad
59
- def load_coverage
60
- Marshal.load(dir_path.join(BASENAME).binread).tap do |version:, coverage:|
61
- raise "dump version mismatch: #{version}, currently #{DeepCover::VERSION}" unless version == DeepCover::VERSION
62
- return coverage
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
- def load_trackers
67
- tracker_files.each do |full_path|
68
- Marshal.load(full_path.binread).tap do |version:, global:, trackers:|
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
- merge_trackers(eval("#{global} ||= {}"), trackers) # rubocop:disable Security/Eval
59
+ return coverage
71
60
  end
72
61
  end
73
- end
74
- # rubocop:enable Security/MarshalLoad
75
62
 
76
- def merge_trackers(hash, to_merge)
77
- hash.merge!(to_merge) do |_key, current, to_add|
78
- unless current.empty? || current.size == to_add.size
79
- warn "Merging trackers of different sizes: #{current.size} vs #{to_add.size}"
80
- end
81
- to_add.zip(current).map { |a, b| a + b }
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
- end
72
+ # rubocop:enable Security/MarshalLoad
84
73
 
85
- def tracker_files
86
- basename = format(TRACKER_TEMPLATE, unique: '*')
87
- Pathname.glob(dir_path.join(basename))
88
- end
74
+ def tracker_files
75
+ basename = format(TRACKER_TEMPLATE, unique: '*')
76
+ Pathname.glob(dir_path.join(basename))
77
+ end
89
78
 
90
- def delete_trackers
91
- tracker_files.each(&:delete)
79
+ def delete_trackers
80
+ tracker_files.each(&:delete)
81
+ end
92
82
  end
93
83
  end
94
84
  end