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