@empiricalrun/playwright-utils 0.46.4 → 0.47.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/CHANGELOG.md +6 -0
- package/dist/reporter/attachment-cleanup-test-reporter.d.ts +19 -0
- package/dist/reporter/attachment-cleanup-test-reporter.d.ts.map +1 -0
- package/dist/reporter/attachment-cleanup-test-reporter.js +95 -0
- package/dist/reporter/attachment-cleanup.d.ts +44 -10
- package/dist/reporter/attachment-cleanup.d.ts.map +1 -1
- package/dist/reporter/attachment-cleanup.js +131 -26
- package/dist/reporter/empirical-reporter.d.ts.map +1 -1
- package/dist/reporter/empirical-reporter.js +4 -2
- package/package.json +2 -2
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-only reporter used by attachment-cleanup integration tests.
|
|
3
|
+
*
|
|
4
|
+
* Records a timestamped event log of register/markUploaded/eviction events
|
|
5
|
+
* so tests can make precise assertions about ordering and timing.
|
|
6
|
+
*/
|
|
7
|
+
import type { FullResult, Reporter, TestCase, TestResult } from "@playwright/test/reporter";
|
|
8
|
+
declare class AttachmentCleanupTestReporter implements Reporter {
|
|
9
|
+
private _timeline;
|
|
10
|
+
private _cleanup;
|
|
11
|
+
private _allAttachmentPaths;
|
|
12
|
+
private _pendingUploads;
|
|
13
|
+
private _queue;
|
|
14
|
+
constructor();
|
|
15
|
+
onTestEnd(_test: TestCase, result: TestResult): void;
|
|
16
|
+
onEnd(_result: FullResult): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export default AttachmentCleanupTestReporter;
|
|
19
|
+
//# sourceMappingURL=attachment-cleanup-test-reporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-cleanup-test-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/attachment-cleanup-test-reporter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAwCnC,cAAM,6BAA8B,YAAW,QAAQ;IACrD,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,mBAAmB,CAAgB;IAC3C,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,MAAM,CAAkC;;IAYhD,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IA2BvC,KAAK,CAAC,OAAO,EAAE,UAAU;CA0BhC;AAED,eAAe,6BAA6B,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Test-only reporter used by attachment-cleanup integration tests.
|
|
4
|
+
*
|
|
5
|
+
* Records a timestamped event log of register/markUploaded/eviction events
|
|
6
|
+
* so tests can make precise assertions about ordering and timing.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const attachment_cleanup_1 = require("./attachment-cleanup");
|
|
15
|
+
const SUMMARY_FILE = path_1.default.join(process.cwd(), "attachment-cleanup-summary.json");
|
|
16
|
+
const UPLOAD_DELAY_MS = parseInt(process.env.TEST_UPLOAD_DELAY_MS || "100", 10);
|
|
17
|
+
const MAX_CONCURRENT = parseInt(process.env.UPLOAD_MAX_QUEUE_SIZE || "10", 10);
|
|
18
|
+
class AsyncQueue {
|
|
19
|
+
_maxConcurrent;
|
|
20
|
+
_running = 0;
|
|
21
|
+
_waiting = [];
|
|
22
|
+
constructor(_maxConcurrent) {
|
|
23
|
+
this._maxConcurrent = _maxConcurrent;
|
|
24
|
+
}
|
|
25
|
+
async run(fn) {
|
|
26
|
+
while (this._running >= this._maxConcurrent) {
|
|
27
|
+
await new Promise((resolve) => this._waiting.push(resolve));
|
|
28
|
+
}
|
|
29
|
+
this._running++;
|
|
30
|
+
try {
|
|
31
|
+
return await fn();
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
this._running--;
|
|
35
|
+
const next = this._waiting.shift();
|
|
36
|
+
if (next)
|
|
37
|
+
next();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
class AttachmentCleanupTestReporter {
|
|
42
|
+
_timeline = [];
|
|
43
|
+
_cleanup = new attachment_cleanup_1.AttachmentCleanup();
|
|
44
|
+
_allAttachmentPaths = [];
|
|
45
|
+
_pendingUploads = [];
|
|
46
|
+
_queue = new AsyncQueue(MAX_CONCURRENT);
|
|
47
|
+
constructor() {
|
|
48
|
+
this._cleanup.onEvict = (evictedPath) => {
|
|
49
|
+
this._timeline.push({
|
|
50
|
+
type: "eviction",
|
|
51
|
+
path: evictedPath,
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
onTestEnd(_test, result) {
|
|
57
|
+
for (const attachment of result.attachments) {
|
|
58
|
+
if (attachment.path) {
|
|
59
|
+
this._allAttachmentPaths.push(attachment.path);
|
|
60
|
+
this._cleanup.register(attachment);
|
|
61
|
+
this._timeline.push({
|
|
62
|
+
type: "register",
|
|
63
|
+
path: attachment.path,
|
|
64
|
+
timestamp: Date.now(),
|
|
65
|
+
});
|
|
66
|
+
const attachmentPath = attachment.path;
|
|
67
|
+
const uploadPromise = this._queue.run(async () => {
|
|
68
|
+
await new Promise((r) => setTimeout(r, UPLOAD_DELAY_MS));
|
|
69
|
+
this._cleanup.markUploaded(attachmentPath);
|
|
70
|
+
this._timeline.push({
|
|
71
|
+
type: "markUploaded",
|
|
72
|
+
path: attachmentPath,
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
this._pendingUploads.push(uploadPromise);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async onEnd(_result) {
|
|
81
|
+
await Promise.allSettled(this._pendingUploads);
|
|
82
|
+
this._cleanup.logSummary();
|
|
83
|
+
const stats = this._cleanup.stats;
|
|
84
|
+
const existingPaths = this._allAttachmentPaths.filter((p) => fs_1.default.existsSync(p));
|
|
85
|
+
const deletedPaths = this._allAttachmentPaths.filter((p) => !fs_1.default.existsSync(p));
|
|
86
|
+
fs_1.default.writeFileSync(SUMMARY_FILE, JSON.stringify({
|
|
87
|
+
...stats,
|
|
88
|
+
allAttachmentPaths: this._allAttachmentPaths,
|
|
89
|
+
existingPaths,
|
|
90
|
+
deletedPaths,
|
|
91
|
+
timeline: this._timeline,
|
|
92
|
+
}, null, 2));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.default = AttachmentCleanupTestReporter;
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tracks
|
|
2
|
+
* Tracks test attachments and evicts the oldest uploaded ones from disk
|
|
3
3
|
* when total size exceeds the configured limit.
|
|
4
4
|
*
|
|
5
|
+
* Two-phase tracking:
|
|
6
|
+
* 1. register() — called immediately when an attachment is created.
|
|
7
|
+
* Adds the file to the total size accounting so the LRU knows about
|
|
8
|
+
* all disk usage, even while uploads are still queued.
|
|
9
|
+
* 2. markUploaded() — called after the file is uploaded to R2.
|
|
10
|
+
* Marks the file as safe to evict (delete from disk).
|
|
11
|
+
*
|
|
12
|
+
* Eviction only deletes files that have been uploaded. This ensures we
|
|
13
|
+
* never delete a file before its upload completes.
|
|
14
|
+
*
|
|
5
15
|
* Why this exists:
|
|
6
16
|
* Test attachments (videos, traces, screenshots) accumulate on disk during a
|
|
7
17
|
* test run. On Fargate workers with 50 GB ephemeral storage, large test suites
|
|
@@ -9,23 +19,47 @@
|
|
|
9
19
|
* safely delete them from disk — but we can't delete them immediately because
|
|
10
20
|
* Playwright's blob reporter reads them during onEnd to embed as resources/.
|
|
11
21
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* gracefully skips missing files, and patchBlobZip replaces resources/ with
|
|
22
|
+
* The blob reporter's statSync guard (Playwright 1.57+) gracefully skips
|
|
23
|
+
* missing files, and patchBlobZip replaces resources/ with
|
|
15
24
|
* _empirical_urls.json pointing to R2 URLs anyway.
|
|
16
|
-
*
|
|
17
|
-
* Future: when the incremental blob reporter is mature enough to replace the
|
|
18
|
-
* standard blob reporter for all runs (not just spot instances), this class
|
|
19
|
-
* can be removed — the incremental reporter never embeds attachments as resources.
|
|
20
25
|
*/
|
|
21
26
|
export declare class AttachmentCleanup {
|
|
22
|
-
private
|
|
27
|
+
private _pending;
|
|
28
|
+
private _uploaded;
|
|
23
29
|
private _totalSize;
|
|
24
30
|
private _limitBytes;
|
|
31
|
+
private _registeredCount;
|
|
32
|
+
private _uploadedCount;
|
|
33
|
+
private _evictedCount;
|
|
34
|
+
private _evictedBytes;
|
|
35
|
+
private _diskLogTimer;
|
|
36
|
+
private _onEvict;
|
|
25
37
|
constructor(limitBytes?: number);
|
|
26
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Phase 1: Register an attachment immediately when it's created.
|
|
40
|
+
* Adds to total size accounting and attempts eviction of already-uploaded files.
|
|
41
|
+
*/
|
|
42
|
+
register(attachment: {
|
|
27
43
|
path?: string | undefined;
|
|
28
44
|
}): void;
|
|
45
|
+
/**
|
|
46
|
+
* Phase 2: Mark an attachment as uploaded (safe to evict).
|
|
47
|
+
* Moves from pending to the uploaded queue and attempts eviction.
|
|
48
|
+
*/
|
|
49
|
+
markUploaded(attachmentPath: string): void;
|
|
29
50
|
private _evict;
|
|
51
|
+
private _logDiskStatus;
|
|
52
|
+
logSummary(): void;
|
|
53
|
+
set onEvict(callback: (path: string) => void);
|
|
54
|
+
get stats(): {
|
|
55
|
+
registeredCount: number;
|
|
56
|
+
uploadedCount: number;
|
|
57
|
+
pendingCount: number;
|
|
58
|
+
evictedCount: number;
|
|
59
|
+
evictedBytes: number;
|
|
60
|
+
activeCount: number;
|
|
61
|
+
totalSize: number;
|
|
62
|
+
limitBytes: number;
|
|
63
|
+
};
|
|
30
64
|
}
|
|
31
65
|
//# sourceMappingURL=attachment-cleanup.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-cleanup.d.ts","sourceRoot":"","sources":["../../src/reporter/attachment-cleanup.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"attachment-cleanup.d.ts","sourceRoot":"","sources":["../../src/reporter/attachment-cleanup.ts"],"names":[],"mappings":"AAqCA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,SAAS,CAAwC;IACzD,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,aAAa,CAA+C;IACpE,OAAO,CAAC,QAAQ,CAAyC;gBAE7C,UAAU,GAAE,MAAmC;IAY3D;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE;IAelD;;;OAGG;IACH,YAAY,CAAC,cAAc,EAAE,MAAM;IASnC,OAAO,CAAC,MAAM;IA4Bd,OAAO,CAAC,cAAc;IAMtB,UAAU;IAUV,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAE3C;IAED,IAAI,KAAK;;;;;;;;;MAWR;CACF"}
|
|
@@ -4,13 +4,53 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.AttachmentCleanup = void 0;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
7
8
|
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const logger_1 = require("../logger");
|
|
9
|
-
|
|
10
|
+
function getDefaultDiskLimitBytes() {
|
|
11
|
+
const raw = process.env.ATTACHMENT_CLEANUP_LIMIT_BYTES;
|
|
12
|
+
if (raw != null) {
|
|
13
|
+
const parsed = Number(raw);
|
|
14
|
+
if (Number.isSafeInteger(parsed) && parsed > 0)
|
|
15
|
+
return parsed;
|
|
16
|
+
logger_1.logger.warn(`[AttachmentCleanup] Invalid ATTACHMENT_CLEANUP_LIMIT_BYTES=${raw}, using default`);
|
|
17
|
+
}
|
|
18
|
+
return 10 * 1024 * 1024 * 1024; // 10 GB
|
|
19
|
+
}
|
|
20
|
+
const DISK_LOG_INTERVAL_MS = 30_000;
|
|
21
|
+
function formatBytes(bytes) {
|
|
22
|
+
if (bytes < 1024 * 1024)
|
|
23
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
24
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
25
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
26
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
27
|
+
}
|
|
28
|
+
function getDiskUsage() {
|
|
29
|
+
try {
|
|
30
|
+
const output = (0, child_process_1.execSync)("df -h / | tail -1", {
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
timeout: 5000,
|
|
33
|
+
}).trim();
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return "unavailable";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
10
40
|
/**
|
|
11
|
-
* Tracks
|
|
41
|
+
* Tracks test attachments and evicts the oldest uploaded ones from disk
|
|
12
42
|
* when total size exceeds the configured limit.
|
|
13
43
|
*
|
|
44
|
+
* Two-phase tracking:
|
|
45
|
+
* 1. register() — called immediately when an attachment is created.
|
|
46
|
+
* Adds the file to the total size accounting so the LRU knows about
|
|
47
|
+
* all disk usage, even while uploads are still queued.
|
|
48
|
+
* 2. markUploaded() — called after the file is uploaded to R2.
|
|
49
|
+
* Marks the file as safe to evict (delete from disk).
|
|
50
|
+
*
|
|
51
|
+
* Eviction only deletes files that have been uploaded. This ensures we
|
|
52
|
+
* never delete a file before its upload completes.
|
|
53
|
+
*
|
|
14
54
|
* Why this exists:
|
|
15
55
|
* Test attachments (videos, traces, screenshots) accumulate on disk during a
|
|
16
56
|
* test run. On Fargate workers with 50 GB ephemeral storage, large test suites
|
|
@@ -18,48 +58,113 @@ const DEFAULT_DISK_LIMIT_BYTES = 10 * 1024 * 1024 * 1024; // 10 GB
|
|
|
18
58
|
* safely delete them from disk — but we can't delete them immediately because
|
|
19
59
|
* Playwright's blob reporter reads them during onEnd to embed as resources/.
|
|
20
60
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* gracefully skips missing files, and patchBlobZip replaces resources/ with
|
|
61
|
+
* The blob reporter's statSync guard (Playwright 1.57+) gracefully skips
|
|
62
|
+
* missing files, and patchBlobZip replaces resources/ with
|
|
24
63
|
* _empirical_urls.json pointing to R2 URLs anyway.
|
|
25
|
-
*
|
|
26
|
-
* Future: when the incremental blob reporter is mature enough to replace the
|
|
27
|
-
* standard blob reporter for all runs (not just spot instances), this class
|
|
28
|
-
* can be removed — the incremental reporter never embeds attachments as resources.
|
|
29
64
|
*/
|
|
30
65
|
class AttachmentCleanup {
|
|
31
|
-
|
|
66
|
+
_pending = new Map();
|
|
67
|
+
_uploaded = [];
|
|
32
68
|
_totalSize = 0;
|
|
33
69
|
_limitBytes;
|
|
34
|
-
|
|
70
|
+
_registeredCount = 0;
|
|
71
|
+
_uploadedCount = 0;
|
|
72
|
+
_evictedCount = 0;
|
|
73
|
+
_evictedBytes = 0;
|
|
74
|
+
_diskLogTimer = null;
|
|
75
|
+
_onEvict = null;
|
|
76
|
+
constructor(limitBytes = getDefaultDiskLimitBytes()) {
|
|
35
77
|
this._limitBytes = limitBytes;
|
|
78
|
+
logger_1.logger.info(`[AttachmentCleanup] Initialized with limit: ${formatBytes(limitBytes)}`);
|
|
79
|
+
this._diskLogTimer = setInterval(() => {
|
|
80
|
+
this._logDiskStatus();
|
|
81
|
+
}, DISK_LOG_INTERVAL_MS);
|
|
82
|
+
// Don't keep the process alive just for this timer
|
|
83
|
+
this._diskLogTimer.unref();
|
|
36
84
|
}
|
|
37
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Phase 1: Register an attachment immediately when it's created.
|
|
87
|
+
* Adds to total size accounting and attempts eviction of already-uploaded files.
|
|
88
|
+
*/
|
|
89
|
+
register(attachment) {
|
|
38
90
|
try {
|
|
39
|
-
if (attachment.path)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
91
|
+
if (!attachment.path)
|
|
92
|
+
return;
|
|
93
|
+
if (this._pending.has(attachment.path))
|
|
94
|
+
return;
|
|
95
|
+
const stat = fs_1.default.statSync(attachment.path);
|
|
96
|
+
const sizeBytes = stat.size;
|
|
97
|
+
this._pending.set(attachment.path, sizeBytes);
|
|
98
|
+
this._totalSize += sizeBytes;
|
|
99
|
+
this._registeredCount++;
|
|
100
|
+
this._evict();
|
|
46
101
|
}
|
|
47
102
|
catch {
|
|
48
|
-
// Ignore errors
|
|
103
|
+
// Ignore errors (file may not exist)
|
|
49
104
|
}
|
|
50
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Phase 2: Mark an attachment as uploaded (safe to evict).
|
|
108
|
+
* Moves from pending to the uploaded queue and attempts eviction.
|
|
109
|
+
*/
|
|
110
|
+
markUploaded(attachmentPath) {
|
|
111
|
+
const size = this._pending.get(attachmentPath);
|
|
112
|
+
if (size === undefined)
|
|
113
|
+
return;
|
|
114
|
+
this._pending.delete(attachmentPath);
|
|
115
|
+
this._uploaded.push({ path: attachmentPath, size });
|
|
116
|
+
this._uploadedCount++;
|
|
117
|
+
this._evict();
|
|
118
|
+
}
|
|
51
119
|
_evict() {
|
|
52
|
-
while (this._totalSize > this._limitBytes && this.
|
|
53
|
-
const oldest = this.
|
|
120
|
+
while (this._totalSize > this._limitBytes && this._uploaded.length > 0) {
|
|
121
|
+
const oldest = this._uploaded[0];
|
|
54
122
|
try {
|
|
55
123
|
fs_1.default.unlinkSync(oldest.path);
|
|
56
|
-
|
|
124
|
+
this._uploaded.shift();
|
|
125
|
+
this._evictedCount++;
|
|
126
|
+
this._evictedBytes += oldest.size;
|
|
127
|
+
this._totalSize -= oldest.size;
|
|
128
|
+
logger_1.logger.info(`[AttachmentCleanup] Evicted ${oldest.path} (${formatBytes(oldest.size)}), totalSize=${formatBytes(this._totalSize)}, pending=${this._pending.size}, uploaded=${this._uploaded.length}`);
|
|
129
|
+
this._onEvict?.(oldest.path);
|
|
57
130
|
}
|
|
58
|
-
catch {
|
|
59
|
-
|
|
131
|
+
catch (err) {
|
|
132
|
+
const code = err?.code;
|
|
133
|
+
if (code === "ENOENT") {
|
|
134
|
+
this._uploaded.shift();
|
|
135
|
+
this._totalSize -= oldest.size;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
logger_1.logger.warn(`[AttachmentCleanup] Failed to evict ${oldest.path}: ${code ?? err}`);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
60
141
|
}
|
|
61
|
-
this._totalSize -= oldest.size;
|
|
62
142
|
}
|
|
63
143
|
}
|
|
144
|
+
_logDiskStatus() {
|
|
145
|
+
logger_1.logger.info(`[AttachmentCleanup] Periodic: registered=${this._registeredCount}, pending=${this._pending.size}, uploaded=${this._uploadedCount}, evicted=${this._evictedCount} (${formatBytes(this._evictedBytes)}), totalSize=${formatBytes(this._totalSize)}, limit=${formatBytes(this._limitBytes)}, disk=${getDiskUsage()}`);
|
|
146
|
+
}
|
|
147
|
+
logSummary() {
|
|
148
|
+
if (this._diskLogTimer) {
|
|
149
|
+
clearInterval(this._diskLogTimer);
|
|
150
|
+
this._diskLogTimer = null;
|
|
151
|
+
}
|
|
152
|
+
logger_1.logger.info(`[AttachmentCleanup] Summary: registered=${this._registeredCount}, pending=${this._pending.size}, uploaded=${this._uploadedCount}, evicted=${this._evictedCount} (${formatBytes(this._evictedBytes)}), totalSize=${formatBytes(this._totalSize)}, disk=${getDiskUsage()}`);
|
|
153
|
+
}
|
|
154
|
+
set onEvict(callback) {
|
|
155
|
+
this._onEvict = callback;
|
|
156
|
+
}
|
|
157
|
+
get stats() {
|
|
158
|
+
return {
|
|
159
|
+
registeredCount: this._registeredCount,
|
|
160
|
+
uploadedCount: this._uploadedCount,
|
|
161
|
+
pendingCount: this._pending.size,
|
|
162
|
+
evictedCount: this._evictedCount,
|
|
163
|
+
evictedBytes: this._evictedBytes,
|
|
164
|
+
activeCount: this._uploaded.length,
|
|
165
|
+
totalSize: this._totalSize,
|
|
166
|
+
limitBytes: this._limitBytes,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
64
169
|
}
|
|
65
170
|
exports.AttachmentCleanup = AttachmentCleanup;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"empirical-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/empirical-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAuBnC,cAAM,iBAAkB,YAAW,QAAQ;IACzC,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,oBAAoB,CAG1B;IACF,OAAO,CAAC,sBAAsB,CAAuB;IACrD,OAAO,CAAC,YAAY,CAAgD;IACpE,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,kBAAkB,CAA2B;;IAMrD,OAAO,CAAC,6BAA6B,
|
|
1
|
+
{"version":3,"file":"empirical-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/empirical-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAuBnC,cAAM,iBAAkB,YAAW,QAAQ;IACzC,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,oBAAoB,CAG1B;IACF,OAAO,CAAC,sBAAsB,CAAuB;IACrD,OAAO,CAAC,YAAY,CAAgD;IACpE,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,kBAAkB,CAA2B;;IAMrD,OAAO,CAAC,6BAA6B,CAqCnC;IAEF,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IAwGtC,KAAK,CAAC,MAAM,EAAE,UAAU;IAmG9B,OAAO,CAAC,qBAAqB;YAkBf,gBAAgB;YAOhB,iBAAiB;CAmChC;AAED,eAAe,iBAAiB,CAAC"}
|
|
@@ -38,11 +38,12 @@ class EmpiricalReporter {
|
|
|
38
38
|
logger_1.logger.error(`[Empirical Reporter] Attachment does not exist: ${attachment.path}`);
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
+
this._attachmentCleanup.register(attachment);
|
|
41
42
|
const relativePath = path_1.default.relative(this._testResultSourceDir, attachment.path);
|
|
42
43
|
const destinationPath = `data/${relativePath}`;
|
|
43
44
|
const publicUrl = await this._uploader.uploadFile(attachment.path, destinationPath);
|
|
44
45
|
if (publicUrl) {
|
|
45
|
-
this._attachmentCleanup.
|
|
46
|
+
this._attachmentCleanup.markUploaded(attachment.path);
|
|
46
47
|
return { [attachment.path]: publicUrl };
|
|
47
48
|
}
|
|
48
49
|
};
|
|
@@ -134,7 +135,7 @@ class EmpiricalReporter {
|
|
|
134
135
|
return;
|
|
135
136
|
if (!this._uploader)
|
|
136
137
|
return;
|
|
137
|
-
logger_1.logger.
|
|
138
|
+
logger_1.logger.info(`[Empirical Reporter] Test run completed with status: ${result.status}`);
|
|
138
139
|
logger_1.logger.debug(`[Empirical Reporter] State: cwd=${this._currentWorkingDir}, hasSharding=${this._hasSharding}`);
|
|
139
140
|
const startTime = Date.now();
|
|
140
141
|
const reportDir = path_1.default.join(this._currentWorkingDir, "playwright-report");
|
|
@@ -186,6 +187,7 @@ class EmpiricalReporter {
|
|
|
186
187
|
else {
|
|
187
188
|
logger_1.logger.info(`[Empirical Reporter] Sharding not enabled (HAS_SHARDING=${process.env.HAS_SHARDING})`);
|
|
188
189
|
}
|
|
190
|
+
this._attachmentCleanup.logSummary();
|
|
189
191
|
logger_1.logger.debug("[Empirical Reporter] All uploads finished");
|
|
190
192
|
const timeDiff = Date.now() - startTime;
|
|
191
193
|
logger_1.logger.debug("[Empirical Reporter] Time taken to upload after tests ended: ", timeDiff, "ms");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empiricalrun/playwright-utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/",
|
|
6
6
|
"access": "public"
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"puppeteer-extra-plugin-recaptcha": "^3.6.8",
|
|
44
44
|
"rimraf": "^6.0.1",
|
|
45
45
|
"ts-morph": "^23.0.0",
|
|
46
|
-
"@empiricalrun/cua": "^0.3.0",
|
|
47
46
|
"@empiricalrun/dashboard-client": "^0.2.0",
|
|
48
47
|
"@empiricalrun/llm": "^0.26.0",
|
|
49
48
|
"@empiricalrun/r2-uploader": "^0.9.1",
|
|
49
|
+
"@empiricalrun/cua": "^0.3.0",
|
|
50
50
|
"@empiricalrun/reporter": "^0.28.1"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/email.ts","./src/index.ts","./src/kv.ts","./src/logger.ts","./src/mailosaur-client.ts","./src/playwright-extensions.ts","./src/postgres.ts","./src/telemetry.ts","./src/webhook.ts","./src/auth/google.ts","./src/auth/index.ts","./src/auth/types.ts","./src/captcha/index.ts","./src/config/index.ts","./src/config/proxy.ts","./src/config/devices/types.ts","./src/overlay-tests/cache.spec.ts","./src/overlay-tests/click.spec.ts","./src/overlay-tests/fixtures.ts","./src/overlay-tests/patch.spec.ts","./src/reporter/attachment-cleanup.ts","./src/reporter/blob-utils.ts","./src/reporter/empirical-reporter.ts","./src/reporter/failing-line.ts","./src/reporter/harness.ts","./src/reporter/ibr-utils.ts","./src/reporter/incremental-blob-reporter.ts","./src/reporter/lifecycle-events.ts","./src/reporter/local-test.ts","./src/reporter/reporter-state.ts","./src/reporter/uploader.ts","./src/reporter/util.ts","./src/test/constants.ts","./src/test/coverage.ts","./src/test/index.ts","./src/test/types.ts","./src/test/video-labels.ts","./src/test/expect/index.ts","./src/test/expect/types.ts","./src/test/expect/visual.ts","./src/test/expect/webhook.ts","./src/test/scripts/agent-capabilities.ts","./src/test/scripts/index.ts","./src/test/scripts/locator-highlights.ts","./src/test/scripts/locator-vision.ts","./src/test/scripts/mouse-pointer.ts","./src/test/scripts/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/cache.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/prompt.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/utils.ts","./src/test/scripts/pw-locator-patch/highlight/click.ts","./src/test/scripts/pw-locator-patch/highlight/expect.ts","./src/test/scripts/pw-locator-patch/highlight/hover.ts","./src/test/scripts/pw-locator-patch/highlight/inner-text.ts","./src/test/scripts/pw-locator-patch/highlight/input-value.ts","./src/test/scripts/pw-locator-patch/highlight/is-checked.ts","./src/test/scripts/pw-locator-patch/highlight/is-disabled.ts","./src/test/scripts/pw-locator-patch/highlight/is-editable.ts","./src/test/scripts/pw-locator-patch/highlight/text-content.ts","./src/test/scripts/pw-locator-patch/utils/index.ts","./src/test/scripts/pw-locator-patch/vision/query.ts"],"version":"5.8.3"}
|
|
1
|
+
{"root":["./src/email.ts","./src/index.ts","./src/kv.ts","./src/logger.ts","./src/mailosaur-client.ts","./src/playwright-extensions.ts","./src/postgres.ts","./src/telemetry.ts","./src/webhook.ts","./src/auth/google.ts","./src/auth/index.ts","./src/auth/types.ts","./src/captcha/index.ts","./src/config/index.ts","./src/config/proxy.ts","./src/config/devices/types.ts","./src/overlay-tests/cache.spec.ts","./src/overlay-tests/click.spec.ts","./src/overlay-tests/fixtures.ts","./src/overlay-tests/patch.spec.ts","./src/reporter/attachment-cleanup-test-reporter.ts","./src/reporter/attachment-cleanup.ts","./src/reporter/blob-utils.ts","./src/reporter/empirical-reporter.ts","./src/reporter/failing-line.ts","./src/reporter/harness.ts","./src/reporter/ibr-utils.ts","./src/reporter/incremental-blob-reporter.ts","./src/reporter/lifecycle-events.ts","./src/reporter/local-test.ts","./src/reporter/reporter-state.ts","./src/reporter/uploader.ts","./src/reporter/util.ts","./src/test/constants.ts","./src/test/coverage.ts","./src/test/index.ts","./src/test/types.ts","./src/test/video-labels.ts","./src/test/expect/index.ts","./src/test/expect/types.ts","./src/test/expect/visual.ts","./src/test/expect/webhook.ts","./src/test/scripts/agent-capabilities.ts","./src/test/scripts/index.ts","./src/test/scripts/locator-highlights.ts","./src/test/scripts/locator-vision.ts","./src/test/scripts/mouse-pointer.ts","./src/test/scripts/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/cache.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/prompt.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/utils.ts","./src/test/scripts/pw-locator-patch/highlight/click.ts","./src/test/scripts/pw-locator-patch/highlight/expect.ts","./src/test/scripts/pw-locator-patch/highlight/hover.ts","./src/test/scripts/pw-locator-patch/highlight/inner-text.ts","./src/test/scripts/pw-locator-patch/highlight/input-value.ts","./src/test/scripts/pw-locator-patch/highlight/is-checked.ts","./src/test/scripts/pw-locator-patch/highlight/is-disabled.ts","./src/test/scripts/pw-locator-patch/highlight/is-editable.ts","./src/test/scripts/pw-locator-patch/highlight/text-content.ts","./src/test/scripts/pw-locator-patch/utils/index.ts","./src/test/scripts/pw-locator-patch/vision/query.ts"],"version":"5.8.3"}
|