@elench/testkit 0.1.16 → 0.1.18
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 +44 -19
- package/bin/testkit.mjs +1 -1
- package/lib/cli/args.mjs +57 -0
- package/lib/cli/args.test.mjs +62 -0
- package/lib/cli/index.mjs +88 -0
- package/lib/config/index.mjs +294 -0
- package/lib/config/index.test.mjs +12 -0
- package/lib/config/model.mjs +422 -0
- package/lib/config/model.test.mjs +193 -0
- package/lib/database/fingerprint.mjs +61 -0
- package/lib/database/fingerprint.test.mjs +93 -0
- package/lib/{database.mjs → database/index.mjs} +45 -160
- package/lib/database/naming.mjs +47 -0
- package/lib/database/naming.test.mjs +39 -0
- package/lib/database/state.mjs +52 -0
- package/lib/database/state.test.mjs +66 -0
- package/lib/reporters/playwright.mjs +125 -0
- package/lib/reporters/playwright.test.mjs +73 -0
- package/lib/runner/index.mjs +1221 -0
- package/lib/runner/metadata.mjs +55 -0
- package/lib/runner/metadata.test.mjs +52 -0
- package/lib/runner/planning.mjs +270 -0
- package/lib/runner/planning.test.mjs +127 -0
- package/lib/runner/results.mjs +285 -0
- package/lib/runner/results.test.mjs +144 -0
- package/lib/runner/state.mjs +71 -0
- package/lib/runner/state.test.mjs +64 -0
- package/lib/runner/template.mjs +320 -0
- package/lib/runner/template.test.mjs +150 -0
- package/lib/telemetry/index.mjs +43 -0
- package/lib/timing/index.mjs +73 -0
- package/lib/timing/index.test.mjs +64 -0
- package/package.json +11 -3
- package/infra/neon-down.sh +0 -18
- package/infra/neon-up.sh +0 -124
- package/lib/cli.mjs +0 -132
- package/lib/config.mjs +0 -666
- package/lib/exec.mjs +0 -20
- package/lib/runner.mjs +0 -1165
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
export function buildServiceTrackers(servicePlans, startedAt) {
|
|
4
|
+
const trackers = new Map();
|
|
5
|
+
|
|
6
|
+
for (const plan of servicePlans) {
|
|
7
|
+
if (plan.skipped) {
|
|
8
|
+
trackers.set(plan.config.name, {
|
|
9
|
+
name: plan.config.name,
|
|
10
|
+
dbBackend: plan.config.testkit.database?.selectedBackend || null,
|
|
11
|
+
skipped: true,
|
|
12
|
+
suiteCount: 0,
|
|
13
|
+
suites: [],
|
|
14
|
+
suitesByKey: new Map(),
|
|
15
|
+
errors: [],
|
|
16
|
+
errorSet: new Set(),
|
|
17
|
+
startedAt,
|
|
18
|
+
firstTaskAt: null,
|
|
19
|
+
lastTaskAt: null,
|
|
20
|
+
});
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const suites = plan.suites.map((suite) => ({
|
|
25
|
+
key: `${suite.type}:${suite.name}`,
|
|
26
|
+
name: suite.name,
|
|
27
|
+
type: suite.type,
|
|
28
|
+
framework: suite.framework,
|
|
29
|
+
orderIndex: suite.orderIndex,
|
|
30
|
+
fileCount: suite.files.length,
|
|
31
|
+
completedFileCount: 0,
|
|
32
|
+
failedFiles: [],
|
|
33
|
+
failedFileSet: new Set(),
|
|
34
|
+
fileResults: [],
|
|
35
|
+
fileResultsByPath: new Map(),
|
|
36
|
+
durationMs: 0,
|
|
37
|
+
error: null,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
trackers.set(plan.config.name, {
|
|
41
|
+
name: plan.config.name,
|
|
42
|
+
dbBackend: plan.config.testkit.database?.selectedBackend || null,
|
|
43
|
+
skipped: false,
|
|
44
|
+
suiteCount: suites.length,
|
|
45
|
+
suites,
|
|
46
|
+
suitesByKey: new Map(suites.map((suite) => [suite.key, suite])),
|
|
47
|
+
errors: [],
|
|
48
|
+
errorSet: new Set(),
|
|
49
|
+
startedAt,
|
|
50
|
+
firstTaskAt: null,
|
|
51
|
+
lastTaskAt: null,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return trackers;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function recordTaskOutcome(trackers, task, outcome, finishedAt = Date.now()) {
|
|
59
|
+
const tracker = trackers.get(task.serviceName);
|
|
60
|
+
if (!tracker || tracker.skipped) return;
|
|
61
|
+
|
|
62
|
+
if (!tracker.firstTaskAt) tracker.firstTaskAt = finishedAt;
|
|
63
|
+
tracker.lastTaskAt = finishedAt;
|
|
64
|
+
|
|
65
|
+
const suite = tracker.suitesByKey.get(task.suiteKey);
|
|
66
|
+
if (!suite) return;
|
|
67
|
+
|
|
68
|
+
suite.completedFileCount += 1;
|
|
69
|
+
suite.durationMs += outcome.durationMs;
|
|
70
|
+
const normalizedPath = normalizePathSeparators(task.file);
|
|
71
|
+
const existingFileResult = suite.fileResultsByPath.get(normalizedPath);
|
|
72
|
+
if (existingFileResult) {
|
|
73
|
+
existingFileResult.failed = outcome.failed;
|
|
74
|
+
existingFileResult.durationMs = outcome.durationMs;
|
|
75
|
+
existingFileResult.error = outcome.error;
|
|
76
|
+
} else {
|
|
77
|
+
const fileResult = {
|
|
78
|
+
path: normalizedPath,
|
|
79
|
+
failed: outcome.failed,
|
|
80
|
+
durationMs: outcome.durationMs,
|
|
81
|
+
error: outcome.error,
|
|
82
|
+
};
|
|
83
|
+
suite.fileResultsByPath.set(normalizedPath, fileResult);
|
|
84
|
+
suite.fileResults.push(fileResult);
|
|
85
|
+
}
|
|
86
|
+
if (outcome.failed && !suite.failedFileSet.has(task.file)) {
|
|
87
|
+
suite.failedFileSet.add(task.file);
|
|
88
|
+
suite.failedFiles.push(task.file);
|
|
89
|
+
}
|
|
90
|
+
if (outcome.error && !suite.error) {
|
|
91
|
+
suite.error = outcome.error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function recordGraphError(trackers, graph, message, now = Date.now()) {
|
|
96
|
+
const targetNames = graph?.assignedTargets || [];
|
|
97
|
+
for (const targetName of targetNames) {
|
|
98
|
+
const tracker = trackers.get(targetName);
|
|
99
|
+
if (tracker && !tracker.skipped) {
|
|
100
|
+
addTrackerError(tracker, message);
|
|
101
|
+
tracker.lastTaskAt = now;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function addTrackerError(tracker, message) {
|
|
107
|
+
if (tracker.errorSet.has(message)) return;
|
|
108
|
+
tracker.errorSet.add(message);
|
|
109
|
+
tracker.errors.push(message);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function finalizeServiceResult(tracker, startedAt, finishedAt) {
|
|
113
|
+
if (!tracker || tracker.skipped) {
|
|
114
|
+
return {
|
|
115
|
+
name: tracker?.name || "unknown",
|
|
116
|
+
failed: false,
|
|
117
|
+
skipped: true,
|
|
118
|
+
suiteCount: 0,
|
|
119
|
+
completedSuiteCount: 0,
|
|
120
|
+
failedSuiteCount: 0,
|
|
121
|
+
durationMs: 0,
|
|
122
|
+
suites: [],
|
|
123
|
+
errors: [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const suites = [...tracker.suites].sort(
|
|
128
|
+
(a, b) => a.orderIndex - b.orderIndex || a.name.localeCompare(b.name)
|
|
129
|
+
);
|
|
130
|
+
const completedSuiteCount = suites.filter(
|
|
131
|
+
(suite) => suite.completedFileCount === suite.fileCount
|
|
132
|
+
).length;
|
|
133
|
+
const failedSuiteCount = suites.filter((suite) => suite.failedFiles.length > 0).length;
|
|
134
|
+
const accumulatedDurationMs = suites.reduce((sum, suite) => sum + suite.durationMs, 0);
|
|
135
|
+
const durationMs =
|
|
136
|
+
tracker.firstTaskAt && tracker.lastTaskAt
|
|
137
|
+
? Math.max(tracker.lastTaskAt - tracker.firstTaskAt, accumulatedDurationMs)
|
|
138
|
+
: Math.max(finishedAt - startedAt, accumulatedDurationMs);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
name: tracker.name,
|
|
142
|
+
dbBackend: tracker.dbBackend,
|
|
143
|
+
failed:
|
|
144
|
+
failedSuiteCount > 0 ||
|
|
145
|
+
tracker.errors.length > 0 ||
|
|
146
|
+
completedSuiteCount < tracker.suiteCount,
|
|
147
|
+
skipped: false,
|
|
148
|
+
suiteCount: tracker.suiteCount,
|
|
149
|
+
completedSuiteCount,
|
|
150
|
+
failedSuiteCount,
|
|
151
|
+
durationMs,
|
|
152
|
+
suites: suites.map((suite) => ({
|
|
153
|
+
name: suite.name,
|
|
154
|
+
type: suite.type,
|
|
155
|
+
framework: suite.framework,
|
|
156
|
+
failed: suite.failedFiles.length > 0,
|
|
157
|
+
fileCount: suite.fileCount,
|
|
158
|
+
failedFiles: suite.failedFiles,
|
|
159
|
+
durationMs: suite.durationMs,
|
|
160
|
+
error: suite.error,
|
|
161
|
+
files: suite.fileResults
|
|
162
|
+
.slice()
|
|
163
|
+
.sort((a, b) => a.path.localeCompare(b.path))
|
|
164
|
+
.map((file) => ({
|
|
165
|
+
path: file.path,
|
|
166
|
+
failed: file.failed,
|
|
167
|
+
durationMs: file.durationMs,
|
|
168
|
+
error: file.error,
|
|
169
|
+
})),
|
|
170
|
+
})),
|
|
171
|
+
errors: tracker.errors,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function buildRunArtifact({
|
|
176
|
+
productDir,
|
|
177
|
+
results,
|
|
178
|
+
startedAt,
|
|
179
|
+
finishedAt,
|
|
180
|
+
requestedJobs,
|
|
181
|
+
workerCount,
|
|
182
|
+
suiteType,
|
|
183
|
+
suiteNames,
|
|
184
|
+
framework,
|
|
185
|
+
shard,
|
|
186
|
+
serviceFilter,
|
|
187
|
+
metadata,
|
|
188
|
+
}) {
|
|
189
|
+
const executed = results.filter((result) => !result.skipped);
|
|
190
|
+
const failedServices = executed.filter((result) => result.failed);
|
|
191
|
+
const totalSuites = executed.reduce((sum, result) => sum + result.suiteCount, 0);
|
|
192
|
+
const completedSuites = executed.reduce((sum, result) => sum + result.completedSuiteCount, 0);
|
|
193
|
+
const failedSuites = executed.reduce((sum, result) => sum + result.failedSuiteCount, 0);
|
|
194
|
+
const dbBackend = summarizeDbBackend(results);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
schemaVersion: 1,
|
|
198
|
+
source: "testkit",
|
|
199
|
+
generatedAt: new Date(finishedAt).toISOString(),
|
|
200
|
+
product: {
|
|
201
|
+
name: path.basename(productDir),
|
|
202
|
+
directory: productDir,
|
|
203
|
+
},
|
|
204
|
+
git: metadata.git,
|
|
205
|
+
host: metadata.host,
|
|
206
|
+
run: {
|
|
207
|
+
status: failedServices.length > 0 ? "failed" : "passed",
|
|
208
|
+
startedAt: new Date(startedAt).toISOString(),
|
|
209
|
+
finishedAt: new Date(finishedAt).toISOString(),
|
|
210
|
+
durationMs: finishedAt - startedAt,
|
|
211
|
+
requestedJobs,
|
|
212
|
+
workerCount,
|
|
213
|
+
dbBackend,
|
|
214
|
+
suiteType,
|
|
215
|
+
suiteNames,
|
|
216
|
+
framework,
|
|
217
|
+
shard,
|
|
218
|
+
serviceFilter,
|
|
219
|
+
testkitVersion: metadata.testkitVersion,
|
|
220
|
+
},
|
|
221
|
+
summary: {
|
|
222
|
+
services: {
|
|
223
|
+
total: executed.length,
|
|
224
|
+
passed: executed.length - failedServices.length,
|
|
225
|
+
failed: failedServices.length,
|
|
226
|
+
},
|
|
227
|
+
suites: {
|
|
228
|
+
total: totalSuites,
|
|
229
|
+
completed: completedSuites,
|
|
230
|
+
passed: completedSuites - failedSuites,
|
|
231
|
+
failed: failedSuites,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
services: results.map((result) => ({
|
|
235
|
+
name: result.name,
|
|
236
|
+
failed: result.failed,
|
|
237
|
+
skipped: result.skipped,
|
|
238
|
+
suiteCount: result.suiteCount,
|
|
239
|
+
completedSuiteCount: result.completedSuiteCount,
|
|
240
|
+
failedSuiteCount: result.failedSuiteCount,
|
|
241
|
+
durationMs: result.durationMs,
|
|
242
|
+
dbBackend: result.dbBackend,
|
|
243
|
+
suites: result.suites,
|
|
244
|
+
errors: result.errors,
|
|
245
|
+
})),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function summarizeDbBackend(results) {
|
|
250
|
+
const values = [...new Set(results.map((result) => result.dbBackend).filter(Boolean))];
|
|
251
|
+
if (values.length === 0) return null;
|
|
252
|
+
if (values.length === 1) return values[0];
|
|
253
|
+
return "mixed";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function formatDuration(durationMs) {
|
|
257
|
+
const totalSeconds = Math.max(0, Math.round(durationMs / 1000));
|
|
258
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
259
|
+
const seconds = totalSeconds % 60;
|
|
260
|
+
if (minutes === 0) return `${seconds}s`;
|
|
261
|
+
return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function formatServiceSummary(result) {
|
|
265
|
+
const passedSuites = result.completedSuiteCount - result.failedSuiteCount;
|
|
266
|
+
const notRun = result.suiteCount - result.completedSuiteCount;
|
|
267
|
+
let detail = `${passedSuites}/${result.suiteCount} suites passed`;
|
|
268
|
+
if (notRun > 0) {
|
|
269
|
+
detail += `, ${notRun} not run`;
|
|
270
|
+
}
|
|
271
|
+
return detail;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function formatError(error) {
|
|
275
|
+
if (error instanceof Error) return error.message;
|
|
276
|
+
return String(error);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function longestServiceName(results) {
|
|
280
|
+
return results.reduce((max, result) => Math.max(max, result.name.length), 4);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function normalizePathSeparators(filePath) {
|
|
284
|
+
return filePath.split(path.sep).join("/");
|
|
285
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
addTrackerError,
|
|
4
|
+
buildRunArtifact,
|
|
5
|
+
buildServiceTrackers,
|
|
6
|
+
finalizeServiceResult,
|
|
7
|
+
formatDuration,
|
|
8
|
+
formatError,
|
|
9
|
+
formatServiceSummary,
|
|
10
|
+
recordGraphError,
|
|
11
|
+
recordTaskOutcome,
|
|
12
|
+
summarizeDbBackend,
|
|
13
|
+
} from "./results.mjs";
|
|
14
|
+
|
|
15
|
+
describe("runner-results", () => {
|
|
16
|
+
it("tracks task outcomes and graph errors", () => {
|
|
17
|
+
const trackers = buildServiceTrackers(
|
|
18
|
+
[
|
|
19
|
+
{
|
|
20
|
+
skipped: false,
|
|
21
|
+
config: {
|
|
22
|
+
name: "api",
|
|
23
|
+
testkit: {
|
|
24
|
+
database: {
|
|
25
|
+
selectedBackend: "local",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
suites: [
|
|
30
|
+
{
|
|
31
|
+
name: "health",
|
|
32
|
+
type: "integration",
|
|
33
|
+
framework: "k6",
|
|
34
|
+
files: ["tests/health.js"],
|
|
35
|
+
orderIndex: 0,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
1000
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
recordTaskOutcome(
|
|
44
|
+
trackers,
|
|
45
|
+
{
|
|
46
|
+
serviceName: "api",
|
|
47
|
+
suiteKey: "integration:health",
|
|
48
|
+
file: "tests/health.js",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
failed: true,
|
|
52
|
+
durationMs: 250,
|
|
53
|
+
error: "boom",
|
|
54
|
+
},
|
|
55
|
+
1250
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const tracker = trackers.get("api");
|
|
59
|
+
addTrackerError(tracker, "worker failed");
|
|
60
|
+
addTrackerError(tracker, "worker failed");
|
|
61
|
+
recordGraphError(trackers, { assignedTargets: ["api"] }, "graph failed", 1300);
|
|
62
|
+
|
|
63
|
+
const result = finalizeServiceResult(tracker, 1000, 1500);
|
|
64
|
+
expect(result.failed).toBe(true);
|
|
65
|
+
expect(result.failedSuiteCount).toBe(1);
|
|
66
|
+
expect(result.errors).toEqual(["worker failed", "graph failed"]);
|
|
67
|
+
expect(result.suites[0].files).toEqual([
|
|
68
|
+
{
|
|
69
|
+
path: "tests/health.js",
|
|
70
|
+
failed: true,
|
|
71
|
+
durationMs: 250,
|
|
72
|
+
error: "boom",
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("builds run artifacts and formatting helpers", () => {
|
|
78
|
+
const results = [
|
|
79
|
+
{
|
|
80
|
+
name: "api",
|
|
81
|
+
failed: false,
|
|
82
|
+
skipped: false,
|
|
83
|
+
suiteCount: 1,
|
|
84
|
+
completedSuiteCount: 1,
|
|
85
|
+
failedSuiteCount: 0,
|
|
86
|
+
durationMs: 1200,
|
|
87
|
+
dbBackend: "local",
|
|
88
|
+
suites: [],
|
|
89
|
+
errors: [],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "frontend",
|
|
93
|
+
failed: false,
|
|
94
|
+
skipped: true,
|
|
95
|
+
suiteCount: 0,
|
|
96
|
+
completedSuiteCount: 0,
|
|
97
|
+
failedSuiteCount: 0,
|
|
98
|
+
durationMs: 0,
|
|
99
|
+
dbBackend: null,
|
|
100
|
+
suites: [],
|
|
101
|
+
errors: [],
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const artifact = buildRunArtifact({
|
|
106
|
+
productDir: "/tmp/my-product",
|
|
107
|
+
results,
|
|
108
|
+
startedAt: 1000,
|
|
109
|
+
finishedAt: 4000,
|
|
110
|
+
requestedJobs: 2,
|
|
111
|
+
workerCount: 1,
|
|
112
|
+
suiteType: "all",
|
|
113
|
+
suiteNames: [],
|
|
114
|
+
framework: "all",
|
|
115
|
+
shard: null,
|
|
116
|
+
serviceFilter: null,
|
|
117
|
+
metadata: {
|
|
118
|
+
git: {
|
|
119
|
+
branch: "main",
|
|
120
|
+
commitSha: "abc",
|
|
121
|
+
repoRoot: "/tmp",
|
|
122
|
+
},
|
|
123
|
+
host: {
|
|
124
|
+
hostname: "local",
|
|
125
|
+
username: "dev",
|
|
126
|
+
},
|
|
127
|
+
testkitVersion: "0.1.17",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(artifact.product.name).toBe("my-product");
|
|
132
|
+
expect(artifact.summary.services.total).toBe(1);
|
|
133
|
+
expect(summarizeDbBackend(results)).toBe("local");
|
|
134
|
+
expect(formatDuration(65_000)).toBe("1m 05s");
|
|
135
|
+
expect(
|
|
136
|
+
formatServiceSummary({
|
|
137
|
+
completedSuiteCount: 2,
|
|
138
|
+
failedSuiteCount: 1,
|
|
139
|
+
suiteCount: 3,
|
|
140
|
+
})
|
|
141
|
+
).toBe("1/3 suites passed, 1 not run");
|
|
142
|
+
expect(formatError(new Error("boom"))).toBe("boom");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const GRAPH_METADATA = "graph.json";
|
|
5
|
+
|
|
6
|
+
export function findRuntimeStateDirs(rootDir, isDatabaseStateDir) {
|
|
7
|
+
const found = [];
|
|
8
|
+
|
|
9
|
+
const visit = (dir) => {
|
|
10
|
+
if (!fs.existsSync(dir)) return;
|
|
11
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
12
|
+
if (isDatabaseStateDir(dir)) {
|
|
13
|
+
found.push(dir);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
visit(path.join(dir, entry.name));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
visit(rootDir);
|
|
24
|
+
return found.sort((a, b) => b.length - a.length);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function findGraphDirsForService(productDir, serviceName) {
|
|
28
|
+
const graphsRoot = path.join(productDir, ".testkit", "_graphs");
|
|
29
|
+
if (!fs.existsSync(graphsRoot)) return [];
|
|
30
|
+
|
|
31
|
+
const matches = [];
|
|
32
|
+
for (const entry of fs.readdirSync(graphsRoot, { withFileTypes: true })) {
|
|
33
|
+
if (!entry.isDirectory()) continue;
|
|
34
|
+
const graphDir = path.join(graphsRoot, entry.name);
|
|
35
|
+
const metadata = readGraphMetadata(graphDir);
|
|
36
|
+
if (!metadata) continue;
|
|
37
|
+
if ((metadata.runtimeServices || []).includes(serviceName)) {
|
|
38
|
+
matches.push(graphDir);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return matches.sort();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function writeGraphMetadata(graphDir, graph) {
|
|
46
|
+
fs.mkdirSync(graphDir, { recursive: true });
|
|
47
|
+
const metadata = {
|
|
48
|
+
runtimeServices: graph.runtimeNames,
|
|
49
|
+
assignedTargets: [...graph.assignedTargets].sort(),
|
|
50
|
+
rootService: graph.rootConfig.name,
|
|
51
|
+
};
|
|
52
|
+
fs.writeFileSync(
|
|
53
|
+
path.join(graphDir, GRAPH_METADATA),
|
|
54
|
+
JSON.stringify(metadata, null, 2)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function readGraphMetadata(graphDir) {
|
|
59
|
+
const metadataPath = path.join(graphDir, GRAPH_METADATA);
|
|
60
|
+
if (!fs.existsSync(metadataPath)) return null;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizePathSeparators(filePath) {
|
|
70
|
+
return filePath.split(path.sep).join("/");
|
|
71
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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 {
|
|
6
|
+
findGraphDirsForService,
|
|
7
|
+
findRuntimeStateDirs,
|
|
8
|
+
normalizePathSeparators,
|
|
9
|
+
readGraphMetadata,
|
|
10
|
+
writeGraphMetadata,
|
|
11
|
+
} from "./state.mjs";
|
|
12
|
+
|
|
13
|
+
const tempDirs = [];
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
for (const dir of tempDirs.splice(0)) {
|
|
17
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function mkTempDir() {
|
|
22
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-runner-state-"));
|
|
23
|
+
tempDirs.push(dir);
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("runner-state", () => {
|
|
28
|
+
it("finds runtime state directories", () => {
|
|
29
|
+
const rootDir = mkTempDir();
|
|
30
|
+
const deep = path.join(rootDir, "workers", "w1", "deps", "api");
|
|
31
|
+
fs.mkdirSync(deep, { recursive: true });
|
|
32
|
+
fs.writeFileSync(path.join(rootDir, "workers", "database_backend"), "local");
|
|
33
|
+
fs.writeFileSync(path.join(deep, "database_backend"), "local");
|
|
34
|
+
|
|
35
|
+
const found = findRuntimeStateDirs(
|
|
36
|
+
rootDir,
|
|
37
|
+
(dir) => fs.existsSync(path.join(dir, "database_backend"))
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(found).toEqual([
|
|
41
|
+
deep,
|
|
42
|
+
path.join(rootDir, "workers"),
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("writes, reads, and finds graph metadata", () => {
|
|
47
|
+
const productDir = mkTempDir();
|
|
48
|
+
const graphDir = path.join(productDir, ".testkit", "_graphs", "api__frontend");
|
|
49
|
+
writeGraphMetadata(graphDir, {
|
|
50
|
+
runtimeNames: ["api", "frontend"],
|
|
51
|
+
assignedTargets: ["frontend", "api"],
|
|
52
|
+
rootConfig: { name: "api" },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(readGraphMetadata(graphDir)).toEqual({
|
|
56
|
+
runtimeServices: ["api", "frontend"],
|
|
57
|
+
assignedTargets: ["api", "frontend"],
|
|
58
|
+
rootService: "api",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(findGraphDirsForService(productDir, "frontend")).toEqual([graphDir]);
|
|
62
|
+
expect(normalizePathSeparators(`a${path.sep}b${path.sep}c`)).toBe("a/b/c");
|
|
63
|
+
});
|
|
64
|
+
});
|