@plune-ai/cli 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -3
- package/dist/cli.cjs +467 -257
- package/dist/index.cjs +34 -3
- package/dist/index.js +34 -3
- package/dist/reporter-core.cjs +270 -169
- package/dist/reporter-core.d.cts +68 -7
- package/dist/reporter-core.d.ts +68 -7
- package/dist/reporter-core.js +267 -167
- package/package.json +1 -1
package/dist/reporter-core.cjs
CHANGED
|
@@ -34,6 +34,7 @@ __export(reporter_core_exports, {
|
|
|
34
34
|
RESULT_KEY_MAX: () => RESULT_KEY_MAX,
|
|
35
35
|
UNSUPPORTED_VARS: () => UNSUPPORTED_VARS,
|
|
36
36
|
appendBatch: () => appendBatch,
|
|
37
|
+
ciCommit: () => ciCommit,
|
|
37
38
|
createClient: () => createClient,
|
|
38
39
|
errorContextOf: () => errorContextOf,
|
|
39
40
|
failureOf: () => failureOf,
|
|
@@ -48,8 +49,9 @@ module.exports = __toCommonJS(reporter_core_exports);
|
|
|
48
49
|
|
|
49
50
|
// src/reporter-core/session.ts
|
|
50
51
|
var import_node_crypto = require("crypto");
|
|
51
|
-
var
|
|
52
|
-
var
|
|
52
|
+
var import_node_fs3 = require("fs");
|
|
53
|
+
var import_node_path3 = require("path");
|
|
54
|
+
var import_node_util2 = require("util");
|
|
53
55
|
|
|
54
56
|
// src/cli/api-url.ts
|
|
55
57
|
var DEFAULT_API_URL = "https://beta-api.plune.ai";
|
|
@@ -247,6 +249,159 @@ function defaultRunTitle(env = process.env, now = /* @__PURE__ */ new Date(), cw
|
|
|
247
249
|
const where = flag(env, "CI") || flag(env, "GITHUB_ACTIONS") ? "ci" : "local";
|
|
248
250
|
return `${path.basename(cwd)} \xB7 ${stamp} \xB7 ${where}`;
|
|
249
251
|
}
|
|
252
|
+
var PROVIDERS = [
|
|
253
|
+
{ is: "GITHUB_ACTIONS", sha: "GITHUB_SHA", branch: ["GITHUB_HEAD_REF", "GITHUB_REF_NAME"] },
|
|
254
|
+
{ is: "GITLAB_CI", sha: "CI_COMMIT_SHA", branch: ["CI_COMMIT_REF_NAME"] },
|
|
255
|
+
{ is: "JENKINS_URL", sha: "GIT_COMMIT", branch: ["GIT_BRANCH"] }
|
|
256
|
+
];
|
|
257
|
+
function ciCommit(reported, env = process.env) {
|
|
258
|
+
const said = typeof reported === "object" && reported !== null ? reported : {};
|
|
259
|
+
const text = (raw) => typeof raw === "string" && raw.trim() !== "" ? raw.trim() : void 0;
|
|
260
|
+
const provider = PROVIDERS.find((p) => value(env, p.is) !== void 0);
|
|
261
|
+
const job = {
|
|
262
|
+
sha: provider === void 0 ? void 0 : value(env, provider.sha),
|
|
263
|
+
branch: provider?.branch.map((name) => value(env, name)).find((v) => v !== void 0)
|
|
264
|
+
};
|
|
265
|
+
const sha = text(said["commitHash"]);
|
|
266
|
+
const sameCommit = sha === void 0 || sha === job.sha;
|
|
267
|
+
const commit = sha ?? job.sha;
|
|
268
|
+
const branch = text(said["branch"]) ?? (sameCommit ? job.branch : void 0);
|
|
269
|
+
return {
|
|
270
|
+
...commit !== void 0 ? { sha: commit } : {},
|
|
271
|
+
...branch !== void 0 ? { branch } : {}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/reporter-core/failure-detail.ts
|
|
276
|
+
var import_node_fs2 = require("fs");
|
|
277
|
+
var import_node_os2 = require("os");
|
|
278
|
+
var import_node_path2 = require("path");
|
|
279
|
+
var import_node_util = require("util");
|
|
280
|
+
function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
281
|
+
const error = chosen(attempt);
|
|
282
|
+
const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
|
|
283
|
+
const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
|
|
284
|
+
const steps = chainOf(attempt.steps);
|
|
285
|
+
const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
|
|
286
|
+
const artifacts = attempt.attachments.filter((a) => a.path !== void 0 && !a.name.startsWith("_") && a.contentType !== METADATA_TYPE).map(({ name, contentType }) => ({ name: fileNameOf(name), ...contentType ? { contentType } : {} })).filter((a) => a.name !== "");
|
|
287
|
+
const ciUrl = webLink(attempt.buildHref);
|
|
288
|
+
const failure = {
|
|
289
|
+
...headline !== void 0 ? { headline } : {},
|
|
290
|
+
...steps.length > 0 ? { steps } : {},
|
|
291
|
+
...location !== void 0 ? { location } : {},
|
|
292
|
+
...artifacts.length > 0 ? { artifacts } : {},
|
|
293
|
+
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
294
|
+
};
|
|
295
|
+
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
296
|
+
}
|
|
297
|
+
function repoRootOf(dir, stopAt) {
|
|
298
|
+
for (let here = (0, import_node_path2.resolve)(dir); ; here = (0, import_node_path2.dirname)(here)) {
|
|
299
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(here, ".git"))) return here;
|
|
300
|
+
if (here === stopAt || (0, import_node_path2.dirname)(here) === here) return void 0;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
304
|
+
const text = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
305
|
+
return cut(machineless(text, attempt.repoRoot, home));
|
|
306
|
+
}
|
|
307
|
+
var mediaTypeOf = (contentType) => contentType?.split(";")[0]?.trim().toLowerCase() || void 0;
|
|
308
|
+
var TEXT_TYPES = /* @__PURE__ */ new Set(["application/json", "text/plain"]);
|
|
309
|
+
var isTextType = (contentType) => TEXT_TYPES.has(mediaTypeOf(contentType) ?? "");
|
|
310
|
+
function filesOf(attempt) {
|
|
311
|
+
return attempt.attachments.filter((a) => a.path !== void 0 || a.body !== void 0 && isTextType(a.contentType));
|
|
312
|
+
}
|
|
313
|
+
function machineless(raw, repoRoot, home) {
|
|
314
|
+
let text = (0, import_node_util.stripVTControlCharacters)(raw).replaceAll("\x1B", "");
|
|
315
|
+
const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
|
|
316
|
+
if (root !== void 0) text = text.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
|
|
317
|
+
const own = pathIn(home);
|
|
318
|
+
if (own !== void 0) text = text.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
|
|
319
|
+
if (repoRoot === void 0) text = text.replace(ANY_HOME, "~");
|
|
320
|
+
return text;
|
|
321
|
+
}
|
|
322
|
+
var PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
323
|
+
var TEXT_LIMIT = 524288;
|
|
324
|
+
var HEADLINE_LIMIT = 8 * 1024;
|
|
325
|
+
var jsonBytes = (text) => Buffer.byteLength(JSON.stringify(text)) - 2;
|
|
326
|
+
var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:(?:file|webpack|webpack-internal):(?:\\*\/){2,3})?(?:\\*\/home\\*\/|\\*\/Users\\*\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
|
|
327
|
+
function pathIn(path3) {
|
|
328
|
+
const parts = path3.split(/[\\/]+/);
|
|
329
|
+
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
330
|
+
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
331
|
+
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
332
|
+
const escaped = /^\/(?!\/)/.test(path3) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
333
|
+
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
334
|
+
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
335
|
+
}
|
|
336
|
+
var fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
337
|
+
function cut(text) {
|
|
338
|
+
if (jsonBytes(text) <= TEXT_LIMIT) return text;
|
|
339
|
+
const lines = text.split("\n");
|
|
340
|
+
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
341
|
+
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
342
|
+
let used = 0;
|
|
343
|
+
let head = 0;
|
|
344
|
+
let tail = lines.length;
|
|
345
|
+
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
346
|
+
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
347
|
+
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
348
|
+
}
|
|
349
|
+
var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
350
|
+
var METADATA_TYPE = "application/plune.metadata+json";
|
|
351
|
+
var REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
352
|
+
var FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
353
|
+
var FRAME_MAX = 8192;
|
|
354
|
+
var firstLine = (text) => (0, import_node_util.stripVTControlCharacters)(text).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
355
|
+
function chosen({ status, errors }) {
|
|
356
|
+
if (status === "timedOut") {
|
|
357
|
+
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
358
|
+
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
359
|
+
}
|
|
360
|
+
return errors[0];
|
|
361
|
+
}
|
|
362
|
+
function chainOf(steps) {
|
|
363
|
+
const chain = [];
|
|
364
|
+
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
365
|
+
chain.push(failed.title);
|
|
366
|
+
}
|
|
367
|
+
return chain;
|
|
368
|
+
}
|
|
369
|
+
function placeOf(error, testFile) {
|
|
370
|
+
const own = normal(testFile);
|
|
371
|
+
let first;
|
|
372
|
+
for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
|
|
373
|
+
if (line.length > FRAME_MAX) continue;
|
|
374
|
+
const frame = FRAME.exec(line);
|
|
375
|
+
if (frame === null) continue;
|
|
376
|
+
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
377
|
+
if (normal(place.file) === own) return place;
|
|
378
|
+
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
379
|
+
}
|
|
380
|
+
return error.location ?? first;
|
|
381
|
+
}
|
|
382
|
+
function fileOf(raw) {
|
|
383
|
+
if (!raw.startsWith("file://")) return raw;
|
|
384
|
+
let path3 = raw.slice("file://".length);
|
|
385
|
+
try {
|
|
386
|
+
path3 = decodeURIComponent(path3);
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
return /^\/[A-Za-z]:\//.test(path3) ? path3.slice(1) : path3;
|
|
390
|
+
}
|
|
391
|
+
var normal = (path3) => import_node_path2.posix.normalize(path3.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
392
|
+
function fromRoot(place, root) {
|
|
393
|
+
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
394
|
+
const base = normal(root).replace(/\/+$/, "");
|
|
395
|
+
const file = normal(place.file);
|
|
396
|
+
if (!file.startsWith(`${base}/`)) return void 0;
|
|
397
|
+
const relative = file.slice(base.length + 1);
|
|
398
|
+
if (!REPO_RELATIVE.test(relative)) return void 0;
|
|
399
|
+
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
400
|
+
return column === void 0 ? { file: relative, line: place.line } : { file: relative, line: place.line, column };
|
|
401
|
+
}
|
|
402
|
+
function webLink(href) {
|
|
403
|
+
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
404
|
+
}
|
|
250
405
|
|
|
251
406
|
// src/reporter-core/fallback.ts
|
|
252
407
|
var fs = __toESM(require("fs"), 1);
|
|
@@ -258,6 +413,8 @@ function appendBatch(file, batch) {
|
|
|
258
413
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
259
414
|
runId: batch.runId,
|
|
260
415
|
externalKey: batch.externalKey,
|
|
416
|
+
...batch.session !== void 0 ? { session: batch.session } : {},
|
|
417
|
+
...batch.meta !== void 0 ? { meta: batch.meta } : {},
|
|
261
418
|
results: batch.results
|
|
262
419
|
};
|
|
263
420
|
fs.appendFileSync(file, `${JSON.stringify(line)}
|
|
@@ -276,6 +433,9 @@ var COUNTED = ["accepted", "duplicate", "conflict", "rejected"];
|
|
|
276
433
|
var SCREENSHOT_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
277
434
|
var SCREENSHOT_BYTES = 2 * 1024 * 1024;
|
|
278
435
|
var SCREENSHOTS_PER_RESULT = 20;
|
|
436
|
+
var TEXT_BYTES = 512 * 1024;
|
|
437
|
+
var TEXTS_PER_RESULT = 10;
|
|
438
|
+
var UTF8 = new import_node_util2.TextDecoder("utf-8", { fatal: true });
|
|
279
439
|
var TYPE_BY_EXTENSION = {
|
|
280
440
|
".png": "image/png",
|
|
281
441
|
".jpg": "image/jpeg",
|
|
@@ -285,9 +445,9 @@ var TYPE_BY_EXTENSION = {
|
|
|
285
445
|
var UPLOADS_AT_ONCE = 4;
|
|
286
446
|
var NAME_MAX = 200;
|
|
287
447
|
function nameOf(file) {
|
|
288
|
-
const own = file.name.split(/[\\/]/).pop()?.trim() || (0,
|
|
289
|
-
const fileExt = (0,
|
|
290
|
-
const ownExt = (0,
|
|
448
|
+
const own = file.name.split(/[\\/]/).pop()?.trim() || (file.path === void 0 ? "" : (0, import_node_path3.basename)(file.path));
|
|
449
|
+
const fileExt = file.path === void 0 ? "" : (0, import_node_path3.extname)(file.path);
|
|
450
|
+
const ownExt = (0, import_node_path3.extname)(own);
|
|
291
451
|
const typed = ownExt !== "" && (ownExt.toLowerCase() === fileExt.toLowerCase() || TYPE_BY_EXTENSION[ownExt.toLowerCase()] !== void 0);
|
|
292
452
|
const ext = typed ? ownExt : fileExt;
|
|
293
453
|
let stem = typed ? own.slice(0, -ownExt.length) : own;
|
|
@@ -302,8 +462,46 @@ var SKIPPED = {
|
|
|
302
462
|
type: "Plune takes PNG, JPEG and WebP",
|
|
303
463
|
limit: `a result holds at most ${SCREENSHOTS_PER_RESULT}`,
|
|
304
464
|
missing: "the file cannot be read on this machine",
|
|
305
|
-
size: "Plune takes a file of at most 2 MB"
|
|
465
|
+
size: "Plune takes a file of at most 2 MB",
|
|
466
|
+
textLimit: `a result holds at most ${TEXTS_PER_RESULT} text files`,
|
|
467
|
+
textSize: "Plune takes a text file of at most 512 KiB",
|
|
468
|
+
empty: "the file is empty",
|
|
469
|
+
binary: "the file holds a NUL byte, and Plune takes UTF-8 text without one",
|
|
470
|
+
encoding: "Plune takes UTF-8 text only"
|
|
471
|
+
};
|
|
472
|
+
var KINDS = {
|
|
473
|
+
screenshot: { label: "screenshot", bytes: SCREENSHOT_BYTES, room: SCREENSHOTS_PER_RESULT, limit: "limit", size: "size" },
|
|
474
|
+
text: { label: "text file", bytes: TEXT_BYTES, room: TEXTS_PER_RESULT, limit: "textLimit", size: "textSize" }
|
|
306
475
|
};
|
|
476
|
+
function kindOf(type) {
|
|
477
|
+
if (type.startsWith("image/")) return "screenshot";
|
|
478
|
+
return TEXT_TYPES.has(type) ? "text" : void 0;
|
|
479
|
+
}
|
|
480
|
+
function textTypeOf(contentType, type) {
|
|
481
|
+
const charset = /;\s*charset\s*=\s*"?([^\s";]+)"?/i.exec(contentType)?.[1];
|
|
482
|
+
return charset?.toLowerCase() === "utf-8" ? `${type}; charset=utf-8` : type;
|
|
483
|
+
}
|
|
484
|
+
function textRefusal(bytes) {
|
|
485
|
+
if (bytes.length > TEXT_BYTES) return "textSize";
|
|
486
|
+
if (bytes.length === 0) return "empty";
|
|
487
|
+
if (bytes.includes(0)) return "binary";
|
|
488
|
+
try {
|
|
489
|
+
UTF8.decode(bytes);
|
|
490
|
+
} catch {
|
|
491
|
+
return "encoding";
|
|
492
|
+
}
|
|
493
|
+
return void 0;
|
|
494
|
+
}
|
|
495
|
+
function onDisk(result) {
|
|
496
|
+
if (!("files" in result) || result.files === void 0) return result;
|
|
497
|
+
const { files, ...rest } = result;
|
|
498
|
+
const kept = files.filter((f) => f.path !== void 0).map(({ body: _body, ...file }) => file);
|
|
499
|
+
return kept.length === 0 ? rest : { ...rest, files: kept };
|
|
500
|
+
}
|
|
501
|
+
function tally(counts, noun) {
|
|
502
|
+
if (counts.uploaded + counts.skipped + counts.failed === 0) return void 0;
|
|
503
|
+
return `${counts.uploaded} ${noun}(s) uploaded` + (counts.skipped > 0 ? `, ${counts.skipped} skipped` : "") + (counts.failed > 0 ? `, ${counts.failed} failed to upload` : "");
|
|
504
|
+
}
|
|
307
505
|
function boundKey(key) {
|
|
308
506
|
if (key.value.length <= KEY_MAX) return key;
|
|
309
507
|
const digest = (0, import_node_crypto.createHash)("sha256").update(key.value).digest("hex").slice(0, 16);
|
|
@@ -311,6 +509,12 @@ function boundKey(key) {
|
|
|
311
509
|
if (/[\uD800-\uDBFF]$/.test(head)) head = head.slice(0, -1);
|
|
312
510
|
return { ...key, value: `${head}#${digest}` };
|
|
313
511
|
}
|
|
512
|
+
function whyUnoffered(out) {
|
|
513
|
+
if (out.ok || out.status !== 429) return "other";
|
|
514
|
+
if (/review queue quota reached/i.test(out.detail)) return "queue";
|
|
515
|
+
if (/test case quota reached/i.test(out.detail)) return "cases";
|
|
516
|
+
return "other";
|
|
517
|
+
}
|
|
314
518
|
function boundTitle(title) {
|
|
315
519
|
return title.length <= TITLE_MAX ? title : `${title.slice(0, TITLE_MAX - 1)}\u2026`;
|
|
316
520
|
}
|
|
@@ -354,6 +558,7 @@ async function startRun(passed, declared) {
|
|
|
354
558
|
`));
|
|
355
559
|
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
356
560
|
const externalKey = cfg.externalKey ?? null;
|
|
561
|
+
const sessionMarker = (0, import_node_crypto.randomUUID)();
|
|
357
562
|
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
358
563
|
const offerDiscovered = cfg.offerDiscovered ?? false;
|
|
359
564
|
const token = cfg.token ?? loadToken() ?? "";
|
|
@@ -372,7 +577,8 @@ async function startRun(passed, declared) {
|
|
|
372
577
|
created: 0,
|
|
373
578
|
unoffered: 0,
|
|
374
579
|
deferred: 0,
|
|
375
|
-
screenshots: { uploaded: 0, skipped: 0, failed: 0 }
|
|
580
|
+
screenshots: { uploaded: 0, skipped: 0, failed: 0 },
|
|
581
|
+
textFiles: { uploaded: 0, skipped: 0, failed: 0 }
|
|
376
582
|
};
|
|
377
583
|
const resolved = /* @__PURE__ */ new Map();
|
|
378
584
|
let buffer = [];
|
|
@@ -383,6 +589,7 @@ async function startRun(passed, declared) {
|
|
|
383
589
|
let done = false;
|
|
384
590
|
const shortened = /* @__PURE__ */ new Set();
|
|
385
591
|
const skipsSaid = /* @__PURE__ */ new Set();
|
|
592
|
+
const countsOf = (kind) => kind === "text" ? stats.textFiles : stats.screenshots;
|
|
386
593
|
function bound(keys) {
|
|
387
594
|
return keys.map((key) => {
|
|
388
595
|
const fit = boundKey(key);
|
|
@@ -405,7 +612,13 @@ async function startRun(passed, declared) {
|
|
|
405
612
|
log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
|
|
406
613
|
}
|
|
407
614
|
function defer(results) {
|
|
408
|
-
appendBatch(fallbackPath, {
|
|
615
|
+
appendBatch(fallbackPath, {
|
|
616
|
+
runId,
|
|
617
|
+
externalKey,
|
|
618
|
+
session: sessionMarker,
|
|
619
|
+
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
620
|
+
results: results.map(onDisk)
|
|
621
|
+
});
|
|
409
622
|
stats.deferred += results.length;
|
|
410
623
|
}
|
|
411
624
|
async function resolveKeys(values) {
|
|
@@ -502,6 +715,7 @@ async function startRun(passed, declared) {
|
|
|
502
715
|
});
|
|
503
716
|
if (!out.ok || !Array.isArray(out.body?.results)) {
|
|
504
717
|
stats.unoffered = batches.slice(index).reduce((n, rest) => n + rest.length, 0);
|
|
718
|
+
stats.unofferedWhy = whyUnoffered(out);
|
|
505
719
|
log(
|
|
506
720
|
`plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.ok ? "the answer had no results" : out.detail || out.kind}).`
|
|
507
721
|
);
|
|
@@ -511,57 +725,74 @@ async function startRun(passed, declared) {
|
|
|
511
725
|
stats.created += out.body.results.filter((r) => r?.outcome === "created").length;
|
|
512
726
|
}
|
|
513
727
|
}
|
|
514
|
-
function skip(name, why) {
|
|
515
|
-
|
|
516
|
-
if (skipsSaid.has(why)) return;
|
|
517
|
-
skipsSaid.add(why);
|
|
518
|
-
log(`plune:
|
|
728
|
+
function skip(kind, name, why) {
|
|
729
|
+
countsOf(kind).skipped += 1;
|
|
730
|
+
if (skipsSaid.has(`${kind}/${why}`)) return;
|
|
731
|
+
skipsSaid.add(`${kind}/${why}`);
|
|
732
|
+
log(`plune: ${KINDS[kind].label} "${name}" not uploaded \u2014 ${SKIPPED[why]}.`);
|
|
519
733
|
}
|
|
520
|
-
async function uploadOne(
|
|
734
|
+
async function uploadOne(job) {
|
|
735
|
+
const { kind } = job;
|
|
736
|
+
const { label, bytes: ceiling, size } = KINDS[kind];
|
|
521
737
|
let bytes;
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
738
|
+
if (job.body !== void 0) bytes = job.body;
|
|
739
|
+
else if (job.path === void 0) return skip(kind, job.name, "missing");
|
|
740
|
+
else {
|
|
741
|
+
try {
|
|
742
|
+
if ((await import_node_fs3.promises.stat(job.path)).size > ceiling) return skip(kind, job.name, size);
|
|
743
|
+
bytes = await import_node_fs3.promises.readFile(job.path);
|
|
744
|
+
} catch {
|
|
745
|
+
return skip(kind, job.name, "missing");
|
|
746
|
+
}
|
|
527
747
|
}
|
|
748
|
+
const refusal = kind === "text" ? textRefusal(bytes) : void 0;
|
|
749
|
+
if (refusal !== void 0) return skip(kind, job.name, refusal);
|
|
528
750
|
const out = await client.upload(
|
|
529
|
-
`/v1/results/${encodeURIComponent(
|
|
751
|
+
`/v1/results/${encodeURIComponent(job.id)}/files?name=${encodeURIComponent(job.name)}`,
|
|
530
752
|
bytes,
|
|
531
|
-
|
|
753
|
+
job.type
|
|
532
754
|
);
|
|
755
|
+
const counts = countsOf(kind);
|
|
533
756
|
if (out.ok) {
|
|
534
|
-
|
|
757
|
+
counts.uploaded += 1;
|
|
535
758
|
return;
|
|
536
759
|
}
|
|
537
|
-
|
|
538
|
-
if (
|
|
539
|
-
log(`plune: could not upload a
|
|
760
|
+
counts.failed += 1;
|
|
761
|
+
if (counts.failed === 1) {
|
|
762
|
+
log(`plune: could not upload a ${label} (${out.detail || out.kind}) \u2014 its result is reported without it.`);
|
|
540
763
|
}
|
|
541
764
|
}
|
|
542
765
|
async function upload(items, filesByKey) {
|
|
543
766
|
if (filesByKey.size === 0 || !Array.isArray(items)) return;
|
|
544
|
-
const
|
|
767
|
+
const jobs = [];
|
|
545
768
|
for (const item of items) {
|
|
546
769
|
if (item?.status !== "accepted" || typeof item.id !== "string" || typeof item.resultKey !== "string") continue;
|
|
547
|
-
|
|
770
|
+
const room = { screenshot: KINDS.screenshot.room, text: KINDS.text.room };
|
|
548
771
|
for (const file of filesByKey.get(item.resultKey) ?? []) {
|
|
549
|
-
const type = file.contentType
|
|
550
|
-
|
|
772
|
+
const type = mediaTypeOf(file.contentType) ?? (file.path === void 0 ? void 0 : TYPE_BY_EXTENSION[(0, import_node_path3.extname)(file.path).toLowerCase()]);
|
|
773
|
+
const kind = type === void 0 ? void 0 : kindOf(type);
|
|
774
|
+
if (type === void 0 || kind === void 0 || kind === "screenshot" && file.path === void 0) continue;
|
|
551
775
|
const name = nameOf(file);
|
|
552
|
-
if (file.name.startsWith("_")) skip(name, "hidden");
|
|
553
|
-
else if (!SCREENSHOT_TYPES.has(type)) skip(name, "type");
|
|
554
|
-
else if (room === 0) skip(name,
|
|
776
|
+
if (file.name.startsWith("_")) skip(kind, name, "hidden");
|
|
777
|
+
else if (kind === "screenshot" && !SCREENSHOT_TYPES.has(type)) skip(kind, name, "type");
|
|
778
|
+
else if (room[kind] === 0) skip(kind, name, KINDS[kind].limit);
|
|
555
779
|
else {
|
|
556
|
-
room -= 1;
|
|
557
|
-
|
|
780
|
+
room[kind] -= 1;
|
|
781
|
+
jobs.push({
|
|
782
|
+
id: item.id,
|
|
783
|
+
kind,
|
|
784
|
+
name,
|
|
785
|
+
type: kind === "text" ? textTypeOf(file.contentType ?? "", type) : type,
|
|
786
|
+
...file.body !== void 0 ? { body: file.body } : {},
|
|
787
|
+
...file.path !== void 0 ? { path: file.path } : {}
|
|
788
|
+
});
|
|
558
789
|
}
|
|
559
790
|
}
|
|
560
791
|
}
|
|
561
792
|
let next = 0;
|
|
562
793
|
await Promise.all(
|
|
563
|
-
Array.from({ length: Math.min(UPLOADS_AT_ONCE,
|
|
564
|
-
while (next <
|
|
794
|
+
Array.from({ length: Math.min(UPLOADS_AT_ONCE, jobs.length) }, async () => {
|
|
795
|
+
while (next < jobs.length) await uploadOne(jobs[next++]);
|
|
565
796
|
})
|
|
566
797
|
);
|
|
567
798
|
}
|
|
@@ -639,11 +870,8 @@ async function startRun(passed, declared) {
|
|
|
639
870
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
640
871
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
641
872
|
if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
parts.push(
|
|
645
|
-
`${shots.uploaded} screenshot(s) uploaded` + (shots.skipped > 0 ? `, ${shots.skipped} skipped` : "") + (shots.failed > 0 ? `, ${shots.failed} failed to upload` : "")
|
|
646
|
-
);
|
|
873
|
+
for (const said of [tally(stats.screenshots, "screenshot"), tally(stats.textFiles, "text file")]) {
|
|
874
|
+
if (said !== void 0) parts.push(said);
|
|
647
875
|
}
|
|
648
876
|
if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
|
|
649
877
|
if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
|
|
@@ -710,140 +938,13 @@ function resultKey(testId, retry) {
|
|
|
710
938
|
const plain = `${testId}#${retry}`;
|
|
711
939
|
return plain.length <= RESULT_KEY_MAX ? plain : `${sha256(testId).slice(0, HASH_CHARS)}#${retry}`;
|
|
712
940
|
}
|
|
713
|
-
|
|
714
|
-
// src/reporter-core/failure-detail.ts
|
|
715
|
-
var import_node_fs3 = require("fs");
|
|
716
|
-
var import_node_os2 = require("os");
|
|
717
|
-
var import_node_path3 = require("path");
|
|
718
|
-
var import_node_util = require("util");
|
|
719
|
-
function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
720
|
-
const error = chosen(attempt);
|
|
721
|
-
const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
|
|
722
|
-
const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
|
|
723
|
-
const steps = chainOf(attempt.steps);
|
|
724
|
-
const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
|
|
725
|
-
const artifacts = attempt.attachments.filter((a) => a.path !== void 0 && !a.name.startsWith("_") && a.contentType !== METADATA_TYPE).map(({ name, contentType }) => ({ name: fileNameOf(name), ...contentType ? { contentType } : {} })).filter((a) => a.name !== "");
|
|
726
|
-
const ciUrl = webLink(attempt.buildHref);
|
|
727
|
-
const failure = {
|
|
728
|
-
...headline !== void 0 ? { headline } : {},
|
|
729
|
-
...steps.length > 0 ? { steps } : {},
|
|
730
|
-
...location !== void 0 ? { location } : {},
|
|
731
|
-
...artifacts.length > 0 ? { artifacts } : {},
|
|
732
|
-
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
733
|
-
};
|
|
734
|
-
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
735
|
-
}
|
|
736
|
-
function repoRootOf(dir, stopAt) {
|
|
737
|
-
for (let here = (0, import_node_path3.resolve)(dir); ; here = (0, import_node_path3.dirname)(here)) {
|
|
738
|
-
if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(here, ".git"))) return here;
|
|
739
|
-
if (here === stopAt || (0, import_node_path3.dirname)(here) === here) return void 0;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
743
|
-
const text = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
744
|
-
return cut(machineless(text, attempt.repoRoot, home));
|
|
745
|
-
}
|
|
746
|
-
function filesOf(attempt) {
|
|
747
|
-
return attempt.attachments.filter((a) => a.path !== void 0);
|
|
748
|
-
}
|
|
749
|
-
function machineless(raw, repoRoot, home) {
|
|
750
|
-
let text = (0, import_node_util.stripVTControlCharacters)(raw).replaceAll("\x1B", "");
|
|
751
|
-
const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
|
|
752
|
-
if (root !== void 0) text = text.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
|
|
753
|
-
const own = pathIn(home);
|
|
754
|
-
if (own !== void 0) text = text.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
|
|
755
|
-
if (repoRoot === void 0) text = text.replace(ANY_HOME, "~");
|
|
756
|
-
return text;
|
|
757
|
-
}
|
|
758
|
-
var PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
759
|
-
var TEXT_LIMIT = 524288;
|
|
760
|
-
var HEADLINE_LIMIT = 8 * 1024;
|
|
761
|
-
var jsonBytes = (text) => Buffer.byteLength(JSON.stringify(text)) - 2;
|
|
762
|
-
var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:(?:file|webpack|webpack-internal):(?:\\*\/){2,3})?(?:\\*\/home\\*\/|\\*\/Users\\*\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
|
|
763
|
-
function pathIn(path3) {
|
|
764
|
-
const parts = path3.split(/[\\/]+/);
|
|
765
|
-
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
766
|
-
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
767
|
-
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
768
|
-
const escaped = /^\/(?!\/)/.test(path3) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
769
|
-
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
770
|
-
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
771
|
-
}
|
|
772
|
-
var fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
773
|
-
function cut(text) {
|
|
774
|
-
if (jsonBytes(text) <= TEXT_LIMIT) return text;
|
|
775
|
-
const lines = text.split("\n");
|
|
776
|
-
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
777
|
-
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
778
|
-
let used = 0;
|
|
779
|
-
let head = 0;
|
|
780
|
-
let tail = lines.length;
|
|
781
|
-
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
782
|
-
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
783
|
-
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
784
|
-
}
|
|
785
|
-
var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
786
|
-
var METADATA_TYPE = "application/plune.metadata+json";
|
|
787
|
-
var REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
788
|
-
var FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
789
|
-
var FRAME_MAX = 8192;
|
|
790
|
-
var firstLine = (text) => (0, import_node_util.stripVTControlCharacters)(text).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
791
|
-
function chosen({ status, errors }) {
|
|
792
|
-
if (status === "timedOut") {
|
|
793
|
-
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
794
|
-
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
795
|
-
}
|
|
796
|
-
return errors[0];
|
|
797
|
-
}
|
|
798
|
-
function chainOf(steps) {
|
|
799
|
-
const chain = [];
|
|
800
|
-
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
801
|
-
chain.push(failed.title);
|
|
802
|
-
}
|
|
803
|
-
return chain;
|
|
804
|
-
}
|
|
805
|
-
function placeOf(error, testFile) {
|
|
806
|
-
const own = normal(testFile);
|
|
807
|
-
let first;
|
|
808
|
-
for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
|
|
809
|
-
if (line.length > FRAME_MAX) continue;
|
|
810
|
-
const frame = FRAME.exec(line);
|
|
811
|
-
if (frame === null) continue;
|
|
812
|
-
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
813
|
-
if (normal(place.file) === own) return place;
|
|
814
|
-
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
815
|
-
}
|
|
816
|
-
return error.location ?? first;
|
|
817
|
-
}
|
|
818
|
-
function fileOf(raw) {
|
|
819
|
-
if (!raw.startsWith("file://")) return raw;
|
|
820
|
-
let path3 = raw.slice("file://".length);
|
|
821
|
-
try {
|
|
822
|
-
path3 = decodeURIComponent(path3);
|
|
823
|
-
} catch {
|
|
824
|
-
}
|
|
825
|
-
return /^\/[A-Za-z]:\//.test(path3) ? path3.slice(1) : path3;
|
|
826
|
-
}
|
|
827
|
-
var normal = (path3) => import_node_path3.posix.normalize(path3.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
828
|
-
function fromRoot(place, root) {
|
|
829
|
-
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
830
|
-
const base = normal(root).replace(/\/+$/, "");
|
|
831
|
-
const file = normal(place.file);
|
|
832
|
-
if (!file.startsWith(`${base}/`)) return void 0;
|
|
833
|
-
const relative = file.slice(base.length + 1);
|
|
834
|
-
if (!REPO_RELATIVE.test(relative)) return void 0;
|
|
835
|
-
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
836
|
-
return column === void 0 ? { file: relative, line: place.line } : { file: relative, line: place.line, column };
|
|
837
|
-
}
|
|
838
|
-
function webLink(href) {
|
|
839
|
-
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
840
|
-
}
|
|
841
941
|
// Annotate the CommonJS export names for ESM import in node:
|
|
842
942
|
0 && (module.exports = {
|
|
843
943
|
DEFAULT_FALLBACK_PATH,
|
|
844
944
|
RESULT_KEY_MAX,
|
|
845
945
|
UNSUPPORTED_VARS,
|
|
846
946
|
appendBatch,
|
|
947
|
+
ciCommit,
|
|
847
948
|
createClient,
|
|
848
949
|
errorContextOf,
|
|
849
950
|
failureOf,
|