@aplaytest/core 0.1.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/dist/.tsbuildinfo +1 -0
- package/dist/config/schema.d.ts +247 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +279 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/evidence/id.d.ts +17 -0
- package/dist/evidence/id.d.ts.map +1 -0
- package/dist/evidence/id.js +20 -0
- package/dist/evidence/id.js.map +1 -0
- package/dist/evidence/redact.d.ts +22 -0
- package/dist/evidence/redact.d.ts.map +1 -0
- package/dist/evidence/redact.js +97 -0
- package/dist/evidence/redact.js.map +1 -0
- package/dist/evidence/store.d.ts +51 -0
- package/dist/evidence/store.d.ts.map +1 -0
- package/dist/evidence/store.js +148 -0
- package/dist/evidence/store.js.map +1 -0
- package/dist/evidence/types.d.ts +183 -0
- package/dist/evidence/types.d.ts.map +1 -0
- package/dist/evidence/types.js +21 -0
- package/dist/evidence/types.js.map +1 -0
- package/dist/history/ingest-playwright.d.ts +26 -0
- package/dist/history/ingest-playwright.d.ts.map +1 -0
- package/dist/history/ingest-playwright.js +73 -0
- package/dist/history/ingest-playwright.js.map +1 -0
- package/dist/history/ingest.d.ts +29 -0
- package/dist/history/ingest.d.ts.map +1 -0
- package/dist/history/ingest.js +86 -0
- package/dist/history/ingest.js.map +1 -0
- package/dist/history/memory-index.d.ts +88 -0
- package/dist/history/memory-index.d.ts.map +1 -0
- package/dist/history/memory-index.js +236 -0
- package/dist/history/memory-index.js.map +1 -0
- package/dist/history/playwright-report.d.ts +102 -0
- package/dist/history/playwright-report.d.ts.map +1 -0
- package/dist/history/playwright-report.js +274 -0
- package/dist/history/playwright-report.js.map +1 -0
- package/dist/history/store.d.ts +73 -0
- package/dist/history/store.d.ts.map +1 -0
- package/dist/history/store.js +243 -0
- package/dist/history/store.js.map +1 -0
- package/dist/history/types.d.ts +69 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/history/types.js +18 -0
- package/dist/history/types.js.map +1 -0
- package/dist/history/url.d.ts +79 -0
- package/dist/history/url.d.ts.map +1 -0
- package/dist/history/url.js +121 -0
- package/dist/history/url.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/locator/stability.d.ts +63 -0
- package/dist/locator/stability.d.ts.map +1 -0
- package/dist/locator/stability.js +127 -0
- package/dist/locator/stability.js.map +1 -0
- package/dist/taxonomy/classify.d.ts +63 -0
- package/dist/taxonomy/classify.d.ts.map +1 -0
- package/dist/taxonomy/classify.js +344 -0
- package/dist/taxonomy/classify.js.map +1 -0
- package/dist/taxonomy/kinds.d.ts +49 -0
- package/dist/taxonomy/kinds.d.ts.map +1 -0
- package/dist/taxonomy/kinds.js +139 -0
- package/dist/taxonomy/kinds.js.map +1 -0
- package/dist/taxonomy/parse-error.d.ts +85 -0
- package/dist/taxonomy/parse-error.d.ts.map +1 -0
- package/dist/taxonomy/parse-error.js +179 -0
- package/dist/taxonomy/parse-error.js.map +1 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +10 -0
- package/dist/version.js.map +1 -0
- package/dist-cjs/.tsbuildinfo +1 -0
- package/dist-cjs/config/schema.js +283 -0
- package/dist-cjs/config/schema.js.map +1 -0
- package/dist-cjs/evidence/id.js +24 -0
- package/dist-cjs/evidence/id.js.map +1 -0
- package/dist-cjs/evidence/redact.js +103 -0
- package/dist-cjs/evidence/redact.js.map +1 -0
- package/dist-cjs/evidence/store.js +156 -0
- package/dist-cjs/evidence/store.js.map +1 -0
- package/dist-cjs/evidence/types.js +25 -0
- package/dist-cjs/evidence/types.js.map +1 -0
- package/dist-cjs/history/ingest-playwright.js +76 -0
- package/dist-cjs/history/ingest-playwright.js.map +1 -0
- package/dist-cjs/history/ingest.js +89 -0
- package/dist-cjs/history/ingest.js.map +1 -0
- package/dist-cjs/history/memory-index.js +242 -0
- package/dist-cjs/history/memory-index.js.map +1 -0
- package/dist-cjs/history/playwright-report.js +277 -0
- package/dist-cjs/history/playwright-report.js.map +1 -0
- package/dist-cjs/history/store.js +247 -0
- package/dist-cjs/history/store.js.map +1 -0
- package/dist-cjs/history/types.js +23 -0
- package/dist-cjs/history/types.js.map +1 -0
- package/dist-cjs/history/url.js +127 -0
- package/dist-cjs/history/url.js.map +1 -0
- package/dist-cjs/index.js +88 -0
- package/dist-cjs/index.js.map +1 -0
- package/dist-cjs/locator/stability.js +134 -0
- package/dist-cjs/locator/stability.js.map +1 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/taxonomy/classify.js +348 -0
- package/dist-cjs/taxonomy/classify.js.map +1 -0
- package/dist-cjs/taxonomy/kinds.js +145 -0
- package/dist-cjs/taxonomy/kinds.js.map +1 -0
- package/dist-cjs/taxonomy/parse-error.js +186 -0
- package/dist-cjs/taxonomy/parse-error.js.map +1 -0
- package/dist-cjs/version.js +13 -0
- package/dist-cjs/version.js.map +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Map a merged Playwright JSON report onto history records.
|
|
4
|
+
*
|
|
5
|
+
* This is a runner adapter for suites that cannot install the atest reporter —
|
|
6
|
+
* most importantly, a suite whose test job is owned by a shared CI template
|
|
7
|
+
* that only uploads `blob-report/` and the merged JSON. Playwright's own
|
|
8
|
+
* `--reporter=json` output is then the ONLY channel through which run data
|
|
9
|
+
* escapes, and it carries more than it first appears to.
|
|
10
|
+
*
|
|
11
|
+
* Pure and synchronous, over structural interfaces: the whole mapping is
|
|
12
|
+
* unit-testable against a plain object, with no store and no filesystem.
|
|
13
|
+
* `ingestPlaywrightJson` does the I/O and hands the result in.
|
|
14
|
+
*
|
|
15
|
+
* ── What this path gives up, and what it does not ───────────────────────────
|
|
16
|
+
* No evidence bundle: no ARIA snapshot, no locator candidates, no network
|
|
17
|
+
* ledger, no console capture. Healing therefore still requires the reporter.
|
|
18
|
+
*
|
|
19
|
+
* What it does NOT give up is identity. Every field below is read from the
|
|
20
|
+
* report rather than invented, because a history row that cannot be joined to
|
|
21
|
+
* the rows around it is worse than no row at all — it looks like data and
|
|
22
|
+
* scores like noise.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.mapPlaywrightReport = mapPlaywrightReport;
|
|
26
|
+
const node_crypto_1 = require("node:crypto");
|
|
27
|
+
const classify_js_1 = require("../taxonomy/classify.js");
|
|
28
|
+
const parse_error_js_1 = require("../taxonomy/parse-error.js");
|
|
29
|
+
const version_js_1 = require("../version.js");
|
|
30
|
+
const types_js_1 = require("./types.js");
|
|
31
|
+
const RUN_ID_PREFIX = 'pwjson_';
|
|
32
|
+
const RUN_ID_LENGTH = 12;
|
|
33
|
+
function toOutcome(status) {
|
|
34
|
+
if (status === 'passed')
|
|
35
|
+
return 'passed';
|
|
36
|
+
if (status === 'timedOut')
|
|
37
|
+
return 'timedOut';
|
|
38
|
+
if (status === 'skipped')
|
|
39
|
+
return 'skipped';
|
|
40
|
+
if (status === 'interrupted')
|
|
41
|
+
return 'interrupted';
|
|
42
|
+
return 'failed';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Playwright's stable test id, which the JSON reporter emits as `spec.id` and
|
|
46
|
+
* which is exactly what `AttemptRecord.testId` is documented to hold. It is
|
|
47
|
+
* unique per (test, project) and survives a title rename.
|
|
48
|
+
*
|
|
49
|
+
* Taking it from the report rather than composing one is the single most
|
|
50
|
+
* important line in this file. A composed `file::title` id is a DIFFERENT ID
|
|
51
|
+
* SPACE from the reporter's, so history gathered on this path would never join
|
|
52
|
+
* to history gathered after the reporter is installed: every test would look
|
|
53
|
+
* new, every score would reset to "insufficient data", and the old rows would
|
|
54
|
+
* linger as phantom tests forever. The adoption sequence that starts on JSON
|
|
55
|
+
* and later adds the reporter is the normal one, so that migration cliff has to
|
|
56
|
+
* not exist.
|
|
57
|
+
*
|
|
58
|
+
* The fallback exists only for a report old enough to lack `spec.id`. It
|
|
59
|
+
* includes the title path, so two same-titled tests in different `describe`
|
|
60
|
+
* blocks stay distinct — but it is still a separate id space, and the caller is
|
|
61
|
+
* told about it.
|
|
62
|
+
*/
|
|
63
|
+
function attemptTestId(spec, file, titlePath) {
|
|
64
|
+
if (typeof spec.id === 'string' && spec.id !== '')
|
|
65
|
+
return spec.id;
|
|
66
|
+
return [file, ...titlePath, spec.title ?? ''].join('::');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Classify a failing attempt from its error text.
|
|
70
|
+
*
|
|
71
|
+
* The classifier reads `message + stack` for nearly every rule, so this path
|
|
72
|
+
* recovers most of the taxonomy without an evidence bundle — including `infra`,
|
|
73
|
+
* which matters more than it looks: doc 06 requires infra attempts to be
|
|
74
|
+
* excluded from flake statistics, and a hard-coded `'unknown'` would have let
|
|
75
|
+
* every browser crash count as evidence of flakiness.
|
|
76
|
+
*
|
|
77
|
+
* One honest gap. `contract.app-error` needs captured console output, which
|
|
78
|
+
* only the reporter has. Without it, an application exception whose sole trace
|
|
79
|
+
* was a console message classifies as whatever assertion failed downstream of
|
|
80
|
+
* it — typically `assertion_visibility`. That over-weights a real bug as flake
|
|
81
|
+
* rather than hiding it, which is the safer direction to be wrong in, but it is
|
|
82
|
+
* a reason to prefer the reporter once evidence egress exists.
|
|
83
|
+
*/
|
|
84
|
+
function classifyAttempt(result, timeoutMs) {
|
|
85
|
+
const { message, stack } = (0, parse_error_js_1.joinErrors)(result.errors);
|
|
86
|
+
if (message === '' && stack === '')
|
|
87
|
+
return 'unknown';
|
|
88
|
+
const parsed = (0, parse_error_js_1.parsePlaywrightError)(message, stack);
|
|
89
|
+
const duration = result.duration ?? 0;
|
|
90
|
+
return (0, classify_js_1.classify)({
|
|
91
|
+
message,
|
|
92
|
+
stack,
|
|
93
|
+
matcher: parsed.matcher,
|
|
94
|
+
timedOut: parsed.timedOut || result.status === 'timedOut',
|
|
95
|
+
// The reporter's sidecars. Absent here by construction, not by accident.
|
|
96
|
+
consoleErrors: [],
|
|
97
|
+
failedRequests: [],
|
|
98
|
+
budgetUsedRatio: timeoutMs > 0 ? duration / timeoutMs : 0,
|
|
99
|
+
}).kind;
|
|
100
|
+
}
|
|
101
|
+
function collectAttempts(suites, file, titlePath) {
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const suite of suites ?? []) {
|
|
104
|
+
const suiteFile = suite.file ?? file;
|
|
105
|
+
const path = suite.title !== undefined && suite.title !== '' ? [...titlePath, suite.title] : titlePath;
|
|
106
|
+
for (const spec of suite.specs ?? []) {
|
|
107
|
+
const specFile = spec.file ?? suiteFile;
|
|
108
|
+
for (const test of spec.tests ?? []) {
|
|
109
|
+
for (const result of test.results ?? []) {
|
|
110
|
+
const outcome = toOutcome(result.status);
|
|
111
|
+
const failing = outcome === 'failed' || outcome === 'timedOut';
|
|
112
|
+
out.push({
|
|
113
|
+
testId: attemptTestId(spec, specFile, path),
|
|
114
|
+
title: spec.title ?? '',
|
|
115
|
+
titlePath: path,
|
|
116
|
+
file: specFile,
|
|
117
|
+
line: spec.line ?? 0,
|
|
118
|
+
project: test.projectName ?? 'default',
|
|
119
|
+
tags: spec.tags ?? [],
|
|
120
|
+
retry: result.retry ?? 0,
|
|
121
|
+
outcome,
|
|
122
|
+
failureKind: failing ? classifyAttempt(result, test.timeout ?? 0) : null,
|
|
123
|
+
durationMs: result.duration ?? 0,
|
|
124
|
+
workerIndex: result.workerIndex ?? 0,
|
|
125
|
+
// A merged report has already folded its shards together and does
|
|
126
|
+
// not record which one an attempt came from.
|
|
127
|
+
shard: null,
|
|
128
|
+
traceId: null,
|
|
129
|
+
evidenceId: null,
|
|
130
|
+
coScheduled: [],
|
|
131
|
+
routes: [],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
out.push(...collectAttempts(suite.suites, suiteFile, path));
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
/** Ordered so a duplicate can never mask the worse outcome. */
|
|
141
|
+
const SEVERITY = {
|
|
142
|
+
skipped: 0,
|
|
143
|
+
passed: 1,
|
|
144
|
+
interrupted: 2,
|
|
145
|
+
timedOut: 3,
|
|
146
|
+
failed: 4,
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Collapse attempts that share the store's primary key
|
|
150
|
+
* `(run_id, test_id, project, retry)`.
|
|
151
|
+
*
|
|
152
|
+
* With a real `spec.id` this should never fire — it did before, only because
|
|
153
|
+
* the composed id mapped two genuinely distinct setup tests onto one key. It
|
|
154
|
+
* stays as a safety net because the failure mode it guards is severe out of all
|
|
155
|
+
* proportion to its cause: `store.ingest` is a single transaction, so ONE
|
|
156
|
+
* duplicate row used to roll back the entire run. On a real 8-shard acceptance
|
|
157
|
+
* report that cost all 120 attempts, and the only symptom was a single
|
|
158
|
+
* "could not be stored" warning — history silently never accumulated while
|
|
159
|
+
* every CI job reported success.
|
|
160
|
+
*
|
|
161
|
+
* Collisions are returned, not swallowed. A silent dedupe here would be the
|
|
162
|
+
* same class of bug one level down: quietly discarding evidence and looking
|
|
163
|
+
* healthy while doing it.
|
|
164
|
+
*/
|
|
165
|
+
function collapseDuplicates(attempts) {
|
|
166
|
+
const byKey = new Map();
|
|
167
|
+
const collapsed = [];
|
|
168
|
+
for (const attempt of attempts) {
|
|
169
|
+
const key = `${attempt.testId}\x00${attempt.project}\x00${attempt.retry}`;
|
|
170
|
+
const existing = byKey.get(key);
|
|
171
|
+
if (existing === undefined) {
|
|
172
|
+
byKey.set(key, attempt);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// Keep the more severe outcome. A duplicate that passed must never mask
|
|
176
|
+
// one that failed — that is precisely the instability scoring exists for.
|
|
177
|
+
const keep = SEVERITY[attempt.outcome] > SEVERITY[existing.outcome] ? attempt : existing;
|
|
178
|
+
byKey.set(key, keep);
|
|
179
|
+
collapsed.push(`${attempt.title} [${attempt.project}] retry ${attempt.retry}`);
|
|
180
|
+
}
|
|
181
|
+
return { kept: [...byKey.values()], collapsed };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A run id derived from the report, never from the clock.
|
|
185
|
+
*
|
|
186
|
+
* `store.ingest` promises idempotency — "CI re-runs, shard merges, and repeated
|
|
187
|
+
* artifact ingestion all replay the same run id" — and enforces it by deleting
|
|
188
|
+
* a run's attempts before reinserting them. A `Date.now()` id opts out of that
|
|
189
|
+
* guarantee silently: re-ingesting one artifact twice (a re-run of the analyze
|
|
190
|
+
* job, a replay of an archived report) writes both copies and double-counts
|
|
191
|
+
* every attempt, which corrupts every score derived from them.
|
|
192
|
+
*
|
|
193
|
+
* The CI build URL is the natural key where the report carries one. Otherwise
|
|
194
|
+
* digest the attempt tuples, which is stable for a given report and distinct
|
|
195
|
+
* across runs because attempt start times are millisecond-precision.
|
|
196
|
+
*/
|
|
197
|
+
function deriveRunId(report, attempts) {
|
|
198
|
+
const buildHref = report.config?.metadata?.ci?.buildHref;
|
|
199
|
+
const seed = buildHref !== undefined && buildHref !== ''
|
|
200
|
+
? buildHref
|
|
201
|
+
: attempts
|
|
202
|
+
.map(a => `${a.testId} ${a.project} ${a.retry} ${a.durationMs}`)
|
|
203
|
+
.sort()
|
|
204
|
+
.join('\n');
|
|
205
|
+
const digest = (0, node_crypto_1.createHash)('sha256').update(seed).digest('hex');
|
|
206
|
+
return `${RUN_ID_PREFIX}${digest.slice(0, RUN_ID_LENGTH)}`;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The wall-clock window the run actually occupied.
|
|
210
|
+
*
|
|
211
|
+
* Flake scoring weights attempts by recency (`halfLifeDays`), so the timestamp
|
|
212
|
+
* has to be when the test RAN, not when somebody ingested the file. Using
|
|
213
|
+
* ingest time would make a replayed six-month-old archive weigh as heavily as
|
|
214
|
+
* this morning's run — and back-filling history from archived artifacts is one
|
|
215
|
+
* of the reasons this adapter exists.
|
|
216
|
+
*/
|
|
217
|
+
function runWindow(report, now) {
|
|
218
|
+
const spans = [];
|
|
219
|
+
const walk = (suites) => {
|
|
220
|
+
for (const suite of suites ?? []) {
|
|
221
|
+
for (const spec of suite.specs ?? []) {
|
|
222
|
+
for (const test of spec.tests ?? []) {
|
|
223
|
+
for (const result of test.results ?? []) {
|
|
224
|
+
if (result.startTime === undefined)
|
|
225
|
+
continue;
|
|
226
|
+
const start = Date.parse(result.startTime);
|
|
227
|
+
if (Number.isNaN(start))
|
|
228
|
+
continue;
|
|
229
|
+
spans.push({ start, end: start + (result.duration ?? 0) });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
walk(suite.suites);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
walk(report.suites);
|
|
237
|
+
if (spans.length === 0) {
|
|
238
|
+
const fallback = new Date(now()).toISOString();
|
|
239
|
+
return { startedAt: fallback, finishedAt: fallback };
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
startedAt: new Date(Math.min(...spans.map(s => s.start))).toISOString(),
|
|
243
|
+
finishedAt: new Date(Math.max(...spans.map(s => s.end))).toISOString(),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function mapPlaywrightReport(report, options) {
|
|
247
|
+
const { identity } = options;
|
|
248
|
+
const now = options.now ?? Date.now;
|
|
249
|
+
const { kept, collapsed } = collapseDuplicates(collectAttempts(report.suites, '', []));
|
|
250
|
+
const { startedAt, finishedAt } = runWindow(report, now);
|
|
251
|
+
const ci = report.config?.metadata?.ci;
|
|
252
|
+
const shard = report.config?.shard;
|
|
253
|
+
const run = {
|
|
254
|
+
schemaVersion: types_js_1.RUN_SCHEMA_VERSION,
|
|
255
|
+
runId: deriveRunId(report, kept),
|
|
256
|
+
startedAt,
|
|
257
|
+
finishedAt,
|
|
258
|
+
// Report first, environment second. Same-commit variance is a documented
|
|
259
|
+
// flake signal and it needs the commit the tests actually ran against, not
|
|
260
|
+
// whatever is checked out in the job doing the ingest.
|
|
261
|
+
commit: ci?.commitHash ?? identity.commit,
|
|
262
|
+
branch: ci?.branch ?? identity.branch,
|
|
263
|
+
appEnv: identity.appEnv,
|
|
264
|
+
ci: ci !== undefined || identity.ci,
|
|
265
|
+
// `actualWorkers` is what the run really used; `workers` is what was asked
|
|
266
|
+
// for, and the two differ under sharding.
|
|
267
|
+
workers: report.config?.metadata?.actualWorkers ?? report.config?.workers ?? 0,
|
|
268
|
+
shard: shard !== null && shard !== undefined && shard.current !== undefined && shard.total !== undefined
|
|
269
|
+
? { current: shard.current, total: shard.total }
|
|
270
|
+
: null,
|
|
271
|
+
atestVersion: version_js_1.ATEST_VERSION,
|
|
272
|
+
playwrightVersion: report.config?.version ?? null,
|
|
273
|
+
attempts: kept,
|
|
274
|
+
};
|
|
275
|
+
return { run, collapsed };
|
|
276
|
+
}
|
|
277
|
+
//# sourceMappingURL=playwright-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playwright-report.js","sourceRoot":"","sources":["../../src/history/playwright-report.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Test history — the substrate the flaky engine reasons over.
|
|
4
|
+
*
|
|
5
|
+
* Backed by `node:sqlite`, which ships with Node. That is a deliberate choice
|
|
6
|
+
* over `better-sqlite3`: a native module would need a compiler on every
|
|
7
|
+
* developer machine and every CI image, and the first time it failed to build
|
|
8
|
+
* people would disable the tool rather than debug it. Zero native
|
|
9
|
+
* dependencies is what makes this adoptable.
|
|
10
|
+
*
|
|
11
|
+
* The interface is async so a Postgres driver can slot in unchanged at scale;
|
|
12
|
+
* the SQLite driver simply resolves immediately.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.SqliteHistoryStore = void 0;
|
|
16
|
+
const node_sqlite_1 = require("node:sqlite");
|
|
17
|
+
const node_fs_1 = require("node:fs");
|
|
18
|
+
const node_path_1 = require("node:path");
|
|
19
|
+
const SCHEMA = `
|
|
20
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
21
|
+
run_id TEXT PRIMARY KEY,
|
|
22
|
+
started_at TEXT NOT NULL,
|
|
23
|
+
finished_at TEXT,
|
|
24
|
+
commit_sha TEXT,
|
|
25
|
+
branch TEXT,
|
|
26
|
+
app_env TEXT,
|
|
27
|
+
ci INTEGER NOT NULL,
|
|
28
|
+
workers INTEGER NOT NULL,
|
|
29
|
+
shard_current INTEGER,
|
|
30
|
+
shard_total INTEGER,
|
|
31
|
+
atest_version TEXT NOT NULL,
|
|
32
|
+
playwright_version TEXT
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS attempts (
|
|
36
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
37
|
+
test_id TEXT NOT NULL,
|
|
38
|
+
project TEXT NOT NULL,
|
|
39
|
+
retry INTEGER NOT NULL,
|
|
40
|
+
title TEXT NOT NULL,
|
|
41
|
+
title_path TEXT NOT NULL,
|
|
42
|
+
file TEXT NOT NULL,
|
|
43
|
+
line INTEGER NOT NULL,
|
|
44
|
+
tags TEXT NOT NULL,
|
|
45
|
+
outcome TEXT NOT NULL,
|
|
46
|
+
failure_kind TEXT,
|
|
47
|
+
duration_ms INTEGER NOT NULL,
|
|
48
|
+
worker_index INTEGER NOT NULL,
|
|
49
|
+
shard_current INTEGER,
|
|
50
|
+
shard_total INTEGER,
|
|
51
|
+
trace_id TEXT,
|
|
52
|
+
evidence_id TEXT,
|
|
53
|
+
co_scheduled TEXT NOT NULL,
|
|
54
|
+
routes TEXT NOT NULL DEFAULT '[]',
|
|
55
|
+
PRIMARY KEY (run_id, test_id, project, retry)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_attempts_test ON attempts(test_id, project);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_attempts_outcome ON attempts(outcome, failure_kind);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at);
|
|
61
|
+
`;
|
|
62
|
+
function parseJsonArray(raw) {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
return Array.isArray(parsed) ? parsed.filter((v) => typeof v === 'string') : [];
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function toHistoricalAttempt(row) {
|
|
72
|
+
return {
|
|
73
|
+
runId: row.run_id,
|
|
74
|
+
testId: row.test_id,
|
|
75
|
+
project: row.project,
|
|
76
|
+
retry: row.retry,
|
|
77
|
+
title: row.title,
|
|
78
|
+
titlePath: parseJsonArray(row.title_path),
|
|
79
|
+
file: row.file,
|
|
80
|
+
line: row.line,
|
|
81
|
+
tags: parseJsonArray(row.tags),
|
|
82
|
+
outcome: row.outcome,
|
|
83
|
+
failureKind: row.failure_kind,
|
|
84
|
+
durationMs: row.duration_ms,
|
|
85
|
+
workerIndex: row.worker_index,
|
|
86
|
+
shard: row.shard_current === null || row.shard_total === null
|
|
87
|
+
? null
|
|
88
|
+
: { current: row.shard_current, total: row.shard_total },
|
|
89
|
+
traceId: row.trace_id,
|
|
90
|
+
evidenceId: row.evidence_id,
|
|
91
|
+
coScheduled: parseJsonArray(row.co_scheduled),
|
|
92
|
+
routes: parseJsonArray(row.routes ?? '[]'),
|
|
93
|
+
startedAt: row.started_at,
|
|
94
|
+
commit: row.commit_sha,
|
|
95
|
+
branch: row.branch,
|
|
96
|
+
ci: row.ci === 1,
|
|
97
|
+
workers: row.workers,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* `undefined` is not bindable by node:sqlite — only `null` is.
|
|
102
|
+
*
|
|
103
|
+
* The distinction never shows up in a hand-written fixture where every field is
|
|
104
|
+
* present, and it is invisible in the type system because `string | null` and a
|
|
105
|
+
* MISSING key both satisfy a `Partial<RunRecord>` shape check. What it produces
|
|
106
|
+
* at runtime is `Provided value cannot be bound to SQLite parameter 6`, which
|
|
107
|
+
* names neither the field nor the file — and because the insert is inside a
|
|
108
|
+
* transaction, the whole batch rolls back. One run record written by a
|
|
109
|
+
* different atest version therefore silently costs you every other record in
|
|
110
|
+
* the directory.
|
|
111
|
+
*/
|
|
112
|
+
function nullable(value) {
|
|
113
|
+
return value ?? null;
|
|
114
|
+
}
|
|
115
|
+
class SqliteHistoryStore {
|
|
116
|
+
db;
|
|
117
|
+
constructor(url) {
|
|
118
|
+
if (url !== ':memory:')
|
|
119
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(url), { recursive: true });
|
|
120
|
+
this.db = new node_sqlite_1.DatabaseSync(url);
|
|
121
|
+
this.db.exec('PRAGMA foreign_keys = ON');
|
|
122
|
+
this.db.exec(SCHEMA);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Idempotent by construction. CI re-runs, shard merges, and repeated
|
|
126
|
+
* artifact ingestion all replay the same run id — double-counting an
|
|
127
|
+
* attempt would silently corrupt every score derived from it.
|
|
128
|
+
*/
|
|
129
|
+
async ingest(run) {
|
|
130
|
+
const insertRun = this.db.prepare(`
|
|
131
|
+
-- UPSERT, never INSERT OR REPLACE.
|
|
132
|
+
--
|
|
133
|
+
-- SQLite implements OR REPLACE as DELETE-then-INSERT, so replacing a run
|
|
134
|
+
-- row fires the attempts table's ON DELETE CASCADE. Under sharding every
|
|
135
|
+
-- shard shares one run id, so ingesting shard 2 deleted shard 1's
|
|
136
|
+
-- attempts before writing its own — the shard-scoped delete below cannot
|
|
137
|
+
-- help, because the rows are already gone by the time it runs. Measured:
|
|
138
|
+
-- three shard files carrying four attempts ingested as zero.
|
|
139
|
+
--
|
|
140
|
+
-- DO UPDATE mutates the row in place. No delete, no cascade.
|
|
141
|
+
INSERT INTO runs
|
|
142
|
+
(run_id, started_at, finished_at, commit_sha, branch, app_env, ci, workers,
|
|
143
|
+
shard_current, shard_total, atest_version, playwright_version)
|
|
144
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
145
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
146
|
+
finished_at = excluded.finished_at,
|
|
147
|
+
commit_sha = excluded.commit_sha,
|
|
148
|
+
branch = excluded.branch,
|
|
149
|
+
app_env = excluded.app_env,
|
|
150
|
+
ci = excluded.ci,
|
|
151
|
+
workers = excluded.workers,
|
|
152
|
+
atest_version = excluded.atest_version,
|
|
153
|
+
playwright_version = excluded.playwright_version
|
|
154
|
+
-- started_at keeps the EARLIEST shard's value, and shard_* are left
|
|
155
|
+
-- alone: a run spanning shards has no single shard number, and letting
|
|
156
|
+
-- the last writer stamp one would misreport the run.
|
|
157
|
+
`);
|
|
158
|
+
// Scoped to THIS shard, not the whole run.
|
|
159
|
+
//
|
|
160
|
+
// `DELETE FROM attempts WHERE run_id = ?` is correct for an unsharded run
|
|
161
|
+
// and catastrophic for a sharded one: every shard shares the run id, so
|
|
162
|
+
// ingesting six shard files left only the sixth shard's attempts — one
|
|
163
|
+
// sixth of the data, with no error anywhere. `IS` rather than `=` because
|
|
164
|
+
// an unsharded record carries NULL, and NULL = NULL is never true.
|
|
165
|
+
const deleteAttempts = this.db.prepare('DELETE FROM attempts WHERE run_id = ? AND shard_current IS ? AND shard_total IS ?');
|
|
166
|
+
const insertAttempt = this.db.prepare(`
|
|
167
|
+
INSERT INTO attempts
|
|
168
|
+
(run_id, test_id, project, retry, title, title_path, file, line, tags, outcome,
|
|
169
|
+
failure_kind, duration_ms, worker_index, shard_current, shard_total,
|
|
170
|
+
trace_id, evidence_id, co_scheduled, routes)
|
|
171
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
172
|
+
`);
|
|
173
|
+
this.db.exec('BEGIN');
|
|
174
|
+
try {
|
|
175
|
+
insertRun.run(run.runId, run.startedAt, nullable(run.finishedAt), nullable(run.commit), nullable(run.branch), nullable(run.appEnv), run.ci ? 1 : 0, run.workers ?? 0, run.shard?.current ?? null, run.shard?.total ?? null, run.atestVersion ?? '', nullable(run.playwrightVersion));
|
|
176
|
+
deleteAttempts.run(run.runId, run.shard?.current ?? null, run.shard?.total ?? null);
|
|
177
|
+
for (const attempt of run.attempts) {
|
|
178
|
+
insertAttempt.run(run.runId, attempt.testId, attempt.project, attempt.retry, attempt.title, JSON.stringify(attempt.titlePath ?? []), attempt.file, attempt.line, JSON.stringify(attempt.tags ?? []), attempt.outcome, nullable(attempt.failureKind), attempt.durationMs ?? 0, attempt.workerIndex ?? 0,
|
|
179
|
+
// Fall back to the RUN's shard. The scoped delete above matches on
|
|
180
|
+
// these columns, so an attempt that omits its shard while the run
|
|
181
|
+
// declares one would never be cleaned up — a re-run of that shard
|
|
182
|
+
// would leave the previous attempt rows behind and inflate every
|
|
183
|
+
// statistic derived from them. An attempt executed during shard 1
|
|
184
|
+
// of 3 belongs to shard 1 of 3; recording anything else is wrong.
|
|
185
|
+
attempt.shard?.current ?? run.shard?.current ?? null, attempt.shard?.total ?? run.shard?.total ?? null, nullable(attempt.traceId), nullable(attempt.evidenceId), JSON.stringify(attempt.coScheduled ?? []), JSON.stringify(attempt.routes ?? []));
|
|
186
|
+
}
|
|
187
|
+
this.db.exec('COMMIT');
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
this.db.exec('ROLLBACK');
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async attempts(query = {}) {
|
|
195
|
+
const where = [];
|
|
196
|
+
const params = [];
|
|
197
|
+
if (query.testId !== undefined) {
|
|
198
|
+
where.push('a.test_id = ?');
|
|
199
|
+
params.push(query.testId);
|
|
200
|
+
}
|
|
201
|
+
if (query.project !== undefined) {
|
|
202
|
+
where.push('a.project = ?');
|
|
203
|
+
params.push(query.project);
|
|
204
|
+
}
|
|
205
|
+
if (query.since !== undefined) {
|
|
206
|
+
where.push('r.started_at >= ?');
|
|
207
|
+
params.push(query.since);
|
|
208
|
+
}
|
|
209
|
+
const clause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
|
210
|
+
const limit = query.limit === undefined ? '' : `LIMIT ${Number(query.limit)}`;
|
|
211
|
+
const rows = this.db
|
|
212
|
+
.prepare(`SELECT a.*, r.started_at, r.commit_sha, r.branch, r.ci, r.workers
|
|
213
|
+
FROM attempts a JOIN runs r ON r.run_id = a.run_id
|
|
214
|
+
${clause}
|
|
215
|
+
ORDER BY r.started_at DESC, a.retry ASC
|
|
216
|
+
${limit}`)
|
|
217
|
+
.all(...params);
|
|
218
|
+
return rows.map(toHistoricalAttempt);
|
|
219
|
+
}
|
|
220
|
+
async testKeys() {
|
|
221
|
+
const rows = this.db
|
|
222
|
+
.prepare(`SELECT test_id, project, title, file FROM attempts
|
|
223
|
+
GROUP BY test_id, project
|
|
224
|
+
ORDER BY file, title`)
|
|
225
|
+
.all();
|
|
226
|
+
return rows.map(r => ({
|
|
227
|
+
testId: r.test_id,
|
|
228
|
+
project: r.project,
|
|
229
|
+
title: r.title,
|
|
230
|
+
file: r.file,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
async runCount() {
|
|
234
|
+
const row = this.db.prepare('SELECT COUNT(*) AS n FROM runs').get();
|
|
235
|
+
return row.n;
|
|
236
|
+
}
|
|
237
|
+
async prune(olderThanIso) {
|
|
238
|
+
const before = await this.runCount();
|
|
239
|
+
this.db.prepare('DELETE FROM runs WHERE started_at < ?').run(olderThanIso);
|
|
240
|
+
return before - (await this.runCount());
|
|
241
|
+
}
|
|
242
|
+
async close() {
|
|
243
|
+
this.db.close();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
exports.SqliteHistoryStore = SqliteHistoryStore;
|
|
247
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/history/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Run and attempt records — the unit of test history.
|
|
4
|
+
*
|
|
5
|
+
* The reporter writes these as JSON; the history store ingests them
|
|
6
|
+
* into SQLite. Keeping the reporter's output a plain file rather than a
|
|
7
|
+
* database write matters: the reporter runs INSIDE the test process, where a
|
|
8
|
+
* native SQLite binding, a migration, or a lock contention bug would become a
|
|
9
|
+
* test failure. The run path stays dependency-light on purpose.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.RUN_SCHEMA_VERSION = void 0;
|
|
13
|
+
exports.isFailure = isFailure;
|
|
14
|
+
exports.isConclusive = isConclusive;
|
|
15
|
+
exports.RUN_SCHEMA_VERSION = 1;
|
|
16
|
+
function isFailure(outcome) {
|
|
17
|
+
return outcome === 'failed' || outcome === 'timedOut';
|
|
18
|
+
}
|
|
19
|
+
/** Skipped and interrupted attempts are not evidence about a test's behaviour. */
|
|
20
|
+
function isConclusive(outcome) {
|
|
21
|
+
return outcome === 'passed' || outcome === 'failed' || outcome === 'timedOut';
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/history/types.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AAyDH,8BAEC;AAGD,oCAEC;AATY,QAAA,kBAAkB,GAAG,CAAU,CAAC;AAE7C,SAAgB,SAAS,CAAC,OAAgB;IACxC,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,UAAU,CAAC;AACxD,CAAC;AAED,kFAAkF;AAClF,SAAgB,YAAY,CAAC,OAAgB;IAC3C,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,UAAU,CAAC;AAChF,CAAC"}
|