@elench/testkit 0.1.22 → 0.1.23
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 +12 -13
- package/lib/bundler/index.mjs +2 -10
- package/lib/bundler/index.test.mjs +8 -7
- package/lib/cli/args.mjs +3 -2
- package/lib/cli/args.test.mjs +2 -1
- package/lib/cli/index.mjs +6 -4
- package/lib/index.mjs +13 -1
- package/lib/runner/index.mjs +20 -4
- package/lib/runner/results.mjs +16 -5
- package/lib/runner/results.test.mjs +2 -3
- package/lib/runtime/index.mjs +31 -190
- package/lib/runtime-manager/index.mjs +190 -0
- package/package.json +3 -4
- package/lib/k6/checks.mjs +0 -1
- package/lib/k6/dal-suite.mjs +0 -1
- package/lib/k6/dal.mjs +0 -1
- package/lib/k6/http.mjs +0 -1
- package/lib/k6/index.mjs +0 -30
- package/lib/k6/suite.mjs +0 -1
package/README.md
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# @elench/testkit
|
|
2
2
|
|
|
3
|
-
CLI that reads `testkit.config.json` from a product repo, discovers `*.testkit.ts` files by convention, starts the required local services, provisions local Postgres isolation, and runs
|
|
3
|
+
CLI that reads `testkit.config.json` from a product repo, discovers `*.testkit.ts` files by convention, starts the required local services, provisions local Postgres isolation, and runs HTTP, DAL, and Playwright suites.
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
7
|
-
`@elench/testkit` ships its own
|
|
8
|
-
If you need to force a different binary, set `TESTKIT_K6_BIN` to an absolute or relative path.
|
|
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.
|
|
9
8
|
|
|
10
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`.
|
|
11
10
|
|
|
@@ -24,7 +23,7 @@ npx @elench/testkit e2e
|
|
|
24
23
|
|
|
25
24
|
# Filter by framework
|
|
26
25
|
npx @elench/testkit --framework playwright
|
|
27
|
-
npx @elench/testkit --framework
|
|
26
|
+
npx @elench/testkit --framework default
|
|
28
27
|
|
|
29
28
|
# Parallelize with isolated worker stacks
|
|
30
29
|
npx @elench/testkit --jobs 3
|
|
@@ -48,9 +47,9 @@ npx @elench/testkit status
|
|
|
48
47
|
npx @elench/testkit destroy
|
|
49
48
|
```
|
|
50
49
|
|
|
51
|
-
##
|
|
50
|
+
## Authoring
|
|
52
51
|
|
|
53
|
-
Consumer
|
|
52
|
+
Consumer tests can import the shared authoring API directly from the package:
|
|
54
53
|
|
|
55
54
|
```js
|
|
56
55
|
import { defineHttpSuite } from "@elench/testkit";
|
|
@@ -76,15 +75,15 @@ const suite = defineHttpSuite({ auth: clerkSessionAuth }, ({ req, setupData }) =
|
|
|
76
75
|
});
|
|
77
76
|
```
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
generated `_testkit` files or direct package-manager path imports.
|
|
78
|
+
Low-level runtime primitives are also available without exposing the underlying engine:
|
|
81
79
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
TESTKIT_K6_BIN=/path/to/k6 npx @elench/testkit int
|
|
80
|
+
```js
|
|
81
|
+
import { check, group, http } from "@elench/testkit/runtime";
|
|
86
82
|
```
|
|
87
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
|
+
|
|
88
87
|
Legacy compatibility:
|
|
89
88
|
|
|
90
89
|
- `testkit runtime install`
|
|
@@ -109,7 +108,7 @@ npx @elench/testkit --dir my-product api int -s health
|
|
|
109
108
|
3. **Database** — provisions Docker-managed local Postgres when a service declares one
|
|
110
109
|
4. **Seed** — runs optional product seed commands against the provisioned database
|
|
111
110
|
5. **Runtime** — starts required local services, waits for readiness, and injects test env
|
|
112
|
-
6. **Execution** — schedules file-level execution tasks across a global worker pool, reuses warm dependency graphs when possible, bundles
|
|
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
|
|
113
112
|
7. **Cleanup** — stops local processes and preserves `.testkit/` state for inspection or later destroy
|
|
114
113
|
|
|
115
114
|
Each run ends with a compact summary that shows per-service pass/fail status, suite totals, failed suites, and total duration.
|
package/lib/bundler/index.mjs
CHANGED
|
@@ -7,8 +7,7 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
|
|
8
8
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
9
9
|
const ROOT_ENTRY = path.join(PACKAGE_ROOT, "lib", "index.mjs");
|
|
10
|
-
const
|
|
11
|
-
const K6_DIR = path.join(PACKAGE_ROOT, "lib", "k6");
|
|
10
|
+
const RUNTIME_ENTRY = path.join(PACKAGE_ROOT, "lib", "runtime", "index.mjs");
|
|
12
11
|
const bundleCache = new Map();
|
|
13
12
|
|
|
14
13
|
export async function bundleK6File({
|
|
@@ -82,14 +81,7 @@ function testkitPackageAliasPlugin() {
|
|
|
82
81
|
function resolvePackageSubpath(specifier) {
|
|
83
82
|
const subpath = specifier.slice("@elench/testkit".length);
|
|
84
83
|
if (!subpath) return ROOT_ENTRY;
|
|
85
|
-
if (subpath === "/
|
|
86
|
-
if (subpath.startsWith("/k6/")) {
|
|
87
|
-
const rel = subpath.slice("/k6/".length);
|
|
88
|
-
const candidate = path.join(K6_DIR, `${rel}.mjs`);
|
|
89
|
-
if (fs.existsSync(candidate)) {
|
|
90
|
-
return candidate;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
84
|
+
if (subpath === "/runtime") return RUNTIME_ENTRY;
|
|
93
85
|
|
|
94
86
|
throw new Error(`Unsupported @elench/testkit import "${specifier}" in ${os.platform()}`);
|
|
95
87
|
}
|
|
@@ -12,8 +12,8 @@ afterEach(() => {
|
|
|
12
12
|
}
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
describe("
|
|
16
|
-
it("bundles root package imports for
|
|
15
|
+
describe("runtime bundler", () => {
|
|
16
|
+
it("bundles root and runtime package imports for execution", async () => {
|
|
17
17
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-bundle-"));
|
|
18
18
|
cleanups.push(() => fs.rmSync(tmpDir, { force: true, recursive: true }));
|
|
19
19
|
|
|
@@ -21,8 +21,8 @@ describe("k6 bundler", () => {
|
|
|
21
21
|
fs.writeFileSync(
|
|
22
22
|
sourceFile,
|
|
23
23
|
[
|
|
24
|
-
'import { defineHttpSuite
|
|
25
|
-
'import { check } from "
|
|
24
|
+
'import { defineHttpSuite } from "@elench/testkit";',
|
|
25
|
+
'import { check, json } from "@elench/testkit/runtime";',
|
|
26
26
|
"const suite = defineHttpSuite(({ rawReq }) => {",
|
|
27
27
|
' const res = rawReq("GET", "/health");',
|
|
28
28
|
" check(json(res), {",
|
|
@@ -44,10 +44,11 @@ describe("k6 bundler", () => {
|
|
|
44
44
|
|
|
45
45
|
const bundled = fs.readFileSync(bundledFile, "utf8");
|
|
46
46
|
expect(bundled).toContain("defineHttpSuite");
|
|
47
|
-
expect(bundled).toContain('import { check
|
|
47
|
+
expect(bundled).toContain('import { check');
|
|
48
|
+
expect(bundled).toContain('from "k6"');
|
|
48
49
|
});
|
|
49
50
|
|
|
50
|
-
it("bundles
|
|
51
|
+
it("bundles DAL execution through the public package surface", async () => {
|
|
51
52
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-bundle-"));
|
|
52
53
|
cleanups.push(() => fs.rmSync(tmpDir, { force: true, recursive: true }));
|
|
53
54
|
|
|
@@ -55,7 +56,7 @@ describe("k6 bundler", () => {
|
|
|
55
56
|
fs.writeFileSync(
|
|
56
57
|
sourceFile,
|
|
57
58
|
[
|
|
58
|
-
'import { defineDalSuite } from "@elench/testkit
|
|
59
|
+
'import { defineDalSuite } from "@elench/testkit";',
|
|
59
60
|
"const suite = defineDalSuite(({ db }) => {",
|
|
60
61
|
' db.query("SELECT 1");',
|
|
61
62
|
"});",
|
package/lib/cli/args.mjs
CHANGED
|
@@ -22,11 +22,12 @@ export function resolveCliSelection({ first, second, serviceNames }) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export function validateFrameworkOption(value) {
|
|
25
|
-
if (!["all", "
|
|
25
|
+
if (!["all", "default", "playwright"].includes(value)) {
|
|
26
26
|
throw new Error(
|
|
27
|
-
`Unknown framework "${value}". Expected one of: all,
|
|
27
|
+
`Unknown framework "${value}". Expected one of: all, default, playwright.`
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
|
+
return value === "default" ? "k6" : value;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export function parseJobsOption(value) {
|
package/lib/cli/args.test.mjs
CHANGED
|
@@ -44,7 +44,8 @@ describe("cli-args", () => {
|
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
it("validates framework names", () => {
|
|
47
|
-
expect(
|
|
47
|
+
expect(validateFrameworkOption("playwright")).toBe("playwright");
|
|
48
|
+
expect(validateFrameworkOption("default")).toBe("k6");
|
|
48
49
|
expect(() => validateFrameworkOption("jest")).toThrow("Unknown framework");
|
|
49
50
|
});
|
|
50
51
|
|
package/lib/cli/index.mjs
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
validateFrameworkOption,
|
|
9
9
|
} from "./args.mjs";
|
|
10
10
|
import * as runner from "../runner/index.mjs";
|
|
11
|
-
import * as runtime from "../runtime/index.mjs";
|
|
12
11
|
|
|
13
12
|
export function run() {
|
|
14
13
|
const cli = cac("testkit");
|
|
@@ -18,11 +17,13 @@ export function run() {
|
|
|
18
17
|
.option("--dir <path>", "Explicit product directory")
|
|
19
18
|
.option("--path <path>", "Target runtime path relative to the product directory")
|
|
20
19
|
.option("--strict", "Exit non-zero when runtime status is missing or drifted")
|
|
21
|
-
.action((action, options) => {
|
|
20
|
+
.action(async (action, options) => {
|
|
22
21
|
if (!["install", "status", "update"].includes(action)) {
|
|
23
22
|
throw new Error('Unknown runtime action. Expected one of: install, status, update.');
|
|
24
23
|
}
|
|
25
24
|
|
|
25
|
+
const runtime = await import("../runtime-manager/index.mjs");
|
|
26
|
+
|
|
26
27
|
if (action === "status") {
|
|
27
28
|
const status = runtime.getRuntimeStatus(options);
|
|
28
29
|
console.log(runtime.formatRuntimeStatus(status));
|
|
@@ -47,7 +48,7 @@ export function run() {
|
|
|
47
48
|
default: "1",
|
|
48
49
|
})
|
|
49
50
|
.option("--shard <i/n>", "Run only shard i of n at suite granularity")
|
|
50
|
-
.option("--framework <name>", "Filter by framework (
|
|
51
|
+
.option("--framework <name>", "Filter by framework (default, playwright, all)", {
|
|
51
52
|
default: "all",
|
|
52
53
|
})
|
|
53
54
|
.option("--write-status", "Write a deterministic testkit.status.json snapshot")
|
|
@@ -91,7 +92,7 @@ export function run() {
|
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
validateFrameworkOption(options.framework);
|
|
95
|
+
const framework = validateFrameworkOption(options.framework);
|
|
95
96
|
|
|
96
97
|
const jobs = parseJobsOption(options.jobs);
|
|
97
98
|
const shard = parseShardOption(options.shard);
|
|
@@ -105,6 +106,7 @@ export function run() {
|
|
|
105
106
|
suiteNames,
|
|
106
107
|
{
|
|
107
108
|
...options,
|
|
109
|
+
framework,
|
|
108
110
|
fileNames,
|
|
109
111
|
jobs,
|
|
110
112
|
shard,
|
package/lib/index.mjs
CHANGED
|
@@ -1 +1,13 @@
|
|
|
1
|
-
export
|
|
1
|
+
export {
|
|
2
|
+
defineDalSuite,
|
|
3
|
+
} from "./runtime-src/k6/dal-suite.js";
|
|
4
|
+
export {
|
|
5
|
+
defineHttpSuite,
|
|
6
|
+
} from "./runtime-src/k6/suite.js";
|
|
7
|
+
|
|
8
|
+
export function createAuthAdapter({ setup, headers } = {}) {
|
|
9
|
+
return {
|
|
10
|
+
setup,
|
|
11
|
+
headers,
|
|
12
|
+
};
|
|
13
|
+
}
|
package/lib/runner/index.mjs
CHANGED
|
@@ -535,11 +535,11 @@ async function startLocalService(config) {
|
|
|
535
535
|
async function runHttpK6Batch(targetConfig, batch) {
|
|
536
536
|
const baseUrl = targetConfig.testkit.local?.baseUrl;
|
|
537
537
|
if (!baseUrl) {
|
|
538
|
-
throw new Error(`Service "${targetConfig.name}" requires local.baseUrl for HTTP
|
|
538
|
+
throw new Error(`Service "${targetConfig.name}" requires local.baseUrl for HTTP suites`);
|
|
539
539
|
}
|
|
540
540
|
|
|
541
541
|
console.log(
|
|
542
|
-
`\n── ${targetConfig.workerLabel} ${batch.type}:${batch.tasks[0].suiteName}
|
|
542
|
+
`\n── ${targetConfig.workerLabel} ${batch.type}:${batch.tasks[0].suiteName}${formatBatchDescriptor(batch)} ──`
|
|
543
543
|
);
|
|
544
544
|
|
|
545
545
|
return Promise.all(
|
|
@@ -586,7 +586,7 @@ async function runDalBatch(targetConfig, batch) {
|
|
|
586
586
|
}
|
|
587
587
|
|
|
588
588
|
console.log(
|
|
589
|
-
`\n── ${targetConfig.workerLabel} ${batch.type}:${batch.tasks[0].suiteName}
|
|
589
|
+
`\n── ${targetConfig.workerLabel} ${batch.type}:${batch.tasks[0].suiteName}${formatBatchDescriptor(batch)} ──`
|
|
590
590
|
);
|
|
591
591
|
|
|
592
592
|
return Promise.all(
|
|
@@ -866,7 +866,7 @@ function printRunSummary(results, durationMs) {
|
|
|
866
866
|
const fileDetail =
|
|
867
867
|
suite.failedFiles.length > 0 ? ` (${suite.failedFiles.join(", ")})` : "";
|
|
868
868
|
console.log(
|
|
869
|
-
` - ${suite.type}:${suite.name}
|
|
869
|
+
` - ${suite.type}:${suite.name}${formatSuiteFramework(suite.framework)}${fileDetail} · ${formatDuration(suite.durationMs)}`
|
|
870
870
|
);
|
|
871
871
|
if (suite.error) {
|
|
872
872
|
console.log(` ${suite.error}`);
|
|
@@ -911,6 +911,22 @@ function longestServiceName(results) {
|
|
|
911
911
|
return longestServiceNameModel(results);
|
|
912
912
|
}
|
|
913
913
|
|
|
914
|
+
function formatBatchDescriptor(batch) {
|
|
915
|
+
const fileLabel = `${batch.tasks.length} file${batch.tasks.length === 1 ? "" : "s"}`;
|
|
916
|
+
const frameworkLabel = formatFrameworkLabel(batch.framework);
|
|
917
|
+
return frameworkLabel ? ` (${frameworkLabel}, ${fileLabel})` : ` (${fileLabel})`;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function formatFrameworkLabel(framework) {
|
|
921
|
+
if (!framework || framework === "k6") return "";
|
|
922
|
+
return framework;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function formatSuiteFramework(framework) {
|
|
926
|
+
const label = formatFrameworkLabel(framework);
|
|
927
|
+
return label ? ` [${label}]` : "";
|
|
928
|
+
}
|
|
929
|
+
|
|
914
930
|
function buildRunArtifact({
|
|
915
931
|
productDir,
|
|
916
932
|
results,
|
package/lib/runner/results.mjs
CHANGED
|
@@ -177,7 +177,7 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
|
|
|
177
177
|
suites: suites.map((suite) => ({
|
|
178
178
|
name: suite.name,
|
|
179
179
|
type: suite.type,
|
|
180
|
-
framework: suite.framework,
|
|
180
|
+
framework: formatFrameworkForArtifact(suite.framework),
|
|
181
181
|
failed: suite.failedFiles.length > 0,
|
|
182
182
|
fileCount: suite.fileCount,
|
|
183
183
|
failedFiles: suite.failedFiles,
|
|
@@ -212,7 +212,6 @@ export function buildStatusArtifact({
|
|
|
212
212
|
tests.push({
|
|
213
213
|
service: result.name,
|
|
214
214
|
type: suite.type,
|
|
215
|
-
framework: suite.framework,
|
|
216
215
|
path: file.path,
|
|
217
216
|
status: file.status,
|
|
218
217
|
});
|
|
@@ -301,7 +300,7 @@ export function buildRunArtifact({
|
|
|
301
300
|
suiteType,
|
|
302
301
|
suiteNames,
|
|
303
302
|
fileNames,
|
|
304
|
-
framework,
|
|
303
|
+
framework: formatFrameworkForArtifact(framework),
|
|
305
304
|
shard,
|
|
306
305
|
serviceFilter,
|
|
307
306
|
testkitVersion: metadata.testkitVersion,
|
|
@@ -360,8 +359,8 @@ export function formatServiceSummary(result) {
|
|
|
360
359
|
}
|
|
361
360
|
|
|
362
361
|
export function formatError(error) {
|
|
363
|
-
if (error instanceof Error) return error.message;
|
|
364
|
-
return String(error);
|
|
362
|
+
if (error instanceof Error) return sanitizeErrorMessage(error.message);
|
|
363
|
+
return sanitizeErrorMessage(String(error));
|
|
365
364
|
}
|
|
366
365
|
|
|
367
366
|
export function longestServiceName(results) {
|
|
@@ -371,3 +370,15 @@ export function longestServiceName(results) {
|
|
|
371
370
|
function normalizePathSeparators(filePath) {
|
|
372
371
|
return filePath.split(path.sep).join("/");
|
|
373
372
|
}
|
|
373
|
+
|
|
374
|
+
function formatFrameworkForArtifact(framework) {
|
|
375
|
+
if (framework === "k6") return "default";
|
|
376
|
+
return framework;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function sanitizeErrorMessage(message) {
|
|
380
|
+
return message
|
|
381
|
+
.replace(/Command failed with exit code (\d+): .*?[\\/]vendor[\\/]k6 run\b/g, "Default runtime failed with exit code $1:")
|
|
382
|
+
.replace(/Command failed with exit code (\d+): k6 run\b/g, "Default runtime failed with exit code $1:")
|
|
383
|
+
.replace(/[\\/]vendor[\\/]k6\b/g, "default-runtime");
|
|
384
|
+
}
|
|
@@ -65,6 +65,7 @@ describe("runner-results", () => {
|
|
|
65
65
|
expect(result.failed).toBe(true);
|
|
66
66
|
expect(result.failedSuiteCount).toBe(1);
|
|
67
67
|
expect(result.errors).toEqual(["worker failed", "graph failed"]);
|
|
68
|
+
expect(result.suites[0].framework).toBe("default");
|
|
68
69
|
expect(result.suites[0].files).toEqual([
|
|
69
70
|
{
|
|
70
71
|
path: "tests/health.js",
|
|
@@ -169,7 +170,7 @@ describe("runner-results", () => {
|
|
|
169
170
|
suiteType: "int",
|
|
170
171
|
suiteNames: ["health"],
|
|
171
172
|
fileNames: ["tests/api/integration/b.int.testkit.ts"],
|
|
172
|
-
framework: "
|
|
173
|
+
framework: "default",
|
|
173
174
|
shard: null,
|
|
174
175
|
serviceFilter: "api",
|
|
175
176
|
metadata: {
|
|
@@ -210,14 +211,12 @@ describe("runner-results", () => {
|
|
|
210
211
|
{
|
|
211
212
|
service: "api",
|
|
212
213
|
type: "integration",
|
|
213
|
-
framework: "k6",
|
|
214
214
|
path: "tests/api/integration/a.int.testkit.ts",
|
|
215
215
|
status: "passed",
|
|
216
216
|
},
|
|
217
217
|
{
|
|
218
218
|
service: "api",
|
|
219
219
|
type: "integration",
|
|
220
|
-
framework: "k6",
|
|
221
220
|
path: "tests/api/integration/b.int.testkit.ts",
|
|
222
221
|
status: "failed",
|
|
223
222
|
},
|
package/lib/runtime/index.mjs
CHANGED
|
@@ -1,190 +1,31 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
};
|
|
33
|
-
fs.writeFileSync(
|
|
34
|
-
path.join(runtimeDir, METADATA_FILE),
|
|
35
|
-
`${JSON.stringify(metadata, null, 2)}\n`
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
productDir,
|
|
40
|
-
runtimeDir,
|
|
41
|
-
relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
|
|
42
|
-
files: metadata.files,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function getRuntimeStatus(options = {}) {
|
|
47
|
-
const productDir = resolveProductDir(process.cwd(), options.dir);
|
|
48
|
-
const runtimeDir = resolveRuntimeDir(productDir, options.path);
|
|
49
|
-
const sourceFiles = readBundledRuntimeFiles();
|
|
50
|
-
const metadataPath = path.join(runtimeDir, METADATA_FILE);
|
|
51
|
-
|
|
52
|
-
if (!fs.existsSync(runtimeDir) || !fs.existsSync(metadataPath)) {
|
|
53
|
-
return {
|
|
54
|
-
status: "missing",
|
|
55
|
-
productDir,
|
|
56
|
-
runtimeDir,
|
|
57
|
-
relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
|
|
58
|
-
missingFiles: sourceFiles.map((file) => file.path),
|
|
59
|
-
driftedFiles: [],
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const missingFiles = [];
|
|
64
|
-
const driftedFiles = [];
|
|
65
|
-
|
|
66
|
-
for (const file of sourceFiles) {
|
|
67
|
-
const targetPath = path.join(runtimeDir, file.path);
|
|
68
|
-
if (!fs.existsSync(targetPath)) {
|
|
69
|
-
missingFiles.push(file.path);
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const installed = fs.readFileSync(targetPath, "utf8");
|
|
74
|
-
if (installed !== file.content) {
|
|
75
|
-
driftedFiles.push(file.path);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
|
80
|
-
const versionMatches = metadata.version === readPackageVersion();
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
status:
|
|
84
|
-
missingFiles.length === 0 && driftedFiles.length === 0 && versionMatches
|
|
85
|
-
? "installed"
|
|
86
|
-
: "drifted",
|
|
87
|
-
productDir,
|
|
88
|
-
runtimeDir,
|
|
89
|
-
relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
|
|
90
|
-
versionMatches,
|
|
91
|
-
missingFiles,
|
|
92
|
-
driftedFiles,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function formatRuntimeStatus(result) {
|
|
97
|
-
if (result.status === "missing") {
|
|
98
|
-
return `Runtime not installed at ${result.relativeRuntimeDir}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (result.status === "installed") {
|
|
102
|
-
return `Runtime at ${result.relativeRuntimeDir} is up to date`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const problems = [];
|
|
106
|
-
if (result.missingFiles.length > 0) {
|
|
107
|
-
problems.push(`missing: ${result.missingFiles.join(", ")}`);
|
|
108
|
-
}
|
|
109
|
-
if (result.driftedFiles.length > 0) {
|
|
110
|
-
problems.push(`drifted: ${result.driftedFiles.join(", ")}`);
|
|
111
|
-
}
|
|
112
|
-
if (result.versionMatches === false) {
|
|
113
|
-
problems.push("version drift");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return `Runtime at ${result.relativeRuntimeDir} is drifted (${problems.join("; ")})`;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function resolveProductDir(cwd, explicitDir) {
|
|
120
|
-
const dir = explicitDir ? path.resolve(cwd, explicitDir) : cwd;
|
|
121
|
-
ensureProductFiles(dir);
|
|
122
|
-
return dir;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function ensureProductFiles(dir) {
|
|
126
|
-
const missing = [TESTKIT_CONFIG].filter(
|
|
127
|
-
(file) => !fs.existsSync(path.join(dir, file))
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
if (missing.length > 0) {
|
|
131
|
-
throw new Error(
|
|
132
|
-
`Expected ${missing.join(" and ")} in ${dir}. Either cd into a product directory or use --dir.`
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function resolveRuntimeDir(productDir, explicitPath) {
|
|
138
|
-
return path.resolve(productDir, explicitPath || DEFAULT_RUNTIME_DIR);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function relativeToProduct(productDir, targetPath) {
|
|
142
|
-
return path.relative(productDir, targetPath) || ".";
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function readBundledRuntimeFiles() {
|
|
146
|
-
const sourceDir = path.resolve(
|
|
147
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
148
|
-
"..",
|
|
149
|
-
"runtime-src"
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
return walkRuntimeFiles(sourceDir);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function walkRuntimeFiles(rootDir, relativeDir = "") {
|
|
156
|
-
const entries = fs.readdirSync(path.join(rootDir, relativeDir), {
|
|
157
|
-
withFileTypes: true,
|
|
158
|
-
});
|
|
159
|
-
const files = [];
|
|
160
|
-
|
|
161
|
-
for (const entry of entries) {
|
|
162
|
-
const nextRelative = path.join(relativeDir, entry.name);
|
|
163
|
-
if (entry.isDirectory()) {
|
|
164
|
-
files.push(...walkRuntimeFiles(rootDir, nextRelative));
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const absolute = path.join(rootDir, nextRelative);
|
|
169
|
-
files.push({
|
|
170
|
-
path: nextRelative.split(path.sep).join("/"),
|
|
171
|
-
content: fs.readFileSync(absolute, "utf8"),
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function readPackageVersion() {
|
|
179
|
-
const packagePath = path.resolve(
|
|
180
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
181
|
-
"..",
|
|
182
|
-
"..",
|
|
183
|
-
"package.json"
|
|
184
|
-
);
|
|
185
|
-
return JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function hashContent(content) {
|
|
189
|
-
return crypto.createHash("sha256").update(content).digest("hex");
|
|
190
|
-
}
|
|
1
|
+
import rawHttp from "k6/http";
|
|
2
|
+
import { check, fail, group, sleep } from "k6";
|
|
3
|
+
|
|
4
|
+
export { check, fail, group, sleep };
|
|
5
|
+
export const http = rawHttp;
|
|
6
|
+
|
|
7
|
+
export function file(data, filename, contentType) {
|
|
8
|
+
return rawHttp.file(data, filename, contentType);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
allMatch,
|
|
13
|
+
contains,
|
|
14
|
+
defaultOptions,
|
|
15
|
+
isSorted,
|
|
16
|
+
json,
|
|
17
|
+
singleIterationOptions,
|
|
18
|
+
} from "../runtime-src/k6/checks.js";
|
|
19
|
+
export {
|
|
20
|
+
createDalContext,
|
|
21
|
+
openDb,
|
|
22
|
+
truncate,
|
|
23
|
+
} from "../runtime-src/k6/dal.js";
|
|
24
|
+
export {
|
|
25
|
+
createHttpClient,
|
|
26
|
+
defaultOptions as httpDefaultOptions,
|
|
27
|
+
getEnv,
|
|
28
|
+
makeGetWithHeaders,
|
|
29
|
+
makeRawReq,
|
|
30
|
+
makeReq,
|
|
31
|
+
} from "../runtime-src/k6/http.js";
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const TESTKIT_CONFIG = "testkit.config.json";
|
|
7
|
+
const DEFAULT_RUNTIME_DIR = path.join("tests", "_testkit");
|
|
8
|
+
const METADATA_FILE = ".runtime-manifest.json";
|
|
9
|
+
const RUNTIME_FORMAT = 1;
|
|
10
|
+
|
|
11
|
+
export function installRuntime(options = {}) {
|
|
12
|
+
const productDir = resolveProductDir(process.cwd(), options.dir);
|
|
13
|
+
const runtimeDir = resolveRuntimeDir(productDir, options.path);
|
|
14
|
+
const sourceFiles = readBundledRuntimeFiles();
|
|
15
|
+
|
|
16
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
17
|
+
|
|
18
|
+
for (const file of sourceFiles) {
|
|
19
|
+
const targetPath = path.join(runtimeDir, file.path);
|
|
20
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
21
|
+
fs.writeFileSync(targetPath, file.content);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const metadata = {
|
|
25
|
+
format: RUNTIME_FORMAT,
|
|
26
|
+
package: "@elench/testkit",
|
|
27
|
+
version: readPackageVersion(),
|
|
28
|
+
files: sourceFiles.map((file) => ({
|
|
29
|
+
path: file.path,
|
|
30
|
+
sha256: hashContent(file.content),
|
|
31
|
+
})),
|
|
32
|
+
};
|
|
33
|
+
fs.writeFileSync(
|
|
34
|
+
path.join(runtimeDir, METADATA_FILE),
|
|
35
|
+
`${JSON.stringify(metadata, null, 2)}\n`
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
productDir,
|
|
40
|
+
runtimeDir,
|
|
41
|
+
relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
|
|
42
|
+
files: metadata.files,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getRuntimeStatus(options = {}) {
|
|
47
|
+
const productDir = resolveProductDir(process.cwd(), options.dir);
|
|
48
|
+
const runtimeDir = resolveRuntimeDir(productDir, options.path);
|
|
49
|
+
const sourceFiles = readBundledRuntimeFiles();
|
|
50
|
+
const metadataPath = path.join(runtimeDir, METADATA_FILE);
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(runtimeDir) || !fs.existsSync(metadataPath)) {
|
|
53
|
+
return {
|
|
54
|
+
status: "missing",
|
|
55
|
+
productDir,
|
|
56
|
+
runtimeDir,
|
|
57
|
+
relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
|
|
58
|
+
missingFiles: sourceFiles.map((file) => file.path),
|
|
59
|
+
driftedFiles: [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const missingFiles = [];
|
|
64
|
+
const driftedFiles = [];
|
|
65
|
+
|
|
66
|
+
for (const file of sourceFiles) {
|
|
67
|
+
const targetPath = path.join(runtimeDir, file.path);
|
|
68
|
+
if (!fs.existsSync(targetPath)) {
|
|
69
|
+
missingFiles.push(file.path);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const installed = fs.readFileSync(targetPath, "utf8");
|
|
74
|
+
if (installed !== file.content) {
|
|
75
|
+
driftedFiles.push(file.path);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
|
80
|
+
const versionMatches = metadata.version === readPackageVersion();
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
status:
|
|
84
|
+
missingFiles.length === 0 && driftedFiles.length === 0 && versionMatches
|
|
85
|
+
? "installed"
|
|
86
|
+
: "drifted",
|
|
87
|
+
productDir,
|
|
88
|
+
runtimeDir,
|
|
89
|
+
relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
|
|
90
|
+
versionMatches,
|
|
91
|
+
missingFiles,
|
|
92
|
+
driftedFiles,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function formatRuntimeStatus(result) {
|
|
97
|
+
if (result.status === "missing") {
|
|
98
|
+
return `Runtime not installed at ${result.relativeRuntimeDir}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (result.status === "installed") {
|
|
102
|
+
return `Runtime at ${result.relativeRuntimeDir} is up to date`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const problems = [];
|
|
106
|
+
if (result.missingFiles.length > 0) {
|
|
107
|
+
problems.push(`missing: ${result.missingFiles.join(", ")}`);
|
|
108
|
+
}
|
|
109
|
+
if (result.driftedFiles.length > 0) {
|
|
110
|
+
problems.push(`drifted: ${result.driftedFiles.join(", ")}`);
|
|
111
|
+
}
|
|
112
|
+
if (result.versionMatches === false) {
|
|
113
|
+
problems.push("version drift");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return `Runtime at ${result.relativeRuntimeDir} is drifted (${problems.join("; ")})`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveProductDir(cwd, explicitDir) {
|
|
120
|
+
const dir = explicitDir ? path.resolve(cwd, explicitDir) : cwd;
|
|
121
|
+
ensureProductFiles(dir);
|
|
122
|
+
return dir;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function ensureProductFiles(dir) {
|
|
126
|
+
const missing = [TESTKIT_CONFIG].filter(
|
|
127
|
+
(file) => !fs.existsSync(path.join(dir, file))
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (missing.length > 0) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Expected ${missing.join(" and ")} in ${dir}. Either cd into a product directory or use --dir.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveRuntimeDir(productDir, explicitPath) {
|
|
138
|
+
return path.resolve(productDir, explicitPath || DEFAULT_RUNTIME_DIR);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function relativeToProduct(productDir, targetPath) {
|
|
142
|
+
return path.relative(productDir, targetPath) || ".";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readBundledRuntimeFiles() {
|
|
146
|
+
const sourceDir = path.resolve(
|
|
147
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
148
|
+
"..",
|
|
149
|
+
"runtime-src"
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return walkRuntimeFiles(sourceDir);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function walkRuntimeFiles(rootDir, relativeDir = "") {
|
|
156
|
+
const entries = fs.readdirSync(path.join(rootDir, relativeDir), {
|
|
157
|
+
withFileTypes: true,
|
|
158
|
+
});
|
|
159
|
+
const files = [];
|
|
160
|
+
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
const nextRelative = path.join(relativeDir, entry.name);
|
|
163
|
+
if (entry.isDirectory()) {
|
|
164
|
+
files.push(...walkRuntimeFiles(rootDir, nextRelative));
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const absolute = path.join(rootDir, nextRelative);
|
|
169
|
+
files.push({
|
|
170
|
+
path: nextRelative.split(path.sep).join("/"),
|
|
171
|
+
content: fs.readFileSync(absolute, "utf8"),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readPackageVersion() {
|
|
179
|
+
const packagePath = path.resolve(
|
|
180
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
181
|
+
"..",
|
|
182
|
+
"..",
|
|
183
|
+
"package.json"
|
|
184
|
+
);
|
|
185
|
+
return JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function hashContent(content) {
|
|
189
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
190
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI for discovering and running local
|
|
3
|
+
"version": "0.1.23",
|
|
4
|
+
"description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./lib/index.mjs",
|
|
8
|
-
"./
|
|
9
|
-
"./k6/*": "./lib/k6/*.mjs",
|
|
8
|
+
"./runtime": "./lib/runtime/index.mjs",
|
|
10
9
|
"./package.json": "./package.json"
|
|
11
10
|
},
|
|
12
11
|
"bin": {
|
package/lib/k6/checks.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "../runtime-src/k6/checks.js";
|
package/lib/k6/dal-suite.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "../runtime-src/k6/dal-suite.js";
|
package/lib/k6/dal.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "../runtime-src/k6/dal.js";
|
package/lib/k6/http.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "../runtime-src/k6/http.js";
|
package/lib/k6/index.mjs
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
allMatch,
|
|
3
|
-
contains,
|
|
4
|
-
defaultOptions,
|
|
5
|
-
isSorted,
|
|
6
|
-
json,
|
|
7
|
-
singleIterationOptions,
|
|
8
|
-
} from "../runtime-src/k6/checks.js";
|
|
9
|
-
export {
|
|
10
|
-
createDalContext,
|
|
11
|
-
openDb,
|
|
12
|
-
truncate,
|
|
13
|
-
} from "../runtime-src/k6/dal.js";
|
|
14
|
-
export { defineDalSuite } from "../runtime-src/k6/dal-suite.js";
|
|
15
|
-
export {
|
|
16
|
-
createHttpClient,
|
|
17
|
-
defaultOptions as httpDefaultOptions,
|
|
18
|
-
getEnv,
|
|
19
|
-
makeGetWithHeaders,
|
|
20
|
-
makeRawReq,
|
|
21
|
-
makeReq,
|
|
22
|
-
} from "../runtime-src/k6/http.js";
|
|
23
|
-
export { defineHttpSuite } from "../runtime-src/k6/suite.js";
|
|
24
|
-
|
|
25
|
-
export function createAuthAdapter({ setup, headers } = {}) {
|
|
26
|
-
return {
|
|
27
|
-
setup,
|
|
28
|
-
headers,
|
|
29
|
-
};
|
|
30
|
-
}
|
package/lib/k6/suite.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "../runtime-src/k6/suite.js";
|