deep-cover 0.5.2 → 0.5.3

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 (84) hide show
  1. checksums.yaml +5 -5
  2. data/.deep_cover.rb +8 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +15 -1
  5. data/.travis.yml +1 -0
  6. data/README.md +30 -1
  7. data/Rakefile +10 -1
  8. data/bin/cov +1 -1
  9. data/deep_cover.gemspec +4 -5
  10. data/exe/deep-cover +5 -3
  11. data/lib/deep_cover.rb +1 -1
  12. data/lib/deep_cover/analyser/node.rb +1 -1
  13. data/lib/deep_cover/analyser/ruby25_like_branch.rb +209 -0
  14. data/lib/deep_cover/auto_run.rb +19 -19
  15. data/lib/deep_cover/autoload_tracker.rb +181 -44
  16. data/lib/deep_cover/backports.rb +3 -1
  17. data/lib/deep_cover/base.rb +13 -8
  18. data/lib/deep_cover/basics.rb +1 -1
  19. data/lib/deep_cover/cli/debugger.rb +2 -2
  20. data/lib/deep_cover/cli/instrumented_clone_reporter.rb +21 -8
  21. data/lib/deep_cover/cli/runner.rb +126 -0
  22. data/lib/deep_cover/config_setter.rb +1 -0
  23. data/lib/deep_cover/core_ext/autoload_overrides.rb +82 -14
  24. data/lib/deep_cover/core_ext/coverage_replacement.rb +34 -5
  25. data/lib/deep_cover/core_ext/exec_callbacks.rb +27 -0
  26. data/lib/deep_cover/core_ext/load_overrides.rb +4 -6
  27. data/lib/deep_cover/core_ext/require_overrides.rb +1 -3
  28. data/lib/deep_cover/coverage.rb +105 -2
  29. data/lib/deep_cover/coverage/analysis.rb +30 -28
  30. data/lib/deep_cover/coverage/persistence.rb +60 -70
  31. data/lib/deep_cover/covered_code.rb +16 -49
  32. data/lib/deep_cover/custom_requirer.rb +112 -51
  33. data/lib/deep_cover/load.rb +10 -6
  34. data/lib/deep_cover/memoize.rb +1 -3
  35. data/lib/deep_cover/module_override.rb +7 -0
  36. data/lib/deep_cover/node/assignments.rb +2 -1
  37. data/lib/deep_cover/node/base.rb +6 -6
  38. data/lib/deep_cover/node/block.rb +10 -8
  39. data/lib/deep_cover/node/case.rb +3 -3
  40. data/lib/deep_cover/node/collections.rb +8 -0
  41. data/lib/deep_cover/node/if.rb +19 -3
  42. data/lib/deep_cover/node/literals.rb +28 -7
  43. data/lib/deep_cover/node/mixin/can_augment_children.rb +4 -4
  44. data/lib/deep_cover/node/mixin/child_can_be_empty.rb +1 -1
  45. data/lib/deep_cover/node/mixin/filters.rb +6 -2
  46. data/lib/deep_cover/node/mixin/has_child.rb +8 -8
  47. data/lib/deep_cover/node/mixin/has_child_handler.rb +3 -3
  48. data/lib/deep_cover/node/mixin/has_tracker.rb +7 -3
  49. data/lib/deep_cover/node/root.rb +1 -1
  50. data/lib/deep_cover/node/send.rb +53 -7
  51. data/lib/deep_cover/node/short_circuit.rb +11 -3
  52. data/lib/deep_cover/parser_ext/range.rb +11 -27
  53. data/lib/deep_cover/problem_with_diagnostic.rb +1 -1
  54. data/lib/deep_cover/reporter.rb +0 -1
  55. data/lib/deep_cover/reporter/base.rb +68 -0
  56. data/lib/deep_cover/reporter/html.rb +1 -1
  57. data/lib/deep_cover/reporter/html/index.rb +4 -8
  58. data/lib/deep_cover/reporter/html/site.rb +10 -18
  59. data/lib/deep_cover/reporter/html/source.rb +3 -3
  60. data/lib/deep_cover/reporter/html/template/source.html.erb +1 -1
  61. data/lib/deep_cover/reporter/istanbul.rb +86 -56
  62. data/lib/deep_cover/reporter/text.rb +5 -13
  63. data/lib/deep_cover/reporter/{util/tree.rb → tree/util.rb} +19 -21
  64. data/lib/deep_cover/tools/blank.rb +25 -0
  65. data/lib/deep_cover/tools/builtin_coverage.rb +8 -8
  66. data/lib/deep_cover/tools/dump_covered_code.rb +2 -9
  67. data/lib/deep_cover/tools/execute_sample.rb +17 -6
  68. data/lib/deep_cover/tools/format_generated_code.rb +1 -1
  69. data/lib/deep_cover/tools/indent_string.rb +26 -0
  70. data/lib/deep_cover/tools/our_coverage.rb +2 -2
  71. data/lib/deep_cover/tools/strip_heredoc.rb +18 -0
  72. data/lib/deep_cover/tracker_bucket.rb +50 -0
  73. data/lib/deep_cover/tracker_hits_per_path.rb +35 -0
  74. data/lib/deep_cover/tracker_storage.rb +76 -0
  75. data/lib/deep_cover/tracker_storage_per_path.rb +34 -0
  76. data/lib/deep_cover/version.rb +1 -1
  77. data/lib/deep_cover_entry.rb +3 -0
  78. metadata +30 -37
  79. data/bin/gemcov +0 -8
  80. data/bin/selfcov +0 -21
  81. data/lib/deep_cover/cli/deep_cover.rb +0 -126
  82. data/lib/deep_cover/coverage/base.rb +0 -81
  83. data/lib/deep_cover/coverage/istanbul.rb +0 -34
  84. data/lib/deep_cover/tools/transform_keys.rb +0 -9
@@ -1,18 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeepCover
4
- module Reporter
5
- class Text
6
- include Util::Tree
7
- def initialize(coverage, **options)
8
- @coverage = coverage
9
- @options = options
10
- end
11
-
12
- def analysis
13
- Coverage::Analysis.new(@coverage.covered_codes, **@options)
14
- end
4
+ require_relative 'base'
15
5
 
6
+ module Reporter
7
+ class Text < Base
16
8
  INDENT = ' '
17
9
  def report
18
10
  formatted_headings = headings.map.with_index { |h, i| {value: h, alignment: :center} }
@@ -24,7 +16,7 @@ module DeepCover
24
16
  style: {border_bottom: false, border_top: false, alignment: :right},
25
17
  )
26
18
  table.align_column 0, :left
27
- table.render
19
+ table.render + "\n\nOverall: #{analysis.overall}%"
28
20
  end
29
21
 
30
22
  def self.report(coverage, **options)
@@ -43,7 +35,7 @@ module DeepCover
43
35
  end
44
36
 
45
37
  def rows
46
- populate_stats(analysis).map do |full_path, partial_path, data, children|
38
+ populate_stats.map do |full_path, partial_path, data, children|
47
39
  [partial_path, *transform_data(data)]
48
40
  end
49
41
  end
@@ -2,9 +2,26 @@
2
2
 
3
3
  module DeepCover
4
4
  module Reporter
5
- module Util
5
+ class Tree
6
6
  # Utility functions to deal with trees
7
- module Tree
7
+ module Util
8
+ extend self
9
+
10
+ def populate_from_map(tree:, map:, merge:)
11
+ return to_enum(__method__, tree: tree, map: map, merge: merge) unless block_given?
12
+ final_results, _final_data = populate(tree) do |full_path, partial_path, children|
13
+ if children.empty?
14
+ data = map.fetch(full_path)
15
+ else
16
+ child_results, child_data = children.transpose
17
+ data = merge.call(child_data)
18
+ end
19
+ result = yield full_path, partial_path, data, child_results || []
20
+ [result, data]
21
+ end.transpose
22
+ final_results
23
+ end
24
+
8
25
  def paths_to_tree(paths)
9
26
  twigs = paths.map do |path|
10
27
  partials = path_to_partial_paths(path)
@@ -63,25 +80,6 @@ module DeepCover
63
80
  yield full_path, path, children
64
81
  end
65
82
  end
66
-
67
- # Same as populate, but also yields data, which is either the analysis data (for leaves)
68
- # of the sum of the children (for subtrees)
69
- def populate_stats(analysis)
70
- return to_enum(__method__, analysis) unless block_given?
71
- map = Tools.transform_keys(analysis.stat_map, &:name)
72
- tree = paths_to_tree(map.keys)
73
- final_results, _final_data = populate(tree) do |full_path, partial_path, children|
74
- if children.empty?
75
- data = map[full_path]
76
- else
77
- child_results, child_data = children.transpose
78
- data = Tools.merge(*child_data, :+)
79
- end
80
- result = yield full_path, partial_path, data, child_results || []
81
- [result, data]
82
- end.transpose
83
- final_results
84
- end
85
83
  end
86
84
  end
87
85
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ module Tools::Blank
5
+ BLANK_RE = /\A[[:space:]]*\z/
6
+
7
+ # Homemade poor-man's blank?
8
+ # Based, but modified, on https://github.com/rails/rails/blob/5-0-stable/activesupport/lib/active_support/core_ext/object/blank.rb
9
+ def blank?(obj)
10
+ if obj.is_a?(String)
11
+ obj.empty? || obj =~ BLANK_RE
12
+ else
13
+ obj.respond_to?(:empty?) ? !!obj.empty? : !obj
14
+ end
15
+ end
16
+
17
+ def present?(obj)
18
+ !blank?(obj)
19
+ end
20
+
21
+ def presence(obj)
22
+ obj if present?(obj)
23
+ end
24
+ end
25
+ end
@@ -2,22 +2,22 @@
2
2
 
3
3
  module DeepCover
4
4
  module Tools::BuiltinCoverage
5
- def builtin_coverage(source, fn, lineno)
5
+ def builtin_coverage(source, filename, lineno)
6
6
  require 'coverage'
7
- fn = File.absolute_path(File.expand_path(fn))
7
+ filename = File.absolute_path(File.expand_path(filename))
8
8
  ::Coverage.start
9
9
  Tools.silence_warnings do
10
- execute_sample -> { run_with_line_coverage(source, fn, lineno) }
10
+ execute_sample -> { run_with_line_coverage(source, filename, lineno) }
11
11
  end
12
- unshift_coverage(::Coverage.result.fetch(fn), lineno)
12
+ unshift_coverage(::Coverage.result.fetch(filename), lineno)
13
13
  end
14
14
 
15
15
  if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
16
16
  # Executes the source as if it was in the specified file while
17
17
  # builtin coverage information is still captured
18
- def run_with_line_coverage(source, fn = nil, lineno = 1)
18
+ def run_with_line_coverage(source, filename = nil, lineno = 1)
19
19
  source = shift_source(source, lineno)
20
- Object.to_java.getRuntime.executeScript(source, fn)
20
+ Object.to_java.getRuntime.executeScript(source, filename)
21
21
  end
22
22
  else
23
23
  # In ruby 2.0 and 2.1, using 2, 3 or 4 as lineno with RubyVM::InstructionSequence.compile
@@ -32,9 +32,9 @@ module DeepCover
32
32
 
33
33
  # Executes the source as if it was in the specified file while
34
34
  # builtin coverage information is still captured
35
- def run_with_line_coverage(source, fn = nil, lineno = 1)
35
+ def run_with_line_coverage(source, filename = nil, lineno = 1)
36
36
  source = shift_source(source, lineno)
37
- RubyVM::InstructionSequence.compile(source, fn).eval
37
+ RubyVM::InstructionSequence.compile(source, filename).eval
38
38
  end
39
39
  end
40
40
 
@@ -5,22 +5,15 @@ module DeepCover
5
5
  require 'with_progress'
6
6
  end
7
7
  module Tools::DumpCoveredCode
8
- def dump_covered_code_and_save(source_path, dest_path: Dir.mktmpdir)
9
- coverage = Coverage.new(tracker_global: '$_sc')
10
- dump_covered_code(source_path, coverage: coverage, dest_path: dest_path)
11
- coverage.save(dest_path)
12
- end
13
-
14
- def dump_covered_code(source_path, coverage:, dest_path: Dir.mktmpdir, root_path: source_path)
8
+ def dump_covered_code(source_path, coverage:, dest_path: Dir.mktmpdir)
15
9
  source_path = File.join(File.expand_path(source_path), '')
16
10
  dest_path = File.join(File.expand_path(dest_path), '')
17
- root_path = Pathname.new(root_path)
18
11
  skipped = []
19
12
  file_paths = Dir.glob("#{source_path}**/*.rb").select { |p| File.file?(p) }
20
13
  file_paths.each.with_progress(title: 'Rewriting') do |path|
21
14
  new_path = Pathname(path.gsub(source_path, dest_path))
22
15
  begin
23
- covered_code = coverage.covered_code(path, name: new_path.relative_path_from(root_path))
16
+ covered_code = coverage.covered_code(path)
24
17
  rescue Parser::SyntaxError
25
18
  skipped << path
26
19
  next
@@ -2,8 +2,11 @@
2
2
 
3
3
  module DeepCover
4
4
  module Tools::ExecuteSample
5
+ class ExceptionInSample < StandardError
6
+ end
7
+
5
8
  # Returns true if the code would have continued, false if the rescue was triggered.
6
- def execute_sample(to_execute)
9
+ def execute_sample(to_execute, source: nil)
7
10
  # Disable some annoying warning by ruby. We are testing edge cases, so warnings are to be expected.
8
11
  Tools.silence_warnings do
9
12
  if to_execute.is_a?(CoveredCode)
@@ -13,11 +16,19 @@ module DeepCover
13
16
  end
14
17
  end
15
18
  true
16
- rescue RuntimeError => e
17
- # In our samples, a simple `raise` doesn't need to be rescued
18
- # Other exceptions are not rescued
19
- raise unless e.message.empty?
20
- false
19
+ rescue StandardError => e
20
+ # In our samples, a simple `raise` is expected and doesn't need to be rescued
21
+ return false if e.is_a?(RuntimeError) && e.message.empty?
22
+
23
+ source = to_execute.covered_source if to_execute.is_a?(CoveredCode)
24
+ raise unless source
25
+
26
+ inner_msg = Tools.indent_string("#{e.class.name}: #{e.message}", 4)
27
+ source = Tools.indent_string(source, 4)
28
+ msg = "Exception when executing the sample:\n#{inner_msg}\n*Code follows*\n#{source}"
29
+ new_exc = ExceptionInSample.new(msg)
30
+ new_exc.set_backtrace(e.backtrace)
31
+ raise new_exc
21
32
  end
22
33
  end
23
34
  end
@@ -15,7 +15,7 @@ module DeepCover
15
15
  inserts.each do |exp_limit, size|
16
16
  # Line index starts at 1, so array index returns the next line
17
17
  comment_line = generated_lines[exp_limit.line]
18
- next unless comment_line.present?
18
+ next if Tools.blank?(comment_line)
19
19
  next unless comment_line.start_with?('#>')
20
20
  next if comment_line.start_with?('#>X')
21
21
  next unless comment_line.size >= exp_limit.column
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ module Tools::IndentString
5
+ # In-place implementation copied from active-support.
6
+ IMPLEMENTATION = ->(amount, indent_string = nil, indent_empty_lines = false) do
7
+ indent_string = indent_string || self[/^[ \t]/] || ' '
8
+ re = indent_empty_lines ? /^/ : /^(?!$)/
9
+ gsub!(re, indent_string * amount)
10
+ end
11
+
12
+ # Same as #indent! from active-support
13
+ # https://github.com/rails/rails/blob/10e1f1f9a129f2f197a44009a99b73b8ff9dbc0d/activesupport/lib/active_support/core_ext/string/indent.rb#L7
14
+ def indent_string!(string, *args)
15
+ string.instance_exec(*args, &IMPLEMENTATION)
16
+ end
17
+
18
+ # Same as #indent from active-support
19
+ # https://github.com/rails/rails/blob/10e1f1f9a129f2f197a44009a99b73b8ff9dbc0d/activesupport/lib/active_support/core_ext/string/indent.rb#L42
20
+ def indent_string(string, *args)
21
+ string = string.dup
22
+ indent_string!(string, *args)
23
+ string
24
+ end
25
+ end
26
+ end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module DeepCover
4
4
  module Tools::OurCoverage
5
- def our_coverage(source, fn, lineno, **options)
6
- covered_code = CoveredCode.new(source: source, path: fn, lineno: lineno)
5
+ def our_coverage(source, filename, lineno, **options)
6
+ covered_code = CoveredCode.new(source: source, path: filename, lineno: lineno)
7
7
  Tools.execute_sample(covered_code)
8
8
  covered_code.line_coverage(options)[(lineno - 1)..-1]
9
9
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ module Tools::StripHeredoc
5
+ # In-place implementation copied from active-support.
6
+ IMPLEMENTATION = -> do
7
+ gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, ''.freeze).tap do |stripped|
8
+ stripped.freeze if frozen?
9
+ end
10
+ end
11
+
12
+ # Same as #strip_heredoc from active-support
13
+ # https://github.com/rails/rails/blob/16574409f813e2197f88e4a06b527618d64d9ff0/activesupport/lib/active_support/core_ext/string/strip.rb#L22
14
+ def strip_heredoc(string)
15
+ string.instance_exec(&IMPLEMENTATION)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ bootstrap
5
+
6
+ require_relative 'tracker_storage'
7
+
8
+ # A holder for TrackerStorages, using some `global_name`.
9
+ class TrackerBucket
10
+ @@index = {}
11
+
12
+ def self.[](global_name)
13
+ raise ArgumentError, "'#{global_name}' is not a valid global name" unless global_name.start_with? '$'
14
+ @@index[global_name] ||= new(global_name)
15
+ end
16
+
17
+ def setup_source
18
+ "#{source} ||= {}"
19
+ end
20
+
21
+ def source
22
+ @global_name
23
+ end
24
+
25
+ class << self
26
+ alias_method :_load, :[]
27
+ private :_load, :new
28
+ end
29
+
30
+ def inspect
31
+ %{#<DeepCover::TrackerBucket "#{@global_name}">}
32
+ end
33
+
34
+ def create_storage(index = nil)
35
+ index ||= @global.size
36
+ TrackerStorage.new(bucket: self, array: @global[index] ||= [], index: index)
37
+ end
38
+
39
+ private
40
+
41
+ def initialize(global_name)
42
+ @global_name = global_name
43
+ @global = eval(setup_source) # rubocop:disable Security/Eval
44
+ end
45
+
46
+ def _dump(_level)
47
+ @global_name
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ bootstrap
5
+
6
+ # Should be seen as a hash like {path => tracker_hits, ...},
7
+ # where tracker_hits is simply an array of integers returned from
8
+ # TrackerStorage#tracker_hits.
9
+ # Make it easier to separate some concerns, as well as marshalling.
10
+ #
11
+ class TrackerHitsPerPath
12
+ extend Forwardable
13
+ def_delegators :@index, :each, :each_key, :map, :transform_values, :to_h, :to_hash
14
+
15
+ def initialize(index = {})
16
+ @index = index
17
+ end
18
+
19
+ def [](val)
20
+ @index[val] ||= []
21
+ end
22
+
23
+ def merge!(tracker_hits_per_path)
24
+ @index.merge!(tracker_hits_per_path) { |_h, actual, to_merge| merge_tracker_hits(actual, to_merge) }
25
+ self
26
+ end
27
+
28
+ private def merge_tracker_hits(hits, to_merge)
29
+ unless hits.size == to_merge.size
30
+ raise "Attempting to merge trackers of different sizes: #{hits.size} vs #{to_merge.size}"
31
+ end
32
+ hits.map!.with_index { |val, i| val + to_merge[i] }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ bootstrap
5
+
6
+ # List of allocated trackers from a bucket.
7
+ # Should be thought of as a simple array of integers with
8
+ # a limited interface.
9
+ class TrackerBucket
10
+ class TrackerStorage
11
+ extend Forwardable
12
+ def_delegators :@array, :[], :size, :each, :map, :fetch
13
+
14
+ attr_reader :bucket
15
+
16
+ def initialize(bucket:, array:, index:)
17
+ @bucket = bucket
18
+ @array = array
19
+ @index = index
20
+ @allocated = 0
21
+ end
22
+
23
+ # Returns a range of tracker ids
24
+ def allocate_trackers(nb_needed)
25
+ prev = @allocated
26
+ @allocated += nb_needed
27
+ missing = @allocated - @array.size
28
+ @array.concat(Array.new(missing, 0)) if missing > 0
29
+ prev...@allocated
30
+ end
31
+
32
+ def setup_source
33
+ "(#{bucket.setup_source})[#{@index}]||=Array.new(#{size},0)"
34
+ end
35
+
36
+ def tracker_source(tracker_id)
37
+ "#{bucket.source}[#{@index}][#{tracker_id}]+=1"
38
+ end
39
+
40
+ def tracker_hits
41
+ @array.dup.freeze
42
+ end
43
+
44
+ def tracker_hits=(new_hits)
45
+ if new_hits.size != @array.size
46
+ warn 'Replacing tracker hits with array of different size'
47
+ end
48
+ @array.replace(new_hits)
49
+ end
50
+
51
+ private
52
+
53
+ def dump
54
+ {bucket: @bucket, index: @index, size: @array.size}
55
+ end
56
+
57
+ def _dump(_level)
58
+ Marshal.dump(dump)
59
+ end
60
+
61
+ class << self
62
+ private def load(bucket:, index:, size:)
63
+ storage = bucket.create_storage(index)
64
+ storage.allocate_trackers(size - storage.size)
65
+ storage
66
+ end
67
+
68
+ private def _load(data)
69
+ load(Marshal.load(data)) # rubocop:disable Security/MarshalLoad
70
+ end
71
+ end
72
+ end
73
+
74
+ private_constant :TrackerStorage
75
+ end
76
+ end