@elench/testkit 0.1.26 → 0.1.27
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 +103 -82
- package/lib/bundler/index.mjs +46 -7
- package/lib/cli/index.mjs +3 -32
- package/lib/config/discovery.mjs +209 -54
- package/lib/config/discovery.test.mjs +57 -28
- package/lib/config/index.mjs +297 -154
- package/lib/config/setup-loader.mjs +98 -0
- package/lib/index.d.ts +1 -0
- package/lib/package.test.mjs +5 -0
- package/lib/runner/template.mjs +1 -1
- package/lib/runtime-src/k6/http.js +1 -0
- package/lib/runtime-src/k6/suite.js +66 -23
- package/lib/setup/index.d.ts +104 -0
- package/lib/setup/index.mjs +292 -0
- package/lib/setup/runtime.mjs +79 -0
- package/package.json +5 -1
- package/lib/config/model.mjs +0 -320
- package/lib/config/model.test.mjs +0 -163
- package/lib/runtime-manager/index.mjs +0 -190
package/lib/config/model.mjs
DELETED
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
export const VALID_FRAMEWORKS = new Set(["k6", "playwright"]);
|
|
2
|
-
export const VALID_DB_PROVIDERS = new Set(["local"]);
|
|
3
|
-
|
|
4
|
-
export function parseDotenvString(source) {
|
|
5
|
-
const env = {};
|
|
6
|
-
for (const line of String(source).split("\n")) {
|
|
7
|
-
const trimmed = line.trim();
|
|
8
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
9
|
-
const eq = trimmed.indexOf("=");
|
|
10
|
-
if (eq === -1) continue;
|
|
11
|
-
const key = trimmed.slice(0, eq).trim();
|
|
12
|
-
let val = trimmed.slice(eq + 1).trim();
|
|
13
|
-
if (
|
|
14
|
-
(val.startsWith("'") && val.endsWith("'")) ||
|
|
15
|
-
(val.startsWith("\"") && val.endsWith("\""))
|
|
16
|
-
) {
|
|
17
|
-
val = val.slice(1, -1);
|
|
18
|
-
}
|
|
19
|
-
env[key] = val;
|
|
20
|
-
}
|
|
21
|
-
return env;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function validateConfigCoverage(
|
|
25
|
-
config,
|
|
26
|
-
configName = "testkit.config.json"
|
|
27
|
-
) {
|
|
28
|
-
for (const serviceName of Object.keys(config.services || {})) {
|
|
29
|
-
for (const depName of config.services[serviceName].dependsOn || []) {
|
|
30
|
-
if (!config.services[depName]) {
|
|
31
|
-
throw new Error(
|
|
32
|
-
`Service "${serviceName}" depends on "${depName}", but ${depName} is missing from ${configName}`
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const databaseFrom = config.services[serviceName].databaseFrom;
|
|
38
|
-
if (databaseFrom && !config.services[databaseFrom]) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
`Service "${serviceName}" databaseFrom "${databaseFrom}" is missing from ${configName}`
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function validateServiceConfig(name, service, configPath) {
|
|
47
|
-
if (!isObject(service)) {
|
|
48
|
-
throw new Error(`Service "${name}" in ${configPath} must be an object`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (service.dependsOn !== undefined) {
|
|
52
|
-
if (!Array.isArray(service.dependsOn) || service.dependsOn.some((v) => typeof v !== "string")) {
|
|
53
|
-
throw new Error(`Service "${name}" dependsOn must be an array of service names`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (service.databaseFrom !== undefined && typeof service.databaseFrom !== "string") {
|
|
58
|
-
throw new Error(`Service "${name}" databaseFrom must be a string`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (service.database !== undefined && service.databaseFrom !== undefined) {
|
|
62
|
-
throw new Error(`Service "${name}" cannot define both database and databaseFrom`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (service.envFile !== undefined && typeof service.envFile !== "string") {
|
|
66
|
-
throw new Error(`Service "${name}" envFile must be a string`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
service.envFiles !== undefined &&
|
|
71
|
-
(!Array.isArray(service.envFiles) || service.envFiles.some((v) => typeof v !== "string"))
|
|
72
|
-
) {
|
|
73
|
-
throw new Error(`Service "${name}" envFiles must be an array of strings`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (service.database !== undefined) {
|
|
77
|
-
if (!isObject(service.database)) {
|
|
78
|
-
throw new Error(`Service "${name}" database must be an object`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (service.database.provider !== undefined) {
|
|
82
|
-
validateDatabaseProviderConfig(name, service.database, `Service "${name}" database`);
|
|
83
|
-
} else if (service.database.backends !== undefined) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
`Service "${name}" database.backends is no longer supported. Define a single local database provider instead.`
|
|
86
|
-
);
|
|
87
|
-
} else {
|
|
88
|
-
throw new Error(`Service "${name}" database must define provider`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (service.migrate !== undefined) {
|
|
93
|
-
validateLifecycleConfig(name, service.migrate, "migrate");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (service.seed !== undefined) {
|
|
97
|
-
validateLifecycleConfig(name, service.seed, "seed");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (service.local !== undefined) {
|
|
101
|
-
if (!isObject(service.local)) {
|
|
102
|
-
throw new Error(`Service "${name}" local must be an object`);
|
|
103
|
-
}
|
|
104
|
-
requireString(service.local, "start", `Service "${name}" local.start`);
|
|
105
|
-
requireString(service.local, "baseUrl", `Service "${name}" local.baseUrl`);
|
|
106
|
-
requireString(service.local, "readyUrl", `Service "${name}" local.readyUrl`);
|
|
107
|
-
if (
|
|
108
|
-
service.local.port !== undefined &&
|
|
109
|
-
(!Number.isInteger(service.local.port) || service.local.port <= 0)
|
|
110
|
-
) {
|
|
111
|
-
throw new Error(`Service "${name}" local.port must be a positive integer`);
|
|
112
|
-
}
|
|
113
|
-
if (service.local.cwd !== undefined && typeof service.local.cwd !== "string") {
|
|
114
|
-
throw new Error(`Service "${name}" local.cwd must be a string`);
|
|
115
|
-
}
|
|
116
|
-
if (
|
|
117
|
-
service.local.readyTimeoutMs !== undefined &&
|
|
118
|
-
(!Number.isInteger(service.local.readyTimeoutMs) || service.local.readyTimeoutMs <= 0)
|
|
119
|
-
) {
|
|
120
|
-
throw new Error(`Service "${name}" local.readyTimeoutMs must be a positive integer`);
|
|
121
|
-
}
|
|
122
|
-
if (service.local.env !== undefined && !isObject(service.local.env)) {
|
|
123
|
-
throw new Error(`Service "${name}" local.env must be an object`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (service.discovery !== undefined) {
|
|
128
|
-
throw new Error(
|
|
129
|
-
`Service "${name}" cannot define discovery. Testkit discovers *.testkit.ts files from the filesystem automatically.`
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function validateTelemetryConfig(
|
|
135
|
-
telemetry,
|
|
136
|
-
configPath,
|
|
137
|
-
configName = "testkit.config.json"
|
|
138
|
-
) {
|
|
139
|
-
if (!isObject(telemetry)) {
|
|
140
|
-
throw new Error(`${configName} telemetry must be an object (${configPath})`);
|
|
141
|
-
}
|
|
142
|
-
if (telemetry.enabled !== undefined && typeof telemetry.enabled !== "boolean") {
|
|
143
|
-
throw new Error(`${configName} telemetry.enabled must be a boolean (${configPath})`);
|
|
144
|
-
}
|
|
145
|
-
if (telemetry.endpoint !== undefined) {
|
|
146
|
-
requireString(telemetry, "endpoint", `${configName} telemetry.endpoint`);
|
|
147
|
-
validateHttpUrl(telemetry.endpoint, `${configName} telemetry.endpoint`);
|
|
148
|
-
}
|
|
149
|
-
if (telemetry.tokenEnv !== undefined) {
|
|
150
|
-
requireString(telemetry, "tokenEnv", `${configName} telemetry.tokenEnv`);
|
|
151
|
-
}
|
|
152
|
-
if (
|
|
153
|
-
telemetry.timeoutMs !== undefined &&
|
|
154
|
-
(!Number.isInteger(telemetry.timeoutMs) || telemetry.timeoutMs <= 0)
|
|
155
|
-
) {
|
|
156
|
-
throw new Error(`${configName} telemetry.timeoutMs must be a positive integer (${configPath})`);
|
|
157
|
-
}
|
|
158
|
-
if (telemetry.enabled === true) {
|
|
159
|
-
requireString(telemetry, "endpoint", `${configName} telemetry.endpoint`);
|
|
160
|
-
requireString(telemetry, "tokenEnv", `${configName} telemetry.tokenEnv`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function getServiceEnvFiles(serviceConfig) {
|
|
165
|
-
const files = [];
|
|
166
|
-
if (serviceConfig.envFile) files.push(serviceConfig.envFile);
|
|
167
|
-
if (Array.isArray(serviceConfig.envFiles)) {
|
|
168
|
-
files.push(...serviceConfig.envFiles);
|
|
169
|
-
}
|
|
170
|
-
return files;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function resolveSelectedDatabase(name, serviceConfig) {
|
|
174
|
-
if (!serviceConfig.database) return undefined;
|
|
175
|
-
|
|
176
|
-
const database = serviceConfig.database;
|
|
177
|
-
if (database.provider !== "local") {
|
|
178
|
-
throw new Error(
|
|
179
|
-
`Service "${name}" database.provider must be "local". Received "${database.provider}"`
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
...database,
|
|
185
|
-
provider: "local",
|
|
186
|
-
selectedBackend: "local",
|
|
187
|
-
reset: database.reset !== false,
|
|
188
|
-
template: normalizeTemplateConfig(database.template),
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function validateDatabaseProviderConfig(name, db, label) {
|
|
193
|
-
if (!VALID_DB_PROVIDERS.has(db.provider)) {
|
|
194
|
-
throw new Error(
|
|
195
|
-
`${label}.provider must be one of: ${[...VALID_DB_PROVIDERS].join(", ")}`
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (db.reset !== undefined && typeof db.reset !== "boolean") {
|
|
200
|
-
throw new Error(`${label}.reset must be a boolean`);
|
|
201
|
-
}
|
|
202
|
-
if (db.template !== undefined) {
|
|
203
|
-
validateTemplateConfig(name, db.template, `${label}.template`);
|
|
204
|
-
}
|
|
205
|
-
if (db.image !== undefined && typeof db.image !== "string") {
|
|
206
|
-
throw new Error(`${label}.image must be a string`);
|
|
207
|
-
}
|
|
208
|
-
if (db.user !== undefined && typeof db.user !== "string") {
|
|
209
|
-
throw new Error(`${label}.user must be a string`);
|
|
210
|
-
}
|
|
211
|
-
if (db.password !== undefined && typeof db.password !== "string") {
|
|
212
|
-
throw new Error(`${label}.password must be a string`);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function validateTemplateConfig(name, template, label) {
|
|
217
|
-
if (!isObject(template)) {
|
|
218
|
-
throw new Error(`${label} must be an object`);
|
|
219
|
-
}
|
|
220
|
-
if (
|
|
221
|
-
template.inputs !== undefined &&
|
|
222
|
-
(!Array.isArray(template.inputs) || template.inputs.some((value) => typeof value !== "string"))
|
|
223
|
-
) {
|
|
224
|
-
throw new Error(`${label}.inputs must be an array of strings`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function normalizeTemplateConfig(template) {
|
|
229
|
-
return {
|
|
230
|
-
inputs: Array.isArray(template?.inputs) ? [...template.inputs] : [],
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export function validateLifecycleConfig(name, value, label) {
|
|
235
|
-
if (!isObject(value)) {
|
|
236
|
-
throw new Error(`Service "${name}" ${label} must be an object`);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (value.cmd !== undefined) {
|
|
240
|
-
requireString(value, "cmd", `Service "${name}" ${label}.cmd`);
|
|
241
|
-
}
|
|
242
|
-
if (value.cwd !== undefined && typeof value.cwd !== "string") {
|
|
243
|
-
throw new Error(`Service "${name}" ${label}.cwd must be a string`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (value.backends !== undefined) {
|
|
247
|
-
if (!isObject(value.backends)) {
|
|
248
|
-
throw new Error(`Service "${name}" ${label}.backends must be an object`);
|
|
249
|
-
}
|
|
250
|
-
for (const [backendName, override] of Object.entries(value.backends)) {
|
|
251
|
-
if (!isObject(override)) {
|
|
252
|
-
throw new Error(
|
|
253
|
-
`Service "${name}" ${label}.backends.${backendName} must be an object`
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
if (override.cmd !== undefined) {
|
|
257
|
-
requireString(override, "cmd", `Service "${name}" ${label}.backends.${backendName}.cmd`);
|
|
258
|
-
}
|
|
259
|
-
if (override.cwd !== undefined && typeof override.cwd !== "string") {
|
|
260
|
-
throw new Error(
|
|
261
|
-
`Service "${name}" ${label}.backends.${backendName}.cwd must be a string`
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (value.cmd === undefined && value.backends === undefined) {
|
|
268
|
-
throw new Error(`Service "${name}" ${label} must define cmd or backends`);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export function resolveLifecycleConfig(value, selectedBackend) {
|
|
273
|
-
if (!value) return value;
|
|
274
|
-
|
|
275
|
-
const override =
|
|
276
|
-
selectedBackend && isObject(value.backends) ? value.backends[selectedBackend] || {} : {};
|
|
277
|
-
const resolved = {
|
|
278
|
-
...value,
|
|
279
|
-
...override,
|
|
280
|
-
};
|
|
281
|
-
delete resolved.backends;
|
|
282
|
-
return resolved;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export function requireString(obj, key, label) {
|
|
286
|
-
if (typeof obj[key] !== "string" || obj[key].length === 0) {
|
|
287
|
-
throw new Error(`${label} must be a non-empty string`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export function isDalSuiteType(suite, runnerService, suitesForType) {
|
|
292
|
-
if (suite.framework && suite.framework !== "k6") return false;
|
|
293
|
-
return suitesForType === runnerService.suites.dal;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export function isObject(value) {
|
|
297
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export function normalizeTelemetryConfig(telemetry) {
|
|
301
|
-
if (!isObject(telemetry)) return null;
|
|
302
|
-
return {
|
|
303
|
-
enabled: telemetry.enabled === true,
|
|
304
|
-
endpoint: telemetry.endpoint || null,
|
|
305
|
-
tokenEnv: telemetry.tokenEnv || null,
|
|
306
|
-
timeoutMs: telemetry.timeoutMs || 3_000,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export function validateHttpUrl(value, label) {
|
|
311
|
-
let parsed;
|
|
312
|
-
try {
|
|
313
|
-
parsed = new URL(value);
|
|
314
|
-
} catch {
|
|
315
|
-
throw new Error(`${label} must be a valid URL`);
|
|
316
|
-
}
|
|
317
|
-
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
318
|
-
throw new Error(`${label} must use http or https`);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
getServiceEnvFiles,
|
|
4
|
-
isDalSuiteType,
|
|
5
|
-
normalizeTelemetryConfig,
|
|
6
|
-
parseDotenvString,
|
|
7
|
-
resolveLifecycleConfig,
|
|
8
|
-
resolveSelectedDatabase,
|
|
9
|
-
validateConfigCoverage,
|
|
10
|
-
validateLifecycleConfig,
|
|
11
|
-
validateServiceConfig,
|
|
12
|
-
validateTelemetryConfig,
|
|
13
|
-
} from "./model.mjs";
|
|
14
|
-
|
|
15
|
-
describe("config-model", () => {
|
|
16
|
-
it("parses dotenv text", () => {
|
|
17
|
-
expect(
|
|
18
|
-
parseDotenvString(`
|
|
19
|
-
# comment
|
|
20
|
-
FOO=bar
|
|
21
|
-
BAR="baz"
|
|
22
|
-
QUX='zap'
|
|
23
|
-
`)
|
|
24
|
-
).toEqual({
|
|
25
|
-
FOO: "bar",
|
|
26
|
-
BAR: "baz",
|
|
27
|
-
QUX: "zap",
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("validates config coverage", () => {
|
|
32
|
-
const config = {
|
|
33
|
-
services: {
|
|
34
|
-
frontend: {
|
|
35
|
-
dependsOn: ["api"],
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
expect(() => validateConfigCoverage(config)).toThrow(
|
|
41
|
-
'Service "frontend" depends on "api", but api is missing from testkit.config.json'
|
|
42
|
-
);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("validates service and telemetry config", () => {
|
|
46
|
-
expect(() =>
|
|
47
|
-
validateServiceConfig("api", {
|
|
48
|
-
local: {
|
|
49
|
-
start: "npm run dev",
|
|
50
|
-
baseUrl: "http://127.0.0.1:3000",
|
|
51
|
-
readyUrl: "http://127.0.0.1:3000/health",
|
|
52
|
-
port: 3000,
|
|
53
|
-
},
|
|
54
|
-
}, "testkit.config.json")
|
|
55
|
-
).not.toThrow();
|
|
56
|
-
|
|
57
|
-
expect(() =>
|
|
58
|
-
validateServiceConfig("api", {
|
|
59
|
-
database: {
|
|
60
|
-
backends: {},
|
|
61
|
-
},
|
|
62
|
-
}, "testkit.config.json")
|
|
63
|
-
).toThrow("database.backends is no longer supported");
|
|
64
|
-
|
|
65
|
-
expect(() =>
|
|
66
|
-
validateServiceConfig("api", {
|
|
67
|
-
discovery: {},
|
|
68
|
-
}, "testkit.config.json")
|
|
69
|
-
).toThrow("cannot define discovery");
|
|
70
|
-
|
|
71
|
-
expect(() =>
|
|
72
|
-
validateTelemetryConfig(
|
|
73
|
-
{
|
|
74
|
-
enabled: true,
|
|
75
|
-
endpoint: "ftp://bad.example.com/upload",
|
|
76
|
-
tokenEnv: "TOKEN",
|
|
77
|
-
},
|
|
78
|
-
"testkit.config.json"
|
|
79
|
-
)
|
|
80
|
-
).toThrow("must use http or https");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("collects env files and resolves lifecycle overrides", () => {
|
|
84
|
-
expect(
|
|
85
|
-
getServiceEnvFiles({
|
|
86
|
-
envFile: ".env.shared",
|
|
87
|
-
envFiles: [".env.local"],
|
|
88
|
-
})
|
|
89
|
-
).toEqual([".env.shared", ".env.local"]);
|
|
90
|
-
|
|
91
|
-
expect(() =>
|
|
92
|
-
validateLifecycleConfig("api", { backends: {} }, "migrate")
|
|
93
|
-
).not.toThrow();
|
|
94
|
-
|
|
95
|
-
expect(
|
|
96
|
-
resolveLifecycleConfig(
|
|
97
|
-
{
|
|
98
|
-
cmd: "npm run migrate",
|
|
99
|
-
backends: {
|
|
100
|
-
local: {
|
|
101
|
-
cmd: "npm run migrate:local",
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
"local"
|
|
106
|
-
)
|
|
107
|
-
).toEqual({
|
|
108
|
-
cmd: "npm run migrate:local",
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("normalizes telemetry and database config", () => {
|
|
113
|
-
expect(
|
|
114
|
-
resolveSelectedDatabase("api", {
|
|
115
|
-
database: {
|
|
116
|
-
provider: "local",
|
|
117
|
-
image: "pgvector/pgvector:pg16",
|
|
118
|
-
template: {
|
|
119
|
-
inputs: ["schema"],
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
})
|
|
123
|
-
).toEqual({
|
|
124
|
-
provider: "local",
|
|
125
|
-
image: "pgvector/pgvector:pg16",
|
|
126
|
-
selectedBackend: "local",
|
|
127
|
-
reset: true,
|
|
128
|
-
template: {
|
|
129
|
-
inputs: ["schema"],
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
expect(
|
|
134
|
-
normalizeTelemetryConfig({
|
|
135
|
-
enabled: true,
|
|
136
|
-
endpoint: "https://example.com",
|
|
137
|
-
tokenEnv: "TOKEN",
|
|
138
|
-
})
|
|
139
|
-
).toEqual({
|
|
140
|
-
enabled: true,
|
|
141
|
-
endpoint: "https://example.com",
|
|
142
|
-
tokenEnv: "TOKEN",
|
|
143
|
-
timeoutMs: 3000,
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("detects dal suite types", () => {
|
|
148
|
-
const suite = { name: "db", files: ["a.js"] };
|
|
149
|
-
const runnerService = {
|
|
150
|
-
suites: {
|
|
151
|
-
dal: [suite],
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
expect(isDalSuiteType(suite, runnerService, runnerService.suites.dal)).toBe(true);
|
|
155
|
-
expect(
|
|
156
|
-
isDalSuiteType(
|
|
157
|
-
{ ...suite, framework: "playwright" },
|
|
158
|
-
runnerService,
|
|
159
|
-
runnerService.suites.dal
|
|
160
|
-
)
|
|
161
|
-
).toBe(false);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import crypto from "crypto";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
|
|
6
|
-
const TESTKIT_CONFIG = "testkit.config.json";
|
|
7
|
-
const DEFAULT_RUNTIME_DIR = path.join("tests", "_testkit");
|
|
8
|
-
const METADATA_FILE = ".runtime-manifest.json";
|
|
9
|
-
const RUNTIME_FORMAT = 1;
|
|
10
|
-
|
|
11
|
-
export function installRuntime(options = {}) {
|
|
12
|
-
const productDir = resolveProductDir(process.cwd(), options.dir);
|
|
13
|
-
const runtimeDir = resolveRuntimeDir(productDir, options.path);
|
|
14
|
-
const sourceFiles = readBundledRuntimeFiles();
|
|
15
|
-
|
|
16
|
-
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
17
|
-
|
|
18
|
-
for (const file of sourceFiles) {
|
|
19
|
-
const targetPath = path.join(runtimeDir, file.path);
|
|
20
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
21
|
-
fs.writeFileSync(targetPath, file.content);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const metadata = {
|
|
25
|
-
format: RUNTIME_FORMAT,
|
|
26
|
-
package: "@elench/testkit",
|
|
27
|
-
version: readPackageVersion(),
|
|
28
|
-
files: sourceFiles.map((file) => ({
|
|
29
|
-
path: file.path,
|
|
30
|
-
sha256: hashContent(file.content),
|
|
31
|
-
})),
|
|
32
|
-
};
|
|
33
|
-
fs.writeFileSync(
|
|
34
|
-
path.join(runtimeDir, METADATA_FILE),
|
|
35
|
-
`${JSON.stringify(metadata, null, 2)}\n`
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
productDir,
|
|
40
|
-
runtimeDir,
|
|
41
|
-
relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
|
|
42
|
-
files: metadata.files,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function getRuntimeStatus(options = {}) {
|
|
47
|
-
const productDir = resolveProductDir(process.cwd(), options.dir);
|
|
48
|
-
const runtimeDir = resolveRuntimeDir(productDir, options.path);
|
|
49
|
-
const sourceFiles = readBundledRuntimeFiles();
|
|
50
|
-
const metadataPath = path.join(runtimeDir, METADATA_FILE);
|
|
51
|
-
|
|
52
|
-
if (!fs.existsSync(runtimeDir) || !fs.existsSync(metadataPath)) {
|
|
53
|
-
return {
|
|
54
|
-
status: "missing",
|
|
55
|
-
productDir,
|
|
56
|
-
runtimeDir,
|
|
57
|
-
relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
|
|
58
|
-
missingFiles: sourceFiles.map((file) => file.path),
|
|
59
|
-
driftedFiles: [],
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const missingFiles = [];
|
|
64
|
-
const driftedFiles = [];
|
|
65
|
-
|
|
66
|
-
for (const file of sourceFiles) {
|
|
67
|
-
const targetPath = path.join(runtimeDir, file.path);
|
|
68
|
-
if (!fs.existsSync(targetPath)) {
|
|
69
|
-
missingFiles.push(file.path);
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const installed = fs.readFileSync(targetPath, "utf8");
|
|
74
|
-
if (installed !== file.content) {
|
|
75
|
-
driftedFiles.push(file.path);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
|
80
|
-
const versionMatches = metadata.version === readPackageVersion();
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
status:
|
|
84
|
-
missingFiles.length === 0 && driftedFiles.length === 0 && versionMatches
|
|
85
|
-
? "installed"
|
|
86
|
-
: "drifted",
|
|
87
|
-
productDir,
|
|
88
|
-
runtimeDir,
|
|
89
|
-
relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
|
|
90
|
-
versionMatches,
|
|
91
|
-
missingFiles,
|
|
92
|
-
driftedFiles,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function formatRuntimeStatus(result) {
|
|
97
|
-
if (result.status === "missing") {
|
|
98
|
-
return `Runtime not installed at ${result.relativeRuntimeDir}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (result.status === "installed") {
|
|
102
|
-
return `Runtime at ${result.relativeRuntimeDir} is up to date`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const problems = [];
|
|
106
|
-
if (result.missingFiles.length > 0) {
|
|
107
|
-
problems.push(`missing: ${result.missingFiles.join(", ")}`);
|
|
108
|
-
}
|
|
109
|
-
if (result.driftedFiles.length > 0) {
|
|
110
|
-
problems.push(`drifted: ${result.driftedFiles.join(", ")}`);
|
|
111
|
-
}
|
|
112
|
-
if (result.versionMatches === false) {
|
|
113
|
-
problems.push("version drift");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return `Runtime at ${result.relativeRuntimeDir} is drifted (${problems.join("; ")})`;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function resolveProductDir(cwd, explicitDir) {
|
|
120
|
-
const dir = explicitDir ? path.resolve(cwd, explicitDir) : cwd;
|
|
121
|
-
ensureProductFiles(dir);
|
|
122
|
-
return dir;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function ensureProductFiles(dir) {
|
|
126
|
-
const missing = [TESTKIT_CONFIG].filter(
|
|
127
|
-
(file) => !fs.existsSync(path.join(dir, file))
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
if (missing.length > 0) {
|
|
131
|
-
throw new Error(
|
|
132
|
-
`Expected ${missing.join(" and ")} in ${dir}. Either cd into a product directory or use --dir.`
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function resolveRuntimeDir(productDir, explicitPath) {
|
|
138
|
-
return path.resolve(productDir, explicitPath || DEFAULT_RUNTIME_DIR);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function relativeToProduct(productDir, targetPath) {
|
|
142
|
-
return path.relative(productDir, targetPath) || ".";
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function readBundledRuntimeFiles() {
|
|
146
|
-
const sourceDir = path.resolve(
|
|
147
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
148
|
-
"..",
|
|
149
|
-
"runtime-src"
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
return walkRuntimeFiles(sourceDir);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function walkRuntimeFiles(rootDir, relativeDir = "") {
|
|
156
|
-
const entries = fs.readdirSync(path.join(rootDir, relativeDir), {
|
|
157
|
-
withFileTypes: true,
|
|
158
|
-
});
|
|
159
|
-
const files = [];
|
|
160
|
-
|
|
161
|
-
for (const entry of entries) {
|
|
162
|
-
const nextRelative = path.join(relativeDir, entry.name);
|
|
163
|
-
if (entry.isDirectory()) {
|
|
164
|
-
files.push(...walkRuntimeFiles(rootDir, nextRelative));
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const absolute = path.join(rootDir, nextRelative);
|
|
169
|
-
files.push({
|
|
170
|
-
path: nextRelative.split(path.sep).join("/"),
|
|
171
|
-
content: fs.readFileSync(absolute, "utf8"),
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function readPackageVersion() {
|
|
179
|
-
const packagePath = path.resolve(
|
|
180
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
181
|
-
"..",
|
|
182
|
-
"..",
|
|
183
|
-
"package.json"
|
|
184
|
-
);
|
|
185
|
-
return JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function hashContent(content) {
|
|
189
|
-
return crypto.createHash("sha256").update(content).digest("hex");
|
|
190
|
-
}
|