@empiricalrun/playwright-utils 0.58.0 → 0.58.2
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 +14 -0
- package/dist/generated/api-worker-openapi.d.ts +1226 -707
- package/dist/generated/api-worker-openapi.d.ts.map +1 -1
- package/dist/reporter/empirical-reporter.d.ts.map +1 -1
- package/dist/reporter/empirical-reporter.js +3 -3
- package/dist/reporter/harness.d.ts +44 -0
- package/dist/reporter/harness.d.ts.map +1 -1
- package/dist/reporter/harness.js +117 -32
- package/dist/reporter/incremental-blob-reporter.d.ts +7 -1
- package/dist/reporter/incremental-blob-reporter.d.ts.map +1 -1
- package/dist/reporter/incremental-blob-reporter.js +57 -27
- package/dist/reporter/uploader.d.ts +2 -2
- package/dist/reporter/uploader.d.ts.map +1 -1
- package/dist/reporter/uploader.js +5 -1
- package/package.json +6 -6
|
@@ -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,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;IAGrD,OAAO,CAAC,YAAY,CAAqB;IAEzC,cAEC;IAED,OAAO,CAAC,MAAM,EAAE,UAAU,QAEzB;IAED,OAAO,CAAC,aAAa;IAUrB,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,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;IAGrD,OAAO,CAAC,YAAY,CAAqB;IAEzC,cAEC;IAED,OAAO,CAAC,MAAM,EAAE,UAAU,QAEzB;IAED,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,6BAA6B,CA2CnC;IAEF,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,QAqI3C;IAEK,KAAK,CAAC,MAAM,EAAE,UAAU,iBAiH7B;IAED,OAAO,CAAC,qBAAqB;YAkBf,gBAAgB;YAOhB,iBAAiB;CAqChC;eAEc,iBAAiB"}
|
|
@@ -39,7 +39,7 @@ class EmpiricalReporter {
|
|
|
39
39
|
}
|
|
40
40
|
console.log(message);
|
|
41
41
|
}
|
|
42
|
-
enqueTestAttachmentUploadTask = async (attachment) => {
|
|
42
|
+
enqueTestAttachmentUploadTask = async (attachment, keepHot) => {
|
|
43
43
|
if (!this._uploader) {
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
@@ -55,7 +55,7 @@ class EmpiricalReporter {
|
|
|
55
55
|
this._attachmentCleanup.register(attachment);
|
|
56
56
|
const relativePath = path_1.default.relative(this._testResultSourceDir, attachment.path);
|
|
57
57
|
const destinationPath = `data/${relativePath}`;
|
|
58
|
-
const publicUrl = await this._uploader.uploadFile(attachment.path, destinationPath);
|
|
58
|
+
const publicUrl = await this._uploader.uploadFile(attachment.path, destinationPath, keepHot ? "STANDARD" : undefined);
|
|
59
59
|
if (publicUrl) {
|
|
60
60
|
this._attachmentCleanup.markUploaded(attachment.path);
|
|
61
61
|
return { [attachment.path]: publicUrl };
|
|
@@ -77,7 +77,7 @@ class EmpiricalReporter {
|
|
|
77
77
|
try {
|
|
78
78
|
const attachmentPromises = result.attachments.map(async (attachment) => {
|
|
79
79
|
try {
|
|
80
|
-
const fileMap = await this.enqueTestAttachmentUploadTask(attachment);
|
|
80
|
+
const fileMap = await this.enqueTestAttachmentUploadTask(attachment, result.status === "passed");
|
|
81
81
|
if (!fileMap) {
|
|
82
82
|
logger_1.logger.error(`No upload result for ${attachment.name}`);
|
|
83
83
|
return undefined;
|
|
@@ -29,6 +29,48 @@ export interface RunResult {
|
|
|
29
29
|
stderr: string;
|
|
30
30
|
}
|
|
31
31
|
export type WaitFor = "first-passed" | "first-failed";
|
|
32
|
+
/** A `@@ NAME @@` line printed by a fixture suite, with its offset from run start. */
|
|
33
|
+
export interface Marker {
|
|
34
|
+
name: string;
|
|
35
|
+
atMs: number;
|
|
36
|
+
}
|
|
37
|
+
export interface SignalRunResult extends RunResult {
|
|
38
|
+
exitCode: number | null;
|
|
39
|
+
exitSignal: NodeJS.Signals | null;
|
|
40
|
+
markers: Marker[];
|
|
41
|
+
/** Offset from run start at which the first signal was delivered. */
|
|
42
|
+
signalSentAtMs: number;
|
|
43
|
+
/** Wall clock at run start, so `atMs` offsets can be compared to file mtimes. */
|
|
44
|
+
startedAtEpochMs: number;
|
|
45
|
+
}
|
|
46
|
+
export interface SignalRunOptions {
|
|
47
|
+
workers: number;
|
|
48
|
+
/**
|
|
49
|
+
* Signal as soon as this `@@ MARKER @@` is printed. Preferred over `waitFor`:
|
|
50
|
+
* it pins the signal to a known point in the run instead of a sleep.
|
|
51
|
+
*/
|
|
52
|
+
waitForMarker?: string;
|
|
53
|
+
/** Legacy trigger: wait for the first pass/fail line in stdout. */
|
|
54
|
+
waitFor?: WaitFor;
|
|
55
|
+
/** Settling delay between the trigger and the signal. */
|
|
56
|
+
delayBeforeSignalMs?: number;
|
|
57
|
+
/** Defaults to SIGINT, which is what production sends. */
|
|
58
|
+
signal?: NodeJS.Signals;
|
|
59
|
+
/** Send the same signal again this long after the first one. */
|
|
60
|
+
secondSignalAfterMs?: number;
|
|
61
|
+
/** SIGKILL the group this long after the first signal, like the cmd.ts grace cap. */
|
|
62
|
+
killAfterMs?: number;
|
|
63
|
+
/** How long to wait for the trigger before giving up and signalling anyway. */
|
|
64
|
+
triggerTimeoutMs?: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Spawns `playwright test` with the IBR reporter in its own process group,
|
|
68
|
+
* signals the group mid-run, and waits for exit.
|
|
69
|
+
*
|
|
70
|
+
* The process group and the signal both match production: `cmd.ts` spawns the
|
|
71
|
+
* runner detached and does `process.kill(-pid, signal)`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function runPlaywrightWithSignal(ctx: IBRTestContext, opts: SignalRunOptions): Promise<SignalRunResult>;
|
|
32
74
|
/**
|
|
33
75
|
* Spawns `playwright test` with the IBR reporter, waits for a test outcome,
|
|
34
76
|
* sends SIGINT, and waits for the process to exit.
|
|
@@ -39,6 +81,8 @@ export declare function runPlaywrightWithSigint(ctx: IBRTestContext, opts: {
|
|
|
39
81
|
workers: number;
|
|
40
82
|
waitFor: WaitFor;
|
|
41
83
|
}): Promise<RunResult>;
|
|
84
|
+
/** Marker names in the order they were printed. */
|
|
85
|
+
export declare function markerNames(result: SignalRunResult): string[];
|
|
42
86
|
export declare function runPlaywrightList(ctx: TestCaseSyncTestContext): Promise<RunResult>;
|
|
43
87
|
export interface BlobEvent {
|
|
44
88
|
method: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/reporter/harness.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErD;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACzD,GAAG,EAAE,cAAc,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC,CAaD;AAqCD,wBAAsB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACrE,GAAG,EAAE,uBAAuB,CAAC;IAC7B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC,CAaD;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAI3E;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG,cAAc,CAAC;AAOtD;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC1C,OAAO,CAAC,SAAS,CAAC,
|
|
1
|
+
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/reporter/harness.ts"],"names":[],"mappings":"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"}
|
package/dist/reporter/harness.js
CHANGED
|
@@ -7,7 +7,9 @@ exports.sleep = sleep;
|
|
|
7
7
|
exports.setupFixture = setupFixture;
|
|
8
8
|
exports.setupTestCaseSyncFixture = setupTestCaseSyncFixture;
|
|
9
9
|
exports.cleanBlobArtifacts = cleanBlobArtifacts;
|
|
10
|
+
exports.runPlaywrightWithSignal = runPlaywrightWithSignal;
|
|
10
11
|
exports.runPlaywrightWithSigint = runPlaywrightWithSigint;
|
|
12
|
+
exports.markerNames = markerNames;
|
|
11
13
|
exports.runPlaywrightList = runPlaywrightList;
|
|
12
14
|
exports.readBlobEvents = readBlobEvents;
|
|
13
15
|
const zip_1 = require("@empiricalrun/r2-uploader/zip");
|
|
@@ -85,15 +87,18 @@ const WAIT_FOR_PATTERNS = {
|
|
|
85
87
|
"first-passed": /passed|✓/,
|
|
86
88
|
"first-failed": /failed|✘|×/,
|
|
87
89
|
};
|
|
90
|
+
const MARKER_RE = /@@ ([A-Z0-9-]+) @@/g;
|
|
88
91
|
/**
|
|
89
|
-
* Spawns `playwright test` with the IBR reporter
|
|
90
|
-
*
|
|
92
|
+
* Spawns `playwright test` with the IBR reporter in its own process group,
|
|
93
|
+
* signals the group mid-run, and waits for exit.
|
|
91
94
|
*
|
|
92
|
-
*
|
|
95
|
+
* The process group and the signal both match production: `cmd.ts` spawns the
|
|
96
|
+
* runner detached and does `process.kill(-pid, signal)`.
|
|
93
97
|
*/
|
|
94
|
-
async function
|
|
95
|
-
const { workers, waitFor } = opts;
|
|
96
|
-
const
|
|
98
|
+
async function runPlaywrightWithSignal(ctx, opts) {
|
|
99
|
+
const { workers, waitForMarker, waitFor, delayBeforeSignalMs = 0, signal = "SIGINT", secondSignalAfterMs, killAfterMs, triggerTimeoutMs = 30_000, } = opts;
|
|
100
|
+
const startedAt = Date.now();
|
|
101
|
+
const since = () => Date.now() - startedAt;
|
|
97
102
|
const child = (0, child_process_1.spawn)(PLAYWRIGHT_BIN, ["test", `--workers=${workers}`], {
|
|
98
103
|
cwd: ctx.tempDir,
|
|
99
104
|
env: {
|
|
@@ -101,40 +106,120 @@ async function runPlaywrightWithSigint(ctx, opts) {
|
|
|
101
106
|
LOCAL_TEST: "false",
|
|
102
107
|
SHARD_INDEX: "1",
|
|
103
108
|
TOTAL_SHARDS: "1",
|
|
109
|
+
FORCE_COLOR: "0",
|
|
104
110
|
},
|
|
105
111
|
stdio: ["pipe", "pipe", "pipe"],
|
|
106
112
|
detached: true,
|
|
107
113
|
});
|
|
108
114
|
let stdout = "";
|
|
109
115
|
let stderr = "";
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
116
|
+
const markers = [];
|
|
117
|
+
const collect = (chunk, isStderr) => {
|
|
118
|
+
const text = chunk.toString();
|
|
119
|
+
if (isStderr) {
|
|
120
|
+
stderr += text;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
stdout += text;
|
|
124
|
+
}
|
|
125
|
+
for (const match of text.matchAll(MARKER_RE)) {
|
|
126
|
+
markers.push({ name: match[1], atMs: since() });
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
child.stdout?.on("data", (data) => collect(data, false));
|
|
130
|
+
child.stderr?.on("data", (data) => collect(data, true));
|
|
131
|
+
let exitCode = null;
|
|
132
|
+
let exitSignal = null;
|
|
133
|
+
let hasExited = false;
|
|
134
|
+
const closed = new Promise((resolve) => {
|
|
135
|
+
child.on("close", (code, sig) => {
|
|
136
|
+
hasExited = true;
|
|
137
|
+
exitCode = code;
|
|
138
|
+
exitSignal = sig;
|
|
139
|
+
resolve();
|
|
140
|
+
});
|
|
128
141
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
142
|
+
const signalGroup = (sig) => {
|
|
143
|
+
if (hasExited || !child.pid) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
process.kill(-child.pid, sig);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// The group is already gone.
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const pattern = waitFor ? WAIT_FOR_PATTERNS[waitFor] : undefined;
|
|
154
|
+
const isTriggered = () => waitForMarker
|
|
155
|
+
? markers.some((m) => m.name === waitForMarker)
|
|
156
|
+
: pattern
|
|
157
|
+
? pattern.test(stdout)
|
|
158
|
+
: true;
|
|
159
|
+
await Promise.race([
|
|
160
|
+
new Promise((resolve) => {
|
|
161
|
+
const interval = setInterval(() => {
|
|
162
|
+
if (isTriggered()) {
|
|
163
|
+
clearInterval(interval);
|
|
164
|
+
resolve();
|
|
165
|
+
}
|
|
166
|
+
}, 50);
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
clearInterval(interval);
|
|
169
|
+
resolve();
|
|
170
|
+
}, triggerTimeoutMs);
|
|
171
|
+
}),
|
|
172
|
+
closed,
|
|
173
|
+
]);
|
|
174
|
+
if (delayBeforeSignalMs > 0) {
|
|
175
|
+
await sleep(delayBeforeSignalMs);
|
|
176
|
+
}
|
|
177
|
+
const signalSentAtMs = since();
|
|
178
|
+
signalGroup(signal);
|
|
179
|
+
const timers = [];
|
|
180
|
+
if (secondSignalAfterMs !== undefined) {
|
|
181
|
+
timers.push(setTimeout(() => signalGroup(signal), secondSignalAfterMs));
|
|
182
|
+
}
|
|
183
|
+
if (killAfterMs !== undefined) {
|
|
184
|
+
timers.push(setTimeout(() => signalGroup("SIGKILL"), killAfterMs));
|
|
185
|
+
}
|
|
186
|
+
// Backstop: never leave a runner behind if the suite misbehaves.
|
|
187
|
+
timers.push(setTimeout(() => signalGroup("SIGKILL"), 120_000));
|
|
188
|
+
try {
|
|
189
|
+
await closed;
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
for (const timer of timers) {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
stdout,
|
|
198
|
+
stderr,
|
|
199
|
+
exitCode,
|
|
200
|
+
exitSignal,
|
|
201
|
+
markers,
|
|
202
|
+
signalSentAtMs,
|
|
203
|
+
startedAtEpochMs: startedAt,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Spawns `playwright test` with the IBR reporter, waits for a test outcome,
|
|
208
|
+
* sends SIGINT, and waits for the process to exit.
|
|
209
|
+
*
|
|
210
|
+
* Returns captured stdout/stderr.
|
|
211
|
+
*/
|
|
212
|
+
async function runPlaywrightWithSigint(ctx, opts) {
|
|
213
|
+
return runPlaywrightWithSignal(ctx, {
|
|
214
|
+
workers: opts.workers,
|
|
215
|
+
waitFor: opts.waitFor,
|
|
216
|
+
// Waiting for 1 sec to ensure second test has started
|
|
217
|
+
delayBeforeSignalMs: 1_000,
|
|
136
218
|
});
|
|
137
|
-
|
|
219
|
+
}
|
|
220
|
+
/** Marker names in the order they were printed. */
|
|
221
|
+
function markerNames(result) {
|
|
222
|
+
return result.markers.map((m) => m.name);
|
|
138
223
|
}
|
|
139
224
|
async function runPlaywrightList(ctx) {
|
|
140
225
|
const child = (0, child_process_1.spawn)(PLAYWRIGHT_BIN, ["test", "--list"], {
|
|
@@ -31,7 +31,13 @@ declare class IncrementalBlobReporter implements Reporter {
|
|
|
31
31
|
private _removeSignalHandler;
|
|
32
32
|
private _buildSyntheticResult;
|
|
33
33
|
private _finalizeAndMaybeUpload;
|
|
34
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Computes the lines to write, without touching `_reportLines`: recording
|
|
36
|
+
* continues after the SIGINT salvage, and the onEnd pass re-finalizes from
|
|
37
|
+
* the full stream — mutating here would make the second pass work from the
|
|
38
|
+
* first pass's pruned leftovers.
|
|
39
|
+
*/
|
|
40
|
+
private _buildFinalReportLines;
|
|
35
41
|
private _computeCompletedSets;
|
|
36
42
|
private _removeIncompleteTestArtifacts;
|
|
37
43
|
private _patchProjectSuites;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"incremental-blob-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/incremental-blob-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,UAAU,EACV,QAAQ,EACT,MAAM,2BAA2B,CAAC;AAwBnC;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"incremental-blob-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/incremental-blob-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,UAAU,EACV,QAAQ,EACT,MAAM,2BAA2B,CAAC;AAwBnC;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AASnD,cAAM,uBAAwB,YAAW,QAAQ;IAC/C,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,WAAW,CAIjB;IACF,OAAO,CAAC,UAAU,CAGhB;IACF,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,mBAAmB,CAA0B;IACrD,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,YAAY,CAAsD;IAC1E,OAAO,CAAC,UAAU,CAAsB;IACxC,OAAO,CAAC,gBAAgB,CAA8B;IAEtD,cAWC;IAED,OAAO,CAAC,cAAc,CAA6B;IAEnD,OAAO,CAAC,mBAAmB;IA+B3B,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,qBAAqB;YASf,uBAAuB;IAqErC;;;;;OAKG;IACH,OAAO,CAAC,sBAAsB;IAuB9B,OAAO,CAAC,qBAAqB;IA0B7B,OAAO,CAAC,8BAA8B;IAWtC,OAAO,CAAC,mBAAmB;IAmB3B,OAAO,KAAK,QAAQ,GAKnB;IAED,OAAO,KAAK,UAAU,GAErB;IAED,OAAO,KAAK,aAAa,GAExB;IAED,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,aAAa;YAUP,SAAS;IAwBvB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAc9C;IAED,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAEpD;IAED,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CA0BlD;IAED,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAUpE;IAED,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAUlE;IAEK,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAiC7C;IAED;;OAEG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAE3D;IAED,aAAa,IAAI,OAAO,CAEvB;CACF;AAID,wBAAgB,0BAA0B,IAAI,uBAAuB,GAAG,IAAI,CAE3E;AAED,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,uBAAuB,GAChC,IAAI,CAEN;eAEc,uBAAuB"}
|
|
@@ -28,7 +28,7 @@ class IncrementalBlobReporter {
|
|
|
28
28
|
_completedTestIds = new Set();
|
|
29
29
|
_completedResultIds = new Set();
|
|
30
30
|
_testRetries = new Map(); // testId → configured retries
|
|
31
|
-
_testResults = new Map(); // testId → retryIndex →
|
|
31
|
+
_testResults = new Map(); // testId → retryIndex → RecordedRetry
|
|
32
32
|
_startTime = Date.now();
|
|
33
33
|
_finalizePromise = null;
|
|
34
34
|
constructor() {
|
|
@@ -101,8 +101,8 @@ class IncrementalBlobReporter {
|
|
|
101
101
|
if (this._uploader) {
|
|
102
102
|
await this._uploader.waitForUploads();
|
|
103
103
|
}
|
|
104
|
-
this.
|
|
105
|
-
await this._writeZip();
|
|
104
|
+
const lines = this._buildFinalReportLines(result, pruneIncompleteTests);
|
|
105
|
+
await this._writeZip(lines);
|
|
106
106
|
if (!upload) {
|
|
107
107
|
logFinalizeComplete();
|
|
108
108
|
return;
|
|
@@ -120,32 +120,49 @@ class IncrementalBlobReporter {
|
|
|
120
120
|
}
|
|
121
121
|
logFinalizeComplete();
|
|
122
122
|
}
|
|
123
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Computes the lines to write, without touching `_reportLines`: recording
|
|
125
|
+
* continues after the SIGINT salvage, and the onEnd pass re-finalizes from
|
|
126
|
+
* the full stream — mutating here would make the second pass work from the
|
|
127
|
+
* first pass's pruned leftovers.
|
|
128
|
+
*/
|
|
129
|
+
_buildFinalReportLines(result, pruneIncompleteTests) {
|
|
130
|
+
let lines = [...this._reportLines];
|
|
124
131
|
if (pruneIncompleteTests) {
|
|
125
132
|
// Determine which tests are fully complete (passed or all retries finished)
|
|
126
133
|
this._computeCompletedSets();
|
|
127
134
|
// Remove unrun tests from onProject suites
|
|
128
|
-
this._patchProjectSuites();
|
|
135
|
+
lines = this._patchProjectSuites(lines);
|
|
129
136
|
// Remove events (onTestBegin, onStepBegin, etc.) for incomplete tests
|
|
130
|
-
this._removeIncompleteTestArtifacts();
|
|
137
|
+
lines = this._removeIncompleteTestArtifacts(lines);
|
|
131
138
|
}
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
139
|
+
// The onEnd event belongs only in the written report, never in the
|
|
140
|
+
// in-memory stream — a later finalize pass appends its own.
|
|
141
|
+
lines.push(JSON.stringify((0, lifecycle_events_1.buildOnEndEvent)(result)));
|
|
142
|
+
return lines;
|
|
135
143
|
}
|
|
136
144
|
_computeCompletedSets() {
|
|
137
145
|
for (const [testId, results] of this._testResults) {
|
|
138
146
|
const maxRetries = this._testRetries.get(testId);
|
|
139
|
-
|
|
147
|
+
// Shutdown artifacts are not finished retries: an "interrupted" result
|
|
148
|
+
// is a test cut off mid-flight, and a post-signal "skipped" is a serial
|
|
149
|
+
// sibling Playwright marks off while shutting down. The resume shard
|
|
150
|
+
// re-runs both, so counting them as complete would put their results in
|
|
151
|
+
// the final zip alongside the resume shard's, duplicating tests in the
|
|
152
|
+
// merge. A post-signal passed/failed is real, though: the test finished
|
|
153
|
+
// its body and only its aftermath overlapped the shutdown.
|
|
154
|
+
const finished = new Map([...results].filter(([, r]) => r.status !== "interrupted" &&
|
|
155
|
+
!(r.postSignal && r.status === "skipped")));
|
|
156
|
+
if ((0, reporter_1.areRetriesComplete)(maxRetries, finished)) {
|
|
140
157
|
this._completedTestIds.add(testId);
|
|
141
|
-
for (const { resultId } of
|
|
158
|
+
for (const { resultId } of finished.values()) {
|
|
142
159
|
this._completedResultIds.add(resultId);
|
|
143
160
|
}
|
|
144
161
|
}
|
|
145
162
|
}
|
|
146
163
|
}
|
|
147
|
-
_removeIncompleteTestArtifacts() {
|
|
148
|
-
|
|
164
|
+
_removeIncompleteTestArtifacts(lines) {
|
|
165
|
+
return lines.filter((line) => {
|
|
149
166
|
const parsed = JSON.parse(line);
|
|
150
167
|
const resultId = parsed.params?.resultId ?? parsed.params?.result?.id;
|
|
151
168
|
if (!resultId) {
|
|
@@ -154,20 +171,19 @@ class IncrementalBlobReporter {
|
|
|
154
171
|
return this._completedResultIds.has(resultId);
|
|
155
172
|
});
|
|
156
173
|
}
|
|
157
|
-
_patchProjectSuites() {
|
|
158
|
-
|
|
159
|
-
const line = this._reportLines[i];
|
|
174
|
+
_patchProjectSuites(lines) {
|
|
175
|
+
return lines.map((line) => {
|
|
160
176
|
const parsed = JSON.parse(line);
|
|
161
177
|
if (parsed.method !== "onProject") {
|
|
162
|
-
|
|
178
|
+
return line;
|
|
163
179
|
}
|
|
164
180
|
const project = parsed.params.project;
|
|
165
181
|
const filteredSuites = project.suites
|
|
166
182
|
.map((suite) => (0, ibr_utils_1.filterSuiteByCompletedTests)(suite, this._completedTestIds))
|
|
167
183
|
.filter(Boolean);
|
|
168
184
|
project.suites = filteredSuites;
|
|
169
|
-
|
|
170
|
-
}
|
|
185
|
+
return JSON.stringify(parsed);
|
|
186
|
+
});
|
|
171
187
|
}
|
|
172
188
|
get _zipPath() {
|
|
173
189
|
return path_1.default.join(this._outputDir, `incremental-report-${this._shardIndex}.zip`);
|
|
@@ -183,9 +199,10 @@ class IncrementalBlobReporter {
|
|
|
183
199
|
fs_1.default.mkdirSync(this._outputDir, { recursive: true });
|
|
184
200
|
}
|
|
185
201
|
_appendEvent(event) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
202
|
+
// Deliberately not gated on isFinalized(): the SIGINT salvage seals a
|
|
203
|
+
// *snapshot*, not the stream. Playwright keeps running afterEach/afterAll
|
|
204
|
+
// teardown after the signal, and those events feed the second finalize
|
|
205
|
+
// pass in onEnd.
|
|
189
206
|
this._reportLines.push(JSON.stringify(event));
|
|
190
207
|
return true;
|
|
191
208
|
}
|
|
@@ -198,10 +215,10 @@ class IncrementalBlobReporter {
|
|
|
198
215
|
}
|
|
199
216
|
return allAppended;
|
|
200
217
|
}
|
|
201
|
-
async _writeZip() {
|
|
218
|
+
async _writeZip(lines) {
|
|
202
219
|
this._ensureDirs();
|
|
203
220
|
// Write report.jsonl to the output directory
|
|
204
|
-
const jsonlContent =
|
|
221
|
+
const jsonlContent = lines.join("\n") + "\n";
|
|
205
222
|
fs_1.default.writeFileSync(this._jsonlPath, jsonlContent, "utf8");
|
|
206
223
|
// Write _empirical_urls.json with attachment URL mappings
|
|
207
224
|
const urlsJson = (0, ibr_utils_1.buildUrlsJson)(this._attachmentUrlMap);
|
|
@@ -209,7 +226,7 @@ class IncrementalBlobReporter {
|
|
|
209
226
|
// Create zip from the staging directory
|
|
210
227
|
const zipBuffer = await (0, zip_1.createZipFromDirectory)(this._stagingDir);
|
|
211
228
|
fs_1.default.writeFileSync(this._zipPath, zipBuffer);
|
|
212
|
-
console.log(`[IncrementalBlobReporter] Zip updated: ${this._zipPath} (${
|
|
229
|
+
console.log(`[IncrementalBlobReporter] Zip updated: ${this._zipPath} (${lines.length} events, ${this._attachmentUrlMap.size} attachments)`);
|
|
213
230
|
}
|
|
214
231
|
onBegin(config, suite) {
|
|
215
232
|
this._ensureDirs();
|
|
@@ -234,6 +251,9 @@ class IncrementalBlobReporter {
|
|
|
234
251
|
resultId,
|
|
235
252
|
status: result.status,
|
|
236
253
|
expectedStatus: test.expectedStatus,
|
|
254
|
+
// The SIGINT salvage has started; this result arrived during
|
|
255
|
+
// interruption shutdown rather than the normal run.
|
|
256
|
+
postSignal: this._finalizePromise !== null,
|
|
237
257
|
});
|
|
238
258
|
}
|
|
239
259
|
}
|
|
@@ -246,9 +266,19 @@ class IncrementalBlobReporter {
|
|
|
246
266
|
}
|
|
247
267
|
async onEnd(result) {
|
|
248
268
|
this._removeSignalHandler();
|
|
249
|
-
//
|
|
250
|
-
//
|
|
269
|
+
// Second phase of an interrupted run: the SIGINT pass salvaged a snapshot
|
|
270
|
+
// within milliseconds (SIGKILL insurance); by the time Playwright calls
|
|
271
|
+
// onEnd the teardown chain has finished, so re-finalize from the full
|
|
272
|
+
// stream and overwrite the salvage blob with the post-teardown report.
|
|
251
273
|
if (this._finalizePromise) {
|
|
274
|
+
await this._finalizePromise;
|
|
275
|
+
this._finalizePromise = this._finalizeAndMaybeUpload({
|
|
276
|
+
result,
|
|
277
|
+
source: "onEnd",
|
|
278
|
+
markGlobalFinalized: false,
|
|
279
|
+
pruneIncompleteTests: true,
|
|
280
|
+
upload: true,
|
|
281
|
+
});
|
|
252
282
|
await this._finalizePromise;
|
|
253
283
|
return;
|
|
254
284
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StorageCredentials } from "@empiricalrun/r2-uploader";
|
|
1
|
+
import { StorageCredentials, UploadStorageClass } from "@empiricalrun/r2-uploader";
|
|
2
2
|
export interface UploaderConfig {
|
|
3
3
|
projectSlug: string;
|
|
4
4
|
runId: string;
|
|
@@ -19,7 +19,7 @@ export declare class Uploader {
|
|
|
19
19
|
get baseUrl(): string;
|
|
20
20
|
get destinationDir(): string;
|
|
21
21
|
get urlPrefix(): string;
|
|
22
|
-
uploadFile(filePath: string, destinationPath: string): Promise<string | null>;
|
|
22
|
+
uploadFile(filePath: string, destinationPath: string, storageClass?: UploadStorageClass): Promise<string | null>;
|
|
23
23
|
uploadDirectory(sourceDir: string, destinationPrefix: string): Promise<void>;
|
|
24
24
|
waitForUploads(): Promise<void>;
|
|
25
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/reporter/uploader.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,kBAAkB,
|
|
1
|
+
{"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/reporter/uploader.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,kBAAkB,EAElB,kBAAkB,EAEnB,MAAM,2BAA2B,CAAC;AAKnC,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,YAAY,CAAqB;IAEzC,YAAY,MAAM,EAAE,cAAc,EASjC;IAED,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAgBhC,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAUhC,MAAM,CAAC,qBAAqB,CAAC,QAAQ,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CAKzE;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAEK,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,kBAAkB,GAChC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgCxB;IAEK,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,IAAI,CAAC,CAef;IAEK,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAEpC;CACF;AAID,wBAAgB,WAAW,IAAI,QAAQ,GAAG,IAAI,CAM7C"}
|
|
@@ -60,7 +60,7 @@ class Uploader {
|
|
|
60
60
|
get urlPrefix() {
|
|
61
61
|
return `${this._baseUrl}/${this._destinationDir}`;
|
|
62
62
|
}
|
|
63
|
-
async uploadFile(filePath, destinationPath) {
|
|
63
|
+
async uploadFile(filePath, destinationPath, storageClass) {
|
|
64
64
|
try {
|
|
65
65
|
const fullDestinationPath = path_1.default.join(this._destinationDir, destinationPath);
|
|
66
66
|
const sourceDir = path_1.default.dirname(filePath);
|
|
@@ -71,6 +71,10 @@ class Uploader {
|
|
|
71
71
|
uploadBucket: this._uploadBucket,
|
|
72
72
|
baseUrl: this._baseUrl,
|
|
73
73
|
credentials: this._credentials,
|
|
74
|
+
storageClass: this._credentials.provider === "s3" ||
|
|
75
|
+
this._credentials.provider === "s3-iam"
|
|
76
|
+
? storageClass
|
|
77
|
+
: undefined,
|
|
74
78
|
});
|
|
75
79
|
const result = await (0, r2_uploader_1.sendTaskToQueue)(uploadTask);
|
|
76
80
|
if (result) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empiricalrun/playwright-utils",
|
|
3
|
-
"version": "0.58.
|
|
3
|
+
"version": "0.58.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/",
|
|
6
6
|
"access": "public"
|
|
@@ -30,16 +30,16 @@
|
|
|
30
30
|
},
|
|
31
31
|
"author": "Empirical Team <hey@empirical.run>",
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@playwright/test": "1.
|
|
33
|
+
"@playwright/test": "1.62.1",
|
|
34
34
|
"@types/async-retry": "^1.4.8",
|
|
35
35
|
"@types/authenticator": "^1.1.4",
|
|
36
36
|
"@types/babel__code-frame": "^7.0.6",
|
|
37
37
|
"@types/console-log-level": "^1.4.5",
|
|
38
38
|
"@types/mime": "3.0.0",
|
|
39
|
-
"@types/node": "^
|
|
39
|
+
"@types/node": "^22",
|
|
40
40
|
"@types/serve-handler": "^6.1.4",
|
|
41
41
|
"openapi-typescript": "7.6.1",
|
|
42
|
-
"playwright-core": "1.
|
|
42
|
+
"playwright-core": "1.62.1",
|
|
43
43
|
"serve-handler": "^6.1.6",
|
|
44
44
|
"@empiricalrun/shared-types": "0.15.3"
|
|
45
45
|
},
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"ts-morph": "^23.0.0",
|
|
57
57
|
"@empiricalrun/cua": "^0.4.1",
|
|
58
58
|
"@empiricalrun/llm": "^0.27.0",
|
|
59
|
-
"@empiricalrun/
|
|
60
|
-
"@empiricalrun/
|
|
59
|
+
"@empiricalrun/r2-uploader": "^0.10.1",
|
|
60
|
+
"@empiricalrun/reporter": "^0.29.1"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"dev": "pnpm gen:api-worker-openapi && tsgo --build --watch",
|