@elench/testkit 0.1.21 → 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 +14 -15
- 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/config/discovery.mjs +140 -99
- package/lib/config/discovery.test.mjs +75 -17
- package/lib/config/index.mjs +3 -2
- package/lib/config/model.mjs +3 -17
- package/lib/config/model.test.mjs +2 -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/lib/config/model.mjs
CHANGED
|
@@ -213,23 +213,9 @@ export function validateServiceConfig(name, service, configPath) {
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
if (service.discovery !== undefined) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
service.discovery.include !== undefined &&
|
|
221
|
-
(!Array.isArray(service.discovery.include) ||
|
|
222
|
-
service.discovery.include.some((value) => typeof value !== "string" || value.length === 0))
|
|
223
|
-
) {
|
|
224
|
-
throw new Error(`Service "${name}" discovery.include must be an array of glob strings`);
|
|
225
|
-
}
|
|
226
|
-
if (
|
|
227
|
-
service.discovery.exclude !== undefined &&
|
|
228
|
-
(!Array.isArray(service.discovery.exclude) ||
|
|
229
|
-
service.discovery.exclude.some((value) => typeof value !== "string" || value.length === 0))
|
|
230
|
-
) {
|
|
231
|
-
throw new Error(`Service "${name}" discovery.exclude must be an array of glob strings`);
|
|
232
|
-
}
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Service "${name}" cannot define discovery. Testkit discovers *.testkit.ts files from the filesystem automatically.`
|
|
218
|
+
);
|
|
233
219
|
}
|
|
234
220
|
}
|
|
235
221
|
|
|
@@ -95,11 +95,9 @@ QUX='zap'
|
|
|
95
95
|
|
|
96
96
|
expect(() =>
|
|
97
97
|
validateServiceConfig("api", {
|
|
98
|
-
discovery: {
|
|
99
|
-
include: "tests/**/*.int.testkit.ts",
|
|
100
|
-
},
|
|
98
|
+
discovery: {},
|
|
101
99
|
}, "testkit.config.json")
|
|
102
|
-
).toThrow("
|
|
100
|
+
).toThrow("cannot define discovery");
|
|
103
101
|
|
|
104
102
|
expect(() =>
|
|
105
103
|
validateTelemetryConfig(
|
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";
|