@elench/testkit 0.1.36 → 0.1.37
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/runner/artifacts.mjs +72 -0
- package/lib/runner/default-runtime-runner.mjs +51 -2
- package/lib/runner/orchestrator.mjs +8 -1
- package/lib/runner/results.mjs +7 -0
- package/lib/runtime/index.d.ts +11 -0
- package/lib/runtime/index.mjs +3 -0
- package/lib/runtime-src/k6/artifacts.js +36 -0
- package/package.json +1 -1
package/lib/runner/artifacts.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "../timing/index.mjs";
|
|
8
8
|
|
|
9
9
|
const TIMINGS_FILENAME = "timings.json";
|
|
10
|
+
const RESULT_ARTIFACTS_DIRNAME = "artifacts";
|
|
10
11
|
|
|
11
12
|
export function writeRunArtifact(productDir, artifact) {
|
|
12
13
|
const resultsDir = path.join(productDir, ".testkit", "results");
|
|
@@ -21,6 +22,65 @@ export function writeStatusArtifact(productDir, artifact) {
|
|
|
21
22
|
);
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
export function resetResultArtifacts(productDir) {
|
|
26
|
+
fs.rmSync(path.join(productDir, ".testkit", "results", RESULT_ARTIFACTS_DIRNAME), {
|
|
27
|
+
recursive: true,
|
|
28
|
+
force: true,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function persistTaskArtifacts(productDir, task, emittedArtifacts) {
|
|
33
|
+
if (!Array.isArray(emittedArtifacts) || emittedArtifacts.length === 0) return [];
|
|
34
|
+
|
|
35
|
+
const artifactsDir = path.join(
|
|
36
|
+
productDir,
|
|
37
|
+
".testkit",
|
|
38
|
+
"results",
|
|
39
|
+
RESULT_ARTIFACTS_DIRNAME,
|
|
40
|
+
sanitizePathSegment(task.serviceName || "service")
|
|
41
|
+
);
|
|
42
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
43
|
+
|
|
44
|
+
return emittedArtifacts.map((artifact, index) => {
|
|
45
|
+
const fileName = `task-${task.id}-${String(index + 1).padStart(2, "0")}-${sanitizePathSegment(artifact.name || "artifact")}.json`;
|
|
46
|
+
const relativePath = path.join(
|
|
47
|
+
".testkit",
|
|
48
|
+
"results",
|
|
49
|
+
RESULT_ARTIFACTS_DIRNAME,
|
|
50
|
+
sanitizePathSegment(task.serviceName || "service"),
|
|
51
|
+
fileName
|
|
52
|
+
);
|
|
53
|
+
const absolutePath = path.join(productDir, relativePath);
|
|
54
|
+
const payload = {
|
|
55
|
+
schemaVersion: 1,
|
|
56
|
+
source: "testkit-runtime-artifact",
|
|
57
|
+
service: task.serviceName,
|
|
58
|
+
suite: {
|
|
59
|
+
key: task.suiteKey,
|
|
60
|
+
name: task.suiteName,
|
|
61
|
+
type: task.type,
|
|
62
|
+
},
|
|
63
|
+
file: task.file,
|
|
64
|
+
taskId: task.id,
|
|
65
|
+
index,
|
|
66
|
+
name: artifact.name,
|
|
67
|
+
kind: artifact.kind || null,
|
|
68
|
+
summary: artifact.summary || null,
|
|
69
|
+
contentType: artifact.contentType || "application/json",
|
|
70
|
+
emittedAt: artifact.emittedAt || null,
|
|
71
|
+
data: artifact.data,
|
|
72
|
+
};
|
|
73
|
+
fs.writeFileSync(absolutePath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
74
|
+
return {
|
|
75
|
+
name: payload.name,
|
|
76
|
+
kind: payload.kind,
|
|
77
|
+
summary: payload.summary,
|
|
78
|
+
contentType: payload.contentType,
|
|
79
|
+
path: normalizePath(relativePath),
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
24
84
|
export function loadTimings(productDir) {
|
|
25
85
|
const filePath = path.join(productDir, ".testkit", TIMINGS_FILENAME);
|
|
26
86
|
if (!fs.existsSync(filePath)) {
|
|
@@ -41,3 +101,15 @@ export function saveTimings(productDir, timings, updates) {
|
|
|
41
101
|
fs.mkdirSync(rootDir, { recursive: true });
|
|
42
102
|
fs.writeFileSync(path.join(rootDir, TIMINGS_FILENAME), JSON.stringify(next, null, 2));
|
|
43
103
|
}
|
|
104
|
+
|
|
105
|
+
function sanitizePathSegment(value) {
|
|
106
|
+
return String(value)
|
|
107
|
+
.trim()
|
|
108
|
+
.toLowerCase()
|
|
109
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
110
|
+
.replace(/^-+|-+$/g, "") || "artifact";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizePath(filePath) {
|
|
114
|
+
return filePath.split(path.sep).join("/");
|
|
115
|
+
}
|
|
@@ -3,6 +3,8 @@ import path from "path";
|
|
|
3
3
|
import { execa } from "execa";
|
|
4
4
|
import { bundleK6File } from "../bundler/index.mjs";
|
|
5
5
|
import { resolveK6Binary } from "../config/index.mjs";
|
|
6
|
+
import { persistTaskArtifacts } from "./artifacts.mjs";
|
|
7
|
+
import { RUNTIME_ARTIFACT_MARKER } from "../runtime-src/k6/artifacts.js";
|
|
6
8
|
import { determineDefaultRuntimeFailure } from "./default-runtime-errors.mjs";
|
|
7
9
|
import { formatBatchDescriptor } from "./formatting.mjs";
|
|
8
10
|
import { buildExecutionEnv } from "./template.mjs";
|
|
@@ -85,10 +87,17 @@ export async function runDefaultRuntimeTask(targetConfig, task, args, lifecycle,
|
|
|
85
87
|
}
|
|
86
88
|
);
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
const stdout = parseDefaultRuntimeOutput(result.stdout || "");
|
|
91
|
+
const stderr = parseDefaultRuntimeOutput(result.stderr || "");
|
|
92
|
+
if (stdout.visibleOutput) process.stdout.write(stdout.visibleOutput);
|
|
93
|
+
if (stderr.visibleOutput) process.stderr.write(stderr.visibleOutput);
|
|
90
94
|
|
|
91
95
|
const summary = readDefaultRuntimeSummary(summaryFile);
|
|
96
|
+
const runtimeArtifacts = persistTaskArtifacts(
|
|
97
|
+
targetConfig.productDir,
|
|
98
|
+
task,
|
|
99
|
+
[...stdout.artifacts, ...stderr.artifacts]
|
|
100
|
+
);
|
|
92
101
|
const runtimeError = determineDefaultRuntimeFailure(result, summary, firstLine);
|
|
93
102
|
const finishedAt = Date.now();
|
|
94
103
|
|
|
@@ -99,6 +108,7 @@ export async function runDefaultRuntimeTask(targetConfig, task, args, lifecycle,
|
|
|
99
108
|
durationMs: finishedAt - startedAt,
|
|
100
109
|
startedAt,
|
|
101
110
|
finishedAt,
|
|
111
|
+
artifacts: runtimeArtifacts,
|
|
102
112
|
};
|
|
103
113
|
}
|
|
104
114
|
|
|
@@ -117,3 +127,42 @@ export function readDefaultRuntimeSummary(filePath) {
|
|
|
117
127
|
return null;
|
|
118
128
|
}
|
|
119
129
|
}
|
|
130
|
+
|
|
131
|
+
function parseDefaultRuntimeOutput(output) {
|
|
132
|
+
if (!output) {
|
|
133
|
+
return {
|
|
134
|
+
visibleOutput: "",
|
|
135
|
+
artifacts: [],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const visibleLines = [];
|
|
140
|
+
const artifacts = [];
|
|
141
|
+
for (const line of output.split(/\r?\n/)) {
|
|
142
|
+
const rawPayload = extractArtifactPayload(line);
|
|
143
|
+
if (rawPayload !== null) {
|
|
144
|
+
try {
|
|
145
|
+
artifacts.push(JSON.parse(decodeURIComponent(rawPayload)));
|
|
146
|
+
} catch {
|
|
147
|
+
visibleLines.push(line);
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
visibleLines.push(line);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
visibleOutput: visibleLines.join("\n"),
|
|
156
|
+
artifacts,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function extractArtifactPayload(line) {
|
|
161
|
+
if (line.startsWith(RUNTIME_ARTIFACT_MARKER)) {
|
|
162
|
+
return line.slice(RUNTIME_ARTIFACT_MARKER.length);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const k6ConsoleMatch = line.match(/msg="TESTKIT_ARTIFACT:(.*)"(?:\s+source=console)?$/);
|
|
166
|
+
if (!k6ConsoleMatch) return null;
|
|
167
|
+
return k6ConsoleMatch[1];
|
|
168
|
+
}
|
|
@@ -16,7 +16,13 @@ import {
|
|
|
16
16
|
} from "./results.mjs";
|
|
17
17
|
import { buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
|
|
18
18
|
import { buildRunSummaryLines, formatError } from "./formatting.mjs";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
loadTimings,
|
|
21
|
+
resetResultArtifacts,
|
|
22
|
+
saveTimings,
|
|
23
|
+
writeRunArtifact,
|
|
24
|
+
writeStatusArtifact,
|
|
25
|
+
} from "./artifacts.mjs";
|
|
20
26
|
import {
|
|
21
27
|
cleanupRunById,
|
|
22
28
|
cleanupRuns,
|
|
@@ -39,6 +45,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
39
45
|
const telemetry = configs[0]?.telemetry || null;
|
|
40
46
|
const productDir = configs[0]?.productDir || process.cwd();
|
|
41
47
|
await cleanupStaleRuns(productDir);
|
|
48
|
+
resetResultArtifacts(productDir);
|
|
42
49
|
const metadata = {
|
|
43
50
|
git: collectGitMetadata(productDir),
|
|
44
51
|
host: {
|
package/lib/runner/results.mjs
CHANGED
|
@@ -45,6 +45,7 @@ export function buildServiceTrackers(servicePlans, startedAt) {
|
|
|
45
45
|
error: null,
|
|
46
46
|
reason: null,
|
|
47
47
|
status: "not_run",
|
|
48
|
+
artifacts: [],
|
|
48
49
|
},
|
|
49
50
|
];
|
|
50
51
|
}),
|
|
@@ -57,6 +58,7 @@ export function buildServiceTrackers(servicePlans, startedAt) {
|
|
|
57
58
|
error: null,
|
|
58
59
|
reason: file.reason,
|
|
59
60
|
status: "skipped",
|
|
61
|
+
artifacts: [],
|
|
60
62
|
},
|
|
61
63
|
]),
|
|
62
64
|
]),
|
|
@@ -118,6 +120,7 @@ export function recordTaskOutcome(trackers, task, outcome, finishedAt = Date.now
|
|
|
118
120
|
existingFileResult.error = outcome.error;
|
|
119
121
|
existingFileResult.reason = outcome.reason || null;
|
|
120
122
|
existingFileResult.status = status;
|
|
123
|
+
existingFileResult.artifacts = Array.isArray(outcome.artifacts) ? outcome.artifacts : [];
|
|
121
124
|
} else {
|
|
122
125
|
suite.fileResultsByPath.set(normalizedPath, {
|
|
123
126
|
path: normalizedPath,
|
|
@@ -126,6 +129,7 @@ export function recordTaskOutcome(trackers, task, outcome, finishedAt = Date.now
|
|
|
126
129
|
error: outcome.error,
|
|
127
130
|
reason: outcome.reason || null,
|
|
128
131
|
status,
|
|
132
|
+
artifacts: Array.isArray(outcome.artifacts) ? outcome.artifacts : [],
|
|
129
133
|
});
|
|
130
134
|
}
|
|
131
135
|
if (status === "failed" && !suite.failedFileSet.has(task.file)) {
|
|
@@ -243,6 +247,9 @@ function finalizeSuite(suite) {
|
|
|
243
247
|
durationMs: file.durationMs,
|
|
244
248
|
error: file.error,
|
|
245
249
|
reason: file.reason,
|
|
250
|
+
...(Array.isArray(file.artifacts) && file.artifacts.length > 0
|
|
251
|
+
? { artifacts: file.artifacts }
|
|
252
|
+
: {}),
|
|
246
253
|
}));
|
|
247
254
|
|
|
248
255
|
return {
|
package/lib/runtime/index.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export interface RuntimeOptions {
|
|
|
23
23
|
thresholds?: Record<string, unknown>;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface RuntimeArtifactOptions {
|
|
27
|
+
contentType?: string;
|
|
28
|
+
kind?: string;
|
|
29
|
+
summary?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
export interface RuntimeEnv {
|
|
27
33
|
BASE: string;
|
|
28
34
|
MACHINE_ID?: string;
|
|
@@ -133,6 +139,11 @@ export declare const http: RuntimeHttpClient;
|
|
|
133
139
|
|
|
134
140
|
export declare function file(data: unknown, filename?: string, contentType?: string): unknown;
|
|
135
141
|
export declare function json<T = unknown>(response: Pick<RuntimeResponse, "body">): T;
|
|
142
|
+
export declare function emitArtifact(
|
|
143
|
+
name: string,
|
|
144
|
+
data: unknown,
|
|
145
|
+
options?: RuntimeArtifactOptions
|
|
146
|
+
): void;
|
|
136
147
|
export declare function contains<T extends Record<string, unknown>>(
|
|
137
148
|
rows: T[],
|
|
138
149
|
field: keyof T | string,
|
package/lib/runtime/index.mjs
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const RUNTIME_ARTIFACT_MARKER = "TESTKIT_ARTIFACT:";
|
|
2
|
+
|
|
3
|
+
export function emitArtifact(name, data, options = {}) {
|
|
4
|
+
const normalizedName = normalizeArtifactName(name);
|
|
5
|
+
const payload = encodeURIComponent(
|
|
6
|
+
JSON.stringify({
|
|
7
|
+
name: normalizedName,
|
|
8
|
+
kind: normalizeOptionalString(options.kind),
|
|
9
|
+
summary: normalizeOptionalString(options.summary),
|
|
10
|
+
contentType: normalizeContentType(options.contentType),
|
|
11
|
+
data,
|
|
12
|
+
emittedAt: new Date().toISOString(),
|
|
13
|
+
})
|
|
14
|
+
);
|
|
15
|
+
console.log(
|
|
16
|
+
`${RUNTIME_ARTIFACT_MARKER}${payload}`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeArtifactName(name) {
|
|
21
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
22
|
+
throw new Error("emitArtifact(name, data) requires a non-empty artifact name");
|
|
23
|
+
}
|
|
24
|
+
return name.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeOptionalString(value) {
|
|
28
|
+
if (value === undefined || value === null) return null;
|
|
29
|
+
const normalized = String(value).trim();
|
|
30
|
+
return normalized.length > 0 ? normalized : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeContentType(value) {
|
|
34
|
+
const normalized = normalizeOptionalString(value);
|
|
35
|
+
return normalized || "application/json";
|
|
36
|
+
}
|