@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/README.md
CHANGED
|
@@ -165,6 +165,11 @@ JPEG or WebP, up to 2 MB each, 20 per result — is uploaded to that result, a p
|
|
|
165
165
|
run in Plune shows each test with what it looked at. `plune run import` finds them where the report
|
|
166
166
|
says they are, so run it where the report was written; the summary counts any it could not find.
|
|
167
167
|
|
|
168
|
+
JSON and plain text come the same way — the answer of an API, a log — whether a test attached them
|
|
169
|
+
with a `path` or as a `body` (a report holds a body itself, so `plune run import` needs no file for
|
|
170
|
+
it). Only `application/json` and `text/plain`, as the attachment names them; UTF-8 without NUL
|
|
171
|
+
bytes, up to 512 KiB each and 10 per result. The summary counts them apart from the screenshots.
|
|
172
|
+
|
|
168
173
|
### Several jobs, one run
|
|
169
174
|
|
|
170
175
|
Two suites, or a sharded matrix, report into a single run when every job shares a key **and** knows
|
package/dist/cli.cjs
CHANGED
|
@@ -2456,11 +2456,149 @@ var init_fallback = __esm({
|
|
|
2456
2456
|
}
|
|
2457
2457
|
});
|
|
2458
2458
|
|
|
2459
|
+
// src/reporter-core/failure-detail.ts
|
|
2460
|
+
function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
2461
|
+
const error = chosen(attempt);
|
|
2462
|
+
const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
|
|
2463
|
+
const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
|
|
2464
|
+
const steps = chainOf(attempt.steps);
|
|
2465
|
+
const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
|
|
2466
|
+
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 !== "");
|
|
2467
|
+
const ciUrl = webLink(attempt.buildHref);
|
|
2468
|
+
const failure = {
|
|
2469
|
+
...headline !== void 0 ? { headline } : {},
|
|
2470
|
+
...steps.length > 0 ? { steps } : {},
|
|
2471
|
+
...location !== void 0 ? { location } : {},
|
|
2472
|
+
...artifacts.length > 0 ? { artifacts } : {},
|
|
2473
|
+
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
2474
|
+
};
|
|
2475
|
+
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
2476
|
+
}
|
|
2477
|
+
function repoRootOf(dir, stopAt) {
|
|
2478
|
+
for (let here = (0, import_node_path2.resolve)(dir); ; here = (0, import_node_path2.dirname)(here)) {
|
|
2479
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(here, ".git"))) return here;
|
|
2480
|
+
if (here === stopAt || (0, import_node_path2.dirname)(here) === here) return void 0;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
2484
|
+
const text2 = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
2485
|
+
return cut(machineless(text2, attempt.repoRoot, home));
|
|
2486
|
+
}
|
|
2487
|
+
function filesOf(attempt) {
|
|
2488
|
+
return attempt.attachments.filter((a) => a.path !== void 0 || a.body !== void 0 && isTextType(a.contentType));
|
|
2489
|
+
}
|
|
2490
|
+
function machineless(raw, repoRoot, home) {
|
|
2491
|
+
let text2 = (0, import_node_util.stripVTControlCharacters)(raw).replaceAll("\x1B", "");
|
|
2492
|
+
const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
|
|
2493
|
+
if (root !== void 0) text2 = text2.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
|
|
2494
|
+
const own = pathIn(home);
|
|
2495
|
+
if (own !== void 0) text2 = text2.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
|
|
2496
|
+
if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
|
|
2497
|
+
return text2;
|
|
2498
|
+
}
|
|
2499
|
+
function pathIn(path15) {
|
|
2500
|
+
const parts = path15.split(/[\\/]+/);
|
|
2501
|
+
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
2502
|
+
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
2503
|
+
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
2504
|
+
const escaped = /^\/(?!\/)/.test(path15) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
2505
|
+
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
2506
|
+
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
2507
|
+
}
|
|
2508
|
+
function cut(text2) {
|
|
2509
|
+
if (jsonBytes(text2) <= TEXT_LIMIT) return text2;
|
|
2510
|
+
const lines = text2.split("\n");
|
|
2511
|
+
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
2512
|
+
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
2513
|
+
let used = 0;
|
|
2514
|
+
let head = 0;
|
|
2515
|
+
let tail = lines.length;
|
|
2516
|
+
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
2517
|
+
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
2518
|
+
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
2519
|
+
}
|
|
2520
|
+
function chosen({ status, errors }) {
|
|
2521
|
+
if (status === "timedOut") {
|
|
2522
|
+
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
2523
|
+
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
2524
|
+
}
|
|
2525
|
+
return errors[0];
|
|
2526
|
+
}
|
|
2527
|
+
function chainOf(steps) {
|
|
2528
|
+
const chain = [];
|
|
2529
|
+
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
2530
|
+
chain.push(failed.title);
|
|
2531
|
+
}
|
|
2532
|
+
return chain;
|
|
2533
|
+
}
|
|
2534
|
+
function placeOf(error, testFile) {
|
|
2535
|
+
const own = normal(testFile);
|
|
2536
|
+
let first;
|
|
2537
|
+
for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
|
|
2538
|
+
if (line.length > FRAME_MAX) continue;
|
|
2539
|
+
const frame = FRAME.exec(line);
|
|
2540
|
+
if (frame === null) continue;
|
|
2541
|
+
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
2542
|
+
if (normal(place.file) === own) return place;
|
|
2543
|
+
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
2544
|
+
}
|
|
2545
|
+
return error.location ?? first;
|
|
2546
|
+
}
|
|
2547
|
+
function fileOf(raw) {
|
|
2548
|
+
if (!raw.startsWith("file://")) return raw;
|
|
2549
|
+
let path15 = raw.slice("file://".length);
|
|
2550
|
+
try {
|
|
2551
|
+
path15 = decodeURIComponent(path15);
|
|
2552
|
+
} catch {
|
|
2553
|
+
}
|
|
2554
|
+
return /^\/[A-Za-z]:\//.test(path15) ? path15.slice(1) : path15;
|
|
2555
|
+
}
|
|
2556
|
+
function fromRoot(place, root) {
|
|
2557
|
+
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
2558
|
+
const base = normal(root).replace(/\/+$/, "");
|
|
2559
|
+
const file = normal(place.file);
|
|
2560
|
+
if (!file.startsWith(`${base}/`)) return void 0;
|
|
2561
|
+
const relative2 = file.slice(base.length + 1);
|
|
2562
|
+
if (!REPO_RELATIVE.test(relative2)) return void 0;
|
|
2563
|
+
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
2564
|
+
return column === void 0 ? { file: relative2, line: place.line } : { file: relative2, line: place.line, column };
|
|
2565
|
+
}
|
|
2566
|
+
function webLink(href) {
|
|
2567
|
+
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
2568
|
+
}
|
|
2569
|
+
var import_node_fs2, import_node_os2, import_node_path2, import_node_util, mediaTypeOf, TEXT_TYPES, isTextType, PATH_START, TEXT_LIMIT, HEADLINE_LIMIT, jsonBytes, ANY_HOME, fileNameOf, omitted, METADATA_TYPE, REPO_RELATIVE, FRAME, FRAME_MAX, firstLine, normal;
|
|
2570
|
+
var init_failure_detail = __esm({
|
|
2571
|
+
"src/reporter-core/failure-detail.ts"() {
|
|
2572
|
+
"use strict";
|
|
2573
|
+
init_cjs_shims();
|
|
2574
|
+
import_node_fs2 = require("fs");
|
|
2575
|
+
import_node_os2 = require("os");
|
|
2576
|
+
import_node_path2 = require("path");
|
|
2577
|
+
import_node_util = require("util");
|
|
2578
|
+
mediaTypeOf = (contentType) => contentType?.split(";")[0]?.trim().toLowerCase() || void 0;
|
|
2579
|
+
TEXT_TYPES = /* @__PURE__ */ new Set(["application/json", "text/plain"]);
|
|
2580
|
+
isTextType = (contentType) => TEXT_TYPES.has(mediaTypeOf(contentType) ?? "");
|
|
2581
|
+
PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
2582
|
+
TEXT_LIMIT = 524288;
|
|
2583
|
+
HEADLINE_LIMIT = 8 * 1024;
|
|
2584
|
+
jsonBytes = (text2) => Buffer.byteLength(JSON.stringify(text2)) - 2;
|
|
2585
|
+
ANY_HOME = /(?<=^|[\s'"`(=,[])(?:(?:file|webpack|webpack-internal):(?:\\*\/){2,3})?(?:\\*\/home\\*\/|\\*\/Users\\*\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
|
|
2586
|
+
fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
2587
|
+
omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
2588
|
+
METADATA_TYPE = "application/plune.metadata+json";
|
|
2589
|
+
REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
2590
|
+
FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
2591
|
+
FRAME_MAX = 8192;
|
|
2592
|
+
firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
2593
|
+
normal = (path15) => import_node_path2.posix.normalize(path15.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
2594
|
+
}
|
|
2595
|
+
});
|
|
2596
|
+
|
|
2459
2597
|
// src/reporter-core/session.ts
|
|
2460
2598
|
function nameOf(file) {
|
|
2461
|
-
const own = file.name.split(/[\\/]/).pop()?.trim() || (0,
|
|
2462
|
-
const fileExt = (0,
|
|
2463
|
-
const ownExt = (0,
|
|
2599
|
+
const own = file.name.split(/[\\/]/).pop()?.trim() || (file.path === void 0 ? "" : (0, import_node_path3.basename)(file.path));
|
|
2600
|
+
const fileExt = file.path === void 0 ? "" : (0, import_node_path3.extname)(file.path);
|
|
2601
|
+
const ownExt = (0, import_node_path3.extname)(own);
|
|
2464
2602
|
const typed = ownExt !== "" && (ownExt.toLowerCase() === fileExt.toLowerCase() || TYPE_BY_EXTENSION[ownExt.toLowerCase()] !== void 0);
|
|
2465
2603
|
const ext = typed ? ownExt : fileExt;
|
|
2466
2604
|
let stem = typed ? own.slice(0, -ownExt.length) : own;
|
|
@@ -2470,6 +2608,35 @@ function nameOf(file) {
|
|
|
2470
2608
|
}
|
|
2471
2609
|
return `${stem}${ext}`;
|
|
2472
2610
|
}
|
|
2611
|
+
function kindOf(type) {
|
|
2612
|
+
if (type.startsWith("image/")) return "screenshot";
|
|
2613
|
+
return TEXT_TYPES.has(type) ? "text" : void 0;
|
|
2614
|
+
}
|
|
2615
|
+
function textTypeOf(contentType, type) {
|
|
2616
|
+
const charset = /;\s*charset\s*=\s*"?([^\s";]+)"?/i.exec(contentType)?.[1];
|
|
2617
|
+
return charset?.toLowerCase() === "utf-8" ? `${type}; charset=utf-8` : type;
|
|
2618
|
+
}
|
|
2619
|
+
function textRefusal(bytes) {
|
|
2620
|
+
if (bytes.length > TEXT_BYTES) return "textSize";
|
|
2621
|
+
if (bytes.length === 0) return "empty";
|
|
2622
|
+
if (bytes.includes(0)) return "binary";
|
|
2623
|
+
try {
|
|
2624
|
+
UTF8.decode(bytes);
|
|
2625
|
+
} catch {
|
|
2626
|
+
return "encoding";
|
|
2627
|
+
}
|
|
2628
|
+
return void 0;
|
|
2629
|
+
}
|
|
2630
|
+
function onDisk(result) {
|
|
2631
|
+
if (!("files" in result) || result.files === void 0) return result;
|
|
2632
|
+
const { files, ...rest } = result;
|
|
2633
|
+
const kept = files.filter((f) => f.path !== void 0).map(({ body: _body, ...file }) => file);
|
|
2634
|
+
return kept.length === 0 ? rest : { ...rest, files: kept };
|
|
2635
|
+
}
|
|
2636
|
+
function tally(counts, noun) {
|
|
2637
|
+
if (counts.uploaded + counts.skipped + counts.failed === 0) return void 0;
|
|
2638
|
+
return `${counts.uploaded} ${noun}(s) uploaded` + (counts.skipped > 0 ? `, ${counts.skipped} skipped` : "") + (counts.failed > 0 ? `, ${counts.failed} failed to upload` : "");
|
|
2639
|
+
}
|
|
2473
2640
|
function boundKey(key) {
|
|
2474
2641
|
if (key.value.length <= KEY_MAX) return key;
|
|
2475
2642
|
const digest = (0, import_node_crypto2.createHash)("sha256").update(key.value).digest("hex").slice(0, 16);
|
|
@@ -2543,7 +2710,8 @@ async function startRun(passed, declared) {
|
|
|
2543
2710
|
created: 0,
|
|
2544
2711
|
unoffered: 0,
|
|
2545
2712
|
deferred: 0,
|
|
2546
|
-
screenshots: { uploaded: 0, skipped: 0, failed: 0 }
|
|
2713
|
+
screenshots: { uploaded: 0, skipped: 0, failed: 0 },
|
|
2714
|
+
textFiles: { uploaded: 0, skipped: 0, failed: 0 }
|
|
2547
2715
|
};
|
|
2548
2716
|
const resolved = /* @__PURE__ */ new Map();
|
|
2549
2717
|
let buffer = [];
|
|
@@ -2554,6 +2722,7 @@ async function startRun(passed, declared) {
|
|
|
2554
2722
|
let done = false;
|
|
2555
2723
|
const shortened = /* @__PURE__ */ new Set();
|
|
2556
2724
|
const skipsSaid = /* @__PURE__ */ new Set();
|
|
2725
|
+
const countsOf = (kind) => kind === "text" ? stats.textFiles : stats.screenshots;
|
|
2557
2726
|
function bound(keys) {
|
|
2558
2727
|
return keys.map((key) => {
|
|
2559
2728
|
const fit = boundKey(key);
|
|
@@ -2581,7 +2750,7 @@ async function startRun(passed, declared) {
|
|
|
2581
2750
|
externalKey,
|
|
2582
2751
|
session: sessionMarker,
|
|
2583
2752
|
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
2584
|
-
results
|
|
2753
|
+
results: results.map(onDisk)
|
|
2585
2754
|
});
|
|
2586
2755
|
stats.deferred += results.length;
|
|
2587
2756
|
}
|
|
@@ -2689,57 +2858,74 @@ async function startRun(passed, declared) {
|
|
|
2689
2858
|
stats.created += out.body.results.filter((r) => r?.outcome === "created").length;
|
|
2690
2859
|
}
|
|
2691
2860
|
}
|
|
2692
|
-
function skip(name, why) {
|
|
2693
|
-
|
|
2694
|
-
if (skipsSaid.has(why)) return;
|
|
2695
|
-
skipsSaid.add(why);
|
|
2696
|
-
log(`plune:
|
|
2861
|
+
function skip(kind, name, why) {
|
|
2862
|
+
countsOf(kind).skipped += 1;
|
|
2863
|
+
if (skipsSaid.has(`${kind}/${why}`)) return;
|
|
2864
|
+
skipsSaid.add(`${kind}/${why}`);
|
|
2865
|
+
log(`plune: ${KINDS[kind].label} "${name}" not uploaded \u2014 ${SKIPPED[why]}.`);
|
|
2697
2866
|
}
|
|
2698
|
-
async function uploadOne(
|
|
2867
|
+
async function uploadOne(job) {
|
|
2868
|
+
const { kind } = job;
|
|
2869
|
+
const { label, bytes: ceiling, size } = KINDS[kind];
|
|
2699
2870
|
let bytes;
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2871
|
+
if (job.body !== void 0) bytes = job.body;
|
|
2872
|
+
else if (job.path === void 0) return skip(kind, job.name, "missing");
|
|
2873
|
+
else {
|
|
2874
|
+
try {
|
|
2875
|
+
if ((await import_node_fs3.promises.stat(job.path)).size > ceiling) return skip(kind, job.name, size);
|
|
2876
|
+
bytes = await import_node_fs3.promises.readFile(job.path);
|
|
2877
|
+
} catch {
|
|
2878
|
+
return skip(kind, job.name, "missing");
|
|
2879
|
+
}
|
|
2705
2880
|
}
|
|
2881
|
+
const refusal = kind === "text" ? textRefusal(bytes) : void 0;
|
|
2882
|
+
if (refusal !== void 0) return skip(kind, job.name, refusal);
|
|
2706
2883
|
const out = await client.upload(
|
|
2707
|
-
`/v1/results/${encodeURIComponent(
|
|
2884
|
+
`/v1/results/${encodeURIComponent(job.id)}/files?name=${encodeURIComponent(job.name)}`,
|
|
2708
2885
|
bytes,
|
|
2709
|
-
|
|
2886
|
+
job.type
|
|
2710
2887
|
);
|
|
2888
|
+
const counts = countsOf(kind);
|
|
2711
2889
|
if (out.ok) {
|
|
2712
|
-
|
|
2890
|
+
counts.uploaded += 1;
|
|
2713
2891
|
return;
|
|
2714
2892
|
}
|
|
2715
|
-
|
|
2716
|
-
if (
|
|
2717
|
-
log(`plune: could not upload a
|
|
2893
|
+
counts.failed += 1;
|
|
2894
|
+
if (counts.failed === 1) {
|
|
2895
|
+
log(`plune: could not upload a ${label} (${out.detail || out.kind}) \u2014 its result is reported without it.`);
|
|
2718
2896
|
}
|
|
2719
2897
|
}
|
|
2720
2898
|
async function upload(items, filesByKey) {
|
|
2721
2899
|
if (filesByKey.size === 0 || !Array.isArray(items)) return;
|
|
2722
|
-
const
|
|
2900
|
+
const jobs = [];
|
|
2723
2901
|
for (const item of items) {
|
|
2724
2902
|
if (item?.status !== "accepted" || typeof item.id !== "string" || typeof item.resultKey !== "string") continue;
|
|
2725
|
-
|
|
2903
|
+
const room = { screenshot: KINDS.screenshot.room, text: KINDS.text.room };
|
|
2726
2904
|
for (const file of filesByKey.get(item.resultKey) ?? []) {
|
|
2727
|
-
const type = file.contentType
|
|
2728
|
-
|
|
2905
|
+
const type = mediaTypeOf(file.contentType) ?? (file.path === void 0 ? void 0 : TYPE_BY_EXTENSION[(0, import_node_path3.extname)(file.path).toLowerCase()]);
|
|
2906
|
+
const kind = type === void 0 ? void 0 : kindOf(type);
|
|
2907
|
+
if (type === void 0 || kind === void 0 || kind === "screenshot" && file.path === void 0) continue;
|
|
2729
2908
|
const name = nameOf(file);
|
|
2730
|
-
if (file.name.startsWith("_")) skip(name, "hidden");
|
|
2731
|
-
else if (!SCREENSHOT_TYPES.has(type)) skip(name, "type");
|
|
2732
|
-
else if (room === 0) skip(name,
|
|
2909
|
+
if (file.name.startsWith("_")) skip(kind, name, "hidden");
|
|
2910
|
+
else if (kind === "screenshot" && !SCREENSHOT_TYPES.has(type)) skip(kind, name, "type");
|
|
2911
|
+
else if (room[kind] === 0) skip(kind, name, KINDS[kind].limit);
|
|
2733
2912
|
else {
|
|
2734
|
-
room -= 1;
|
|
2735
|
-
|
|
2913
|
+
room[kind] -= 1;
|
|
2914
|
+
jobs.push({
|
|
2915
|
+
id: item.id,
|
|
2916
|
+
kind,
|
|
2917
|
+
name,
|
|
2918
|
+
type: kind === "text" ? textTypeOf(file.contentType ?? "", type) : type,
|
|
2919
|
+
...file.body !== void 0 ? { body: file.body } : {},
|
|
2920
|
+
...file.path !== void 0 ? { path: file.path } : {}
|
|
2921
|
+
});
|
|
2736
2922
|
}
|
|
2737
2923
|
}
|
|
2738
2924
|
}
|
|
2739
2925
|
let next = 0;
|
|
2740
2926
|
await Promise.all(
|
|
2741
|
-
Array.from({ length: Math.min(UPLOADS_AT_ONCE,
|
|
2742
|
-
while (next <
|
|
2927
|
+
Array.from({ length: Math.min(UPLOADS_AT_ONCE, jobs.length) }, async () => {
|
|
2928
|
+
while (next < jobs.length) await uploadOne(jobs[next++]);
|
|
2743
2929
|
})
|
|
2744
2930
|
);
|
|
2745
2931
|
}
|
|
@@ -2817,11 +3003,8 @@ async function startRun(passed, declared) {
|
|
|
2817
3003
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
2818
3004
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
2819
3005
|
if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
|
|
2820
|
-
const
|
|
2821
|
-
|
|
2822
|
-
parts.push(
|
|
2823
|
-
`${shots.uploaded} screenshot(s) uploaded` + (shots.skipped > 0 ? `, ${shots.skipped} skipped` : "") + (shots.failed > 0 ? `, ${shots.failed} failed to upload` : "")
|
|
2824
|
-
);
|
|
3006
|
+
for (const said of [tally(stats.screenshots, "screenshot"), tally(stats.textFiles, "text file")]) {
|
|
3007
|
+
if (said !== void 0) parts.push(said);
|
|
2825
3008
|
}
|
|
2826
3009
|
if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
|
|
2827
3010
|
if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
|
|
@@ -2874,18 +3057,20 @@ async function startRun(passed, declared) {
|
|
|
2874
3057
|
}
|
|
2875
3058
|
};
|
|
2876
3059
|
}
|
|
2877
|
-
var import_node_crypto2,
|
|
3060
|
+
var import_node_crypto2, import_node_fs3, import_node_path3, import_node_util2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX, KEY_MAX, TITLE_MAX, COUNTED, SCREENSHOT_TYPES, SCREENSHOT_BYTES, SCREENSHOTS_PER_RESULT, TEXT_BYTES, TEXTS_PER_RESULT, UTF8, TYPE_BY_EXTENSION, UPLOADS_AT_ONCE, NAME_MAX, SKIPPED, KINDS, BATCH_BYTES, ENVELOPE_BYTES;
|
|
2878
3061
|
var init_session = __esm({
|
|
2879
3062
|
"src/reporter-core/session.ts"() {
|
|
2880
3063
|
"use strict";
|
|
2881
3064
|
init_cjs_shims();
|
|
2882
3065
|
import_node_crypto2 = require("crypto");
|
|
2883
|
-
|
|
2884
|
-
|
|
3066
|
+
import_node_fs3 = require("fs");
|
|
3067
|
+
import_node_path3 = require("path");
|
|
3068
|
+
import_node_util2 = require("util");
|
|
2885
3069
|
init_api_url();
|
|
2886
3070
|
init_credentials();
|
|
2887
3071
|
init_client();
|
|
2888
3072
|
init_env2();
|
|
3073
|
+
init_failure_detail();
|
|
2889
3074
|
init_fallback();
|
|
2890
3075
|
RESOLVE_CHUNK = 500;
|
|
2891
3076
|
BATCH_MAX = 500;
|
|
@@ -2898,6 +3083,9 @@ var init_session = __esm({
|
|
|
2898
3083
|
SCREENSHOT_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
2899
3084
|
SCREENSHOT_BYTES = 2 * 1024 * 1024;
|
|
2900
3085
|
SCREENSHOTS_PER_RESULT = 20;
|
|
3086
|
+
TEXT_BYTES = 512 * 1024;
|
|
3087
|
+
TEXTS_PER_RESULT = 10;
|
|
3088
|
+
UTF8 = new import_node_util2.TextDecoder("utf-8", { fatal: true });
|
|
2901
3089
|
TYPE_BY_EXTENSION = {
|
|
2902
3090
|
".png": "image/png",
|
|
2903
3091
|
".jpg": "image/jpeg",
|
|
@@ -2911,7 +3099,16 @@ var init_session = __esm({
|
|
|
2911
3099
|
type: "Plune takes PNG, JPEG and WebP",
|
|
2912
3100
|
limit: `a result holds at most ${SCREENSHOTS_PER_RESULT}`,
|
|
2913
3101
|
missing: "the file cannot be read on this machine",
|
|
2914
|
-
size: "Plune takes a file of at most 2 MB"
|
|
3102
|
+
size: "Plune takes a file of at most 2 MB",
|
|
3103
|
+
textLimit: `a result holds at most ${TEXTS_PER_RESULT} text files`,
|
|
3104
|
+
textSize: "Plune takes a text file of at most 512 KiB",
|
|
3105
|
+
empty: "the file is empty",
|
|
3106
|
+
binary: "the file holds a NUL byte, and Plune takes UTF-8 text without one",
|
|
3107
|
+
encoding: "Plune takes UTF-8 text only"
|
|
3108
|
+
};
|
|
3109
|
+
KINDS = {
|
|
3110
|
+
screenshot: { label: "screenshot", bytes: SCREENSHOT_BYTES, room: SCREENSHOTS_PER_RESULT, limit: "limit", size: "size" },
|
|
3111
|
+
text: { label: "text file", bytes: TEXT_BYTES, room: TEXTS_PER_RESULT, limit: "textLimit", size: "textSize" }
|
|
2915
3112
|
};
|
|
2916
3113
|
BATCH_BYTES = 8 * 1024 * 1024;
|
|
2917
3114
|
ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ results: [] }));
|
|
@@ -3397,141 +3594,6 @@ var init_junit = __esm({
|
|
|
3397
3594
|
}
|
|
3398
3595
|
});
|
|
3399
3596
|
|
|
3400
|
-
// src/reporter-core/failure-detail.ts
|
|
3401
|
-
function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
3402
|
-
const error = chosen(attempt);
|
|
3403
|
-
const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
|
|
3404
|
-
const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
|
|
3405
|
-
const steps = chainOf(attempt.steps);
|
|
3406
|
-
const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
|
|
3407
|
-
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 !== "");
|
|
3408
|
-
const ciUrl = webLink(attempt.buildHref);
|
|
3409
|
-
const failure = {
|
|
3410
|
-
...headline !== void 0 ? { headline } : {},
|
|
3411
|
-
...steps.length > 0 ? { steps } : {},
|
|
3412
|
-
...location !== void 0 ? { location } : {},
|
|
3413
|
-
...artifacts.length > 0 ? { artifacts } : {},
|
|
3414
|
-
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
3415
|
-
};
|
|
3416
|
-
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
3417
|
-
}
|
|
3418
|
-
function repoRootOf(dir, stopAt) {
|
|
3419
|
-
for (let here = (0, import_node_path3.resolve)(dir); ; here = (0, import_node_path3.dirname)(here)) {
|
|
3420
|
-
if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(here, ".git"))) return here;
|
|
3421
|
-
if (here === stopAt || (0, import_node_path3.dirname)(here) === here) return void 0;
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3424
|
-
function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
3425
|
-
const text2 = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
3426
|
-
return cut(machineless(text2, attempt.repoRoot, home));
|
|
3427
|
-
}
|
|
3428
|
-
function filesOf(attempt) {
|
|
3429
|
-
return attempt.attachments.filter((a) => a.path !== void 0);
|
|
3430
|
-
}
|
|
3431
|
-
function machineless(raw, repoRoot, home) {
|
|
3432
|
-
let text2 = (0, import_node_util.stripVTControlCharacters)(raw).replaceAll("\x1B", "");
|
|
3433
|
-
const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
|
|
3434
|
-
if (root !== void 0) text2 = text2.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
|
|
3435
|
-
const own = pathIn(home);
|
|
3436
|
-
if (own !== void 0) text2 = text2.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
|
|
3437
|
-
if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
|
|
3438
|
-
return text2;
|
|
3439
|
-
}
|
|
3440
|
-
function pathIn(path15) {
|
|
3441
|
-
const parts = path15.split(/[\\/]+/);
|
|
3442
|
-
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
3443
|
-
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
3444
|
-
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
3445
|
-
const escaped = /^\/(?!\/)/.test(path15) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
3446
|
-
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
3447
|
-
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
3448
|
-
}
|
|
3449
|
-
function cut(text2) {
|
|
3450
|
-
if (jsonBytes(text2) <= TEXT_LIMIT) return text2;
|
|
3451
|
-
const lines = text2.split("\n");
|
|
3452
|
-
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
3453
|
-
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
3454
|
-
let used = 0;
|
|
3455
|
-
let head = 0;
|
|
3456
|
-
let tail = lines.length;
|
|
3457
|
-
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
3458
|
-
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
3459
|
-
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
3460
|
-
}
|
|
3461
|
-
function chosen({ status, errors }) {
|
|
3462
|
-
if (status === "timedOut") {
|
|
3463
|
-
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
3464
|
-
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
3465
|
-
}
|
|
3466
|
-
return errors[0];
|
|
3467
|
-
}
|
|
3468
|
-
function chainOf(steps) {
|
|
3469
|
-
const chain = [];
|
|
3470
|
-
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
3471
|
-
chain.push(failed.title);
|
|
3472
|
-
}
|
|
3473
|
-
return chain;
|
|
3474
|
-
}
|
|
3475
|
-
function placeOf(error, testFile) {
|
|
3476
|
-
const own = normal(testFile);
|
|
3477
|
-
let first;
|
|
3478
|
-
for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
|
|
3479
|
-
if (line.length > FRAME_MAX) continue;
|
|
3480
|
-
const frame = FRAME.exec(line);
|
|
3481
|
-
if (frame === null) continue;
|
|
3482
|
-
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
3483
|
-
if (normal(place.file) === own) return place;
|
|
3484
|
-
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
3485
|
-
}
|
|
3486
|
-
return error.location ?? first;
|
|
3487
|
-
}
|
|
3488
|
-
function fileOf(raw) {
|
|
3489
|
-
if (!raw.startsWith("file://")) return raw;
|
|
3490
|
-
let path15 = raw.slice("file://".length);
|
|
3491
|
-
try {
|
|
3492
|
-
path15 = decodeURIComponent(path15);
|
|
3493
|
-
} catch {
|
|
3494
|
-
}
|
|
3495
|
-
return /^\/[A-Za-z]:\//.test(path15) ? path15.slice(1) : path15;
|
|
3496
|
-
}
|
|
3497
|
-
function fromRoot(place, root) {
|
|
3498
|
-
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
3499
|
-
const base = normal(root).replace(/\/+$/, "");
|
|
3500
|
-
const file = normal(place.file);
|
|
3501
|
-
if (!file.startsWith(`${base}/`)) return void 0;
|
|
3502
|
-
const relative2 = file.slice(base.length + 1);
|
|
3503
|
-
if (!REPO_RELATIVE.test(relative2)) return void 0;
|
|
3504
|
-
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
3505
|
-
return column === void 0 ? { file: relative2, line: place.line } : { file: relative2, line: place.line, column };
|
|
3506
|
-
}
|
|
3507
|
-
function webLink(href) {
|
|
3508
|
-
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
3509
|
-
}
|
|
3510
|
-
var import_node_fs3, import_node_os2, import_node_path3, import_node_util, PATH_START, TEXT_LIMIT, HEADLINE_LIMIT, jsonBytes, ANY_HOME, fileNameOf, omitted, METADATA_TYPE, REPO_RELATIVE, FRAME, FRAME_MAX, firstLine, normal;
|
|
3511
|
-
var init_failure_detail = __esm({
|
|
3512
|
-
"src/reporter-core/failure-detail.ts"() {
|
|
3513
|
-
"use strict";
|
|
3514
|
-
init_cjs_shims();
|
|
3515
|
-
import_node_fs3 = require("fs");
|
|
3516
|
-
import_node_os2 = require("os");
|
|
3517
|
-
import_node_path3 = require("path");
|
|
3518
|
-
import_node_util = require("util");
|
|
3519
|
-
PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
3520
|
-
TEXT_LIMIT = 524288;
|
|
3521
|
-
HEADLINE_LIMIT = 8 * 1024;
|
|
3522
|
-
jsonBytes = (text2) => Buffer.byteLength(JSON.stringify(text2)) - 2;
|
|
3523
|
-
ANY_HOME = /(?<=^|[\s'"`(=,[])(?:(?:file|webpack|webpack-internal):(?:\\*\/){2,3})?(?:\\*\/home\\*\/|\\*\/Users\\*\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
|
|
3524
|
-
fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
3525
|
-
omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
3526
|
-
METADATA_TYPE = "application/plune.metadata+json";
|
|
3527
|
-
REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
3528
|
-
FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
3529
|
-
FRAME_MAX = 8192;
|
|
3530
|
-
firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
3531
|
-
normal = (path15) => import_node_path3.posix.normalize(path15.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
3532
|
-
}
|
|
3533
|
-
});
|
|
3534
|
-
|
|
3535
3597
|
// src/importers/playwright-json.ts
|
|
3536
3598
|
function statedIdOf(test, title) {
|
|
3537
3599
|
const annotation = test.annotations?.find((a) => a.type === "PluneId")?.description;
|
|
@@ -3547,10 +3609,13 @@ function attemptOf(result, file, ctx) {
|
|
|
3547
3609
|
...typeof at?.file === "string" && typeof at.line === "number" ? { location: { file: at.file, line: at.line, ...typeof at.column === "number" ? { column: at.column } : {} } } : {}
|
|
3548
3610
|
})),
|
|
3549
3611
|
steps: steps(result.steps),
|
|
3550
|
-
attachments: (result.attachments ?? []).map(({ name, contentType, path: path15 }) => ({
|
|
3612
|
+
attachments: (result.attachments ?? []).map(({ name, contentType, path: path15, body }) => ({
|
|
3551
3613
|
name: name ?? "",
|
|
3552
3614
|
...contentType !== void 0 ? { contentType } : {},
|
|
3553
|
-
...path15 !== void 0 ? { path: path15 } : {}
|
|
3615
|
+
...path15 !== void 0 ? { path: path15 } : {},
|
|
3616
|
+
// Decoded only where the platform would take it (plune-ai/plune#928): a report of screenshots kept as
|
|
3617
|
+
// bodies is megabytes of base64 that nothing here will read.
|
|
3618
|
+
...typeof body === "string" && isTextType(contentType) ? { body: Buffer.from(body, "base64") } : {}
|
|
3554
3619
|
})),
|
|
3555
3620
|
...ctx.buildHref !== void 0 ? { buildHref: ctx.buildHref } : {},
|
|
3556
3621
|
// Spec files are posix and relative to the root dir; the core reads either slash.
|
|
@@ -3767,6 +3832,7 @@ async function handleRunImport(options) {
|
|
|
3767
3832
|
...stats.unofferedWhy !== void 0 ? { unofferedWhy: stats.unofferedWhy } : {},
|
|
3768
3833
|
deferred: stats.deferred,
|
|
3769
3834
|
screenshots: { ...stats.screenshots },
|
|
3835
|
+
textFiles: { ...stats.textFiles },
|
|
3770
3836
|
unlocatable: results.filter((r) => r.specRef === void 0).length,
|
|
3771
3837
|
offering: options.create === true
|
|
3772
3838
|
};
|
|
@@ -3790,11 +3856,15 @@ function describe(result, apiUrl) {
|
|
|
3790
3856
|
if (result.conflict > 0 || result.rejected > 0) {
|
|
3791
3857
|
lines.push(`${result.conflict} conflicted and ${result.rejected} were refused.`);
|
|
3792
3858
|
}
|
|
3793
|
-
const
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
)
|
|
3859
|
+
for (const [counts, noun] of [
|
|
3860
|
+
[result.screenshots, "screenshot"],
|
|
3861
|
+
[result.textFiles, "text file"]
|
|
3862
|
+
]) {
|
|
3863
|
+
if (counts.uploaded + counts.skipped + counts.failed > 0) {
|
|
3864
|
+
lines.push(
|
|
3865
|
+
`${counts.uploaded} ${noun}(s) uploaded to their results` + (counts.skipped > 0 ? `, ${counts.skipped} skipped` : "") + (counts.failed > 0 ? `, ${counts.failed} could not be uploaded` : "") + "."
|
|
3866
|
+
);
|
|
3867
|
+
}
|
|
3798
3868
|
}
|
|
3799
3869
|
if (result.deferred > 0) {
|
|
3800
3870
|
lines.push(
|