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,49 @@
|
|
|
1
|
+
module PathCoverage
|
|
2
|
+
# The two heading rows of the HTML index's files tables: File
|
|
3
|
+
# and Sites, then each total's name over its taken, / of and %
|
|
4
|
+
# columns, every column sortable.
|
|
5
|
+
module HtmlIndexHead
|
|
6
|
+
# The class and label of each of a total's three columns.
|
|
7
|
+
PARTS = [%w[taken taken], ['of', '/ of'], %w[percent %]].freeze
|
|
8
|
+
|
|
9
|
+
# The two heading rows; methods' total only when methods are
|
|
10
|
+
# measured.
|
|
11
|
+
def self.of(methods)
|
|
12
|
+
names = ['Within each decision']
|
|
13
|
+
names << 'Within each method' if methods
|
|
14
|
+
rows = [top_row(names), part_row(names)]
|
|
15
|
+
rows.map { |row| "<tr>\n#{row}\n</tr>" }.join("\n")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The first heading row: File and Sites, each over both rows,
|
|
19
|
+
# then each total's name over its three columns.
|
|
20
|
+
def self.top_row(names)
|
|
21
|
+
fixed = [
|
|
22
|
+
heading(' rowspan="2"', 0, 'text', 'File'),
|
|
23
|
+
heading(' class="num" rowspan="2"', 1, 'number', 'Sites')
|
|
24
|
+
]
|
|
25
|
+
groups = names.map do |name|
|
|
26
|
+
%(<th class="group" colspan="3">#{name}</th>)
|
|
27
|
+
end
|
|
28
|
+
(fixed + groups).join("\n")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The second heading row: each total's taken, / of and %.
|
|
32
|
+
def self.part_row(names)
|
|
33
|
+
names.each_index.flat_map do |group|
|
|
34
|
+
PARTS.each_with_index.map do |(css, label), i|
|
|
35
|
+
column = 2 + (3 * group) + i
|
|
36
|
+
heading(%( class="#{css}"), column, 'number', label)
|
|
37
|
+
end
|
|
38
|
+
end.join("\n")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A sortable heading, attrs its th's, sorting the column-th
|
|
42
|
+
# column as type, its button showing label.
|
|
43
|
+
def self.heading(attrs, column, type, label)
|
|
44
|
+
data = %(data-column="#{column}" data-type="#{type}")
|
|
45
|
+
button = %(<button type="button" #{data}>#{label}</button>)
|
|
46
|
+
%(<th#{attrs} aria-sort="none">#{button}</th>)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -15,9 +15,11 @@ module PathCoverage
|
|
|
15
15
|
# Writes every page. operands and site_paths are as Result
|
|
16
16
|
# gives them, site_paths holding every site in files; files
|
|
17
17
|
# is { full path => {source:, sites:} } for each covered
|
|
18
|
-
# file, as the kernel parsed it.
|
|
18
|
+
# file, as the kernel parsed it. The files dir is emptied
|
|
19
|
+
# first, so it holds no page for a file no longer covered.
|
|
19
20
|
def self.write(operands, files, config, paths = nil, site_paths:)
|
|
20
21
|
dir = config.output_dir
|
|
22
|
+
FileUtils.rm_rf("#{dir}/files")
|
|
21
23
|
index = index(files, config, paths, site_paths)
|
|
22
24
|
File.write("#{dir}/index.html", index)
|
|
23
25
|
files.each do |full_path, parsed|
|
|
@@ -32,8 +34,10 @@ module PathCoverage
|
|
|
32
34
|
|
|
33
35
|
# The index page: a row per file of its totals within each
|
|
34
36
|
# decision, and, when paths are given, as they are when
|
|
35
|
-
# methods are measured,
|
|
36
|
-
# then their Total row.
|
|
37
|
+
# methods are measured, within each method too;
|
|
38
|
+
# then their Total row. With groups configured, each group,
|
|
39
|
+
# and the files in none, get a tab of their own too. Any
|
|
40
|
+
# operators config ignored are named after them.
|
|
37
41
|
def self.index(files, config, paths, site_paths)
|
|
38
42
|
methods = files.map do |full_path, parsed|
|
|
39
43
|
path = config.relative(full_path)
|
|
@@ -41,7 +45,10 @@ module PathCoverage
|
|
|
41
45
|
end
|
|
42
46
|
all = methods.reduce({}, :merge)
|
|
43
47
|
rows = Summary.files(site_paths, all, paths)
|
|
44
|
-
|
|
48
|
+
groups = config.groups
|
|
49
|
+
tabs = groups.empty? ? [] : Summary.groups(rows, groups)
|
|
50
|
+
ignored = config.ignored
|
|
51
|
+
HtmlIndex.page(rows, Summary.total(rows), tabs, ignored:)
|
|
45
52
|
end
|
|
46
53
|
|
|
47
54
|
# simplecov:disable
|
|
@@ -81,24 +88,34 @@ module PathCoverage
|
|
|
81
88
|
# on it and on each of its operands: a leaf's from its byte
|
|
82
89
|
# range, a condition's and an arm's from its extent, the
|
|
83
90
|
# whole of it as written. Columns count characters, not
|
|
84
|
-
# bytes.
|
|
91
|
+
# bytes. A node past the condition's operands, eg owner at
|
|
92
|
+
# the end of user&.account&.owner, is kept as it is.
|
|
85
93
|
def self.located(source, condition, tree)
|
|
86
94
|
pairs = condition.operands.zip(tree[:operands])
|
|
87
95
|
operands = pairs.map do |operand, node|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
located_operand(source, operand, node)
|
|
97
|
+
end
|
|
98
|
+
rest = tree[:operands].drop(pairs.size)
|
|
99
|
+
tree.merge(at: at(source, condition.extent),
|
|
100
|
+
operands: operands + rest)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# SPIKE: the node of operand with its at:, as located gives
|
|
104
|
+
# a condition's, an arm's or a leaf's.
|
|
105
|
+
def self.located_operand(source, operand, node)
|
|
106
|
+
case operand
|
|
107
|
+
when Condition then located(source, operand, node)
|
|
108
|
+
when Arm then located_arm(source, operand, node)
|
|
109
|
+
else node.merge(at: at(source, operand))
|
|
93
110
|
end
|
|
94
|
-
tree.merge(at: at(source, condition.extent), operands:)
|
|
95
111
|
end
|
|
96
112
|
|
|
97
113
|
# An arm's node with its at:, nil for an arm with no
|
|
98
114
|
# statements, and its body's tree located.
|
|
99
115
|
def self.located_arm(source, arm, node)
|
|
100
|
-
body = arm.body
|
|
101
|
-
|
|
116
|
+
body = arm.body ? located(source, arm.body, node[:body]) : nil
|
|
117
|
+
at = arm.extent ? at(source, arm.extent) : nil
|
|
118
|
+
node.merge(at:, body:)
|
|
102
119
|
end
|
|
103
120
|
|
|
104
121
|
# [line, column, line, column] of the byte range in source.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'cgi/escape'
|
|
2
|
+
|
|
3
|
+
module PathCoverage
|
|
4
|
+
# The index page's tab bar, one tab per table: All files,
|
|
5
|
+
# then each group.
|
|
6
|
+
module HtmlTabs
|
|
7
|
+
# The tab bar of names, the first tab selected, each tab
|
|
8
|
+
# controlling the table of the same index.
|
|
9
|
+
def self.bar(names)
|
|
10
|
+
tabs = names.each_with_index.map { |name, i| tab(name, i) }
|
|
11
|
+
nav = %(<nav class="tabs" role="tablist" aria-label="Groups">)
|
|
12
|
+
"#{nav}#{tabs.join}</nav>"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# The index-th tab, named name.
|
|
16
|
+
def self.tab(name, index)
|
|
17
|
+
selected = index.zero?
|
|
18
|
+
[
|
|
19
|
+
%(<button type="button" role="tab"),
|
|
20
|
+
%( aria-selected="#{selected}"),
|
|
21
|
+
%( aria-controls="files-#{index}">),
|
|
22
|
+
CGI.escapeHTML(name), '</button>'
|
|
23
|
+
].join
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'cgi/escape'
|
|
2
2
|
require_relative 'condition'
|
|
3
|
+
require_relative 'kinds'
|
|
3
4
|
|
|
4
5
|
module PathCoverage
|
|
5
6
|
# The tree of a span's sites on a file's page, from SiteTree.of
|
|
@@ -8,14 +9,13 @@ module PathCoverage
|
|
|
8
9
|
OUTCOMES = {
|
|
9
10
|
true => 'T', false => 'F', nil => '',
|
|
10
11
|
ITEMS[0] => '0', ITEMS[1] => '1', ITEMS[2] => '*',
|
|
11
|
-
ANSWERS[0] => 'T', ANSWERS[1] => 'F', ANSWERS[2] => 'TF'
|
|
12
|
+
ANSWERS[0] => 'T', ANSWERS[1] => 'F', ANSWERS[2] => 'TF',
|
|
13
|
+
PRESENCE[0] => [0x25CF].pack('U'),
|
|
14
|
+
PRESENCE[1] => [0x2205].pack('U')
|
|
12
15
|
}.freeze
|
|
13
|
-
# The choices whose arms are named by their kind, eg then,
|
|
14
|
-
# else or when, as their text is statements.
|
|
15
|
-
NAMED_ARMS = [*STATEMENT_CHOICES, 'case'].freeze
|
|
16
16
|
|
|
17
|
-
# A
|
|
18
|
-
TAKEN = { true => [
|
|
17
|
+
# A tick marks a path taking an arm.
|
|
18
|
+
TAKEN = { true => [0x2713].pack('U'), false => '' }.freeze
|
|
19
19
|
|
|
20
20
|
# The tree of a span, open when its sites have gaps, else
|
|
21
21
|
# hidden: each site's rows in a group of their own.
|
|
@@ -45,10 +45,13 @@ module PathCoverage
|
|
|
45
45
|
end.join
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
# One cell per path, naming each with names
|
|
48
|
+
# One cell per path, naming each with names; a &. receiver's,
|
|
49
|
+
# shown as a glyph, titled not nil or nil for its hover text.
|
|
49
50
|
def self.cells(values, names = OUTCOMES)
|
|
50
51
|
values.map do |value|
|
|
51
|
-
%(
|
|
52
|
+
title = PRESENCE.include?(value) ? %( title="#{value}") : ''
|
|
53
|
+
attrs = %(class="cell" data-cell=""#{title})
|
|
54
|
+
%(<span #{attrs}>#{names[value]}</span>)
|
|
52
55
|
end.join
|
|
53
56
|
end
|
|
54
57
|
|
|
@@ -61,10 +64,16 @@ module PathCoverage
|
|
|
61
64
|
# The rows of a condition's operands, each branching from
|
|
62
65
|
# its operator after indent; a nested condition's operands,
|
|
63
66
|
# and those of an arm's condition, follow its own row,
|
|
64
|
-
# indented under its branch.
|
|
67
|
+
# indented under its branch. A leaf with sites inside it
|
|
68
|
+
# branches as those sites, then the leaf, in the order they
|
|
69
|
+
# run.
|
|
65
70
|
def self.operand_rows(condition, indent)
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
own = Kinds.of(condition[:operator]).rows(condition, indent)
|
|
72
|
+
return own if own
|
|
73
|
+
|
|
74
|
+
operands = unfolded(condition[:operands])
|
|
75
|
+
last = operands.size - 1
|
|
76
|
+
operands.each_with_index.flat_map do |operand, i|
|
|
68
77
|
tip = i == last
|
|
69
78
|
branch = indent + (tip ? '└─ ' : '├─ ')
|
|
70
79
|
under = indent + (tip ? ' ' : '│ ')
|
|
@@ -91,7 +100,7 @@ module PathCoverage
|
|
|
91
100
|
# plain arm marks the paths taking it.
|
|
92
101
|
def self.arm_rows(arm, branch, under, choice)
|
|
93
102
|
body = arm[:body]
|
|
94
|
-
values = cells(
|
|
103
|
+
values = body ? cells(body[:cells]) : cells(arm[:cells], TAKEN)
|
|
95
104
|
text = CGI.escapeHTML(arm_label(arm, choice))
|
|
96
105
|
lead = branch_cell(branch) +
|
|
97
106
|
%(<span class="arm" data-label="">#{text}</span>)
|
|
@@ -99,13 +108,20 @@ module PathCoverage
|
|
|
99
108
|
body ? [row, *operand_rows(body, under)] : [row]
|
|
100
109
|
end
|
|
101
110
|
|
|
102
|
-
# The name of an arm of a condition whose operator is choice
|
|
111
|
+
# The name of an arm of a condition whose operator is choice,
|
|
112
|
+
# as its kind names it, eg then for an if's.
|
|
103
113
|
def self.arm_label(arm, choice)
|
|
104
|
-
|
|
114
|
+
Kinds.of(choice).arm_label(arm, choice)
|
|
115
|
+
end
|
|
105
116
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
# operands with each node standing for the nodes its kind
|
|
118
|
+
# unfolds it into, eg a leaf with sites inside it for those
|
|
119
|
+
# sites, then the leaf; a leaf or an arm for itself.
|
|
120
|
+
def self.unfolded(operands)
|
|
121
|
+
operands.flat_map do |operand|
|
|
122
|
+
operator = operand[:operator]
|
|
123
|
+
operator ? Kinds.of(operator).unfold(operand) : [operand]
|
|
124
|
+
end
|
|
109
125
|
end
|
|
110
126
|
|
|
111
127
|
# A nested condition's row: its branch, its operator, and
|
|
@@ -122,20 +138,25 @@ module PathCoverage
|
|
|
122
138
|
%(<div class="row"#{attrs}>#{lead}#{values}</div>)
|
|
123
139
|
end
|
|
124
140
|
|
|
125
|
-
# The cell naming a condition's operator
|
|
141
|
+
# The cell naming a condition's operator, or its label when
|
|
142
|
+
# it has one, eg select { for a site inside a leaf.
|
|
126
143
|
def self.operator(condition)
|
|
127
|
-
name = CGI.escapeHTML(condition[:operator])
|
|
144
|
+
name = CGI.escapeHTML(condition[:label] || condition[:operator])
|
|
128
145
|
%(<span class="operator" data-operator="">#{name}</span>)
|
|
129
146
|
end
|
|
130
147
|
|
|
131
148
|
# One operand's row: its branch, text, and its outcome on
|
|
132
149
|
# each path.
|
|
150
|
+
# An operand that is only called, eg owner at the end of
|
|
151
|
+
# user&.account&.owner, has a tick on each path that called
|
|
152
|
+
# it instead.
|
|
133
153
|
def self.operand_row(operand, branch)
|
|
134
154
|
text = CGI.escapeHTML(operand[:text].to_s)
|
|
135
155
|
lead = branch_cell(branch) +
|
|
136
156
|
%(<span class="text" data-text="">#{text}</span>)
|
|
137
157
|
attrs = %( data-operand=""#{at(operand)})
|
|
138
|
-
|
|
158
|
+
names = operand[:called] ? TAKEN : OUTCOMES
|
|
159
|
+
row(attrs, lead, cells(operand[:cells], names))
|
|
139
160
|
end
|
|
140
161
|
|
|
141
162
|
# The cell drawing a row's branch from its operator.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require_relative 'kinds'
|
|
3
|
+
require_relative 'merge'
|
|
4
|
+
|
|
5
|
+
module PathCoverage
|
|
6
|
+
# Reads back the JSON reports JsonWriter writes, merging the
|
|
7
|
+
# counts of several runs, and refusing any report whose
|
|
8
|
+
# sites' paths are listed differently from a render's.
|
|
9
|
+
module JsonReader
|
|
10
|
+
# The keys of a JSON report's fingerprint.
|
|
11
|
+
FINGERPRINT = %w[ignored treats].freeze
|
|
12
|
+
|
|
13
|
+
# The operands of the JSON reports at paths, merged; raises
|
|
14
|
+
# unless each measured the kinds a render does, with
|
|
15
|
+
# fingerprint, as Configuration#fingerprint gives it.
|
|
16
|
+
def self.operands(paths, fingerprint)
|
|
17
|
+
Merge.operands(paths.map { |path| read(path, fingerprint) })
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The method paths of the JSON reports at paths, merged.
|
|
21
|
+
def self.method_paths(paths)
|
|
22
|
+
merged(paths, 'method_paths')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The paths taken of the JSON reports at paths, as
|
|
26
|
+
# Render.run takes them: site_paths:, each site's possible
|
|
27
|
+
# paths, and outside:, those a treatment rules out, each
|
|
28
|
+
# merged.
|
|
29
|
+
def self.taken(paths)
|
|
30
|
+
site_paths = Merge.paths(paths.map { |path| paths_of(path) })
|
|
31
|
+
{ site_paths:, outside: merged(paths, 'outside') }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Raises unless json, the JSON report at path, measured the
|
|
35
|
+
# kinds a render does, with fingerprint; one that measured
|
|
36
|
+
# others, or names none, lists its sites' paths differently,
|
|
37
|
+
# so its counts cannot be trusted to the paths a render
|
|
38
|
+
# lists.
|
|
39
|
+
def self.same_kinds(path, json, fingerprint)
|
|
40
|
+
kinds = json['kinds'] == Kinds.names
|
|
41
|
+
return if kinds && json.slice(*FINGERPRINT) == fingerprint
|
|
42
|
+
|
|
43
|
+
raise ArgumentError, "#{path} measured other kinds"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The key lists of the JSON reports at paths, as listed_of
|
|
47
|
+
# gives them, each path's count the sum of every report's.
|
|
48
|
+
def self.merged(paths, key)
|
|
49
|
+
Merge.method_paths(paths.map { |path| listed_of(path, key) })
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The JSON report at path's list under key, eg its method
|
|
53
|
+
# paths as the recorder counts them, { site => { path =>
|
|
54
|
+
# count } }; none for a report that has none, eg from a run
|
|
55
|
+
# that did not measure methods, so the render claims no
|
|
56
|
+
# path taken.
|
|
57
|
+
def self.listed_of(path, key)
|
|
58
|
+
json = JSON.parse(File.read(path))
|
|
59
|
+
json.fetch(key, {}).transform_values do |taken|
|
|
60
|
+
taken.to_h { |each| [each['path'], each['count']] }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The site paths of the JSON report at path, as
|
|
65
|
+
# Result#paths gives them; none for a report that has none.
|
|
66
|
+
def self.paths_of(path)
|
|
67
|
+
json = JSON.parse(File.read(path))
|
|
68
|
+
json.fetch('paths', {}).transform_values do |site|
|
|
69
|
+
site['taken'].map do |each|
|
|
70
|
+
{ path: each['path'], count: each['count'] }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The operands of the JSON report at path, which raises
|
|
76
|
+
# unless it measured the kinds a render does, with
|
|
77
|
+
# fingerprint.
|
|
78
|
+
def self.read(path, fingerprint)
|
|
79
|
+
json = JSON.parse(File.read(path))
|
|
80
|
+
same_kinds(path, json, fingerprint)
|
|
81
|
+
json['sites'].transform_values do |leaves|
|
|
82
|
+
leaves.map { |leaf| counts(leaf) }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# One operand's counts as JSON holds them, with the keys,
|
|
87
|
+
# and any only: value, as the symbols Result#operands uses.
|
|
88
|
+
def self.counts(leaf)
|
|
89
|
+
counts = { truthy: leaf['truthy'], falsy: leaf['falsy'] }
|
|
90
|
+
only = leaf['only']
|
|
91
|
+
only ? counts.merge(only: only.to_sym) : counts
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -1,25 +1,57 @@
|
|
|
1
1
|
require 'json'
|
|
2
|
+
require_relative 'kinds'
|
|
2
3
|
|
|
3
4
|
module PathCoverage
|
|
4
5
|
# The base reporter: the result as raw JSON, which every
|
|
5
6
|
# other reporter can read back.
|
|
6
7
|
module JsonWriter
|
|
7
8
|
# Writes operands, as Result gives them, paths, as
|
|
8
|
-
# PathsReport gives them,
|
|
9
|
-
# them,
|
|
10
|
-
# {
|
|
9
|
+
# PathsReport gives them, totals, as Kernel.totals gives
|
|
10
|
+
# them, and method_paths, as the recorder counts them, to
|
|
11
|
+
# path as {"sites": {site: [counts, ...]}, "paths":
|
|
12
|
+
# {site: {leaves:, taken:}}, "method_paths": {site:
|
|
13
|
+
# [{path:, count:}, ...]}, "totals": {"decisions": [taken,
|
|
11
14
|
# possible]}}.
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
# parts are json's.
|
|
16
|
+
def self.write(operands, path, paths = {}, **parts)
|
|
17
|
+
File.write(path, json(operands, paths, **parts))
|
|
14
18
|
end
|
|
15
19
|
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
# The keys of parts listed as { site => [{path:, count:}] }:
|
|
21
|
+
# each method's paths taken, and each path taken that a
|
|
22
|
+
# treatment rules out, as Result#outside gives them.
|
|
23
|
+
LISTED = %i[method_paths outside].freeze
|
|
24
|
+
|
|
25
|
+
# operands, and any paths, and parts' method_paths:,
|
|
26
|
+
# outside: and totals:, as the JSON report's text, with
|
|
27
|
+
# parts' fingerprint:, as Configuration#fingerprint gives
|
|
28
|
+
# it, and the names of the kinds measured.
|
|
29
|
+
def self.json(operands, paths = {}, **parts)
|
|
30
|
+
fingerprint = parts.fetch(:fingerprint, {})
|
|
31
|
+
report = { 'sites' => operands, **fingerprint }
|
|
20
32
|
report['paths'] = paths unless paths.empty?
|
|
33
|
+
report.merge!(lists(parts))
|
|
34
|
+
totals = parts.fetch(:totals, {})
|
|
21
35
|
report['totals'] = totals unless totals.empty?
|
|
36
|
+
report['kinds'] = Kinds.names
|
|
22
37
|
JSON.pretty_generate(report)
|
|
23
38
|
end
|
|
39
|
+
|
|
40
|
+
# { key => its list } of each of parts' LISTED keys that
|
|
41
|
+
# has one, as listed gives them.
|
|
42
|
+
def self.lists(parts)
|
|
43
|
+
LISTED.filter_map do |key|
|
|
44
|
+
taken = parts.fetch(key, {})
|
|
45
|
+
[key.to_s, listed(taken)] unless taken.empty?
|
|
46
|
+
end.to_h
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# taken, { site => { path => count } }, as JSON lists it,
|
|
50
|
+
# eg { 'k.rb:2:2' => [{ path: [true, false], count: 1 }] }.
|
|
51
|
+
def self.listed(taken)
|
|
52
|
+
taken.transform_values do |counts|
|
|
53
|
+
counts.map { |path, count| { path:, count: } }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
24
56
|
end
|
|
25
57
|
end
|
data/lib/path_coverage/kernel.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'fileutils'
|
|
2
2
|
require_relative 'calls'
|
|
3
3
|
require_relative 'configuration'
|
|
4
|
+
require_relative 'console_summary'
|
|
4
5
|
require_relative 'json_writer'
|
|
5
6
|
require_relative 'manifest'
|
|
6
7
|
require_relative 'path_gaps'
|
|
@@ -10,7 +11,6 @@ require_relative 'recorder'
|
|
|
10
11
|
require_relative 'render'
|
|
11
12
|
require_relative 'result'
|
|
12
13
|
require_relative 'rewriter'
|
|
13
|
-
require_relative 'totals'
|
|
14
14
|
|
|
15
15
|
# Path coverage for Ruby. This file is the kernel: it loads
|
|
16
16
|
# before any covered file and is never itself instrumented, since
|
|
@@ -48,18 +48,26 @@ module PathCoverage
|
|
|
48
48
|
# Starts from settings in env alone, for a run whose code
|
|
49
49
|
# cannot be changed, eg one started by RUBYOPT=-r: covers
|
|
50
50
|
# the comma-separated PATH_COVERAGE_COVER globs under
|
|
51
|
-
# PATH_COVERAGE_ROOT,
|
|
52
|
-
#
|
|
51
|
+
# PATH_COVERAGE_ROOT, skipping any matching the
|
|
52
|
+
# comma-separated PATH_COVERAGE_SKIP globs, writing the JSON
|
|
53
|
+
# report into PATH_COVERAGE_DIR and POSTing it to
|
|
53
54
|
# PATH_COVERAGE_COLLECTOR. Without a collector it
|
|
54
55
|
# starts nothing, so RUBYOPT can be set for every run.
|
|
55
56
|
def self.start_from_env(env, on_exit: method(:at_exit))
|
|
56
57
|
return unless env.key?('PATH_COVERAGE_COLLECTOR')
|
|
57
58
|
|
|
58
|
-
start(on_exit
|
|
59
|
+
start(on_exit:, &env_settings(env))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The start block of the settings in env, as start_from_env
|
|
63
|
+
# reads them.
|
|
64
|
+
def self.env_settings(env)
|
|
65
|
+
covers = env.fetch('PATH_COVERAGE_COVER').split(',')
|
|
66
|
+
skips = env.fetch('PATH_COVERAGE_SKIP', '').split(',')
|
|
67
|
+
proc do
|
|
59
68
|
root env.fetch('PATH_COVERAGE_ROOT')
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
end
|
|
69
|
+
covers.each { |glob| cover glob }
|
|
70
|
+
skips.each { |glob| skip glob }
|
|
63
71
|
coverage_dir env.fetch('PATH_COVERAGE_DIR')
|
|
64
72
|
collector env.fetch('PATH_COVERAGE_COLLECTOR')
|
|
65
73
|
report :post
|
|
@@ -105,9 +113,7 @@ module PathCoverage
|
|
|
105
113
|
result = collect(files, config)
|
|
106
114
|
methods = config.measures.include?(:methods)
|
|
107
115
|
paths = methods ? @recorder.paths : nil
|
|
108
|
-
Render.run(
|
|
109
|
-
result.operands, files, config, paths, site_paths: result.paths
|
|
110
|
-
)
|
|
116
|
+
Render.run(result.operands, files, config, paths, **result.taken)
|
|
111
117
|
return unless config.reporters.include?(:gaps)
|
|
112
118
|
|
|
113
119
|
methods = path_methods_of(files, config)
|
|
@@ -130,23 +136,39 @@ module PathCoverage
|
|
|
130
136
|
sites = merged_sites(files)
|
|
131
137
|
result = Result.new(sites, @recorder.counts, @recorder.site_paths)
|
|
132
138
|
FileUtils.mkdir_p(config.output_dir)
|
|
139
|
+
write_json(result, files, config)
|
|
140
|
+
result
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Writes result as the JSON report at config's json_path: its
|
|
144
|
+
# operands, site paths, paths a treatment rules out, and
|
|
145
|
+
# totals, and the method paths the recorder counted, so a
|
|
146
|
+
# render from the JSON alone has them, and config's
|
|
147
|
+
# fingerprint.
|
|
148
|
+
def self.write_json(result, files, config)
|
|
133
149
|
report = PathsReport.of(result.paths, files)
|
|
134
150
|
totals = totals(result, files, config)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
JsonWriter.write(result.operands, config.json_path, report,
|
|
152
|
+
totals:, method_paths: @recorder.paths,
|
|
153
|
+
outside: result.outside,
|
|
154
|
+
fingerprint: config.fingerprint)
|
|
138
155
|
end
|
|
139
156
|
|
|
140
157
|
# The JSON report's totals, each [taken, possible] paths:
|
|
141
158
|
# within each decision, and, when config measures methods,
|
|
142
|
-
#
|
|
159
|
+
# within each method too; with groups configured, each group's
|
|
160
|
+
# own too, under groups.
|
|
143
161
|
def self.totals(result, files, config)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
totals
|
|
162
|
+
measured = config.measures.include?(:methods)
|
|
163
|
+
methods = measured ? path_methods_of(files, config) : {}
|
|
164
|
+
paths = measured ? @recorder.paths : nil
|
|
165
|
+
totals = ConsoleSummary.totals(result.paths, methods, paths)
|
|
166
|
+
groups = config.groups
|
|
167
|
+
return totals if groups.empty?
|
|
168
|
+
|
|
169
|
+
summary = ConsoleSummary
|
|
170
|
+
own = summary.group_totals(result.paths, methods, paths, groups)
|
|
171
|
+
totals.merge('groups' => own)
|
|
150
172
|
end
|
|
151
173
|
|
|
152
174
|
# { full path => {source:, sites:} } for every covered file:
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module PathCoverage
|
|
2
|
+
module Kinds
|
|
3
|
+
# The hooks every kind has, each as most kinds need it; a
|
|
4
|
+
# kind extends Base and defines its own in place of any.
|
|
5
|
+
module Base
|
|
6
|
+
# The ways a site of this kind as a whole can be evaluated:
|
|
7
|
+
# as it can be nested in another.
|
|
8
|
+
def root_ways(node, fixed)
|
|
9
|
+
ways(node, fixed)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Whether its leaves, nested in a site, need calls of their
|
|
13
|
+
# own in place of record calls; false for most.
|
|
14
|
+
def own_calls?
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The inserts for a site of this kind: its leaves' records,
|
|
19
|
+
# and its leave around them.
|
|
20
|
+
def site_inserts(site, condition)
|
|
21
|
+
Rewriter.record_inserts(site, condition) +
|
|
22
|
+
Rewriter.trail_inserts(site, condition)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# What a site of this kind's leave call wraps: the whole of
|
|
26
|
+
# it, for most.
|
|
27
|
+
def trailed(condition)
|
|
28
|
+
condition
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Its node in a site's tree, each leaf taking the next index
|
|
32
|
+
# of leaves: its operands' nodes, then its arms', if any.
|
|
33
|
+
def node(source, condition, outcomes, leaves)
|
|
34
|
+
built = [outcomes, leaves]
|
|
35
|
+
operands = SiteTree.operands_of(source, condition, *built)
|
|
36
|
+
SiteTree.arms(source, condition, operands, *built)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The nodes its node stands for among its siblings' in a
|
|
40
|
+
# site's tree on a file's page: itself, for most.
|
|
41
|
+
def unfold(node)
|
|
42
|
+
[node]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The inserts, besides its leaves' calls, for one nested in
|
|
46
|
+
# a site at its at-th leaf: none for most.
|
|
47
|
+
def inserts(_site, _inner, _at, _commands)
|
|
48
|
+
[]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# nodes, the tree nodes of condition's operands, with any
|
|
52
|
+
# texts that repeat others cut: none for most.
|
|
53
|
+
def texted(_source, _condition, nodes)
|
|
54
|
+
nodes
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The text a site of this kind opens and closes with, as
|
|
58
|
+
# [opening, closing], eg ['{', '}'] for a block's braces.
|
|
59
|
+
# Most kinds have none, so ['', ''].
|
|
60
|
+
def brackets(_source, _site)
|
|
61
|
+
['', '']
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Its operands' rows in a site's tree on a file's page, nil
|
|
65
|
+
# for most, whose operands branch from its operator.
|
|
66
|
+
def rows(_condition, _indent)
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|