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.
- checksums.yaml +7 -0
- data/.rspec +4 -0
- data/.rspec_all +3 -0
- data/.rubocop.yml +1 -0
- data/Gemfile +11 -0
- data/deep_cover_core.gemspec +46 -0
- data/lib/deep-cover.rb +3 -0
- data/lib/deep_cover/analyser/base.rb +104 -0
- data/lib/deep_cover/analyser/branch.rb +41 -0
- data/lib/deep_cover/analyser/covered_code_source.rb +21 -0
- data/lib/deep_cover/analyser/function.rb +14 -0
- data/lib/deep_cover/analyser/node.rb +54 -0
- data/lib/deep_cover/analyser/per_char.rb +38 -0
- data/lib/deep_cover/analyser/per_line.rb +41 -0
- data/lib/deep_cover/analyser/ruby25_like_branch.rb +211 -0
- data/lib/deep_cover/analyser/statement.rb +33 -0
- data/lib/deep_cover/analyser/stats.rb +54 -0
- data/lib/deep_cover/analyser/subset.rb +27 -0
- data/lib/deep_cover/analyser.rb +23 -0
- data/lib/deep_cover/auto_run.rb +71 -0
- data/lib/deep_cover/autoload_tracker.rb +215 -0
- data/lib/deep_cover/backports.rb +22 -0
- data/lib/deep_cover/base.rb +117 -0
- data/lib/deep_cover/basics.rb +22 -0
- data/lib/deep_cover/builtin_takeover.rb +10 -0
- data/lib/deep_cover/config.rb +104 -0
- data/lib/deep_cover/config_setter.rb +33 -0
- data/lib/deep_cover/core_ext/autoload_overrides.rb +112 -0
- data/lib/deep_cover/core_ext/coverage_replacement.rb +61 -0
- data/lib/deep_cover/core_ext/exec_callbacks.rb +27 -0
- data/lib/deep_cover/core_ext/instruction_sequence_load_iseq.rb +32 -0
- data/lib/deep_cover/core_ext/load_overrides.rb +19 -0
- data/lib/deep_cover/core_ext/require_overrides.rb +28 -0
- data/lib/deep_cover/coverage/analysis.rb +42 -0
- data/lib/deep_cover/coverage/persistence.rb +84 -0
- data/lib/deep_cover/coverage.rb +125 -0
- data/lib/deep_cover/covered_code.rb +145 -0
- data/lib/deep_cover/custom_requirer.rb +187 -0
- data/lib/deep_cover/flag_comment_associator.rb +68 -0
- data/lib/deep_cover/load.rb +66 -0
- data/lib/deep_cover/memoize.rb +48 -0
- data/lib/deep_cover/module_override.rb +39 -0
- data/lib/deep_cover/node/arguments.rb +51 -0
- data/lib/deep_cover/node/assignments.rb +273 -0
- data/lib/deep_cover/node/base.rb +155 -0
- data/lib/deep_cover/node/begin.rb +27 -0
- data/lib/deep_cover/node/block.rb +61 -0
- data/lib/deep_cover/node/branch.rb +32 -0
- data/lib/deep_cover/node/case.rb +113 -0
- data/lib/deep_cover/node/collections.rb +31 -0
- data/lib/deep_cover/node/const.rb +12 -0
- data/lib/deep_cover/node/def.rb +40 -0
- data/lib/deep_cover/node/empty_body.rb +32 -0
- data/lib/deep_cover/node/exceptions.rb +79 -0
- data/lib/deep_cover/node/if.rb +73 -0
- data/lib/deep_cover/node/keywords.rb +86 -0
- data/lib/deep_cover/node/literals.rb +100 -0
- data/lib/deep_cover/node/loops.rb +74 -0
- data/lib/deep_cover/node/mixin/can_augment_children.rb +65 -0
- data/lib/deep_cover/node/mixin/check_completion.rb +18 -0
- data/lib/deep_cover/node/mixin/child_can_be_empty.rb +27 -0
- data/lib/deep_cover/node/mixin/executed_after_children.rb +15 -0
- data/lib/deep_cover/node/mixin/execution_location.rb +66 -0
- data/lib/deep_cover/node/mixin/filters.rb +47 -0
- data/lib/deep_cover/node/mixin/flow_accounting.rb +71 -0
- data/lib/deep_cover/node/mixin/has_child.rb +145 -0
- data/lib/deep_cover/node/mixin/has_child_handler.rb +75 -0
- data/lib/deep_cover/node/mixin/has_tracker.rb +46 -0
- data/lib/deep_cover/node/mixin/is_statement.rb +20 -0
- data/lib/deep_cover/node/mixin/rewriting.rb +35 -0
- data/lib/deep_cover/node/mixin/wrapper.rb +15 -0
- data/lib/deep_cover/node/module.rb +66 -0
- data/lib/deep_cover/node/root.rb +20 -0
- data/lib/deep_cover/node/send.rb +161 -0
- data/lib/deep_cover/node/short_circuit.rb +42 -0
- data/lib/deep_cover/node/splat.rb +15 -0
- data/lib/deep_cover/node/variables.rb +16 -0
- data/lib/deep_cover/node.rb +23 -0
- data/lib/deep_cover/parser_ext/range.rb +21 -0
- data/lib/deep_cover/problem_with_diagnostic.rb +63 -0
- data/lib/deep_cover/reporter/base.rb +68 -0
- data/lib/deep_cover/reporter/html/base.rb +14 -0
- data/lib/deep_cover/reporter/html/index.rb +59 -0
- data/lib/deep_cover/reporter/html/site.rb +68 -0
- data/lib/deep_cover/reporter/html/source.rb +136 -0
- data/lib/deep_cover/reporter/html/template/assets/32px.png +0 -0
- data/lib/deep_cover/reporter/html/template/assets/40px.png +0 -0
- data/lib/deep_cover/reporter/html/template/assets/deep_cover.css +291 -0
- data/lib/deep_cover/reporter/html/template/assets/deep_cover.css.sass +336 -0
- data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.js +4 -0
- data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.map +1 -0
- data/lib/deep_cover/reporter/html/template/assets/jstree.css +1108 -0
- data/lib/deep_cover/reporter/html/template/assets/jstree.js +8424 -0
- data/lib/deep_cover/reporter/html/template/assets/jstreetable.js +1069 -0
- data/lib/deep_cover/reporter/html/template/assets/throbber.gif +0 -0
- data/lib/deep_cover/reporter/html/template/index.html.erb +75 -0
- data/lib/deep_cover/reporter/html/template/source.html.erb +35 -0
- data/lib/deep_cover/reporter/html.rb +15 -0
- data/lib/deep_cover/reporter/istanbul.rb +184 -0
- data/lib/deep_cover/reporter/text.rb +58 -0
- data/lib/deep_cover/reporter/tree/util.rb +86 -0
- data/lib/deep_cover/reporter.rb +10 -0
- data/lib/deep_cover/tools/blank.rb +25 -0
- data/lib/deep_cover/tools/builtin_coverage.rb +55 -0
- data/lib/deep_cover/tools/camelize.rb +13 -0
- data/lib/deep_cover/tools/content_tag.rb +11 -0
- data/lib/deep_cover/tools/covered.rb +9 -0
- data/lib/deep_cover/tools/execute_sample.rb +34 -0
- data/lib/deep_cover/tools/format.rb +18 -0
- data/lib/deep_cover/tools/format_char_cover.rb +19 -0
- data/lib/deep_cover/tools/format_generated_code.rb +27 -0
- data/lib/deep_cover/tools/indent_string.rb +26 -0
- data/lib/deep_cover/tools/merge.rb +16 -0
- data/lib/deep_cover/tools/number_lines.rb +22 -0
- data/lib/deep_cover/tools/our_coverage.rb +11 -0
- data/lib/deep_cover/tools/profiling.rb +68 -0
- data/lib/deep_cover/tools/render_template.rb +13 -0
- data/lib/deep_cover/tools/require_relative_dir.rb +12 -0
- data/lib/deep_cover/tools/scan_match_datas.rb +10 -0
- data/lib/deep_cover/tools/silence_warnings.rb +18 -0
- data/lib/deep_cover/tools/slice.rb +9 -0
- data/lib/deep_cover/tools/strip_heredoc.rb +18 -0
- data/lib/deep_cover/tools/truncate_backtrace.rb +32 -0
- data/lib/deep_cover/tools.rb +22 -0
- data/lib/deep_cover/tracker_bucket.rb +50 -0
- data/lib/deep_cover/tracker_hits_per_path.rb +35 -0
- data/lib/deep_cover/tracker_storage.rb +76 -0
- data/lib/deep_cover/tracker_storage_per_path.rb +34 -0
- data/lib/deep_cover/version.rb +5 -0
- data/lib/deep_cover.rb +22 -0
- 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
data/.rspec_all
ADDED
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,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
|