deep-cover-core 0.6.3.pre

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 (131) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +4 -0
  3. data/.rspec_all +3 -0
  4. data/.rubocop.yml +1 -0
  5. data/Gemfile +11 -0
  6. data/deep_cover_core.gemspec +46 -0
  7. data/lib/deep-cover.rb +3 -0
  8. data/lib/deep_cover/analyser/base.rb +104 -0
  9. data/lib/deep_cover/analyser/branch.rb +41 -0
  10. data/lib/deep_cover/analyser/covered_code_source.rb +21 -0
  11. data/lib/deep_cover/analyser/function.rb +14 -0
  12. data/lib/deep_cover/analyser/node.rb +54 -0
  13. data/lib/deep_cover/analyser/per_char.rb +38 -0
  14. data/lib/deep_cover/analyser/per_line.rb +41 -0
  15. data/lib/deep_cover/analyser/ruby25_like_branch.rb +211 -0
  16. data/lib/deep_cover/analyser/statement.rb +33 -0
  17. data/lib/deep_cover/analyser/stats.rb +54 -0
  18. data/lib/deep_cover/analyser/subset.rb +27 -0
  19. data/lib/deep_cover/analyser.rb +23 -0
  20. data/lib/deep_cover/auto_run.rb +71 -0
  21. data/lib/deep_cover/autoload_tracker.rb +215 -0
  22. data/lib/deep_cover/backports.rb +22 -0
  23. data/lib/deep_cover/base.rb +117 -0
  24. data/lib/deep_cover/basics.rb +22 -0
  25. data/lib/deep_cover/builtin_takeover.rb +10 -0
  26. data/lib/deep_cover/config.rb +104 -0
  27. data/lib/deep_cover/config_setter.rb +33 -0
  28. data/lib/deep_cover/core_ext/autoload_overrides.rb +112 -0
  29. data/lib/deep_cover/core_ext/coverage_replacement.rb +61 -0
  30. data/lib/deep_cover/core_ext/exec_callbacks.rb +27 -0
  31. data/lib/deep_cover/core_ext/instruction_sequence_load_iseq.rb +32 -0
  32. data/lib/deep_cover/core_ext/load_overrides.rb +19 -0
  33. data/lib/deep_cover/core_ext/require_overrides.rb +28 -0
  34. data/lib/deep_cover/coverage/analysis.rb +42 -0
  35. data/lib/deep_cover/coverage/persistence.rb +84 -0
  36. data/lib/deep_cover/coverage.rb +125 -0
  37. data/lib/deep_cover/covered_code.rb +145 -0
  38. data/lib/deep_cover/custom_requirer.rb +187 -0
  39. data/lib/deep_cover/flag_comment_associator.rb +68 -0
  40. data/lib/deep_cover/load.rb +66 -0
  41. data/lib/deep_cover/memoize.rb +48 -0
  42. data/lib/deep_cover/module_override.rb +39 -0
  43. data/lib/deep_cover/node/arguments.rb +51 -0
  44. data/lib/deep_cover/node/assignments.rb +273 -0
  45. data/lib/deep_cover/node/base.rb +155 -0
  46. data/lib/deep_cover/node/begin.rb +27 -0
  47. data/lib/deep_cover/node/block.rb +61 -0
  48. data/lib/deep_cover/node/branch.rb +32 -0
  49. data/lib/deep_cover/node/case.rb +113 -0
  50. data/lib/deep_cover/node/collections.rb +31 -0
  51. data/lib/deep_cover/node/const.rb +12 -0
  52. data/lib/deep_cover/node/def.rb +40 -0
  53. data/lib/deep_cover/node/empty_body.rb +32 -0
  54. data/lib/deep_cover/node/exceptions.rb +79 -0
  55. data/lib/deep_cover/node/if.rb +73 -0
  56. data/lib/deep_cover/node/keywords.rb +86 -0
  57. data/lib/deep_cover/node/literals.rb +100 -0
  58. data/lib/deep_cover/node/loops.rb +74 -0
  59. data/lib/deep_cover/node/mixin/can_augment_children.rb +65 -0
  60. data/lib/deep_cover/node/mixin/check_completion.rb +18 -0
  61. data/lib/deep_cover/node/mixin/child_can_be_empty.rb +27 -0
  62. data/lib/deep_cover/node/mixin/executed_after_children.rb +15 -0
  63. data/lib/deep_cover/node/mixin/execution_location.rb +66 -0
  64. data/lib/deep_cover/node/mixin/filters.rb +47 -0
  65. data/lib/deep_cover/node/mixin/flow_accounting.rb +71 -0
  66. data/lib/deep_cover/node/mixin/has_child.rb +145 -0
  67. data/lib/deep_cover/node/mixin/has_child_handler.rb +75 -0
  68. data/lib/deep_cover/node/mixin/has_tracker.rb +46 -0
  69. data/lib/deep_cover/node/mixin/is_statement.rb +20 -0
  70. data/lib/deep_cover/node/mixin/rewriting.rb +35 -0
  71. data/lib/deep_cover/node/mixin/wrapper.rb +15 -0
  72. data/lib/deep_cover/node/module.rb +66 -0
  73. data/lib/deep_cover/node/root.rb +20 -0
  74. data/lib/deep_cover/node/send.rb +161 -0
  75. data/lib/deep_cover/node/short_circuit.rb +42 -0
  76. data/lib/deep_cover/node/splat.rb +15 -0
  77. data/lib/deep_cover/node/variables.rb +16 -0
  78. data/lib/deep_cover/node.rb +23 -0
  79. data/lib/deep_cover/parser_ext/range.rb +21 -0
  80. data/lib/deep_cover/problem_with_diagnostic.rb +63 -0
  81. data/lib/deep_cover/reporter/base.rb +68 -0
  82. data/lib/deep_cover/reporter/html/base.rb +14 -0
  83. data/lib/deep_cover/reporter/html/index.rb +59 -0
  84. data/lib/deep_cover/reporter/html/site.rb +68 -0
  85. data/lib/deep_cover/reporter/html/source.rb +136 -0
  86. data/lib/deep_cover/reporter/html/template/assets/32px.png +0 -0
  87. data/lib/deep_cover/reporter/html/template/assets/40px.png +0 -0
  88. data/lib/deep_cover/reporter/html/template/assets/deep_cover.css +291 -0
  89. data/lib/deep_cover/reporter/html/template/assets/deep_cover.css.sass +336 -0
  90. data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.js +4 -0
  91. data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.map +1 -0
  92. data/lib/deep_cover/reporter/html/template/assets/jstree.css +1108 -0
  93. data/lib/deep_cover/reporter/html/template/assets/jstree.js +8424 -0
  94. data/lib/deep_cover/reporter/html/template/assets/jstreetable.js +1069 -0
  95. data/lib/deep_cover/reporter/html/template/assets/throbber.gif +0 -0
  96. data/lib/deep_cover/reporter/html/template/index.html.erb +75 -0
  97. data/lib/deep_cover/reporter/html/template/source.html.erb +35 -0
  98. data/lib/deep_cover/reporter/html.rb +15 -0
  99. data/lib/deep_cover/reporter/istanbul.rb +184 -0
  100. data/lib/deep_cover/reporter/text.rb +58 -0
  101. data/lib/deep_cover/reporter/tree/util.rb +86 -0
  102. data/lib/deep_cover/reporter.rb +10 -0
  103. data/lib/deep_cover/tools/blank.rb +25 -0
  104. data/lib/deep_cover/tools/builtin_coverage.rb +55 -0
  105. data/lib/deep_cover/tools/camelize.rb +13 -0
  106. data/lib/deep_cover/tools/content_tag.rb +11 -0
  107. data/lib/deep_cover/tools/covered.rb +9 -0
  108. data/lib/deep_cover/tools/execute_sample.rb +34 -0
  109. data/lib/deep_cover/tools/format.rb +18 -0
  110. data/lib/deep_cover/tools/format_char_cover.rb +19 -0
  111. data/lib/deep_cover/tools/format_generated_code.rb +27 -0
  112. data/lib/deep_cover/tools/indent_string.rb +26 -0
  113. data/lib/deep_cover/tools/merge.rb +16 -0
  114. data/lib/deep_cover/tools/number_lines.rb +22 -0
  115. data/lib/deep_cover/tools/our_coverage.rb +11 -0
  116. data/lib/deep_cover/tools/profiling.rb +68 -0
  117. data/lib/deep_cover/tools/render_template.rb +13 -0
  118. data/lib/deep_cover/tools/require_relative_dir.rb +12 -0
  119. data/lib/deep_cover/tools/scan_match_datas.rb +10 -0
  120. data/lib/deep_cover/tools/silence_warnings.rb +18 -0
  121. data/lib/deep_cover/tools/slice.rb +9 -0
  122. data/lib/deep_cover/tools/strip_heredoc.rb +18 -0
  123. data/lib/deep_cover/tools/truncate_backtrace.rb +32 -0
  124. data/lib/deep_cover/tools.rb +22 -0
  125. data/lib/deep_cover/tracker_bucket.rb +50 -0
  126. data/lib/deep_cover/tracker_hits_per_path.rb +35 -0
  127. data/lib/deep_cover/tracker_storage.rb +76 -0
  128. data/lib/deep_cover/tracker_storage_per_path.rb +34 -0
  129. data/lib/deep_cover/version.rb +5 -0
  130. data/lib/deep_cover.rb +22 -0
  131. metadata +329 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: debeec0965c606820ce563e1c2fb785d14106c58
4
+ data.tar.gz: 35883be54022a4cc9340b15df798c3a589f3967e
5
+ SHA512:
6
+ metadata.gz: 9e0f41ed90e2c47113239eeb0d31cad3663545280aadfc5f9146027002c8e21617759e2627682a23cb3c64d558da41bc065d5f85694401dc88d8c7dc140a6002
7
+ data.tar.gz: ca966b619af0f4bd829bcc16c34818108f43a398d7ca8b23dc3063697a83d4d542da6d3a7f468ac2057d9cd1fe9742648cd69b65df5a2cb7c2f00c403ac4e6ea
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --exclude-pattern "spec/code_fixtures/**/*.rb"
3
+ --tag ~slow
4
+ --tag ~skip
data/.rspec_all ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --exclude-pattern "spec/code_fixtures/**/*.rb"
3
+ --tag ~skip
data/.rubocop.yml ADDED
@@ -0,0 +1 @@
1
+ inherit_from: ../.rubocop.yml
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in deep_cover.gemspec
8
+ gemspec
9
+
10
+ local = File.expand_path('Gemfile.local', __dir__)
11
+ eval_gemfile local if File.exist?(local)
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/deep_cover/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'deep-cover-core'
7
+ spec.version = DeepCover::VERSION
8
+ spec.authors = ['Marc-André Lafortune', 'Maxime Lapointe']
9
+ spec.email = ['github@marc-andre.ca', 'hunter_spawn@hotmail.com']
10
+
11
+ spec.summary = 'In depth coverage of your Ruby code.'
12
+ spec.description = 'Core functionality for the DeepCover gem.'
13
+ spec.homepage = 'https://github.com/deep-cover/deep-cover'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ ### Runtime dependencies
24
+ spec.required_ruby_version = '>= 2.1.0'
25
+
26
+ # Main dependency
27
+ spec.add_runtime_dependency 'parser', '~> 2.5.0'
28
+
29
+ # Support
30
+ spec.add_runtime_dependency 'backports', '>= 3.11.0'
31
+ spec.add_runtime_dependency 'binding_of_caller'
32
+
33
+ # Reporters
34
+ spec.add_runtime_dependency 'terminal-table'
35
+
36
+ # While in 0.x
37
+ spec.add_runtime_dependency 'pry'
38
+
39
+ ### Dev dependencies
40
+ spec.add_development_dependency 'bundler', '~> 1.15'
41
+ spec.add_development_dependency 'psych', '>= 2.0'
42
+ spec.add_development_dependency 'rake', '~> 12.0'
43
+ spec.add_development_dependency 'rspec', '~> 3.0'
44
+ spec.add_development_dependency 'rubocop', '0.53.0' # About every single release breaks something
45
+ spec.add_development_dependency 'sass'
46
+ end
data/lib/deep-cover.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'deep_cover'
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ module Analyser::Base
5
+ include Tools::Covered
6
+
7
+ attr_reader :source, :options
8
+
9
+ def initialize(source, **options)
10
+ @source = to_source(source, **options)
11
+ @options = options
12
+ end
13
+
14
+ # Looking exclusively at our subset of nodes, returns the node's direct descendants
15
+ def node_children(node)
16
+ @source.node_children(node)
17
+ end
18
+
19
+ # Returns the number of runs of the node (assumed to be in our subset)
20
+ def node_runs(node)
21
+ @source.node_runs(node)
22
+ end
23
+
24
+ def node_covered?(node)
25
+ covered?(node_runs(node))
26
+ end
27
+
28
+ def node_runs_map
29
+ each_node.map do |node|
30
+ [node, node_runs(node)]
31
+ end.to_h
32
+ end
33
+
34
+ # Analyser-specific output
35
+ def results
36
+ node_runs_map
37
+ end
38
+
39
+ def node_stat_type(node)
40
+ return :not_executable unless node.executable?
41
+ case node_runs(node)
42
+ when nil
43
+ :ignored
44
+ when 0
45
+ :not_executed
46
+ else
47
+ :executed
48
+ end
49
+ end
50
+
51
+ def node_stat_contributions(nodes)
52
+ if respond_to? :node_stat_contribution
53
+ nodes.sum { |n| node_stat_contribution(n) }
54
+ else
55
+ nodes.size
56
+ end
57
+ end
58
+
59
+ def stats
60
+ st = each_node.group_by { |n| node_stat_type(n) }
61
+ .transform_values { |nodes| node_stat_contributions(nodes) }
62
+ Analyser::Stats.new(**st)
63
+ end
64
+
65
+ # Iterates on nodes in the subset.
66
+ # Yields the node and it's children (within the subset)
67
+ def each_node(from = covered_code.root, &block)
68
+ return to_enum(:each_node) unless block_given?
69
+ begin
70
+ yield from unless from.is_a?(Node::Root)
71
+ rescue ProblemWithDiagnostic
72
+ raise
73
+ rescue StandardError, SystemStackError => e
74
+ raise ProblemWithDiagnostic.new(covered_code, from.diagnostic_expression, e)
75
+ end
76
+ node_children(from).each do |child|
77
+ each_node(child, &block)
78
+ end
79
+ end
80
+
81
+ def covered_code
82
+ @source.covered_code
83
+ end
84
+
85
+ protected
86
+
87
+ def convert(covered_code, **options)
88
+ Analyser::Node.new(covered_code, **options)
89
+ end
90
+
91
+ def to_source(source, **options)
92
+ case source
93
+ when Analyser
94
+ source
95
+ when CoveredCode
96
+ convert(source, **options)
97
+ when Node
98
+ convert(source.covered_code, **options)
99
+ else
100
+ raise ArgumentError, "expected Analyser, Node or CoveredCode, got #{source.class}"
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'subset'
4
+
5
+ module DeepCover
6
+ class Analyser::Branch < Analyser
7
+ def self.human_name
8
+ 'Branches'
9
+ end
10
+ include Analyser::Subset
11
+ SUBSET_CLASSES = [Node::Branch].freeze
12
+
13
+ def node_runs(node)
14
+ runs = super
15
+ if node.is_a?(Node::Branch) && covered?(runs)
16
+ worst = worst_branch_runs(node)
17
+ runs = worst unless covered?(worst)
18
+ end
19
+ runs
20
+ end
21
+
22
+ def results
23
+ each_node.map do |node|
24
+ branches_runs = node.branches.map { |jump| [jump, branch_runs(jump)] }.to_h
25
+ [node, branches_runs]
26
+ end.to_h
27
+ end
28
+
29
+ private
30
+
31
+ def worst_branch_runs(fork)
32
+ fork.branches.map { |jump| branch_runs(jump) }
33
+ .sort_by { |runs| runs == 0 ? -2 : runs || -1 }
34
+ .first
35
+ end
36
+
37
+ def branch_runs(branch)
38
+ branch.flow_entry_count.nonzero? || source.node_runs(branch)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ class Analyser::CoveredCodeSource < Analyser
5
+ attr_reader :covered_code
6
+
7
+ def initialize(covered_code)
8
+ @covered_code = covered_code.freeze
9
+ end
10
+
11
+ # Looking exclusively at our subset of nodes, returns the node's direct descendants
12
+ def node_children(node)
13
+ node.children_nodes
14
+ end
15
+
16
+ # Returns the number of runs of the node (assumed to be in our subset)
17
+ def node_runs(node)
18
+ node.execution_count if node.executable?
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'subset'
4
+
5
+ module DeepCover
6
+ class Analyser::Function < Analyser
7
+ include Analyser::Subset
8
+ SUBSET_CLASSES = [Node::Block, Node::Defs, Node::Def].freeze
9
+
10
+ def node_runs(node)
11
+ super(node.body)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ class Analyser::Node < Analyser
5
+ include Analyser::Subset
6
+
7
+ def self.human_name
8
+ 'Nodes'
9
+ end
10
+
11
+ def initialize(source, ignore_uncovered: [], **options)
12
+ @cache = {}.compare_by_identity
13
+ super
14
+ @allow_filters = Array(ignore_uncovered).map { |kind| Node.filter_to_method_name(kind) }
15
+ @nocov_ranges = FlagCommentAssociator.new(covered_code)
16
+ end
17
+
18
+ def node_runs(node)
19
+ @cache.fetch(node) do
20
+ runs = super
21
+ runs = nil if runs == 0 && should_be_ignored?(node)
22
+ @cache[node] = runs
23
+ end
24
+ end
25
+
26
+ def in_subset?(node, _parent)
27
+ node.executable?
28
+ end
29
+
30
+ protected
31
+
32
+ def convert(node, **) # rubocop:disable Naming/UncommunicativeMethodParamName [#5436]
33
+ Analyser::CoveredCodeSource.new(node)
34
+ end
35
+
36
+ private
37
+
38
+ def should_be_ignored?(node)
39
+ @nocov_ranges.include?(node) ||
40
+ @allow_filters.any? { |f| node.public_send(f) } ||
41
+ is_ignored?(node.parent)
42
+ end
43
+
44
+ def is_ignored?(node)
45
+ if node == nil
46
+ false
47
+ elsif node.executable?
48
+ node_runs(node).nil?
49
+ else
50
+ is_ignored?(node.parent)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ class Analyser::PerChar < Analyser
5
+ def self.human_name
6
+ 'Chars'
7
+ end
8
+
9
+ # Returns an array of characters for each line of code.
10
+ # Each character is either ' ' (executed), '-' (not executable) or 'x' (not covered)
11
+ def results
12
+ bc = buffer.source_lines.map { |line| '-' * line.size }
13
+ each_node do |node|
14
+ runs = node_runs(node)
15
+ next if runs == nil
16
+ node.proper_range.each do |pos|
17
+ bc[buffer.line_for_position(pos) - buffer.first_line][buffer.column_for_position(pos)] = runs > 0 ? ' ' : 'x'
18
+ end
19
+ end
20
+ bc.zip(buffer.source_lines) { |cov, line| cov[line.size..-1] = '' } # remove extraneous character for end lines, in any
21
+ bc
22
+ end
23
+
24
+ def node_stat_contribution(node)
25
+ node.executed_locs.sum(&:size)
26
+ end
27
+
28
+ def stats
29
+ s = super
30
+ actual_total = buffer.source.size
31
+ s.with not_executable: actual_total - s.total
32
+ end
33
+
34
+ def buffer
35
+ covered_code.buffer
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ class Analyser::PerLine < Analyser
5
+ # Returns an array of runs, one per line.
6
+ # allow_partial can be one of:
7
+ # true: Allow any partial covering. Basically ruby's line coverage,
8
+ # if any thing is executed, it is considered executed
9
+ # branch: Only allow branches to be partially covered.
10
+ # if a node is not executed, the line has to be marked as not executed, even if part of it was.
11
+ # false: Allow nothing to be partially covered.
12
+ # same as :branch, but also:
13
+ # if an empty branch is not executed, the line has to be marked as not executed.
14
+ # This is only for empty branches because, if they are not empty, there will already
15
+ # be some red from the partial node covering. We don't everything to become red,
16
+ # just want 100% coverage to be as hard as branch + node coverage.
17
+ def results
18
+ allow_partial = options.fetch(:allow_partial, true)
19
+ line_hits = Array.new(covered_code.nb_lines + covered_code.lineno - 1)
20
+ disallowed_lines = Set.new
21
+ each_node do |node|
22
+ next unless (runs = node_runs(node))
23
+ node.executed_locs.each do |loc|
24
+ line_index = loc.line - 1
25
+
26
+ next if disallowed_lines.include?(line_index)
27
+ disallowed_lines << line_index if [nil, false, :branch].include?(allow_partial) && runs == 0
28
+ disallowed_lines << line_index if !allow_partial && missed_empty_branch?(node)
29
+
30
+ line_hits[line_index] = [line_hits[line_index] || 0, runs].max
31
+ end
32
+ end
33
+ disallowed_lines.each { |line_index| line_hits[line_index] = 0 }
34
+ line_hits
35
+ end
36
+
37
+ def missed_empty_branch?(node)
38
+ node.is_a?(Node::Branch) && node.branches.any? { |b| b.is_a?(Node::EmptyBody) && !Tools.covered?(node_runs(b)) }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'subset'
4
+
5
+ module DeepCover
6
+ class Analyser::Ruby25LikeBranch < Analyser
7
+ def self.human_name
8
+ 'Ruby25 branches'
9
+ end
10
+ include Analyser::Subset
11
+ SUBSET_CLASSES = [Node::Case, Node::Csend, Node::If, Node::ShortCircuit,
12
+ Node::Until, Node::UntilPost, Node::While, Node::WhilePost,
13
+ ].freeze
14
+
15
+ def initialize(*args)
16
+ super
17
+ @loc_index = 0
18
+ end
19
+
20
+ def results
21
+ extractor = NodeCoverageExtrator.new
22
+ each_node.map do |node|
23
+ extractor.branch_coverage(node)
24
+ end.to_h
25
+ end
26
+
27
+ # This is the class doing the work. Since everything is about the node, the class delegates
28
+ # missing methods to the node, simplifying the code.
29
+ class NodeCoverageExtrator < SimpleDelegator
30
+ def initialize(node = nil)
31
+ self.node = node
32
+ @loc_index = 0
33
+ end
34
+
35
+ alias_method :node=, :__setobj__
36
+ alias_method :node, :__getobj__
37
+
38
+ def branch_coverage(node)
39
+ self.node = node
40
+ case node
41
+ when Node::Case
42
+ handle_case
43
+ when Node::Csend
44
+ handle_csend
45
+ when Node::If
46
+ handle_if
47
+ when Node::ShortCircuit
48
+ handle_short_circuit
49
+ when Node::Until, Node::While, Node::UntilPost, Node::WhilePost
50
+ handle_until_while
51
+ end
52
+ end
53
+
54
+ def handle_case
55
+ cond_info = [:case, *node_loc_infos]
56
+
57
+ sub_keys = [:when] * (branches.size - 1) + [:else]
58
+ empty_fallbacks = whens.map { |w| (w.loc_hash[:begin] || w.loc_hash[:expression]).wrap_rwhitespace_and_comments.end }
59
+ empty_fallbacks.map!(&:begin)
60
+
61
+ if loc_hash[:else]
62
+ empty_fallbacks << loc_hash[:end].begin
63
+ else
64
+ # DeepCover manually inserts a `else` for Case when there isn't one for tracker purposes.
65
+ # The normal behavior of ruby25's branch coverage when there is no else is to return the loc of the node
66
+ # So we sent that fallback.
67
+ empty_fallbacks << expression
68
+ end
69
+
70
+ branches_locs = whens.map do |when_node|
71
+ next when_node.body if when_node.body.is_a?(Node::EmptyBody)
72
+
73
+ start_at = when_node.loc_hash[:begin]
74
+ start_at = start_at.wrap_rwhitespace_and_comments.end if start_at
75
+ start_at ||= when_node.body.expression.begin
76
+
77
+ end_at = when_node.body.expression.end
78
+ start_at.with(end_pos: end_at.end_pos)
79
+ end
80
+
81
+ branches_locs << node.else
82
+ clauses_infos = infos_for_branches(branches_locs, sub_keys, empty_fallbacks, execution_counts: branches.map(&:execution_count))
83
+
84
+ [cond_info, clauses_infos]
85
+ end
86
+
87
+ def handle_csend
88
+ # csend wraps the comment but not the newlines
89
+ node_range = node.expression.wrap_rwhitespace_and_comments(whitespaces: /\A[ \t\r\f]+/)
90
+ cond_info = [:"&.", *node_loc_infos(node_range)]
91
+ false_branch, true_branch = branches
92
+ [cond_info, {[:then, *node_loc_infos(node_range)] => true_branch.execution_count,
93
+ [:else, *node_loc_infos(node_range)] => false_branch.execution_count,
94
+ },
95
+ ]
96
+ end
97
+
98
+ def handle_if
99
+ key = style == :unless ? :unless : :if
100
+
101
+ node_range = extend_elsif_range
102
+ cond_info = [key, *node_loc_infos(node_range)]
103
+
104
+ sub_keys = [:then, :else]
105
+ if style == :ternary
106
+ empty_fallback_locs = [nil, nil]
107
+ else
108
+ else_loc = loc_hash[:else]
109
+
110
+ first_clause_fallback = loc_hash[:begin]
111
+ if first_clause_fallback
112
+ first_clause_fallback = first_clause_fallback.wrap_rwhitespace_and_comments.end
113
+ elsif else_loc
114
+ first_clause_fallback = else_loc.begin
115
+ end
116
+
117
+ if else_loc
118
+ second_clause_fallback = else_loc.wrap_rwhitespace_and_comments.end
119
+ end
120
+ end_loc = root_if_node.loc_hash[:end]
121
+ end_loc = end_loc.begin if end_loc
122
+
123
+ empty_fallback_locs = [first_clause_fallback || end_loc, second_clause_fallback || end_loc]
124
+ end
125
+ # loc can be nil if the clause can't be empty, such as ternary and modifer if/unless
126
+
127
+ if key == :unless
128
+ sub_keys.reverse!
129
+ empty_fallback_locs.reverse!
130
+ end
131
+
132
+ branches_locs = branches
133
+ execution_counts = branches_locs.map(&:execution_count)
134
+ branches_locs[1] = extend_elsif_range(branches_locs[1])
135
+
136
+ clauses_infos = infos_for_branches(branches_locs, sub_keys, empty_fallback_locs, execution_counts: execution_counts, node_range: node_range)
137
+ [cond_info, clauses_infos]
138
+ end
139
+
140
+ def handle_short_circuit
141
+ cond_info = [operator, *node_loc_infos]
142
+ sub_keys = [:then, :else]
143
+ sub_keys.reverse! if node.is_a?(Node::Or)
144
+
145
+ [cond_info, infos_for_branches(branches, sub_keys, [nil, nil])]
146
+ end
147
+
148
+ def handle_until_while
149
+ key = loc_hash[:keyword].source.to_sym
150
+ base_info = [key, *node_loc_infos]
151
+ body_node = if node.is_a?(Node::WhilePost) || node.is_a?(Node::UntilPost)
152
+ if !body.instructions.empty?
153
+ end_pos = body.instructions.last.expression.end_pos
154
+ body.instructions.first.expression.with(end_pos: end_pos)
155
+ else
156
+ body.loc_hash[:end].begin
157
+ end
158
+ elsif body.is_a?(Node::Begin) && !node.body.expressions.empty?
159
+ end_pos = body.expressions.last.expression.end_pos
160
+ body.expressions.first.expression.with(end_pos: end_pos)
161
+ else
162
+ body
163
+ end
164
+
165
+ [base_info, {[:body, *node_loc_infos(body_node)] => body.execution_count}]
166
+ end
167
+
168
+ protected
169
+
170
+ # If the actual else clause (final one) of an if...elsif...end is empty, then Ruby makes the
171
+ # node reach the `end` in its branch coverage output
172
+ def extend_elsif_range(possible_elsif = node)
173
+ return possible_elsif unless possible_elsif.is_a?(Node::If) && possible_elsif.style == :elsif
174
+ deepest_if = possible_elsif.deepest_elsif_node
175
+ if deepest_if.false_branch.is_a?(Node::EmptyBody)
176
+ return possible_elsif.expression.with(end_pos: possible_elsif.root_if_node.loc_hash[:end].begin_pos)
177
+ end
178
+ possible_elsif
179
+ end
180
+
181
+ def infos_for_branch(branch, key, empty_fallback_loc, execution_count: nil, node_range: node)
182
+ if !branch.is_a?(Node::EmptyBody)
183
+ loc = branch
184
+ elsif branch.expression
185
+ # There is clause, but it is empty
186
+ loc = empty_fallback_loc
187
+ else
188
+ # There is no clause
189
+ loc = node_range
190
+ end
191
+
192
+ execution_count ||= branch.execution_count
193
+ [[key, *node_loc_infos(loc)], execution_count]
194
+ end
195
+
196
+ def infos_for_branches(branches, keys, empty_fallback_locs, execution_counts: [], node_range: node)
197
+ branches_infos = branches.map.with_index do |branch, i|
198
+ infos_for_branch(branch, keys[i], empty_fallback_locs[i], execution_count: execution_counts[i], node_range: node_range)
199
+ end
200
+ branches_infos.to_h
201
+ end
202
+
203
+ def node_loc_infos(node_or_range = node)
204
+ source_range = node_or_range.is_a?(Node) ? node_or_range.expression : node_or_range
205
+
206
+ @loc_index += 1
207
+ [@loc_index, source_range.line, source_range.column, source_range.last_line, source_range.last_column]
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'subset'
4
+
5
+ module DeepCover
6
+ class Analyser::Statement < Analyser
7
+ include Analyser::Subset
8
+ # Returns a map of Range => runs
9
+ def results
10
+ each_node.map do |node|
11
+ [node.expression, node_runs(node)]
12
+ end.to_h
13
+ end
14
+
15
+ private
16
+
17
+ def in_subset?(node, parent)
18
+ is_statement = node.is_statement
19
+ if node.expression.nil?
20
+ false
21
+ elsif is_statement != :if_incompatible
22
+ is_statement
23
+ else
24
+ !compatible_runs?(node_runs(parent), node_runs(node))
25
+ end
26
+ end
27
+
28
+ def compatible_runs?(expression_runs, sub_expression_runs)
29
+ sub_expression_runs.nil? ||
30
+ (sub_expression_runs == 0) == (expression_runs == 0)
31
+ end
32
+ end
33
+ end