path_coverage 0.0.1 → 0.0.2
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/LICENSE.md +22 -0
- data/README.md +316 -0
- data/exe/path_coverage_collector +26 -0
- data/lib/path_coverage/auto.rb +5 -0
- data/lib/path_coverage/collector.rb +46 -0
- data/lib/path_coverage/condition.rb +45 -0
- data/lib/path_coverage/configuration.rb +127 -0
- data/lib/path_coverage/gaps.rb +60 -0
- data/lib/path_coverage/html/file.html +33 -0
- data/lib/path_coverage/html/index.html +38 -0
- data/lib/path_coverage/html/side.js +531 -0
- data/lib/path_coverage/html/sort.js +28 -0
- data/lib/path_coverage/html/style.css +334 -0
- data/lib/path_coverage/html/toggle.js +12 -0
- data/lib/path_coverage/html_file.rb +109 -0
- data/lib/path_coverage/html_index.rb +43 -0
- data/lib/path_coverage/html_nav.rb +67 -0
- data/lib/path_coverage/html_paths.rb +138 -0
- data/lib/path_coverage/html_report.rb +103 -0
- data/lib/path_coverage/html_template.rb +20 -0
- data/lib/path_coverage/html_tree.rb +137 -0
- data/lib/path_coverage/json_writer.rb +21 -0
- data/lib/path_coverage/kernel.rb +179 -0
- data/lib/path_coverage/manifest.rb +177 -0
- data/lib/path_coverage/manifest_choices.rb +113 -0
- data/lib/path_coverage/merge.rb +35 -0
- data/lib/path_coverage/path_gaps.rb +41 -0
- data/lib/path_coverage/path_manifest.rb +110 -0
- data/lib/path_coverage/paths_report.rb +55 -0
- data/lib/path_coverage/post.rb +26 -0
- data/lib/path_coverage/recorder.rb +99 -0
- data/lib/path_coverage/render.rb +92 -0
- data/lib/path_coverage/result.rb +59 -0
- data/lib/path_coverage/rewriter.rb +120 -0
- data/lib/path_coverage/site.rb +11 -0
- data/lib/path_coverage/site_paths.rb +74 -0
- data/lib/path_coverage/site_tree.rb +94 -0
- data/lib/path_coverage/source_lines.rb +79 -0
- data/lib/path_coverage/summary.rb +33 -0
- data/lib/path_coverage/version.rb +3 -0
- data/lib/path_coverage.rb +2 -5
- metadata +68 -13
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/* Blue (seen both ways) and orange (missing) stay distinct for
|
|
2
|
+
red-green colour blindness; states also differ in form. */
|
|
3
|
+
:root {
|
|
4
|
+
--bg: #FBFBFC;
|
|
5
|
+
--surface: #FFFFFF;
|
|
6
|
+
--panel: #F2F4F7;
|
|
7
|
+
--fg: #1C2230;
|
|
8
|
+
--muted: #5B6577;
|
|
9
|
+
--rule: #D5DAE2;
|
|
10
|
+
--ok: #1F5FBF;
|
|
11
|
+
--ok-tint: #E6EEFA;
|
|
12
|
+
--warn: #A35200;
|
|
13
|
+
--bad: #B54708;
|
|
14
|
+
--on-status: #FFFFFF;
|
|
15
|
+
--link: #1F5FBF;
|
|
16
|
+
/* The tree is inverted from the page, so it never reads as
|
|
17
|
+
more source lines. */
|
|
18
|
+
--tree-bg: #1E2638;
|
|
19
|
+
--tree-fg: #E8ECF4;
|
|
20
|
+
--tree-muted: #A3AEC2;
|
|
21
|
+
--tree-rule: #3A4560;
|
|
22
|
+
--tree-gap: #F5A25D;
|
|
23
|
+
--font-body: "Helvetica Neue", Arial, sans-serif;
|
|
24
|
+
--font-code: Menlo, Consolas, "DejaVu Sans Mono", monospace;
|
|
25
|
+
}
|
|
26
|
+
@media (prefers-color-scheme: dark) {
|
|
27
|
+
:root {
|
|
28
|
+
--bg: #12151C; --surface: #181C25; --panel: #212633; --fg: #E4E8EF;
|
|
29
|
+
--muted: #9AA4B5; --rule: #323949; --ok: #7FB0FF; --ok-tint: #1A2740;
|
|
30
|
+
--warn: #F0A35E; --bad: #F08A3C; --on-status: #12151C; --link: #7FB0FF;
|
|
31
|
+
--tree-bg: #E6EAF2; --tree-fg: #1C2230; --tree-muted: #4E5869;
|
|
32
|
+
--tree-rule: #BCC4D2; --tree-gap: #A8480A;
|
|
33
|
+
color-scheme: dark;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
body { margin: 0; background: var(--bg); color: var(--fg); font-family: var(--font-body); font-size: 14px; }
|
|
37
|
+
a { color: var(--link); }
|
|
38
|
+
.page {
|
|
39
|
+
max-width: 1080px; margin: 0 auto; display: flex; flex-direction: column; gap: 24px;
|
|
40
|
+
padding: 40px max(16px, env(safe-area-inset-right, 0px)) 64px max(16px, env(safe-area-inset-left, 0px));
|
|
41
|
+
}
|
|
42
|
+
h1 { margin: 0; font-size: 24px; font-weight: 600; }
|
|
43
|
+
.box { border: 1px solid var(--rule); border-radius: 8px; background: var(--surface); overflow-x: auto; }
|
|
44
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; font-variant-numeric: tabular-nums; }
|
|
45
|
+
th, td { padding: 10px 16px; text-align: left; }
|
|
46
|
+
th { background: var(--panel); }
|
|
47
|
+
td { border-top: 1px solid var(--rule); }
|
|
48
|
+
td:first-child { font-family: var(--font-code); }
|
|
49
|
+
.num, td:not(:first-child) { text-align: right; }
|
|
50
|
+
th button {
|
|
51
|
+
font: 600 13px var(--font-body); color: var(--fg); background: none; border: 0;
|
|
52
|
+
padding: 4px 0; cursor: pointer;
|
|
53
|
+
}
|
|
54
|
+
th button::after { content: " \2195"; color: var(--muted); }
|
|
55
|
+
th[aria-sort="ascending"] button::after { content: " \25B2"; color: var(--fg); }
|
|
56
|
+
th[aria-sort="descending"] button::after { content: " \25BC"; color: var(--fg); }
|
|
57
|
+
th button:focus-visible { outline: 3px solid var(--link); outline-offset: 2px; }
|
|
58
|
+
|
|
59
|
+
/* A file's page: plain source; on lines holding a condition the
|
|
60
|
+
line numbers are one button. Every number has the same box, so
|
|
61
|
+
digits stay aligned whether or not they are on a button. */
|
|
62
|
+
.back { margin: 0; font-size: 13px; }
|
|
63
|
+
h1.path { font-family: var(--font-code); font-size: 18px; }
|
|
64
|
+
/* The back link, path and site lists stick together as one
|
|
65
|
+
header. */
|
|
66
|
+
.head {
|
|
67
|
+
position: sticky; top: 0; z-index: 5;
|
|
68
|
+
display: flex; flex-direction: column; gap: 10px;
|
|
69
|
+
margin: 0 -16px; padding: 12px 16px; background: var(--bg);
|
|
70
|
+
border-bottom: 1px solid var(--rule);
|
|
71
|
+
}
|
|
72
|
+
.jumps { display: flex; flex-direction: column; gap: 8px; }
|
|
73
|
+
.jump-list { display: flex; gap: 6px; align-items: center; overflow-x: auto; white-space: nowrap; min-width: 0; }
|
|
74
|
+
.jump-label { width: 64px; flex-shrink: 0; font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
|
|
75
|
+
a.jump {
|
|
76
|
+
font: 600 12px/22px var(--font-code); padding: 0 9px; border-radius: 999px;
|
|
77
|
+
text-decoration: none; border: 1px solid; font-variant-numeric: tabular-nums;
|
|
78
|
+
}
|
|
79
|
+
/* A site's jump wears the condition tint, edged in its status
|
|
80
|
+
colour on the top and left only, as its line's button is. */
|
|
81
|
+
a.jump[data-status] { background: var(--cond-tint); border-right-color: transparent; border-bottom-color: transparent; }
|
|
82
|
+
a.jump[data-status="covered"] { color: var(--ok); border-top-color: var(--ok); border-left-color: var(--ok); }
|
|
83
|
+
a.jump[data-status="gaps"] { color: var(--bad); border-top-color: var(--bad); border-left-color: var(--bad); }
|
|
84
|
+
a.jump:focus-visible, .nav a:focus-visible { outline: 3px solid var(--link); outline-offset: 2px; }
|
|
85
|
+
/* The source pane's border and background start after the nav
|
|
86
|
+
column, so the arrows sit outside the pane yet scroll with
|
|
87
|
+
their line. */
|
|
88
|
+
.box:has(> .code) { position: relative; isolation: isolate; border: 0; background: none; border-radius: 0; }
|
|
89
|
+
.box:has(> .code)::before {
|
|
90
|
+
content: ""; position: absolute; inset: 0 0 0 26px; z-index: -1;
|
|
91
|
+
border: 1px solid var(--rule); border-radius: 8px;
|
|
92
|
+
background: var(--surface); pointer-events: none;
|
|
93
|
+
}
|
|
94
|
+
/* The previous-gap arrow sits above the next, each half a line
|
|
95
|
+
tall, keeping the gutter narrow. */
|
|
96
|
+
.nav { display: flex; flex-direction: column; justify-content: center; width: 26px; flex-shrink: 0; }
|
|
97
|
+
.nav a { width: 22px; text-align: center; line-height: 14px; font-size: 12px; color: var(--bad); text-decoration: none; }
|
|
98
|
+
.nav a:hover { color: var(--fg); }
|
|
99
|
+
/* Clears the sticky header when a link jumps to a line. */
|
|
100
|
+
.lineno { scroll-margin-top: 190px; }
|
|
101
|
+
button.lnum:has(.lineno:target) { outline: 3px solid var(--link); outline-offset: 1px; }
|
|
102
|
+
.code { font-family: var(--font-code); font-size: 13px; line-height: 32px; padding: 10px 0; min-width: 720px; }
|
|
103
|
+
.line, .block { display: flex; align-items: flex-start; }
|
|
104
|
+
.srcs { display: flex; flex-direction: column; }
|
|
105
|
+
.src { white-space: pre; }
|
|
106
|
+
.lineno {
|
|
107
|
+
display: block; width: 52px; padding-right: 20px; text-align: right;
|
|
108
|
+
color: var(--muted); font-variant-numeric: tabular-nums; flex-shrink: 0;
|
|
109
|
+
}
|
|
110
|
+
button.lnum {
|
|
111
|
+
display: flex; flex-direction: column; flex-shrink: 0; margin: 4px 8px 4px 0; padding: 0;
|
|
112
|
+
border: 1px solid transparent; border-radius: 6px; cursor: pointer;
|
|
113
|
+
font: 600 13px/24px var(--font-code); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.22);
|
|
114
|
+
}
|
|
115
|
+
button.lnum { position: relative; }
|
|
116
|
+
button.lnum::before {
|
|
117
|
+
content: "\25B8"; position: absolute; left: 7px; top: 4px;
|
|
118
|
+
font-size: 11px; line-height: 24px; transition: transform 120ms;
|
|
119
|
+
}
|
|
120
|
+
button.lnum[aria-expanded="true"]::before { transform: rotate(90deg); }
|
|
121
|
+
/* With a site count the count and the triangle share the
|
|
122
|
+
button's height, one 11px line each. */
|
|
123
|
+
button.lnum:has(.site-count)::before { top: 12px; line-height: 11px; }
|
|
124
|
+
/* On a button over two or more lines the triangle sits at the
|
|
125
|
+
very bottom. */
|
|
126
|
+
button.lnum:has(.lineno + .lineno)::before { top: auto; bottom: 2px; line-height: 11px; }
|
|
127
|
+
.site-count {
|
|
128
|
+
position: absolute; left: 7px; top: 1px;
|
|
129
|
+
font-size: 9px; line-height: 11px; font-weight: 700;
|
|
130
|
+
}
|
|
131
|
+
/* The rule between sites spans the tree's full width, through
|
|
132
|
+
its 16px side padding. */
|
|
133
|
+
.site + .site {
|
|
134
|
+
border-top: 1px solid var(--tree-rule); margin: 6px -16px 0; padding: 6px 16px 0;
|
|
135
|
+
}
|
|
136
|
+
button.lnum .lineno { width: 50px; padding-right: 11px; color: inherit; line-height: 32px; margin: -4px 0; }
|
|
137
|
+
/* A site's button wears the condition tint, edged in its
|
|
138
|
+
status colour on the top and left only, as a method's bar is. */
|
|
139
|
+
button.lnum[data-status] { box-shadow: none; background: var(--cond-tint); border-right-color: transparent; border-bottom-color: transparent; }
|
|
140
|
+
button.lnum[data-status="covered"] { color: var(--ok); border-top-color: var(--ok); border-left-color: var(--ok); }
|
|
141
|
+
button.lnum[data-status="gaps"] { color: var(--bad); border-top-color: var(--bad); border-left-color: var(--bad); }
|
|
142
|
+
button.lnum:hover { filter: brightness(1.1); }
|
|
143
|
+
button.lnum:focus-visible { outline: 3px solid var(--link); outline-offset: 1px; }
|
|
144
|
+
.tree {
|
|
145
|
+
margin: 4px 20px 12px 120px; padding: 10px 16px; border: 1px solid var(--tree-rule);
|
|
146
|
+
border-radius: 8px; background: var(--tree-bg); color: var(--tree-fg); line-height: 30px;
|
|
147
|
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
|
|
148
|
+
}
|
|
149
|
+
/* A row is its branch and label, then one fixed-width column per
|
|
150
|
+
path: flowing by column, each cell after the label takes the
|
|
151
|
+
next path's column, however many paths the site has. */
|
|
152
|
+
.row {
|
|
153
|
+
display: grid; grid-template-columns: auto minmax(200px, 1fr);
|
|
154
|
+
grid-auto-flow: column; grid-auto-columns: 32px; column-gap: 16px;
|
|
155
|
+
}
|
|
156
|
+
.operator { grid-column: span 2; font-weight: 600; }
|
|
157
|
+
.row[data-subtree] .operator { grid-column: auto; }
|
|
158
|
+
.row .arm { white-space: pre; font-weight: 600; }
|
|
159
|
+
.branch { white-space: pre; color: var(--tree-muted); margin-right: -16px; }
|
|
160
|
+
.row .text { white-space: pre; }
|
|
161
|
+
/* A branch glyph drawn as a cell a whole row tall: v a vertical
|
|
162
|
+
line, t a vertical with a stub right, l the top half of a
|
|
163
|
+
vertical with a stub right, h a horizontal, s nothing. */
|
|
164
|
+
.bx { display: inline-block; width: 1ch; height: 30px; vertical-align: top; --stroke: linear-gradient(currentColor, currentColor); }
|
|
165
|
+
.bx[data-bx="v"] { background: var(--stroke) 50% 0 / 1px 100% no-repeat; }
|
|
166
|
+
.bx[data-bx="t"] { background: var(--stroke) 50% 0 / 1px 100% no-repeat, var(--stroke) 100% 50% / 50% 1px no-repeat; }
|
|
167
|
+
.bx[data-bx="l"] { background: var(--stroke) 50% 0 / 1px 50% no-repeat, var(--stroke) 100% 50% / 50% 1px no-repeat; }
|
|
168
|
+
.bx[data-bx="h"] { background: var(--stroke) 0 50% / 100% 1px no-repeat; }
|
|
169
|
+
/* Each path's count heads its column, orange for a path never
|
|
170
|
+
taken; a leaf's T or F on that path sits in a chip, as in a
|
|
171
|
+
path grid, and a blank means the path never reached it. */
|
|
172
|
+
.row .count { text-align: center; font-variant-numeric: tabular-nums; color: var(--ok); font-weight: 600; }
|
|
173
|
+
.row .count.never { color: var(--tree-gap); }
|
|
174
|
+
.row .cell { text-align: center; }
|
|
175
|
+
.row .cell:not(:empty) {
|
|
176
|
+
align-self: center; line-height: 22px; border-radius: 3px;
|
|
177
|
+
background: var(--surface); color: var(--fg); font: 600 12px var(--font-code);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* SPIKE: the code and a rail share the box; each tree sits in
|
|
181
|
+
the rail, level with its site (its top set by side.js). */
|
|
182
|
+
body[data-layout="beside"] .page { max-width: 1800px; }
|
|
183
|
+
/* The code sits between two rails, condition trees on the left
|
|
184
|
+
and path grids on the right, each as wide as --cc-rail or
|
|
185
|
+
--pc-rail. Both are 0 until one opens; registered, so they
|
|
186
|
+
animate, sliding the code and its frame aside together. */
|
|
187
|
+
@property --cc-rail { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
188
|
+
@property --pc-rail { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
189
|
+
body[data-layout="beside"] { transition: --cc-rail 250ms ease, --pc-rail 250ms ease; }
|
|
190
|
+
body[data-open="cc"] { --cc-rail: 560px; }
|
|
191
|
+
body[data-open="pc"] { --pc-rail: 560px; }
|
|
192
|
+
body[data-layout="beside"] .box:has(> .code) {
|
|
193
|
+
display: grid; column-gap: 12px;
|
|
194
|
+
grid-template-columns: var(--cc-rail) minmax(0, 1fr) var(--pc-rail);
|
|
195
|
+
}
|
|
196
|
+
/* Every row is as wide as the longest line (or the pane, if
|
|
197
|
+
wider), so a site's band and outline, and a lit line, reach
|
|
198
|
+
past the code when it scrolls sideways. */
|
|
199
|
+
body[data-layout="beside"] .code {
|
|
200
|
+
min-width: 0; overflow-x: auto;
|
|
201
|
+
display: grid; grid-template-columns: minmax(max-content, 1fr);
|
|
202
|
+
}
|
|
203
|
+
body[data-layout="beside"] .box:has(> .code)::before {
|
|
204
|
+
left: calc(var(--cc-rail) + 38px); right: calc(var(--pc-rail) + 12px);
|
|
205
|
+
}
|
|
206
|
+
.rail { position: relative; min-width: 0; }
|
|
207
|
+
/* The rail's tree sits on the condition tint, in the page's own
|
|
208
|
+
text colours, as a path grid sits on the path tint. */
|
|
209
|
+
.rail .tree {
|
|
210
|
+
--tree-bg: var(--cond-tint); --tree-fg: var(--fg); --tree-muted: var(--muted);
|
|
211
|
+
--tree-rule: var(--rule); --tree-gap: var(--bad);
|
|
212
|
+
}
|
|
213
|
+
/* The rail's tree has narrower columns, to fit its width; a
|
|
214
|
+
thin rule under each site's counts row, across the tree's
|
|
215
|
+
full width through its 16px side padding. */
|
|
216
|
+
.rail .row { grid-template-columns: auto minmax(120px, 1fr); grid-auto-columns: 26px; column-gap: 8px; }
|
|
217
|
+
.rail .site > .row:first-child {
|
|
218
|
+
border-bottom: 1px solid var(--tree-rule); margin: 0 -16px 6px; padding: 0 16px 4px;
|
|
219
|
+
}
|
|
220
|
+
.rail .tree { position: absolute; left: 0; right: 0; margin: 0; z-index: 2; font-family: var(--font-code); font-size: 13px; }
|
|
221
|
+
/* Beside, a site's whole block, its lines' code included, is
|
|
222
|
+
tinted by status and opens its tree, so each tree lines up
|
|
223
|
+
with a band the eye can follow across. */
|
|
224
|
+
:root { --bad-tint: #FBE6D6; }
|
|
225
|
+
@media (prefers-color-scheme: dark) { :root { --bad-tint: #3A2416; } }
|
|
226
|
+
/* Everything a site's grid draws has this tint, as everything
|
|
227
|
+
a method's grid draws has --path-tint; status is only ever
|
|
228
|
+
the orange or blue of an edge. */
|
|
229
|
+
:root { --cond-tint: #E2F2D3; }
|
|
230
|
+
@media (prefers-color-scheme: dark) { :root { --cond-tint: #26361A; } }
|
|
231
|
+
body[data-layout="beside"] .block { cursor: pointer; border-radius: 6px; }
|
|
232
|
+
/* A site's own characters are tinted, not its whole lines: the
|
|
233
|
+
expression is what its grid is about. */
|
|
234
|
+
/* The padding stretches the tint to the full 32px line, as tall
|
|
235
|
+
as a method's band, without moving the text. */
|
|
236
|
+
mark.site { color: inherit; border-radius: 3px; padding: 8px 0; }
|
|
237
|
+
mark.site { background: var(--cond-tint); }
|
|
238
|
+
mark.site[data-status="gaps"] { box-shadow: inset 0 -1px 0 var(--bad); }
|
|
239
|
+
mark.site[data-status="covered"] { box-shadow: inset 0 -1px 0 var(--ok); }
|
|
240
|
+
/* An open site, like an open method's bar, has its status edges
|
|
241
|
+
doubled to 2px: its button's top and left, and its
|
|
242
|
+
expressions' underlines. */
|
|
243
|
+
body[data-layout="beside"] button.lnum[aria-expanded="true"][data-status="gaps"] { box-shadow: inset 1px 1px 0 var(--bad); }
|
|
244
|
+
body[data-layout="beside"] button.lnum[aria-expanded="true"][data-status="covered"] { box-shadow: inset 1px 1px 0 var(--ok); }
|
|
245
|
+
body[data-layout="beside"] .block:has(> button.lnum[aria-expanded="true"]) mark.site[data-status="gaps"] { box-shadow: inset 0 -2px 0 var(--bad); }
|
|
246
|
+
body[data-layout="beside"] .block:has(> button.lnum[aria-expanded="true"]) mark.site[data-status="covered"] { box-shadow: inset 0 -2px 0 var(--ok); }
|
|
247
|
+
/* Beside, a jump to a site opens its tree, whose doubled edges
|
|
248
|
+
mark it, so its button has no outline of the jump to it; and
|
|
249
|
+
while a method's paths are open, none keeps a focus outline. */
|
|
250
|
+
body[data-layout="beside"] button.lnum:has(.lineno:target) { outline: none; }
|
|
251
|
+
body[data-open="pc"] button.lnum:focus-visible { outline: none; }
|
|
252
|
+
body[data-layout="beside"] .block .srcs { flex: 1; }
|
|
253
|
+
body[data-layout="beside"] .block:hover { filter: brightness(0.97); }
|
|
254
|
+
/* Beside, an open site's triangle points left, showing that a
|
|
255
|
+
click closes its tree, rather than down. */
|
|
256
|
+
/* A site's tree opens in the left rail: a closed button points
|
|
257
|
+
left, where its tree will open, an open one right, the way it
|
|
258
|
+
will close. */
|
|
259
|
+
body[data-layout="beside"] button.lnum::before { content: "\25C2"; transform: none; }
|
|
260
|
+
body[data-layout="beside"] button.lnum[aria-expanded="true"]::before { content: "\25B8"; transform: none; }
|
|
261
|
+
|
|
262
|
+
/* SPIKE: a path method's bar on the code pane's right edge,
|
|
263
|
+
def to end, which opens its grid out into the rail: a column
|
|
264
|
+
per path, never-taken first, each row level with its line. */
|
|
265
|
+
.pathbar {
|
|
266
|
+
position: absolute; width: 16px; padding: 0; z-index: 3;
|
|
267
|
+
border: 0; border-radius: 4px; cursor: pointer;
|
|
268
|
+
display: flex; align-items: center; justify-content: center;
|
|
269
|
+
color: var(--on-status); font-size: 11px;
|
|
270
|
+
}
|
|
271
|
+
.pathbar::after { content: "\25B8"; }
|
|
272
|
+
.pathbar[aria-expanded="true"]::after { content: "\25C2"; }
|
|
273
|
+
/* A method's def line, lit from its def to its bar, under the
|
|
274
|
+
code's text; the code is its own stacking context so the band
|
|
275
|
+
can sit beneath its rows. */
|
|
276
|
+
body[data-layout="beside"] .code { position: relative; isolation: isolate; }
|
|
277
|
+
.methodband { position: absolute; z-index: -1; border-radius: 6px 0 0 6px; pointer-events: none; }
|
|
278
|
+
/* A method's band is violet, whatever its status, so it never
|
|
279
|
+
reads as a site's band; its status is its bar's colour and its
|
|
280
|
+
bottom line's. */
|
|
281
|
+
:root { --path-tint: #E9E0FB; }
|
|
282
|
+
@media (prefers-color-scheme: dark) { :root { --path-tint: #33284F; } }
|
|
283
|
+
.methodband { background: var(--path-tint); }
|
|
284
|
+
.methodband[data-status="gaps"] { box-shadow: inset 0 1px 0 var(--bad); }
|
|
285
|
+
.methodband[data-status="covered"] { box-shadow: inset 0 1px 0 var(--ok); }
|
|
286
|
+
.line:has(.lineno) { cursor: default; }
|
|
287
|
+
/* A bar is violet like its method's band, so violet always means
|
|
288
|
+
paths; its status is a line down its right edge, as the band's
|
|
289
|
+
is a line along its top. */
|
|
290
|
+
.pathbar {
|
|
291
|
+
background: var(--path-tint); color: var(--fg);
|
|
292
|
+
border-radius: 0 0 4px 4px; align-items: flex-start; line-height: 32px;
|
|
293
|
+
}
|
|
294
|
+
.pathbar[data-status="gaps"] { box-shadow: inset -1px 0 0 var(--bad), inset 0 1px 0 var(--bad); }
|
|
295
|
+
.pathbar[data-status="covered"] { box-shadow: inset -1px 0 0 var(--ok), inset 0 1px 0 var(--ok); }
|
|
296
|
+
/* Open, a method's status lines thicken, on its bar and band. */
|
|
297
|
+
.pathbar[aria-expanded="true"][data-status="gaps"] { box-shadow: inset -2px 0 0 var(--bad), inset 0 2px 0 var(--bad); }
|
|
298
|
+
.pathbar[aria-expanded="true"][data-status="covered"] { box-shadow: inset -2px 0 0 var(--ok), inset 0 2px 0 var(--ok); }
|
|
299
|
+
.methodband.open[data-status="gaps"] { box-shadow: inset 0 2px 0 var(--bad); }
|
|
300
|
+
.methodband.open[data-status="covered"] { box-shadow: inset 0 2px 0 var(--ok); }
|
|
301
|
+
.pathbar:hover { filter: brightness(1.15); }
|
|
302
|
+
.rail .pathgrid {
|
|
303
|
+
position: absolute; left: 0; right: 0; z-index: 2; overflow-x: auto; overflow-y: hidden;
|
|
304
|
+
background: var(--path-tint); border-radius: 6px; font: 600 12px var(--font-code);
|
|
305
|
+
}
|
|
306
|
+
.prow { position: absolute; left: 0; display: flex; gap: 2px; white-space: nowrap; }
|
|
307
|
+
.plabel { width: 48px; flex-shrink: 0; text-align: right; padding-right: 8px; color: var(--muted); line-height: 24px; }
|
|
308
|
+
.pcell { width: 24px; flex-shrink: 0; text-align: center; line-height: 24px; border-radius: 3px; }
|
|
309
|
+
.pcell.ended { background: transparent; }
|
|
310
|
+
.pcell.true, .pcell.false { background: var(--surface); color: var(--fg); }
|
|
311
|
+
.prow.calls .pcell.taken { background: var(--ok-tint); color: var(--ok); }
|
|
312
|
+
.prow.calls .pcell.never { background: var(--bad); color: var(--on-status); }
|
|
313
|
+
.prow.calls .plabel { color: var(--fg); }
|
|
314
|
+
/* SPIKE: hovering a path's column shows it in the code: the
|
|
315
|
+
column outlined, skipped lines faded, each decision's line
|
|
316
|
+
chipped with its outcome. */
|
|
317
|
+
.pcell { cursor: default; }
|
|
318
|
+
.pcell { cursor: pointer; }
|
|
319
|
+
.pcell.hot { outline: 2px solid var(--fg); outline-offset: -1px; }
|
|
320
|
+
.pcell.pinned { outline: 3px solid var(--link); outline-offset: -2px; }
|
|
321
|
+
:root { --lit: #FFE98A; }
|
|
322
|
+
@media (prefers-color-scheme: dark) { :root { --lit: #5A4A12; } }
|
|
323
|
+
/* A lit line's background spans the code pane's full width. */
|
|
324
|
+
.line .src { flex: 1; }
|
|
325
|
+
.src.lit { background: var(--path-tint); }
|
|
326
|
+
mark.chars { background: var(--lit); color: inherit; border-radius: 3px; box-shadow: 0 0 0 2px var(--lit); }
|
|
327
|
+
.tree .row[data-at] { cursor: default; }
|
|
328
|
+
.tree .row[data-at]:hover { background: rgba(255, 255, 255, 0.08); }
|
|
329
|
+
.nav[data-chip] { flex-direction: row; align-items: center; justify-content: flex-end; padding-right: 2px; box-sizing: border-box; }
|
|
330
|
+
.nav[data-chip] a { display: none; }
|
|
331
|
+
.nav[data-chip]::after {
|
|
332
|
+
content: attr(data-chip); padding: 0 7px; border-radius: 4px;
|
|
333
|
+
font: 700 11px/20px var(--font-body); background: var(--fg); color: var(--bg);
|
|
334
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Opens and closes the tree a line-number button names in its
|
|
2
|
+
// aria-controls, keeping aria-expanded in step.
|
|
3
|
+
(function () {
|
|
4
|
+
document.querySelectorAll('button[aria-controls]').forEach(function (button) {
|
|
5
|
+
button.addEventListener('click', function () {
|
|
6
|
+
var tree = document.getElementById(button.getAttribute('aria-controls'));
|
|
7
|
+
var open = button.getAttribute('aria-expanded') === 'true';
|
|
8
|
+
button.setAttribute('aria-expanded', open ? 'false' : 'true');
|
|
9
|
+
tree.hidden = open;
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
})();
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require 'cgi/escape'
|
|
2
|
+
require_relative 'html_nav'
|
|
3
|
+
require_relative 'html_template'
|
|
4
|
+
require_relative 'html_tree'
|
|
5
|
+
|
|
6
|
+
module PathCoverage
|
|
7
|
+
# One covered file's HTML page: its source, with the numbers
|
|
8
|
+
# of the lines holding a condition as the one button that
|
|
9
|
+
# opens the condition's tree.
|
|
10
|
+
module HtmlFile
|
|
11
|
+
# The whole page for the file at path (relative to root),
|
|
12
|
+
# written at files/<path>.html, so it links back to the
|
|
13
|
+
# index one folder up for each folder in that.
|
|
14
|
+
def self.page(path, lines, trees, grids = '')
|
|
15
|
+
HtmlTemplate.fill(
|
|
16
|
+
HtmlTemplate.asset('file.html'),
|
|
17
|
+
'path' => CGI.escapeHTML(path),
|
|
18
|
+
'index' => "#{'../' * (path.count('/') + 1)}index.html",
|
|
19
|
+
'style' => HtmlTemplate.asset('style.css'),
|
|
20
|
+
'script' => script,
|
|
21
|
+
'grids' => grids,
|
|
22
|
+
'jumps' => HtmlNav.jumps(lines),
|
|
23
|
+
'listing' => listing(lines, trees)
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The page's script: its tree toggles, then its side rails.
|
|
28
|
+
def self.script
|
|
29
|
+
HtmlTemplate.asset('toggle.js') + HtmlTemplate.asset('side.js')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The source listing of lines, as SourceLines.of gives them:
|
|
33
|
+
# each run of lines sharing a span is one block, followed
|
|
34
|
+
# by its hidden tree. trees is { span's first line =>
|
|
35
|
+
# [SiteTree.of, ...] }.
|
|
36
|
+
def self.listing(lines, trees = {})
|
|
37
|
+
blocks = lines.chunk_while do |line, following|
|
|
38
|
+
line[:span] && line[:span] == following[:span]
|
|
39
|
+
end
|
|
40
|
+
gaps = HtmlNav.gap_lines(blocks)
|
|
41
|
+
body = blocks.map { |block| block(block, trees, gaps) }
|
|
42
|
+
%(<div class="code">\n#{body.join("\n")}\n</div>)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A plain line, or the lines of one span behind one button
|
|
46
|
+
# with the span's tree straight after them; either way after
|
|
47
|
+
# its nav cell.
|
|
48
|
+
def self.block(lines, trees, gaps)
|
|
49
|
+
first = lines[0]
|
|
50
|
+
number = first[:number]
|
|
51
|
+
cell = HtmlNav.cell(number, gaps)
|
|
52
|
+
return plain_line(first, cell) if first[:span].nil?
|
|
53
|
+
|
|
54
|
+
site_trees = trees.fetch(number, [])
|
|
55
|
+
open = first[:status] == :gaps
|
|
56
|
+
span_block(lines, site_trees.size, cell) +
|
|
57
|
+
HtmlTree.of(number, site_trees, open)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The lines of one span after their nav cell: one button over
|
|
61
|
+
# all their numbers, and how many sites it opens when that is
|
|
62
|
+
# two or more, beside their code.
|
|
63
|
+
def self.span_block(lines, sites, cell)
|
|
64
|
+
numbers = lines.map { |line| number(line[:number]) }.join
|
|
65
|
+
codes = lines.map { |line| code(line) }.join
|
|
66
|
+
srcs = %(<div class="srcs">#{codes}</div>)
|
|
67
|
+
content = site_count(sites) + numbers
|
|
68
|
+
button = button(lines[0], content)
|
|
69
|
+
%(<div class="block">#{cell}#{button}#{srcs}</div>)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The count of a button's sites, shown only for two or more.
|
|
73
|
+
def self.site_count(sites)
|
|
74
|
+
return '' if sites < 2
|
|
75
|
+
|
|
76
|
+
%(<span class="site-count" data-site-count="">#{sites}</span>)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A line with no condition: its nav cell, number and code.
|
|
80
|
+
def self.plain_line(line, cell)
|
|
81
|
+
parts = cell + number(line[:number]) + code(line)
|
|
82
|
+
%(<div class="line">#{parts}</div>)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The button over a span's numbers, toggling the tree named
|
|
86
|
+
# after its first line; expanded when that tree starts open.
|
|
87
|
+
def self.button(first, numbers)
|
|
88
|
+
expanded = first[:status] == :gaps
|
|
89
|
+
[
|
|
90
|
+
%(<button type="button" class="lnum"),
|
|
91
|
+
%( data-status="#{first[:status]}"),
|
|
92
|
+
%( aria-expanded="#{expanded}"),
|
|
93
|
+
%( aria-controls="tree-#{first[:number]}">#{numbers}</button>)
|
|
94
|
+
].join
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# One line's number, the target of a link to #L<number>.
|
|
98
|
+
def self.number(number)
|
|
99
|
+
attributes = %(id="L#{number}" data-line="#{number}")
|
|
100
|
+
%(<span class="lineno" #{attributes}>#{number}</span>)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# One line's code, escaped.
|
|
104
|
+
def self.code(line)
|
|
105
|
+
text = CGI.escapeHTML(line[:text])
|
|
106
|
+
%(<span class="src" data-src="">#{text}</span>)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'cgi/escape'
|
|
2
|
+
require_relative 'html_template'
|
|
3
|
+
|
|
4
|
+
module PathCoverage
|
|
5
|
+
# The HTML index page: one sortable row per covered file,
|
|
6
|
+
# each linking to that file's own page.
|
|
7
|
+
module HtmlIndex
|
|
8
|
+
# The whole index page: the files table, sortable by any
|
|
9
|
+
# column, with one row per Summary.files row.
|
|
10
|
+
def self.page(rows)
|
|
11
|
+
HtmlTemplate.fill(
|
|
12
|
+
HtmlTemplate.asset('index.html'),
|
|
13
|
+
'style' => HtmlTemplate.asset('style.css'),
|
|
14
|
+
'script' => HtmlTemplate.asset('sort.js'),
|
|
15
|
+
'rows' => rows.map { |row| row(row) }.join("\n")
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The table row of one Summary.files row; each cell has a
|
|
20
|
+
# data-sort value for the page's column sorting.
|
|
21
|
+
def self.row(row)
|
|
22
|
+
"<tr>#{cells(row).join}</tr>"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The four cells: file, sites, outcomes seen, missing.
|
|
26
|
+
def self.cells(row)
|
|
27
|
+
path = CGI.escapeHTML(row[:path])
|
|
28
|
+
link = %(<a href="files/#{path}.html">#{path}</a>)
|
|
29
|
+
seen = "#{row[:seen]} / #{row[:outcomes]} · #{row[:percent]}%"
|
|
30
|
+
[
|
|
31
|
+
cell(path, link),
|
|
32
|
+
cell(row[:sites], row[:sites]),
|
|
33
|
+
cell(row[:percent], seen),
|
|
34
|
+
cell(row[:missing], row[:missing])
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# One table cell, sorted by sort_value, showing content.
|
|
39
|
+
def self.cell(sort_value, content)
|
|
40
|
+
%(<td data-sort="#{sort_value}">#{content}</td>)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module PathCoverage
|
|
2
|
+
# The ways around a file's page: the header's lists of links to
|
|
3
|
+
# each span, and the arrows between the spans with gaps.
|
|
4
|
+
module HtmlNav
|
|
5
|
+
JUMP_LISTS = { 'covered' => 'Covered', 'gaps' => 'Gaps' }.freeze
|
|
6
|
+
|
|
7
|
+
# The header's lists of links to the first line of each span:
|
|
8
|
+
# those covered, then those with gaps.
|
|
9
|
+
def self.jumps(lines)
|
|
10
|
+
firsts = lines.select { |line| first_of_span?(line) }
|
|
11
|
+
lists = JUMP_LISTS.map do |status, label|
|
|
12
|
+
spans = firsts.select { |line| line[:status].to_s == status }
|
|
13
|
+
jump_list(status, label, spans.map { |line| line[:number] })
|
|
14
|
+
end
|
|
15
|
+
%(<nav class="jumps" aria-label="Sites">#{lists.join}</nav>)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# True for the line a span starts on.
|
|
19
|
+
def self.first_of_span?(line)
|
|
20
|
+
line[:span]&.begin == line[:number]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# One labelled list of links to lines.
|
|
24
|
+
def self.jump_list(status, label, numbers)
|
|
25
|
+
links = numbers.map { |number| jump(status, number) }
|
|
26
|
+
name = %(<span class="jump-label">#{label}</span>)
|
|
27
|
+
list = %(<div class="jump-list" data-jumps="#{status}">)
|
|
28
|
+
"#{list}#{name}#{links.join}</div>"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# One link to a line.
|
|
32
|
+
def self.jump(status, number)
|
|
33
|
+
attributes = %(data-status="#{status}" href="#L#{number}")
|
|
34
|
+
%(<a class="jump" #{attributes}>#{number}</a>)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The first line of each span whose sites have gaps, given the
|
|
38
|
+
# listing's blocks.
|
|
39
|
+
def self.gap_lines(blocks)
|
|
40
|
+
firsts = blocks.map(&:first)
|
|
41
|
+
firsts.select { |line| line[:status] == :gaps }.map do |line|
|
|
42
|
+
line[:number]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The cell left of a row's number: at a span with gaps, links
|
|
47
|
+
# to the previous and next such span, wrapping round at the
|
|
48
|
+
# ends; empty on every other row, so the numbers stay aligned.
|
|
49
|
+
def self.cell(number, gaps)
|
|
50
|
+
index = gaps.index(number)
|
|
51
|
+
links = index ? arrows(gaps, index) : ''
|
|
52
|
+
%(<span class="nav" data-nav="">#{links}</span>)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The links to the spans with gaps either side of gaps[index].
|
|
56
|
+
def self.arrows(gaps, index)
|
|
57
|
+
previous = gaps[index - 1]
|
|
58
|
+
following = gaps[(index + 1) % gaps.size]
|
|
59
|
+
[
|
|
60
|
+
%(<a data-prev="" href="#L#{previous}"),
|
|
61
|
+
%( aria-label="Previous gap, line #{previous}">▲</a>),
|
|
62
|
+
%(<a data-next="" href="#L#{following}"),
|
|
63
|
+
%( aria-label="Next gap, line #{following}">▼</a>)
|
|
64
|
+
].join
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|