@plune-ai/cli 0.17.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 +116 -1
- package/dist/cli.cjs +875 -223
- 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 +244 -40
- package/dist/reporter-core.d.cts +35 -0
- package/dist/reporter-core.d.ts +35 -0
- package/dist/reporter-core.js +240 -36
- package/package.json +2 -1
package/dist/cli.cjs
CHANGED
|
@@ -512,7 +512,7 @@ var init_retry = __esm({
|
|
|
512
512
|
"use strict";
|
|
513
513
|
init_cjs_shims();
|
|
514
514
|
init_errors2();
|
|
515
|
-
defaultSleep = (ms) => new Promise((
|
|
515
|
+
defaultSleep = (ms) => new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
516
516
|
}
|
|
517
517
|
});
|
|
518
518
|
|
|
@@ -874,7 +874,7 @@ var init_embedder = __esm({
|
|
|
874
874
|
throw new Error(
|
|
875
875
|
"The `semantic-similarity` and RAG assertions need local embeddings, which ship separately: install @huggingface/transformers to enable them. Every other assertion works without it."
|
|
876
876
|
);
|
|
877
|
-
}).then(({ pipeline }) =>
|
|
877
|
+
}).then(({ pipeline: pipeline2 }) => pipeline2("feature-extraction", MODEL_ID));
|
|
878
878
|
return this.extractor;
|
|
879
879
|
}
|
|
880
880
|
};
|
|
@@ -2226,7 +2226,7 @@ var init_credentials = __esm({
|
|
|
2226
2226
|
|
|
2227
2227
|
// src/reporter-core/client.ts
|
|
2228
2228
|
function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
2229
|
-
return new Promise((
|
|
2229
|
+
return new Promise((resolve9, reject) => {
|
|
2230
2230
|
const target = new URL(url);
|
|
2231
2231
|
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
2232
2232
|
const req = send(target, { method: init.method, headers: init.headers }, (res) => {
|
|
@@ -2236,7 +2236,7 @@ function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
|
2236
2236
|
res.on("end", () => {
|
|
2237
2237
|
const status = res.statusCode ?? 0;
|
|
2238
2238
|
const text2 = Buffer.concat(chunks).toString("utf8");
|
|
2239
|
-
|
|
2239
|
+
resolve9({
|
|
2240
2240
|
ok: status >= 200 && status <= 299,
|
|
2241
2241
|
status,
|
|
2242
2242
|
headers: {
|
|
@@ -2254,12 +2254,42 @@ function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
|
2254
2254
|
req.end(init.body);
|
|
2255
2255
|
});
|
|
2256
2256
|
}
|
|
2257
|
+
function nodePut(url, headers, source, idleMs) {
|
|
2258
|
+
return new Promise((resolve9, reject) => {
|
|
2259
|
+
const target = new URL(url);
|
|
2260
|
+
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
2261
|
+
const length = "body" in source ? source.body.length : source.size;
|
|
2262
|
+
let answered;
|
|
2263
|
+
const settle = (err) => answered !== void 0 ? resolve9(answered) : reject(err);
|
|
2264
|
+
const req = send(
|
|
2265
|
+
target,
|
|
2266
|
+
{ method: "PUT", headers: { ...withoutLength(headers), "content-length": String(length) } },
|
|
2267
|
+
(res) => {
|
|
2268
|
+
const retry = res.headers["retry-after"];
|
|
2269
|
+
answered = { status: res.statusCode ?? 0, retryAfter: Array.isArray(retry) ? retry[0] ?? null : retry ?? null };
|
|
2270
|
+
res.on("error", settle);
|
|
2271
|
+
res.on("close", () => {
|
|
2272
|
+
resolve9(answered);
|
|
2273
|
+
req.destroy();
|
|
2274
|
+
});
|
|
2275
|
+
res.resume();
|
|
2276
|
+
}
|
|
2277
|
+
);
|
|
2278
|
+
req.setTimeout(idleMs, () => req.destroy(Object.assign(new Error("idle"), { code: "ETIMEDOUT" })));
|
|
2279
|
+
req.on("error", settle);
|
|
2280
|
+
if ("body" in source) req.end(source.body);
|
|
2281
|
+
else (0, import_node_stream.pipeline)((0, import_node_fs2.createReadStream)(source.path), req, (err) => err ? settle(err) : void 0);
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2257
2284
|
async function detailOf(res) {
|
|
2258
2285
|
try {
|
|
2259
2286
|
const body = await res.json();
|
|
2260
|
-
return
|
|
2287
|
+
return {
|
|
2288
|
+
detail: typeof body.error === "string" ? body.error : "",
|
|
2289
|
+
...typeof body.code === "string" && /^[a-z0-9_]{1,60}$/.test(body.code) ? { code: body.code } : {}
|
|
2290
|
+
};
|
|
2261
2291
|
} catch {
|
|
2262
|
-
return "";
|
|
2292
|
+
return { detail: "" };
|
|
2263
2293
|
}
|
|
2264
2294
|
}
|
|
2265
2295
|
function retryAfterMs2(res) {
|
|
@@ -2280,7 +2310,7 @@ function createClient(opts) {
|
|
|
2280
2310
|
const wait = opts.wait ?? sleep;
|
|
2281
2311
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
2282
2312
|
const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
|
|
2283
|
-
async function send(method,
|
|
2313
|
+
async function send(method, path17, payload) {
|
|
2284
2314
|
let last = {
|
|
2285
2315
|
kind: "unavailable",
|
|
2286
2316
|
status: null,
|
|
@@ -2289,7 +2319,7 @@ function createClient(opts) {
|
|
|
2289
2319
|
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt += 1) {
|
|
2290
2320
|
let res;
|
|
2291
2321
|
try {
|
|
2292
|
-
res = await doFetch(`${base}${
|
|
2322
|
+
res = await doFetch(`${base}${path17}`, {
|
|
2293
2323
|
method,
|
|
2294
2324
|
headers: { "content-type": payload?.type ?? "application/json", authorization: `Bearer ${opts.token}` },
|
|
2295
2325
|
// A DELETE carries none. Sending `"undefined"` as a body is what a naive
|
|
@@ -2310,10 +2340,16 @@ function createClient(opts) {
|
|
|
2310
2340
|
return { ok: false, kind: "other", status: res.status, detail: "the answer was not JSON" };
|
|
2311
2341
|
}
|
|
2312
2342
|
}
|
|
2313
|
-
const
|
|
2343
|
+
const said = await detailOf(res);
|
|
2314
2344
|
const kind = classify3(res.status);
|
|
2315
|
-
last = { kind, status: res.status, detail };
|
|
2345
|
+
last = { kind, status: res.status, detail: scrub(said.detail), ...said.code !== void 0 ? { code: said.code } : {} };
|
|
2316
2346
|
if (kind !== "unavailable") break;
|
|
2347
|
+
if (res.status === 429 && retryAfterMs2(res) === null) {
|
|
2348
|
+
last.kind = "invalid";
|
|
2349
|
+
last.detail = "quota exceeded (no Retry-After)";
|
|
2350
|
+
if (said.detail) last.detail += ` \u2014 ${said.detail}`;
|
|
2351
|
+
break;
|
|
2352
|
+
}
|
|
2317
2353
|
if (attempt < RETRY_DELAYS_MS.length) {
|
|
2318
2354
|
await wait(retryAfterMs2(res) ?? RETRY_DELAYS_MS[attempt]);
|
|
2319
2355
|
}
|
|
@@ -2321,20 +2357,57 @@ function createClient(opts) {
|
|
|
2321
2357
|
return { ok: false, ...last };
|
|
2322
2358
|
}
|
|
2323
2359
|
return {
|
|
2324
|
-
post: (
|
|
2325
|
-
upload: (
|
|
2326
|
-
del: (
|
|
2360
|
+
post: (path17, body) => send("POST", path17, { body: JSON.stringify(body), type: "application/json" }),
|
|
2361
|
+
upload: (path17, bytes, contentType) => send("POST", path17, { body: bytes, type: contentType }),
|
|
2362
|
+
del: (path17) => send("DELETE", path17),
|
|
2363
|
+
async put(url, headers, source) {
|
|
2364
|
+
let last = { ok: false, status: null, code: "network" };
|
|
2365
|
+
for (let attempt = 0; attempt <= PUT_DELAYS_MS.length; attempt += 1) {
|
|
2366
|
+
try {
|
|
2367
|
+
let status;
|
|
2368
|
+
let retryAfter;
|
|
2369
|
+
if (opts.fetchImpl === void 0) ({ status, retryAfter } = await nodePut(url, headers, source, IDLE_MS));
|
|
2370
|
+
else {
|
|
2371
|
+
const body = "body" in source ? source.body : await (0, import_node_fs2.openAsBlob)(source.path);
|
|
2372
|
+
const res = await opts.fetchImpl(url, { method: "PUT", headers: withoutLength(headers), body });
|
|
2373
|
+
await res.arrayBuffer().catch(() => void 0);
|
|
2374
|
+
status = res.status;
|
|
2375
|
+
retryAfter = res.headers.get("retry-after");
|
|
2376
|
+
}
|
|
2377
|
+
if (status >= 200 && status <= 299) return { ok: true };
|
|
2378
|
+
last = { ok: false, status, code: `bucket_${status}` };
|
|
2379
|
+
if (status < 500 && status !== 429) return last;
|
|
2380
|
+
const seconds = retryAfter === null ? NaN : Number(retryAfter.trim());
|
|
2381
|
+
if (attempt < PUT_DELAYS_MS.length) {
|
|
2382
|
+
await wait(Number.isFinite(seconds) && seconds >= 0 ? Math.min(seconds, 60) * 1e3 : PUT_DELAYS_MS[attempt]);
|
|
2383
|
+
}
|
|
2384
|
+
continue;
|
|
2385
|
+
} catch (err) {
|
|
2386
|
+
last = { ok: false, status: null, code: codeOfError(err) };
|
|
2387
|
+
}
|
|
2388
|
+
if (attempt < PUT_DELAYS_MS.length) await wait(PUT_DELAYS_MS[attempt]);
|
|
2389
|
+
}
|
|
2390
|
+
return last;
|
|
2391
|
+
}
|
|
2327
2392
|
};
|
|
2328
2393
|
}
|
|
2329
|
-
var import_node_http, import_node_https, RETRY_DELAYS_MS, IDLE_MS, sleep;
|
|
2394
|
+
var import_node_fs2, import_node_stream, import_node_http, import_node_https, RETRY_DELAYS_MS, IDLE_MS, PUT_DELAYS_MS, withoutLength, codeOfError, sleep;
|
|
2330
2395
|
var init_client = __esm({
|
|
2331
2396
|
"src/reporter-core/client.ts"() {
|
|
2332
2397
|
"use strict";
|
|
2333
2398
|
init_cjs_shims();
|
|
2399
|
+
import_node_fs2 = require("fs");
|
|
2400
|
+
import_node_stream = require("stream");
|
|
2334
2401
|
import_node_http = require("http");
|
|
2335
2402
|
import_node_https = require("https");
|
|
2336
2403
|
RETRY_DELAYS_MS = [300, 900];
|
|
2337
2404
|
IDLE_MS = 3e5;
|
|
2405
|
+
PUT_DELAYS_MS = [500, 1500, 4e3];
|
|
2406
|
+
withoutLength = (headers) => Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== "content-length"));
|
|
2407
|
+
codeOfError = (err) => {
|
|
2408
|
+
const code = err?.code;
|
|
2409
|
+
return typeof code === "string" && /^[A-Z0-9_]{2,40}$/.test(code) ? code : "network";
|
|
2410
|
+
};
|
|
2338
2411
|
sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2339
2412
|
}
|
|
2340
2413
|
});
|
|
@@ -2476,7 +2549,7 @@ function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
|
2476
2549
|
}
|
|
2477
2550
|
function repoRootOf(dir, stopAt) {
|
|
2478
2551
|
for (let here = (0, import_node_path2.resolve)(dir); ; here = (0, import_node_path2.dirname)(here)) {
|
|
2479
|
-
if ((0,
|
|
2552
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path2.join)(here, ".git"))) return here;
|
|
2480
2553
|
if (here === stopAt || (0, import_node_path2.dirname)(here) === here) return void 0;
|
|
2481
2554
|
}
|
|
2482
2555
|
}
|
|
@@ -2496,12 +2569,12 @@ function machineless(raw, repoRoot, home) {
|
|
|
2496
2569
|
if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
|
|
2497
2570
|
return text2;
|
|
2498
2571
|
}
|
|
2499
|
-
function pathIn(
|
|
2500
|
-
const parts =
|
|
2572
|
+
function pathIn(path17) {
|
|
2573
|
+
const parts = path17.split(/[\\/]+/);
|
|
2501
2574
|
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
2502
2575
|
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
2503
2576
|
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
2504
|
-
const escaped = /^\/(?!\/)/.test(
|
|
2577
|
+
const escaped = /^\/(?!\/)/.test(path17) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
2505
2578
|
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
2506
2579
|
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
2507
2580
|
}
|
|
@@ -2546,35 +2619,36 @@ function placeOf(error, testFile) {
|
|
|
2546
2619
|
}
|
|
2547
2620
|
function fileOf(raw) {
|
|
2548
2621
|
if (!raw.startsWith("file://")) return raw;
|
|
2549
|
-
let
|
|
2622
|
+
let path17 = raw.slice("file://".length);
|
|
2550
2623
|
try {
|
|
2551
|
-
|
|
2624
|
+
path17 = decodeURIComponent(path17);
|
|
2552
2625
|
} catch {
|
|
2553
2626
|
}
|
|
2554
|
-
return /^\/[A-Za-z]:\//.test(
|
|
2627
|
+
return /^\/[A-Za-z]:\//.test(path17) ? path17.slice(1) : path17;
|
|
2555
2628
|
}
|
|
2556
2629
|
function fromRoot(place, root) {
|
|
2557
2630
|
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
2558
2631
|
const base = normal(root).replace(/\/+$/, "");
|
|
2559
2632
|
const file = normal(place.file);
|
|
2560
2633
|
if (!file.startsWith(`${base}/`)) return void 0;
|
|
2561
|
-
const
|
|
2562
|
-
if (!REPO_RELATIVE.test(
|
|
2634
|
+
const relative3 = file.slice(base.length + 1);
|
|
2635
|
+
if (!REPO_RELATIVE.test(relative3)) return void 0;
|
|
2563
2636
|
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
2564
|
-
return column === void 0 ? { file:
|
|
2637
|
+
return column === void 0 ? { file: relative3, line: place.line } : { file: relative3, line: place.line, column };
|
|
2565
2638
|
}
|
|
2566
2639
|
function webLink(href) {
|
|
2567
2640
|
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
2568
2641
|
}
|
|
2569
|
-
var
|
|
2642
|
+
var import_node_fs3, import_node_os2, import_node_path2, import_node_util, printable, 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
2643
|
var init_failure_detail = __esm({
|
|
2571
2644
|
"src/reporter-core/failure-detail.ts"() {
|
|
2572
2645
|
"use strict";
|
|
2573
2646
|
init_cjs_shims();
|
|
2574
|
-
|
|
2647
|
+
import_node_fs3 = require("fs");
|
|
2575
2648
|
import_node_os2 = require("os");
|
|
2576
2649
|
import_node_path2 = require("path");
|
|
2577
2650
|
import_node_util = require("util");
|
|
2651
|
+
printable = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, "?");
|
|
2578
2652
|
mediaTypeOf = (contentType) => contentType?.split(";")[0]?.trim().toLowerCase() || void 0;
|
|
2579
2653
|
TEXT_TYPES = /* @__PURE__ */ new Set(["application/json", "text/plain"]);
|
|
2580
2654
|
isTextType = (contentType) => TEXT_TYPES.has(mediaTypeOf(contentType) ?? "");
|
|
@@ -2590,7 +2664,7 @@ var init_failure_detail = __esm({
|
|
|
2590
2664
|
FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
2591
2665
|
FRAME_MAX = 8192;
|
|
2592
2666
|
firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
2593
|
-
normal = (
|
|
2667
|
+
normal = (path17) => import_node_path2.posix.normalize(path17.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
2594
2668
|
}
|
|
2595
2669
|
});
|
|
2596
2670
|
|
|
@@ -2610,7 +2684,13 @@ function nameOf(file) {
|
|
|
2610
2684
|
}
|
|
2611
2685
|
function kindOf(type) {
|
|
2612
2686
|
if (type.startsWith("image/")) return "screenshot";
|
|
2613
|
-
return TEXT_TYPES.has(type) ? "text" :
|
|
2687
|
+
return TEXT_TYPES.has(type) ? "text" : "file";
|
|
2688
|
+
}
|
|
2689
|
+
function linkOf(item) {
|
|
2690
|
+
const link = item;
|
|
2691
|
+
if (typeof link?.id !== "string" || typeof link.url !== "string") return void 0;
|
|
2692
|
+
const headers = typeof link.headers === "object" && link.headers !== null ? link.headers : {};
|
|
2693
|
+
return { id: link.id, url: link.url, headers };
|
|
2614
2694
|
}
|
|
2615
2695
|
function textTypeOf(contentType, type) {
|
|
2616
2696
|
const charset = /;\s*charset\s*=\s*"?([^\s";]+)"?/i.exec(contentType)?.[1];
|
|
@@ -2683,7 +2763,7 @@ function warnIfDropped(sent, run, log) {
|
|
|
2683
2763
|
`plune: this deployment stored none of ${names.join(", ")} \u2014 it is older than the fields. The run is otherwise reported in full.`
|
|
2684
2764
|
);
|
|
2685
2765
|
}
|
|
2686
|
-
async function startRun(passed,
|
|
2766
|
+
async function startRun(passed, declared2) {
|
|
2687
2767
|
const env = readEnv();
|
|
2688
2768
|
const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
|
|
2689
2769
|
const cfg = { ...env.config, ...stated };
|
|
@@ -2711,7 +2791,8 @@ async function startRun(passed, declared) {
|
|
|
2711
2791
|
unoffered: 0,
|
|
2712
2792
|
deferred: 0,
|
|
2713
2793
|
screenshots: { uploaded: 0, skipped: 0, failed: 0 },
|
|
2714
|
-
textFiles: { uploaded: 0, skipped: 0, failed: 0 }
|
|
2794
|
+
textFiles: { uploaded: 0, skipped: 0, failed: 0 },
|
|
2795
|
+
otherFiles: { uploaded: 0, skipped: 0, failed: 0 }
|
|
2715
2796
|
};
|
|
2716
2797
|
const resolved = /* @__PURE__ */ new Map();
|
|
2717
2798
|
let buffer = [];
|
|
@@ -2722,7 +2803,13 @@ async function startRun(passed, declared) {
|
|
|
2722
2803
|
let done = false;
|
|
2723
2804
|
const shortened = /* @__PURE__ */ new Set();
|
|
2724
2805
|
const skipsSaid = /* @__PURE__ */ new Set();
|
|
2725
|
-
const countsOf = (kind) => kind === "text" ? stats.textFiles : stats.screenshots;
|
|
2806
|
+
const countsOf = (kind) => kind === "text" ? stats.textFiles : kind === "file" ? stats.otherFiles : stats.screenshots;
|
|
2807
|
+
let bucketSeen = false;
|
|
2808
|
+
let noStorage = 0;
|
|
2809
|
+
let uploadsMissing = false;
|
|
2810
|
+
const failedSaid = /* @__PURE__ */ new Set();
|
|
2811
|
+
const codesSaid = /* @__PURE__ */ new Set();
|
|
2812
|
+
const rooms = /* @__PURE__ */ new Map();
|
|
2726
2813
|
function bound(keys) {
|
|
2727
2814
|
return keys.map((key) => {
|
|
2728
2815
|
const fit = boundKey(key);
|
|
@@ -2730,14 +2817,14 @@ async function startRun(passed, declared) {
|
|
|
2730
2817
|
shortened.add(key.value);
|
|
2731
2818
|
if (shortened.size === 1) {
|
|
2732
2819
|
log(
|
|
2733
|
-
`plune: a test name longer than ${KEY_MAX} characters was shortened to fit its key \u2014
|
|
2820
|
+
`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.`
|
|
2734
2821
|
);
|
|
2735
2822
|
}
|
|
2736
2823
|
}
|
|
2737
2824
|
return fit;
|
|
2738
2825
|
});
|
|
2739
2826
|
}
|
|
2740
|
-
const expected =
|
|
2827
|
+
const expected = declared2?.map(bound);
|
|
2741
2828
|
if (offline) {
|
|
2742
2829
|
log('plune: no API token \u2014 run "plune login" first. Results will be written to the fallback file.');
|
|
2743
2830
|
}
|
|
@@ -2860,20 +2947,49 @@ async function startRun(passed, declared) {
|
|
|
2860
2947
|
}
|
|
2861
2948
|
function skip(kind, name, why) {
|
|
2862
2949
|
countsOf(kind).skipped += 1;
|
|
2863
|
-
if (
|
|
2864
|
-
|
|
2865
|
-
|
|
2950
|
+
if (why === "noStorage") noStorage += 1;
|
|
2951
|
+
const said = why === "noStorage" ? why : `${kind}/${why}`;
|
|
2952
|
+
if (skipsSaid.has(said)) return;
|
|
2953
|
+
skipsSaid.add(said);
|
|
2954
|
+
if (why === "noStorage") log(`plune: "${name}" not stored \u2014 ${SKIPPED[why]}.`);
|
|
2955
|
+
else log(`plune: ${KINDS[kind].label} "${name}" not uploaded \u2014 ${SKIPPED[why]}.`);
|
|
2956
|
+
}
|
|
2957
|
+
function ask(job, size) {
|
|
2958
|
+
return client.post(`/v1/results/${encodeURIComponent(job.id)}/files/uploads`, {
|
|
2959
|
+
files: [{ name: job.name, contentType: job.type, size }]
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
function failed(job, why) {
|
|
2963
|
+
countsOf(job.kind).failed += 1;
|
|
2964
|
+
if (!failedSaid.has(job.kind)) {
|
|
2965
|
+
failedSaid.add(job.kind);
|
|
2966
|
+
log(`plune: could not upload a ${KINDS[job.kind].label} (${why}) \u2014 its result is reported without it.`);
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
function failedToBucket(job, code) {
|
|
2970
|
+
countsOf(job.kind).failed += 1;
|
|
2971
|
+
if (codesSaid.has(code)) return;
|
|
2972
|
+
codesSaid.add(code);
|
|
2973
|
+
log(`plune: "${job.name}" not uploaded to your bucket (${code}) \u2014 its result is reported without it.`);
|
|
2866
2974
|
}
|
|
2867
|
-
|
|
2975
|
+
function refusedHere(job, size) {
|
|
2976
|
+
if (job.kind === "screenshot") {
|
|
2977
|
+
if (!SCREENSHOT_TYPES.has(job.type)) return "type";
|
|
2978
|
+
if (size > SCREENSHOT_BYTES) return "size";
|
|
2979
|
+
}
|
|
2980
|
+
if (job.kind === "text" && size > TEXT_BYTES) return "textSize";
|
|
2981
|
+
return void 0;
|
|
2982
|
+
}
|
|
2983
|
+
async function uploadDirect(job, size) {
|
|
2868
2984
|
const { kind } = job;
|
|
2869
|
-
|
|
2985
|
+
if (kind === "file") return skip(kind, job.name, "noStorage");
|
|
2986
|
+
const { bytes: ceiling, size: tooBig } = KINDS[kind];
|
|
2987
|
+
if (size > ceiling) return skip(kind, job.name, tooBig);
|
|
2870
2988
|
let bytes;
|
|
2871
2989
|
if (job.body !== void 0) bytes = job.body;
|
|
2872
|
-
else if (job.path === void 0) return skip(kind, job.name, "missing");
|
|
2873
2990
|
else {
|
|
2874
2991
|
try {
|
|
2875
|
-
|
|
2876
|
-
bytes = await import_node_fs3.promises.readFile(job.path);
|
|
2992
|
+
bytes = await import_node_fs4.promises.readFile(job.path);
|
|
2877
2993
|
} catch {
|
|
2878
2994
|
return skip(kind, job.name, "missing");
|
|
2879
2995
|
}
|
|
@@ -2885,32 +3001,96 @@ async function startRun(passed, declared) {
|
|
|
2885
3001
|
bytes,
|
|
2886
3002
|
job.type
|
|
2887
3003
|
);
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
3004
|
+
if (out.ok) countsOf(kind).uploaded += 1;
|
|
3005
|
+
else failed(job, out.detail || out.kind);
|
|
3006
|
+
}
|
|
3007
|
+
async function uploadToBucket(job, size, first) {
|
|
3008
|
+
const source = job.body !== void 0 ? { body: job.body } : { path: job.path, size };
|
|
3009
|
+
let link = first;
|
|
3010
|
+
for (let renewed = false; ; ) {
|
|
3011
|
+
const put = await client.put(link.url, link.headers, source);
|
|
3012
|
+
if (put.ok) break;
|
|
3013
|
+
if (put.status !== 403 || renewed) return failedToBucket(job, put.code);
|
|
3014
|
+
renewed = true;
|
|
3015
|
+
const again = await ask(job, size);
|
|
3016
|
+
if (!again.ok) return failedToBucket(job, codeOf2(again));
|
|
3017
|
+
const fresh = linkOf(again.body?.files?.[0]);
|
|
3018
|
+
if (fresh === void 0) return failedToBucket(job, "bad_answer");
|
|
3019
|
+
link = fresh;
|
|
2892
3020
|
}
|
|
2893
|
-
|
|
2894
|
-
if (
|
|
2895
|
-
|
|
3021
|
+
const done2 = await client.post(`/v1/files/${encodeURIComponent(link.id)}/complete`, {});
|
|
3022
|
+
if (!done2.ok) return failedToBucket(job, codeOf2(done2));
|
|
3023
|
+
countsOf(job.kind).uploaded += 1;
|
|
3024
|
+
}
|
|
3025
|
+
let free = UPLOADS_AT_ONCE;
|
|
3026
|
+
const waiting = [];
|
|
3027
|
+
async function gated(work) {
|
|
3028
|
+
if (free === 0) await new Promise((resolve9) => waiting.push(resolve9));
|
|
3029
|
+
else free -= 1;
|
|
3030
|
+
try {
|
|
3031
|
+
return await work();
|
|
3032
|
+
} finally {
|
|
3033
|
+
const next = waiting.shift();
|
|
3034
|
+
if (next !== void 0) next();
|
|
3035
|
+
else free += 1;
|
|
2896
3036
|
}
|
|
2897
3037
|
}
|
|
3038
|
+
async function plan(job) {
|
|
3039
|
+
const { kind } = job;
|
|
3040
|
+
let size;
|
|
3041
|
+
if (job.body !== void 0) size = job.body.length;
|
|
3042
|
+
else if (job.path === void 0) return void skip(kind, job.name, "missing");
|
|
3043
|
+
else {
|
|
3044
|
+
try {
|
|
3045
|
+
size = (await import_node_fs4.promises.stat(job.path)).size;
|
|
3046
|
+
} catch {
|
|
3047
|
+
return void skip(kind, job.name, "missing");
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
if (size === 0) return void skip(kind, job.name, "empty");
|
|
3051
|
+
const direct = () => {
|
|
3052
|
+
if (kind === "file") return void skip(kind, job.name, "noStorage");
|
|
3053
|
+
const roomKey = `${job.id}/${kind}`;
|
|
3054
|
+
const left = rooms.get(roomKey) ?? KINDS[kind].room;
|
|
3055
|
+
if (left === 0) return void skip(kind, job.name, KINDS[kind].limit);
|
|
3056
|
+
rooms.set(roomKey, left - 1);
|
|
3057
|
+
return () => uploadDirect(job, size);
|
|
3058
|
+
};
|
|
3059
|
+
if (uploadsMissing) return direct();
|
|
3060
|
+
const out = await gated(() => ask(job, size));
|
|
3061
|
+
if (!out.ok) {
|
|
3062
|
+
if (out.status === 404 && out.detail === "" && out.code === void 0) {
|
|
3063
|
+
uploadsMissing = true;
|
|
3064
|
+
return direct();
|
|
3065
|
+
}
|
|
3066
|
+
if (out.code !== void 0) failedToBucket(job, out.code);
|
|
3067
|
+
else failed(job, out.detail || out.kind);
|
|
3068
|
+
return void 0;
|
|
3069
|
+
}
|
|
3070
|
+
const item = out.body?.files?.[0];
|
|
3071
|
+
if (item?.skipped === "no_storage") return void skip(kind, job.name, refusedHere(job, size) ?? "noStorage");
|
|
3072
|
+
if (item?.direct === true) return direct();
|
|
3073
|
+
const link = linkOf(item);
|
|
3074
|
+
if (link === void 0) return void failed(job, "bad_answer");
|
|
3075
|
+
if (!bucketSeen) {
|
|
3076
|
+
bucketSeen = true;
|
|
3077
|
+
log(BUCKET_FOUND);
|
|
3078
|
+
}
|
|
3079
|
+
return () => uploadToBucket(job, size, link);
|
|
3080
|
+
}
|
|
2898
3081
|
async function upload(items, filesByKey) {
|
|
2899
3082
|
if (filesByKey.size === 0 || !Array.isArray(items)) return;
|
|
2900
3083
|
const jobs = [];
|
|
2901
3084
|
for (const item of items) {
|
|
2902
3085
|
if (item?.status !== "accepted" || typeof item.id !== "string" || typeof item.resultKey !== "string") continue;
|
|
2903
|
-
const room = { screenshot: KINDS.screenshot.room, text: KINDS.text.room };
|
|
2904
3086
|
for (const file of filesByKey.get(item.resultKey) ?? []) {
|
|
2905
3087
|
const type = mediaTypeOf(file.contentType) ?? (file.path === void 0 ? void 0 : TYPE_BY_EXTENSION[(0, import_node_path3.extname)(file.path).toLowerCase()]);
|
|
2906
|
-
|
|
2907
|
-
|
|
3088
|
+
if (type === void 0 || type === METADATA_TYPE2) continue;
|
|
3089
|
+
const kind = kindOf(type);
|
|
3090
|
+
if (kind !== "text" && file.path === void 0) continue;
|
|
2908
3091
|
const name = nameOf(file);
|
|
2909
3092
|
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);
|
|
2912
3093
|
else {
|
|
2913
|
-
room[kind] -= 1;
|
|
2914
3094
|
jobs.push({
|
|
2915
3095
|
id: item.id,
|
|
2916
3096
|
kind,
|
|
@@ -2922,10 +3102,16 @@ async function startRun(passed, declared) {
|
|
|
2922
3102
|
}
|
|
2923
3103
|
}
|
|
2924
3104
|
}
|
|
2925
|
-
|
|
3105
|
+
const byResult = /* @__PURE__ */ new Map();
|
|
3106
|
+
for (const job of jobs) byResult.set(job.id, [...byResult.get(job.id) ?? [], job]);
|
|
2926
3107
|
await Promise.all(
|
|
2927
|
-
|
|
2928
|
-
|
|
3108
|
+
[...byResult.values()].map(async (group) => {
|
|
3109
|
+
const work = [];
|
|
3110
|
+
for (const job of group) {
|
|
3111
|
+
const act = await plan(job);
|
|
3112
|
+
if (act !== void 0) work.push(gated(act));
|
|
3113
|
+
}
|
|
3114
|
+
await Promise.all(work);
|
|
2929
3115
|
})
|
|
2930
3116
|
);
|
|
2931
3117
|
}
|
|
@@ -3003,8 +3189,20 @@ async function startRun(passed, declared) {
|
|
|
3003
3189
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
3004
3190
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
3005
3191
|
if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
|
|
3006
|
-
|
|
3007
|
-
|
|
3192
|
+
const kinds = [stats.screenshots, stats.textFiles, stats.otherFiles];
|
|
3193
|
+
const sum = (what) => kinds.reduce((n, counts) => n + counts[what], 0);
|
|
3194
|
+
if (bucketSeen) {
|
|
3195
|
+
const other = sum("skipped") - noStorage;
|
|
3196
|
+
parts.push(
|
|
3197
|
+
`${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` : "")
|
|
3198
|
+
);
|
|
3199
|
+
} else {
|
|
3200
|
+
for (const said of [tally(stats.screenshots, "screenshot"), tally(stats.textFiles, "text file")]) {
|
|
3201
|
+
if (said !== void 0) parts.push(said);
|
|
3202
|
+
}
|
|
3203
|
+
const other = stats.otherFiles;
|
|
3204
|
+
if (other.skipped > 0) parts.push(`${other.skipped} file(s) not stored (no bucket connected)`);
|
|
3205
|
+
if (other.failed > 0) parts.push(`${other.failed} file(s) failed to upload`);
|
|
3008
3206
|
}
|
|
3009
3207
|
if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
|
|
3010
3208
|
if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
|
|
@@ -3057,13 +3255,13 @@ async function startRun(passed, declared) {
|
|
|
3057
3255
|
}
|
|
3058
3256
|
};
|
|
3059
3257
|
}
|
|
3060
|
-
var import_node_crypto2,
|
|
3258
|
+
var import_node_crypto2, import_node_fs4, 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, METADATA_TYPE2, UPLOADS_AT_ONCE, NAME_MAX, SKIPPED, BUCKET_FOUND, KINDS, codeOf2, BATCH_BYTES, ENVELOPE_BYTES;
|
|
3061
3259
|
var init_session = __esm({
|
|
3062
3260
|
"src/reporter-core/session.ts"() {
|
|
3063
3261
|
"use strict";
|
|
3064
3262
|
init_cjs_shims();
|
|
3065
3263
|
import_node_crypto2 = require("crypto");
|
|
3066
|
-
|
|
3264
|
+
import_node_fs4 = require("fs");
|
|
3067
3265
|
import_node_path3 = require("path");
|
|
3068
3266
|
import_node_util2 = require("util");
|
|
3069
3267
|
init_api_url();
|
|
@@ -3092,6 +3290,7 @@ var init_session = __esm({
|
|
|
3092
3290
|
".jpeg": "image/jpeg",
|
|
3093
3291
|
".webp": "image/webp"
|
|
3094
3292
|
};
|
|
3293
|
+
METADATA_TYPE2 = "application/plune.metadata+json";
|
|
3095
3294
|
UPLOADS_AT_ONCE = 4;
|
|
3096
3295
|
NAME_MAX = 200;
|
|
3097
3296
|
SKIPPED = {
|
|
@@ -3104,12 +3303,17 @@ var init_session = __esm({
|
|
|
3104
3303
|
textSize: "Plune takes a text file of at most 512 KiB",
|
|
3105
3304
|
empty: "the file is empty",
|
|
3106
3305
|
binary: "the file holds a NUL byte, and Plune takes UTF-8 text without one",
|
|
3107
|
-
encoding: "Plune takes UTF-8 text only"
|
|
3306
|
+
encoding: "Plune takes UTF-8 text only",
|
|
3307
|
+
noStorage: "no storage bucket is connected to this project"
|
|
3108
3308
|
};
|
|
3309
|
+
BUCKET_FOUND = "plune: storage bucket found for this project \u2014 screenshots, text, trace and video go straight to it over short-lived links.";
|
|
3109
3310
|
KINDS = {
|
|
3110
3311
|
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" }
|
|
3312
|
+
text: { label: "text file", bytes: TEXT_BYTES, room: TEXTS_PER_RESULT, limit: "textLimit", size: "textSize" },
|
|
3313
|
+
// 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.
|
|
3314
|
+
file: { label: "file", bytes: 0, room: 0, limit: "limit", size: "size" }
|
|
3112
3315
|
};
|
|
3316
|
+
codeOf2 = (out) => out.code ?? (out.status === null ? "network" : `http_${out.status}`);
|
|
3113
3317
|
BATCH_BYTES = 8 * 1024 * 1024;
|
|
3114
3318
|
ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ results: [] }));
|
|
3115
3319
|
}
|
|
@@ -3200,21 +3404,21 @@ async function handleRunDelete(id, options = {}) {
|
|
|
3200
3404
|
);
|
|
3201
3405
|
}
|
|
3202
3406
|
write(`Run ${id} deleted, with its results and the review-queue entries it produced.`);
|
|
3203
|
-
write("Recoverable for
|
|
3407
|
+
write("Recoverable for 30 days \u2014 ask Plune to put it back.");
|
|
3204
3408
|
}
|
|
3205
3409
|
async function handleRunExec(options) {
|
|
3206
3410
|
const [command, ...args] = options.argv;
|
|
3207
3411
|
if (command === void 0) throw new RunCommandError("Nothing to run \u2014 pass a command after `--`.");
|
|
3208
3412
|
const run = await handleRunStart(options);
|
|
3209
3413
|
const spawnImpl = options.spawnImpl ?? import_node_child_process.spawn;
|
|
3210
|
-
const code = await new Promise((
|
|
3414
|
+
const code = await new Promise((resolve9) => {
|
|
3211
3415
|
const child = spawnImpl(command, args, {
|
|
3212
3416
|
stdio: "inherit",
|
|
3213
3417
|
shell: process.platform === "win32",
|
|
3214
3418
|
env: { ...process.env, PLUNE_RUN: run.externalKey, PLUNE_PROCEED: "1" }
|
|
3215
3419
|
});
|
|
3216
|
-
child.on("close", (exitCode) =>
|
|
3217
|
-
child.on("error", () =>
|
|
3420
|
+
child.on("close", (exitCode) => resolve9(exitCode ?? 1));
|
|
3421
|
+
child.on("error", () => resolve9(1));
|
|
3218
3422
|
});
|
|
3219
3423
|
await handleRunFinish(run.id, options);
|
|
3220
3424
|
return code;
|
|
@@ -3345,6 +3549,346 @@ var init_result_key = __esm({
|
|
|
3345
3549
|
}
|
|
3346
3550
|
});
|
|
3347
3551
|
|
|
3552
|
+
// src/importers/attachments.ts
|
|
3553
|
+
function reportFolder(report) {
|
|
3554
|
+
const folder = path9.dirname(path9.resolve(report));
|
|
3555
|
+
try {
|
|
3556
|
+
return fs8.realpathSync(folder);
|
|
3557
|
+
} catch {
|
|
3558
|
+
return folder;
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
function reportFolders(report) {
|
|
3562
|
+
return { lexical: path9.dirname(path9.resolve(report)), real: reportFolder(report) };
|
|
3563
|
+
}
|
|
3564
|
+
function within(folder, target) {
|
|
3565
|
+
const rel = path9.relative(folder, target);
|
|
3566
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel);
|
|
3567
|
+
}
|
|
3568
|
+
function resolveAttachment(folder, ref, lexical = folder) {
|
|
3569
|
+
if (/^[\\/]{2}/.test(ref) || ref.includes("\0")) return { why: "outside" };
|
|
3570
|
+
const face = path9.resolve(lexical, ref);
|
|
3571
|
+
if (!within(lexical, face) && !within(folder, face)) return { why: "outside" };
|
|
3572
|
+
let real;
|
|
3573
|
+
try {
|
|
3574
|
+
real = fs8.realpathSync(path9.resolve(folder, ref));
|
|
3575
|
+
} catch {
|
|
3576
|
+
return { why: "absent" };
|
|
3577
|
+
}
|
|
3578
|
+
if (!within(folder, real)) return { why: "outside" };
|
|
3579
|
+
try {
|
|
3580
|
+
return fs8.statSync(real).isFile() ? { file: real } : { why: "not-a-file" };
|
|
3581
|
+
} catch {
|
|
3582
|
+
return { why: "absent" };
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
function markersOf(test) {
|
|
3586
|
+
const refs = /* @__PURE__ */ new Set();
|
|
3587
|
+
for (const stream of test.children) {
|
|
3588
|
+
if (stream.name !== "system-out" && stream.name !== "system-err") continue;
|
|
3589
|
+
for (const match of stream.text.matchAll(MARKER)) {
|
|
3590
|
+
const ref = match[1]?.trim() ?? "";
|
|
3591
|
+
if (ref !== "") refs.add(ref);
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
return [...refs];
|
|
3595
|
+
}
|
|
3596
|
+
function filesOfTest(test, folder, title, missing, options = {}) {
|
|
3597
|
+
const files = [];
|
|
3598
|
+
const all = markersOf(test);
|
|
3599
|
+
if (all.length > MARKERS_PER_TEST) {
|
|
3600
|
+
options.notes?.push(
|
|
3601
|
+
`plune: ${JSON.stringify(printable(title))}: ${all.length - MARKERS_PER_TEST} more markers ignored (at most ${MARKERS_PER_TEST} per test)`
|
|
3602
|
+
);
|
|
3603
|
+
}
|
|
3604
|
+
for (const ref of all.slice(0, MARKERS_PER_TEST)) {
|
|
3605
|
+
const found = resolveAttachment(folder, ref, options.lexical);
|
|
3606
|
+
if (!("file" in found)) {
|
|
3607
|
+
missing.push({ title, path: ref, ...options.resultKey !== void 0 ? { resultKey: options.resultKey } : {} });
|
|
3608
|
+
continue;
|
|
3609
|
+
}
|
|
3610
|
+
const real = found.file;
|
|
3611
|
+
if (!files.some((f) => f.path === real)) {
|
|
3612
|
+
files.push({ name: printable(ref.split(/[\\/]/).pop() || path9.basename(real)), contentType: typeOfFile(real), path: real });
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
return files;
|
|
3616
|
+
}
|
|
3617
|
+
var fs8, path9, MARKER, TYPES, typeOfFile, MARKERS_PER_TEST;
|
|
3618
|
+
var init_attachments = __esm({
|
|
3619
|
+
"src/importers/attachments.ts"() {
|
|
3620
|
+
"use strict";
|
|
3621
|
+
init_cjs_shims();
|
|
3622
|
+
fs8 = __toESM(require("fs"), 1);
|
|
3623
|
+
path9 = __toESM(require("path"), 1);
|
|
3624
|
+
init_failure_detail();
|
|
3625
|
+
MARKER = /^[ \t]*\[\[ATTACHMENT\|([^\r\n]*?)\]\][ \t]*$/gm;
|
|
3626
|
+
TYPES = {
|
|
3627
|
+
".png": "image/png",
|
|
3628
|
+
".jpg": "image/jpeg",
|
|
3629
|
+
".jpeg": "image/jpeg",
|
|
3630
|
+
".webp": "image/webp",
|
|
3631
|
+
".gif": "image/gif",
|
|
3632
|
+
".svg": "image/svg+xml",
|
|
3633
|
+
".json": "application/json",
|
|
3634
|
+
".txt": "text/plain",
|
|
3635
|
+
".log": "text/plain",
|
|
3636
|
+
".md": "text/plain",
|
|
3637
|
+
".csv": "text/csv",
|
|
3638
|
+
".html": "text/html",
|
|
3639
|
+
".htm": "text/html",
|
|
3640
|
+
".xml": "application/xml",
|
|
3641
|
+
".pdf": "application/pdf",
|
|
3642
|
+
".zip": "application/zip",
|
|
3643
|
+
".webm": "video/webm",
|
|
3644
|
+
".mp4": "video/mp4",
|
|
3645
|
+
".har": "application/json"
|
|
3646
|
+
};
|
|
3647
|
+
typeOfFile = (file) => TYPES[path9.extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
3648
|
+
MARKERS_PER_TEST = 100;
|
|
3649
|
+
}
|
|
3650
|
+
});
|
|
3651
|
+
|
|
3652
|
+
// src/importers/allure.ts
|
|
3653
|
+
function isFolder(path17) {
|
|
3654
|
+
try {
|
|
3655
|
+
return fs9.statSync(path17).isDirectory();
|
|
3656
|
+
} catch {
|
|
3657
|
+
return false;
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
function load(file, maxBytes) {
|
|
3661
|
+
let size;
|
|
3662
|
+
try {
|
|
3663
|
+
size = fs9.statSync(file).size;
|
|
3664
|
+
} catch (err) {
|
|
3665
|
+
throw new AllureReportError(`${printable(file)} \u2014 cannot read \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
3666
|
+
}
|
|
3667
|
+
if (size > maxBytes) {
|
|
3668
|
+
throw new AllureReportError(`${printable(file)} \u2014 ${size} bytes, over the ${maxBytes} an Allure result may have; not read.`);
|
|
3669
|
+
}
|
|
3670
|
+
let parsed;
|
|
3671
|
+
try {
|
|
3672
|
+
parsed = JSON.parse(fs9.readFileSync(file, "utf8"));
|
|
3673
|
+
} catch (err) {
|
|
3674
|
+
throw new AllureReportError(`${printable(file)} \u2014 not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
3675
|
+
}
|
|
3676
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
3677
|
+
throw new AllureReportError(`${printable(file)} \u2014 not an Allure result: expected a JSON object.`);
|
|
3678
|
+
}
|
|
3679
|
+
return parsed;
|
|
3680
|
+
}
|
|
3681
|
+
function listOf(v) {
|
|
3682
|
+
const out = [];
|
|
3683
|
+
if (!Array.isArray(v)) return out;
|
|
3684
|
+
for (const x of v) if (typeof x === "object" && x !== null && !Array.isArray(x)) out.push(x);
|
|
3685
|
+
return out;
|
|
3686
|
+
}
|
|
3687
|
+
function keyOf(r, name) {
|
|
3688
|
+
const full = textOf(r.fullName);
|
|
3689
|
+
if (name.endsWith("]") && full.includes("#")) return `${full.split("#")[0]}#${name}`;
|
|
3690
|
+
let base;
|
|
3691
|
+
if (full.includes(" \u203A ")) base = full.split(" \u203A ").join("#");
|
|
3692
|
+
else if (full.includes("#")) base = full;
|
|
3693
|
+
else if (labelOf(r, "package") !== "") base = `${labelOf(r, "package")}#${name}`;
|
|
3694
|
+
else base = full !== "" ? full : name;
|
|
3695
|
+
const params = listOf(r.parameters).map((p) => textOf(p.value));
|
|
3696
|
+
return params.length > 0 && !base.endsWith("]") ? `${base}[${params.join("-")}]` : base;
|
|
3697
|
+
}
|
|
3698
|
+
function labelGroup(r) {
|
|
3699
|
+
const out = [];
|
|
3700
|
+
for (const l of ["parentSuite", "suite", "subSuite"]) {
|
|
3701
|
+
const v = labelOf(r, l);
|
|
3702
|
+
if (v !== "" && out.at(-1) !== v) out.push(v);
|
|
3703
|
+
}
|
|
3704
|
+
return out;
|
|
3705
|
+
}
|
|
3706
|
+
function attachmentsOf(holder, out) {
|
|
3707
|
+
let deep = false;
|
|
3708
|
+
const todo = [{ h: holder, depth: 0 }];
|
|
3709
|
+
while (todo.length > 0) {
|
|
3710
|
+
const { h, depth } = todo.pop();
|
|
3711
|
+
for (const a of listOf(h.attachments)) out.push(a);
|
|
3712
|
+
const steps = listOf(h.steps);
|
|
3713
|
+
if (depth >= MAX_DEPTH) {
|
|
3714
|
+
deep ||= steps.length > 0;
|
|
3715
|
+
continue;
|
|
3716
|
+
}
|
|
3717
|
+
for (let i = steps.length - 1; i >= 0; i--) todo.push({ h: steps[i], depth: depth + 1 });
|
|
3718
|
+
}
|
|
3719
|
+
return deep;
|
|
3720
|
+
}
|
|
3721
|
+
function readAllure(dir, missing = [], notes = [], maxBytes = MAX_JSON_BYTES) {
|
|
3722
|
+
if (!isFolder(dir)) throw new AllureReportError(`${printable(dir)} \u2014 not a folder.`);
|
|
3723
|
+
const resultNames = namesIn(dir, RESULT);
|
|
3724
|
+
if (resultNames.length === 0) {
|
|
3725
|
+
throw new AllureReportError(
|
|
3726
|
+
`${printable(dir)} \u2014 no *-result.json files in it, so there is no Allure report to read. Point at the folder an Allure adapter wrote (pytest --alluredir, allure-results), or at a JUnit or Playwright JSON file.`
|
|
3727
|
+
);
|
|
3728
|
+
}
|
|
3729
|
+
const { lexical, real: folder } = reportFolders(path10.join(dir, "_"));
|
|
3730
|
+
const tests = resultNames.map((n) => load(path10.join(dir, n), maxBytes));
|
|
3731
|
+
const containers = namesIn(dir, CONTAINER).map((n) => load(path10.join(dir, n), maxBytes));
|
|
3732
|
+
const direct = /* @__PURE__ */ new Map();
|
|
3733
|
+
for (const c of containers) {
|
|
3734
|
+
for (const child of Array.isArray(c.children) ? c.children : []) {
|
|
3735
|
+
if (typeof child !== "string") continue;
|
|
3736
|
+
const list2 = direct.get(child);
|
|
3737
|
+
if (list2 === void 0) direct.set(child, [c]);
|
|
3738
|
+
else list2.push(c);
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
const around = (uuid) => {
|
|
3742
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3743
|
+
const todo = [uuid];
|
|
3744
|
+
while (todo.length > 0) {
|
|
3745
|
+
for (const c of direct.get(todo.pop()) ?? []) {
|
|
3746
|
+
if (seen.has(c)) continue;
|
|
3747
|
+
seen.add(c);
|
|
3748
|
+
if (typeof c.uuid === "string") todo.push(c.uuid);
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
return [...seen];
|
|
3752
|
+
};
|
|
3753
|
+
const ctx = { folder, lexical, missing, notes, around, direct };
|
|
3754
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3755
|
+
tests.forEach((r, i) => {
|
|
3756
|
+
const id = textOf(r.historyId);
|
|
3757
|
+
const g = id === "" ? `\0${i}` : id;
|
|
3758
|
+
const list2 = groups.get(g);
|
|
3759
|
+
if (list2 === void 0) groups.set(g, [r]);
|
|
3760
|
+
else list2.push(r);
|
|
3761
|
+
});
|
|
3762
|
+
const out = [];
|
|
3763
|
+
for (const attempts of groups.values()) {
|
|
3764
|
+
const ordered = attempts.map((r, i) => ({ r, i })).sort(
|
|
3765
|
+
(a, b) => stopOf(a.r) - stopOf(b.r) || (timeOf(a.r.start) ?? 0) - (timeOf(b.r.start) ?? 0) || compareText(textOf(a.r.uuid), textOf(b.r.uuid)) || a.i - b.i
|
|
3766
|
+
).map((x) => x.r);
|
|
3767
|
+
ordered.forEach((r, retry) => out.push(pendingOf(r, retry, ctx)));
|
|
3768
|
+
}
|
|
3769
|
+
return out;
|
|
3770
|
+
}
|
|
3771
|
+
function pendingOf(r, retry, ctx) {
|
|
3772
|
+
const name = textOf(r.name);
|
|
3773
|
+
const uuid = textOf(r.uuid);
|
|
3774
|
+
const key = keyOf(r, name);
|
|
3775
|
+
const ancestors = uuid === "" ? [] : ctx.around(uuid);
|
|
3776
|
+
const suite = (uuid === "" ? [] : ctx.direct.get(uuid) ?? []).filter((c) => textOf(c.name) !== "" && c.name !== name).sort((a, b) => (Array.isArray(b.children) ? b.children.length : 0) - (Array.isArray(a.children) ? a.children.length : 0))[0];
|
|
3777
|
+
const group = suite !== void 0 ? [textOf(suite.name)] : labelGroup(r);
|
|
3778
|
+
const title = [...group, name].join(" \u203A ");
|
|
3779
|
+
const safeTitle = printable(title);
|
|
3780
|
+
const rkey = resultKey(key, retry);
|
|
3781
|
+
const files = [];
|
|
3782
|
+
const announced = /* @__PURE__ */ new Set();
|
|
3783
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
3784
|
+
const announce = (att) => {
|
|
3785
|
+
const source = textOf(att.source);
|
|
3786
|
+
if (source === "" || announced.has(source) || ignored.has(source)) return;
|
|
3787
|
+
if (announced.size >= MARKERS_PER_TEST) {
|
|
3788
|
+
ignored.add(source);
|
|
3789
|
+
return;
|
|
3790
|
+
}
|
|
3791
|
+
announced.add(source);
|
|
3792
|
+
const found = resolveAttachment(ctx.folder, source, ctx.lexical);
|
|
3793
|
+
if (!("file" in found)) {
|
|
3794
|
+
ctx.missing.push({ title: safeTitle, resultKey: rkey, path: source });
|
|
3795
|
+
return;
|
|
3796
|
+
}
|
|
3797
|
+
if (files.some((f) => f.path === found.file)) return;
|
|
3798
|
+
const type = textOf(att.type).trim();
|
|
3799
|
+
const name2 = printable(textOf(att.name) !== "" ? textOf(att.name) : path10.basename(found.file));
|
|
3800
|
+
files.push({ name: name2, contentType: type !== "" ? type : typeOfFile(found.file), path: found.file });
|
|
3801
|
+
};
|
|
3802
|
+
const holders = [];
|
|
3803
|
+
for (const c of [...ancestors].reverse()) for (const h of listOf(c.befores)) holders.push(h);
|
|
3804
|
+
holders.push(r);
|
|
3805
|
+
for (const c of ancestors) for (const h of listOf(c.afters)) holders.push(h);
|
|
3806
|
+
const atts = [];
|
|
3807
|
+
let deep = false;
|
|
3808
|
+
for (const h of holders) deep = attachmentsOf(h, atts) || deep;
|
|
3809
|
+
atts.forEach(announce);
|
|
3810
|
+
if (ignored.size > 0) {
|
|
3811
|
+
ctx.notes.push(`plune: ${JSON.stringify(safeTitle)}: ${ignored.size} more markers ignored (at most ${MARKERS_PER_TEST} per test)`);
|
|
3812
|
+
}
|
|
3813
|
+
if (deep) ctx.notes.push(`plune: ${JSON.stringify(safeTitle)}: steps nested deeper than ${MAX_DEPTH} are ignored`);
|
|
3814
|
+
const status = textOf(r.status) !== "" ? textOf(r.status) : "unknown";
|
|
3815
|
+
const details = typeof r.statusDetails === "object" && r.statusDetails !== null ? r.statusDetails : {};
|
|
3816
|
+
const message = textOf(details.message);
|
|
3817
|
+
const trace = textOf(details.trace);
|
|
3818
|
+
const attempt = {
|
|
3819
|
+
status,
|
|
3820
|
+
errors: [{ text: [message, trace].filter((s) => s !== "").join("\n\n") }],
|
|
3821
|
+
steps: declared(r.steps),
|
|
3822
|
+
attachments: files.map(({ name: n, contentType, path: p }) => ({
|
|
3823
|
+
name: n,
|
|
3824
|
+
...contentType !== void 0 ? { contentType } : {},
|
|
3825
|
+
...p !== void 0 ? { path: p } : {}
|
|
3826
|
+
})),
|
|
3827
|
+
testFile: ""
|
|
3828
|
+
};
|
|
3829
|
+
const errorContext = errorContextOf(attempt);
|
|
3830
|
+
const failure = FAILED.has(status) ? failureOf(attempt) : void 0;
|
|
3831
|
+
const pkg2 = labelOf(r, "package");
|
|
3832
|
+
const where = (key.includes("#") ? key.split("#")[0] : pkg2)?.replace(/^[A-Za-z][A-Za-z0-9_-]+:(?![\\/])/, "");
|
|
3833
|
+
const stated = TOKEN.exec(name)?.[1];
|
|
3834
|
+
const started = timeOf(r.start);
|
|
3835
|
+
const stopped = timeOf(r.stop);
|
|
3836
|
+
const durationMs = started !== void 0 && stopped !== void 0 ? Math.max(0, stopped - started) : 0;
|
|
3837
|
+
return {
|
|
3838
|
+
resultKey: rkey,
|
|
3839
|
+
...stated !== void 0 ? { testCaseId: stated } : {},
|
|
3840
|
+
keys: [{ kind: "path-title", value: key }],
|
|
3841
|
+
source: SOURCE,
|
|
3842
|
+
rawStatus: status,
|
|
3843
|
+
title,
|
|
3844
|
+
...where !== void 0 && where !== "" ? { specRef: where } : {},
|
|
3845
|
+
...started !== void 0 ? {
|
|
3846
|
+
execution: {
|
|
3847
|
+
startedAt: new Date(started).toISOString(),
|
|
3848
|
+
finishedAt: new Date(started + durationMs).toISOString(),
|
|
3849
|
+
durationMs,
|
|
3850
|
+
retry
|
|
3851
|
+
}
|
|
3852
|
+
} : {},
|
|
3853
|
+
...errorContext !== "" ? { errorContext } : {},
|
|
3854
|
+
...failure !== void 0 ? { failure } : {},
|
|
3855
|
+
...files.length > 0 ? { files } : {}
|
|
3856
|
+
};
|
|
3857
|
+
}
|
|
3858
|
+
var fs9, path10, SOURCE, TOKEN, AllureReportError, RESULT, CONTAINER, MAX_JSON_BYTES, MAX_DEPTH, MAX_TIME, namesIn, timeOf, textOf, labelOf, FAILED, declared, stopOf, compareText;
|
|
3859
|
+
var init_allure = __esm({
|
|
3860
|
+
"src/importers/allure.ts"() {
|
|
3861
|
+
"use strict";
|
|
3862
|
+
init_cjs_shims();
|
|
3863
|
+
fs9 = __toESM(require("fs"), 1);
|
|
3864
|
+
path10 = __toESM(require("path"), 1);
|
|
3865
|
+
init_failure_detail();
|
|
3866
|
+
init_result_key();
|
|
3867
|
+
init_attachments();
|
|
3868
|
+
SOURCE = "allure";
|
|
3869
|
+
TOKEN = /@P([A-Za-z0-9_-]+)/;
|
|
3870
|
+
AllureReportError = class extends Error {
|
|
3871
|
+
constructor(message) {
|
|
3872
|
+
super(message);
|
|
3873
|
+
this.name = "AllureReportError";
|
|
3874
|
+
}
|
|
3875
|
+
};
|
|
3876
|
+
RESULT = /-result\.json$/;
|
|
3877
|
+
CONTAINER = /-container\.json$/;
|
|
3878
|
+
MAX_JSON_BYTES = 64 * 1024 * 1024;
|
|
3879
|
+
MAX_DEPTH = 256;
|
|
3880
|
+
MAX_TIME = 864e13;
|
|
3881
|
+
namesIn = (dir, pattern) => fs9.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile() && pattern.test(d.name)).map((d) => d.name).sort();
|
|
3882
|
+
timeOf = (v) => typeof v === "number" && Math.abs(v) <= MAX_TIME ? v : void 0;
|
|
3883
|
+
textOf = (v) => typeof v === "string" ? v : "";
|
|
3884
|
+
labelOf = (r, name) => textOf(listOf(r.labels).find((l) => l.name === name)?.value);
|
|
3885
|
+
FAILED = /* @__PURE__ */ new Set(["failed", "broken"]);
|
|
3886
|
+
declared = (steps, depth = 0) => depth >= MAX_DEPTH ? [] : listOf(steps).map((s) => ({ title: textOf(s.name), failed: FAILED.has(textOf(s.status)), steps: declared(s.steps, depth + 1) }));
|
|
3887
|
+
stopOf = (r) => timeOf(r.stop) ?? timeOf(r.start) ?? 0;
|
|
3888
|
+
compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
3889
|
+
}
|
|
3890
|
+
});
|
|
3891
|
+
|
|
3348
3892
|
// src/importers/xml.ts
|
|
3349
3893
|
function decode(raw) {
|
|
3350
3894
|
if (!raw.includes("&")) return raw;
|
|
@@ -3409,9 +3953,9 @@ function parseXml(source, file) {
|
|
|
3409
3953
|
if (end === -1) throw new XmlParseError(file, line, "a closing tag is never closed");
|
|
3410
3954
|
const name2 = source.slice(i + 2, end).trim();
|
|
3411
3955
|
const open = top();
|
|
3412
|
-
if (open === root) throw new XmlParseError(file, line, `</${name2}> closes nothing`);
|
|
3956
|
+
if (open === root) throw new XmlParseError(file, line, `</${printable(name2)}> closes nothing`);
|
|
3413
3957
|
if (open.name !== name2) {
|
|
3414
|
-
throw new XmlParseError(file, line, `</${name2}> closes <${open.name}>, opened on line ${open.line}`);
|
|
3958
|
+
throw new XmlParseError(file, line, `</${printable(name2)}> closes <${printable(open.name)}>, opened on line ${open.line}`);
|
|
3415
3959
|
}
|
|
3416
3960
|
stack.pop();
|
|
3417
3961
|
advance(end + 1);
|
|
@@ -3427,7 +3971,7 @@ function parseXml(source, file) {
|
|
|
3427
3971
|
let selfClosing = false;
|
|
3428
3972
|
for (; ; ) {
|
|
3429
3973
|
while (j < source.length && SPACE.test(source[j])) j += 1;
|
|
3430
|
-
if (j >= source.length) throw new XmlParseError(file, startLine, `<${name}> is never closed`);
|
|
3974
|
+
if (j >= source.length) throw new XmlParseError(file, startLine, `<${printable(name)}> is never closed`);
|
|
3431
3975
|
if (source[j] === "/" && source[j + 1] === ">") {
|
|
3432
3976
|
selfClosing = true;
|
|
3433
3977
|
j += 2;
|
|
@@ -3440,19 +3984,19 @@ function parseXml(source, file) {
|
|
|
3440
3984
|
const attrStart = j;
|
|
3441
3985
|
while (j < source.length && !ATTR_NAME_END.test(source[j])) j += 1;
|
|
3442
3986
|
const attr = source.slice(attrStart, j);
|
|
3443
|
-
if (attr === "") throw new XmlParseError(file, startLine, `<${name}> has an attribute with no name`);
|
|
3987
|
+
if (attr === "") throw new XmlParseError(file, startLine, `<${printable(name)}> has an attribute with no name`);
|
|
3444
3988
|
while (j < source.length && SPACE.test(source[j])) j += 1;
|
|
3445
3989
|
if (source[j] !== "=") {
|
|
3446
|
-
throw new XmlParseError(file, startLine, `<${name}> attribute "${attr}" has no value`);
|
|
3990
|
+
throw new XmlParseError(file, startLine, `<${printable(name)}> attribute "${printable(attr)}" has no value`);
|
|
3447
3991
|
}
|
|
3448
3992
|
j += 1;
|
|
3449
3993
|
while (j < source.length && SPACE.test(source[j])) j += 1;
|
|
3450
3994
|
const quote = source[j];
|
|
3451
3995
|
if (quote !== '"' && quote !== "'") {
|
|
3452
|
-
throw new XmlParseError(file, startLine, `<${name}> attribute "${attr}" is not quoted`);
|
|
3996
|
+
throw new XmlParseError(file, startLine, `<${printable(name)}> attribute "${printable(attr)}" is not quoted`);
|
|
3453
3997
|
}
|
|
3454
3998
|
const close = source.indexOf(quote, j + 1);
|
|
3455
|
-
if (close === -1) throw new XmlParseError(file, startLine, `<${name}> attribute "${attr}" is never closed`);
|
|
3999
|
+
if (close === -1) throw new XmlParseError(file, startLine, `<${printable(name)}> attribute "${printable(attr)}" is never closed`);
|
|
3456
4000
|
el.attrs[attr] = decode(source.slice(j + 1, close));
|
|
3457
4001
|
j = close + 1;
|
|
3458
4002
|
}
|
|
@@ -3462,7 +4006,7 @@ function parseXml(source, file) {
|
|
|
3462
4006
|
}
|
|
3463
4007
|
if (stack.length > 1) {
|
|
3464
4008
|
const open = top();
|
|
3465
|
-
throw new XmlParseError(file, open.line, `<${open.name}> is never closed`);
|
|
4009
|
+
throw new XmlParseError(file, open.line, `<${printable(open.name)}> is never closed`);
|
|
3466
4010
|
}
|
|
3467
4011
|
if (root.children.length === 0) throw new XmlParseError(file, 1, "no elements \u2014 is this an XML report?");
|
|
3468
4012
|
return root;
|
|
@@ -3472,6 +4016,7 @@ var init_xml = __esm({
|
|
|
3472
4016
|
"src/importers/xml.ts"() {
|
|
3473
4017
|
"use strict";
|
|
3474
4018
|
init_cjs_shims();
|
|
4019
|
+
init_failure_detail();
|
|
3475
4020
|
XmlParseError = class extends Error {
|
|
3476
4021
|
constructor(file, line, detail) {
|
|
3477
4022
|
super(`${file}:${line} \u2014 ${detail}`);
|
|
@@ -3518,7 +4063,7 @@ function playwrightOf(classname, name) {
|
|
|
3518
4063
|
if (!SPEC_FILE.test(classname)) return void 0;
|
|
3519
4064
|
return { file: classname.replaceAll("\\", "/"), titles: name.split(" \u203A ") };
|
|
3520
4065
|
}
|
|
3521
|
-
function
|
|
4066
|
+
function keyOf2(classname, name, playwright) {
|
|
3522
4067
|
if (playwright !== void 0) return { kind: "path-title", value: [playwright.file, ...playwright.titles].join("#") };
|
|
3523
4068
|
return { kind: "path-title", value: classname === "" ? name : `${classname}#${name}` };
|
|
3524
4069
|
}
|
|
@@ -3551,8 +4096,9 @@ function executionOf(test, suite) {
|
|
|
3551
4096
|
retry: 0
|
|
3552
4097
|
};
|
|
3553
4098
|
}
|
|
3554
|
-
function readJUnit(source, file) {
|
|
4099
|
+
function readJUnit(source, file, missing = [], notes = []) {
|
|
3555
4100
|
const doc = parseXml(source, file);
|
|
4101
|
+
const { lexical, real: folder } = reportFolders(file);
|
|
3556
4102
|
const found = [];
|
|
3557
4103
|
locate(doc, void 0, found);
|
|
3558
4104
|
return found.map(({ test, suite }) => {
|
|
@@ -3560,36 +4106,41 @@ function readJUnit(source, file) {
|
|
|
3560
4106
|
const classname = test.attrs["classname"] ?? test.attrs["class"] ?? "";
|
|
3561
4107
|
const { status, detail } = outcomeOf(test);
|
|
3562
4108
|
const playwright = playwrightOf(classname, name);
|
|
3563
|
-
const key =
|
|
3564
|
-
const stated =
|
|
4109
|
+
const key = keyOf2(classname, name, playwright);
|
|
4110
|
+
const stated = TOKEN2.exec(playwright?.titles.at(-1) ?? name)?.[1];
|
|
3565
4111
|
const specRef = specRefOf(test, suite, classname);
|
|
3566
4112
|
const errorContext = errorTextOf(detail);
|
|
3567
4113
|
const execution = executionOf(test, suite);
|
|
4114
|
+
const title = titleOf(classname, name);
|
|
4115
|
+
const rkey = resultKey(key.value, 0);
|
|
4116
|
+
const files = filesOfTest(test, folder, title, missing, { lexical, resultKey: rkey, notes });
|
|
3568
4117
|
return {
|
|
3569
|
-
resultKey:
|
|
4118
|
+
resultKey: rkey,
|
|
3570
4119
|
...stated !== void 0 ? { testCaseId: stated } : {},
|
|
3571
4120
|
keys: [key],
|
|
3572
|
-
source:
|
|
4121
|
+
source: SOURCE2,
|
|
3573
4122
|
rawStatus: status,
|
|
3574
|
-
title
|
|
4123
|
+
title,
|
|
3575
4124
|
...specRef !== void 0 ? { specRef } : {},
|
|
3576
4125
|
...execution !== void 0 ? { execution } : {},
|
|
3577
|
-
...errorContext !== "" ? { errorContext } : {}
|
|
4126
|
+
...errorContext !== "" ? { errorContext } : {},
|
|
4127
|
+
...files.length > 0 ? { files } : {}
|
|
3578
4128
|
};
|
|
3579
4129
|
});
|
|
3580
4130
|
}
|
|
3581
4131
|
function looksLikeJUnit(source) {
|
|
3582
4132
|
return /<\s*testsuites?\b/.test(source.slice(0, 4096));
|
|
3583
4133
|
}
|
|
3584
|
-
var
|
|
4134
|
+
var SOURCE2, TOKEN2, SPEC_FILE;
|
|
3585
4135
|
var init_junit = __esm({
|
|
3586
4136
|
"src/importers/junit.ts"() {
|
|
3587
4137
|
"use strict";
|
|
3588
4138
|
init_cjs_shims();
|
|
3589
4139
|
init_result_key();
|
|
4140
|
+
init_attachments();
|
|
3590
4141
|
init_xml();
|
|
3591
|
-
|
|
3592
|
-
|
|
4142
|
+
SOURCE2 = "junit";
|
|
4143
|
+
TOKEN2 = /@P([A-Za-z0-9_-]+)/;
|
|
3593
4144
|
SPEC_FILE = /\.(?:spec|test)\.[cm]?[jt]sx?$/;
|
|
3594
4145
|
}
|
|
3595
4146
|
});
|
|
@@ -3598,7 +4149,7 @@ var init_junit = __esm({
|
|
|
3598
4149
|
function statedIdOf(test, title) {
|
|
3599
4150
|
const annotation = test.annotations?.find((a) => a.type === "PluneId")?.description;
|
|
3600
4151
|
if (annotation !== void 0 && annotation !== "") return annotation;
|
|
3601
|
-
return
|
|
4152
|
+
return TOKEN3.exec(title)?.[1];
|
|
3602
4153
|
}
|
|
3603
4154
|
function attemptOf(result, file, ctx) {
|
|
3604
4155
|
const steps = (list2) => (list2 ?? []).map((s) => ({ title: s.title ?? "", failed: s.error !== void 0, steps: steps(s.steps) }));
|
|
@@ -3609,10 +4160,10 @@ function attemptOf(result, file, ctx) {
|
|
|
3609
4160
|
...typeof at?.file === "string" && typeof at.line === "number" ? { location: { file: at.file, line: at.line, ...typeof at.column === "number" ? { column: at.column } : {} } } : {}
|
|
3610
4161
|
})),
|
|
3611
4162
|
steps: steps(result.steps),
|
|
3612
|
-
attachments: (result.attachments ?? []).map(({ name, contentType, path:
|
|
4163
|
+
attachments: (result.attachments ?? []).map(({ name, contentType, path: path17, body }) => ({
|
|
3613
4164
|
name: name ?? "",
|
|
3614
4165
|
...contentType !== void 0 ? { contentType } : {},
|
|
3615
|
-
...
|
|
4166
|
+
...path17 !== void 0 ? { path: path17 } : {},
|
|
3616
4167
|
// Decoded only where the platform would take it (plune-ai/plune#928): a report of screenshots kept as
|
|
3617
4168
|
// bodies is megabytes of base64 that nothing here will read.
|
|
3618
4169
|
...typeof body === "string" && isTextType(contentType) ? { body: Buffer.from(body, "base64") } : {}
|
|
@@ -3645,7 +4196,7 @@ function pendingFrom(spec, test, result, file, titles, ctx) {
|
|
|
3645
4196
|
keys,
|
|
3646
4197
|
title: titles.join(" \u203A "),
|
|
3647
4198
|
specRef: spec.line !== void 0 ? `${file}:${spec.line}` : file,
|
|
3648
|
-
source:
|
|
4199
|
+
source: SOURCE3,
|
|
3649
4200
|
// Untouched, as everywhere: `timedOut` means what the project's map says it means, not what
|
|
3650
4201
|
// this file decided.
|
|
3651
4202
|
rawStatus: result.status ?? "unknown",
|
|
@@ -3680,7 +4231,7 @@ function readPlaywrightJson(source, file) {
|
|
|
3680
4231
|
try {
|
|
3681
4232
|
parsed = JSON.parse(source);
|
|
3682
4233
|
} catch (err) {
|
|
3683
|
-
throw new JsonReportError(file, `not JSON \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
4234
|
+
throw new JsonReportError(file, `not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
3684
4235
|
}
|
|
3685
4236
|
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.suites)) {
|
|
3686
4237
|
throw new JsonReportError(file, 'no "suites" array \u2014 is this a Playwright JSON report?');
|
|
@@ -3688,7 +4239,7 @@ function readPlaywrightJson(source, file) {
|
|
|
3688
4239
|
const { config, suites } = parsed;
|
|
3689
4240
|
const rootDir = typeof config?.rootDir === "string" ? config.rootDir : void 0;
|
|
3690
4241
|
const buildHref = config?.metadata?.ci?.buildHref;
|
|
3691
|
-
const repoRoot = rootDir !== void 0 && (0,
|
|
4242
|
+
const repoRoot = rootDir !== void 0 && (0, import_node_fs5.existsSync)(rootDir) ? repoRootOf(rootDir) : void 0;
|
|
3692
4243
|
const ctx = {
|
|
3693
4244
|
...rootDir !== void 0 ? { rootDir } : {},
|
|
3694
4245
|
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
@@ -3707,20 +4258,20 @@ function readPlaywrightJson(source, file) {
|
|
|
3707
4258
|
function looksLikePlaywrightJson(source) {
|
|
3708
4259
|
return /^\s*\{/.test(source) && source.includes('"suites"');
|
|
3709
4260
|
}
|
|
3710
|
-
var
|
|
4261
|
+
var import_node_fs5, SOURCE3, TOKEN3, NOT_FAILED, JsonReportError;
|
|
3711
4262
|
var init_playwright_json = __esm({
|
|
3712
4263
|
"src/importers/playwright-json.ts"() {
|
|
3713
4264
|
"use strict";
|
|
3714
4265
|
init_cjs_shims();
|
|
3715
|
-
|
|
4266
|
+
import_node_fs5 = require("fs");
|
|
3716
4267
|
init_failure_detail();
|
|
3717
4268
|
init_result_key();
|
|
3718
|
-
|
|
3719
|
-
|
|
4269
|
+
SOURCE3 = "playwright";
|
|
4270
|
+
TOKEN3 = /@P([A-Za-z0-9_-]+)/;
|
|
3720
4271
|
NOT_FAILED = /* @__PURE__ */ new Set(["passed", "skipped"]);
|
|
3721
4272
|
JsonReportError = class extends Error {
|
|
3722
4273
|
constructor(file, detail) {
|
|
3723
|
-
super(`${file} \u2014 ${detail}`);
|
|
4274
|
+
super(`${printable(file)} \u2014 ${detail}`);
|
|
3724
4275
|
this.file = file;
|
|
3725
4276
|
this.name = "JsonReportError";
|
|
3726
4277
|
}
|
|
@@ -3732,11 +4283,14 @@ var init_playwright_json = __esm({
|
|
|
3732
4283
|
// src/importers/index.ts
|
|
3733
4284
|
var importers_exports = {};
|
|
3734
4285
|
__export(importers_exports, {
|
|
4286
|
+
AllureReportError: () => AllureReportError,
|
|
3735
4287
|
IMPORT_FORMATS: () => IMPORT_FORMATS,
|
|
3736
4288
|
JsonReportError: () => JsonReportError,
|
|
3737
4289
|
UnknownFormatError: () => UnknownFormatError,
|
|
3738
4290
|
XmlParseError: () => XmlParseError,
|
|
3739
4291
|
detectFormat: () => detectFormat,
|
|
4292
|
+
isFolder: () => isFolder,
|
|
4293
|
+
readFolder: () => readFolder,
|
|
3740
4294
|
readReport: () => readReport
|
|
3741
4295
|
});
|
|
3742
4296
|
function detectFormat(source, file) {
|
|
@@ -3747,21 +4301,34 @@ function detectFormat(source, file) {
|
|
|
3747
4301
|
'no <testsuite> and no Playwright "suites" \u2014 this is not a report we can read.'
|
|
3748
4302
|
);
|
|
3749
4303
|
}
|
|
4304
|
+
function readFolder(dir, format) {
|
|
4305
|
+
if (format !== void 0 && format !== "allure") {
|
|
4306
|
+
throw new UnknownFormatError(dir, `this is a folder, and only allure-results is read from one, not ${format}.`);
|
|
4307
|
+
}
|
|
4308
|
+
const missing = [];
|
|
4309
|
+
const notes = [];
|
|
4310
|
+
return { format: "allure", results: readAllure(dir, missing, notes), missing, notes };
|
|
4311
|
+
}
|
|
3750
4312
|
function readReport(source, file, format) {
|
|
3751
4313
|
const chosen2 = format ?? detectFormat(source, file);
|
|
3752
|
-
|
|
3753
|
-
|
|
4314
|
+
const missing = [];
|
|
4315
|
+
if (chosen2 === "allure") throw new UnknownFormatError(file, "allure-results is a folder, and this is a file.");
|
|
4316
|
+
const notes = [];
|
|
4317
|
+
if (chosen2 === "junit") return { format: chosen2, results: readJUnit(source, file, missing, notes), missing, notes };
|
|
4318
|
+
return { format: chosen2, ...readPlaywrightJson(source, file), missing, notes };
|
|
3754
4319
|
}
|
|
3755
4320
|
var IMPORT_FORMATS, UnknownFormatError;
|
|
3756
4321
|
var init_importers = __esm({
|
|
3757
4322
|
"src/importers/index.ts"() {
|
|
3758
4323
|
"use strict";
|
|
3759
4324
|
init_cjs_shims();
|
|
4325
|
+
init_allure();
|
|
3760
4326
|
init_junit();
|
|
3761
4327
|
init_playwright_json();
|
|
3762
4328
|
init_xml();
|
|
3763
4329
|
init_playwright_json();
|
|
3764
|
-
|
|
4330
|
+
init_allure();
|
|
4331
|
+
IMPORT_FORMATS = ["junit", "playwright-json", "allure"];
|
|
3765
4332
|
UnknownFormatError = class extends Error {
|
|
3766
4333
|
constructor(file, detail) {
|
|
3767
4334
|
super(`${file} \u2014 ${detail} Say which with --format ${IMPORT_FORMATS.join(" | ")}.`);
|
|
@@ -3779,16 +4346,25 @@ __export(run_import_exports, {
|
|
|
3779
4346
|
async function handleRunImport(options) {
|
|
3780
4347
|
const write = options.write ?? ((line) => void process.stdout.write(`${line}
|
|
3781
4348
|
`));
|
|
3782
|
-
let
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
}
|
|
3786
|
-
|
|
4349
|
+
let read;
|
|
4350
|
+
if (isFolder(options.file)) {
|
|
4351
|
+
read = readFolder(options.file, options.format);
|
|
4352
|
+
} else {
|
|
4353
|
+
let source;
|
|
4354
|
+
try {
|
|
4355
|
+
source = fs10.readFileSync(options.file, "utf8");
|
|
4356
|
+
} catch {
|
|
4357
|
+
throw new RunCommandError(`Cannot read ${options.file}.`);
|
|
4358
|
+
}
|
|
4359
|
+
read = readReport(source, options.file, options.format);
|
|
3787
4360
|
}
|
|
3788
|
-
const { format, results, ciUrl, ci } =
|
|
4361
|
+
const { format, results, missing, notes, ciUrl, ci } = read;
|
|
3789
4362
|
if (results.length === 0) {
|
|
3790
4363
|
throw new RunCommandError(`No test cases in ${options.file} \u2014 it parsed, but there is nothing in it.`);
|
|
3791
4364
|
}
|
|
4365
|
+
for (const m of missing) write(`plune: attachment not found \u2014 ${JSON.stringify(printable(m.title))}: ${printable(m.path)}`);
|
|
4366
|
+
for (const note of notes) write(note);
|
|
4367
|
+
const announced = (format === "junit" || format === "allure") && (missing.length > 0 || results.some((r) => (r.files?.length ?? 0) > 0));
|
|
3792
4368
|
const env = readEnv();
|
|
3793
4369
|
const apiUrl = resolveApiUrl(options.apiUrl ?? env.config.apiUrl);
|
|
3794
4370
|
const token = options.token ?? env.config.token ?? loadToken() ?? "";
|
|
@@ -3833,6 +4409,13 @@ async function handleRunImport(options) {
|
|
|
3833
4409
|
deferred: stats.deferred,
|
|
3834
4410
|
screenshots: { ...stats.screenshots },
|
|
3835
4411
|
textFiles: { ...stats.textFiles },
|
|
4412
|
+
missingAttachments: missing,
|
|
4413
|
+
filesUploaded: stats.screenshots.uploaded + stats.textFiles.uploaded + stats.otherFiles.uploaded,
|
|
4414
|
+
announced,
|
|
4415
|
+
filesNotSent: Math.max(
|
|
4416
|
+
0,
|
|
4417
|
+
results.reduce((n, r) => n + (r.files?.length ?? 0), 0) - [stats.screenshots, stats.textFiles, stats.otherFiles].reduce((n, c) => n + c.uploaded + c.skipped + c.failed, 0)
|
|
4418
|
+
),
|
|
3836
4419
|
unlocatable: results.filter((r) => r.specRef === void 0).length,
|
|
3837
4420
|
offering: options.create === true
|
|
3838
4421
|
};
|
|
@@ -3851,21 +4434,11 @@ function describe(result, apiUrl) {
|
|
|
3851
4434
|
// worked (#622).
|
|
3852
4435
|
// `not delivered` always, zero included (#790): a number that only appears when it is bad is one
|
|
3853
4436
|
// nobody learns to look for. The `[0-9]+ unmatched` the workflows read stays whole.
|
|
3854
|
-
`Read ${result.parsed} result(s) from a ${result.format} report: ${result.accepted} accepted, ${result.duplicate} already there, ${result.unresolved} unmatched, ${result.deferred} not delivered.`
|
|
4437
|
+
`Read ${result.parsed} result(s) from ${result.format === "allure" ? "an" : "a"} ${result.format} report: ${result.accepted} accepted, ${result.duplicate} already there, ${result.unresolved} unmatched, ${result.deferred} not delivered.`
|
|
3855
4438
|
];
|
|
3856
4439
|
if (result.conflict > 0 || result.rejected > 0) {
|
|
3857
4440
|
lines.push(`${result.conflict} conflicted and ${result.rejected} were refused.`);
|
|
3858
4441
|
}
|
|
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
|
-
}
|
|
3868
|
-
}
|
|
3869
4442
|
if (result.deferred > 0) {
|
|
3870
4443
|
lines.push(
|
|
3871
4444
|
`${result.deferred} could not be sent and are in the fallback file \u2014 "plune run report" retries them.`
|
|
@@ -3893,16 +4466,22 @@ function describe(result, apiUrl) {
|
|
|
3893
4466
|
`${result.unlocatable} test(s) carry no file or class in the report, so they cannot be offered for review \u2014 a reviewer would have nowhere to look.`
|
|
3894
4467
|
);
|
|
3895
4468
|
}
|
|
4469
|
+
if (result.announced) {
|
|
4470
|
+
lines.push(
|
|
4471
|
+
`Imported ${result.accepted} results \xB7 ${result.filesUploaded} files uploaded \xB7 ${result.missingAttachments.length} files not found` + (result.filesNotSent > 0 ? ` \xB7 ${result.filesNotSent} files not sent (their result was not stored)` : "")
|
|
4472
|
+
);
|
|
4473
|
+
}
|
|
3896
4474
|
if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
|
|
3897
4475
|
return lines;
|
|
3898
4476
|
}
|
|
3899
|
-
var
|
|
4477
|
+
var fs10, NEXT;
|
|
3900
4478
|
var init_run_import = __esm({
|
|
3901
4479
|
"src/cli/commands/run-import.ts"() {
|
|
3902
4480
|
"use strict";
|
|
3903
4481
|
init_cjs_shims();
|
|
3904
|
-
|
|
4482
|
+
fs10 = __toESM(require("fs"), 1);
|
|
3905
4483
|
init_session();
|
|
4484
|
+
init_failure_detail();
|
|
3906
4485
|
init_importers();
|
|
3907
4486
|
init_run_lifecycle();
|
|
3908
4487
|
init_api_url();
|
|
@@ -3929,10 +4508,10 @@ function isRunResult(v) {
|
|
|
3929
4508
|
}
|
|
3930
4509
|
function handleReport(opts = {}) {
|
|
3931
4510
|
const cwd = opts.cwd ?? process.cwd();
|
|
3932
|
-
const file =
|
|
4511
|
+
const file = path11.join(cwd, ".plune", "last-run.json");
|
|
3933
4512
|
let raw;
|
|
3934
4513
|
try {
|
|
3935
|
-
raw =
|
|
4514
|
+
raw = fs11.readFileSync(file, "utf8");
|
|
3936
4515
|
} catch {
|
|
3937
4516
|
throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
|
|
3938
4517
|
}
|
|
@@ -3947,13 +4526,13 @@ function handleReport(opts = {}) {
|
|
|
3947
4526
|
}
|
|
3948
4527
|
return parsed;
|
|
3949
4528
|
}
|
|
3950
|
-
var
|
|
4529
|
+
var fs11, path11, ReportNotFoundError;
|
|
3951
4530
|
var init_report = __esm({
|
|
3952
4531
|
"src/cli/commands/report.ts"() {
|
|
3953
4532
|
"use strict";
|
|
3954
4533
|
init_cjs_shims();
|
|
3955
|
-
|
|
3956
|
-
|
|
4534
|
+
fs11 = __toESM(require("fs"), 1);
|
|
4535
|
+
path11 = __toESM(require("path"), 1);
|
|
3957
4536
|
ReportNotFoundError = class extends Error {
|
|
3958
4537
|
code = "NO_SAVED_RUN";
|
|
3959
4538
|
constructor(message) {
|
|
@@ -4027,7 +4606,7 @@ function isRunResult2(v) {
|
|
|
4027
4606
|
function loadRunResult(file, label) {
|
|
4028
4607
|
let raw;
|
|
4029
4608
|
try {
|
|
4030
|
-
raw =
|
|
4609
|
+
raw = fs12.readFileSync(file, "utf8");
|
|
4031
4610
|
} catch {
|
|
4032
4611
|
throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
|
|
4033
4612
|
}
|
|
@@ -4047,12 +4626,12 @@ function handleDiff(opts) {
|
|
|
4047
4626
|
const current = loadRunResult(opts.currentPath, "current");
|
|
4048
4627
|
return diffRuns(baseline, current);
|
|
4049
4628
|
}
|
|
4050
|
-
var
|
|
4629
|
+
var fs12, DiffInputError;
|
|
4051
4630
|
var init_diff2 = __esm({
|
|
4052
4631
|
"src/cli/commands/diff.ts"() {
|
|
4053
4632
|
"use strict";
|
|
4054
4633
|
init_cjs_shims();
|
|
4055
|
-
|
|
4634
|
+
fs12 = __toESM(require("fs"), 1);
|
|
4056
4635
|
init_diff();
|
|
4057
4636
|
DiffInputError = class extends Error {
|
|
4058
4637
|
code = "DIFF_INPUT";
|
|
@@ -4177,8 +4756,8 @@ async function runInitWizard(cwd) {
|
|
|
4177
4756
|
process.exit(1);
|
|
4178
4757
|
return;
|
|
4179
4758
|
}
|
|
4180
|
-
const outputPath =
|
|
4181
|
-
if (
|
|
4759
|
+
const outputPath = path12.join(cwd, "plune.yaml");
|
|
4760
|
+
if (fs13.existsSync(outputPath)) {
|
|
4182
4761
|
const overwrite = await clack.confirm({
|
|
4183
4762
|
message: "plune.yaml already exists. Overwrite?"
|
|
4184
4763
|
});
|
|
@@ -4217,18 +4796,18 @@ async function runInitWizard(cwd) {
|
|
|
4217
4796
|
`;
|
|
4218
4797
|
const spin = clack.spinner();
|
|
4219
4798
|
spin.start("Writing plune.yaml\u2026");
|
|
4220
|
-
|
|
4799
|
+
fs13.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
|
|
4221
4800
|
spin.stop("plune.yaml created!");
|
|
4222
4801
|
clack.outro("plune.yaml is ready.");
|
|
4223
4802
|
}
|
|
4224
|
-
var clack,
|
|
4803
|
+
var clack, fs13, path12, import_yaml2;
|
|
4225
4804
|
var init_wizard = __esm({
|
|
4226
4805
|
"src/config/init/wizard.ts"() {
|
|
4227
4806
|
"use strict";
|
|
4228
4807
|
init_cjs_shims();
|
|
4229
4808
|
clack = __toESM(require("@clack/prompts"), 1);
|
|
4230
|
-
|
|
4231
|
-
|
|
4809
|
+
fs13 = __toESM(require("fs"), 1);
|
|
4810
|
+
path12 = __toESM(require("path"), 1);
|
|
4232
4811
|
import_yaml2 = require("yaml");
|
|
4233
4812
|
init_errors();
|
|
4234
4813
|
}
|
|
@@ -4236,21 +4815,21 @@ var init_wizard = __esm({
|
|
|
4236
4815
|
|
|
4237
4816
|
// src/cli/templates/index.ts
|
|
4238
4817
|
function writeTemplateFile(cwd, rel, content, force) {
|
|
4239
|
-
const target =
|
|
4240
|
-
if (!force &&
|
|
4818
|
+
const target = path13.join(cwd, rel);
|
|
4819
|
+
if (!force && fs14.existsSync(target)) {
|
|
4241
4820
|
return "skipped";
|
|
4242
4821
|
}
|
|
4243
|
-
|
|
4244
|
-
|
|
4822
|
+
fs14.mkdirSync(path13.dirname(target), { recursive: true });
|
|
4823
|
+
fs14.writeFileSync(target, content, "utf8");
|
|
4245
4824
|
return "written";
|
|
4246
4825
|
}
|
|
4247
|
-
var
|
|
4826
|
+
var fs14, path13, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
|
|
4248
4827
|
var init_templates = __esm({
|
|
4249
4828
|
"src/cli/templates/index.ts"() {
|
|
4250
4829
|
"use strict";
|
|
4251
4830
|
init_cjs_shims();
|
|
4252
|
-
|
|
4253
|
-
|
|
4831
|
+
fs14 = __toESM(require("fs"), 1);
|
|
4832
|
+
path13 = __toESM(require("path"), 1);
|
|
4254
4833
|
ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
|
|
4255
4834
|
# .env is gitignored; .env.example is safe to commit.
|
|
4256
4835
|
# Plune reads whichever key matches your configured provider.
|
|
@@ -4428,68 +5007,136 @@ var init_logout = __esm({
|
|
|
4428
5007
|
}
|
|
4429
5008
|
});
|
|
4430
5009
|
|
|
5010
|
+
// src/types/test-case-schema.ts
|
|
5011
|
+
var import_zod2, TEST_CASE_SCHEMA_VERSION, externalKeySchema, manualStepSchema, assertionResultRecordSchema, manualCaseSchema, automatedCaseSchema, evalCaseSchema, testCaseBaseSchema, testCaseSchema;
|
|
5012
|
+
var init_test_case_schema = __esm({
|
|
5013
|
+
"src/types/test-case-schema.ts"() {
|
|
5014
|
+
"use strict";
|
|
5015
|
+
init_cjs_shims();
|
|
5016
|
+
import_zod2 = require("zod");
|
|
5017
|
+
TEST_CASE_SCHEMA_VERSION = 2;
|
|
5018
|
+
externalKeySchema = import_zod2.z.object({
|
|
5019
|
+
kind: import_zod2.z.enum(["playwright-id", "path-title", "allure-history", "cairn-stable", "eval-id", "qase", "testrail"]),
|
|
5020
|
+
value: import_zod2.z.string().min(1)
|
|
5021
|
+
});
|
|
5022
|
+
manualStepSchema = import_zod2.z.object({
|
|
5023
|
+
action: import_zod2.z.string().min(1),
|
|
5024
|
+
expected: import_zod2.z.string().min(1).optional()
|
|
5025
|
+
});
|
|
5026
|
+
assertionResultRecordSchema = import_zod2.z.object({
|
|
5027
|
+
type: import_zod2.z.string().min(1),
|
|
5028
|
+
passed: import_zod2.z.boolean(),
|
|
5029
|
+
score: import_zod2.z.number().optional(),
|
|
5030
|
+
reason: import_zod2.z.string().optional()
|
|
5031
|
+
});
|
|
5032
|
+
manualCaseSchema = import_zod2.z.object({
|
|
5033
|
+
type: import_zod2.z.literal("manual"),
|
|
5034
|
+
steps: import_zod2.z.array(manualStepSchema).min(1),
|
|
5035
|
+
expected: import_zod2.z.string().min(1).optional()
|
|
5036
|
+
});
|
|
5037
|
+
automatedCaseSchema = import_zod2.z.object({
|
|
5038
|
+
type: import_zod2.z.literal("automated"),
|
|
5039
|
+
specRef: import_zod2.z.string().min(1)
|
|
5040
|
+
});
|
|
5041
|
+
evalCaseSchema = import_zod2.z.object({
|
|
5042
|
+
type: import_zod2.z.literal("eval"),
|
|
5043
|
+
prompt: import_zod2.z.string().min(1),
|
|
5044
|
+
goldenSetId: import_zod2.z.string().min(1).optional(),
|
|
5045
|
+
assertions: import_zod2.z.array(assertionResultRecordSchema).min(1)
|
|
5046
|
+
});
|
|
5047
|
+
testCaseBaseSchema = import_zod2.z.object({
|
|
5048
|
+
schemaVersion: import_zod2.z.literal(TEST_CASE_SCHEMA_VERSION),
|
|
5049
|
+
id: import_zod2.z.string().min(1),
|
|
5050
|
+
externalKeys: import_zod2.z.array(externalKeySchema).optional(),
|
|
5051
|
+
/** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
|
|
5052
|
+
is_flaky: import_zod2.z.boolean().optional(),
|
|
5053
|
+
/** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
|
|
5054
|
+
is_muted: import_zod2.z.boolean().optional()
|
|
5055
|
+
});
|
|
5056
|
+
testCaseSchema = import_zod2.z.discriminatedUnion("type", [
|
|
5057
|
+
testCaseBaseSchema.merge(manualCaseSchema),
|
|
5058
|
+
testCaseBaseSchema.merge(automatedCaseSchema),
|
|
5059
|
+
testCaseBaseSchema.merge(evalCaseSchema)
|
|
5060
|
+
]);
|
|
5061
|
+
}
|
|
5062
|
+
});
|
|
5063
|
+
|
|
5064
|
+
// src/types/results.ts
|
|
5065
|
+
var ROW_INCONCLUSIVE;
|
|
5066
|
+
var init_results = __esm({
|
|
5067
|
+
"src/types/results.ts"() {
|
|
5068
|
+
"use strict";
|
|
5069
|
+
init_cjs_shims();
|
|
5070
|
+
init_test_case_schema();
|
|
5071
|
+
ROW_INCONCLUSIVE = ["blocked", "skipped"];
|
|
5072
|
+
}
|
|
5073
|
+
});
|
|
5074
|
+
|
|
4431
5075
|
// src/types/run-result-schema.ts
|
|
4432
5076
|
function parseRunResult(value2) {
|
|
4433
5077
|
return runResultSchema.parse(value2);
|
|
4434
5078
|
}
|
|
4435
|
-
var
|
|
5079
|
+
var import_zod3, RUN_RESULT_SCHEMA_VERSION, binaryVerdictSchema, assertionResultRecordSchema2, usageSchema, runErrorSchema, rowResultSchema, summarySchema, evalResultSchema, runResultSchema;
|
|
4436
5080
|
var init_run_result_schema = __esm({
|
|
4437
5081
|
"src/types/run-result-schema.ts"() {
|
|
4438
5082
|
"use strict";
|
|
4439
5083
|
init_cjs_shims();
|
|
4440
|
-
|
|
5084
|
+
import_zod3 = require("zod");
|
|
5085
|
+
init_results();
|
|
4441
5086
|
RUN_RESULT_SCHEMA_VERSION = 1;
|
|
4442
|
-
binaryVerdictSchema =
|
|
4443
|
-
question:
|
|
4444
|
-
passed:
|
|
5087
|
+
binaryVerdictSchema = import_zod3.z.object({
|
|
5088
|
+
question: import_zod3.z.string().min(1),
|
|
5089
|
+
passed: import_zod3.z.boolean()
|
|
4445
5090
|
});
|
|
4446
|
-
|
|
4447
|
-
type:
|
|
4448
|
-
passed:
|
|
4449
|
-
score:
|
|
4450
|
-
reason:
|
|
4451
|
-
binaryVerdicts:
|
|
5091
|
+
assertionResultRecordSchema2 = import_zod3.z.object({
|
|
5092
|
+
type: import_zod3.z.string(),
|
|
5093
|
+
passed: import_zod3.z.boolean(),
|
|
5094
|
+
score: import_zod3.z.number().optional(),
|
|
5095
|
+
reason: import_zod3.z.string().optional(),
|
|
5096
|
+
binaryVerdicts: import_zod3.z.array(binaryVerdictSchema).optional()
|
|
4452
5097
|
});
|
|
4453
|
-
usageSchema =
|
|
4454
|
-
input_tokens:
|
|
4455
|
-
output_tokens:
|
|
4456
|
-
cost_usd:
|
|
5098
|
+
usageSchema = import_zod3.z.object({
|
|
5099
|
+
input_tokens: import_zod3.z.number(),
|
|
5100
|
+
output_tokens: import_zod3.z.number(),
|
|
5101
|
+
cost_usd: import_zod3.z.number()
|
|
4457
5102
|
});
|
|
4458
|
-
runErrorSchema =
|
|
4459
|
-
code:
|
|
4460
|
-
message:
|
|
5103
|
+
runErrorSchema = import_zod3.z.object({
|
|
5104
|
+
code: import_zod3.z.string(),
|
|
5105
|
+
message: import_zod3.z.string()
|
|
4461
5106
|
});
|
|
4462
|
-
rowResultSchema =
|
|
4463
|
-
vars:
|
|
4464
|
-
output:
|
|
4465
|
-
cached:
|
|
5107
|
+
rowResultSchema = import_zod3.z.object({
|
|
5108
|
+
vars: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()),
|
|
5109
|
+
output: import_zod3.z.string().nullable(),
|
|
5110
|
+
cached: import_zod3.z.boolean(),
|
|
4466
5111
|
usage: usageSchema.optional(),
|
|
4467
|
-
latency_ms:
|
|
5112
|
+
latency_ms: import_zod3.z.number().optional(),
|
|
4468
5113
|
error: runErrorSchema.optional(),
|
|
4469
|
-
assertions:
|
|
5114
|
+
assertions: import_zod3.z.array(assertionResultRecordSchema2),
|
|
5115
|
+
/** ADR 0045. Optional and additive — the same enum the platform validates on `POST /v1/runs`. */
|
|
5116
|
+
inconclusive: import_zod3.z.enum(ROW_INCONCLUSIVE).optional()
|
|
4470
5117
|
});
|
|
4471
|
-
summarySchema =
|
|
4472
|
-
total:
|
|
4473
|
-
passed:
|
|
4474
|
-
failed:
|
|
4475
|
-
errored:
|
|
4476
|
-
cost_usd:
|
|
4477
|
-
duration_ms:
|
|
5118
|
+
summarySchema = import_zod3.z.object({
|
|
5119
|
+
total: import_zod3.z.number(),
|
|
5120
|
+
passed: import_zod3.z.number(),
|
|
5121
|
+
failed: import_zod3.z.number(),
|
|
5122
|
+
errored: import_zod3.z.number(),
|
|
5123
|
+
cost_usd: import_zod3.z.number(),
|
|
5124
|
+
duration_ms: import_zod3.z.number()
|
|
4478
5125
|
});
|
|
4479
|
-
evalResultSchema =
|
|
4480
|
-
id:
|
|
4481
|
-
tags:
|
|
4482
|
-
rows:
|
|
4483
|
-
passed:
|
|
5126
|
+
evalResultSchema = import_zod3.z.object({
|
|
5127
|
+
id: import_zod3.z.string(),
|
|
5128
|
+
tags: import_zod3.z.array(import_zod3.z.string()),
|
|
5129
|
+
rows: import_zod3.z.array(rowResultSchema),
|
|
5130
|
+
passed: import_zod3.z.boolean()
|
|
4484
5131
|
});
|
|
4485
|
-
runResultSchema =
|
|
4486
|
-
schemaVersion:
|
|
4487
|
-
plune_version:
|
|
4488
|
-
started_at:
|
|
4489
|
-
finished_at:
|
|
4490
|
-
config_hash:
|
|
5132
|
+
runResultSchema = import_zod3.z.object({
|
|
5133
|
+
schemaVersion: import_zod3.z.literal(RUN_RESULT_SCHEMA_VERSION),
|
|
5134
|
+
plune_version: import_zod3.z.string(),
|
|
5135
|
+
started_at: import_zod3.z.string(),
|
|
5136
|
+
finished_at: import_zod3.z.string(),
|
|
5137
|
+
config_hash: import_zod3.z.string(),
|
|
4491
5138
|
summary: summarySchema,
|
|
4492
|
-
evals:
|
|
5139
|
+
evals: import_zod3.z.array(evalResultSchema)
|
|
4493
5140
|
});
|
|
4494
5141
|
}
|
|
4495
5142
|
});
|
|
@@ -4506,10 +5153,10 @@ __export(sync_exports, {
|
|
|
4506
5153
|
reportSyncFailure: () => reportSyncFailure
|
|
4507
5154
|
});
|
|
4508
5155
|
function readRun(cwd, file) {
|
|
4509
|
-
const target = file !== void 0 ?
|
|
5156
|
+
const target = file !== void 0 ? path14.resolve(cwd, file) : path14.join(cwd, ".plune", "last-run.json");
|
|
4510
5157
|
let raw;
|
|
4511
5158
|
try {
|
|
4512
|
-
raw =
|
|
5159
|
+
raw = fs15.readFileSync(target, "utf8");
|
|
4513
5160
|
} catch {
|
|
4514
5161
|
throw new SyncFileError(
|
|
4515
5162
|
file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
|
|
@@ -4573,13 +5220,13 @@ function reportSyncFailure(err, write) {
|
|
|
4573
5220
|
}
|
|
4574
5221
|
return null;
|
|
4575
5222
|
}
|
|
4576
|
-
var
|
|
5223
|
+
var fs15, path14, NotLoggedInError, TokenRejectedError2, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
4577
5224
|
var init_sync = __esm({
|
|
4578
5225
|
"src/cli/commands/sync.ts"() {
|
|
4579
5226
|
"use strict";
|
|
4580
5227
|
init_cjs_shims();
|
|
4581
|
-
|
|
4582
|
-
|
|
5228
|
+
fs15 = __toESM(require("fs"), 1);
|
|
5229
|
+
path14 = __toESM(require("path"), 1);
|
|
4583
5230
|
init_run_result_schema();
|
|
4584
5231
|
init_api_url();
|
|
4585
5232
|
init_credentials();
|
|
@@ -4624,14 +5271,14 @@ var init_sync = __esm({
|
|
|
4624
5271
|
|
|
4625
5272
|
// src/cli/commands/cairn-artifact.ts
|
|
4626
5273
|
async function findLatestCairnRun(runsDir = "runs") {
|
|
4627
|
-
const entries = await
|
|
5274
|
+
const entries = await fs16.readdir(runsDir, { withFileTypes: true }).catch(() => null);
|
|
4628
5275
|
if (entries === null) throw new NoCairnRunsError(runsDir);
|
|
4629
5276
|
let newest;
|
|
4630
5277
|
for (const entry of entries) {
|
|
4631
5278
|
if (!entry.isDirectory()) continue;
|
|
4632
|
-
const dir =
|
|
5279
|
+
const dir = path15.join(runsDir, entry.name);
|
|
4633
5280
|
try {
|
|
4634
|
-
const report = await
|
|
5281
|
+
const report = await fs16.lstat(path15.join(dir, "report.json"));
|
|
4635
5282
|
if (!report.isFile()) continue;
|
|
4636
5283
|
if (newest === void 0 || report.mtimeMs > newest.at) {
|
|
4637
5284
|
newest = { dir, at: report.mtimeMs };
|
|
@@ -4644,13 +5291,13 @@ async function findLatestCairnRun(runsDir = "runs") {
|
|
|
4644
5291
|
}
|
|
4645
5292
|
async function readJson(file) {
|
|
4646
5293
|
try {
|
|
4647
|
-
return JSON.parse(await
|
|
5294
|
+
return JSON.parse(await fs16.readFile(file, "utf8"));
|
|
4648
5295
|
} catch {
|
|
4649
5296
|
return void 0;
|
|
4650
5297
|
}
|
|
4651
5298
|
}
|
|
4652
5299
|
async function defaultReadEvidence(dir) {
|
|
4653
|
-
const raw = await readJson(
|
|
5300
|
+
const raw = await readJson(path15.join(dir, "api-evidence.json"));
|
|
4654
5301
|
if (!Array.isArray(raw)) return void 0;
|
|
4655
5302
|
return raw.map((r) => {
|
|
4656
5303
|
const rec = asRecord(r);
|
|
@@ -4692,7 +5339,7 @@ function casesFromApi(report, verdicts) {
|
|
|
4692
5339
|
});
|
|
4693
5340
|
}
|
|
4694
5341
|
async function readCairnRun(dir, deps = {}) {
|
|
4695
|
-
const raw = await readJson(
|
|
5342
|
+
const raw = await readJson(path15.join(dir, "report.json"));
|
|
4696
5343
|
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
4697
5344
|
const report = asRecord(raw);
|
|
4698
5345
|
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
@@ -4722,20 +5369,20 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
4722
5369
|
return {
|
|
4723
5370
|
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
4724
5371
|
run: {
|
|
4725
|
-
runId: str(report["runId"],
|
|
5372
|
+
runId: str(report["runId"], path15.basename(dir)),
|
|
4726
5373
|
mode,
|
|
4727
5374
|
target: str(report["url"], "unknown")
|
|
4728
5375
|
},
|
|
4729
5376
|
cases
|
|
4730
5377
|
};
|
|
4731
5378
|
}
|
|
4732
|
-
var
|
|
5379
|
+
var fs16, path15, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
|
|
4733
5380
|
var init_cairn_artifact = __esm({
|
|
4734
5381
|
"src/cli/commands/cairn-artifact.ts"() {
|
|
4735
5382
|
"use strict";
|
|
4736
5383
|
init_cjs_shims();
|
|
4737
|
-
|
|
4738
|
-
|
|
5384
|
+
fs16 = __toESM(require("fs/promises"), 1);
|
|
5385
|
+
path15 = __toESM(require("path"), 1);
|
|
4739
5386
|
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
4740
5387
|
INGEST_SCHEMA_VERSION = 1;
|
|
4741
5388
|
NoCairnRunsError = class extends Error {
|
|
@@ -4902,7 +5549,7 @@ __export(cases_exports, {
|
|
|
4902
5549
|
function gitDirty(file) {
|
|
4903
5550
|
try {
|
|
4904
5551
|
const out = (0, import_node_child_process2.execFileSync)("git", ["status", "--porcelain", "--", file], {
|
|
4905
|
-
cwd:
|
|
5552
|
+
cwd: path16.dirname(file),
|
|
4906
5553
|
stdio: ["ignore", "pipe", "ignore"],
|
|
4907
5554
|
encoding: "utf8"
|
|
4908
5555
|
});
|
|
@@ -4928,9 +5575,9 @@ async function handlePull(deps = {}) {
|
|
|
4928
5575
|
const cwd = deps.cwd ?? process.cwd();
|
|
4929
5576
|
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4930
5577
|
const token = tokenOf(deps);
|
|
4931
|
-
const file =
|
|
4932
|
-
if (
|
|
4933
|
-
throw new DirtyFileError(
|
|
5578
|
+
const file = path16.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
5579
|
+
if (fs17.existsSync(file) && !deps.force && (deps.isDirty ?? gitDirty)(file))
|
|
5580
|
+
throw new DirtyFileError(path16.relative(cwd, file) || file);
|
|
4934
5581
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
4935
5582
|
const endpoint = deps.suiteId !== void 0 ? `${apiUrl}/v1/suites/${encodeURIComponent(deps.suiteId)}/markdown` : `${apiUrl}/v1/test-cases/markdown`;
|
|
4936
5583
|
let res;
|
|
@@ -4945,18 +5592,18 @@ async function handlePull(deps = {}) {
|
|
|
4945
5592
|
throw new SyncHttpError(res.status, detail);
|
|
4946
5593
|
}
|
|
4947
5594
|
const text2 = await res.text();
|
|
4948
|
-
|
|
4949
|
-
|
|
5595
|
+
fs17.mkdirSync(path16.dirname(file), { recursive: true });
|
|
5596
|
+
fs17.writeFileSync(file, text2, "utf8");
|
|
4950
5597
|
return { file, cases: Number(res.headers.get("x-plune-cases") ?? "0") };
|
|
4951
5598
|
}
|
|
4952
5599
|
async function handlePush(deps = {}) {
|
|
4953
5600
|
const cwd = deps.cwd ?? process.cwd();
|
|
4954
5601
|
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4955
5602
|
const token = tokenOf(deps);
|
|
4956
|
-
const file =
|
|
5603
|
+
const file = path16.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
4957
5604
|
let text2;
|
|
4958
5605
|
try {
|
|
4959
|
-
text2 =
|
|
5606
|
+
text2 = fs17.readFileSync(file, "utf8");
|
|
4960
5607
|
} catch {
|
|
4961
5608
|
throw new SyncFileError(
|
|
4962
5609
|
`No document to push: ${file}. Run "plune pull" first (or pass a file).`
|
|
@@ -5007,18 +5654,18 @@ function reportCasesFailure(err, write) {
|
|
|
5007
5654
|
}
|
|
5008
5655
|
return null;
|
|
5009
5656
|
}
|
|
5010
|
-
var import_node_child_process2,
|
|
5657
|
+
var import_node_child_process2, fs17, path16, DEFAULT_CASES_FILE, DirtyFileError;
|
|
5011
5658
|
var init_cases = __esm({
|
|
5012
5659
|
"src/cli/commands/cases.ts"() {
|
|
5013
5660
|
"use strict";
|
|
5014
5661
|
init_cjs_shims();
|
|
5015
5662
|
import_node_child_process2 = require("child_process");
|
|
5016
|
-
|
|
5017
|
-
|
|
5663
|
+
fs17 = __toESM(require("fs"), 1);
|
|
5664
|
+
path16 = __toESM(require("path"), 1);
|
|
5018
5665
|
init_api_url();
|
|
5019
5666
|
init_credentials();
|
|
5020
5667
|
init_sync();
|
|
5021
|
-
DEFAULT_CASES_FILE =
|
|
5668
|
+
DEFAULT_CASES_FILE = path16.join("plune", "cases.md");
|
|
5022
5669
|
DirtyFileError = class extends Error {
|
|
5023
5670
|
constructor(file) {
|
|
5024
5671
|
super(`${file} has uncommitted changes; commit them or pass --force to overwrite.`);
|
|
@@ -5099,11 +5746,11 @@ module.exports = __toCommonJS(cli_exports);
|
|
|
5099
5746
|
init_cjs_shims();
|
|
5100
5747
|
init_api_url();
|
|
5101
5748
|
var import_commander = require("commander");
|
|
5102
|
-
var
|
|
5749
|
+
var import_node_fs6 = require("fs");
|
|
5103
5750
|
var import_node_path4 = require("path");
|
|
5104
5751
|
var import_node_url = require("url");
|
|
5105
5752
|
var __dirname = (0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
5106
|
-
var pkg = JSON.parse((0,
|
|
5753
|
+
var pkg = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path4.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
5107
5754
|
function toFormat(s) {
|
|
5108
5755
|
return s === "json" || s === "markdown" ? s : "console";
|
|
5109
5756
|
}
|
|
@@ -5112,7 +5759,7 @@ function shouldColor(outputPath) {
|
|
|
5112
5759
|
}
|
|
5113
5760
|
function writeReport(text2, outputPath) {
|
|
5114
5761
|
if (outputPath !== void 0) {
|
|
5115
|
-
(0,
|
|
5762
|
+
(0, import_node_fs6.writeFileSync)(outputPath, text2);
|
|
5116
5763
|
} else {
|
|
5117
5764
|
process.stdout.write(text2 + "\n");
|
|
5118
5765
|
}
|
|
@@ -5303,11 +5950,15 @@ function createProgram() {
|
|
|
5303
5950
|
await failRunCommand(err, verboseOf(command));
|
|
5304
5951
|
}
|
|
5305
5952
|
});
|
|
5306
|
-
runCommand.command("import").argument("<file>", "A JUnit XML or Playwright JSON report").description("Turn a report another runner wrote into a run in Plune \u2014 no provider key needed").option("--format <fmt>", "junit | playwright-json; detected from the file when omitted").option("--key <externalKey>", "The key several jobs share so their reports land in one run").option("--create", "Offer tests nothing matched to the review queue", false).
|
|
5953
|
+
runCommand.command("import").argument("<file>", "A JUnit XML or Playwright JSON report, or an Allure allure-results folder").description("Turn a report another runner wrote into a run in Plune \u2014 no provider key needed").option("--format <fmt>", "junit | playwright-json | allure; detected from the file (a folder is allure) when omitted").option("--key <externalKey>", "The key several jobs share so their reports land in one run").option("--create", "Offer tests nothing matched to the review queue", false).option(
|
|
5954
|
+
"--fail-on-missing-attachments",
|
|
5955
|
+
"Exit 1 when a file the report names ([[ATTACHMENT|path]] in JUnit, a source in allure-results) is not there; the results are sent either way",
|
|
5956
|
+
false
|
|
5957
|
+
).action(
|
|
5307
5958
|
async (file, options, command) => {
|
|
5308
5959
|
const [
|
|
5309
5960
|
{ handleRunImport: handleRunImport2 },
|
|
5310
|
-
{ IMPORT_FORMATS: IMPORT_FORMATS2, UnknownFormatError: UnknownFormatError2, XmlParseError: XmlParseError2, JsonReportError: JsonReportError2 }
|
|
5961
|
+
{ IMPORT_FORMATS: IMPORT_FORMATS2, UnknownFormatError: UnknownFormatError2, XmlParseError: XmlParseError2, JsonReportError: JsonReportError2, AllureReportError: AllureReportError2 }
|
|
5311
5962
|
] = await Promise.all([
|
|
5312
5963
|
Promise.resolve().then(() => (init_run_import(), run_import_exports)),
|
|
5313
5964
|
Promise.resolve().then(() => (init_importers(), importers_exports))
|
|
@@ -5323,14 +5974,15 @@ function createProgram() {
|
|
|
5323
5974
|
return;
|
|
5324
5975
|
}
|
|
5325
5976
|
try {
|
|
5326
|
-
await handleRunImport2({
|
|
5977
|
+
const imported = await handleRunImport2({
|
|
5327
5978
|
file,
|
|
5328
5979
|
...format !== void 0 ? { format } : {},
|
|
5329
5980
|
...options.key !== void 0 ? { key: options.key } : {},
|
|
5330
5981
|
...options.create ? { create: true } : {}
|
|
5331
5982
|
});
|
|
5983
|
+
if (options.failOnMissingAttachments && imported.missingAttachments.length > 0) process.exitCode = 1;
|
|
5332
5984
|
} catch (err) {
|
|
5333
|
-
if (err instanceof UnknownFormatError2 || err instanceof XmlParseError2 || err instanceof JsonReportError2) {
|
|
5985
|
+
if (err instanceof UnknownFormatError2 || err instanceof XmlParseError2 || err instanceof JsonReportError2 || err instanceof AllureReportError2) {
|
|
5334
5986
|
process.stderr.write(err.message + "\n");
|
|
5335
5987
|
process.exit(2);
|
|
5336
5988
|
return;
|
|
@@ -5409,10 +6061,10 @@ function createProgram() {
|
|
|
5409
6061
|
const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
5410
6062
|
try {
|
|
5411
6063
|
const token = options.token ?? await readTokenFromStdin();
|
|
5412
|
-
const { path:
|
|
6064
|
+
const { path: path17, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
|
|
5413
6065
|
process.stdout.write(
|
|
5414
|
-
verified ? `Logged in. Token checked against the API and saved to ${
|
|
5415
|
-
` : `Token saved to ${
|
|
6066
|
+
verified ? `Logged in. Token checked against the API and saved to ${path17}
|
|
6067
|
+
` : `Token saved to ${path17} WITHOUT being checked (--skip-verify).
|
|
5416
6068
|
`
|
|
5417
6069
|
);
|
|
5418
6070
|
} catch (err) {
|
|
@@ -5429,9 +6081,9 @@ function createProgram() {
|
|
|
5429
6081
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
5430
6082
|
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
5431
6083
|
try {
|
|
5432
|
-
const { removed, path:
|
|
6084
|
+
const { removed, path: path17 } = handleLogout2();
|
|
5433
6085
|
process.stdout.write(
|
|
5434
|
-
removed ? `Logged out. Removed ${
|
|
6086
|
+
removed ? `Logged out. Removed ${path17}
|
|
5435
6087
|
` : "Not logged in \u2014 nothing to remove.\n"
|
|
5436
6088
|
);
|
|
5437
6089
|
} catch (err) {
|
|
@@ -5560,7 +6212,7 @@ function isMainModule() {
|
|
|
5560
6212
|
const entry = process.argv[1];
|
|
5561
6213
|
if (entry === void 0) return false;
|
|
5562
6214
|
try {
|
|
5563
|
-
return (0,
|
|
6215
|
+
return (0, import_node_fs6.realpathSync)(entry) === (0, import_node_fs6.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
5564
6216
|
} catch {
|
|
5565
6217
|
return false;
|
|
5566
6218
|
}
|