@piwitests/reporter 0.27.0 → 0.28.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 +2 -0
- package/dist/global-setup-module.js +21 -0
- package/dist/index.d.ts +22 -2
- package/dist/index.js +240 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,8 @@ export default wrapConfig(defineConfig({}), {
|
|
|
38
38
|
|
|
39
39
|
Run `npx playwright test` — results are uploaded automatically.
|
|
40
40
|
|
|
41
|
+
> Piwi rebuilds the run from the data it collects, so you don't also need Playwright's built-in `html` reporter — running it alongside Piwi just repeats the end-of-run report generation. Keep it only if you want Playwright's standalone report uploaded too.
|
|
42
|
+
|
|
41
43
|
**Recommended: enable the capture fixtures.** One small file unlocks the dashboard's richest features:
|
|
42
44
|
|
|
43
45
|
```typescript
|
|
@@ -83,6 +83,7 @@ var DEFAULTS = {
|
|
|
83
83
|
streaming: true,
|
|
84
84
|
streamingBatchSize: 5,
|
|
85
85
|
streamingBatchDelay: 2e3,
|
|
86
|
+
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
86
87
|
failOnFlakyTests: false,
|
|
87
88
|
username: null,
|
|
88
89
|
password: null,
|
|
@@ -102,6 +103,7 @@ var PIWI_ENV_KEYS = {
|
|
|
102
103
|
streaming: "PIWI_STREAMING",
|
|
103
104
|
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
104
105
|
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
106
|
+
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
105
107
|
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
106
108
|
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
107
109
|
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
@@ -140,6 +142,7 @@ var ENV_FALLBACK_SPECS = [
|
|
|
140
142
|
{ option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
|
|
141
143
|
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
142
144
|
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
145
|
+
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
143
146
|
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
144
147
|
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
145
148
|
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
@@ -389,6 +392,12 @@ var HttpClient = class {
|
|
|
389
392
|
res.on("end", () => {
|
|
390
393
|
resolve2({ status: res.statusCode ?? 0, text: data, headers: res.headers });
|
|
391
394
|
});
|
|
395
|
+
res.on("error", reject);
|
|
396
|
+
res.on("close", () => {
|
|
397
|
+
if (!res.complete) {
|
|
398
|
+
reject(new Error(`Connection to ${pathname} closed before the response completed`));
|
|
399
|
+
}
|
|
400
|
+
});
|
|
392
401
|
}
|
|
393
402
|
);
|
|
394
403
|
req.on("error", reject);
|
|
@@ -483,6 +492,13 @@ function isUiMode(argv = process.argv) {
|
|
|
483
492
|
const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
|
|
484
493
|
return rest.some((tok) => PW_UI_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
|
|
485
494
|
}
|
|
495
|
+
var PW_LIST_FLAGS = ["--list"];
|
|
496
|
+
function isListMode(argv = process.argv) {
|
|
497
|
+
const args = argv.slice(2);
|
|
498
|
+
const testIdx = args.indexOf("test");
|
|
499
|
+
const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
|
|
500
|
+
return rest.some((tok) => PW_LIST_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
|
|
501
|
+
}
|
|
486
502
|
|
|
487
503
|
// src/public/global-setup.ts
|
|
488
504
|
function createGlobalSetup(options, userSetup) {
|
|
@@ -512,6 +528,11 @@ function createGlobalSetup(options, userSetup) {
|
|
|
512
528
|
if (userSetup) return userSetup(config);
|
|
513
529
|
return;
|
|
514
530
|
}
|
|
531
|
+
if (isListMode()) {
|
|
532
|
+
logger.debug("List mode detected \u2014 skipping run registration.");
|
|
533
|
+
if (userSetup) return userSetup(config);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
515
536
|
if (opts.enabled === false || !opts.serverUrl) {
|
|
516
537
|
logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
|
|
517
538
|
if (userSetup) return userSetup(config);
|
package/dist/index.d.ts
CHANGED
|
@@ -115,6 +115,18 @@ interface PiwiDashboardOptions {
|
|
|
115
115
|
streamingBatchSize?: number;
|
|
116
116
|
/** Max delay (ms) before flushing pending events during streaming. Defaults to `2000`. */
|
|
117
117
|
streamingBatchDelay?: number;
|
|
118
|
+
/**
|
|
119
|
+
* Byte budget for the in-memory stream buffer (the queue of events waiting to
|
|
120
|
+
* reach the dashboard, and the crash-recovery file it writes if delivery
|
|
121
|
+
* fails). When the server is unreachable or a huge suite outruns delivery, the
|
|
122
|
+
* queue is capped here instead of growing without bound: the lowest-value
|
|
123
|
+
* events are shed first — live step progress, then per-test `begin` markers,
|
|
124
|
+
* and only as a last resort test results (the final counts and the end-of-run
|
|
125
|
+
* batch submit stay complete regardless). Defaults to `104857600` (100 MB).
|
|
126
|
+
* Set to `0` to disable the cap (unbounded, the pre-`0.28` behavior). Can also
|
|
127
|
+
* be set with `PIWI_MAX_STREAM_BUFFER_BYTES`.
|
|
128
|
+
*/
|
|
129
|
+
maxStreamBufferBytes?: number;
|
|
118
130
|
/**
|
|
119
131
|
* Fail the run when any test was flaky (passed only after a retry). Forwarded
|
|
120
132
|
* to Playwright's native `failOnFlakyTests` config option (Playwright 1.52+)
|
|
@@ -237,8 +249,10 @@ declare function createGlobalSetup(options?: PiwiDashboardOptions, userSetup?: (
|
|
|
237
249
|
*
|
|
238
250
|
* The `globalSetup` field is set to a `string` (or `string[]` if the user
|
|
239
251
|
* already has a global setup) referencing the Piwi global setup module,
|
|
240
|
-
* which registers the run on the server.
|
|
241
|
-
*
|
|
252
|
+
* which registers the run on the server. Piwi's module is chained first so the
|
|
253
|
+
* run registers before the user's own setup, appearing as "initializing" on the
|
|
254
|
+
* dashboard while that setup runs; the original setup path(s) are preserved and
|
|
255
|
+
* executed after it.
|
|
242
256
|
*
|
|
243
257
|
* Playwright options required in `globalSetup` are forwarded via `PIWI_*`
|
|
244
258
|
* environment variables (see `applyOptionsToEnv` in `config.ts` for the
|
|
@@ -288,6 +302,12 @@ declare class PiwiDashboardReporter {
|
|
|
288
302
|
private shardInfo;
|
|
289
303
|
private metadata;
|
|
290
304
|
private enabled;
|
|
305
|
+
/**
|
|
306
|
+
* True under `playwright test --list`, where Playwright still constructs this
|
|
307
|
+
* reporter and fires `onBegin`/`onEnd` but runs no tests. Registering and
|
|
308
|
+
* finalizing a run would create an empty phantom run and upload an empty report.
|
|
309
|
+
*/
|
|
310
|
+
private readonly listMode;
|
|
291
311
|
/** True when the server URL and API key came from the desktop app, not from config. */
|
|
292
312
|
private viaDesktopApp;
|
|
293
313
|
private isFullRun;
|
package/dist/index.js
CHANGED
|
@@ -82,6 +82,7 @@ var DEFAULTS = {
|
|
|
82
82
|
streaming: true,
|
|
83
83
|
streamingBatchSize: 5,
|
|
84
84
|
streamingBatchDelay: 2e3,
|
|
85
|
+
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
85
86
|
failOnFlakyTests: false,
|
|
86
87
|
username: null,
|
|
87
88
|
password: null,
|
|
@@ -101,6 +102,7 @@ var PIWI_ENV_KEYS = {
|
|
|
101
102
|
streaming: "PIWI_STREAMING",
|
|
102
103
|
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
103
104
|
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
105
|
+
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
104
106
|
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
105
107
|
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
106
108
|
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
@@ -146,6 +148,7 @@ var ENV_FALLBACK_SPECS = [
|
|
|
146
148
|
{ option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
|
|
147
149
|
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
148
150
|
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
151
|
+
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
149
152
|
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
150
153
|
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
151
154
|
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
@@ -436,6 +439,12 @@ var HttpClient = class {
|
|
|
436
439
|
res.on("end", () => {
|
|
437
440
|
resolve5({ status: res.statusCode ?? 0, text: data, headers: res.headers });
|
|
438
441
|
});
|
|
442
|
+
res.on("error", reject);
|
|
443
|
+
res.on("close", () => {
|
|
444
|
+
if (!res.complete) {
|
|
445
|
+
reject(new Error(`Connection to ${pathname} closed before the response completed`));
|
|
446
|
+
}
|
|
447
|
+
});
|
|
439
448
|
}
|
|
440
449
|
);
|
|
441
450
|
req.on("error", reject);
|
|
@@ -1187,6 +1196,20 @@ function resolveScmPrNumber(env) {
|
|
|
1187
1196
|
const trimmed = raw?.trim();
|
|
1188
1197
|
return trimmed && /^\d+$/.test(trimmed) ? trimmed : void 0;
|
|
1189
1198
|
}
|
|
1199
|
+
function resolveScmBaseBranch(env) {
|
|
1200
|
+
const override = env.PIWI_BASE_BRANCH?.trim();
|
|
1201
|
+
if (override) return override;
|
|
1202
|
+
let target;
|
|
1203
|
+
if (env.JENKINS_URL) target = env.CHANGE_TARGET;
|
|
1204
|
+
else if (env.GITHUB_ACTIONS) target = env.GITHUB_BASE_REF;
|
|
1205
|
+
else if (env.GITLAB_CI) target = env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
|
|
1206
|
+
else if (env.TRAVIS)
|
|
1207
|
+
target = env.TRAVIS_PULL_REQUEST && env.TRAVIS_PULL_REQUEST !== "false" ? env.TRAVIS_BRANCH : void 0;
|
|
1208
|
+
else if (env.TF_BUILD) target = env.SYSTEM_PULLREQUEST_TARGETBRANCH;
|
|
1209
|
+
else if (env.BITBUCKET_BUILD_NUMBER) target = env.BITBUCKET_PR_DESTINATION_BRANCH;
|
|
1210
|
+
const trimmed = target?.trim();
|
|
1211
|
+
return trimmed ? normalizeRef(trimmed) : void 0;
|
|
1212
|
+
}
|
|
1190
1213
|
function normalizeRef(ref) {
|
|
1191
1214
|
return ref.replace(/^refs\/heads\//, "");
|
|
1192
1215
|
}
|
|
@@ -1319,6 +1342,8 @@ var MetadataCollector = class {
|
|
|
1319
1342
|
if (branch) scm.branch = branch;
|
|
1320
1343
|
const prNumber = resolveScmPrNumber(process.env);
|
|
1321
1344
|
if (prNumber) scm.prNumber = prNumber;
|
|
1345
|
+
const baseBranch = resolveScmBaseBranch(process.env);
|
|
1346
|
+
if (baseBranch) scm.baseBranch = baseBranch;
|
|
1322
1347
|
return Object.keys(scm).length > 0 ? scm : void 0;
|
|
1323
1348
|
}
|
|
1324
1349
|
collectCiInfo() {
|
|
@@ -1397,6 +1422,132 @@ var MetadataCollector = class {
|
|
|
1397
1422
|
// src/internal/streaming/stream-manager.ts
|
|
1398
1423
|
var fs8 = __toESM(require("fs"));
|
|
1399
1424
|
|
|
1425
|
+
// src/internal/streaming/bounded-queue.ts
|
|
1426
|
+
var DROP_TIER = {
|
|
1427
|
+
"step-begin": 0,
|
|
1428
|
+
"step-end": 0,
|
|
1429
|
+
begin: 1,
|
|
1430
|
+
complete: 2
|
|
1431
|
+
};
|
|
1432
|
+
var CRITICAL_TIER = 2;
|
|
1433
|
+
var BoundedEventQueue = class {
|
|
1434
|
+
/**
|
|
1435
|
+
* @param maxBytes Byte budget for the buffered events. `<= 0` or non-finite
|
|
1436
|
+
* disables the bound (unbounded, i.e. the pre-existing behavior).
|
|
1437
|
+
*/
|
|
1438
|
+
constructor(maxBytes) {
|
|
1439
|
+
this.maxBytes = maxBytes;
|
|
1440
|
+
this.items = [];
|
|
1441
|
+
this._bytes = 0;
|
|
1442
|
+
this._peakBytes = 0;
|
|
1443
|
+
this._dropped = 0;
|
|
1444
|
+
this._droppedByType = {};
|
|
1445
|
+
this._lostResults = false;
|
|
1446
|
+
}
|
|
1447
|
+
/** Number of buffered events. */
|
|
1448
|
+
get length() {
|
|
1449
|
+
return this.items.length;
|
|
1450
|
+
}
|
|
1451
|
+
/** Whether the queue holds no events. */
|
|
1452
|
+
get isEmpty() {
|
|
1453
|
+
return this.items.length === 0;
|
|
1454
|
+
}
|
|
1455
|
+
/** Approximate byte size of the buffered events. */
|
|
1456
|
+
get bytes() {
|
|
1457
|
+
return this._bytes;
|
|
1458
|
+
}
|
|
1459
|
+
/** Highest byte size the queue reached over its lifetime. */
|
|
1460
|
+
get peakBytes() {
|
|
1461
|
+
return this._peakBytes;
|
|
1462
|
+
}
|
|
1463
|
+
/** How many events have been dropped by eviction. */
|
|
1464
|
+
get droppedCount() {
|
|
1465
|
+
return this._dropped;
|
|
1466
|
+
}
|
|
1467
|
+
/** Dropped-event counts broken down by event type. */
|
|
1468
|
+
get droppedByType() {
|
|
1469
|
+
return this._droppedByType;
|
|
1470
|
+
}
|
|
1471
|
+
/** True once a test-result (`complete`) event had to be shed — real live-data loss. */
|
|
1472
|
+
get lostResults() {
|
|
1473
|
+
return this._lostResults;
|
|
1474
|
+
}
|
|
1475
|
+
/** Append one event to the back of the queue, then evict if over budget. */
|
|
1476
|
+
enqueue(event) {
|
|
1477
|
+
const size = estimateSize(event);
|
|
1478
|
+
this.items.push({ event, size });
|
|
1479
|
+
this._bytes += size;
|
|
1480
|
+
this.afterGrow();
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Put events back at the front (oldest position), preserving their order, then
|
|
1484
|
+
* evict. Used to re-queue a failed flush ahead of anything that arrived while
|
|
1485
|
+
* it was in flight, and to replay events reloaded from the on-disk buffer.
|
|
1486
|
+
*/
|
|
1487
|
+
prepend(events) {
|
|
1488
|
+
if (events.length === 0) return;
|
|
1489
|
+
const head = [];
|
|
1490
|
+
let added = 0;
|
|
1491
|
+
for (const event of events) {
|
|
1492
|
+
const size = estimateSize(event);
|
|
1493
|
+
head.push({ event, size });
|
|
1494
|
+
added += size;
|
|
1495
|
+
}
|
|
1496
|
+
this.items = head.concat(this.items);
|
|
1497
|
+
this._bytes += added;
|
|
1498
|
+
this.afterGrow();
|
|
1499
|
+
}
|
|
1500
|
+
/** Remove and return every queued event, resetting the size counter. */
|
|
1501
|
+
takeAll() {
|
|
1502
|
+
const out = this.items.map((it) => it.event);
|
|
1503
|
+
this.items = [];
|
|
1504
|
+
this._bytes = 0;
|
|
1505
|
+
return out;
|
|
1506
|
+
}
|
|
1507
|
+
/** Read the queued events without removing them. */
|
|
1508
|
+
snapshot() {
|
|
1509
|
+
return this.items.map((it) => it.event);
|
|
1510
|
+
}
|
|
1511
|
+
/** Drop every queued event without counting it as an eviction. */
|
|
1512
|
+
clear() {
|
|
1513
|
+
this.items = [];
|
|
1514
|
+
this._bytes = 0;
|
|
1515
|
+
}
|
|
1516
|
+
afterGrow() {
|
|
1517
|
+
if (this._bytes > this._peakBytes) this._peakBytes = this._bytes;
|
|
1518
|
+
this.evict();
|
|
1519
|
+
}
|
|
1520
|
+
// Shed events tier by tier, oldest-first within a tier, until the buffer fits
|
|
1521
|
+
// its budget. A single event larger than the whole budget is kept rather than
|
|
1522
|
+
// leaving the queue empty, so the most recent result always survives.
|
|
1523
|
+
evict() {
|
|
1524
|
+
if (this.maxBytes <= 0 || !Number.isFinite(this.maxBytes)) return;
|
|
1525
|
+
for (let tier = 0; tier <= CRITICAL_TIER && this._bytes > this.maxBytes; tier++) {
|
|
1526
|
+
let i = 0;
|
|
1527
|
+
while (i < this.items.length && this._bytes > this.maxBytes) {
|
|
1528
|
+
if (tier === CRITICAL_TIER && this.items.length <= 1) break;
|
|
1529
|
+
const it = this.items[i];
|
|
1530
|
+
if (DROP_TIER[it.event.type] === tier) {
|
|
1531
|
+
this.items.splice(i, 1);
|
|
1532
|
+
this._bytes -= it.size;
|
|
1533
|
+
this._dropped++;
|
|
1534
|
+
this._droppedByType[it.event.type] = (this._droppedByType[it.event.type] ?? 0) + 1;
|
|
1535
|
+
if (tier === CRITICAL_TIER) this._lostResults = true;
|
|
1536
|
+
} else {
|
|
1537
|
+
i++;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
function estimateSize(event) {
|
|
1544
|
+
try {
|
|
1545
|
+
return JSON.stringify(event).length;
|
|
1546
|
+
} catch {
|
|
1547
|
+
return 0;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1400
1551
|
// src/internal/support/limiter.ts
|
|
1401
1552
|
function createLimiter(maxConcurrent) {
|
|
1402
1553
|
const limitValue = Math.max(1, Math.floor(maxConcurrent));
|
|
@@ -1463,8 +1614,8 @@ var StreamManager = class {
|
|
|
1463
1614
|
this.fileHandler = fileHandler;
|
|
1464
1615
|
this.options = options;
|
|
1465
1616
|
this.logger = logger;
|
|
1466
|
-
|
|
1467
|
-
this.
|
|
1617
|
+
/** Guards `reportBufferPressure` so its summary is logged at most once. */
|
|
1618
|
+
this.bufferPressureReported = false;
|
|
1468
1619
|
this.flushTimer = null;
|
|
1469
1620
|
this.flushPromises = [];
|
|
1470
1621
|
this.liveUploadPromises = [];
|
|
@@ -1489,6 +1640,9 @@ var StreamManager = class {
|
|
|
1489
1640
|
this._token = null;
|
|
1490
1641
|
this._auth = null;
|
|
1491
1642
|
this._startPromise = null;
|
|
1643
|
+
const maxBytes = this.options.maxStreamBufferBytes ?? 0;
|
|
1644
|
+
this.pendingEvents = new BoundedEventQueue(maxBytes);
|
|
1645
|
+
this.pendingBeginEvents = new BoundedEventQueue(maxBytes);
|
|
1492
1646
|
}
|
|
1493
1647
|
/** Whether the streaming session is active */
|
|
1494
1648
|
get enabled() {
|
|
@@ -1510,6 +1664,15 @@ var StreamManager = class {
|
|
|
1510
1664
|
get startPromise() {
|
|
1511
1665
|
return this._startPromise;
|
|
1512
1666
|
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Whether buffer pressure forced a test-result (`complete`) event to be
|
|
1669
|
+
* dropped from the live stream. When true the submitter finalizes via the
|
|
1670
|
+
* end-of-run batch (which re-sends the full run from the reporter's own
|
|
1671
|
+
* memory) instead of `/finish`, so the dropped detail is not lost.
|
|
1672
|
+
*/
|
|
1673
|
+
get bufferLostResults() {
|
|
1674
|
+
return this.pendingEvents.lostResults || this.pendingBeginEvents.lostResults;
|
|
1675
|
+
}
|
|
1513
1676
|
/** Begin the streaming session after `onBegin` fires. Non-blocking — the actual handshake runs asynchronously. */
|
|
1514
1677
|
start(startTime, metadata, instanceId, playwrightVersion, reporterVersion, shardInfo, isFullRun, filterDetails) {
|
|
1515
1678
|
this._startPromise = this._doStart(
|
|
@@ -1603,8 +1766,7 @@ var StreamManager = class {
|
|
|
1603
1766
|
this.lastActivityAt = Date.now();
|
|
1604
1767
|
this.scheduleHeartbeat();
|
|
1605
1768
|
if (this.pendingBeginEvents.length > 0) {
|
|
1606
|
-
this.pendingEvents
|
|
1607
|
-
this.pendingBeginEvents = [];
|
|
1769
|
+
this.pendingEvents.prepend(this.pendingBeginEvents.takeAll());
|
|
1608
1770
|
}
|
|
1609
1771
|
if (this.pendingEvents.length > 0) this.flush();
|
|
1610
1772
|
}
|
|
@@ -1628,12 +1790,12 @@ var StreamManager = class {
|
|
|
1628
1790
|
if (this._enabled && this._runId) {
|
|
1629
1791
|
this.queueEvent(event);
|
|
1630
1792
|
} else {
|
|
1631
|
-
this.pendingBeginEvents.
|
|
1793
|
+
this.pendingBeginEvents.enqueue(event);
|
|
1632
1794
|
}
|
|
1633
1795
|
}
|
|
1634
1796
|
/** Queue a test-case event. Triggers an immediate flush when the batch size is reached, otherwise schedules a timer-based flush. */
|
|
1635
1797
|
queueEvent(event) {
|
|
1636
|
-
this.pendingEvents.
|
|
1798
|
+
this.pendingEvents.enqueue(event);
|
|
1637
1799
|
if (this.pendingEvents.length >= this.options.streamingBatchSize) {
|
|
1638
1800
|
this.flush();
|
|
1639
1801
|
} else if (!this.flushTimer) {
|
|
@@ -1646,8 +1808,8 @@ var StreamManager = class {
|
|
|
1646
1808
|
clearTimeout(this.flushTimer);
|
|
1647
1809
|
this.flushTimer = null;
|
|
1648
1810
|
}
|
|
1649
|
-
if (this.pendingEvents.
|
|
1650
|
-
const events = this.pendingEvents.
|
|
1811
|
+
if (this.pendingEvents.isEmpty || !this._enabled || !this._runId) return null;
|
|
1812
|
+
const events = this.pendingEvents.takeAll();
|
|
1651
1813
|
const promise = this.httpClient.postJSON(`/api/test-runs/${this._runId}/events`, { streamToken: this._token, testCases: events }, this._auth).then(
|
|
1652
1814
|
() => {
|
|
1653
1815
|
this.retryCount = 0;
|
|
@@ -1655,7 +1817,7 @@ var StreamManager = class {
|
|
|
1655
1817
|
return true;
|
|
1656
1818
|
},
|
|
1657
1819
|
(error) => {
|
|
1658
|
-
this.pendingEvents
|
|
1820
|
+
this.pendingEvents.prepend(events);
|
|
1659
1821
|
this.scheduleRetry(errorMessage(error));
|
|
1660
1822
|
return false;
|
|
1661
1823
|
}
|
|
@@ -1676,7 +1838,7 @@ var StreamManager = class {
|
|
|
1676
1838
|
const buffered = this.streamBuffer.load();
|
|
1677
1839
|
if (buffered.length > 0) {
|
|
1678
1840
|
this.streamBuffer.clear();
|
|
1679
|
-
this.pendingEvents
|
|
1841
|
+
this.pendingEvents.prepend(buffered);
|
|
1680
1842
|
}
|
|
1681
1843
|
if (this.pendingEvents.length > 0) this.flush();
|
|
1682
1844
|
}, delay);
|
|
@@ -1728,26 +1890,33 @@ var StreamManager = class {
|
|
|
1728
1890
|
}
|
|
1729
1891
|
/** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off. */
|
|
1730
1892
|
async drain() {
|
|
1893
|
+
try {
|
|
1894
|
+
await this._drain();
|
|
1895
|
+
} finally {
|
|
1896
|
+
this.reportBufferPressure();
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
async _drain() {
|
|
1731
1900
|
this.stopHeartbeat();
|
|
1732
1901
|
this.clearRetryTimer();
|
|
1733
1902
|
if (!this._enabled) {
|
|
1734
|
-
this.pendingEvents
|
|
1903
|
+
this.pendingEvents.clear();
|
|
1735
1904
|
this.flushPromises = [];
|
|
1736
1905
|
return;
|
|
1737
1906
|
}
|
|
1738
1907
|
try {
|
|
1739
1908
|
const MAX_ATTEMPTS = 10;
|
|
1740
1909
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
1741
|
-
if (this._enabled && this.pendingEvents.
|
|
1910
|
+
if (this._enabled && !this.pendingEvents.isEmpty) this.flush();
|
|
1742
1911
|
if (this.flushPromises.length > 0) {
|
|
1743
1912
|
await Promise.allSettled(this.flushPromises);
|
|
1744
1913
|
this.flushPromises = [];
|
|
1745
1914
|
}
|
|
1746
|
-
if (this.pendingEvents.
|
|
1915
|
+
if (this.pendingEvents.isEmpty) {
|
|
1747
1916
|
const buffered = this.streamBuffer.load();
|
|
1748
1917
|
if (buffered.length > 0) {
|
|
1749
|
-
this.pendingEvents = buffered;
|
|
1750
1918
|
this.streamBuffer.clear();
|
|
1919
|
+
this.pendingEvents.prepend(buffered);
|
|
1751
1920
|
continue;
|
|
1752
1921
|
}
|
|
1753
1922
|
return;
|
|
@@ -1762,17 +1931,41 @@ var StreamManager = class {
|
|
|
1762
1931
|
);
|
|
1763
1932
|
await new Promise((resolve5) => setTimeout(resolve5, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
|
|
1764
1933
|
}
|
|
1765
|
-
if (this.pendingEvents.
|
|
1934
|
+
if (!this.pendingEvents.isEmpty) {
|
|
1935
|
+
const remaining = this.pendingEvents.takeAll();
|
|
1766
1936
|
this.logger.warn(
|
|
1767
|
-
`Could not deliver ${
|
|
1937
|
+
`Could not deliver ${remaining.length} live event(s) to the dashboard \u2014 continuing with the end-of-run submit.`
|
|
1768
1938
|
);
|
|
1769
|
-
this.streamBuffer.append(
|
|
1770
|
-
this.pendingEvents = [];
|
|
1939
|
+
this.streamBuffer.append(remaining);
|
|
1771
1940
|
}
|
|
1772
1941
|
} finally {
|
|
1773
1942
|
this.clearRetryTimer();
|
|
1774
1943
|
}
|
|
1775
1944
|
}
|
|
1945
|
+
// Emit a single summary when buffer pressure shed events, so a full disk or a
|
|
1946
|
+
// stalled server does not silently swallow data. Reported once per drain.
|
|
1947
|
+
reportBufferPressure() {
|
|
1948
|
+
if (this.bufferPressureReported) return;
|
|
1949
|
+
const dropped = this.pendingEvents.droppedCount + this.pendingBeginEvents.droppedCount;
|
|
1950
|
+
if (dropped === 0) return;
|
|
1951
|
+
this.bufferPressureReported = true;
|
|
1952
|
+
const byType = this.pendingEvents.droppedByType;
|
|
1953
|
+
const beginByType = this.pendingBeginEvents.droppedByType;
|
|
1954
|
+
const count = (type) => (byType[type] ?? 0) + (beginByType[type] ?? 0);
|
|
1955
|
+
const steps = count("step-begin") + count("step-end");
|
|
1956
|
+
const begins = count("begin");
|
|
1957
|
+
const results = count("complete");
|
|
1958
|
+
const limitMb = ((this.options.maxStreamBufferBytes ?? 0) / (1024 * 1024)).toFixed(0);
|
|
1959
|
+
if (results > 0) {
|
|
1960
|
+
this.logger.warn(
|
|
1961
|
+
`Stream buffer hit its ${limitMb} MB limit \u2014 dropped ${dropped} live event(s), including ${results} test result(s) that will not show live on the dashboard (the run's final counts stay accurate, and the end-of-run submit still carries the full run). Raise \`maxStreamBufferBytes\` or check the dashboard connection.`
|
|
1962
|
+
);
|
|
1963
|
+
} else {
|
|
1964
|
+
this.logger.warn(
|
|
1965
|
+
`Stream buffer hit its ${limitMb} MB limit \u2014 dropped ${dropped} live progress event(s) (${steps} step, ${begins} begin) to stay within budget. No test results were lost.`
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1776
1969
|
/** Schedule a live upload of trace and attachment files for a test case. Skips cases with no files. Concurrency is limited to 2 simultaneous uploads. */
|
|
1777
1970
|
scheduleLiveUpload(tc) {
|
|
1778
1971
|
const hasTrace = !!this.options.uploadTraces && this.fileHandler.findTraceFiles(tc).some((p) => fs8.existsSync(p));
|
|
@@ -2190,6 +2383,22 @@ function readSelectionStamp(env = process.env) {
|
|
|
2190
2383
|
return { key, version, resolvedHash, resolvedCount };
|
|
2191
2384
|
}
|
|
2192
2385
|
|
|
2386
|
+
// src/internal/support/run-mode.ts
|
|
2387
|
+
var PW_UI_FLAGS = ["--ui", "--ui-host", "--ui-port"];
|
|
2388
|
+
function isUiMode(argv = process.argv) {
|
|
2389
|
+
const args = argv.slice(2);
|
|
2390
|
+
const testIdx = args.indexOf("test");
|
|
2391
|
+
const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
|
|
2392
|
+
return rest.some((tok) => PW_UI_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
|
|
2393
|
+
}
|
|
2394
|
+
var PW_LIST_FLAGS = ["--list"];
|
|
2395
|
+
function isListMode(argv = process.argv) {
|
|
2396
|
+
const args = argv.slice(2);
|
|
2397
|
+
const testIdx = args.indexOf("test");
|
|
2398
|
+
const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
|
|
2399
|
+
return rest.some((tok) => PW_LIST_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2193
2402
|
// src/public/global-setup.ts
|
|
2194
2403
|
var path11 = __toESM(require("path"));
|
|
2195
2404
|
var fs12 = __toESM(require("fs"));
|
|
@@ -2243,15 +2452,6 @@ function isDueForAriaSample(testInfo) {
|
|
|
2243
2452
|
return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
|
|
2244
2453
|
}
|
|
2245
2454
|
|
|
2246
|
-
// src/internal/support/run-mode.ts
|
|
2247
|
-
var PW_UI_FLAGS = ["--ui", "--ui-host", "--ui-port"];
|
|
2248
|
-
function isUiMode(argv = process.argv) {
|
|
2249
|
-
const args = argv.slice(2);
|
|
2250
|
-
const testIdx = args.indexOf("test");
|
|
2251
|
-
const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
|
|
2252
|
-
return rest.some((tok) => PW_UI_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
2455
|
// src/public/global-setup.ts
|
|
2256
2456
|
function createGlobalSetup(options, userSetup) {
|
|
2257
2457
|
return async function globalSetupFn(config) {
|
|
@@ -2280,6 +2480,11 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2280
2480
|
if (userSetup) return userSetup(config);
|
|
2281
2481
|
return;
|
|
2282
2482
|
}
|
|
2483
|
+
if (isListMode()) {
|
|
2484
|
+
logger.debug("List mode detected \u2014 skipping run registration.");
|
|
2485
|
+
if (userSetup) return userSetup(config);
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2283
2488
|
if (opts.enabled === false || !opts.serverUrl) {
|
|
2284
2489
|
logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
|
|
2285
2490
|
if (userSetup) return userSetup(config);
|
|
@@ -2429,12 +2634,11 @@ function resolveSetupModule() {
|
|
|
2429
2634
|
}
|
|
2430
2635
|
function wrapConfig(config, piwiOptions) {
|
|
2431
2636
|
if (piwiOptions) applyOptionsToEnv(piwiOptions);
|
|
2432
|
-
const globalSetupModules = [];
|
|
2637
|
+
const globalSetupModules = [resolveSetupModule()];
|
|
2433
2638
|
if (config.globalSetup) {
|
|
2434
2639
|
const orig = Array.isArray(config.globalSetup) ? config.globalSetup : [config.globalSetup];
|
|
2435
2640
|
globalSetupModules.push(...orig);
|
|
2436
2641
|
}
|
|
2437
|
-
globalSetupModules.push(resolveSetupModule());
|
|
2438
2642
|
const forwarded = {};
|
|
2439
2643
|
const failOnFlaky = piwiOptions?.failOnFlakyTests ?? readBool(process.env[PIWI_ENV_KEYS.failOnFlakyTests]);
|
|
2440
2644
|
if (failOnFlaky === true) forwarded.failOnFlakyTests = true;
|
|
@@ -2807,7 +3011,7 @@ var RunSubmitter = class {
|
|
|
2807
3011
|
}
|
|
2808
3012
|
if (!sm) await this.recovery.tryUpload(this.httpClient, auth);
|
|
2809
3013
|
let outcome = { done: false, output: null };
|
|
2810
|
-
if (sm?.enabled && sm?.runId != null) {
|
|
3014
|
+
if (sm?.enabled && sm?.runId != null && !sm.bufferLostResults) {
|
|
2811
3015
|
outcome = await this.tryFinishStreaming(run, overallStatus, duration, auth);
|
|
2812
3016
|
}
|
|
2813
3017
|
if (!outcome.done && (this.hasReports(run) || run.options.uploadTraces)) {
|
|
@@ -3834,6 +4038,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
3834
4038
|
this.setupSteps = [];
|
|
3835
4039
|
this.options = resolveOptions(rawOptions);
|
|
3836
4040
|
this.enabled = this.options.enabled !== false && !!this.options.serverUrl;
|
|
4041
|
+
this.listMode = isListMode();
|
|
3837
4042
|
this.viaDesktopApp = usedDesktopDiscovery();
|
|
3838
4043
|
this.runLabel = this.options.runLabel || detectCiRunLabel();
|
|
3839
4044
|
this.instanceId = computeInstanceId(this.options.projectName, this.runLabel);
|
|
@@ -3875,6 +4080,10 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
3875
4080
|
}
|
|
3876
4081
|
/** Playwright reporter hook: called once at the start of the test run */
|
|
3877
4082
|
onBegin(config, suite) {
|
|
4083
|
+
if (this.listMode) {
|
|
4084
|
+
this.logger.debug("List mode (--list) detected \u2014 no run registered or report uploaded.");
|
|
4085
|
+
return;
|
|
4086
|
+
}
|
|
3878
4087
|
if (!this.enabled) {
|
|
3879
4088
|
this.logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
|
|
3880
4089
|
return;
|
|
@@ -4156,7 +4365,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
4156
4365
|
}
|
|
4157
4366
|
/** Playwright reporter hook: called when the full test run finishes */
|
|
4158
4367
|
async onEnd(result) {
|
|
4159
|
-
if (!this.enabled) return;
|
|
4368
|
+
if (this.listMode || !this.enabled) return;
|
|
4160
4369
|
const unrunReason = resolveUnrunReason(result?.status, {
|
|
4161
4370
|
maxFailures: this.maxFailures,
|
|
4162
4371
|
failures: this.failedTests + this.timedOutTests
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piwitests/reporter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Playwright reporter that streams results, traces and HTML reports to a Piwi Dashboard instance",
|
|
5
5
|
"url": "https://github.com/PiwiTests/platform",
|
|
6
6
|
"homepage": "https://piwitests.dev",
|