@piwitests/reporter 0.4.4 → 0.6.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 +37 -26
- package/dist/global-setup-module.js +2 -2
- package/dist/index.d.ts +15 -8
- package/dist/index.js +23 -5
- package/dist/internal/capture/attachments.d.ts +19 -0
- package/dist/internal/capture/attachments.js +22 -0
- package/dist/{fixtures.d.ts → internal/capture/capture-fixtures.d.ts} +6 -4
- package/dist/internal/capture/capture-fixtures.js +495 -0
- package/dist/internal/capture/locator-healing.d.ts +152 -0
- package/dist/internal/capture/locator-healing.js +668 -0
- package/dist/internal/collect/error-text.d.ts +18 -0
- package/dist/internal/collect/error-text.js +79 -0
- package/dist/{metadata-collector.d.ts → internal/collect/metadata-collector.d.ts} +2 -2
- package/dist/{metadata-collector.js → internal/collect/metadata-collector.js} +12 -9
- package/dist/{skip-classify.d.ts → internal/collect/skip-classify.d.ts} +1 -1
- package/dist/{step-analyzer.d.ts → internal/collect/step-analyzer.d.ts} +6 -0
- package/dist/{step-analyzer.js → internal/collect/step-analyzer.js} +7 -2
- package/dist/internal/config/env.d.ts +45 -0
- package/dist/{config.js → internal/config/env.js} +59 -33
- package/dist/internal/files/compression.js +69 -0
- package/dist/{file-handler.d.ts → internal/files/file-handler.d.ts} +4 -4
- package/dist/{file-handler.js → internal/files/file-handler.js} +61 -29
- package/dist/{crash-recovery.d.ts → internal/streaming/crash-recovery.d.ts} +5 -5
- package/dist/{crash-recovery.js → internal/streaming/crash-recovery.js} +9 -8
- package/dist/{stream-buffer.d.ts → internal/streaming/stream-buffer.d.ts} +2 -2
- package/dist/{stream-buffer.js → internal/streaming/stream-buffer.js} +5 -5
- package/dist/{stream-manager.d.ts → internal/streaming/stream-manager.d.ts} +22 -8
- package/dist/{stream-manager.js → internal/streaming/stream-manager.js} +66 -12
- package/dist/{run-submitter.d.ts → internal/submit/run-submitter.d.ts} +7 -7
- package/dist/{run-submitter.js → internal/submit/run-submitter.js} +13 -9
- package/dist/{serializer.d.ts → internal/submit/serializer.d.ts} +1 -1
- package/dist/{serializer.js → internal/submit/serializer.js} +2 -0
- package/dist/{uploader.d.ts → internal/submit/uploader.d.ts} +9 -8
- package/dist/{uploader.js → internal/submit/uploader.js} +45 -10
- package/dist/internal/support/ci.d.ts +2 -0
- package/dist/internal/support/ci.js +32 -0
- package/dist/internal/support/cli-filters.d.ts +1 -0
- package/dist/internal/support/cli-filters.js +51 -0
- package/dist/internal/support/errors.d.ts +8 -0
- package/dist/internal/support/errors.js +15 -0
- package/dist/internal/support/instance-id.d.ts +4 -0
- package/dist/internal/support/instance-id.js +48 -0
- package/dist/internal/support/limiter.d.ts +2 -0
- package/dist/internal/support/limiter.js +27 -0
- package/dist/internal/support/setup-file.d.ts +13 -0
- package/dist/internal/support/setup-file.js +61 -0
- package/dist/internal/support/source-snippet.d.ts +12 -0
- package/dist/internal/support/source-snippet.js +97 -0
- package/dist/internal/support/worker-index.d.ts +7 -0
- package/dist/internal/support/worker-index.js +14 -0
- package/dist/{http-client.d.ts → internal/transport/http-client.d.ts} +11 -1
- package/dist/{http-client.js → internal/transport/http-client.js} +57 -10
- package/dist/{config-wrapper.d.ts → public/config-wrapper.d.ts} +1 -1
- package/dist/{config-wrapper.js → public/config-wrapper.js} +4 -4
- package/dist/public/global-setup.d.ts +13 -0
- package/dist/public/global-setup.js +146 -0
- package/dist/{config.d.ts → public/options.d.ts} +18 -44
- package/dist/public/options.js +2 -0
- package/dist/{reporter.d.ts → public/reporter.d.ts} +3 -1
- package/dist/{reporter.js → public/reporter.js} +66 -34
- package/dist/types/collected.d.ts +96 -0
- package/dist/types/collected.js +10 -0
- package/dist/types/wire.d.ts +174 -0
- package/dist/types/wire.js +14 -0
- package/dist/types.d.ts +8 -248
- package/dist/types.js +23 -10
- package/package.json +1 -6
- package/dist/compression.js +0 -39
- package/dist/fixtures.js +0 -156
- package/dist/helpers.d.ts +0 -44
- package/dist/helpers.js +0 -288
- /package/dist/{skip-classify.js → internal/collect/skip-classify.js} +0 -0
- /package/dist/{compression.d.ts → internal/files/compression.d.ts} +0 -0
- /package/dist/{logger.d.ts → internal/support/logger.d.ts} +0 -0
- /package/dist/{logger.js → internal/support/logger.js} +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { HttpClient } from '
|
|
2
|
-
import { Logger } from '
|
|
1
|
+
import type { HttpClient } from '../transport/http-client.js';
|
|
2
|
+
import { Logger } from '../support/logger.js';
|
|
3
3
|
/**
|
|
4
4
|
* Persists a test-run payload to disk when all upload strategies fail, so the
|
|
5
5
|
* data can be retried on the next run.
|
|
6
6
|
*/
|
|
7
7
|
export declare class CrashRecovery {
|
|
8
|
-
private filePath;
|
|
9
8
|
private readonly logger;
|
|
9
|
+
private readonly filePath;
|
|
10
10
|
/**
|
|
11
11
|
* @param projectName Used to derive the temp-file name so recovery data is project-scoped.
|
|
12
12
|
* @param logger Prefixed logger.
|
|
13
13
|
*/
|
|
14
14
|
constructor(projectName: string, logger?: Logger);
|
|
15
15
|
/** Serialise the payload to a temp file for later retry */
|
|
16
|
-
save(data: Record<string,
|
|
16
|
+
save(data: Record<string, unknown>): void;
|
|
17
17
|
/** Read the saved payload from disk, or return `null` if none exists */
|
|
18
|
-
load(): Record<string,
|
|
18
|
+
load(): Record<string, unknown> | null;
|
|
19
19
|
/** Delete the recovery file from disk */
|
|
20
20
|
clear(): void;
|
|
21
21
|
/** Attempt to submit a previously saved payload. Clears the recovery file on success. */
|
|
@@ -34,11 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.CrashRecovery = void 0;
|
|
37
|
-
const path = __importStar(require("path"));
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
const errors_js_1 = require("../support/errors.js");
|
|
39
|
+
const os = __importStar(require("node:os"));
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const instance_id_js_1 = require("../support/instance-id.js");
|
|
42
|
+
const logger_js_1 = require("../support/logger.js");
|
|
42
43
|
/**
|
|
43
44
|
* Persists a test-run payload to disk when all upload strategies fail, so the
|
|
44
45
|
* data can be retried on the next run.
|
|
@@ -50,7 +51,7 @@ class CrashRecovery {
|
|
|
50
51
|
*/
|
|
51
52
|
constructor(projectName, logger = new logger_js_1.Logger()) {
|
|
52
53
|
this.logger = logger;
|
|
53
|
-
this.filePath = path.join(os.tmpdir(), `piwi-dashboard-recovery-${(0,
|
|
54
|
+
this.filePath = path.join(os.tmpdir(), `piwi-dashboard-recovery-${(0, instance_id_js_1.hashForProject)(projectName)}.json`);
|
|
54
55
|
}
|
|
55
56
|
/** Serialise the payload to a temp file for later retry */
|
|
56
57
|
save(data) {
|
|
@@ -59,7 +60,7 @@ class CrashRecovery {
|
|
|
59
60
|
this.logger.info('Saved recovery data for later upload');
|
|
60
61
|
}
|
|
61
62
|
catch (error) {
|
|
62
|
-
this.logger.error(`Failed to save recovery data: ${error
|
|
63
|
+
this.logger.error(`Failed to save recovery data: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
/** Read the saved payload from disk, or return `null` if none exists */
|
|
@@ -98,7 +99,7 @@ class CrashRecovery {
|
|
|
98
99
|
this.clear();
|
|
99
100
|
}
|
|
100
101
|
catch (error) {
|
|
101
|
-
this.logger.warn(`Could not upload saved test data: ${error
|
|
102
|
+
this.logger.warn(`Could not upload saved test data: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { StreamEvent } from '
|
|
1
|
+
import type { StreamEvent } from '../../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Persistent JSONL buffer on disk. Events are appended to a temp file so they
|
|
4
4
|
* survive a crash and can be replayed when the reporter restarts.
|
|
5
5
|
*/
|
|
6
6
|
export declare class StreamBuffer {
|
|
7
|
-
private filePath;
|
|
7
|
+
private readonly filePath;
|
|
8
8
|
constructor(projectName: string);
|
|
9
9
|
/** Append one or more events to the on-disk buffer */
|
|
10
10
|
append(events: StreamEvent[]): void;
|
|
@@ -34,17 +34,17 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.StreamBuffer = void 0;
|
|
37
|
-
const path = __importStar(require("path"));
|
|
38
|
-
const os = __importStar(require("os"));
|
|
39
|
-
const fs = __importStar(require("fs"));
|
|
40
|
-
const
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
const os = __importStar(require("node:os"));
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const instance_id_js_1 = require("../support/instance-id.js");
|
|
41
41
|
/**
|
|
42
42
|
* Persistent JSONL buffer on disk. Events are appended to a temp file so they
|
|
43
43
|
* survive a crash and can be replayed when the reporter restarts.
|
|
44
44
|
*/
|
|
45
45
|
class StreamBuffer {
|
|
46
46
|
constructor(projectName) {
|
|
47
|
-
this.filePath = path.join(os.tmpdir(), `piwi-dashboard-stream-${(0,
|
|
47
|
+
this.filePath = path.join(os.tmpdir(), `piwi-dashboard-stream-${(0, instance_id_js_1.hashForProject)(projectName)}.jsonl`);
|
|
48
48
|
}
|
|
49
49
|
/** Append one or more events to the on-disk buffer */
|
|
50
50
|
append(events) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { PiwiDashboardOptions, ShardInfo } from '
|
|
2
|
-
import { HttpClient } from '
|
|
3
|
-
import { StreamBuffer } from './stream-buffer.js';
|
|
4
|
-
import { CrashRecovery } from './crash-recovery.js';
|
|
5
|
-
import { Uploader } from '
|
|
6
|
-
import { FileHandler } from '
|
|
7
|
-
import { Logger } from '
|
|
8
|
-
import type { CollectedTestCase, StreamEvent, FilterDetails } from '
|
|
1
|
+
import type { PiwiDashboardOptions, ShardInfo } from '../../public/options.js';
|
|
2
|
+
import type { HttpClient } from '../transport/http-client.js';
|
|
3
|
+
import type { StreamBuffer } from './stream-buffer.js';
|
|
4
|
+
import type { CrashRecovery } from './crash-recovery.js';
|
|
5
|
+
import type { Uploader } from '../submit/uploader.js';
|
|
6
|
+
import type { FileHandler } from '../files/file-handler.js';
|
|
7
|
+
import { Logger } from '../support/logger.js';
|
|
8
|
+
import type { CollectedTestCase, StreamEvent, FilterDetails } from '../../types.js';
|
|
9
9
|
/**
|
|
10
10
|
* Manages the streaming protocol: queues events (begin / complete), flushes
|
|
11
11
|
* them in batches, schedules retries on failure, and handles per-test-case
|
|
@@ -28,6 +28,16 @@ export declare class StreamManager {
|
|
|
28
28
|
private retryCount;
|
|
29
29
|
private retryTimer;
|
|
30
30
|
private readonly maxRetryDelay;
|
|
31
|
+
/**
|
|
32
|
+
* Idle heartbeat: while the run is open but no events are flowing (e.g. a single
|
|
33
|
+
* long test, or `beforeAll` setup), a periodic ping keeps the server's activity
|
|
34
|
+
* timestamp fresh so the stale-run reaper can tell a live run from a crashed one.
|
|
35
|
+
* The interval must stay well below the server's stale timeout.
|
|
36
|
+
*/
|
|
37
|
+
private heartbeatTimer;
|
|
38
|
+
private heartbeatStopped;
|
|
39
|
+
private lastActivityAt;
|
|
40
|
+
private readonly heartbeatInterval;
|
|
31
41
|
/** Tracks cases whose files have already been uploaded live, so `uploadRemaining` can skip them. */
|
|
32
42
|
private readonly uploadedCaseFiles;
|
|
33
43
|
private _enabled;
|
|
@@ -65,6 +75,10 @@ export declare class StreamManager {
|
|
|
65
75
|
/** Flush all pending events to the server. Returns a promise that resolves to `true` on success or `false` on failure (events are re-queued for retry). */
|
|
66
76
|
flush(): Promise<boolean> | null;
|
|
67
77
|
private scheduleRetry;
|
|
78
|
+
private scheduleHeartbeat;
|
|
79
|
+
private sendHeartbeat;
|
|
80
|
+
/** Stop the idle heartbeat permanently (called when the run is wrapping up). */
|
|
81
|
+
private stopHeartbeat;
|
|
68
82
|
/** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off. */
|
|
69
83
|
drain(): Promise<void>;
|
|
70
84
|
/** 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. */
|
|
@@ -34,9 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.StreamManager = void 0;
|
|
37
|
-
const fs = __importStar(require("fs"));
|
|
38
|
-
const
|
|
39
|
-
const
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const errors_js_1 = require("../support/errors.js");
|
|
39
|
+
const http_client_js_1 = require("../transport/http-client.js");
|
|
40
|
+
const logger_js_1 = require("../support/logger.js");
|
|
41
|
+
const limiter_js_1 = require("../support/limiter.js");
|
|
42
|
+
const setup_file_js_1 = require("../support/setup-file.js");
|
|
40
43
|
/**
|
|
41
44
|
* Manages the streaming protocol: queues events (begin / complete), flushes
|
|
42
45
|
* them in batches, schedules retries on failure, and handles per-test-case
|
|
@@ -85,10 +88,20 @@ class StreamManager {
|
|
|
85
88
|
this.flushTimer = null;
|
|
86
89
|
this.flushPromises = [];
|
|
87
90
|
this.liveUploadPromises = [];
|
|
88
|
-
this.limitLiveUpload = (0,
|
|
91
|
+
this.limitLiveUpload = (0, limiter_js_1.createLimiter)(2);
|
|
89
92
|
this.retryCount = 0;
|
|
90
93
|
this.retryTimer = null;
|
|
91
94
|
this.maxRetryDelay = 30000;
|
|
95
|
+
/**
|
|
96
|
+
* Idle heartbeat: while the run is open but no events are flowing (e.g. a single
|
|
97
|
+
* long test, or `beforeAll` setup), a periodic ping keeps the server's activity
|
|
98
|
+
* timestamp fresh so the stale-run reaper can tell a live run from a crashed one.
|
|
99
|
+
* The interval must stay well below the server's stale timeout.
|
|
100
|
+
*/
|
|
101
|
+
this.heartbeatTimer = null;
|
|
102
|
+
this.heartbeatStopped = false;
|
|
103
|
+
this.lastActivityAt = 0;
|
|
104
|
+
this.heartbeatInterval = 15000;
|
|
92
105
|
/** Tracks cases whose files have already been uploaded live, so `uploadRemaining` can skip them. */
|
|
93
106
|
this.uploadedCaseFiles = new WeakSet();
|
|
94
107
|
this._enabled = false;
|
|
@@ -102,7 +115,7 @@ class StreamManager {
|
|
|
102
115
|
this._startPromise = this._doStart(startTime, metadata, instanceId, playwrightVersion, shardInfo, isFullRun, filterDetails);
|
|
103
116
|
}
|
|
104
117
|
async _doStart(startTime, metadata, instanceId, playwrightVersion, shardInfo, isFullRun, filterDetails) {
|
|
105
|
-
const setupInfo = (0,
|
|
118
|
+
const setupInfo = (0, setup_file_js_1.readSetupInfo)(this.options.projectName);
|
|
106
119
|
try {
|
|
107
120
|
this._auth = await this.httpClient.resolveAuth(this.options);
|
|
108
121
|
await this.recovery.tryUpload(this.httpClient, this._auth);
|
|
@@ -122,11 +135,11 @@ class StreamManager {
|
|
|
122
135
|
filterDetails,
|
|
123
136
|
}, this._auth);
|
|
124
137
|
}
|
|
125
|
-
catch (
|
|
138
|
+
catch (error) {
|
|
126
139
|
// Stale setupInfo — the run was cancelled (e.g. by crash recovery submit).
|
|
127
140
|
// Fall back to creating a fresh run instead of silently disabling streaming.
|
|
128
|
-
if (!
|
|
129
|
-
throw
|
|
141
|
+
if (!(error instanceof http_client_js_1.HttpError && error.status === 409))
|
|
142
|
+
throw error;
|
|
130
143
|
this.logger.debug(`Setup info expired, creating fresh run...`);
|
|
131
144
|
response = await this.httpClient.postJSON('/api/test-runs/start', {
|
|
132
145
|
projectName: this.options.projectName,
|
|
@@ -165,6 +178,8 @@ class StreamManager {
|
|
|
165
178
|
this._token = response.streamToken;
|
|
166
179
|
this._enabled = true;
|
|
167
180
|
this.logger.info(`Streaming enabled. Run ID: ${response.runId}`);
|
|
181
|
+
this.lastActivityAt = Date.now();
|
|
182
|
+
this.scheduleHeartbeat();
|
|
168
183
|
if (this.pendingBeginEvents.length > 0) {
|
|
169
184
|
this.pendingEvents = [...this.pendingBeginEvents, ...this.pendingEvents];
|
|
170
185
|
this.pendingBeginEvents = [];
|
|
@@ -173,7 +188,7 @@ class StreamManager {
|
|
|
173
188
|
}
|
|
174
189
|
}
|
|
175
190
|
catch (error) {
|
|
176
|
-
this.logger.debug(`Streaming not available: ${error
|
|
191
|
+
this.logger.debug(`Streaming not available: ${(0, errors_js_1.errorMessage)(error)}. Will use batch mode.`);
|
|
177
192
|
this._enabled = false;
|
|
178
193
|
}
|
|
179
194
|
}
|
|
@@ -213,6 +228,7 @@ class StreamManager {
|
|
|
213
228
|
.postJSON(`/api/test-runs/${this._runId}/events`, { streamToken: this._token, testCases: events }, this._auth)
|
|
214
229
|
.then(() => {
|
|
215
230
|
this.retryCount = 0;
|
|
231
|
+
this.lastActivityAt = Date.now();
|
|
216
232
|
return true;
|
|
217
233
|
}, () => {
|
|
218
234
|
this.pendingEvents = events.concat(this.pendingEvents);
|
|
@@ -239,8 +255,46 @@ class StreamManager {
|
|
|
239
255
|
this.flush();
|
|
240
256
|
}, delay);
|
|
241
257
|
}
|
|
258
|
+
// Schedule the next idle heartbeat. Self-rescheduling; cleared by stopHeartbeat.
|
|
259
|
+
scheduleHeartbeat() {
|
|
260
|
+
if (this.heartbeatStopped || this.heartbeatTimer)
|
|
261
|
+
return;
|
|
262
|
+
this.heartbeatTimer = setTimeout(() => {
|
|
263
|
+
this.heartbeatTimer = null;
|
|
264
|
+
void this.sendHeartbeat();
|
|
265
|
+
}, this.heartbeatInterval);
|
|
266
|
+
}
|
|
267
|
+
// Ping the server only when the run has actually been idle. Real event traffic
|
|
268
|
+
// already bumps the server's activity timestamp, so a recent flush (or pending
|
|
269
|
+
// events about to flush) makes the ping redundant — skip it and reschedule.
|
|
270
|
+
async sendHeartbeat() {
|
|
271
|
+
if (this.heartbeatStopped || !this._enabled || !this._runId || !this._token)
|
|
272
|
+
return;
|
|
273
|
+
const idleFor = Date.now() - this.lastActivityAt;
|
|
274
|
+
if (idleFor < this.heartbeatInterval || this.pendingEvents.length > 0) {
|
|
275
|
+
this.scheduleHeartbeat();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
await this.httpClient.postJSON(`/api/test-runs/${this._runId}/heartbeat`, { streamToken: this._token }, this._auth);
|
|
280
|
+
this.lastActivityAt = Date.now();
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
this.logger.debug(`Heartbeat failed: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
284
|
+
}
|
|
285
|
+
this.scheduleHeartbeat();
|
|
286
|
+
}
|
|
287
|
+
/** Stop the idle heartbeat permanently (called when the run is wrapping up). */
|
|
288
|
+
stopHeartbeat() {
|
|
289
|
+
this.heartbeatStopped = true;
|
|
290
|
+
if (this.heartbeatTimer) {
|
|
291
|
+
clearTimeout(this.heartbeatTimer);
|
|
292
|
+
this.heartbeatTimer = null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
242
295
|
/** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off. */
|
|
243
296
|
async drain() {
|
|
297
|
+
this.stopHeartbeat();
|
|
244
298
|
if (!this._enabled) {
|
|
245
299
|
this.pendingEvents = [];
|
|
246
300
|
this.flushPromises = [];
|
|
@@ -303,9 +357,9 @@ class StreamManager {
|
|
|
303
357
|
return;
|
|
304
358
|
}
|
|
305
359
|
catch (error) {
|
|
306
|
-
const retryable = error.
|
|
360
|
+
const retryable = error instanceof http_client_js_1.HttpError && error.status === 404;
|
|
307
361
|
if (!retryable || attempt === delays.length - 1) {
|
|
308
|
-
this.logger.debug(`Live file upload failed for "${tc.title}": ${error
|
|
362
|
+
this.logger.debug(`Live file upload failed for "${tc.title}": ${(0, errors_js_1.errorMessage)(error)}`);
|
|
309
363
|
return; // The end-of-run pass retries cases that failed here
|
|
310
364
|
}
|
|
311
365
|
}
|
|
@@ -330,7 +384,7 @@ class StreamManager {
|
|
|
330
384
|
this.uploadedCaseFiles.add(tc);
|
|
331
385
|
}
|
|
332
386
|
catch (error) {
|
|
333
|
-
this.logger.warn(`Failed to upload files for "${tc.title}": ${error
|
|
387
|
+
this.logger.warn(`Failed to upload files for "${tc.title}": ${(0, errors_js_1.errorMessage)(error)}`);
|
|
334
388
|
}
|
|
335
389
|
}
|
|
336
390
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { FullResult } from '@playwright/test/reporter';
|
|
2
|
-
import type { PiwiDashboardOptions, ShardInfo } from '
|
|
3
|
-
import { HttpClient } from '
|
|
4
|
-
import { Uploader } from './uploader.js';
|
|
5
|
-
import { CrashRecovery } from '
|
|
6
|
-
import { StreamManager } from '
|
|
7
|
-
import { Logger } from '
|
|
8
|
-
import type { CollectedTestCase, SetupStep, FilterDetails } from '
|
|
2
|
+
import type { PiwiDashboardOptions, ShardInfo } from '../../public/options.js';
|
|
3
|
+
import type { HttpClient } from '../transport/http-client.js';
|
|
4
|
+
import type { Uploader } from './uploader.js';
|
|
5
|
+
import type { CrashRecovery } from '../streaming/crash-recovery.js';
|
|
6
|
+
import type { StreamManager } from '../streaming/stream-manager.js';
|
|
7
|
+
import { Logger } from '../support/logger.js';
|
|
8
|
+
import type { CollectedTestCase, SetupStep, FilterDetails } from '../../types.js';
|
|
9
9
|
/**
|
|
10
10
|
* Snapshot of everything the reporter has collected by `onEnd`, handed off to
|
|
11
11
|
* the `RunSubmitter` so the reporter itself stays a thin collect-and-hand-off
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RunSubmitter = void 0;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const errors_js_1 = require("../support/errors.js");
|
|
5
|
+
const http_client_js_1 = require("../transport/http-client.js");
|
|
6
|
+
const logger_js_1 = require("../support/logger.js");
|
|
7
|
+
const step_analyzer_js_1 = require("../collect/step-analyzer.js");
|
|
6
8
|
const serializer_js_1 = require("./serializer.js");
|
|
7
9
|
/**
|
|
8
10
|
* Owns the three-tier submit/fallback ladder:
|
|
@@ -54,7 +56,7 @@ class RunSubmitter {
|
|
|
54
56
|
auth = sm?.auth ?? (await this.httpClient.resolveAuth(run.options));
|
|
55
57
|
}
|
|
56
58
|
catch (error) {
|
|
57
|
-
this.logger.error(`Authentication failed: ${error
|
|
59
|
+
this.logger.error(`Authentication failed: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
58
60
|
throw error;
|
|
59
61
|
}
|
|
60
62
|
if (sm?.enabled && sm?.runId != null) {
|
|
@@ -87,6 +89,7 @@ class RunSubmitter {
|
|
|
87
89
|
totalTests: run.totalTests,
|
|
88
90
|
passedTests: run.passedTests,
|
|
89
91
|
failedTests: run.failedTests,
|
|
92
|
+
timedOutTests: run.timedOutTests,
|
|
90
93
|
skippedTests: run.skippedTests,
|
|
91
94
|
didNotRunTests: run.didNotRunTests,
|
|
92
95
|
environment: run.options.environment,
|
|
@@ -114,6 +117,7 @@ class RunSubmitter {
|
|
|
114
117
|
totalTests: run.totalTests,
|
|
115
118
|
passedTests: run.passedTests,
|
|
116
119
|
failedTests: run.failedTests,
|
|
120
|
+
timedOutTests: run.timedOutTests,
|
|
117
121
|
skippedTests: run.skippedTests,
|
|
118
122
|
didNotRunTests: run.didNotRunTests,
|
|
119
123
|
flakyTests,
|
|
@@ -138,13 +142,13 @@ class RunSubmitter {
|
|
|
138
142
|
await this.uploader.uploadReportsForStreamingRun(run.options.projectName, sm.runId, this.reportOptions(run), run.startTime, auth);
|
|
139
143
|
}
|
|
140
144
|
catch (error) {
|
|
141
|
-
this.logger.warn(`Failed to upload reports for streaming run: ${error
|
|
145
|
+
this.logger.warn(`Failed to upload reports for streaming run: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
142
146
|
}
|
|
143
147
|
}
|
|
144
148
|
return true;
|
|
145
149
|
}
|
|
146
150
|
catch (error) {
|
|
147
|
-
this.logger.warn(`Failed to finalize streaming run: ${error
|
|
151
|
+
this.logger.warn(`Failed to finalize streaming run: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
148
152
|
this.logger.info('Falling back to batch upload...');
|
|
149
153
|
return false;
|
|
150
154
|
}
|
|
@@ -156,9 +160,9 @@ class RunSubmitter {
|
|
|
156
160
|
return true;
|
|
157
161
|
}
|
|
158
162
|
catch (error) {
|
|
159
|
-
if (error.
|
|
163
|
+
if (error instanceof http_client_js_1.HttpError && error.status === 401 && !auth)
|
|
160
164
|
throw error;
|
|
161
|
-
this.logger.warn(`Failed to upload with files: ${error
|
|
165
|
+
this.logger.warn(`Failed to upload with files: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
162
166
|
this.logger.info('Falling back to JSON upload...');
|
|
163
167
|
return false;
|
|
164
168
|
}
|
|
@@ -172,9 +176,9 @@ class RunSubmitter {
|
|
|
172
176
|
catch (error) {
|
|
173
177
|
// If the server returned 401 and no auth was configured, this is a
|
|
174
178
|
// configuration error — throw so the caller knows it's fatal.
|
|
175
|
-
if (error.
|
|
179
|
+
if (error instanceof http_client_js_1.HttpError && error.status === 401 && !auth)
|
|
176
180
|
throw error;
|
|
177
|
-
this.logger.error(`All upload methods failed: ${error
|
|
181
|
+
this.logger.error(`All upload methods failed: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
178
182
|
// Save the wire-serialized form so the recovery file matches the
|
|
179
183
|
// original submit payload (no raw attachments / internal fields).
|
|
180
184
|
this.recovery.save((0, serializer_js_1.serializeRun)(payload, { includeTestCases: true }));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FullResult } from '@playwright/test/reporter';
|
|
2
|
-
import type { CollectedTestCase, WireTestCase } from '
|
|
2
|
+
import type { CollectedTestCase, WireTestCase } from '../../types.js';
|
|
3
3
|
import type { RunPayload } from './uploader.js';
|
|
4
4
|
/**
|
|
5
5
|
* Resolve the overall Piwi run status from Playwright's `FullResult.status`
|
|
@@ -64,6 +64,7 @@ function toWireTestCase(tc) {
|
|
|
64
64
|
suitePath: rest.suitePath ?? null,
|
|
65
65
|
suiteConfig: rest.suiteConfig ?? null,
|
|
66
66
|
testAnnotations: rest.testAnnotations ?? null,
|
|
67
|
+
locatorSnapshots: rest.locatorSnapshots || null,
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
/**
|
|
@@ -85,6 +86,7 @@ function serializeRun(payload, opts) {
|
|
|
85
86
|
totalTests: payload.totalTests,
|
|
86
87
|
passedTests: payload.passedTests,
|
|
87
88
|
failedTests: payload.failedTests,
|
|
89
|
+
timedOutTests: payload.timedOutTests ?? 0,
|
|
88
90
|
skippedTests: payload.skippedTests,
|
|
89
91
|
didNotRunTests: payload.didNotRunTests ?? 0,
|
|
90
92
|
environment: payload.environment ?? null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { HttpClient } from '
|
|
2
|
-
import { FileHandler } from '
|
|
3
|
-
import { Logger } from '
|
|
4
|
-
import type { CollectedTestCase, FilterDetails } from '
|
|
1
|
+
import type { HttpClient } from '../transport/http-client.js';
|
|
2
|
+
import type { FileHandler } from '../files/file-handler.js';
|
|
3
|
+
import { Logger } from '../support/logger.js';
|
|
4
|
+
import type { CollectedTestCase, FilterDetails } from '../../types.js';
|
|
5
5
|
/** Payload for a batch test-run submission */
|
|
6
6
|
export interface RunPayload {
|
|
7
7
|
/** Name of the project the run belongs to */
|
|
@@ -17,6 +17,7 @@ export interface RunPayload {
|
|
|
17
17
|
totalTests: number;
|
|
18
18
|
passedTests: number;
|
|
19
19
|
failedTests: number;
|
|
20
|
+
timedOutTests?: number;
|
|
20
21
|
skippedTests: number;
|
|
21
22
|
/** Tests that never executed (cut short by `maxFailures` or a serial-group failure) */
|
|
22
23
|
didNotRunTests?: number;
|
|
@@ -60,15 +61,15 @@ export interface ReportOptions {
|
|
|
60
61
|
* uploads for already-submitted streaming runs.
|
|
61
62
|
*/
|
|
62
63
|
export declare class Uploader {
|
|
63
|
-
private httpClient;
|
|
64
|
-
private fileHandler;
|
|
65
|
-
private logger;
|
|
64
|
+
private readonly httpClient;
|
|
65
|
+
private readonly fileHandler;
|
|
66
|
+
private readonly logger;
|
|
66
67
|
/**
|
|
67
68
|
* @param httpClient HTTP client for server communication.
|
|
68
69
|
* @param fileHandler File discovery and compression helper.
|
|
69
70
|
* @param logger Prefixed logger.
|
|
70
71
|
*/
|
|
71
|
-
constructor(httpClient: HttpClient, fileHandler: FileHandler, logger
|
|
72
|
+
constructor(httpClient: HttpClient, fileHandler: FileHandler, logger?: Logger);
|
|
72
73
|
/** Submit test results as a plain JSON payload (no file attachments) */
|
|
73
74
|
uploadJSON(payload: RunPayload, auth: string | null): Promise<any>;
|
|
74
75
|
/** Submit test results as a multipart form with trace files and compressed report directories */
|
|
@@ -1,12 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.Uploader = void 0;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
9
42
|
const form_data_1 = __importDefault(require("form-data"));
|
|
43
|
+
const http_client_js_1 = require("../transport/http-client.js");
|
|
44
|
+
const logger_js_1 = require("../support/logger.js");
|
|
10
45
|
const serializer_js_1 = require("./serializer.js");
|
|
11
46
|
/**
|
|
12
47
|
* Handles all upload strategies: plain JSON, multipart (with reports and
|
|
@@ -19,7 +54,7 @@ class Uploader {
|
|
|
19
54
|
* @param fileHandler File discovery and compression helper.
|
|
20
55
|
* @param logger Prefixed logger.
|
|
21
56
|
*/
|
|
22
|
-
constructor(httpClient, fileHandler, logger) {
|
|
57
|
+
constructor(httpClient, fileHandler, logger = new logger_js_1.Logger()) {
|
|
23
58
|
this.httpClient = httpClient;
|
|
24
59
|
this.fileHandler = fileHandler;
|
|
25
60
|
this.logger = logger;
|
|
@@ -104,15 +139,15 @@ class Uploader {
|
|
|
104
139
|
if (traceInfo) {
|
|
105
140
|
form.append('trace_hash', traceInfo.hash);
|
|
106
141
|
if (includeTraceFile) {
|
|
107
|
-
form.append('trace',
|
|
108
|
-
filename:
|
|
142
|
+
form.append('trace', fs.createReadStream(traceInfo.tracePath), {
|
|
143
|
+
filename: path.basename(traceInfo.tracePath),
|
|
109
144
|
});
|
|
110
145
|
}
|
|
111
146
|
}
|
|
112
147
|
if (attachments.length > 0) {
|
|
113
148
|
form.append('attach_meta', JSON.stringify(attachments.map((a) => ({ name: a.name, contentType: a.contentType, originalName: a.originalName }))));
|
|
114
149
|
for (const a of attachments) {
|
|
115
|
-
form.append('attach_file',
|
|
150
|
+
form.append('attach_file', fs.createReadStream(a.path), { filename: a.originalName });
|
|
116
151
|
}
|
|
117
152
|
}
|
|
118
153
|
return form;
|
|
@@ -123,7 +158,7 @@ class Uploader {
|
|
|
123
158
|
}
|
|
124
159
|
catch (error) {
|
|
125
160
|
// 422: the server doesn't have the blob after all — resend with the file
|
|
126
|
-
if (traceInfo && !includeTraceFile && error.
|
|
161
|
+
if (traceInfo && !includeTraceFile && error instanceof http_client_js_1.HttpError && error.status === 422) {
|
|
127
162
|
await this.httpClient.postFormData(`/api/test-runs/${runId}/case-files`, buildForm(true), auth);
|
|
128
163
|
}
|
|
129
164
|
else {
|
|
@@ -168,7 +203,7 @@ class Uploader {
|
|
|
168
203
|
originalName: a.originalName,
|
|
169
204
|
}))));
|
|
170
205
|
for (const a of attachments) {
|
|
171
|
-
form.append(`attach_file_${i}`,
|
|
206
|
+
form.append(`attach_file_${i}`, fs.createReadStream(a.path), { filename: a.originalName });
|
|
172
207
|
attachmentCount++;
|
|
173
208
|
}
|
|
174
209
|
}
|
|
@@ -179,8 +214,8 @@ class Uploader {
|
|
|
179
214
|
let traceCount = 0;
|
|
180
215
|
for (const [i, tc] of testCases.entries()) {
|
|
181
216
|
for (const tp of this.fileHandler.findTraceFiles(tc)) {
|
|
182
|
-
if (
|
|
183
|
-
form.append(`trace_${i}`,
|
|
217
|
+
if (fs.existsSync(tp)) {
|
|
218
|
+
form.append(`trace_${i}`, fs.createReadStream(tp), { filename: path.basename(tp) });
|
|
184
219
|
traceCount++;
|
|
185
220
|
}
|
|
186
221
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectCiRunLabel = detectCiRunLabel;
|
|
4
|
+
/** Detect a stable CI run label from well-known environment variables. Returns null outside CI. */
|
|
5
|
+
function detectCiRunLabel() {
|
|
6
|
+
const env = process.env;
|
|
7
|
+
if (env.GITHUB_ACTIONS && env.GITHUB_RUN_ID)
|
|
8
|
+
return env.GITHUB_RUN_ID;
|
|
9
|
+
if (env.GITLAB_CI && env.CI_PIPELINE_ID)
|
|
10
|
+
return env.CI_PIPELINE_ID;
|
|
11
|
+
if (env.CIRCLECI && env.CIRCLE_WORKFLOW_ID)
|
|
12
|
+
return env.CIRCLE_WORKFLOW_ID;
|
|
13
|
+
if (env.TRAVIS && env.TRAVIS_BUILD_ID)
|
|
14
|
+
return env.TRAVIS_BUILD_ID;
|
|
15
|
+
if (env.TF_BUILD && env.BUILD_BUILDID)
|
|
16
|
+
return env.BUILD_BUILDID;
|
|
17
|
+
if (env.JENKINS_URL && env.BUILD_ID)
|
|
18
|
+
return env.BUILD_ID;
|
|
19
|
+
if (env.BUILDKITE_BUILD_ID)
|
|
20
|
+
return env.BUILDKITE_BUILD_ID;
|
|
21
|
+
if (env.TEAMCITY_BUILD_ID)
|
|
22
|
+
return env.TEAMCITY_BUILD_ID;
|
|
23
|
+
if (env.BITBUCKET_BUILD_NUMBER)
|
|
24
|
+
return env.BITBUCKET_BUILD_NUMBER;
|
|
25
|
+
if (env.SEMAPHORE_WORKFLOW_ID)
|
|
26
|
+
return env.SEMAPHORE_WORKFLOW_ID;
|
|
27
|
+
if (env.APPVEYOR_BUILD_ID)
|
|
28
|
+
return env.APPVEYOR_BUILD_ID;
|
|
29
|
+
if (env.DRONE_BUILD_NUMBER)
|
|
30
|
+
return env.DRONE_BUILD_NUMBER;
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function detectCliFileFilters(argv?: string[]): string[];
|