deep-cover-core 0.6.4 → 0.7.0

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/deep_cover_core.gemspec +1 -0
  3. data/lib/deep_cover.rb +3 -20
  4. data/lib/deep_cover/auto_run.rb +10 -35
  5. data/lib/deep_cover/autoload_tracker.rb +9 -5
  6. data/lib/deep_cover/base.rb +85 -12
  7. data/lib/deep_cover/basics.rb +15 -3
  8. data/lib/deep_cover/config.rb +36 -2
  9. data/lib/deep_cover/config_setter.rb +1 -1
  10. data/lib/deep_cover/core_ext/exec_callbacks.rb +15 -9
  11. data/lib/deep_cover/core_ext/instruction_sequence_load_iseq.rb +1 -1
  12. data/lib/deep_cover/coverage.rb +51 -41
  13. data/lib/deep_cover/covered_code.rb +54 -17
  14. data/lib/deep_cover/custom_requirer.rb +10 -10
  15. data/lib/deep_cover/global_variables.rb +32 -0
  16. data/lib/deep_cover/load.rb +25 -9
  17. data/lib/deep_cover/node/base.rb +6 -6
  18. data/lib/deep_cover/node/begin.rb +1 -2
  19. data/lib/deep_cover/node/block.rb +5 -1
  20. data/lib/deep_cover/node/case.rb +1 -1
  21. data/lib/deep_cover/node/empty_body.rb +1 -5
  22. data/lib/deep_cover/node/if.rb +3 -4
  23. data/lib/deep_cover/node/loops.rb +1 -1
  24. data/lib/deep_cover/node/mixin/flow_accounting.rb +1 -8
  25. data/lib/deep_cover/node/mixin/has_child.rb +3 -12
  26. data/lib/deep_cover/node/mixin/has_child_handler.rb +2 -5
  27. data/lib/deep_cover/node/mixin/has_tracker.rb +3 -7
  28. data/lib/deep_cover/node/module.rb +20 -23
  29. data/lib/deep_cover/node/root.rb +1 -1
  30. data/lib/deep_cover/node/send.rb +4 -5
  31. data/lib/deep_cover/node/short_circuit.rb +1 -1
  32. data/lib/deep_cover/persistence.rb +100 -0
  33. data/lib/deep_cover/problem_with_diagnostic.rb +2 -2
  34. data/lib/deep_cover/setup/clone_mode_entry_template.rb +66 -0
  35. data/lib/deep_cover/setup/deep_cover_auto_run.rb +21 -0
  36. data/lib/deep_cover/setup/deep_cover_config.rb +19 -0
  37. data/lib/deep_cover/setup/deep_cover_without_config.rb +15 -0
  38. data/lib/deep_cover/tools.rb +0 -2
  39. data/lib/deep_cover/tools/after_tests.rb +33 -0
  40. data/lib/deep_cover/tools/looks_like_rails_project.rb +13 -0
  41. data/lib/deep_cover/tools/our_coverage.rb +1 -1
  42. data/lib/deep_cover/tools/scan_match_datas.rb +1 -1
  43. data/lib/deep_cover/version.rb +4 -2
  44. metadata +24 -7
  45. data/lib/deep_cover/coverage/persistence.rb +0 -84
  46. data/lib/deep_cover/tracker_bucket.rb +0 -50
  47. data/lib/deep_cover/tracker_hits_per_path.rb +0 -35
  48. data/lib/deep_cover/tracker_storage.rb +0 -76
  49. data/lib/deep_cover/tracker_storage_per_path.rb +0 -34
@@ -9,12 +9,8 @@ module DeepCover
9
9
  class Coverage
10
10
  include Enumerable
11
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])
12
+ def initialize
13
+ @covered_code_per_path = {}
18
14
  end
19
15
 
20
16
  def covered_codes
@@ -26,15 +22,13 @@ module DeepCover
26
22
  end
27
23
 
28
24
  def covered_code?(path)
29
- @covered_code_index.include?(path)
25
+ @covered_code_per_path.include?(path)
30
26
  end
31
27
 
32
28
  def covered_code(path, **options)
33
29
  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)
30
+ @covered_code_per_path[path] ||= CoveredCode.new(path: path,
31
+ **options)
38
32
  end
39
33
 
40
34
  def covered_code_or_warn(path, **options)
@@ -51,7 +45,7 @@ module DeepCover
51
45
 
52
46
  def each
53
47
  return to_enum unless block_given?
54
- @tracker_storage_per_path.each_key do |path|
48
+ @covered_code_per_path.each_key do |path|
55
49
  begin
56
50
  cov_code = covered_code(path)
57
51
  rescue Parser::SyntaxError
@@ -62,8 +56,36 @@ module DeepCover
62
56
  self
63
57
  end
64
58
 
65
- def report(**options)
66
- case (reporter = options.fetch(:reporter, DEFAULTS[:reporter]).to_sym)
59
+ # If a file wasn't required, it won't be in the trackers. This adds those mossing files
60
+ def add_missing_covered_codes
61
+ top_level_path = DeepCover.config.paths.detect do |path|
62
+ next unless path.is_a?(String)
63
+ path = File.expand_path(path)
64
+ File.dirname(path) == path
65
+ end
66
+ if top_level_path
67
+ # One of the paths is a root path.
68
+ # Either a mistake, or the user wanted to check everything that gets loaded. Either way,
69
+ # we will probably hang from searching the files to load. (and then loading them, as there
70
+ # can be lots of ruby files) We prefer to warn the user and not do anything.
71
+ suggestion = "#{top_level_path}/**/*.rb"
72
+ warn ["Because the `paths` configured in DeepCover include #{top_level_path.inspect}, which is a root of your fs, ",
73
+ 'DeepCover will not attempt to find Ruby files that were not required. Your coverage report will not include ',
74
+ 'files that were not instrumented. This is to avoid extremely long wait times. If you actually want this to ',
75
+ "happen, then replace the specified `paths` with #{suggestion.inspect}.",
76
+ ].join
77
+ return
78
+ end
79
+
80
+ DeepCover.all_tracked_file_paths.each do |path|
81
+ covered_code(path, tracker_hits: :zeroes)
82
+ end
83
+ nil
84
+ end
85
+
86
+ def report(reporter: DEFAULTS[:reporter], **options)
87
+ add_missing_covered_codes
88
+ case reporter.to_sym
67
89
  when :html
68
90
  msg = if (output = options.fetch(:output, DEFAULTS[:output]))
69
91
  Reporter::HTML.report(self, **options)
@@ -85,41 +107,29 @@ module DeepCover
85
107
  end
86
108
  end
87
109
 
88
- def self.load(dest_path, dirname = 'deep_cover', with_trackers: true)
89
- Persistence.new(dest_path, dirname).load(with_trackers: with_trackers)
90
- end
110
+ def self.load(cache_directory = DeepCover.config.cache_directory)
111
+ tracker_hits_per_path = Persistence.new(cache_directory).load_trackers
112
+ coverage = Coverage.new
91
113
 
92
- def self.saved?(dest_path, dirname = 'deep_cover')
93
- Persistence.new(dest_path, dirname).saved?
94
- end
114
+ tracker_hits_per_path.each do |path, tracker_hits|
115
+ coverage.covered_code(path, tracker_hits: tracker_hits)
116
+ end
95
117
 
96
- def save(dest_path, dirname = 'deep_cover')
97
- Persistence.new(dest_path, dirname).save(self)
98
- self
118
+ coverage
99
119
  end
100
120
 
101
- def save_trackers(dest_path, dirname = 'deep_cover')
102
- Persistence.new(dest_path, dirname).save_trackers(@tracker_storage_per_path.tracker_hits_per_path)
103
- self
104
- end
121
+ def save_trackers
122
+ tracker_hits_per_path = @covered_code_per_path.map do |path, covered_code|
123
+ [path, covered_code.tracker_hits]
124
+ end
125
+ tracker_hits_per_path = tracker_hits_per_path.to_h
105
126
 
106
- def tracker_global
107
- @options.fetch(:tracker_global, DEFAULTS[:tracker_global])
127
+ DeepCover.persistence.save_trackers(tracker_hits_per_path)
128
+ self
108
129
  end
109
130
 
110
131
  def analysis(**options)
111
- Analysis.new(covered_codes, options)
112
- end
113
-
114
- private
115
-
116
- def marshal_dump
117
- {options: @options, tracker_storage_per_path: @tracker_storage_per_path}
118
- end
119
-
120
- def marshal_load(options:, tracker_storage_per_path:)
121
- initialize(**options)
122
- @tracker_storage_per_path = tracker_storage_per_path
132
+ Analysis.new(covered_codes, **options)
123
133
  end
124
134
  end
125
135
  end
@@ -5,25 +5,28 @@ module DeepCover
5
5
  load_parser
6
6
 
7
7
  class CoveredCode
8
- attr_accessor :covered_source, :buffer, :tracker_storage, :local_var, :path
8
+ attr_accessor :buffer, :local_var, :path
9
9
 
10
10
  def initialize(
11
11
  path: nil,
12
12
  source: nil,
13
13
  lineno: 1,
14
- tracker_global: DEFAULTS[:tracker_global],
15
- tracker_storage: TrackerBucket[tracker_global].create_storage,
16
- local_var: '_temp'
14
+ local_var: '_temp',
15
+ tracker_hits: nil
17
16
  )
18
17
  raise 'Must provide either path or source' unless path || source
19
18
 
20
19
  @path = path &&= Pathname(path)
21
20
  @buffer = Parser::Source::Buffer.new('', lineno)
22
21
  @buffer.source = source || path.read
23
- @tracker_count = 0
24
- @tracker_storage = tracker_storage
22
+ @index = nil # Set in #instrument_source
25
23
  @local_var = local_var
26
- @covered_source = instrument_source
24
+ @covered_source = nil # Set in #covered_source
25
+ @tracker_hits = tracker_hits # Loaded from global in #tracker_hits, or when received right away when loading data
26
+ @nb_allocated_trackers = 0
27
+ # We parse the code now so that problems happen early
28
+ covered_ast
29
+ @tracker_hits = Array.new(@nb_allocated_trackers, 0) if @tracker_hits == :zeroes
27
30
  end
28
31
 
29
32
  def lineno
@@ -40,7 +43,7 @@ module DeepCover
40
43
  end
41
44
 
42
45
  def execute_code(binding: DeepCover::GLOBAL_BINDING.dup)
43
- eval(@covered_source, binding, (@path || '<raw_code>').to_s, lineno) # rubocop:disable Security/Eval
46
+ eval(covered_source, binding, (@path || '<raw_code>').to_s, lineno) # rubocop:disable Security/Eval
44
47
  self
45
48
  end
46
49
 
@@ -50,10 +53,6 @@ module DeepCover
50
53
  end
51
54
  end
52
55
 
53
- def cover
54
- global[nb] ||= Array.new(@tracker_count, 0)
55
- end
56
-
57
56
  def line_coverage(**options)
58
57
  Analyser::PerLine.new(self, **options).results
59
58
  end
@@ -62,10 +61,6 @@ module DeepCover
62
61
  Analyser::PerChar.new(self, **options).results
63
62
  end
64
63
 
65
- def tracker_hits(tracker_id)
66
- cover.fetch(tracker_id)
67
- end
68
-
69
64
  def covered_ast
70
65
  root.main
71
66
  end
@@ -86,9 +81,45 @@ module DeepCover
86
81
  covered_ast.each_node(*args, &block)
87
82
  end
88
83
 
84
+ def setup_tracking_source
85
+ src = "(#{DeepCover.config.tracker_global}||={})[#{@index}]||=Array.new(#{@nb_allocated_trackers},0)"
86
+ src += ";(#{DeepCover.config.tracker_global}_p||={})[#{@index}]=#{path.to_s.inspect}" if path
87
+ src
88
+ end
89
+
90
+ def increment_tracker_source(tracker_id)
91
+ "#{DeepCover.config.tracker_global}[#{@index}][#{tracker_id}]+=1"
92
+ end
93
+
94
+ def allocate_trackers(nb_needed)
95
+ return @nb_allocated_trackers...@nb_allocated_trackers if nb_needed == 0
96
+ prev = @nb_allocated_trackers
97
+ @nb_allocated_trackers += nb_needed
98
+ prev...@nb_allocated_trackers
99
+ end
100
+
101
+ def tracker_hits
102
+ return @tracker_hits if @tracker_hits
103
+ global_trackers = DeepCover::GlobalVariables.trackers[@index]
104
+
105
+ return unless global_trackers
106
+
107
+ if global_trackers.size != @nb_allocated_trackers
108
+ raise "Cannot sync path: #{path.inspect}, global[#{@index}] is of size #{global_trackers.size} instead of expected #{@nb_allocated_trackers}"
109
+ end
110
+
111
+ @tracker_hits = global_trackers
112
+ end
113
+
114
+ def covered_source
115
+ @covered_source ||= instrument_source
116
+ end
117
+
89
118
  def instrument_source
119
+ @index ||= self.class.next_global_index
120
+
90
121
  rewriter = Parser::Source::TreeRewriter.new(@buffer)
91
- covered_ast.each_node(:postorder) do |node|
122
+ covered_ast.each_node do |node|
92
123
  node.rewriting_rules.each do |range, rule|
93
124
  prefix, _node, suffix = rule.partition('%{node}')
94
125
  prefix = yield prefix, node, range.begin, :prefix if block_given? && !prefix.empty?
@@ -111,6 +142,7 @@ module DeepCover
111
142
 
112
143
  def freeze
113
144
  unless frozen? # Guard against reentrance
145
+ tracker_hits
114
146
  super
115
147
  root.each_node(&:freeze)
116
148
  end
@@ -133,6 +165,11 @@ module DeepCover
133
165
  nil
134
166
  end
135
167
 
168
+ def self.next_global_index
169
+ @last_allocated_global_index ||= -1
170
+ @last_allocated_global_index += 1
171
+ end
172
+
136
173
  private
137
174
 
138
175
  def parser
@@ -22,12 +22,12 @@ module DeepCover
22
22
  #
23
23
  # An absolute path is returned directly if it exists, otherwise nil is returned
24
24
  # without searching anywhere else.
25
- def resolve_path(path, extensions_to_try = ['.rb', '.so'])
26
- if extensions_to_try
27
- extensions_to_try = [''] if extensions_to_try.any? { |ext| path.end_with?(ext) }
28
- else
29
- extensions_to_try = ['']
30
- end
25
+ def resolve_path(path, try_extensions: true)
26
+ extensions_to_try = if try_extensions && !REQUIRABLE_EXTENSIONS.include?(File.extname(path))
27
+ REQUIRABLE_EXTENSION_KEYS
28
+ else
29
+ ['']
30
+ end
31
31
 
32
32
  abs_path = File.absolute_path(path)
33
33
  path = abs_path if path.start_with?('./', '../')
@@ -89,8 +89,8 @@ module DeepCover
89
89
  return yield(:not_found) unless found_path
90
90
 
91
91
  @paths_being_required.add(found_path)
92
- return yield(:not_in_covered_paths) unless DeepCover.within_lookup_paths?(found_path)
93
- return yield(:not_supported) if found_path.end_with?('.so')
92
+ return yield(:not_in_covered_paths) unless DeepCover.tracked_file_path?(found_path)
93
+ return yield(:not_supported) if REQUIRABLE_EXTENSIONS[File.extname(found_path)] == :native_extension
94
94
  return yield(:skipped) if filter && filter.call(found_path)
95
95
 
96
96
  cover_and_execute(found_path) { |reason| return yield(reason) }
@@ -111,7 +111,7 @@ module DeepCover
111
111
  def load(path) # &fallback_block
112
112
  path = path.to_s
113
113
 
114
- found_path = resolve_path(path, nil)
114
+ found_path = resolve_path(path, try_extensions: false)
115
115
 
116
116
  if found_path.nil?
117
117
  # #load has a final fallback of always trying relative to current work directory
@@ -120,7 +120,7 @@ module DeepCover
120
120
  end
121
121
 
122
122
  return yield(:not_found) unless found_path
123
- return yield(:not_in_covered_paths) unless DeepCover.within_lookup_paths?(found_path)
123
+ return yield(:not_in_covered_paths) unless DeepCover.tracked_file_path?(found_path)
124
124
 
125
125
  cover_and_execute(found_path) { |reason| return yield(reason) }
126
126
 
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is used by projects cloned with clone mode. As such, special care must be taken to
4
+ # be compatible with any projects.
5
+ # THERE MUST NOT BE ANY USE/REQUIRE OF DEPENDENCIES OF DeepCover HERE
6
+ # See deep-cover/core_gem/lib/deep_cover/setup/clone_mode_entry_template.rb for explanation of
7
+ # clone mode and of this top_level_module stuff.
8
+ top_level_module = Thread.current['_deep_cover_top_level_module'] || Object # rubocop:disable Lint/UselessAssignment
9
+
10
+ module top_level_module::DeepCover # rubocop:disable Naming/ClassAndModuleCamelCase
11
+ module GlobalVariables
12
+ def self.trackers(global_name = nil)
13
+ @trackers ||= {}
14
+ global_name ||= DeepCover.config.tracker_global
15
+ @trackers[global_name] ||= eval("#{global_name} ||= {}") # rubocop:disable Security/Eval
16
+ end
17
+
18
+ def self.path_per_index(global_name = nil)
19
+ @path_per_index ||= {}
20
+ global_name ||= DeepCover.config.tracker_global
21
+ @path_per_index[global_name] ||= eval("#{global_name}_p ||= {}") # rubocop:disable Security/Eval
22
+ end
23
+
24
+ def self.tracker_hits_per_path(global_name = nil)
25
+ cur_trackers = self.trackers(global_name)
26
+ hits_per_path = path_per_index(global_name).map do |index, path|
27
+ [path, cur_trackers[index]]
28
+ end
29
+ hits_per_path.to_h
30
+ end
31
+ end
32
+ end
@@ -4,9 +4,8 @@ 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
8
- flag_comment_associator memoize module_override node
9
- problem_with_diagnostic reporter tracker_bucket
7
+ flag_comment_associator global_variables memoize module_override node
8
+ persistence problem_with_diagnostic reporter
10
9
  ]
11
10
 
12
11
  def load_absolute_basics
@@ -18,10 +17,27 @@ module DeepCover
18
17
  DeepCover.autoload(Tools::Camelize.camelize(module_name), "#{__dir__}/#{module_name}")
19
18
  end
20
19
  DeepCover.autoload :VERSION, "#{__dir__}/version"
21
- Object.autoload :Term, 'term/ansicolor'
22
- Object.autoload :Terminal, 'terminal-table'
23
- Object.autoload :YAML, 'yaml'
20
+
24
21
  Object.autoload :Forwardable, 'forwardable'
22
+ Object.autoload :YAML, 'yaml'
23
+
24
+ # In ruby 2.2 and less, autoload doesn't work for gems which are not already on the `$LOAD_PATH`.
25
+ # The fix is to just require right away for those rubies
26
+ #
27
+ # Low-level: autoload not working for gems not on the `$LOAD_PATH` is because those rubies don't
28
+ # call the regular `#require` when triggering an autoload, and the gem system monkey-patches `#require`
29
+ # so that when a file is not found in the `$LOAD_PATH`, but can be found in an existing gem, that gem's
30
+ # path is added to the `$LOAD_PATH`
31
+ {JSON: 'json',
32
+ Term: 'term/ansicolor',
33
+ Terminal: 'terminal-table',
34
+ }.each do |const, require_path|
35
+ if RUBY_VERSION < '2.3'
36
+ require require_path
37
+ else
38
+ Object.autoload const, require_path
39
+ end
40
+ end
25
41
  end
26
42
 
27
43
  def bootstrap
@@ -35,7 +51,7 @@ module DeepCover
35
51
  def load_parser
36
52
  @parser_loaded ||= false # Avoid warning
37
53
  return if @parser_loaded
38
- silence_warnings do
54
+ Tools.silence_warnings do
39
55
  require 'parser'
40
56
  require 'parser/current'
41
57
  end
@@ -44,8 +60,8 @@ module DeepCover
44
60
  end
45
61
 
46
62
  def load_pry
47
- silence_warnings do # Avoid "WARN: Unresolved specs during Gem::Specification.reset"
48
- require 'pry' # after `pry` calls `Gem.refresh`
63
+ Tools.silence_warnings do # Avoid "WARN: Unresolved specs during Gem::Specification.reset"
64
+ require 'pry' # after `pry` calls `Gem.refresh`
49
65
  end
50
66
  end
51
67
 
@@ -114,17 +114,17 @@ module DeepCover
114
114
  end
115
115
 
116
116
  def type
117
- return base_node.type if base_node.respond_to? :type
117
+ return base_node.type if base_node
118
118
  self.class.name.split('::').last.to_sym
119
119
  end
120
120
 
121
- def each_node(order = :postorder, &block)
122
- return to_enum :each_node, order unless block_given?
123
- yield self unless order == :postorder
121
+ # Yields its children and itself
122
+ def each_node(&block)
123
+ return to_enum :each_node unless block_given?
124
124
  children_nodes.each do |child|
125
- child.each_node(order, &block)
125
+ child.each_node(&block)
126
126
  end
127
- yield self if order == :postorder
127
+ yield self
128
128
  self
129
129
  end
130
130
 
@@ -18,8 +18,7 @@ module DeepCover
18
18
  when 'else', '#{'
19
19
  %i[begin end]
20
20
  else
21
- warn 'Unknown context for Begin node'
22
- []
21
+ raise "Unknown context for Begin node: #{loc_hash[:begin].source}"
23
22
  end
24
23
  end
25
24
  end
@@ -38,8 +38,12 @@ module DeepCover
38
38
  is_statement: true
39
39
  executed_loc_keys # none
40
40
 
41
+ def execution_count
42
+ call.execution_count
43
+ end
44
+
41
45
  def children_nodes_in_flow_order
42
- [call, args] # Similarly to a def, the body is actually not part of the flow of this node...
46
+ [call] # Similarly to a def, the body (and Args) are actually not part of the flow of this node...
43
47
  end
44
48
 
45
49
  alias_method :rewrite_for_completion, :rewrite