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
@@ -1,8 +1,17 @@
1
1
  require_relative 'condition'
2
+ require_relative 'recorder_blocks'
3
+ require_relative 'recorder_cases'
4
+ require_relative 'recorder_chains'
5
+ require_relative 'recorder_writes'
2
6
 
3
7
  module PathCoverage
4
8
  # Records the outcome of each instrumented operand.
5
9
  class Recorder
10
+ include RecorderBlocks
11
+ include RecorderCases
12
+ include RecorderChains
13
+ include RecorderWrites
14
+
6
15
  # One call's way through its method: the outcome of each
7
16
  # decision made so far, of the number of decisions its
8
17
  # method has.
@@ -129,54 +138,5 @@ module PathCoverage
129
138
  leave(trails(site).last, matched) if last
130
139
  matched
131
140
  end
132
-
133
- # Starts an evaluation of a stop-early block method's call
134
- # at site, its block not yet called with any item.
135
- def enter_items(site)
136
- trail = enter(site, 2)
137
- trail.outcomes[0] = ITEMS[0]
138
- trail
139
- end
140
-
141
- # Notes value, the block's answer, as its last, and one more
142
- # item it was called with; returns value unchanged. A block
143
- # called where its call was not entered, eg in another
144
- # fiber, is counted but noted on no path.
145
- def item(site, value)
146
- trail = trails(site).last
147
- one_more(trail) if trail
148
- record(site, 1, value)
149
- end
150
-
151
- # Notes value, the block's answer for one more item, among
152
- # all its answers so far: all true, all false, or both;
153
- # returns value unchanged. A block called where its call
154
- # was not entered is counted but noted on no path.
155
- def answer(site, value)
156
- trail = trails(site).last
157
- if trail
158
- one_more(trail)
159
- now = value ? ANSWERS[0] : ANSWERS[1]
160
- same = [nil, now].include?(trail.outcomes[1])
161
- trail.outcomes[1] = same ? now : ANSWERS[2]
162
- end
163
- @counts[[site, 1]][value ? :truthy : :falsy] += 1
164
- value
165
- end
166
-
167
- # Notes on trail one more item its block was called with.
168
- def one_more(trail)
169
- called = ITEMS.index(trail.outcomes[0]) + 1
170
- trail.outcomes[0] = ITEMS[[called, 2].min]
171
- end
172
-
173
- # Ends a block method's call, counting its receiver truthy
174
- # when its block was called, falsy when not, and its path;
175
- # returns value, the call's, unchanged.
176
- def leave_items(trail, value)
177
- called = trail.outcomes[0] != ITEMS[0]
178
- @counts[[trail.site, 0]][called ? :truthy : :falsy] += 1
179
- leave(trail, value)
180
- end
181
141
  end
182
142
  end
@@ -0,0 +1,68 @@
1
+ require_relative 'condition'
2
+
3
+ module PathCoverage
4
+ # The part of the recorder that notes block method calls, eg
5
+ # ids.any? { |id| id.odd? }: how many items the block was called
6
+ # with, as the receiver's leaf, at, and its answers, as the next
7
+ # leaf; at is 0 for a call that is a site of its own. It relies
8
+ # on its includer's counts, record and trails.
9
+ module RecorderBlocks
10
+ # Starts a block method's call in site, its receiver the
11
+ # site's at-th leaf, its block not yet called with any item;
12
+ # returns the site's current trail, nil for none.
13
+ def begin_items(site, at)
14
+ trail = trails(site).last
15
+ trail.outcomes[at] = ITEMS[0] if trail
16
+ trail
17
+ end
18
+
19
+ # Notes value, the block's answer, as its last, and one more
20
+ # item it was called with, for a call whose receiver is
21
+ # site's at-th leaf; returns value unchanged. A block called
22
+ # where its call was not entered, eg in another fiber, is
23
+ # counted but noted on no path.
24
+ def item_at(site, at, value)
25
+ trail = trails(site).last
26
+ one_more(trail, at) if trail
27
+ record(site, at + 1, value)
28
+ end
29
+
30
+ # Notes value, the block's answer for one more item, among
31
+ # all its answers so far: all true, all false, or both, for a
32
+ # call whose receiver is site's at-th leaf; returns value
33
+ # unchanged. A block called where its call was not entered
34
+ # is counted but noted on no path.
35
+ def answer_at(site, at, value)
36
+ trail = trails(site).last
37
+ note_answer(trail, at, value) if trail
38
+ @counts[[site, at + 1]][value ? :truthy : :falsy] += 1
39
+ value
40
+ end
41
+
42
+ # Notes on trail one more item, and value among the answers
43
+ # so far of the block whose receiver is its at-th leaf.
44
+ def note_answer(trail, at, value)
45
+ one_more(trail, at)
46
+ now = value ? ANSWERS[0] : ANSWERS[1]
47
+ same = [nil, now].include?(trail.outcomes[at + 1])
48
+ trail.outcomes[at + 1] = same ? now : ANSWERS[2]
49
+ end
50
+
51
+ # Notes on trail one more item the block whose receiver is its
52
+ # at-th leaf was called with.
53
+ def one_more(trail, at)
54
+ called = ITEMS.index(trail.outcomes[at]) + 1
55
+ trail.outcomes[at] = ITEMS[[called, 2].min]
56
+ end
57
+
58
+ # Ends a block method's call whose receiver is site's at-th
59
+ # leaf, counting the receiver truthy when its block was
60
+ # called, falsy when not; returns value unchanged. With no
61
+ # trail, as begin_items found none, it is counted as called.
62
+ def end_items(site, at, trail, value)
63
+ called = trail.nil? || trail.outcomes[at] != ITEMS[0]
64
+ @counts[[site, at]][called ? :truthy : :falsy] += 1
65
+ value
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,23 @@
1
+ module PathCoverage
2
+ # The part of the recorder that notes a case nested in another
3
+ # site, eg (case a when 1 then b end) && c: the site around it
4
+ # enters and leaves, so its patterns only note whether they
5
+ # matched. It relies on its includer's record.
6
+ module RecorderCases
7
+ # A stand-in for the pattern of a case nested in site, its
8
+ # match noted as the site's index-th leaf.
9
+ NestedMatch = Struct.new(:recorder, :site, :index, :pattern) do
10
+ # Whether pattern matches subject, exactly as pattern says,
11
+ # noted on the site's path.
12
+ def ===(subject)
13
+ recorder.record(site, index, pattern === subject)
14
+ end
15
+ end
16
+
17
+ # The stand-in for pattern, the index-th leaf of site, in a
18
+ # case nested in it.
19
+ def pattern(site, index, pattern)
20
+ NestedMatch.new(self, site, index, pattern)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,40 @@
1
+ require_relative 'condition'
2
+
3
+ module PathCoverage
4
+ # The part of the recorder that notes &. chains. It relies on
5
+ # its includer's counts, record and trails.
6
+ module RecorderChains
7
+ # Notes value, the index-th leaf of site, a &. receiver, as
8
+ # not nil or nil, and returns it unchanged; it counts truthy
9
+ # when not nil. A receiver after a nil one in its chain,
10
+ # whose leaves start at first, was never reached, as Ruby
11
+ # skips the rest of the chain, so is not noted.
12
+ def present(site, index, first, value)
13
+ return value unless reached?(site, first, index)
14
+
15
+ @counts[[site, index]][value.nil? ? :falsy : :truthy] += 1
16
+ trail = trails(site).last
17
+ trail.outcomes[index] = PRESENCE[value.nil? ? 1 : 0] if trail
18
+ value
19
+ end
20
+
21
+ # Notes value, a &. chain's value as the index-th leaf of
22
+ # site, as record does, when every receiver of the chain,
23
+ # whose leaves start at first, was not nil; returns value
24
+ # unchanged.
25
+ def chained(site, index, first, value)
26
+ return value unless reached?(site, first, index)
27
+
28
+ record(site, index, value)
29
+ end
30
+
31
+ # True when site's current evaluation reached its index-th
32
+ # leaf: every leaf of its chain before it, from first, was
33
+ # not nil. An evaluation with no trail, eg in another fiber,
34
+ # is taken to reach it.
35
+ def reached?(site, first, index)
36
+ trail = trails(site).last
37
+ trail.nil? || trail.outcomes[first...index].all?(PRESENCE[0])
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,26 @@
1
+ module PathCoverage
2
+ # The part of the recorder that notes ||= and &&= writes: the
3
+ # target's outcome at its place among its site's leaves, at 0
4
+ # for a write that is a site of its own. It relies on its
5
+ # includer's record and trails.
6
+ module RecorderWrites
7
+ # Notes outcome as the target's, site's at-th leaf, of the
8
+ # write whose right side ran, false for ||=, true for &&=,
9
+ # and returns value, the right side's, unchanged. The target
10
+ # is never read, as reading it could call a method twice.
11
+ def written_at(site, at, outcome, value)
12
+ record(site, at, outcome)
13
+ value
14
+ end
15
+
16
+ # Notes skipped as the outcome of the write's target, site's
17
+ # at-th leaf, when its right side never ran, so noted none;
18
+ # returns value, the write's, unchanged.
19
+ def settle(site, at, skipped, value)
20
+ trail = trails(site).last
21
+ unnoted = trail.nil? || trail.outcomes[at].nil?
22
+ record(site, at, skipped) if unnoted
23
+ value
24
+ end
25
+ end
26
+ end
@@ -1,8 +1,8 @@
1
- require 'json'
1
+ require_relative 'console_summary'
2
2
  require_relative 'gaps'
3
3
  require_relative 'html_report'
4
+ require_relative 'json_reader'
4
5
  require_relative 'manifest'
5
- require_relative 'merge'
6
6
  require_relative 'paths_report'
7
7
  require_relative 'post'
8
8
  require_relative 'site_paths'
@@ -11,20 +11,55 @@ module PathCoverage
11
11
  # The reporters, run from operands alone: needing nothing the
12
12
  # run recorded beyond them, they can run in another process.
13
13
  module Render
14
- # Runs each reporter chosen in config. operands and
15
- # site_paths are as Result gives them; files is { full path
16
- # => {source:, sites:} } for each covered file.
17
- def self.run(operands, files, config, paths = nil, site_paths: {})
18
- site_paths = every_site(site_paths, files)
14
+ # Runs each reporter chosen in config. operands, and taken's
15
+ # site_paths: and outside:, are as Result gives them; files
16
+ # is { full path => {source:, sites:} } for each covered file.
17
+ def self.run(operands, files, config, paths = nil, **taken)
18
+ site_paths = every_site(taken.fetch(:site_paths, {}), files)
19
19
  reporters = config.reporters
20
- gaps(operands, files, site_paths) if reporters.include?(:gaps)
21
- if reporters.include?(:html)
22
- HtmlReport.write(operands, files, config, paths, site_paths:)
20
+ if reporters.include?(:gaps)
21
+ gaps(operands, files, site_paths, taken.fetch(:outside, {}))
23
22
  end
23
+ html_and_summary(operands, files, config, [paths, site_paths])
24
24
  return unless reporters.include?(:post)
25
25
 
26
+ post(operands, files, config, site_paths)
27
+ end
28
+
29
+ # Runs the :post reporter: operands, and the paths report of
30
+ # site_paths in files, with config's fingerprint.
31
+ def self.post(operands, files, config, site_paths)
26
32
  report = PathsReport.of(site_paths, files)
27
- Post.deliver(operands, config.collector_url, report)
33
+ url = config.collector_url
34
+ fingerprint = config.fingerprint
35
+ Post.deliver(operands, url, report, fingerprint:)
36
+ end
37
+
38
+ # Runs the :html and :summary reporters, each when chosen.
39
+ def self.html_and_summary(operands, files, config, paths_pair)
40
+ paths, site_paths = paths_pair
41
+ reporters = config.reporters
42
+ if reporters.include?(:html)
43
+ HtmlReport.write(operands, files, config, paths, site_paths:)
44
+ end
45
+ return unless reporters.include?(:summary)
46
+
47
+ summary(files, config, paths, site_paths)
48
+ end
49
+
50
+ # Prints the summary lines: the paths taken of all possible
51
+ # within decisions, and, when paths are given, as they are
52
+ # when methods are measured, within methods; then any
53
+ # operators ignored.
54
+ def self.summary(files, config, paths, site_paths)
55
+ methods = files.map do |full_path, parsed|
56
+ path = config.relative(full_path)
57
+ PathManifest.methods(parsed[:source], path)
58
+ end
59
+ all = methods.reduce({}, :merge)
60
+ groups = config.groups
61
+ puts ConsoleSummary.lines(site_paths, all, paths, groups) +
62
+ ConsoleSummary.ignored_lines(config.ignored)
28
63
  end
29
64
 
30
65
  # site_paths with every site in files that it lacks, eg
@@ -42,51 +77,28 @@ module PathCoverage
42
77
  none.merge(site_paths)
43
78
  end
44
79
 
45
- # Prints the gap lines: each path a site never took, each
46
- # leaf named by its text in files.
47
- def self.gaps(operands, files, site_paths)
80
+ # Prints the gap lines: each path a site never took, and each
81
+ # path taken that a treatment rules out, as outside lists
82
+ # them, each leaf named by its text in files.
83
+ def self.gaps(operands, files, site_paths, outside)
48
84
  texts = PathsReport.texts(files)
49
- lines = Gaps.lines(operands, paths: site_paths, texts:)
85
+ paths = site_paths
86
+ lines = Gaps.lines(operands, paths:, texts:, outside:)
50
87
  lines.each { |line| puts line }
51
88
  end
52
89
 
53
90
  # Runs the reporters chosen in config from the JSON reports
54
91
  # at paths, their counts merged, parsing each covered file
55
- # afresh. paths is config.json_path alone by default.
92
+ # afresh; with methods measured, the method paths they hold
93
+ # too. paths is config.json_path alone by default.
56
94
  def self.from_json(config, paths = [config.json_path])
57
- operands = Merge.operands(paths.map { |path| read(path) })
58
- site_paths = Merge.paths(paths.map { |path| paths_of(path) })
95
+ operands = JsonReader.operands(paths, config.fingerprint)
59
96
  files = config.files.to_h do |path|
60
97
  [path, Manifest.parse(path, config)]
61
98
  end
62
- run(operands, files, config, site_paths:)
63
- end
64
-
65
- # The site paths of the JSON report at path, as
66
- # Result#paths gives them; none for a report that has none.
67
- def self.paths_of(path)
68
- json = JSON.parse(File.read(path))
69
- json.fetch('paths', {}).transform_values do |site|
70
- site['taken'].map do |each|
71
- { path: each['path'], count: each['count'] }
72
- end
73
- end
74
- end
75
-
76
- # The operands of the JSON report at path.
77
- def self.read(path)
78
- json = JSON.parse(File.read(path))
79
- json['sites'].transform_values do |leaves|
80
- leaves.map { |leaf| counts(leaf) }
81
- end
82
- end
83
-
84
- # One operand's counts as JSON holds them, with the keys,
85
- # and any only: value, as the symbols Result#operands uses.
86
- def self.counts(leaf)
87
- counts = { truthy: leaf['truthy'], falsy: leaf['falsy'] }
88
- only = leaf['only']
89
- only ? counts.merge(only: only.to_sym) : counts
99
+ methods = config.measures.include?(:methods)
100
+ taken = methods ? JsonReader.method_paths(paths) : nil
101
+ run(operands, files, config, taken, **JsonReader.taken(paths))
90
102
  end
91
103
  end
92
104
  end
@@ -30,6 +30,25 @@ module PathCoverage
30
30
  end
31
31
  end
32
32
 
33
+ # { site => { path => evaluations } } of each path taken
34
+ # that is none of its site's possible paths, eg a &. receiver
35
+ # nil though treated as present; only sites with one. Such a
36
+ # path shows a treatment to be wrong, so it is reported, never
37
+ # dropped.
38
+ def outside
39
+ all = @sites.to_h do |site, condition|
40
+ taken = @site_paths.fetch(site, {})
41
+ [site, taken.except(*SitePaths.possible(condition))]
42
+ end
43
+ all.reject { |_, paths| paths.empty? }
44
+ end
45
+
46
+ # The paths taken, as Render.run takes them: site_paths:,
47
+ # as paths gives them, and outside:.
48
+ def taken
49
+ { site_paths: paths, outside: }
50
+ end
51
+
33
52
  # { site => [counts of operand 0, counts of operand 1, ...] },
34
53
  # with zero counts for an operand that never ran, and
35
54
  # only: naming the one outcome a fixed leaf can have.
@@ -1,4 +1,6 @@
1
1
  require_relative 'condition'
2
+ require_relative 'kinds'
3
+ require_relative 'rewriter_nested'
2
4
 
3
5
  module PathCoverage
4
6
  # Instruments source text so that each leaf operand records its
@@ -20,56 +22,28 @@ module PathCoverage
20
22
  apply(source.dup, inserts + path_inserts(methods))
21
23
  end
22
24
 
23
- # The inserts for one site, by its kind: a case's patterns'
24
- # stand-ins, a block method's call and block, or any other
25
- # site's leaves' records and its leave.
25
+ # The inserts for one site, as its kind gives them, eg its
26
+ # leaves' records and its leave; a block method's call or a
27
+ # write is noted as if nested at leaf 0.
26
28
  def self.site_inserts(site, condition)
27
- kind = condition.operator
28
- return case_inserts(site, condition) if kind == 'case'
29
-
30
- blocks = BLOCK_PATHS.key?(kind)
31
- return block_inserts(site, condition) if blocks
32
-
33
- record_inserts(site, condition) + trail_inserts(site, condition)
34
- end
35
-
36
- # The inserts that wrap a block method's call whole in a
37
- # leave_items call that enters it first, inside any leave of
38
- # a site it is the predicate of, and its block's expression,
39
- # outside any site's leave there, in an item call, noting its
40
- # last answer, or for a method that calls it with every item
41
- # an answer call, noting every answer.
42
- def self.block_inserts(site, condition)
43
- _receiver, body = condition.leaves
44
- enter = "PathCoverage.enter_items(#{site.inspect})"
45
- call = "PathCoverage.leave_items(#{enter}, "
46
- every = EVERY_ITEM.include?(condition.operator)
47
- noter = every ? 'answer' : 'item'
48
- item = "PathCoverage.#{noter}(#{site.inspect}, "
49
- command = condition.commands.include?(body)
50
- wrap(call, condition.extent, false, 0.5) +
51
- wrap(item, body, command, 1.5)
52
- end
53
-
54
- # The inserts that wrap each pattern of a case site in a
55
- # when_match call, its stand-in, which enters and leaves the
56
- # site itself, as the case tries its patterns one by one.
57
- def self.case_inserts(site, condition)
58
- size = condition.leaves.size
59
- condition.leaves.each_with_index.flat_map do |range, index|
60
- call = "PathCoverage.when_match(#{site.inspect}, " \
61
- "#{index}, #{size}, "
62
- wrap(call, range, false, 0)
63
- end
29
+ Kinds.of(condition.operator).site_inserts(site, condition)
64
30
  end
65
31
 
66
32
  # The inserts that wrap each of site's leaves in a record
67
- # call, innermost of all.
33
+ # call, innermost of all; a leaf of a nested site that needs
34
+ # a call of its own, eg a &. chain's, in the call
35
+ # RewriterNested gives it, or in none, and that site's own
36
+ # inserts too.
68
37
  def self.record_inserts(site, condition)
69
- condition.leaves.each_with_index.flat_map do |range, index|
70
- command = condition.commands.include?(range)
71
- wrap(record_call(site, index), range, command, 0)
38
+ nested = RewriterNested.calls(site, condition)
39
+ leaves = condition.leaves
40
+ records = leaves.each_with_index.flat_map do |range, i|
41
+ call = nested.fetch(range) { record_call(site, i) }
42
+ next [] unless call
43
+
44
+ wrap(call, range, condition.commands.include?(range), :leaf)
72
45
  end
46
+ records + RewriterNested.inserts(site, condition)
73
47
  end
74
48
 
75
49
  WORD_OPERATORS = %w[and or].freeze
@@ -83,22 +57,10 @@ module PathCoverage
83
57
  enter = "PathCoverage.enter(#{site.inspect}, " \
84
58
  "#{condition.leaves.size})"
85
59
  call = "PathCoverage.leave(#{enter}, "
86
- wrapped = trailed(condition)
60
+ wrapped = Kinds.of(condition.operator).trailed(condition)
87
61
  nested = wrapped.is_a?(Condition)
88
62
  word = nested && WORD_OPERATORS.include?(wrapped.operator)
89
- wrap(call, nested ? wrapped.extent : wrapped, word, 1)
90
- end
91
-
92
- # What a site's leave call wraps: an if's or an unless's
93
- # predicate, a leaf's byte range or a nested Condition, as
94
- # its path is decided there and its arms are statements that
95
- # may return or raise; any other site as a whole.
96
- def self.trailed(condition)
97
- unless STATEMENT_CHOICES.include?(condition.operator)
98
- return condition
99
- end
100
-
101
- condition.operands.first
63
+ wrap(call, nested ? wrapped.extent : wrapped, word, :site)
102
64
  end
103
65
 
104
66
  # The opening of the record call around operand index of
@@ -114,16 +76,23 @@ module PathCoverage
114
76
  def self.path_inserts(methods)
115
77
  methods.flat_map do |site, method|
116
78
  decisions = method.decisions
117
- start = path_start(site, decisions.size)
118
79
  decides = decisions.each_with_index.flat_map do |decision, i|
119
80
  call = "PathCoverage.decide(#{PATH_NAME}, #{i}, "
120
81
  tail = decision.exits.nil? ? '' : ", #{decision.exits}"
121
- wrap(call, decision.range, decision.command, 2, tail)
82
+ command = decision.command
83
+ wrap(call, decision.range, command, :decision, tail)
122
84
  end
123
- [[method.entry, start, 3]] + decides
85
+ [start_insert(site, method)] + decides
124
86
  end
125
87
  end
126
88
 
89
+ # The insert at method's entry that starts a call's path,
90
+ # outermost of all there.
91
+ def self.start_insert(site, method)
92
+ start = path_start(site, method.decisions.size)
93
+ [method.entry, start, [Float::INFINITY, LAYERS.index(:path)]]
94
+ end
95
+
127
96
  # The statement that starts a call's path through the
128
97
  # method at site, which has size decisions.
129
98
  def self.path_start(site, size)
@@ -131,31 +100,43 @@ module PathCoverage
131
100
  "#{PATH_NAME} = #{call}; "
132
101
  end
133
102
 
134
- # The two inserts that wrap the code at range in call, of
135
- # rank among inserts at the same offset, with tail as the
136
- # call's further arguments. A command call is parenthesised,
137
- # as it is no bare argument; nothing else is, since
138
- # parentheses make a statement, which gives its line a
139
- # line-coverage count of its own.
140
- def self.wrap(call, range, command, rank, tail = '')
103
+ # The layers of inserts, innermost first, that break a tie
104
+ # between inserts wrapping the same range: a leaf's call, a
105
+ # nested kind's call around its whole, eg end_items, a site's
106
+ # leave, a method's decide, and a method's path start.
107
+ LAYERS = %i[leaf kind site decision path].freeze
108
+
109
+ # The two inserts that wrap the code at range in call, with
110
+ # tail as the call's further arguments. Their order, [size,
111
+ # rank], nests them among inserts at the same offset: the
112
+ # longer range outside, the rank of layer in LAYERS breaking
113
+ # a tie. A command call is parenthesised, as it is no bare
114
+ # argument; nothing else is, since parentheses make a
115
+ # statement, which gives its line a line-coverage count of
116
+ # its own.
117
+ def self.wrap(call, range, command, layer, tail = '')
141
118
  open, close = command ? ['(', ")#{tail})"] : ['', "#{tail})"]
119
+ order = [range.size, LAYERS.index(layer)]
142
120
  [
143
- [range.begin, call + open, rank],
144
- [range.end, close, rank, :close]
121
+ [range.begin, call + open, order],
122
+ [range.end, close, order, :close]
145
123
  ]
146
124
  end
147
125
 
148
- # Splices each [offset, text, rank] insert into source, last
149
- # offset first so earlier offsets stay valid; a closing
126
+ # Splices each [offset, text, order] insert into source,
127
+ # last offset first so earlier offsets stay valid; a closing
150
128
  # insert is marked :close. Text spliced in later at an
151
129
  # offset lands before text already there, so at one offset
152
- # opening inserts go in lowest rank first, leaving the
153
- # highest outermost, then closing ones highest rank first,
130
+ # opening inserts go in lowest order first, leaving the
131
+ # highest outermost, then closing ones highest order first,
154
132
  # so the innermost call closes first and each tail stays
155
- # with its own call.
133
+ # with its own call. Eg a ternary that is a method's
134
+ # decision is a site too: its leave, wrapping the whole
135
+ # ternary, opens outside the decide wrapping its predicate.
156
136
  def self.apply(source, inserts)
157
- sorted = inserts.sort_by do |offset, _, rank, close|
158
- close ? [-offset, 1, -rank] : [-offset, 0, rank]
137
+ sorted = inserts.sort_by do |offset, _, order, close|
138
+ size, rank = order
139
+ close ? [-offset, 1, -size, -rank] : [-offset, 0, size, rank]
159
140
  end
160
141
  sorted.each do |offset, text, _|
161
142
  source.bytesplice(offset, 0, text)
@@ -0,0 +1,62 @@
1
+ require_relative 'condition'
2
+ require_relative 'kinds'
3
+
4
+ module PathCoverage
5
+ # The part of the rewriter that instruments the sites nested in
6
+ # another, eg the &. chain in ok && user&.admin?. The site
7
+ # around them enters and leaves; each nested site's leaves are
8
+ # noted as that site's, at their place among its leaves, by the
9
+ # calls its kind gives in place of plain records.
10
+ module RewriterNested
11
+ # { leaf range => the opening of its call, nil for none } for
12
+ # the leaves of every site nested in condition whose kind has
13
+ # calls of its own, condition itself included.
14
+ def self.calls(site, condition)
15
+ leaves = condition.leaves
16
+ rooted(condition).reduce({}) do |calls, inner|
17
+ kind = Kinds.of(inner.operator)
18
+ calls.merge(kind.calls(site, inner, leaves))
19
+ end
20
+ end
21
+
22
+ # The inserts, besides its leaves' calls, for every site
23
+ # nested in condition whose kind has calls of its own,
24
+ # condition itself included, as its kind gives them.
25
+ def self.inserts(site, condition)
26
+ leaves = condition.leaves
27
+ rooted(condition).flat_map do |inner|
28
+ at = leaves.index(inner.operands.first)
29
+ kind = Kinds.of(inner.operator)
30
+ kind.inserts(site, inner, at, condition.commands)
31
+ end
32
+ end
33
+
34
+ # condition alone when its kind has calls of its own, eg a
35
+ # block method's call that is a site of its own, its leaves
36
+ # then noted at their places from 0; else the sites nested in
37
+ # it.
38
+ def self.rooted(condition)
39
+ return [condition] if own_calls?(condition)
40
+
41
+ nested(condition)
42
+ end
43
+
44
+ # The sites nested in condition whose kind has calls of its
45
+ # own, in its operands and in its arms' conditions.
46
+ def self.nested(condition)
47
+ condition.operands.flat_map do |operand|
48
+ case operand
49
+ when Condition
50
+ own_calls?(operand) ? [operand] : nested(operand)
51
+ when Arm then operand.body ? nested(operand.body) : []
52
+ else []
53
+ end
54
+ end
55
+ end
56
+
57
+ # Whether condition's kind gives its leaves calls of their own.
58
+ def self.own_calls?(condition)
59
+ Kinds.of(condition.operator).own_calls?
60
+ end
61
+ end
62
+ end