deep-cover 0.1.16 → 0.2.0

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