@elench/testkit 0.1.25 → 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 -79
- 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 +93 -0
- package/lib/package.test.mjs +29 -0
- package/lib/runner/index.mjs +3 -0
- package/lib/runner/results.mjs +49 -3
- package/lib/runner/results.test.mjs +24 -1
- package/lib/runner/template.mjs +1 -1
- package/lib/runtime/index.d.ts +183 -0
- 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 +14 -3
- package/lib/config/model.mjs +0 -408
- package/lib/config/model.test.mjs +0 -194
- package/lib/runtime-manager/index.mjs +0 -190
package/README.md
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# @elench/testkit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@elench/testkit` discovers `*.testkit.ts` files, infers suite ownership from the
|
|
4
|
+
filesystem, starts local services, provisions isolated local Postgres databases,
|
|
5
|
+
and runs HTTP, DAL, and Playwright suites.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
`@elench/testkit` ships its own execution engine for HTTP and DAL suites. Consumers do not need to install or invoke any separate load-testing binary.
|
|
8
|
-
|
|
9
|
-
Database isolation uses Docker-managed local Postgres containers. `testkit` creates and reuses template databases automatically, then clones per-worker databases from those templates for fast reruns. The default image is `pgvector/pgvector:pg16`.
|
|
7
|
+
The package is now driven by `testkit.setup.ts`, not `testkit.config.json`.
|
|
10
8
|
|
|
11
9
|
## Usage
|
|
12
10
|
|
|
@@ -30,14 +28,13 @@ npx @elench/testkit --jobs 3
|
|
|
30
28
|
|
|
31
29
|
# Run a deterministic shard
|
|
32
30
|
npx @elench/testkit --shard 1/3
|
|
33
|
-
npx @elench/testkit --jobs 2 --shard 2/3
|
|
34
31
|
|
|
35
32
|
# Specific service / suite
|
|
36
33
|
npx @elench/testkit frontend e2e -s auth
|
|
37
34
|
npx @elench/testkit api int -s health
|
|
38
35
|
|
|
39
36
|
# Exact file
|
|
40
|
-
npx @elench/testkit int --file
|
|
37
|
+
npx @elench/testkit int --file __testkit__/health/health.int.testkit.ts
|
|
41
38
|
|
|
42
39
|
# Deterministic git-trackable status snapshot
|
|
43
40
|
npx @elench/testkit int --write-status
|
|
@@ -47,15 +44,69 @@ npx @elench/testkit status
|
|
|
47
44
|
npx @elench/testkit destroy
|
|
48
45
|
```
|
|
49
46
|
|
|
47
|
+
## Setup
|
|
48
|
+
|
|
49
|
+
Create `testkit.setup.ts` at repo root:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import {
|
|
53
|
+
defineTestkitSetup,
|
|
54
|
+
lifecycle,
|
|
55
|
+
localDatabase,
|
|
56
|
+
nextService,
|
|
57
|
+
service,
|
|
58
|
+
tsxService,
|
|
59
|
+
} from "@elench/testkit/setup";
|
|
60
|
+
|
|
61
|
+
export default defineTestkitSetup({
|
|
62
|
+
services: {
|
|
63
|
+
api: service({
|
|
64
|
+
...tsxService({
|
|
65
|
+
cwd: ".",
|
|
66
|
+
entry: "src/index.ts",
|
|
67
|
+
port: 3004,
|
|
68
|
+
readyPath: "/health",
|
|
69
|
+
}),
|
|
70
|
+
envFiles: [".env.testkit"],
|
|
71
|
+
database: localDatabase(),
|
|
72
|
+
migrate: lifecycle("npm run db:migrate", {
|
|
73
|
+
testkitCmd: "npm run db:migrate:testkit",
|
|
74
|
+
}),
|
|
75
|
+
}),
|
|
76
|
+
frontend: service({
|
|
77
|
+
...nextService({
|
|
78
|
+
cwd: "frontend",
|
|
79
|
+
port: 3000,
|
|
80
|
+
env: {
|
|
81
|
+
NEXT_PUBLIC_API_URL: "{baseUrl:api}",
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
dependsOn: ["api"],
|
|
85
|
+
envFiles: ["frontend/.env.testkit"],
|
|
86
|
+
}),
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`testkit.setup.ts` is optional for simple repos, but it is the primary escape hatch
|
|
92
|
+
for:
|
|
93
|
+
|
|
94
|
+
- multi-service graphs
|
|
95
|
+
- local DB configuration
|
|
96
|
+
- migrate / seed commands
|
|
97
|
+
- test-local migrate / seed overrides
|
|
98
|
+
- named HTTP suite profiles
|
|
99
|
+
- telemetry upload configuration
|
|
100
|
+
|
|
50
101
|
## Authoring
|
|
51
102
|
|
|
52
|
-
|
|
103
|
+
HTTP suites:
|
|
53
104
|
|
|
54
|
-
```
|
|
105
|
+
```ts
|
|
55
106
|
import { defineHttpSuite } from "@elench/testkit";
|
|
56
107
|
|
|
57
108
|
const suite = defineHttpSuite(({ rawReq }) => {
|
|
58
|
-
|
|
109
|
+
rawReq("GET", "/health");
|
|
59
110
|
});
|
|
60
111
|
|
|
61
112
|
export const options = suite.options;
|
|
@@ -63,94 +114,72 @@ export const setup = suite.setup;
|
|
|
63
114
|
export default suite.exec;
|
|
64
115
|
```
|
|
65
116
|
|
|
66
|
-
|
|
67
|
-
to the tests and pass it into the generic suite factory:
|
|
117
|
+
Named HTTP profiles live in `testkit.setup.ts` and can be referenced by name:
|
|
68
118
|
|
|
69
|
-
```
|
|
119
|
+
```ts
|
|
70
120
|
import { defineHttpSuite } from "@elench/testkit";
|
|
71
|
-
import { clerkSessionAuth } from "../helpers/testkit-auth.js";
|
|
72
121
|
|
|
73
|
-
const suite = defineHttpSuite({
|
|
74
|
-
req("GET", "/api/auth/
|
|
122
|
+
const suite = defineHttpSuite({ profile: "default-auth" }, ({ req, setupData }) => {
|
|
123
|
+
req("GET", "/api/auth/session", setupData);
|
|
75
124
|
});
|
|
76
125
|
```
|
|
77
126
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
```js
|
|
81
|
-
import { check, group, http } from "@elench/testkit/runtime";
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
`testkit` bundles these imports before execution, so tests do not need
|
|
85
|
-
generated `_testkit` files, direct package-manager path imports, or any separate engine installation.
|
|
127
|
+
DAL suites:
|
|
86
128
|
|
|
87
|
-
|
|
129
|
+
```ts
|
|
130
|
+
import { defineDalSuite } from "@elench/testkit";
|
|
88
131
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
still exist, but direct package imports are now the preferred model.
|
|
94
|
-
|
|
95
|
-
From outside the product repo, use `--dir` explicitly:
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
npx @elench/testkit --dir my-product int
|
|
99
|
-
npx @elench/testkit --dir my-product api int -s health
|
|
132
|
+
const suite = defineDalSuite(({ db }) => {
|
|
133
|
+
db.query("select 1");
|
|
134
|
+
});
|
|
100
135
|
```
|
|
101
136
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
1. **Discovery** — reads `testkit.config.json` for services, then discovers files by suffix:
|
|
105
|
-
`*.int.testkit.ts`, `*.e2e.testkit.ts`, `*.dal.testkit.ts`, `*.load.testkit.ts`, `*.pw.testkit.ts`
|
|
106
|
-
2. **Config** — reads `testkit.config.json` for local runtime, migration, dependency, and database settings
|
|
107
|
-
Per-service `.env` files declared in config are loaded when present.
|
|
108
|
-
3. **Database** — provisions Docker-managed local Postgres when a service declares one
|
|
109
|
-
4. **Seed** — runs optional product seed commands against the provisioned database
|
|
110
|
-
5. **Runtime** — starts required local services, waits for readiness, and injects test env
|
|
111
|
-
6. **Execution** — schedules file-level execution tasks across a global worker pool, reuses warm dependency graphs when possible, bundles test files before execution so package imports resolve cleanly, and batches Playwright files per worker
|
|
112
|
-
7. **Cleanup** — stops local processes and preserves `.testkit/` state for inspection or later destroy
|
|
137
|
+
Low-level runtime primitives remain available:
|
|
113
138
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
139
|
+
```ts
|
|
140
|
+
import { check, group, http } from "@elench/testkit/runtime";
|
|
141
|
+
```
|
|
117
142
|
|
|
118
|
-
##
|
|
143
|
+
## Discovery
|
|
119
144
|
|
|
120
|
-
|
|
121
|
-
- `testkit.status.json`: optional deterministic run snapshot written by `--write-status`
|
|
145
|
+
`testkit` discovers suites from `__testkit__/` directories.
|
|
122
146
|
|
|
123
|
-
|
|
147
|
+
Example layouts:
|
|
124
148
|
|
|
125
|
-
- `
|
|
126
|
-
-
|
|
127
|
-
- `
|
|
128
|
-
- `
|
|
129
|
-
- `database.provider` for local Postgres settings
|
|
130
|
-
- `database.template.inputs` to define the local template cache invalidation inputs
|
|
131
|
-
- `migrate.backends` / `seed.backends` for optional local-only command overrides
|
|
149
|
+
- `src/api/routes/__testkit__/auth/me.int.testkit.ts`
|
|
150
|
+
- `src/db/__testkit__/sessions/count-type.dal.testkit.ts`
|
|
151
|
+
- `frontend/__testkit__/navigation/navigation.pw.testkit.ts`
|
|
152
|
+
- `avocado_api/internal/handler/__testkit__/repos/crud.int.testkit.ts`
|
|
132
153
|
|
|
133
|
-
|
|
154
|
+
`testkit` uses these suffixes automatically:
|
|
134
155
|
|
|
135
|
-
|
|
156
|
+
- `*.int.testkit.ts`
|
|
157
|
+
- `*.e2e.testkit.ts`
|
|
158
|
+
- `*.dal.testkit.ts`
|
|
159
|
+
- `*.load.testkit.ts`
|
|
160
|
+
- `*.pw.testkit.ts`
|
|
136
161
|
|
|
137
|
-
|
|
162
|
+
Ownership is inferred from:
|
|
138
163
|
|
|
139
|
-
|
|
164
|
+
- the deepest matching service root from `services.<name>.local.cwd`
|
|
165
|
+
- optional `services.<name>.discovery.roots` overrides for shared-root edge cases
|
|
140
166
|
|
|
141
|
-
|
|
142
|
-
- `.testkit` state subtree
|
|
143
|
-
- local service ports
|
|
167
|
+
Suite names are inferred from the colocated path:
|
|
144
168
|
|
|
145
|
-
|
|
169
|
+
- `auth/__testkit__/*.int.testkit.ts` => `auth`
|
|
170
|
+
- `routes/__testkit__/auth/*.int.testkit.ts` => `auth`
|
|
146
171
|
|
|
147
|
-
|
|
172
|
+
## Local Databases
|
|
148
173
|
|
|
149
|
-
|
|
174
|
+
`@elench/testkit` provisions Docker-managed local Postgres automatically for
|
|
175
|
+
services that define `database: localDatabase(...)`.
|
|
150
176
|
|
|
151
|
-
|
|
177
|
+
- template databases are cached
|
|
178
|
+
- worker databases are cloned from templates
|
|
179
|
+
- template fingerprints are derived automatically from env files, migrate/seed
|
|
180
|
+
config, and repo contents
|
|
152
181
|
|
|
153
|
-
|
|
182
|
+
Manual `template.inputs` overrides are still available for edge cases.
|
|
154
183
|
|
|
155
184
|
## Development Tests
|
|
156
185
|
|
|
@@ -160,8 +189,3 @@ npm run test:unit
|
|
|
160
189
|
npm run test:integration
|
|
161
190
|
npm run test:system
|
|
162
191
|
```
|
|
163
|
-
|
|
164
|
-
`test:system` runs real end-to-end fixtures from `test/fixtures/system/`. It requires:
|
|
165
|
-
|
|
166
|
-
- Docker with a running daemon
|
|
167
|
-
- Playwright Chromium browser assets available to `@playwright/test`
|
package/lib/bundler/index.mjs
CHANGED
|
@@ -4,9 +4,11 @@ import path from "path";
|
|
|
4
4
|
import crypto from "crypto";
|
|
5
5
|
import { build } from "esbuild";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
+
import { findSetupFile } from "../config/setup-loader.mjs";
|
|
7
8
|
|
|
8
9
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
9
10
|
const ROOT_ENTRY = path.join(PACKAGE_ROOT, "lib", "index.mjs");
|
|
11
|
+
const SETUP_ENTRY = path.join(PACKAGE_ROOT, "lib", "setup", "index.mjs");
|
|
10
12
|
const RUNTIME_ENTRY = path.join(PACKAGE_ROOT, "lib", "runtime", "index.mjs");
|
|
11
13
|
const bundleCache = new Map();
|
|
12
14
|
|
|
@@ -19,7 +21,8 @@ export async function bundleK6File({
|
|
|
19
21
|
const bundleDir = path.join(productDir, ".testkit", "_bundles", serviceName || "shared");
|
|
20
22
|
fs.mkdirSync(bundleDir, { recursive: true });
|
|
21
23
|
|
|
22
|
-
const
|
|
24
|
+
const setupFile = findSetupFile(productDir);
|
|
25
|
+
const cacheKey = await buildCacheKey(absoluteSource, setupFile);
|
|
23
26
|
const cached = bundleCache.get(cacheKey);
|
|
24
27
|
if (cached && fs.existsSync(cached)) {
|
|
25
28
|
return cached;
|
|
@@ -29,11 +32,19 @@ export async function bundleK6File({
|
|
|
29
32
|
bundleDir,
|
|
30
33
|
`${path.basename(sourceFile, path.extname(sourceFile))}-${cacheKey.slice(0, 12)}.js`
|
|
31
34
|
);
|
|
35
|
+
const entryFile = path.join(
|
|
36
|
+
bundleDir,
|
|
37
|
+
`${path.basename(sourceFile, path.extname(sourceFile))}-${cacheKey.slice(0, 12)}.entry.mjs`
|
|
38
|
+
);
|
|
39
|
+
fs.writeFileSync(entryFile, buildBundleEntryModule({
|
|
40
|
+
sourceFile: absoluteSource,
|
|
41
|
+
setupFile,
|
|
42
|
+
}));
|
|
32
43
|
|
|
33
44
|
await build({
|
|
34
|
-
absWorkingDir:
|
|
45
|
+
absWorkingDir: bundleDir,
|
|
35
46
|
bundle: true,
|
|
36
|
-
entryPoints: [
|
|
47
|
+
entryPoints: [entryFile],
|
|
37
48
|
format: "esm",
|
|
38
49
|
legalComments: "none",
|
|
39
50
|
outfile: outputFile,
|
|
@@ -51,17 +62,23 @@ export async function bundleK6File({
|
|
|
51
62
|
return outputFile;
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
async function buildCacheKey(sourceFile) {
|
|
65
|
+
async function buildCacheKey(sourceFile, setupFile = null) {
|
|
55
66
|
const source = await fs.promises.readFile(sourceFile, "utf8");
|
|
56
67
|
const packageJson = await fs.promises.readFile(path.join(PACKAGE_ROOT, "package.json"), "utf8");
|
|
57
|
-
|
|
68
|
+
const hash = crypto
|
|
58
69
|
.createHash("sha256")
|
|
59
70
|
.update(sourceFile)
|
|
60
71
|
.update("\0")
|
|
61
72
|
.update(source)
|
|
62
73
|
.update("\0")
|
|
63
|
-
.update(packageJson)
|
|
64
|
-
|
|
74
|
+
.update(packageJson);
|
|
75
|
+
|
|
76
|
+
if (setupFile && fs.existsSync(setupFile)) {
|
|
77
|
+
hash.update("\0").update(setupFile).update("\0");
|
|
78
|
+
hash.update(await fs.promises.readFile(setupFile, "utf8"));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return hash.digest("hex");
|
|
65
82
|
}
|
|
66
83
|
|
|
67
84
|
function testkitPackageAliasPlugin() {
|
|
@@ -81,7 +98,29 @@ function testkitPackageAliasPlugin() {
|
|
|
81
98
|
function resolvePackageSubpath(specifier) {
|
|
82
99
|
const subpath = specifier.slice("@elench/testkit".length);
|
|
83
100
|
if (!subpath) return ROOT_ENTRY;
|
|
101
|
+
if (subpath === "/setup") return SETUP_ENTRY;
|
|
84
102
|
if (subpath === "/runtime") return RUNTIME_ENTRY;
|
|
85
103
|
|
|
86
104
|
throw new Error(`Unsupported @elench/testkit import "${specifier}" in ${os.platform()}`);
|
|
87
105
|
}
|
|
106
|
+
|
|
107
|
+
function buildBundleEntryModule({ sourceFile, setupFile }) {
|
|
108
|
+
const sourceImport = JSON.stringify(sourceFile);
|
|
109
|
+
const setupRegistration = setupFile
|
|
110
|
+
? `
|
|
111
|
+
import * as repoSetupModule from ${JSON.stringify(setupFile)};
|
|
112
|
+
registerRepoSetup(repoSetupModule.default || repoSetupModule || null);
|
|
113
|
+
`
|
|
114
|
+
: `
|
|
115
|
+
registerRepoSetup(null);
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
return `
|
|
119
|
+
import { registerRepoSetup } from "@elench/testkit/setup";
|
|
120
|
+
import * as suiteModule from ${sourceImport};
|
|
121
|
+
${setupRegistration}
|
|
122
|
+
export const options = suiteModule.options;
|
|
123
|
+
export const setup = suiteModule.setup;
|
|
124
|
+
export default suiteModule.default;
|
|
125
|
+
`;
|
|
126
|
+
}
|
package/lib/cli/index.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { cac } from "cac";
|
|
2
|
-
import { loadConfigs
|
|
2
|
+
import { loadConfigs } from "../config/index.mjs";
|
|
3
3
|
import {
|
|
4
4
|
parseJobsOption,
|
|
5
5
|
parseShardOption,
|
|
6
|
-
RESERVED,
|
|
7
6
|
resolveRequestedFiles,
|
|
8
7
|
resolveCliSelection,
|
|
9
8
|
validateFrameworkOption,
|
|
@@ -13,33 +12,6 @@ import * as runner from "../runner/index.mjs";
|
|
|
13
12
|
export function run() {
|
|
14
13
|
const cli = cac("testkit");
|
|
15
14
|
|
|
16
|
-
cli
|
|
17
|
-
.command("runtime <action>", "Install or inspect the consumer runtime bundle")
|
|
18
|
-
.option("--dir <path>", "Explicit product directory")
|
|
19
|
-
.option("--path <path>", "Target runtime path relative to the product directory")
|
|
20
|
-
.option("--strict", "Exit non-zero when runtime status is missing or drifted")
|
|
21
|
-
.action(async (action, options) => {
|
|
22
|
-
if (!["install", "status", "update"].includes(action)) {
|
|
23
|
-
throw new Error('Unknown runtime action. Expected one of: install, status, update.');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const runtime = await import("../runtime-manager/index.mjs");
|
|
27
|
-
|
|
28
|
-
if (action === "status") {
|
|
29
|
-
const status = runtime.getRuntimeStatus(options);
|
|
30
|
-
console.log(runtime.formatRuntimeStatus(status));
|
|
31
|
-
if (options.strict && status.status !== "installed") {
|
|
32
|
-
process.exitCode = 1;
|
|
33
|
-
}
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const result = runtime.installRuntime(options);
|
|
38
|
-
console.log(
|
|
39
|
-
`Installed testkit runtime to ${result.relativeRuntimeDir} (${result.files.length} file${result.files.length === 1 ? "" : "s"})`
|
|
40
|
-
);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
15
|
cli
|
|
44
16
|
.command("[first] [second] [third]", "Run test suites (int, e2e, dal, all)")
|
|
45
17
|
.option("-s, --suite <name>", "Run specific suite(s)", { default: [] })
|
|
@@ -68,14 +40,13 @@ export function run() {
|
|
|
68
40
|
// testkit --dir my-product api int → one service, int
|
|
69
41
|
|
|
70
42
|
// Now resolve service vs type from remaining args
|
|
71
|
-
const
|
|
43
|
+
const allConfigs = await loadConfigs({ dir: options.dir });
|
|
44
|
+
const serviceNames = new Set(allConfigs.map((config) => config.name));
|
|
72
45
|
const { service, type } = resolveCliSelection({
|
|
73
46
|
first,
|
|
74
47
|
second,
|
|
75
48
|
serviceNames,
|
|
76
49
|
});
|
|
77
|
-
|
|
78
|
-
const allConfigs = loadConfigs({ dir: options.dir });
|
|
79
50
|
const configs = service
|
|
80
51
|
? allConfigs.filter((config) => config.name === service)
|
|
81
52
|
: allConfigs;
|