@empiricalrun/playwright-utils 0.58.4 → 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 +11 -0
- package/dist/generated/api-worker-openapi.d.ts +0 -88
- package/dist/generated/api-worker-openapi.d.ts.map +1 -1
- 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 +2 -2
- package/upstream-oracle/README.md +36 -7
- package/upstream-oracle/ibr-adapter.js +60 -5
- package/upstream-oracle/run.mjs +84 -7
|
@@ -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"
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"ts-morph": "^23.0.0",
|
|
57
57
|
"@empiricalrun/cua": "^0.4.1",
|
|
58
58
|
"@empiricalrun/llm": "^0.27.0",
|
|
59
|
-
"@empiricalrun/r2-uploader": "^0.10.
|
|
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
|
}
|
package/upstream-oracle/run.mjs
CHANGED
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
* Both modes run the same spec files; environment noise cancels in the
|
|
13
13
|
* baseline diff. Any IBR-only failure fails the run — the job stays red
|
|
14
14
|
* until IBR reaches parity on these specs. It staying green is the signal
|
|
15
|
-
* that the built-in blob reporter is safe to drop.
|
|
15
|
+
* that the built-in blob reporter is safe to drop. The one carve-out is
|
|
16
|
+
* EXPECTED_IBR_ONLY below: failures caused by injecting an external reporter
|
|
17
|
+
* rather than by the blob format, each with the reason it cannot pass, and
|
|
18
|
+
* each policed by a staleness check so the list cannot rot.
|
|
16
19
|
*
|
|
17
20
|
* Usage:
|
|
18
21
|
* node run.mjs --pw-repo <upstream clone at the pinned tag>
|
|
@@ -65,8 +68,51 @@ const EXCLUSION_RULES = [
|
|
|
65
68
|
"semantics are covered by reporter-blob.spec.ts) and they busy-loop " +
|
|
66
69
|
"workers under load, wedging the sweep",
|
|
67
70
|
],
|
|
71
|
+
// The two specs below are SLOW, not wedged — measured, they make steady
|
|
72
|
+
// progress and simply do not fit in SPEC_TIMEOUT_MS. The oracle runs one
|
|
73
|
+
// spec per invocation and Playwright keeps a file's tests on a single
|
|
74
|
+
// worker (fullyParallel defaults to false), so a long file runs strictly
|
|
75
|
+
// serially however many --workers it is given. Both were already
|
|
76
|
+
// contributing no signal, just a 10-minute timeout each.
|
|
77
|
+
[
|
|
78
|
+
(name) => name === "only-changed.spec.ts",
|
|
79
|
+
"git-based run selection (--only-changed), not report format; 13 tests " +
|
|
80
|
+
"that each shell out to git take ~13min serially here",
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
(name) => name === "web-server.spec.ts",
|
|
84
|
+
"webServer config and lifecycle, not report format; 39 tests that each " +
|
|
85
|
+
"boot and tear down a server take longer still",
|
|
86
|
+
],
|
|
68
87
|
];
|
|
69
88
|
|
|
89
|
+
// Upstream tests that fail through IBR for reasons OUTSIDE the blob format —
|
|
90
|
+
// artifacts of injecting an external reporter into upstream's harness, not
|
|
91
|
+
// gaps in what IBR writes. They are reported and subtracted from the failure
|
|
92
|
+
// count instead of turning the job red.
|
|
93
|
+
//
|
|
94
|
+
// Kept honest by the staleness check at the end: an entry whose spec ran and
|
|
95
|
+
// did NOT fail is reported and fails the run, so a fixed test cannot sit here
|
|
96
|
+
// pretending to be unfixable. Add an entry only with the reason it can never
|
|
97
|
+
// pass; anything that could be fixed belongs in the failure list.
|
|
98
|
+
const EXPECTED_IBR_ONLY = [
|
|
99
|
+
{
|
|
100
|
+
key:
|
|
101
|
+
"cache.spec.ts:53 › should automatically clean cached versions of a " +
|
|
102
|
+
"changed file",
|
|
103
|
+
reason:
|
|
104
|
+
"asserts the transform cache holds exactly one directory; loading IBR " +
|
|
105
|
+
"from a path runs its bundle through the same transform pipeline, so " +
|
|
106
|
+
"the cache gains a directory per module (32 observed). The built-in " +
|
|
107
|
+
"blob reporter ships inside Playwright and never enters that cache.",
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
/** Spec file name out of a failure key ("cache.spec.ts:53 › title"). */
|
|
112
|
+
function specOfKey(key) {
|
|
113
|
+
return key.slice(0, key.indexOf(":"));
|
|
114
|
+
}
|
|
115
|
+
|
|
70
116
|
// Tests skipped by title via --grep-invert. Reserved for tests that WEDGE
|
|
71
117
|
// the sweep — a busy-looped worker no per-test timeout can interrupt — so
|
|
72
118
|
// exclusion is surgical (exact title match) and every entry carries the
|
|
@@ -316,12 +362,18 @@ console.log(`Specs: ${args.specs.join(", ")}`);
|
|
|
316
362
|
for (const { pattern, reason } of HANGING_TESTS) {
|
|
317
363
|
console.log(` [skipped-by-title] /${pattern}/ — ${reason}`);
|
|
318
364
|
}
|
|
365
|
+
for (const { key, reason } of EXPECTED_IBR_ONLY) {
|
|
366
|
+
console.log(` [expected-ibr-only] ${key} — ${reason}`);
|
|
367
|
+
}
|
|
319
368
|
// Each spec runs both modes back to back as its own bounded invocation
|
|
320
369
|
// (see SPEC_TIMEOUT_MS); a hang or count mismatch quarantines that spec and
|
|
321
370
|
// the sweep continues.
|
|
322
371
|
const ibrOnly = [];
|
|
323
372
|
const hungSpecs = [];
|
|
324
373
|
const mismatchedSpecs = [];
|
|
374
|
+
const expectedByKey = new Map(EXPECTED_IBR_ONLY.map((e) => [e.key, e]));
|
|
375
|
+
const matchedExpected = new Set();
|
|
376
|
+
const comparedSpecs = new Set();
|
|
325
377
|
let baselineNoise = 0;
|
|
326
378
|
|
|
327
379
|
for (const spec of args.specs) {
|
|
@@ -355,21 +407,31 @@ for (const spec of args.specs) {
|
|
|
355
407
|
mismatchedSpecs.push(spec);
|
|
356
408
|
continue;
|
|
357
409
|
}
|
|
410
|
+
comparedSpecs.add(spec);
|
|
358
411
|
const specIbrOnly = [...ibr.failures]
|
|
359
412
|
.filter((key) => !blob.failures.has(key))
|
|
360
413
|
.sort();
|
|
361
414
|
baselineNoise += blob.failures.size;
|
|
362
|
-
|
|
415
|
+
const specExpected = specIbrOnly.filter((key) => expectedByKey.has(key));
|
|
416
|
+
for (const key of specExpected) {
|
|
417
|
+
matchedExpected.add(key);
|
|
418
|
+
}
|
|
419
|
+
const specUnexpected = specIbrOnly.filter((key) => !expectedByKey.has(key));
|
|
420
|
+
ibrOnly.push(...specUnexpected);
|
|
363
421
|
console.log(
|
|
364
422
|
` ${spec}: ${blob.totalRun} tests, ` +
|
|
365
|
-
`${blob.failures.size} baseline noise, ${
|
|
366
|
-
|
|
423
|
+
`${blob.failures.size} baseline noise, ${specUnexpected.length} ibr-only` +
|
|
424
|
+
(specExpected.length ? `, ${specExpected.length} expected` : "") +
|
|
425
|
+
` (blob ${blob.elapsedSeconds}s, ibr ${ibr.elapsedSeconds}s)`,
|
|
367
426
|
);
|
|
368
427
|
}
|
|
369
428
|
|
|
370
429
|
console.log(
|
|
371
430
|
`\nBaseline failures (env noise, cancelled in diff): ${baselineNoise}`,
|
|
372
431
|
);
|
|
432
|
+
console.log(
|
|
433
|
+
`Expected IBR-only failures (not counted): ${matchedExpected.size}`,
|
|
434
|
+
);
|
|
373
435
|
console.log(`IBR-only failures: ${ibrOnly.length}`);
|
|
374
436
|
for (const key of ibrOnly.sort()) {
|
|
375
437
|
console.log(` [ibr-only] ${key}`);
|
|
@@ -381,12 +443,27 @@ for (const spec of mismatchedSpecs) {
|
|
|
381
443
|
console.log(` [mismatch] ${spec}`);
|
|
382
444
|
}
|
|
383
445
|
|
|
384
|
-
|
|
446
|
+
// An expected failure whose spec ran and passed means the list is stale —
|
|
447
|
+
// that is a fix to celebrate by deleting the entry, not a silent pass.
|
|
448
|
+
const staleExpected = EXPECTED_IBR_ONLY.filter(
|
|
449
|
+
({ key }) => comparedSpecs.has(specOfKey(key)) && !matchedExpected.has(key),
|
|
450
|
+
);
|
|
451
|
+
for (const { key } of staleExpected) {
|
|
452
|
+
console.log(` [stale-expected] ${key} — now passes; drop it from the list`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (
|
|
456
|
+
ibrOnly.length > 0 ||
|
|
457
|
+
hungSpecs.length > 0 ||
|
|
458
|
+
mismatchedSpecs.length > 0 ||
|
|
459
|
+
staleExpected.length > 0
|
|
460
|
+
) {
|
|
385
461
|
console.error(
|
|
386
462
|
`\nFAIL: ${ibrOnly.length} upstream test(s) fail only through IBR, ` +
|
|
387
463
|
`${hungSpecs.length} spec run(s) hung, ${mismatchedSpecs.length} ` +
|
|
388
|
-
`spec(s) mismatched.
|
|
389
|
-
`
|
|
464
|
+
`spec(s) mismatched, ${staleExpected.length} stale expected-failure ` +
|
|
465
|
+
`entr${staleExpected.length === 1 ? "y" : "ies"}. The job stays red ` +
|
|
466
|
+
`until IBR reaches blob parity on these specs.`,
|
|
390
467
|
);
|
|
391
468
|
process.exit(1);
|
|
392
469
|
}
|