@elench/testkit 0.1.51 → 0.1.53
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 +42 -7
- 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/run-reporter.mjs +91 -0
- package/lib/cli/tui/watch-app.mjs +104 -0
- package/lib/cli/viewer.mjs +163 -0
- package/lib/runner/artifacts.mjs +35 -0
- package/lib/runner/default-runtime-runner.mjs +44 -10
- package/lib/runner/formatting.mjs +97 -0
- package/lib/runner/formatting.test.mjs +4 -6
- package/lib/runner/logs.mjs +72 -0
- package/lib/runner/orchestrator.mjs +41 -19
- 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/worker-loop.mjs +8 -7
- package/lib/setup/index.d.ts +46 -13
- package/lib/setup/index.mjs +47 -0
- package/lib/setup/index.test.mjs +109 -1
- package/lib/toolchains/index.mjs +6 -3
- package/package.json +11 -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.extraDetails.slice(0, 2)) {
|
|
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,53 @@ 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 detailMessages = (file.failureDetails || [])
|
|
213
|
+
.map((detail) => detail.message || detail.title)
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
.map((message) => sanitizeErrorMessage(String(message).trim()));
|
|
216
|
+
failures.push({
|
|
217
|
+
file,
|
|
218
|
+
primaryMessage: sanitizeErrorMessage(file.error || detailMessages[0] || suite.error || "Failed"),
|
|
219
|
+
extraDetails: detailMessages.slice(file.error ? 0 : 1),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return failures.sort((left, right) => left.file.path.localeCompare(right.file.path));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function collectServiceErrors(results) {
|
|
228
|
+
const items = [];
|
|
229
|
+
for (const result of results) {
|
|
230
|
+
for (const error of result.errors || []) {
|
|
231
|
+
items.push({
|
|
232
|
+
service: result.name,
|
|
233
|
+
message: sanitizeErrorMessage(error),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return items;
|
|
238
|
+
}
|
|
239
|
+
|
|
143
240
|
function pluralize(value, singular, plural) {
|
|
144
241
|
return value === 1 ? singular : plural;
|
|
145
242
|
}
|
|
@@ -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
|
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
function sanitizePathSegment(value) {
|
|
63
|
+
return String(value)
|
|
64
|
+
.trim()
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
67
|
+
.replace(/^-+|-+$/g, "") || "log";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizePath(filePath) {
|
|
71
|
+
return filePath.split(path.sep).join("/");
|
|
72
|
+
}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "./results.mjs";
|
|
17
17
|
import { buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
|
|
18
18
|
import { applyKnownFailuresToArtifacts, loadKnownFailuresConfig } from "./triage.mjs";
|
|
19
|
-
import {
|
|
19
|
+
import { formatError } from "./formatting.mjs";
|
|
20
20
|
import {
|
|
21
21
|
shouldFailKnownFailureIssueValidation,
|
|
22
22
|
validateKnownFailureIssues,
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
writeRunArtifact,
|
|
29
29
|
writeStatusArtifact,
|
|
30
30
|
} from "./artifacts.mjs";
|
|
31
|
+
import { createRunLogRegistry } from "./logs.mjs";
|
|
31
32
|
import {
|
|
32
33
|
cleanupRunById,
|
|
33
34
|
cleanupRuns,
|
|
@@ -65,6 +66,8 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
65
66
|
productDir,
|
|
66
67
|
configs[0]?.testkit?.reporting || null
|
|
67
68
|
);
|
|
69
|
+
const reporter = opts.reporter || null;
|
|
70
|
+
const logRegistry = createRunLogRegistry(productDir);
|
|
68
71
|
const requestedFiles = opts.fileNames || [];
|
|
69
72
|
if (requestedFiles.length > 0) {
|
|
70
73
|
const unmatchedFiles = findUnmatchedRequestedFiles(
|
|
@@ -109,7 +112,8 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
109
112
|
typeValues,
|
|
110
113
|
suiteSelectors,
|
|
111
114
|
opts,
|
|
112
|
-
execution
|
|
115
|
+
execution,
|
|
116
|
+
reporter
|
|
113
117
|
);
|
|
114
118
|
const trackers = buildServiceTrackers(servicePlans, startedAt);
|
|
115
119
|
const executedPlans = servicePlans.filter((plan) => !plan.skipped);
|
|
@@ -138,6 +142,10 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
138
142
|
productDir,
|
|
139
143
|
graphs,
|
|
140
144
|
lifecycle,
|
|
145
|
+
runtimeOptions: {
|
|
146
|
+
reporter,
|
|
147
|
+
logRegistry,
|
|
148
|
+
},
|
|
141
149
|
});
|
|
142
150
|
const timingUpdates = [];
|
|
143
151
|
|
|
@@ -153,7 +161,8 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
153
161
|
lifecycle,
|
|
154
162
|
claimNextTask,
|
|
155
163
|
recordTaskOutcome,
|
|
156
|
-
recordGraphError
|
|
164
|
+
recordGraphError,
|
|
165
|
+
reporter
|
|
157
166
|
)
|
|
158
167
|
)
|
|
159
168
|
);
|
|
@@ -194,6 +203,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
194
203
|
serviceFilter: opts.serviceFilter || null,
|
|
195
204
|
metadata,
|
|
196
205
|
summarizeDbBackend,
|
|
206
|
+
serviceLogs: logRegistry.listServiceLogs(),
|
|
197
207
|
});
|
|
198
208
|
const statusArtifact = opts.writeStatus
|
|
199
209
|
? buildStatusArtifact({
|
|
@@ -231,13 +241,19 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
231
241
|
writeStatusArtifact(productDir, enrichedArtifacts.statusArtifact);
|
|
232
242
|
}
|
|
233
243
|
|
|
234
|
-
|
|
235
|
-
await reportTelemetry(telemetry, enrichedArtifacts.runArtifact);
|
|
244
|
+
reporter?.runSummary?.(results, finishedAt - startedAt, knownFailureIssueValidation);
|
|
245
|
+
await reportTelemetry(telemetry, enrichedArtifacts.runArtifact, reporter);
|
|
236
246
|
if (results.some((result) => result.failed)) exitCode = 1;
|
|
237
247
|
if (shouldFailKnownFailureIssueValidation(knownFailureIssueValidation)) {
|
|
238
248
|
exitCode = 1;
|
|
239
249
|
}
|
|
240
250
|
if (lifecycle.isStopRequested()) exitCode = Math.max(exitCode, 130);
|
|
251
|
+
return {
|
|
252
|
+
runArtifact: enrichedArtifacts.runArtifact,
|
|
253
|
+
statusArtifact: enrichedArtifacts.statusArtifact,
|
|
254
|
+
results,
|
|
255
|
+
exitCode,
|
|
256
|
+
};
|
|
241
257
|
} finally {
|
|
242
258
|
if (lifecycle.isStopRequested()) {
|
|
243
259
|
exitCode = Math.max(exitCode, 130);
|
|
@@ -250,21 +266,22 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
250
266
|
await cleanupRuns(productDir, { includeActive: false });
|
|
251
267
|
lifecycle.removeManifest();
|
|
252
268
|
lifecycle.dispose();
|
|
269
|
+
logRegistry.closeAll();
|
|
253
270
|
process.exitCode = exitCode;
|
|
254
271
|
}
|
|
255
272
|
}
|
|
256
273
|
|
|
257
|
-
function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opts, execution) {
|
|
274
|
+
function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opts, execution, reporter) {
|
|
258
275
|
return configs.map((config) => {
|
|
259
|
-
console.log(`\n══ ${config.name} ══`);
|
|
260
276
|
const suites = applyShard(
|
|
261
277
|
collectSuites(config, typeValues, suiteSelectors, opts.fileNames || [], opts),
|
|
262
278
|
opts.shard
|
|
263
279
|
);
|
|
264
280
|
|
|
265
281
|
if (suites.length === 0) {
|
|
266
|
-
|
|
267
|
-
|
|
282
|
+
reporter?.serviceSkipped?.(
|
|
283
|
+
config,
|
|
284
|
+
`no matching files (types=${typeValues.join(",") || "all"} suites=${suiteSelectors.map((selector) => selector.raw).join(",") || "all"} files=${(opts.fileNames || []).join(",") || "all"})`
|
|
268
285
|
);
|
|
269
286
|
return {
|
|
270
287
|
config,
|
|
@@ -277,6 +294,17 @@ function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opt
|
|
|
277
294
|
};
|
|
278
295
|
}
|
|
279
296
|
|
|
297
|
+
for (const suite of suites) {
|
|
298
|
+
for (const skippedFile of suite.skippedFiles || []) {
|
|
299
|
+
reporter?.plannedSkip?.({
|
|
300
|
+
serviceName: config.name,
|
|
301
|
+
type: suite.displayType || suite.type,
|
|
302
|
+
file: skippedFile.path,
|
|
303
|
+
reason: skippedFile.reason,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
280
308
|
const runtimeConfigs = resolveRuntimeConfigs(config, configMap);
|
|
281
309
|
return {
|
|
282
310
|
config,
|
|
@@ -290,30 +318,24 @@ function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opt
|
|
|
290
318
|
});
|
|
291
319
|
}
|
|
292
320
|
|
|
293
|
-
function
|
|
294
|
-
for (const line of buildRunSummaryLines(results, durationMs, knownFailureIssueValidation)) {
|
|
295
|
-
console.log(line);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async function reportTelemetry(telemetry, artifact) {
|
|
321
|
+
async function reportTelemetry(telemetry, artifact, reporter = null) {
|
|
300
322
|
if (!telemetry?.enabled) return;
|
|
301
323
|
|
|
302
324
|
try {
|
|
303
325
|
const outcome = await uploadTelemetryArtifact(telemetry, artifact);
|
|
304
326
|
if (outcome?.ok) {
|
|
305
|
-
|
|
327
|
+
reporter?.telemetry?.("Telemetry: uploaded run artifact");
|
|
306
328
|
return;
|
|
307
329
|
}
|
|
308
330
|
if (outcome?.reason === "missing-token") {
|
|
309
|
-
|
|
331
|
+
reporter?.telemetry?.(
|
|
310
332
|
`Telemetry: skipped upload because ${telemetry.tokenEnv || "configured token env"} is not set`
|
|
311
333
|
);
|
|
312
334
|
return;
|
|
313
335
|
}
|
|
314
336
|
if (outcome?.reason && !outcome.skipped) return;
|
|
315
337
|
} catch (error) {
|
|
316
|
-
|
|
338
|
+
reporter?.telemetry?.(`Telemetry: upload failed (${formatError(error)})`);
|
|
317
339
|
}
|
|
318
340
|
}
|
|
319
341
|
|
|
@@ -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
|
}
|
package/lib/runner/processes.mjs
CHANGED
|
@@ -55,9 +55,12 @@ export function killChildProcess(child, signal) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
export function
|
|
58
|
+
export function captureOutput(stream, options = {}) {
|
|
59
59
|
if (!stream) return Promise.resolve();
|
|
60
60
|
|
|
61
|
+
const onLine = typeof options.onLine === "function" ? options.onLine : null;
|
|
62
|
+
const liveWriter = typeof options.liveWriter === "function" ? options.liveWriter : null;
|
|
63
|
+
const livePrefix = options.livePrefix || "";
|
|
61
64
|
let pending = "";
|
|
62
65
|
return new Promise((resolve) => {
|
|
63
66
|
let settled = false;
|
|
@@ -72,12 +75,15 @@ export function pipeOutput(stream, prefix) {
|
|
|
72
75
|
const lines = pending.split(/\r?\n/);
|
|
73
76
|
pending = lines.pop() || "";
|
|
74
77
|
for (const line of lines) {
|
|
75
|
-
if (line.length
|
|
78
|
+
if (line.length === 0) continue;
|
|
79
|
+
onLine?.(line);
|
|
80
|
+
if (liveWriter) liveWriter(livePrefix ? `${livePrefix} ${line}` : line);
|
|
76
81
|
}
|
|
77
82
|
});
|
|
78
83
|
stream.on("end", () => {
|
|
79
84
|
if (pending.length > 0) {
|
|
80
|
-
|
|
85
|
+
onLine?.(pending);
|
|
86
|
+
if (liveWriter) liveWriter(livePrefix ? `${livePrefix} ${pending}` : pending);
|
|
81
87
|
}
|
|
82
88
|
settle();
|
|
83
89
|
});
|
|
@@ -86,14 +92,6 @@ export function pipeOutput(stream, prefix) {
|
|
|
86
92
|
});
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
export function printBufferedOutput(output, prefix) {
|
|
90
|
-
for (const line of output.split(/\r?\n/)) {
|
|
91
|
-
if (line.trim().length > 0) {
|
|
92
|
-
console.log(`${prefix} ${line}`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
95
|
export async function stopChildProcess(child, outputDrains = []) {
|
|
98
96
|
if (!child) return;
|
|
99
97
|
if (child.exitCode !== null) {
|
package/lib/runner/reporting.mjs
CHANGED
|
@@ -111,6 +111,7 @@ export function buildRunArtifact({
|
|
|
111
111
|
serviceFilter,
|
|
112
112
|
metadata,
|
|
113
113
|
summarizeDbBackend,
|
|
114
|
+
serviceLogs = [],
|
|
114
115
|
}) {
|
|
115
116
|
const executed = results.filter((result) => !result.skipped);
|
|
116
117
|
const effectivelySkippedServices = executed.filter(isEffectivelySkippedService);
|
|
@@ -127,7 +128,7 @@ export function buildRunArtifact({
|
|
|
127
128
|
const dbBackend = summarizeDbBackend(results);
|
|
128
129
|
|
|
129
130
|
return {
|
|
130
|
-
schemaVersion:
|
|
131
|
+
schemaVersion: 7,
|
|
131
132
|
source: "testkit",
|
|
132
133
|
generatedAt: new Date(finishedAt).toISOString(),
|
|
133
134
|
product: {
|
|
@@ -176,6 +177,9 @@ export function buildRunArtifact({
|
|
|
176
177
|
notRun: notRunFiles,
|
|
177
178
|
},
|
|
178
179
|
},
|
|
180
|
+
logs: {
|
|
181
|
+
services: serviceLogs,
|
|
182
|
+
},
|
|
179
183
|
services: results.map((result) => ({
|
|
180
184
|
name: result.name,
|
|
181
185
|
failed: result.failed,
|
|
@@ -78,7 +78,7 @@ describe("runner reporting", () => {
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
expect(artifact.product.name).toBe("my-product");
|
|
81
|
-
expect(artifact.schemaVersion).toBe(
|
|
81
|
+
expect(artifact.schemaVersion).toBe(7);
|
|
82
82
|
expect(artifact.run).toMatchObject({
|
|
83
83
|
workers: 2,
|
|
84
84
|
fileTimeoutSeconds: 60,
|
|
@@ -107,6 +107,9 @@ describe("runner reporting", () => {
|
|
|
107
107
|
});
|
|
108
108
|
expect(artifact.services[0].durationMs).toBe(1200);
|
|
109
109
|
expect(artifact.services[0].totalTaskDurationMs).toBe(2400);
|
|
110
|
+
expect(artifact.logs).toEqual({
|
|
111
|
+
services: [],
|
|
112
|
+
});
|
|
110
113
|
});
|
|
111
114
|
|
|
112
115
|
it("builds deterministic status artifacts", () => {
|
|
@@ -35,12 +35,12 @@ export function createRuntimeInstanceContext(runtimeId, graph, productDir) {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export async function ensureRuntimeInstanceReady(context, task, lifecycle) {
|
|
38
|
+
export async function ensureRuntimeInstanceReady(context, task, lifecycle, options = {}) {
|
|
39
39
|
if (!context.prepared) {
|
|
40
40
|
if (!context.preparationPromise) {
|
|
41
41
|
context.preparationPromise = (async () => {
|
|
42
42
|
await prepareDatabases(context.runtimeConfigs);
|
|
43
|
-
await prepareRuntimeServices(context.runtimeConfigs);
|
|
43
|
+
await prepareRuntimeServices(context.runtimeConfigs, options);
|
|
44
44
|
context.prepared = true;
|
|
45
45
|
})().finally(() => {
|
|
46
46
|
context.preparationPromise = null;
|
|
@@ -52,7 +52,11 @@ export async function ensureRuntimeInstanceReady(context, task, lifecycle) {
|
|
|
52
52
|
if (taskNeedsLocalRuntime(task) && !context.started) {
|
|
53
53
|
if (!context.startupPromise) {
|
|
54
54
|
context.startupPromise = (async () => {
|
|
55
|
-
context.startedServices = await startLocalServices(
|
|
55
|
+
context.startedServices = await startLocalServices(
|
|
56
|
+
context.runtimeConfigs,
|
|
57
|
+
lifecycle,
|
|
58
|
+
options
|
|
59
|
+
);
|
|
56
60
|
context.started = true;
|
|
57
61
|
})().finally(() => {
|
|
58
62
|
context.startupPromise = null;
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
ensureRuntimeInstanceReady,
|
|
9
9
|
} from "./runtime-contexts.mjs";
|
|
10
10
|
|
|
11
|
-
export function createRuntimeManager({ productDir, graphs, lifecycle, hooks = {} }) {
|
|
11
|
+
export function createRuntimeManager({ productDir, graphs, lifecycle, hooks = {}, runtimeOptions = {} }) {
|
|
12
12
|
const graphByKey = new Map(graphs.map((graph) => [graph.key, graph]));
|
|
13
13
|
const pools = new Map();
|
|
14
14
|
const locks = new Map();
|
|
@@ -19,6 +19,7 @@ export function createRuntimeManager({ productDir, graphs, lifecycle, hooks = {}
|
|
|
19
19
|
createRuntimeInstanceContext,
|
|
20
20
|
ensureRuntimeInstanceReady,
|
|
21
21
|
sleep,
|
|
22
|
+
options: runtimeOptions,
|
|
22
23
|
...hooks,
|
|
23
24
|
};
|
|
24
25
|
|
|
@@ -199,7 +200,12 @@ async function getReadyContext(slot, productDir, task, lifecycle, runtimeHooks)
|
|
|
199
200
|
slot.contextPromise = Promise.resolve(slot.context);
|
|
200
201
|
}
|
|
201
202
|
await slot.contextPromise;
|
|
202
|
-
await runtimeHooks.ensureRuntimeInstanceReady(
|
|
203
|
+
await runtimeHooks.ensureRuntimeInstanceReady(
|
|
204
|
+
slot.context,
|
|
205
|
+
task,
|
|
206
|
+
lifecycle,
|
|
207
|
+
runtimeHooks.options || {}
|
|
208
|
+
);
|
|
203
209
|
return slot.context;
|
|
204
210
|
}
|
|
205
211
|
|
|
@@ -13,13 +13,13 @@ import {
|
|
|
13
13
|
|
|
14
14
|
const MANIFEST_FILE = "prepare-manifest.json";
|
|
15
15
|
|
|
16
|
-
export async function prepareRuntimeServices(runtimeConfigs) {
|
|
16
|
+
export async function prepareRuntimeServices(runtimeConfigs, options = {}) {
|
|
17
17
|
for (const config of runtimeConfigs) {
|
|
18
|
-
await prepareRuntimeService(config);
|
|
18
|
+
await prepareRuntimeService(config, options);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export async function prepareRuntimeService(config) {
|
|
22
|
+
export async function prepareRuntimeService(config, options = {}) {
|
|
23
23
|
const prepare = config.testkit.runtime.prepare;
|
|
24
24
|
if (!prepare || prepare.steps.length === 0) return;
|
|
25
25
|
|
|
@@ -43,12 +43,17 @@ export async function prepareRuntimeService(config) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
try {
|
|
46
|
-
await announceResolvedToolchain(
|
|
46
|
+
await announceResolvedToolchain(
|
|
47
|
+
config,
|
|
48
|
+
await resolveConfiguredToolchain(config),
|
|
49
|
+
options.reporter
|
|
50
|
+
);
|
|
47
51
|
await runConfiguredSteps({
|
|
48
52
|
config,
|
|
49
53
|
steps: prepare.steps,
|
|
50
54
|
env,
|
|
51
55
|
labelPrefix: "runtime:prepare",
|
|
56
|
+
reporter: options.reporter,
|
|
52
57
|
});
|
|
53
58
|
writePrepareManifest(manifestPath, {
|
|
54
59
|
fingerprint,
|