deep-cover 0.1.16 → 0.2.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +3 -8
  4. data/.travis.yml +4 -4
  5. data/CHANGELOG.md +10 -0
  6. data/Gemfile +3 -1
  7. data/README.md +9 -4
  8. data/Rakefile +6 -3
  9. data/deep_cover.gemspec +2 -2
  10. data/lib/deep_cover.rb +10 -0
  11. data/lib/deep_cover/analyser.rb +1 -2
  12. data/lib/deep_cover/analyser/base.rb +32 -0
  13. data/lib/deep_cover/analyser/branch.rb +19 -4
  14. data/lib/deep_cover/analyser/node.rb +52 -0
  15. data/lib/deep_cover/analyser/per_char.rb +18 -1
  16. data/lib/deep_cover/analyser/stats.rb +54 -0
  17. data/lib/deep_cover/backports.rb +1 -0
  18. data/lib/deep_cover/base.rb +17 -1
  19. data/lib/deep_cover/builtin_takeover.rb +5 -0
  20. data/lib/deep_cover/cli/deep_cover.rb +2 -1
  21. data/lib/deep_cover/cli/instrumented_clone_reporter.rb +12 -10
  22. data/lib/deep_cover/config.rb +22 -8
  23. data/lib/deep_cover/core_ext/coverage_replacement.rb +22 -18
  24. data/lib/deep_cover/coverage.rb +3 -203
  25. data/lib/deep_cover/coverage/analysis.rb +36 -0
  26. data/lib/deep_cover/coverage/base.rb +91 -0
  27. data/lib/deep_cover/coverage/istanbul.rb +34 -0
  28. data/lib/deep_cover/coverage/persistence.rb +93 -0
  29. data/lib/deep_cover/covered_code.rb +12 -22
  30. data/lib/deep_cover/custom_requirer.rb +6 -2
  31. data/lib/deep_cover/node/base.rb +1 -1
  32. data/lib/deep_cover/node/case.rb +13 -2
  33. data/lib/deep_cover/node/exceptions.rb +2 -2
  34. data/lib/deep_cover/node/if.rb +21 -2
  35. data/lib/deep_cover/node/mixin/flow_accounting.rb +1 -0
  36. data/lib/deep_cover/node/send.rb +9 -2
  37. data/lib/deep_cover/node/short_circuit.rb +10 -0
  38. data/lib/deep_cover/parser_ext/range.rb +4 -4
  39. data/lib/deep_cover/reporter/html.rb +15 -0
  40. data/lib/deep_cover/reporter/html/base.rb +14 -0
  41. data/lib/deep_cover/reporter/html/index.rb +78 -0
  42. data/lib/deep_cover/reporter/html/site.rb +78 -0
  43. data/lib/deep_cover/reporter/html/source.rb +136 -0
  44. data/lib/deep_cover/reporter/html/template/assets/32px.png +0 -0
  45. data/lib/deep_cover/reporter/html/template/assets/40px.png +0 -0
  46. data/lib/deep_cover/reporter/html/template/assets/deep_cover.css.sass +338 -0
  47. data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.js +4 -0
  48. data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.map +1 -0
  49. data/lib/deep_cover/reporter/html/template/assets/jstree.css +1108 -0
  50. data/lib/deep_cover/reporter/html/template/assets/jstree.js +8424 -0
  51. data/lib/deep_cover/reporter/html/template/assets/jstreetable.js +1069 -0
  52. data/lib/deep_cover/reporter/html/template/assets/throbber.gif +0 -0
  53. data/lib/deep_cover/reporter/html/template/index.html.erb +75 -0
  54. data/lib/deep_cover/reporter/html/template/source.html.erb +35 -0
  55. data/lib/deep_cover/reporter/html/tree.rb +55 -0
  56. data/lib/deep_cover/tools/content_tag.rb +11 -0
  57. data/lib/deep_cover/tools/covered.rb +9 -0
  58. data/lib/deep_cover/tools/merge.rb +16 -0
  59. data/lib/deep_cover/tools/render_template.rb +13 -0
  60. data/lib/deep_cover/tools/transform_keys.rb +9 -0
  61. data/lib/deep_cover/version.rb +1 -1
  62. metadata +33 -7
  63. data/lib/deep_cover/analyser/ignore_uncovered.rb +0 -21
  64. data/lib/deep_cover/analyser/optionally_covered.rb +0 -19
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ module Coverage::Istanbul
5
+ def to_istanbul(**options)
6
+ map do |covered_code|
7
+ next {} unless covered_code.has_executed?
8
+ covered_code.to_istanbul(**options)
9
+ end.inject(:merge)
10
+ end
11
+
12
+ def output_istanbul(dir: '.', name: '.nyc_output', **options)
13
+ path = Pathname.new(dir).expand_path.join(name)
14
+ path.mkpath
15
+ path.each_child(&:delete)
16
+ path.join('deep_cover.json').write(JSON.pretty_generate(to_istanbul(**options)))
17
+ path
18
+ end
19
+
20
+ def report_istanbul(output: nil, **options)
21
+ dir = output_istanbul(**options).dirname
22
+ unless [nil, '', 'false'].include? output
23
+ output = File.expand_path(output)
24
+ html = "--reporter=html --report-dir='#{output}'"
25
+ if options[:open]
26
+ html += " && open '#{output}/index.html'"
27
+ else
28
+ msg = "\nHTML coverage written to: '#{output}/index.html'"
29
+ end
30
+ end
31
+ `cd #{dir} && nyc report --reporter=text #{html}` + msg.to_s
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ require 'securerandom'
5
+ class Coverage::Persistence
6
+ # rubocop:disable Security/MarshalLoad
7
+ BASENAME = 'coverage.dc'
8
+ TRACKER_TEMPLATE = 'trackers%{unique}.dct'
9
+
10
+ attr_reader :dir_path
11
+ def initialize(dest_path, dirname)
12
+ @dir_path = Pathname(dest_path).join(dirname).expand_path
13
+ end
14
+
15
+ def load(with_trackers: true)
16
+ saved?
17
+ load_trackers if with_trackers
18
+ load_coverage
19
+ end
20
+
21
+ def save(coverage)
22
+ create_if_needed
23
+ delete_trackers
24
+ save_coverage(coverage)
25
+ end
26
+
27
+ def save_trackers(global)
28
+ saved?
29
+ trackers = eval(global) # rubocop:disable Security/Eval
30
+ # Some testing involves more than one process, some of which don't run any of our covered code.
31
+ # Don't save anything if that's the case
32
+ return if trackers.nil?
33
+ basename = format(TRACKER_TEMPLATE, unique: SecureRandom.urlsafe_base64)
34
+ dir_path.join(basename).binwrite(Marshal.dump(
35
+ version: DeepCover::VERSION,
36
+ global: global,
37
+ trackers: trackers,
38
+ ))
39
+ end
40
+
41
+ def saved?
42
+ raise "Can't find folder '#{dir_path}'" unless dir_path.exist?
43
+ self
44
+ end
45
+
46
+ private
47
+
48
+ def create_if_needed
49
+ dir_path.mkpath
50
+ end
51
+
52
+ def save_coverage(coverage)
53
+ dir_path.join(BASENAME).binwrite(Marshal.dump(
54
+ version: DeepCover::VERSION,
55
+ coverage: coverage,
56
+ ))
57
+ end
58
+
59
+ def load_coverage
60
+ Marshal.load(dir_path.join(BASENAME).binread).tap do |version: raise, coverage: raise|
61
+ raise "dump version mismatch: #{version}, currently #{DeepCover::VERSION}" unless version == DeepCover::VERSION
62
+ return coverage
63
+ end
64
+ end
65
+
66
+ def load_trackers
67
+ tracker_files.each do |full_path|
68
+ Marshal.load(full_path.binread).tap do |version: raise, global: raise, trackers: raise|
69
+ raise "dump version mismatch: #{version}, currently #{DeepCover::VERSION}" unless version == DeepCover::VERSION
70
+ merge_trackers(eval("#{global} ||= {}"), trackers) # rubocop:disable Security/Eval
71
+ end
72
+ end
73
+ end
74
+
75
+ def merge_trackers(hash, to_merge)
76
+ hash.merge!(to_merge) do |_key, current, to_add|
77
+ unless current.empty? || current.size == to_add.size
78
+ warn "Merging trackers of different sizes: #{current.size} vs #{to_add.size}"
79
+ end
80
+ to_add.zip(current).map { |a, b| a + b }
81
+ end
82
+ end
83
+
84
+ def tracker_files
85
+ basename = format(TRACKER_TEMPLATE, unique: '*')
86
+ Pathname.glob(dir_path.join(basename))
87
+ end
88
+
89
+ def delete_trackers
90
+ tracker_files.each(&:delete)
91
+ end
92
+ end
93
+ end
@@ -12,11 +12,11 @@ module DeepCover
12
12
  raise 'Must provide either path or source' unless path || source
13
13
 
14
14
  @buffer = ::Parser::Source::Buffer.new(path, lineno)
15
- @buffer.source = source ||= File.read(path)
15
+ @buffer.source = source || File.read(path)
16
16
  @tracker_count = 0
17
17
  @tracker_global = tracker_global
18
18
  @local_var = local_var
19
- @name = name || (source ? '(source)' : File.basename(path))
19
+ @name = name.to_s || (path ? File.basename(path) : '(source)')
20
20
  @covered_source = instrument_source
21
21
  end
22
22
 
@@ -39,28 +39,23 @@ module DeepCover
39
39
 
40
40
  def execute_code(binding: DeepCover::GLOBAL_BINDING.dup)
41
41
  return if has_executed?
42
- global[nb] = Array.new(@tracker_count, 0)
43
42
  eval(@covered_source, binding, @buffer.name || '<raw_code>', lineno) # rubocop:disable Security/Eval
44
43
  self
45
44
  end
46
45
 
47
46
  def cover
48
- must_have_executed
49
- global[nb]
47
+ global[nb] ||= Array.new(@tracker_count, 0)
50
48
  end
51
49
 
52
50
  def line_coverage(**options)
53
- must_have_executed
54
51
  Analyser::PerLine.new(self, **options).results
55
52
  end
56
53
 
57
54
  def to_istanbul(**options)
58
- must_have_executed
59
55
  Reporter::Istanbul.new(self, **options).convert
60
56
  end
61
57
 
62
58
  def char_cover(**options)
63
- must_have_executed
64
59
  Analyser::PerChar.new(self, **options).results
65
60
  end
66
61
 
@@ -103,18 +98,13 @@ module DeepCover
103
98
  end
104
99
 
105
100
  def instrument_source
106
- rewriter = ::Parser::Source::Rewriter.new(@buffer)
101
+ rewriter = ::Parser::Source::TreeRewriter.new(@buffer)
107
102
  covered_ast.each_node(:postorder) do |node|
108
103
  node.rewriting_rules.each do |range, rule|
109
104
  prefix, _node, suffix = rule.partition('%{node}')
110
- unless prefix.empty?
111
- prefix = yield prefix, node, range.begin, :prefix if block_given?
112
- rewriter.insert_before_multi range, prefix
113
- end
114
- unless suffix.empty?
115
- suffix = yield suffix, node, range.end, :suffix if block_given?
116
- rewriter.insert_after_multi range, suffix
117
- end
105
+ prefix = yield prefix, node, range.begin, :prefix if block_given? && !prefix.empty?
106
+ suffix = yield suffix, node, range.end, :suffix if block_given? && !suffix.empty?
107
+ rewriter.wrap(range, prefix, suffix)
118
108
  end
119
109
  end
120
110
  rewriter.process
@@ -126,21 +116,21 @@ module DeepCover
126
116
 
127
117
  def freeze
128
118
  unless frozen? # Guard against reentrance
129
- must_have_executed
130
119
  super
131
120
  root.each_node(&:freeze)
132
121
  end
133
122
  self
134
123
  end
135
124
 
125
+ def inspect
126
+ %{#<DeepCover::CoveredCode "#{name}">}
127
+ end
128
+ alias_method :to_s, :inspect
129
+
136
130
  protected
137
131
 
138
132
  def global
139
133
  @@globals[tracker_global]
140
134
  end
141
-
142
- def must_have_executed
143
- raise "cover for #{buffer.name} not available, file wasn't executed" unless has_executed?
144
- end
145
135
  end
146
136
  end
@@ -5,6 +5,8 @@
5
5
  module DeepCover
6
6
  class CustomRequirer
7
7
  class LoadPathsSubset
8
+ attr_reader :last_lookup_path
9
+
8
10
  def initialize(load_paths: raise, lookup_paths: raise)
9
11
  @original_load_paths = load_paths
10
12
  @cached_load_paths_subset = []
@@ -27,7 +29,7 @@ module DeepCover
27
29
 
28
30
  # E.g. '/a/b' => true when a lookup path is '/a/'
29
31
  def within_lookup?(full_path)
30
- @lookup_paths.any? { |p| full_path.start_with? p }
32
+ @lookup_paths.any? { |p| full_path.start_with?(p) && @last_lookup_path = p }
31
33
  end
32
34
 
33
35
  def exist?(full_path)
@@ -86,6 +88,7 @@ module DeepCover
86
88
  # It is *NOT* recommended to simply delegate to the default #require, since it
87
89
  # might not be safe to run part of the code again.
88
90
  def require(path)
91
+ path = path.to_s
89
92
  ext = File.extname(path)
90
93
  throw :use_fallback, :not_supported if ext == '.so'
91
94
  path += '.rb' if ext != '.rb'
@@ -128,7 +131,8 @@ module DeepCover
128
131
 
129
132
  def cover_and_execute(path)
130
133
  begin
131
- covered_code = DeepCover.coverage.covered_code(path)
134
+ name = path.sub(%r{^#{@load_paths_subset.last_lookup_path}/}, '') if @load_paths_subset
135
+ covered_code = DeepCover.coverage.covered_code(path, name: name)
132
136
  rescue Parser::SyntaxError => e
133
137
  if e.message =~ /contains escape sequences incompatible with UTF-8/
134
138
  warn "Can't cover #{path} because of incompatible encoding (see issue #9)"
@@ -36,7 +36,7 @@ module DeepCover
36
36
  # Search self and descendants for a particular Class or type
37
37
  def find_all(lookup)
38
38
  case lookup
39
- when ::Class
39
+ when ::Module
40
40
  each_node.grep(lookup)
41
41
  when ::Symbol
42
42
  each_node.find_all { |n| n.type == lookup }
@@ -51,10 +51,10 @@ module DeepCover
51
51
  if (after_then = base_node.loc.begin)
52
52
  after_then.end
53
53
  else
54
- base_node.loc.expression.end
54
+ base_node.loc.expression.end.succ
55
55
  end
56
56
  },
57
- rewrite: ';%{body_entry_tracker};%{local}=nil;%{node}',
57
+ rewrite: '%{body_entry_tracker};%{local}=nil;%{node}',
58
58
  is_statement: true,
59
59
  flow_entry_count: :body_entry_tracker_hits
60
60
 
@@ -89,6 +89,17 @@ module DeepCover
89
89
  whens.map(&:body) << self.else
90
90
  end
91
91
 
92
+ def branches_summary(of = branches)
93
+ texts = []
94
+ n = of.size
95
+ if of.include? self.else
96
+ texts << "#{'implicit ' unless has_else?}else"
97
+ n -= 1
98
+ end
99
+ texts.unshift "#{n} when clause#{'s' if n > 1}" if n > 0
100
+ texts.join(' and ')
101
+ end
102
+
92
103
  def execution_count
93
104
  return evaluate.flow_completion_count if evaluate
94
105
  flow_entry_count
@@ -10,7 +10,7 @@ module DeepCover
10
10
  has_child exception: [Node::Array, nil]
11
11
  has_child assignment: [Lvasgn, nil], flow_entry_count: :entered_body_tracker_hits
12
12
  has_child body: Node,
13
- can_be_empty: -> { base_node.loc.expression.end },
13
+ can_be_empty: -> { (base_node.loc.begin || base_node.loc.expression.succ).end },
14
14
  flow_entry_count: :entered_body_tracker_hits,
15
15
  is_statement: true,
16
16
  rewrite: '((%{entered_body_tracker};%{local}=nil;%{node}))'
@@ -67,7 +67,7 @@ module DeepCover
67
67
  can_be_empty: -> { base_node.loc.expression.begin },
68
68
  is_statement: true
69
69
  has_child ensure: Node,
70
- can_be_empty: -> { base_node.loc.expression.end },
70
+ can_be_empty: -> { base_node.loc.expression.end.succ },
71
71
  is_statement: true,
72
72
  flow_entry_count: -> { body.flow_entry_count }
73
73
 
@@ -9,21 +9,36 @@ module DeepCover
9
9
  has_tracker :truthy
10
10
  has_child condition: Node, rewrite: '((%{node}) && %{truthy_tracker})'
11
11
  has_child true_branch: Node,
12
- can_be_empty: true,
13
12
  executed_loc_keys: -> { :else if style == :unless },
14
13
  flow_entry_count: :truthy_tracker_hits,
15
14
  is_statement: true
16
15
  has_child false_branch: Node,
17
- can_be_empty: true,
18
16
  executed_loc_keys: -> { [:else, :colon] if style != :unless },
19
17
  flow_entry_count: -> { condition.flow_completion_count - truthy_tracker_hits },
20
18
  is_statement: true
21
19
  executed_loc_keys :keyword, :question
22
20
 
21
+ def child_can_be_empty(child, name)
22
+ return false if name == :condition || style == :ternary
23
+ if (name == :true_branch) == (style == :if)
24
+ (base_node.loc.begin || base_node.children[0].loc.expression.succ).end
25
+ elsif has_else?
26
+ base_node.loc.else.end.succ
27
+ else
28
+ true # implicit else
29
+ end
30
+ end
31
+
23
32
  def branches
24
33
  [true_branch, false_branch]
25
34
  end
26
35
 
36
+ def branches_summary(of = branches)
37
+ of.map do |jump|
38
+ "#{'implicit ' if jump.is_a?(EmptyBody) && !has_else?}#{jump == false_branch ? 'falsy' : 'truthy'} branch"
39
+ end.join(' and ')
40
+ end
41
+
27
42
  def execution_count
28
43
  condition.flow_completion_count
29
44
  end
@@ -33,6 +48,10 @@ module DeepCover
33
48
  keyword = loc_hash[:keyword]
34
49
  keyword ? keyword.source.to_sym : :ternary
35
50
  end
51
+
52
+ def has_else?
53
+ !!base_node.loc.to_hash[:else]
54
+ end
36
55
  end
37
56
  end
38
57
  end
@@ -36,6 +36,7 @@ module DeepCover
36
36
  end
37
37
 
38
38
  # Returns number of times the node itself was "executed". Definition of executed depends on the node.
39
+ # For now at least, don't return `nil`, instead return `false` in `executable?`
39
40
  def execution_count
40
41
  flow_entry_count
41
42
  end
@@ -58,6 +58,7 @@ module DeepCover
58
58
  arguments.empty? || # No issue when no arguments
59
59
  loc_hash[:selector_end] || # No issue with foo[bar]= and such
60
60
  loc_hash[:operator] || # No issue with foo.bar=
61
+ (receiver && !loc_hash[:dot]) || # No issue with foo + bar
61
62
  loc_hash[:begin] # Ok if has parentheses
62
63
  end
63
64
  end
@@ -92,10 +93,16 @@ module DeepCover
92
93
  end
93
94
 
94
95
  def branches
95
- [actual_send,
96
- TrivialBranch.new(condition: receiver, other_branch: actual_send),
96
+ [TrivialBranch.new(condition: receiver, other_branch: actual_send),
97
+ actual_send,
97
98
  ]
98
99
  end
100
+
101
+ def branches_summary(of = branches)
102
+ of.map do |jump|
103
+ jump == actual_send ? 'safe send' : 'nil shortcut'
104
+ end.join(' and ')
105
+ end
99
106
  end
100
107
 
101
108
  class MatchWithLvasgn < Node
@@ -17,6 +17,16 @@ module DeepCover
17
17
  TrivialBranch.new(condition: lhs, other_branch: conditional),
18
18
  ]
19
19
  end
20
+
21
+ def branches_summary(of = branches)
22
+ of.map do |jump|
23
+ if jump == conditional
24
+ 'left-hand side'
25
+ else
26
+ "#{type == :and ? 'falsy' : 'truthy'} shortcut"
27
+ end
28
+ end.join(' and ')
29
+ end
20
30
  end
21
31
 
22
32
  And = Or = ShortCircuit
@@ -1,10 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Parser::Source::Range
4
- def with(begin_pos: @begin_pos, end_pos: @end_pos)
5
- Parser::Source::Range.new(@source_buffer, begin_pos, end_pos)
6
- end
7
-
8
4
  # (1...10).split(2...3, 6...8) => [1...2, 3...6, 7...10]
9
5
  # Assumes inner_ranges are exclusive, and included in self
10
6
  def split(*inner_ranges)
@@ -34,4 +30,8 @@ class Parser::Source::Range
34
30
  def strip(pattern = /\s*/)
35
31
  lstrip(pattern).rstrip(pattern)
36
32
  end
33
+
34
+ def succ
35
+ adjust(begin_pos: +1, end_pos: +1)
36
+ end
37
37
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ Reporter::HTML = Module.new
5
+
6
+ require_relative_dir 'html'
7
+
8
+ module Reporter::HTML
9
+ class << self
10
+ def report(coverage, **options)
11
+ Site.save(coverage.covered_codes, **options)
12
+ end
13
+ end
14
+ end
15
+ end