@giamme/forge 0.11.0 → 0.12.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.
- package/CHANGELOG.md +47 -0
- package/SKILL.md +4 -2
- package/VERSION +1 -1
- package/docs/artifacts.md +46 -12
- package/docs/commands.md +48 -9
- package/docs/decomposed-runs.md +43 -2
- package/docs/forge-ui.md +27 -6
- package/docs/reviews.md +31 -6
- package/package.json +1 -1
- package/references/dataset.md +3 -1
- package/references/decompose.md +159 -17
- package/references/harnesses.md +13 -1
- package/references/ui.md +14 -4
- package/scripts/forge-control.py +268 -25
- package/scripts/forge-dispatch.sh +80 -3
- package/scripts/forge-guard.py +156 -15
- package/scripts/forge-issues.py +1 -1
- package/scripts/forge-lib-plans.sh +9 -3
- package/scripts/forge-lib-review.sh +22 -3
- package/scripts/forge-lib-task.sh +51 -4
- package/scripts/forge-lib-verify.sh +137 -6
- package/scripts/forge-parallel-options.sh +59 -0
- package/scripts/forge-parallel.sh +212 -28
- package/scripts/forge-ripwire.py +1 -1
- package/scripts/forge-route.py +157 -5
- package/scripts/forge-schedule.py +687 -50
- package/scripts/forge-ui.py +8 -4
- package/scripts/forge-verify.py +878 -0
- package/scripts/forge_dataset/forecast.py +29 -4
- package/scripts/forge_dataset/harvest.py +10 -4
- package/scripts/forge_verify_reports.py +225 -0
- package/ui/README.md +19 -4
- package/ui/docs/api.md +139 -25
- package/ui/forge_ui/control.py +26 -7
- package/ui/forge_ui/export.py +20 -7
- package/ui/forge_ui/reader.py +307 -11
- package/ui/forge_ui/server.py +16 -0
- package/ui/forge_ui/static/app.js +2 -1
- package/ui/forge_ui/static/controls.js +217 -33
- package/ui/forge_ui/static/findingsview.js +37 -12
- package/ui/forge_ui/static/gantt.js +14 -9
- package/ui/forge_ui/static/index.html +1 -0
- package/ui/forge_ui/static/panel.js +2 -1
- package/ui/forge_ui/static/runmodel.js +92 -11
- package/ui/forge_ui/static/scene3d.js +9 -5
- package/ui/forge_ui/static/settings.js +48 -12
- package/ui/forge_ui/static/style.css +60 -0
- package/ui/forge_ui/static/verifyview.js +324 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/* forge-ui verification view: what the verification runs now, its lanes, and which tests failed
|
|
2
|
+
(window.ForgeVerify, and a CommonJS module under node:test). panel.js's verification inspector delegates here.
|
|
3
|
+
The pure helpers build a view model from RunDetail.verification (ui/docs/api.md) and are tested in
|
|
4
|
+
ui/tests/js/verifyview.test.js; render(host, ctx) is thin DOM over them. Test names, messages, excerpts and log
|
|
5
|
+
lines are untrusted: they only ever enter the page as text.
|
|
6
|
+
ctx = { run, now (epoch seconds), fetchJSON(path), rerender(), hash(sel), onSelect(sel) } */
|
|
7
|
+
(function (root, factory) {
|
|
8
|
+
'use strict';
|
|
9
|
+
const api = factory(root);
|
|
10
|
+
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
11
|
+
else root.ForgeVerify = api;
|
|
12
|
+
})(typeof self !== 'undefined' ? self : this, function (root) {
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const IDLE_WARN_S = 300; // silence past this is flagged
|
|
16
|
+
const CONTEXT_LINES = 10; // lines shown above an opened log line
|
|
17
|
+
const list = v => Array.isArray(v) ? v : [];
|
|
18
|
+
const num = v => typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
19
|
+
const STEP = /^::forge step name=(\S+) state=(start|end)(?: exit=(-?\d+))?$/;
|
|
20
|
+
const ANSI = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_]/g;
|
|
21
|
+
const LANE_CHIP = { waiting: 'pending', running: 'running', pass: 'pass', fail: 'fail', flaky: 'flaky', known: 'known',
|
|
22
|
+
skipped: 'none', interrupted: 'interrupted' };
|
|
23
|
+
|
|
24
|
+
/* ---------- pure ---------- */
|
|
25
|
+
function dur(s) {
|
|
26
|
+
s = num(s); if (s === null) return '—';
|
|
27
|
+
s = Math.max(0, Math.round(s));
|
|
28
|
+
if (s < 60) return s + 's';
|
|
29
|
+
if (s < 3600) return Math.floor(s / 60) + 'm' + String(s % 60).padStart(2, '0') + 's';
|
|
30
|
+
return Math.floor(s / 3600) + 'h' + String(Math.floor(s % 3600 / 60)).padStart(2, '0') + 'm';
|
|
31
|
+
}
|
|
32
|
+
const since = (start, now) => num(start) !== null && num(now) !== null ? Math.max(0, now - start) : null;
|
|
33
|
+
// "no output for N min" past five minutes of silence, else null
|
|
34
|
+
function idleWarning(idle) {
|
|
35
|
+
idle = num(idle);
|
|
36
|
+
return idle !== null && idle > IDLE_WARN_S ? `no output for ${Math.floor(idle / 60)} min` : null;
|
|
37
|
+
}
|
|
38
|
+
// The last step a log started and no later line ended (steps nest), or null.
|
|
39
|
+
function openStep(text) {
|
|
40
|
+
const open = [];
|
|
41
|
+
for (const raw of String(text || '').split('\n')) {
|
|
42
|
+
const m = STEP.exec(raw.replace(ANSI, '').trim());
|
|
43
|
+
if (!m) continue;
|
|
44
|
+
if (m[2] === 'start') open.push(m[1]);
|
|
45
|
+
else { const i = open.lastIndexOf(m[1]); if (i >= 0) open.splice(i, 1); }
|
|
46
|
+
}
|
|
47
|
+
return open.length ? open[open.length - 1] : null;
|
|
48
|
+
}
|
|
49
|
+
function countsText(tests) {
|
|
50
|
+
if (!tests || typeof tests !== 'object') return null;
|
|
51
|
+
const n = k => num(tests[k]) || 0;
|
|
52
|
+
const parts = [`${n('pass')} passed`, `${n('fail') + n('error')} failed`];
|
|
53
|
+
if (n('skip')) parts.push(`${n('skip')} skipped`);
|
|
54
|
+
return parts.join(' · ');
|
|
55
|
+
}
|
|
56
|
+
// "rerun 1 of 2" once a lane ran again (of its own budget, when the lane set one)
|
|
57
|
+
function runsText(lane) {
|
|
58
|
+
const reruns = list(lane && lane.runs).filter(r => r && r.kind === 'rerun').length;
|
|
59
|
+
if (!reruns) return '';
|
|
60
|
+
return `rerun ${reruns}${num(lane.retries) !== null ? ' of ' + lane.retries : ''}`;
|
|
61
|
+
}
|
|
62
|
+
function laneRow(lane, now) {
|
|
63
|
+
const running = lane.state === 'running';
|
|
64
|
+
const took = running && num(lane.started) !== null && num(now) !== null ? since(lane.started, now) : num(lane.duration_s);
|
|
65
|
+
return {
|
|
66
|
+
name: String(lane.name || ''), state: lane.state || 'unknown', chip: LANE_CHIP[lane.state] || 'unknown',
|
|
67
|
+
time: took === null ? '' : dur(took), running,
|
|
68
|
+
runs: runsText(lane), counts: countsText(lane.tests), hasTests: !!(lane.tests && typeof lane.tests === 'object'),
|
|
69
|
+
step: running && lane.step && lane.step.name ? lane.step.name : null,
|
|
70
|
+
last_line: running && lane.last_line ? String(lane.last_line) : null,
|
|
71
|
+
idle: running ? idleWarning(lane.idle_s) : null,
|
|
72
|
+
labels: [lane.first ? 'first' : null, lane.group ? 'group ' + lane.group : null].filter(Boolean),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// failing tests in lane order: [{lane, entries: [{name, badge: new|known, message, excerpt, log_line}], fresh, known}]
|
|
76
|
+
function groupFailing(failing) {
|
|
77
|
+
const groups = new Map();
|
|
78
|
+
for (const f of list(failing)) {
|
|
79
|
+
if (!f || typeof f !== 'object') continue;
|
|
80
|
+
const lane = String(f.lane || '');
|
|
81
|
+
if (!groups.has(lane)) groups.set(lane, { lane, entries: [], fresh: 0, known: 0 });
|
|
82
|
+
const g = groups.get(lane), known = f.known === true;
|
|
83
|
+
g.entries.push({ name: String(f.name || '?'), badge: known ? 'known' : 'new', message: f.message || '', excerpt: f.excerpt || '',
|
|
84
|
+
log_line: num(f.log_line) !== null && f.log_line > 0 ? f.log_line : null, suite: f.suite || null, file: f.file || null });
|
|
85
|
+
if (known) g.known++; else g.fresh++;
|
|
86
|
+
}
|
|
87
|
+
return Array.from(groups.values());
|
|
88
|
+
}
|
|
89
|
+
// The lane whose log the live tail follows: the chosen one if it still exists, else the running lane that printed
|
|
90
|
+
// last, else the first running, failed or listed lane. null in single mode.
|
|
91
|
+
function tailLane(v, chosen) {
|
|
92
|
+
const lanes = list(v && v.lanes);
|
|
93
|
+
if (!lanes.length) return null;
|
|
94
|
+
if (chosen && lanes.some(l => l.name === chosen)) return chosen;
|
|
95
|
+
const running = lanes.filter(l => l.state === 'running');
|
|
96
|
+
const printed = running.filter(l => num(l.last_output_at) !== null).sort((a, b) => b.last_output_at - a.last_output_at);
|
|
97
|
+
const pick = printed[0] || running[0] || lanes.find(l => l.state === 'fail') || lanes[0];
|
|
98
|
+
return pick.name;
|
|
99
|
+
}
|
|
100
|
+
function model(v, now) {
|
|
101
|
+
if (!v) return null;
|
|
102
|
+
const running = v.state === 'running', status = String(v.status || '').toUpperCase();
|
|
103
|
+
const lanes = list(v.lanes).filter(l => l && typeof l === 'object').map(l => laneRow(l, now));
|
|
104
|
+
const failed = !running && (status === 'FAIL' || lanes.some(l => l.state === 'fail'));
|
|
105
|
+
const live = running && v.live ? {
|
|
106
|
+
elapsed: dur(since(v.live.started, now)),
|
|
107
|
+
step: v.live.step && v.live.step.name ? v.live.step.name : null,
|
|
108
|
+
stepElapsed: v.live.step && num(v.live.step.started) !== null ? dur(since(v.live.step.started, now)) : null,
|
|
109
|
+
last_line: v.live.last_line || null,
|
|
110
|
+
idle: idleWarning(v.live.idle_s),
|
|
111
|
+
} : null;
|
|
112
|
+
return {
|
|
113
|
+
running, failed, passed: !running && !failed && status === 'PASS', mode: v.mode === 'lanes' ? 'lanes' : 'single',
|
|
114
|
+
live, lanes, groups: groupFailing(v.failing), mutated: v.mutated === true,
|
|
115
|
+
reportless: failed ? lanes.filter(l => l.state === 'fail' && !l.hasTests).map(l => l.name) : [],
|
|
116
|
+
flaky: lanes.filter(l => l.state === 'flaky').map(l => l.name),
|
|
117
|
+
known: lanes.filter(l => l.state === 'known').map(l => l.name),
|
|
118
|
+
// a single command that ended: the step it was in, from its output's step markers
|
|
119
|
+
endStep: !running && v.mode !== 'lanes' && v.state ? openStep(v.log_tail) : null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const runsBase = key => `/api/runs/${encodeURIComponent(key)}/verification`;
|
|
123
|
+
// the log endpoint of the single command (lane null; RETRY for its rerun's log), a lane, or one run k of a lane
|
|
124
|
+
const RETRY = { retry: true };
|
|
125
|
+
function logPath(key, lane, k) {
|
|
126
|
+
if (lane === RETRY) return runsBase(key) + '/retry/log';
|
|
127
|
+
if (!lane) return runsBase(key) + '/log';
|
|
128
|
+
return `${runsBase(key)}/lanes/${encodeURIComponent(lane)}${num(k) !== null ? '/runs/' + k : ''}/log`;
|
|
129
|
+
}
|
|
130
|
+
// the range that shows `line` with a few lines above it
|
|
131
|
+
const linesPath = (key, lane, line) => `${logPath(key, lane)}/lines/${Math.max(1, line - CONTEXT_LINES)}`;
|
|
132
|
+
// lines of a tail as [{n, text}], numbered from tail_first_line when the server knew it
|
|
133
|
+
function tailLines(body) {
|
|
134
|
+
const text = String(body && body.tail || ''), first = num(body && body.tail_first_line);
|
|
135
|
+
const lines = text.endsWith('\n') ? text.slice(0, -1).split('\n') : text.split('\n');
|
|
136
|
+
return (text ? lines : []).map((t, i) => ({ n: first === null ? null : first + i, text: t }));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* ---------- DOM ---------- */
|
|
140
|
+
const views = new Map(); // run key → { lane (followed by the tail), open: {lane, line} | null }
|
|
141
|
+
const cache = new Map(); // path → { data, error, pending, fresh }
|
|
142
|
+
function el(tag, text, className) {
|
|
143
|
+
const node = document.createElement(tag);
|
|
144
|
+
if (text !== undefined && text !== null) node.textContent = String(text);
|
|
145
|
+
if (className) node.className = className;
|
|
146
|
+
return node;
|
|
147
|
+
}
|
|
148
|
+
const chip = (text, cls) => el('span', text || '—', 'chip st-' + cls);
|
|
149
|
+
function section(parent, title, cls) {
|
|
150
|
+
const s = el('section', undefined, 'insp-section' + (cls ? ' ' + cls : ''));
|
|
151
|
+
s.append(el('h3', title)); parent.append(s); return s;
|
|
152
|
+
}
|
|
153
|
+
function button(text, onClick, cls) {
|
|
154
|
+
const b = el('button', text, cls || 'vfy-link'); b.type = 'button';
|
|
155
|
+
b.addEventListener('click', onClick); return b;
|
|
156
|
+
}
|
|
157
|
+
// a fetch that lands once per `fresh` token and re-renders the view when it does
|
|
158
|
+
function load(ctx, path, fresh) {
|
|
159
|
+
const c = cache.get(path);
|
|
160
|
+
if (c && (c.fresh === fresh || c.pending)) return c;
|
|
161
|
+
const entry = { pending: true, fresh, data: c ? c.data : null, error: null };
|
|
162
|
+
cache.set(path, entry);
|
|
163
|
+
ctx.fetchJSON(path).then(data => { entry.data = data; entry.error = null; }, err => { entry.error = err; })
|
|
164
|
+
.finally(() => { entry.pending = false; ctx.rerender(); });
|
|
165
|
+
return entry;
|
|
166
|
+
}
|
|
167
|
+
function logBlock(parent, rows, mark, scrollKey) {
|
|
168
|
+
const pre = el('pre', undefined, 'vfy-log'); pre.dataset.scroll = scrollKey;
|
|
169
|
+
for (const r of rows) {
|
|
170
|
+
const line = el('span', undefined, 'vfy-line' + (mark !== null && r.n === mark ? ' vfy-hl' : ''));
|
|
171
|
+
if (r.n !== null) line.append(el('span', r.n, 'vfy-ln'));
|
|
172
|
+
line.append(el('span', r.text, 'vfy-lt'));
|
|
173
|
+
pre.append(line);
|
|
174
|
+
}
|
|
175
|
+
if (!rows.length) pre.append(el('span', '(empty)', 'vfy-lt'));
|
|
176
|
+
parent.append(pre);
|
|
177
|
+
return pre;
|
|
178
|
+
}
|
|
179
|
+
function tail(parent, ctx, lane, fresh, scrollKey) {
|
|
180
|
+
const entry = load(ctx, logPath(ctx.run.key, lane), fresh);
|
|
181
|
+
if (!entry.data) {
|
|
182
|
+
const why = !entry.error ? 'Reading the log…' : entry.error.snapshot ? 'This export holds no logs.'
|
|
183
|
+
: entry.error.status === 404 ? 'No log to show yet.' : 'Could not read the log.';
|
|
184
|
+
parent.append(el('p', why, 'muted'));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (entry.data.truncated) parent.append(el('p', `the last ${Math.round(String(entry.data.tail || '').length / 1024)} KB`, 'muted vfy-note'));
|
|
188
|
+
logBlock(parent, tailLines(entry.data), null, scrollKey);
|
|
189
|
+
}
|
|
190
|
+
function openedLog(parent, ctx, open) {
|
|
191
|
+
const s = section(parent, `Log of ${open.lane} at line ${open.line}`, 'vfy-opened');
|
|
192
|
+
const head = el('div', undefined, 'vfy-section-head');
|
|
193
|
+
head.append(s.firstChild, button('Close the log', () => { views.get(ctx.run.key).open = null; ctx.rerender(); }, 'vfy-close'));
|
|
194
|
+
s.prepend(head);
|
|
195
|
+
const entry = load(ctx, linesPath(ctx.run.key, open.lane, open.line), 'once');
|
|
196
|
+
if (entry.error) {
|
|
197
|
+
// an export holds no line ranges: the tail, with the line marked when it is in it
|
|
198
|
+
const back = load(ctx, logPath(ctx.run.key, open.lane), 'once');
|
|
199
|
+
if (!back.data) { s.append(el('p', back.error ? 'This log is not available here.' : 'Reading the log…', 'muted')); return; }
|
|
200
|
+
const rows = tailLines(back.data);
|
|
201
|
+
if (!rows.some(r => r.n === open.line)) s.append(el('p', `Line ${open.line} is not in this export; this is the end of the log.`, 'muted vfy-note'));
|
|
202
|
+
reveal(open, logBlock(s, rows, open.line, 'vfy-opened-log'));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!entry.data) { s.append(el('p', 'Reading the log…', 'muted')); return; }
|
|
206
|
+
const first = num(entry.data.first_line) || 1;
|
|
207
|
+
const pre = logBlock(s, list(entry.data.lines).map((t, i) => ({ n: first + i, text: t })), open.line, 'vfy-opened-log');
|
|
208
|
+
if (entry.data.truncated) s.append(el('p', 'The log goes on past these lines.', 'muted vfy-note'));
|
|
209
|
+
reveal(open, pre);
|
|
210
|
+
}
|
|
211
|
+
// Bring the marked line into view once, after the inspector put its own scroll back.
|
|
212
|
+
function reveal(open, pre) {
|
|
213
|
+
if (open.revealed || !pre.querySelector('.vfy-hl')) return;
|
|
214
|
+
open.revealed = true;
|
|
215
|
+
setTimeout(() => { if (pre.isConnected) pre.querySelector('.vfy-hl')?.scrollIntoView({ block: 'center' }); }, 0);
|
|
216
|
+
}
|
|
217
|
+
function liveBlock(parent, m) {
|
|
218
|
+
const box = el('div', undefined, 'vfy-live');
|
|
219
|
+
const row = el('p', undefined, 'vfy-now');
|
|
220
|
+
row.append(el('span', 'running for ', 'muted'), el('span', m.live.elapsed, 'vfy-num'));
|
|
221
|
+
if (m.live.step) {
|
|
222
|
+
row.append(el('span', ' · step ', 'muted'), el('span', m.live.step, 'mono vfy-step'));
|
|
223
|
+
if (m.live.stepElapsed) row.append(el('span', ' for ', 'muted'), el('span', m.live.stepElapsed, 'vfy-num'));
|
|
224
|
+
}
|
|
225
|
+
box.append(row);
|
|
226
|
+
if (m.live.last_line) box.append(el('p', m.live.last_line, 'mono vfy-last'));
|
|
227
|
+
if (m.live.idle) box.append(el('p', m.live.idle, 'warn vfy-idle'));
|
|
228
|
+
parent.append(box);
|
|
229
|
+
}
|
|
230
|
+
function failingBlock(parent, ctx, m, view) {
|
|
231
|
+
const total = m.groups.reduce((n, g) => n + g.entries.length, 0);
|
|
232
|
+
const s = section(parent, `Failing tests (${total})`, 'vfy-failing');
|
|
233
|
+
for (const g of m.groups) {
|
|
234
|
+
const box = el('div', undefined, 'vfy-group');
|
|
235
|
+
const h = el('h4', undefined, 'vfy-group-head');
|
|
236
|
+
h.append(el('span', g.lane || 'the command', 'mono'), el('span', [g.fresh ? `${g.fresh} new` : '', g.known ? `${g.known} known` : ''].filter(Boolean).join(' · '), 'muted vfy-num'));
|
|
237
|
+
box.append(h);
|
|
238
|
+
const ul = el('ul', undefined, 'plain vfy-tests');
|
|
239
|
+
for (const e of g.entries) {
|
|
240
|
+
const li = el('li', undefined, 'vfy-test vfy-' + e.badge);
|
|
241
|
+
const top = el('div', undefined, 'vfy-test-head');
|
|
242
|
+
top.append(el('span', e.badge, 'vfy-badge vfy-badge-' + e.badge), el('span', e.name, 'mono vfy-test-name'));
|
|
243
|
+
if (e.log_line !== null && g.lane) {
|
|
244
|
+
top.append(button(`open in log (line ${e.log_line})`, () => { view.open = { lane: g.lane, line: e.log_line }; ctx.rerender(); }));
|
|
245
|
+
}
|
|
246
|
+
li.append(top);
|
|
247
|
+
if (e.message) li.append(el('p', e.message, 'vfy-message'));
|
|
248
|
+
if (e.excerpt) {
|
|
249
|
+
const d = el('details', undefined, 'vfy-excerpt'); d.dataset.keep = `vfy-${g.lane}-${e.name}`;
|
|
250
|
+
d.append(el('summary', 'excerpt'), el('pre', e.excerpt));
|
|
251
|
+
li.append(d);
|
|
252
|
+
}
|
|
253
|
+
ul.append(li);
|
|
254
|
+
}
|
|
255
|
+
box.append(ul);
|
|
256
|
+
s.append(box);
|
|
257
|
+
}
|
|
258
|
+
return s;
|
|
259
|
+
}
|
|
260
|
+
function lanesTable(parent, ctx, m, view, followed) {
|
|
261
|
+
const s = section(parent, `Lanes (${m.lanes.length})`, 'vfy-lanes');
|
|
262
|
+
const wrap = el('div', undefined, 'vfy-table-wrap'); wrap.dataset.scroll = 'vfy-lanes';
|
|
263
|
+
const table = el('table', undefined, 'vfy-table');
|
|
264
|
+
const head = el('tr');
|
|
265
|
+
for (const h of ['Lane', 'State', 'Time', 'Runs', 'Tests']) head.append(el('th', h));
|
|
266
|
+
const thead = el('thead'); thead.append(head);
|
|
267
|
+
const body = el('tbody');
|
|
268
|
+
for (const l of m.lanes) {
|
|
269
|
+
const tr = el('tr', undefined, l.name === followed ? 'vfy-followed' : '');
|
|
270
|
+
const name = el('td', undefined, 'vfy-lane');
|
|
271
|
+
name.append(button(l.name, () => { view.lane = l.name; ctx.rerender(); }, 'vfy-link mono'));
|
|
272
|
+
for (const label of l.labels) name.append(el('span', label, 'plate vfy-label'));
|
|
273
|
+
if (l.step || l.last_line) {
|
|
274
|
+
const now = el('div', undefined, 'vfy-lane-now');
|
|
275
|
+
if (l.step) now.append(el('span', 'step ' + l.step, 'mono vfy-step'));
|
|
276
|
+
if (l.last_line) now.append(el('span', l.last_line, 'mono vfy-last'));
|
|
277
|
+
name.append(now);
|
|
278
|
+
}
|
|
279
|
+
if (l.idle) name.append(el('div', l.idle, 'warn vfy-idle'));
|
|
280
|
+
const state = el('td'); state.append(chip(l.state, l.chip));
|
|
281
|
+
tr.append(name, state, el('td', l.time, 'vfy-num vfy-nowrap'), el('td', l.runs, 'vfy-num vfy-nowrap'), el('td', l.counts || '', 'vfy-num'));
|
|
282
|
+
body.append(tr);
|
|
283
|
+
}
|
|
284
|
+
table.append(thead, body); wrap.append(table); s.append(wrap);
|
|
285
|
+
}
|
|
286
|
+
function render(host, ctx) {
|
|
287
|
+
const run = ctx.run, v = run.verification, m = model(v, num(ctx.now) !== null ? ctx.now : Date.now() / 1000);
|
|
288
|
+
if (!m) return;
|
|
289
|
+
if (!views.has(run.key)) views.set(run.key, { lane: null, open: null });
|
|
290
|
+
const view = views.get(run.key);
|
|
291
|
+
// logs are read again when the run's files moved: every poll while the verification prints
|
|
292
|
+
const fresh = `${run.updated}|${v.live ? v.live.last_output_at : ''}`;
|
|
293
|
+
if (m.running && m.live) liveBlock(host, m);
|
|
294
|
+
if (m.failed && m.groups.length) failingBlock(host, ctx, m, view);
|
|
295
|
+
if (view.open && list(v.lanes).some(l => l.name === view.open.lane)) openedLog(host, ctx, view.open);
|
|
296
|
+
if (m.mutated) host.append(el('p', 'A lane changed the checkout, so no lane was rerun after it.', 'warn vfy-mutated'));
|
|
297
|
+
for (const name of m.reportless) {
|
|
298
|
+
const s = section(host, `${name}: no test report; see the log`, 'vfy-reportless');
|
|
299
|
+
tail(s, ctx, name, fresh, 'vfy-tail-' + name);
|
|
300
|
+
}
|
|
301
|
+
if (m.endStep) host.append(el('p', `It ended in step ${m.endStep}.`, 'vfy-endstep'));
|
|
302
|
+
if (m.passed && m.lanes.length) {
|
|
303
|
+
const p = el('p', undefined, 'vfy-summary');
|
|
304
|
+
p.append(el('span', `${m.lanes.length} lane${m.lanes.length === 1 ? '' : 's'} passed`));
|
|
305
|
+
if (m.flaky.length) p.append(el('span', ` · flaky (passed on a rerun): ${m.flaky.join(', ')}`, 'warn'));
|
|
306
|
+
if (m.known.length) p.append(el('span', ` · known failures only: ${m.known.join(', ')}`, 'muted'));
|
|
307
|
+
host.append(p);
|
|
308
|
+
}
|
|
309
|
+
const followed = m.running ? tailLane(v, view.lane) : view.lane;
|
|
310
|
+
if (m.lanes.length) lanesTable(host, ctx, m, view, followed);
|
|
311
|
+
if (m.running) {
|
|
312
|
+
const s = section(host, m.mode === 'lanes' && followed ? `Live output: ${followed}` : 'Live output', 'vfy-tail');
|
|
313
|
+
// a single command's rerun writes its own log: the live output follows the one that moved last
|
|
314
|
+
const single = v.live && v.live.log === 'verification.retry.log' ? RETRY : null;
|
|
315
|
+
tail(s, ctx, m.mode === 'lanes' && followed ? followed : single, fresh, 'vfy-live-tail');
|
|
316
|
+
} else if (m.mode === 'lanes' && view.lane) {
|
|
317
|
+
const s = section(host, `Output: ${view.lane}`, 'vfy-tail');
|
|
318
|
+
tail(s, ctx, view.lane, fresh, 'vfy-lane-tail');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { render, model, laneRow, groupFailing, idleWarning, openStep, countsText, runsText, tailLane, logPath, linesPath, RETRY,
|
|
323
|
+
tailLines, dur, IDLE_WARN_S };
|
|
324
|
+
});
|