@elench/testkit 0.1.52 → 0.1.54
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 -0
- package/bin/testkit.mjs +4 -6
- package/lib/cli/command-helpers.mjs +170 -0
- package/lib/cli/commands/artifacts.mjs +45 -0
- package/lib/cli/commands/cleanup.mjs +15 -0
- package/lib/cli/commands/db/snapshot/capture.mjs +22 -0
- package/lib/cli/commands/destroy.mjs +15 -0
- package/lib/cli/commands/known-failures/render.mjs +19 -0
- package/lib/cli/commands/known-failures/validate.mjs +20 -0
- package/lib/cli/commands/logs.mjs +47 -0
- package/lib/cli/commands/run.mjs +23 -0
- package/lib/cli/commands/show.mjs +47 -0
- package/lib/cli/commands/status.mjs +15 -0
- package/lib/cli/commands/watch.mjs +23 -0
- package/lib/cli/entrypoint.mjs +83 -0
- package/lib/cli/index.mjs +6 -116
- package/lib/cli/presentation/code-frames.mjs +57 -0
- package/lib/cli/presentation/code-frames.test.mjs +71 -0
- package/lib/cli/presentation/colors.mjs +29 -0
- package/lib/cli/presentation/run-reporter.mjs +100 -0
- package/lib/cli/tui/watch-app.mjs +104 -0
- package/lib/cli/viewer.mjs +268 -0
- package/lib/known-failures/index.mjs +1 -1
- package/lib/known-failures/index.test.mjs +46 -0
- package/lib/runner/artifacts.mjs +35 -0
- package/lib/runner/default-runtime-errors.mjs +66 -0
- package/lib/runner/default-runtime-runner.mjs +52 -11
- package/lib/runner/failure-details.mjs +31 -0
- package/lib/runner/failure-details.test.mjs +51 -0
- package/lib/runner/formatting.mjs +207 -0
- package/lib/runner/formatting.test.mjs +81 -6
- package/lib/runner/logs.mjs +89 -0
- package/lib/runner/orchestrator.mjs +51 -20
- package/lib/runner/playwright-runner.mjs +15 -7
- package/lib/runner/processes.mjs +9 -11
- package/lib/runner/reporting.mjs +5 -1
- package/lib/runner/reporting.test.mjs +4 -1
- package/lib/runner/runtime-contexts.mjs +7 -3
- package/lib/runner/runtime-manager.mjs +8 -2
- package/lib/runner/runtime-preparation.mjs +9 -4
- package/lib/runner/services.mjs +25 -8
- package/lib/runner/template-steps.mjs +4 -3
- package/lib/runner/triage.mjs +67 -0
- package/lib/runner/worker-loop.mjs +8 -7
- package/lib/runtime/index.d.ts +60 -0
- package/lib/runtime/index.mjs +12 -0
- package/lib/runtime-src/k6/checks.js +45 -12
- package/lib/runtime-src/k6/http-assertions.js +214 -0
- package/lib/runtime-src/k6/http.js +261 -13
- package/lib/runtime-src/k6/suite.js +46 -1
- package/lib/toolchains/index.mjs +6 -3
- package/package.json +13 -3
package/lib/runner/processes.mjs
CHANGED
|
@@ -55,9 +55,12 @@ export function killChildProcess(child, signal) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
export function
|
|
58
|
+
export function captureOutput(stream, options = {}) {
|
|
59
59
|
if (!stream) return Promise.resolve();
|
|
60
60
|
|
|
61
|
+
const onLine = typeof options.onLine === "function" ? options.onLine : null;
|
|
62
|
+
const liveWriter = typeof options.liveWriter === "function" ? options.liveWriter : null;
|
|
63
|
+
const livePrefix = options.livePrefix || "";
|
|
61
64
|
let pending = "";
|
|
62
65
|
return new Promise((resolve) => {
|
|
63
66
|
let settled = false;
|
|
@@ -72,12 +75,15 @@ export function pipeOutput(stream, prefix) {
|
|
|
72
75
|
const lines = pending.split(/\r?\n/);
|
|
73
76
|
pending = lines.pop() || "";
|
|
74
77
|
for (const line of lines) {
|
|
75
|
-
if (line.length
|
|
78
|
+
if (line.length === 0) continue;
|
|
79
|
+
onLine?.(line);
|
|
80
|
+
if (liveWriter) liveWriter(livePrefix ? `${livePrefix} ${line}` : line);
|
|
76
81
|
}
|
|
77
82
|
});
|
|
78
83
|
stream.on("end", () => {
|
|
79
84
|
if (pending.length > 0) {
|
|
80
|
-
|
|
85
|
+
onLine?.(pending);
|
|
86
|
+
if (liveWriter) liveWriter(livePrefix ? `${livePrefix} ${pending}` : pending);
|
|
81
87
|
}
|
|
82
88
|
settle();
|
|
83
89
|
});
|
|
@@ -86,14 +92,6 @@ export function pipeOutput(stream, prefix) {
|
|
|
86
92
|
});
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
export function printBufferedOutput(output, prefix) {
|
|
90
|
-
for (const line of output.split(/\r?\n/)) {
|
|
91
|
-
if (line.trim().length > 0) {
|
|
92
|
-
console.log(`${prefix} ${line}`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
95
|
export async function stopChildProcess(child, outputDrains = []) {
|
|
98
96
|
if (!child) return;
|
|
99
97
|
if (child.exitCode !== null) {
|
package/lib/runner/reporting.mjs
CHANGED
|
@@ -111,6 +111,7 @@ export function buildRunArtifact({
|
|
|
111
111
|
serviceFilter,
|
|
112
112
|
metadata,
|
|
113
113
|
summarizeDbBackend,
|
|
114
|
+
serviceLogs = [],
|
|
114
115
|
}) {
|
|
115
116
|
const executed = results.filter((result) => !result.skipped);
|
|
116
117
|
const effectivelySkippedServices = executed.filter(isEffectivelySkippedService);
|
|
@@ -127,7 +128,7 @@ export function buildRunArtifact({
|
|
|
127
128
|
const dbBackend = summarizeDbBackend(results);
|
|
128
129
|
|
|
129
130
|
return {
|
|
130
|
-
schemaVersion:
|
|
131
|
+
schemaVersion: 7,
|
|
131
132
|
source: "testkit",
|
|
132
133
|
generatedAt: new Date(finishedAt).toISOString(),
|
|
133
134
|
product: {
|
|
@@ -176,6 +177,9 @@ export function buildRunArtifact({
|
|
|
176
177
|
notRun: notRunFiles,
|
|
177
178
|
},
|
|
178
179
|
},
|
|
180
|
+
logs: {
|
|
181
|
+
services: serviceLogs,
|
|
182
|
+
},
|
|
179
183
|
services: results.map((result) => ({
|
|
180
184
|
name: result.name,
|
|
181
185
|
failed: result.failed,
|
|
@@ -78,7 +78,7 @@ describe("runner reporting", () => {
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
expect(artifact.product.name).toBe("my-product");
|
|
81
|
-
expect(artifact.schemaVersion).toBe(
|
|
81
|
+
expect(artifact.schemaVersion).toBe(7);
|
|
82
82
|
expect(artifact.run).toMatchObject({
|
|
83
83
|
workers: 2,
|
|
84
84
|
fileTimeoutSeconds: 60,
|
|
@@ -107,6 +107,9 @@ describe("runner reporting", () => {
|
|
|
107
107
|
});
|
|
108
108
|
expect(artifact.services[0].durationMs).toBe(1200);
|
|
109
109
|
expect(artifact.services[0].totalTaskDurationMs).toBe(2400);
|
|
110
|
+
expect(artifact.logs).toEqual({
|
|
111
|
+
services: [],
|
|
112
|
+
});
|
|
110
113
|
});
|
|
111
114
|
|
|
112
115
|
it("builds deterministic status artifacts", () => {
|
|
@@ -35,12 +35,12 @@ export function createRuntimeInstanceContext(runtimeId, graph, productDir) {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export async function ensureRuntimeInstanceReady(context, task, lifecycle) {
|
|
38
|
+
export async function ensureRuntimeInstanceReady(context, task, lifecycle, options = {}) {
|
|
39
39
|
if (!context.prepared) {
|
|
40
40
|
if (!context.preparationPromise) {
|
|
41
41
|
context.preparationPromise = (async () => {
|
|
42
42
|
await prepareDatabases(context.runtimeConfigs);
|
|
43
|
-
await prepareRuntimeServices(context.runtimeConfigs);
|
|
43
|
+
await prepareRuntimeServices(context.runtimeConfigs, options);
|
|
44
44
|
context.prepared = true;
|
|
45
45
|
})().finally(() => {
|
|
46
46
|
context.preparationPromise = null;
|
|
@@ -52,7 +52,11 @@ export async function ensureRuntimeInstanceReady(context, task, lifecycle) {
|
|
|
52
52
|
if (taskNeedsLocalRuntime(task) && !context.started) {
|
|
53
53
|
if (!context.startupPromise) {
|
|
54
54
|
context.startupPromise = (async () => {
|
|
55
|
-
context.startedServices = await startLocalServices(
|
|
55
|
+
context.startedServices = await startLocalServices(
|
|
56
|
+
context.runtimeConfigs,
|
|
57
|
+
lifecycle,
|
|
58
|
+
options
|
|
59
|
+
);
|
|
56
60
|
context.started = true;
|
|
57
61
|
})().finally(() => {
|
|
58
62
|
context.startupPromise = null;
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
ensureRuntimeInstanceReady,
|
|
9
9
|
} from "./runtime-contexts.mjs";
|
|
10
10
|
|
|
11
|
-
export function createRuntimeManager({ productDir, graphs, lifecycle, hooks = {} }) {
|
|
11
|
+
export function createRuntimeManager({ productDir, graphs, lifecycle, hooks = {}, runtimeOptions = {} }) {
|
|
12
12
|
const graphByKey = new Map(graphs.map((graph) => [graph.key, graph]));
|
|
13
13
|
const pools = new Map();
|
|
14
14
|
const locks = new Map();
|
|
@@ -19,6 +19,7 @@ export function createRuntimeManager({ productDir, graphs, lifecycle, hooks = {}
|
|
|
19
19
|
createRuntimeInstanceContext,
|
|
20
20
|
ensureRuntimeInstanceReady,
|
|
21
21
|
sleep,
|
|
22
|
+
options: runtimeOptions,
|
|
22
23
|
...hooks,
|
|
23
24
|
};
|
|
24
25
|
|
|
@@ -199,7 +200,12 @@ async function getReadyContext(slot, productDir, task, lifecycle, runtimeHooks)
|
|
|
199
200
|
slot.contextPromise = Promise.resolve(slot.context);
|
|
200
201
|
}
|
|
201
202
|
await slot.contextPromise;
|
|
202
|
-
await runtimeHooks.ensureRuntimeInstanceReady(
|
|
203
|
+
await runtimeHooks.ensureRuntimeInstanceReady(
|
|
204
|
+
slot.context,
|
|
205
|
+
task,
|
|
206
|
+
lifecycle,
|
|
207
|
+
runtimeHooks.options || {}
|
|
208
|
+
);
|
|
203
209
|
return slot.context;
|
|
204
210
|
}
|
|
205
211
|
|
|
@@ -13,13 +13,13 @@ import {
|
|
|
13
13
|
|
|
14
14
|
const MANIFEST_FILE = "prepare-manifest.json";
|
|
15
15
|
|
|
16
|
-
export async function prepareRuntimeServices(runtimeConfigs) {
|
|
16
|
+
export async function prepareRuntimeServices(runtimeConfigs, options = {}) {
|
|
17
17
|
for (const config of runtimeConfigs) {
|
|
18
|
-
await prepareRuntimeService(config);
|
|
18
|
+
await prepareRuntimeService(config, options);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export async function prepareRuntimeService(config) {
|
|
22
|
+
export async function prepareRuntimeService(config, options = {}) {
|
|
23
23
|
const prepare = config.testkit.runtime.prepare;
|
|
24
24
|
if (!prepare || prepare.steps.length === 0) return;
|
|
25
25
|
|
|
@@ -43,12 +43,17 @@ export async function prepareRuntimeService(config) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
try {
|
|
46
|
-
await announceResolvedToolchain(
|
|
46
|
+
await announceResolvedToolchain(
|
|
47
|
+
config,
|
|
48
|
+
await resolveConfiguredToolchain(config),
|
|
49
|
+
options.reporter
|
|
50
|
+
);
|
|
47
51
|
await runConfiguredSteps({
|
|
48
52
|
config,
|
|
49
53
|
steps: prepare.steps,
|
|
50
54
|
env,
|
|
51
55
|
labelPrefix: "runtime:prepare",
|
|
56
|
+
reporter: options.reporter,
|
|
52
57
|
});
|
|
53
58
|
writePrepareManifest(manifestPath, {
|
|
54
59
|
fingerprint,
|
package/lib/runner/services.mjs
CHANGED
|
@@ -6,16 +6,16 @@ import {
|
|
|
6
6
|
} from "../toolchains/index.mjs";
|
|
7
7
|
import { buildExecutionEnv, numericPortFromUrl } from "./template.mjs";
|
|
8
8
|
import { DEFAULT_READY_TIMEOUT_MS, assertLocalServicePortsAvailable, isPortInUse, waitForReady } from "./readiness.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { captureOutput, killChildProcess, startDetachedCommand, stopChildProcess, sleep } from "./processes.mjs";
|
|
10
10
|
import { readDatabaseUrl } from "./state-io.mjs";
|
|
11
11
|
|
|
12
|
-
export async function startLocalServices(runtimeConfigs, lifecycle) {
|
|
12
|
+
export async function startLocalServices(runtimeConfigs, lifecycle, options = {}) {
|
|
13
13
|
const started = [];
|
|
14
14
|
|
|
15
15
|
try {
|
|
16
16
|
for (const config of runtimeConfigs) {
|
|
17
17
|
if (!config.testkit.local) continue;
|
|
18
|
-
const proc = await startLocalService(config, lifecycle);
|
|
18
|
+
const proc = await startLocalService(config, lifecycle, options);
|
|
19
19
|
started.push(proc);
|
|
20
20
|
}
|
|
21
21
|
} catch (error) {
|
|
@@ -26,10 +26,10 @@ export async function startLocalServices(runtimeConfigs, lifecycle) {
|
|
|
26
26
|
return started;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export async function startLocalService(config, lifecycle) {
|
|
29
|
+
export async function startLocalService(config, lifecycle, options = {}) {
|
|
30
30
|
const cwd = resolveServiceCwd(config.productDir, config.testkit.local.cwd);
|
|
31
31
|
const resolvedToolchain = await resolveConfiguredToolchain(config);
|
|
32
|
-
await announceResolvedToolchain(config, resolvedToolchain);
|
|
32
|
+
await announceResolvedToolchain(config, resolvedToolchain, options.reporter);
|
|
33
33
|
const env = applyToolchainEnv(
|
|
34
34
|
buildExecutionEnv(config, config.testkit.local.env, process.env),
|
|
35
35
|
resolvedToolchain
|
|
@@ -46,12 +46,29 @@ export async function startLocalService(config, lifecycle) {
|
|
|
46
46
|
|
|
47
47
|
await assertLocalServicePortsAvailable(config, isPortInUse);
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
options.reporter?.localServiceStarting?.(config, config.testkit.local.start);
|
|
50
50
|
const child = startDetachedCommand(config.testkit.local.start, cwd, env);
|
|
51
|
+
const logRecord = options.logRegistry?.ensureServiceLogRecord(config);
|
|
52
|
+
const liveWriter =
|
|
53
|
+
options.reporter?.outputMode === "debug"
|
|
54
|
+
? (line) => options.reporter.writeDebugLine?.(line)
|
|
55
|
+
: null;
|
|
51
56
|
|
|
52
57
|
const outputDrains = [
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
captureOutput(child.stdout, {
|
|
59
|
+
livePrefix: `[${config.runtimeLabel}:${config.name}]`,
|
|
60
|
+
liveWriter,
|
|
61
|
+
onLine(line) {
|
|
62
|
+
if (logRecord) options.logRegistry.append(logRecord, "stdout", line);
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
captureOutput(child.stderr, {
|
|
66
|
+
livePrefix: `[${config.runtimeLabel}:${config.name}]`,
|
|
67
|
+
liveWriter,
|
|
68
|
+
onLine(line) {
|
|
69
|
+
if (logRecord) options.logRegistry.append(logRecord, "stderr", line);
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
55
72
|
];
|
|
56
73
|
lifecycle.registerService(config, child, cwd, () => {
|
|
57
74
|
killChildProcess(child, "SIGTERM");
|
|
@@ -23,14 +23,15 @@ const MODULE_RUNNER_ENTRY = path.join(
|
|
|
23
23
|
"template-step-module-runner.mjs"
|
|
24
24
|
);
|
|
25
25
|
|
|
26
|
-
export async function runConfiguredSteps({ config, steps = [], env, labelPrefix }) {
|
|
26
|
+
export async function runConfiguredSteps({ config, steps = [], env, labelPrefix, reporter = null }) {
|
|
27
27
|
if (steps.length === 0) return;
|
|
28
28
|
const resolvedToolchain = await resolveConfiguredToolchain(config);
|
|
29
|
-
await announceResolvedToolchain(config, resolvedToolchain);
|
|
29
|
+
await announceResolvedToolchain(config, resolvedToolchain, reporter);
|
|
30
30
|
|
|
31
31
|
for (const [index, step] of steps.entries()) {
|
|
32
32
|
const label = `${labelPrefix}:${config.name}:${index + 1}`;
|
|
33
|
-
|
|
33
|
+
if (reporter?.phaseStarted) reporter.phaseStarted(label);
|
|
34
|
+
else console.log(`\n── ${label} ──`);
|
|
34
35
|
await runConfiguredStep(config, step, env, resolvedToolchain);
|
|
35
36
|
}
|
|
36
37
|
}
|
package/lib/runner/triage.mjs
CHANGED
|
@@ -80,6 +80,49 @@ export function applyKnownFailuresToArtifacts(runArtifact, statusArtifact, known
|
|
|
80
80
|
return { runArtifact, statusArtifact };
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
export function applyKnownFailureIssueValidationToArtifacts(
|
|
84
|
+
runArtifact,
|
|
85
|
+
statusArtifact,
|
|
86
|
+
issueValidation
|
|
87
|
+
) {
|
|
88
|
+
if (!issueValidation) return { runArtifact, statusArtifact };
|
|
89
|
+
|
|
90
|
+
const validationById = new Map(
|
|
91
|
+
(issueValidation.entries || []).map((entry) => [entry.id, entry])
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
for (const entry of [...extractRunFileEntries(runArtifact), ...extractStatusFileEntries(statusArtifact)]) {
|
|
95
|
+
const triage = entry.target ? entry.target.triage : entry.triage;
|
|
96
|
+
if (!triage?.entries?.length) continue;
|
|
97
|
+
|
|
98
|
+
const matchedValidationEntries = triage.entries
|
|
99
|
+
.map((triageEntry) => validationById.get(triageEntry.id))
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
|
|
102
|
+
if (matchedValidationEntries.length === 0) continue;
|
|
103
|
+
|
|
104
|
+
const enrichedEntries = triage.entries.map((triageEntry) => {
|
|
105
|
+
const validationEntry = validationById.get(triageEntry.id);
|
|
106
|
+
if (!validationEntry) return triageEntry;
|
|
107
|
+
return {
|
|
108
|
+
...triageEntry,
|
|
109
|
+
github: validationEntry.github,
|
|
110
|
+
validationStatus: validationEntry.status,
|
|
111
|
+
findings: validationEntry.findings,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const nextTriage = {
|
|
116
|
+
...triage,
|
|
117
|
+
entries: enrichedEntries,
|
|
118
|
+
availability: summarizeIssueValidationAvailability(issueValidation, matchedValidationEntries),
|
|
119
|
+
};
|
|
120
|
+
setEntryTriage(entry, nextTriage);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { runArtifact, statusArtifact };
|
|
124
|
+
}
|
|
125
|
+
|
|
83
126
|
function toArtifactTriageEntry(entry) {
|
|
84
127
|
return {
|
|
85
128
|
id: entry.id,
|
|
@@ -152,3 +195,27 @@ function setEntryTriage(entry, triage) {
|
|
|
152
195
|
}
|
|
153
196
|
entry.triage = triage;
|
|
154
197
|
}
|
|
198
|
+
|
|
199
|
+
function summarizeIssueValidationAvailability(issueValidation, entries) {
|
|
200
|
+
if (entries.some((entry) => entry.github?.cached)) {
|
|
201
|
+
return {
|
|
202
|
+
mode: "cache",
|
|
203
|
+
reason: issueValidation.availability?.usedCachedFallback
|
|
204
|
+
? "used stale cache"
|
|
205
|
+
: "used cached issue metadata",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (entries.some((entry) => entry.status === "validation_unavailable")) {
|
|
209
|
+
const globalReason = (issueValidation.findings || []).find(
|
|
210
|
+
(finding) => finding.code === "validation_unavailable"
|
|
211
|
+
);
|
|
212
|
+
return {
|
|
213
|
+
mode: "offline",
|
|
214
|
+
reason: globalReason?.message || "validation unavailable",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
mode: "live",
|
|
219
|
+
reason: null,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -23,10 +23,10 @@ export async function runWorker(
|
|
|
23
23
|
lifecycle,
|
|
24
24
|
claimNextTask,
|
|
25
25
|
recordTaskOutcome,
|
|
26
|
-
recordGraphError
|
|
26
|
+
recordGraphError,
|
|
27
|
+
reporter = null
|
|
27
28
|
) {
|
|
28
29
|
const startedAt = Date.now();
|
|
29
|
-
console.log(`\n══ worker ${worker.workerId} ══`);
|
|
30
30
|
const errors = [];
|
|
31
31
|
|
|
32
32
|
try {
|
|
@@ -69,8 +69,9 @@ export async function runWorker(
|
|
|
69
69
|
}
|
|
70
70
|
worker.currentGraphKey = task.graphKey;
|
|
71
71
|
lease = await runtimeManager.acquire(task);
|
|
72
|
-
const outcome = await runTask(lease.context, task, lifecycle, lease);
|
|
72
|
+
const outcome = await runTask(lease.context, task, lifecycle, lease, reporter);
|
|
73
73
|
recordTaskOutcome(trackers, outcome.task, outcome);
|
|
74
|
+
reporter?.taskFinished?.(outcome.task, outcome);
|
|
74
75
|
timingUpdates.push({
|
|
75
76
|
key: outcome.task.timingKey,
|
|
76
77
|
durationMs: outcome.durationMs,
|
|
@@ -100,20 +101,20 @@ export async function runWorker(
|
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
async function runTask(context, task, lifecycle, lease) {
|
|
104
|
+
async function runTask(context, task, lifecycle, lease, reporter = null) {
|
|
104
105
|
const targetConfig = context.configByName.get(task.targetName);
|
|
105
106
|
if (!targetConfig) {
|
|
106
107
|
throw new Error(`Runtime instance is missing target config "${task.targetName}"`);
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
if (task.framework === "playwright") {
|
|
110
|
-
return runPlaywrightTask(targetConfig, task, lifecycle, lease);
|
|
111
|
+
return runPlaywrightTask(targetConfig, task, lifecycle, lease, reporter);
|
|
111
112
|
}
|
|
112
113
|
if (task.type === "dal") {
|
|
113
|
-
return runDalTask(targetConfig, task, lifecycle, lease);
|
|
114
|
+
return runDalTask(targetConfig, task, lifecycle, lease, reporter);
|
|
114
115
|
}
|
|
115
116
|
if (task.framework === "k6" && HTTP_K6_TYPES.has(task.type)) {
|
|
116
|
-
return runHttpK6Task(targetConfig, task, lifecycle, lease);
|
|
117
|
+
return runHttpK6Task(targetConfig, task, lifecycle, lease, reporter);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
throw new Error(
|
package/lib/runtime/index.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ export interface RuntimeCookie {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface RuntimeResponse {
|
|
12
|
+
__testkit?: {
|
|
13
|
+
httpTrace?: RuntimeHttpTrace;
|
|
14
|
+
};
|
|
12
15
|
body: string;
|
|
13
16
|
cookies?: Record<string, RuntimeCookie[]>;
|
|
14
17
|
headers?: Record<string, string | string[] | undefined>;
|
|
@@ -18,6 +21,24 @@ export interface RuntimeResponse {
|
|
|
18
21
|
};
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
export interface RuntimeHttpTrace {
|
|
25
|
+
id: string;
|
|
26
|
+
requestId: string;
|
|
27
|
+
startedAt: string;
|
|
28
|
+
finishedAt?: string;
|
|
29
|
+
durationMs?: number;
|
|
30
|
+
method: RuntimeMethod;
|
|
31
|
+
path: string;
|
|
32
|
+
url: string;
|
|
33
|
+
requestHeaders: Record<string, string | string[] | null>;
|
|
34
|
+
response?: {
|
|
35
|
+
status: number | null;
|
|
36
|
+
contentType: string | string[] | null;
|
|
37
|
+
bodyPreview: string | null;
|
|
38
|
+
bodyTruncated: boolean;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
21
42
|
export interface RuntimeOptions {
|
|
22
43
|
[key: string]: unknown;
|
|
23
44
|
thresholds?: Record<string, unknown>;
|
|
@@ -144,6 +165,12 @@ export declare const check: <T>(
|
|
|
144
165
|
value: T,
|
|
145
166
|
checks: Record<string, (value: T) => boolean>
|
|
146
167
|
) => boolean;
|
|
168
|
+
export declare const evaluateCheck: <T>(
|
|
169
|
+
value: T,
|
|
170
|
+
checkName: string,
|
|
171
|
+
predicate: (value: T) => boolean,
|
|
172
|
+
detailFactory?: (() => Record<string, unknown>) | null
|
|
173
|
+
) => boolean;
|
|
147
174
|
export declare const fail: (message: string) => never;
|
|
148
175
|
export declare const group: (name: string, fn: () => void) => void;
|
|
149
176
|
export declare const sleep: (seconds?: number) => void;
|
|
@@ -152,6 +179,9 @@ export declare const http: RuntimeHttpClient;
|
|
|
152
179
|
|
|
153
180
|
export declare function file(data: unknown, filename?: string, contentType?: string): unknown;
|
|
154
181
|
export declare function json<T = unknown>(response: Pick<RuntimeResponse, "body">): T;
|
|
182
|
+
export declare function safeJson<T = unknown>(
|
|
183
|
+
response: Pick<RuntimeResponse, "body">
|
|
184
|
+
): { ok: true; value: T } | { ok: false; error: string };
|
|
155
185
|
export declare function remainingTimeSeconds(): number;
|
|
156
186
|
export declare function waitFor<T>(
|
|
157
187
|
read: () => T,
|
|
@@ -188,6 +218,9 @@ export declare function truncate(db: RuntimeDb, ...tables: string[]): void;
|
|
|
188
218
|
export declare function getTestkitContext(): TestkitRuntimeContext;
|
|
189
219
|
|
|
190
220
|
export declare function getEnv(): RuntimeEnv;
|
|
221
|
+
export declare function getHttpTrace(response: RuntimeResponse): RuntimeHttpTrace | null;
|
|
222
|
+
export declare function summarizeHttpTrace(response: RuntimeResponse): RuntimeHttpTrace | null;
|
|
223
|
+
export declare function toBodyPreview(response: RuntimeResponse): string | null;
|
|
191
224
|
export declare function createHttpClient<TSetup = unknown>(
|
|
192
225
|
config: HttpClientConfig<TSetup>
|
|
193
226
|
): HttpClient<TSetup>;
|
|
@@ -207,6 +240,33 @@ export declare function makeGetWithHeaders<TSetup = unknown>(
|
|
|
207
240
|
getHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void
|
|
208
241
|
): HttpClient<TSetup>["getWithHeaders"];
|
|
209
242
|
|
|
243
|
+
export declare function expectStatus(
|
|
244
|
+
response: RuntimeResponse,
|
|
245
|
+
expected: number,
|
|
246
|
+
label?: string | null
|
|
247
|
+
): boolean;
|
|
248
|
+
export declare function expectStatusOneOf(
|
|
249
|
+
response: RuntimeResponse,
|
|
250
|
+
expectedValues: number[],
|
|
251
|
+
label?: string | null
|
|
252
|
+
): boolean;
|
|
253
|
+
export declare function expectNotStatus(
|
|
254
|
+
response: RuntimeResponse,
|
|
255
|
+
unexpected: number,
|
|
256
|
+
label?: string | null
|
|
257
|
+
): boolean;
|
|
258
|
+
export declare function expectJson<T = unknown>(
|
|
259
|
+
response: RuntimeResponse,
|
|
260
|
+
predicate: (value: T) => boolean,
|
|
261
|
+
label?: string | null
|
|
262
|
+
): boolean;
|
|
263
|
+
export declare function expectJsonPath(
|
|
264
|
+
response: RuntimeResponse,
|
|
265
|
+
path: string,
|
|
266
|
+
predicate: (value: unknown) => boolean,
|
|
267
|
+
label?: string | null
|
|
268
|
+
): boolean;
|
|
269
|
+
|
|
210
270
|
declare global {
|
|
211
271
|
const __ENV: Record<string, string | undefined>;
|
|
212
272
|
}
|
package/lib/runtime/index.mjs
CHANGED
|
@@ -26,10 +26,18 @@ export {
|
|
|
26
26
|
allMatch,
|
|
27
27
|
contains,
|
|
28
28
|
defaultOptions,
|
|
29
|
+
evaluateCheck,
|
|
29
30
|
isSorted,
|
|
30
31
|
json,
|
|
31
32
|
singleIterationOptions,
|
|
32
33
|
} from "../runtime-src/k6/checks.js";
|
|
34
|
+
export {
|
|
35
|
+
expectJson,
|
|
36
|
+
expectJsonPath,
|
|
37
|
+
expectNotStatus,
|
|
38
|
+
expectStatus,
|
|
39
|
+
expectStatusOneOf,
|
|
40
|
+
} from "../runtime-src/k6/http-assertions.js";
|
|
33
41
|
export {
|
|
34
42
|
createDalContext,
|
|
35
43
|
openDb,
|
|
@@ -39,9 +47,13 @@ export {
|
|
|
39
47
|
createHttpClient,
|
|
40
48
|
defaultOptions as httpDefaultOptions,
|
|
41
49
|
getEnv,
|
|
50
|
+
getHttpTrace,
|
|
42
51
|
makeGetWithHeaders,
|
|
43
52
|
makeRawReq,
|
|
44
53
|
makeReq,
|
|
54
|
+
safeJson,
|
|
55
|
+
summarizeHttpTrace,
|
|
56
|
+
toBodyPreview,
|
|
45
57
|
} from "../runtime-src/k6/http.js";
|
|
46
58
|
|
|
47
59
|
export function getTestkitContext() {
|
|
@@ -24,23 +24,33 @@ export function check(value, checks) {
|
|
|
24
24
|
|
|
25
25
|
for (const [name, predicate] of Object.entries(checks || {})) {
|
|
26
26
|
const checkName = normalizeLabel(name, "unnamed check");
|
|
27
|
-
const passed =
|
|
28
|
-
if (!passed)
|
|
29
|
-
recordFailureDetail({
|
|
30
|
-
kind: "k6-check",
|
|
31
|
-
key: buildFailureKey(failureState.groupStack, checkName),
|
|
32
|
-
title: checkName,
|
|
33
|
-
checkName,
|
|
34
|
-
groupPath: [...failureState.groupStack],
|
|
35
|
-
phase: failureState.phase,
|
|
36
|
-
});
|
|
37
|
-
allPassed = false;
|
|
38
|
-
}
|
|
27
|
+
const passed = evaluateCheck(value, checkName, predicate);
|
|
28
|
+
if (!passed) allPassed = false;
|
|
39
29
|
}
|
|
40
30
|
|
|
41
31
|
return allPassed;
|
|
42
32
|
}
|
|
43
33
|
|
|
34
|
+
export function evaluateCheck(value, checkName, predicate, detailFactory = null) {
|
|
35
|
+
const normalizedName = normalizeLabel(checkName, "unnamed check");
|
|
36
|
+
const passed = k6Check(value, { [normalizedName]: predicate });
|
|
37
|
+
if (!passed) {
|
|
38
|
+
recordFailureDetail(
|
|
39
|
+
typeof detailFactory === "function"
|
|
40
|
+
? detailFactory()
|
|
41
|
+
: {
|
|
42
|
+
kind: "k6-check",
|
|
43
|
+
key: buildFailureKey(failureState.groupStack, normalizedName),
|
|
44
|
+
title: normalizedName,
|
|
45
|
+
checkName: normalizedName,
|
|
46
|
+
groupPath: [...failureState.groupStack],
|
|
47
|
+
phase: failureState.phase,
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return passed;
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
export function group(name, fn) {
|
|
45
55
|
const groupName = normalizeLabel(name, "unnamed group");
|
|
46
56
|
|
|
@@ -158,6 +168,24 @@ function normalizeFailureDetail(detail) {
|
|
|
158
168
|
const message = normalizeLabel(detail.message, null);
|
|
159
169
|
if (message) normalized.message = message;
|
|
160
170
|
|
|
171
|
+
if (detail.expected !== undefined) normalized.expected = detail.expected;
|
|
172
|
+
if (detail.actual !== undefined) normalized.actual = detail.actual;
|
|
173
|
+
|
|
174
|
+
const traceId = normalizeLabel(detail.traceId, null);
|
|
175
|
+
if (traceId) normalized.traceId = traceId;
|
|
176
|
+
|
|
177
|
+
const request = normalizeObject(detail.request);
|
|
178
|
+
if (request) normalized.request = request;
|
|
179
|
+
|
|
180
|
+
const response = normalizeObject(detail.response);
|
|
181
|
+
if (response) normalized.response = response;
|
|
182
|
+
|
|
183
|
+
const location = normalizeObject(detail.location);
|
|
184
|
+
if (location) normalized.location = location;
|
|
185
|
+
|
|
186
|
+
const stack = normalizeLabel(detail.stack, null);
|
|
187
|
+
if (stack) normalized.stack = stack;
|
|
188
|
+
|
|
161
189
|
const groupPath = Array.isArray(detail.groupPath)
|
|
162
190
|
? detail.groupPath.map((entry) => normalizeLabel(entry, null)).filter(Boolean)
|
|
163
191
|
: [];
|
|
@@ -176,3 +204,8 @@ function normalizeLabel(value, fallback) {
|
|
|
176
204
|
const normalized = value.trim();
|
|
177
205
|
return normalized.length > 0 ? normalized : fallback;
|
|
178
206
|
}
|
|
207
|
+
|
|
208
|
+
function normalizeObject(value) {
|
|
209
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
210
|
+
return JSON.parse(JSON.stringify(value));
|
|
211
|
+
}
|