@elench/testkit 0.1.48 → 0.1.50
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 +14 -0
- package/lib/config/index.mjs +58 -3
- package/lib/database/index.mjs +105 -12
- package/lib/database/index.test.mjs +95 -0
- package/lib/database/template-steps.mjs +35 -193
- package/lib/runner/processes.mjs +16 -2
- package/lib/runner/processes.test.mjs +21 -0
- package/lib/runner/results.mjs +2 -1
- package/lib/runner/results.test.mjs +61 -0
- package/lib/runner/runtime-contexts.mjs +2 -0
- package/lib/runner/runtime-manager.mjs +34 -0
- package/lib/runner/runtime-manager.test.mjs +46 -0
- package/lib/runner/runtime-preparation.mjs +107 -0
- package/lib/runner/runtime-preparation.test.mjs +141 -0
- package/lib/runner/template-steps.mjs +191 -0
- package/lib/runner/template.mjs +41 -0
- package/lib/runner/template.test.mjs +64 -0
- package/lib/runner/worker-loop.mjs +21 -0
- package/lib/setup/index.d.ts +4 -0
- package/lib/setup/index.mjs +5 -5
- package/lib/setup/index.test.mjs +26 -0
- package/package.json +1 -1
package/lib/runner/template.mjs
CHANGED
|
@@ -113,12 +113,14 @@ export function resolveRuntimeConfig(
|
|
|
113
113
|
urlMappings
|
|
114
114
|
) {
|
|
115
115
|
const stateDir = resolveServiceStateDir(runtimeDir, config);
|
|
116
|
+
const prepareDir = resolveServicePrepareDir(runtimeDir, config);
|
|
116
117
|
const context = {
|
|
117
118
|
runtimeId,
|
|
118
119
|
runtimeLabel,
|
|
119
120
|
runtimeDir,
|
|
120
121
|
serviceName: config.name,
|
|
121
122
|
serviceStateDir: stateDir,
|
|
123
|
+
prepareDir,
|
|
122
124
|
portMap,
|
|
123
125
|
baseUrlByService,
|
|
124
126
|
readyUrlByService,
|
|
@@ -135,6 +137,11 @@ export function resolveRuntimeConfig(
|
|
|
135
137
|
}
|
|
136
138
|
: undefined;
|
|
137
139
|
|
|
140
|
+
const runtime = {
|
|
141
|
+
...config.testkit.runtime,
|
|
142
|
+
prepare: finalizeRuntimePrepare(config.testkit.runtime.prepare, context),
|
|
143
|
+
};
|
|
144
|
+
|
|
138
145
|
const local = config.testkit.local
|
|
139
146
|
? {
|
|
140
147
|
...config.testkit.local,
|
|
@@ -158,7 +165,9 @@ export function resolveRuntimeConfig(
|
|
|
158
165
|
testkit: {
|
|
159
166
|
...config.testkit,
|
|
160
167
|
database,
|
|
168
|
+
prepareDir,
|
|
161
169
|
templateContext: context,
|
|
170
|
+
runtime,
|
|
162
171
|
local,
|
|
163
172
|
},
|
|
164
173
|
};
|
|
@@ -197,6 +206,10 @@ export function resolveServiceStateDir(runtimeDir, config) {
|
|
|
197
206
|
return path.join(runtimeDir, "services", config.name);
|
|
198
207
|
}
|
|
199
208
|
|
|
209
|
+
export function resolveServicePrepareDir(runtimeDir, config) {
|
|
210
|
+
return path.join(resolveServiceStateDir(runtimeDir, config), "prepared");
|
|
211
|
+
}
|
|
212
|
+
|
|
200
213
|
export function buildExecutionEnv(config, extraEnv = {}, processEnv = process.env) {
|
|
201
214
|
return buildExecutionEnvWithContext(config, null, extraEnv, processEnv);
|
|
202
215
|
}
|
|
@@ -244,6 +257,7 @@ function buildTemplateContext(config, lease) {
|
|
|
244
257
|
runtimeLabel: config.runtimeLabel || baseContext.runtimeLabel || null,
|
|
245
258
|
serviceName: config.name,
|
|
246
259
|
serviceStateDir: config.stateDir || baseContext.serviceStateDir || null,
|
|
260
|
+
prepareDir: config.testkit?.prepareDir || baseContext.prepareDir || null,
|
|
247
261
|
leaseId: lease?.leaseId || null,
|
|
248
262
|
leaseDir: lease?.leaseDir || null,
|
|
249
263
|
};
|
|
@@ -280,6 +294,8 @@ export function resolveTemplateString(value, context) {
|
|
|
280
294
|
return context.serviceName;
|
|
281
295
|
case "stateDir":
|
|
282
296
|
return context.serviceStateDir;
|
|
297
|
+
case "prepareDir":
|
|
298
|
+
return context.prepareDir || "";
|
|
283
299
|
case "lease":
|
|
284
300
|
return context.leaseId ? String(context.leaseId) : "";
|
|
285
301
|
case "leaseDir":
|
|
@@ -332,6 +348,31 @@ function resolveEnvTemplates(values, templateContext) {
|
|
|
332
348
|
);
|
|
333
349
|
}
|
|
334
350
|
|
|
351
|
+
function finalizeRuntimePrepare(prepare, context) {
|
|
352
|
+
if (!prepare) {
|
|
353
|
+
return {
|
|
354
|
+
inputs: [],
|
|
355
|
+
steps: [],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const finalizeStep = (step) => ({
|
|
360
|
+
...step,
|
|
361
|
+
...(typeof step.cmd === "string" ? { cmd: finalizeString(step.cmd, context) } : {}),
|
|
362
|
+
...(typeof step.cwd === "string" ? { cwd: finalizeString(step.cwd, context) } : {}),
|
|
363
|
+
...(typeof step.path === "string" ? { path: finalizeString(step.path, context) } : {}),
|
|
364
|
+
...(typeof step.specifier === "string"
|
|
365
|
+
? { specifier: finalizeString(step.specifier, context) }
|
|
366
|
+
: {}),
|
|
367
|
+
inputs: (step.inputs || []).map((input) => finalizeString(input, context)),
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
inputs: (prepare.inputs || []).map((input) => finalizeString(input, context)),
|
|
372
|
+
steps: (prepare.steps || []).map(finalizeStep),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
335
376
|
function resolveDatabaseTemplateValue(token, serviceName, context) {
|
|
336
377
|
const stateDir = context.stateDirByService?.get(serviceName);
|
|
337
378
|
if (!stateDir) {
|
|
@@ -22,8 +22,18 @@ function makeRuntimeConfig(name, local, extras = {}) {
|
|
|
22
22
|
name,
|
|
23
23
|
stateDir: extras.stateDir,
|
|
24
24
|
runtimeId: extras.runtimeId,
|
|
25
|
+
productDir: extras.productDir || process.cwd(),
|
|
25
26
|
testkit: {
|
|
26
27
|
local,
|
|
28
|
+
runtime: extras.runtime || {
|
|
29
|
+
instances: 1,
|
|
30
|
+
maxConcurrentTasks: Infinity,
|
|
31
|
+
prepare: {
|
|
32
|
+
inputs: [],
|
|
33
|
+
steps: [],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
envFiles: extras.envFiles || [],
|
|
27
37
|
serviceEnv: extras.serviceEnv || {},
|
|
28
38
|
databaseFrom: extras.databaseFrom,
|
|
29
39
|
database: extras.database,
|
|
@@ -88,6 +98,9 @@ describe("runner-template", () => {
|
|
|
88
98
|
expect(resolveTemplateString("{runtime}:{service}:{lease}", context)).toBe(
|
|
89
99
|
"runtime-2:frontend:lease-1"
|
|
90
100
|
);
|
|
101
|
+
expect(resolveTemplateString("{prepareDir}", { ...context, prepareDir: "/tmp/prepare-1" })).toBe(
|
|
102
|
+
"/tmp/prepare-1"
|
|
103
|
+
);
|
|
91
104
|
expect(resolveTemplateString("{baseUrl:api}", context)).toBe("http://127.0.0.1:3100");
|
|
92
105
|
expect(resolveTemplateString("{dbHost:api}:{dbPort:api}/{dbName:api}", context)).toBe(
|
|
93
106
|
"127.0.0.1:55432/runtime_db"
|
|
@@ -140,6 +153,7 @@ describe("runner-template", () => {
|
|
|
140
153
|
expect(resolved[0].testkit.local.port).toBe(3100);
|
|
141
154
|
expect(resolved[0].runtimeLabel).toBe("api__frontend/runtime-2");
|
|
142
155
|
expect(resolveServiceStateDir(runtimeDir, api)).toBe(`${runtimeDir}/services/api`);
|
|
156
|
+
expect(resolved[0].testkit.prepareDir).toBe(`${runtimeDir}/services/api/prepared`);
|
|
143
157
|
|
|
144
158
|
fs.mkdirSync(path.join(runtimeDir, "services", "api"), { recursive: true });
|
|
145
159
|
fs.writeFileSync(
|
|
@@ -197,6 +211,56 @@ describe("runner-template", () => {
|
|
|
197
211
|
});
|
|
198
212
|
});
|
|
199
213
|
|
|
214
|
+
it("finalizes runtime.prepare templates with prepareDir", () => {
|
|
215
|
+
const runtimeDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-runtime-prepare-"));
|
|
216
|
+
const api = makeRuntimeConfig(
|
|
217
|
+
"api",
|
|
218
|
+
{
|
|
219
|
+
cwd: ".",
|
|
220
|
+
start: "npm run api",
|
|
221
|
+
port: 3000,
|
|
222
|
+
baseUrl: "http://127.0.0.1:{port}",
|
|
223
|
+
readyUrl: "http://127.0.0.1:{port}/health",
|
|
224
|
+
env: {},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
runtime: {
|
|
228
|
+
instances: 1,
|
|
229
|
+
maxConcurrentTasks: 2,
|
|
230
|
+
prepare: {
|
|
231
|
+
inputs: ["src/{service}.ts"],
|
|
232
|
+
steps: [
|
|
233
|
+
{
|
|
234
|
+
kind: "command",
|
|
235
|
+
cmd: "node scripts/prepare.mjs {prepareDir}",
|
|
236
|
+
cwd: ".",
|
|
237
|
+
inputs: ["src/{service}.ts"],
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const [resolved] = resolveRuntimeInstanceConfigs([api], "runtime-1", runtimeDir, {
|
|
246
|
+
graphDirName: "api",
|
|
247
|
+
portNamespaceIndex: 0,
|
|
248
|
+
portNamespaceStride: 1,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(resolved.testkit.runtime.prepare).toEqual({
|
|
252
|
+
inputs: ["src/api.ts"],
|
|
253
|
+
steps: [
|
|
254
|
+
{
|
|
255
|
+
kind: "command",
|
|
256
|
+
cmd: `node scripts/prepare.mjs ${runtimeDir}/services/api/prepared`,
|
|
257
|
+
cwd: ".",
|
|
258
|
+
inputs: ["src/api.ts"],
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
200
264
|
it("parses runtime sockets", () => {
|
|
201
265
|
expect(numericPortFromUrl("http://localhost:3000")).toBe(3000);
|
|
202
266
|
expect(socketFromUrl("http://localhost:3000")).toEqual({
|
|
@@ -36,6 +36,27 @@ export async function runWorker(
|
|
|
36
36
|
runtimeManager.canAcquire(candidate)
|
|
37
37
|
);
|
|
38
38
|
if (!task) {
|
|
39
|
+
const blockedTasks = runtimeManager.drainFailedTasks(queue);
|
|
40
|
+
if (blockedTasks.length > 0) {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const recordedGraphs = new Set();
|
|
43
|
+
for (const blocked of blockedTasks) {
|
|
44
|
+
recordTaskOutcome(trackers, blocked.task, {
|
|
45
|
+
failed: false,
|
|
46
|
+
status: "not_run",
|
|
47
|
+
reason: blocked.reason,
|
|
48
|
+
durationMs: 0,
|
|
49
|
+
startedAt: now,
|
|
50
|
+
finishedAt: now,
|
|
51
|
+
error: null,
|
|
52
|
+
});
|
|
53
|
+
if (!recordedGraphs.has(blocked.graph.key)) {
|
|
54
|
+
recordGraphError(trackers, blocked.graph, blocked.message, now);
|
|
55
|
+
recordedGraphs.add(blocked.graph.key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
39
60
|
if (queue.length === 0) break;
|
|
40
61
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
41
62
|
continue;
|
package/lib/setup/index.d.ts
CHANGED
|
@@ -58,6 +58,10 @@ export interface SkipConfig {
|
|
|
58
58
|
export interface RuntimeConfig {
|
|
59
59
|
instances?: number;
|
|
60
60
|
maxConcurrentTasks?: number;
|
|
61
|
+
prepare?: {
|
|
62
|
+
inputs?: string[];
|
|
63
|
+
steps?: TemplateLifecycleStepConfig[];
|
|
64
|
+
};
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
export interface SuiteRequirementRule {
|
package/lib/setup/index.mjs
CHANGED
|
@@ -85,7 +85,7 @@ export function nextService(options = {}) {
|
|
|
85
85
|
...service(options),
|
|
86
86
|
local: {
|
|
87
87
|
cwd,
|
|
88
|
-
start: options.start || "
|
|
88
|
+
start: options.start || "./node_modules/.bin/next dev -p {port}",
|
|
89
89
|
port,
|
|
90
90
|
baseUrl,
|
|
91
91
|
readyUrl: options.readyUrl || baseUrl,
|
|
@@ -105,7 +105,7 @@ export function tsxService(options = {}) {
|
|
|
105
105
|
...service(options),
|
|
106
106
|
local: {
|
|
107
107
|
cwd,
|
|
108
|
-
start: options.start ||
|
|
108
|
+
start: options.start || `./node_modules/.bin/tsx watch ${entry}`,
|
|
109
109
|
port,
|
|
110
110
|
baseUrl,
|
|
111
111
|
readyUrl: options.readyUrl || `${baseUrl}${options.readyPath || "/health"}`,
|
|
@@ -196,12 +196,12 @@ export {
|
|
|
196
196
|
|
|
197
197
|
function defaultGoStartCommand(options) {
|
|
198
198
|
if (options.command) {
|
|
199
|
-
return `
|
|
199
|
+
return `go run ${options.command}`;
|
|
200
200
|
}
|
|
201
201
|
if (options.entrypoint) {
|
|
202
|
-
return `
|
|
202
|
+
return `go run ${options.entrypoint}`;
|
|
203
203
|
}
|
|
204
|
-
return "
|
|
204
|
+
return "go run ./cmd/server";
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
function requiredNumber(value, label) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { goService, nextService, tsxService } from "./index.mjs";
|
|
3
|
+
|
|
4
|
+
describe("setup helpers", () => {
|
|
5
|
+
it("emits plain next start commands without an exec prefix", () => {
|
|
6
|
+
const config = nextService({ port: 3000 });
|
|
7
|
+
|
|
8
|
+
expect(config.local.start).toBe("./node_modules/.bin/next dev -p {port}");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("emits plain tsx start commands without an exec prefix", () => {
|
|
12
|
+
const config = tsxService({ port: 3000, entry: "src/server.ts" });
|
|
13
|
+
|
|
14
|
+
expect(config.local.start).toBe("./node_modules/.bin/tsx watch src/server.ts");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("emits plain go start commands without an exec prefix", () => {
|
|
18
|
+
expect(goService({ port: 3000 }).local.start).toBe("go run ./cmd/server");
|
|
19
|
+
expect(goService({ port: 3000, entrypoint: "./cmd/api" }).local.start).toBe(
|
|
20
|
+
"go run ./cmd/api"
|
|
21
|
+
);
|
|
22
|
+
expect(goService({ port: 3000, command: "./cmd/worker" }).local.start).toBe(
|
|
23
|
+
"go run ./cmd/worker"
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
});
|