@elench/testkit 0.1.23 → 0.1.25
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/lib/cli/args.mjs +36 -0
- package/lib/cli/args.test.mjs +15 -0
- package/lib/cli/index.mjs +6 -1
- package/lib/runner/index.mjs +169 -50
- package/lib/runner/results.mjs +22 -0
- package/lib/runner/results.test.mjs +31 -0
- package/lib/runtime/index.mjs +2 -0
- package/lib/runtime-src/k6/checks.js +9 -0
- package/lib/runtime-src/k6/dal-suite.js +25 -7
- package/lib/runtime-src/k6/suite.js +28 -10
- package/package.json +1 -1
package/lib/cli/args.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
1
3
|
export const SUITE_TYPES = new Set(["int", "integration", "e2e", "dal", "load", "all"]);
|
|
2
4
|
export const LIFECYCLE = new Set(["status", "destroy"]);
|
|
3
5
|
export const RESERVED = new Set([...SUITE_TYPES, ...LIFECYCLE]);
|
|
@@ -56,3 +58,37 @@ export function parseShardOption(value) {
|
|
|
56
58
|
|
|
57
59
|
return { index, total };
|
|
58
60
|
}
|
|
61
|
+
|
|
62
|
+
export function resolveRequestedFiles(fileNames, productDir, invocationCwd = process.cwd()) {
|
|
63
|
+
const resolved = [];
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
|
|
66
|
+
for (const rawFile of fileNames || []) {
|
|
67
|
+
const normalized = resolveRequestedFile(rawFile, productDir, invocationCwd);
|
|
68
|
+
if (seen.has(normalized)) continue;
|
|
69
|
+
seen.add(normalized);
|
|
70
|
+
resolved.push(normalized);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return resolved;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveRequestedFile(rawFile, productDir, invocationCwd) {
|
|
77
|
+
const rawValue = String(rawFile);
|
|
78
|
+
const candidates = path.isAbsolute(rawValue)
|
|
79
|
+
? [rawValue]
|
|
80
|
+
: [path.resolve(invocationCwd, rawValue), path.resolve(productDir, rawValue)];
|
|
81
|
+
|
|
82
|
+
for (const candidate of candidates) {
|
|
83
|
+
const relative = normalizePathSeparators(path.relative(productDir, candidate));
|
|
84
|
+
if (!relative.startsWith("../") && relative !== ".." && !path.isAbsolute(relative)) {
|
|
85
|
+
return relative.replace(/^\.\/+/, "");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return normalizePathSeparators(rawValue).replace(/^\.\/+/, "");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizePathSeparators(filePath) {
|
|
93
|
+
return String(filePath).split(path.sep).join("/");
|
|
94
|
+
}
|
package/lib/cli/args.test.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
parseJobsOption,
|
|
4
4
|
parseShardOption,
|
|
5
|
+
resolveRequestedFiles,
|
|
5
6
|
resolveCliSelection,
|
|
6
7
|
validateFrameworkOption,
|
|
7
8
|
} from "./args.mjs";
|
|
@@ -60,4 +61,18 @@ describe("cli-args", () => {
|
|
|
60
61
|
expect(() => parseShardOption("2-of-5")).toThrow("Invalid --shard value");
|
|
61
62
|
expect(() => parseShardOption("3/2")).toThrow("Expected 1 <= i <= n");
|
|
62
63
|
});
|
|
64
|
+
|
|
65
|
+
it("normalizes requested file paths against the product directory", () => {
|
|
66
|
+
expect(
|
|
67
|
+
resolveRequestedFiles(
|
|
68
|
+
[
|
|
69
|
+
"/tmp/product/tests/api/integration/health.int.testkit.ts",
|
|
70
|
+
"./tests/api/integration/health.int.testkit.ts",
|
|
71
|
+
"product/tests/api/integration/health.int.testkit.ts",
|
|
72
|
+
],
|
|
73
|
+
"/tmp/product",
|
|
74
|
+
"/tmp"
|
|
75
|
+
)
|
|
76
|
+
).toEqual(["tests/api/integration/health.int.testkit.ts"]);
|
|
77
|
+
});
|
|
63
78
|
});
|
package/lib/cli/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
parseJobsOption,
|
|
5
5
|
parseShardOption,
|
|
6
6
|
RESERVED,
|
|
7
|
+
resolveRequestedFiles,
|
|
7
8
|
resolveCliSelection,
|
|
8
9
|
validateFrameworkOption,
|
|
9
10
|
} from "./args.mjs";
|
|
@@ -52,6 +53,7 @@ export function run() {
|
|
|
52
53
|
default: "all",
|
|
53
54
|
})
|
|
54
55
|
.option("--write-status", "Write a deterministic testkit.status.json snapshot")
|
|
56
|
+
.option("--allow-partial-status", "Allow --write-status for filtered runs")
|
|
55
57
|
.action(async (first, second, third, options) => {
|
|
56
58
|
// Resolve: service filter, suite type, and --dir.
|
|
57
59
|
//
|
|
@@ -99,7 +101,9 @@ export function run() {
|
|
|
99
101
|
|
|
100
102
|
const suiteType = type || "all";
|
|
101
103
|
const suiteNames = Array.isArray(options.suite) ? options.suite : [options.suite].filter(Boolean);
|
|
102
|
-
const
|
|
104
|
+
const rawFileNames = Array.isArray(options.file) ? options.file : [options.file].filter(Boolean);
|
|
105
|
+
const productDir = allConfigs[0]?.productDir || process.cwd();
|
|
106
|
+
const fileNames = resolveRequestedFiles(rawFileNames, productDir, process.cwd());
|
|
103
107
|
await runner.runAll(
|
|
104
108
|
configs,
|
|
105
109
|
suiteType,
|
|
@@ -110,6 +114,7 @@ export function run() {
|
|
|
110
114
|
fileNames,
|
|
111
115
|
jobs,
|
|
112
116
|
shard,
|
|
117
|
+
serviceFilter: service,
|
|
113
118
|
},
|
|
114
119
|
allConfigs
|
|
115
120
|
);
|
package/lib/runner/index.mjs
CHANGED
|
@@ -108,6 +108,28 @@ export async function runAll(configs, suiteType, suiteNames, opts, allConfigs =
|
|
|
108
108
|
},
|
|
109
109
|
testkitVersion: readPackageMetadata().version,
|
|
110
110
|
};
|
|
111
|
+
const requestedFiles = opts.fileNames || [];
|
|
112
|
+
if (requestedFiles.length > 0) {
|
|
113
|
+
const unmatchedFiles = findUnmatchedRequestedFiles(
|
|
114
|
+
configs,
|
|
115
|
+
suiteType,
|
|
116
|
+
suiteNames,
|
|
117
|
+
opts.framework || "all",
|
|
118
|
+
requestedFiles
|
|
119
|
+
);
|
|
120
|
+
if (unmatchedFiles.length > 0) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Requested file${unmatchedFiles.length === 1 ? "" : "s"} did not match any selected suites:\n` +
|
|
123
|
+
unmatchedFiles.map((file) => `- ${file}`).join("\n")
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (opts.writeStatus && !opts.allowPartialStatus && !isFullRunSelection(suiteType, suiteNames, requestedFiles, opts)) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
"Refusing to overwrite testkit.status.json from a filtered run. " +
|
|
130
|
+
"Run the full suite with --write-status, or pass --allow-partial-status to opt in."
|
|
131
|
+
);
|
|
132
|
+
}
|
|
111
133
|
const servicePlans = collectServicePlans(configs, configMap, suiteType, suiteNames, opts);
|
|
112
134
|
const trackers = buildServiceTrackers(servicePlans, startedAt);
|
|
113
135
|
const executedPlans = servicePlans.filter((plan) => !plan.skipped);
|
|
@@ -155,10 +177,10 @@ export async function runAll(configs, suiteType, suiteNames, opts, allConfigs =
|
|
|
155
177
|
workerCount,
|
|
156
178
|
suiteType,
|
|
157
179
|
suiteNames,
|
|
158
|
-
fileNames:
|
|
180
|
+
fileNames: requestedFiles,
|
|
159
181
|
framework: opts.framework || "all",
|
|
160
182
|
shard: opts.shard || null,
|
|
161
|
-
serviceFilter:
|
|
183
|
+
serviceFilter: opts.serviceFilter || null,
|
|
162
184
|
metadata,
|
|
163
185
|
});
|
|
164
186
|
|
|
@@ -171,10 +193,10 @@ export async function runAll(configs, suiteType, suiteNames, opts, allConfigs =
|
|
|
171
193
|
results,
|
|
172
194
|
suiteType,
|
|
173
195
|
suiteNames,
|
|
174
|
-
fileNames:
|
|
196
|
+
fileNames: requestedFiles,
|
|
175
197
|
framework: opts.framework || "all",
|
|
176
198
|
shard: opts.shard || null,
|
|
177
|
-
serviceFilter:
|
|
199
|
+
serviceFilter: opts.serviceFilter || null,
|
|
178
200
|
metadata,
|
|
179
201
|
})
|
|
180
202
|
);
|
|
@@ -556,27 +578,14 @@ async function runHttpK6Task(targetConfig, task, baseUrl) {
|
|
|
556
578
|
sourceFile: absFile,
|
|
557
579
|
});
|
|
558
580
|
console.log(`·· ${targetConfig.workerLabel}:${task.suiteName} → ${task.file}`);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
task,
|
|
568
|
-
failed: false,
|
|
569
|
-
error: null,
|
|
570
|
-
durationMs: Date.now() - startedAt,
|
|
571
|
-
};
|
|
572
|
-
} catch (error) {
|
|
573
|
-
return {
|
|
574
|
-
task,
|
|
575
|
-
failed: true,
|
|
576
|
-
error: formatError(error),
|
|
577
|
-
durationMs: Date.now() - startedAt,
|
|
578
|
-
};
|
|
579
|
-
}
|
|
581
|
+
return runDefaultRuntimeTask(targetConfig, task, [
|
|
582
|
+
"run",
|
|
583
|
+
"--address",
|
|
584
|
+
"127.0.0.1:0",
|
|
585
|
+
"-e",
|
|
586
|
+
`BASE_URL=${baseUrl}`,
|
|
587
|
+
bundledFile,
|
|
588
|
+
]);
|
|
580
589
|
}
|
|
581
590
|
|
|
582
591
|
async function runDalBatch(targetConfig, batch) {
|
|
@@ -603,31 +612,14 @@ async function runDalTask(targetConfig, task, databaseUrl) {
|
|
|
603
612
|
sourceFile: absFile,
|
|
604
613
|
});
|
|
605
614
|
console.log(`·· ${targetConfig.workerLabel}:${task.suiteName} → ${task.file}`);
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
stdio: "inherit",
|
|
615
|
-
}
|
|
616
|
-
);
|
|
617
|
-
return {
|
|
618
|
-
task,
|
|
619
|
-
failed: false,
|
|
620
|
-
error: null,
|
|
621
|
-
durationMs: Date.now() - startedAt,
|
|
622
|
-
};
|
|
623
|
-
} catch (error) {
|
|
624
|
-
return {
|
|
625
|
-
task,
|
|
626
|
-
failed: true,
|
|
627
|
-
error: formatError(error),
|
|
628
|
-
durationMs: Date.now() - startedAt,
|
|
629
|
-
};
|
|
630
|
-
}
|
|
615
|
+
return runDefaultRuntimeTask(targetConfig, task, [
|
|
616
|
+
"run",
|
|
617
|
+
"--address",
|
|
618
|
+
"127.0.0.1:0",
|
|
619
|
+
"-e",
|
|
620
|
+
`DATABASE_URL=${databaseUrl}`,
|
|
621
|
+
bundledFile,
|
|
622
|
+
]);
|
|
631
623
|
}
|
|
632
624
|
|
|
633
625
|
async function runPlaywrightBatch(targetConfig, batch) {
|
|
@@ -968,11 +960,23 @@ function writeRunArtifact(productDir, artifact) {
|
|
|
968
960
|
function buildStatusArtifact({
|
|
969
961
|
productDir,
|
|
970
962
|
results,
|
|
963
|
+
suiteType,
|
|
964
|
+
suiteNames,
|
|
965
|
+
fileNames,
|
|
966
|
+
framework,
|
|
967
|
+
shard,
|
|
968
|
+
serviceFilter,
|
|
971
969
|
metadata,
|
|
972
970
|
}) {
|
|
973
971
|
return buildStatusArtifactModel({
|
|
974
972
|
productDir,
|
|
975
973
|
results,
|
|
974
|
+
suiteType,
|
|
975
|
+
suiteNames,
|
|
976
|
+
fileNames,
|
|
977
|
+
framework,
|
|
978
|
+
shard,
|
|
979
|
+
serviceFilter,
|
|
976
980
|
metadata,
|
|
977
981
|
});
|
|
978
982
|
}
|
|
@@ -1064,6 +1068,121 @@ function formatError(error) {
|
|
|
1064
1068
|
return formatErrorModel(error);
|
|
1065
1069
|
}
|
|
1066
1070
|
|
|
1071
|
+
async function runDefaultRuntimeTask(targetConfig, task, args) {
|
|
1072
|
+
const k6Binary = resolveK6Binary();
|
|
1073
|
+
const summaryFile = buildDefaultRuntimeSummaryPath(targetConfig, task);
|
|
1074
|
+
fs.mkdirSync(path.dirname(summaryFile), { recursive: true });
|
|
1075
|
+
const startedAt = Date.now();
|
|
1076
|
+
const result = await execa(k6Binary, [...args.slice(0, 1), "--summary-export", summaryFile, ...args.slice(1)], {
|
|
1077
|
+
cwd: targetConfig.productDir,
|
|
1078
|
+
env: buildExecutionEnv(targetConfig),
|
|
1079
|
+
reject: false,
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
1083
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
1084
|
+
|
|
1085
|
+
const summary = readDefaultRuntimeSummary(summaryFile);
|
|
1086
|
+
const runtimeError = determineDefaultRuntimeFailure(result, summary);
|
|
1087
|
+
|
|
1088
|
+
return {
|
|
1089
|
+
task,
|
|
1090
|
+
failed: runtimeError !== null,
|
|
1091
|
+
error: runtimeError,
|
|
1092
|
+
durationMs: Date.now() - startedAt,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function buildDefaultRuntimeSummaryPath(targetConfig, task) {
|
|
1097
|
+
return path.join(
|
|
1098
|
+
targetConfig.stateDir || path.join(targetConfig.productDir, ".testkit"),
|
|
1099
|
+
"_runtime",
|
|
1100
|
+
`task-${task.id}.summary.json`
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function readDefaultRuntimeSummary(filePath) {
|
|
1105
|
+
try {
|
|
1106
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1107
|
+
} catch {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function determineDefaultRuntimeFailure(result, summary) {
|
|
1113
|
+
const fatalRuntimeError = extractDefaultRuntimeFatalError(result.stderr || "");
|
|
1114
|
+
if (fatalRuntimeError) {
|
|
1115
|
+
return `Default runtime uncaught error: ${fatalRuntimeError}`;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const failedThresholds = extractDefaultRuntimeThresholdFailures(summary);
|
|
1119
|
+
if (failedThresholds.length > 0) {
|
|
1120
|
+
return `Default runtime thresholds failed: ${failedThresholds.join(", ")}`;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (result.exitCode !== 0) {
|
|
1124
|
+
return sanitizeDefaultRuntimeExitError(result.exitCode, result.stderr || result.stdout || "");
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function extractDefaultRuntimeFatalError(stderr) {
|
|
1131
|
+
if (!stderr || !/source=stacktrace/.test(stderr)) return null;
|
|
1132
|
+
const matched = stderr.match(/Error:\s([^\n]+)/);
|
|
1133
|
+
return matched?.[1]?.trim() || firstLine(stderr);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function extractDefaultRuntimeThresholdFailures(summary) {
|
|
1137
|
+
const metrics = summary?.metrics;
|
|
1138
|
+
if (!metrics || typeof metrics !== "object") return [];
|
|
1139
|
+
|
|
1140
|
+
const failures = [];
|
|
1141
|
+
for (const [metricName, metricSummary] of Object.entries(metrics)) {
|
|
1142
|
+
const thresholds = metricSummary?.thresholds;
|
|
1143
|
+
if (!thresholds || typeof thresholds !== "object") continue;
|
|
1144
|
+
for (const [threshold, crossed] of Object.entries(thresholds)) {
|
|
1145
|
+
if (crossed === true) {
|
|
1146
|
+
failures.push(`${metricName}(${threshold})`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
return failures.sort();
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function sanitizeDefaultRuntimeExitError(exitCode, output) {
|
|
1155
|
+
const message = firstLine(output);
|
|
1156
|
+
if (message) {
|
|
1157
|
+
return `Default runtime failed with exit code ${exitCode}: ${message}`;
|
|
1158
|
+
}
|
|
1159
|
+
return `Default runtime failed with exit code ${exitCode}`;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function findUnmatchedRequestedFiles(configs, suiteType, suiteNames, framework, fileNames) {
|
|
1163
|
+
const matchedFiles = new Set();
|
|
1164
|
+
for (const config of configs) {
|
|
1165
|
+
const suites = collectSuites(config, suiteType, suiteNames, framework, []);
|
|
1166
|
+
for (const suite of suites) {
|
|
1167
|
+
for (const file of suite.files) {
|
|
1168
|
+
matchedFiles.add(normalizePathSeparators(file));
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
return [...new Set(fileNames.map(normalizePathSeparators))].filter((file) => !matchedFiles.has(file));
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function isFullRunSelection(suiteType, suiteNames, fileNames, opts) {
|
|
1177
|
+
return (
|
|
1178
|
+
(suiteNames || []).length === 0 &&
|
|
1179
|
+
(fileNames || []).length === 0 &&
|
|
1180
|
+
(opts.framework || "all") === "all" &&
|
|
1181
|
+
(opts.shard || null) === null &&
|
|
1182
|
+
(opts.serviceFilter || null) === null
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1067
1186
|
function loadTimings(productDir) {
|
|
1068
1187
|
const filePath = path.join(productDir, ".testkit", TIMINGS_FILENAME);
|
|
1069
1188
|
if (!fs.existsSync(filePath)) {
|
package/lib/runner/results.mjs
CHANGED
|
@@ -201,6 +201,12 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
|
|
|
201
201
|
export function buildStatusArtifact({
|
|
202
202
|
productDir,
|
|
203
203
|
results,
|
|
204
|
+
suiteType,
|
|
205
|
+
suiteNames,
|
|
206
|
+
fileNames,
|
|
207
|
+
framework,
|
|
208
|
+
shard,
|
|
209
|
+
serviceFilter,
|
|
204
210
|
metadata,
|
|
205
211
|
}) {
|
|
206
212
|
const executedResults = results.filter((result) => !result.skipped);
|
|
@@ -240,6 +246,21 @@ export function buildStatusArtifact({
|
|
|
240
246
|
},
|
|
241
247
|
};
|
|
242
248
|
|
|
249
|
+
const scope = {
|
|
250
|
+
suiteType,
|
|
251
|
+
suiteNames: [...(suiteNames || [])].sort(),
|
|
252
|
+
fileNames: [...(fileNames || [])].sort(),
|
|
253
|
+
framework: formatFrameworkForArtifact(framework || "all"),
|
|
254
|
+
shard: shard || null,
|
|
255
|
+
serviceFilter: serviceFilter || null,
|
|
256
|
+
};
|
|
257
|
+
scope.isFullRun =
|
|
258
|
+
scope.suiteNames.length === 0 &&
|
|
259
|
+
scope.fileNames.length === 0 &&
|
|
260
|
+
scope.framework === "all" &&
|
|
261
|
+
scope.shard === null &&
|
|
262
|
+
scope.serviceFilter === null;
|
|
263
|
+
|
|
243
264
|
return {
|
|
244
265
|
schemaVersion: 1,
|
|
245
266
|
source: "testkit",
|
|
@@ -252,6 +273,7 @@ export function buildStatusArtifact({
|
|
|
252
273
|
commitSha: metadata.git?.commitSha || null,
|
|
253
274
|
},
|
|
254
275
|
testkitVersion: metadata.testkitVersion,
|
|
276
|
+
scope,
|
|
255
277
|
summary,
|
|
256
278
|
tests,
|
|
257
279
|
};
|
|
@@ -194,6 +194,15 @@ describe("runner-results", () => {
|
|
|
194
194
|
commitSha: "abc123",
|
|
195
195
|
},
|
|
196
196
|
testkitVersion: "0.1.20",
|
|
197
|
+
scope: {
|
|
198
|
+
suiteType: "int",
|
|
199
|
+
suiteNames: ["health"],
|
|
200
|
+
fileNames: ["tests/api/integration/b.int.testkit.ts"],
|
|
201
|
+
framework: "default",
|
|
202
|
+
shard: null,
|
|
203
|
+
serviceFilter: "api",
|
|
204
|
+
isFullRun: false,
|
|
205
|
+
},
|
|
197
206
|
summary: {
|
|
198
207
|
services: {
|
|
199
208
|
total: 1,
|
|
@@ -223,4 +232,26 @@ describe("runner-results", () => {
|
|
|
223
232
|
],
|
|
224
233
|
});
|
|
225
234
|
});
|
|
235
|
+
|
|
236
|
+
it("marks unfiltered status artifacts as full runs", () => {
|
|
237
|
+
const status = buildStatusArtifact({
|
|
238
|
+
productDir: "/tmp/my-product",
|
|
239
|
+
results: [],
|
|
240
|
+
suiteType: "all",
|
|
241
|
+
suiteNames: [],
|
|
242
|
+
fileNames: [],
|
|
243
|
+
framework: "all",
|
|
244
|
+
shard: null,
|
|
245
|
+
serviceFilter: null,
|
|
246
|
+
metadata: {
|
|
247
|
+
git: {
|
|
248
|
+
branch: "main",
|
|
249
|
+
commitSha: "abc123",
|
|
250
|
+
},
|
|
251
|
+
testkitVersion: "0.1.20",
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(status.scope.isFullRun).toBe(true);
|
|
256
|
+
});
|
|
226
257
|
});
|
package/lib/runtime/index.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import rawHttp from "k6/http";
|
|
2
|
+
import { Rate, Trend } from "k6/metrics";
|
|
2
3
|
import { check, fail, group, sleep } from "k6";
|
|
3
4
|
|
|
4
5
|
export { check, fail, group, sleep };
|
|
6
|
+
export { Rate, Trend };
|
|
5
7
|
export const http = rawHttp;
|
|
6
8
|
|
|
7
9
|
export function file(data, filename, contentType) {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { Rate } from "k6/metrics";
|
|
2
|
+
|
|
3
|
+
export const runtimeFailures = new Rate("testkit_runtime_failures");
|
|
4
|
+
|
|
1
5
|
export function singleIterationOptions(overrides = {}) {
|
|
2
6
|
return {
|
|
3
7
|
iterations: 1,
|
|
4
8
|
thresholds: {
|
|
5
9
|
checks: ["rate==1.0"],
|
|
10
|
+
testkit_runtime_failures: ["rate==0"],
|
|
6
11
|
...(overrides.thresholds || {}),
|
|
7
12
|
},
|
|
8
13
|
...overrides,
|
|
@@ -37,3 +42,7 @@ export function isSorted(rows, field, direction = "asc") {
|
|
|
37
42
|
|
|
38
43
|
return true;
|
|
39
44
|
}
|
|
45
|
+
|
|
46
|
+
export function recordRuntimeFailure() {
|
|
47
|
+
runtimeFailures.add(1);
|
|
48
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fail } from "k6";
|
|
2
|
+
import { defaultOptions, recordRuntimeFailure } from "./checks.js";
|
|
2
3
|
import { createDalContext, openDb } from "./dal.js";
|
|
3
4
|
|
|
4
5
|
export function defineDalSuite(configOrRun, maybeRun) {
|
|
@@ -10,14 +11,24 @@ export function defineDalSuite(configOrRun, maybeRun) {
|
|
|
10
11
|
options: config.options || defaultOptions,
|
|
11
12
|
setup() {
|
|
12
13
|
if (typeof config.setup !== "function") return null;
|
|
13
|
-
|
|
14
|
+
try {
|
|
15
|
+
return config.setup({ db, dal });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
recordRuntimeFailure();
|
|
18
|
+
fail(formatFatalSuiteError("setup", error));
|
|
19
|
+
}
|
|
14
20
|
},
|
|
15
21
|
exec(setupData) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
try {
|
|
23
|
+
return run({
|
|
24
|
+
db,
|
|
25
|
+
dal,
|
|
26
|
+
setupData,
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
recordRuntimeFailure();
|
|
30
|
+
fail(formatFatalSuiteError("exec", error));
|
|
31
|
+
}
|
|
21
32
|
},
|
|
22
33
|
};
|
|
23
34
|
}
|
|
@@ -31,3 +42,10 @@ function normalizeSuiteArgs(configOrRun, maybeRun) {
|
|
|
31
42
|
}
|
|
32
43
|
return { config: configOrRun || {}, run: maybeRun };
|
|
33
44
|
}
|
|
45
|
+
|
|
46
|
+
function formatFatalSuiteError(phase, error) {
|
|
47
|
+
if (error instanceof Error) {
|
|
48
|
+
return `Uncaught testkit suite error during ${phase}: ${error.message}`;
|
|
49
|
+
}
|
|
50
|
+
return `Uncaught testkit suite error during ${phase}: ${String(error)}`;
|
|
51
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fail } from "k6";
|
|
2
|
+
import { defaultOptions, recordRuntimeFailure } from "./checks.js";
|
|
2
3
|
import { createHttpClient, getEnv } from "./http.js";
|
|
3
4
|
|
|
4
5
|
export function defineHttpSuite(configOrRun, maybeRun) {
|
|
@@ -24,17 +25,27 @@ export function defineHttpSuite(configOrRun, maybeRun) {
|
|
|
24
25
|
options: config.options || defaultOptions,
|
|
25
26
|
setup() {
|
|
26
27
|
if (typeof auth?.setup !== "function") return null;
|
|
27
|
-
|
|
28
|
+
try {
|
|
29
|
+
return auth.setup({ env });
|
|
30
|
+
} catch (error) {
|
|
31
|
+
recordRuntimeFailure();
|
|
32
|
+
fail(formatFatalSuiteError("setup", error));
|
|
33
|
+
}
|
|
28
34
|
},
|
|
29
35
|
exec(setupData) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
try {
|
|
37
|
+
return run({
|
|
38
|
+
env,
|
|
39
|
+
req: client.request,
|
|
40
|
+
rawReq: client.raw,
|
|
41
|
+
getWithHeaders: client.getWithHeaders,
|
|
42
|
+
setupData,
|
|
43
|
+
session: setupData,
|
|
44
|
+
});
|
|
45
|
+
} catch (error) {
|
|
46
|
+
recordRuntimeFailure();
|
|
47
|
+
fail(formatFatalSuiteError("exec", error));
|
|
48
|
+
}
|
|
38
49
|
},
|
|
39
50
|
};
|
|
40
51
|
}
|
|
@@ -53,3 +64,10 @@ function callHeaders(builder, setupData, env) {
|
|
|
53
64
|
if (typeof builder !== "function") return {};
|
|
54
65
|
return builder(setupData, { env }) || {};
|
|
55
66
|
}
|
|
67
|
+
|
|
68
|
+
function formatFatalSuiteError(phase, error) {
|
|
69
|
+
if (error instanceof Error) {
|
|
70
|
+
return `Uncaught testkit suite error during ${phase}: ${error.message}`;
|
|
71
|
+
}
|
|
72
|
+
return `Uncaught testkit suite error during ${phase}: ${String(error)}`;
|
|
73
|
+
}
|