@plune-ai/cli 0.12.0 → 0.14.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 +3 -2
- package/dist/cli.cjs +365 -61
- package/dist/index.cjs +4 -2
- package/dist/index.js +4 -2
- package/dist/reporter-core.cjs +209 -24
- package/dist/reporter-core.d.cts +98 -11
- package/dist/reporter-core.d.ts +98 -11
- package/dist/reporter-core.js +204 -23
- package/package.json +1 -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((resolve8) => setTimeout(resolve8, ms));
|
|
516
516
|
}
|
|
517
517
|
});
|
|
518
518
|
|
|
@@ -2159,6 +2159,8 @@ function saveToken(token) {
|
|
|
2159
2159
|
return file;
|
|
2160
2160
|
}
|
|
2161
2161
|
function loadToken() {
|
|
2162
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
2163
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
2162
2164
|
try {
|
|
2163
2165
|
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
2164
2166
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
@@ -2184,6 +2186,35 @@ var init_credentials = __esm({
|
|
|
2184
2186
|
});
|
|
2185
2187
|
|
|
2186
2188
|
// src/reporter-core/client.ts
|
|
2189
|
+
function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
2190
|
+
return new Promise((resolve8, reject) => {
|
|
2191
|
+
const target = new URL(url);
|
|
2192
|
+
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
2193
|
+
const req = send(target, { method: init.method, headers: init.headers }, (res) => {
|
|
2194
|
+
const chunks = [];
|
|
2195
|
+
res.on("data", (chunk2) => chunks.push(chunk2));
|
|
2196
|
+
res.on("error", reject);
|
|
2197
|
+
res.on("end", () => {
|
|
2198
|
+
const status = res.statusCode ?? 0;
|
|
2199
|
+
const text2 = Buffer.concat(chunks).toString("utf8");
|
|
2200
|
+
resolve8({
|
|
2201
|
+
ok: status >= 200 && status <= 299,
|
|
2202
|
+
status,
|
|
2203
|
+
headers: {
|
|
2204
|
+
get: (name) => {
|
|
2205
|
+
const value2 = res.headers[name.toLowerCase()];
|
|
2206
|
+
return value2 === void 0 ? null : Array.isArray(value2) ? value2.join(", ") : value2;
|
|
2207
|
+
}
|
|
2208
|
+
},
|
|
2209
|
+
json: async () => JSON.parse(text2)
|
|
2210
|
+
});
|
|
2211
|
+
});
|
|
2212
|
+
});
|
|
2213
|
+
req.setTimeout(idleMs, () => req.destroy(new Error(`no answer from ${target.origin} in ${idleMs} ms`)));
|
|
2214
|
+
req.on("error", reject);
|
|
2215
|
+
req.end(init.body);
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2187
2218
|
async function detailOf(res) {
|
|
2188
2219
|
try {
|
|
2189
2220
|
const body = await res.json();
|
|
@@ -2206,7 +2237,7 @@ function classify3(status) {
|
|
|
2206
2237
|
return "other";
|
|
2207
2238
|
}
|
|
2208
2239
|
function createClient(opts) {
|
|
2209
|
-
const doFetch = opts.fetchImpl ??
|
|
2240
|
+
const doFetch = opts.fetchImpl ?? nodeTransport;
|
|
2210
2241
|
const wait = opts.wait ?? sleep;
|
|
2211
2242
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
2212
2243
|
const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
|
|
@@ -2251,12 +2282,15 @@ function createClient(opts) {
|
|
|
2251
2282
|
del: (path14) => send("DELETE", path14)
|
|
2252
2283
|
};
|
|
2253
2284
|
}
|
|
2254
|
-
var RETRY_DELAYS_MS, sleep;
|
|
2285
|
+
var import_node_http, import_node_https, RETRY_DELAYS_MS, IDLE_MS, sleep;
|
|
2255
2286
|
var init_client = __esm({
|
|
2256
2287
|
"src/reporter-core/client.ts"() {
|
|
2257
2288
|
"use strict";
|
|
2258
2289
|
init_cjs_shims();
|
|
2290
|
+
import_node_http = require("http");
|
|
2291
|
+
import_node_https = require("https");
|
|
2259
2292
|
RETRY_DELAYS_MS = [300, 900];
|
|
2293
|
+
IDLE_MS = 3e5;
|
|
2260
2294
|
sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2261
2295
|
}
|
|
2262
2296
|
});
|
|
@@ -2365,6 +2399,23 @@ function boundKey(key) {
|
|
|
2365
2399
|
function boundTitle(title) {
|
|
2366
2400
|
return title.length <= TITLE_MAX ? title : `${title.slice(0, TITLE_MAX - 1)}\u2026`;
|
|
2367
2401
|
}
|
|
2402
|
+
function packed(results) {
|
|
2403
|
+
const out = [];
|
|
2404
|
+
let part = [];
|
|
2405
|
+
let bytes = ENVELOPE_BYTES;
|
|
2406
|
+
for (const result of results) {
|
|
2407
|
+
const size = Buffer.byteLength(JSON.stringify(result));
|
|
2408
|
+
if (part.length > 0 && bytes + 1 + size > BATCH_BYTES) {
|
|
2409
|
+
out.push(part);
|
|
2410
|
+
part = [];
|
|
2411
|
+
bytes = ENVELOPE_BYTES;
|
|
2412
|
+
}
|
|
2413
|
+
bytes += (part.length > 0 ? 1 : 0) + size;
|
|
2414
|
+
part.push(result);
|
|
2415
|
+
}
|
|
2416
|
+
if (part.length > 0) out.push(part);
|
|
2417
|
+
return out;
|
|
2418
|
+
}
|
|
2368
2419
|
function chunk(items, size) {
|
|
2369
2420
|
const out = [];
|
|
2370
2421
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
@@ -2566,27 +2617,32 @@ async function startRun(passed, declared) {
|
|
|
2566
2617
|
progress();
|
|
2567
2618
|
return;
|
|
2568
2619
|
}
|
|
2569
|
-
const
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2620
|
+
for (const part of packed(submissions)) {
|
|
2621
|
+
if (offline) {
|
|
2622
|
+
defer(part);
|
|
2623
|
+
continue;
|
|
2624
|
+
}
|
|
2625
|
+
const out = await client.post(`/v1/runs/${runId}/results`, {
|
|
2626
|
+
results: part
|
|
2627
|
+
});
|
|
2628
|
+
if (out.ok) {
|
|
2629
|
+
stats.accepted += out.body.counts.accepted ?? 0;
|
|
2630
|
+
stats.duplicate += out.body.counts.duplicate ?? 0;
|
|
2631
|
+
stats.conflict += out.body.counts.conflict ?? 0;
|
|
2632
|
+
stats.rejected += out.body.counts.rejected ?? 0;
|
|
2633
|
+
progress();
|
|
2634
|
+
continue;
|
|
2635
|
+
}
|
|
2636
|
+
if (out.kind === "auth") {
|
|
2637
|
+
log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
|
|
2638
|
+
offline = true;
|
|
2639
|
+
} else if (out.kind === "conflict") {
|
|
2640
|
+
log(`plune: this run is closed \u2014 ${out.detail}`);
|
|
2641
|
+
} else {
|
|
2642
|
+
log(`plune: could not send results (${out.detail || out.kind}).`);
|
|
2643
|
+
}
|
|
2644
|
+
defer(part);
|
|
2588
2645
|
}
|
|
2589
|
-
defer(submissions);
|
|
2590
2646
|
}
|
|
2591
2647
|
function progress() {
|
|
2592
2648
|
const total = expected?.length ?? 0;
|
|
@@ -2602,9 +2658,12 @@ async function startRun(passed, declared) {
|
|
|
2602
2658
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
2603
2659
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
2604
2660
|
if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
|
|
2605
|
-
if (stats.deferred > 0) parts.push(`${stats.deferred} written to ${fallbackPath}`);
|
|
2661
|
+
if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
|
|
2606
2662
|
if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
|
|
2607
2663
|
log(`plune: ${parts.join(" \xB7 ")}`);
|
|
2664
|
+
if (stats.deferred > 0 && process.env["GITHUB_ACTIONS"] === "true") {
|
|
2665
|
+
log(`::warning::${stats.deferred} result(s) were not delivered to Plune \u2014 see the reporting step's log.`);
|
|
2666
|
+
}
|
|
2608
2667
|
}
|
|
2609
2668
|
return {
|
|
2610
2669
|
get runId() {
|
|
@@ -2650,7 +2709,7 @@ async function startRun(passed, declared) {
|
|
|
2650
2709
|
}
|
|
2651
2710
|
};
|
|
2652
2711
|
}
|
|
2653
|
-
var import_node_crypto2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX, KEY_MAX, TITLE_MAX;
|
|
2712
|
+
var import_node_crypto2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX, KEY_MAX, TITLE_MAX, BATCH_BYTES, ENVELOPE_BYTES;
|
|
2654
2713
|
var init_session = __esm({
|
|
2655
2714
|
"src/reporter-core/session.ts"() {
|
|
2656
2715
|
"use strict";
|
|
@@ -2668,6 +2727,8 @@ var init_session = __esm({
|
|
|
2668
2727
|
EXPECTED_MAX = 1e4;
|
|
2669
2728
|
KEY_MAX = 1024;
|
|
2670
2729
|
TITLE_MAX = 300;
|
|
2730
|
+
BATCH_BYTES = 8 * 1024 * 1024;
|
|
2731
|
+
ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ results: [] }));
|
|
2671
2732
|
}
|
|
2672
2733
|
});
|
|
2673
2734
|
|
|
@@ -2763,14 +2824,14 @@ async function handleRunExec(options) {
|
|
|
2763
2824
|
if (command === void 0) throw new RunCommandError("Nothing to run \u2014 pass a command after `--`.");
|
|
2764
2825
|
const run = await handleRunStart(options);
|
|
2765
2826
|
const spawnImpl = options.spawnImpl ?? import_node_child_process.spawn;
|
|
2766
|
-
const code = await new Promise((
|
|
2827
|
+
const code = await new Promise((resolve8) => {
|
|
2767
2828
|
const child = spawnImpl(command, args, {
|
|
2768
2829
|
stdio: "inherit",
|
|
2769
2830
|
shell: process.platform === "win32",
|
|
2770
2831
|
env: { ...process.env, PLUNE_RUN: run.externalKey, PLUNE_PROCEED: "1" }
|
|
2771
2832
|
});
|
|
2772
|
-
child.on("close", (exitCode) =>
|
|
2773
|
-
child.on("error", () =>
|
|
2833
|
+
child.on("close", (exitCode) => resolve8(exitCode ?? 1));
|
|
2834
|
+
child.on("error", () => resolve8(1));
|
|
2774
2835
|
});
|
|
2775
2836
|
await handleRunFinish(run.id, options);
|
|
2776
2837
|
return code;
|
|
@@ -3102,21 +3163,163 @@ var init_junit = __esm({
|
|
|
3102
3163
|
}
|
|
3103
3164
|
});
|
|
3104
3165
|
|
|
3166
|
+
// src/reporter-core/failure-detail.ts
|
|
3167
|
+
function failureOf(attempt) {
|
|
3168
|
+
const error = chosen(attempt);
|
|
3169
|
+
const headline = error === void 0 ? void 0 : firstLine(error.text);
|
|
3170
|
+
const steps = chainOf(attempt.steps);
|
|
3171
|
+
const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
|
|
3172
|
+
const artifacts = attempt.attachments.filter((a) => a.path !== void 0 && a.name !== "" && !a.name.startsWith("_") && a.contentType !== METADATA_TYPE).map(({ name, contentType }) => contentType === void 0 ? { name } : { name, contentType });
|
|
3173
|
+
const ciUrl = webLink(attempt.buildHref);
|
|
3174
|
+
const failure = {
|
|
3175
|
+
...headline !== void 0 ? { headline } : {},
|
|
3176
|
+
...steps.length > 0 ? { steps } : {},
|
|
3177
|
+
...location !== void 0 ? { location } : {},
|
|
3178
|
+
...artifacts.length > 0 ? { artifacts } : {},
|
|
3179
|
+
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
3180
|
+
};
|
|
3181
|
+
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
3182
|
+
}
|
|
3183
|
+
function repoRootOf(dir, stopAt) {
|
|
3184
|
+
for (let here = (0, import_node_path2.resolve)(dir); ; here = (0, import_node_path2.dirname)(here)) {
|
|
3185
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(here, ".git"))) return here;
|
|
3186
|
+
if (here === stopAt || (0, import_node_path2.dirname)(here) === here) return void 0;
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
3190
|
+
let text2 = (0, import_node_util.stripVTControlCharacters)(
|
|
3191
|
+
attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n")
|
|
3192
|
+
).replaceAll("\x1B", "");
|
|
3193
|
+
const root = attempt.repoRoot === void 0 ? void 0 : pathIn(attempt.repoRoot);
|
|
3194
|
+
if (root !== void 0) text2 = text2.replace(new RegExp(`${root.source}[\\\\/]`, root.flags), "");
|
|
3195
|
+
const own = pathIn(home);
|
|
3196
|
+
if (own !== void 0) text2 = text2.replace(new RegExp(`${own.source}(?![\\w.-])`, own.flags), "~");
|
|
3197
|
+
if (attempt.repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
|
|
3198
|
+
return cut(text2);
|
|
3199
|
+
}
|
|
3200
|
+
function pathIn(path14) {
|
|
3201
|
+
const parts = path14.split(/[\\/]+/);
|
|
3202
|
+
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
3203
|
+
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
3204
|
+
const escaped = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]");
|
|
3205
|
+
return new RegExp(`(?:file:\\/\\/\\/?)?${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
3206
|
+
}
|
|
3207
|
+
function cut(text2) {
|
|
3208
|
+
if (Buffer.byteLength(text2) <= TEXT_LIMIT) return text2;
|
|
3209
|
+
const lines = text2.split("\n");
|
|
3210
|
+
const size = lines.map((line) => Buffer.byteLength(line) + 1);
|
|
3211
|
+
const budget = TEXT_LIMIT - Buffer.byteLength(omitted(lines.length));
|
|
3212
|
+
let used = 0;
|
|
3213
|
+
let head = 0;
|
|
3214
|
+
let tail = lines.length;
|
|
3215
|
+
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
3216
|
+
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
3217
|
+
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
3218
|
+
}
|
|
3219
|
+
function chosen({ status, errors }) {
|
|
3220
|
+
if (status === "timedOut") {
|
|
3221
|
+
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
3222
|
+
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
3223
|
+
}
|
|
3224
|
+
return errors[0];
|
|
3225
|
+
}
|
|
3226
|
+
function chainOf(steps) {
|
|
3227
|
+
const chain = [];
|
|
3228
|
+
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
3229
|
+
chain.push(failed.title);
|
|
3230
|
+
}
|
|
3231
|
+
return chain;
|
|
3232
|
+
}
|
|
3233
|
+
function placeOf(error, testFile) {
|
|
3234
|
+
const own = normal(testFile);
|
|
3235
|
+
let first;
|
|
3236
|
+
for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
|
|
3237
|
+
const frame = FRAME.exec(line);
|
|
3238
|
+
if (frame === null) continue;
|
|
3239
|
+
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
3240
|
+
if (normal(place.file) === own) return place;
|
|
3241
|
+
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
3242
|
+
}
|
|
3243
|
+
return error.location ?? first;
|
|
3244
|
+
}
|
|
3245
|
+
function fileOf(raw) {
|
|
3246
|
+
if (!raw.startsWith("file://")) return raw;
|
|
3247
|
+
const path14 = decodeURIComponent(raw.slice("file://".length));
|
|
3248
|
+
return /^\/[A-Za-z]:\//.test(path14) ? path14.slice(1) : path14;
|
|
3249
|
+
}
|
|
3250
|
+
function fromRoot(place, root) {
|
|
3251
|
+
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
3252
|
+
const base = normal(root).replace(/\/+$/, "");
|
|
3253
|
+
const file = normal(place.file);
|
|
3254
|
+
if (!file.startsWith(`${base}/`)) return void 0;
|
|
3255
|
+
const relative2 = file.slice(base.length + 1);
|
|
3256
|
+
if (!REPO_RELATIVE.test(relative2)) return void 0;
|
|
3257
|
+
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
3258
|
+
return column === void 0 ? { file: relative2, line: place.line } : { file: relative2, line: place.line, column };
|
|
3259
|
+
}
|
|
3260
|
+
function webLink(href) {
|
|
3261
|
+
if (href === void 0) return void 0;
|
|
3262
|
+
try {
|
|
3263
|
+
const { protocol } = new URL(href);
|
|
3264
|
+
return protocol === "http:" || protocol === "https:" ? href : void 0;
|
|
3265
|
+
} catch {
|
|
3266
|
+
return void 0;
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
var import_node_fs2, import_node_os2, import_node_path2, import_node_util, TEXT_LIMIT, ANY_HOME, omitted, METADATA_TYPE, REPO_RELATIVE, FRAME, firstLine, normal;
|
|
3270
|
+
var init_failure_detail = __esm({
|
|
3271
|
+
"src/reporter-core/failure-detail.ts"() {
|
|
3272
|
+
"use strict";
|
|
3273
|
+
init_cjs_shims();
|
|
3274
|
+
import_node_fs2 = require("fs");
|
|
3275
|
+
import_node_os2 = require("os");
|
|
3276
|
+
import_node_path2 = require("path");
|
|
3277
|
+
import_node_util = require("util");
|
|
3278
|
+
TEXT_LIMIT = 524288;
|
|
3279
|
+
ANY_HOME = /(?<=^|[\s'"`(=,[])(?:file:\/\/\/?)?(?:\/home\/|\/Users\/|[A-Za-z]:[\\/]Users[\\/])[^\\/\s'"`:*?<>|]+/g;
|
|
3280
|
+
omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
3281
|
+
METADATA_TYPE = "application/plune.metadata+json";
|
|
3282
|
+
REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
3283
|
+
FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
3284
|
+
firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
3285
|
+
normal = (path14) => import_node_path2.posix.normalize(path14.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
3286
|
+
}
|
|
3287
|
+
});
|
|
3288
|
+
|
|
3105
3289
|
// src/importers/playwright-json.ts
|
|
3106
3290
|
function statedIdOf(test, title) {
|
|
3107
3291
|
const annotation = test.annotations?.find((a) => a.type === "PluneId")?.description;
|
|
3108
3292
|
if (annotation !== void 0 && annotation !== "") return annotation;
|
|
3109
3293
|
return TOKEN.exec(title)?.[1];
|
|
3110
3294
|
}
|
|
3111
|
-
function
|
|
3112
|
-
|
|
3295
|
+
function attemptOf(result, file, ctx) {
|
|
3296
|
+
const steps = (list2) => (list2 ?? []).map((s) => ({ title: s.title ?? "", failed: s.error !== void 0, steps: steps(s.steps) }));
|
|
3297
|
+
return {
|
|
3298
|
+
status: result.status ?? "unknown",
|
|
3299
|
+
errors: (result.errors ?? []).map(({ message, value: value2, location: at }) => ({
|
|
3300
|
+
text: message ?? value2 ?? "",
|
|
3301
|
+
...typeof at?.file === "string" && typeof at.line === "number" ? { location: { file: at.file, line: at.line, ...typeof at.column === "number" ? { column: at.column } : {} } } : {}
|
|
3302
|
+
})),
|
|
3303
|
+
steps: steps(result.steps),
|
|
3304
|
+
attachments: (result.attachments ?? []).map(({ name, contentType, path: path14 }) => ({
|
|
3305
|
+
name: name ?? "",
|
|
3306
|
+
...contentType !== void 0 ? { contentType } : {},
|
|
3307
|
+
...path14 !== void 0 ? { path: path14 } : {}
|
|
3308
|
+
})),
|
|
3309
|
+
...ctx.buildHref !== void 0 ? { buildHref: ctx.buildHref } : {},
|
|
3310
|
+
// Spec files are posix and relative to the root dir; the core reads either slash.
|
|
3311
|
+
testFile: ctx.rootDir === void 0 ? file : `${ctx.rootDir.replace(/[\\/]+$/, "")}/${file}`,
|
|
3312
|
+
...ctx.repoRoot !== void 0 ? { repoRoot: ctx.repoRoot } : {}
|
|
3313
|
+
};
|
|
3113
3314
|
}
|
|
3114
|
-
function pendingFrom(spec, test, result, file, titles) {
|
|
3315
|
+
function pendingFrom(spec, test, result, file, titles, ctx) {
|
|
3115
3316
|
const pathTitle = [file, ...titles].join("#");
|
|
3116
3317
|
const seed = test.id ?? pathTitle;
|
|
3117
3318
|
const stated = statedIdOf(test, spec.title ?? "");
|
|
3118
3319
|
const expected = test.expectedStatus;
|
|
3119
|
-
const
|
|
3320
|
+
const attempt = attemptOf(result, file, ctx);
|
|
3321
|
+
const errorContext = errorContextOf(attempt);
|
|
3322
|
+
const failure = NOT_FAILED.has(attempt.status) ? void 0 : failureOf(attempt);
|
|
3120
3323
|
const keys = [
|
|
3121
3324
|
...test.id !== void 0 ? [{ kind: "playwright-id", value: test.id }] : [],
|
|
3122
3325
|
{ kind: "path-title", value: pathTitle }
|
|
@@ -3144,19 +3347,20 @@ function pendingFrom(spec, test, result, file, titles) {
|
|
|
3144
3347
|
...result.workerIndex !== void 0 ? { worker: String(result.workerIndex) } : {}
|
|
3145
3348
|
}
|
|
3146
3349
|
} : {},
|
|
3147
|
-
...errorContext !== "" ? { errorContext } : {}
|
|
3350
|
+
...errorContext !== "" ? { errorContext } : {},
|
|
3351
|
+
...failure !== void 0 ? { failure } : {}
|
|
3148
3352
|
};
|
|
3149
3353
|
}
|
|
3150
|
-
function walk(suite, describes, depth, out) {
|
|
3354
|
+
function walk(suite, describes, depth, ctx, out) {
|
|
3151
3355
|
const here = depth === 0 ? [] : [...describes, suite.title ?? ""];
|
|
3152
3356
|
for (const spec of suite.specs ?? []) {
|
|
3153
3357
|
const file = spec.file ?? suite.file ?? suite.title ?? "";
|
|
3154
3358
|
const titles = [...here, spec.title ?? ""];
|
|
3155
3359
|
for (const test of spec.tests ?? []) {
|
|
3156
|
-
for (const result of test.results ?? []) out.push(pendingFrom(spec, test, result, file, titles));
|
|
3360
|
+
for (const result of test.results ?? []) out.push(pendingFrom(spec, test, result, file, titles, ctx));
|
|
3157
3361
|
}
|
|
3158
3362
|
}
|
|
3159
|
-
for (const child of suite.suites ?? []) walk(child, here, depth + 1, out);
|
|
3363
|
+
for (const child of suite.suites ?? []) walk(child, here, depth + 1, ctx, out);
|
|
3160
3364
|
}
|
|
3161
3365
|
function readPlaywrightJson(source, file) {
|
|
3162
3366
|
let parsed;
|
|
@@ -3168,22 +3372,34 @@ function readPlaywrightJson(source, file) {
|
|
|
3168
3372
|
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.suites)) {
|
|
3169
3373
|
throw new JsonReportError(file, 'no "suites" array \u2014 is this a Playwright JSON report?');
|
|
3170
3374
|
}
|
|
3375
|
+
const { config, suites } = parsed;
|
|
3376
|
+
const rootDir = typeof config?.rootDir === "string" ? config.rootDir : void 0;
|
|
3377
|
+
const buildHref = config?.metadata?.ci?.buildHref;
|
|
3378
|
+
const repoRoot = rootDir !== void 0 && (0, import_node_fs3.existsSync)(rootDir) ? repoRootOf(rootDir) : void 0;
|
|
3379
|
+
const ctx = {
|
|
3380
|
+
...rootDir !== void 0 ? { rootDir } : {},
|
|
3381
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
3382
|
+
...typeof buildHref === "string" ? { buildHref } : {}
|
|
3383
|
+
};
|
|
3171
3384
|
const out = [];
|
|
3172
|
-
for (const suite of
|
|
3173
|
-
|
|
3385
|
+
for (const suite of suites) walk(suite, [], 0, ctx, out);
|
|
3386
|
+
const ciUrl = webLink(ctx.buildHref);
|
|
3387
|
+
return ciUrl === void 0 ? { results: out } : { results: out, ciUrl };
|
|
3174
3388
|
}
|
|
3175
3389
|
function looksLikePlaywrightJson(source) {
|
|
3176
|
-
|
|
3177
|
-
return head.trimStart().startsWith("{") && head.includes('"suites"');
|
|
3390
|
+
return /^\s*\{/.test(source) && source.includes('"suites"');
|
|
3178
3391
|
}
|
|
3179
|
-
var SOURCE2, TOKEN, JsonReportError;
|
|
3392
|
+
var import_node_fs3, SOURCE2, TOKEN, NOT_FAILED, JsonReportError;
|
|
3180
3393
|
var init_playwright_json = __esm({
|
|
3181
3394
|
"src/importers/playwright-json.ts"() {
|
|
3182
3395
|
"use strict";
|
|
3183
3396
|
init_cjs_shims();
|
|
3397
|
+
import_node_fs3 = require("fs");
|
|
3398
|
+
init_failure_detail();
|
|
3184
3399
|
init_result_key();
|
|
3185
3400
|
SOURCE2 = "playwright";
|
|
3186
3401
|
TOKEN = /@P([A-Za-z0-9_-]+)/;
|
|
3402
|
+
NOT_FAILED = /* @__PURE__ */ new Set(["passed", "skipped"]);
|
|
3187
3403
|
JsonReportError = class extends Error {
|
|
3188
3404
|
constructor(file, detail) {
|
|
3189
3405
|
super(`${file} \u2014 ${detail}`);
|
|
@@ -3214,11 +3430,9 @@ function detectFormat(source, file) {
|
|
|
3214
3430
|
);
|
|
3215
3431
|
}
|
|
3216
3432
|
function readReport(source, file, format) {
|
|
3217
|
-
const
|
|
3218
|
-
return {
|
|
3219
|
-
|
|
3220
|
-
results: chosen === "junit" ? readJUnit(source, file) : readPlaywrightJson(source, file)
|
|
3221
|
-
};
|
|
3433
|
+
const chosen2 = format ?? detectFormat(source, file);
|
|
3434
|
+
if (chosen2 === "junit") return { format: chosen2, results: readJUnit(source, file) };
|
|
3435
|
+
return { format: chosen2, ...readPlaywrightJson(source, file) };
|
|
3222
3436
|
}
|
|
3223
3437
|
var IMPORT_FORMATS, UnknownFormatError;
|
|
3224
3438
|
var init_importers = __esm({
|
|
@@ -3253,7 +3467,7 @@ async function handleRunImport(options) {
|
|
|
3253
3467
|
} catch {
|
|
3254
3468
|
throw new RunCommandError(`Cannot read ${options.file}.`);
|
|
3255
3469
|
}
|
|
3256
|
-
const { format, results } = readReport(source, options.file, options.format);
|
|
3470
|
+
const { format, results, ciUrl } = readReport(source, options.file, options.format);
|
|
3257
3471
|
if (results.length === 0) {
|
|
3258
3472
|
throw new RunCommandError(`No test cases in ${options.file} \u2014 it parsed, but there is nothing in it.`);
|
|
3259
3473
|
}
|
|
@@ -3269,7 +3483,8 @@ async function handleRunImport(options) {
|
|
|
3269
3483
|
...options.key !== void 0 ? { externalKey: options.key } : {},
|
|
3270
3484
|
...options.create === true ? { offerDiscovered: true } : {},
|
|
3271
3485
|
kind: "automated",
|
|
3272
|
-
|
|
3486
|
+
// The CI run the report names, never this machine's: whoever imports may be another job (#790).
|
|
3487
|
+
meta: { runner: format, ...ciUrl !== void 0 ? { ciUrl } : {} },
|
|
3273
3488
|
log: write
|
|
3274
3489
|
},
|
|
3275
3490
|
// The file is the complete list of what ran, so the platform can resolve every test in one
|
|
@@ -3310,7 +3525,9 @@ function describe(result, apiUrl) {
|
|
|
3310
3525
|
// «Imported 555 result(s): 0 accepted, 0 already there, 0 unmatched» was a sentence whose two
|
|
3311
3526
|
// halves disagreed, and readers stop at the verb — a run that reached nobody read as one that
|
|
3312
3527
|
// worked (#622).
|
|
3313
|
-
`
|
|
3528
|
+
// `not delivered` always, zero included (#790): a number that only appears when it is bad is one
|
|
3529
|
+
// nobody learns to look for. The `[0-9]+ unmatched` the workflows read stays whole.
|
|
3530
|
+
`Read ${result.parsed} result(s) from a ${result.format} report: ${result.accepted} accepted, ${result.duplicate} already there, ${result.unresolved} unmatched, ${result.deferred} not delivered.`
|
|
3314
3531
|
];
|
|
3315
3532
|
if (result.conflict > 0 || result.rejected > 0) {
|
|
3316
3533
|
lines.push(`${result.conflict} conflicted and ${result.rejected} were refused.`);
|
|
@@ -4029,7 +4246,7 @@ var init_sync = __esm({
|
|
|
4029
4246
|
init_credentials();
|
|
4030
4247
|
NotLoggedInError = class extends Error {
|
|
4031
4248
|
constructor() {
|
|
4032
|
-
super('Not logged in. Run "plune login" first
|
|
4249
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
4033
4250
|
this.name = "NotLoggedInError";
|
|
4034
4251
|
}
|
|
4035
4252
|
};
|
|
@@ -4306,7 +4523,7 @@ var init_ingest = __esm({
|
|
|
4306
4523
|
init_cairn_artifact();
|
|
4307
4524
|
IngestNotLoggedInError = class extends Error {
|
|
4308
4525
|
constructor() {
|
|
4309
|
-
super('Not logged in. Run "plune login" first
|
|
4526
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
4310
4527
|
this.name = "IngestNotLoggedInError";
|
|
4311
4528
|
}
|
|
4312
4529
|
};
|
|
@@ -4472,6 +4689,68 @@ var init_cases = __esm({
|
|
|
4472
4689
|
}
|
|
4473
4690
|
});
|
|
4474
4691
|
|
|
4692
|
+
// src/cli/commands/plan.ts
|
|
4693
|
+
var plan_exports = {};
|
|
4694
|
+
__export(plan_exports, {
|
|
4695
|
+
formatGrep: () => formatGrep,
|
|
4696
|
+
grepFragment: () => grepFragment,
|
|
4697
|
+
handlePlanGrep: () => handlePlanGrep
|
|
4698
|
+
});
|
|
4699
|
+
function grepFragment(c) {
|
|
4700
|
+
const pathTitle = c.externalKeys.find((k) => k.kind === "path-title")?.value;
|
|
4701
|
+
const segments = pathTitle?.split("#") ?? [c.title];
|
|
4702
|
+
return (segments.length > 1 ? segments.slice(1) : segments).flatMap((s) => s.split(/ [>›] /)).map(escape).join(BOUNDARY);
|
|
4703
|
+
}
|
|
4704
|
+
function formatGrep(cases) {
|
|
4705
|
+
if (cases.length === 0) return "(?!)";
|
|
4706
|
+
return cases.map(grepFragment).join("|");
|
|
4707
|
+
}
|
|
4708
|
+
async function safeErrorDetail4(res) {
|
|
4709
|
+
try {
|
|
4710
|
+
const body = await res.json();
|
|
4711
|
+
return typeof body.error === "string" ? body.error : "";
|
|
4712
|
+
} catch {
|
|
4713
|
+
return "";
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
async function handlePlanGrep(deps) {
|
|
4717
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4718
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
4719
|
+
if (token === null) throw new NotLoggedInError();
|
|
4720
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
4721
|
+
let res;
|
|
4722
|
+
try {
|
|
4723
|
+
res = await doFetch(`${apiUrl}/v1/plans/${encodeURIComponent(deps.planId)}/cases`, {
|
|
4724
|
+
headers: { authorization: `Bearer ${token}` }
|
|
4725
|
+
});
|
|
4726
|
+
} catch {
|
|
4727
|
+
throw new SyncNetworkError(apiUrl);
|
|
4728
|
+
}
|
|
4729
|
+
if (!res.ok) {
|
|
4730
|
+
const detail = await safeErrorDetail4(res);
|
|
4731
|
+
if (res.status === 401) throw new TokenRejectedError2();
|
|
4732
|
+
throw new SyncHttpError(res.status, detail);
|
|
4733
|
+
}
|
|
4734
|
+
const { cases } = await res.json();
|
|
4735
|
+
return {
|
|
4736
|
+
pattern: formatGrep(cases),
|
|
4737
|
+
cases: cases.length,
|
|
4738
|
+
keyed: cases.filter((c) => c.externalKeys.some((k) => k.kind === "path-title")).length
|
|
4739
|
+
};
|
|
4740
|
+
}
|
|
4741
|
+
var escape, BOUNDARY;
|
|
4742
|
+
var init_plan = __esm({
|
|
4743
|
+
"src/cli/commands/plan.ts"() {
|
|
4744
|
+
"use strict";
|
|
4745
|
+
init_cjs_shims();
|
|
4746
|
+
init_api_url();
|
|
4747
|
+
init_credentials();
|
|
4748
|
+
init_sync();
|
|
4749
|
+
escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4750
|
+
BOUNDARY = "[ >#]+";
|
|
4751
|
+
}
|
|
4752
|
+
});
|
|
4753
|
+
|
|
4475
4754
|
// src/cli.ts
|
|
4476
4755
|
var cli_exports = {};
|
|
4477
4756
|
__export(cli_exports, {
|
|
@@ -4481,11 +4760,11 @@ module.exports = __toCommonJS(cli_exports);
|
|
|
4481
4760
|
init_cjs_shims();
|
|
4482
4761
|
init_api_url();
|
|
4483
4762
|
var import_commander = require("commander");
|
|
4484
|
-
var
|
|
4485
|
-
var
|
|
4763
|
+
var import_node_fs4 = require("fs");
|
|
4764
|
+
var import_node_path3 = require("path");
|
|
4486
4765
|
var import_node_url = require("url");
|
|
4487
|
-
var __dirname = (0,
|
|
4488
|
-
var pkg = JSON.parse((0,
|
|
4766
|
+
var __dirname = (0, import_node_path3.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
4767
|
+
var pkg = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path3.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
4489
4768
|
function toFormat(s) {
|
|
4490
4769
|
return s === "json" || s === "markdown" ? s : "console";
|
|
4491
4770
|
}
|
|
@@ -4494,7 +4773,7 @@ function shouldColor(outputPath) {
|
|
|
4494
4773
|
}
|
|
4495
4774
|
function writeReport(text2, outputPath) {
|
|
4496
4775
|
if (outputPath !== void 0) {
|
|
4497
|
-
(0,
|
|
4776
|
+
(0, import_node_fs4.writeFileSync)(outputPath, text2);
|
|
4498
4777
|
} else {
|
|
4499
4778
|
process.stdout.write(text2 + "\n");
|
|
4500
4779
|
}
|
|
@@ -4560,7 +4839,7 @@ function createProgram() {
|
|
|
4560
4839
|
Promise.resolve().then(() => (init_errors(), errors_exports)),
|
|
4561
4840
|
Promise.resolve().then(() => (init_errors2(), errors_exports2))
|
|
4562
4841
|
]);
|
|
4563
|
-
loadEnv2(configPath !== void 0 ? (0,
|
|
4842
|
+
loadEnv2(configPath !== void 0 ? (0, import_node_path3.dirname)((0, import_node_path3.resolve)(configPath)) : process.cwd());
|
|
4564
4843
|
try {
|
|
4565
4844
|
const result = await handleRun2({
|
|
4566
4845
|
dryRun: options.dryRun,
|
|
@@ -4691,7 +4970,8 @@ function createProgram() {
|
|
|
4691
4970
|
Promise.resolve().then(() => (init_run_import(), run_import_exports)),
|
|
4692
4971
|
Promise.resolve().then(() => (init_importers(), importers_exports))
|
|
4693
4972
|
]);
|
|
4694
|
-
const
|
|
4973
|
+
const run = command.parent;
|
|
4974
|
+
const format = options.format ?? (run?.getOptionValueSource("format") === "cli" ? run.opts().format : void 0);
|
|
4695
4975
|
if (format !== void 0 && !IMPORT_FORMATS2.includes(format)) {
|
|
4696
4976
|
process.stderr.write(
|
|
4697
4977
|
`Unknown --format "${format}". Use ${IMPORT_FORMATS2.join(" or ")}.
|
|
@@ -4724,7 +5004,7 @@ function createProgram() {
|
|
|
4724
5004
|
Promise.resolve().then(() => (init_report(), report_exports)),
|
|
4725
5005
|
Promise.resolve().then(() => (init_reporters(), reporters_exports))
|
|
4726
5006
|
]);
|
|
4727
|
-
const cwd = globals.config !== void 0 ? (0,
|
|
5007
|
+
const cwd = globals.config !== void 0 ? (0, import_node_path3.dirname)((0, import_node_path3.resolve)(globals.config)) : process.cwd();
|
|
4728
5008
|
loadEnv2(cwd);
|
|
4729
5009
|
try {
|
|
4730
5010
|
const result = handleReport2({ cwd });
|
|
@@ -4822,7 +5102,7 @@ function createProgram() {
|
|
|
4822
5102
|
const globals = command.optsWithGlobals();
|
|
4823
5103
|
const verbose = globals.verbose === true;
|
|
4824
5104
|
const { handleSync: handleSync2, reportSyncFailure: reportSyncFailure2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4825
|
-
const cwd = globals.config !== void 0 ? (0,
|
|
5105
|
+
const cwd = globals.config !== void 0 ? (0, import_node_path3.dirname)((0, import_node_path3.resolve)(globals.config)) : process.cwd();
|
|
4826
5106
|
try {
|
|
4827
5107
|
const { id, url } = await handleSync2({
|
|
4828
5108
|
cwd,
|
|
@@ -4905,6 +5185,30 @@ function createProgram() {
|
|
|
4905
5185
|
failUnexpected(err, verbose);
|
|
4906
5186
|
}
|
|
4907
5187
|
});
|
|
5188
|
+
const planCommand = program.command("plan").description("Test plans on the platform \u2014 what a plan collects, as something a runner can take");
|
|
5189
|
+
planCommand.command("grep").argument("<id>", "The plan").description(
|
|
5190
|
+
"Print the plan's cases as one --grep pattern for the runner (GET /v1/plans/:id/cases)"
|
|
5191
|
+
).action(async (id, _options, command) => {
|
|
5192
|
+
const verbose = command.optsWithGlobals().verbose === true;
|
|
5193
|
+
const { handlePlanGrep: handlePlanGrep2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
5194
|
+
const { reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
|
|
5195
|
+
try {
|
|
5196
|
+
const out = await handlePlanGrep2({ planId: id });
|
|
5197
|
+
process.stdout.write(out.pattern + "\n");
|
|
5198
|
+
process.stderr.write(
|
|
5199
|
+
out.cases === 0 ? "plune: the plan matches no case \u2014 the pattern matches nothing.\n" : `plune: ${out.cases} case(s) in the plan, ${out.keyed} with a test key; the rest by title.
|
|
5200
|
+
`
|
|
5201
|
+
);
|
|
5202
|
+
} catch (err) {
|
|
5203
|
+
const code = reportCasesFailure2(err, (s) => process.stderr.write(s));
|
|
5204
|
+
if (code !== null) {
|
|
5205
|
+
maybeStack(err, verbose);
|
|
5206
|
+
process.exitCode = code;
|
|
5207
|
+
return;
|
|
5208
|
+
}
|
|
5209
|
+
failUnexpected(err, verbose);
|
|
5210
|
+
}
|
|
5211
|
+
});
|
|
4908
5212
|
program.on("command:*", (operands) => {
|
|
4909
5213
|
process.stderr.write(`Unknown command: ${operands.join(" ")}
|
|
4910
5214
|
`);
|
|
@@ -4916,7 +5220,7 @@ function isMainModule() {
|
|
|
4916
5220
|
const entry = process.argv[1];
|
|
4917
5221
|
if (entry === void 0) return false;
|
|
4918
5222
|
try {
|
|
4919
|
-
return (0,
|
|
5223
|
+
return (0, import_node_fs4.realpathSync)(entry) === (0, import_node_fs4.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
4920
5224
|
} catch {
|
|
4921
5225
|
return false;
|
|
4922
5226
|
}
|