path_coverage 0.0.2 → 0.1.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 +57 -20
- data/exe/path_coverage_collector +12 -1
- data/lib/path_coverage/calls.rb +67 -0
- data/lib/path_coverage/collector.rb +88 -22
- data/lib/path_coverage/condition.rb +54 -3
- data/lib/path_coverage/configuration.rb +9 -6
- data/lib/path_coverage/html/index.html +5 -2
- data/lib/path_coverage/html/side.js +15 -4
- data/lib/path_coverage/html/sort.js +4 -1
- data/lib/path_coverage/html/style.css +4 -0
- data/lib/path_coverage/html_index.rb +48 -17
- data/lib/path_coverage/html_report.rb +18 -3
- data/lib/path_coverage/html_tree.rb +11 -2
- data/lib/path_coverage/json_writer.rb +11 -7
- data/lib/path_coverage/kernel.rb +21 -35
- data/lib/path_coverage/manifest.rb +40 -12
- data/lib/path_coverage/manifest_blocks.rb +57 -0
- data/lib/path_coverage/manifest_case.rb +54 -0
- data/lib/path_coverage/manifest_choices.rb +37 -18
- data/lib/path_coverage/recorder.rb +83 -0
- data/lib/path_coverage/rewriter.rb +53 -7
- data/lib/path_coverage/site.rb +6 -1
- data/lib/path_coverage/site_paths.rb +21 -9
- data/lib/path_coverage/site_tree.rb +50 -10
- data/lib/path_coverage/summary.rb +36 -17
- data/lib/path_coverage/totals.rb +29 -0
- data/lib/path_coverage/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7146cc322217826aadc2bd040b560b5e77d6fad7b6de4d2ae43e8bdfaa893940
|
|
4
|
+
data.tar.gz: 29135bab12a1cc92d1a15dd8d383d3e79d75b3b8716fff7efa95fbbb6f8c3b34
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4ebf32a5b048eb5d3d1b1cbbca0e2ed9c217e429481d1bd77a242f0a3baff83ee7b83a2b4a4cd99f6a6611885d1c165a56b6bd6f814be28c4cb6a92cffbc8282
|
|
7
|
+
data.tar.gz: 9ee6e1f2aea70b34ffb38ec5fa16a52f92ae5daf0a680727903db858fcdb557c244d0ae61a048e1a7bc09d99128c0b7068dd408686b2aca683b9f037afe3f6f3
|
data/README.md
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
# path_coverage
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/path_coverage)
|
|
4
|
+
[](https://rubygems.org/gems/path_coverage)
|
|
5
|
+
[](LICENSE.md)
|
|
6
|
+
|
|
3
7
|
Path coverage for Ruby, to run alongside SimpleCov's line and branch
|
|
4
8
|
coverage.
|
|
5
9
|
|
|
6
10
|
Gem: https://rubygems.org/gems/path_coverage (which shows the latest
|
|
7
11
|
version).
|
|
8
12
|
|
|
9
|
-
Status: sites cover `&&`, `||`, `and`, `or`, the ternary `?:`,
|
|
10
|
-
`if`/`elsif`/`else
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
Status: sites cover `&&`, `||`, `and`, `or`, the ternary `?:`,
|
|
14
|
+
`if`/`elsif`/`else` and `unless` (modifiers included), `case`/`when`
|
|
15
|
+
with a subject, and the block methods `any?` `all?` `none?` `find`
|
|
16
|
+
`detect` `find_index` `take_while` `select` `filter` `reject`
|
|
17
|
+
`partition` `count` `filter_map`. Method paths cover `if` decisions.
|
|
18
|
+
More decisions hidden inside calls come next (see Roadmap).
|
|
13
19
|
|
|
14
20
|
Demo: https://cyber-dojo.github.io/path_coverage/ shows the HTML report
|
|
15
21
|
for the mock source tree in `demo/`. `make demo` builds and opens the
|
|
@@ -65,7 +71,7 @@ PathCoverage.start do
|
|
|
65
71
|
root APP_DIR # cover globs and site names are relative to it
|
|
66
72
|
cover 'source/**/*.rb' # each matching file is instrumented as it loads
|
|
67
73
|
coverage_dir '/tmp/coverage' # where path_coverage.json is written
|
|
68
|
-
measure :
|
|
74
|
+
measure :decisions, :methods # :decisions alone unless :methods is named
|
|
69
75
|
report :gaps, :html # the reporters run at exit (default :gaps)
|
|
70
76
|
end
|
|
71
77
|
```
|
|
@@ -93,9 +99,18 @@ each report POSTed to it as a numbered file, and once every run is done
|
|
|
93
99
|
they render as one report:
|
|
94
100
|
|
|
95
101
|
```sh
|
|
96
|
-
path_coverage_collector 4570 /tmp/paths/runs # until
|
|
102
|
+
path_coverage_collector 4570 /tmp/paths/runs # until sent TERM
|
|
97
103
|
```
|
|
98
104
|
|
|
105
|
+
- A report is saved before it is answered 200, so a 200 means it is on
|
|
106
|
+
disk. A report cut short, eg by a sender dying mid-send, is answered
|
|
107
|
+
400 and not saved. One that cannot be saved is answered 500.
|
|
108
|
+
- `GET /alive` answers 200 while the collector runs.
|
|
109
|
+
- `GET /ready` answers 200 when its dir is writable, 503 when not. Wait
|
|
110
|
+
for it before starting the runs.
|
|
111
|
+
- On TERM it stops accepting connections, finishes the report in hand,
|
|
112
|
+
and exits 0.
|
|
113
|
+
|
|
99
114
|
```ruby
|
|
100
115
|
runs = Dir['/tmp/paths/runs/*.json']
|
|
101
116
|
PathCoverage::Render.from_json(config, runs)
|
|
@@ -124,7 +139,9 @@ source/kata.rb:4:24 never reached
|
|
|
124
139
|
source/fee.rb:2:2 fee path never taken: line 4 true, line 7 false
|
|
125
140
|
```
|
|
126
141
|
|
|
127
|
-
###
|
|
142
|
+
### Decisions
|
|
143
|
+
|
|
144
|
+
Measured always, as `:decisions`: the paths within each decision.
|
|
128
145
|
|
|
129
146
|
A site is named by the `path:line:column` where it starts, the path
|
|
130
147
|
relative to `root`. A site in a covered file the run never loaded is
|
|
@@ -157,7 +174,7 @@ across the whole of it, nested conditions included:
|
|
|
157
174
|
ok?(id) && new?(id) && id.size == 6 # one && site, leaves 0 1 2
|
|
158
175
|
ok?(id) && (new?(id) || id.size == 6) # an || nested in an && site, 0 1 2
|
|
159
176
|
ok?(id) && new?(id) || id.size == 6 # an && nested in an || site, 0 1 2
|
|
160
|
-
ids.
|
|
177
|
+
ids.map { |k| k && k.ok? } && ok?(id) # two sites: the block's is its own
|
|
161
178
|
ok?(id) ? :ok : :no # a ?: site, its predicate leaf 0
|
|
162
179
|
ok?(id) && new?(id) ? :ok : :no # an && nested in a ?: site, 0 1
|
|
163
180
|
ok?(id) ? new?(id) && id.size == 6 : :no # one ?: site, its then arm 1 2
|
|
@@ -170,12 +187,30 @@ paths run through whichever arm the predicate picks. Any other arm, like a
|
|
|
170
187
|
block, is not part of it, though a condition inside it is a site of its
|
|
171
188
|
own.
|
|
172
189
|
|
|
173
|
-
An `if`
|
|
174
|
-
predicate is a single leaf: its predicate nests in it, an `&&` there
|
|
190
|
+
An `if` or an `unless`, a modifier included, is a site too, even when
|
|
191
|
+
its predicate is a single leaf: its predicate nests in it, an `&&` there
|
|
175
192
|
being no site of its own, and its `then` and `else` arms are plain, as
|
|
176
|
-
they are statements
|
|
177
|
-
|
|
178
|
-
|
|
193
|
+
they are statements; an `unless` takes its `then` arm when its predicate
|
|
194
|
+
is false. It is named at its keyword, and its extent is its keyword to
|
|
195
|
+
the end of its predicate, so a site inside an arm keeps its own button.
|
|
196
|
+
An `elsif` is a site of its own.
|
|
197
|
+
|
|
198
|
+
A `case` with a subject is a site whose leaves are its `when` patterns,
|
|
199
|
+
tried in order until one matches. Each pattern is wrapped in a stand-in
|
|
200
|
+
whose `===` calls the pattern's `===` once, exactly as the `case` would,
|
|
201
|
+
and notes whether it matched. A `case` with no subject, or with a splat
|
|
202
|
+
among its patterns, is no site.
|
|
203
|
+
|
|
204
|
+
A call to a block method, with no arguments and a block of one
|
|
205
|
+
expression, is a site named at its method. Its two leaves are its
|
|
206
|
+
receiver, showing how many items its block was called with (`0 items`,
|
|
207
|
+
`1 item` or `2+ items`), and its block's expression: for `any?` `all?`
|
|
208
|
+
`none?` `find` `detect` `find_index` and `take_while`, which stop once
|
|
209
|
+
their block decides, its last answer; for `select` `filter` `reject`
|
|
210
|
+
`partition` `count` and `filter_map`, which call it with every item,
|
|
211
|
+
whether its answers were `all true`, `all false` or `both`. Both are
|
|
212
|
+
noted from the block's own calls, so the receiver is never asked for its
|
|
213
|
+
size, which could consume a sequence that can be read only once.
|
|
179
214
|
|
|
180
215
|
A chain of one operator is flat; a change of operator, by precedence or by
|
|
181
216
|
parentheses, nests. Parentheses are not part of the tree, since they decide
|
|
@@ -195,7 +230,7 @@ constants, since one can hold nil or false (`VERBOSE = false`). `nil` and
|
|
|
195
230
|
|
|
196
231
|
### Methods
|
|
197
232
|
|
|
198
|
-
With `measure :
|
|
233
|
+
With `measure :decisions, :methods`, each method with two or more
|
|
199
234
|
decisions also counts each call's path through them: the outcome of each
|
|
200
235
|
decision it reached, in order. A method is named by the `path:line:column`
|
|
201
236
|
of its `def`, then its name. Each decision is one outcome on the path,
|
|
@@ -207,8 +242,11 @@ leaves the method ends the path there.
|
|
|
207
242
|
### Reporters
|
|
208
243
|
|
|
209
244
|
- `:html` writes `index.html` to `coverage_dir`: the covered files with
|
|
210
|
-
their sites
|
|
211
|
-
|
|
245
|
+
their sites and their paths taken of all possible, within each decision
|
|
246
|
+
and, when `:methods` is measured, across a method's decisions (eg
|
|
247
|
+
`9 / 28 (32%)`, or `-` for a file with no method of two or more
|
|
248
|
+
decisions), each column sortable, and a Total row for every file
|
|
249
|
+
together. Each file name opens `files/<path>.html`, the file's
|
|
212
250
|
source. On a line holding a site the line number is a button (blue
|
|
213
251
|
and outlined when every outcome was seen, solid orange when any is
|
|
214
252
|
missing) that opens a grid of the site's paths, on the left. Its rows
|
|
@@ -275,16 +313,15 @@ the run never loaded are read and parsed then.
|
|
|
275
313
|
## Roadmap
|
|
276
314
|
|
|
277
315
|
- decisions hidden inside calls, each with its own outcome classes:
|
|
278
|
-
- `
|
|
279
|
-
|
|
280
|
-
- `find` / `detect`: found vs nil
|
|
316
|
+
- `one?`, whose last answer does not decide it, and block methods
|
|
317
|
+
with blocks of more than one expression
|
|
281
318
|
- `fetch(k, default)`, `dig`, `Hash#[]` with a default: present vs default
|
|
282
319
|
- `x || default` in value context, `&.` safe navigation: nil vs not nil
|
|
283
320
|
- `||=` and `&&=`, on variables, attributes and index targets: the
|
|
284
321
|
right side evaluated vs skipped, counted without evaluating the
|
|
285
322
|
target twice
|
|
286
323
|
- `x rescue y` modifier: raised vs not raised
|
|
287
|
-
- `
|
|
324
|
+
- `case` with no subject, and `case`/`in` patterns
|
|
288
325
|
- regex matches: nil vs MatchData
|
|
289
326
|
|
|
290
327
|
## Constraints
|
data/exe/path_coverage_collector
CHANGED
|
@@ -9,7 +9,13 @@ if ARGV.first == '-h'
|
|
|
9
9
|
|
|
10
10
|
Listens on PORT and saves each JSON report the :post reporter
|
|
11
11
|
sends as DIR/run-0001.json, DIR/run-0002.json, and so on,
|
|
12
|
-
creating DIR if need be.
|
|
12
|
+
creating DIR if need be. A 200 means the report is saved.
|
|
13
|
+
|
|
14
|
+
GET /alive answers 200 while it runs.
|
|
15
|
+
GET /ready answers 200 when DIR is writable, 503 when not.
|
|
16
|
+
|
|
17
|
+
Runs until sent TERM. It then stops accepting connections,
|
|
18
|
+
finishes the report in hand, and exits 0.
|
|
13
19
|
|
|
14
20
|
Options:
|
|
15
21
|
-h Show this help
|
|
@@ -23,4 +29,9 @@ end
|
|
|
23
29
|
port, dir = ARGV
|
|
24
30
|
FileUtils.mkdir_p(dir)
|
|
25
31
|
server = TCPServer.new('0.0.0.0', Integer(port))
|
|
32
|
+
# Closing the server on TERM makes serve return once the
|
|
33
|
+
# report in hand, if any, is saved and answered. The close
|
|
34
|
+
# runs in a thread of its own, as the trap runs on the thread
|
|
35
|
+
# that may be waiting in accept, where a close deadlocks.
|
|
36
|
+
Signal.trap('TERM') { Thread.new { server.close } }
|
|
26
37
|
PathCoverage::Collector.serve(server, dir)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# The calls rewritten code makes, each handed to the kernel's
|
|
2
|
+
# one recorder. Like the kernel, this file loads before any
|
|
3
|
+
# covered file and is never itself instrumented.
|
|
4
|
+
module PathCoverage
|
|
5
|
+
# The call rewritten code makes around each operand; returns
|
|
6
|
+
# value unchanged.
|
|
7
|
+
def self.record(site, index, value)
|
|
8
|
+
@recorder.record(site, index, value)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# The call rewritten code makes before a site's first leaf,
|
|
12
|
+
# site having size leaves; returns the evaluation's trail.
|
|
13
|
+
def self.enter(site, size)
|
|
14
|
+
@recorder.enter(site, size)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The call rewritten code makes around a site as a whole,
|
|
18
|
+
# with the trail enter gave it; returns value unchanged.
|
|
19
|
+
def self.leave(trail, value)
|
|
20
|
+
@recorder.leave(trail, value)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The call rewritten code makes around a case's pattern, the
|
|
24
|
+
# index-th of size at site; returns the pattern's stand-in.
|
|
25
|
+
def self.when_match(site, index, size, pattern)
|
|
26
|
+
@recorder.when_match(site, index, size, pattern)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The call rewritten code makes before a block method's call
|
|
30
|
+
# at site runs; returns the evaluation's trail.
|
|
31
|
+
def self.enter_items(site)
|
|
32
|
+
@recorder.enter_items(site)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The call rewritten code makes around a block method's
|
|
36
|
+
# block's expression at site; returns value unchanged.
|
|
37
|
+
def self.item(site, value)
|
|
38
|
+
@recorder.item(site, value)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The call rewritten code makes around the block's expression
|
|
42
|
+
# of a block method that calls it with every item; returns
|
|
43
|
+
# value unchanged.
|
|
44
|
+
def self.answer(site, value)
|
|
45
|
+
@recorder.answer(site, value)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The call rewritten code makes around a block method's call
|
|
49
|
+
# as a whole, with the trail enter_items gave it; returns
|
|
50
|
+
# value unchanged.
|
|
51
|
+
def self.leave_items(trail, value)
|
|
52
|
+
@recorder.leave_items(trail, value)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The call rewritten code makes as a call reaches its
|
|
56
|
+
# method's first decision; returns the call's new path.
|
|
57
|
+
def self.path(site, size)
|
|
58
|
+
@recorder.path(site, size)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The call rewritten code makes around each decision's
|
|
62
|
+
# predicate, with exits for one whose arm leaves the method;
|
|
63
|
+
# returns value unchanged.
|
|
64
|
+
def self.decide(path, index, value, exits = nil)
|
|
65
|
+
@recorder.decide(path, index, value, exits)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -3,44 +3,110 @@ module PathCoverage
|
|
|
3
3
|
# runs that sent them, eg failing docker build steps, can
|
|
4
4
|
# be merged and rendered once they are all done.
|
|
5
5
|
module Collector
|
|
6
|
-
# Answers every request on server
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# Answers every request on server until server is closed,
|
|
7
|
+
# then returns.
|
|
8
|
+
# Each report is saved in dir as run-0001.json,
|
|
9
|
+
# run-0002.json, and so on, in the order they arrive.
|
|
10
|
+
# A connection sending no request is skipped.
|
|
10
11
|
def self.serve(server, dir)
|
|
11
12
|
n = 0
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
n += 1
|
|
17
|
-
File.write(format('%<dir>s/run-%<n>04d.json', dir:, n:), body)
|
|
13
|
+
while (client = accept(server))
|
|
14
|
+
saved = receive(client, dir) do |body|
|
|
15
|
+
save(dir, format('run-%<n>04d.json', n: n + 1), body)
|
|
16
|
+
end
|
|
17
|
+
n += 1 if saved
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
# The
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
# The next connection to server; nil once server is closed,
|
|
22
|
+
# which is how serve is told to return.
|
|
23
|
+
def self.accept(server)
|
|
24
|
+
server.accept
|
|
25
|
+
rescue IOError
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Saves body as dir/name whole or not at all: it is written
|
|
30
|
+
# to name.tmp and renamed, so a collector stopped mid-write
|
|
31
|
+
# leaves no partial report under a run's name.
|
|
32
|
+
def self.save(dir, name, body)
|
|
33
|
+
path = "#{dir}/#{name}"
|
|
34
|
+
File.write("#{path}.tmp", body)
|
|
35
|
+
File.rename("#{path}.tmp", path)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Hands the body of the one HTTP request on client to the
|
|
39
|
+
# block, which saves it, and returns whether it was saved.
|
|
40
|
+
# dir is where reports are saved, which GET /ready checks.
|
|
41
|
+
# A client that closes without sending a request, eg a
|
|
42
|
+
# check that the collector is up, is closed unanswered and
|
|
43
|
+
# nothing is saved.
|
|
44
|
+
def self.receive(client, dir, &)
|
|
25
45
|
headers = []
|
|
26
46
|
while (line = client.gets) && line != "\r\n"
|
|
27
47
|
headers << line
|
|
28
48
|
end
|
|
29
|
-
|
|
49
|
+
saved = !headers.empty? && respond(client, headers, dir, &)
|
|
30
50
|
client.close
|
|
31
|
-
|
|
51
|
+
saved
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The check each GET path makes of the collector's dir.
|
|
55
|
+
# GET /alive passes whenever the collector answers.
|
|
56
|
+
# GET /ready passes only when dir is a writable directory,
|
|
57
|
+
# so a report sent now could be saved.
|
|
58
|
+
PROBES = {
|
|
59
|
+
'/alive' => ->(_dir) { true },
|
|
60
|
+
'/ready' => lambda do |dir|
|
|
61
|
+
File.directory?(dir) && File.writable?(dir)
|
|
62
|
+
end
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
# Answers the request with headers on client, and returns
|
|
66
|
+
# whether a report was saved in dir.
|
|
67
|
+
# A GET of a path in PROBES is answered 200 when its check
|
|
68
|
+
# passes, 503 when not. Nothing is saved for it.
|
|
69
|
+
# Any other request is a report, handed to answer.
|
|
70
|
+
def self.respond(client, headers, dir, &)
|
|
71
|
+
probe = PROBES[headers.first[/\AGET (\S+) /, 1]]
|
|
72
|
+
return answer(client, headers, &) if probe.nil?
|
|
73
|
+
|
|
74
|
+
up = probe.call(dir)
|
|
75
|
+
reply(client, up ? '200 OK' : '503 Service Unavailable')
|
|
76
|
+
false
|
|
32
77
|
end
|
|
33
78
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
79
|
+
# Reads the body of a request with headers from client and
|
|
80
|
+
# yields it to be saved. Returns whether it was saved.
|
|
81
|
+
# The 200 is sent only once it is saved.
|
|
82
|
+
# So a 200 always means the report is on disk.
|
|
83
|
+
# A body shorter than its Content-Length is answered 400.
|
|
84
|
+
# Eg its sender died mid-send. It is not saved.
|
|
85
|
+
# A save that fails is answered 500.
|
|
36
86
|
def self.answer(client, headers)
|
|
37
|
-
length = headers
|
|
87
|
+
length = content_length(headers)
|
|
88
|
+
body = client.read(length).to_s
|
|
89
|
+
whole = body.bytesize == length
|
|
90
|
+
yield body if whole
|
|
91
|
+
reply(client, whole ? '200 OK' : '400 Bad Request')
|
|
92
|
+
whole
|
|
93
|
+
rescue SystemCallError
|
|
94
|
+
reply(client, '500 Internal Server Error')
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The byte length headers give the request's body; 0 when
|
|
99
|
+
# they give none.
|
|
100
|
+
def self.content_length(headers)
|
|
101
|
+
headers.sum do |line|
|
|
38
102
|
name, value = line.split(':', 2)
|
|
39
103
|
name.casecmp?('content-length') ? value.to_i : 0
|
|
40
104
|
end
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Writes an HTTP response of status, with no body, to client.
|
|
108
|
+
def self.reply(client, status)
|
|
109
|
+
client.write("HTTP/1.1 #{status}\r\nContent-Length: 0\r\n\r\n")
|
|
44
110
|
end
|
|
45
111
|
end
|
|
46
112
|
end
|
|
@@ -1,9 +1,60 @@
|
|
|
1
1
|
module PathCoverage
|
|
2
2
|
# The operators whose predicate picks an arm.
|
|
3
|
-
CHOICES = %w[?: if].freeze
|
|
3
|
+
CHOICES = %w[?: if unless].freeze
|
|
4
4
|
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# The choices whose arms are statements, so whose path is
|
|
6
|
+
# decided by their predicate alone.
|
|
7
|
+
STATEMENT_CHOICES = %w[if unless].freeze
|
|
8
|
+
|
|
9
|
+
# The predicate outcome on which each choice takes its then
|
|
10
|
+
# arm: an unless's is false.
|
|
11
|
+
THEN_ON = Hash.new(true).merge('unless' => false).freeze
|
|
12
|
+
|
|
13
|
+
# The methods that call their block an item at a time until
|
|
14
|
+
# it decides, its last answer deciding the result.
|
|
15
|
+
STOP_EARLY = %w[
|
|
16
|
+
any? all? none? find detect find_index take_while
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
# How many items a block method's block was called with, as
|
|
20
|
+
# its receiver's leaf outcome: none, one, or two or more.
|
|
21
|
+
ITEMS = ['0 items', '1 item', '2+ items'].freeze
|
|
22
|
+
|
|
23
|
+
# Every path a stop-early block method's call can take: how
|
|
24
|
+
# many items its block was called with, and its last answer,
|
|
25
|
+
# nil when it was never called.
|
|
26
|
+
STOP_EARLY_PATHS = [
|
|
27
|
+
[ITEMS[0], nil],
|
|
28
|
+
[ITEMS[1], true], [ITEMS[1], false],
|
|
29
|
+
[ITEMS[2], true], [ITEMS[2], false]
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
# The methods that call their block with every item.
|
|
33
|
+
EVERY_ITEM = %w[
|
|
34
|
+
select filter reject partition count filter_map
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
# A block's answers over every item it was called with.
|
|
38
|
+
ANSWERS = ['all true', 'all false', 'both'].freeze
|
|
39
|
+
|
|
40
|
+
# Every path an every-item block method's call can take: how
|
|
41
|
+
# many items its block was called with, and its answers,
|
|
42
|
+
# which can be both only with two or more items.
|
|
43
|
+
EVERY_ITEM_PATHS = [
|
|
44
|
+
[ITEMS[0], nil],
|
|
45
|
+
[ITEMS[1], ANSWERS[0]], [ITEMS[1], ANSWERS[1]],
|
|
46
|
+
[ITEMS[2], ANSWERS[0]], [ITEMS[2], ANSWERS[1]],
|
|
47
|
+
[ITEMS[2], ANSWERS[2]]
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
50
|
+
# Every block method whose call is a site, by its paths.
|
|
51
|
+
BLOCK_PATHS = [
|
|
52
|
+
*STOP_EARLY.map { |name| [name, STOP_EARLY_PATHS] },
|
|
53
|
+
*EVERY_ITEM.map { |name| [name, EVERY_ITEM_PATHS] }
|
|
54
|
+
].to_h.freeze
|
|
55
|
+
|
|
56
|
+
# One arm of a ternary, an if or an unless: kind is :then or
|
|
57
|
+
# :else, extent its byte range, nil for an arm with no
|
|
7
58
|
# statements, and body the Condition it is, when it is one,
|
|
8
59
|
# else nil for a plain value or statements, which decide
|
|
9
60
|
# nothing.
|
|
@@ -9,25 +9,28 @@ module PathCoverage
|
|
|
9
9
|
collector cover coverage_dir measure report root
|
|
10
10
|
].freeze
|
|
11
11
|
REPORTERS = %w[gaps html post].freeze
|
|
12
|
-
MEASURES = %w[
|
|
12
|
+
MEASURES = %w[decisions methods].freeze
|
|
13
13
|
|
|
14
14
|
# Evaluates the settings block against a new configuration.
|
|
15
15
|
def initialize(&)
|
|
16
16
|
@globs = []
|
|
17
17
|
@reporters = [:gaps]
|
|
18
|
-
@measures = [:
|
|
18
|
+
@measures = [:decisions]
|
|
19
19
|
instance_eval(&)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# The reporters chosen with report, :gaps when none were.
|
|
23
23
|
attr_reader :reporters
|
|
24
24
|
|
|
25
|
-
# What is measured, chosen with measure
|
|
26
|
-
# nothing
|
|
25
|
+
# What is measured, chosen with measure.
|
|
26
|
+
# :decisions, the paths within each decision, when nothing
|
|
27
|
+
# was chosen.
|
|
27
28
|
attr_reader :measures
|
|
28
29
|
|
|
29
|
-
# Chooses what is measured
|
|
30
|
-
#
|
|
30
|
+
# Chooses what is measured.
|
|
31
|
+
# Eg measure :decisions, :methods also measures the paths
|
|
32
|
+
# across each method's decisions.
|
|
33
|
+
# Refuses a name there is no measure for.
|
|
31
34
|
def measure(*names)
|
|
32
35
|
check('measure', names, MEASURES)
|
|
33
36
|
@measures = names.map(&:to_sym)
|
|
@@ -19,13 +19,16 @@
|
|
|
19
19
|
<tr>
|
|
20
20
|
<th aria-sort="none"><button type="button" data-column="0" data-type="text">File</button></th>
|
|
21
21
|
<th class="num" aria-sort="none"><button type="button" data-column="1" data-type="number">Sites</button></th>
|
|
22
|
-
<th class="num" aria-sort="none"><button type="button" data-column="2" data-type="number">
|
|
23
|
-
|
|
22
|
+
<th class="num" aria-sort="none"><button type="button" data-column="2" data-type="number">Within each decision</button></th>
|
|
23
|
+
{{across}}
|
|
24
24
|
</tr>
|
|
25
25
|
</thead>
|
|
26
26
|
<tbody>
|
|
27
27
|
{{rows}}
|
|
28
28
|
</tbody>
|
|
29
|
+
<tfoot>
|
|
30
|
+
{{total}}
|
|
31
|
+
</tfoot>
|
|
29
32
|
</table>
|
|
30
33
|
</div>
|
|
31
34
|
</main>
|
|
@@ -71,19 +71,30 @@
|
|
|
71
71
|
});
|
|
72
72
|
var side = !panel ? '' : panel.classList.contains('tree') ? 'cc' : 'pc';
|
|
73
73
|
document.body.setAttribute('data-open', side);
|
|
74
|
-
slide();
|
|
74
|
+
slide(panel);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Scrolls the page just enough to show all of panel when it
|
|
78
|
+
// is a tree, eg one opened from a line near the bottom of the
|
|
79
|
+
// window, so its last rows are not below the fold. Its first
|
|
80
|
+
// row stays level with its site's line.
|
|
81
|
+
function showWhole(panel) {
|
|
82
|
+
if (panel && panel.classList.contains('tree')) {
|
|
83
|
+
panel.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
84
|
+
}
|
|
75
85
|
}
|
|
76
86
|
|
|
77
87
|
// How long a rail takes to open or close, as style.css has it.
|
|
78
88
|
var SLIDE_MS = 250;
|
|
79
89
|
|
|
80
90
|
// Places everything on every frame while the rails slide, as
|
|
81
|
-
// the bars and bands follow the code across
|
|
82
|
-
|
|
91
|
+
// the bars and bands follow the code across, then shows all
|
|
92
|
+
// of panel, the one just opened, once it is in its place.
|
|
93
|
+
function slide(panel) {
|
|
83
94
|
var end = performance.now() + SLIDE_MS + 50;
|
|
84
95
|
function frame(now) {
|
|
85
96
|
place();
|
|
86
|
-
if (now < end) { requestAnimationFrame(frame); }
|
|
97
|
+
if (now < end) { requestAnimationFrame(frame); } else { showWhole(panel); }
|
|
87
98
|
}
|
|
88
99
|
requestAnimationFrame(frame);
|
|
89
100
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Sorts the files table by the clicked column's data-sort values;
|
|
2
|
-
// a second click on the same column reverses the order.
|
|
2
|
+
// a second click on the same column reverses the order. A cell
|
|
3
|
+
// with an empty data-sort, one with no value such as a -, stays
|
|
4
|
+
// last in both orders, so either end of the values can be first.
|
|
3
5
|
(function () {
|
|
4
6
|
var table = document.getElementById('files');
|
|
5
7
|
var body = table.tBodies[0];
|
|
@@ -15,6 +17,7 @@
|
|
|
15
17
|
rows.sort(function (a, b) {
|
|
16
18
|
var x = a.cells[column].dataset.sort;
|
|
17
19
|
var y = b.cells[column].dataset.sort;
|
|
20
|
+
if (x === '' || y === '') { return (x === '') - (y === ''); }
|
|
18
21
|
var order = numeric ? Number(x) - Number(y) : x.localeCompare(y);
|
|
19
22
|
return state.ascending ? order : -order;
|
|
20
23
|
});
|
|
@@ -46,6 +46,10 @@ th, td { padding: 10px 16px; text-align: left; }
|
|
|
46
46
|
th { background: var(--panel); }
|
|
47
47
|
td { border-top: 1px solid var(--rule); }
|
|
48
48
|
td:first-child { font-family: var(--font-code); }
|
|
49
|
+
/* The Total row frames the table's foot as the headings do its
|
|
50
|
+
head, so it reads as a sum, not as one more file. */
|
|
51
|
+
tfoot td { background: var(--panel); font-weight: 600; border-top: 1px solid var(--fg); }
|
|
52
|
+
tfoot td:first-child { font-family: inherit; }
|
|
49
53
|
.num, td:not(:first-child) { text-align: right; }
|
|
50
54
|
th button {
|
|
51
55
|
font: 600 13px var(--font-body); color: var(--fg); background: none; border: 0;
|