@plune-ai/cli 0.16.0 → 0.17.1
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/README.md +121 -1
- package/dist/cli.cjs +1073 -351
- package/dist/index.cjs +105 -42
- package/dist/index.d.cts +188 -1
- package/dist/index.d.ts +188 -1
- package/dist/index.js +101 -41
- package/dist/reporter-core.cjs +441 -176
- package/dist/reporter-core.d.cts +65 -6
- package/dist/reporter-core.d.ts +65 -6
- package/dist/reporter-core.js +438 -173
- package/package.json +2 -1
package/dist/reporter-core.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { createHash, randomUUID } from "crypto";
|
|
3
3
|
import { promises as disk } from "fs";
|
|
4
4
|
import { basename as basename2, extname } from "path";
|
|
5
|
+
import { TextDecoder } from "util";
|
|
5
6
|
|
|
6
7
|
// src/cli/api-url.ts
|
|
7
8
|
var DEFAULT_API_URL = "https://beta-api.plune.ai";
|
|
@@ -30,6 +31,8 @@ function loadToken() {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
// src/reporter-core/client.ts
|
|
34
|
+
import { createReadStream, openAsBlob } from "fs";
|
|
35
|
+
import { pipeline } from "stream";
|
|
33
36
|
import { request as httpRequest } from "http";
|
|
34
37
|
import { request as httpsRequest } from "https";
|
|
35
38
|
var RETRY_DELAYS_MS = [300, 900];
|
|
@@ -63,13 +66,49 @@ function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
|
63
66
|
req.end(init.body);
|
|
64
67
|
});
|
|
65
68
|
}
|
|
69
|
+
var PUT_DELAYS_MS = [500, 1500, 4e3];
|
|
70
|
+
var withoutLength = (headers) => Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== "content-length"));
|
|
71
|
+
function nodePut(url, headers, source, idleMs) {
|
|
72
|
+
return new Promise((resolve2, reject) => {
|
|
73
|
+
const target = new URL(url);
|
|
74
|
+
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
75
|
+
const length = "body" in source ? source.body.length : source.size;
|
|
76
|
+
let answered;
|
|
77
|
+
const settle = (err) => answered !== void 0 ? resolve2(answered) : reject(err);
|
|
78
|
+
const req = send(
|
|
79
|
+
target,
|
|
80
|
+
{ method: "PUT", headers: { ...withoutLength(headers), "content-length": String(length) } },
|
|
81
|
+
(res) => {
|
|
82
|
+
const retry = res.headers["retry-after"];
|
|
83
|
+
answered = { status: res.statusCode ?? 0, retryAfter: Array.isArray(retry) ? retry[0] ?? null : retry ?? null };
|
|
84
|
+
res.on("error", settle);
|
|
85
|
+
res.on("close", () => {
|
|
86
|
+
resolve2(answered);
|
|
87
|
+
req.destroy();
|
|
88
|
+
});
|
|
89
|
+
res.resume();
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
req.setTimeout(idleMs, () => req.destroy(Object.assign(new Error("idle"), { code: "ETIMEDOUT" })));
|
|
93
|
+
req.on("error", settle);
|
|
94
|
+
if ("body" in source) req.end(source.body);
|
|
95
|
+
else pipeline(createReadStream(source.path), req, (err) => err ? settle(err) : void 0);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
var codeOfError = (err) => {
|
|
99
|
+
const code = err?.code;
|
|
100
|
+
return typeof code === "string" && /^[A-Z0-9_]{2,40}$/.test(code) ? code : "network";
|
|
101
|
+
};
|
|
66
102
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
67
103
|
async function detailOf(res) {
|
|
68
104
|
try {
|
|
69
105
|
const body = await res.json();
|
|
70
|
-
return
|
|
106
|
+
return {
|
|
107
|
+
detail: typeof body.error === "string" ? body.error : "",
|
|
108
|
+
...typeof body.code === "string" && /^[a-z0-9_]{1,60}$/.test(body.code) ? { code: body.code } : {}
|
|
109
|
+
};
|
|
71
110
|
} catch {
|
|
72
|
-
return "";
|
|
111
|
+
return { detail: "" };
|
|
73
112
|
}
|
|
74
113
|
}
|
|
75
114
|
function retryAfterMs(res) {
|
|
@@ -120,10 +159,16 @@ function createClient(opts) {
|
|
|
120
159
|
return { ok: false, kind: "other", status: res.status, detail: "the answer was not JSON" };
|
|
121
160
|
}
|
|
122
161
|
}
|
|
123
|
-
const
|
|
162
|
+
const said = await detailOf(res);
|
|
124
163
|
const kind = classify(res.status);
|
|
125
|
-
last = { kind, status: res.status, detail };
|
|
164
|
+
last = { kind, status: res.status, detail: scrub(said.detail), ...said.code !== void 0 ? { code: said.code } : {} };
|
|
126
165
|
if (kind !== "unavailable") break;
|
|
166
|
+
if (res.status === 429 && retryAfterMs(res) === null) {
|
|
167
|
+
last.kind = "invalid";
|
|
168
|
+
last.detail = "quota exceeded (no Retry-After)";
|
|
169
|
+
if (said.detail) last.detail += ` \u2014 ${said.detail}`;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
127
172
|
if (attempt < RETRY_DELAYS_MS.length) {
|
|
128
173
|
await wait(retryAfterMs(res) ?? RETRY_DELAYS_MS[attempt]);
|
|
129
174
|
}
|
|
@@ -133,7 +178,36 @@ function createClient(opts) {
|
|
|
133
178
|
return {
|
|
134
179
|
post: (path3, body) => send("POST", path3, { body: JSON.stringify(body), type: "application/json" }),
|
|
135
180
|
upload: (path3, bytes, contentType) => send("POST", path3, { body: bytes, type: contentType }),
|
|
136
|
-
del: (path3) => send("DELETE", path3)
|
|
181
|
+
del: (path3) => send("DELETE", path3),
|
|
182
|
+
async put(url, headers, source) {
|
|
183
|
+
let last = { ok: false, status: null, code: "network" };
|
|
184
|
+
for (let attempt = 0; attempt <= PUT_DELAYS_MS.length; attempt += 1) {
|
|
185
|
+
try {
|
|
186
|
+
let status;
|
|
187
|
+
let retryAfter;
|
|
188
|
+
if (opts.fetchImpl === void 0) ({ status, retryAfter } = await nodePut(url, headers, source, IDLE_MS));
|
|
189
|
+
else {
|
|
190
|
+
const body = "body" in source ? source.body : await openAsBlob(source.path);
|
|
191
|
+
const res = await opts.fetchImpl(url, { method: "PUT", headers: withoutLength(headers), body });
|
|
192
|
+
await res.arrayBuffer().catch(() => void 0);
|
|
193
|
+
status = res.status;
|
|
194
|
+
retryAfter = res.headers.get("retry-after");
|
|
195
|
+
}
|
|
196
|
+
if (status >= 200 && status <= 299) return { ok: true };
|
|
197
|
+
last = { ok: false, status, code: `bucket_${status}` };
|
|
198
|
+
if (status < 500 && status !== 429) return last;
|
|
199
|
+
const seconds = retryAfter === null ? NaN : Number(retryAfter.trim());
|
|
200
|
+
if (attempt < PUT_DELAYS_MS.length) {
|
|
201
|
+
await wait(Number.isFinite(seconds) && seconds >= 0 ? Math.min(seconds, 60) * 1e3 : PUT_DELAYS_MS[attempt]);
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
last = { ok: false, status: null, code: codeOfError(err) };
|
|
206
|
+
}
|
|
207
|
+
if (attempt < PUT_DELAYS_MS.length) await wait(PUT_DELAYS_MS[attempt]);
|
|
208
|
+
}
|
|
209
|
+
return last;
|
|
210
|
+
}
|
|
137
211
|
};
|
|
138
212
|
}
|
|
139
213
|
|
|
@@ -222,6 +296,138 @@ function ciCommit(reported, env = process.env) {
|
|
|
222
296
|
};
|
|
223
297
|
}
|
|
224
298
|
|
|
299
|
+
// src/reporter-core/failure-detail.ts
|
|
300
|
+
import { existsSync as existsSync2 } from "fs";
|
|
301
|
+
import { homedir as homedir2 } from "os";
|
|
302
|
+
import { dirname as dirname2, join as join2, posix, resolve } from "path";
|
|
303
|
+
import { stripVTControlCharacters } from "util";
|
|
304
|
+
function failureOf(attempt, home = homedir2()) {
|
|
305
|
+
const error = chosen(attempt);
|
|
306
|
+
const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
|
|
307
|
+
const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
|
|
308
|
+
const steps = chainOf(attempt.steps);
|
|
309
|
+
const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
|
|
310
|
+
const artifacts = attempt.attachments.filter((a) => a.path !== void 0 && !a.name.startsWith("_") && a.contentType !== METADATA_TYPE).map(({ name, contentType }) => ({ name: fileNameOf(name), ...contentType ? { contentType } : {} })).filter((a) => a.name !== "");
|
|
311
|
+
const ciUrl = webLink(attempt.buildHref);
|
|
312
|
+
const failure = {
|
|
313
|
+
...headline !== void 0 ? { headline } : {},
|
|
314
|
+
...steps.length > 0 ? { steps } : {},
|
|
315
|
+
...location !== void 0 ? { location } : {},
|
|
316
|
+
...artifacts.length > 0 ? { artifacts } : {},
|
|
317
|
+
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
318
|
+
};
|
|
319
|
+
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
320
|
+
}
|
|
321
|
+
function repoRootOf(dir, stopAt) {
|
|
322
|
+
for (let here = resolve(dir); ; here = dirname2(here)) {
|
|
323
|
+
if (existsSync2(join2(here, ".git"))) return here;
|
|
324
|
+
if (here === stopAt || dirname2(here) === here) return void 0;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function errorContextOf(attempt, home = homedir2()) {
|
|
328
|
+
const text = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
329
|
+
return cut(machineless(text, attempt.repoRoot, home));
|
|
330
|
+
}
|
|
331
|
+
var printable = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f]/g, "?");
|
|
332
|
+
var mediaTypeOf = (contentType) => contentType?.split(";")[0]?.trim().toLowerCase() || void 0;
|
|
333
|
+
var TEXT_TYPES = /* @__PURE__ */ new Set(["application/json", "text/plain"]);
|
|
334
|
+
var isTextType = (contentType) => TEXT_TYPES.has(mediaTypeOf(contentType) ?? "");
|
|
335
|
+
function filesOf(attempt) {
|
|
336
|
+
return attempt.attachments.filter((a) => a.path !== void 0 || a.body !== void 0 && isTextType(a.contentType));
|
|
337
|
+
}
|
|
338
|
+
function machineless(raw, repoRoot, home) {
|
|
339
|
+
let text = stripVTControlCharacters(raw).replaceAll("\x1B", "");
|
|
340
|
+
const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
|
|
341
|
+
if (root !== void 0) text = text.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
|
|
342
|
+
const own = pathIn(home);
|
|
343
|
+
if (own !== void 0) text = text.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
|
|
344
|
+
if (repoRoot === void 0) text = text.replace(ANY_HOME, "~");
|
|
345
|
+
return text;
|
|
346
|
+
}
|
|
347
|
+
var PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
348
|
+
var TEXT_LIMIT = 524288;
|
|
349
|
+
var HEADLINE_LIMIT = 8 * 1024;
|
|
350
|
+
var jsonBytes = (text) => Buffer.byteLength(JSON.stringify(text)) - 2;
|
|
351
|
+
var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:(?:file|webpack|webpack-internal):(?:\\*\/){2,3})?(?:\\*\/home\\*\/|\\*\/Users\\*\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
|
|
352
|
+
function pathIn(path3) {
|
|
353
|
+
const parts = path3.split(/[\\/]+/);
|
|
354
|
+
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
355
|
+
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
356
|
+
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
357
|
+
const escaped = /^\/(?!\/)/.test(path3) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
358
|
+
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
359
|
+
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
360
|
+
}
|
|
361
|
+
var fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
362
|
+
function cut(text) {
|
|
363
|
+
if (jsonBytes(text) <= TEXT_LIMIT) return text;
|
|
364
|
+
const lines = text.split("\n");
|
|
365
|
+
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
366
|
+
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
367
|
+
let used = 0;
|
|
368
|
+
let head = 0;
|
|
369
|
+
let tail = lines.length;
|
|
370
|
+
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
371
|
+
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
372
|
+
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
373
|
+
}
|
|
374
|
+
var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
375
|
+
var METADATA_TYPE = "application/plune.metadata+json";
|
|
376
|
+
var REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
377
|
+
var FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
378
|
+
var FRAME_MAX = 8192;
|
|
379
|
+
var firstLine = (text) => stripVTControlCharacters(text).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
380
|
+
function chosen({ status, errors }) {
|
|
381
|
+
if (status === "timedOut") {
|
|
382
|
+
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
383
|
+
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
384
|
+
}
|
|
385
|
+
return errors[0];
|
|
386
|
+
}
|
|
387
|
+
function chainOf(steps) {
|
|
388
|
+
const chain = [];
|
|
389
|
+
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
390
|
+
chain.push(failed.title);
|
|
391
|
+
}
|
|
392
|
+
return chain;
|
|
393
|
+
}
|
|
394
|
+
function placeOf(error, testFile) {
|
|
395
|
+
const own = normal(testFile);
|
|
396
|
+
let first;
|
|
397
|
+
for (const line of stripVTControlCharacters(error.text).split("\n")) {
|
|
398
|
+
if (line.length > FRAME_MAX) continue;
|
|
399
|
+
const frame = FRAME.exec(line);
|
|
400
|
+
if (frame === null) continue;
|
|
401
|
+
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
402
|
+
if (normal(place.file) === own) return place;
|
|
403
|
+
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
404
|
+
}
|
|
405
|
+
return error.location ?? first;
|
|
406
|
+
}
|
|
407
|
+
function fileOf(raw) {
|
|
408
|
+
if (!raw.startsWith("file://")) return raw;
|
|
409
|
+
let path3 = raw.slice("file://".length);
|
|
410
|
+
try {
|
|
411
|
+
path3 = decodeURIComponent(path3);
|
|
412
|
+
} catch {
|
|
413
|
+
}
|
|
414
|
+
return /^\/[A-Za-z]:\//.test(path3) ? path3.slice(1) : path3;
|
|
415
|
+
}
|
|
416
|
+
var normal = (path3) => posix.normalize(path3.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
417
|
+
function fromRoot(place, root) {
|
|
418
|
+
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
419
|
+
const base = normal(root).replace(/\/+$/, "");
|
|
420
|
+
const file = normal(place.file);
|
|
421
|
+
if (!file.startsWith(`${base}/`)) return void 0;
|
|
422
|
+
const relative = file.slice(base.length + 1);
|
|
423
|
+
if (!REPO_RELATIVE.test(relative)) return void 0;
|
|
424
|
+
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
425
|
+
return column === void 0 ? { file: relative, line: place.line } : { file: relative, line: place.line, column };
|
|
426
|
+
}
|
|
427
|
+
function webLink(href) {
|
|
428
|
+
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
225
431
|
// src/reporter-core/fallback.ts
|
|
226
432
|
import * as fs from "fs";
|
|
227
433
|
import * as path2 from "path";
|
|
@@ -252,17 +458,21 @@ var COUNTED = ["accepted", "duplicate", "conflict", "rejected"];
|
|
|
252
458
|
var SCREENSHOT_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
253
459
|
var SCREENSHOT_BYTES = 2 * 1024 * 1024;
|
|
254
460
|
var SCREENSHOTS_PER_RESULT = 20;
|
|
461
|
+
var TEXT_BYTES = 512 * 1024;
|
|
462
|
+
var TEXTS_PER_RESULT = 10;
|
|
463
|
+
var UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
255
464
|
var TYPE_BY_EXTENSION = {
|
|
256
465
|
".png": "image/png",
|
|
257
466
|
".jpg": "image/jpeg",
|
|
258
467
|
".jpeg": "image/jpeg",
|
|
259
468
|
".webp": "image/webp"
|
|
260
469
|
};
|
|
470
|
+
var METADATA_TYPE2 = "application/plune.metadata+json";
|
|
261
471
|
var UPLOADS_AT_ONCE = 4;
|
|
262
472
|
var NAME_MAX = 200;
|
|
263
473
|
function nameOf(file) {
|
|
264
|
-
const own = file.name.split(/[\\/]/).pop()?.trim() || basename2(file.path);
|
|
265
|
-
const fileExt = extname(file.path);
|
|
474
|
+
const own = file.name.split(/[\\/]/).pop()?.trim() || (file.path === void 0 ? "" : basename2(file.path));
|
|
475
|
+
const fileExt = file.path === void 0 ? "" : extname(file.path);
|
|
266
476
|
const ownExt = extname(own);
|
|
267
477
|
const typed = ownExt !== "" && (ownExt.toLowerCase() === fileExt.toLowerCase() || TYPE_BY_EXTENSION[ownExt.toLowerCase()] !== void 0);
|
|
268
478
|
const ext = typed ? ownExt : fileExt;
|
|
@@ -278,8 +488,57 @@ var SKIPPED = {
|
|
|
278
488
|
type: "Plune takes PNG, JPEG and WebP",
|
|
279
489
|
limit: `a result holds at most ${SCREENSHOTS_PER_RESULT}`,
|
|
280
490
|
missing: "the file cannot be read on this machine",
|
|
281
|
-
size: "Plune takes a file of at most 2 MB"
|
|
491
|
+
size: "Plune takes a file of at most 2 MB",
|
|
492
|
+
textLimit: `a result holds at most ${TEXTS_PER_RESULT} text files`,
|
|
493
|
+
textSize: "Plune takes a text file of at most 512 KiB",
|
|
494
|
+
empty: "the file is empty",
|
|
495
|
+
binary: "the file holds a NUL byte, and Plune takes UTF-8 text without one",
|
|
496
|
+
encoding: "Plune takes UTF-8 text only",
|
|
497
|
+
noStorage: "no storage bucket is connected to this project"
|
|
498
|
+
};
|
|
499
|
+
var BUCKET_FOUND = "plune: storage bucket found for this project \u2014 screenshots, text, trace and video go straight to it over short-lived links.";
|
|
500
|
+
var KINDS = {
|
|
501
|
+
screenshot: { label: "screenshot", bytes: SCREENSHOT_BYTES, room: SCREENSHOTS_PER_RESULT, limit: "limit", size: "size" },
|
|
502
|
+
text: { label: "text file", bytes: TEXT_BYTES, room: TEXTS_PER_RESULT, limit: "textLimit", size: "textSize" },
|
|
503
|
+
// Only the label is used: a file of this kind has no home in the database, so no ceiling of its own is known here.
|
|
504
|
+
file: { label: "file", bytes: 0, room: 0, limit: "limit", size: "size" }
|
|
282
505
|
};
|
|
506
|
+
function kindOf(type) {
|
|
507
|
+
if (type.startsWith("image/")) return "screenshot";
|
|
508
|
+
return TEXT_TYPES.has(type) ? "text" : "file";
|
|
509
|
+
}
|
|
510
|
+
var codeOf = (out) => out.code ?? (out.status === null ? "network" : `http_${out.status}`);
|
|
511
|
+
function linkOf(item) {
|
|
512
|
+
const link = item;
|
|
513
|
+
if (typeof link?.id !== "string" || typeof link.url !== "string") return void 0;
|
|
514
|
+
const headers = typeof link.headers === "object" && link.headers !== null ? link.headers : {};
|
|
515
|
+
return { id: link.id, url: link.url, headers };
|
|
516
|
+
}
|
|
517
|
+
function textTypeOf(contentType, type) {
|
|
518
|
+
const charset = /;\s*charset\s*=\s*"?([^\s";]+)"?/i.exec(contentType)?.[1];
|
|
519
|
+
return charset?.toLowerCase() === "utf-8" ? `${type}; charset=utf-8` : type;
|
|
520
|
+
}
|
|
521
|
+
function textRefusal(bytes) {
|
|
522
|
+
if (bytes.length > TEXT_BYTES) return "textSize";
|
|
523
|
+
if (bytes.length === 0) return "empty";
|
|
524
|
+
if (bytes.includes(0)) return "binary";
|
|
525
|
+
try {
|
|
526
|
+
UTF8.decode(bytes);
|
|
527
|
+
} catch {
|
|
528
|
+
return "encoding";
|
|
529
|
+
}
|
|
530
|
+
return void 0;
|
|
531
|
+
}
|
|
532
|
+
function onDisk(result) {
|
|
533
|
+
if (!("files" in result) || result.files === void 0) return result;
|
|
534
|
+
const { files, ...rest } = result;
|
|
535
|
+
const kept = files.filter((f) => f.path !== void 0).map(({ body: _body, ...file }) => file);
|
|
536
|
+
return kept.length === 0 ? rest : { ...rest, files: kept };
|
|
537
|
+
}
|
|
538
|
+
function tally(counts, noun) {
|
|
539
|
+
if (counts.uploaded + counts.skipped + counts.failed === 0) return void 0;
|
|
540
|
+
return `${counts.uploaded} ${noun}(s) uploaded` + (counts.skipped > 0 ? `, ${counts.skipped} skipped` : "") + (counts.failed > 0 ? `, ${counts.failed} failed to upload` : "");
|
|
541
|
+
}
|
|
283
542
|
function boundKey(key) {
|
|
284
543
|
if (key.value.length <= KEY_MAX) return key;
|
|
285
544
|
const digest = createHash("sha256").update(key.value).digest("hex").slice(0, 16);
|
|
@@ -355,7 +614,9 @@ async function startRun(passed, declared) {
|
|
|
355
614
|
created: 0,
|
|
356
615
|
unoffered: 0,
|
|
357
616
|
deferred: 0,
|
|
358
|
-
screenshots: { uploaded: 0, skipped: 0, failed: 0 }
|
|
617
|
+
screenshots: { uploaded: 0, skipped: 0, failed: 0 },
|
|
618
|
+
textFiles: { uploaded: 0, skipped: 0, failed: 0 },
|
|
619
|
+
otherFiles: { uploaded: 0, skipped: 0, failed: 0 }
|
|
359
620
|
};
|
|
360
621
|
const resolved = /* @__PURE__ */ new Map();
|
|
361
622
|
let buffer = [];
|
|
@@ -366,6 +627,13 @@ async function startRun(passed, declared) {
|
|
|
366
627
|
let done = false;
|
|
367
628
|
const shortened = /* @__PURE__ */ new Set();
|
|
368
629
|
const skipsSaid = /* @__PURE__ */ new Set();
|
|
630
|
+
const countsOf = (kind) => kind === "text" ? stats.textFiles : kind === "file" ? stats.otherFiles : stats.screenshots;
|
|
631
|
+
let bucketSeen = false;
|
|
632
|
+
let noStorage = 0;
|
|
633
|
+
let uploadsMissing = false;
|
|
634
|
+
const failedSaid = /* @__PURE__ */ new Set();
|
|
635
|
+
const codesSaid = /* @__PURE__ */ new Set();
|
|
636
|
+
const rooms = /* @__PURE__ */ new Map();
|
|
369
637
|
function bound(keys) {
|
|
370
638
|
return keys.map((key) => {
|
|
371
639
|
const fit = boundKey(key);
|
|
@@ -373,7 +641,7 @@ async function startRun(passed, declared) {
|
|
|
373
641
|
shortened.add(key.value);
|
|
374
642
|
if (shortened.size === 1) {
|
|
375
643
|
log(
|
|
376
|
-
`plune: a test name longer than ${KEY_MAX} characters was shortened to fit its key \u2014
|
|
644
|
+
`plune: a test name longer than ${KEY_MAX} characters was shortened to fit its key \u2014 ${JSON.stringify(printable(key.value.slice(0, 80)))}\u2026 Give the test a shorter name; until then this is its identity.`
|
|
377
645
|
);
|
|
378
646
|
}
|
|
379
647
|
}
|
|
@@ -393,7 +661,7 @@ async function startRun(passed, declared) {
|
|
|
393
661
|
externalKey,
|
|
394
662
|
session: sessionMarker,
|
|
395
663
|
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
396
|
-
results
|
|
664
|
+
results: results.map(onDisk)
|
|
397
665
|
});
|
|
398
666
|
stats.deferred += results.length;
|
|
399
667
|
}
|
|
@@ -501,57 +769,173 @@ async function startRun(passed, declared) {
|
|
|
501
769
|
stats.created += out.body.results.filter((r) => r?.outcome === "created").length;
|
|
502
770
|
}
|
|
503
771
|
}
|
|
504
|
-
function skip(name, why) {
|
|
505
|
-
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
772
|
+
function skip(kind, name, why) {
|
|
773
|
+
countsOf(kind).skipped += 1;
|
|
774
|
+
if (why === "noStorage") noStorage += 1;
|
|
775
|
+
const said = why === "noStorage" ? why : `${kind}/${why}`;
|
|
776
|
+
if (skipsSaid.has(said)) return;
|
|
777
|
+
skipsSaid.add(said);
|
|
778
|
+
if (why === "noStorage") log(`plune: "${name}" not stored \u2014 ${SKIPPED[why]}.`);
|
|
779
|
+
else log(`plune: ${KINDS[kind].label} "${name}" not uploaded \u2014 ${SKIPPED[why]}.`);
|
|
509
780
|
}
|
|
510
|
-
|
|
781
|
+
function ask(job, size) {
|
|
782
|
+
return client.post(`/v1/results/${encodeURIComponent(job.id)}/files/uploads`, {
|
|
783
|
+
files: [{ name: job.name, contentType: job.type, size }]
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
function failed(job, why) {
|
|
787
|
+
countsOf(job.kind).failed += 1;
|
|
788
|
+
if (!failedSaid.has(job.kind)) {
|
|
789
|
+
failedSaid.add(job.kind);
|
|
790
|
+
log(`plune: could not upload a ${KINDS[job.kind].label} (${why}) \u2014 its result is reported without it.`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function failedToBucket(job, code) {
|
|
794
|
+
countsOf(job.kind).failed += 1;
|
|
795
|
+
if (codesSaid.has(code)) return;
|
|
796
|
+
codesSaid.add(code);
|
|
797
|
+
log(`plune: "${job.name}" not uploaded to your bucket (${code}) \u2014 its result is reported without it.`);
|
|
798
|
+
}
|
|
799
|
+
function refusedHere(job, size) {
|
|
800
|
+
if (job.kind === "screenshot") {
|
|
801
|
+
if (!SCREENSHOT_TYPES.has(job.type)) return "type";
|
|
802
|
+
if (size > SCREENSHOT_BYTES) return "size";
|
|
803
|
+
}
|
|
804
|
+
if (job.kind === "text" && size > TEXT_BYTES) return "textSize";
|
|
805
|
+
return void 0;
|
|
806
|
+
}
|
|
807
|
+
async function uploadDirect(job, size) {
|
|
808
|
+
const { kind } = job;
|
|
809
|
+
if (kind === "file") return skip(kind, job.name, "noStorage");
|
|
810
|
+
const { bytes: ceiling, size: tooBig } = KINDS[kind];
|
|
811
|
+
if (size > ceiling) return skip(kind, job.name, tooBig);
|
|
511
812
|
let bytes;
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
813
|
+
if (job.body !== void 0) bytes = job.body;
|
|
814
|
+
else {
|
|
815
|
+
try {
|
|
816
|
+
bytes = await disk.readFile(job.path);
|
|
817
|
+
} catch {
|
|
818
|
+
return skip(kind, job.name, "missing");
|
|
819
|
+
}
|
|
517
820
|
}
|
|
821
|
+
const refusal = kind === "text" ? textRefusal(bytes) : void 0;
|
|
822
|
+
if (refusal !== void 0) return skip(kind, job.name, refusal);
|
|
518
823
|
const out = await client.upload(
|
|
519
|
-
`/v1/results/${encodeURIComponent(
|
|
824
|
+
`/v1/results/${encodeURIComponent(job.id)}/files?name=${encodeURIComponent(job.name)}`,
|
|
520
825
|
bytes,
|
|
521
|
-
|
|
826
|
+
job.type
|
|
522
827
|
);
|
|
523
|
-
if (out.ok)
|
|
524
|
-
|
|
525
|
-
|
|
828
|
+
if (out.ok) countsOf(kind).uploaded += 1;
|
|
829
|
+
else failed(job, out.detail || out.kind);
|
|
830
|
+
}
|
|
831
|
+
async function uploadToBucket(job, size, first) {
|
|
832
|
+
const source = job.body !== void 0 ? { body: job.body } : { path: job.path, size };
|
|
833
|
+
let link = first;
|
|
834
|
+
for (let renewed = false; ; ) {
|
|
835
|
+
const put = await client.put(link.url, link.headers, source);
|
|
836
|
+
if (put.ok) break;
|
|
837
|
+
if (put.status !== 403 || renewed) return failedToBucket(job, put.code);
|
|
838
|
+
renewed = true;
|
|
839
|
+
const again = await ask(job, size);
|
|
840
|
+
if (!again.ok) return failedToBucket(job, codeOf(again));
|
|
841
|
+
const fresh = linkOf(again.body?.files?.[0]);
|
|
842
|
+
if (fresh === void 0) return failedToBucket(job, "bad_answer");
|
|
843
|
+
link = fresh;
|
|
526
844
|
}
|
|
527
|
-
|
|
528
|
-
if (
|
|
529
|
-
|
|
845
|
+
const done2 = await client.post(`/v1/files/${encodeURIComponent(link.id)}/complete`, {});
|
|
846
|
+
if (!done2.ok) return failedToBucket(job, codeOf(done2));
|
|
847
|
+
countsOf(job.kind).uploaded += 1;
|
|
848
|
+
}
|
|
849
|
+
let free = UPLOADS_AT_ONCE;
|
|
850
|
+
const waiting = [];
|
|
851
|
+
async function gated(work) {
|
|
852
|
+
if (free === 0) await new Promise((resolve2) => waiting.push(resolve2));
|
|
853
|
+
else free -= 1;
|
|
854
|
+
try {
|
|
855
|
+
return await work();
|
|
856
|
+
} finally {
|
|
857
|
+
const next = waiting.shift();
|
|
858
|
+
if (next !== void 0) next();
|
|
859
|
+
else free += 1;
|
|
530
860
|
}
|
|
531
861
|
}
|
|
862
|
+
async function plan(job) {
|
|
863
|
+
const { kind } = job;
|
|
864
|
+
let size;
|
|
865
|
+
if (job.body !== void 0) size = job.body.length;
|
|
866
|
+
else if (job.path === void 0) return void skip(kind, job.name, "missing");
|
|
867
|
+
else {
|
|
868
|
+
try {
|
|
869
|
+
size = (await disk.stat(job.path)).size;
|
|
870
|
+
} catch {
|
|
871
|
+
return void skip(kind, job.name, "missing");
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (size === 0) return void skip(kind, job.name, "empty");
|
|
875
|
+
const direct = () => {
|
|
876
|
+
if (kind === "file") return void skip(kind, job.name, "noStorage");
|
|
877
|
+
const roomKey = `${job.id}/${kind}`;
|
|
878
|
+
const left = rooms.get(roomKey) ?? KINDS[kind].room;
|
|
879
|
+
if (left === 0) return void skip(kind, job.name, KINDS[kind].limit);
|
|
880
|
+
rooms.set(roomKey, left - 1);
|
|
881
|
+
return () => uploadDirect(job, size);
|
|
882
|
+
};
|
|
883
|
+
if (uploadsMissing) return direct();
|
|
884
|
+
const out = await gated(() => ask(job, size));
|
|
885
|
+
if (!out.ok) {
|
|
886
|
+
if (out.status === 404 && out.detail === "" && out.code === void 0) {
|
|
887
|
+
uploadsMissing = true;
|
|
888
|
+
return direct();
|
|
889
|
+
}
|
|
890
|
+
if (out.code !== void 0) failedToBucket(job, out.code);
|
|
891
|
+
else failed(job, out.detail || out.kind);
|
|
892
|
+
return void 0;
|
|
893
|
+
}
|
|
894
|
+
const item = out.body?.files?.[0];
|
|
895
|
+
if (item?.skipped === "no_storage") return void skip(kind, job.name, refusedHere(job, size) ?? "noStorage");
|
|
896
|
+
if (item?.direct === true) return direct();
|
|
897
|
+
const link = linkOf(item);
|
|
898
|
+
if (link === void 0) return void failed(job, "bad_answer");
|
|
899
|
+
if (!bucketSeen) {
|
|
900
|
+
bucketSeen = true;
|
|
901
|
+
log(BUCKET_FOUND);
|
|
902
|
+
}
|
|
903
|
+
return () => uploadToBucket(job, size, link);
|
|
904
|
+
}
|
|
532
905
|
async function upload(items, filesByKey) {
|
|
533
906
|
if (filesByKey.size === 0 || !Array.isArray(items)) return;
|
|
534
|
-
const
|
|
907
|
+
const jobs = [];
|
|
535
908
|
for (const item of items) {
|
|
536
909
|
if (item?.status !== "accepted" || typeof item.id !== "string" || typeof item.resultKey !== "string") continue;
|
|
537
|
-
let room = SCREENSHOTS_PER_RESULT;
|
|
538
910
|
for (const file of filesByKey.get(item.resultKey) ?? []) {
|
|
539
|
-
const type = file.contentType
|
|
540
|
-
if (
|
|
911
|
+
const type = mediaTypeOf(file.contentType) ?? (file.path === void 0 ? void 0 : TYPE_BY_EXTENSION[extname(file.path).toLowerCase()]);
|
|
912
|
+
if (type === void 0 || type === METADATA_TYPE2) continue;
|
|
913
|
+
const kind = kindOf(type);
|
|
914
|
+
if (kind !== "text" && file.path === void 0) continue;
|
|
541
915
|
const name = nameOf(file);
|
|
542
|
-
if (file.name.startsWith("_")) skip(name, "hidden");
|
|
543
|
-
else if (!SCREENSHOT_TYPES.has(type)) skip(name, "type");
|
|
544
|
-
else if (room === 0) skip(name, "limit");
|
|
916
|
+
if (file.name.startsWith("_")) skip(kind, name, "hidden");
|
|
545
917
|
else {
|
|
546
|
-
|
|
547
|
-
|
|
918
|
+
jobs.push({
|
|
919
|
+
id: item.id,
|
|
920
|
+
kind,
|
|
921
|
+
name,
|
|
922
|
+
type: kind === "text" ? textTypeOf(file.contentType ?? "", type) : type,
|
|
923
|
+
...file.body !== void 0 ? { body: file.body } : {},
|
|
924
|
+
...file.path !== void 0 ? { path: file.path } : {}
|
|
925
|
+
});
|
|
548
926
|
}
|
|
549
927
|
}
|
|
550
928
|
}
|
|
551
|
-
|
|
929
|
+
const byResult = /* @__PURE__ */ new Map();
|
|
930
|
+
for (const job of jobs) byResult.set(job.id, [...byResult.get(job.id) ?? [], job]);
|
|
552
931
|
await Promise.all(
|
|
553
|
-
|
|
554
|
-
|
|
932
|
+
[...byResult.values()].map(async (group) => {
|
|
933
|
+
const work = [];
|
|
934
|
+
for (const job of group) {
|
|
935
|
+
const act = await plan(job);
|
|
936
|
+
if (act !== void 0) work.push(gated(act));
|
|
937
|
+
}
|
|
938
|
+
await Promise.all(work);
|
|
555
939
|
})
|
|
556
940
|
);
|
|
557
941
|
}
|
|
@@ -629,11 +1013,20 @@ async function startRun(passed, declared) {
|
|
|
629
1013
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
630
1014
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
631
1015
|
if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
|
|
632
|
-
const
|
|
633
|
-
|
|
1016
|
+
const kinds = [stats.screenshots, stats.textFiles, stats.otherFiles];
|
|
1017
|
+
const sum = (what) => kinds.reduce((n, counts) => n + counts[what], 0);
|
|
1018
|
+
if (bucketSeen) {
|
|
1019
|
+
const other = sum("skipped") - noStorage;
|
|
634
1020
|
parts.push(
|
|
635
|
-
`${
|
|
1021
|
+
`${sum("uploaded")} file(s) uploaded to your bucket` + (noStorage > 0 ? `, ${noStorage} skipped (no bucket connected)` : "") + (other > 0 ? `, ${other} skipped` : "") + (sum("failed") > 0 ? `, ${sum("failed")} failed to upload` : "")
|
|
636
1022
|
);
|
|
1023
|
+
} else {
|
|
1024
|
+
for (const said of [tally(stats.screenshots, "screenshot"), tally(stats.textFiles, "text file")]) {
|
|
1025
|
+
if (said !== void 0) parts.push(said);
|
|
1026
|
+
}
|
|
1027
|
+
const other = stats.otherFiles;
|
|
1028
|
+
if (other.skipped > 0) parts.push(`${other.skipped} file(s) not stored (no bucket connected)`);
|
|
1029
|
+
if (other.failed > 0) parts.push(`${other.failed} file(s) failed to upload`);
|
|
637
1030
|
}
|
|
638
1031
|
if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
|
|
639
1032
|
if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
|
|
@@ -700,134 +1093,6 @@ function resultKey(testId, retry) {
|
|
|
700
1093
|
const plain = `${testId}#${retry}`;
|
|
701
1094
|
return plain.length <= RESULT_KEY_MAX ? plain : `${sha256(testId).slice(0, HASH_CHARS)}#${retry}`;
|
|
702
1095
|
}
|
|
703
|
-
|
|
704
|
-
// src/reporter-core/failure-detail.ts
|
|
705
|
-
import { existsSync as existsSync2 } from "fs";
|
|
706
|
-
import { homedir as homedir2 } from "os";
|
|
707
|
-
import { dirname as dirname3, join as join3, posix, resolve } from "path";
|
|
708
|
-
import { stripVTControlCharacters } from "util";
|
|
709
|
-
function failureOf(attempt, home = homedir2()) {
|
|
710
|
-
const error = chosen(attempt);
|
|
711
|
-
const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
|
|
712
|
-
const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
|
|
713
|
-
const steps = chainOf(attempt.steps);
|
|
714
|
-
const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
|
|
715
|
-
const artifacts = attempt.attachments.filter((a) => a.path !== void 0 && !a.name.startsWith("_") && a.contentType !== METADATA_TYPE).map(({ name, contentType }) => ({ name: fileNameOf(name), ...contentType ? { contentType } : {} })).filter((a) => a.name !== "");
|
|
716
|
-
const ciUrl = webLink(attempt.buildHref);
|
|
717
|
-
const failure = {
|
|
718
|
-
...headline !== void 0 ? { headline } : {},
|
|
719
|
-
...steps.length > 0 ? { steps } : {},
|
|
720
|
-
...location !== void 0 ? { location } : {},
|
|
721
|
-
...artifacts.length > 0 ? { artifacts } : {},
|
|
722
|
-
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
723
|
-
};
|
|
724
|
-
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
725
|
-
}
|
|
726
|
-
function repoRootOf(dir, stopAt) {
|
|
727
|
-
for (let here = resolve(dir); ; here = dirname3(here)) {
|
|
728
|
-
if (existsSync2(join3(here, ".git"))) return here;
|
|
729
|
-
if (here === stopAt || dirname3(here) === here) return void 0;
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
function errorContextOf(attempt, home = homedir2()) {
|
|
733
|
-
const text = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
734
|
-
return cut(machineless(text, attempt.repoRoot, home));
|
|
735
|
-
}
|
|
736
|
-
function filesOf(attempt) {
|
|
737
|
-
return attempt.attachments.filter((a) => a.path !== void 0);
|
|
738
|
-
}
|
|
739
|
-
function machineless(raw, repoRoot, home) {
|
|
740
|
-
let text = stripVTControlCharacters(raw).replaceAll("\x1B", "");
|
|
741
|
-
const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
|
|
742
|
-
if (root !== void 0) text = text.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
|
|
743
|
-
const own = pathIn(home);
|
|
744
|
-
if (own !== void 0) text = text.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
|
|
745
|
-
if (repoRoot === void 0) text = text.replace(ANY_HOME, "~");
|
|
746
|
-
return text;
|
|
747
|
-
}
|
|
748
|
-
var PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
749
|
-
var TEXT_LIMIT = 524288;
|
|
750
|
-
var HEADLINE_LIMIT = 8 * 1024;
|
|
751
|
-
var jsonBytes = (text) => Buffer.byteLength(JSON.stringify(text)) - 2;
|
|
752
|
-
var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:(?:file|webpack|webpack-internal):(?:\\*\/){2,3})?(?:\\*\/home\\*\/|\\*\/Users\\*\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
|
|
753
|
-
function pathIn(path3) {
|
|
754
|
-
const parts = path3.split(/[\\/]+/);
|
|
755
|
-
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
756
|
-
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
757
|
-
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
758
|
-
const escaped = /^\/(?!\/)/.test(path3) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
759
|
-
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
760
|
-
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
761
|
-
}
|
|
762
|
-
var fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
763
|
-
function cut(text) {
|
|
764
|
-
if (jsonBytes(text) <= TEXT_LIMIT) return text;
|
|
765
|
-
const lines = text.split("\n");
|
|
766
|
-
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
767
|
-
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
768
|
-
let used = 0;
|
|
769
|
-
let head = 0;
|
|
770
|
-
let tail = lines.length;
|
|
771
|
-
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
772
|
-
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
773
|
-
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
774
|
-
}
|
|
775
|
-
var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
776
|
-
var METADATA_TYPE = "application/plune.metadata+json";
|
|
777
|
-
var REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
778
|
-
var FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
779
|
-
var FRAME_MAX = 8192;
|
|
780
|
-
var firstLine = (text) => stripVTControlCharacters(text).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
781
|
-
function chosen({ status, errors }) {
|
|
782
|
-
if (status === "timedOut") {
|
|
783
|
-
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
784
|
-
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
785
|
-
}
|
|
786
|
-
return errors[0];
|
|
787
|
-
}
|
|
788
|
-
function chainOf(steps) {
|
|
789
|
-
const chain = [];
|
|
790
|
-
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
791
|
-
chain.push(failed.title);
|
|
792
|
-
}
|
|
793
|
-
return chain;
|
|
794
|
-
}
|
|
795
|
-
function placeOf(error, testFile) {
|
|
796
|
-
const own = normal(testFile);
|
|
797
|
-
let first;
|
|
798
|
-
for (const line of stripVTControlCharacters(error.text).split("\n")) {
|
|
799
|
-
if (line.length > FRAME_MAX) continue;
|
|
800
|
-
const frame = FRAME.exec(line);
|
|
801
|
-
if (frame === null) continue;
|
|
802
|
-
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
803
|
-
if (normal(place.file) === own) return place;
|
|
804
|
-
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
805
|
-
}
|
|
806
|
-
return error.location ?? first;
|
|
807
|
-
}
|
|
808
|
-
function fileOf(raw) {
|
|
809
|
-
if (!raw.startsWith("file://")) return raw;
|
|
810
|
-
let path3 = raw.slice("file://".length);
|
|
811
|
-
try {
|
|
812
|
-
path3 = decodeURIComponent(path3);
|
|
813
|
-
} catch {
|
|
814
|
-
}
|
|
815
|
-
return /^\/[A-Za-z]:\//.test(path3) ? path3.slice(1) : path3;
|
|
816
|
-
}
|
|
817
|
-
var normal = (path3) => posix.normalize(path3.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
818
|
-
function fromRoot(place, root) {
|
|
819
|
-
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
820
|
-
const base = normal(root).replace(/\/+$/, "");
|
|
821
|
-
const file = normal(place.file);
|
|
822
|
-
if (!file.startsWith(`${base}/`)) return void 0;
|
|
823
|
-
const relative = file.slice(base.length + 1);
|
|
824
|
-
if (!REPO_RELATIVE.test(relative)) return void 0;
|
|
825
|
-
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
826
|
-
return column === void 0 ? { file: relative, line: place.line } : { file: relative, line: place.line, column };
|
|
827
|
-
}
|
|
828
|
-
function webLink(href) {
|
|
829
|
-
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
830
|
-
}
|
|
831
1096
|
export {
|
|
832
1097
|
DEFAULT_FALLBACK_PATH,
|
|
833
1098
|
RESULT_KEY_MAX,
|