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
@@ -89,7 +89,7 @@ module DeepCover
89
89
  whens.map(&:body) << self.else
90
90
  end
91
91
 
92
- def branches_summary(of_branches = branches)
92
+ def branches_summary(of_branches)
93
93
  texts = []
94
94
  n = of_branches.size
95
95
  if of_branches.include? self.else
@@ -7,10 +7,6 @@ module DeepCover
7
7
  super(base_node, parent: parent, index: index, base_children: [])
8
8
  end
9
9
 
10
- def type
11
- :EmptyBody
12
- end
13
-
14
10
  def loc_hash
15
11
  return {} if @position == true
16
12
  {expression: @position}
@@ -21,7 +17,7 @@ module DeepCover
21
17
  end
22
18
 
23
19
  # When parent rewrites us, the %{node} must always be at the beginning because our location can
24
- # also be rewritten by out parent, and we want the rewrite to be after it.
20
+ # also be rewritten by our parent, and we want the rewrite to be after it.
25
21
  def rewriting_rules
26
22
  rules = super
27
23
  rules.map do |expression, rule|
@@ -19,7 +19,7 @@ module DeepCover
19
19
  executed_loc_keys :keyword, :question
20
20
 
21
21
  def child_can_be_empty(child, name)
22
- return false if name == :condition || style == :ternary
22
+ raise 'Unexpected empty body' if name == :condition || style == :ternary
23
23
  if (name == :true_branch) == [:if, :elsif].include?(style)
24
24
  (base_node.loc.begin || base_node.children[0].loc.expression.succ).end
25
25
  elsif has_else?
@@ -33,7 +33,7 @@ module DeepCover
33
33
  [true_branch, false_branch]
34
34
  end
35
35
 
36
- def branches_summary(of_branches = branches)
36
+ def branches_summary(of_branches)
37
37
  of_branches.map do |jump|
38
38
  "#{'implicit ' if jump.is_a?(EmptyBody) && !has_else?}#{jump == false_branch ? 'falsy' : 'truthy'} branch"
39
39
  end.join(' and ')
@@ -58,13 +58,12 @@ module DeepCover
58
58
  end
59
59
 
60
60
  def deepest_elsif_node
61
- return if style != :elsif
61
+ raise 'Not an elsif' if style != :elsif
62
62
  return self if loc_hash[:else] && loc_hash[:else].source == 'else'
63
63
  return self if false_branch.is_a?(EmptyBody)
64
64
  false_branch.deepest_elsif_node
65
65
  end
66
66
 
67
-
68
67
  def has_else?
69
68
  !!base_node.loc.to_hash[:else]
70
69
  end
@@ -4,7 +4,7 @@ module DeepCover
4
4
  class Node
5
5
  class For < Node
6
6
  has_tracker :body
7
- has_child assignments: [Mlhs, VariableAssignment], flow_entry_count: -> { body.flow_entry_count if body }
7
+ has_child assignments: [Mlhs, VariableAssignment], flow_entry_count: -> { body.flow_entry_count }
8
8
  has_child iterable: [Node], flow_entry_count: -> { flow_entry_count }
9
9
  has_child body: Node,
10
10
  can_be_empty: -> { base_node.loc.end.begin },
@@ -9,9 +9,7 @@ module DeepCover
9
9
 
10
10
  # Returns true iff it is executable and if was successfully executed
11
11
  def was_executed?
12
- # There is a rare case of non executable nodes that have important data in flow_entry_count / flow_completion_count,
13
- # like `if cond; end`, so make sure it's actually executable first...
14
- executable? && execution_count > 0
12
+ execution_count > 0
15
13
  end
16
14
 
17
15
  # Returns the control flow entered the node.
@@ -61,11 +59,6 @@ module DeepCover
61
59
  flow_entry_count
62
60
  end
63
61
  end
64
-
65
- # Returns the counts in a hash
66
- def counts
67
- {flow_entry: flow_entry_count, flow_completion: flow_completion_count, execution: execution_count}
68
- end
69
62
  end
70
63
  end
71
64
  end
@@ -26,12 +26,12 @@ module DeepCover
26
26
  end
27
27
 
28
28
  module ClassMethods
29
- def has_child(rest_: false, refine_: false, **args)
29
+ def has_child(rest_: false, **args)
30
30
  raise "Needs exactly one custom named argument, got #{args.size}" if args.size != 1
31
31
  name, types = args.first
32
32
  raise TypeError, "Expect a Symbol for name, got a #{name.class} (#{name.inspect})" unless name.is_a?(Symbol)
33
- update_children_const(name, rest: rest_) unless refine_
34
- define_accessor(name) unless refine_
33
+ update_children_const(name, rest: rest_)
34
+ define_accessor(name)
35
35
  add_runtime_check(name, types)
36
36
  self
37
37
  end
@@ -40,13 +40,6 @@ module DeepCover
40
40
  has_child(**args, rest_: true)
41
41
  end
42
42
 
43
- def refine_child(child_name = nil, **args)
44
- if child_name
45
- args = {child_name => self::CHILDREN_TYPES.fetch(child_name), **args}
46
- end
47
- has_child(**args, refine_: true)
48
- end
49
-
50
43
  def child_index_to_name(index, nb_children)
51
44
  self::CHILDREN.each do |name, i|
52
45
  return name if i == index || (i == index - nb_children) ||
@@ -91,8 +84,6 @@ module DeepCover
91
84
  expected.any? { |exp| node_matches_type?(node, exp) }
92
85
  when Class
93
86
  node.is_a?(expected)
94
- when Symbol
95
- node.is_a?(Node) && node.type == expected
96
87
  else
97
88
  raise "Unrecognized expected type #{expected}"
98
89
  end
@@ -56,11 +56,8 @@ module DeepCover
56
56
  when Symbol
57
57
  define_method(method_name) do |*args|
58
58
  arity = method(action).arity
59
- if arity < 0
60
- send(action, *args)
61
- else
62
- send(action, *args[0...arity])
63
- end
59
+ raise NotImplementedError if arity < 0
60
+ send(action, *args[0...arity])
64
61
  end
65
62
  when Proc
66
63
  define_method(method_name, &action)
@@ -9,14 +9,10 @@ module DeepCover
9
9
  TRACKERS = {}
10
10
 
11
11
  def initialize(*)
12
- @tracker_offset = tracker_storage.allocate_trackers(self.class::TRACKERS.size).begin
12
+ @tracker_offset = covered_code.allocate_trackers(self.class::TRACKERS.size).begin
13
13
  super
14
14
  end
15
15
 
16
- def tracker_storage
17
- covered_code.tracker_storage
18
- end
19
-
20
16
  def tracker_sources
21
17
  self.class::TRACKERS.map do |name, _|
22
18
  [:"#{name}_tracker", send(:"#{name}_tracker_source")]
@@ -33,10 +29,10 @@ module DeepCover
33
29
  i = self::TRACKERS[name] = self::TRACKERS.size
34
30
  class_eval <<-EVAL, __FILE__, __LINE__ + 1
35
31
  def #{name}_tracker_source
36
- tracker_storage.tracker_source(@tracker_offset + #{i})
32
+ covered_code.increment_tracker_source(@tracker_offset + #{i})
37
33
  end
38
34
  def #{name}_tracker_hits
39
- tracker_storage[@tracker_offset + #{i}]
35
+ covered_code.tracker_hits[@tracker_offset + #{i}]
40
36
  end
41
37
  EVAL
42
38
  end
@@ -21,46 +21,43 @@ module DeepCover
21
21
  end
22
22
  end
23
23
 
24
- class Module < Node
24
+ def self.define_module_class
25
25
  check_completion
26
26
  has_tracker :body_entry
27
- has_child const: {const: ModuleName}
27
+ yield
28
28
  has_child body: Node,
29
29
  can_be_empty: -> { base_node.loc.end.begin },
30
- rewrite: '%{body_entry_tracker};%{local}=nil;%{node}',
30
+ rewrite: '%{body_entry_tracker};%{node}',
31
31
  is_statement: true,
32
32
  flow_entry_count: :body_entry_tracker_hits
33
33
  executed_loc_keys :keyword
34
34
 
35
- def execution_count
36
- body_entry_tracker_hits
35
+ class_eval do
36
+ def execution_count # Overrides ExecutedAfterChildren
37
+ body_entry_tracker_hits
38
+ end
37
39
  end
38
40
  end
39
41
 
40
- class Class < Node
41
- check_completion
42
- has_tracker :body_entry
43
- has_child const: {const: ModuleName}
44
- has_child inherit: [Node, nil] # TODO
45
- has_child body: Node,
46
- can_be_empty: -> { base_node.loc.end.begin },
47
- rewrite: '%{body_entry_tracker};%{node}',
48
- is_statement: true,
49
- flow_entry_count: :body_entry_tracker_hits
50
- executed_loc_keys :keyword
42
+ class Module < Node
43
+ define_module_class do
44
+ has_child const: {const: ModuleName}
45
+ end
46
+ end
51
47
 
52
- def execution_count
53
- body_entry_tracker_hits
48
+ class Class < Node
49
+ define_module_class do
50
+ has_child const: {const: ModuleName}
51
+ has_child inherit: [Node, nil] # TODO
54
52
  end
55
53
  end
56
54
 
57
55
  # class << foo
58
56
  class Sclass < Node
59
- has_child object: Node
60
- has_child body: Node,
61
- can_be_empty: -> { base_node.loc.end.begin },
62
- is_statement: true
63
- # TODO
57
+ define_module_class do
58
+ has_child object: Node
59
+ end
60
+ executed_loc_keys :keyword, :operator
64
61
  end
65
62
  end
66
63
  end
@@ -7,7 +7,7 @@ module DeepCover
7
7
  can_be_empty: -> { Parser::Source::Range.new(covered_code.buffer, 0, 0) },
8
8
  is_statement: true,
9
9
  rewrite: -> {
10
- "#{tracker_storage.setup_source};%{root_tracker};%{local}=nil;%{node}"
10
+ "#{covered_code.setup_tracking_source};%{root_tracker};%{local}=nil;%{node}"
11
11
  }
12
12
  attr_reader :covered_code
13
13
  alias_method :flow_entry_count, :root_tracker_hits
@@ -122,6 +122,9 @@ module DeepCover
122
122
 
123
123
  executed_loc_keys :dot
124
124
 
125
+ extend Forwardable
126
+ def_delegators :actual_send, :message, :arguments
127
+
125
128
  def has_block?
126
129
  parent.is_a?(Block) && parent.child_index_to_name(index) == :call
127
130
  end
@@ -134,17 +137,13 @@ module DeepCover
134
137
  receiver.flow_completion_count
135
138
  end
136
139
 
137
- def message
138
- actual_send.message
139
- end
140
-
141
140
  def branches
142
141
  [TrivialBranch.new(condition: receiver, other_branch: actual_send),
143
142
  actual_send,
144
143
  ]
145
144
  end
146
145
 
147
- def branches_summary(of_branches = branches)
146
+ def branches_summary(of_branches)
148
147
  of_branches.map do |jump|
149
148
  jump == actual_send ? 'safe send' : 'nil shortcut'
150
149
  end.join(' and ')
@@ -18,7 +18,7 @@ module DeepCover
18
18
  ]
19
19
  end
20
20
 
21
- def branches_summary(of_branches = branches)
21
+ def branches_summary(of_branches)
22
22
  of_branches.map do |jump|
23
23
  if jump == conditional
24
24
  'right-hand side'
@@ -0,0 +1,100 @@
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
+ require 'securerandom'
12
+ class Persistence
13
+ BASENAME = 'coverage.dc'
14
+ TRACKER_TEMPLATE = 'trackers%{unique}.dct'
15
+
16
+ attr_reader :dir_path
17
+ def initialize(cache_directory)
18
+ @dir_path = Pathname(cache_directory).expand_path
19
+ end
20
+
21
+ def save_trackers(tracker_hits_per_path)
22
+ create_directory_if_needed
23
+ basename = format(TRACKER_TEMPLATE, unique: SecureRandom.urlsafe_base64)
24
+
25
+ dir_path.join(basename).binwrite(JSON.dump(
26
+ version: VERSION,
27
+ tracker_hits_per_path: tracker_hits_per_path,
28
+ ))
29
+ end
30
+
31
+ # returns a TrackerHitsPerPath
32
+ def load_trackers
33
+ tracker_hits_per_path_hashes = tracker_files.map do |full_path|
34
+ JSON.parse(full_path.binread).transform_keys(&:to_sym).yield_self do |version:, tracker_hits_per_path:|
35
+ raise "dump version mismatch: #{version}, currently #{VERSION}" unless version == VERSION
36
+ tracker_hits_per_path
37
+ end
38
+ end
39
+
40
+ self.class.merge_tracker_hits_per_paths(*tracker_hits_per_path_hashes)
41
+ end
42
+
43
+ def merge_persisted_trackers
44
+ tracker_hits_per_path = load_trackers
45
+ return if tracker_hits_per_path.empty?
46
+ tracker_files_before = tracker_files
47
+ save_trackers(tracker_hits_per_path)
48
+ tracker_files_before.each(&:delete)
49
+ end
50
+
51
+ def delete_trackers
52
+ tracker_files.each(&:delete)
53
+ end
54
+
55
+ def clear_directory
56
+ delete_trackers
57
+ begin
58
+ dir_path.rmdir
59
+ rescue SystemCallError # rubocop:disable Lint/HandleExceptions
60
+ end
61
+ end
62
+
63
+ def self.merge_tracker_hits_per_paths(*tracker_hits_per_path_hashes)
64
+ return {} if tracker_hits_per_path_hashes.empty?
65
+
66
+ result = tracker_hits_per_path_hashes[0].transform_values(&:dup)
67
+
68
+ tracker_hits_per_path_hashes[1..-1].each do |tracker_hits_per_path|
69
+ tracker_hits_per_path.each do |path, tracker_hits|
70
+ matching_result = result[path]
71
+ if matching_result.nil?
72
+ result[path] = tracker_hits.dup
73
+ next
74
+ end
75
+
76
+ if matching_result.size != tracker_hits.size
77
+ raise "Attempting to merge trackers of different sizes: #{matching_result.size} vs #{tracker_hits.size}, for path #{path}"
78
+ end
79
+
80
+ tracker_hits.each_with_index do |nb_hits, i|
81
+ matching_result[i] += nb_hits
82
+ end
83
+ end
84
+ end
85
+
86
+ result
87
+ end
88
+
89
+ private
90
+
91
+ def create_directory_if_needed
92
+ dir_path.mkpath
93
+ end
94
+
95
+ def tracker_files
96
+ basename = format(TRACKER_TEMPLATE, unique: '*')
97
+ Pathname.glob(dir_path.join(basename))
98
+ end
99
+ end
100
+ end
@@ -49,11 +49,11 @@ module DeepCover
49
49
  first_index = line_range.begin - nb_context_line - buffer.first_line
50
50
  first_index = 0 if first_index < 0
51
51
  last_index = line_range.end + nb_context_line - buffer.first_line
52
- last_index = 0 if last_index < 0
53
52
 
54
53
  lines = buffer.source_lines[first_index..last_index]
54
+ lines.pop if lines.last.empty?
55
55
 
56
- Tools.number_lines(lines, lineno: buffer.first_line, bad_linenos: line_range.to_a)
56
+ Tools.number_lines(lines, lineno: buffer.first_line + first_index, bad_linenos: line_range.to_a)
57
57
  end
58
58
 
59
59
  def buffer
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is used by `deep-cover clone`.
4
+ #
5
+ # This is a template for the setup code that clone mode makes the cloned code execute.
6
+ # We don't actually load deep-cover at all. We do need to little parts of it in a non name-clashing way,
7
+ # so this file takes care of this.
8
+ #
9
+ # Clone mode will copy this file and gsub some "global variables" with their actual values.
10
+ # The the fake global variables are: $_cache_directory, $_core_gem_lib_directory, $_global_name
11
+ # This template is important because with the anonymous top-level module, it becomes problematic
12
+ # to call the content of this file from the place that required it. Doing it with a template means
13
+ # that it is not necessary to do anything beyond requiring the file. It feels cleaner than the
14
+ # alternatives I thought of.
15
+ #
16
+ # It is important to avoid using any of deep-cover's dependencies (from other gems), because they may not be in
17
+ # the Gemfile of the project being cloned, and so will not be found.
18
+ #
19
+ # In order to avoid any possible name clashing with a DeepCover that would be used in the program
20
+ # being covered with clone mode, we create a unique top-level module under which we nest everything
21
+ # that we need. (The main use-case for this is when doing coverage of DeepCover itself)
22
+ top_level_module = Module.new
23
+
24
+ module top_level_module::DeepCover # rubocop:disable Naming/ClassAndModuleCamelCase
25
+ def self.setup
26
+ Tools::AfterTests.after_tests { save }
27
+ ExecCallbacks.before_exec { save }
28
+ end
29
+
30
+ def self.save
31
+ return if saved?
32
+
33
+ Persistence.new($_cache_directory).save_trackers(GlobalVariables.tracker_hits_per_path($_global_name))
34
+ @saved = true
35
+ end
36
+
37
+ def self.saved?
38
+ @saved ||= false
39
+ end
40
+ end
41
+
42
+ # The files that we need all use this top-level module when it is defined. We avoid any kind of race-condition
43
+ # by using Thread.current instead of a global variable. (Think of what would happen if another thread decided to
44
+ # require 'deep-cover' while this thread is loading it's special top-level dependencies)
45
+ Thread.current['_deep_cover_top_level_module'] = top_level_module
46
+ # To avoid any issue, we load the files instead of requiring them. Since this file is being required, the
47
+ # result is the same, but if, for any reason, this file or another template gets executed again, then there will be
48
+ # a new top-level module, so we must fill it correctly, which means loading the files again.
49
+ load $_core_gem_lib_directory + '/deep_cover/global_variables.rb'
50
+ load $_core_gem_lib_directory + '/deep_cover/persistence.rb'
51
+ load $_core_gem_lib_directory + '/deep_cover/version.rb'
52
+ load $_core_gem_lib_directory + '/deep_cover/core_ext/exec_callbacks.rb'
53
+ load $_core_gem_lib_directory + '/deep_cover/tools/after_tests.rb'
54
+
55
+ Thread.current['_deep_cover_top_level_module'] = nil
56
+
57
+ Object.autoload :JSON, 'json'
58
+
59
+ # This is really just to make debugging less of a pain, it gives a way to the code to access the anonymous top-level module
60
+ module DeepCover
61
+ CLONE_MODE_ENTRY_TOP_LEVEL_MODULES ||= []
62
+ end
63
+ DeepCover::CLONE_MODE_ENTRY_TOP_LEVEL_MODULES << top_level_module
64
+
65
+ # Activate everything!
66
+ top_level_module::DeepCover.setup