@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.cjs
CHANGED
|
@@ -49,8 +49,9 @@ module.exports = __toCommonJS(reporter_core_exports);
|
|
|
49
49
|
|
|
50
50
|
// src/reporter-core/session.ts
|
|
51
51
|
var import_node_crypto = require("crypto");
|
|
52
|
-
var
|
|
53
|
-
var
|
|
52
|
+
var import_node_fs4 = require("fs");
|
|
53
|
+
var import_node_path3 = require("path");
|
|
54
|
+
var import_node_util2 = require("util");
|
|
54
55
|
|
|
55
56
|
// src/cli/api-url.ts
|
|
56
57
|
var DEFAULT_API_URL = "https://beta-api.plune.ai";
|
|
@@ -79,6 +80,8 @@ function loadToken() {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
// src/reporter-core/client.ts
|
|
83
|
+
var import_node_fs2 = require("fs");
|
|
84
|
+
var import_node_stream = require("stream");
|
|
82
85
|
var import_node_http = require("http");
|
|
83
86
|
var import_node_https = require("https");
|
|
84
87
|
var RETRY_DELAYS_MS = [300, 900];
|
|
@@ -112,13 +115,49 @@ function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
|
112
115
|
req.end(init.body);
|
|
113
116
|
});
|
|
114
117
|
}
|
|
118
|
+
var PUT_DELAYS_MS = [500, 1500, 4e3];
|
|
119
|
+
var withoutLength = (headers) => Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== "content-length"));
|
|
120
|
+
function nodePut(url, headers, source, idleMs) {
|
|
121
|
+
return new Promise((resolve2, reject) => {
|
|
122
|
+
const target = new URL(url);
|
|
123
|
+
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
124
|
+
const length = "body" in source ? source.body.length : source.size;
|
|
125
|
+
let answered;
|
|
126
|
+
const settle = (err) => answered !== void 0 ? resolve2(answered) : reject(err);
|
|
127
|
+
const req = send(
|
|
128
|
+
target,
|
|
129
|
+
{ method: "PUT", headers: { ...withoutLength(headers), "content-length": String(length) } },
|
|
130
|
+
(res) => {
|
|
131
|
+
const retry = res.headers["retry-after"];
|
|
132
|
+
answered = { status: res.statusCode ?? 0, retryAfter: Array.isArray(retry) ? retry[0] ?? null : retry ?? null };
|
|
133
|
+
res.on("error", settle);
|
|
134
|
+
res.on("close", () => {
|
|
135
|
+
resolve2(answered);
|
|
136
|
+
req.destroy();
|
|
137
|
+
});
|
|
138
|
+
res.resume();
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
req.setTimeout(idleMs, () => req.destroy(Object.assign(new Error("idle"), { code: "ETIMEDOUT" })));
|
|
142
|
+
req.on("error", settle);
|
|
143
|
+
if ("body" in source) req.end(source.body);
|
|
144
|
+
else (0, import_node_stream.pipeline)((0, import_node_fs2.createReadStream)(source.path), req, (err) => err ? settle(err) : void 0);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
var codeOfError = (err) => {
|
|
148
|
+
const code = err?.code;
|
|
149
|
+
return typeof code === "string" && /^[A-Z0-9_]{2,40}$/.test(code) ? code : "network";
|
|
150
|
+
};
|
|
115
151
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
116
152
|
async function detailOf(res) {
|
|
117
153
|
try {
|
|
118
154
|
const body = await res.json();
|
|
119
|
-
return
|
|
155
|
+
return {
|
|
156
|
+
detail: typeof body.error === "string" ? body.error : "",
|
|
157
|
+
...typeof body.code === "string" && /^[a-z0-9_]{1,60}$/.test(body.code) ? { code: body.code } : {}
|
|
158
|
+
};
|
|
120
159
|
} catch {
|
|
121
|
-
return "";
|
|
160
|
+
return { detail: "" };
|
|
122
161
|
}
|
|
123
162
|
}
|
|
124
163
|
function retryAfterMs(res) {
|
|
@@ -169,10 +208,16 @@ function createClient(opts) {
|
|
|
169
208
|
return { ok: false, kind: "other", status: res.status, detail: "the answer was not JSON" };
|
|
170
209
|
}
|
|
171
210
|
}
|
|
172
|
-
const
|
|
211
|
+
const said = await detailOf(res);
|
|
173
212
|
const kind = classify(res.status);
|
|
174
|
-
last = { kind, status: res.status, detail };
|
|
213
|
+
last = { kind, status: res.status, detail: scrub(said.detail), ...said.code !== void 0 ? { code: said.code } : {} };
|
|
175
214
|
if (kind !== "unavailable") break;
|
|
215
|
+
if (res.status === 429 && retryAfterMs(res) === null) {
|
|
216
|
+
last.kind = "invalid";
|
|
217
|
+
last.detail = "quota exceeded (no Retry-After)";
|
|
218
|
+
if (said.detail) last.detail += ` \u2014 ${said.detail}`;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
176
221
|
if (attempt < RETRY_DELAYS_MS.length) {
|
|
177
222
|
await wait(retryAfterMs(res) ?? RETRY_DELAYS_MS[attempt]);
|
|
178
223
|
}
|
|
@@ -182,7 +227,36 @@ function createClient(opts) {
|
|
|
182
227
|
return {
|
|
183
228
|
post: (path3, body) => send("POST", path3, { body: JSON.stringify(body), type: "application/json" }),
|
|
184
229
|
upload: (path3, bytes, contentType) => send("POST", path3, { body: bytes, type: contentType }),
|
|
185
|
-
del: (path3) => send("DELETE", path3)
|
|
230
|
+
del: (path3) => send("DELETE", path3),
|
|
231
|
+
async put(url, headers, source) {
|
|
232
|
+
let last = { ok: false, status: null, code: "network" };
|
|
233
|
+
for (let attempt = 0; attempt <= PUT_DELAYS_MS.length; attempt += 1) {
|
|
234
|
+
try {
|
|
235
|
+
let status;
|
|
236
|
+
let retryAfter;
|
|
237
|
+
if (opts.fetchImpl === void 0) ({ status, retryAfter } = await nodePut(url, headers, source, IDLE_MS));
|
|
238
|
+
else {
|
|
239
|
+
const body = "body" in source ? source.body : await (0, import_node_fs2.openAsBlob)(source.path);
|
|
240
|
+
const res = await opts.fetchImpl(url, { method: "PUT", headers: withoutLength(headers), body });
|
|
241
|
+
await res.arrayBuffer().catch(() => void 0);
|
|
242
|
+
status = res.status;
|
|
243
|
+
retryAfter = res.headers.get("retry-after");
|
|
244
|
+
}
|
|
245
|
+
if (status >= 200 && status <= 299) return { ok: true };
|
|
246
|
+
last = { ok: false, status, code: `bucket_${status}` };
|
|
247
|
+
if (status < 500 && status !== 429) return last;
|
|
248
|
+
const seconds = retryAfter === null ? NaN : Number(retryAfter.trim());
|
|
249
|
+
if (attempt < PUT_DELAYS_MS.length) {
|
|
250
|
+
await wait(Number.isFinite(seconds) && seconds >= 0 ? Math.min(seconds, 60) * 1e3 : PUT_DELAYS_MS[attempt]);
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
last = { ok: false, status: null, code: codeOfError(err) };
|
|
255
|
+
}
|
|
256
|
+
if (attempt < PUT_DELAYS_MS.length) await wait(PUT_DELAYS_MS[attempt]);
|
|
257
|
+
}
|
|
258
|
+
return last;
|
|
259
|
+
}
|
|
186
260
|
};
|
|
187
261
|
}
|
|
188
262
|
|
|
@@ -271,6 +345,138 @@ function ciCommit(reported, env = process.env) {
|
|
|
271
345
|
};
|
|
272
346
|
}
|
|
273
347
|
|
|
348
|
+
// src/reporter-core/failure-detail.ts
|
|
349
|
+
var import_node_fs3 = require("fs");
|
|
350
|
+
var import_node_os2 = require("os");
|
|
351
|
+
var import_node_path2 = require("path");
|
|
352
|
+
var import_node_util = require("util");
|
|
353
|
+
function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
354
|
+
const error = chosen(attempt);
|
|
355
|
+
const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
|
|
356
|
+
const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
|
|
357
|
+
const steps = chainOf(attempt.steps);
|
|
358
|
+
const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
|
|
359
|
+
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 !== "");
|
|
360
|
+
const ciUrl = webLink(attempt.buildHref);
|
|
361
|
+
const failure = {
|
|
362
|
+
...headline !== void 0 ? { headline } : {},
|
|
363
|
+
...steps.length > 0 ? { steps } : {},
|
|
364
|
+
...location !== void 0 ? { location } : {},
|
|
365
|
+
...artifacts.length > 0 ? { artifacts } : {},
|
|
366
|
+
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
367
|
+
};
|
|
368
|
+
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
369
|
+
}
|
|
370
|
+
function repoRootOf(dir, stopAt) {
|
|
371
|
+
for (let here = (0, import_node_path2.resolve)(dir); ; here = (0, import_node_path2.dirname)(here)) {
|
|
372
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path2.join)(here, ".git"))) return here;
|
|
373
|
+
if (here === stopAt || (0, import_node_path2.dirname)(here) === here) return void 0;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
377
|
+
const text = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
378
|
+
return cut(machineless(text, attempt.repoRoot, home));
|
|
379
|
+
}
|
|
380
|
+
var printable = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f]/g, "?");
|
|
381
|
+
var mediaTypeOf = (contentType) => contentType?.split(";")[0]?.trim().toLowerCase() || void 0;
|
|
382
|
+
var TEXT_TYPES = /* @__PURE__ */ new Set(["application/json", "text/plain"]);
|
|
383
|
+
var isTextType = (contentType) => TEXT_TYPES.has(mediaTypeOf(contentType) ?? "");
|
|
384
|
+
function filesOf(attempt) {
|
|
385
|
+
return attempt.attachments.filter((a) => a.path !== void 0 || a.body !== void 0 && isTextType(a.contentType));
|
|
386
|
+
}
|
|
387
|
+
function machineless(raw, repoRoot, home) {
|
|
388
|
+
let text = (0, import_node_util.stripVTControlCharacters)(raw).replaceAll("\x1B", "");
|
|
389
|
+
const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
|
|
390
|
+
if (root !== void 0) text = text.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
|
|
391
|
+
const own = pathIn(home);
|
|
392
|
+
if (own !== void 0) text = text.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
|
|
393
|
+
if (repoRoot === void 0) text = text.replace(ANY_HOME, "~");
|
|
394
|
+
return text;
|
|
395
|
+
}
|
|
396
|
+
var PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
397
|
+
var TEXT_LIMIT = 524288;
|
|
398
|
+
var HEADLINE_LIMIT = 8 * 1024;
|
|
399
|
+
var jsonBytes = (text) => Buffer.byteLength(JSON.stringify(text)) - 2;
|
|
400
|
+
var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:(?:file|webpack|webpack-internal):(?:\\*\/){2,3})?(?:\\*\/home\\*\/|\\*\/Users\\*\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
|
|
401
|
+
function pathIn(path3) {
|
|
402
|
+
const parts = path3.split(/[\\/]+/);
|
|
403
|
+
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
404
|
+
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
405
|
+
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
406
|
+
const escaped = /^\/(?!\/)/.test(path3) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
407
|
+
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
408
|
+
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
409
|
+
}
|
|
410
|
+
var fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
411
|
+
function cut(text) {
|
|
412
|
+
if (jsonBytes(text) <= TEXT_LIMIT) return text;
|
|
413
|
+
const lines = text.split("\n");
|
|
414
|
+
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
415
|
+
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
416
|
+
let used = 0;
|
|
417
|
+
let head = 0;
|
|
418
|
+
let tail = lines.length;
|
|
419
|
+
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
420
|
+
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
421
|
+
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
422
|
+
}
|
|
423
|
+
var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
424
|
+
var METADATA_TYPE = "application/plune.metadata+json";
|
|
425
|
+
var REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
426
|
+
var FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
427
|
+
var FRAME_MAX = 8192;
|
|
428
|
+
var firstLine = (text) => (0, import_node_util.stripVTControlCharacters)(text).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
429
|
+
function chosen({ status, errors }) {
|
|
430
|
+
if (status === "timedOut") {
|
|
431
|
+
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
432
|
+
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
433
|
+
}
|
|
434
|
+
return errors[0];
|
|
435
|
+
}
|
|
436
|
+
function chainOf(steps) {
|
|
437
|
+
const chain = [];
|
|
438
|
+
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
439
|
+
chain.push(failed.title);
|
|
440
|
+
}
|
|
441
|
+
return chain;
|
|
442
|
+
}
|
|
443
|
+
function placeOf(error, testFile) {
|
|
444
|
+
const own = normal(testFile);
|
|
445
|
+
let first;
|
|
446
|
+
for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
|
|
447
|
+
if (line.length > FRAME_MAX) continue;
|
|
448
|
+
const frame = FRAME.exec(line);
|
|
449
|
+
if (frame === null) continue;
|
|
450
|
+
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
451
|
+
if (normal(place.file) === own) return place;
|
|
452
|
+
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
453
|
+
}
|
|
454
|
+
return error.location ?? first;
|
|
455
|
+
}
|
|
456
|
+
function fileOf(raw) {
|
|
457
|
+
if (!raw.startsWith("file://")) return raw;
|
|
458
|
+
let path3 = raw.slice("file://".length);
|
|
459
|
+
try {
|
|
460
|
+
path3 = decodeURIComponent(path3);
|
|
461
|
+
} catch {
|
|
462
|
+
}
|
|
463
|
+
return /^\/[A-Za-z]:\//.test(path3) ? path3.slice(1) : path3;
|
|
464
|
+
}
|
|
465
|
+
var normal = (path3) => import_node_path2.posix.normalize(path3.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
466
|
+
function fromRoot(place, root) {
|
|
467
|
+
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
468
|
+
const base = normal(root).replace(/\/+$/, "");
|
|
469
|
+
const file = normal(place.file);
|
|
470
|
+
if (!file.startsWith(`${base}/`)) return void 0;
|
|
471
|
+
const relative = file.slice(base.length + 1);
|
|
472
|
+
if (!REPO_RELATIVE.test(relative)) return void 0;
|
|
473
|
+
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
474
|
+
return column === void 0 ? { file: relative, line: place.line } : { file: relative, line: place.line, column };
|
|
475
|
+
}
|
|
476
|
+
function webLink(href) {
|
|
477
|
+
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
478
|
+
}
|
|
479
|
+
|
|
274
480
|
// src/reporter-core/fallback.ts
|
|
275
481
|
var fs = __toESM(require("fs"), 1);
|
|
276
482
|
var path2 = __toESM(require("path"), 1);
|
|
@@ -301,18 +507,22 @@ var COUNTED = ["accepted", "duplicate", "conflict", "rejected"];
|
|
|
301
507
|
var SCREENSHOT_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
302
508
|
var SCREENSHOT_BYTES = 2 * 1024 * 1024;
|
|
303
509
|
var SCREENSHOTS_PER_RESULT = 20;
|
|
510
|
+
var TEXT_BYTES = 512 * 1024;
|
|
511
|
+
var TEXTS_PER_RESULT = 10;
|
|
512
|
+
var UTF8 = new import_node_util2.TextDecoder("utf-8", { fatal: true });
|
|
304
513
|
var TYPE_BY_EXTENSION = {
|
|
305
514
|
".png": "image/png",
|
|
306
515
|
".jpg": "image/jpeg",
|
|
307
516
|
".jpeg": "image/jpeg",
|
|
308
517
|
".webp": "image/webp"
|
|
309
518
|
};
|
|
519
|
+
var METADATA_TYPE2 = "application/plune.metadata+json";
|
|
310
520
|
var UPLOADS_AT_ONCE = 4;
|
|
311
521
|
var NAME_MAX = 200;
|
|
312
522
|
function nameOf(file) {
|
|
313
|
-
const own = file.name.split(/[\\/]/).pop()?.trim() || (0,
|
|
314
|
-
const fileExt = (0,
|
|
315
|
-
const ownExt = (0,
|
|
523
|
+
const own = file.name.split(/[\\/]/).pop()?.trim() || (file.path === void 0 ? "" : (0, import_node_path3.basename)(file.path));
|
|
524
|
+
const fileExt = file.path === void 0 ? "" : (0, import_node_path3.extname)(file.path);
|
|
525
|
+
const ownExt = (0, import_node_path3.extname)(own);
|
|
316
526
|
const typed = ownExt !== "" && (ownExt.toLowerCase() === fileExt.toLowerCase() || TYPE_BY_EXTENSION[ownExt.toLowerCase()] !== void 0);
|
|
317
527
|
const ext = typed ? ownExt : fileExt;
|
|
318
528
|
let stem = typed ? own.slice(0, -ownExt.length) : own;
|
|
@@ -327,8 +537,57 @@ var SKIPPED = {
|
|
|
327
537
|
type: "Plune takes PNG, JPEG and WebP",
|
|
328
538
|
limit: `a result holds at most ${SCREENSHOTS_PER_RESULT}`,
|
|
329
539
|
missing: "the file cannot be read on this machine",
|
|
330
|
-
size: "Plune takes a file of at most 2 MB"
|
|
540
|
+
size: "Plune takes a file of at most 2 MB",
|
|
541
|
+
textLimit: `a result holds at most ${TEXTS_PER_RESULT} text files`,
|
|
542
|
+
textSize: "Plune takes a text file of at most 512 KiB",
|
|
543
|
+
empty: "the file is empty",
|
|
544
|
+
binary: "the file holds a NUL byte, and Plune takes UTF-8 text without one",
|
|
545
|
+
encoding: "Plune takes UTF-8 text only",
|
|
546
|
+
noStorage: "no storage bucket is connected to this project"
|
|
547
|
+
};
|
|
548
|
+
var BUCKET_FOUND = "plune: storage bucket found for this project \u2014 screenshots, text, trace and video go straight to it over short-lived links.";
|
|
549
|
+
var KINDS = {
|
|
550
|
+
screenshot: { label: "screenshot", bytes: SCREENSHOT_BYTES, room: SCREENSHOTS_PER_RESULT, limit: "limit", size: "size" },
|
|
551
|
+
text: { label: "text file", bytes: TEXT_BYTES, room: TEXTS_PER_RESULT, limit: "textLimit", size: "textSize" },
|
|
552
|
+
// 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.
|
|
553
|
+
file: { label: "file", bytes: 0, room: 0, limit: "limit", size: "size" }
|
|
331
554
|
};
|
|
555
|
+
function kindOf(type) {
|
|
556
|
+
if (type.startsWith("image/")) return "screenshot";
|
|
557
|
+
return TEXT_TYPES.has(type) ? "text" : "file";
|
|
558
|
+
}
|
|
559
|
+
var codeOf = (out) => out.code ?? (out.status === null ? "network" : `http_${out.status}`);
|
|
560
|
+
function linkOf(item) {
|
|
561
|
+
const link = item;
|
|
562
|
+
if (typeof link?.id !== "string" || typeof link.url !== "string") return void 0;
|
|
563
|
+
const headers = typeof link.headers === "object" && link.headers !== null ? link.headers : {};
|
|
564
|
+
return { id: link.id, url: link.url, headers };
|
|
565
|
+
}
|
|
566
|
+
function textTypeOf(contentType, type) {
|
|
567
|
+
const charset = /;\s*charset\s*=\s*"?([^\s";]+)"?/i.exec(contentType)?.[1];
|
|
568
|
+
return charset?.toLowerCase() === "utf-8" ? `${type}; charset=utf-8` : type;
|
|
569
|
+
}
|
|
570
|
+
function textRefusal(bytes) {
|
|
571
|
+
if (bytes.length > TEXT_BYTES) return "textSize";
|
|
572
|
+
if (bytes.length === 0) return "empty";
|
|
573
|
+
if (bytes.includes(0)) return "binary";
|
|
574
|
+
try {
|
|
575
|
+
UTF8.decode(bytes);
|
|
576
|
+
} catch {
|
|
577
|
+
return "encoding";
|
|
578
|
+
}
|
|
579
|
+
return void 0;
|
|
580
|
+
}
|
|
581
|
+
function onDisk(result) {
|
|
582
|
+
if (!("files" in result) || result.files === void 0) return result;
|
|
583
|
+
const { files, ...rest } = result;
|
|
584
|
+
const kept = files.filter((f) => f.path !== void 0).map(({ body: _body, ...file }) => file);
|
|
585
|
+
return kept.length === 0 ? rest : { ...rest, files: kept };
|
|
586
|
+
}
|
|
587
|
+
function tally(counts, noun) {
|
|
588
|
+
if (counts.uploaded + counts.skipped + counts.failed === 0) return void 0;
|
|
589
|
+
return `${counts.uploaded} ${noun}(s) uploaded` + (counts.skipped > 0 ? `, ${counts.skipped} skipped` : "") + (counts.failed > 0 ? `, ${counts.failed} failed to upload` : "");
|
|
590
|
+
}
|
|
332
591
|
function boundKey(key) {
|
|
333
592
|
if (key.value.length <= KEY_MAX) return key;
|
|
334
593
|
const digest = (0, import_node_crypto.createHash)("sha256").update(key.value).digest("hex").slice(0, 16);
|
|
@@ -404,7 +663,9 @@ async function startRun(passed, declared) {
|
|
|
404
663
|
created: 0,
|
|
405
664
|
unoffered: 0,
|
|
406
665
|
deferred: 0,
|
|
407
|
-
screenshots: { uploaded: 0, skipped: 0, failed: 0 }
|
|
666
|
+
screenshots: { uploaded: 0, skipped: 0, failed: 0 },
|
|
667
|
+
textFiles: { uploaded: 0, skipped: 0, failed: 0 },
|
|
668
|
+
otherFiles: { uploaded: 0, skipped: 0, failed: 0 }
|
|
408
669
|
};
|
|
409
670
|
const resolved = /* @__PURE__ */ new Map();
|
|
410
671
|
let buffer = [];
|
|
@@ -415,6 +676,13 @@ async function startRun(passed, declared) {
|
|
|
415
676
|
let done = false;
|
|
416
677
|
const shortened = /* @__PURE__ */ new Set();
|
|
417
678
|
const skipsSaid = /* @__PURE__ */ new Set();
|
|
679
|
+
const countsOf = (kind) => kind === "text" ? stats.textFiles : kind === "file" ? stats.otherFiles : stats.screenshots;
|
|
680
|
+
let bucketSeen = false;
|
|
681
|
+
let noStorage = 0;
|
|
682
|
+
let uploadsMissing = false;
|
|
683
|
+
const failedSaid = /* @__PURE__ */ new Set();
|
|
684
|
+
const codesSaid = /* @__PURE__ */ new Set();
|
|
685
|
+
const rooms = /* @__PURE__ */ new Map();
|
|
418
686
|
function bound(keys) {
|
|
419
687
|
return keys.map((key) => {
|
|
420
688
|
const fit = boundKey(key);
|
|
@@ -422,7 +690,7 @@ async function startRun(passed, declared) {
|
|
|
422
690
|
shortened.add(key.value);
|
|
423
691
|
if (shortened.size === 1) {
|
|
424
692
|
log(
|
|
425
|
-
`plune: a test name longer than ${KEY_MAX} characters was shortened to fit its key \u2014
|
|
693
|
+
`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.`
|
|
426
694
|
);
|
|
427
695
|
}
|
|
428
696
|
}
|
|
@@ -442,7 +710,7 @@ async function startRun(passed, declared) {
|
|
|
442
710
|
externalKey,
|
|
443
711
|
session: sessionMarker,
|
|
444
712
|
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
445
|
-
results
|
|
713
|
+
results: results.map(onDisk)
|
|
446
714
|
});
|
|
447
715
|
stats.deferred += results.length;
|
|
448
716
|
}
|
|
@@ -550,57 +818,173 @@ async function startRun(passed, declared) {
|
|
|
550
818
|
stats.created += out.body.results.filter((r) => r?.outcome === "created").length;
|
|
551
819
|
}
|
|
552
820
|
}
|
|
553
|
-
function skip(name, why) {
|
|
554
|
-
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
|
|
821
|
+
function skip(kind, name, why) {
|
|
822
|
+
countsOf(kind).skipped += 1;
|
|
823
|
+
if (why === "noStorage") noStorage += 1;
|
|
824
|
+
const said = why === "noStorage" ? why : `${kind}/${why}`;
|
|
825
|
+
if (skipsSaid.has(said)) return;
|
|
826
|
+
skipsSaid.add(said);
|
|
827
|
+
if (why === "noStorage") log(`plune: "${name}" not stored \u2014 ${SKIPPED[why]}.`);
|
|
828
|
+
else log(`plune: ${KINDS[kind].label} "${name}" not uploaded \u2014 ${SKIPPED[why]}.`);
|
|
829
|
+
}
|
|
830
|
+
function ask(job, size) {
|
|
831
|
+
return client.post(`/v1/results/${encodeURIComponent(job.id)}/files/uploads`, {
|
|
832
|
+
files: [{ name: job.name, contentType: job.type, size }]
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
function failed(job, why) {
|
|
836
|
+
countsOf(job.kind).failed += 1;
|
|
837
|
+
if (!failedSaid.has(job.kind)) {
|
|
838
|
+
failedSaid.add(job.kind);
|
|
839
|
+
log(`plune: could not upload a ${KINDS[job.kind].label} (${why}) \u2014 its result is reported without it.`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function failedToBucket(job, code) {
|
|
843
|
+
countsOf(job.kind).failed += 1;
|
|
844
|
+
if (codesSaid.has(code)) return;
|
|
845
|
+
codesSaid.add(code);
|
|
846
|
+
log(`plune: "${job.name}" not uploaded to your bucket (${code}) \u2014 its result is reported without it.`);
|
|
558
847
|
}
|
|
559
|
-
|
|
848
|
+
function refusedHere(job, size) {
|
|
849
|
+
if (job.kind === "screenshot") {
|
|
850
|
+
if (!SCREENSHOT_TYPES.has(job.type)) return "type";
|
|
851
|
+
if (size > SCREENSHOT_BYTES) return "size";
|
|
852
|
+
}
|
|
853
|
+
if (job.kind === "text" && size > TEXT_BYTES) return "textSize";
|
|
854
|
+
return void 0;
|
|
855
|
+
}
|
|
856
|
+
async function uploadDirect(job, size) {
|
|
857
|
+
const { kind } = job;
|
|
858
|
+
if (kind === "file") return skip(kind, job.name, "noStorage");
|
|
859
|
+
const { bytes: ceiling, size: tooBig } = KINDS[kind];
|
|
860
|
+
if (size > ceiling) return skip(kind, job.name, tooBig);
|
|
560
861
|
let bytes;
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
862
|
+
if (job.body !== void 0) bytes = job.body;
|
|
863
|
+
else {
|
|
864
|
+
try {
|
|
865
|
+
bytes = await import_node_fs4.promises.readFile(job.path);
|
|
866
|
+
} catch {
|
|
867
|
+
return skip(kind, job.name, "missing");
|
|
868
|
+
}
|
|
566
869
|
}
|
|
870
|
+
const refusal = kind === "text" ? textRefusal(bytes) : void 0;
|
|
871
|
+
if (refusal !== void 0) return skip(kind, job.name, refusal);
|
|
567
872
|
const out = await client.upload(
|
|
568
|
-
`/v1/results/${encodeURIComponent(
|
|
873
|
+
`/v1/results/${encodeURIComponent(job.id)}/files?name=${encodeURIComponent(job.name)}`,
|
|
569
874
|
bytes,
|
|
570
|
-
|
|
875
|
+
job.type
|
|
571
876
|
);
|
|
572
|
-
if (out.ok)
|
|
573
|
-
|
|
574
|
-
|
|
877
|
+
if (out.ok) countsOf(kind).uploaded += 1;
|
|
878
|
+
else failed(job, out.detail || out.kind);
|
|
879
|
+
}
|
|
880
|
+
async function uploadToBucket(job, size, first) {
|
|
881
|
+
const source = job.body !== void 0 ? { body: job.body } : { path: job.path, size };
|
|
882
|
+
let link = first;
|
|
883
|
+
for (let renewed = false; ; ) {
|
|
884
|
+
const put = await client.put(link.url, link.headers, source);
|
|
885
|
+
if (put.ok) break;
|
|
886
|
+
if (put.status !== 403 || renewed) return failedToBucket(job, put.code);
|
|
887
|
+
renewed = true;
|
|
888
|
+
const again = await ask(job, size);
|
|
889
|
+
if (!again.ok) return failedToBucket(job, codeOf(again));
|
|
890
|
+
const fresh = linkOf(again.body?.files?.[0]);
|
|
891
|
+
if (fresh === void 0) return failedToBucket(job, "bad_answer");
|
|
892
|
+
link = fresh;
|
|
893
|
+
}
|
|
894
|
+
const done2 = await client.post(`/v1/files/${encodeURIComponent(link.id)}/complete`, {});
|
|
895
|
+
if (!done2.ok) return failedToBucket(job, codeOf(done2));
|
|
896
|
+
countsOf(job.kind).uploaded += 1;
|
|
897
|
+
}
|
|
898
|
+
let free = UPLOADS_AT_ONCE;
|
|
899
|
+
const waiting = [];
|
|
900
|
+
async function gated(work) {
|
|
901
|
+
if (free === 0) await new Promise((resolve2) => waiting.push(resolve2));
|
|
902
|
+
else free -= 1;
|
|
903
|
+
try {
|
|
904
|
+
return await work();
|
|
905
|
+
} finally {
|
|
906
|
+
const next = waiting.shift();
|
|
907
|
+
if (next !== void 0) next();
|
|
908
|
+
else free += 1;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
async function plan(job) {
|
|
912
|
+
const { kind } = job;
|
|
913
|
+
let size;
|
|
914
|
+
if (job.body !== void 0) size = job.body.length;
|
|
915
|
+
else if (job.path === void 0) return void skip(kind, job.name, "missing");
|
|
916
|
+
else {
|
|
917
|
+
try {
|
|
918
|
+
size = (await import_node_fs4.promises.stat(job.path)).size;
|
|
919
|
+
} catch {
|
|
920
|
+
return void skip(kind, job.name, "missing");
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (size === 0) return void skip(kind, job.name, "empty");
|
|
924
|
+
const direct = () => {
|
|
925
|
+
if (kind === "file") return void skip(kind, job.name, "noStorage");
|
|
926
|
+
const roomKey = `${job.id}/${kind}`;
|
|
927
|
+
const left = rooms.get(roomKey) ?? KINDS[kind].room;
|
|
928
|
+
if (left === 0) return void skip(kind, job.name, KINDS[kind].limit);
|
|
929
|
+
rooms.set(roomKey, left - 1);
|
|
930
|
+
return () => uploadDirect(job, size);
|
|
931
|
+
};
|
|
932
|
+
if (uploadsMissing) return direct();
|
|
933
|
+
const out = await gated(() => ask(job, size));
|
|
934
|
+
if (!out.ok) {
|
|
935
|
+
if (out.status === 404 && out.detail === "" && out.code === void 0) {
|
|
936
|
+
uploadsMissing = true;
|
|
937
|
+
return direct();
|
|
938
|
+
}
|
|
939
|
+
if (out.code !== void 0) failedToBucket(job, out.code);
|
|
940
|
+
else failed(job, out.detail || out.kind);
|
|
941
|
+
return void 0;
|
|
575
942
|
}
|
|
576
|
-
|
|
577
|
-
if (
|
|
578
|
-
|
|
943
|
+
const item = out.body?.files?.[0];
|
|
944
|
+
if (item?.skipped === "no_storage") return void skip(kind, job.name, refusedHere(job, size) ?? "noStorage");
|
|
945
|
+
if (item?.direct === true) return direct();
|
|
946
|
+
const link = linkOf(item);
|
|
947
|
+
if (link === void 0) return void failed(job, "bad_answer");
|
|
948
|
+
if (!bucketSeen) {
|
|
949
|
+
bucketSeen = true;
|
|
950
|
+
log(BUCKET_FOUND);
|
|
579
951
|
}
|
|
952
|
+
return () => uploadToBucket(job, size, link);
|
|
580
953
|
}
|
|
581
954
|
async function upload(items, filesByKey) {
|
|
582
955
|
if (filesByKey.size === 0 || !Array.isArray(items)) return;
|
|
583
|
-
const
|
|
956
|
+
const jobs = [];
|
|
584
957
|
for (const item of items) {
|
|
585
958
|
if (item?.status !== "accepted" || typeof item.id !== "string" || typeof item.resultKey !== "string") continue;
|
|
586
|
-
let room = SCREENSHOTS_PER_RESULT;
|
|
587
959
|
for (const file of filesByKey.get(item.resultKey) ?? []) {
|
|
588
|
-
const type = file.contentType
|
|
589
|
-
if (
|
|
960
|
+
const type = mediaTypeOf(file.contentType) ?? (file.path === void 0 ? void 0 : TYPE_BY_EXTENSION[(0, import_node_path3.extname)(file.path).toLowerCase()]);
|
|
961
|
+
if (type === void 0 || type === METADATA_TYPE2) continue;
|
|
962
|
+
const kind = kindOf(type);
|
|
963
|
+
if (kind !== "text" && file.path === void 0) continue;
|
|
590
964
|
const name = nameOf(file);
|
|
591
|
-
if (file.name.startsWith("_")) skip(name, "hidden");
|
|
592
|
-
else if (!SCREENSHOT_TYPES.has(type)) skip(name, "type");
|
|
593
|
-
else if (room === 0) skip(name, "limit");
|
|
965
|
+
if (file.name.startsWith("_")) skip(kind, name, "hidden");
|
|
594
966
|
else {
|
|
595
|
-
|
|
596
|
-
|
|
967
|
+
jobs.push({
|
|
968
|
+
id: item.id,
|
|
969
|
+
kind,
|
|
970
|
+
name,
|
|
971
|
+
type: kind === "text" ? textTypeOf(file.contentType ?? "", type) : type,
|
|
972
|
+
...file.body !== void 0 ? { body: file.body } : {},
|
|
973
|
+
...file.path !== void 0 ? { path: file.path } : {}
|
|
974
|
+
});
|
|
597
975
|
}
|
|
598
976
|
}
|
|
599
977
|
}
|
|
600
|
-
|
|
978
|
+
const byResult = /* @__PURE__ */ new Map();
|
|
979
|
+
for (const job of jobs) byResult.set(job.id, [...byResult.get(job.id) ?? [], job]);
|
|
601
980
|
await Promise.all(
|
|
602
|
-
|
|
603
|
-
|
|
981
|
+
[...byResult.values()].map(async (group) => {
|
|
982
|
+
const work = [];
|
|
983
|
+
for (const job of group) {
|
|
984
|
+
const act = await plan(job);
|
|
985
|
+
if (act !== void 0) work.push(gated(act));
|
|
986
|
+
}
|
|
987
|
+
await Promise.all(work);
|
|
604
988
|
})
|
|
605
989
|
);
|
|
606
990
|
}
|
|
@@ -678,11 +1062,20 @@ async function startRun(passed, declared) {
|
|
|
678
1062
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
679
1063
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
680
1064
|
if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
|
|
681
|
-
const
|
|
682
|
-
|
|
1065
|
+
const kinds = [stats.screenshots, stats.textFiles, stats.otherFiles];
|
|
1066
|
+
const sum = (what) => kinds.reduce((n, counts) => n + counts[what], 0);
|
|
1067
|
+
if (bucketSeen) {
|
|
1068
|
+
const other = sum("skipped") - noStorage;
|
|
683
1069
|
parts.push(
|
|
684
|
-
`${
|
|
1070
|
+
`${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` : "")
|
|
685
1071
|
);
|
|
1072
|
+
} else {
|
|
1073
|
+
for (const said of [tally(stats.screenshots, "screenshot"), tally(stats.textFiles, "text file")]) {
|
|
1074
|
+
if (said !== void 0) parts.push(said);
|
|
1075
|
+
}
|
|
1076
|
+
const other = stats.otherFiles;
|
|
1077
|
+
if (other.skipped > 0) parts.push(`${other.skipped} file(s) not stored (no bucket connected)`);
|
|
1078
|
+
if (other.failed > 0) parts.push(`${other.failed} file(s) failed to upload`);
|
|
686
1079
|
}
|
|
687
1080
|
if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
|
|
688
1081
|
if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
|
|
@@ -749,134 +1142,6 @@ function resultKey(testId, retry) {
|
|
|
749
1142
|
const plain = `${testId}#${retry}`;
|
|
750
1143
|
return plain.length <= RESULT_KEY_MAX ? plain : `${sha256(testId).slice(0, HASH_CHARS)}#${retry}`;
|
|
751
1144
|
}
|
|
752
|
-
|
|
753
|
-
// src/reporter-core/failure-detail.ts
|
|
754
|
-
var import_node_fs3 = require("fs");
|
|
755
|
-
var import_node_os2 = require("os");
|
|
756
|
-
var import_node_path3 = require("path");
|
|
757
|
-
var import_node_util = require("util");
|
|
758
|
-
function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
759
|
-
const error = chosen(attempt);
|
|
760
|
-
const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
|
|
761
|
-
const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
|
|
762
|
-
const steps = chainOf(attempt.steps);
|
|
763
|
-
const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
|
|
764
|
-
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 !== "");
|
|
765
|
-
const ciUrl = webLink(attempt.buildHref);
|
|
766
|
-
const failure = {
|
|
767
|
-
...headline !== void 0 ? { headline } : {},
|
|
768
|
-
...steps.length > 0 ? { steps } : {},
|
|
769
|
-
...location !== void 0 ? { location } : {},
|
|
770
|
-
...artifacts.length > 0 ? { artifacts } : {},
|
|
771
|
-
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
772
|
-
};
|
|
773
|
-
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
774
|
-
}
|
|
775
|
-
function repoRootOf(dir, stopAt) {
|
|
776
|
-
for (let here = (0, import_node_path3.resolve)(dir); ; here = (0, import_node_path3.dirname)(here)) {
|
|
777
|
-
if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(here, ".git"))) return here;
|
|
778
|
-
if (here === stopAt || (0, import_node_path3.dirname)(here) === here) return void 0;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
782
|
-
const text = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
783
|
-
return cut(machineless(text, attempt.repoRoot, home));
|
|
784
|
-
}
|
|
785
|
-
function filesOf(attempt) {
|
|
786
|
-
return attempt.attachments.filter((a) => a.path !== void 0);
|
|
787
|
-
}
|
|
788
|
-
function machineless(raw, repoRoot, home) {
|
|
789
|
-
let text = (0, import_node_util.stripVTControlCharacters)(raw).replaceAll("\x1B", "");
|
|
790
|
-
const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
|
|
791
|
-
if (root !== void 0) text = text.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
|
|
792
|
-
const own = pathIn(home);
|
|
793
|
-
if (own !== void 0) text = text.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
|
|
794
|
-
if (repoRoot === void 0) text = text.replace(ANY_HOME, "~");
|
|
795
|
-
return text;
|
|
796
|
-
}
|
|
797
|
-
var PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
798
|
-
var TEXT_LIMIT = 524288;
|
|
799
|
-
var HEADLINE_LIMIT = 8 * 1024;
|
|
800
|
-
var jsonBytes = (text) => Buffer.byteLength(JSON.stringify(text)) - 2;
|
|
801
|
-
var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:(?:file|webpack|webpack-internal):(?:\\*\/){2,3})?(?:\\*\/home\\*\/|\\*\/Users\\*\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
|
|
802
|
-
function pathIn(path3) {
|
|
803
|
-
const parts = path3.split(/[\\/]+/);
|
|
804
|
-
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
805
|
-
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
806
|
-
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
807
|
-
const escaped = /^\/(?!\/)/.test(path3) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
808
|
-
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
809
|
-
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
810
|
-
}
|
|
811
|
-
var fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
812
|
-
function cut(text) {
|
|
813
|
-
if (jsonBytes(text) <= TEXT_LIMIT) return text;
|
|
814
|
-
const lines = text.split("\n");
|
|
815
|
-
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
816
|
-
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
817
|
-
let used = 0;
|
|
818
|
-
let head = 0;
|
|
819
|
-
let tail = lines.length;
|
|
820
|
-
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
821
|
-
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
822
|
-
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
823
|
-
}
|
|
824
|
-
var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
825
|
-
var METADATA_TYPE = "application/plune.metadata+json";
|
|
826
|
-
var REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
827
|
-
var FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
828
|
-
var FRAME_MAX = 8192;
|
|
829
|
-
var firstLine = (text) => (0, import_node_util.stripVTControlCharacters)(text).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
830
|
-
function chosen({ status, errors }) {
|
|
831
|
-
if (status === "timedOut") {
|
|
832
|
-
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
833
|
-
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
834
|
-
}
|
|
835
|
-
return errors[0];
|
|
836
|
-
}
|
|
837
|
-
function chainOf(steps) {
|
|
838
|
-
const chain = [];
|
|
839
|
-
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
840
|
-
chain.push(failed.title);
|
|
841
|
-
}
|
|
842
|
-
return chain;
|
|
843
|
-
}
|
|
844
|
-
function placeOf(error, testFile) {
|
|
845
|
-
const own = normal(testFile);
|
|
846
|
-
let first;
|
|
847
|
-
for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
|
|
848
|
-
if (line.length > FRAME_MAX) continue;
|
|
849
|
-
const frame = FRAME.exec(line);
|
|
850
|
-
if (frame === null) continue;
|
|
851
|
-
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
852
|
-
if (normal(place.file) === own) return place;
|
|
853
|
-
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
854
|
-
}
|
|
855
|
-
return error.location ?? first;
|
|
856
|
-
}
|
|
857
|
-
function fileOf(raw) {
|
|
858
|
-
if (!raw.startsWith("file://")) return raw;
|
|
859
|
-
let path3 = raw.slice("file://".length);
|
|
860
|
-
try {
|
|
861
|
-
path3 = decodeURIComponent(path3);
|
|
862
|
-
} catch {
|
|
863
|
-
}
|
|
864
|
-
return /^\/[A-Za-z]:\//.test(path3) ? path3.slice(1) : path3;
|
|
865
|
-
}
|
|
866
|
-
var normal = (path3) => import_node_path3.posix.normalize(path3.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
867
|
-
function fromRoot(place, root) {
|
|
868
|
-
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
869
|
-
const base = normal(root).replace(/\/+$/, "");
|
|
870
|
-
const file = normal(place.file);
|
|
871
|
-
if (!file.startsWith(`${base}/`)) return void 0;
|
|
872
|
-
const relative = file.slice(base.length + 1);
|
|
873
|
-
if (!REPO_RELATIVE.test(relative)) return void 0;
|
|
874
|
-
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
875
|
-
return column === void 0 ? { file: relative, line: place.line } : { file: relative, line: place.line, column };
|
|
876
|
-
}
|
|
877
|
-
function webLink(href) {
|
|
878
|
-
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
879
|
-
}
|
|
880
1145
|
// Annotate the CommonJS export names for ESM import in node:
|
|
881
1146
|
0 && (module.exports = {
|
|
882
1147
|
DEFAULT_FALLBACK_PATH,
|