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.
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