@plune-ai/cli 0.31.0 → 0.32.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 +39 -9
- package/dist/cli.cjs +108 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ the last resort, used only when `PLUNE_TOKEN` is set nowhere else. Nothing write
|
|
|
122
122
|
| `plune login` | Save a [Plune platform](https://plune.ai/platform) API token so `sync` and `ingest` can reach it. The token is **checked against the API before it is saved**, so a wrong one fails here rather than two commands later. Get one at `https://beta.plune.ai` → Account → API tokens. Flags: `--token <token>` (omit to paste it or pipe it via stdin), `--skip-verify` (save without checking, for offline setup). In CI, skip this step: every platform command reads `PLUNE_TOKEN` from the environment first, and the saved login only when it is not set. |
|
|
123
123
|
| `plune logout` | Remove the saved token. |
|
|
124
124
|
| `plune sync` | Upload the latest local run to the platform. Flags: `--file <path>` to send a specific run JSON, `--fail-on-failures` to read the run's summary back once after the upload and exit with [the gate](#a-release-gate---fail-on-failures)'s code. Each upload carries an `Idempotency-Key` (SHA-256 of the body), so sending the same run again returns the same run rather than creating a second one. Against a platform with `POST /v1/jobs` the upload is also reported as a job (kind `sync`, counting the run's rows) that ends completed, or failed with the error text if the sync fails; an older platform without the route is not told, and a job report never fails the sync: when the sync goes through, one the platform did not take is said in one line on stderr. |
|
|
125
|
-
| `plune run import <file>` | Turn a **JUnit XML** or **Playwright JSON** report, or an **Allure `allure-results` folder**, into a run in Plune. Needs no provider key — nothing is generated. Flags: `--format junit\|playwright-json\|allure` (detected
|
|
125
|
+
| `plune run import <file \| folder>` | Turn a **JUnit XML** or **Playwright JSON** report, a **folder of JUnit XML files** (Maven Surefire's `target/surefire-reports`, Gradle's `build/test-results/test`), or an **Allure `allure-results` folder**, into a run in Plune. Needs no provider key — nothing is generated. Flags: `--format junit\|playwright-json\|allure` (detected when omitted: a folder with a `*-result.json` is Allure, any other folder is read as the JUnit `*.xml` files in it), `--name <text>` to call the run something other than the [folder-based default](#what-the-run-is-called), `--key <externalKey>` to land several reports in one run (with `PLUNE_SHARED_RUN=1` — see below), `--create` to offer unmatched tests to the review queue, `--fail-on-missing-attachments` to exit 1 when a file a JUnit report names with `[[ATTACHMENT\|path]]`, or an Allure result names as a `source`, is not there (the results are sent either way), `--upload-concurrency <n>` for files uploaded at once ([1 to 32, default 16](#many-screenshots-upload-concurrency-and-retries)), `--fail-on-failures` to exit with [the gate](#a-release-gate---fail-on-failures)'s code on the summary the platform froze when this import finished the run. |
|
|
126
126
|
| `plune run start` | Open a platform run — or join the one already carrying `--key` — and print its id. Flags: `--key <externalKey>` (generated when absent), `--name <text>` to call the run something other than the [default](#what-the-run-is-called), `--json` for one machine-readable line. |
|
|
127
127
|
| `plune run finish <id>` | Close a platform run. This is what the reporter tells you to do for a run it had to leave open. Flags: `--terminate` to record it as cut short, `--reason <text>`, `--fail-on-failures` to exit with [the gate](#a-release-gate---fail-on-failures)'s code on the summary the platform froze. |
|
|
128
128
|
| `plune run exec -- <command>` | Open a run, run the command inside it, close the run — and exit with whatever the command returned. Sets `PLUNE_SHARED_RUN` for you, so anything reporting inside joins that run. Flags: `--key <externalKey>`, `--name <text>` (as on `run start`), `--fail-on-failures` to exit with [the gate](#a-release-gate---fail-on-failures)'s code instead — or with the command's own when no result reached the run. |
|
|
@@ -239,6 +239,7 @@ costs nothing beyond a Plune token.
|
|
|
239
239
|
```bash
|
|
240
240
|
plune login # once, on your machine; in CI export PLUNE_TOKEN instead
|
|
241
241
|
plune run import ./junit.xml # Jest, Vitest, pytest, PHPUnit, Surefire, Cypress, …
|
|
242
|
+
plune run import target/surefire-reports # or a folder of JUnit files: Maven, Gradle — see Java below
|
|
242
243
|
plune run import ./playwright.json # or Playwright's own JSON report
|
|
243
244
|
plune run import ./allure-results # or the folder an Allure adapter wrote — see below
|
|
244
245
|
```
|
|
@@ -301,13 +302,15 @@ pytest --junitxml=report.xml -o junit_logging=system-out
|
|
|
301
302
|
plune run import report.xml
|
|
302
303
|
```
|
|
303
304
|
|
|
304
|
-
Java, JUnit 5 (Maven Surefire writes the test's output into its testcase
|
|
305
|
+
Java, JUnit 5 (Maven Surefire writes the test's output into its testcase; Gradle only with
|
|
306
|
+
`reports.junitXml.outputPerTestCase = true`). The path counts from the folder the XML file is in
|
|
307
|
+
(`target/surefire-reports`, `build/test-results/test`), so the file has to be below it:
|
|
305
308
|
|
|
306
309
|
```java
|
|
307
310
|
@Test
|
|
308
311
|
void checkout() {
|
|
309
|
-
// ...
|
|
310
|
-
System.out.println("[[ATTACHMENT|
|
|
312
|
+
// ... save the screenshot as target/surefire-reports/artifacts/checkout.png
|
|
313
|
+
System.out.println("[[ATTACHMENT|artifacts/checkout.png]]");
|
|
311
314
|
}
|
|
312
315
|
```
|
|
313
316
|
|
|
@@ -330,7 +333,7 @@ For Jest, use Allure below.
|
|
|
330
333
|
|
|
331
334
|
Every Allure adapter (pytest, JUnit 5, TestNG, Playwright, Cypress, Jest, Vitest, WebdriverIO, .NET) writes
|
|
332
335
|
the same folder: a `*-result.json` per test, a `*-container.json` per suite, and the files the tests attached.
|
|
333
|
-
Point `plune run import` at the folder (a folder is read as Allure, no flag needed). Each test arrives with its
|
|
336
|
+
Point `plune run import` at the folder (a folder with a `*-result.json` in it is read as Allure, no flag needed). Each test arrives with its
|
|
334
337
|
name, its status as Allure shows it (`passed`, `failed`, `broken`, `skipped`), its time, the failure message and
|
|
335
338
|
trace, and its steps; a test run again (same `historyId`) is one test with attempts, the last one counting.
|
|
336
339
|
The files come from the test, from its steps and from its fixtures (`befores`, `afters`), each with the
|
|
@@ -354,11 +357,36 @@ def test_checkout(page):
|
|
|
354
357
|
allure.attach.file("artifacts/checkout.png", name="checkout", attachment_type=allure.attachment_type.PNG)
|
|
355
358
|
```
|
|
356
359
|
|
|
357
|
-
Java
|
|
358
|
-
|
|
360
|
+
Java. Maven Surefire and Gradle write JUnit XML on their own, a `TEST-<class>.xml` per class, so a Java suite
|
|
361
|
+
needs no Allure: point the import at the folder.
|
|
359
362
|
|
|
360
363
|
```bash
|
|
361
364
|
mvn test
|
|
365
|
+
plune run import target/surefire-reports
|
|
366
|
+
|
|
367
|
+
./gradlew test
|
|
368
|
+
plune run import build/test-results/test
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Every `*.xml` in the folder that is a JUnit report is read, in the order of the file names, and together they make
|
|
372
|
+
one run. The folder's other files and its subfolders (Gradle's `binary/`) are not opened; an `*.xml` that is not a
|
|
373
|
+
JUnit report is skipped with a line in the output. Each test arrives with its class and name as the key
|
|
374
|
+
(`cart.CartTest#addsAnItem` from Surefire; Gradle keeps the brackets, `cart.CartTest#addsAnItem()`), its status
|
|
375
|
+
(`pass`, `failure`, `error`, `skipped`), the failure's message, type and stack trace, and its duration when the suite
|
|
376
|
+
has a `timestamp`. Gradle writes one; Surefire's report has none, so Maven tests arrive without a duration for now
|
|
377
|
+
(plune-ai/plune#2310). A file reaches a test only
|
|
378
|
+
through a `[[ATTACHMENT|path]]` line in its own output, [as above](#files-from-a-junit-report), the path counted from
|
|
379
|
+
the folder the XML file is in. Gradle puts that output in the test's testcase only with
|
|
380
|
+
`reports.junitXml.outputPerTestCase = true` (by default it writes it once per suite).
|
|
381
|
+
|
|
382
|
+
Allure adds the steps, and files attached without printing a line. The JUnit 5 adapter is
|
|
383
|
+
`io.qameta.allure:allure-jupiter` (from 2.35.1, `allure-junit5` is only a relocation to it, with no jar), with the
|
|
384
|
+
AspectJ agent from Allure's setup. It writes `allure-results` in the working folder; to have it in the build's
|
|
385
|
+
output, set `allure.results.directory` (in `src/test/resources/allure.properties`, or as a system property) to
|
|
386
|
+
`target/allure-results` for Maven or `build/allure-results` for Gradle:
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
mvn test # with allure.results.directory=target/allure-results
|
|
362
390
|
plune run import target/allure-results
|
|
363
391
|
```
|
|
364
392
|
|
|
@@ -372,11 +400,13 @@ void checkout() {
|
|
|
372
400
|
Jest has no per-test output in JUnit; `allure-jest` gives it one (`allure.attachment(...)` in a test or a step).
|
|
373
401
|
Tests are matched to Plune cases by the same key as a JUnit report's, `classname#name`, where the adapter's
|
|
374
402
|
names allow it (pytest, JUnit 5, Playwright); for Vitest, Jest and Cypress the key is the runner, the file and the
|
|
375
|
-
test's full name (`vt:test/cart.test.js#cart adds an item`, `jest:…`, `cy:…`).
|
|
403
|
+
test's full name (`vt:test/cart.test.js#cart adds an item`, `jest:…`, `cy:…`). For JUnit 5 the name carries the
|
|
404
|
+
method's parentheses in Allure and in Gradle's JUnit XML (`shop.CartTest#coupon()`) but not in Surefire's
|
|
405
|
+
(`shop.CartTest#coupon`), so a project that moves between Allure and Surefire meets its cases under other keys. A file whose name starts with `_`
|
|
376
406
|
is not sent: the core hides it as Playwright's own reports do, and Allure has no such convention, so name
|
|
377
407
|
attachments without a leading underscore.
|
|
378
408
|
|
|
379
|
-
|
|
409
|
+
For a JUnit report, check that the output reaches the report: Gradle writes it only with `reports.junitXml.outputPerTestCase = true`,
|
|
380
410
|
and Maven Surefire moves it out of the XML when `redirectTestOutputToFile` is `true`. As in the Playwright
|
|
381
411
|
reporter, a file whose name starts with an underscore is skipped, and a file several tests name is uploaded once per test.
|
|
382
412
|
|
package/dist/cli.cjs
CHANGED
|
@@ -2565,7 +2565,7 @@ function createClient(opts) {
|
|
|
2565
2565
|
}
|
|
2566
2566
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
2567
2567
|
const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
|
|
2568
|
-
async function send(method,
|
|
2568
|
+
async function send(method, path19, payload, retries = DEFAULT_RETRIES) {
|
|
2569
2569
|
let last = {
|
|
2570
2570
|
kind: "unavailable",
|
|
2571
2571
|
status: null,
|
|
@@ -2576,7 +2576,7 @@ function createClient(opts) {
|
|
|
2576
2576
|
await respectPause(mine);
|
|
2577
2577
|
let res;
|
|
2578
2578
|
try {
|
|
2579
|
-
res = await doFetch(`${base}${
|
|
2579
|
+
res = await doFetch(`${base}${path19}`, {
|
|
2580
2580
|
method,
|
|
2581
2581
|
headers: { "content-type": payload?.type ?? "application/json", authorization: `Bearer ${opts.token}` },
|
|
2582
2582
|
// A DELETE carries none. Sending `"undefined"` as a body is what a naive
|
|
@@ -2615,10 +2615,10 @@ function createClient(opts) {
|
|
|
2615
2615
|
return { ok: false, ...last };
|
|
2616
2616
|
}
|
|
2617
2617
|
return {
|
|
2618
|
-
post: (
|
|
2619
|
-
upload: (
|
|
2620
|
-
del: (
|
|
2621
|
-
get: (
|
|
2618
|
+
post: (path19, body, retries) => send("POST", path19, { body: JSON.stringify(body), type: "application/json" }, retries),
|
|
2619
|
+
upload: (path19, bytes, contentType) => send("POST", path19, { body: bytes, type: contentType }, uploadRetries ?? DEFAULT_RETRIES),
|
|
2620
|
+
del: (path19) => send("DELETE", path19),
|
|
2621
|
+
get: (path19) => send("GET", path19),
|
|
2622
2622
|
async put(url, headers, source) {
|
|
2623
2623
|
const putRetries = uploadRetries ?? 3;
|
|
2624
2624
|
let last = { ok: false, status: null, code: "network" };
|
|
@@ -2899,12 +2899,12 @@ function machineless(raw, repoRoot, home) {
|
|
|
2899
2899
|
if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
|
|
2900
2900
|
return text2;
|
|
2901
2901
|
}
|
|
2902
|
-
function pathIn(
|
|
2903
|
-
const parts =
|
|
2902
|
+
function pathIn(path19) {
|
|
2903
|
+
const parts = path19.split(/[\\/]+/);
|
|
2904
2904
|
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
2905
2905
|
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
2906
2906
|
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
2907
|
-
const escaped = /^\/(?!\/)/.test(
|
|
2907
|
+
const escaped = /^\/(?!\/)/.test(path19) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
2908
2908
|
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
2909
2909
|
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
2910
2910
|
}
|
|
@@ -2950,12 +2950,12 @@ function placeOf(error, testFile) {
|
|
|
2950
2950
|
}
|
|
2951
2951
|
function fileOf(raw) {
|
|
2952
2952
|
if (!raw.startsWith("file://")) return raw;
|
|
2953
|
-
let
|
|
2953
|
+
let path19 = raw.slice("file://".length);
|
|
2954
2954
|
try {
|
|
2955
|
-
|
|
2955
|
+
path19 = decodeURIComponent(path19);
|
|
2956
2956
|
} catch {
|
|
2957
2957
|
}
|
|
2958
|
-
return /^\/[A-Za-z]:\//.test(
|
|
2958
|
+
return /^\/[A-Za-z]:\//.test(path19) ? path19.slice(1) : path19;
|
|
2959
2959
|
}
|
|
2960
2960
|
function fromRoot(place, root) {
|
|
2961
2961
|
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
@@ -2995,14 +2995,14 @@ var init_failure_detail = __esm({
|
|
|
2995
2995
|
FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
2996
2996
|
FRAME_MAX = 8192;
|
|
2997
2997
|
firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
2998
|
-
normal = (
|
|
2998
|
+
normal = (path19) => import_node_path2.posix.normalize(path19.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
2999
2999
|
}
|
|
3000
3000
|
});
|
|
3001
3001
|
|
|
3002
3002
|
// src/reporter-core/files-batch.ts
|
|
3003
|
-
async function sha256File(
|
|
3003
|
+
async function sha256File(path19) {
|
|
3004
3004
|
const hash = (0, import_node_crypto2.createHash)("sha256");
|
|
3005
|
-
for await (const part of (0, import_node_fs4.createReadStream)(
|
|
3005
|
+
for await (const part of (0, import_node_fs4.createReadStream)(path19)) hash.update(part);
|
|
3006
3006
|
return hash.digest("hex");
|
|
3007
3007
|
}
|
|
3008
3008
|
var import_node_crypto2, import_node_fs4, FILES_BATCH_MAX, DIRECT_BATCH_BYTES, BATCH_PIPELINES, sha256Bytes, BUCKET_CODES;
|
|
@@ -4632,10 +4632,10 @@ function platformGet(deps) {
|
|
|
4632
4632
|
const token = (deps.loadToken ?? loadToken)();
|
|
4633
4633
|
if (token === null) throw new NotLoggedInError();
|
|
4634
4634
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
4635
|
-
return async (
|
|
4635
|
+
return async (path19) => {
|
|
4636
4636
|
let res;
|
|
4637
4637
|
try {
|
|
4638
|
-
res = await doFetch(`${apiUrl}${
|
|
4638
|
+
res = await doFetch(`${apiUrl}${path19}`, { headers: { authorization: `Bearer ${token}` } });
|
|
4639
4639
|
} catch {
|
|
4640
4640
|
throw new SyncNetworkError(apiUrl);
|
|
4641
4641
|
}
|
|
@@ -4976,9 +4976,9 @@ var init_attachments = __esm({
|
|
|
4976
4976
|
});
|
|
4977
4977
|
|
|
4978
4978
|
// src/importers/allure.ts
|
|
4979
|
-
function isFolder(
|
|
4979
|
+
function isFolder(path19) {
|
|
4980
4980
|
try {
|
|
4981
|
-
return fs11.statSync(
|
|
4981
|
+
return fs11.statSync(path19).isDirectory();
|
|
4982
4982
|
} catch {
|
|
4983
4983
|
return false;
|
|
4984
4984
|
}
|
|
@@ -5345,7 +5345,7 @@ var init_xml = __esm({
|
|
|
5345
5345
|
init_failure_detail();
|
|
5346
5346
|
XmlParseError = class extends Error {
|
|
5347
5347
|
constructor(file, line, detail) {
|
|
5348
|
-
super(`${file}:${line} \u2014 ${detail}`);
|
|
5348
|
+
super(`${printable(file)}:${line} \u2014 ${detail}`);
|
|
5349
5349
|
this.file = file;
|
|
5350
5350
|
this.line = line;
|
|
5351
5351
|
this.name = "XmlParseError";
|
|
@@ -5486,10 +5486,10 @@ function attemptOf(result, file, ctx) {
|
|
|
5486
5486
|
...typeof at?.file === "string" && typeof at.line === "number" ? { location: { file: at.file, line: at.line, ...typeof at.column === "number" ? { column: at.column } : {} } } : {}
|
|
5487
5487
|
})),
|
|
5488
5488
|
steps: steps(result.steps),
|
|
5489
|
-
attachments: (result.attachments ?? []).map(({ name, contentType, path:
|
|
5489
|
+
attachments: (result.attachments ?? []).map(({ name, contentType, path: path19, body }) => ({
|
|
5490
5490
|
name: name ?? "",
|
|
5491
5491
|
...contentType !== void 0 ? { contentType } : {},
|
|
5492
|
-
...
|
|
5492
|
+
...path19 !== void 0 ? { path: path19 } : {},
|
|
5493
5493
|
// Decoded only where the platform would take it (plune-ai/plune#928): a report of screenshots kept as
|
|
5494
5494
|
// bodies is megabytes of base64 that nothing here will read.
|
|
5495
5495
|
...typeof body === "string" && isTextType(contentType) ? { body: Buffer.from(body, "base64") } : {}
|
|
@@ -5628,12 +5628,40 @@ function detectFormat(source, file) {
|
|
|
5628
5628
|
);
|
|
5629
5629
|
}
|
|
5630
5630
|
function readFolder(dir, format) {
|
|
5631
|
-
if (format
|
|
5632
|
-
throw new UnknownFormatError(dir,
|
|
5631
|
+
if (format === "playwright-json") {
|
|
5632
|
+
throw new UnknownFormatError(dir, "this is a folder, and only allure-results or JUnit XML files are read from one, not playwright-json.");
|
|
5633
5633
|
}
|
|
5634
5634
|
const missing = [];
|
|
5635
5635
|
const notes = [];
|
|
5636
|
-
|
|
5636
|
+
if (format === "allure" || format === void 0 && namesIn(dir, RESULT).length > 0) {
|
|
5637
|
+
return { format: "allure", results: readAllure(dir, missing, notes), missing, notes };
|
|
5638
|
+
}
|
|
5639
|
+
const results = [];
|
|
5640
|
+
const skipped = [];
|
|
5641
|
+
let reports = 0;
|
|
5642
|
+
for (const name of namesIn(dir, /\.xml$/)) {
|
|
5643
|
+
const file = path13.join(dir, name);
|
|
5644
|
+
let source;
|
|
5645
|
+
try {
|
|
5646
|
+
source = fs12.readFileSync(file, "utf8");
|
|
5647
|
+
} catch {
|
|
5648
|
+
throw new RunCommandError(`Cannot read ${printable(file)}.`);
|
|
5649
|
+
}
|
|
5650
|
+
if (!looksLikeJUnit(source)) {
|
|
5651
|
+
skipped.push(name);
|
|
5652
|
+
notes.push(`plune: ${printable(file)} is not a JUnit report \u2014 skipped.`);
|
|
5653
|
+
continue;
|
|
5654
|
+
}
|
|
5655
|
+
reports += 1;
|
|
5656
|
+
for (const r of readJUnit(source, file, missing, notes)) results.push(r);
|
|
5657
|
+
}
|
|
5658
|
+
if (reports === 0) {
|
|
5659
|
+
const names = skipped.slice(0, 3).map(printable).join(", ") + (skipped.length > 3 ? ` and ${skipped.length - 3} more` : "");
|
|
5660
|
+
throw new AllureReportError(
|
|
5661
|
+
`${printable(dir)} \u2014 no ${format === void 0 ? "*-result.json files and no " : ""}JUnit XML files in it${skipped.length > 0 ? ` (skipped, not JUnit: ${names})` : ""}, so there is no report to read. Point at the folder an Allure adapter wrote (pytest --alluredir, allure-results), the one Maven or Gradle wrote (target/surefire-reports, build/test-results/test), or at a JUnit or Playwright JSON file.`
|
|
5662
|
+
);
|
|
5663
|
+
}
|
|
5664
|
+
return { format: "junit", results, missing, notes };
|
|
5637
5665
|
}
|
|
5638
5666
|
function readReport(source, file, format) {
|
|
5639
5667
|
const chosen2 = format ?? detectFormat(source, file);
|
|
@@ -5643,11 +5671,15 @@ function readReport(source, file, format) {
|
|
|
5643
5671
|
if (chosen2 === "junit") return { format: chosen2, results: readJUnit(source, file, missing, notes), missing, notes };
|
|
5644
5672
|
return { format: chosen2, ...readPlaywrightJson(source, file), missing, notes };
|
|
5645
5673
|
}
|
|
5646
|
-
var IMPORT_FORMATS, UnknownFormatError;
|
|
5674
|
+
var fs12, path13, IMPORT_FORMATS, UnknownFormatError;
|
|
5647
5675
|
var init_importers = __esm({
|
|
5648
5676
|
"src/importers/index.ts"() {
|
|
5649
5677
|
"use strict";
|
|
5650
5678
|
init_cjs_shims();
|
|
5679
|
+
fs12 = __toESM(require("fs"), 1);
|
|
5680
|
+
path13 = __toESM(require("path"), 1);
|
|
5681
|
+
init_run_lifecycle();
|
|
5682
|
+
init_failure_detail();
|
|
5651
5683
|
init_allure();
|
|
5652
5684
|
init_junit();
|
|
5653
5685
|
init_playwright_json();
|
|
@@ -5694,9 +5726,9 @@ async function handleRunImport(options) {
|
|
|
5694
5726
|
} else {
|
|
5695
5727
|
let source;
|
|
5696
5728
|
try {
|
|
5697
|
-
source =
|
|
5729
|
+
source = fs13.readFileSync(options.file, "utf8");
|
|
5698
5730
|
} catch {
|
|
5699
|
-
throw new RunCommandError(`Cannot read ${options.file}.`);
|
|
5731
|
+
throw new RunCommandError(`Cannot read ${printable(options.file)}.`);
|
|
5700
5732
|
}
|
|
5701
5733
|
read = readReport(source, options.file, options.format);
|
|
5702
5734
|
}
|
|
@@ -5847,12 +5879,12 @@ function describe(result, apiUrl) {
|
|
|
5847
5879
|
if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
|
|
5848
5880
|
return lines;
|
|
5849
5881
|
}
|
|
5850
|
-
var
|
|
5882
|
+
var fs13, ImportTimeoutError, NEXT;
|
|
5851
5883
|
var init_run_import = __esm({
|
|
5852
5884
|
"src/cli/commands/run-import.ts"() {
|
|
5853
5885
|
"use strict";
|
|
5854
5886
|
init_cjs_shims();
|
|
5855
|
-
|
|
5887
|
+
fs13 = __toESM(require("fs"), 1);
|
|
5856
5888
|
init_session();
|
|
5857
5889
|
init_failure_detail();
|
|
5858
5890
|
init_importers();
|
|
@@ -5884,10 +5916,10 @@ function isRunResult(v) {
|
|
|
5884
5916
|
}
|
|
5885
5917
|
function handleReport(opts = {}) {
|
|
5886
5918
|
const cwd = opts.cwd ?? process.cwd();
|
|
5887
|
-
const file =
|
|
5919
|
+
const file = path14.join(cwd, ".plune", "last-run.json");
|
|
5888
5920
|
let raw;
|
|
5889
5921
|
try {
|
|
5890
|
-
raw =
|
|
5922
|
+
raw = fs14.readFileSync(file, "utf8");
|
|
5891
5923
|
} catch {
|
|
5892
5924
|
throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
|
|
5893
5925
|
}
|
|
@@ -5902,13 +5934,13 @@ function handleReport(opts = {}) {
|
|
|
5902
5934
|
}
|
|
5903
5935
|
return parsed;
|
|
5904
5936
|
}
|
|
5905
|
-
var
|
|
5937
|
+
var fs14, path14, ReportNotFoundError;
|
|
5906
5938
|
var init_report = __esm({
|
|
5907
5939
|
"src/cli/commands/report.ts"() {
|
|
5908
5940
|
"use strict";
|
|
5909
5941
|
init_cjs_shims();
|
|
5910
|
-
|
|
5911
|
-
|
|
5942
|
+
fs14 = __toESM(require("fs"), 1);
|
|
5943
|
+
path14 = __toESM(require("path"), 1);
|
|
5912
5944
|
ReportNotFoundError = class extends Error {
|
|
5913
5945
|
code = "NO_SAVED_RUN";
|
|
5914
5946
|
constructor(message) {
|
|
@@ -5978,7 +6010,7 @@ function isRunResult2(v) {
|
|
|
5978
6010
|
function loadRunResult(file, label) {
|
|
5979
6011
|
let raw;
|
|
5980
6012
|
try {
|
|
5981
|
-
raw =
|
|
6013
|
+
raw = fs15.readFileSync(file, "utf8");
|
|
5982
6014
|
} catch {
|
|
5983
6015
|
throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
|
|
5984
6016
|
}
|
|
@@ -5998,12 +6030,12 @@ function handleDiff(opts) {
|
|
|
5998
6030
|
const current = loadRunResult(opts.currentPath, "current");
|
|
5999
6031
|
return diffRuns(baseline, current);
|
|
6000
6032
|
}
|
|
6001
|
-
var
|
|
6033
|
+
var fs15, DiffInputError;
|
|
6002
6034
|
var init_diff2 = __esm({
|
|
6003
6035
|
"src/cli/commands/diff.ts"() {
|
|
6004
6036
|
"use strict";
|
|
6005
6037
|
init_cjs_shims();
|
|
6006
|
-
|
|
6038
|
+
fs15 = __toESM(require("fs"), 1);
|
|
6007
6039
|
init_diff();
|
|
6008
6040
|
DiffInputError = class extends Error {
|
|
6009
6041
|
code = "DIFF_INPUT";
|
|
@@ -6128,8 +6160,8 @@ async function runInitWizard(cwd) {
|
|
|
6128
6160
|
process.exit(1);
|
|
6129
6161
|
return;
|
|
6130
6162
|
}
|
|
6131
|
-
const outputPath =
|
|
6132
|
-
if (
|
|
6163
|
+
const outputPath = path15.join(cwd, "plune.yaml");
|
|
6164
|
+
if (fs16.existsSync(outputPath)) {
|
|
6133
6165
|
const overwrite = await clack.confirm({
|
|
6134
6166
|
message: "plune.yaml already exists. Overwrite?"
|
|
6135
6167
|
});
|
|
@@ -6168,18 +6200,18 @@ async function runInitWizard(cwd) {
|
|
|
6168
6200
|
`;
|
|
6169
6201
|
const spin = clack.spinner();
|
|
6170
6202
|
spin.start("Writing plune.yaml\u2026");
|
|
6171
|
-
|
|
6203
|
+
fs16.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
|
|
6172
6204
|
spin.stop("plune.yaml created!");
|
|
6173
6205
|
clack.outro("plune.yaml is ready.");
|
|
6174
6206
|
}
|
|
6175
|
-
var clack,
|
|
6207
|
+
var clack, fs16, path15, import_yaml2;
|
|
6176
6208
|
var init_wizard = __esm({
|
|
6177
6209
|
"src/config/init/wizard.ts"() {
|
|
6178
6210
|
"use strict";
|
|
6179
6211
|
init_cjs_shims();
|
|
6180
6212
|
clack = __toESM(require("@clack/prompts"), 1);
|
|
6181
|
-
|
|
6182
|
-
|
|
6213
|
+
fs16 = __toESM(require("fs"), 1);
|
|
6214
|
+
path15 = __toESM(require("path"), 1);
|
|
6183
6215
|
import_yaml2 = require("yaml");
|
|
6184
6216
|
init_errors();
|
|
6185
6217
|
}
|
|
@@ -6187,21 +6219,21 @@ var init_wizard = __esm({
|
|
|
6187
6219
|
|
|
6188
6220
|
// src/cli/templates/index.ts
|
|
6189
6221
|
function writeTemplateFile(cwd, rel, content, force) {
|
|
6190
|
-
const target =
|
|
6191
|
-
if (!force &&
|
|
6222
|
+
const target = path16.join(cwd, rel);
|
|
6223
|
+
if (!force && fs17.existsSync(target)) {
|
|
6192
6224
|
return "skipped";
|
|
6193
6225
|
}
|
|
6194
|
-
|
|
6195
|
-
|
|
6226
|
+
fs17.mkdirSync(path16.dirname(target), { recursive: true });
|
|
6227
|
+
fs17.writeFileSync(target, content, "utf8");
|
|
6196
6228
|
return "written";
|
|
6197
6229
|
}
|
|
6198
|
-
var
|
|
6230
|
+
var fs17, path16, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
|
|
6199
6231
|
var init_templates = __esm({
|
|
6200
6232
|
"src/cli/templates/index.ts"() {
|
|
6201
6233
|
"use strict";
|
|
6202
6234
|
init_cjs_shims();
|
|
6203
|
-
|
|
6204
|
-
|
|
6235
|
+
fs17 = __toESM(require("fs"), 1);
|
|
6236
|
+
path16 = __toESM(require("path"), 1);
|
|
6205
6237
|
ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
|
|
6206
6238
|
# .env is gitignored; .env.example is safe to commit.
|
|
6207
6239
|
# Plune reads whichever key matches your configured provider.
|
|
@@ -6381,14 +6413,14 @@ var init_logout = __esm({
|
|
|
6381
6413
|
|
|
6382
6414
|
// src/cli/commands/cairn-artifact.ts
|
|
6383
6415
|
async function findLatestCairnRun(runsDir = "runs") {
|
|
6384
|
-
const entries = await
|
|
6416
|
+
const entries = await fs18.readdir(runsDir, { withFileTypes: true }).catch(() => null);
|
|
6385
6417
|
if (entries === null) throw new NoCairnRunsError(runsDir);
|
|
6386
6418
|
let newest;
|
|
6387
6419
|
for (const entry of entries) {
|
|
6388
6420
|
if (!entry.isDirectory()) continue;
|
|
6389
|
-
const dir =
|
|
6421
|
+
const dir = path17.join(runsDir, entry.name);
|
|
6390
6422
|
try {
|
|
6391
|
-
const report = await
|
|
6423
|
+
const report = await fs18.lstat(path17.join(dir, "report.json"));
|
|
6392
6424
|
if (!report.isFile()) continue;
|
|
6393
6425
|
if (newest === void 0 || report.mtimeMs > newest.at) {
|
|
6394
6426
|
newest = { dir, at: report.mtimeMs };
|
|
@@ -6401,13 +6433,13 @@ async function findLatestCairnRun(runsDir = "runs") {
|
|
|
6401
6433
|
}
|
|
6402
6434
|
async function readJson(file) {
|
|
6403
6435
|
try {
|
|
6404
|
-
return JSON.parse(await
|
|
6436
|
+
return JSON.parse(await fs18.readFile(file, "utf8"));
|
|
6405
6437
|
} catch {
|
|
6406
6438
|
return void 0;
|
|
6407
6439
|
}
|
|
6408
6440
|
}
|
|
6409
6441
|
async function defaultReadEvidence(dir) {
|
|
6410
|
-
const raw = await readJson(
|
|
6442
|
+
const raw = await readJson(path17.join(dir, "api-evidence.json"));
|
|
6411
6443
|
if (!Array.isArray(raw)) return void 0;
|
|
6412
6444
|
return raw.map((r) => {
|
|
6413
6445
|
const rec = asRecord(r);
|
|
@@ -6450,7 +6482,7 @@ function casesFromApi(report, verdicts) {
|
|
|
6450
6482
|
});
|
|
6451
6483
|
}
|
|
6452
6484
|
async function readCairnRun(dir, deps = {}) {
|
|
6453
|
-
const raw = await readJson(
|
|
6485
|
+
const raw = await readJson(path17.join(dir, "report.json"));
|
|
6454
6486
|
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
6455
6487
|
const report = asRecord(raw);
|
|
6456
6488
|
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
@@ -6480,20 +6512,20 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
6480
6512
|
return {
|
|
6481
6513
|
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
6482
6514
|
run: {
|
|
6483
|
-
runId: str(report["runId"],
|
|
6515
|
+
runId: str(report["runId"], path17.basename(dir)),
|
|
6484
6516
|
mode,
|
|
6485
6517
|
target: str(report["url"], "unknown")
|
|
6486
6518
|
},
|
|
6487
6519
|
cases
|
|
6488
6520
|
};
|
|
6489
6521
|
}
|
|
6490
|
-
var
|
|
6522
|
+
var fs18, path17, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
|
|
6491
6523
|
var init_cairn_artifact = __esm({
|
|
6492
6524
|
"src/cli/commands/cairn-artifact.ts"() {
|
|
6493
6525
|
"use strict";
|
|
6494
6526
|
init_cjs_shims();
|
|
6495
|
-
|
|
6496
|
-
|
|
6527
|
+
fs18 = __toESM(require("fs/promises"), 1);
|
|
6528
|
+
path17 = __toESM(require("path"), 1);
|
|
6497
6529
|
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
6498
6530
|
INGEST_SCHEMA_VERSION = 1;
|
|
6499
6531
|
NoCairnRunsError = class extends Error {
|
|
@@ -6565,7 +6597,7 @@ async function handleIngest(deps) {
|
|
|
6565
6597
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
6566
6598
|
const endpoint = `${apiUrl}/v1/ingest/cairn`;
|
|
6567
6599
|
const job = new JobReporter(doFetch, apiUrl, token, "ingest");
|
|
6568
|
-
await job.open(
|
|
6600
|
+
await job.open(path18.basename(path18.resolve(dir)), payload.cases.length);
|
|
6569
6601
|
try {
|
|
6570
6602
|
const result = await upload();
|
|
6571
6603
|
await job.uploaded({ subjectId: result.runId || void 0 });
|
|
@@ -6624,12 +6656,12 @@ function formatIngestResult(r) {
|
|
|
6624
6656
|
return `${lines.join("\n")}
|
|
6625
6657
|
`;
|
|
6626
6658
|
}
|
|
6627
|
-
var
|
|
6659
|
+
var path18, IngestNotLoggedInError, IngestTokenRejectedError, IngestNetworkError, IngestHttpError;
|
|
6628
6660
|
var init_ingest = __esm({
|
|
6629
6661
|
"src/cli/commands/ingest.ts"() {
|
|
6630
6662
|
"use strict";
|
|
6631
6663
|
init_cjs_shims();
|
|
6632
|
-
|
|
6664
|
+
path18 = __toESM(require("path"), 1);
|
|
6633
6665
|
init_api_url();
|
|
6634
6666
|
init_credentials();
|
|
6635
6667
|
init_cairn_artifact();
|
|
@@ -7577,7 +7609,13 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7577
7609
|
await failRunCommand(err, verboseOf(command));
|
|
7578
7610
|
}
|
|
7579
7611
|
});
|
|
7580
|
-
runCommand.command("import").argument(
|
|
7612
|
+
runCommand.command("import").argument(
|
|
7613
|
+
"<file>",
|
|
7614
|
+
"A JUnit XML or Playwright JSON report, a folder of JUnit XML files (target/surefire-reports, build/test-results/test), or an Allure allure-results folder"
|
|
7615
|
+
).description("Turn a report another runner wrote into a run in Plune \u2014 no provider key needed").option(
|
|
7616
|
+
"--format <fmt>",
|
|
7617
|
+
"junit | playwright-json | allure; detected when omitted (a folder with a *-result.json is allure, any other folder is its *.xml files as junit)"
|
|
7618
|
+
).option("--key <externalKey>", "The key several jobs share so their reports land in one run").option("--name <text>", "What to call the run in the list (default: the folder name, the time and where it ran; PLUNE_RUN_TITLE sets it too)").option("--create", "Offer tests nothing matched to the review queue", false).option(
|
|
7581
7619
|
"--fail-on-missing-attachments",
|
|
7582
7620
|
"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",
|
|
7583
7621
|
false
|
|
@@ -7707,10 +7745,10 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7707
7745
|
const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
7708
7746
|
try {
|
|
7709
7747
|
const token = options.token ?? await readTokenFromStdin();
|
|
7710
|
-
const { path:
|
|
7748
|
+
const { path: path19, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
|
|
7711
7749
|
process.stdout.write(
|
|
7712
|
-
verified ? `Logged in. Token checked against the API and saved to ${
|
|
7713
|
-
` : `Token saved to ${
|
|
7750
|
+
verified ? `Logged in. Token checked against the API and saved to ${path19}
|
|
7751
|
+
` : `Token saved to ${path19} WITHOUT being checked (--skip-verify).
|
|
7714
7752
|
`
|
|
7715
7753
|
);
|
|
7716
7754
|
} catch (err) {
|
|
@@ -7727,9 +7765,9 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7727
7765
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
7728
7766
|
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
7729
7767
|
try {
|
|
7730
|
-
const { removed, path:
|
|
7768
|
+
const { removed, path: path19 } = handleLogout2();
|
|
7731
7769
|
process.stdout.write(
|
|
7732
|
-
removed ? `Logged out. Removed ${
|
|
7770
|
+
removed ? `Logged out. Removed ${path19}
|
|
7733
7771
|
` : "Not logged in \u2014 nothing to remove.\n"
|
|
7734
7772
|
);
|
|
7735
7773
|
} catch (err) {
|
package/package.json
CHANGED