@elench/testkit 0.1.54 → 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/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 +37 -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/runner/artifacts.mjs +16 -0
- package/lib/runner/logs.mjs +54 -6
- package/lib/runner/orchestrator.mjs +53 -6
- 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/toolchains/index.mjs +0 -4
- package/package.json +1 -1
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
collectConfiguredInputs,
|
|
7
7
|
runConfiguredSteps,
|
|
8
8
|
} from "../runner/template-steps.mjs";
|
|
9
|
+
import { captureOutput } from "../runner/processes.mjs";
|
|
9
10
|
|
|
10
|
-
export async function runTemplateStage(config, stageName, databaseUrl) {
|
|
11
|
+
export async function runTemplateStage(config, stageName, databaseUrl, options = {}) {
|
|
11
12
|
const steps = config.testkit.database?.template?.[stageName] || [];
|
|
12
13
|
if (steps.length === 0) return;
|
|
13
14
|
|
|
@@ -21,6 +22,9 @@ export async function runTemplateStage(config, stageName, databaseUrl) {
|
|
|
21
22
|
steps,
|
|
22
23
|
env,
|
|
23
24
|
labelPrefix: `template:${stageName}`,
|
|
25
|
+
reporter: options.reporter || null,
|
|
26
|
+
setupRegistry: options.setupRegistry || null,
|
|
27
|
+
parentOperation: options.parentOperation || null,
|
|
24
28
|
});
|
|
25
29
|
}
|
|
26
30
|
|
|
@@ -32,12 +36,12 @@ export function collectTemplateInputs(productDir, template = {}) {
|
|
|
32
36
|
});
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
export async function captureTemplateSnapshot(config, outputPath, databaseUrl) {
|
|
39
|
+
export async function captureTemplateSnapshot(config, outputPath, databaseUrl, options = {}) {
|
|
36
40
|
const templateDbUrl = databaseUrl;
|
|
37
41
|
const absoluteOutputPath = path.resolve(config.productDir, outputPath);
|
|
38
42
|
fs.mkdirSync(path.dirname(absoluteOutputPath), { recursive: true });
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
const child = execa(
|
|
41
45
|
"pg_dump",
|
|
42
46
|
[
|
|
43
47
|
"--schema-only",
|
|
@@ -53,19 +57,54 @@ export async function captureTemplateSnapshot(config, outputPath, databaseUrl) {
|
|
|
53
57
|
...buildExecutionEnv(config, {}, process.env),
|
|
54
58
|
DATABASE_URL: templateDbUrl,
|
|
55
59
|
},
|
|
56
|
-
|
|
60
|
+
stdout: "pipe",
|
|
61
|
+
stderr: "pipe",
|
|
62
|
+
reject: false,
|
|
57
63
|
}
|
|
58
64
|
);
|
|
65
|
+
const liveWriter =
|
|
66
|
+
options.reporter?.outputMode === "debug"
|
|
67
|
+
? (line) => options.reporter.writeDebugLine?.(line)
|
|
68
|
+
: null;
|
|
69
|
+
const logRecord = options.logRecord || null;
|
|
70
|
+
const drains = [
|
|
71
|
+
captureOutput(child.stdout, {
|
|
72
|
+
livePrefix: `[${config.runtimeLabel || config.name}:${config.name}]`,
|
|
73
|
+
liveWriter,
|
|
74
|
+
onLine(line) {
|
|
75
|
+
if (logRecord) logRecord.stream.write(`${new Date().toISOString()} [stdout] ${line}\n`);
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
captureOutput(child.stderr, {
|
|
79
|
+
livePrefix: `[${config.runtimeLabel || config.name}:${config.name}]`,
|
|
80
|
+
liveWriter,
|
|
81
|
+
onLine(line) {
|
|
82
|
+
if (logRecord) logRecord.stream.write(`${new Date().toISOString()} [stderr] ${line}\n`);
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
];
|
|
86
|
+
const result = await child;
|
|
87
|
+
await Promise.all(drains);
|
|
88
|
+
if (result.exitCode !== 0) {
|
|
89
|
+
throw new Error(result.shortMessage || result.stderr || result.stdout || "pg_dump failed");
|
|
90
|
+
}
|
|
59
91
|
|
|
60
92
|
sanitizeSnapshotFile(absoluteOutputPath);
|
|
61
93
|
return absoluteOutputPath;
|
|
62
94
|
}
|
|
63
95
|
|
|
64
|
-
function sanitizeSnapshotFile(filePath) {
|
|
96
|
+
export function sanitizeSnapshotFile(filePath) {
|
|
65
97
|
const dump = fs.readFileSync(filePath, "utf8");
|
|
66
98
|
const sanitized = dump
|
|
67
99
|
.split("\n")
|
|
68
|
-
.filter((line) =>
|
|
100
|
+
.filter((line) => {
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
return (
|
|
103
|
+
trimmed !== "SET transaction_timeout = 0;" &&
|
|
104
|
+
!trimmed.startsWith("\\restrict ") &&
|
|
105
|
+
!trimmed.startsWith("\\unrestrict ")
|
|
106
|
+
);
|
|
107
|
+
})
|
|
69
108
|
.join("\n");
|
|
70
109
|
|
|
71
110
|
if (sanitized !== dump) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { sanitizeSnapshotFile } from "./template-steps.mjs";
|
|
6
|
+
|
|
7
|
+
const tempDirs = [];
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
while (tempDirs.length > 0) {
|
|
11
|
+
fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function makeTempDir(prefix) {
|
|
16
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
17
|
+
tempDirs.push(dir);
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("template snapshot sanitization", () => {
|
|
22
|
+
it("removes volatile pg_dump control lines", () => {
|
|
23
|
+
const dir = makeTempDir("testkit-template-snapshot-");
|
|
24
|
+
const filePath = path.join(dir, "schema.sql");
|
|
25
|
+
fs.writeFileSync(
|
|
26
|
+
filePath,
|
|
27
|
+
[
|
|
28
|
+
"SET statement_timeout = 0;",
|
|
29
|
+
"SET transaction_timeout = 0;",
|
|
30
|
+
"\\restrict abc123",
|
|
31
|
+
"CREATE TABLE public.widgets (id integer);",
|
|
32
|
+
"\\unrestrict abc123",
|
|
33
|
+
"",
|
|
34
|
+
].join("\n")
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
sanitizeSnapshotFile(filePath);
|
|
38
|
+
|
|
39
|
+
expect(fs.readFileSync(filePath, "utf8")).toBe(
|
|
40
|
+
["SET statement_timeout = 0;", "CREATE TABLE public.widgets (id integer);", ""].join("\n")
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
});
|
package/lib/runner/artifacts.mjs
CHANGED
|
@@ -9,11 +9,20 @@ import {
|
|
|
9
9
|
const TIMINGS_FILENAME = "timings.json";
|
|
10
10
|
const RESULT_ARTIFACTS_DIRNAME = "artifacts";
|
|
11
11
|
const RESULT_LOGS_DIRNAME = "logs";
|
|
12
|
+
const RESULT_SETUP_DIRNAME = "setup";
|
|
13
|
+
const LIVE_ARTIFACT_FILENAME = "live.json";
|
|
12
14
|
|
|
13
15
|
export function writeRunArtifact(productDir, artifact) {
|
|
14
16
|
const resultsDir = path.join(productDir, ".testkit", "results");
|
|
15
17
|
fs.mkdirSync(resultsDir, { recursive: true });
|
|
16
18
|
fs.writeFileSync(path.join(resultsDir, "latest.json"), JSON.stringify(artifact, null, 2));
|
|
19
|
+
fs.rmSync(path.join(resultsDir, LIVE_ARTIFACT_FILENAME), { force: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function writeLiveRunArtifact(productDir, artifact) {
|
|
23
|
+
const resultsDir = path.join(productDir, ".testkit", "results");
|
|
24
|
+
fs.mkdirSync(resultsDir, { recursive: true });
|
|
25
|
+
fs.writeFileSync(path.join(resultsDir, LIVE_ARTIFACT_FILENAME), JSON.stringify(artifact, null, 2));
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
export function writeStatusArtifact(productDir, artifact) {
|
|
@@ -32,6 +41,13 @@ export function resetResultArtifacts(productDir) {
|
|
|
32
41
|
recursive: true,
|
|
33
42
|
force: true,
|
|
34
43
|
});
|
|
44
|
+
fs.rmSync(path.join(productDir, ".testkit", "results", RESULT_SETUP_DIRNAME), {
|
|
45
|
+
recursive: true,
|
|
46
|
+
force: true,
|
|
47
|
+
});
|
|
48
|
+
fs.rmSync(path.join(productDir, ".testkit", "results", LIVE_ARTIFACT_FILENAME), {
|
|
49
|
+
force: true,
|
|
50
|
+
});
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
export function persistTaskArtifacts(productDir, task, emittedArtifacts) {
|
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
|
},
|
|
@@ -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,40 @@ 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
|
+
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
|
+
});
|
|
123
161
|
const executedPlans = servicePlans.filter((plan) => !plan.skipped);
|
|
124
|
-
let workerCount = 0;
|
|
125
|
-
let runtimeInstanceCount = 0;
|
|
126
|
-
let runtimeStats = [];
|
|
127
162
|
let exitCode = 0;
|
|
128
163
|
const lifecycle = createRunLifecycle(productDir);
|
|
129
164
|
lifecycle.markRunning();
|
|
@@ -131,6 +166,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
131
166
|
let results = [];
|
|
132
167
|
let finishedAt = Date.now();
|
|
133
168
|
let knownFailureIssueValidation = null;
|
|
169
|
+
writeLiveSnapshot();
|
|
134
170
|
|
|
135
171
|
try {
|
|
136
172
|
if (executedPlans.length > 0) {
|
|
@@ -149,6 +185,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
149
185
|
runtimeOptions: {
|
|
150
186
|
reporter,
|
|
151
187
|
logRegistry,
|
|
188
|
+
setupRegistry,
|
|
152
189
|
},
|
|
153
190
|
});
|
|
154
191
|
const timingUpdates = [];
|
|
@@ -164,8 +201,14 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
164
201
|
timingUpdates,
|
|
165
202
|
lifecycle,
|
|
166
203
|
claimNextTask,
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
},
|
|
169
212
|
reporter
|
|
170
213
|
)
|
|
171
214
|
)
|
|
@@ -177,9 +220,11 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
177
220
|
for (const tracker of trackers.values()) {
|
|
178
221
|
if (!tracker.skipped) addTrackerError(tracker, message);
|
|
179
222
|
}
|
|
223
|
+
writeLiveSnapshot();
|
|
180
224
|
}
|
|
181
225
|
}
|
|
182
226
|
runtimeStats = runtimeManager.getStats();
|
|
227
|
+
writeLiveSnapshot();
|
|
183
228
|
} finally {
|
|
184
229
|
await runtimeManager.cleanupAll();
|
|
185
230
|
}
|
|
@@ -208,6 +253,8 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
208
253
|
metadata,
|
|
209
254
|
summarizeDbBackend,
|
|
210
255
|
serviceLogs: logRegistry.listServiceLogs(),
|
|
256
|
+
setupLogs: logRegistry.listSetupLogs(),
|
|
257
|
+
setupOperations: setupRegistry.listOperations(),
|
|
211
258
|
});
|
|
212
259
|
const statusArtifact = opts.writeStatus
|
|
213
260
|
? buildStatusArtifact({
|
package/lib/runner/reporting.mjs
CHANGED
|
@@ -112,6 +112,9 @@ export function buildRunArtifact({
|
|
|
112
112
|
metadata,
|
|
113
113
|
summarizeDbBackend,
|
|
114
114
|
serviceLogs = [],
|
|
115
|
+
setupLogs = [],
|
|
116
|
+
setupOperations = [],
|
|
117
|
+
runStatus = null,
|
|
115
118
|
}) {
|
|
116
119
|
const executed = results.filter((result) => !result.skipped);
|
|
117
120
|
const effectivelySkippedServices = executed.filter(isEffectivelySkippedService);
|
|
@@ -128,7 +131,7 @@ export function buildRunArtifact({
|
|
|
128
131
|
const dbBackend = summarizeDbBackend(results);
|
|
129
132
|
|
|
130
133
|
return {
|
|
131
|
-
schemaVersion:
|
|
134
|
+
schemaVersion: 8,
|
|
132
135
|
source: "testkit",
|
|
133
136
|
generatedAt: new Date(finishedAt).toISOString(),
|
|
134
137
|
product: {
|
|
@@ -138,7 +141,7 @@ export function buildRunArtifact({
|
|
|
138
141
|
git: metadata.git,
|
|
139
142
|
host: metadata.host,
|
|
140
143
|
run: {
|
|
141
|
-
status: failedServices.length > 0 ? "failed" : "passed",
|
|
144
|
+
status: runStatus || (failedServices.length > 0 ? "failed" : "passed"),
|
|
142
145
|
startedAt: new Date(startedAt).toISOString(),
|
|
143
146
|
finishedAt: new Date(finishedAt).toISOString(),
|
|
144
147
|
durationMs: finishedAt - startedAt,
|
|
@@ -179,6 +182,10 @@ export function buildRunArtifact({
|
|
|
179
182
|
},
|
|
180
183
|
logs: {
|
|
181
184
|
services: serviceLogs,
|
|
185
|
+
setup: setupLogs,
|
|
186
|
+
},
|
|
187
|
+
setup: {
|
|
188
|
+
operations: setupOperations,
|
|
182
189
|
},
|
|
183
190
|
services: results.map((result) => ({
|
|
184
191
|
name: result.name,
|
|
@@ -203,6 +210,49 @@ export function buildRunArtifact({
|
|
|
203
210
|
};
|
|
204
211
|
}
|
|
205
212
|
|
|
213
|
+
export function buildLiveRunArtifact({
|
|
214
|
+
productDir,
|
|
215
|
+
results,
|
|
216
|
+
startedAt,
|
|
217
|
+
updatedAt,
|
|
218
|
+
execution,
|
|
219
|
+
workerCount,
|
|
220
|
+
runtimeInstanceCount,
|
|
221
|
+
runtimeStats,
|
|
222
|
+
typeValues,
|
|
223
|
+
suiteSelectors,
|
|
224
|
+
fileNames,
|
|
225
|
+
shard,
|
|
226
|
+
serviceFilter,
|
|
227
|
+
metadata,
|
|
228
|
+
summarizeDbBackend,
|
|
229
|
+
serviceLogs = [],
|
|
230
|
+
setupLogs = [],
|
|
231
|
+
setupOperations = [],
|
|
232
|
+
}) {
|
|
233
|
+
return buildRunArtifact({
|
|
234
|
+
productDir,
|
|
235
|
+
results,
|
|
236
|
+
startedAt,
|
|
237
|
+
finishedAt: updatedAt,
|
|
238
|
+
execution,
|
|
239
|
+
workerCount,
|
|
240
|
+
runtimeInstanceCount,
|
|
241
|
+
runtimeStats,
|
|
242
|
+
typeValues,
|
|
243
|
+
suiteSelectors,
|
|
244
|
+
fileNames,
|
|
245
|
+
shard,
|
|
246
|
+
serviceFilter,
|
|
247
|
+
metadata,
|
|
248
|
+
summarizeDbBackend,
|
|
249
|
+
serviceLogs,
|
|
250
|
+
setupLogs,
|
|
251
|
+
setupOperations,
|
|
252
|
+
runStatus: "running",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
206
256
|
function isEffectivelySkippedService(result) {
|
|
207
257
|
return (
|
|
208
258
|
!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", () => {
|
|
@@ -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(8);
|
|
82
82
|
expect(artifact.run).toMatchObject({
|
|
83
83
|
workers: 2,
|
|
84
84
|
fileTimeoutSeconds: 60,
|
|
@@ -109,7 +109,85 @@ describe("runner reporting", () => {
|
|
|
109
109
|
expect(artifact.services[0].totalTaskDurationMs).toBe(2400);
|
|
110
110
|
expect(artifact.logs).toEqual({
|
|
111
111
|
services: [],
|
|
112
|
+
setup: [],
|
|
112
113
|
});
|
|
114
|
+
expect(artifact.setup).toEqual({
|
|
115
|
+
operations: [],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("builds live run artifacts with running status and setup details", () => {
|
|
120
|
+
const artifact = buildLiveRunArtifact({
|
|
121
|
+
productDir: "/tmp/my-product",
|
|
122
|
+
results: [],
|
|
123
|
+
startedAt: 1_000,
|
|
124
|
+
updatedAt: 2_000,
|
|
125
|
+
execution: {
|
|
126
|
+
workers: 2,
|
|
127
|
+
fileTimeoutSeconds: 60,
|
|
128
|
+
},
|
|
129
|
+
workerCount: 1,
|
|
130
|
+
runtimeInstanceCount: 1,
|
|
131
|
+
runtimeStats: [],
|
|
132
|
+
typeValues: ["int"],
|
|
133
|
+
suiteSelectors: [],
|
|
134
|
+
fileNames: [],
|
|
135
|
+
shard: null,
|
|
136
|
+
serviceFilter: null,
|
|
137
|
+
metadata: {
|
|
138
|
+
git: {
|
|
139
|
+
branch: "main",
|
|
140
|
+
commitSha: "abc",
|
|
141
|
+
repoRoot: "/tmp",
|
|
142
|
+
},
|
|
143
|
+
host: {
|
|
144
|
+
hostname: "local",
|
|
145
|
+
username: "dev",
|
|
146
|
+
},
|
|
147
|
+
testkitVersion: "0.1.54",
|
|
148
|
+
},
|
|
149
|
+
summarizeDbBackend: () => "local",
|
|
150
|
+
serviceLogs: [],
|
|
151
|
+
setupLogs: [
|
|
152
|
+
{
|
|
153
|
+
serviceName: "api",
|
|
154
|
+
runtimeLabel: "api",
|
|
155
|
+
stage: "runtime:prepare",
|
|
156
|
+
path: ".testkit/results/setup/api__api__runtime-prepare.log",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
setupOperations: [
|
|
160
|
+
{
|
|
161
|
+
id: "setup-1",
|
|
162
|
+
serviceName: "api",
|
|
163
|
+
runtimeLabel: "api",
|
|
164
|
+
stage: "runtime:prepare",
|
|
165
|
+
kind: "runtime-prepare",
|
|
166
|
+
summary: "runtime prepare",
|
|
167
|
+
parentId: null,
|
|
168
|
+
status: "running",
|
|
169
|
+
startedAt: "1970-01-01T00:00:01.000Z",
|
|
170
|
+
finishedAt: null,
|
|
171
|
+
durationMs: null,
|
|
172
|
+
error: null,
|
|
173
|
+
logRef: {
|
|
174
|
+
path: ".testkit/results/setup/api__api__runtime-prepare.log",
|
|
175
|
+
stage: "runtime:prepare",
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(artifact.run.status).toBe("running");
|
|
182
|
+
expect(artifact.logs.setup).toEqual([
|
|
183
|
+
{
|
|
184
|
+
serviceName: "api",
|
|
185
|
+
runtimeLabel: "api",
|
|
186
|
+
stage: "runtime:prepare",
|
|
187
|
+
path: ".testkit/results/setup/api__api__runtime-prepare.log",
|
|
188
|
+
},
|
|
189
|
+
]);
|
|
190
|
+
expect(artifact.setup.operations).toHaveLength(1);
|
|
113
191
|
});
|
|
114
192
|
|
|
115
193
|
it("builds deterministic status artifacts", () => {
|
|
@@ -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
|
}
|