@elench/testkit 0.1.53 → 0.1.55
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/lib/cli/commands/artifacts.mjs +2 -2
- package/lib/cli/commands/logs.mjs +2 -2
- package/lib/cli/commands/show.mjs +2 -2
- package/lib/cli/db.mjs +17 -2
- 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 +41 -7
- package/lib/cli/presentation/run-reporter.test.mjs +80 -0
- package/lib/cli/tui/watch-app.mjs +134 -18
- package/lib/cli/viewer.mjs +146 -4
- package/lib/database/index.mjs +85 -11
- package/lib/database/template-steps.mjs +45 -6
- package/lib/database/template-steps.test.mjs +43 -0
- package/lib/known-failures/index.mjs +1 -1
- package/lib/known-failures/index.test.mjs +46 -0
- package/lib/runner/artifacts.mjs +16 -0
- package/lib/runner/default-runtime-errors.mjs +66 -0
- package/lib/runner/default-runtime-runner.mjs +8 -1
- package/lib/runner/failure-details.mjs +31 -0
- package/lib/runner/failure-details.test.mjs +51 -0
- package/lib/runner/formatting.mjs +114 -4
- package/lib/runner/formatting.test.mjs +77 -0
- package/lib/runner/logs.mjs +71 -6
- package/lib/runner/orchestrator.mjs +63 -7
- package/lib/runner/reporting.mjs +52 -2
- package/lib/runner/reporting.test.mjs +80 -2
- package/lib/runner/runtime-contexts.mjs +3 -3
- package/lib/runner/runtime-preparation.mjs +31 -0
- package/lib/runner/setup-operations.mjs +115 -0
- package/lib/runner/setup-operations.test.mjs +94 -0
- package/lib/runner/template-steps.mjs +129 -11
- package/lib/runner/triage.mjs +67 -0
- 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 +0 -4
- package/package.json +3 -1
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
formatFileTimeoutBudgetError,
|
|
9
9
|
} from "../shared/file-timeout.mjs";
|
|
10
10
|
import { persistTaskArtifacts, persistTaskOutputArtifacts } from "./artifacts.mjs";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
determineDefaultRuntimeFailure,
|
|
13
|
+
extractDefaultRuntimeFatalDetail,
|
|
14
|
+
} from "./default-runtime-errors.mjs";
|
|
12
15
|
import { collectFailureDetailsFromRuntimeArtifacts } from "./failure-details.mjs";
|
|
13
16
|
import { RUNTIME_ARTIFACT_MARKER } from "../runtime-src/k6/artifacts.js";
|
|
14
17
|
import { readDatabaseUrl } from "./state-io.mjs";
|
|
@@ -147,6 +150,10 @@ export async function runDefaultRuntimeTask(
|
|
|
147
150
|
: null,
|
|
148
151
|
]);
|
|
149
152
|
const failureDetails = collectFailureDetailsFromRuntimeArtifacts(rawRuntimeArtifacts);
|
|
153
|
+
const fatalRuntimeDetail = extractDefaultRuntimeFatalDetail(result.stderr || "", getFirstLine);
|
|
154
|
+
if (fatalRuntimeDetail && !failureDetails.some((detail) => detail?.kind === "runtime-exception")) {
|
|
155
|
+
failureDetails.unshift(fatalRuntimeDetail);
|
|
156
|
+
}
|
|
150
157
|
const runtimeError = timedOut
|
|
151
158
|
? formatFileTimeoutBudgetError(fileTimeoutSeconds)
|
|
152
159
|
: determineDefaultRuntimeFailure(result, summary, getFirstLine);
|
|
@@ -28,6 +28,24 @@ export function normalizeFailureDetail(detail) {
|
|
|
28
28
|
const message = normalizeNonEmptyString(detail.message);
|
|
29
29
|
if (message) normalized.message = message;
|
|
30
30
|
|
|
31
|
+
if (detail.expected !== undefined) normalized.expected = cloneJsonValue(detail.expected);
|
|
32
|
+
if (detail.actual !== undefined) normalized.actual = cloneJsonValue(detail.actual);
|
|
33
|
+
|
|
34
|
+
const traceId = normalizeNonEmptyString(detail.traceId);
|
|
35
|
+
if (traceId) normalized.traceId = traceId;
|
|
36
|
+
|
|
37
|
+
const request = normalizeRecord(detail.request);
|
|
38
|
+
if (request) normalized.request = request;
|
|
39
|
+
|
|
40
|
+
const response = normalizeRecord(detail.response);
|
|
41
|
+
if (response) normalized.response = response;
|
|
42
|
+
|
|
43
|
+
const location = normalizeRecord(detail.location);
|
|
44
|
+
if (location) normalized.location = location;
|
|
45
|
+
|
|
46
|
+
const stack = normalizeNonEmptyString(detail.stack);
|
|
47
|
+
if (stack) normalized.stack = stack;
|
|
48
|
+
|
|
31
49
|
return normalized;
|
|
32
50
|
}
|
|
33
51
|
|
|
@@ -89,3 +107,16 @@ function normalizePositiveInteger(value) {
|
|
|
89
107
|
if (!Number.isInteger(value) || value <= 0) return null;
|
|
90
108
|
return value;
|
|
91
109
|
}
|
|
110
|
+
|
|
111
|
+
function normalizeRecord(value) {
|
|
112
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
113
|
+
return cloneJsonValue(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function cloneJsonValue(value) {
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(JSON.stringify(value));
|
|
119
|
+
} catch {
|
|
120
|
+
return String(value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -60,4 +60,55 @@ describe("runner failure details", () => {
|
|
|
60
60
|
},
|
|
61
61
|
]);
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
it("preserves rich assertion metadata", () => {
|
|
65
|
+
expect(
|
|
66
|
+
mergeFailureDetails([
|
|
67
|
+
{
|
|
68
|
+
kind: "http-assertion",
|
|
69
|
+
key: "GET /health > status is 200",
|
|
70
|
+
title: "status is 200",
|
|
71
|
+
expected: 200,
|
|
72
|
+
actual: 404,
|
|
73
|
+
request: {
|
|
74
|
+
method: "GET",
|
|
75
|
+
path: "/health",
|
|
76
|
+
requestId: "req-1",
|
|
77
|
+
},
|
|
78
|
+
response: {
|
|
79
|
+
status: 404,
|
|
80
|
+
bodyPreview: '{"error":"nope"}',
|
|
81
|
+
},
|
|
82
|
+
location: {
|
|
83
|
+
path: "/tmp/example.ts",
|
|
84
|
+
line: 12,
|
|
85
|
+
column: 4,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
])
|
|
89
|
+
).toEqual([
|
|
90
|
+
{
|
|
91
|
+
kind: "http-assertion",
|
|
92
|
+
key: "GET /health > status is 200",
|
|
93
|
+
title: "status is 200",
|
|
94
|
+
count: 1,
|
|
95
|
+
expected: 200,
|
|
96
|
+
actual: 404,
|
|
97
|
+
request: {
|
|
98
|
+
method: "GET",
|
|
99
|
+
path: "/health",
|
|
100
|
+
requestId: "req-1",
|
|
101
|
+
},
|
|
102
|
+
response: {
|
|
103
|
+
status: 404,
|
|
104
|
+
bodyPreview: '{"error":"nope"}',
|
|
105
|
+
},
|
|
106
|
+
location: {
|
|
107
|
+
path: "/tmp/example.ts",
|
|
108
|
+
line: 12,
|
|
109
|
+
column: 4,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
63
114
|
});
|
|
@@ -69,7 +69,7 @@ export function buildCompactRunSummaryLines(
|
|
|
69
69
|
for (const failure of failures) {
|
|
70
70
|
lines.push(` ${failure.file.path}`);
|
|
71
71
|
lines.push(` ${failure.primaryMessage}`);
|
|
72
|
-
for (const detail of failure.
|
|
72
|
+
for (const detail of failure.extraLines.slice(0, 3)) {
|
|
73
73
|
lines.push(` ${detail}`);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -209,14 +209,33 @@ function collectFailedFiles(results) {
|
|
|
209
209
|
for (const suite of result.suites || []) {
|
|
210
210
|
for (const file of suite.files || []) {
|
|
211
211
|
if (file.status !== "failed") continue;
|
|
212
|
-
const
|
|
212
|
+
const rankedDetails = rankFailureDetails(file.failureDetails || []);
|
|
213
|
+
const primaryDetail = rankedDetails[0] || null;
|
|
214
|
+
const fallbackMessages = rankedDetails
|
|
213
215
|
.map((detail) => detail.message || detail.title)
|
|
214
216
|
.filter(Boolean)
|
|
215
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
|
+
}
|
|
216
235
|
failures.push({
|
|
217
236
|
file,
|
|
218
|
-
primaryMessage:
|
|
219
|
-
|
|
237
|
+
primaryMessage: resolvePrimaryFailureMessage(file, suite, primaryDetail, fallbackMessages),
|
|
238
|
+
extraLines,
|
|
220
239
|
});
|
|
221
240
|
}
|
|
222
241
|
}
|
|
@@ -224,6 +243,97 @@ function collectFailedFiles(results) {
|
|
|
224
243
|
return failures.sort((left, right) => left.file.path.localeCompare(right.file.path));
|
|
225
244
|
}
|
|
226
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
|
+
|
|
227
337
|
function collectServiceErrors(results) {
|
|
228
338
|
const items = [];
|
|
229
339
|
for (const result of results) {
|
|
@@ -162,4 +162,81 @@ describe("runner formatting", () => {
|
|
|
162
162
|
expect(lines.join("\n")).toContain("2 closed issues still failing");
|
|
163
163
|
expect(lines.join("\n")).toContain("1 title mismatch");
|
|
164
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
|
+
});
|
|
165
242
|
});
|
package/lib/runner/logs.mjs
CHANGED
|
@@ -2,21 +2,26 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
|
|
4
4
|
const RESULT_LOGS_DIRNAME = "logs";
|
|
5
|
+
const RESULT_SETUP_DIRNAME = "setup";
|
|
5
6
|
|
|
6
7
|
export function createRunLogRegistry(productDir) {
|
|
7
|
-
const
|
|
8
|
+
const serviceRecords = new Map();
|
|
9
|
+
const setupRecords = new Map();
|
|
8
10
|
|
|
9
11
|
return {
|
|
10
12
|
ensureServiceLogRecord(config) {
|
|
11
13
|
const key = `${config.runtimeLabel || config.name}:${config.name}`;
|
|
12
|
-
const existing =
|
|
14
|
+
const existing = serviceRecords.get(key);
|
|
13
15
|
if (existing) return existing;
|
|
14
16
|
|
|
15
17
|
const fileName = `${sanitizePathSegment(config.runtimeLabel || config.name)}__${sanitizePathSegment(config.name)}.log`;
|
|
16
18
|
const relativePath = path.join(".testkit", "results", RESULT_LOGS_DIRNAME, fileName);
|
|
17
19
|
const absolutePath = path.join(productDir, relativePath);
|
|
18
20
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
19
|
-
const stream = fs.createWriteStream(absolutePath, {
|
|
21
|
+
const stream = fs.createWriteStream(absolutePath, {
|
|
22
|
+
fd: fs.openSync(absolutePath, "a"),
|
|
23
|
+
flags: "a",
|
|
24
|
+
});
|
|
20
25
|
const record = {
|
|
21
26
|
key,
|
|
22
27
|
serviceName: config.name,
|
|
@@ -25,7 +30,32 @@ export function createRunLogRegistry(productDir) {
|
|
|
25
30
|
absolutePath,
|
|
26
31
|
stream,
|
|
27
32
|
};
|
|
28
|
-
|
|
33
|
+
serviceRecords.set(key, record);
|
|
34
|
+
return record;
|
|
35
|
+
},
|
|
36
|
+
ensureSetupLogRecord(config, stage) {
|
|
37
|
+
const key = `${config.runtimeLabel || config.name}:${config.name}:${stage}`;
|
|
38
|
+
const existing = setupRecords.get(key);
|
|
39
|
+
if (existing) return existing;
|
|
40
|
+
|
|
41
|
+
const fileName = `${sanitizePathSegment(config.runtimeLabel || config.name)}__${sanitizePathSegment(config.name)}__${sanitizePathSegment(stage)}.log`;
|
|
42
|
+
const relativePath = path.join(".testkit", "results", RESULT_SETUP_DIRNAME, fileName);
|
|
43
|
+
const absolutePath = path.join(productDir, relativePath);
|
|
44
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
45
|
+
const stream = fs.createWriteStream(absolutePath, {
|
|
46
|
+
fd: fs.openSync(absolutePath, "a"),
|
|
47
|
+
flags: "a",
|
|
48
|
+
});
|
|
49
|
+
const record = {
|
|
50
|
+
key,
|
|
51
|
+
serviceName: config.name,
|
|
52
|
+
runtimeLabel: config.runtimeLabel || config.name,
|
|
53
|
+
stage,
|
|
54
|
+
path: normalizePath(relativePath),
|
|
55
|
+
absolutePath,
|
|
56
|
+
stream,
|
|
57
|
+
};
|
|
58
|
+
setupRecords.set(key, record);
|
|
29
59
|
return record;
|
|
30
60
|
},
|
|
31
61
|
append(record, streamName, line) {
|
|
@@ -33,7 +63,7 @@ export function createRunLogRegistry(productDir) {
|
|
|
33
63
|
record.stream.write(`${new Date().toISOString()} [${streamName}] ${line}\n`);
|
|
34
64
|
},
|
|
35
65
|
listServiceLogs() {
|
|
36
|
-
return [...
|
|
66
|
+
return [...serviceRecords.values()]
|
|
37
67
|
.map((record) => ({
|
|
38
68
|
serviceName: record.serviceName,
|
|
39
69
|
runtimeLabel: record.runtimeLabel,
|
|
@@ -45,8 +75,26 @@ export function createRunLogRegistry(productDir) {
|
|
|
45
75
|
left.runtimeLabel.localeCompare(right.runtimeLabel)
|
|
46
76
|
);
|
|
47
77
|
},
|
|
78
|
+
listSetupLogs() {
|
|
79
|
+
return [...setupRecords.values()]
|
|
80
|
+
.map((record) => ({
|
|
81
|
+
serviceName: record.serviceName,
|
|
82
|
+
runtimeLabel: record.runtimeLabel,
|
|
83
|
+
stage: record.stage,
|
|
84
|
+
path: record.path,
|
|
85
|
+
}))
|
|
86
|
+
.sort(
|
|
87
|
+
(left, right) =>
|
|
88
|
+
left.serviceName.localeCompare(right.serviceName) ||
|
|
89
|
+
left.runtimeLabel.localeCompare(right.runtimeLabel) ||
|
|
90
|
+
left.stage.localeCompare(right.stage)
|
|
91
|
+
);
|
|
92
|
+
},
|
|
48
93
|
closeAll() {
|
|
49
|
-
for (const record of
|
|
94
|
+
for (const record of serviceRecords.values()) {
|
|
95
|
+
record.stream.end();
|
|
96
|
+
}
|
|
97
|
+
for (const record of setupRecords.values()) {
|
|
50
98
|
record.stream.end();
|
|
51
99
|
}
|
|
52
100
|
},
|
|
@@ -59,6 +107,23 @@ export function readLogTail(absolutePath, lineCount = 80) {
|
|
|
59
107
|
return lines.slice(Math.max(0, lines.length - lineCount));
|
|
60
108
|
}
|
|
61
109
|
|
|
110
|
+
export function findLogSliceByRequestId(absolutePath, requestId, contextLines = 2) {
|
|
111
|
+
if (!absolutePath || !requestId || !fs.existsSync(absolutePath)) return [];
|
|
112
|
+
const lines = fs.readFileSync(absolutePath, "utf8").split(/\r?\n/).filter(Boolean);
|
|
113
|
+
const matches = [];
|
|
114
|
+
|
|
115
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
116
|
+
if (!lines[index].includes(requestId)) continue;
|
|
117
|
+
const start = Math.max(0, index - contextLines);
|
|
118
|
+
const end = Math.min(lines.length, index + contextLines + 1);
|
|
119
|
+
for (let cursor = start; cursor < end; cursor += 1) {
|
|
120
|
+
if (!matches.includes(lines[cursor])) matches.push(lines[cursor]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return matches;
|
|
125
|
+
}
|
|
126
|
+
|
|
62
127
|
function sanitizePathSegment(value) {
|
|
63
128
|
return String(value)
|
|
64
129
|
.trim()
|
|
@@ -14,8 +14,12 @@ import {
|
|
|
14
14
|
recordTaskOutcome,
|
|
15
15
|
summarizeDbBackend,
|
|
16
16
|
} from "./results.mjs";
|
|
17
|
-
import { buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
|
|
18
|
-
import {
|
|
17
|
+
import { buildLiveRunArtifact, buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
|
|
18
|
+
import {
|
|
19
|
+
applyKnownFailureIssueValidationToArtifacts,
|
|
20
|
+
applyKnownFailuresToArtifacts,
|
|
21
|
+
loadKnownFailuresConfig,
|
|
22
|
+
} from "./triage.mjs";
|
|
19
23
|
import { formatError } from "./formatting.mjs";
|
|
20
24
|
import {
|
|
21
25
|
shouldFailKnownFailureIssueValidation,
|
|
@@ -25,10 +29,12 @@ import {
|
|
|
25
29
|
loadTimings,
|
|
26
30
|
resetResultArtifacts,
|
|
27
31
|
saveTimings,
|
|
32
|
+
writeLiveRunArtifact,
|
|
28
33
|
writeRunArtifact,
|
|
29
34
|
writeStatusArtifact,
|
|
30
35
|
} from "./artifacts.mjs";
|
|
31
36
|
import { createRunLogRegistry } from "./logs.mjs";
|
|
37
|
+
import { createSetupOperationRegistry } from "./setup-operations.mjs";
|
|
32
38
|
import {
|
|
33
39
|
cleanupRunById,
|
|
34
40
|
cleanupRuns,
|
|
@@ -68,6 +74,9 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
68
74
|
);
|
|
69
75
|
const reporter = opts.reporter || null;
|
|
70
76
|
const logRegistry = createRunLogRegistry(productDir);
|
|
77
|
+
let workerCount = 0;
|
|
78
|
+
let runtimeInstanceCount = 0;
|
|
79
|
+
let runtimeStats = [];
|
|
71
80
|
const requestedFiles = opts.fileNames || [];
|
|
72
81
|
if (requestedFiles.length > 0) {
|
|
73
82
|
const unmatchedFiles = findUnmatchedRequestedFiles(
|
|
@@ -116,10 +125,40 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
116
125
|
reporter
|
|
117
126
|
);
|
|
118
127
|
const trackers = buildServiceTrackers(servicePlans, startedAt);
|
|
128
|
+
const writeLiveSnapshot = () => {
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
const partialResults = configs.map((config) =>
|
|
131
|
+
finalizeServiceResult(trackers.get(config.name), startedAt, now)
|
|
132
|
+
);
|
|
133
|
+
writeLiveRunArtifact(
|
|
134
|
+
productDir,
|
|
135
|
+
buildLiveRunArtifact({
|
|
136
|
+
productDir,
|
|
137
|
+
results: partialResults,
|
|
138
|
+
startedAt,
|
|
139
|
+
updatedAt: now,
|
|
140
|
+
execution,
|
|
141
|
+
workerCount,
|
|
142
|
+
runtimeInstanceCount,
|
|
143
|
+
runtimeStats,
|
|
144
|
+
typeValues,
|
|
145
|
+
suiteSelectors,
|
|
146
|
+
fileNames: requestedFiles,
|
|
147
|
+
shard: opts.shard || null,
|
|
148
|
+
serviceFilter: opts.serviceFilter || null,
|
|
149
|
+
metadata,
|
|
150
|
+
summarizeDbBackend,
|
|
151
|
+
serviceLogs: logRegistry.listServiceLogs(),
|
|
152
|
+
setupLogs: logRegistry.listSetupLogs(),
|
|
153
|
+
setupOperations: setupRegistry.listOperations(),
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
const setupRegistry = createSetupOperationRegistry({
|
|
158
|
+
logRegistry,
|
|
159
|
+
onChange: writeLiveSnapshot,
|
|
160
|
+
});
|
|
119
161
|
const executedPlans = servicePlans.filter((plan) => !plan.skipped);
|
|
120
|
-
let workerCount = 0;
|
|
121
|
-
let runtimeInstanceCount = 0;
|
|
122
|
-
let runtimeStats = [];
|
|
123
162
|
let exitCode = 0;
|
|
124
163
|
const lifecycle = createRunLifecycle(productDir);
|
|
125
164
|
lifecycle.markRunning();
|
|
@@ -127,6 +166,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
127
166
|
let results = [];
|
|
128
167
|
let finishedAt = Date.now();
|
|
129
168
|
let knownFailureIssueValidation = null;
|
|
169
|
+
writeLiveSnapshot();
|
|
130
170
|
|
|
131
171
|
try {
|
|
132
172
|
if (executedPlans.length > 0) {
|
|
@@ -145,6 +185,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
145
185
|
runtimeOptions: {
|
|
146
186
|
reporter,
|
|
147
187
|
logRegistry,
|
|
188
|
+
setupRegistry,
|
|
148
189
|
},
|
|
149
190
|
});
|
|
150
191
|
const timingUpdates = [];
|
|
@@ -160,8 +201,14 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
160
201
|
timingUpdates,
|
|
161
202
|
lifecycle,
|
|
162
203
|
claimNextTask,
|
|
163
|
-
|
|
164
|
-
|
|
204
|
+
(allTrackers, task, outcome, now) => {
|
|
205
|
+
recordTaskOutcome(allTrackers, task, outcome, now);
|
|
206
|
+
writeLiveSnapshot();
|
|
207
|
+
},
|
|
208
|
+
(allTrackers, graph, message, now) => {
|
|
209
|
+
recordGraphError(allTrackers, graph, message, now);
|
|
210
|
+
writeLiveSnapshot();
|
|
211
|
+
},
|
|
165
212
|
reporter
|
|
166
213
|
)
|
|
167
214
|
)
|
|
@@ -173,9 +220,11 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
173
220
|
for (const tracker of trackers.values()) {
|
|
174
221
|
if (!tracker.skipped) addTrackerError(tracker, message);
|
|
175
222
|
}
|
|
223
|
+
writeLiveSnapshot();
|
|
176
224
|
}
|
|
177
225
|
}
|
|
178
226
|
runtimeStats = runtimeManager.getStats();
|
|
227
|
+
writeLiveSnapshot();
|
|
179
228
|
} finally {
|
|
180
229
|
await runtimeManager.cleanupAll();
|
|
181
230
|
}
|
|
@@ -204,6 +253,8 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
204
253
|
metadata,
|
|
205
254
|
summarizeDbBackend,
|
|
206
255
|
serviceLogs: logRegistry.listServiceLogs(),
|
|
256
|
+
setupLogs: logRegistry.listSetupLogs(),
|
|
257
|
+
setupOperations: setupRegistry.listOperations(),
|
|
207
258
|
});
|
|
208
259
|
const statusArtifact = opts.writeStatus
|
|
209
260
|
? buildStatusArtifact({
|
|
@@ -230,6 +281,11 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
230
281
|
config: configs[0]?.testkit?.reporting?.issueValidation || null,
|
|
231
282
|
gitMetadata: metadata.git,
|
|
232
283
|
});
|
|
284
|
+
applyKnownFailureIssueValidationToArtifacts(
|
|
285
|
+
enrichedArtifacts.runArtifact,
|
|
286
|
+
enrichedArtifacts.statusArtifact,
|
|
287
|
+
knownFailureIssueValidation
|
|
288
|
+
);
|
|
233
289
|
attachKnownFailureIssueValidation(
|
|
234
290
|
enrichedArtifacts.runArtifact,
|
|
235
291
|
enrichedArtifacts.statusArtifact,
|