@plune-ai/cli 0.16.0 → 0.17.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/README.md +5 -0
- package/dist/cli.cjs +254 -184
- package/dist/reporter-core.cjs +230 -169
- package/dist/reporter-core.d.cts +30 -6
- package/dist/reporter-core.d.ts +30 -6
- package/dist/reporter-core.js +227 -166
- package/package.json +1 -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";
|
|
@@ -222,6 +223,137 @@ function ciCommit(reported, env = process.env) {
|
|
|
222
223
|
};
|
|
223
224
|
}
|
|
224
225
|
|
|
226
|
+
// src/reporter-core/failure-detail.ts
|
|
227
|
+
import { existsSync as existsSync2 } from "fs";
|
|
228
|
+
import { homedir as homedir2 } from "os";
|
|
229
|
+
import { dirname as dirname2, join as join2, posix, resolve } from "path";
|
|
230
|
+
import { stripVTControlCharacters } from "util";
|
|
231
|
+
function failureOf(attempt, home = homedir2()) {
|
|
232
|
+
const error = chosen(attempt);
|
|
233
|
+
const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
|
|
234
|
+
const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
|
|
235
|
+
const steps = chainOf(attempt.steps);
|
|
236
|
+
const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
|
|
237
|
+
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 !== "");
|
|
238
|
+
const ciUrl = webLink(attempt.buildHref);
|
|
239
|
+
const failure = {
|
|
240
|
+
...headline !== void 0 ? { headline } : {},
|
|
241
|
+
...steps.length > 0 ? { steps } : {},
|
|
242
|
+
...location !== void 0 ? { location } : {},
|
|
243
|
+
...artifacts.length > 0 ? { artifacts } : {},
|
|
244
|
+
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
245
|
+
};
|
|
246
|
+
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
247
|
+
}
|
|
248
|
+
function repoRootOf(dir, stopAt) {
|
|
249
|
+
for (let here = resolve(dir); ; here = dirname2(here)) {
|
|
250
|
+
if (existsSync2(join2(here, ".git"))) return here;
|
|
251
|
+
if (here === stopAt || dirname2(here) === here) return void 0;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function errorContextOf(attempt, home = homedir2()) {
|
|
255
|
+
const text = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
256
|
+
return cut(machineless(text, attempt.repoRoot, home));
|
|
257
|
+
}
|
|
258
|
+
var mediaTypeOf = (contentType) => contentType?.split(";")[0]?.trim().toLowerCase() || void 0;
|
|
259
|
+
var TEXT_TYPES = /* @__PURE__ */ new Set(["application/json", "text/plain"]);
|
|
260
|
+
var isTextType = (contentType) => TEXT_TYPES.has(mediaTypeOf(contentType) ?? "");
|
|
261
|
+
function filesOf(attempt) {
|
|
262
|
+
return attempt.attachments.filter((a) => a.path !== void 0 || a.body !== void 0 && isTextType(a.contentType));
|
|
263
|
+
}
|
|
264
|
+
function machineless(raw, repoRoot, home) {
|
|
265
|
+
let text = stripVTControlCharacters(raw).replaceAll("\x1B", "");
|
|
266
|
+
const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
|
|
267
|
+
if (root !== void 0) text = text.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
|
|
268
|
+
const own = pathIn(home);
|
|
269
|
+
if (own !== void 0) text = text.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
|
|
270
|
+
if (repoRoot === void 0) text = text.replace(ANY_HOME, "~");
|
|
271
|
+
return text;
|
|
272
|
+
}
|
|
273
|
+
var PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
274
|
+
var TEXT_LIMIT = 524288;
|
|
275
|
+
var HEADLINE_LIMIT = 8 * 1024;
|
|
276
|
+
var jsonBytes = (text) => Buffer.byteLength(JSON.stringify(text)) - 2;
|
|
277
|
+
var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:(?:file|webpack|webpack-internal):(?:\\*\/){2,3})?(?:\\*\/home\\*\/|\\*\/Users\\*\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
|
|
278
|
+
function pathIn(path3) {
|
|
279
|
+
const parts = path3.split(/[\\/]+/);
|
|
280
|
+
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
281
|
+
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
282
|
+
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
283
|
+
const escaped = /^\/(?!\/)/.test(path3) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
284
|
+
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
285
|
+
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
286
|
+
}
|
|
287
|
+
var fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
288
|
+
function cut(text) {
|
|
289
|
+
if (jsonBytes(text) <= TEXT_LIMIT) return text;
|
|
290
|
+
const lines = text.split("\n");
|
|
291
|
+
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
292
|
+
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
293
|
+
let used = 0;
|
|
294
|
+
let head = 0;
|
|
295
|
+
let tail = lines.length;
|
|
296
|
+
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
297
|
+
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
298
|
+
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
299
|
+
}
|
|
300
|
+
var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
301
|
+
var METADATA_TYPE = "application/plune.metadata+json";
|
|
302
|
+
var REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
303
|
+
var FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
304
|
+
var FRAME_MAX = 8192;
|
|
305
|
+
var firstLine = (text) => stripVTControlCharacters(text).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
306
|
+
function chosen({ status, errors }) {
|
|
307
|
+
if (status === "timedOut") {
|
|
308
|
+
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
309
|
+
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
310
|
+
}
|
|
311
|
+
return errors[0];
|
|
312
|
+
}
|
|
313
|
+
function chainOf(steps) {
|
|
314
|
+
const chain = [];
|
|
315
|
+
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
316
|
+
chain.push(failed.title);
|
|
317
|
+
}
|
|
318
|
+
return chain;
|
|
319
|
+
}
|
|
320
|
+
function placeOf(error, testFile) {
|
|
321
|
+
const own = normal(testFile);
|
|
322
|
+
let first;
|
|
323
|
+
for (const line of stripVTControlCharacters(error.text).split("\n")) {
|
|
324
|
+
if (line.length > FRAME_MAX) continue;
|
|
325
|
+
const frame = FRAME.exec(line);
|
|
326
|
+
if (frame === null) continue;
|
|
327
|
+
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
328
|
+
if (normal(place.file) === own) return place;
|
|
329
|
+
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
330
|
+
}
|
|
331
|
+
return error.location ?? first;
|
|
332
|
+
}
|
|
333
|
+
function fileOf(raw) {
|
|
334
|
+
if (!raw.startsWith("file://")) return raw;
|
|
335
|
+
let path3 = raw.slice("file://".length);
|
|
336
|
+
try {
|
|
337
|
+
path3 = decodeURIComponent(path3);
|
|
338
|
+
} catch {
|
|
339
|
+
}
|
|
340
|
+
return /^\/[A-Za-z]:\//.test(path3) ? path3.slice(1) : path3;
|
|
341
|
+
}
|
|
342
|
+
var normal = (path3) => posix.normalize(path3.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
343
|
+
function fromRoot(place, root) {
|
|
344
|
+
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
345
|
+
const base = normal(root).replace(/\/+$/, "");
|
|
346
|
+
const file = normal(place.file);
|
|
347
|
+
if (!file.startsWith(`${base}/`)) return void 0;
|
|
348
|
+
const relative = file.slice(base.length + 1);
|
|
349
|
+
if (!REPO_RELATIVE.test(relative)) return void 0;
|
|
350
|
+
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
351
|
+
return column === void 0 ? { file: relative, line: place.line } : { file: relative, line: place.line, column };
|
|
352
|
+
}
|
|
353
|
+
function webLink(href) {
|
|
354
|
+
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
225
357
|
// src/reporter-core/fallback.ts
|
|
226
358
|
import * as fs from "fs";
|
|
227
359
|
import * as path2 from "path";
|
|
@@ -252,6 +384,9 @@ var COUNTED = ["accepted", "duplicate", "conflict", "rejected"];
|
|
|
252
384
|
var SCREENSHOT_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
253
385
|
var SCREENSHOT_BYTES = 2 * 1024 * 1024;
|
|
254
386
|
var SCREENSHOTS_PER_RESULT = 20;
|
|
387
|
+
var TEXT_BYTES = 512 * 1024;
|
|
388
|
+
var TEXTS_PER_RESULT = 10;
|
|
389
|
+
var UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
255
390
|
var TYPE_BY_EXTENSION = {
|
|
256
391
|
".png": "image/png",
|
|
257
392
|
".jpg": "image/jpeg",
|
|
@@ -261,8 +396,8 @@ var TYPE_BY_EXTENSION = {
|
|
|
261
396
|
var UPLOADS_AT_ONCE = 4;
|
|
262
397
|
var NAME_MAX = 200;
|
|
263
398
|
function nameOf(file) {
|
|
264
|
-
const own = file.name.split(/[\\/]/).pop()?.trim() || basename2(file.path);
|
|
265
|
-
const fileExt = extname(file.path);
|
|
399
|
+
const own = file.name.split(/[\\/]/).pop()?.trim() || (file.path === void 0 ? "" : basename2(file.path));
|
|
400
|
+
const fileExt = file.path === void 0 ? "" : extname(file.path);
|
|
266
401
|
const ownExt = extname(own);
|
|
267
402
|
const typed = ownExt !== "" && (ownExt.toLowerCase() === fileExt.toLowerCase() || TYPE_BY_EXTENSION[ownExt.toLowerCase()] !== void 0);
|
|
268
403
|
const ext = typed ? ownExt : fileExt;
|
|
@@ -278,8 +413,46 @@ var SKIPPED = {
|
|
|
278
413
|
type: "Plune takes PNG, JPEG and WebP",
|
|
279
414
|
limit: `a result holds at most ${SCREENSHOTS_PER_RESULT}`,
|
|
280
415
|
missing: "the file cannot be read on this machine",
|
|
281
|
-
size: "Plune takes a file of at most 2 MB"
|
|
416
|
+
size: "Plune takes a file of at most 2 MB",
|
|
417
|
+
textLimit: `a result holds at most ${TEXTS_PER_RESULT} text files`,
|
|
418
|
+
textSize: "Plune takes a text file of at most 512 KiB",
|
|
419
|
+
empty: "the file is empty",
|
|
420
|
+
binary: "the file holds a NUL byte, and Plune takes UTF-8 text without one",
|
|
421
|
+
encoding: "Plune takes UTF-8 text only"
|
|
422
|
+
};
|
|
423
|
+
var KINDS = {
|
|
424
|
+
screenshot: { label: "screenshot", bytes: SCREENSHOT_BYTES, room: SCREENSHOTS_PER_RESULT, limit: "limit", size: "size" },
|
|
425
|
+
text: { label: "text file", bytes: TEXT_BYTES, room: TEXTS_PER_RESULT, limit: "textLimit", size: "textSize" }
|
|
282
426
|
};
|
|
427
|
+
function kindOf(type) {
|
|
428
|
+
if (type.startsWith("image/")) return "screenshot";
|
|
429
|
+
return TEXT_TYPES.has(type) ? "text" : void 0;
|
|
430
|
+
}
|
|
431
|
+
function textTypeOf(contentType, type) {
|
|
432
|
+
const charset = /;\s*charset\s*=\s*"?([^\s";]+)"?/i.exec(contentType)?.[1];
|
|
433
|
+
return charset?.toLowerCase() === "utf-8" ? `${type}; charset=utf-8` : type;
|
|
434
|
+
}
|
|
435
|
+
function textRefusal(bytes) {
|
|
436
|
+
if (bytes.length > TEXT_BYTES) return "textSize";
|
|
437
|
+
if (bytes.length === 0) return "empty";
|
|
438
|
+
if (bytes.includes(0)) return "binary";
|
|
439
|
+
try {
|
|
440
|
+
UTF8.decode(bytes);
|
|
441
|
+
} catch {
|
|
442
|
+
return "encoding";
|
|
443
|
+
}
|
|
444
|
+
return void 0;
|
|
445
|
+
}
|
|
446
|
+
function onDisk(result) {
|
|
447
|
+
if (!("files" in result) || result.files === void 0) return result;
|
|
448
|
+
const { files, ...rest } = result;
|
|
449
|
+
const kept = files.filter((f) => f.path !== void 0).map(({ body: _body, ...file }) => file);
|
|
450
|
+
return kept.length === 0 ? rest : { ...rest, files: kept };
|
|
451
|
+
}
|
|
452
|
+
function tally(counts, noun) {
|
|
453
|
+
if (counts.uploaded + counts.skipped + counts.failed === 0) return void 0;
|
|
454
|
+
return `${counts.uploaded} ${noun}(s) uploaded` + (counts.skipped > 0 ? `, ${counts.skipped} skipped` : "") + (counts.failed > 0 ? `, ${counts.failed} failed to upload` : "");
|
|
455
|
+
}
|
|
283
456
|
function boundKey(key) {
|
|
284
457
|
if (key.value.length <= KEY_MAX) return key;
|
|
285
458
|
const digest = createHash("sha256").update(key.value).digest("hex").slice(0, 16);
|
|
@@ -355,7 +528,8 @@ async function startRun(passed, declared) {
|
|
|
355
528
|
created: 0,
|
|
356
529
|
unoffered: 0,
|
|
357
530
|
deferred: 0,
|
|
358
|
-
screenshots: { uploaded: 0, skipped: 0, failed: 0 }
|
|
531
|
+
screenshots: { uploaded: 0, skipped: 0, failed: 0 },
|
|
532
|
+
textFiles: { uploaded: 0, skipped: 0, failed: 0 }
|
|
359
533
|
};
|
|
360
534
|
const resolved = /* @__PURE__ */ new Map();
|
|
361
535
|
let buffer = [];
|
|
@@ -366,6 +540,7 @@ async function startRun(passed, declared) {
|
|
|
366
540
|
let done = false;
|
|
367
541
|
const shortened = /* @__PURE__ */ new Set();
|
|
368
542
|
const skipsSaid = /* @__PURE__ */ new Set();
|
|
543
|
+
const countsOf = (kind) => kind === "text" ? stats.textFiles : stats.screenshots;
|
|
369
544
|
function bound(keys) {
|
|
370
545
|
return keys.map((key) => {
|
|
371
546
|
const fit = boundKey(key);
|
|
@@ -393,7 +568,7 @@ async function startRun(passed, declared) {
|
|
|
393
568
|
externalKey,
|
|
394
569
|
session: sessionMarker,
|
|
395
570
|
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
396
|
-
results
|
|
571
|
+
results: results.map(onDisk)
|
|
397
572
|
});
|
|
398
573
|
stats.deferred += results.length;
|
|
399
574
|
}
|
|
@@ -501,57 +676,74 @@ async function startRun(passed, declared) {
|
|
|
501
676
|
stats.created += out.body.results.filter((r) => r?.outcome === "created").length;
|
|
502
677
|
}
|
|
503
678
|
}
|
|
504
|
-
function skip(name, why) {
|
|
505
|
-
|
|
506
|
-
if (skipsSaid.has(why)) return;
|
|
507
|
-
skipsSaid.add(why);
|
|
508
|
-
log(`plune:
|
|
679
|
+
function skip(kind, name, why) {
|
|
680
|
+
countsOf(kind).skipped += 1;
|
|
681
|
+
if (skipsSaid.has(`${kind}/${why}`)) return;
|
|
682
|
+
skipsSaid.add(`${kind}/${why}`);
|
|
683
|
+
log(`plune: ${KINDS[kind].label} "${name}" not uploaded \u2014 ${SKIPPED[why]}.`);
|
|
509
684
|
}
|
|
510
|
-
async function uploadOne(
|
|
685
|
+
async function uploadOne(job) {
|
|
686
|
+
const { kind } = job;
|
|
687
|
+
const { label, bytes: ceiling, size } = KINDS[kind];
|
|
511
688
|
let bytes;
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
689
|
+
if (job.body !== void 0) bytes = job.body;
|
|
690
|
+
else if (job.path === void 0) return skip(kind, job.name, "missing");
|
|
691
|
+
else {
|
|
692
|
+
try {
|
|
693
|
+
if ((await disk.stat(job.path)).size > ceiling) return skip(kind, job.name, size);
|
|
694
|
+
bytes = await disk.readFile(job.path);
|
|
695
|
+
} catch {
|
|
696
|
+
return skip(kind, job.name, "missing");
|
|
697
|
+
}
|
|
517
698
|
}
|
|
699
|
+
const refusal = kind === "text" ? textRefusal(bytes) : void 0;
|
|
700
|
+
if (refusal !== void 0) return skip(kind, job.name, refusal);
|
|
518
701
|
const out = await client.upload(
|
|
519
|
-
`/v1/results/${encodeURIComponent(
|
|
702
|
+
`/v1/results/${encodeURIComponent(job.id)}/files?name=${encodeURIComponent(job.name)}`,
|
|
520
703
|
bytes,
|
|
521
|
-
|
|
704
|
+
job.type
|
|
522
705
|
);
|
|
706
|
+
const counts = countsOf(kind);
|
|
523
707
|
if (out.ok) {
|
|
524
|
-
|
|
708
|
+
counts.uploaded += 1;
|
|
525
709
|
return;
|
|
526
710
|
}
|
|
527
|
-
|
|
528
|
-
if (
|
|
529
|
-
log(`plune: could not upload a
|
|
711
|
+
counts.failed += 1;
|
|
712
|
+
if (counts.failed === 1) {
|
|
713
|
+
log(`plune: could not upload a ${label} (${out.detail || out.kind}) \u2014 its result is reported without it.`);
|
|
530
714
|
}
|
|
531
715
|
}
|
|
532
716
|
async function upload(items, filesByKey) {
|
|
533
717
|
if (filesByKey.size === 0 || !Array.isArray(items)) return;
|
|
534
|
-
const
|
|
718
|
+
const jobs = [];
|
|
535
719
|
for (const item of items) {
|
|
536
720
|
if (item?.status !== "accepted" || typeof item.id !== "string" || typeof item.resultKey !== "string") continue;
|
|
537
|
-
|
|
721
|
+
const room = { screenshot: KINDS.screenshot.room, text: KINDS.text.room };
|
|
538
722
|
for (const file of filesByKey.get(item.resultKey) ?? []) {
|
|
539
|
-
const type = file.contentType
|
|
540
|
-
|
|
723
|
+
const type = mediaTypeOf(file.contentType) ?? (file.path === void 0 ? void 0 : TYPE_BY_EXTENSION[extname(file.path).toLowerCase()]);
|
|
724
|
+
const kind = type === void 0 ? void 0 : kindOf(type);
|
|
725
|
+
if (type === void 0 || kind === void 0 || kind === "screenshot" && file.path === void 0) continue;
|
|
541
726
|
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,
|
|
727
|
+
if (file.name.startsWith("_")) skip(kind, name, "hidden");
|
|
728
|
+
else if (kind === "screenshot" && !SCREENSHOT_TYPES.has(type)) skip(kind, name, "type");
|
|
729
|
+
else if (room[kind] === 0) skip(kind, name, KINDS[kind].limit);
|
|
545
730
|
else {
|
|
546
|
-
room -= 1;
|
|
547
|
-
|
|
731
|
+
room[kind] -= 1;
|
|
732
|
+
jobs.push({
|
|
733
|
+
id: item.id,
|
|
734
|
+
kind,
|
|
735
|
+
name,
|
|
736
|
+
type: kind === "text" ? textTypeOf(file.contentType ?? "", type) : type,
|
|
737
|
+
...file.body !== void 0 ? { body: file.body } : {},
|
|
738
|
+
...file.path !== void 0 ? { path: file.path } : {}
|
|
739
|
+
});
|
|
548
740
|
}
|
|
549
741
|
}
|
|
550
742
|
}
|
|
551
743
|
let next = 0;
|
|
552
744
|
await Promise.all(
|
|
553
|
-
Array.from({ length: Math.min(UPLOADS_AT_ONCE,
|
|
554
|
-
while (next <
|
|
745
|
+
Array.from({ length: Math.min(UPLOADS_AT_ONCE, jobs.length) }, async () => {
|
|
746
|
+
while (next < jobs.length) await uploadOne(jobs[next++]);
|
|
555
747
|
})
|
|
556
748
|
);
|
|
557
749
|
}
|
|
@@ -629,11 +821,8 @@ async function startRun(passed, declared) {
|
|
|
629
821
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
630
822
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
631
823
|
if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
parts.push(
|
|
635
|
-
`${shots.uploaded} screenshot(s) uploaded` + (shots.skipped > 0 ? `, ${shots.skipped} skipped` : "") + (shots.failed > 0 ? `, ${shots.failed} failed to upload` : "")
|
|
636
|
-
);
|
|
824
|
+
for (const said of [tally(stats.screenshots, "screenshot"), tally(stats.textFiles, "text file")]) {
|
|
825
|
+
if (said !== void 0) parts.push(said);
|
|
637
826
|
}
|
|
638
827
|
if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
|
|
639
828
|
if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
|
|
@@ -700,134 +889,6 @@ function resultKey(testId, retry) {
|
|
|
700
889
|
const plain = `${testId}#${retry}`;
|
|
701
890
|
return plain.length <= RESULT_KEY_MAX ? plain : `${sha256(testId).slice(0, HASH_CHARS)}#${retry}`;
|
|
702
891
|
}
|
|
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
892
|
export {
|
|
832
893
|
DEFAULT_FALLBACK_PATH,
|
|
833
894
|
RESULT_KEY_MAX,
|
package/package.json
CHANGED