deep-cover 0.5.2 → 0.5.3

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