@plune-ai/cli 0.31.0 → 0.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -9
- package/dist/cli.cjs +122 -79
- 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. Gradle writes
|
|
376
|
+
a `timestamp` on the suite, and a test's span starts there; Surefire writes none, so a Maven test's span ends when the
|
|
377
|
+
import reads the report (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";
|
|
@@ -5406,23 +5406,28 @@ function specRefOf(test, suite, classname) {
|
|
|
5406
5406
|
}
|
|
5407
5407
|
return classname === "" ? void 0 : classname;
|
|
5408
5408
|
}
|
|
5409
|
-
function executionOf(test, suite) {
|
|
5410
|
-
const stamp = suite?.attrs["timestamp"];
|
|
5411
|
-
if (stamp === void 0 || stamp === "") return void 0;
|
|
5412
|
-
const started = new Date(stamp);
|
|
5413
|
-
if (Number.isNaN(started.getTime())) return void 0;
|
|
5409
|
+
function executionOf(test, suite, now) {
|
|
5414
5410
|
const seconds = Number.parseFloat(test.attrs["time"] ?? "");
|
|
5415
|
-
const
|
|
5411
|
+
const timed = Number.isFinite(seconds) && seconds >= 0;
|
|
5412
|
+
const durationMs = timed ? Math.round(seconds * 1e3) : 0;
|
|
5413
|
+
const stamp = suite?.attrs["timestamp"];
|
|
5414
|
+
const dated = stamp === void 0 || stamp === "" ? void 0 : new Date(stamp);
|
|
5415
|
+
let started;
|
|
5416
|
+
if (dated !== void 0 && !Number.isNaN(dated.getTime())) started = dated;
|
|
5417
|
+
else if (timed) started = new Date(now.getTime() - durationMs);
|
|
5418
|
+
else return void 0;
|
|
5419
|
+
const finished = new Date(started.getTime() + durationMs);
|
|
5420
|
+
if (Number.isNaN(started.getTime()) || Number.isNaN(finished.getTime())) return void 0;
|
|
5416
5421
|
return {
|
|
5417
5422
|
startedAt: started.toISOString(),
|
|
5418
|
-
finishedAt:
|
|
5423
|
+
finishedAt: finished.toISOString(),
|
|
5419
5424
|
durationMs,
|
|
5420
5425
|
// The base format has no reruns. Surefire's `<rerunFailure>` does, and reading it is work for
|
|
5421
5426
|
// the day somebody asks; reporting 0 says «first attempt», which is true of every case here.
|
|
5422
5427
|
retry: 0
|
|
5423
5428
|
};
|
|
5424
5429
|
}
|
|
5425
|
-
function readJUnit(source, file, missing = [], notes = []) {
|
|
5430
|
+
function readJUnit(source, file, missing = [], notes = [], now = /* @__PURE__ */ new Date()) {
|
|
5426
5431
|
const doc = parseXml(source, file);
|
|
5427
5432
|
const { lexical, real: folder } = reportFolders(file);
|
|
5428
5433
|
const found = [];
|
|
@@ -5436,7 +5441,7 @@ function readJUnit(source, file, missing = [], notes = []) {
|
|
|
5436
5441
|
const stated = TOKEN2.exec(playwright?.titles.at(-1) ?? name)?.[1];
|
|
5437
5442
|
const specRef = specRefOf(test, suite, classname);
|
|
5438
5443
|
const errorContext = errorTextOf(detail);
|
|
5439
|
-
const execution = executionOf(test, suite);
|
|
5444
|
+
const execution = executionOf(test, suite, now);
|
|
5440
5445
|
const title = titleOf(classname, name);
|
|
5441
5446
|
const rkey = resultKey(key.value, 0);
|
|
5442
5447
|
const files = filesOfTest(test, folder, title, missing, { lexical, resultKey: rkey, notes });
|
|
@@ -5486,10 +5491,10 @@ function attemptOf(result, file, ctx) {
|
|
|
5486
5491
|
...typeof at?.file === "string" && typeof at.line === "number" ? { location: { file: at.file, line: at.line, ...typeof at.column === "number" ? { column: at.column } : {} } } : {}
|
|
5487
5492
|
})),
|
|
5488
5493
|
steps: steps(result.steps),
|
|
5489
|
-
attachments: (result.attachments ?? []).map(({ name, contentType, path:
|
|
5494
|
+
attachments: (result.attachments ?? []).map(({ name, contentType, path: path19, body }) => ({
|
|
5490
5495
|
name: name ?? "",
|
|
5491
5496
|
...contentType !== void 0 ? { contentType } : {},
|
|
5492
|
-
...
|
|
5497
|
+
...path19 !== void 0 ? { path: path19 } : {},
|
|
5493
5498
|
// Decoded only where the platform would take it (plune-ai/plune#928): a report of screenshots kept as
|
|
5494
5499
|
// bodies is megabytes of base64 that nothing here will read.
|
|
5495
5500
|
...typeof body === "string" && isTextType(contentType) ? { body: Buffer.from(body, "base64") } : {}
|
|
@@ -5628,12 +5633,40 @@ function detectFormat(source, file) {
|
|
|
5628
5633
|
);
|
|
5629
5634
|
}
|
|
5630
5635
|
function readFolder(dir, format) {
|
|
5631
|
-
if (format
|
|
5632
|
-
throw new UnknownFormatError(dir,
|
|
5636
|
+
if (format === "playwright-json") {
|
|
5637
|
+
throw new UnknownFormatError(dir, "this is a folder, and only allure-results or JUnit XML files are read from one, not playwright-json.");
|
|
5633
5638
|
}
|
|
5634
5639
|
const missing = [];
|
|
5635
5640
|
const notes = [];
|
|
5636
|
-
|
|
5641
|
+
if (format === "allure" || format === void 0 && namesIn(dir, RESULT).length > 0) {
|
|
5642
|
+
return { format: "allure", results: readAllure(dir, missing, notes), missing, notes };
|
|
5643
|
+
}
|
|
5644
|
+
const results = [];
|
|
5645
|
+
const skipped = [];
|
|
5646
|
+
let reports = 0;
|
|
5647
|
+
for (const name of namesIn(dir, /\.xml$/)) {
|
|
5648
|
+
const file = path13.join(dir, name);
|
|
5649
|
+
let source;
|
|
5650
|
+
try {
|
|
5651
|
+
source = fs12.readFileSync(file, "utf8");
|
|
5652
|
+
} catch {
|
|
5653
|
+
throw new RunCommandError(`Cannot read ${printable(file)}.`);
|
|
5654
|
+
}
|
|
5655
|
+
if (!looksLikeJUnit(source)) {
|
|
5656
|
+
skipped.push(name);
|
|
5657
|
+
notes.push(`plune: ${printable(file)} is not a JUnit report \u2014 skipped.`);
|
|
5658
|
+
continue;
|
|
5659
|
+
}
|
|
5660
|
+
reports += 1;
|
|
5661
|
+
for (const r of readJUnit(source, file, missing, notes)) results.push(r);
|
|
5662
|
+
}
|
|
5663
|
+
if (reports === 0) {
|
|
5664
|
+
const names = skipped.slice(0, 3).map(printable).join(", ") + (skipped.length > 3 ? ` and ${skipped.length - 3} more` : "");
|
|
5665
|
+
throw new AllureReportError(
|
|
5666
|
+
`${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.`
|
|
5667
|
+
);
|
|
5668
|
+
}
|
|
5669
|
+
return { format: "junit", results, missing, notes };
|
|
5637
5670
|
}
|
|
5638
5671
|
function readReport(source, file, format) {
|
|
5639
5672
|
const chosen2 = format ?? detectFormat(source, file);
|
|
@@ -5643,11 +5676,15 @@ function readReport(source, file, format) {
|
|
|
5643
5676
|
if (chosen2 === "junit") return { format: chosen2, results: readJUnit(source, file, missing, notes), missing, notes };
|
|
5644
5677
|
return { format: chosen2, ...readPlaywrightJson(source, file), missing, notes };
|
|
5645
5678
|
}
|
|
5646
|
-
var IMPORT_FORMATS, UnknownFormatError;
|
|
5679
|
+
var fs12, path13, IMPORT_FORMATS, UnknownFormatError;
|
|
5647
5680
|
var init_importers = __esm({
|
|
5648
5681
|
"src/importers/index.ts"() {
|
|
5649
5682
|
"use strict";
|
|
5650
5683
|
init_cjs_shims();
|
|
5684
|
+
fs12 = __toESM(require("fs"), 1);
|
|
5685
|
+
path13 = __toESM(require("path"), 1);
|
|
5686
|
+
init_run_lifecycle();
|
|
5687
|
+
init_failure_detail();
|
|
5651
5688
|
init_allure();
|
|
5652
5689
|
init_junit();
|
|
5653
5690
|
init_playwright_json();
|
|
@@ -5694,9 +5731,9 @@ async function handleRunImport(options) {
|
|
|
5694
5731
|
} else {
|
|
5695
5732
|
let source;
|
|
5696
5733
|
try {
|
|
5697
|
-
source =
|
|
5734
|
+
source = fs13.readFileSync(options.file, "utf8");
|
|
5698
5735
|
} catch {
|
|
5699
|
-
throw new RunCommandError(`Cannot read ${options.file}.`);
|
|
5736
|
+
throw new RunCommandError(`Cannot read ${printable(options.file)}.`);
|
|
5700
5737
|
}
|
|
5701
5738
|
read = readReport(source, options.file, options.format);
|
|
5702
5739
|
}
|
|
@@ -5847,12 +5884,12 @@ function describe(result, apiUrl) {
|
|
|
5847
5884
|
if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
|
|
5848
5885
|
return lines;
|
|
5849
5886
|
}
|
|
5850
|
-
var
|
|
5887
|
+
var fs13, ImportTimeoutError, NEXT;
|
|
5851
5888
|
var init_run_import = __esm({
|
|
5852
5889
|
"src/cli/commands/run-import.ts"() {
|
|
5853
5890
|
"use strict";
|
|
5854
5891
|
init_cjs_shims();
|
|
5855
|
-
|
|
5892
|
+
fs13 = __toESM(require("fs"), 1);
|
|
5856
5893
|
init_session();
|
|
5857
5894
|
init_failure_detail();
|
|
5858
5895
|
init_importers();
|
|
@@ -5884,10 +5921,10 @@ function isRunResult(v) {
|
|
|
5884
5921
|
}
|
|
5885
5922
|
function handleReport(opts = {}) {
|
|
5886
5923
|
const cwd = opts.cwd ?? process.cwd();
|
|
5887
|
-
const file =
|
|
5924
|
+
const file = path14.join(cwd, ".plune", "last-run.json");
|
|
5888
5925
|
let raw;
|
|
5889
5926
|
try {
|
|
5890
|
-
raw =
|
|
5927
|
+
raw = fs14.readFileSync(file, "utf8");
|
|
5891
5928
|
} catch {
|
|
5892
5929
|
throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
|
|
5893
5930
|
}
|
|
@@ -5902,13 +5939,13 @@ function handleReport(opts = {}) {
|
|
|
5902
5939
|
}
|
|
5903
5940
|
return parsed;
|
|
5904
5941
|
}
|
|
5905
|
-
var
|
|
5942
|
+
var fs14, path14, ReportNotFoundError;
|
|
5906
5943
|
var init_report = __esm({
|
|
5907
5944
|
"src/cli/commands/report.ts"() {
|
|
5908
5945
|
"use strict";
|
|
5909
5946
|
init_cjs_shims();
|
|
5910
|
-
|
|
5911
|
-
|
|
5947
|
+
fs14 = __toESM(require("fs"), 1);
|
|
5948
|
+
path14 = __toESM(require("path"), 1);
|
|
5912
5949
|
ReportNotFoundError = class extends Error {
|
|
5913
5950
|
code = "NO_SAVED_RUN";
|
|
5914
5951
|
constructor(message) {
|
|
@@ -5978,7 +6015,7 @@ function isRunResult2(v) {
|
|
|
5978
6015
|
function loadRunResult(file, label) {
|
|
5979
6016
|
let raw;
|
|
5980
6017
|
try {
|
|
5981
|
-
raw =
|
|
6018
|
+
raw = fs15.readFileSync(file, "utf8");
|
|
5982
6019
|
} catch {
|
|
5983
6020
|
throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
|
|
5984
6021
|
}
|
|
@@ -5998,12 +6035,12 @@ function handleDiff(opts) {
|
|
|
5998
6035
|
const current = loadRunResult(opts.currentPath, "current");
|
|
5999
6036
|
return diffRuns(baseline, current);
|
|
6000
6037
|
}
|
|
6001
|
-
var
|
|
6038
|
+
var fs15, DiffInputError;
|
|
6002
6039
|
var init_diff2 = __esm({
|
|
6003
6040
|
"src/cli/commands/diff.ts"() {
|
|
6004
6041
|
"use strict";
|
|
6005
6042
|
init_cjs_shims();
|
|
6006
|
-
|
|
6043
|
+
fs15 = __toESM(require("fs"), 1);
|
|
6007
6044
|
init_diff();
|
|
6008
6045
|
DiffInputError = class extends Error {
|
|
6009
6046
|
code = "DIFF_INPUT";
|
|
@@ -6128,8 +6165,8 @@ async function runInitWizard(cwd) {
|
|
|
6128
6165
|
process.exit(1);
|
|
6129
6166
|
return;
|
|
6130
6167
|
}
|
|
6131
|
-
const outputPath =
|
|
6132
|
-
if (
|
|
6168
|
+
const outputPath = path15.join(cwd, "plune.yaml");
|
|
6169
|
+
if (fs16.existsSync(outputPath)) {
|
|
6133
6170
|
const overwrite = await clack.confirm({
|
|
6134
6171
|
message: "plune.yaml already exists. Overwrite?"
|
|
6135
6172
|
});
|
|
@@ -6168,18 +6205,18 @@ async function runInitWizard(cwd) {
|
|
|
6168
6205
|
`;
|
|
6169
6206
|
const spin = clack.spinner();
|
|
6170
6207
|
spin.start("Writing plune.yaml\u2026");
|
|
6171
|
-
|
|
6208
|
+
fs16.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
|
|
6172
6209
|
spin.stop("plune.yaml created!");
|
|
6173
6210
|
clack.outro("plune.yaml is ready.");
|
|
6174
6211
|
}
|
|
6175
|
-
var clack,
|
|
6212
|
+
var clack, fs16, path15, import_yaml2;
|
|
6176
6213
|
var init_wizard = __esm({
|
|
6177
6214
|
"src/config/init/wizard.ts"() {
|
|
6178
6215
|
"use strict";
|
|
6179
6216
|
init_cjs_shims();
|
|
6180
6217
|
clack = __toESM(require("@clack/prompts"), 1);
|
|
6181
|
-
|
|
6182
|
-
|
|
6218
|
+
fs16 = __toESM(require("fs"), 1);
|
|
6219
|
+
path15 = __toESM(require("path"), 1);
|
|
6183
6220
|
import_yaml2 = require("yaml");
|
|
6184
6221
|
init_errors();
|
|
6185
6222
|
}
|
|
@@ -6187,21 +6224,21 @@ var init_wizard = __esm({
|
|
|
6187
6224
|
|
|
6188
6225
|
// src/cli/templates/index.ts
|
|
6189
6226
|
function writeTemplateFile(cwd, rel, content, force) {
|
|
6190
|
-
const target =
|
|
6191
|
-
if (!force &&
|
|
6227
|
+
const target = path16.join(cwd, rel);
|
|
6228
|
+
if (!force && fs17.existsSync(target)) {
|
|
6192
6229
|
return "skipped";
|
|
6193
6230
|
}
|
|
6194
|
-
|
|
6195
|
-
|
|
6231
|
+
fs17.mkdirSync(path16.dirname(target), { recursive: true });
|
|
6232
|
+
fs17.writeFileSync(target, content, "utf8");
|
|
6196
6233
|
return "written";
|
|
6197
6234
|
}
|
|
6198
|
-
var
|
|
6235
|
+
var fs17, path16, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
|
|
6199
6236
|
var init_templates = __esm({
|
|
6200
6237
|
"src/cli/templates/index.ts"() {
|
|
6201
6238
|
"use strict";
|
|
6202
6239
|
init_cjs_shims();
|
|
6203
|
-
|
|
6204
|
-
|
|
6240
|
+
fs17 = __toESM(require("fs"), 1);
|
|
6241
|
+
path16 = __toESM(require("path"), 1);
|
|
6205
6242
|
ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
|
|
6206
6243
|
# .env is gitignored; .env.example is safe to commit.
|
|
6207
6244
|
# Plune reads whichever key matches your configured provider.
|
|
@@ -6381,14 +6418,14 @@ var init_logout = __esm({
|
|
|
6381
6418
|
|
|
6382
6419
|
// src/cli/commands/cairn-artifact.ts
|
|
6383
6420
|
async function findLatestCairnRun(runsDir = "runs") {
|
|
6384
|
-
const entries = await
|
|
6421
|
+
const entries = await fs18.readdir(runsDir, { withFileTypes: true }).catch(() => null);
|
|
6385
6422
|
if (entries === null) throw new NoCairnRunsError(runsDir);
|
|
6386
6423
|
let newest;
|
|
6387
6424
|
for (const entry of entries) {
|
|
6388
6425
|
if (!entry.isDirectory()) continue;
|
|
6389
|
-
const dir =
|
|
6426
|
+
const dir = path17.join(runsDir, entry.name);
|
|
6390
6427
|
try {
|
|
6391
|
-
const report = await
|
|
6428
|
+
const report = await fs18.lstat(path17.join(dir, "report.json"));
|
|
6392
6429
|
if (!report.isFile()) continue;
|
|
6393
6430
|
if (newest === void 0 || report.mtimeMs > newest.at) {
|
|
6394
6431
|
newest = { dir, at: report.mtimeMs };
|
|
@@ -6401,13 +6438,13 @@ async function findLatestCairnRun(runsDir = "runs") {
|
|
|
6401
6438
|
}
|
|
6402
6439
|
async function readJson(file) {
|
|
6403
6440
|
try {
|
|
6404
|
-
return JSON.parse(await
|
|
6441
|
+
return JSON.parse(await fs18.readFile(file, "utf8"));
|
|
6405
6442
|
} catch {
|
|
6406
6443
|
return void 0;
|
|
6407
6444
|
}
|
|
6408
6445
|
}
|
|
6409
6446
|
async function defaultReadEvidence(dir) {
|
|
6410
|
-
const raw = await readJson(
|
|
6447
|
+
const raw = await readJson(path17.join(dir, "api-evidence.json"));
|
|
6411
6448
|
if (!Array.isArray(raw)) return void 0;
|
|
6412
6449
|
return raw.map((r) => {
|
|
6413
6450
|
const rec = asRecord(r);
|
|
@@ -6450,7 +6487,7 @@ function casesFromApi(report, verdicts) {
|
|
|
6450
6487
|
});
|
|
6451
6488
|
}
|
|
6452
6489
|
async function readCairnRun(dir, deps = {}) {
|
|
6453
|
-
const raw = await readJson(
|
|
6490
|
+
const raw = await readJson(path17.join(dir, "report.json"));
|
|
6454
6491
|
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
6455
6492
|
const report = asRecord(raw);
|
|
6456
6493
|
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
@@ -6480,20 +6517,20 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
6480
6517
|
return {
|
|
6481
6518
|
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
6482
6519
|
run: {
|
|
6483
|
-
runId: str(report["runId"],
|
|
6520
|
+
runId: str(report["runId"], path17.basename(dir)),
|
|
6484
6521
|
mode,
|
|
6485
6522
|
target: str(report["url"], "unknown")
|
|
6486
6523
|
},
|
|
6487
6524
|
cases
|
|
6488
6525
|
};
|
|
6489
6526
|
}
|
|
6490
|
-
var
|
|
6527
|
+
var fs18, path17, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
|
|
6491
6528
|
var init_cairn_artifact = __esm({
|
|
6492
6529
|
"src/cli/commands/cairn-artifact.ts"() {
|
|
6493
6530
|
"use strict";
|
|
6494
6531
|
init_cjs_shims();
|
|
6495
|
-
|
|
6496
|
-
|
|
6532
|
+
fs18 = __toESM(require("fs/promises"), 1);
|
|
6533
|
+
path17 = __toESM(require("path"), 1);
|
|
6497
6534
|
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
6498
6535
|
INGEST_SCHEMA_VERSION = 1;
|
|
6499
6536
|
NoCairnRunsError = class extends Error {
|
|
@@ -6565,7 +6602,7 @@ async function handleIngest(deps) {
|
|
|
6565
6602
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
6566
6603
|
const endpoint = `${apiUrl}/v1/ingest/cairn`;
|
|
6567
6604
|
const job = new JobReporter(doFetch, apiUrl, token, "ingest");
|
|
6568
|
-
await job.open(
|
|
6605
|
+
await job.open(path18.basename(path18.resolve(dir)), payload.cases.length);
|
|
6569
6606
|
try {
|
|
6570
6607
|
const result = await upload();
|
|
6571
6608
|
await job.uploaded({ subjectId: result.runId || void 0 });
|
|
@@ -6624,12 +6661,12 @@ function formatIngestResult(r) {
|
|
|
6624
6661
|
return `${lines.join("\n")}
|
|
6625
6662
|
`;
|
|
6626
6663
|
}
|
|
6627
|
-
var
|
|
6664
|
+
var path18, IngestNotLoggedInError, IngestTokenRejectedError, IngestNetworkError, IngestHttpError;
|
|
6628
6665
|
var init_ingest = __esm({
|
|
6629
6666
|
"src/cli/commands/ingest.ts"() {
|
|
6630
6667
|
"use strict";
|
|
6631
6668
|
init_cjs_shims();
|
|
6632
|
-
|
|
6669
|
+
path18 = __toESM(require("path"), 1);
|
|
6633
6670
|
init_api_url();
|
|
6634
6671
|
init_credentials();
|
|
6635
6672
|
init_cairn_artifact();
|
|
@@ -7577,7 +7614,13 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7577
7614
|
await failRunCommand(err, verboseOf(command));
|
|
7578
7615
|
}
|
|
7579
7616
|
});
|
|
7580
|
-
runCommand.command("import").argument(
|
|
7617
|
+
runCommand.command("import").argument(
|
|
7618
|
+
"<file>",
|
|
7619
|
+
"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"
|
|
7620
|
+
).description("Turn a report another runner wrote into a run in Plune \u2014 no provider key needed").option(
|
|
7621
|
+
"--format <fmt>",
|
|
7622
|
+
"junit | playwright-json | allure; detected when omitted (a folder with a *-result.json is allure, any other folder is its *.xml files as junit)"
|
|
7623
|
+
).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
7624
|
"--fail-on-missing-attachments",
|
|
7582
7625
|
"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
7626
|
false
|
|
@@ -7707,10 +7750,10 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7707
7750
|
const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
7708
7751
|
try {
|
|
7709
7752
|
const token = options.token ?? await readTokenFromStdin();
|
|
7710
|
-
const { path:
|
|
7753
|
+
const { path: path19, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
|
|
7711
7754
|
process.stdout.write(
|
|
7712
|
-
verified ? `Logged in. Token checked against the API and saved to ${
|
|
7713
|
-
` : `Token saved to ${
|
|
7755
|
+
verified ? `Logged in. Token checked against the API and saved to ${path19}
|
|
7756
|
+
` : `Token saved to ${path19} WITHOUT being checked (--skip-verify).
|
|
7714
7757
|
`
|
|
7715
7758
|
);
|
|
7716
7759
|
} catch (err) {
|
|
@@ -7727,9 +7770,9 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7727
7770
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
7728
7771
|
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
7729
7772
|
try {
|
|
7730
|
-
const { removed, path:
|
|
7773
|
+
const { removed, path: path19 } = handleLogout2();
|
|
7731
7774
|
process.stdout.write(
|
|
7732
|
-
removed ? `Logged out. Removed ${
|
|
7775
|
+
removed ? `Logged out. Removed ${path19}
|
|
7733
7776
|
` : "Not logged in \u2014 nothing to remove.\n"
|
|
7734
7777
|
);
|
|
7735
7778
|
} catch (err) {
|
package/package.json
CHANGED