deep-cover-core 0.6.4 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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