@appthrust/kest 0.3.0 → 0.3.2
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 +62 -28
- package/package.json +2 -1
- package/ts/actions/apply-status.ts +8 -1
- package/ts/actions/apply.ts +8 -1
- package/ts/actions/assert-absence.ts +2 -0
- package/ts/actions/assert-list.ts +3 -0
- package/ts/actions/assert.ts +3 -0
- package/ts/actions/{apply-namespace.ts → create-namespace.ts} +16 -7
- package/ts/actions/create.ts +34 -0
- package/ts/actions/delete.ts +3 -0
- package/ts/actions/exec.ts +3 -0
- package/ts/actions/get.ts +3 -0
- package/ts/actions/label.ts +3 -0
- package/ts/actions/types.ts +3 -0
- package/ts/apis/index.ts +88 -0
- package/ts/k8s-resource/index.ts +22 -0
- package/ts/recording/index.ts +81 -115
- package/ts/reporter/markdown/index.ts +23 -0
- package/ts/reporter/markdown/model.ts +63 -0
- package/ts/reporter/markdown/parser/index.ts +361 -0
- package/ts/reporter/markdown/renderer/index.ts +296 -0
- package/ts/reporter/shiki.ts +58 -0
- package/ts/retry.ts +0 -6
- package/ts/scenario/index.ts +35 -35
- package/ts/test.ts +2 -1
- package/ts/reporter/index.ts +0 -0
- package/ts/reporter/markdown.ts +0 -962
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { codeToANSIForcedColors } from "../../shiki";
|
|
2
|
+
import type { MarkdownReporterOptions } from "../index";
|
|
3
|
+
import type { Action, Report } from "../model";
|
|
4
|
+
|
|
5
|
+
const markdownLang = "markdown";
|
|
6
|
+
const markdownTheme = "catppuccin-mocha";
|
|
7
|
+
|
|
8
|
+
type StdinReplacement = Readonly<{
|
|
9
|
+
placeholder: string;
|
|
10
|
+
stdin: string;
|
|
11
|
+
stdinLanguage: string;
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
function normalizeStdin(stdin: string): string {
|
|
15
|
+
// Match `ts/reporter/markdown.ts` behavior: keep content stable.
|
|
16
|
+
return stdin.replace(/^\n/, "").replace(/\s+$/, "");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function applyStdinReplacements(
|
|
20
|
+
highlightedMarkdown: string,
|
|
21
|
+
replacements: ReadonlyArray<StdinReplacement>
|
|
22
|
+
): string {
|
|
23
|
+
if (replacements.length === 0) {
|
|
24
|
+
return highlightedMarkdown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let current = highlightedMarkdown;
|
|
28
|
+
for (const r of replacements) {
|
|
29
|
+
const lines = current.split("\n");
|
|
30
|
+
const stdinLines = r.stdin.split("\n");
|
|
31
|
+
const out: Array<string> = [];
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
if (stripAnsi(line).includes(r.placeholder)) {
|
|
34
|
+
out.push(...stdinLines);
|
|
35
|
+
} else {
|
|
36
|
+
out.push(line);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
current = out.join("\n");
|
|
40
|
+
}
|
|
41
|
+
return current;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function highlightMarkdown(
|
|
45
|
+
markdown: string,
|
|
46
|
+
stdinReplacements: ReadonlyArray<StdinReplacement>
|
|
47
|
+
): Promise<string> {
|
|
48
|
+
const stripped = stripAnsi(markdown);
|
|
49
|
+
try {
|
|
50
|
+
const highlightedMarkdown = await codeToANSIForcedColors(
|
|
51
|
+
stripped,
|
|
52
|
+
markdownLang,
|
|
53
|
+
markdownTheme
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (stdinReplacements.length === 0) {
|
|
57
|
+
// Keep output shape stable: always end with a single trailing newline.
|
|
58
|
+
return highlightedMarkdown.replace(/\n+$/, "\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const highlightedStdinList = await Promise.all(
|
|
62
|
+
stdinReplacements.map(async (r) => {
|
|
63
|
+
const highlightedStdin = await codeToANSIForcedColors(
|
|
64
|
+
r.stdin,
|
|
65
|
+
r.stdinLanguage,
|
|
66
|
+
markdownTheme
|
|
67
|
+
);
|
|
68
|
+
// Avoid inserting an extra blank line before `EOF`.
|
|
69
|
+
const trimmed = trimFinalNewline(
|
|
70
|
+
highlightedStdin.replace(/\n+$/, "\n")
|
|
71
|
+
);
|
|
72
|
+
return { ...r, stdin: trimmed } satisfies StdinReplacement;
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const replaced = applyStdinReplacements(
|
|
77
|
+
highlightedMarkdown,
|
|
78
|
+
highlightedStdinList
|
|
79
|
+
);
|
|
80
|
+
return replaced.replace(/\n+$/, "\n");
|
|
81
|
+
} catch {
|
|
82
|
+
return stripped;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stripAnsi(input: string): string {
|
|
87
|
+
// Prefer Bun's built-in ANSI stripper when available.
|
|
88
|
+
if (typeof Bun !== "undefined" && typeof Bun.stripANSI === "function") {
|
|
89
|
+
return Bun.stripANSI(input);
|
|
90
|
+
}
|
|
91
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: intended
|
|
92
|
+
return input.replace(/\u001b\[[0-9;]*m/g, "");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function trimFinalNewline(input: string): string {
|
|
96
|
+
return input.replace(/\n$/, "");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toBddHeading(keyword: string): string {
|
|
100
|
+
if (keyword.length === 0) {
|
|
101
|
+
return keyword;
|
|
102
|
+
}
|
|
103
|
+
return keyword.charAt(0).toUpperCase() + keyword.slice(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const statusEmojiByStatus = {
|
|
107
|
+
pending: "⏳",
|
|
108
|
+
success: "✅",
|
|
109
|
+
failure: "❌",
|
|
110
|
+
} as const;
|
|
111
|
+
|
|
112
|
+
function statusEmoji(status: keyof typeof statusEmojiByStatus): string {
|
|
113
|
+
return statusEmojiByStatus[status];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: rendering is intentionally linear and explicit
|
|
117
|
+
export function renderReport(
|
|
118
|
+
report: Report,
|
|
119
|
+
options: MarkdownReporterOptions
|
|
120
|
+
): Promise<string> {
|
|
121
|
+
const enableANSI = options.enableANSI ?? false;
|
|
122
|
+
|
|
123
|
+
const renderedScenarios: Array<string> = [];
|
|
124
|
+
const stdinReplacements: Array<StdinReplacement> = [];
|
|
125
|
+
let stdinSeq = 0;
|
|
126
|
+
|
|
127
|
+
for (const scenario of report.scenarios) {
|
|
128
|
+
const isEmpty =
|
|
129
|
+
scenario.overview.length === 0 &&
|
|
130
|
+
scenario.details.length === 0 &&
|
|
131
|
+
scenario.cleanup.length === 0;
|
|
132
|
+
if (isEmpty) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const overviewStatusByName = new Map<
|
|
137
|
+
string,
|
|
138
|
+
"pending" | "success" | "failure"
|
|
139
|
+
>(scenario.overview.map((o) => [o.name, o.status]));
|
|
140
|
+
|
|
141
|
+
const lines: Array<string> = [];
|
|
142
|
+
|
|
143
|
+
lines.push(`# ${stripAnsi(scenario.name)}`);
|
|
144
|
+
lines.push("");
|
|
145
|
+
|
|
146
|
+
// Overview
|
|
147
|
+
lines.push("## Scenario Overview");
|
|
148
|
+
lines.push("");
|
|
149
|
+
lines.push("| # | Action | Status |");
|
|
150
|
+
lines.push("|---|--------|--------|");
|
|
151
|
+
for (const [i, item] of scenario.overview.entries()) {
|
|
152
|
+
lines.push(
|
|
153
|
+
`| ${i + 1} | ${stripAnsi(item.name)} | ${statusEmoji(item.status)} |`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
lines.push("");
|
|
157
|
+
|
|
158
|
+
// Details
|
|
159
|
+
lines.push("## Scenario Details");
|
|
160
|
+
lines.push("");
|
|
161
|
+
|
|
162
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: rendering is intentionally linear and explicit
|
|
163
|
+
const renderAction = (action: Action) => {
|
|
164
|
+
let status = overviewStatusByName.get(action.name);
|
|
165
|
+
if (!status) {
|
|
166
|
+
if (action.error) {
|
|
167
|
+
status = "failure";
|
|
168
|
+
} else if (action.command) {
|
|
169
|
+
status = "success";
|
|
170
|
+
} else {
|
|
171
|
+
status = "pending";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const emoji = statusEmoji(status);
|
|
175
|
+
const attemptsSuffix =
|
|
176
|
+
status === "failure" && typeof action.attempts === "number"
|
|
177
|
+
? ` (Failed after ${action.attempts} attempts)`
|
|
178
|
+
: "";
|
|
179
|
+
|
|
180
|
+
lines.push(`**${emoji} ${stripAnsi(action.name)}**${attemptsSuffix}`);
|
|
181
|
+
lines.push("");
|
|
182
|
+
|
|
183
|
+
const cmd = action.command;
|
|
184
|
+
if (cmd) {
|
|
185
|
+
const base = [cmd.cmd, ...cmd.args].join(" ").trim();
|
|
186
|
+
const stdin = cmd.stdin?.text;
|
|
187
|
+
const stdinLanguage = cmd.stdin?.language ?? "text";
|
|
188
|
+
|
|
189
|
+
lines.push("```shell");
|
|
190
|
+
if (typeof stdin === "string") {
|
|
191
|
+
lines.push(`${base} <<EOF`);
|
|
192
|
+
if (enableANSI) {
|
|
193
|
+
const placeholder = `__KEST_STDIN_${stdinSeq++}__`;
|
|
194
|
+
stdinReplacements.push({
|
|
195
|
+
placeholder,
|
|
196
|
+
stdin: normalizeStdin(stripAnsi(stdin)),
|
|
197
|
+
stdinLanguage,
|
|
198
|
+
});
|
|
199
|
+
lines.push(placeholder);
|
|
200
|
+
} else {
|
|
201
|
+
lines.push(stripAnsi(stdin));
|
|
202
|
+
}
|
|
203
|
+
lines.push("EOF");
|
|
204
|
+
} else {
|
|
205
|
+
lines.push(base);
|
|
206
|
+
}
|
|
207
|
+
lines.push("```");
|
|
208
|
+
lines.push("");
|
|
209
|
+
|
|
210
|
+
const stdout = stripAnsi(cmd.stdout?.text ?? "");
|
|
211
|
+
if (stdout.trim().length > 0) {
|
|
212
|
+
const lang = cmd.stdout?.language ?? "text";
|
|
213
|
+
lines.push(`\`\`\`${lang} title="stdout"`);
|
|
214
|
+
lines.push(trimFinalNewline(stdout));
|
|
215
|
+
lines.push("```");
|
|
216
|
+
lines.push("");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const stderr = stripAnsi(cmd.stderr?.text ?? "");
|
|
220
|
+
if (stderr.trim().length > 0) {
|
|
221
|
+
const lang = cmd.stderr?.language ?? "text";
|
|
222
|
+
lines.push(`\`\`\`${lang} title="stderr"`);
|
|
223
|
+
lines.push(trimFinalNewline(stderr));
|
|
224
|
+
lines.push("```");
|
|
225
|
+
lines.push("");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (status === "failure" && action.error?.message?.text) {
|
|
230
|
+
const messageText = stripAnsi(action.error.message.text);
|
|
231
|
+
const lang = action.error.message.language ?? "text";
|
|
232
|
+
lines.push("Error:");
|
|
233
|
+
lines.push("");
|
|
234
|
+
lines.push(`\`\`\`${lang}`);
|
|
235
|
+
lines.push(trimFinalNewline(messageText));
|
|
236
|
+
lines.push("```");
|
|
237
|
+
lines.push("");
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
for (const item of scenario.details) {
|
|
242
|
+
if (item.type === "BDDSection") {
|
|
243
|
+
lines.push(
|
|
244
|
+
`### ${toBddHeading(item.keyword)}: ${stripAnsi(item.description)}`
|
|
245
|
+
);
|
|
246
|
+
lines.push("");
|
|
247
|
+
for (const action of item.actions) {
|
|
248
|
+
renderAction(action);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (item.type === "Action") {
|
|
252
|
+
renderAction(item);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Cleanup
|
|
257
|
+
if (scenario.cleanup.length > 0) {
|
|
258
|
+
lines.push("### Cleanup");
|
|
259
|
+
lines.push("");
|
|
260
|
+
lines.push("| # | Action | Status |");
|
|
261
|
+
lines.push("|---|--------|--------|");
|
|
262
|
+
for (const [i, item] of scenario.cleanup.entries()) {
|
|
263
|
+
lines.push(
|
|
264
|
+
`| ${i + 1} | ${stripAnsi(item.action)} | ${item.status === "success" ? "✅" : "❌"} |`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
lines.push("");
|
|
268
|
+
|
|
269
|
+
lines.push("```shellsession");
|
|
270
|
+
for (const [i, item] of scenario.cleanup.entries()) {
|
|
271
|
+
if (i > 0) {
|
|
272
|
+
lines.push("");
|
|
273
|
+
}
|
|
274
|
+
const base = [item.command.cmd, ...item.command.args].join(" ").trim();
|
|
275
|
+
lines.push(`$ ${stripAnsi(base)}`);
|
|
276
|
+
const output = stripAnsi(item.command.output);
|
|
277
|
+
if (output.trim().length > 0) {
|
|
278
|
+
lines.push(trimFinalNewline(output));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
lines.push("```");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
renderedScenarios.push(lines.join("\n"));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (renderedScenarios.length === 0) {
|
|
288
|
+
return Promise.resolve("");
|
|
289
|
+
}
|
|
290
|
+
const rendered = renderedScenarios.join("\n\n");
|
|
291
|
+
const markdown = rendered.endsWith("\n") ? rendered : `${rendered}\n`;
|
|
292
|
+
if (!enableANSI) {
|
|
293
|
+
return Promise.resolve(markdown);
|
|
294
|
+
}
|
|
295
|
+
return highlightMarkdown(markdown, stdinReplacements);
|
|
296
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type CodeToANSI = (
|
|
2
|
+
code: string,
|
|
3
|
+
language: string,
|
|
4
|
+
theme: string
|
|
5
|
+
) => Promise<string>;
|
|
6
|
+
|
|
7
|
+
let codeToANSIPromise: Promise<CodeToANSI> | undefined;
|
|
8
|
+
|
|
9
|
+
function loadCodeToANSI(): Promise<CodeToANSI> {
|
|
10
|
+
if (codeToANSIPromise) {
|
|
11
|
+
return codeToANSIPromise;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
codeToANSIPromise = (async () => {
|
|
15
|
+
const env = typeof process !== "undefined" ? process.env : undefined;
|
|
16
|
+
const prevNoColor = env?.["NO_COLOR"];
|
|
17
|
+
const prevForceColor = env?.["FORCE_COLOR"];
|
|
18
|
+
|
|
19
|
+
// `@shikijs/cli` uses `ansis`, which disables colors when NO_COLOR is set.
|
|
20
|
+
// Force ANSI output when our callers explicitly request ANSI.
|
|
21
|
+
if (env) {
|
|
22
|
+
// biome-ignore lint/performance/noDelete: required to actually unset env vars
|
|
23
|
+
delete env["NO_COLOR"];
|
|
24
|
+
env["FORCE_COLOR"] = "3";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const mod = await import("@shikijs/cli");
|
|
29
|
+
return mod.codeToANSI as unknown as CodeToANSI;
|
|
30
|
+
} finally {
|
|
31
|
+
if (env) {
|
|
32
|
+
if (prevNoColor === undefined) {
|
|
33
|
+
// biome-ignore lint/performance/noDelete: required to restore absence
|
|
34
|
+
delete env["NO_COLOR"];
|
|
35
|
+
} else {
|
|
36
|
+
env["NO_COLOR"] = prevNoColor;
|
|
37
|
+
}
|
|
38
|
+
if (prevForceColor === undefined) {
|
|
39
|
+
// biome-ignore lint/performance/noDelete: required to restore absence
|
|
40
|
+
delete env["FORCE_COLOR"];
|
|
41
|
+
} else {
|
|
42
|
+
env["FORCE_COLOR"] = prevForceColor;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
|
|
48
|
+
return codeToANSIPromise;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function codeToANSIForcedColors(
|
|
52
|
+
code: string,
|
|
53
|
+
language: string,
|
|
54
|
+
theme: string
|
|
55
|
+
): Promise<string> {
|
|
56
|
+
const fn = await loadCodeToANSI();
|
|
57
|
+
return await fn(code, language, theme);
|
|
58
|
+
}
|
package/ts/retry.ts
CHANGED
|
@@ -77,7 +77,6 @@ export async function retryUntil<T>(
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
retries += 1;
|
|
80
|
-
recorder?.record("RetryAttempt", { attempt: retries });
|
|
81
80
|
|
|
82
81
|
try {
|
|
83
82
|
const value = await fn();
|
|
@@ -89,11 +88,6 @@ export async function retryUntil<T>(
|
|
|
89
88
|
return value;
|
|
90
89
|
} catch (err) {
|
|
91
90
|
lastError = err;
|
|
92
|
-
const error = err as Error;
|
|
93
|
-
recorder?.record("RetryFailure", {
|
|
94
|
-
attempt: retries,
|
|
95
|
-
error: { name: error.name, message: error.message },
|
|
96
|
-
});
|
|
97
91
|
}
|
|
98
92
|
}
|
|
99
93
|
|
package/ts/scenario/index.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { apply } from "../actions/apply";
|
|
2
|
-
import {
|
|
3
|
-
type ApplyNamespaceInput,
|
|
4
|
-
applyNamespace,
|
|
5
|
-
} from "../actions/apply-namespace";
|
|
6
2
|
import { applyStatus } from "../actions/apply-status";
|
|
7
3
|
import { assert } from "../actions/assert";
|
|
8
4
|
import { assertAbsence } from "../actions/assert-absence";
|
|
9
5
|
import { assertList } from "../actions/assert-list";
|
|
6
|
+
import { create } from "../actions/create";
|
|
7
|
+
import {
|
|
8
|
+
type CreateNamespaceInput,
|
|
9
|
+
createNamespace,
|
|
10
|
+
} from "../actions/create-namespace";
|
|
10
11
|
import { deleteResource } from "../actions/delete";
|
|
11
12
|
import { exec } from "../actions/exec";
|
|
12
13
|
import { get } from "../actions/get";
|
|
@@ -33,9 +34,9 @@ export interface InternalScenario extends Scenario {
|
|
|
33
34
|
|
|
34
35
|
export function createScenario(deps: CreateScenarioOptions): InternalScenario {
|
|
35
36
|
const { recorder, reporter, reverting } = deps;
|
|
36
|
-
recorder.record("ScenarioStarted", { name: deps.name });
|
|
37
37
|
return {
|
|
38
38
|
apply: createMutateFn(deps, apply),
|
|
39
|
+
create: createMutateFn(deps, create),
|
|
39
40
|
applyStatus: createOneWayMutateFn(deps, applyStatus),
|
|
40
41
|
delete: createOneWayMutateFn(deps, deleteResource),
|
|
41
42
|
label: createOneWayMutateFn(deps, label),
|
|
@@ -83,17 +84,25 @@ const createMutateFn =
|
|
|
83
84
|
options?: undefined | ActionOptions
|
|
84
85
|
): Promise<Output> => {
|
|
85
86
|
const { recorder, kubectl, reverting } = deps;
|
|
86
|
-
const {
|
|
87
|
-
|
|
87
|
+
const { mutate, describe } = action;
|
|
88
|
+
function recordActionStart() {
|
|
89
|
+
recorder.record("ActionStart", {
|
|
90
|
+
description: describe(input),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function recordActionEnd(error: undefined | Error) {
|
|
94
|
+
recorder.record("ActionEnd", { ok: error === undefined, error });
|
|
95
|
+
}
|
|
96
|
+
recordActionStart();
|
|
88
97
|
const fn = mutate({ kubectl });
|
|
89
|
-
let mutateErr:
|
|
98
|
+
let mutateErr: undefined | Error;
|
|
90
99
|
try {
|
|
91
100
|
const { revert, output } = await retryUntil(() => fn(input), {
|
|
92
101
|
...options,
|
|
93
102
|
recorder,
|
|
94
103
|
});
|
|
95
104
|
reverting.add(async () => {
|
|
96
|
-
|
|
105
|
+
recordActionStart(); // to record revert action start
|
|
97
106
|
let revertErr: unknown;
|
|
98
107
|
try {
|
|
99
108
|
await revert();
|
|
@@ -101,25 +110,15 @@ const createMutateFn =
|
|
|
101
110
|
revertErr = err;
|
|
102
111
|
throw err;
|
|
103
112
|
} finally {
|
|
104
|
-
|
|
105
|
-
action: name,
|
|
106
|
-
phase: "revert",
|
|
107
|
-
ok: revertErr === undefined,
|
|
108
|
-
error: revertErr as Error,
|
|
109
|
-
});
|
|
113
|
+
recordActionEnd(revertErr as Error); // to record revert action end
|
|
110
114
|
}
|
|
111
115
|
});
|
|
112
116
|
return output;
|
|
113
117
|
} catch (error) {
|
|
114
|
-
mutateErr = error;
|
|
118
|
+
mutateErr = error as Error;
|
|
115
119
|
throw error;
|
|
116
120
|
} finally {
|
|
117
|
-
|
|
118
|
-
action: name,
|
|
119
|
-
phase: type,
|
|
120
|
-
ok: mutateErr === undefined,
|
|
121
|
-
error: mutateErr as Error,
|
|
122
|
-
});
|
|
121
|
+
recordActionEnd(mutateErr as Error);
|
|
123
122
|
}
|
|
124
123
|
};
|
|
125
124
|
|
|
@@ -137,8 +136,8 @@ const createOneWayMutateFn =
|
|
|
137
136
|
options?: undefined | ActionOptions
|
|
138
137
|
): Promise<Output> => {
|
|
139
138
|
const { recorder, kubectl } = deps;
|
|
140
|
-
const {
|
|
141
|
-
recorder.record("ActionStart", {
|
|
139
|
+
const { mutate, describe } = action;
|
|
140
|
+
recorder.record("ActionStart", { description: describe(input) });
|
|
142
141
|
const fn = mutate({ kubectl });
|
|
143
142
|
let mutateErr: unknown;
|
|
144
143
|
try {
|
|
@@ -148,8 +147,6 @@ const createOneWayMutateFn =
|
|
|
148
147
|
throw error;
|
|
149
148
|
} finally {
|
|
150
149
|
recorder.record("ActionEnd", {
|
|
151
|
-
action: name,
|
|
152
|
-
phase: "mutate",
|
|
153
150
|
ok: mutateErr === undefined,
|
|
154
151
|
error: mutateErr as Error,
|
|
155
152
|
});
|
|
@@ -170,29 +167,30 @@ const createQueryFn =
|
|
|
170
167
|
options?: undefined | ActionOptions
|
|
171
168
|
): Promise<Output> => {
|
|
172
169
|
const { recorder, kubectl } = deps;
|
|
173
|
-
const {
|
|
174
|
-
recorder.record("ActionStart", {
|
|
170
|
+
const { query, describe } = action;
|
|
171
|
+
recorder.record("ActionStart", { description: describe(input) });
|
|
175
172
|
const fn = query({ kubectl });
|
|
173
|
+
let queryErr: unknown;
|
|
176
174
|
try {
|
|
177
175
|
return await retryUntil(() => fn(input), { ...options, recorder });
|
|
178
176
|
} catch (error) {
|
|
177
|
+
queryErr = error;
|
|
178
|
+
throw error;
|
|
179
|
+
} finally {
|
|
179
180
|
recorder.record("ActionEnd", {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
ok: false,
|
|
183
|
-
error: error as Error,
|
|
181
|
+
ok: queryErr === undefined,
|
|
182
|
+
error: queryErr as Error,
|
|
184
183
|
});
|
|
185
|
-
throw error;
|
|
186
184
|
}
|
|
187
185
|
};
|
|
188
186
|
|
|
189
187
|
const createNewNamespaceFn =
|
|
190
188
|
(scenarioDeps: CreateScenarioOptions) =>
|
|
191
189
|
async (
|
|
192
|
-
name?:
|
|
190
|
+
name?: CreateNamespaceInput,
|
|
193
191
|
options?: undefined | ActionOptions
|
|
194
192
|
): Promise<Namespace> => {
|
|
195
|
-
const namespaceName = await createMutateFn(scenarioDeps,
|
|
193
|
+
const namespaceName = await createMutateFn(scenarioDeps, createNamespace)(
|
|
196
194
|
name,
|
|
197
195
|
options
|
|
198
196
|
);
|
|
@@ -202,6 +200,7 @@ const createNewNamespaceFn =
|
|
|
202
200
|
return {
|
|
203
201
|
name: namespaceName,
|
|
204
202
|
apply: createMutateFn(namespacedDeps, apply),
|
|
203
|
+
create: createMutateFn(namespacedDeps, create),
|
|
205
204
|
applyStatus: createOneWayMutateFn(namespacedDeps, applyStatus),
|
|
206
205
|
delete: createOneWayMutateFn(namespacedDeps, deleteResource),
|
|
207
206
|
label: createOneWayMutateFn(namespacedDeps, label),
|
|
@@ -224,6 +223,7 @@ const createUseClusterFn =
|
|
|
224
223
|
const clusterDeps = { ...scenarioDeps, kubectl: clusterKubectl };
|
|
225
224
|
return {
|
|
226
225
|
apply: createMutateFn(clusterDeps, apply),
|
|
226
|
+
create: createMutateFn(clusterDeps, create),
|
|
227
227
|
applyStatus: createOneWayMutateFn(clusterDeps, applyStatus),
|
|
228
228
|
delete: createOneWayMutateFn(clusterDeps, deleteResource),
|
|
229
229
|
label: createOneWayMutateFn(clusterDeps, label),
|
package/ts/test.ts
CHANGED
|
@@ -59,7 +59,7 @@ function makeScenarioTest(runner: BunTestRunner): TestFunction {
|
|
|
59
59
|
reverting,
|
|
60
60
|
reporter,
|
|
61
61
|
});
|
|
62
|
-
|
|
62
|
+
recorder.record("ScenarioStart", { name: label });
|
|
63
63
|
let testErr: undefined | Error;
|
|
64
64
|
try {
|
|
65
65
|
await fn(scenario);
|
|
@@ -67,6 +67,7 @@ function makeScenarioTest(runner: BunTestRunner): TestFunction {
|
|
|
67
67
|
testErr = error as Error;
|
|
68
68
|
}
|
|
69
69
|
await scenario.cleanup();
|
|
70
|
+
recorder.record("ScenarioEnd", {});
|
|
70
71
|
await report(recorder, scenario, testErr);
|
|
71
72
|
if (testErr) {
|
|
72
73
|
throw testErr;
|
package/ts/reporter/index.ts
DELETED
|
File without changes
|