@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.
Files changed (39) hide show
  1. package/README.md +44 -19
  2. package/bin/testkit.mjs +1 -1
  3. package/lib/cli/args.mjs +57 -0
  4. package/lib/cli/args.test.mjs +62 -0
  5. package/lib/cli/index.mjs +88 -0
  6. package/lib/config/index.mjs +294 -0
  7. package/lib/config/index.test.mjs +12 -0
  8. package/lib/config/model.mjs +422 -0
  9. package/lib/config/model.test.mjs +193 -0
  10. package/lib/database/fingerprint.mjs +61 -0
  11. package/lib/database/fingerprint.test.mjs +93 -0
  12. package/lib/{database.mjs → database/index.mjs} +45 -160
  13. package/lib/database/naming.mjs +47 -0
  14. package/lib/database/naming.test.mjs +39 -0
  15. package/lib/database/state.mjs +52 -0
  16. package/lib/database/state.test.mjs +66 -0
  17. package/lib/reporters/playwright.mjs +125 -0
  18. package/lib/reporters/playwright.test.mjs +73 -0
  19. package/lib/runner/index.mjs +1221 -0
  20. package/lib/runner/metadata.mjs +55 -0
  21. package/lib/runner/metadata.test.mjs +52 -0
  22. package/lib/runner/planning.mjs +270 -0
  23. package/lib/runner/planning.test.mjs +127 -0
  24. package/lib/runner/results.mjs +285 -0
  25. package/lib/runner/results.test.mjs +144 -0
  26. package/lib/runner/state.mjs +71 -0
  27. package/lib/runner/state.test.mjs +64 -0
  28. package/lib/runner/template.mjs +320 -0
  29. package/lib/runner/template.test.mjs +150 -0
  30. package/lib/telemetry/index.mjs +43 -0
  31. package/lib/timing/index.mjs +73 -0
  32. package/lib/timing/index.test.mjs +64 -0
  33. package/package.json +11 -3
  34. package/infra/neon-down.sh +0 -18
  35. package/infra/neon-up.sh +0 -124
  36. package/lib/cli.mjs +0 -132
  37. package/lib/config.mjs +0 -666
  38. package/lib/exec.mjs +0 -20
  39. package/lib/runner.mjs +0 -1165
package/lib/config.mjs DELETED
@@ -1,666 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { fileURLToPath } from "url";
4
-
5
- const RUNNER_MANIFEST = "runner.manifest.json";
6
- const TESTKIT_CONFIG = "testkit.config.json";
7
- const VALID_FRAMEWORKS = new Set(["k6", "playwright"]);
8
- const VALID_DB_PROVIDERS = new Set(["neon", "local"]);
9
-
10
- /**
11
- * Parse a .env file into an object. Supports KEY=VALUE, KEY='VALUE', KEY="VALUE".
12
- * Skips comments (#) and blank lines.
13
- */
14
- export function parseDotenv(filePath) {
15
- if (!fs.existsSync(filePath)) return {};
16
- const env = {};
17
- for (const line of fs.readFileSync(filePath, "utf8").split("\n")) {
18
- const trimmed = line.trim();
19
- if (!trimmed || trimmed.startsWith("#")) continue;
20
- const eq = trimmed.indexOf("=");
21
- if (eq === -1) continue;
22
- const key = trimmed.slice(0, eq).trim();
23
- let val = trimmed.slice(eq + 1).trim();
24
- if (
25
- (val.startsWith("'") && val.endsWith("'")) ||
26
- (val.startsWith('"') && val.endsWith('"'))
27
- ) {
28
- val = val.slice(1, -1);
29
- }
30
- env[key] = val;
31
- }
32
- return env;
33
- }
34
-
35
- export function getServiceNames(cwd) {
36
- const dir = cwd || process.cwd();
37
- const runnerPath = path.join(dir, RUNNER_MANIFEST);
38
- if (!fs.existsSync(runnerPath)) return [];
39
- const runner = JSON.parse(fs.readFileSync(runnerPath, "utf8"));
40
- if (!isObject(runner.services)) return [];
41
- return Object.keys(runner.services);
42
- }
43
-
44
- export function loadConfigs(opts = {}) {
45
- const productDir = resolveProductDir(process.cwd(), opts.dir);
46
- const runner = loadRunnerManifest(productDir);
47
- const config = loadTestkitConfig(productDir);
48
- validateConfigCoverage(runner, config);
49
-
50
- const dotenv = parseDotenv(path.join(productDir, ".env"));
51
- Object.assign(process.env, dotenv);
52
-
53
- const serviceEntries = Object.entries(runner.services);
54
- const filtered = opts.service
55
- ? serviceEntries.filter(([name]) => name === opts.service)
56
- : serviceEntries;
57
-
58
- if (opts.service && filtered.length === 0) {
59
- const available = serviceEntries.map(([name]) => name).join(", ");
60
- throw new Error(`Service "${opts.service}" not found. Available: ${available}`);
61
- }
62
-
63
- return filtered.map(([name, runnerService]) => {
64
- const serviceConfig = config.services[name];
65
- if (!serviceConfig) {
66
- throw new Error(
67
- `Service "${name}" exists in ${RUNNER_MANIFEST} but is missing from ${TESTKIT_CONFIG}`
68
- );
69
- }
70
-
71
- const resolvedDatabase = resolveSelectedDatabase(name, serviceConfig, opts.dbBackend);
72
- const serviceEnv = loadServiceEnv(productDir, serviceConfig);
73
- const selectedBackend = resolvedDatabase?.selectedBackend;
74
- const resolvedMigrate = resolveLifecycleConfig(serviceConfig.migrate, selectedBackend);
75
- const resolvedSeed = resolveLifecycleConfig(serviceConfig.seed, selectedBackend);
76
- validateMergedService(
77
- name,
78
- runnerService,
79
- serviceConfig,
80
- resolvedDatabase,
81
- resolvedMigrate,
82
- resolvedSeed,
83
- productDir
84
- );
85
-
86
- return {
87
- name,
88
- productDir,
89
- stateDir: path.join(productDir, ".testkit", name),
90
- suites: runnerService.suites,
91
- testkit: {
92
- ...serviceConfig,
93
- database: resolvedDatabase,
94
- migrate: resolvedMigrate,
95
- seed: resolvedSeed,
96
- envFiles: getServiceEnvFiles(serviceConfig),
97
- serviceEnv,
98
- },
99
- };
100
- });
101
- }
102
-
103
- export function requireNeonApiKey() {
104
- if (!process.env.NEON_API_KEY) {
105
- throw new Error(
106
- "NEON_API_KEY not found. Set it in your shell environment, .envrc, or .env"
107
- );
108
- }
109
- }
110
-
111
- export function resolveDalBinary() {
112
- const thisFile = fileURLToPath(import.meta.url);
113
- const abs = path.resolve(path.dirname(thisFile), "..", "vendor", "k6");
114
- if (!fs.existsSync(abs)) {
115
- throw new Error(`Bundled DAL k6 binary not found: ${abs}`);
116
- }
117
- return abs;
118
- }
119
-
120
- export function resolveServiceCwd(productDir, maybeRelative) {
121
- return path.resolve(productDir, maybeRelative || ".");
122
- }
123
-
124
- function loadRunnerManifest(productDir) {
125
- const manifestPath = path.join(productDir, RUNNER_MANIFEST);
126
- const raw = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
127
-
128
- if (!isObject(raw.services)) {
129
- throw new Error(`${RUNNER_MANIFEST} must have a "services" object (${manifestPath})`);
130
- }
131
-
132
- for (const [serviceName, service] of Object.entries(raw.services)) {
133
- if (!isObject(service)) {
134
- throw new Error(`Service "${serviceName}" in ${RUNNER_MANIFEST} must be an object`);
135
- }
136
- if (!isObject(service.suites)) {
137
- throw new Error(`Service "${serviceName}" in ${RUNNER_MANIFEST} must define suites`);
138
- }
139
-
140
- for (const [suiteType, suites] of Object.entries(service.suites)) {
141
- if (!Array.isArray(suites)) {
142
- throw new Error(
143
- `Service "${serviceName}" suite type "${suiteType}" must be an array`
144
- );
145
- }
146
-
147
- const seenNames = new Set();
148
- for (const suite of suites) {
149
- if (!isObject(suite)) {
150
- throw new Error(
151
- `Service "${serviceName}" suite type "${suiteType}" contains a non-object suite`
152
- );
153
- }
154
- if (typeof suite.name !== "string" || !suite.name.length) {
155
- throw new Error(
156
- `Service "${serviceName}" suite type "${suiteType}" has a suite with no name`
157
- );
158
- }
159
- if (seenNames.has(suite.name)) {
160
- throw new Error(
161
- `Service "${serviceName}" suite type "${suiteType}" has duplicate suite name "${suite.name}"`
162
- );
163
- }
164
- seenNames.add(suite.name);
165
-
166
- if (!Array.isArray(suite.files) || suite.files.length === 0) {
167
- throw new Error(
168
- `Service "${serviceName}" suite "${suite.name}" must define one or more files`
169
- );
170
- }
171
- for (const file of suite.files) {
172
- if (typeof file !== "string" || !file.length) {
173
- throw new Error(
174
- `Service "${serviceName}" suite "${suite.name}" contains an invalid file entry`
175
- );
176
- }
177
- }
178
-
179
- const framework = suite.framework || "k6";
180
- if (!VALID_FRAMEWORKS.has(framework)) {
181
- throw new Error(
182
- `Service "${serviceName}" suite "${suite.name}" uses unsupported framework "${framework}"`
183
- );
184
- }
185
-
186
- if (suite.testkit !== undefined) {
187
- if (!isObject(suite.testkit)) {
188
- throw new Error(
189
- `Service "${serviceName}" suite "${suite.name}" testkit config must be an object`
190
- );
191
- }
192
- if (
193
- suite.testkit.maxFileConcurrency !== undefined &&
194
- (!Number.isInteger(suite.testkit.maxFileConcurrency) ||
195
- suite.testkit.maxFileConcurrency <= 0)
196
- ) {
197
- throw new Error(
198
- `Service "${serviceName}" suite "${suite.name}" testkit.maxFileConcurrency must be a positive integer`
199
- );
200
- }
201
- if (
202
- suite.testkit.weight !== undefined &&
203
- (!Number.isInteger(suite.testkit.weight) || suite.testkit.weight <= 0)
204
- ) {
205
- throw new Error(
206
- `Service "${serviceName}" suite "${suite.name}" testkit.weight must be a positive integer`
207
- );
208
- }
209
- }
210
- }
211
- }
212
- }
213
-
214
- return raw;
215
- }
216
-
217
- function loadTestkitConfig(productDir) {
218
- const configPath = path.join(productDir, TESTKIT_CONFIG);
219
- const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
220
-
221
- if (!isObject(raw.services)) {
222
- throw new Error(`${TESTKIT_CONFIG} must have a "services" object (${configPath})`);
223
- }
224
-
225
- for (const [serviceName, service] of Object.entries(raw.services)) {
226
- validateServiceConfig(serviceName, service, configPath);
227
- }
228
-
229
- return raw;
230
- }
231
-
232
- function validateConfigCoverage(runner, config) {
233
- for (const serviceName of Object.keys(config.services)) {
234
- if (!runner.services[serviceName]) {
235
- throw new Error(
236
- `Service "${serviceName}" exists in ${TESTKIT_CONFIG} but not in ${RUNNER_MANIFEST}`
237
- );
238
- }
239
-
240
- for (const depName of config.services[serviceName].dependsOn || []) {
241
- if (!config.services[depName]) {
242
- throw new Error(
243
- `Service "${serviceName}" depends on "${depName}", but ${depName} is missing from ${TESTKIT_CONFIG}`
244
- );
245
- }
246
- if (!runner.services[depName]) {
247
- throw new Error(
248
- `Service "${serviceName}" depends on "${depName}", but ${depName} is missing from ${RUNNER_MANIFEST}`
249
- );
250
- }
251
- }
252
-
253
- const databaseFrom = config.services[serviceName].databaseFrom;
254
- if (databaseFrom) {
255
- if (!config.services[databaseFrom]) {
256
- throw new Error(
257
- `Service "${serviceName}" databaseFrom "${databaseFrom}" is missing from ${TESTKIT_CONFIG}`
258
- );
259
- }
260
- if (!runner.services[databaseFrom]) {
261
- throw new Error(
262
- `Service "${serviceName}" databaseFrom "${databaseFrom}" is missing from ${RUNNER_MANIFEST}`
263
- );
264
- }
265
- }
266
- }
267
- }
268
-
269
- function resolveProductDir(cwd, explicitDir) {
270
- if (explicitDir) {
271
- const dir = path.resolve(cwd, explicitDir);
272
- ensureProductFiles(dir);
273
- return dir;
274
- }
275
-
276
- ensureProductFiles(cwd);
277
- return cwd;
278
- }
279
-
280
- function ensureProductFiles(dir) {
281
- const missing = [RUNNER_MANIFEST, TESTKIT_CONFIG].filter(
282
- (file) => !fs.existsSync(path.join(dir, file))
283
- );
284
- if (missing.length > 0) {
285
- throw new Error(
286
- `Expected ${missing.join(" and ")} in ${dir}. Either cd into a product directory or use --dir.`
287
- );
288
- }
289
- }
290
-
291
- export function isSiblingProduct(name) {
292
- const candidate = path.join(process.cwd(), name);
293
- return (
294
- fs.existsSync(path.join(candidate, RUNNER_MANIFEST)) &&
295
- fs.existsSync(path.join(candidate, TESTKIT_CONFIG))
296
- );
297
- }
298
-
299
- function validateMergedService(
300
- name,
301
- runnerService,
302
- serviceConfig,
303
- resolvedDatabase,
304
- resolvedMigrate,
305
- resolvedSeed,
306
- productDir
307
- ) {
308
- const usesLocalExecution = Object.values(runnerService.suites).some((suites) =>
309
- suites.some(
310
- (suite) =>
311
- (suite.framework && suite.framework !== "k6") ||
312
- !isDalSuiteType(suite, runnerService, suites)
313
- )
314
- );
315
-
316
- if (usesLocalExecution && !isObject(serviceConfig.local)) {
317
- throw new Error(
318
- `Service "${name}" defines non-DAL suites but has no local runtime in ${TESTKIT_CONFIG}`
319
- );
320
- }
321
-
322
- if (serviceConfig.dependsOn) {
323
- for (const dep of serviceConfig.dependsOn) {
324
- if (dep === name) {
325
- throw new Error(`Service "${name}" cannot depend on itself`);
326
- }
327
- }
328
- }
329
-
330
- if (
331
- resolvedDatabase?.provider === "local" &&
332
- (serviceConfig.migrate || serviceConfig.seed) &&
333
- resolvedDatabase.template.inputs.length === 0
334
- ) {
335
- throw new Error(
336
- `Service "${name}" uses local database provisioning with migrations or seeds, so database.template.inputs must list the files/directories that define the template cache`
337
- );
338
- }
339
-
340
- if (serviceConfig.local?.cwd) {
341
- const cwdPath = resolveServiceCwd(productDir, serviceConfig.local.cwd);
342
- if (!fs.existsSync(cwdPath)) {
343
- throw new Error(
344
- `Service "${name}" local.cwd does not exist: ${serviceConfig.local.cwd}`
345
- );
346
- }
347
- }
348
-
349
- if (resolvedMigrate?.cwd) {
350
- const cwdPath = resolveServiceCwd(productDir, resolvedMigrate.cwd);
351
- if (!fs.existsSync(cwdPath)) {
352
- throw new Error(
353
- `Service "${name}" migrate.cwd does not exist: ${resolvedMigrate.cwd}`
354
- );
355
- }
356
- }
357
-
358
- if (resolvedSeed?.cwd) {
359
- const cwdPath = resolveServiceCwd(productDir, resolvedSeed.cwd);
360
- if (!fs.existsSync(cwdPath)) {
361
- throw new Error(
362
- `Service "${name}" seed.cwd does not exist: ${resolvedSeed.cwd}`
363
- );
364
- }
365
- }
366
-
367
- }
368
-
369
- function validateServiceConfig(name, service, configPath) {
370
- if (!isObject(service)) {
371
- throw new Error(`Service "${name}" in ${configPath} must be an object`);
372
- }
373
-
374
- if (service.dependsOn !== undefined) {
375
- if (!Array.isArray(service.dependsOn) || service.dependsOn.some((v) => typeof v !== "string")) {
376
- throw new Error(`Service "${name}" dependsOn must be an array of service names`);
377
- }
378
- }
379
-
380
- if (service.databaseFrom !== undefined && typeof service.databaseFrom !== "string") {
381
- throw new Error(`Service "${name}" databaseFrom must be a string`);
382
- }
383
-
384
- if (service.database !== undefined && service.databaseFrom !== undefined) {
385
- throw new Error(
386
- `Service "${name}" cannot define both database and databaseFrom`
387
- );
388
- }
389
-
390
- if (service.envFile !== undefined && typeof service.envFile !== "string") {
391
- throw new Error(`Service "${name}" envFile must be a string`);
392
- }
393
-
394
- if (
395
- service.envFiles !== undefined &&
396
- (!Array.isArray(service.envFiles) || service.envFiles.some((v) => typeof v !== "string"))
397
- ) {
398
- throw new Error(`Service "${name}" envFiles must be an array of strings`);
399
- }
400
-
401
- if (service.database !== undefined) {
402
- if (!isObject(service.database)) {
403
- throw new Error(`Service "${name}" database must be an object`);
404
- }
405
-
406
- if (service.database.provider !== undefined) {
407
- validateDatabaseProviderConfig(name, service.database, `Service "${name}" database`);
408
- } else if (service.database.backends !== undefined) {
409
- validateMultiBackendDatabaseConfig(name, service.database);
410
- } else {
411
- throw new Error(
412
- `Service "${name}" database must define either provider or backends`
413
- );
414
- }
415
- }
416
-
417
- if (service.migrate !== undefined) {
418
- validateLifecycleConfig(name, service.migrate, "migrate");
419
- }
420
-
421
- if (service.seed !== undefined) {
422
- validateLifecycleConfig(name, service.seed, "seed");
423
- }
424
-
425
- if (service.local !== undefined) {
426
- if (!isObject(service.local)) {
427
- throw new Error(`Service "${name}" local must be an object`);
428
- }
429
- requireString(service.local, "start", `Service "${name}" local.start`);
430
- requireString(service.local, "baseUrl", `Service "${name}" local.baseUrl`);
431
- requireString(service.local, "readyUrl", `Service "${name}" local.readyUrl`);
432
- if (
433
- service.local.port !== undefined &&
434
- (!Number.isInteger(service.local.port) || service.local.port <= 0)
435
- ) {
436
- throw new Error(`Service "${name}" local.port must be a positive integer`);
437
- }
438
- if (service.local.cwd !== undefined && typeof service.local.cwd !== "string") {
439
- throw new Error(`Service "${name}" local.cwd must be a string`);
440
- }
441
- if (
442
- service.local.readyTimeoutMs !== undefined &&
443
- (!Number.isInteger(service.local.readyTimeoutMs) || service.local.readyTimeoutMs <= 0)
444
- ) {
445
- throw new Error(`Service "${name}" local.readyTimeoutMs must be a positive integer`);
446
- }
447
- if (service.local.env !== undefined && !isObject(service.local.env)) {
448
- throw new Error(`Service "${name}" local.env must be an object`);
449
- }
450
- }
451
- }
452
-
453
- function loadServiceEnv(productDir, serviceConfig) {
454
- const env = {};
455
- for (const envFile of getServiceEnvFiles(serviceConfig)) {
456
- Object.assign(env, parseDotenv(resolveServiceCwd(productDir, envFile)));
457
- }
458
- return env;
459
- }
460
-
461
- function getServiceEnvFiles(serviceConfig) {
462
- const files = [];
463
- if (serviceConfig.envFile) files.push(serviceConfig.envFile);
464
- if (Array.isArray(serviceConfig.envFiles)) {
465
- files.push(...serviceConfig.envFiles);
466
- }
467
- return files;
468
- }
469
-
470
- function resolveSelectedDatabase(name, serviceConfig, requestedBackend) {
471
- if (!serviceConfig.database) return undefined;
472
-
473
- const database = serviceConfig.database;
474
- if (database.provider !== undefined) {
475
- if (requestedBackend && requestedBackend !== database.provider) {
476
- throw new Error(
477
- `Service "${name}" does not define database backend "${requestedBackend}". Available: ${database.provider}`
478
- );
479
- }
480
-
481
- return {
482
- ...database,
483
- provider: database.provider,
484
- selectedBackend: database.provider,
485
- reset: database.reset !== false,
486
- template: normalizeTemplateConfig(database.template),
487
- };
488
- }
489
-
490
- const backends = database.backends || {};
491
- const selectedBackend =
492
- requestedBackend ||
493
- process.env.TESTKIT_DB_BACKEND ||
494
- database.defaultBackend ||
495
- Object.keys(backends)[0];
496
-
497
- if (!selectedBackend || !backends[selectedBackend]) {
498
- throw new Error(
499
- `Service "${name}" does not define database backend "${selectedBackend || "undefined"}"`
500
- );
501
- }
502
-
503
- const backend = backends[selectedBackend];
504
- return {
505
- ...backend,
506
- provider: backend.provider,
507
- selectedBackend,
508
- reset: database.reset !== false,
509
- template: normalizeTemplateConfig(database.template),
510
- };
511
- }
512
-
513
- function validateMultiBackendDatabaseConfig(name, database) {
514
- if (
515
- database.defaultBackend !== undefined &&
516
- (typeof database.defaultBackend !== "string" || !database.defaultBackend.length)
517
- ) {
518
- throw new Error(`Service "${name}" database.defaultBackend must be a non-empty string`);
519
- }
520
- if (database.reset !== undefined && typeof database.reset !== "boolean") {
521
- throw new Error(`Service "${name}" database.reset must be a boolean`);
522
- }
523
- if (database.template !== undefined) {
524
- validateTemplateConfig(name, database.template, `Service "${name}" database.template`);
525
- }
526
- if (!isObject(database.backends) || Object.keys(database.backends).length === 0) {
527
- throw new Error(`Service "${name}" database.backends must be a non-empty object`);
528
- }
529
-
530
- for (const [backendName, backendConfig] of Object.entries(database.backends)) {
531
- if (!isObject(backendConfig)) {
532
- throw new Error(
533
- `Service "${name}" database.backends.${backendName} must be an object`
534
- );
535
- }
536
- validateDatabaseProviderConfig(
537
- name,
538
- backendConfig,
539
- `Service "${name}" database.backends.${backendName}`
540
- );
541
- }
542
-
543
- if (database.defaultBackend && !database.backends[database.defaultBackend]) {
544
- throw new Error(
545
- `Service "${name}" database.defaultBackend "${database.defaultBackend}" is missing from database.backends`
546
- );
547
- }
548
- }
549
-
550
- function validateDatabaseProviderConfig(name, db, label) {
551
- if (!VALID_DB_PROVIDERS.has(db.provider)) {
552
- throw new Error(
553
- `${label}.provider must be one of: ${[...VALID_DB_PROVIDERS].join(", ")}`
554
- );
555
- }
556
-
557
- if (db.reset !== undefined && typeof db.reset !== "boolean") {
558
- throw new Error(`${label}.reset must be a boolean`);
559
- }
560
- if (db.template !== undefined) {
561
- validateTemplateConfig(name, db.template, `${label}.template`);
562
- }
563
-
564
- if (db.provider === "neon") {
565
- requireString(db, "projectId", `${label}.projectId`);
566
- requireString(db, "dbName", `${label}.dbName`);
567
- if (db.branchName !== undefined && typeof db.branchName !== "string") {
568
- throw new Error(`${label}.branchName must be a string`);
569
- }
570
- return;
571
- }
572
-
573
- if (db.image !== undefined && typeof db.image !== "string") {
574
- throw new Error(`${label}.image must be a string`);
575
- }
576
- if (db.user !== undefined && typeof db.user !== "string") {
577
- throw new Error(`${label}.user must be a string`);
578
- }
579
- if (db.password !== undefined && typeof db.password !== "string") {
580
- throw new Error(`${label}.password must be a string`);
581
- }
582
- }
583
-
584
- function validateTemplateConfig(name, template, label) {
585
- if (!isObject(template)) {
586
- throw new Error(`${label} must be an object`);
587
- }
588
- if (
589
- template.inputs !== undefined &&
590
- (!Array.isArray(template.inputs) || template.inputs.some((value) => typeof value !== "string"))
591
- ) {
592
- throw new Error(`${label}.inputs must be an array of strings`);
593
- }
594
- }
595
-
596
- function normalizeTemplateConfig(template) {
597
- return {
598
- inputs: Array.isArray(template?.inputs) ? [...template.inputs] : [],
599
- };
600
- }
601
-
602
- function validateLifecycleConfig(name, value, label) {
603
- if (!isObject(value)) {
604
- throw new Error(`Service "${name}" ${label} must be an object`);
605
- }
606
-
607
- if (value.cmd !== undefined) {
608
- requireString(value, "cmd", `Service "${name}" ${label}.cmd`);
609
- }
610
- if (value.cwd !== undefined && typeof value.cwd !== "string") {
611
- throw new Error(`Service "${name}" ${label}.cwd must be a string`);
612
- }
613
-
614
- if (value.backends !== undefined) {
615
- if (!isObject(value.backends)) {
616
- throw new Error(`Service "${name}" ${label}.backends must be an object`);
617
- }
618
- for (const [backendName, override] of Object.entries(value.backends)) {
619
- if (!isObject(override)) {
620
- throw new Error(
621
- `Service "${name}" ${label}.backends.${backendName} must be an object`
622
- );
623
- }
624
- if (override.cmd !== undefined) {
625
- requireString(override, "cmd", `Service "${name}" ${label}.backends.${backendName}.cmd`);
626
- }
627
- if (override.cwd !== undefined && typeof override.cwd !== "string") {
628
- throw new Error(
629
- `Service "${name}" ${label}.backends.${backendName}.cwd must be a string`
630
- );
631
- }
632
- }
633
- }
634
-
635
- if (value.cmd === undefined && value.backends === undefined) {
636
- throw new Error(`Service "${name}" ${label} must define cmd or backends`);
637
- }
638
- }
639
-
640
- function resolveLifecycleConfig(value, selectedBackend) {
641
- if (!value) return value;
642
-
643
- const override =
644
- selectedBackend && isObject(value.backends) ? value.backends[selectedBackend] || {} : {};
645
- const resolved = {
646
- ...value,
647
- ...override,
648
- };
649
- delete resolved.backends;
650
- return resolved;
651
- }
652
-
653
- function requireString(obj, key, label) {
654
- if (typeof obj[key] !== "string" || obj[key].length === 0) {
655
- throw new Error(`${label} must be a non-empty string`);
656
- }
657
- }
658
-
659
- function isDalSuiteType(suite, runnerService, suitesForType) {
660
- if (suite.framework && suite.framework !== "k6") return false;
661
- return suitesForType === runnerService.suites.dal;
662
- }
663
-
664
- function isObject(value) {
665
- return value !== null && typeof value === "object" && !Array.isArray(value);
666
- }
package/lib/exec.mjs DELETED
@@ -1,20 +0,0 @@
1
- import { execaCommand } from "execa";
2
- import { fileURLToPath } from "url";
3
- import path from "path";
4
-
5
- const INFRA_DIR = path.resolve(
6
- path.dirname(fileURLToPath(import.meta.url)), "../infra"
7
- );
8
-
9
- /**
10
- * Run a bundled infra script with the given env vars.
11
- * Inherits process.env, overlays scriptEnv, streams stdout/stderr.
12
- */
13
- export async function runScript(name, scriptEnv, opts = {}) {
14
- const script = path.join(INFRA_DIR, name);
15
- return execaCommand(`bash ${script}`, {
16
- env: { ...process.env, ...scriptEnv },
17
- stdio: "inherit",
18
- cwd: opts.cwd,
19
- });
20
- }