@elench/testkit 0.1.52 → 0.1.54
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/README.md +14 -0
- package/bin/testkit.mjs +4 -6
- package/lib/cli/command-helpers.mjs +170 -0
- package/lib/cli/commands/artifacts.mjs +45 -0
- package/lib/cli/commands/cleanup.mjs +15 -0
- package/lib/cli/commands/db/snapshot/capture.mjs +22 -0
- package/lib/cli/commands/destroy.mjs +15 -0
- package/lib/cli/commands/known-failures/render.mjs +19 -0
- package/lib/cli/commands/known-failures/validate.mjs +20 -0
- package/lib/cli/commands/logs.mjs +47 -0
- package/lib/cli/commands/run.mjs +23 -0
- package/lib/cli/commands/show.mjs +47 -0
- package/lib/cli/commands/status.mjs +15 -0
- package/lib/cli/commands/watch.mjs +23 -0
- package/lib/cli/entrypoint.mjs +83 -0
- package/lib/cli/index.mjs +6 -116
- package/lib/cli/presentation/code-frames.mjs +57 -0
- package/lib/cli/presentation/code-frames.test.mjs +71 -0
- package/lib/cli/presentation/colors.mjs +29 -0
- package/lib/cli/presentation/run-reporter.mjs +100 -0
- package/lib/cli/tui/watch-app.mjs +104 -0
- package/lib/cli/viewer.mjs +268 -0
- package/lib/known-failures/index.mjs +1 -1
- package/lib/known-failures/index.test.mjs +46 -0
- package/lib/runner/artifacts.mjs +35 -0
- package/lib/runner/default-runtime-errors.mjs +66 -0
- package/lib/runner/default-runtime-runner.mjs +52 -11
- package/lib/runner/failure-details.mjs +31 -0
- package/lib/runner/failure-details.test.mjs +51 -0
- package/lib/runner/formatting.mjs +207 -0
- package/lib/runner/formatting.test.mjs +81 -6
- package/lib/runner/logs.mjs +89 -0
- package/lib/runner/orchestrator.mjs +51 -20
- package/lib/runner/playwright-runner.mjs +15 -7
- package/lib/runner/processes.mjs +9 -11
- package/lib/runner/reporting.mjs +5 -1
- package/lib/runner/reporting.test.mjs +4 -1
- package/lib/runner/runtime-contexts.mjs +7 -3
- package/lib/runner/runtime-manager.mjs +8 -2
- package/lib/runner/runtime-preparation.mjs +9 -4
- package/lib/runner/services.mjs +25 -8
- package/lib/runner/template-steps.mjs +4 -3
- package/lib/runner/triage.mjs +67 -0
- package/lib/runner/worker-loop.mjs +8 -7
- package/lib/runtime/index.d.ts +60 -0
- package/lib/runtime/index.mjs +12 -0
- package/lib/runtime-src/k6/checks.js +45 -12
- package/lib/runtime-src/k6/http-assertions.js +214 -0
- package/lib/runtime-src/k6/http.js +261 -13
- package/lib/runtime-src/k6/suite.js +46 -1
- package/lib/toolchains/index.mjs +6 -3
- package/package.json +13 -3
|
@@ -49,6 +49,56 @@ export function formatSuiteFramework(framework) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function buildRunSummaryLines(results, durationMs, knownFailureIssueValidation = null) {
|
|
52
|
+
return buildCompactRunSummaryLines(results, durationMs, knownFailureIssueValidation);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildCompactRunSummaryLines(
|
|
56
|
+
results,
|
|
57
|
+
durationMs,
|
|
58
|
+
knownFailureIssueValidation = null
|
|
59
|
+
) {
|
|
60
|
+
const totals = summarizeResults(results);
|
|
61
|
+
const lines = [
|
|
62
|
+
"",
|
|
63
|
+
`Summary: ${totals.passedFiles} passed, ${totals.failedFiles} failed, ${totals.skippedFiles} skipped, ${totals.notRunFiles} not run across ${totals.totalFiles} ${pluralize(totals.totalFiles, "file", "files")} in ${formatDuration(durationMs)}`,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const failures = collectFailedFiles(results);
|
|
67
|
+
if (failures.length > 0) {
|
|
68
|
+
lines.push("", "Failures:");
|
|
69
|
+
for (const failure of failures) {
|
|
70
|
+
lines.push(` ${failure.file.path}`);
|
|
71
|
+
lines.push(` ${failure.primaryMessage}`);
|
|
72
|
+
for (const detail of failure.extraLines.slice(0, 3)) {
|
|
73
|
+
lines.push(` ${detail}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const serviceErrors = collectServiceErrors(results);
|
|
79
|
+
if (serviceErrors.length > 0) {
|
|
80
|
+
lines.push("", "Runtime Errors:");
|
|
81
|
+
for (const item of serviceErrors) {
|
|
82
|
+
lines.push(` ${item.service}`);
|
|
83
|
+
lines.push(` ${item.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const knownFailureIssueLines = buildKnownFailureIssueValidationSummaryLines(
|
|
88
|
+
knownFailureIssueValidation
|
|
89
|
+
);
|
|
90
|
+
if (knownFailureIssueLines.length > 0) {
|
|
91
|
+
lines.push(...knownFailureIssueLines);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push(
|
|
96
|
+
totals.failedServices > 0 ? `Result: FAILED (${totals.failedServices}/${totals.totalServices} services failed)` : "Result: PASSED"
|
|
97
|
+
);
|
|
98
|
+
return lines;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildDebugRunSummaryLines(results, durationMs, knownFailureIssueValidation = null) {
|
|
52
102
|
const totalServices = results.length;
|
|
53
103
|
const executedServices = results.filter((result) => !result.skipped);
|
|
54
104
|
const skippedServices = results.filter((result) => result.skipped);
|
|
@@ -140,6 +190,163 @@ function sanitizeErrorMessage(message) {
|
|
|
140
190
|
.replace(/[\\/]vendor[\\/]k6\b/g, "default-runtime");
|
|
141
191
|
}
|
|
142
192
|
|
|
193
|
+
function summarizeResults(results) {
|
|
194
|
+
const executedServices = results.filter((result) => !result.skipped);
|
|
195
|
+
return {
|
|
196
|
+
totalServices: results.length,
|
|
197
|
+
failedServices: executedServices.filter((result) => result.failed).length,
|
|
198
|
+
totalFiles: executedServices.reduce((sum, result) => sum + (result.totalFileCount || 0), 0),
|
|
199
|
+
passedFiles: executedServices.reduce((sum, result) => sum + (result.passedFileCount || 0), 0),
|
|
200
|
+
failedFiles: executedServices.reduce((sum, result) => sum + (result.failedFileCount || 0), 0),
|
|
201
|
+
skippedFiles: executedServices.reduce((sum, result) => sum + (result.skippedFileCount || 0), 0),
|
|
202
|
+
notRunFiles: executedServices.reduce((sum, result) => sum + (result.notRunFileCount || 0), 0),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function collectFailedFiles(results) {
|
|
207
|
+
const failures = [];
|
|
208
|
+
for (const result of results) {
|
|
209
|
+
for (const suite of result.suites || []) {
|
|
210
|
+
for (const file of suite.files || []) {
|
|
211
|
+
if (file.status !== "failed") continue;
|
|
212
|
+
const rankedDetails = rankFailureDetails(file.failureDetails || []);
|
|
213
|
+
const primaryDetail = rankedDetails[0] || null;
|
|
214
|
+
const fallbackMessages = rankedDetails
|
|
215
|
+
.map((detail) => detail.message || detail.title)
|
|
216
|
+
.filter(Boolean)
|
|
217
|
+
.map((message) => sanitizeErrorMessage(String(message).trim()));
|
|
218
|
+
const extraLines = [];
|
|
219
|
+
if (primaryDetail) {
|
|
220
|
+
const responseLine = formatFailureResponsePreview(primaryDetail);
|
|
221
|
+
if (responseLine) extraLines.push(responseLine);
|
|
222
|
+
const triageLine = formatTriageLine(file.triage || null);
|
|
223
|
+
if (triageLine) extraLines.push(triageLine);
|
|
224
|
+
const requestLine = formatFailureRequestHint(primaryDetail);
|
|
225
|
+
if (requestLine) extraLines.push(requestLine);
|
|
226
|
+
} else {
|
|
227
|
+
const triageLine = formatTriageLine(file.triage || null);
|
|
228
|
+
if (triageLine) extraLines.push(triageLine);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const detail of rankedDetails.slice(primaryDetail ? 1 : 0)) {
|
|
232
|
+
const line = sanitizeErrorMessage(String(detail.message || detail.title || "").trim());
|
|
233
|
+
if (line && !extraLines.includes(line)) extraLines.push(line);
|
|
234
|
+
}
|
|
235
|
+
failures.push({
|
|
236
|
+
file,
|
|
237
|
+
primaryMessage: resolvePrimaryFailureMessage(file, suite, primaryDetail, fallbackMessages),
|
|
238
|
+
extraLines,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return failures.sort((left, right) => left.file.path.localeCompare(right.file.path));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function resolvePrimaryFailureMessage(file, suite, primaryDetail, fallbackMessages) {
|
|
247
|
+
if (primaryDetail?.message) {
|
|
248
|
+
return sanitizeErrorMessage(String(primaryDetail.message).trim());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const fileError = sanitizeErrorMessage(file.error || "");
|
|
252
|
+
if (fileError && !isThresholdWrapperMessage(fileError)) {
|
|
253
|
+
return fileError;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (fallbackMessages.length > 0) {
|
|
257
|
+
return fallbackMessages[0];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (fileError) {
|
|
261
|
+
return fileError;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return sanitizeErrorMessage(suite.error || "Failed");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function rankFailureDetails(details) {
|
|
268
|
+
return [...(Array.isArray(details) ? details : [])].sort((left, right) => {
|
|
269
|
+
return failureDetailRank(left) - failureDetailRank(right) || String(left?.key || "").localeCompare(String(right?.key || ""));
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function failureDetailRank(detail) {
|
|
274
|
+
if (detail?.kind === "http-assertion") return 1;
|
|
275
|
+
if (detail?.request && detail?.response) return 2;
|
|
276
|
+
if (detail?.location || detail?.stack) return 3;
|
|
277
|
+
if (detail?.message) return 4;
|
|
278
|
+
return 5;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function formatFailureResponsePreview(detail) {
|
|
282
|
+
const bodyPreview = detail?.response?.bodyPreview;
|
|
283
|
+
if (!bodyPreview) return null;
|
|
284
|
+
return `response: ${sanitizeInline(bodyPreview, 220)}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function formatFailureRequestHint(detail) {
|
|
288
|
+
const method = detail?.request?.method;
|
|
289
|
+
const path = detail?.request?.path;
|
|
290
|
+
if (!method || !path) return null;
|
|
291
|
+
const requestId = detail?.request?.requestId;
|
|
292
|
+
if (requestId) {
|
|
293
|
+
return `logs: requestId=${requestId}`;
|
|
294
|
+
}
|
|
295
|
+
return `request: ${method} ${path}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function formatTriageLine(triage) {
|
|
299
|
+
if (!triage) return null;
|
|
300
|
+
if (triage.status === "untriaged") return "triage: untriaged";
|
|
301
|
+
|
|
302
|
+
const entry = triage.entries?.[0];
|
|
303
|
+
if (!entry?.issue) {
|
|
304
|
+
if (triage.status === "validation_unavailable") {
|
|
305
|
+
const reason = triage.availability?.reason ? ` (${triage.availability.reason})` : "";
|
|
306
|
+
return `triage: validation unavailable${reason}`;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const issueLabel = `#${entry.issue.number}`;
|
|
312
|
+
const validationStatus = entry.validationStatus || null;
|
|
313
|
+
if (validationStatus === "closed_but_failing") {
|
|
314
|
+
return `triage: known issue ${issueLabel} closed but still failing`;
|
|
315
|
+
}
|
|
316
|
+
if (validationStatus === "validation_unavailable") {
|
|
317
|
+
const reason = triage.availability?.mode === "cache" ? "cache" : "validation unavailable";
|
|
318
|
+
return `triage: known issue ${issueLabel} (${reason})`;
|
|
319
|
+
}
|
|
320
|
+
if (entry.github?.state === "open" || entry.state === "open") {
|
|
321
|
+
return `triage: known issue ${issueLabel} open`;
|
|
322
|
+
}
|
|
323
|
+
if (entry.github?.state === "closed" || entry.state === "closed") {
|
|
324
|
+
return `triage: known issue ${issueLabel} closed`;
|
|
325
|
+
}
|
|
326
|
+
return `triage: known issue ${issueLabel}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function isThresholdWrapperMessage(message) {
|
|
330
|
+
return /Default runtime thresholds failed:/.test(String(message || ""));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function sanitizeInline(message, maxLength = 180) {
|
|
334
|
+
return String(message || "").replace(/\s+/g, " ").trim().slice(0, maxLength);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function collectServiceErrors(results) {
|
|
338
|
+
const items = [];
|
|
339
|
+
for (const result of results) {
|
|
340
|
+
for (const error of result.errors || []) {
|
|
341
|
+
items.push({
|
|
342
|
+
service: result.name,
|
|
343
|
+
message: sanitizeErrorMessage(error),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return items;
|
|
348
|
+
}
|
|
349
|
+
|
|
143
350
|
function pluralize(value, singular, plural) {
|
|
144
351
|
return value === 1 ? singular : plural;
|
|
145
352
|
}
|
|
@@ -94,9 +94,9 @@ describe("runner formatting", () => {
|
|
|
94
94
|
20_000
|
|
95
95
|
);
|
|
96
96
|
|
|
97
|
-
expect(lines.join("\n")).toContain("
|
|
98
|
-
expect(lines.join("\n")).toContain("
|
|
99
|
-
expect(lines.join("\n")).toContain("worker
|
|
97
|
+
expect(lines.join("\n")).toContain("Summary: 2 passed, 0 failed, 0 skipped, 0 not run across 3 files");
|
|
98
|
+
expect(lines.join("\n")).toContain("Runtime Errors:");
|
|
99
|
+
expect(lines.join("\n")).toContain("worker broke");
|
|
100
100
|
expect(lines.at(-1)).toBe("Result: FAILED (1/1 services failed)");
|
|
101
101
|
});
|
|
102
102
|
|
|
@@ -123,9 +123,7 @@ describe("runner formatting", () => {
|
|
|
123
123
|
0
|
|
124
124
|
);
|
|
125
125
|
|
|
126
|
-
expect(lines.join("\n")).toContain("
|
|
127
|
-
expect(lines.join("\n")).toContain("files 1 skipped");
|
|
128
|
-
expect(lines.join("\n")).toContain("SKIP api");
|
|
126
|
+
expect(lines.join("\n")).toContain("Summary: 0 passed, 0 failed, 1 skipped, 0 not run across 1 file");
|
|
129
127
|
expect(lines.at(-1)).toBe("Result: PASSED");
|
|
130
128
|
});
|
|
131
129
|
|
|
@@ -164,4 +162,81 @@ describe("runner formatting", () => {
|
|
|
164
162
|
expect(lines.join("\n")).toContain("2 closed issues still failing");
|
|
165
163
|
expect(lines.join("\n")).toContain("1 title mismatch");
|
|
166
164
|
});
|
|
165
|
+
|
|
166
|
+
it("prefers structured HTTP assertion details over threshold wrapper text", () => {
|
|
167
|
+
const lines = buildRunSummaryLines(
|
|
168
|
+
[
|
|
169
|
+
{
|
|
170
|
+
name: "api",
|
|
171
|
+
skipped: false,
|
|
172
|
+
failed: true,
|
|
173
|
+
suiteCount: 1,
|
|
174
|
+
completedSuiteCount: 1,
|
|
175
|
+
skippedSuiteCount: 0,
|
|
176
|
+
failedSuiteCount: 1,
|
|
177
|
+
totalFileCount: 1,
|
|
178
|
+
passedFileCount: 0,
|
|
179
|
+
failedFileCount: 1,
|
|
180
|
+
skippedFileCount: 0,
|
|
181
|
+
notRunFileCount: 0,
|
|
182
|
+
durationMs: 500,
|
|
183
|
+
suites: [
|
|
184
|
+
{
|
|
185
|
+
failed: true,
|
|
186
|
+
type: "int",
|
|
187
|
+
name: "default",
|
|
188
|
+
framework: "k6",
|
|
189
|
+
failedFiles: ["__testkit__/health/health.int.testkit.ts"],
|
|
190
|
+
durationMs: 500,
|
|
191
|
+
files: [
|
|
192
|
+
{
|
|
193
|
+
path: "__testkit__/health/health.int.testkit.ts",
|
|
194
|
+
status: "failed",
|
|
195
|
+
error: "Default runtime thresholds failed: checks(rate==1.0)",
|
|
196
|
+
failureDetails: [
|
|
197
|
+
{
|
|
198
|
+
kind: "http-assertion",
|
|
199
|
+
key: "GET /health > status is 200",
|
|
200
|
+
title: "status is 200",
|
|
201
|
+
message: "GET /health expected 200, got 404",
|
|
202
|
+
request: {
|
|
203
|
+
method: "GET",
|
|
204
|
+
path: "/health",
|
|
205
|
+
requestId: "req-1",
|
|
206
|
+
},
|
|
207
|
+
response: {
|
|
208
|
+
status: 404,
|
|
209
|
+
bodyPreview: '{"error":"nope"}',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
triage: {
|
|
214
|
+
status: "known_failure",
|
|
215
|
+
entries: [
|
|
216
|
+
{
|
|
217
|
+
id: "health-is-bad",
|
|
218
|
+
state: "open",
|
|
219
|
+
issue: {
|
|
220
|
+
repo: "acme/example",
|
|
221
|
+
number: 42,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
error: null,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
errors: [],
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
500
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(lines.join("\n")).toContain("GET /health expected 200, got 404");
|
|
238
|
+
expect(lines.join("\n")).toContain('response: {"error":"nope"}');
|
|
239
|
+
expect(lines.join("\n")).toContain("triage: known issue #42 open");
|
|
240
|
+
expect(lines.join("\n")).not.toContain("Default runtime thresholds failed: checks(rate==1.0)\n response:");
|
|
241
|
+
});
|
|
167
242
|
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const RESULT_LOGS_DIRNAME = "logs";
|
|
5
|
+
|
|
6
|
+
export function createRunLogRegistry(productDir) {
|
|
7
|
+
const records = new Map();
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
ensureServiceLogRecord(config) {
|
|
11
|
+
const key = `${config.runtimeLabel || config.name}:${config.name}`;
|
|
12
|
+
const existing = records.get(key);
|
|
13
|
+
if (existing) return existing;
|
|
14
|
+
|
|
15
|
+
const fileName = `${sanitizePathSegment(config.runtimeLabel || config.name)}__${sanitizePathSegment(config.name)}.log`;
|
|
16
|
+
const relativePath = path.join(".testkit", "results", RESULT_LOGS_DIRNAME, fileName);
|
|
17
|
+
const absolutePath = path.join(productDir, relativePath);
|
|
18
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
19
|
+
const stream = fs.createWriteStream(absolutePath, { flags: "a" });
|
|
20
|
+
const record = {
|
|
21
|
+
key,
|
|
22
|
+
serviceName: config.name,
|
|
23
|
+
runtimeLabel: config.runtimeLabel || config.name,
|
|
24
|
+
path: normalizePath(relativePath),
|
|
25
|
+
absolutePath,
|
|
26
|
+
stream,
|
|
27
|
+
};
|
|
28
|
+
records.set(key, record);
|
|
29
|
+
return record;
|
|
30
|
+
},
|
|
31
|
+
append(record, streamName, line) {
|
|
32
|
+
if (!record || typeof line !== "string") return;
|
|
33
|
+
record.stream.write(`${new Date().toISOString()} [${streamName}] ${line}\n`);
|
|
34
|
+
},
|
|
35
|
+
listServiceLogs() {
|
|
36
|
+
return [...records.values()]
|
|
37
|
+
.map((record) => ({
|
|
38
|
+
serviceName: record.serviceName,
|
|
39
|
+
runtimeLabel: record.runtimeLabel,
|
|
40
|
+
path: record.path,
|
|
41
|
+
}))
|
|
42
|
+
.sort(
|
|
43
|
+
(left, right) =>
|
|
44
|
+
left.serviceName.localeCompare(right.serviceName) ||
|
|
45
|
+
left.runtimeLabel.localeCompare(right.runtimeLabel)
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
closeAll() {
|
|
49
|
+
for (const record of records.values()) {
|
|
50
|
+
record.stream.end();
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function readLogTail(absolutePath, lineCount = 80) {
|
|
57
|
+
if (!absolutePath || !fs.existsSync(absolutePath)) return [];
|
|
58
|
+
const lines = fs.readFileSync(absolutePath, "utf8").split(/\r?\n/).filter(Boolean);
|
|
59
|
+
return lines.slice(Math.max(0, lines.length - lineCount));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function findLogSliceByRequestId(absolutePath, requestId, contextLines = 2) {
|
|
63
|
+
if (!absolutePath || !requestId || !fs.existsSync(absolutePath)) return [];
|
|
64
|
+
const lines = fs.readFileSync(absolutePath, "utf8").split(/\r?\n/).filter(Boolean);
|
|
65
|
+
const matches = [];
|
|
66
|
+
|
|
67
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
68
|
+
if (!lines[index].includes(requestId)) continue;
|
|
69
|
+
const start = Math.max(0, index - contextLines);
|
|
70
|
+
const end = Math.min(lines.length, index + contextLines + 1);
|
|
71
|
+
for (let cursor = start; cursor < end; cursor += 1) {
|
|
72
|
+
if (!matches.includes(lines[cursor])) matches.push(lines[cursor]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return matches;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sanitizePathSegment(value) {
|
|
80
|
+
return String(value)
|
|
81
|
+
.trim()
|
|
82
|
+
.toLowerCase()
|
|
83
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
84
|
+
.replace(/^-+|-+$/g, "") || "log";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizePath(filePath) {
|
|
88
|
+
return filePath.split(path.sep).join("/");
|
|
89
|
+
}
|
|
@@ -15,8 +15,12 @@ import {
|
|
|
15
15
|
summarizeDbBackend,
|
|
16
16
|
} from "./results.mjs";
|
|
17
17
|
import { buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
applyKnownFailureIssueValidationToArtifacts,
|
|
20
|
+
applyKnownFailuresToArtifacts,
|
|
21
|
+
loadKnownFailuresConfig,
|
|
22
|
+
} from "./triage.mjs";
|
|
23
|
+
import { formatError } from "./formatting.mjs";
|
|
20
24
|
import {
|
|
21
25
|
shouldFailKnownFailureIssueValidation,
|
|
22
26
|
validateKnownFailureIssues,
|
|
@@ -28,6 +32,7 @@ import {
|
|
|
28
32
|
writeRunArtifact,
|
|
29
33
|
writeStatusArtifact,
|
|
30
34
|
} from "./artifacts.mjs";
|
|
35
|
+
import { createRunLogRegistry } from "./logs.mjs";
|
|
31
36
|
import {
|
|
32
37
|
cleanupRunById,
|
|
33
38
|
cleanupRuns,
|
|
@@ -65,6 +70,8 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
65
70
|
productDir,
|
|
66
71
|
configs[0]?.testkit?.reporting || null
|
|
67
72
|
);
|
|
73
|
+
const reporter = opts.reporter || null;
|
|
74
|
+
const logRegistry = createRunLogRegistry(productDir);
|
|
68
75
|
const requestedFiles = opts.fileNames || [];
|
|
69
76
|
if (requestedFiles.length > 0) {
|
|
70
77
|
const unmatchedFiles = findUnmatchedRequestedFiles(
|
|
@@ -109,7 +116,8 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
109
116
|
typeValues,
|
|
110
117
|
suiteSelectors,
|
|
111
118
|
opts,
|
|
112
|
-
execution
|
|
119
|
+
execution,
|
|
120
|
+
reporter
|
|
113
121
|
);
|
|
114
122
|
const trackers = buildServiceTrackers(servicePlans, startedAt);
|
|
115
123
|
const executedPlans = servicePlans.filter((plan) => !plan.skipped);
|
|
@@ -138,6 +146,10 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
138
146
|
productDir,
|
|
139
147
|
graphs,
|
|
140
148
|
lifecycle,
|
|
149
|
+
runtimeOptions: {
|
|
150
|
+
reporter,
|
|
151
|
+
logRegistry,
|
|
152
|
+
},
|
|
141
153
|
});
|
|
142
154
|
const timingUpdates = [];
|
|
143
155
|
|
|
@@ -153,7 +165,8 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
153
165
|
lifecycle,
|
|
154
166
|
claimNextTask,
|
|
155
167
|
recordTaskOutcome,
|
|
156
|
-
recordGraphError
|
|
168
|
+
recordGraphError,
|
|
169
|
+
reporter
|
|
157
170
|
)
|
|
158
171
|
)
|
|
159
172
|
);
|
|
@@ -194,6 +207,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
194
207
|
serviceFilter: opts.serviceFilter || null,
|
|
195
208
|
metadata,
|
|
196
209
|
summarizeDbBackend,
|
|
210
|
+
serviceLogs: logRegistry.listServiceLogs(),
|
|
197
211
|
});
|
|
198
212
|
const statusArtifact = opts.writeStatus
|
|
199
213
|
? buildStatusArtifact({
|
|
@@ -220,6 +234,11 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
220
234
|
config: configs[0]?.testkit?.reporting?.issueValidation || null,
|
|
221
235
|
gitMetadata: metadata.git,
|
|
222
236
|
});
|
|
237
|
+
applyKnownFailureIssueValidationToArtifacts(
|
|
238
|
+
enrichedArtifacts.runArtifact,
|
|
239
|
+
enrichedArtifacts.statusArtifact,
|
|
240
|
+
knownFailureIssueValidation
|
|
241
|
+
);
|
|
223
242
|
attachKnownFailureIssueValidation(
|
|
224
243
|
enrichedArtifacts.runArtifact,
|
|
225
244
|
enrichedArtifacts.statusArtifact,
|
|
@@ -231,13 +250,19 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
231
250
|
writeStatusArtifact(productDir, enrichedArtifacts.statusArtifact);
|
|
232
251
|
}
|
|
233
252
|
|
|
234
|
-
|
|
235
|
-
await reportTelemetry(telemetry, enrichedArtifacts.runArtifact);
|
|
253
|
+
reporter?.runSummary?.(results, finishedAt - startedAt, knownFailureIssueValidation);
|
|
254
|
+
await reportTelemetry(telemetry, enrichedArtifacts.runArtifact, reporter);
|
|
236
255
|
if (results.some((result) => result.failed)) exitCode = 1;
|
|
237
256
|
if (shouldFailKnownFailureIssueValidation(knownFailureIssueValidation)) {
|
|
238
257
|
exitCode = 1;
|
|
239
258
|
}
|
|
240
259
|
if (lifecycle.isStopRequested()) exitCode = Math.max(exitCode, 130);
|
|
260
|
+
return {
|
|
261
|
+
runArtifact: enrichedArtifacts.runArtifact,
|
|
262
|
+
statusArtifact: enrichedArtifacts.statusArtifact,
|
|
263
|
+
results,
|
|
264
|
+
exitCode,
|
|
265
|
+
};
|
|
241
266
|
} finally {
|
|
242
267
|
if (lifecycle.isStopRequested()) {
|
|
243
268
|
exitCode = Math.max(exitCode, 130);
|
|
@@ -250,21 +275,22 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
250
275
|
await cleanupRuns(productDir, { includeActive: false });
|
|
251
276
|
lifecycle.removeManifest();
|
|
252
277
|
lifecycle.dispose();
|
|
278
|
+
logRegistry.closeAll();
|
|
253
279
|
process.exitCode = exitCode;
|
|
254
280
|
}
|
|
255
281
|
}
|
|
256
282
|
|
|
257
|
-
function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opts, execution) {
|
|
283
|
+
function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opts, execution, reporter) {
|
|
258
284
|
return configs.map((config) => {
|
|
259
|
-
console.log(`\n══ ${config.name} ══`);
|
|
260
285
|
const suites = applyShard(
|
|
261
286
|
collectSuites(config, typeValues, suiteSelectors, opts.fileNames || [], opts),
|
|
262
287
|
opts.shard
|
|
263
288
|
);
|
|
264
289
|
|
|
265
290
|
if (suites.length === 0) {
|
|
266
|
-
|
|
267
|
-
|
|
291
|
+
reporter?.serviceSkipped?.(
|
|
292
|
+
config,
|
|
293
|
+
`no matching files (types=${typeValues.join(",") || "all"} suites=${suiteSelectors.map((selector) => selector.raw).join(",") || "all"} files=${(opts.fileNames || []).join(",") || "all"})`
|
|
268
294
|
);
|
|
269
295
|
return {
|
|
270
296
|
config,
|
|
@@ -277,6 +303,17 @@ function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opt
|
|
|
277
303
|
};
|
|
278
304
|
}
|
|
279
305
|
|
|
306
|
+
for (const suite of suites) {
|
|
307
|
+
for (const skippedFile of suite.skippedFiles || []) {
|
|
308
|
+
reporter?.plannedSkip?.({
|
|
309
|
+
serviceName: config.name,
|
|
310
|
+
type: suite.displayType || suite.type,
|
|
311
|
+
file: skippedFile.path,
|
|
312
|
+
reason: skippedFile.reason,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
280
317
|
const runtimeConfigs = resolveRuntimeConfigs(config, configMap);
|
|
281
318
|
return {
|
|
282
319
|
config,
|
|
@@ -290,30 +327,24 @@ function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opt
|
|
|
290
327
|
});
|
|
291
328
|
}
|
|
292
329
|
|
|
293
|
-
function
|
|
294
|
-
for (const line of buildRunSummaryLines(results, durationMs, knownFailureIssueValidation)) {
|
|
295
|
-
console.log(line);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async function reportTelemetry(telemetry, artifact) {
|
|
330
|
+
async function reportTelemetry(telemetry, artifact, reporter = null) {
|
|
300
331
|
if (!telemetry?.enabled) return;
|
|
301
332
|
|
|
302
333
|
try {
|
|
303
334
|
const outcome = await uploadTelemetryArtifact(telemetry, artifact);
|
|
304
335
|
if (outcome?.ok) {
|
|
305
|
-
|
|
336
|
+
reporter?.telemetry?.("Telemetry: uploaded run artifact");
|
|
306
337
|
return;
|
|
307
338
|
}
|
|
308
339
|
if (outcome?.reason === "missing-token") {
|
|
309
|
-
|
|
340
|
+
reporter?.telemetry?.(
|
|
310
341
|
`Telemetry: skipped upload because ${telemetry.tokenEnv || "configured token env"} is not set`
|
|
311
342
|
);
|
|
312
343
|
return;
|
|
313
344
|
}
|
|
314
345
|
if (outcome?.reason && !outcome.skipped) return;
|
|
315
346
|
} catch (error) {
|
|
316
|
-
|
|
347
|
+
reporter?.telemetry?.(`Telemetry: upload failed (${formatError(error)})`);
|
|
317
348
|
}
|
|
318
349
|
}
|
|
319
350
|
|
|
@@ -3,14 +3,14 @@ import { execa } from "execa";
|
|
|
3
3
|
import { parsePlaywrightJsonResults } from "../reporters/playwright.mjs";
|
|
4
4
|
import { resolveServiceCwd } from "../config/index.mjs";
|
|
5
5
|
import { formatFileTimeoutBudgetError } from "../shared/file-timeout.mjs";
|
|
6
|
+
import { persistTaskOutputArtifacts } from "./artifacts.mjs";
|
|
6
7
|
import { settleSubprocess } from "./default-runtime-runner.mjs";
|
|
7
8
|
import { ensurePlaywrightTestConfig } from "./playwright-config.mjs";
|
|
8
|
-
import { printBufferedOutput } from "./processes.mjs";
|
|
9
9
|
import { normalizePathSeparators } from "./state.mjs";
|
|
10
10
|
import { buildPlaywrightEnv } from "./template.mjs";
|
|
11
11
|
import { killChildProcess } from "./processes.mjs";
|
|
12
12
|
|
|
13
|
-
export async function runPlaywrightTask(targetConfig, task, lifecycle, lease) {
|
|
13
|
+
export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, reporter = null) {
|
|
14
14
|
const local = targetConfig.testkit.local;
|
|
15
15
|
if (!local?.baseUrl) {
|
|
16
16
|
throw new Error(
|
|
@@ -44,7 +44,7 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease) {
|
|
|
44
44
|
if (subprocess.pid) interruptSubprocess();
|
|
45
45
|
else subprocess.once?.("spawn", interruptSubprocess);
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
reporter?.taskStarted?.(task, targetConfig);
|
|
48
48
|
let result;
|
|
49
49
|
let timedOut;
|
|
50
50
|
try {
|
|
@@ -53,15 +53,22 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease) {
|
|
|
53
53
|
lifecycle.unregisterProcess(subprocess.pid);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
if (result.stderr) {
|
|
57
|
-
printBufferedOutput(result.stderr, `[${targetConfig.runtimeLabel}:${targetConfig.name}:playwright]`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
56
|
const parsed = parsePlaywrightJsonResults(result.stdout || "", cwd);
|
|
61
57
|
const finishedAt = Date.now();
|
|
62
58
|
const durationMs = finishedAt - startedAt;
|
|
63
59
|
const relativeFile = normalizePathSeparators(requestedFile);
|
|
64
60
|
const fileResult = parsed.fileResults.get(relativeFile);
|
|
61
|
+
const outputArtifacts = persistTaskOutputArtifacts(targetConfig.productDir, task, [
|
|
62
|
+
result.stderr
|
|
63
|
+
? {
|
|
64
|
+
name: "playwright-stderr",
|
|
65
|
+
kind: "runtime.output",
|
|
66
|
+
summary: result.stderr.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "captured stderr",
|
|
67
|
+
stream: "stderr",
|
|
68
|
+
text: result.stderr,
|
|
69
|
+
}
|
|
70
|
+
: null,
|
|
71
|
+
]);
|
|
65
72
|
const genericError = timedOut
|
|
66
73
|
? formatFileTimeoutBudgetError(fileTimeoutSeconds)
|
|
67
74
|
: result.exitCode === 0
|
|
@@ -76,6 +83,7 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease) {
|
|
|
76
83
|
durationMs: fileResult?.durationMs > 0 ? fileResult.durationMs : durationMs,
|
|
77
84
|
startedAt,
|
|
78
85
|
finishedAt,
|
|
86
|
+
artifacts: outputArtifacts,
|
|
79
87
|
failureDetails: timedOut ? [] : fileResult?.failureDetails || [],
|
|
80
88
|
};
|
|
81
89
|
}
|