@elench/testkit 0.1.26 → 0.1.28
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 +108 -84
- package/lib/bundler/index.mjs +75 -7
- package/lib/bundler/index.test.mjs +61 -6
- 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/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,113 +44,145 @@ 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
|
-
export
|
|
62
|
-
export const setup = suite.setup;
|
|
63
|
-
export default suite.exec;
|
|
112
|
+
export default suite;
|
|
64
113
|
```
|
|
65
114
|
|
|
66
|
-
|
|
67
|
-
|
|
115
|
+
`testkit` suite files should default-export the suite object returned by
|
|
116
|
+
`defineHttpSuite(...)` or `defineDalSuite(...)`.
|
|
117
|
+
|
|
118
|
+
Named HTTP profiles live in `testkit.setup.ts` and can be referenced by name:
|
|
68
119
|
|
|
69
|
-
```
|
|
120
|
+
```ts
|
|
70
121
|
import { defineHttpSuite } from "@elench/testkit";
|
|
71
|
-
import { clerkSessionAuth } from "../helpers/testkit-auth.js";
|
|
72
122
|
|
|
73
|
-
const suite = defineHttpSuite({
|
|
74
|
-
req("GET", "/api/auth/
|
|
123
|
+
const suite = defineHttpSuite({ profile: "default-auth" }, ({ req, setupData }) => {
|
|
124
|
+
req("GET", "/api/auth/session", setupData);
|
|
75
125
|
});
|
|
76
126
|
```
|
|
77
127
|
|
|
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.
|
|
86
|
-
The published package also ships first-party TypeScript declarations for both
|
|
87
|
-
`@elench/testkit` and `@elench/testkit/runtime`, so consumer repos do not need
|
|
88
|
-
local ambient module shims for the supported authoring surface.
|
|
89
|
-
|
|
90
|
-
Legacy compatibility:
|
|
91
|
-
|
|
92
|
-
- `testkit runtime install`
|
|
93
|
-
- `testkit runtime status`
|
|
94
|
-
- `testkit runtime update`
|
|
128
|
+
DAL suites:
|
|
95
129
|
|
|
96
|
-
|
|
130
|
+
```ts
|
|
131
|
+
import { defineDalSuite } from "@elench/testkit";
|
|
97
132
|
|
|
98
|
-
|
|
133
|
+
const suite = defineDalSuite(({ db }) => {
|
|
134
|
+
db.query("select 1");
|
|
135
|
+
});
|
|
99
136
|
|
|
100
|
-
|
|
101
|
-
npx @elench/testkit --dir my-product int
|
|
102
|
-
npx @elench/testkit --dir my-product api int -s health
|
|
137
|
+
export default suite;
|
|
103
138
|
```
|
|
104
139
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
1. **Discovery** — reads `testkit.config.json` for services, then discovers files by suffix:
|
|
108
|
-
`*.int.testkit.ts`, `*.e2e.testkit.ts`, `*.dal.testkit.ts`, `*.load.testkit.ts`, `*.pw.testkit.ts`
|
|
109
|
-
2. **Config** — reads `testkit.config.json` for local runtime, migration, dependency, and database settings
|
|
110
|
-
Per-service `.env` files declared in config are loaded when present.
|
|
111
|
-
3. **Database** — provisions Docker-managed local Postgres when a service declares one
|
|
112
|
-
4. **Seed** — runs optional product seed commands against the provisioned database
|
|
113
|
-
5. **Runtime** — starts required local services, waits for readiness, and injects test env
|
|
114
|
-
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
|
|
115
|
-
7. **Cleanup** — stops local processes and preserves `.testkit/` state for inspection or later destroy
|
|
140
|
+
Low-level runtime primitives remain available:
|
|
116
141
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
142
|
+
```ts
|
|
143
|
+
import { check, group, http } from "@elench/testkit/runtime";
|
|
144
|
+
```
|
|
120
145
|
|
|
121
|
-
##
|
|
146
|
+
## Discovery
|
|
122
147
|
|
|
123
|
-
|
|
124
|
-
- `testkit.status.json`: optional deterministic run snapshot written by `--write-status`
|
|
148
|
+
`testkit` discovers suites from `__testkit__/` directories.
|
|
125
149
|
|
|
126
|
-
|
|
150
|
+
Example layouts:
|
|
127
151
|
|
|
128
|
-
- `
|
|
129
|
-
-
|
|
130
|
-
- `
|
|
131
|
-
- `
|
|
132
|
-
- `database.provider` for local Postgres settings
|
|
133
|
-
- `database.template.inputs` to define the local template cache invalidation inputs
|
|
134
|
-
- `migrate.backends` / `seed.backends` for optional local-only command overrides
|
|
152
|
+
- `src/api/routes/__testkit__/auth/me.int.testkit.ts`
|
|
153
|
+
- `src/db/__testkit__/sessions/count-type.dal.testkit.ts`
|
|
154
|
+
- `frontend/__testkit__/navigation/navigation.pw.testkit.ts`
|
|
155
|
+
- `avocado_api/internal/handler/__testkit__/repos/crud.int.testkit.ts`
|
|
135
156
|
|
|
136
|
-
|
|
157
|
+
`testkit` uses these suffixes automatically:
|
|
137
158
|
|
|
138
|
-
|
|
159
|
+
- `*.int.testkit.ts`
|
|
160
|
+
- `*.e2e.testkit.ts`
|
|
161
|
+
- `*.dal.testkit.ts`
|
|
162
|
+
- `*.load.testkit.ts`
|
|
163
|
+
- `*.pw.testkit.ts`
|
|
139
164
|
|
|
140
|
-
|
|
165
|
+
Ownership is inferred from:
|
|
141
166
|
|
|
142
|
-
|
|
167
|
+
- the deepest matching service root from `services.<name>.local.cwd`
|
|
168
|
+
- optional `services.<name>.discovery.roots` overrides for shared-root edge cases
|
|
143
169
|
|
|
144
|
-
|
|
145
|
-
- `.testkit` state subtree
|
|
146
|
-
- local service ports
|
|
170
|
+
Suite names are inferred from the colocated path:
|
|
147
171
|
|
|
148
|
-
|
|
172
|
+
- `auth/__testkit__/*.int.testkit.ts` => `auth`
|
|
173
|
+
- `routes/__testkit__/auth/*.int.testkit.ts` => `auth`
|
|
149
174
|
|
|
150
|
-
|
|
175
|
+
## Local Databases
|
|
151
176
|
|
|
152
|
-
|
|
177
|
+
`@elench/testkit` provisions Docker-managed local Postgres automatically for
|
|
178
|
+
services that define `database: localDatabase(...)`.
|
|
153
179
|
|
|
154
|
-
|
|
180
|
+
- template databases are cached
|
|
181
|
+
- worker databases are cloned from templates
|
|
182
|
+
- template fingerprints are derived automatically from env files, migrate/seed
|
|
183
|
+
config, and repo contents
|
|
155
184
|
|
|
156
|
-
|
|
185
|
+
Manual `template.inputs` overrides are still available for edge cases.
|
|
157
186
|
|
|
158
187
|
## Development Tests
|
|
159
188
|
|
|
@@ -163,8 +192,3 @@ npm run test:unit
|
|
|
163
192
|
npm run test:integration
|
|
164
193
|
npm run test:system
|
|
165
194
|
```
|
|
166
|
-
|
|
167
|
-
`test:system` runs real end-to-end fixtures from `test/fixtures/system/`. It requires:
|
|
168
|
-
|
|
169
|
-
- Docker with a running daemon
|
|
170
|
-
- 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,58 @@ 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
|
+
const suite = normalizeTestkitSuite(suiteModule);
|
|
123
|
+
export const options = suite.options;
|
|
124
|
+
export function setup(...args) {
|
|
125
|
+
return suite.setup(...args);
|
|
126
|
+
}
|
|
127
|
+
export default function exec(...args) {
|
|
128
|
+
return suite.exec(...args);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeTestkitSuite(module) {
|
|
132
|
+
const candidate = module?.default;
|
|
133
|
+
if (!candidate || typeof candidate !== "object") {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"testkit suite files must default-export the suite object returned by defineHttpSuite(...) or defineDalSuite(...). Example: export default defineHttpSuite(...) or const suite = defineHttpSuite(...); export default suite;"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (typeof candidate.exec !== "function") {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"testkit suite default export must expose an exec(setupData) function"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const setupFn = typeof candidate["setup"] === "function"
|
|
145
|
+
? (...args) => candidate.setup(...args)
|
|
146
|
+
: () => null;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
options: candidate["options"],
|
|
150
|
+
setup: setupFn,
|
|
151
|
+
exec: (...args) => candidate.exec(...args),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
4
5
|
import { afterEach, describe, expect, it } from "vitest";
|
|
5
6
|
import { bundleK6File } from "./index.mjs";
|
|
6
7
|
|
|
@@ -29,9 +30,7 @@ describe("runtime bundler", () => {
|
|
|
29
30
|
' "has status": (body) => typeof body.status === "string",',
|
|
30
31
|
" });",
|
|
31
32
|
"});",
|
|
32
|
-
"export
|
|
33
|
-
"export const setup = suite.setup;",
|
|
34
|
-
"export default suite.exec;",
|
|
33
|
+
"export default suite;",
|
|
35
34
|
"",
|
|
36
35
|
].join("\n")
|
|
37
36
|
);
|
|
@@ -60,9 +59,7 @@ describe("runtime bundler", () => {
|
|
|
60
59
|
"const suite = defineDalSuite(({ db }) => {",
|
|
61
60
|
' db.query("SELECT 1");',
|
|
62
61
|
"});",
|
|
63
|
-
"export
|
|
64
|
-
"export const setup = suite.setup;",
|
|
65
|
-
"export default suite.exec;",
|
|
62
|
+
"export default suite;",
|
|
66
63
|
"",
|
|
67
64
|
].join("\n")
|
|
68
65
|
);
|
|
@@ -77,4 +74,62 @@ describe("runtime bundler", () => {
|
|
|
77
74
|
expect(bundled).toContain("defineDalSuite");
|
|
78
75
|
expect(bundled).toContain('import sql from "k6/x/sql"');
|
|
79
76
|
});
|
|
77
|
+
|
|
78
|
+
it("normalizes a default-exported suite object with no setup override", async () => {
|
|
79
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-bundle-"));
|
|
80
|
+
cleanups.push(() => fs.rmSync(tmpDir, { force: true, recursive: true }));
|
|
81
|
+
|
|
82
|
+
const sourceFile = path.join(tmpDir, "no-setup.js");
|
|
83
|
+
fs.writeFileSync(
|
|
84
|
+
sourceFile,
|
|
85
|
+
[
|
|
86
|
+
"const suite = {",
|
|
87
|
+
" options: { vus: 1, iterations: 1 },",
|
|
88
|
+
" exec() {",
|
|
89
|
+
" return 'ok';",
|
|
90
|
+
" },",
|
|
91
|
+
"};",
|
|
92
|
+
"export default suite;",
|
|
93
|
+
"",
|
|
94
|
+
].join("\n")
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const bundledFile = await bundleK6File({
|
|
98
|
+
productDir: tmpDir,
|
|
99
|
+
serviceName: "api",
|
|
100
|
+
sourceFile,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const bundled = await import(`${pathToFileURL(bundledFile).href}?v=${Date.now()}`);
|
|
104
|
+
expect(typeof bundled.setup).toBe("function");
|
|
105
|
+
expect(bundled.setup()).toBeNull();
|
|
106
|
+
expect(typeof bundled.default).toBe("function");
|
|
107
|
+
expect(bundled.default()).toBe("ok");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("throws a clear error when the default export is not a suite object", async () => {
|
|
111
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-bundle-"));
|
|
112
|
+
cleanups.push(() => fs.rmSync(tmpDir, { force: true, recursive: true }));
|
|
113
|
+
|
|
114
|
+
const sourceFile = path.join(tmpDir, "legacy-shape.js");
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
sourceFile,
|
|
117
|
+
[
|
|
118
|
+
"export default function exec() {",
|
|
119
|
+
" return 'legacy';",
|
|
120
|
+
"}",
|
|
121
|
+
"",
|
|
122
|
+
].join("\n")
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const bundledFile = await bundleK6File({
|
|
126
|
+
productDir: tmpDir,
|
|
127
|
+
serviceName: "api",
|
|
128
|
+
sourceFile,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await expect(
|
|
132
|
+
import(`${pathToFileURL(bundledFile).href}?v=${Date.now()}`)
|
|
133
|
+
).rejects.toThrow(/default-export the suite object/);
|
|
134
|
+
});
|
|
80
135
|
});
|
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;
|