deep-cover 0.1.1 → 0.1.2

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.travis.yml +3 -1
  4. data/Rakefile +1 -1
  5. data/bin/selfcov +1 -1
  6. data/deep_cover.gemspec +2 -1
  7. data/lib/deep_cover/analyser.rb +1 -2
  8. data/lib/deep_cover/analyser/base.rb +20 -6
  9. data/lib/deep_cover/analyser/covered_code_source.rb +0 -12
  10. data/lib/deep_cover/analyser/node.rb +11 -1
  11. data/lib/deep_cover/analyser/optionally_covered.rb +14 -0
  12. data/lib/deep_cover/auto_run.rb +36 -32
  13. data/lib/deep_cover/backports.rb +9 -0
  14. data/lib/deep_cover/base.rb +8 -1
  15. data/lib/deep_cover/cli/debugger.rb +6 -4
  16. data/lib/deep_cover/cli/deep_cover.rb +37 -6
  17. data/lib/deep_cover/cli/instrumented_clone_reporter.rb +50 -32
  18. data/lib/deep_cover/config.rb +16 -7
  19. data/lib/deep_cover/coverage.rb +8 -9
  20. data/lib/deep_cover/covered_code.rb +17 -18
  21. data/lib/deep_cover/node/base.rb +28 -4
  22. data/lib/deep_cover/node/branch.rb +10 -7
  23. data/lib/deep_cover/node/def.rb +2 -2
  24. data/lib/deep_cover/node/empty_body.rb +1 -1
  25. data/lib/deep_cover/node/mixin/can_augment_children.rb +1 -2
  26. data/lib/deep_cover/node/mixin/execution_location.rb +9 -6
  27. data/lib/deep_cover/node/mixin/has_child.rb +16 -17
  28. data/lib/deep_cover/node/mixin/has_tracker.rb +2 -6
  29. data/lib/deep_cover/node/mixin/rewriting.rb +9 -8
  30. data/lib/deep_cover/node/send.rb +57 -48
  31. data/lib/deep_cover/node/{boolean.rb → short_circuit.rb} +0 -0
  32. data/lib/deep_cover/reporter/istanbul.rb +2 -3
  33. data/lib/deep_cover/tools.rb +2 -1
  34. data/lib/deep_cover/tools/dasherize.rb +8 -0
  35. data/lib/deep_cover/tools/dump_covered_code.rb +12 -6
  36. data/lib/deep_cover/tools/format_char_cover.rb +2 -3
  37. data/lib/deep_cover/tools/slice.rb +7 -0
  38. data/lib/deep_cover/version.rb +1 -1
  39. metadata +7 -18
@@ -6,8 +6,8 @@ module DeepCover
6
6
  allow_partial: false,
7
7
  }
8
8
 
9
- def initialize
10
- @options = copy(DEFAULTS)
9
+ def initialize(**options)
10
+ @options = copy(DEFAULTS.merge(options))
11
11
  end
12
12
 
13
13
  def to_hash
@@ -16,12 +16,14 @@ module DeepCover
16
16
  alias_method :to_h, :to_hash
17
17
 
18
18
  def ignore_uncovered(*keywords)
19
- @options[:ignore_uncovered] -= keywords
19
+ check_uncovered(keywords)
20
+ @options[:ignore_uncovered] += keywords
20
21
  self
21
22
  end
22
23
 
23
24
  def detect_uncovered(*keywords)
24
- @options[:ignore_uncovered] += keywords
25
+ check_uncovered(keywords)
26
+ @options[:ignore_uncovered] -= keywords
25
27
  self
26
28
  end
27
29
 
@@ -31,20 +33,27 @@ module DeepCover
31
33
  end
32
34
 
33
35
  private
36
+ def check_uncovered(keywords)
37
+ unknown = keywords - Analyser.optionally_covered
38
+ raise ArgumentError, "unknown options: #{unknown.join(', ')}" unless unknown.empty?
39
+ end
40
+
34
41
  def copy(h)
35
42
  h.dup.transform_values(&:dup)
36
43
  end
37
44
 
38
45
  module Setter
39
- def configure(&block)
46
+ def config
40
47
  @config ||= Config.new
48
+ end
41
49
 
50
+ def configure(&block)
42
51
  raise "Must provide a block" unless block
43
52
  case block.arity
44
53
  when 0
45
- @config.instance_eval(&block)
54
+ config.instance_eval(&block)
46
55
  when 1
47
- block.call(@config)
56
+ block.call(config)
48
57
  end
49
58
  end
50
59
  end
@@ -4,7 +4,6 @@ module DeepCover
4
4
  require 'parser/current'
5
5
  end
6
6
  require 'pry'
7
- require 'pathname'
8
7
  require_relative 'covered_code'
9
8
  require 'securerandom'
10
9
 
@@ -29,9 +28,9 @@ module DeepCover
29
28
  covered_code(filename).line_coverage(**options)
30
29
  end
31
30
 
32
- def covered_code(path)
31
+ def covered_code(path, **options)
33
32
  raise 'path must be an absolute path' unless Pathname.new(path).absolute?
34
- @covered_codes[path] ||= CoveredCode.new(path: path, **@options)
33
+ @covered_codes[path] ||= CoveredCode.new(path: path, **options, **@options)
35
34
  end
36
35
 
37
36
  def each
@@ -57,7 +56,7 @@ module DeepCover
57
56
 
58
57
  def report_istanbul(output: nil, **options)
59
58
  dir = output_istanbul(**options).dirname
60
- if output
59
+ unless [nil, '', 'false'].include? output
61
60
  output = File.expand_path(output)
62
61
  html = "--reporter=html --report-dir='#{output}' && open '#{output}/index.html'"
63
62
  end
@@ -89,8 +88,8 @@ module DeepCover
89
88
  end
90
89
  end
91
90
 
92
- def self.load(dest_path, dirname = 'deep_cover')
93
- Persistence.new(dest_path, dirname).load
91
+ def self.load(dest_path, dirname = 'deep_cover', with_trackers: true)
92
+ Persistence.new(dest_path, dirname).load(with_trackers: with_trackers)
94
93
  end
95
94
 
96
95
  def self.saved?(dest_path, dirname = 'deep_cover')
@@ -108,7 +107,7 @@ module DeepCover
108
107
  end
109
108
 
110
109
  def tracker_global
111
- @options[:tracker_global]
110
+ @options.fetch(:tracker_global, CoveredCode::DEFAULT_TRACKER_GLOBAL)
112
111
  end
113
112
 
114
113
  class Persistence
@@ -120,9 +119,9 @@ module DeepCover
120
119
  @dir_path = Pathname(dest_path).join(dirname).expand_path
121
120
  end
122
121
 
123
- def load
122
+ def load(with_trackers: true)
124
123
  saved?
125
- load_trackers
124
+ load_trackers if with_trackers
126
125
  load_coverage
127
126
  end
128
127
 
@@ -1,10 +1,12 @@
1
1
  module DeepCover
2
2
  class CoveredCode
3
- attr_accessor :covered_source, :buffer, :tracker_global, :local_var
3
+ DEFAULT_TRACKER_GLOBAL = '$_cov'
4
+
5
+ attr_accessor :covered_source, :buffer, :tracker_global, :local_var, :name
4
6
  @@counter = 0
5
7
  @@globals = Hash.new{|h, global| h[global] = eval("#{global} ||= {}") }
6
8
 
7
- def initialize(path: nil, source: nil, lineno: nil, tracker_global: '$_cov', local_var: '_temp')
9
+ def initialize(path: nil, source: nil, lineno: nil, tracker_global: DEFAULT_TRACKER_GLOBAL, local_var: '_temp', name: nil)
8
10
  raise "Must provide either path or source" unless path || source
9
11
 
10
12
  @buffer = ::Parser::Source::Buffer.new(path)
@@ -17,6 +19,7 @@ module DeepCover
17
19
  @tracker_count = 0
18
20
  @tracker_global = tracker_global
19
21
  @local_var = local_var
22
+ @name = name || (source ? '(source)' : File.basename(path))
20
23
  @covered_source = instrument_source
21
24
  end
22
25
 
@@ -24,10 +27,6 @@ module DeepCover
24
27
  @buffer.name || "(source: '#{@buffer.source[0..20]}...')"
25
28
  end
26
29
 
27
- def name
28
- @buffer.name ? File.basename(@buffer.name) : "(source)"
29
- end
30
-
31
30
  def nb_lines
32
31
  @nb_lines ||= begin
33
32
  lines = buffer.source_lines
@@ -95,7 +94,7 @@ module DeepCover
95
94
 
96
95
  def root
97
96
  @root ||= begin
98
- ast = Parser::CurrentRuby.new.parse(@buffer)
97
+ ast = DeepCover.parser.parse(@buffer)
99
98
  Node::Root.new(ast, self)
100
99
  end
101
100
  end
@@ -106,17 +105,17 @@ module DeepCover
106
105
 
107
106
  def instrument_source
108
107
  rewriter = ::Parser::Source::Rewriter.new(@buffer)
109
- covered_ast.each_node do |node|
110
- prefix, suffix = node.rewrite_prefix_suffix
111
- unless prefix.empty?
112
- expression = node.expression
113
- prefix = yield prefix, node, expression.begin, :prefix if block_given?
114
- rewriter.insert_before_multi expression, prefix rescue binding.pry
115
- end
116
- unless suffix.empty?
117
- expression = node.expression
118
- suffix = yield suffix, node, expression.end, :suffix if block_given?
119
- rewriter.insert_after_multi expression, suffix
108
+ covered_ast.each_node(:postorder) do |node|
109
+ node.rewriting_rules.each do |range, rule|
110
+ prefix, _node, suffix = rule.partition('%{node}')
111
+ unless prefix.empty?
112
+ prefix = yield prefix, node, range.begin, :prefix if block_given?
113
+ rewriter.insert_before_multi range, prefix rescue binding.pry
114
+ end
115
+ unless suffix.empty?
116
+ suffix = yield suffix, node, range.end, :suffix if block_given?
117
+ rewriter.insert_after_multi range, suffix
118
+ end
120
119
  end
121
120
  end
122
121
  rewriter.process
@@ -19,17 +19,25 @@ module DeepCover
19
19
  @base_node = base_node
20
20
  @parent = parent
21
21
  @index = index
22
- @children = augment_children(base_children)
22
+ @children = begin
23
+ augment_children(base_children)
24
+ rescue StandardError => e
25
+ diagnose(e)
26
+ end
23
27
  super()
24
28
  end
25
29
 
26
- ### High level API for coverage purposes
30
+ ### Public API
27
31
 
32
+ # Shortcut to access children
28
33
  def [](v)
29
34
  children[v]
30
35
  end
31
36
 
32
- ### Public API
37
+ # Shortcut to create a node from source code
38
+ def self.[](source)
39
+ CoveredCode.new(source: source).execute_code.covered_ast
40
+ end
33
41
 
34
42
  def children_nodes
35
43
  children.select{|c| c.is_a? Node }
@@ -75,7 +83,7 @@ module DeepCover
75
83
  end
76
84
 
77
85
  def type
78
- return base_node.type if base_node
86
+ return base_node.type if base_node.respond_to? :type
79
87
  self.class.name.split('::').last.to_sym
80
88
  end
81
89
 
@@ -95,5 +103,21 @@ module DeepCover
95
103
  t.casecmp(class_name) == 0 ? t : "#{t}[#{class_name}]"
96
104
  end
97
105
 
106
+ private
107
+ def diagnose(exception)
108
+ if self.class == Node
109
+ exp = base_node.loc.expression
110
+ warn ["Unknown node type encountered: #{base_node.type}",
111
+ 'This node will not be handled properly; its subnodes will be ignored',
112
+ 'Source:',
113
+ exp && exp.source,
114
+ "Original exception:",
115
+ exception.inspect,
116
+ ].join("\n")
117
+ []
118
+ else
119
+ raise exception
120
+ end
121
+ end
98
122
  end
99
123
  end
@@ -1,3 +1,5 @@
1
+ require_relative 'empty_body'
2
+
1
3
  module DeepCover
2
4
  class Node
3
5
  module Branch
@@ -13,14 +15,15 @@ module DeepCover
13
15
  # Also define flow_entry_count
14
16
  end
15
17
 
16
- class TrivialBranch < Struct.new(:condition, :other_branch)
17
- def flow_entry_count
18
- condition.flow_completion_count - other_branch.flow_entry_count
18
+ class TrivialBranch < Node::EmptyBody
19
+ def initialize(condition, other_branch, position: true)
20
+ @condition = condition
21
+ @other_branch = other_branch
22
+ super(nil, parent: condition.parent, position: position)
19
23
  end
20
- alias_method :flow_completion_count, :flow_entry_count
21
- alias_method :execution_count, :flow_entry_count
22
- def executable?
23
- true
24
+
25
+ def flow_entry_count
26
+ @condition.flow_completion_count - @other_branch.flow_entry_count
24
27
  end
25
28
  end
26
29
 
@@ -7,7 +7,7 @@ module DeepCover
7
7
  has_child method_name: Symbol
8
8
  has_child signature: Args
9
9
  has_child body: Node,
10
- rewrite: '%{method_call_tracker};%{local}=nil;%{node};',
10
+ rewrite: '%{method_call_tracker};%{local}=nil;%{node}',
11
11
  can_be_empty: -> { base_node.loc.end.begin },
12
12
  is_statement: true,
13
13
  flow_entry_count: :method_call_tracker_hits
@@ -25,7 +25,7 @@ module DeepCover
25
25
  has_child method_name: Symbol
26
26
  has_child signature: Args
27
27
  has_child body: Node,
28
- rewrite: '%{method_call_tracker};%{local}=nil;%{node};',
28
+ rewrite: '%{method_call_tracker};%{local}=nil;%{node}',
29
29
  can_be_empty: -> { base_node.loc.end.begin },
30
30
  is_statement: true,
31
31
  flow_entry_count: :method_call_tracker_hits
@@ -2,7 +2,6 @@ module DeepCover
2
2
  class Node::EmptyBody < Node
3
3
  def initialize(base_node, parent: raise, index: 0, position: ChildCanBeEmpty.last_empty_position)
4
4
  @position = position
5
- @position = parent.expression.begin if @position == true # Some random position... don't rewrite!
6
5
  super(base_node, parent: parent, index: index, base_children: [])
7
6
  end
8
7
 
@@ -11,6 +10,7 @@ module DeepCover
11
10
  end
12
11
 
13
12
  def loc_hash
13
+ return {} if @position == true
14
14
  {expression: @position}
15
15
  end
16
16
 
@@ -15,8 +15,7 @@ module DeepCover
15
15
  child_base_nodes = [*child_base_nodes, *Array.new(missing)]
16
16
  end
17
17
  child_base_nodes.map.with_index do |child, child_index|
18
- child_name = self.class.child_index_to_name(child_index, child_base_nodes.size) rescue binding.pry
19
-
18
+ child_name = self.class.child_index_to_name(child_index, child_base_nodes.size)
20
19
  if (klass = remap_child(child, child_name))
21
20
  klass.new(child, parent: self, index: child_index)
22
21
  else
@@ -25,17 +25,20 @@ module DeepCover
25
25
  nil
26
26
  end
27
27
 
28
- def executed_locs
28
+ def executed_loc_hash
29
+ h = Tools.slice(loc_hash, *executed_loc_keys)
29
30
  if (keys = parent.child_executed_loc_keys(self))
30
- inherited = parent.loc_hash.values_at(*keys)
31
+ h.merge!(Tools.slice(parent.loc_hash, *keys))
31
32
  end
32
- [ *loc_hash.values_at(*executed_loc_keys),
33
- *inherited
34
- ].compact
33
+ h.reject{|k, v| v.nil? }
34
+ end
35
+
36
+ def executed_locs
37
+ executed_loc_hash.values
35
38
  end
36
39
 
37
40
  def loc_hash
38
- @loc_hash ||= base_node.location.to_hash
41
+ @loc_hash ||= (base_node.respond_to?(:location) ? base_node.location.to_hash : {}).freeze
39
42
  end
40
43
 
41
44
  def expression
@@ -3,14 +3,9 @@ module DeepCover
3
3
  module HasChild
4
4
  def self.included(base)
5
5
  base.extend ClassMethods
6
- setup_constants(base)
7
6
  end
8
-
9
- def self.setup_constants(subclass)
10
- subclass.const_set :CHILDREN, {}
11
- subclass.const_set :CHILDREN_TYPES, {}
12
- end
13
-
7
+ CHILDREN = {}
8
+ CHILDREN_TYPES = {}
14
9
 
15
10
  def initialize(*)
16
11
  super
@@ -25,18 +20,25 @@ module DeepCover
25
20
  end
26
21
 
27
22
  module ClassMethods
28
- def has_child(rest: false, **h)
23
+ def has_child(_rest: false, _refine: false, **h)
29
24
  raise "Needs exactly one custom named argument, got #{h.size}" if h.size != 1
30
25
  name, types = h.first
31
26
  raise TypeError, "Expect a Symbol for name, got a #{name.class} (#{name.inspect})" unless name.is_a?(Symbol)
32
- update_children_const(name, rest: rest)
33
- define_accessor(name)
27
+ update_children_const(name, rest: _rest) unless _refine
28
+ define_accessor(name) unless _refine
34
29
  add_runtime_check(name, types)
35
30
  self
36
31
  end
37
32
 
38
33
  def has_extra_children(**h)
39
- has_child(**h, rest: true)
34
+ has_child(**h, _rest: true)
35
+ end
36
+
37
+ def refine_child(child_name = nil, **h)
38
+ if child_name
39
+ h = {child_name => self::CHILDREN_TYPES.fetch(child_name), **h}
40
+ end
41
+ has_child(**h, _refine: true)
40
42
  end
41
43
 
42
44
  def child_index_to_name(index, nb_children)
@@ -91,14 +93,11 @@ module DeepCover
91
93
  end
92
94
 
93
95
  def inherited(subclass)
94
- HasChild.setup_constants(subclass)
96
+ subclass.const_set :CHILDREN, self::CHILDREN.dup
97
+ subclass.const_set :CHILDREN_TYPES, self::CHILDREN_TYPES.dup
95
98
  super
96
99
  end
97
100
 
98
- def const_missing(name)
99
- const_set(name, self::CHILDREN.fetch(name.downcase) { return super })
100
- end
101
-
102
101
  def update_children_const(name, rest: false)
103
102
  children_map = self::CHILDREN
104
103
  already_has_rest = false
@@ -124,7 +123,7 @@ module DeepCover
124
123
  warn "child name '#{name}' conflicts with existing method for #{self}" if method_defined? name
125
124
  class_eval <<-end_eval, __FILE__, __LINE__
126
125
  def #{name}
127
- children[#{name.upcase}]
126
+ children[CHILDREN.fetch(#{name.inspect})]
128
127
  end
129
128
  end_eval
130
129
  end
@@ -3,12 +3,8 @@ module DeepCover
3
3
  module HasTracker
4
4
  def self.included(base)
5
5
  base.extend ClassMethods
6
- setup_constants(base)
7
- end
8
-
9
- def self.setup_constants(base)
10
- base.const_set :TRACKERS, {}
11
6
  end
7
+ TRACKERS = {}
12
8
 
13
9
  def initialize(*)
14
10
  @tracker_offset = covered_code.allocate_trackers(self.class::TRACKERS.size).begin
@@ -23,8 +19,8 @@ module DeepCover
23
19
 
24
20
  module ClassMethods
25
21
  def inherited(base)
22
+ base.const_set :TRACKERS, self::TRACKERS.dup
26
23
  super
27
- HasTracker.setup_constants(base)
28
24
  end
29
25
 
30
26
  def has_tracker(name)