@elench/testkit 0.1.54 → 0.1.56
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 +22 -0
- package/lib/bundler/index.mjs +1 -1
- package/lib/bundler/index.test.mjs +29 -0
- package/lib/cli/args.mjs +2 -2
- package/lib/cli/args.test.mjs +8 -2
- package/lib/cli/command-helpers.mjs +5 -1
- package/lib/cli/commands/artifacts.mjs +2 -2
- package/lib/cli/commands/logs.mjs +2 -2
- package/lib/cli/commands/run.mjs +2 -2
- package/lib/cli/commands/show.mjs +2 -2
- package/lib/cli/db.mjs +17 -2
- package/lib/cli/entrypoint.mjs +2 -1
- package/lib/cli/presentation/run-reporter.mjs +25 -0
- package/lib/cli/presentation/run-reporter.test.mjs +80 -0
- package/lib/cli/tui/watch-app.mjs +134 -18
- package/lib/cli/viewer.mjs +67 -0
- package/lib/config/discovery.mjs +1 -0
- package/lib/config/discovery.test.mjs +8 -0
- 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/index.d.ts +58 -0
- package/lib/index.mjs +3 -0
- package/lib/runner/artifacts.mjs +16 -0
- package/lib/runner/default-runtime-runner.mjs +4 -1
- package/lib/runner/logs.mjs +54 -6
- package/lib/runner/orchestrator.mjs +67 -14
- package/lib/runner/planning.mjs +1 -1
- package/lib/runner/reporting.mjs +58 -2
- package/lib/runner/reporting.test.mjs +85 -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/suite-selection.mjs +4 -4
- package/lib/runner/suite-selection.test.mjs +9 -2
- package/lib/runner/template-steps.mjs +129 -11
- package/lib/runner/worker-loop.mjs +1 -1
- package/lib/runtime-src/k6/checks.js +9 -0
- package/lib/runtime-src/k6/scenario-runtime.js +234 -0
- package/lib/runtime-src/k6/scenario-suite.js +179 -0
- package/lib/toolchains/index.mjs +0 -4
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
recordTaskOutcome,
|
|
15
15
|
summarizeDbBackend,
|
|
16
16
|
} from "./results.mjs";
|
|
17
|
-
import { buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
|
|
17
|
+
import { buildLiveRunArtifact, buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
|
|
18
18
|
import {
|
|
19
19
|
applyKnownFailureIssueValidationToArtifacts,
|
|
20
20
|
applyKnownFailuresToArtifacts,
|
|
@@ -29,10 +29,12 @@ import {
|
|
|
29
29
|
loadTimings,
|
|
30
30
|
resetResultArtifacts,
|
|
31
31
|
saveTimings,
|
|
32
|
+
writeLiveRunArtifact,
|
|
32
33
|
writeRunArtifact,
|
|
33
34
|
writeStatusArtifact,
|
|
34
35
|
} from "./artifacts.mjs";
|
|
35
36
|
import { createRunLogRegistry } from "./logs.mjs";
|
|
37
|
+
import { createSetupOperationRegistry } from "./setup-operations.mjs";
|
|
36
38
|
import {
|
|
37
39
|
cleanupRunById,
|
|
38
40
|
cleanupRuns,
|
|
@@ -72,6 +74,9 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
72
74
|
);
|
|
73
75
|
const reporter = opts.reporter || null;
|
|
74
76
|
const logRegistry = createRunLogRegistry(productDir);
|
|
77
|
+
let workerCount = 0;
|
|
78
|
+
let runtimeInstanceCount = 0;
|
|
79
|
+
let runtimeStats = [];
|
|
75
80
|
const requestedFiles = opts.fileNames || [];
|
|
76
81
|
if (requestedFiles.length > 0) {
|
|
77
82
|
const unmatchedFiles = findUnmatchedRequestedFiles(
|
|
@@ -120,10 +125,41 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
120
125
|
reporter
|
|
121
126
|
);
|
|
122
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
|
+
scenarioSeed: opts.scenarioSeed || null,
|
|
150
|
+
metadata,
|
|
151
|
+
summarizeDbBackend,
|
|
152
|
+
serviceLogs: logRegistry.listServiceLogs(),
|
|
153
|
+
setupLogs: logRegistry.listSetupLogs(),
|
|
154
|
+
setupOperations: setupRegistry.listOperations(),
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
const setupRegistry = createSetupOperationRegistry({
|
|
159
|
+
logRegistry,
|
|
160
|
+
onChange: writeLiveSnapshot,
|
|
161
|
+
});
|
|
123
162
|
const executedPlans = servicePlans.filter((plan) => !plan.skipped);
|
|
124
|
-
let workerCount = 0;
|
|
125
|
-
let runtimeInstanceCount = 0;
|
|
126
|
-
let runtimeStats = [];
|
|
127
163
|
let exitCode = 0;
|
|
128
164
|
const lifecycle = createRunLifecycle(productDir);
|
|
129
165
|
lifecycle.markRunning();
|
|
@@ -131,12 +167,16 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
131
167
|
let results = [];
|
|
132
168
|
let finishedAt = Date.now();
|
|
133
169
|
let knownFailureIssueValidation = null;
|
|
170
|
+
writeLiveSnapshot();
|
|
134
171
|
|
|
135
172
|
try {
|
|
136
173
|
if (executedPlans.length > 0) {
|
|
137
174
|
const timings = loadTimings(productDir);
|
|
138
175
|
const graphs = buildRuntimeGraphs(executedPlans);
|
|
139
176
|
const queue = buildTaskQueue(executedPlans, graphs, timings);
|
|
177
|
+
for (const task of queue) {
|
|
178
|
+
task.scenarioSeed = opts.scenarioSeed || null;
|
|
179
|
+
}
|
|
140
180
|
workerCount = Math.max(1, Math.min(execution.workers, queue.length));
|
|
141
181
|
runtimeInstanceCount = graphs.reduce((sum, graph) => sum + graph.instanceCount, 0);
|
|
142
182
|
const workers = Array.from({ length: workerCount }, (_unused, index) =>
|
|
@@ -149,6 +189,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
149
189
|
runtimeOptions: {
|
|
150
190
|
reporter,
|
|
151
191
|
logRegistry,
|
|
192
|
+
setupRegistry,
|
|
152
193
|
},
|
|
153
194
|
});
|
|
154
195
|
const timingUpdates = [];
|
|
@@ -164,8 +205,14 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
164
205
|
timingUpdates,
|
|
165
206
|
lifecycle,
|
|
166
207
|
claimNextTask,
|
|
167
|
-
|
|
168
|
-
|
|
208
|
+
(allTrackers, task, outcome, now) => {
|
|
209
|
+
recordTaskOutcome(allTrackers, task, outcome, now);
|
|
210
|
+
writeLiveSnapshot();
|
|
211
|
+
},
|
|
212
|
+
(allTrackers, graph, message, now) => {
|
|
213
|
+
recordGraphError(allTrackers, graph, message, now);
|
|
214
|
+
writeLiveSnapshot();
|
|
215
|
+
},
|
|
169
216
|
reporter
|
|
170
217
|
)
|
|
171
218
|
)
|
|
@@ -177,9 +224,11 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
177
224
|
for (const tracker of trackers.values()) {
|
|
178
225
|
if (!tracker.skipped) addTrackerError(tracker, message);
|
|
179
226
|
}
|
|
227
|
+
writeLiveSnapshot();
|
|
180
228
|
}
|
|
181
229
|
}
|
|
182
230
|
runtimeStats = runtimeManager.getStats();
|
|
231
|
+
writeLiveSnapshot();
|
|
183
232
|
} finally {
|
|
184
233
|
await runtimeManager.cleanupAll();
|
|
185
234
|
}
|
|
@@ -202,12 +251,15 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
202
251
|
runtimeStats,
|
|
203
252
|
typeValues,
|
|
204
253
|
suiteSelectors,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
254
|
+
fileNames: requestedFiles,
|
|
255
|
+
shard: opts.shard || null,
|
|
256
|
+
serviceFilter: opts.serviceFilter || null,
|
|
257
|
+
scenarioSeed: opts.scenarioSeed || null,
|
|
258
|
+
metadata,
|
|
209
259
|
summarizeDbBackend,
|
|
210
260
|
serviceLogs: logRegistry.listServiceLogs(),
|
|
261
|
+
setupLogs: logRegistry.listSetupLogs(),
|
|
262
|
+
setupOperations: setupRegistry.listOperations(),
|
|
211
263
|
});
|
|
212
264
|
const statusArtifact = opts.writeStatus
|
|
213
265
|
? buildStatusArtifact({
|
|
@@ -215,10 +267,11 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
215
267
|
results,
|
|
216
268
|
typeValues,
|
|
217
269
|
suiteSelectors,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
270
|
+
fileNames: requestedFiles,
|
|
271
|
+
shard: opts.shard || null,
|
|
272
|
+
serviceFilter: opts.serviceFilter || null,
|
|
273
|
+
scenarioSeed: opts.scenarioSeed || null,
|
|
274
|
+
metadata,
|
|
222
275
|
})
|
|
223
276
|
: null;
|
|
224
277
|
const enrichedArtifacts = applyKnownFailuresToArtifacts(
|
package/lib/runner/planning.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
suiteSelectionType,
|
|
6
6
|
} from "./suite-selection.mjs";
|
|
7
7
|
|
|
8
|
-
const TYPE_ORDER = ["dal", "integration", "e2e", "load"];
|
|
8
|
+
const TYPE_ORDER = ["dal", "integration", "e2e", "scenario", "load"];
|
|
9
9
|
|
|
10
10
|
export function taskNeedsLocalRuntime(task) {
|
|
11
11
|
return task.type !== "dal";
|
package/lib/runner/reporting.mjs
CHANGED
|
@@ -8,6 +8,7 @@ export function buildStatusArtifact({
|
|
|
8
8
|
fileNames,
|
|
9
9
|
shard,
|
|
10
10
|
serviceFilter,
|
|
11
|
+
scenarioSeed,
|
|
11
12
|
metadata,
|
|
12
13
|
}) {
|
|
13
14
|
const executedResults = results.filter((result) => !result.skipped);
|
|
@@ -68,6 +69,7 @@ export function buildStatusArtifact({
|
|
|
68
69
|
fileNames: [...(fileNames || [])].sort(),
|
|
69
70
|
shard: shard || null,
|
|
70
71
|
serviceFilter: serviceFilter || null,
|
|
72
|
+
scenarioSeed: scenarioSeed || null,
|
|
71
73
|
};
|
|
72
74
|
scope.isFullRun =
|
|
73
75
|
scope.types.length === 1 &&
|
|
@@ -109,9 +111,13 @@ export function buildRunArtifact({
|
|
|
109
111
|
fileNames,
|
|
110
112
|
shard,
|
|
111
113
|
serviceFilter,
|
|
114
|
+
scenarioSeed,
|
|
112
115
|
metadata,
|
|
113
116
|
summarizeDbBackend,
|
|
114
117
|
serviceLogs = [],
|
|
118
|
+
setupLogs = [],
|
|
119
|
+
setupOperations = [],
|
|
120
|
+
runStatus = null,
|
|
115
121
|
}) {
|
|
116
122
|
const executed = results.filter((result) => !result.skipped);
|
|
117
123
|
const effectivelySkippedServices = executed.filter(isEffectivelySkippedService);
|
|
@@ -128,7 +134,7 @@ export function buildRunArtifact({
|
|
|
128
134
|
const dbBackend = summarizeDbBackend(results);
|
|
129
135
|
|
|
130
136
|
return {
|
|
131
|
-
schemaVersion:
|
|
137
|
+
schemaVersion: 8,
|
|
132
138
|
source: "testkit",
|
|
133
139
|
generatedAt: new Date(finishedAt).toISOString(),
|
|
134
140
|
product: {
|
|
@@ -138,7 +144,7 @@ export function buildRunArtifact({
|
|
|
138
144
|
git: metadata.git,
|
|
139
145
|
host: metadata.host,
|
|
140
146
|
run: {
|
|
141
|
-
status: failedServices.length > 0 ? "failed" : "passed",
|
|
147
|
+
status: runStatus || (failedServices.length > 0 ? "failed" : "passed"),
|
|
142
148
|
startedAt: new Date(startedAt).toISOString(),
|
|
143
149
|
finishedAt: new Date(finishedAt).toISOString(),
|
|
144
150
|
durationMs: finishedAt - startedAt,
|
|
@@ -153,6 +159,7 @@ export function buildRunArtifact({
|
|
|
153
159
|
fileNames,
|
|
154
160
|
shard,
|
|
155
161
|
serviceFilter,
|
|
162
|
+
scenarioSeed: scenarioSeed || null,
|
|
156
163
|
testkitVersion: metadata.testkitVersion,
|
|
157
164
|
},
|
|
158
165
|
summary: {
|
|
@@ -179,6 +186,10 @@ export function buildRunArtifact({
|
|
|
179
186
|
},
|
|
180
187
|
logs: {
|
|
181
188
|
services: serviceLogs,
|
|
189
|
+
setup: setupLogs,
|
|
190
|
+
},
|
|
191
|
+
setup: {
|
|
192
|
+
operations: setupOperations,
|
|
182
193
|
},
|
|
183
194
|
services: results.map((result) => ({
|
|
184
195
|
name: result.name,
|
|
@@ -203,6 +214,51 @@ export function buildRunArtifact({
|
|
|
203
214
|
};
|
|
204
215
|
}
|
|
205
216
|
|
|
217
|
+
export function buildLiveRunArtifact({
|
|
218
|
+
productDir,
|
|
219
|
+
results,
|
|
220
|
+
startedAt,
|
|
221
|
+
updatedAt,
|
|
222
|
+
execution,
|
|
223
|
+
workerCount,
|
|
224
|
+
runtimeInstanceCount,
|
|
225
|
+
runtimeStats,
|
|
226
|
+
typeValues,
|
|
227
|
+
suiteSelectors,
|
|
228
|
+
fileNames,
|
|
229
|
+
shard,
|
|
230
|
+
serviceFilter,
|
|
231
|
+
scenarioSeed,
|
|
232
|
+
metadata,
|
|
233
|
+
summarizeDbBackend,
|
|
234
|
+
serviceLogs = [],
|
|
235
|
+
setupLogs = [],
|
|
236
|
+
setupOperations = [],
|
|
237
|
+
}) {
|
|
238
|
+
return buildRunArtifact({
|
|
239
|
+
productDir,
|
|
240
|
+
results,
|
|
241
|
+
startedAt,
|
|
242
|
+
finishedAt: updatedAt,
|
|
243
|
+
execution,
|
|
244
|
+
workerCount,
|
|
245
|
+
runtimeInstanceCount,
|
|
246
|
+
runtimeStats,
|
|
247
|
+
typeValues,
|
|
248
|
+
suiteSelectors,
|
|
249
|
+
fileNames,
|
|
250
|
+
shard,
|
|
251
|
+
serviceFilter,
|
|
252
|
+
scenarioSeed,
|
|
253
|
+
metadata,
|
|
254
|
+
summarizeDbBackend,
|
|
255
|
+
serviceLogs,
|
|
256
|
+
setupLogs,
|
|
257
|
+
setupOperations,
|
|
258
|
+
runStatus: "running",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
206
262
|
function isEffectivelySkippedService(result) {
|
|
207
263
|
return (
|
|
208
264
|
!result.skipped &&
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
|
|
2
|
+
import { buildLiveRunArtifact, buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
|
|
3
3
|
|
|
4
4
|
describe("runner reporting", () => {
|
|
5
5
|
it("builds run artifacts", () => {
|
|
@@ -62,6 +62,7 @@ describe("runner reporting", () => {
|
|
|
62
62
|
fileNames: [],
|
|
63
63
|
shard: null,
|
|
64
64
|
serviceFilter: null,
|
|
65
|
+
scenarioSeed: "demo-seed",
|
|
65
66
|
metadata: {
|
|
66
67
|
git: {
|
|
67
68
|
branch: "main",
|
|
@@ -78,12 +79,13 @@ describe("runner reporting", () => {
|
|
|
78
79
|
});
|
|
79
80
|
|
|
80
81
|
expect(artifact.product.name).toBe("my-product");
|
|
81
|
-
expect(artifact.schemaVersion).toBe(
|
|
82
|
+
expect(artifact.schemaVersion).toBe(8);
|
|
82
83
|
expect(artifact.run).toMatchObject({
|
|
83
84
|
workers: 2,
|
|
84
85
|
fileTimeoutSeconds: 60,
|
|
85
86
|
workerCount: 1,
|
|
86
87
|
runtimeInstanceCount: 2,
|
|
88
|
+
scenarioSeed: "demo-seed",
|
|
87
89
|
});
|
|
88
90
|
expect(artifact.summary.services).toEqual({
|
|
89
91
|
total: 1,
|
|
@@ -109,7 +111,85 @@ describe("runner reporting", () => {
|
|
|
109
111
|
expect(artifact.services[0].totalTaskDurationMs).toBe(2400);
|
|
110
112
|
expect(artifact.logs).toEqual({
|
|
111
113
|
services: [],
|
|
114
|
+
setup: [],
|
|
112
115
|
});
|
|
116
|
+
expect(artifact.setup).toEqual({
|
|
117
|
+
operations: [],
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("builds live run artifacts with running status and setup details", () => {
|
|
122
|
+
const artifact = buildLiveRunArtifact({
|
|
123
|
+
productDir: "/tmp/my-product",
|
|
124
|
+
results: [],
|
|
125
|
+
startedAt: 1_000,
|
|
126
|
+
updatedAt: 2_000,
|
|
127
|
+
execution: {
|
|
128
|
+
workers: 2,
|
|
129
|
+
fileTimeoutSeconds: 60,
|
|
130
|
+
},
|
|
131
|
+
workerCount: 1,
|
|
132
|
+
runtimeInstanceCount: 1,
|
|
133
|
+
runtimeStats: [],
|
|
134
|
+
typeValues: ["int"],
|
|
135
|
+
suiteSelectors: [],
|
|
136
|
+
fileNames: [],
|
|
137
|
+
shard: null,
|
|
138
|
+
serviceFilter: null,
|
|
139
|
+
metadata: {
|
|
140
|
+
git: {
|
|
141
|
+
branch: "main",
|
|
142
|
+
commitSha: "abc",
|
|
143
|
+
repoRoot: "/tmp",
|
|
144
|
+
},
|
|
145
|
+
host: {
|
|
146
|
+
hostname: "local",
|
|
147
|
+
username: "dev",
|
|
148
|
+
},
|
|
149
|
+
testkitVersion: "0.1.54",
|
|
150
|
+
},
|
|
151
|
+
summarizeDbBackend: () => "local",
|
|
152
|
+
serviceLogs: [],
|
|
153
|
+
setupLogs: [
|
|
154
|
+
{
|
|
155
|
+
serviceName: "api",
|
|
156
|
+
runtimeLabel: "api",
|
|
157
|
+
stage: "runtime:prepare",
|
|
158
|
+
path: ".testkit/results/setup/api__api__runtime-prepare.log",
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
setupOperations: [
|
|
162
|
+
{
|
|
163
|
+
id: "setup-1",
|
|
164
|
+
serviceName: "api",
|
|
165
|
+
runtimeLabel: "api",
|
|
166
|
+
stage: "runtime:prepare",
|
|
167
|
+
kind: "runtime-prepare",
|
|
168
|
+
summary: "runtime prepare",
|
|
169
|
+
parentId: null,
|
|
170
|
+
status: "running",
|
|
171
|
+
startedAt: "1970-01-01T00:00:01.000Z",
|
|
172
|
+
finishedAt: null,
|
|
173
|
+
durationMs: null,
|
|
174
|
+
error: null,
|
|
175
|
+
logRef: {
|
|
176
|
+
path: ".testkit/results/setup/api__api__runtime-prepare.log",
|
|
177
|
+
stage: "runtime:prepare",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(artifact.run.status).toBe("running");
|
|
184
|
+
expect(artifact.logs.setup).toEqual([
|
|
185
|
+
{
|
|
186
|
+
serviceName: "api",
|
|
187
|
+
runtimeLabel: "api",
|
|
188
|
+
stage: "runtime:prepare",
|
|
189
|
+
path: ".testkit/results/setup/api__api__runtime-prepare.log",
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
expect(artifact.setup.operations).toHaveLength(1);
|
|
113
193
|
});
|
|
114
194
|
|
|
115
195
|
it("builds deterministic status artifacts", () => {
|
|
@@ -142,6 +222,7 @@ describe("runner reporting", () => {
|
|
|
142
222
|
fileNames: ["tests/api/integration/b.int.testkit.ts"],
|
|
143
223
|
shard: null,
|
|
144
224
|
serviceFilter: "api",
|
|
225
|
+
scenarioSeed: "demo-seed",
|
|
145
226
|
metadata: {
|
|
146
227
|
git: {
|
|
147
228
|
branch: "main",
|
|
@@ -169,6 +250,7 @@ describe("runner reporting", () => {
|
|
|
169
250
|
fileNames: ["tests/api/integration/b.int.testkit.ts"],
|
|
170
251
|
shard: null,
|
|
171
252
|
serviceFilter: "api",
|
|
253
|
+
scenarioSeed: "demo-seed",
|
|
172
254
|
isFullRun: false,
|
|
173
255
|
},
|
|
174
256
|
summary: {
|
|
@@ -213,6 +295,7 @@ describe("runner reporting", () => {
|
|
|
213
295
|
fileNames: [],
|
|
214
296
|
shard: null,
|
|
215
297
|
serviceFilter: null,
|
|
298
|
+
scenarioSeed: null,
|
|
216
299
|
metadata: {
|
|
217
300
|
git: {
|
|
218
301
|
branch: "main",
|
|
@@ -39,7 +39,7 @@ export async function ensureRuntimeInstanceReady(context, task, lifecycle, optio
|
|
|
39
39
|
if (!context.prepared) {
|
|
40
40
|
if (!context.preparationPromise) {
|
|
41
41
|
context.preparationPromise = (async () => {
|
|
42
|
-
await prepareDatabases(context.runtimeConfigs);
|
|
42
|
+
await prepareDatabases(context.runtimeConfigs, options);
|
|
43
43
|
await prepareRuntimeServices(context.runtimeConfigs, options);
|
|
44
44
|
context.prepared = true;
|
|
45
45
|
})().finally(() => {
|
|
@@ -80,8 +80,8 @@ export async function cleanupRuntimeInstanceContext(context, lifecycle) {
|
|
|
80
80
|
await deactivateRuntimeInstanceContext(context, lifecycle);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
export async function prepareDatabases(runtimeConfigs) {
|
|
83
|
+
export async function prepareDatabases(runtimeConfigs, options = {}) {
|
|
84
84
|
for (const config of runtimeConfigs) {
|
|
85
|
-
await prepareDatabaseRuntime(config);
|
|
85
|
+
await prepareDatabaseRuntime(config, options);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -28,6 +28,12 @@ export async function prepareRuntimeService(config, options = {}) {
|
|
|
28
28
|
const manifestPath = path.join(prepareDir, MANIFEST_FILE);
|
|
29
29
|
const existingManifest = readPrepareManifest(manifestPath);
|
|
30
30
|
if (existingManifest?.fingerprint === fingerprint) {
|
|
31
|
+
options.setupRegistry?.recordCached({
|
|
32
|
+
config,
|
|
33
|
+
stage: "runtime:prepare",
|
|
34
|
+
kind: "runtime-prepare",
|
|
35
|
+
summary: "runtime prepare cache hit",
|
|
36
|
+
});
|
|
31
37
|
return;
|
|
32
38
|
}
|
|
33
39
|
|
|
@@ -42,6 +48,14 @@ export async function prepareRuntimeService(config, options = {}) {
|
|
|
42
48
|
env.DATABASE_URL = databaseUrl;
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
const prepareOperation = options.setupRegistry?.start({
|
|
52
|
+
config,
|
|
53
|
+
stage: "runtime:prepare",
|
|
54
|
+
kind: "runtime-prepare",
|
|
55
|
+
summary: "runtime prepare",
|
|
56
|
+
recordLog: false,
|
|
57
|
+
});
|
|
58
|
+
|
|
45
59
|
try {
|
|
46
60
|
await announceResolvedToolchain(
|
|
47
61
|
config,
|
|
@@ -54,7 +68,16 @@ export async function prepareRuntimeService(config, options = {}) {
|
|
|
54
68
|
env,
|
|
55
69
|
labelPrefix: "runtime:prepare",
|
|
56
70
|
reporter: options.reporter,
|
|
71
|
+
setupRegistry: options.setupRegistry || null,
|
|
72
|
+
parentOperation: prepareOperation,
|
|
57
73
|
});
|
|
74
|
+
const finished = prepareOperation
|
|
75
|
+
? options.setupRegistry.finish(prepareOperation, {
|
|
76
|
+
status: "passed",
|
|
77
|
+
summary: "runtime prepare",
|
|
78
|
+
})
|
|
79
|
+
: null;
|
|
80
|
+
if (finished) options.reporter?.setupOperationFinished?.(finished);
|
|
58
81
|
writePrepareManifest(manifestPath, {
|
|
59
82
|
fingerprint,
|
|
60
83
|
preparedAt: new Date().toISOString(),
|
|
@@ -62,6 +85,14 @@ export async function prepareRuntimeService(config, options = {}) {
|
|
|
62
85
|
serviceName: config.name,
|
|
63
86
|
});
|
|
64
87
|
} catch (error) {
|
|
88
|
+
const finished = prepareOperation
|
|
89
|
+
? options.setupRegistry.finish(prepareOperation, {
|
|
90
|
+
status: "failed",
|
|
91
|
+
summary: "runtime prepare",
|
|
92
|
+
error: error?.message || error,
|
|
93
|
+
})
|
|
94
|
+
: null;
|
|
95
|
+
if (finished) options.reporter?.setupOperationFinished?.(finished);
|
|
65
96
|
fs.rmSync(prepareDir, { recursive: true, force: true });
|
|
66
97
|
throw error;
|
|
67
98
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export function createSetupOperationRegistry({ logRegistry = null, onChange = null } = {}) {
|
|
2
|
+
const operations = [];
|
|
3
|
+
const byId = new Map();
|
|
4
|
+
let nextId = 1;
|
|
5
|
+
|
|
6
|
+
function emitChange() {
|
|
7
|
+
onChange?.(listOperations());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function start({
|
|
11
|
+
config,
|
|
12
|
+
stage,
|
|
13
|
+
kind = "setup",
|
|
14
|
+
summary = null,
|
|
15
|
+
parentId = null,
|
|
16
|
+
recordLog = true,
|
|
17
|
+
}) {
|
|
18
|
+
const startedAt = new Date().toISOString();
|
|
19
|
+
const logRecord = recordLog ? logRegistry?.ensureSetupLogRecord(config, stage) || null : null;
|
|
20
|
+
const operation = {
|
|
21
|
+
id: `setup-${nextId++}`,
|
|
22
|
+
serviceName: config.name,
|
|
23
|
+
runtimeLabel: config.runtimeLabel || config.name,
|
|
24
|
+
stage,
|
|
25
|
+
kind,
|
|
26
|
+
summary,
|
|
27
|
+
parentId,
|
|
28
|
+
status: "running",
|
|
29
|
+
startedAt,
|
|
30
|
+
finishedAt: null,
|
|
31
|
+
durationMs: null,
|
|
32
|
+
error: null,
|
|
33
|
+
logRef: logRecord
|
|
34
|
+
? {
|
|
35
|
+
path: logRecord.path,
|
|
36
|
+
stage: logRecord.stage,
|
|
37
|
+
}
|
|
38
|
+
: null,
|
|
39
|
+
_logRecord: logRecord,
|
|
40
|
+
};
|
|
41
|
+
operations.push(operation);
|
|
42
|
+
byId.set(operation.id, operation);
|
|
43
|
+
emitChange();
|
|
44
|
+
return operation;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function finish(target, { status = "passed", summary, error = null } = {}) {
|
|
48
|
+
const operation = typeof target === "string" ? byId.get(target) : target;
|
|
49
|
+
if (!operation) return null;
|
|
50
|
+
const finishedAt = new Date().toISOString();
|
|
51
|
+
operation.finishedAt = finishedAt;
|
|
52
|
+
operation.durationMs = Math.max(
|
|
53
|
+
0,
|
|
54
|
+
Date.parse(finishedAt) - Date.parse(operation.startedAt || finishedAt)
|
|
55
|
+
);
|
|
56
|
+
operation.status = status;
|
|
57
|
+
if (summary !== undefined) operation.summary = summary;
|
|
58
|
+
if (error !== null) operation.error = String(error);
|
|
59
|
+
emitChange();
|
|
60
|
+
return operation;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function recordCached({ config, stage, kind = "setup", summary = null, parentId = null }) {
|
|
64
|
+
const now = new Date().toISOString();
|
|
65
|
+
const operation = {
|
|
66
|
+
id: `setup-${nextId++}`,
|
|
67
|
+
serviceName: config.name,
|
|
68
|
+
runtimeLabel: config.runtimeLabel || config.name,
|
|
69
|
+
stage,
|
|
70
|
+
kind,
|
|
71
|
+
summary,
|
|
72
|
+
parentId,
|
|
73
|
+
status: "cached",
|
|
74
|
+
startedAt: now,
|
|
75
|
+
finishedAt: now,
|
|
76
|
+
durationMs: 0,
|
|
77
|
+
error: null,
|
|
78
|
+
logRef: null,
|
|
79
|
+
_logRecord: null,
|
|
80
|
+
};
|
|
81
|
+
operations.push(operation);
|
|
82
|
+
byId.set(operation.id, operation);
|
|
83
|
+
emitChange();
|
|
84
|
+
return operation;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function listOperations() {
|
|
88
|
+
return operations.map(cloneOperation);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
start,
|
|
93
|
+
finish,
|
|
94
|
+
recordCached,
|
|
95
|
+
listOperations,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cloneOperation(operation) {
|
|
100
|
+
return {
|
|
101
|
+
id: operation.id,
|
|
102
|
+
serviceName: operation.serviceName,
|
|
103
|
+
runtimeLabel: operation.runtimeLabel,
|
|
104
|
+
stage: operation.stage,
|
|
105
|
+
kind: operation.kind,
|
|
106
|
+
summary: operation.summary,
|
|
107
|
+
parentId: operation.parentId,
|
|
108
|
+
status: operation.status,
|
|
109
|
+
startedAt: operation.startedAt,
|
|
110
|
+
finishedAt: operation.finishedAt,
|
|
111
|
+
durationMs: operation.durationMs,
|
|
112
|
+
error: operation.error,
|
|
113
|
+
logRef: operation.logRef,
|
|
114
|
+
};
|
|
115
|
+
}
|