@empiricalrun/playwright-utils 0.58.3 → 0.58.5
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 +20 -0
- package/dist/generated/api-worker-openapi.d.ts +98 -88
- 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 +9 -0
- package/dist/reporter/harness.d.ts +4 -0
- package/dist/reporter/harness.d.ts.map +1 -1
- package/dist/reporter/harness.js +20 -8
- package/dist/reporter/ibr-utils.d.ts +3 -0
- package/dist/reporter/ibr-utils.d.ts.map +1 -1
- package/dist/reporter/ibr-utils.js +25 -18
- package/dist/reporter/incremental-blob-reporter.d.ts +1 -0
- package/dist/reporter/incremental-blob-reporter.d.ts.map +1 -1
- package/dist/reporter/incremental-blob-reporter.js +5 -4
- package/dist/reporter/lifecycle-events.d.ts +15 -5
- package/dist/reporter/lifecycle-events.d.ts.map +1 -1
- package/dist/reporter/lifecycle-events.js +91 -64
- package/dist/reporter/parity-utils.d.ts +21 -0
- package/dist/reporter/parity-utils.d.ts.map +1 -1
- package/dist/reporter/parity-utils.js +73 -1
- package/dist/reporter/uploader.d.ts.map +1 -1
- package/dist/reporter/uploader.js +22 -1
- package/package.json +4 -4
- package/upstream-oracle/README.md +36 -7
- package/upstream-oracle/ibr-adapter.js +60 -5
- package/upstream-oracle/run.mjs +288 -47
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.normalizeJsonReport = normalizeJsonReport;
|
|
4
4
|
exports.stepSignatures = stepSignatures;
|
|
5
|
+
exports.stepAttachmentsAtStepEnd = stepAttachmentsAtStepEnd;
|
|
6
|
+
exports.configAndProjects = configAndProjects;
|
|
5
7
|
exports.missingEventMethods = missingEventMethods;
|
|
6
8
|
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
7
9
|
function normalizeText(text, cwd) {
|
|
@@ -93,11 +95,81 @@ function stepSignatures(events) {
|
|
|
93
95
|
continue;
|
|
94
96
|
}
|
|
95
97
|
const list = signatures.get(key) ?? [];
|
|
96
|
-
|
|
98
|
+
// The location is part of the signature: it travels relative to rootDir
|
|
99
|
+
// and the merged HTML report renders it as-is.
|
|
100
|
+
const location = params.step.location
|
|
101
|
+
? `${params.step.location.file}:${params.step.location.line}`
|
|
102
|
+
: "";
|
|
103
|
+
list.push(`${params.step.category}:${params.step.title}@${location}`);
|
|
97
104
|
signatures.set(key, list);
|
|
98
105
|
}
|
|
99
106
|
return signatures;
|
|
100
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* What a reporter would see for `step.attachments` DURING onStepEnd, replayed
|
|
110
|
+
* from the raw event stream the way teleReceiver does it: onAttach pushes onto
|
|
111
|
+
* `result.attachments`, and a step end carries indices into that array
|
|
112
|
+
* (TeleTestStep.attachments resolves them at access time). An attachment that
|
|
113
|
+
* only arrives at test end therefore reads as `<missing>` here.
|
|
114
|
+
*
|
|
115
|
+
* Keyed by `testId:retry`; each entry is one `title -> [names]` line per
|
|
116
|
+
* onStepEnd, in stream order.
|
|
117
|
+
*/
|
|
118
|
+
function stepAttachmentsAtStepEnd(events) {
|
|
119
|
+
const resultKey = new Map(); // resultId -> testId:retry
|
|
120
|
+
const attachmentNames = new Map(); // resultId -> names so far
|
|
121
|
+
const stepTitles = new Map(); // stepId -> title
|
|
122
|
+
const views = new Map();
|
|
123
|
+
for (const event of events) {
|
|
124
|
+
if (event.method === "onTestBegin") {
|
|
125
|
+
const params = event.params;
|
|
126
|
+
resultKey.set(params.result.id, `${params.testId}:${params.result.retry}`);
|
|
127
|
+
attachmentNames.set(params.result.id, []);
|
|
128
|
+
}
|
|
129
|
+
else if (event.method === "onAttach") {
|
|
130
|
+
const params = event.params;
|
|
131
|
+
const names = attachmentNames.get(params.resultId);
|
|
132
|
+
names?.push(...params.attachments.map((a) => a.name));
|
|
133
|
+
}
|
|
134
|
+
else if (event.method === "onStepBegin") {
|
|
135
|
+
const params = event.params;
|
|
136
|
+
stepTitles.set(params.step.id, params.step.title);
|
|
137
|
+
}
|
|
138
|
+
else if (event.method === "onStepEnd") {
|
|
139
|
+
const params = event.params;
|
|
140
|
+
const key = resultKey.get(params.resultId);
|
|
141
|
+
if (!key) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const names = attachmentNames.get(params.resultId) ?? [];
|
|
145
|
+
const resolved = (params.step.attachments ?? []).map((index) => names[index] ?? "<missing>");
|
|
146
|
+
const list = views.get(key) ?? [];
|
|
147
|
+
list.push(`${stepTitles.get(params.step.id) ?? "?"} -> [${resolved.join(", ")}]`);
|
|
148
|
+
views.set(key, list);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return views;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The onConfigure config payload plus every onProject project payload, keyed
|
|
155
|
+
* by project name. The merged report's FullConfig/FullProject is rebuilt from
|
|
156
|
+
* exactly these, and normalizeJsonReport compares only the suite/stats body —
|
|
157
|
+
* so this is where config and project serialization drift shows up.
|
|
158
|
+
*/
|
|
159
|
+
function configAndProjects(events) {
|
|
160
|
+
let config;
|
|
161
|
+
const projects = {};
|
|
162
|
+
for (const event of events) {
|
|
163
|
+
if (event.method === "onConfigure") {
|
|
164
|
+
config = event.params.config;
|
|
165
|
+
}
|
|
166
|
+
else if (event.method === "onProject") {
|
|
167
|
+
const project = event.params.project;
|
|
168
|
+
projects[project.name] = project;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { config, projects };
|
|
172
|
+
}
|
|
101
173
|
/** Event method names present in `reference` but absent from `candidate`. */
|
|
102
174
|
function missingEventMethods(reference, candidate) {
|
|
103
175
|
const candidateMethods = new Set(candidate.map((e) => e.method));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/reporter/uploader.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/reporter/uploader.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,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,CA4BpC;CACF;AAID,wBAAgB,WAAW,IAAI,QAAQ,GAAG,IAAI,CAM7C"}
|
|
@@ -100,7 +100,28 @@ class Uploader {
|
|
|
100
100
|
await (0, r2_uploader_1.sendTaskToQueue)(uploadTask);
|
|
101
101
|
}
|
|
102
102
|
async waitForUploads() {
|
|
103
|
-
|
|
103
|
+
const { waiting, executing } = (0, r2_uploader_1.getTaskQueueStats)();
|
|
104
|
+
const pending = waiting + executing;
|
|
105
|
+
if (pending === 0) {
|
|
106
|
+
await (0, r2_uploader_1.waitForTaskQueueToFinish)();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// This drain runs after the last test with no other output (observed at
|
|
110
|
+
// 4+ minutes on spot runs), so make it visible: count up front, progress
|
|
111
|
+
// every 30s, duration at the end.
|
|
112
|
+
const start = Date.now();
|
|
113
|
+
console.log(`[Uploader] Waiting for ${pending} pending uploads (${executing} executing, ${waiting} queued)...`);
|
|
114
|
+
const progress = setInterval(() => {
|
|
115
|
+
const stats = (0, r2_uploader_1.getTaskQueueStats)();
|
|
116
|
+
console.log(`[Uploader] Still draining: ${stats.executing + stats.waiting} uploads pending (${stats.executing} executing, ${stats.waiting} queued)`);
|
|
117
|
+
}, 30_000);
|
|
118
|
+
try {
|
|
119
|
+
await (0, r2_uploader_1.waitForTaskQueueToFinish)();
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
clearInterval(progress);
|
|
123
|
+
}
|
|
124
|
+
console.log(`[Uploader] Pending uploads drained in ${((Date.now() - start) / 1000).toFixed(1)}s`);
|
|
104
125
|
}
|
|
105
126
|
}
|
|
106
127
|
exports.Uploader = Uploader;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empiricalrun/playwright-utils",
|
|
3
|
-
"version": "0.58.
|
|
3
|
+
"version": "0.58.5",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/",
|
|
6
6
|
"access": "public"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"openapi-typescript": "7.6.1",
|
|
42
42
|
"playwright-core": "1.62.1",
|
|
43
43
|
"serve-handler": "^6.1.6",
|
|
44
|
-
"@empiricalrun/shared-types": "0.15.
|
|
44
|
+
"@empiricalrun/shared-types": "0.15.4"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@babel/code-frame": "^7.24.7",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"puppeteer-extra-plugin-recaptcha": "^3.6.8",
|
|
55
55
|
"rimraf": "^6.0.1",
|
|
56
56
|
"ts-morph": "^23.0.0",
|
|
57
|
-
"@empiricalrun/llm": "^0.27.0",
|
|
58
57
|
"@empiricalrun/cua": "^0.4.1",
|
|
59
|
-
"@empiricalrun/
|
|
58
|
+
"@empiricalrun/llm": "^0.27.0",
|
|
59
|
+
"@empiricalrun/r2-uploader": "^0.10.2",
|
|
60
60
|
"@empiricalrun/reporter": "^0.29.1"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
@@ -20,16 +20,45 @@ Playwright version-pin bump).
|
|
|
20
20
|
|
|
21
21
|
- `run.mjs` — orchestrator: applies the patch, runs the curated specs in both
|
|
22
22
|
modes, fails on any IBR-only failure.
|
|
23
|
-
- `ibr-adapter.js` — bridges IBR's
|
|
24
|
-
|
|
23
|
+
- `ibr-adapter.js` — bridges IBR's output contract to the blob reporter's: it
|
|
24
|
+
mirrors blob's zip name (`_defaultReportName`), output-dir resolution
|
|
25
|
+
(package root, not cwd) and pre-write cleanup, then relocates IBR's zip into
|
|
26
|
+
`blob-report/`. Sharding needs no bridge — IBR falls back to `config.shard`.
|
|
25
27
|
- `intermediate-mode-ibr.patch` — one-line upstream fixture change making the
|
|
26
28
|
intermediate reporter selectable via `PWTEST_IBR_ADAPTER`.
|
|
27
29
|
|
|
28
|
-
The
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
The failure count reaching zero is the signal that the built-in blob reporter
|
|
31
|
+
is safe to drop. It got there over #7093/#7094/#7095 (the parity gaps and the
|
|
32
|
+
full-suite sweep) and then #7110/#7111/#7114/this PR, which took the full
|
|
33
|
+
sweep from 19 IBR-only failures to 0.
|
|
34
|
+
|
|
35
|
+
### Expected failures
|
|
36
|
+
|
|
37
|
+
`EXPECTED_IBR_ONLY` in `run.mjs` holds upstream tests that fail for reasons
|
|
38
|
+
outside the blob format — artifacts of injecting an external reporter into
|
|
39
|
+
upstream's harness. Each entry carries the reason it can never pass and is
|
|
40
|
+
printed at startup; the count is reported separately and does not turn the job
|
|
41
|
+
red. An entry whose spec runs and PASSES is reported as `[stale-expected]` and
|
|
42
|
+
fails the run, so the list cannot quietly absorb tests that have been fixed.
|
|
43
|
+
Anything that could be fixed does not belong there.
|
|
44
|
+
|
|
45
|
+
### Why two slow specs are excluded
|
|
46
|
+
|
|
47
|
+
`only-changed.spec.ts` and `web-server.spec.ts` are in `EXCLUSION_RULES` for a
|
|
48
|
+
different reason from the rest: cost, not scope. The oracle runs ONE spec per
|
|
49
|
+
invocation, and Playwright keeps a file's tests on a single worker
|
|
50
|
+
(`fullyParallel` defaults to false), so a long file runs strictly serially no
|
|
51
|
+
matter what `--workers` says. Measured, `only-changed` gets through 7 of 13
|
|
52
|
+
tests in 7 minutes — steady progress, no wedge — and `web-server` has 39
|
|
53
|
+
server-booting tests. Neither fits the 10-minute `SPEC_TIMEOUT_MS`, so both
|
|
54
|
+
used to burn the cap, get quarantined, and contribute nothing but a hang that
|
|
55
|
+
kept the job red.
|
|
56
|
+
|
|
57
|
+
If the sweep's wall-clock ever needs fixing properly, the lever is `run.mjs`
|
|
58
|
+
running specs sequentially: several invocations could run concurrently with no
|
|
59
|
+
loss of accuracy (each spec's blob/IBR pair still compares like for like),
|
|
60
|
+
once each gets its own `--output` dir instead of sharing
|
|
61
|
+
`<pwRepo>/test-results`.
|
|
33
62
|
|
|
34
63
|
## Running locally
|
|
35
64
|
|
|
@@ -45,6 +45,55 @@ class IBRBlobAdapter extends IBR {
|
|
|
45
45
|
super();
|
|
46
46
|
this._adapterOptions = options;
|
|
47
47
|
this._adapterCwd = process.cwd();
|
|
48
|
+
this._adapterShard = null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
onBegin(config, suite) {
|
|
52
|
+
this._adapterShard = config.shard;
|
|
53
|
+
super.onBegin(config, suite);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Mirrors resolveOutputFile('BLOB', …) + resolveReporterOutputPath: an
|
|
57
|
+
// explicit outputDir resolves against the config dir, and the DEFAULT
|
|
58
|
+
// blob-report hangs off the nearest package.json at or above the config
|
|
59
|
+
// dir (process.cwd() only as a last resort). Upstream drives the CLI from
|
|
60
|
+
// a nested dir in several tests and then merges <packageRoot>/blob-report,
|
|
61
|
+
// so resolving against the cwd puts the zip where nothing looks for it.
|
|
62
|
+
_resolveOutputDir() {
|
|
63
|
+
const configDir = this._adapterOptions.configDir || this._adapterCwd;
|
|
64
|
+
if (this._adapterOptions.outputDir) {
|
|
65
|
+
return path.resolve(configDir, this._adapterOptions.outputDir);
|
|
66
|
+
}
|
|
67
|
+
return path.resolve(this._packageRoot(configDir), "blob-report");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_packageRoot(configDir) {
|
|
71
|
+
let dir = path.resolve(configDir);
|
|
72
|
+
for (;;) {
|
|
73
|
+
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
74
|
+
return dir;
|
|
75
|
+
}
|
|
76
|
+
const parent = path.dirname(dir);
|
|
77
|
+
if (parent === dir) {
|
|
78
|
+
return this._adapterCwd;
|
|
79
|
+
}
|
|
80
|
+
dir = parent;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Mirrors blob's _defaultReportName: the shard suffix only appears on a
|
|
85
|
+
// sharded run, zero-padded to the width of the shard total. Upstream tests
|
|
86
|
+
// assert on the file name (reporter-blob.spec's "preserve steps in html
|
|
87
|
+
// report" expects exactly ['report.zip']).
|
|
88
|
+
_defaultReportName() {
|
|
89
|
+
if (!this._adapterShard) {
|
|
90
|
+
return "report.zip";
|
|
91
|
+
}
|
|
92
|
+
const padded = `${this._adapterShard.current}`.padStart(
|
|
93
|
+
`${this._adapterShard.total}`.length,
|
|
94
|
+
"0",
|
|
95
|
+
);
|
|
96
|
+
return `report-${padded}.zip`;
|
|
48
97
|
}
|
|
49
98
|
|
|
50
99
|
async onEnd(result) {
|
|
@@ -59,11 +108,17 @@ class IBRBlobAdapter extends IBR {
|
|
|
59
108
|
// Mirrors blob: no zip when there was nothing to report.
|
|
60
109
|
return;
|
|
61
110
|
}
|
|
62
|
-
const outputDir =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
111
|
+
const outputDir = this._resolveOutputDir();
|
|
112
|
+
const fileName = this._adapterOptions.fileName || this._defaultReportName();
|
|
113
|
+
// Mirrors blob's _prepareOutputFile, down to the opt-out: it wipes the
|
|
114
|
+
// output dir before writing unless PWTEST_BLOB_DO_NOT_REMOVE is set.
|
|
115
|
+
// Upstream tests merge whatever sits in blob-report/, and they run
|
|
116
|
+
// several shards from one baseDir — those that want the shards to
|
|
117
|
+
// accumulate set the env var, the rest (shard.spec's "shard 2/3 then
|
|
118
|
+
// 3/3") expect each run to start from an empty dir.
|
|
119
|
+
if (!process.env.PWTEST_BLOB_DO_NOT_REMOVE) {
|
|
120
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
67
122
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
68
123
|
fs.copyFileSync(src, path.join(outputDir, fileName));
|
|
69
124
|
}
|