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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
|
|
3
|
+
module PathCoverage
|
|
4
|
+
module Kinds
|
|
5
|
+
# A block method's call, eg ids.any? { |id| id.odd? }: its
|
|
6
|
+
# receiver's count of items, and its block's answers, its
|
|
7
|
+
# value following from them. Nothing else is evaluated, eg
|
|
8
|
+
# ids.size.
|
|
9
|
+
module Block
|
|
10
|
+
extend Base
|
|
11
|
+
|
|
12
|
+
OPERATORS = BLOCK_PATHS.keys.freeze
|
|
13
|
+
|
|
14
|
+
# A value that holds whatever the path, eg select's array.
|
|
15
|
+
ALWAYS = ->(*) { true }
|
|
16
|
+
|
|
17
|
+
# nil, nothing found, when its block was called with no
|
|
18
|
+
# items or its last answer was false; else the item found,
|
|
19
|
+
# which may itself be nil, so nil, as no path can tell.
|
|
20
|
+
FOUND = ->(none, answer) { none || !answer ? false : nil }
|
|
21
|
+
|
|
22
|
+
# A block method's value from whether its block was called
|
|
23
|
+
# with no items, and its block's last answer; nil when the
|
|
24
|
+
# path cannot tell.
|
|
25
|
+
VALUES = {
|
|
26
|
+
'any?' => ->(none, answer) { !none && answer },
|
|
27
|
+
'all?' => ->(none, answer) { none || answer },
|
|
28
|
+
'none?' => ->(none, answer) { none || !answer },
|
|
29
|
+
'detect' => FOUND,
|
|
30
|
+
'find' => FOUND,
|
|
31
|
+
'find_index' => ->(none, answer) { !none && answer },
|
|
32
|
+
'take_while' => ALWAYS,
|
|
33
|
+
**EVERY_ITEM.to_h { |name| [name, ALWAYS] }
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
# Whether select's result has items: whether any answer
|
|
37
|
+
# was true.
|
|
38
|
+
KEPT = lambda do |items, answers|
|
|
39
|
+
items != ITEMS[0] && answers != ANSWERS[1]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Whether a block method's result has items, from how many
|
|
43
|
+
# items its block was called with and its answers, eg
|
|
44
|
+
# select keeps an item when any answer was true; nil when
|
|
45
|
+
# its path cannot tell, eg take_while's last answer false
|
|
46
|
+
# after 2+ items, which may or may not be its first.
|
|
47
|
+
HAS_ITEMS = {
|
|
48
|
+
'select' => KEPT, 'filter' => KEPT, 'filter_map' => KEPT,
|
|
49
|
+
'reject' => lambda do |items, answers|
|
|
50
|
+
items != ITEMS[0] && answers != ANSWERS[0]
|
|
51
|
+
end,
|
|
52
|
+
'partition' => ALWAYS,
|
|
53
|
+
'take_while' => lambda do |items, answer|
|
|
54
|
+
next false if items == ITEMS[0]
|
|
55
|
+
|
|
56
|
+
answer || (items == ITEMS[1] ? false : nil)
|
|
57
|
+
end
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
# Whether site's result has items on the path that took
|
|
61
|
+
# outcomes; nil when its kind cannot tell, eg for a site
|
|
62
|
+
# that is no block method's call.
|
|
63
|
+
def self.items?(site, outcomes)
|
|
64
|
+
receiver, body = site.operands
|
|
65
|
+
rule = HAS_ITEMS[site.operator]
|
|
66
|
+
rule&.call(outcomes[receiver], outcomes[body])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The ways node can be evaluated, fixed by its shape, its
|
|
70
|
+
# value following from each.
|
|
71
|
+
def self.ways(node, _fixed)
|
|
72
|
+
receiver, body = node.operands
|
|
73
|
+
BLOCK_PATHS[node.operator].map do |items, answer|
|
|
74
|
+
value = path_value(node.operator, items, answer)
|
|
75
|
+
[{ receiver => items, body => answer }, value]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Its value on a path: its count of items and its block's
|
|
80
|
+
# answer, eg false for any? called with 0 items, and always
|
|
81
|
+
# truthy for select, which gives an array; nil for find
|
|
82
|
+
# with a last answer true, whose value no path can tell.
|
|
83
|
+
def self.path_value(operator, items, answer)
|
|
84
|
+
VALUES[operator].call(items == ITEMS[0], answer)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Its value in each column, from its count of items and its
|
|
88
|
+
# block's answer there; nil where it was not reached.
|
|
89
|
+
def self.value(operator, operands)
|
|
90
|
+
receiver, body = operands
|
|
91
|
+
receiver[:cells].zip(body[:cells]).map do |items, answer|
|
|
92
|
+
items && path_value(operator, items, answer)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Its block's opening and closing in source, ['{', '}'] or
|
|
97
|
+
# ['do', 'end']: the opening between its receiver and its
|
|
98
|
+
# block's expression, the closing after that expression.
|
|
99
|
+
def self.brackets(source, site)
|
|
100
|
+
receiver, body = site.operands
|
|
101
|
+
between = source.byteslice(receiver.end...body.begin)
|
|
102
|
+
closing = source.byteslice(body.end...site.extent.end)
|
|
103
|
+
[between[/\{|\bdo\b/], closing.strip]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Its receiver needs no record call, and its block's
|
|
107
|
+
# expression an item_at or answer_at call.
|
|
108
|
+
def self.own_calls?
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The calls for its leaves: none for its receiver, whose
|
|
113
|
+
# leaf notes the items its block was called with, and an
|
|
114
|
+
# item_at call, noting its last answer, or for a method that
|
|
115
|
+
# calls it with every item an answer_at call, noting every
|
|
116
|
+
# answer, for its block's expression.
|
|
117
|
+
def self.calls(site, inner, leaves)
|
|
118
|
+
receiver, body = inner.operands
|
|
119
|
+
at = leaves.index(receiver)
|
|
120
|
+
every = EVERY_ITEM.include?(inner.operator)
|
|
121
|
+
noter = every ? 'answer_at' : 'item_at'
|
|
122
|
+
{ receiver => nil,
|
|
123
|
+
body => "PathCoverage.#{noter}(#{site.inspect}, #{at}, " }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Its call wrapped whole, its receiver the site's at-th leaf,
|
|
127
|
+
# so its items are begun before it runs and counted after.
|
|
128
|
+
def self.inserts(site, inner, at, _commands)
|
|
129
|
+
name = site.inspect
|
|
130
|
+
begun = "PathCoverage.begin_items(#{name}, #{at})"
|
|
131
|
+
call = "PathCoverage.end_items(#{name}, #{at}, #{begun}, "
|
|
132
|
+
Rewriter.wrap(call, inner.extent, false, :kind)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
require_relative 'chain'
|
|
3
|
+
|
|
4
|
+
module PathCoverage
|
|
5
|
+
module Kinds
|
|
6
|
+
# A case, its operands its whens' and else's arms, each when
|
|
7
|
+
# trying its patterns in turn until one matches. Each pattern
|
|
8
|
+
# is wrapped in a stand-in, which matches the case's subject
|
|
9
|
+
# against it once, as the case would have.
|
|
10
|
+
module Case
|
|
11
|
+
extend Base
|
|
12
|
+
|
|
13
|
+
OPERATORS = %w[case].freeze
|
|
14
|
+
|
|
15
|
+
# The ways node can be evaluated: as the chain of its whens'
|
|
16
|
+
# conditions.
|
|
17
|
+
def self.ways(node, fixed)
|
|
18
|
+
whens = node.operands.filter_map(&:body)
|
|
19
|
+
Chain.ways(node.with(operands: whens), fixed)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The inserts that wrap each pattern of a case site in a
|
|
23
|
+
# when_match call, its stand-in, which enters and leaves the
|
|
24
|
+
# site itself, as the case tries its patterns one by one;
|
|
25
|
+
# its arms may return or raise, so no leave wraps it.
|
|
26
|
+
def self.site_inserts(site, condition)
|
|
27
|
+
size = condition.leaves.size
|
|
28
|
+
condition.leaves.each_with_index.flat_map do |range, index|
|
|
29
|
+
call = "PathCoverage.when_match(#{site.inspect}, " \
|
|
30
|
+
"#{index}, #{size}, "
|
|
31
|
+
Rewriter.wrap(call, range, false, :leaf)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Its node in a site's tree from its arms, built in order so
|
|
36
|
+
# its whens' patterns pair with path entries left to right:
|
|
37
|
+
# a when is taken on each path one of its patterns matched,
|
|
38
|
+
# and the else on each none did. A case's value is that of
|
|
39
|
+
# no choice, so is nil.
|
|
40
|
+
def self.node(source, condition, outcomes, leaves)
|
|
41
|
+
matched = []
|
|
42
|
+
operands = condition.operands.map do |arm|
|
|
43
|
+
arm_node(source, arm, matched, [outcomes, leaves])
|
|
44
|
+
end
|
|
45
|
+
cells = Array.new(outcomes.size)
|
|
46
|
+
{ operator: 'case', cells:, operands: }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# One arm's node, its cells whether each column took it;
|
|
50
|
+
# matched holds the cells of each when before it, and a
|
|
51
|
+
# when's own are added.
|
|
52
|
+
def self.arm_node(source, arm, matched, (outcomes, leaves))
|
|
53
|
+
body = arm.body &&
|
|
54
|
+
SiteTree.node(source, arm.body, outcomes, leaves)
|
|
55
|
+
matched << body[:cells].map { |cell| cell == true } if body
|
|
56
|
+
cells = body ? matched.last : unmatched(matched, outcomes)
|
|
57
|
+
text = SiteTree.text_of(source, arm)
|
|
58
|
+
{ arm: arm.kind, text:, cells:, body: }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Whether each column matched none of its whens' patterns.
|
|
62
|
+
def self.unmatched(matched, outcomes)
|
|
63
|
+
outcomes.each_index.map do |column|
|
|
64
|
+
matched.none? { |taken| taken[column] }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The name of one of its arms: when or else.
|
|
69
|
+
def self.arm_label(arm, _choice)
|
|
70
|
+
arm[:arm].to_s
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Its patterns need pattern calls.
|
|
74
|
+
def self.own_calls?
|
|
75
|
+
true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# { pattern range => the opening of its pattern call } for
|
|
79
|
+
# the patterns of inner, a case nested in a site whose leaves
|
|
80
|
+
# are leaves.
|
|
81
|
+
def self.calls(site, inner, leaves)
|
|
82
|
+
inner.leaves.to_h do |range|
|
|
83
|
+
index = leaves.index(range)
|
|
84
|
+
[range, "PathCoverage.pattern(#{site.inspect}, #{index}, "]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
|
|
3
|
+
module PathCoverage
|
|
4
|
+
module Kinds
|
|
5
|
+
# A chain of one operator, eg a && b && c, or a when's
|
|
6
|
+
# patterns, each operand reached only while every one before
|
|
7
|
+
# it gave the outcome its operator goes on with.
|
|
8
|
+
module Chain
|
|
9
|
+
extend Base
|
|
10
|
+
|
|
11
|
+
OPERATORS = %w[&& and || or when].freeze
|
|
12
|
+
|
|
13
|
+
# The outcome of an operand on which each operator goes on
|
|
14
|
+
# to its next operand; any other ends the chain there. A
|
|
15
|
+
# when's patterns, and a case's whens, stop at a match.
|
|
16
|
+
CONTINUES_ON = {
|
|
17
|
+
'&&' => true, 'and' => true, '||' => false, 'or' => false,
|
|
18
|
+
'when' => false, 'case' => false
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# The ways node can be evaluated: each way of its first
|
|
22
|
+
# operand, going on to the next while each outcome is the
|
|
23
|
+
# one its operator goes on with.
|
|
24
|
+
def self.ways(node, fixed)
|
|
25
|
+
first, *rest = node.operands
|
|
26
|
+
goes_on = CONTINUES_ON.fetch(node.operator)
|
|
27
|
+
rest.reduce(SitePaths.walk(first, fixed)) do |ways, operand|
|
|
28
|
+
ways.flat_map do |outcomes, value|
|
|
29
|
+
next [[outcomes, value]] unless value == goes_on
|
|
30
|
+
|
|
31
|
+
SitePaths.walk(operand, fixed).map do |more, after|
|
|
32
|
+
[outcomes.merge(more), after]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Its value in each column: the last operand it reached.
|
|
39
|
+
def self.value(_operator, operands)
|
|
40
|
+
operands.map { |operand| operand[:cells] }
|
|
41
|
+
.transpose.map { |column| column.compact.last }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
|
|
3
|
+
module PathCoverage
|
|
4
|
+
module Kinds
|
|
5
|
+
# The ways and value of a condition whose predicate picks an
|
|
6
|
+
# arm, which Choice and Write share.
|
|
7
|
+
module Arms
|
|
8
|
+
# The ways node can be evaluated: each way of its predicate,
|
|
9
|
+
# then each way of the condition the arm it picks is, if
|
|
10
|
+
# that arm is one; one with no else arm picks none. Its
|
|
11
|
+
# value is that of no choice, so is nil.
|
|
12
|
+
def ways(node, fixed)
|
|
13
|
+
predicate, then_arm, else_arm = node.operands
|
|
14
|
+
then_on = THEN_ON[node.operator]
|
|
15
|
+
SitePaths.walk(predicate, fixed).flat_map do |outcomes, value|
|
|
16
|
+
body = (value == then_on ? then_arm : else_arm)&.body
|
|
17
|
+
next [[outcomes, nil]] if body.nil?
|
|
18
|
+
|
|
19
|
+
SitePaths.walk(body, fixed).map do |more, _|
|
|
20
|
+
[outcomes.merge(more), nil]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Its value in each column: its taken arm's body's, nil for
|
|
26
|
+
# a plain arm.
|
|
27
|
+
def value(_operator, operands)
|
|
28
|
+
_, *arms = operands
|
|
29
|
+
bodies = arms.map { |arm| arm[:body]&.fetch(:cells) }
|
|
30
|
+
arms.first[:cells].each_index.map do |column|
|
|
31
|
+
taken = arms.index { |arm| arm[:cells][column] }
|
|
32
|
+
taken && bodies[taken]&.fetch(column)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# A condition whose predicate picks an arm: a ternary, an if
|
|
38
|
+
# or an unless.
|
|
39
|
+
module Choice
|
|
40
|
+
extend Base
|
|
41
|
+
extend Arms
|
|
42
|
+
|
|
43
|
+
OPERATORS = CHOICES
|
|
44
|
+
|
|
45
|
+
# What its site's leave call wraps: an if's or an unless's
|
|
46
|
+
# predicate, a leaf's byte range or a nested Condition, as
|
|
47
|
+
# its path is decided there and its arms are statements that
|
|
48
|
+
# may return or raise; a ternary as a whole.
|
|
49
|
+
def self.trailed(condition)
|
|
50
|
+
return condition if condition.operator == '?:'
|
|
51
|
+
|
|
52
|
+
condition.operands.first
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The name of one of its arms, of a condition named choice:
|
|
56
|
+
# then or else for an if or an unless, as its text is
|
|
57
|
+
# statements; ? or : for a ternary's, then its condition's
|
|
58
|
+
# operator, or its text for a plain arm.
|
|
59
|
+
def self.arm_label(arm, choice)
|
|
60
|
+
return arm[:arm].to_s if STATEMENT_CHOICES.include?(choice)
|
|
61
|
+
|
|
62
|
+
mark = arm[:arm] == :then ? '?' : ':'
|
|
63
|
+
body = arm[:body]
|
|
64
|
+
"#{mark} #{body ? body[:operator] : arm[:text]}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
require_relative 'block'
|
|
3
|
+
|
|
4
|
+
module PathCoverage
|
|
5
|
+
module Kinds
|
|
6
|
+
# A leaf with sites inside it, eg ids.select { |i| i.odd? }.any?
|
|
7
|
+
# as an operand of an &&: its operands are those sites'
|
|
8
|
+
# Conditions, which run as it does, then the leaf's own byte
|
|
9
|
+
# range, whose outcome is its value.
|
|
10
|
+
module Contains
|
|
11
|
+
extend Base
|
|
12
|
+
|
|
13
|
+
OPERATORS = [CONTAINS].freeze
|
|
14
|
+
|
|
15
|
+
# The ways node can be evaluated: each way of every one of
|
|
16
|
+
# the sites inside it, then each way of the leaf itself, its
|
|
17
|
+
# value the whole's.
|
|
18
|
+
def self.ways(node, fixed)
|
|
19
|
+
inner = node.operands[0...-1]
|
|
20
|
+
before = inner.reduce([{}]) do |ways, site|
|
|
21
|
+
ways.product(SitePaths.walk(site, fixed))
|
|
22
|
+
.map { |outcomes, (more, _)| outcomes.merge(more) }
|
|
23
|
+
end
|
|
24
|
+
before.flat_map do |outcomes|
|
|
25
|
+
leaf_ways(node, outcomes, fixed).map do |more, value|
|
|
26
|
+
[outcomes.merge(more), value]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The ways the leaf of node can be evaluated after the sites
|
|
32
|
+
# inside it took outcomes: the one outcome it must have then,
|
|
33
|
+
# when known, else true or false.
|
|
34
|
+
def self.leaf_ways(node, outcomes, fixed)
|
|
35
|
+
range = node.operands.last
|
|
36
|
+
known = known_outcome(node, outcomes, fixed)
|
|
37
|
+
return SitePaths.leaf(range, fixed) if known.nil?
|
|
38
|
+
|
|
39
|
+
[[{ range => known }, known]]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The outcome the leaf of node must have after the sites
|
|
43
|
+
# inside it took outcomes, nil when it can have either: for
|
|
44
|
+
# a call treated as nonempty, eg any?, whether its receiver
|
|
45
|
+
# has items; for a leaf that is wholly one site, eg a find,
|
|
46
|
+
# that site's value. Each only when the site's kind can
|
|
47
|
+
# tell. A nonempty call has no arguments, so its receiver is
|
|
48
|
+
# the largest of those sites, any other lying within it.
|
|
49
|
+
def self.known_outcome(node, outcomes, fixed)
|
|
50
|
+
*inner, range = node.operands
|
|
51
|
+
if node.fixed[range] == :nonempty
|
|
52
|
+
receiver = inner.max_by { |site| site.extent.size }
|
|
53
|
+
return Block.items?(receiver, outcomes)
|
|
54
|
+
end
|
|
55
|
+
whole = inner.find { |site| site.extent == range }
|
|
56
|
+
whole && value_on(whole, outcomes, fixed)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# site's value on its way that took outcomes, one of its
|
|
60
|
+
# own, nil when its kind cannot tell.
|
|
61
|
+
def self.value_on(site, outcomes, fixed)
|
|
62
|
+
ways = SitePaths.walk(site, fixed)
|
|
63
|
+
ways.find { |more, _| more <= outcomes }.last
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Its value in each column: the leaf's own, its last operand.
|
|
67
|
+
def self.value(_operator, operands)
|
|
68
|
+
operands.last[:cells]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# nodes, labelled and texted so their rows read as the
|
|
72
|
+
# leaf's code does.
|
|
73
|
+
# Each site inside the leaf is labelled with the leaf's text
|
|
74
|
+
# before it, its operator, and its own opening, eg select {
|
|
75
|
+
# for ids.select { |id| id.odd? }.any?, or ( || for
|
|
76
|
+
# (a || b).any?.
|
|
77
|
+
# The leaf is texted with the last site's own closing, then
|
|
78
|
+
# the leaf's text after that site, eg }.any? or ).any?.
|
|
79
|
+
def self.texted(source, condition, nodes)
|
|
80
|
+
*inner, range = condition.operands
|
|
81
|
+
starts = [range.begin, *inner.map { |site| site.extent.end }]
|
|
82
|
+
labelled = inner.zip(starts, nodes).map do |site, at, node|
|
|
83
|
+
node.merge(label: label(source, site, at))
|
|
84
|
+
end
|
|
85
|
+
text = tail(source, inner.last, range)
|
|
86
|
+
[*labelled, nodes.last.merge(text:)]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# site's label inside a leaf: the leaf's text from byte at
|
|
90
|
+
# up to site, then site's operator and its own opening.
|
|
91
|
+
def self.label(source, site, at)
|
|
92
|
+
before = source.byteslice(at...site.extent.begin).strip
|
|
93
|
+
opening, = Kinds.of(site.operator).brackets(source, site)
|
|
94
|
+
[before, site.operator, opening].reject(&:empty?).join(' ')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The text of the leaf at range: the closing of last, the
|
|
98
|
+
# last site inside the leaf, then the leaf's text after it.
|
|
99
|
+
def self.tail(source, last, range)
|
|
100
|
+
_, closing = Kinds.of(last.operator).brackets(source, last)
|
|
101
|
+
closing + source.byteslice(last.extent.end...range.end)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The sites inside it, then the leaf, as siblings, so each
|
|
105
|
+
# site's rows are drawn once, eg select { then }.any?.
|
|
106
|
+
def self.unfold(node)
|
|
107
|
+
node[:operands]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
|
|
3
|
+
module PathCoverage
|
|
4
|
+
module Kinds
|
|
5
|
+
# A &. chain, eg user&.account&.owner, each receiver nil or
|
|
6
|
+
# not, a nil one skipping the rest of the chain. As a site of
|
|
7
|
+
# its own its operands are its receivers; nested in another,
|
|
8
|
+
# they are its receivers then its value, its last leaf. Each
|
|
9
|
+
# receiver is wrapped in a present call, which passes it on
|
|
10
|
+
# unchanged, so it is still evaluated just once.
|
|
11
|
+
module Safe
|
|
12
|
+
extend Base
|
|
13
|
+
|
|
14
|
+
OPERATORS = [SAFE].freeze
|
|
15
|
+
|
|
16
|
+
# The ways node, nested in a condition, can be evaluated,
|
|
17
|
+
# its value its last leaf.
|
|
18
|
+
def self.ways(node, fixed)
|
|
19
|
+
chain(node.operands[0...-1], node.operands.last, fixed)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The ways a &. chain site can be evaluated, every operand
|
|
23
|
+
# a receiver.
|
|
24
|
+
def self.root_ways(node, fixed)
|
|
25
|
+
chain(node.operands, nil, fixed)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The ways a &. chain can be evaluated: each receiver not
|
|
29
|
+
# nil going on to the next, a nil one ending the chain, its
|
|
30
|
+
# value false; then, given a last leaf, the chain's value
|
|
31
|
+
# as an operand of a condition, the ways it can be.
|
|
32
|
+
def self.chain(receivers, last, fixed)
|
|
33
|
+
first, *rest = receivers
|
|
34
|
+
on = if !rest.empty? then chain(rest, last, fixed)
|
|
35
|
+
elsif last then SitePaths.walk(last, fixed)
|
|
36
|
+
else [[{}, true]]
|
|
37
|
+
end
|
|
38
|
+
present = on.map do |outcomes, value|
|
|
39
|
+
[outcomes.merge(first => PRESENCE[0]), value]
|
|
40
|
+
end
|
|
41
|
+
return present if fixed[first] == :present
|
|
42
|
+
|
|
43
|
+
[*present, [{ first => PRESENCE[1] }, false]]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Its value in each column: false where a receiver was nil,
|
|
47
|
+
# as the chain then gives nil, else its last leaf's.
|
|
48
|
+
def self.value(_operator, operands)
|
|
49
|
+
leaves = operands.reject { |operand| operand[:called] }
|
|
50
|
+
columns = leaves.map { |operand| operand[:cells] }.transpose
|
|
51
|
+
columns.map do |column|
|
|
52
|
+
column.include?(PRESENCE[1]) ? false : column.last
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The inserts for a &. chain site: each receiver in a present
|
|
57
|
+
# call, and the whole chain in a leave call that enters it
|
|
58
|
+
# first. Eg user&.account becomes, in effect,
|
|
59
|
+
# leave(enter(site, 1), present(site, 0, 0, user)&.account).
|
|
60
|
+
def self.site_inserts(site, condition)
|
|
61
|
+
leaves = condition.leaves
|
|
62
|
+
enter = "PathCoverage.enter(#{site.inspect}, #{leaves.size})"
|
|
63
|
+
call = "PathCoverage.leave(#{enter}, "
|
|
64
|
+
command = condition.commands.include?(condition.extent)
|
|
65
|
+
presents = leaves.each_with_index.flat_map do |range, index|
|
|
66
|
+
Rewriter.wrap(present(site, index, 0), range, false, :leaf)
|
|
67
|
+
end
|
|
68
|
+
extent = condition.extent
|
|
69
|
+
presents + Rewriter.wrap(call, extent, command, :site)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Its leaves need present and chained calls.
|
|
73
|
+
def self.own_calls?
|
|
74
|
+
true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# { leaf range => the opening of its call } for the leaves of
|
|
78
|
+
# chain nested in a site whose leaves are leaves: a
|
|
79
|
+
# receiver's present call, and the chain's value's chained
|
|
80
|
+
# call, each told where its chain's leaves start.
|
|
81
|
+
def self.calls(site, chain, leaves)
|
|
82
|
+
first = leaves.index(chain.operands.first)
|
|
83
|
+
*receivers, value = chain.operands
|
|
84
|
+
calls = receivers.to_h do |range|
|
|
85
|
+
[range, present(site, leaves.index(range), first)]
|
|
86
|
+
end
|
|
87
|
+
at = leaves.index(value)
|
|
88
|
+
calls.merge(value => chained(site, at, first))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The opening of the chained call around the index-th leaf
|
|
92
|
+
# of site, a chain's value, its chain's leaves starting at
|
|
93
|
+
# first.
|
|
94
|
+
def self.chained(site, index, first)
|
|
95
|
+
"PathCoverage.chained(#{site.inspect}, #{index}, #{first}, "
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The opening of the present call around the index-th leaf
|
|
99
|
+
# of site, its chain's leaves starting at first.
|
|
100
|
+
def self.present(site, index, first)
|
|
101
|
+
"PathCoverage.present(#{site.inspect}, #{index}, #{first}, "
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Its leaves' nodes, each after the first showing only what
|
|
105
|
+
# it adds to the one before, eg account after user.
|
|
106
|
+
# A leading &. is left out, as the chain's own row shows it.
|
|
107
|
+
# Any of the chain after its last leaf follows, eg owner in
|
|
108
|
+
# user&.account&.owner, as a node of its own.
|
|
109
|
+
def self.texted(source, condition, nodes)
|
|
110
|
+
ranges = condition.operands
|
|
111
|
+
texted = with_added(source, ranges, nodes)
|
|
112
|
+
rest = shown(source, ranges.last.end...condition.extent.end)
|
|
113
|
+
rest.empty? ? texted : [*texted, called(rest, texted.last)]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# nodes, the nodes of a chain's leaves at ranges, each after
|
|
117
|
+
# the first showing only what it adds to the one before.
|
|
118
|
+
def self.with_added(source, ranges, nodes)
|
|
119
|
+
nodes.each_with_index.map do |node, index|
|
|
120
|
+
next node if index.zero?
|
|
121
|
+
|
|
122
|
+
added = ranges[index - 1].end...ranges[index].end
|
|
123
|
+
node.merge(text: shown(source, added))
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The text of source at range, less any leading &.
|
|
128
|
+
def self.shown(source, range)
|
|
129
|
+
source.byteslice(range).delete_prefix('&.')
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The node of text, the end of a chain after its last
|
|
133
|
+
# leaf, eg owner: no leaf, as it decides no path, but marked
|
|
134
|
+
# called on each path where last, the last leaf, was not
|
|
135
|
+
# nil.
|
|
136
|
+
def self.called(text, last)
|
|
137
|
+
cells = last[:cells].map { |cell| cell == PRESENCE[0] }
|
|
138
|
+
{ text:, called: true, cells: }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The rows of its leaves, each under the one before, as each
|
|
142
|
+
# is reached only when the one before is not nil.
|
|
143
|
+
def self.rows(chain, indent)
|
|
144
|
+
chain[:operands].each_with_index.map do |operand, depth|
|
|
145
|
+
branch = "#{indent}#{' ' * depth}└─ "
|
|
146
|
+
HtmlTree.operand_row(operand, branch)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
require_relative 'choice'
|
|
3
|
+
|
|
4
|
+
module PathCoverage
|
|
5
|
+
module Kinds
|
|
6
|
+
# A ||= or &&= write, which decides whether its right side
|
|
7
|
+
# runs as an unless or an if decides its arm: its target is
|
|
8
|
+
# its predicate, its right side its then arm, so its ways and
|
|
9
|
+
# value are a choice's. Its target is never wrapped, so never
|
|
10
|
+
# read again.
|
|
11
|
+
module Write
|
|
12
|
+
extend Base
|
|
13
|
+
extend Arms
|
|
14
|
+
|
|
15
|
+
OPERATORS = WRITES
|
|
16
|
+
|
|
17
|
+
# The name of its arm, of a write named choice: the operator
|
|
18
|
+
# and its right side, eg ||= key.upcase.
|
|
19
|
+
def self.arm_label(arm, choice)
|
|
20
|
+
"#{choice} #{arm[:text]}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Its target needs no record call, and its right side a
|
|
24
|
+
# written_at call.
|
|
25
|
+
def self.own_calls?
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# { target range => nil }: its target is never read, so
|
|
30
|
+
# never wrapped.
|
|
31
|
+
def self.calls(_site, inner, _leaves)
|
|
32
|
+
{ inner.operands.first => nil }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# A write, its target the site's at-th leaf: its right side
|
|
36
|
+
# in a written_at call, which notes the target's outcome when
|
|
37
|
+
# it runs, and the whole of it in a settle call, which notes
|
|
38
|
+
# the other when it does not. A right side among commands,
|
|
39
|
+
# eg a command call, is parenthesised. Eg x ||= v becomes,
|
|
40
|
+
# in effect, settle(site, at, true, x ||= written_at(site,
|
|
41
|
+
# at, false, v)).
|
|
42
|
+
def self.inserts(site, inner, at, commands)
|
|
43
|
+
_target, arm = inner.operands
|
|
44
|
+
ran = THEN_ON[inner.operator]
|
|
45
|
+
name = site.inspect
|
|
46
|
+
written = "PathCoverage.written_at(#{name}, #{at}, #{ran}, "
|
|
47
|
+
settle = "PathCoverage.settle(#{name}, #{at}, #{!ran}, "
|
|
48
|
+
command = commands.include?(arm.extent)
|
|
49
|
+
Rewriter.wrap(settle, inner.extent, false, :kind) +
|
|
50
|
+
Rewriter.wrap(written, arm.extent, command, :leaf)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require_relative 'condition'
|
|
2
|
+
require_relative 'kinds/block'
|
|
3
|
+
require_relative 'kinds/case'
|
|
4
|
+
require_relative 'kinds/chain'
|
|
5
|
+
require_relative 'kinds/choice'
|
|
6
|
+
require_relative 'kinds/contains'
|
|
7
|
+
require_relative 'kinds/safe'
|
|
8
|
+
require_relative 'kinds/write'
|
|
9
|
+
|
|
10
|
+
module PathCoverage
|
|
11
|
+
# The kinds of condition, each a module that knows, for a
|
|
12
|
+
# condition named by one of its OPERATORS, the ways it can be
|
|
13
|
+
# evaluated, its value in each column of its site's tree, and
|
|
14
|
+
# the inserts that instrument it. Each extends Kinds::Base,
|
|
15
|
+
# whose hooks give what most kinds need.
|
|
16
|
+
module Kinds
|
|
17
|
+
# Every kind, each named by its operators.
|
|
18
|
+
ALL = [Block, Case, Chain, Choice, Contains, Safe, Write].freeze
|
|
19
|
+
|
|
20
|
+
# { operator => kind } for every operator of every kind.
|
|
21
|
+
BY_OPERATOR = ALL.flat_map do |kind|
|
|
22
|
+
kind::OPERATORS.map { |operator| [operator, kind] }
|
|
23
|
+
end.to_h.freeze
|
|
24
|
+
|
|
25
|
+
# The kind of a condition named operator, eg Chain for &&.
|
|
26
|
+
def self.of(operator)
|
|
27
|
+
BY_OPERATOR.fetch(operator)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The names of the kinds measured, eg chain for Chain: a
|
|
31
|
+
# report's fingerprint, as runs measuring different kinds
|
|
32
|
+
# list a site's paths differently.
|
|
33
|
+
def self.names
|
|
34
|
+
ALL.map { |kind| kind.name.split('::').last.downcase }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|