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
@@ -5,26 +5,27 @@ module DeepCover
5
5
  load_parser
6
6
 
7
7
  class CoveredCode
8
- attr_accessor :covered_source, :buffer, :tracker_global, :local_var, :name
9
- @@counter = 0
10
- @@globals = Hash.new { |h, global| h[global] = eval("#{global} ||= {}") } # rubocop:disable Security/Eval
11
-
12
- def initialize(path: nil, source: nil, lineno: 1, tracker_global: DEFAULTS[:tracker_global], local_var: '_temp', name: nil)
8
+ attr_accessor :covered_source, :buffer, :tracker_storage, :local_var, :path
9
+
10
+ def initialize(
11
+ path: nil,
12
+ source: nil,
13
+ lineno: 1,
14
+ tracker_global: DEFAULTS[:tracker_global],
15
+ tracker_storage: TrackerBucket[tracker_global].create_storage,
16
+ local_var: '_temp'
17
+ )
13
18
  raise 'Must provide either path or source' unless path || source
14
19
 
15
- @buffer = Parser::Source::Buffer.new(path, lineno)
16
- @buffer.source = source || File.read(path)
20
+ @path = path &&= Pathname(path)
21
+ @buffer = Parser::Source::Buffer.new('', lineno)
22
+ @buffer.source = source || path.read
17
23
  @tracker_count = 0
18
- @tracker_global = tracker_global
24
+ @tracker_storage = tracker_storage
19
25
  @local_var = local_var
20
- @name = name.to_s || (path ? File.basename(path) : '(source)')
21
26
  @covered_source = instrument_source
22
27
  end
23
28
 
24
- def path
25
- @buffer.name || "(source: '#{@buffer.source[0..20]}...')"
26
- end
27
-
28
29
  def lineno
29
30
  @buffer.first_line
30
31
  end
@@ -39,8 +40,7 @@ module DeepCover
39
40
  end
40
41
 
41
42
  def execute_code(binding: DeepCover::GLOBAL_BINDING.dup)
42
- return if has_executed?
43
- eval(@covered_source, binding, @buffer.name || '<raw_code>', lineno) # rubocop:disable Security/Eval
43
+ eval(@covered_source, binding, (@path || '<raw_code>').to_s, lineno) # rubocop:disable Security/Eval
44
44
  self
45
45
  end
46
46
 
@@ -52,33 +52,10 @@ module DeepCover
52
52
  Analyser::PerLine.new(self, **options).results
53
53
  end
54
54
 
55
- def to_istanbul(**options)
56
- Reporter::Istanbul.new(self, **options).convert
57
- end
58
-
59
55
  def char_cover(**options)
60
56
  Analyser::PerChar.new(self, **options).results
61
57
  end
62
58
 
63
- def nb
64
- @nb ||= (@@counter += 1)
65
- end
66
-
67
- # Returns a range of tracker ids
68
- def allocate_trackers(nb_needed)
69
- prev = @tracker_count
70
- @tracker_count += nb_needed if nb_needed > 0 # Avoid error if frozen and called with 0.
71
- prev...@tracker_count
72
- end
73
-
74
- def tracker_source(tracker_id)
75
- "#{tracker_global}[#{nb}][#{tracker_id}]+=1"
76
- end
77
-
78
- def trackers_setup_source
79
- "(#{tracker_global}||={})[#{nb}]||=Array.new(#{@tracker_count},0)"
80
- end
81
-
82
59
  def tracker_hits(tracker_id)
83
60
  cover.fetch(tracker_id)
84
61
  end
@@ -116,10 +93,6 @@ module DeepCover
116
93
  rewriter.process
117
94
  end
118
95
 
119
- def has_executed?
120
- global[nb] != nil
121
- end
122
-
123
96
  def freeze
124
97
  unless frozen? # Guard against reentrance
125
98
  super
@@ -129,16 +102,10 @@ module DeepCover
129
102
  end
130
103
 
131
104
  def inspect
132
- %{#<DeepCover::CoveredCode "#{name}">}
105
+ %{#<DeepCover::CoveredCode "#{path}">}
133
106
  end
134
107
  alias_method :to_s, :inspect
135
108
 
136
- protected
137
-
138
- def global
139
- @@globals[tracker_global]
140
- end
141
-
142
109
  private
143
110
 
144
111
  def parser
@@ -1,12 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # TODO: must handle circular requires
4
-
5
3
  module DeepCover
6
4
  class CustomRequirer
7
5
  class LoadPathsSubset
8
- attr_reader :last_lookup_path
9
-
10
6
  def initialize(load_paths:, lookup_paths:)
11
7
  @original_load_paths = load_paths
12
8
  @cached_load_paths_subset = []
@@ -29,7 +25,7 @@ module DeepCover
29
25
 
30
26
  # E.g. '/a/b' => true when a lookup path is '/a/'
31
27
  def within_lookup?(full_path)
32
- @lookup_paths.any? { |p| full_path.start_with?(p) && @last_lookup_path = p }
28
+ @lookup_paths.any? { |p| full_path.start_with?(p) }
33
29
  end
34
30
 
35
31
  def exist?(full_path)
@@ -45,117 +41,182 @@ module DeepCover
45
41
  end
46
42
  end
47
43
 
44
+ class EveryLoadPaths
45
+ attr_reader :load_paths
46
+ def initialize(load_paths)
47
+ @load_paths = load_paths
48
+ end
49
+
50
+ def exist?(full_path)
51
+ File.exist?(full_path)
52
+ end
53
+
54
+ def within_lookup?(full_path)
55
+ true
56
+ end
57
+ end
58
+
48
59
  attr_reader :load_paths, :loaded_features, :filter
49
60
  def initialize(load_paths: $LOAD_PATH, loaded_features: $LOADED_FEATURES, lookup_paths: nil, &filter)
50
61
  @load_paths = load_paths
51
62
  lookup_paths ||= Dir.getwd
52
63
  lookup_paths = Array(lookup_paths)
53
- @load_paths_subset = LoadPathsSubset.new(load_paths: load_paths, lookup_paths: lookup_paths) unless lookup_paths.include? '/'
64
+
65
+ if lookup_paths.include?('/')
66
+ @load_paths_subset = EveryLoadPaths.new(load_paths)
67
+ else
68
+ @load_paths_subset = LoadPathsSubset.new(load_paths: load_paths, lookup_paths: lookup_paths)
69
+ end
70
+
54
71
  @loaded_features = loaded_features
55
72
  @filter = filter
73
+ @paths_being_required = Set.new
56
74
  end
57
75
 
58
76
  # Returns a path to an existing file or nil if none can be found.
59
- # The search follows how ruby search for files using the $LOAD_PATH
77
+ # The search follows how ruby search for files using the $LOAD_PATH, but limits
78
+ # those it checks based on the LoadPathsSubset.
60
79
  #
61
- # An absolute path is returned directly if it exists, otherwise nil
62
- # is returned without searching anywhere else.
63
- def resolve_path(path)
64
- path = File.absolute_path(path) if path.start_with?('./', '../')
80
+ # An absolute path is returned directly if it exists, otherwise nil is returned
81
+ # without searching anywhere else.
82
+ def resolve_path(path, extensions_to_try = ['.rb', '.so'])
83
+ if extensions_to_try
84
+ extensions_to_try = [''] if extensions_to_try.any? { |ext| path.end_with?(ext) }
85
+ else
86
+ extensions_to_try = ['']
87
+ end
65
88
 
66
89
  abs_path = File.absolute_path(path)
90
+ path = abs_path if path.start_with?('./', '../')
91
+
92
+ paths_with_ext = extensions_to_try.map { |ext| path + ext }
93
+
94
+ # Doing this check in every case instead of only for absolute_path because ruby has some
95
+ # built-in $LOADED_FEATURES which aren't an absolute path. Ex: enumerator.so, thread.rb
96
+ paths_with_ext.each do |path_with_ext|
97
+ return path_with_ext if @loaded_features.include?(path_with_ext)
98
+ end
99
+
67
100
  if path == abs_path
68
- path if (@load_paths_subset || File).exist?(path)
101
+ paths_with_ext.each do |path_with_ext|
102
+ return path_with_ext if File.exist?(path_with_ext)
103
+ end
69
104
  else
70
- (@load_paths_subset || self).load_paths.each do |load_path|
71
- possible_path = File.absolute_path(path, load_path)
105
+ possible_paths = paths_with_load_paths(paths_with_ext)
106
+ possible_paths.each do |possible_path|
107
+ return possible_path if @loaded_features.include?(possible_path)
108
+ end
72
109
 
73
- next unless (@load_paths_subset || File).exist?(possible_path)
110
+ possible_paths.each do |possible_path|
111
+ next unless File.exist?(possible_path)
74
112
  # Ruby 2.5 changed some behaviors of require related to symlinks in $LOAD_PATH
75
113
  # https://bugs.ruby-lang.org/issues/10222
76
114
  return File.realpath(possible_path) if RUBY_VERSION >= '2.5'
77
115
  return possible_path
78
116
  end
79
- nil
80
117
  end
118
+ nil
81
119
  end
82
120
 
83
121
  # Homemade #require to be able to instrument the code before it gets executed.
84
122
  # Returns true when everything went right. (Same as regular ruby)
85
123
  # Returns false when the found file was already required. (Same as regular ruby)
86
- # Throws :use_fallback in case caller should delegate to the default #require.
87
- # Reasons given could be:
124
+ # Calls &fallback_block with the reason as parameter if the work couldn't be done.
125
+ # The possible reasons are:
88
126
  # - :not_found if the file couldn't be found.
127
+ # - :not_in_covered_paths if the file is not in the paths to cover
89
128
  # - :cover_failed if DeepCover couldn't apply instrumentation the file found.
90
- # - :not_supported for files that are not supported (such as ike .so files)
129
+ # - :not_supported for files that are not supported (such as .so files)
91
130
  # - :skipped if the filter block returned `true`
92
- # Exceptions raised by the required code bubble up as normal.
93
- # It is *NOT* recommended to simply delegate to the default #require, since it
94
- # might not be safe to run part of the code again.
95
- def require(path)
131
+ # Exceptions raised by the required code bubble up as normal, except for
132
+ # SyntaxError, which is turned into a :cover_failed which calls the fallback_block.
133
+ def require(path) # &fallback_block
96
134
  path = path.to_s
97
- ext = File.extname(path)
98
- throw :use_fallback, :not_supported if ext == '.so'
99
- path += '.rb' if ext != '.rb'
100
- return false if @loaded_features.include?(path)
101
135
 
102
136
  found_path = resolve_path(path)
103
137
 
104
- throw :use_fallback, :not_found unless found_path
105
- return false if @loaded_features.include?(found_path)
138
+ if found_path
139
+ return false if @loaded_features.include?(found_path)
140
+ return false if @paths_being_required.include?(found_path)
141
+ end
142
+
143
+ DeepCover.autoload_tracker.wrap_require(path, found_path) do
144
+ # Either a problem with resolve_path, or a gem that will be added to the load_path by RubyGems
145
+ return yield(:not_found) unless found_path
106
146
 
107
- throw :use_fallback, :skipped if filter && filter.call(found_path)
147
+ begin
148
+ @paths_being_required.add(found_path)
149
+ return yield(:not_in_covered_paths) unless @load_paths_subset.within_lookup?(found_path)
150
+ return yield(:not_supported) if found_path.end_with?('.so')
151
+ return yield(:skipped) if filter && filter.call(found_path)
108
152
 
109
- cover_and_execute(found_path)
153
+ cover_and_execute(found_path) { |reason| return yield(reason) }
110
154
 
111
- @loaded_features << found_path
155
+ @loaded_features << found_path
156
+ ensure
157
+ @paths_being_required.delete(found_path)
158
+ end
159
+ end
112
160
  true
113
161
  end
114
162
 
163
+ ### Not currently used ###
115
164
  # Homemade #load to be able to instrument the code before it gets executed.
116
165
  # Note, this doesn't support the `wrap` parameter that ruby's #load has.
117
166
  # Same return/throw as CustomRequirer#require, except:
118
167
  # Cannot return false since #load doesn't care about a file already being executed.
119
- def load(path)
120
- found_path = resolve_path(path)
168
+ def load(path) # &fallback_block
169
+ found_path = resolve_path(path, nil)
121
170
 
122
171
  if found_path.nil?
123
172
  # #load has a final fallback of always trying relative to current work directory of process
124
173
  possible_path = File.absolute_path(path)
125
- found_path = possible_path if (@load_paths_subset || File).exist?(possible_path)
174
+ found_path = possible_path if File.exist?(possible_path)
126
175
  end
127
176
 
128
- throw :use_fallback, :not_found unless found_path
177
+ return yield(:not_found) unless found_path
129
178
 
130
- cover_and_execute(found_path)
179
+ cover_and_execute(found_path) { |reason| return yield(reason) }
131
180
 
132
181
  true
133
182
  end
134
183
 
184
+ def is_being_required?(path)
185
+ found_path = resolve_path(path)
186
+ @paths_being_required.include?(found_path)
187
+ end
188
+
135
189
  protected
136
190
 
137
- def cover_and_execute(path)
191
+ def paths_with_load_paths(paths)
192
+ paths.flat_map do |path|
193
+ @load_paths.map do |load_path|
194
+ File.absolute_path(path, load_path)
195
+ end
196
+ end
197
+ end
198
+
199
+ def cover_and_execute(path) # &fallback_block
138
200
  begin
139
- name = path.sub(%r{^#{@load_paths_subset.last_lookup_path}/}, '') if @load_paths_subset
140
- covered_code = DeepCover.coverage.covered_code(path, name: name)
201
+ covered_code = DeepCover.coverage.covered_code(path)
141
202
  rescue Parser::SyntaxError => e
142
203
  if e.message =~ /contains escape sequences incompatible with UTF-8/
143
204
  warn "Can't cover #{path} because of incompatible encoding (see issue #9)"
144
205
  else
145
206
  warn "The file #{path} can't be instrumented"
146
207
  end
147
- throw :use_fallback, :cover_failed
208
+ yield(:cover_failed)
209
+ raise "The fallback_block is supposed to either return or break, but didn't do either"
148
210
  end
149
- DeepCover.autoload_tracker.wrap_require(path) do
150
- begin
151
- covered_code.execute_code
152
- rescue ::SyntaxError => e
153
- warn ["DeepCover is getting confused with the file #{path} and it won't be instrumented.",
154
- 'Please report this error and provide the source code around the following:',
155
- e,
156
- ].join("\n")
157
- throw :use_fallback, :cover_failed
158
- end
211
+ begin
212
+ covered_code.execute_code
213
+ rescue ::SyntaxError => e
214
+ warn ["DeepCover is getting confused with the file #{path} and it won't be instrumented.",
215
+ 'Please report this error and provide the source code around the following:',
216
+ e,
217
+ ].join("\n")
218
+ yield(:cover_failed)
219
+ raise "The fallback_block is supposed to either return or break, but didn't do either"
159
220
  end
160
221
  covered_code
161
222
  end
@@ -4,8 +4,9 @@ module DeepCover
4
4
  module Load
5
5
  AUTOLOAD = %i[analyser autoload_tracker auto_run config
6
6
  coverage covered_code custom_requirer
7
+ tracker_hits_per_path tracker_storage_per_path
7
8
  flag_comment_associator memoize module_override node
8
- problem_with_diagnostic reporter
9
+ problem_with_diagnostic reporter tracker_bucket
9
10
  ]
10
11
 
11
12
  def load_absolute_basics
@@ -16,13 +17,15 @@ module DeepCover
16
17
  AUTOLOAD.each do |module_name|
17
18
  DeepCover.autoload(Tools::Camelize.camelize(module_name), "#{__dir__}/#{module_name}")
18
19
  end
19
- DeepCover.autoload :VERSION, 'deep_cover/version'
20
+ DeepCover.autoload :VERSION, "#{__dir__}/version"
20
21
  Object.autoload :Term, 'term/ansicolor'
21
22
  Object.autoload :Terminal, 'terminal-table'
22
23
  Object.autoload :YAML, 'yaml'
24
+ Object.autoload :Forwardable, 'forwardable'
23
25
  end
24
26
 
25
27
  def bootstrap
28
+ @bootstrapped ||= false # Avoid warning
26
29
  return if @bootstrapped
27
30
  require_relative 'backports'
28
31
  require_relative 'tools'
@@ -30,14 +33,14 @@ module DeepCover
30
33
  end
31
34
 
32
35
  def load_parser
36
+ @parser_loaded ||= false # Avoid warning
33
37
  return if @parser_loaded
34
- require 'parser'
35
38
  silence_warnings do
39
+ require 'parser'
36
40
  require 'parser/current'
37
41
  end
38
- require 'parser_tree_rewriter'
39
42
  require_relative_dir 'parser_ext'
40
- @parser_loaded
43
+ @parser_loaded = true
41
44
  end
42
45
 
43
46
  def load_pry
@@ -47,13 +50,14 @@ module DeepCover
47
50
  end
48
51
 
49
52
  def load_all
53
+ @all_loaded ||= false
50
54
  return if @all_loaded
51
55
  bootstrap
52
56
  load_parser
53
57
  AUTOLOAD.each do |module_name|
54
58
  DeepCover.const_get(Tools::Camelize.camelize(module_name))
55
59
  end
56
- DeepCover::VERSION # rubocop:disable Lint/Void
60
+ DeepCover.const_get(:VERSION)
57
61
  @all_loaded = true
58
62
  end
59
63
  end
@@ -33,9 +33,7 @@ module DeepCover
33
33
  end
34
34
 
35
35
  def memoize(*methods)
36
- @memoized ||= []
37
- @memoized |= methods
38
- @memoized.freeze
36
+ @memoized = (memoized | methods).freeze
39
37
 
40
38
  methods.each do |method|
41
39
  memoizer_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -10,6 +10,9 @@ module DeepCover
10
10
  def active=(active)
11
11
  each do |mod, method_name|
12
12
  mod.send :alias_method, method_name, :"#{method_name}_#{active ? 'with' : 'without'}_deep_cover"
13
+ if mod == ::Kernel
14
+ mod.send :private, method_name
15
+ end
13
16
  end
14
17
  end
15
18
 
@@ -18,6 +21,10 @@ module DeepCover
18
21
  each do |mod, method_name|
19
22
  mod.send :alias_method, :"#{method_name}_without_deep_cover", method_name
20
23
  mod.send :define_method, :"#{method_name}_with_deep_cover", instance_method(method_name)
24
+ if mod == ::Kernel
25
+ mod.send :private, :"#{method_name}_without_deep_cover"
26
+ mod.send :private, :"#{method_name}_with_deep_cover"
27
+ end
21
28
  end
22
29
  end
23
30