@elench/testkit 0.1.40 → 0.1.41
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 +25 -13
- package/lib/cli/args.mjs +0 -4
- package/lib/cli/args.test.mjs +0 -5
- package/lib/cli/index.mjs +0 -9
- package/lib/config/index.mjs +67 -24
- package/lib/database/index.mjs +19 -7
- package/lib/database/naming.mjs +2 -2
- package/lib/database/naming.test.mjs +2 -2
- package/lib/runner/default-runtime-runner.mjs +31 -53
- package/lib/runner/execution-config.mjs +14 -70
- package/lib/runner/execution-config.test.mjs +22 -74
- package/lib/runner/formatting.mjs +0 -15
- package/lib/runner/formatting.test.mjs +0 -18
- package/lib/runner/lifecycle.mjs +7 -7
- package/lib/runner/orchestrator.mjs +9 -10
- package/lib/runner/planning.mjs +42 -136
- package/lib/runner/planning.test.mjs +70 -174
- package/lib/runner/playwright-config.mjs +8 -2
- package/lib/runner/playwright-config.test.mjs +20 -5
- package/lib/runner/playwright-runner.mjs +32 -54
- package/lib/runner/readiness.mjs +2 -2
- package/lib/runner/reporting.mjs +2 -3
- package/lib/runner/reporting.test.mjs +2 -5
- package/lib/runner/results.mjs +1 -1
- package/lib/runner/results.test.mjs +1 -1
- package/lib/runner/runtime-contexts.mjs +20 -24
- package/lib/runner/runtime-manager.mjs +181 -0
- package/lib/runner/runtime-manager.test.mjs +181 -0
- package/lib/runner/services.mjs +4 -4
- package/lib/runner/state.mjs +1 -2
- package/lib/runner/state.test.mjs +2 -4
- package/lib/runner/template.mjs +90 -60
- package/lib/runner/template.test.mjs +59 -27
- package/lib/runner/worker-loop.mjs +29 -32
- package/lib/setup/index.d.ts +14 -10
- package/package.json +1 -1
- package/lib/runner/stack-manager.mjs +0 -146
package/lib/runner/services.mjs
CHANGED
|
@@ -36,12 +36,12 @@ export async function startLocalService(config, lifecycle) {
|
|
|
36
36
|
|
|
37
37
|
await assertLocalServicePortsAvailable(config, isPortInUse);
|
|
38
38
|
|
|
39
|
-
console.log(`Starting ${config.
|
|
39
|
+
console.log(`Starting ${config.runtimeLabel}:${config.name}: ${config.testkit.local.start}`);
|
|
40
40
|
const child = startDetachedCommand(config.testkit.local.start, cwd, env);
|
|
41
41
|
|
|
42
42
|
const outputDrains = [
|
|
43
|
-
pipeOutput(child.stdout, `[${config.
|
|
44
|
-
pipeOutput(child.stderr, `[${config.
|
|
43
|
+
pipeOutput(child.stdout, `[${config.runtimeLabel}:${config.name}]`),
|
|
44
|
+
pipeOutput(child.stderr, `[${config.runtimeLabel}:${config.name}]`),
|
|
45
45
|
];
|
|
46
46
|
lifecycle.registerService(config, child, cwd);
|
|
47
47
|
|
|
@@ -49,7 +49,7 @@ export async function startLocalService(config, lifecycle) {
|
|
|
49
49
|
|
|
50
50
|
try {
|
|
51
51
|
await waitForReady({
|
|
52
|
-
name: `${config.
|
|
52
|
+
name: `${config.runtimeLabel}:${config.name}`,
|
|
53
53
|
url: config.testkit.local.readyUrl,
|
|
54
54
|
timeoutMs: readyTimeoutMs,
|
|
55
55
|
process: child,
|
package/lib/runner/state.mjs
CHANGED
|
@@ -46,8 +46,7 @@ export function writeGraphMetadata(graphDir, graph) {
|
|
|
46
46
|
fs.mkdirSync(graphDir, { recursive: true });
|
|
47
47
|
const metadata = {
|
|
48
48
|
runtimeServices: graph.runtimeNames,
|
|
49
|
-
|
|
50
|
-
rootService: graph.rootConfig.name,
|
|
49
|
+
targetServices: [...(graph.targetNames || [])].sort(),
|
|
51
50
|
};
|
|
52
51
|
fs.writeFileSync(
|
|
53
52
|
path.join(graphDir, GRAPH_METADATA),
|
|
@@ -48,14 +48,12 @@ describe("runner-state", () => {
|
|
|
48
48
|
const graphDir = path.join(productDir, ".testkit", "_graphs", "api__frontend");
|
|
49
49
|
writeGraphMetadata(graphDir, {
|
|
50
50
|
runtimeNames: ["api", "frontend"],
|
|
51
|
-
|
|
52
|
-
rootConfig: { name: "api" },
|
|
51
|
+
targetNames: ["frontend", "api"],
|
|
53
52
|
});
|
|
54
53
|
|
|
55
54
|
expect(readGraphMetadata(graphDir)).toEqual({
|
|
56
55
|
runtimeServices: ["api", "frontend"],
|
|
57
|
-
|
|
58
|
-
rootService: "api",
|
|
56
|
+
targetServices: ["api", "frontend"],
|
|
59
57
|
});
|
|
60
58
|
|
|
61
59
|
expect(findGraphDirsForService(productDir, "frontend")).toEqual([graphDir]);
|
package/lib/runner/template.mjs
CHANGED
|
@@ -3,26 +3,28 @@ import { readDatabaseInfo } from "./state-io.mjs";
|
|
|
3
3
|
|
|
4
4
|
const PORT_STRIDE = 100;
|
|
5
5
|
|
|
6
|
-
export function
|
|
7
|
-
const
|
|
6
|
+
export function resolveRuntimeInstanceConfigs(runtimeConfigs, runtimeId, runtimeDir, options = {}) {
|
|
7
|
+
const runtimeLabel = buildRuntimeLabel(options.graphDirName, runtimeId);
|
|
8
|
+
const portMap = buildPortMap(runtimeConfigs, runtimeId, {
|
|
9
|
+
index: options.portNamespaceIndex || 0,
|
|
10
|
+
stride: options.portNamespaceStride || 1,
|
|
11
|
+
});
|
|
8
12
|
const baseUrlByService = new Map();
|
|
9
13
|
const readyUrlByService = new Map();
|
|
10
14
|
const stateDirByService = new Map();
|
|
11
15
|
|
|
12
16
|
for (const config of runtimeConfigs) {
|
|
13
|
-
stateDirByService.set(
|
|
14
|
-
config.name,
|
|
15
|
-
resolveServiceStateDir(stackStateDir, targetConfig.name, config)
|
|
16
|
-
);
|
|
17
|
+
stateDirByService.set(config.name, resolveServiceStateDir(runtimeDir, config));
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
for (const config of runtimeConfigs) {
|
|
20
21
|
if (!config.testkit.local) continue;
|
|
21
22
|
baseUrlByService.set(
|
|
22
23
|
config.name,
|
|
23
|
-
resolveRuntimeUrl(config.testkit.local.baseUrl, config.name,
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
resolveRuntimeUrl(config.testkit.local.baseUrl, config.name, runtimeId, {
|
|
25
|
+
runtimeId,
|
|
26
|
+
runtimeLabel,
|
|
27
|
+
runtimeDir,
|
|
26
28
|
portMap,
|
|
27
29
|
baseUrlByService,
|
|
28
30
|
readyUrlByService,
|
|
@@ -31,9 +33,10 @@ export function resolveStackRuntimeConfigs(targetConfig, runtimeConfigs, stackId
|
|
|
31
33
|
);
|
|
32
34
|
readyUrlByService.set(
|
|
33
35
|
config.name,
|
|
34
|
-
resolveRuntimeUrl(config.testkit.local.readyUrl, config.name,
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
resolveRuntimeUrl(config.testkit.local.readyUrl, config.name, runtimeId, {
|
|
37
|
+
runtimeId,
|
|
38
|
+
runtimeLabel,
|
|
39
|
+
runtimeDir,
|
|
37
40
|
portMap,
|
|
38
41
|
baseUrlByService,
|
|
39
42
|
readyUrlByService,
|
|
@@ -47,20 +50,16 @@ export function resolveStackRuntimeConfigs(targetConfig, runtimeConfigs, stackId
|
|
|
47
50
|
if (!config.testkit.local) continue;
|
|
48
51
|
const resolvedBaseUrl = baseUrlByService.get(config.name);
|
|
49
52
|
const resolvedReadyUrl = readyUrlByService.get(config.name);
|
|
50
|
-
if (resolvedBaseUrl)
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
if (resolvedReadyUrl) {
|
|
54
|
-
urlMappings.push([config.testkit.local.readyUrl, resolvedReadyUrl]);
|
|
55
|
-
}
|
|
53
|
+
if (resolvedBaseUrl) urlMappings.push([config.testkit.local.baseUrl, resolvedBaseUrl]);
|
|
54
|
+
if (resolvedReadyUrl) urlMappings.push([config.testkit.local.readyUrl, resolvedReadyUrl]);
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
return runtimeConfigs.map((config) =>
|
|
59
|
-
|
|
58
|
+
resolveRuntimeConfig(
|
|
60
59
|
config,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
runtimeId,
|
|
61
|
+
runtimeLabel,
|
|
62
|
+
runtimeDir,
|
|
64
63
|
portMap,
|
|
65
64
|
baseUrlByService,
|
|
66
65
|
readyUrlByService,
|
|
@@ -70,10 +69,10 @@ export function resolveStackRuntimeConfigs(targetConfig, runtimeConfigs, stackId
|
|
|
70
69
|
);
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
export function buildPortMap(runtimeConfigs,
|
|
72
|
+
export function buildPortMap(runtimeConfigs, runtimeId, namespace = {}) {
|
|
74
73
|
const portMap = new Map();
|
|
75
74
|
const seen = new Map();
|
|
76
|
-
const offset =
|
|
75
|
+
const offset = runtimePortOffset(runtimeId, namespace);
|
|
77
76
|
|
|
78
77
|
for (const config of runtimeConfigs) {
|
|
79
78
|
if (!config.testkit.local) continue;
|
|
@@ -82,10 +81,16 @@ export function buildPortMap(runtimeConfigs, stackId) {
|
|
|
82
81
|
if (!basePort) continue;
|
|
83
82
|
|
|
84
83
|
const actualPort = basePort + offset;
|
|
84
|
+
if (actualPort > 65_535) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Runtime port resolution exceeded 65535 for service "${config.name}" (${actualPort}). ` +
|
|
87
|
+
`Reduce runtime instances or choose lower local.port/baseUrl ports in testkit.setup.ts.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
85
90
|
const existing = seen.get(actualPort);
|
|
86
91
|
if (existing) {
|
|
87
92
|
throw new Error(
|
|
88
|
-
`
|
|
93
|
+
`Runtime port collision: services "${existing}" and "${config.name}" both resolve to ${actualPort}. ` +
|
|
89
94
|
`Assign distinct local.port/baseUrl ports in testkit.setup.ts.`
|
|
90
95
|
);
|
|
91
96
|
}
|
|
@@ -96,21 +101,22 @@ export function buildPortMap(runtimeConfigs, stackId) {
|
|
|
96
101
|
return portMap;
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
export function
|
|
104
|
+
export function resolveRuntimeConfig(
|
|
100
105
|
config,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
runtimeId,
|
|
107
|
+
runtimeLabel,
|
|
108
|
+
runtimeDir,
|
|
104
109
|
portMap,
|
|
105
110
|
baseUrlByService,
|
|
106
111
|
readyUrlByService,
|
|
107
112
|
stateDirByService,
|
|
108
113
|
urlMappings
|
|
109
114
|
) {
|
|
110
|
-
const stateDir = resolveServiceStateDir(
|
|
115
|
+
const stateDir = resolveServiceStateDir(runtimeDir, config);
|
|
111
116
|
const context = {
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
runtimeId,
|
|
118
|
+
runtimeLabel,
|
|
119
|
+
runtimeDir,
|
|
114
120
|
serviceName: config.name,
|
|
115
121
|
serviceStateDir: stateDir,
|
|
116
122
|
portMap,
|
|
@@ -118,6 +124,8 @@ export function resolveStackConfig(
|
|
|
118
124
|
readyUrlByService,
|
|
119
125
|
stateDirByService,
|
|
120
126
|
urlMappings,
|
|
127
|
+
leaseId: null,
|
|
128
|
+
leaseDir: null,
|
|
121
129
|
};
|
|
122
130
|
|
|
123
131
|
const database = config.testkit.database
|
|
@@ -166,9 +174,8 @@ export function resolveStackConfig(
|
|
|
166
174
|
return {
|
|
167
175
|
...config,
|
|
168
176
|
stateDir,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
targetName: targetConfig.name,
|
|
177
|
+
runtimeId,
|
|
178
|
+
runtimeLabel,
|
|
172
179
|
testkit: {
|
|
173
180
|
...config.testkit,
|
|
174
181
|
database,
|
|
@@ -180,52 +187,66 @@ export function resolveStackConfig(
|
|
|
180
187
|
};
|
|
181
188
|
}
|
|
182
189
|
|
|
183
|
-
export function resolveServiceStateDir(
|
|
184
|
-
|
|
185
|
-
return getStackServiceStateDir(stackStateDir, targetName, dbSource);
|
|
190
|
+
export function resolveServiceStateDir(runtimeDir, config) {
|
|
191
|
+
return path.join(runtimeDir, "services", config.name);
|
|
186
192
|
}
|
|
187
193
|
|
|
188
|
-
export function
|
|
189
|
-
|
|
190
|
-
return stackStateDir;
|
|
191
|
-
}
|
|
192
|
-
return path.join(stackStateDir, "deps", serviceName);
|
|
194
|
+
export function buildExecutionEnv(config, extraEnv = {}, processEnv = process.env) {
|
|
195
|
+
return buildExecutionEnvWithContext(config, null, extraEnv, processEnv);
|
|
193
196
|
}
|
|
194
197
|
|
|
195
|
-
export function
|
|
198
|
+
export function buildTaskExecutionEnv(config, lease, extraEnv = {}, processEnv = process.env) {
|
|
199
|
+
return buildExecutionEnvWithContext(config, lease, extraEnv, processEnv);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function buildExecutionEnvWithContext(config, lease, extraEnv, processEnv) {
|
|
196
203
|
const inheritedEnv = { ...processEnv };
|
|
197
|
-
const templateContext = config
|
|
204
|
+
const templateContext = buildTemplateContext(config, lease);
|
|
198
205
|
const env = {
|
|
199
206
|
...inheritedEnv,
|
|
200
207
|
...resolveEnvTemplates(config.testkit.serviceEnv || {}, templateContext),
|
|
201
208
|
...resolveEnvTemplates(extraEnv, templateContext),
|
|
202
209
|
TESTKIT_ACTIVE: "1",
|
|
203
|
-
...(config.
|
|
210
|
+
...(config.runtimeId ? { TESTKIT_RUNTIME_ID: String(config.runtimeId) } : {}),
|
|
211
|
+
...(lease?.leaseId ? { TESTKIT_LEASE_ID: String(lease.leaseId) } : {}),
|
|
212
|
+
...(lease?.leaseDir ? { TESTKIT_LEASE_DIR: lease.leaseDir } : {}),
|
|
204
213
|
};
|
|
205
214
|
delete env.DATABASE_URL;
|
|
206
215
|
return env;
|
|
207
216
|
}
|
|
208
217
|
|
|
209
|
-
export function buildPlaywrightEnv(config, baseUrl, processEnv = process.env) {
|
|
210
|
-
return
|
|
218
|
+
export function buildPlaywrightEnv(config, baseUrl, lease, processEnv = process.env) {
|
|
219
|
+
return buildTaskExecutionEnv(
|
|
211
220
|
config,
|
|
221
|
+
lease,
|
|
212
222
|
{
|
|
213
223
|
BASE_URL: baseUrl,
|
|
214
224
|
PLAYWRIGHT_HTML_OPEN: "never",
|
|
215
225
|
PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS:
|
|
216
226
|
processEnv.PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS || "1",
|
|
217
227
|
TESTKIT_MANAGED_SERVERS: "1",
|
|
218
|
-
TESTKIT_STACK_ID: String(config.stackId),
|
|
219
228
|
},
|
|
220
229
|
processEnv
|
|
221
230
|
);
|
|
222
231
|
}
|
|
223
232
|
|
|
224
|
-
|
|
233
|
+
function buildTemplateContext(config, lease) {
|
|
234
|
+
const baseContext = config.testkit?.templateContext || {};
|
|
235
|
+
return {
|
|
236
|
+
...baseContext,
|
|
237
|
+
runtimeId: config.runtimeId || baseContext.runtimeId || null,
|
|
238
|
+
runtimeLabel: config.runtimeLabel || baseContext.runtimeLabel || null,
|
|
239
|
+
serviceName: config.name,
|
|
240
|
+
serviceStateDir: config.stateDir || baseContext.serviceStateDir || null,
|
|
241
|
+
leaseId: lease?.leaseId || null,
|
|
242
|
+
leaseDir: lease?.leaseDir || null,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function resolveRuntimeUrl(rawUrl, serviceName, runtimeId, context) {
|
|
225
247
|
const resolved = resolveTemplateString(rawUrl, {
|
|
226
248
|
...context,
|
|
227
|
-
|
|
228
|
-
stackId,
|
|
249
|
+
runtimeId,
|
|
229
250
|
serviceName,
|
|
230
251
|
});
|
|
231
252
|
const actualPort = context.portMap.get(serviceName);
|
|
@@ -247,14 +268,16 @@ export function resolveTemplateString(value, context) {
|
|
|
247
268
|
|
|
248
269
|
return value.replace(/\{([a-zA-Z]+)(?::([a-zA-Z0-9_-]+))?\}/g, (_match, token, arg) => {
|
|
249
270
|
switch (token) {
|
|
250
|
-
case "
|
|
251
|
-
return String(context.
|
|
252
|
-
case "target":
|
|
253
|
-
return context.targetName;
|
|
271
|
+
case "runtime":
|
|
272
|
+
return String(context.runtimeId);
|
|
254
273
|
case "service":
|
|
255
274
|
return context.serviceName;
|
|
256
275
|
case "stateDir":
|
|
257
276
|
return context.serviceStateDir;
|
|
277
|
+
case "lease":
|
|
278
|
+
return context.leaseId ? String(context.leaseId) : "";
|
|
279
|
+
case "leaseDir":
|
|
280
|
+
return context.leaseDir || "";
|
|
258
281
|
case "port": {
|
|
259
282
|
const serviceName = arg || context.serviceName;
|
|
260
283
|
const port = context.portMap.get(serviceName);
|
|
@@ -381,8 +404,15 @@ export function normalizeSocketHost(hostname) {
|
|
|
381
404
|
return hostname;
|
|
382
405
|
}
|
|
383
406
|
|
|
384
|
-
function
|
|
385
|
-
const match = String(
|
|
386
|
-
|
|
387
|
-
|
|
407
|
+
function runtimePortOffset(runtimeId, namespace = {}) {
|
|
408
|
+
const match = String(runtimeId).match(/(\d+)$/);
|
|
409
|
+
const runtimeIndex = match ? Number.parseInt(match[1], 10) - 1 : 0;
|
|
410
|
+
const namespaceIndex = Math.max(0, Number(namespace.index || 0));
|
|
411
|
+
const namespaceStride = Math.max(1, Number(namespace.stride || 1));
|
|
412
|
+
return PORT_STRIDE * (namespaceIndex * namespaceStride + runtimeIndex);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function buildRuntimeLabel(graphDirName, runtimeId) {
|
|
416
|
+
if (!graphDirName) return runtimeId;
|
|
417
|
+
return `${graphDirName}/${runtimeId}`;
|
|
388
418
|
}
|
|
@@ -6,12 +6,12 @@ import {
|
|
|
6
6
|
buildExecutionEnv,
|
|
7
7
|
buildPlaywrightEnv,
|
|
8
8
|
buildPortMap,
|
|
9
|
+
buildTaskExecutionEnv,
|
|
9
10
|
finalizeString,
|
|
10
|
-
getStackServiceStateDir,
|
|
11
11
|
normalizeSocketHost,
|
|
12
12
|
numericPortFromUrl,
|
|
13
|
+
resolveRuntimeInstanceConfigs,
|
|
13
14
|
resolveServiceStateDir,
|
|
14
|
-
resolveStackRuntimeConfigs,
|
|
15
15
|
resolveTemplateString,
|
|
16
16
|
rewriteUrlPort,
|
|
17
17
|
socketFromUrl,
|
|
@@ -20,12 +20,15 @@ import {
|
|
|
20
20
|
function makeRuntimeConfig(name, local, extras = {}) {
|
|
21
21
|
return {
|
|
22
22
|
name,
|
|
23
|
+
stateDir: extras.stateDir,
|
|
24
|
+
runtimeId: extras.runtimeId,
|
|
23
25
|
testkit: {
|
|
24
26
|
local,
|
|
25
27
|
serviceEnv: extras.serviceEnv || {},
|
|
26
28
|
databaseFrom: extras.databaseFrom,
|
|
27
29
|
migrate: extras.migrate,
|
|
28
30
|
seed: extras.seed,
|
|
31
|
+
templateContext: extras.templateContext,
|
|
29
32
|
},
|
|
30
33
|
};
|
|
31
34
|
}
|
|
@@ -37,10 +40,14 @@ describe("runner-template", () => {
|
|
|
37
40
|
makeRuntimeConfig("frontend", { port: 3001, baseUrl: "http://127.0.0.1:{port}" }),
|
|
38
41
|
];
|
|
39
42
|
|
|
40
|
-
expect([...buildPortMap(configs, "
|
|
43
|
+
expect([...buildPortMap(configs, "runtime-2").entries()]).toEqual([
|
|
41
44
|
["api", 3100],
|
|
42
45
|
["frontend", 3101],
|
|
43
46
|
]);
|
|
47
|
+
expect([...buildPortMap(configs, "runtime-1", { index: 1, stride: 2 }).entries()]).toEqual([
|
|
48
|
+
["api", 3200],
|
|
49
|
+
["frontend", 3201],
|
|
50
|
+
]);
|
|
44
51
|
|
|
45
52
|
expect(() =>
|
|
46
53
|
buildPortMap(
|
|
@@ -48,9 +55,10 @@ describe("runner-template", () => {
|
|
|
48
55
|
makeRuntimeConfig("api", { port: 3000, baseUrl: "http://127.0.0.1:{port}" }),
|
|
49
56
|
makeRuntimeConfig("other", { port: 3000, baseUrl: "http://127.0.0.1:{port}" }),
|
|
50
57
|
],
|
|
51
|
-
"
|
|
58
|
+
"runtime-1",
|
|
59
|
+
{ index: 0, stride: 2 }
|
|
52
60
|
)
|
|
53
|
-
).toThrow("
|
|
61
|
+
).toThrow("Runtime port collision");
|
|
54
62
|
});
|
|
55
63
|
|
|
56
64
|
it("resolves template strings and URL rewrites", () => {
|
|
@@ -59,12 +67,11 @@ describe("runner-template", () => {
|
|
|
59
67
|
fs.mkdirSync(apiStateDir, { recursive: true });
|
|
60
68
|
fs.writeFileSync(
|
|
61
69
|
path.join(apiStateDir, "database_url"),
|
|
62
|
-
"postgres://testkit:testkit@127.0.0.1:55432/
|
|
70
|
+
"postgres://testkit:testkit@127.0.0.1:55432/runtime_db"
|
|
63
71
|
);
|
|
64
72
|
|
|
65
73
|
const context = {
|
|
66
|
-
|
|
67
|
-
targetName: "frontend",
|
|
74
|
+
runtimeId: "runtime-2",
|
|
68
75
|
serviceName: "frontend",
|
|
69
76
|
serviceStateDir: "/tmp/state",
|
|
70
77
|
portMap: new Map([
|
|
@@ -75,14 +82,16 @@ describe("runner-template", () => {
|
|
|
75
82
|
readyUrlByService: new Map([["api", "http://127.0.0.1:3100/health"]]),
|
|
76
83
|
stateDirByService: new Map([["api", apiStateDir]]),
|
|
77
84
|
urlMappings: [["http://api:3000", "http://127.0.0.1:3100"]],
|
|
85
|
+
leaseId: "lease-1",
|
|
86
|
+
leaseDir: "/tmp/lease-1",
|
|
78
87
|
};
|
|
79
88
|
|
|
80
|
-
expect(resolveTemplateString("{
|
|
81
|
-
"
|
|
89
|
+
expect(resolveTemplateString("{runtime}:{service}:{lease}", context)).toBe(
|
|
90
|
+
"runtime-2:frontend:lease-1"
|
|
82
91
|
);
|
|
83
92
|
expect(resolveTemplateString("{baseUrl:api}", context)).toBe("http://127.0.0.1:3100");
|
|
84
93
|
expect(resolveTemplateString("{dbHost:api}:{dbPort:api}/{dbName:api}", context)).toBe(
|
|
85
|
-
"127.0.0.1:55432/
|
|
94
|
+
"127.0.0.1:55432/runtime_db"
|
|
86
95
|
);
|
|
87
96
|
expect(finalizeString("API={baseUrl:api} OLD=http://api:3000", context)).toBe(
|
|
88
97
|
"API=http://127.0.0.1:3100 OLD=http://127.0.0.1:3100"
|
|
@@ -92,8 +101,8 @@ describe("runner-template", () => {
|
|
|
92
101
|
);
|
|
93
102
|
});
|
|
94
103
|
|
|
95
|
-
it("builds
|
|
96
|
-
const
|
|
104
|
+
it("builds runtime configs and execution env", () => {
|
|
105
|
+
const runtimeDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-runtime-"));
|
|
97
106
|
const api = makeRuntimeConfig(
|
|
98
107
|
"api",
|
|
99
108
|
{
|
|
@@ -120,22 +129,23 @@ describe("runner-template", () => {
|
|
|
120
129
|
readyUrl: "http://127.0.0.1:{port}",
|
|
121
130
|
env: {
|
|
122
131
|
NEXT_PUBLIC_API_URL: "{baseUrl:api}",
|
|
123
|
-
|
|
132
|
+
API_DB_HOST: "{dbHost:api}",
|
|
124
133
|
},
|
|
125
134
|
});
|
|
126
135
|
|
|
127
|
-
const resolved =
|
|
136
|
+
const resolved = resolveRuntimeInstanceConfigs([api, frontend], "runtime-2", runtimeDir, {
|
|
137
|
+
graphDirName: "api__frontend",
|
|
138
|
+
portNamespaceIndex: 0,
|
|
139
|
+
portNamespaceStride: 2,
|
|
140
|
+
});
|
|
128
141
|
expect(resolved[0].testkit.local.port).toBe(3100);
|
|
129
|
-
expect(resolved[
|
|
130
|
-
expect(resolveServiceStateDir(
|
|
131
|
-
`${stackStateDir}/deps/api`
|
|
132
|
-
);
|
|
133
|
-
expect(getStackServiceStateDir(stackStateDir, "frontend", "frontend")).toBe(stackStateDir);
|
|
142
|
+
expect(resolved[0].runtimeLabel).toBe("api__frontend/runtime-2");
|
|
143
|
+
expect(resolveServiceStateDir(runtimeDir, api)).toBe(`${runtimeDir}/services/api`);
|
|
134
144
|
|
|
135
|
-
fs.mkdirSync(path.join(
|
|
145
|
+
fs.mkdirSync(path.join(runtimeDir, "services", "api"), { recursive: true });
|
|
136
146
|
fs.writeFileSync(
|
|
137
|
-
path.join(
|
|
138
|
-
"postgres://testkit:testkit@127.0.0.1:55432/
|
|
147
|
+
path.join(runtimeDir, "services", "api", "database_url"),
|
|
148
|
+
"postgres://testkit:testkit@127.0.0.1:55432/runtime_db"
|
|
139
149
|
);
|
|
140
150
|
|
|
141
151
|
expect(
|
|
@@ -152,16 +162,38 @@ describe("runner-template", () => {
|
|
|
152
162
|
).toEqual({
|
|
153
163
|
PATH: "/usr/bin",
|
|
154
164
|
NEXT_PUBLIC_API_URL: "http://127.0.0.1:3100",
|
|
155
|
-
|
|
165
|
+
API_DB_HOST: "127.0.0.1",
|
|
156
166
|
TESTKIT_ACTIVE: "1",
|
|
157
|
-
|
|
167
|
+
TESTKIT_RUNTIME_ID: "runtime-2",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(
|
|
171
|
+
buildTaskExecutionEnv(
|
|
172
|
+
resolved[1],
|
|
173
|
+
{
|
|
174
|
+
leaseId: "lease-1",
|
|
175
|
+
leaseDir: "/tmp/lease-1",
|
|
176
|
+
},
|
|
177
|
+
{},
|
|
178
|
+
{}
|
|
179
|
+
)
|
|
180
|
+
).toMatchObject({
|
|
181
|
+
TESTKIT_RUNTIME_ID: "runtime-2",
|
|
182
|
+
TESTKIT_LEASE_ID: "lease-1",
|
|
183
|
+
TESTKIT_LEASE_DIR: "/tmp/lease-1",
|
|
158
184
|
});
|
|
159
185
|
|
|
160
186
|
expect(
|
|
161
|
-
buildPlaywrightEnv(
|
|
187
|
+
buildPlaywrightEnv(
|
|
188
|
+
{ runtimeId: "runtime-1", testkit: { serviceEnv: {}, templateContext: {} } },
|
|
189
|
+
"http://localhost:3000",
|
|
190
|
+
{ leaseId: "lease-2", leaseDir: "/tmp/lease-2" },
|
|
191
|
+
{}
|
|
192
|
+
)
|
|
162
193
|
).toMatchObject({
|
|
163
194
|
BASE_URL: "http://localhost:3000",
|
|
164
|
-
|
|
195
|
+
TESTKIT_RUNTIME_ID: "runtime-1",
|
|
196
|
+
TESTKIT_LEASE_ID: "lease-2",
|
|
165
197
|
PLAYWRIGHT_HTML_OPEN: "never",
|
|
166
198
|
});
|
|
167
199
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { formatError } from "./formatting.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { runDalTask, runHttpK6Task } from "./default-runtime-runner.mjs";
|
|
3
|
+
import { runPlaywrightTask } from "./playwright-runner.mjs";
|
|
4
4
|
|
|
5
5
|
const HTTP_K6_TYPES = new Set(["integration", "e2e", "load"]);
|
|
6
6
|
|
|
@@ -17,11 +17,11 @@ export function createWorker(workerId, productDir) {
|
|
|
17
17
|
export async function runWorker(
|
|
18
18
|
worker,
|
|
19
19
|
queue,
|
|
20
|
-
|
|
20
|
+
runtimeManager,
|
|
21
21
|
trackers,
|
|
22
22
|
timingUpdates,
|
|
23
23
|
lifecycle,
|
|
24
|
-
|
|
24
|
+
claimNextTask,
|
|
25
25
|
recordTaskOutcome,
|
|
26
26
|
recordGraphError
|
|
27
27
|
) {
|
|
@@ -32,32 +32,29 @@ export async function runWorker(
|
|
|
32
32
|
try {
|
|
33
33
|
while (true) {
|
|
34
34
|
if (lifecycle.isStopRequested()) break;
|
|
35
|
-
const
|
|
36
|
-
if (!
|
|
35
|
+
const task = claimNextTask(queue, worker.currentGraphKey);
|
|
36
|
+
if (!task) break;
|
|
37
37
|
|
|
38
38
|
let lease = null;
|
|
39
39
|
try {
|
|
40
|
-
if (worker.currentGraphKey && worker.currentGraphKey !==
|
|
40
|
+
if (worker.currentGraphKey && worker.currentGraphKey !== task.graphKey) {
|
|
41
41
|
worker.graphSwitches += 1;
|
|
42
42
|
}
|
|
43
|
-
worker.currentGraphKey =
|
|
44
|
-
lease = await
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
await stackManager.release(lease, { accessMode: batch.accessMode });
|
|
43
|
+
worker.currentGraphKey = task.graphKey;
|
|
44
|
+
lease = await runtimeManager.acquire(task);
|
|
45
|
+
const outcome = await runTask(lease.context, task, lifecycle, lease);
|
|
46
|
+
recordTaskOutcome(trackers, outcome.task, outcome);
|
|
47
|
+
timingUpdates.push({
|
|
48
|
+
key: outcome.task.timingKey,
|
|
49
|
+
durationMs: outcome.durationMs,
|
|
50
|
+
});
|
|
51
|
+
worker.taskCount += 1;
|
|
52
|
+
await runtimeManager.release(lease);
|
|
55
53
|
} catch (error) {
|
|
56
54
|
const message = formatError(error);
|
|
57
55
|
errors.push(message);
|
|
58
|
-
recordGraphError(trackers, {
|
|
59
|
-
await
|
|
60
|
-
accessMode: batch.accessMode,
|
|
56
|
+
recordGraphError(trackers, { targetNames: lease?.context?.targetNames || [task.targetName] }, message);
|
|
57
|
+
await runtimeManager.release(lease, {
|
|
61
58
|
invalidate: lease !== null,
|
|
62
59
|
});
|
|
63
60
|
}
|
|
@@ -76,23 +73,23 @@ export async function runWorker(
|
|
|
76
73
|
};
|
|
77
74
|
}
|
|
78
75
|
|
|
79
|
-
async function
|
|
80
|
-
const targetConfig = context.configByName.get(
|
|
76
|
+
async function runTask(context, task, lifecycle, lease) {
|
|
77
|
+
const targetConfig = context.configByName.get(task.targetName);
|
|
81
78
|
if (!targetConfig) {
|
|
82
|
-
throw new Error(`
|
|
79
|
+
throw new Error(`Runtime instance is missing target config "${task.targetName}"`);
|
|
83
80
|
}
|
|
84
81
|
|
|
85
|
-
if (
|
|
86
|
-
return
|
|
82
|
+
if (task.framework === "playwright") {
|
|
83
|
+
return runPlaywrightTask(targetConfig, task, lifecycle, lease);
|
|
87
84
|
}
|
|
88
|
-
if (
|
|
89
|
-
return
|
|
85
|
+
if (task.type === "dal") {
|
|
86
|
+
return runDalTask(targetConfig, task, lifecycle, lease);
|
|
90
87
|
}
|
|
91
|
-
if (
|
|
92
|
-
return
|
|
88
|
+
if (task.framework === "k6" && HTTP_K6_TYPES.has(task.type)) {
|
|
89
|
+
return runHttpK6Task(targetConfig, task, lifecycle, lease);
|
|
93
90
|
}
|
|
94
91
|
|
|
95
92
|
throw new Error(
|
|
96
|
-
`Unsupported task combination for ${
|
|
93
|
+
`Unsupported task combination for ${task.targetName}: type=${task.type} framework=${task.framework}`
|
|
97
94
|
);
|
|
98
95
|
}
|