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
|
@@ -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
|
data/lib/path_coverage/render.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
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
|
|
16
|
-
# => {source:, sites:} } for each covered file.
|
|
17
|
-
def self.run(operands, files, config, paths = nil,
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
63
|
-
|
|
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
|
data/lib/path_coverage/result.rb
CHANGED
|
@@ -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,
|
|
24
|
-
#
|
|
25
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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,
|
|
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
|
-
|
|
82
|
+
command = decision.command
|
|
83
|
+
wrap(call, decision.range, command, :decision, tail)
|
|
122
84
|
end
|
|
123
|
-
[
|
|
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
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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,
|
|
144
|
-
[range.end, 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,
|
|
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
|
|
153
|
-
# highest outermost, then closing ones highest
|
|
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, _,
|
|
158
|
-
|
|
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
|