path_coverage 0.1.0 → 0.2.0
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 +4 -4
- data/README.md +287 -78
- data/lib/path_coverage/calls.rb +54 -15
- data/lib/path_coverage/condition.rb +25 -2
- data/lib/path_coverage/configuration.rb +44 -7
- data/lib/path_coverage/configuration_paths.rb +56 -0
- data/lib/path_coverage/console_summary.rb +77 -0
- data/lib/path_coverage/gaps.rb +25 -9
- data/lib/path_coverage/html/index.html +3 -17
- data/lib/path_coverage/html/side.js +21 -2
- data/lib/path_coverage/html/sort.js +24 -21
- data/lib/path_coverage/html/style.css +28 -1
- data/lib/path_coverage/html/tabs.js +15 -0
- data/lib/path_coverage/html_index.rb +86 -31
- data/lib/path_coverage/html_index_head.rb +49 -0
- data/lib/path_coverage/html_report.rb +30 -13
- data/lib/path_coverage/html_tabs.rb +26 -0
- data/lib/path_coverage/html_tree.rb +41 -20
- data/lib/path_coverage/json_reader.rb +94 -0
- data/lib/path_coverage/json_writer.rb +41 -9
- data/lib/path_coverage/kernel.rb +42 -20
- data/lib/path_coverage/kinds/base.rb +71 -0
- data/lib/path_coverage/kinds/block.rb +136 -0
- data/lib/path_coverage/kinds/case.rb +89 -0
- data/lib/path_coverage/kinds/chain.rb +45 -0
- data/lib/path_coverage/kinds/choice.rb +68 -0
- data/lib/path_coverage/kinds/contains.rb +111 -0
- data/lib/path_coverage/kinds/safe.rb +151 -0
- data/lib/path_coverage/kinds/write.rb +54 -0
- data/lib/path_coverage/kinds.rb +37 -0
- data/lib/path_coverage/manifest.rb +58 -19
- data/lib/path_coverage/manifest_blocks.rb +49 -10
- data/lib/path_coverage/manifest_chains.rb +76 -0
- data/lib/path_coverage/manifest_choices.rb +30 -17
- data/lib/path_coverage/manifest_folding.rb +91 -0
- data/lib/path_coverage/manifest_ignoring.rb +35 -0
- data/lib/path_coverage/manifest_writes.rb +67 -0
- data/lib/path_coverage/merge.rb +19 -3
- data/lib/path_coverage/post.rb +8 -8
- data/lib/path_coverage/recorder.rb +9 -49
- data/lib/path_coverage/recorder_blocks.rb +68 -0
- data/lib/path_coverage/recorder_cases.rb +23 -0
- data/lib/path_coverage/recorder_chains.rb +40 -0
- data/lib/path_coverage/recorder_writes.rb +26 -0
- data/lib/path_coverage/render.rb +58 -46
- data/lib/path_coverage/result.rb +19 -0
- data/lib/path_coverage/rewriter.rb +58 -77
- data/lib/path_coverage/rewriter_nested.rb +62 -0
- data/lib/path_coverage/site_paths.rb +9 -58
- data/lib/path_coverage/site_tree.rb +12 -57
- data/lib/path_coverage/summary.rb +32 -4
- data/lib/path_coverage/totals.rb +1 -1
- data/lib/path_coverage/version.rb +1 -1
- metadata +30 -6
|
@@ -2,7 +2,11 @@ require 'prism'
|
|
|
2
2
|
require_relative 'condition'
|
|
3
3
|
require_relative 'manifest_blocks'
|
|
4
4
|
require_relative 'manifest_case'
|
|
5
|
+
require_relative 'manifest_chains'
|
|
5
6
|
require_relative 'manifest_choices'
|
|
7
|
+
require_relative 'manifest_folding'
|
|
8
|
+
require_relative 'manifest_ignoring'
|
|
9
|
+
require_relative 'manifest_writes'
|
|
6
10
|
require_relative 'site'
|
|
7
11
|
|
|
8
12
|
module PathCoverage
|
|
@@ -10,9 +14,15 @@ module PathCoverage
|
|
|
10
14
|
# outermost &&, ||, and or or, each ternary, if, unless and case
|
|
11
15
|
# with a subject, with the conditions nested in it.
|
|
12
16
|
module Manifest
|
|
13
|
-
# { site => Condition } for each site in source
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
# { site => Condition } for each site in source, none of
|
|
18
|
+
# them named by an operator in ignored; a call to a method
|
|
19
|
+
# named in present, as a &.'s receiver, is never nil, and a
|
|
20
|
+
# call to one named in nonempty, eg any?, is true exactly
|
|
21
|
+
# when its receiver has items.
|
|
22
|
+
def self.sites(source, path, ignored = [], present = [],
|
|
23
|
+
nonempty = [])
|
|
24
|
+
collector = ConditionSites.new(path, ignored,
|
|
25
|
+
[present, nonempty])
|
|
16
26
|
Prism.parse(source).value.accept(collector)
|
|
17
27
|
collector.sites
|
|
18
28
|
end
|
|
@@ -20,7 +30,10 @@ module PathCoverage
|
|
|
20
30
|
# The source and sites of the covered file at full_path.
|
|
21
31
|
def self.parse(full_path, config)
|
|
22
32
|
source = File.read(full_path)
|
|
23
|
-
|
|
33
|
+
path = config.relative(full_path)
|
|
34
|
+
treated = %w[present nonempty].map { config.treated(_1, path) }
|
|
35
|
+
ignored = config.ignored
|
|
36
|
+
{ source:, sites: sites(source, path, ignored, *treated) }
|
|
24
37
|
end
|
|
25
38
|
|
|
26
39
|
# True for a call to a named method with arguments and no
|
|
@@ -73,13 +86,24 @@ module PathCoverage
|
|
|
73
86
|
class ConditionSites < Prism::Visitor
|
|
74
87
|
include ManifestBlocks
|
|
75
88
|
include ManifestCase
|
|
89
|
+
include ManifestChains
|
|
76
90
|
include ManifestChoices
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
include ManifestFolding
|
|
92
|
+
include ManifestIgnoring
|
|
93
|
+
include ManifestWrites
|
|
94
|
+
|
|
95
|
+
# Starts with no sites found, to find none named by an
|
|
96
|
+
# operator in ignored, and to take a call to a method named
|
|
97
|
+
# in present as never nil, and to one in nonempty as true
|
|
98
|
+
# exactly when its receiver has items.
|
|
99
|
+
def initialize(path, ignored, (present, nonempty))
|
|
80
100
|
@path = path
|
|
101
|
+
@ignored = ignored
|
|
102
|
+
@present = present
|
|
103
|
+
@nonempty = nonempty
|
|
81
104
|
@sites = {}
|
|
82
105
|
@notes = nil
|
|
106
|
+
@inner = nil
|
|
83
107
|
super()
|
|
84
108
|
end
|
|
85
109
|
|
|
@@ -118,10 +142,11 @@ module PathCoverage
|
|
|
118
142
|
add_site(node)
|
|
119
143
|
end
|
|
120
144
|
|
|
121
|
-
# Notes a
|
|
122
|
-
# every call can be seen; any other call is no
|
|
123
|
-
# a condition inside it may be.
|
|
145
|
+
# Notes a &. chain as a site, or a block method's call, when
|
|
146
|
+
# its block's every call can be seen; any other call is no
|
|
147
|
+
# site, though a condition inside it may be.
|
|
124
148
|
def visit_call_node(node)
|
|
149
|
+
return add_site(node) if safe_chain?(node)
|
|
125
150
|
return super unless block_site?(node)
|
|
126
151
|
|
|
127
152
|
add_site(node)
|
|
@@ -130,12 +155,17 @@ module PathCoverage
|
|
|
130
155
|
# Notes node as a site, named where it starts: an if or an
|
|
131
156
|
# unless at its keyword. It claims its place first, so a
|
|
132
157
|
# site found inside one of its leaves or arms follows it.
|
|
158
|
+
# While a leaf is being visited, any site is collected as
|
|
159
|
+
# part of that leaf instead, as it is part of one expression.
|
|
133
160
|
def add_site(node)
|
|
161
|
+
return visit_child_nodes(node) if ignored?(node)
|
|
162
|
+
return @inner << nested_condition_of(node) if @inner
|
|
163
|
+
|
|
134
164
|
site = Site.at(@path, site_loc(node))
|
|
135
165
|
@sites[site] = nil
|
|
136
166
|
outer = @notes
|
|
137
167
|
@notes = { commands: [], fixed: {} }
|
|
138
|
-
condition = condition_of(node)
|
|
168
|
+
condition = scoped { condition_of(node) }
|
|
139
169
|
@sites[site] = condition.with(**@notes)
|
|
140
170
|
@notes = outer
|
|
141
171
|
end
|
|
@@ -143,11 +173,14 @@ module PathCoverage
|
|
|
143
173
|
# node as a Condition, operands and all, named by its
|
|
144
174
|
# operator as written; a ternary is named ?:, an if if and
|
|
145
175
|
# an unless unless, their operands a predicate and arms; a
|
|
146
|
-
# case case, its operands its arms
|
|
176
|
+
# case case, its operands its arms; a write ||= or &&=, its
|
|
177
|
+
# operands its target and its right side; a &. chain &., its
|
|
178
|
+
# operands its receivers.
|
|
147
179
|
def condition_of(node)
|
|
180
|
+
return write_condition(node) if write?(node)
|
|
148
181
|
return choice_of(node) if CHOICE_TYPES.include?(node.type)
|
|
149
182
|
return case_condition(node) if node.type == :case_node
|
|
150
|
-
return
|
|
183
|
+
return call_condition(node) if node.type == :call_node
|
|
151
184
|
|
|
152
185
|
Condition.new(
|
|
153
186
|
operator: node.operator, operands: operands_of(node),
|
|
@@ -157,12 +190,17 @@ module PathCoverage
|
|
|
157
190
|
|
|
158
191
|
# node's operands: a chain of the same operator flattened
|
|
159
192
|
# into one list, the other operator nested as a Condition,
|
|
160
|
-
#
|
|
193
|
+
# as is a &. chain, its value its last leaf, anything else a
|
|
194
|
+
# leaf's byte range.
|
|
161
195
|
def operands_of(node)
|
|
162
196
|
[node.left, node.right].flat_map do |written|
|
|
163
197
|
operand = grouped(written)
|
|
198
|
+
next [leaf(operand)] if ignored?(operand)
|
|
164
199
|
next operands_of(operand) if operand.type == node.type
|
|
165
200
|
next [condition_of(operand)] if operator?(operand)
|
|
201
|
+
if safe_chain?(operand)
|
|
202
|
+
next [chain_condition(operand, value: true)]
|
|
203
|
+
end
|
|
166
204
|
|
|
167
205
|
[leaf(operand)]
|
|
168
206
|
end
|
|
@@ -179,17 +217,18 @@ module PathCoverage
|
|
|
179
217
|
OPERATOR_TYPES.include?(node.type)
|
|
180
218
|
end
|
|
181
219
|
|
|
182
|
-
# The byte range of a leaf operand,
|
|
183
|
-
#
|
|
184
|
-
#
|
|
220
|
+
# The byte range of a leaf operand, noted among its site's
|
|
221
|
+
# commands and fixed leaves where it is one; or, when sites
|
|
222
|
+
# that fold are found inside it, the leaf with them nested.
|
|
185
223
|
def leaf(operand)
|
|
186
|
-
operand.accept(self)
|
|
224
|
+
inner = collected { operand.accept(self) }
|
|
187
225
|
loc = operand.location
|
|
188
226
|
range = loc.start_offset...loc.end_offset
|
|
189
227
|
@notes[:commands] << range if Manifest.command?(operand)
|
|
190
228
|
outcome = Manifest.fixed_outcome(operand)
|
|
191
229
|
@notes[:fixed][range] = outcome if outcome
|
|
192
|
-
|
|
230
|
+
nonempty = nonempty_call?(operand, inner)
|
|
231
|
+
contained(inner, range, nonempty:)
|
|
193
232
|
end
|
|
194
233
|
|
|
195
234
|
# The one statement inside a parenthesised operand, or the
|
|
@@ -12,13 +12,31 @@ module PathCoverage
|
|
|
12
12
|
# True for a call whose block's every call can be seen: to a
|
|
13
13
|
# block method, on a receiver, with no arguments, which can
|
|
14
14
|
# make it ignore its block, not by &., which skips it for
|
|
15
|
-
# nil, and with a block of one expression
|
|
15
|
+
# nil, and with a block of one expression that is a value,
|
|
16
|
+
# so can be wrapped: not break, next or return.
|
|
16
17
|
def block_site?(node)
|
|
17
|
-
BLOCK_PATHS.key?(node.name.to_s)
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
return false unless BLOCK_PATHS.key?(node.name.to_s)
|
|
19
|
+
|
|
20
|
+
plain_call?(node) && value?(block_expression(node.block))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# True for a call on a receiver, with no arguments, and not
|
|
24
|
+
# by &.
|
|
25
|
+
def plain_call?(node)
|
|
26
|
+
!node.receiver.nil? && node.arguments.nil? &&
|
|
27
|
+
!node.safe_navigation?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# True for a block's one expression that is a value, so can
|
|
31
|
+
# be wrapped: nil, for no one expression, is not, nor is a
|
|
32
|
+
# jump.
|
|
33
|
+
def value?(body)
|
|
34
|
+
!body.nil? && !JUMPS.include?(body.type)
|
|
20
35
|
end
|
|
21
36
|
|
|
37
|
+
# The expressions that leave a block, so are no value.
|
|
38
|
+
JUMPS = %i[break_node next_node return_node].freeze
|
|
39
|
+
|
|
22
40
|
# The one expression of block, nil for no block, a block
|
|
23
41
|
# passed as an argument, or one of other than one expression.
|
|
24
42
|
def block_expression(block)
|
|
@@ -35,23 +53,44 @@ module PathCoverage
|
|
|
35
53
|
# among the commands when it is no bare argument.
|
|
36
54
|
def block_condition(node)
|
|
37
55
|
node.receiver.accept(self)
|
|
38
|
-
body =
|
|
39
|
-
body.accept(self)
|
|
40
|
-
@notes[:commands] << extent_of(body) if bare_less?(body)
|
|
56
|
+
body = block_body(node.block)
|
|
41
57
|
Condition.new(
|
|
42
58
|
operator: node.name.to_s,
|
|
43
|
-
operands: [extent_of(node.receiver),
|
|
59
|
+
operands: [extent_of(node.receiver), body],
|
|
44
60
|
extent: extent_of(node)
|
|
45
61
|
)
|
|
46
62
|
end
|
|
47
63
|
|
|
64
|
+
# The byte range of block's one expression, any site in it
|
|
65
|
+
# visited as one of its own, as the block runs once per item;
|
|
66
|
+
# noted among the commands when it is no bare argument.
|
|
67
|
+
def block_body(block)
|
|
68
|
+
body = block_expression(block)
|
|
69
|
+
scoped { body.accept(self) }
|
|
70
|
+
range = extent_of(body)
|
|
71
|
+
@notes[:commands] << range if bare_less?(body)
|
|
72
|
+
range
|
|
73
|
+
end
|
|
74
|
+
|
|
48
75
|
# True for an expression that is no bare argument: a command
|
|
49
|
-
# call,
|
|
76
|
+
# call, an and or an or, or a modifier, eg x unless y.
|
|
50
77
|
def bare_less?(node)
|
|
51
|
-
return true if Manifest.command?(node)
|
|
78
|
+
return true if Manifest.command?(node) || modifier?(node)
|
|
52
79
|
|
|
53
80
|
%i[and_node or_node].include?(node.type) &&
|
|
54
81
|
%w[and or].include?(node.operator)
|
|
55
82
|
end
|
|
83
|
+
|
|
84
|
+
# True for a modifier if, unless or rescue, eg x if y: one
|
|
85
|
+
# with no end keyword, a ternary being no modifier.
|
|
86
|
+
def modifier?(node)
|
|
87
|
+
case node.type
|
|
88
|
+
when :rescue_modifier_node then true
|
|
89
|
+
when :unless_node then node.end_keyword_loc.nil?
|
|
90
|
+
when :if_node
|
|
91
|
+
!node.if_keyword_loc.nil? && node.end_keyword_loc.nil?
|
|
92
|
+
else false
|
|
93
|
+
end
|
|
94
|
+
end
|
|
56
95
|
end
|
|
57
96
|
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
require_relative 'condition'
|
|
2
|
+
|
|
3
|
+
module PathCoverage
|
|
4
|
+
# The part of the manifest's visitor that builds the sites of
|
|
5
|
+
# safe-navigation chains, eg user&.account&.owner. It relies
|
|
6
|
+
# on its includer's extent_of.
|
|
7
|
+
module ManifestChains
|
|
8
|
+
# The calls of node's chain, outermost first, eg owner then
|
|
9
|
+
# account for user&.account&.owner; none for a node that is
|
|
10
|
+
# no call.
|
|
11
|
+
def chain_calls(node)
|
|
12
|
+
calls = []
|
|
13
|
+
while node&.type == :call_node
|
|
14
|
+
calls << node
|
|
15
|
+
node = node.receiver
|
|
16
|
+
end
|
|
17
|
+
calls
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# True for a call whose chain has a &. in it, eg
|
|
21
|
+
# user&.name.upcase.
|
|
22
|
+
def safe_chain?(node)
|
|
23
|
+
chain_calls(node).any?(&:safe_navigation?)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A call that is a site as a Condition: a &. chain's, or a
|
|
27
|
+
# block method call's. It relies on its includer's
|
|
28
|
+
# block_condition.
|
|
29
|
+
def call_condition(node)
|
|
30
|
+
return block_condition(node) unless safe_chain?(node)
|
|
31
|
+
|
|
32
|
+
chain_condition(node, value: false)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# A &. chain as a Condition named &.: its leaves are the
|
|
36
|
+
# receivers of its &. calls, innermost first, each nil or
|
|
37
|
+
# not nil, and, with value, the whole chain's value last, as
|
|
38
|
+
# when it is an operand of an && or an if's predicate. Any
|
|
39
|
+
# site in a call's arguments or block, or in the chain's
|
|
40
|
+
# first receiver, is a site of its own. A chain that is no
|
|
41
|
+
# bare argument, eg a command call, is noted among the
|
|
42
|
+
# site's commands, to be parenthesised.
|
|
43
|
+
def chain_condition(node, value:)
|
|
44
|
+
calls = chain_calls(node)
|
|
45
|
+
visit_chain(calls)
|
|
46
|
+
safe = calls.select(&:safe_navigation?).reverse
|
|
47
|
+
receivers = safe.map { |call| receiver_of(call) }
|
|
48
|
+
extent = extent_of(node)
|
|
49
|
+
@notes[:commands] << extent if Manifest.command?(node)
|
|
50
|
+
operands = value ? [*receivers, extent] : receivers
|
|
51
|
+
Condition.new(operator: SAFE, operands:, extent:)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The byte range of a &. call's receiver, noted among its
|
|
55
|
+
# site's fixed leaves as :present, never nil, when it is a
|
|
56
|
+
# call to a method treated as present.
|
|
57
|
+
def receiver_of(call)
|
|
58
|
+
receiver = call.receiver
|
|
59
|
+
range = extent_of(receiver)
|
|
60
|
+
named = receiver.type == :call_node &&
|
|
61
|
+
@present.include?(receiver.name.to_s)
|
|
62
|
+
@notes[:fixed][range] = :present if named
|
|
63
|
+
range
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Visits the sites inside a chain's calls' arguments and
|
|
67
|
+
# blocks, and inside its first receiver, eg (a || b) in
|
|
68
|
+
# (a || b)&.size, but never a call of the chain itself.
|
|
69
|
+
def visit_chain(calls)
|
|
70
|
+
calls.each do |call|
|
|
71
|
+
[call.arguments, call.block].compact.each { _1.accept(self) }
|
|
72
|
+
end
|
|
73
|
+
calls.last.receiver&.accept(self)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -11,10 +11,12 @@ module PathCoverage
|
|
|
11
11
|
# being an if_node with no if keyword.
|
|
12
12
|
CHOICE_TYPES = %i[if_node unless_node].freeze
|
|
13
13
|
|
|
14
|
-
# Where node's site starts:
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# Where node's site starts: a &. chain's start; a block
|
|
15
|
+
# method's call at its method; an if's or an unless's
|
|
16
|
+
# keyword, which a modifier's statement comes before;
|
|
17
|
+
# anything else's own start.
|
|
17
18
|
def site_loc(node)
|
|
19
|
+
return node.location if safe_chain?(node)
|
|
18
20
|
return node.message_loc if node.type == :call_node
|
|
19
21
|
return node.location unless CHOICE_TYPES.include?(node.type)
|
|
20
22
|
return node.location if ternary?(node)
|
|
@@ -69,11 +71,11 @@ module PathCoverage
|
|
|
69
71
|
end
|
|
70
72
|
|
|
71
73
|
# An if's arm, statements its code, which never folds into
|
|
72
|
-
# the if's site; any site inside it is a site
|
|
73
|
-
# An arm with no statements has no extent.
|
|
74
|
+
# the if's site; as a new scope, any site inside it is a site
|
|
75
|
+
# of its own. An arm with no statements has no extent.
|
|
74
76
|
def statement_arm(kind, statements)
|
|
75
|
-
statements&.accept(self)
|
|
76
|
-
extent = statements
|
|
77
|
+
scoped { statements&.accept(self) }
|
|
78
|
+
extent = statements ? extent_of(statements) : nil
|
|
77
79
|
Arm.new(kind:, extent:, body: nil)
|
|
78
80
|
end
|
|
79
81
|
|
|
@@ -85,7 +87,7 @@ module PathCoverage
|
|
|
85
87
|
return [statement_arm(:else, subsequent.statements)]
|
|
86
88
|
end
|
|
87
89
|
|
|
88
|
-
subsequent.accept(self)
|
|
90
|
+
scoped { subsequent.accept(self) }
|
|
89
91
|
extent = extent_of(subsequent)
|
|
90
92
|
[Arm.new(kind: :else, extent:, body: nil)]
|
|
91
93
|
end
|
|
@@ -100,19 +102,25 @@ module PathCoverage
|
|
|
100
102
|
end
|
|
101
103
|
|
|
102
104
|
# One arm of a ternary, statements its code: a condition
|
|
103
|
-
# folds in as its body, being part of the one expression
|
|
104
|
-
#
|
|
105
|
+
# folds in as its body, being part of the one expression, as
|
|
106
|
+
# does a plain arm with sites inside it, its value its last
|
|
107
|
+
# leaf; a plain arm with none has no body.
|
|
105
108
|
def arm_of(kind, statements)
|
|
106
109
|
written = grouped(statements.body.first)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
extent = extent_of(statements)
|
|
111
|
+
body = if foldable?(written)
|
|
112
|
+
condition_of(written)
|
|
113
|
+
else
|
|
114
|
+
inner = collected { statements.accept(self) }
|
|
115
|
+
inner.empty? ? nil : contained(inner, extent)
|
|
116
|
+
end
|
|
117
|
+
Arm.new(kind:, extent:, body:)
|
|
110
118
|
end
|
|
111
119
|
|
|
112
120
|
# True for a condition that folds into a ternary's site:
|
|
113
|
-
# an &&, ||, and, or, or a ternary.
|
|
121
|
+
# an &&, ||, and, or, or a ternary, none of them ignored.
|
|
114
122
|
def foldable?(node)
|
|
115
|
-
operator?(node) || ternary?(node)
|
|
123
|
+
(operator?(node) || ternary?(node)) && !ignored?(node)
|
|
116
124
|
end
|
|
117
125
|
|
|
118
126
|
# True for a ternary, an if with no if keyword.
|
|
@@ -121,10 +129,15 @@ module PathCoverage
|
|
|
121
129
|
end
|
|
122
130
|
|
|
123
131
|
# A ternary's or an if's predicate as its operand: an && or
|
|
124
|
-
# || nested as a Condition,
|
|
132
|
+
# || nested as a Condition, as is a &. chain, its value its
|
|
133
|
+
# last leaf, anything else a leaf.
|
|
125
134
|
def predicate_of(node)
|
|
126
|
-
predicate = node.predicate
|
|
135
|
+
predicate = grouped(node.predicate)
|
|
136
|
+
return leaf(predicate) if ignored?(predicate)
|
|
127
137
|
return condition_of(predicate) if operator?(predicate)
|
|
138
|
+
if safe_chain?(predicate)
|
|
139
|
+
return chain_condition(predicate, value: true)
|
|
140
|
+
end
|
|
128
141
|
|
|
129
142
|
leaf(predicate)
|
|
130
143
|
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require_relative 'condition'
|
|
2
|
+
|
|
3
|
+
module PathCoverage
|
|
4
|
+
# The part of the manifest's visitor that folds every site in one
|
|
5
|
+
# expression into one site. While a leaf is visited, each site
|
|
6
|
+
# found in it, the leaf itself included, is collected as a nested
|
|
7
|
+
# Condition of that leaf, rather than noted as a site of its own.
|
|
8
|
+
# A new scope, eg a block's body or an if's arm, stops the
|
|
9
|
+
# collecting, so a site in it is a site of its own. It relies on
|
|
10
|
+
# its includer's condition_of, chain_condition, safe_chain? and
|
|
11
|
+
# extent_of.
|
|
12
|
+
module ManifestFolding
|
|
13
|
+
# node's Condition as a site nested in a leaf: a &. chain's
|
|
14
|
+
# with its value as its last leaf, as its leaf's outcome
|
|
15
|
+
# follows from it.
|
|
16
|
+
def nested_condition_of(node)
|
|
17
|
+
return chain_condition(node, value: true) if safe_chain?(node)
|
|
18
|
+
|
|
19
|
+
condition_of(node)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The Conditions of the sites found while yielding, each one
|
|
23
|
+
# collected rather than noted as a site of its own.
|
|
24
|
+
def collected
|
|
25
|
+
outer = @inner
|
|
26
|
+
@inner = []
|
|
27
|
+
yield
|
|
28
|
+
@inner
|
|
29
|
+
ensure
|
|
30
|
+
@inner = outer
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Yields with no collecting, so a site found is one of its own:
|
|
34
|
+
# for a new scope, eg a block's body or an if's arm.
|
|
35
|
+
def scoped
|
|
36
|
+
outer = @inner
|
|
37
|
+
@inner = nil
|
|
38
|
+
yield
|
|
39
|
+
ensure
|
|
40
|
+
@inner = outer
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# range as a leaf, or, with inner sites found in it, a Condition
|
|
44
|
+
# named contains: those sites, then range, the leaf's outcome,
|
|
45
|
+
# fixed as :nonempty when nonempty. A leaf that is wholly one
|
|
46
|
+
# block method's call whose value follows from its path is
|
|
47
|
+
# that call alone, needing no outcome of its own.
|
|
48
|
+
def contained(inner, range, nonempty: false)
|
|
49
|
+
return range if inner.empty?
|
|
50
|
+
|
|
51
|
+
only = inner.first
|
|
52
|
+
if inner.size == 1 && only.extent == range &&
|
|
53
|
+
VALUED.include?(only.operator)
|
|
54
|
+
return only
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
fixed = nonempty ? { range => :nonempty } : {}
|
|
58
|
+
Condition.new(operator: CONTAINS, operands: [*inner, range],
|
|
59
|
+
extent: range, fixed:)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# True for a call, with no arguments or block, to a method
|
|
63
|
+
# treated as nonempty, eg any?, whose receiver, parenthesised
|
|
64
|
+
# or not, is one of inner, the sites found in it: its outcome
|
|
65
|
+
# then follows from that site's path. It relies on its
|
|
66
|
+
# includer's grouped.
|
|
67
|
+
def nonempty_call?(node, inner)
|
|
68
|
+
return false unless node.type == :call_node
|
|
69
|
+
return false if node.arguments || node.block || !node.receiver
|
|
70
|
+
|
|
71
|
+
receiver = extent_of(grouped(node.receiver))
|
|
72
|
+
@nonempty.include?(node.name.to_s) &&
|
|
73
|
+
inner.any? { |site| site.extent == receiver }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A block's body is a new scope: it runs once per item.
|
|
77
|
+
def visit_block_node(node)
|
|
78
|
+
scoped { super }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A lambda's body is a new scope: it runs when called.
|
|
82
|
+
def visit_lambda_node(node)
|
|
83
|
+
scoped { super }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A def's body is a new scope: it runs when called.
|
|
87
|
+
def visit_def_node(node)
|
|
88
|
+
scoped { super }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module PathCoverage
|
|
2
|
+
# The part of the manifest's visitor that leaves out the sites
|
|
3
|
+
# of ignored operators, eg ignore '&&='. An ignored node is no
|
|
4
|
+
# site, though a site inside it may be; nested in a site, it
|
|
5
|
+
# is a leaf. It relies on its includer's @ignored, ternary?
|
|
6
|
+
# and safe_chain?.
|
|
7
|
+
module ManifestIgnoring
|
|
8
|
+
# The operator of each node type that always names one kind
|
|
9
|
+
# of site.
|
|
10
|
+
NAMED = { unless_node: 'unless', case_node: 'case' }.freeze
|
|
11
|
+
|
|
12
|
+
# True for a node whose operator is ignored.
|
|
13
|
+
def ignored?(node)
|
|
14
|
+
@ignored.include?(operator_of(node))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The operator naming node's kind of site, eg ?: for a
|
|
18
|
+
# ternary or any? for a block method's call; nil for a
|
|
19
|
+
# node with none.
|
|
20
|
+
def operator_of(node)
|
|
21
|
+
type = node.type
|
|
22
|
+
return NAMED[type] if NAMED.key?(type)
|
|
23
|
+
return ternary?(node) ? '?:' : 'if' if type == :if_node
|
|
24
|
+
return call_operator(node) if type == :call_node
|
|
25
|
+
|
|
26
|
+
node.respond_to?(:operator) ? node.operator : nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The operator of a call: &. for a &. chain, else its
|
|
30
|
+
# method's name, eg any?.
|
|
31
|
+
def call_operator(node)
|
|
32
|
+
safe_chain?(node) ? '&.' : node.name.to_s
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require_relative 'condition'
|
|
2
|
+
|
|
3
|
+
module PathCoverage
|
|
4
|
+
# The part of the manifest's visitor that builds the sites of
|
|
5
|
+
# ||= and &&= writes, eg @clock ||= Clock.new. It relies on
|
|
6
|
+
# its includer's add_site, bare_less? and extent_of.
|
|
7
|
+
module ManifestWrites
|
|
8
|
+
# Every node type of a ||= or &&= write, by its target: a
|
|
9
|
+
# variable of each kind, a constant, a constant path, an
|
|
10
|
+
# attribute, eg self.size, and an index, eg store[key].
|
|
11
|
+
WRITE_TYPES = %w[
|
|
12
|
+
local_variable instance_variable class_variable
|
|
13
|
+
global_variable constant constant_path call index
|
|
14
|
+
].flat_map do |target|
|
|
15
|
+
%W[#{target}_or_write_node #{target}_and_write_node]
|
|
16
|
+
end.map(&:to_sym).freeze
|
|
17
|
+
|
|
18
|
+
# Notes each kind of write as a site, eg
|
|
19
|
+
# visit_index_or_write_node for store[key] ||= 0.
|
|
20
|
+
WRITE_TYPES.each do |type|
|
|
21
|
+
define_method(:"visit_#{type}") do |node|
|
|
22
|
+
write?(node) ? add_site(node) : visit_child_nodes(node)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# True for a ||= or &&= write that is a site: any but one
|
|
27
|
+
# through &., eg box&.size ||= 3, which a nil box skips whole,
|
|
28
|
+
# its target never looked at, so neither of its paths taken.
|
|
29
|
+
def write?(node)
|
|
30
|
+
WRITE_TYPES.include?(node.type) && !safe_write?(node)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# True for a write through &., eg box&.size ||= 3.
|
|
34
|
+
def safe_write?(node)
|
|
35
|
+
node.respond_to?(:safe_navigation?) && node.safe_navigation?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A write as a Condition, named by its operator: its one leaf
|
|
39
|
+
# its target, then its right side as an arm, taken when the
|
|
40
|
+
# target is false for ||=, true for &&=. The target is a
|
|
41
|
+
# leaf the rewriter never reads again, as reading it could
|
|
42
|
+
# call a method twice; its outcome follows from whether the
|
|
43
|
+
# arm ran. Any site inside the write, eg in an index or on
|
|
44
|
+
# the right side, is a site of its own. A right side that is
|
|
45
|
+
# no bare argument, eg a command call, is noted among the
|
|
46
|
+
# site's commands, to be parenthesised.
|
|
47
|
+
def write_condition(node)
|
|
48
|
+
visit_child_nodes(node)
|
|
49
|
+
value = extent_of(node.value)
|
|
50
|
+
@notes[:commands] << value if bare_less?(node.value)
|
|
51
|
+
arm = Arm.new(kind: :then, extent: value, body: nil)
|
|
52
|
+
Condition.new(
|
|
53
|
+
operator: node.operator, operands: [target_of(node), arm],
|
|
54
|
+
extent: extent_of(node)
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The byte range of a write's target: from its start to its
|
|
59
|
+
# operator, less any space between, eg `store[key]`.
|
|
60
|
+
def target_of(node)
|
|
61
|
+
start = node.location.start_offset
|
|
62
|
+
before = node.operator_loc.start_offset - start
|
|
63
|
+
text = node.location.slice.byteslice(0, before)
|
|
64
|
+
start...(start + text.rstrip.bytesize)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/path_coverage/merge.rb
CHANGED
|
@@ -12,11 +12,15 @@ module PathCoverage
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
# One site paths hash, as Result#paths gives them, each
|
|
15
|
-
# path's count the sum of every run's
|
|
16
|
-
#
|
|
15
|
+
# path's count the sum of every run's. Runs that list a
|
|
16
|
+
# site's paths differently raise, as adding their counts by
|
|
17
|
+
# position would credit one path's count to another.
|
|
17
18
|
def self.paths(runs)
|
|
18
19
|
runs.reduce do |total, run|
|
|
19
|
-
total.merge(run) do |
|
|
20
|
+
total.merge(run) do |site, mine, theirs|
|
|
21
|
+
same = mine.map { _1[:path] } == theirs.map { _1[:path] }
|
|
22
|
+
raise ArgumentError, "runs differ at #{site}" unless same
|
|
23
|
+
|
|
20
24
|
mine.zip(theirs).map do |a, b|
|
|
21
25
|
{ path: a[:path], count: a[:count] + b[:count] }
|
|
22
26
|
end
|
|
@@ -24,6 +28,18 @@ module PathCoverage
|
|
|
24
28
|
end
|
|
25
29
|
end
|
|
26
30
|
|
|
31
|
+
# One method paths hash, { site => { path => count } }, as
|
|
32
|
+
# the recorder counts them, each path's count the sum of
|
|
33
|
+
# every run's, eg [true, false] taken once in each of two
|
|
34
|
+
# runs is taken twice.
|
|
35
|
+
def self.method_paths(runs)
|
|
36
|
+
runs.reduce do |total, run|
|
|
37
|
+
total.merge(run) do |_site, mine, theirs|
|
|
38
|
+
mine.merge(theirs) { |_path, a, b| a + b }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
27
43
|
# One operand's counts from two runs, added; a fixed leaf's
|
|
28
44
|
# only comes from the source, so either run's will do.
|
|
29
45
|
def self.sum(mine, theirs)
|
data/lib/path_coverage/post.rb
CHANGED
|
@@ -6,18 +6,18 @@ module PathCoverage
|
|
|
6
6
|
# for runs whose own files are thrown away, eg a failing
|
|
7
7
|
# docker build step.
|
|
8
8
|
module Post
|
|
9
|
-
# POSTs operands and paths, as the JSON report
|
|
10
|
-
# never through an http_proxy,
|
|
11
|
-
# collector local to the run. A
|
|
12
|
-
# reached, or does not answer
|
|
13
|
-
# over in silence: the run's own
|
|
14
|
-
# must stay as they are.
|
|
15
|
-
def self.deliver(operands, url, paths = {})
|
|
9
|
+
# POSTs operands and paths, as the JSON report with its
|
|
10
|
+
# fingerprint, to url, never through an http_proxy,
|
|
11
|
+
# which could not reach a collector local to the run. A
|
|
12
|
+
# collector that cannot be reached, or does not answer
|
|
13
|
+
# within a second, is passed over in silence: the run's own
|
|
14
|
+
# output and exit status must stay as they are.
|
|
15
|
+
def self.deliver(operands, url, paths = {}, fingerprint: {})
|
|
16
16
|
uri = URI(url)
|
|
17
17
|
http = Net::HTTP.new(uri.host, uri.port, nil)
|
|
18
18
|
http.open_timeout = http.read_timeout = 1
|
|
19
19
|
headers = { 'Content-Type' => 'application/json' }
|
|
20
|
-
body = JsonWriter.json(operands, paths)
|
|
20
|
+
body = JsonWriter.json(operands, paths, fingerprint:)
|
|
21
21
|
http.post(uri.path, body, headers)
|
|
22
22
|
rescue StandardError
|
|
23
23
|
nil
|