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