@elench/testkit 0.1.12 → 0.1.13
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 +6 -0
- package/lib/config.mjs +63 -2
- package/lib/runner.mjs +50 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ npx @elench/testkit destroy
|
|
|
48
48
|
|
|
49
49
|
1. **Discovery** — reads `runner.manifest.json` for services, suites, files, and frameworks
|
|
50
50
|
2. **Config** — reads `testkit.config.json` for local runtime, migration, dependency, and database settings
|
|
51
|
+
Per-service `.env` files declared in config are loaded when present.
|
|
51
52
|
3. **Database** — provisions a Neon branch when a service declares one
|
|
52
53
|
4. **Seed** — runs optional product seed commands against the provisioned database
|
|
53
54
|
5. **Runtime** — starts required local services, waits for readiness, and injects test env
|
|
@@ -59,6 +60,11 @@ npx @elench/testkit destroy
|
|
|
59
60
|
- `runner.manifest.json`: canonical test inventory
|
|
60
61
|
- `testkit.config.json`: local execution and provisioning config
|
|
61
62
|
|
|
63
|
+
`testkit.config.json` can also declare:
|
|
64
|
+
|
|
65
|
+
- `envFile` / `envFiles` for service-specific environment loading
|
|
66
|
+
- `databaseFrom` for dependency services that must share another service's `DATABASE_URL`
|
|
67
|
+
|
|
62
68
|
## Parallel execution
|
|
63
69
|
|
|
64
70
|
`@elench/testkit` can run suites in parallel with `--jobs <n>`.
|
package/lib/config.mjs
CHANGED
|
@@ -68,13 +68,17 @@ export function loadConfigs(opts = {}) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
validateMergedService(name, runnerService, serviceConfig, productDir);
|
|
71
|
+
const serviceEnv = loadServiceEnv(productDir, serviceConfig);
|
|
71
72
|
|
|
72
73
|
return {
|
|
73
74
|
name,
|
|
74
75
|
productDir,
|
|
75
76
|
stateDir: path.join(productDir, ".testkit", name),
|
|
76
77
|
suites: runnerService.suites,
|
|
77
|
-
testkit:
|
|
78
|
+
testkit: {
|
|
79
|
+
...serviceConfig,
|
|
80
|
+
serviceEnv,
|
|
81
|
+
},
|
|
78
82
|
};
|
|
79
83
|
});
|
|
80
84
|
}
|
|
@@ -228,6 +232,20 @@ function validateConfigCoverage(runner, config) {
|
|
|
228
232
|
);
|
|
229
233
|
}
|
|
230
234
|
}
|
|
235
|
+
|
|
236
|
+
const databaseFrom = config.services[serviceName].databaseFrom;
|
|
237
|
+
if (databaseFrom) {
|
|
238
|
+
if (!config.services[databaseFrom]) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Service "${serviceName}" databaseFrom "${databaseFrom}" is missing from ${TESTKIT_CONFIG}`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
if (!runner.services[databaseFrom]) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`Service "${serviceName}" databaseFrom "${databaseFrom}" is missing from ${RUNNER_MANIFEST}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
231
249
|
}
|
|
232
250
|
}
|
|
233
251
|
|
|
@@ -263,7 +281,11 @@ export function isSiblingProduct(name) {
|
|
|
263
281
|
|
|
264
282
|
function validateMergedService(name, runnerService, serviceConfig, productDir) {
|
|
265
283
|
const usesLocalExecution = Object.values(runnerService.suites).some((suites) =>
|
|
266
|
-
suites.some(
|
|
284
|
+
suites.some(
|
|
285
|
+
(suite) =>
|
|
286
|
+
(suite.framework && suite.framework !== "k6") ||
|
|
287
|
+
!isDalSuiteType(suite, runnerService, suites)
|
|
288
|
+
)
|
|
267
289
|
);
|
|
268
290
|
|
|
269
291
|
if (usesLocalExecution && !isObject(serviceConfig.local)) {
|
|
@@ -306,6 +328,7 @@ function validateMergedService(name, runnerService, serviceConfig, productDir) {
|
|
|
306
328
|
);
|
|
307
329
|
}
|
|
308
330
|
}
|
|
331
|
+
|
|
309
332
|
}
|
|
310
333
|
|
|
311
334
|
function validateServiceConfig(name, service, configPath) {
|
|
@@ -319,6 +342,27 @@ function validateServiceConfig(name, service, configPath) {
|
|
|
319
342
|
}
|
|
320
343
|
}
|
|
321
344
|
|
|
345
|
+
if (service.databaseFrom !== undefined && typeof service.databaseFrom !== "string") {
|
|
346
|
+
throw new Error(`Service "${name}" databaseFrom must be a string`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (service.database !== undefined && service.databaseFrom !== undefined) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`Service "${name}" cannot define both database and databaseFrom`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (service.envFile !== undefined && typeof service.envFile !== "string") {
|
|
356
|
+
throw new Error(`Service "${name}" envFile must be a string`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (
|
|
360
|
+
service.envFiles !== undefined &&
|
|
361
|
+
(!Array.isArray(service.envFiles) || service.envFiles.some((v) => typeof v !== "string"))
|
|
362
|
+
) {
|
|
363
|
+
throw new Error(`Service "${name}" envFiles must be an array of strings`);
|
|
364
|
+
}
|
|
365
|
+
|
|
322
366
|
if (service.database !== undefined) {
|
|
323
367
|
if (!isObject(service.database)) {
|
|
324
368
|
throw new Error(`Service "${name}" database must be an object`);
|
|
@@ -385,6 +429,23 @@ function validateServiceConfig(name, service, configPath) {
|
|
|
385
429
|
}
|
|
386
430
|
}
|
|
387
431
|
|
|
432
|
+
function loadServiceEnv(productDir, serviceConfig) {
|
|
433
|
+
const env = {};
|
|
434
|
+
for (const envFile of getServiceEnvFiles(serviceConfig)) {
|
|
435
|
+
Object.assign(env, parseDotenv(resolveServiceCwd(productDir, envFile)));
|
|
436
|
+
}
|
|
437
|
+
return env;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function getServiceEnvFiles(serviceConfig) {
|
|
441
|
+
const files = [];
|
|
442
|
+
if (serviceConfig.envFile) files.push(serviceConfig.envFile);
|
|
443
|
+
if (Array.isArray(serviceConfig.envFiles)) {
|
|
444
|
+
files.push(...serviceConfig.envFiles);
|
|
445
|
+
}
|
|
446
|
+
return files;
|
|
447
|
+
}
|
|
448
|
+
|
|
388
449
|
function requireString(obj, key, label) {
|
|
389
450
|
if (typeof obj[key] !== "string" || obj[key].length === 0) {
|
|
390
451
|
throw new Error(`${label} must be a non-empty string`);
|
package/lib/runner.mjs
CHANGED
|
@@ -340,7 +340,7 @@ function resolveWorkerConfig(
|
|
|
340
340
|
readyUrlByService,
|
|
341
341
|
urlMappings
|
|
342
342
|
) {
|
|
343
|
-
const stateDir =
|
|
343
|
+
const stateDir = resolveServiceStateDir(workerStateDir, targetConfig.name, config);
|
|
344
344
|
const context = {
|
|
345
345
|
workerId,
|
|
346
346
|
serviceName: config.name,
|
|
@@ -476,7 +476,7 @@ async function runMigrations(runtimeConfigs) {
|
|
|
476
476
|
const migrate = config.testkit.migrate;
|
|
477
477
|
if (!migrate) continue;
|
|
478
478
|
|
|
479
|
-
const env =
|
|
479
|
+
const env = buildExecutionEnv(config);
|
|
480
480
|
const dbUrl = readDatabaseUrl(config.stateDir);
|
|
481
481
|
if (dbUrl) env.DATABASE_URL = dbUrl;
|
|
482
482
|
|
|
@@ -495,7 +495,7 @@ async function runSeeds(runtimeConfigs) {
|
|
|
495
495
|
const seed = config.testkit.seed;
|
|
496
496
|
if (!seed) continue;
|
|
497
497
|
|
|
498
|
-
const env =
|
|
498
|
+
const env = buildExecutionEnv(config);
|
|
499
499
|
const dbUrl = readDatabaseUrl(config.stateDir);
|
|
500
500
|
if (dbUrl) env.DATABASE_URL = dbUrl;
|
|
501
501
|
|
|
@@ -528,10 +528,7 @@ async function startLocalServices(runtimeConfigs) {
|
|
|
528
528
|
|
|
529
529
|
async function startLocalService(config) {
|
|
530
530
|
const cwd = resolveServiceCwd(config.productDir, config.testkit.local.cwd);
|
|
531
|
-
const env =
|
|
532
|
-
...process.env,
|
|
533
|
-
...config.testkit.local.env,
|
|
534
|
-
};
|
|
531
|
+
const env = buildExecutionEnv(config, config.testkit.local.env);
|
|
535
532
|
const port = config.testkit.local.port || numericPortFromUrl(config.testkit.local.baseUrl);
|
|
536
533
|
if (port) {
|
|
537
534
|
env.PORT = String(port);
|
|
@@ -603,7 +600,7 @@ async function runHttpK6Suite(targetConfig, suite) {
|
|
|
603
600
|
try {
|
|
604
601
|
await execa("k6", ["run", "-e", `BASE_URL=${baseUrl}`, absFile], {
|
|
605
602
|
cwd: targetConfig.productDir,
|
|
606
|
-
env:
|
|
603
|
+
env: buildExecutionEnv(targetConfig),
|
|
607
604
|
stdio: "inherit",
|
|
608
605
|
});
|
|
609
606
|
} catch {
|
|
@@ -628,7 +625,7 @@ async function runDalSuite(targetConfig, suite) {
|
|
|
628
625
|
try {
|
|
629
626
|
await execa(k6Binary, ["run", "-e", `DATABASE_URL=${databaseUrl}`, absFile], {
|
|
630
627
|
cwd: targetConfig.productDir,
|
|
631
|
-
env:
|
|
628
|
+
env: buildExecutionEnv(targetConfig),
|
|
632
629
|
stdio: "inherit",
|
|
633
630
|
});
|
|
634
631
|
} catch {
|
|
@@ -655,13 +652,7 @@ async function runPlaywrightSuite(targetConfig, suite) {
|
|
|
655
652
|
try {
|
|
656
653
|
await execa("npx", ["playwright", "test", ...files], {
|
|
657
654
|
cwd,
|
|
658
|
-
env:
|
|
659
|
-
...process.env,
|
|
660
|
-
BASE_URL: local.baseUrl,
|
|
661
|
-
PLAYWRIGHT_HTML_OPEN: "never",
|
|
662
|
-
TESTKIT_MANAGED_SERVERS: "1",
|
|
663
|
-
TESTKIT_WORKER_ID: String(targetConfig.workerId),
|
|
664
|
-
},
|
|
655
|
+
env: buildPlaywrightEnv(targetConfig, local.baseUrl),
|
|
665
656
|
stdio: "inherit",
|
|
666
657
|
});
|
|
667
658
|
return { failed: false };
|
|
@@ -716,6 +707,11 @@ function needsLocalRuntime(suites) {
|
|
|
716
707
|
return suites.some((suite) => suite.type !== "dal");
|
|
717
708
|
}
|
|
718
709
|
|
|
710
|
+
function resolveServiceStateDir(workerStateDir, targetName, config) {
|
|
711
|
+
const dbSource = config.testkit.databaseFrom || config.name;
|
|
712
|
+
return getWorkerServiceStateDir(workerStateDir, targetName, dbSource);
|
|
713
|
+
}
|
|
714
|
+
|
|
719
715
|
function getWorkerServiceStateDir(workerStateDir, targetName, serviceName) {
|
|
720
716
|
if (targetName === serviceName) {
|
|
721
717
|
return workerStateDir;
|
|
@@ -723,6 +719,44 @@ function getWorkerServiceStateDir(workerStateDir, targetName, serviceName) {
|
|
|
723
719
|
return path.join(workerStateDir, "deps", serviceName);
|
|
724
720
|
}
|
|
725
721
|
|
|
722
|
+
function buildExecutionEnv(config, extraEnv = {}) {
|
|
723
|
+
return {
|
|
724
|
+
...process.env,
|
|
725
|
+
...(config.testkit.serviceEnv || {}),
|
|
726
|
+
...extraEnv,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function buildPlaywrightEnv(config, baseUrl) {
|
|
731
|
+
const env = buildExecutionEnv(config, {
|
|
732
|
+
BASE_URL: baseUrl,
|
|
733
|
+
PLAYWRIGHT_HTML_OPEN: "never",
|
|
734
|
+
PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS:
|
|
735
|
+
process.env.PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS || "1",
|
|
736
|
+
TESTKIT_MANAGED_SERVERS: "1",
|
|
737
|
+
TESTKIT_WORKER_ID: String(config.workerId),
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
const browsersPath = resolvePlaywrightBrowsersPath(env.PLAYWRIGHT_BROWSERS_PATH);
|
|
741
|
+
if (browsersPath) {
|
|
742
|
+
env.PLAYWRIGHT_BROWSERS_PATH = browsersPath;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return env;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function resolvePlaywrightBrowsersPath(configuredPath) {
|
|
749
|
+
const home = process.env.HOME;
|
|
750
|
+
if (home) {
|
|
751
|
+
const fallback = path.join(home, ".cache", "ms-playwright");
|
|
752
|
+
if (fs.existsSync(fallback)) {
|
|
753
|
+
return fallback;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return configuredPath;
|
|
758
|
+
}
|
|
759
|
+
|
|
726
760
|
function readDatabaseUrl(stateDir) {
|
|
727
761
|
return readStateValue(path.join(stateDir, "database_url"));
|
|
728
762
|
}
|