@elench/testkit 0.1.10 → 0.1.12
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 +62 -30
- package/lib/cli.mjs +55 -3
- package/lib/config.mjs +295 -176
- package/lib/runner.mjs +874 -362
- package/package.json +4 -3
- package/infra/fly-app-ensure.sh +0 -23
- package/infra/fly-build.sh +0 -55
- package/infra/fly-destroy.sh +0 -21
- package/infra/fly-down.sh +0 -19
- package/infra/fly-up.sh +0 -142
package/README.md
CHANGED
|
@@ -1,44 +1,43 @@
|
|
|
1
1
|
# @elench/testkit
|
|
2
2
|
|
|
3
|
-
CLI that reads `
|
|
3
|
+
CLI that reads `runner.manifest.json` plus `testkit.config.json` from a product repo, starts the required local services, provisions Neon branches when configured, and runs manifest-defined suites across `k6` and Playwright.
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
sudo snap install k6
|
|
9
9
|
sudo apt-get install -y jq
|
|
10
|
-
curl -L https://fly.io/install.sh | sh
|
|
11
|
-
fly auth login
|
|
12
10
|
```
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Add platform secrets to each product's `.env`:
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
NEON_API_KEY='...'
|
|
20
|
-
FLY_API_TOKEN='...'
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Product secrets (`CLERK_SECRET_KEY`, etc.) are also loaded from the same `.env`.
|
|
12
|
+
For DAL suites, `@elench/testkit` ships its own `k6` SQL binary. For suites using a Neon branch, set `NEON_API_KEY` in the product `.env`, shell, or `.envrc`.
|
|
24
13
|
|
|
25
14
|
## Usage
|
|
26
15
|
|
|
27
16
|
```bash
|
|
28
17
|
cd bourne
|
|
29
18
|
|
|
30
|
-
# Run
|
|
19
|
+
# Run every testkit-managed suite
|
|
31
20
|
npx @elench/testkit
|
|
32
21
|
|
|
33
|
-
#
|
|
34
|
-
npx @elench/testkit int
|
|
22
|
+
# Filter by type
|
|
23
|
+
npx @elench/testkit int
|
|
24
|
+
npx @elench/testkit dal
|
|
35
25
|
npx @elench/testkit e2e
|
|
36
26
|
|
|
37
|
-
#
|
|
38
|
-
npx @elench/testkit
|
|
27
|
+
# Filter by framework
|
|
28
|
+
npx @elench/testkit --framework playwright
|
|
29
|
+
npx @elench/testkit --framework k6
|
|
39
30
|
|
|
40
|
-
#
|
|
41
|
-
npx @elench/testkit
|
|
31
|
+
# Parallelize with isolated worker stacks
|
|
32
|
+
npx @elench/testkit --jobs 3
|
|
33
|
+
|
|
34
|
+
# Run a deterministic shard
|
|
35
|
+
npx @elench/testkit --shard 1/3
|
|
36
|
+
npx @elench/testkit --jobs 2 --shard 2/3
|
|
37
|
+
|
|
38
|
+
# Specific service / suite
|
|
39
|
+
npx @elench/testkit frontend e2e -s auth
|
|
40
|
+
npx @elench/testkit bourne int -s health
|
|
42
41
|
|
|
43
42
|
# Lifecycle
|
|
44
43
|
npx @elench/testkit status
|
|
@@ -47,17 +46,50 @@ npx @elench/testkit destroy
|
|
|
47
46
|
|
|
48
47
|
## How it works
|
|
49
48
|
|
|
50
|
-
1. **
|
|
51
|
-
2. **
|
|
52
|
-
3. **
|
|
53
|
-
4. **
|
|
54
|
-
5. **
|
|
55
|
-
6. **
|
|
49
|
+
1. **Discovery** — reads `runner.manifest.json` for services, suites, files, and frameworks
|
|
50
|
+
2. **Config** — reads `testkit.config.json` for local runtime, migration, dependency, and database settings
|
|
51
|
+
3. **Database** — provisions a Neon branch when a service declares one
|
|
52
|
+
4. **Seed** — runs optional product seed commands against the provisioned database
|
|
53
|
+
5. **Runtime** — starts required local services, waits for readiness, and injects test env
|
|
54
|
+
6. **Execution** — distributes suites across isolated worker stacks, runs `k6` suites file-by-file, and runs Playwright suites suite-by-suite
|
|
55
|
+
7. **Cleanup** — stops local processes and preserves `.testkit/` state for inspection or later destroy
|
|
56
|
+
|
|
57
|
+
## File roles
|
|
58
|
+
|
|
59
|
+
- `runner.manifest.json`: canonical test inventory
|
|
60
|
+
- `testkit.config.json`: local execution and provisioning config
|
|
56
61
|
|
|
57
|
-
|
|
62
|
+
## Parallel execution
|
|
63
|
+
|
|
64
|
+
`@elench/testkit` can run suites in parallel with `--jobs <n>`.
|
|
65
|
+
|
|
66
|
+
Each worker gets its own:
|
|
67
|
+
- Neon branch
|
|
68
|
+
- `.testkit` state subtree
|
|
69
|
+
- local service ports
|
|
70
|
+
|
|
71
|
+
This keeps suites isolated while still reusing one stack per worker across multiple assigned suites.
|
|
72
|
+
|
|
73
|
+
Use `--shard <i/n>` to split the selected suite set deterministically before worker scheduling.
|
|
74
|
+
|
|
75
|
+
## Suite metadata
|
|
76
|
+
|
|
77
|
+
`runner.manifest.json` remains the source of truth for suites. Optional per-suite `testkit` metadata can tune execution:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"name": "health",
|
|
82
|
+
"files": ["tests/example.js"],
|
|
83
|
+
"testkit": {
|
|
84
|
+
"maxFileConcurrency": 2,
|
|
85
|
+
"weight": 3
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
58
89
|
|
|
59
|
-
|
|
90
|
+
- `maxFileConcurrency`: k6-only opt-in for running files within the suite concurrently
|
|
91
|
+
- `weight`: optional scheduling weight when distributing suites across workers
|
|
60
92
|
|
|
61
|
-
##
|
|
93
|
+
## Schema
|
|
62
94
|
|
|
63
|
-
See [testkit-
|
|
95
|
+
See [testkit-config-schema.md](testkit-config-schema.md).
|
package/lib/cli.mjs
CHANGED
|
@@ -12,8 +12,14 @@ export function run() {
|
|
|
12
12
|
cli
|
|
13
13
|
.command("[first] [second] [third]", "Run test suites (int, e2e, dal, all)")
|
|
14
14
|
.option("-s, --suite <name>", "Run specific suite(s)", { default: [] })
|
|
15
|
-
.option("--build", "Build image from source first", { default: false })
|
|
16
15
|
.option("--dir <path>", "Explicit product directory")
|
|
16
|
+
.option("--jobs <n>", "Number of isolated worker stacks per service", {
|
|
17
|
+
default: "1",
|
|
18
|
+
})
|
|
19
|
+
.option("--shard <i/n>", "Run only shard i of n at suite granularity")
|
|
20
|
+
.option("--framework <name>", "Filter by framework (k6, playwright, all)", {
|
|
21
|
+
default: "all",
|
|
22
|
+
})
|
|
17
23
|
.action(async (first, second, third, options) => {
|
|
18
24
|
// Resolve: service filter, suite type, and --dir.
|
|
19
25
|
//
|
|
@@ -57,7 +63,14 @@ export function run() {
|
|
|
57
63
|
);
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
const
|
|
66
|
+
const allConfigs = loadConfigs({ dir: options.dir });
|
|
67
|
+
const configs = service
|
|
68
|
+
? allConfigs.filter((config) => config.name === service)
|
|
69
|
+
: allConfigs;
|
|
70
|
+
if (service && configs.length === 0) {
|
|
71
|
+
const available = allConfigs.map((config) => config.name).join(", ");
|
|
72
|
+
throw new Error(`Service "${service}" not found. Available: ${available}`);
|
|
73
|
+
}
|
|
61
74
|
|
|
62
75
|
// Lifecycle commands
|
|
63
76
|
if (type === "status" || type === "destroy") {
|
|
@@ -69,9 +82,48 @@ export function run() {
|
|
|
69
82
|
return;
|
|
70
83
|
}
|
|
71
84
|
|
|
85
|
+
if (!["all", "k6", "playwright"].includes(options.framework)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Unknown framework "${options.framework}". Expected one of: all, k6, playwright.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const jobs = Number.parseInt(String(options.jobs), 10);
|
|
92
|
+
if (!Number.isInteger(jobs) || jobs <= 0) {
|
|
93
|
+
throw new Error(`Invalid --jobs value "${options.jobs}". Expected a positive integer.`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let shard = null;
|
|
97
|
+
if (options.shard) {
|
|
98
|
+
const match = String(options.shard).match(/^(\d+)\/(\d+)$/);
|
|
99
|
+
if (!match) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Invalid --shard value "${options.shard}". Expected the form "i/n", e.g. 1/3.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const index = Number.parseInt(match[1], 10);
|
|
105
|
+
const total = Number.parseInt(match[2], 10);
|
|
106
|
+
if (index <= 0 || total <= 0 || index > total) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Invalid --shard value "${options.shard}". Expected 1 <= i <= n.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
shard = { index, total };
|
|
112
|
+
}
|
|
113
|
+
|
|
72
114
|
const suiteType = type || "all";
|
|
73
115
|
const suiteNames = Array.isArray(options.suite) ? options.suite : [options.suite].filter(Boolean);
|
|
74
|
-
await runner.runAll(
|
|
116
|
+
await runner.runAll(
|
|
117
|
+
configs,
|
|
118
|
+
suiteType,
|
|
119
|
+
suiteNames,
|
|
120
|
+
{
|
|
121
|
+
...options,
|
|
122
|
+
jobs,
|
|
123
|
+
shard,
|
|
124
|
+
},
|
|
125
|
+
allConfigs
|
|
126
|
+
);
|
|
75
127
|
});
|
|
76
128
|
|
|
77
129
|
cli.help();
|