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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +287 -78
  3. data/lib/path_coverage/calls.rb +54 -15
  4. data/lib/path_coverage/condition.rb +25 -2
  5. data/lib/path_coverage/configuration.rb +44 -7
  6. data/lib/path_coverage/configuration_paths.rb +56 -0
  7. data/lib/path_coverage/console_summary.rb +77 -0
  8. data/lib/path_coverage/gaps.rb +25 -9
  9. data/lib/path_coverage/html/index.html +3 -17
  10. data/lib/path_coverage/html/side.js +21 -2
  11. data/lib/path_coverage/html/sort.js +24 -21
  12. data/lib/path_coverage/html/style.css +28 -1
  13. data/lib/path_coverage/html/tabs.js +15 -0
  14. data/lib/path_coverage/html_index.rb +86 -31
  15. data/lib/path_coverage/html_index_head.rb +49 -0
  16. data/lib/path_coverage/html_report.rb +30 -13
  17. data/lib/path_coverage/html_tabs.rb +26 -0
  18. data/lib/path_coverage/html_tree.rb +41 -20
  19. data/lib/path_coverage/json_reader.rb +94 -0
  20. data/lib/path_coverage/json_writer.rb +41 -9
  21. data/lib/path_coverage/kernel.rb +42 -20
  22. data/lib/path_coverage/kinds/base.rb +71 -0
  23. data/lib/path_coverage/kinds/block.rb +136 -0
  24. data/lib/path_coverage/kinds/case.rb +89 -0
  25. data/lib/path_coverage/kinds/chain.rb +45 -0
  26. data/lib/path_coverage/kinds/choice.rb +68 -0
  27. data/lib/path_coverage/kinds/contains.rb +111 -0
  28. data/lib/path_coverage/kinds/safe.rb +151 -0
  29. data/lib/path_coverage/kinds/write.rb +54 -0
  30. data/lib/path_coverage/kinds.rb +37 -0
  31. data/lib/path_coverage/manifest.rb +58 -19
  32. data/lib/path_coverage/manifest_blocks.rb +49 -10
  33. data/lib/path_coverage/manifest_chains.rb +76 -0
  34. data/lib/path_coverage/manifest_choices.rb +30 -17
  35. data/lib/path_coverage/manifest_folding.rb +91 -0
  36. data/lib/path_coverage/manifest_ignoring.rb +35 -0
  37. data/lib/path_coverage/manifest_writes.rb +67 -0
  38. data/lib/path_coverage/merge.rb +19 -3
  39. data/lib/path_coverage/post.rb +8 -8
  40. data/lib/path_coverage/recorder.rb +9 -49
  41. data/lib/path_coverage/recorder_blocks.rb +68 -0
  42. data/lib/path_coverage/recorder_cases.rb +23 -0
  43. data/lib/path_coverage/recorder_chains.rb +40 -0
  44. data/lib/path_coverage/recorder_writes.rb +26 -0
  45. data/lib/path_coverage/render.rb +58 -46
  46. data/lib/path_coverage/result.rb +19 -0
  47. data/lib/path_coverage/rewriter.rb +58 -77
  48. data/lib/path_coverage/rewriter_nested.rb +62 -0
  49. data/lib/path_coverage/site_paths.rb +9 -58
  50. data/lib/path_coverage/site_tree.rb +12 -57
  51. data/lib/path_coverage/summary.rb +32 -4
  52. data/lib/path_coverage/totals.rb +1 -1
  53. data/lib/path_coverage/version.rb +1 -1
  54. 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