@appthrust/kest 0.2.0 → 0.3.1
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 +93 -13
- package/package.json +2 -1
- package/ts/actions/apply-namespace.ts +35 -3
- package/ts/actions/apply-status.ts +8 -1
- package/ts/actions/apply.ts +8 -1
- package/ts/actions/assert-absence.ts +38 -0
- package/ts/actions/assert-list.ts +3 -0
- package/ts/actions/assert.ts +3 -0
- package/ts/actions/create.ts +34 -0
- package/ts/actions/delete.ts +4 -12
- package/ts/actions/exec.ts +3 -0
- package/ts/actions/get.ts +3 -0
- package/ts/actions/kubectl-type.ts +19 -0
- package/ts/actions/label.ts +23 -0
- package/ts/actions/types.ts +3 -0
- package/ts/apis/index.ts +331 -4
- package/ts/k8s-resource/index.ts +22 -0
- package/ts/kubectl/index.ts +54 -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 +43 -31
- 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,11 +1,17 @@
|
|
|
1
1
|
import { apply } from "../actions/apply";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type ApplyNamespaceInput,
|
|
4
|
+
applyNamespace,
|
|
5
|
+
} from "../actions/apply-namespace";
|
|
3
6
|
import { applyStatus } from "../actions/apply-status";
|
|
4
7
|
import { assert } from "../actions/assert";
|
|
8
|
+
import { assertAbsence } from "../actions/assert-absence";
|
|
5
9
|
import { assertList } from "../actions/assert-list";
|
|
10
|
+
import { create } from "../actions/create";
|
|
6
11
|
import { deleteResource } from "../actions/delete";
|
|
7
12
|
import { exec } from "../actions/exec";
|
|
8
13
|
import { get } from "../actions/get";
|
|
14
|
+
import { label } from "../actions/label";
|
|
9
15
|
import type { MutateDef, OneWayMutateDef, QueryDef } from "../actions/types";
|
|
10
16
|
import type {
|
|
11
17
|
ActionOptions,
|
|
@@ -28,14 +34,16 @@ export interface InternalScenario extends Scenario {
|
|
|
28
34
|
|
|
29
35
|
export function createScenario(deps: CreateScenarioOptions): InternalScenario {
|
|
30
36
|
const { recorder, reporter, reverting } = deps;
|
|
31
|
-
recorder.record("ScenarioStarted", { name: deps.name });
|
|
32
37
|
return {
|
|
33
38
|
apply: createMutateFn(deps, apply),
|
|
39
|
+
create: createMutateFn(deps, create),
|
|
34
40
|
applyStatus: createOneWayMutateFn(deps, applyStatus),
|
|
35
41
|
delete: createOneWayMutateFn(deps, deleteResource),
|
|
42
|
+
label: createOneWayMutateFn(deps, label),
|
|
36
43
|
exec: createMutateFn(deps, exec),
|
|
37
44
|
get: createQueryFn(deps, get),
|
|
38
45
|
assert: createQueryFn(deps, assert),
|
|
46
|
+
assertAbsence: createQueryFn(deps, assertAbsence),
|
|
39
47
|
assertList: createQueryFn(deps, assertList),
|
|
40
48
|
given: bdd.given(deps),
|
|
41
49
|
when: bdd.when(deps),
|
|
@@ -76,17 +84,25 @@ const createMutateFn =
|
|
|
76
84
|
options?: undefined | ActionOptions
|
|
77
85
|
): Promise<Output> => {
|
|
78
86
|
const { recorder, kubectl, reverting } = deps;
|
|
79
|
-
const {
|
|
80
|
-
|
|
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();
|
|
81
97
|
const fn = mutate({ kubectl });
|
|
82
|
-
let mutateErr:
|
|
98
|
+
let mutateErr: undefined | Error;
|
|
83
99
|
try {
|
|
84
100
|
const { revert, output } = await retryUntil(() => fn(input), {
|
|
85
101
|
...options,
|
|
86
102
|
recorder,
|
|
87
103
|
});
|
|
88
104
|
reverting.add(async () => {
|
|
89
|
-
|
|
105
|
+
recordActionStart(); // to record revert action start
|
|
90
106
|
let revertErr: unknown;
|
|
91
107
|
try {
|
|
92
108
|
await revert();
|
|
@@ -94,25 +110,15 @@ const createMutateFn =
|
|
|
94
110
|
revertErr = err;
|
|
95
111
|
throw err;
|
|
96
112
|
} finally {
|
|
97
|
-
|
|
98
|
-
action: name,
|
|
99
|
-
phase: "revert",
|
|
100
|
-
ok: revertErr === undefined,
|
|
101
|
-
error: revertErr as Error,
|
|
102
|
-
});
|
|
113
|
+
recordActionEnd(revertErr as Error); // to record revert action end
|
|
103
114
|
}
|
|
104
115
|
});
|
|
105
116
|
return output;
|
|
106
117
|
} catch (error) {
|
|
107
|
-
mutateErr = error;
|
|
118
|
+
mutateErr = error as Error;
|
|
108
119
|
throw error;
|
|
109
120
|
} finally {
|
|
110
|
-
|
|
111
|
-
action: name,
|
|
112
|
-
phase: type,
|
|
113
|
-
ok: mutateErr === undefined,
|
|
114
|
-
error: mutateErr as Error,
|
|
115
|
-
});
|
|
121
|
+
recordActionEnd(mutateErr as Error);
|
|
116
122
|
}
|
|
117
123
|
};
|
|
118
124
|
|
|
@@ -130,8 +136,8 @@ const createOneWayMutateFn =
|
|
|
130
136
|
options?: undefined | ActionOptions
|
|
131
137
|
): Promise<Output> => {
|
|
132
138
|
const { recorder, kubectl } = deps;
|
|
133
|
-
const {
|
|
134
|
-
recorder.record("ActionStart", {
|
|
139
|
+
const { mutate, describe } = action;
|
|
140
|
+
recorder.record("ActionStart", { description: describe(input) });
|
|
135
141
|
const fn = mutate({ kubectl });
|
|
136
142
|
let mutateErr: unknown;
|
|
137
143
|
try {
|
|
@@ -141,8 +147,6 @@ const createOneWayMutateFn =
|
|
|
141
147
|
throw error;
|
|
142
148
|
} finally {
|
|
143
149
|
recorder.record("ActionEnd", {
|
|
144
|
-
action: name,
|
|
145
|
-
phase: "mutate",
|
|
146
150
|
ok: mutateErr === undefined,
|
|
147
151
|
error: mutateErr as Error,
|
|
148
152
|
});
|
|
@@ -163,26 +167,27 @@ const createQueryFn =
|
|
|
163
167
|
options?: undefined | ActionOptions
|
|
164
168
|
): Promise<Output> => {
|
|
165
169
|
const { recorder, kubectl } = deps;
|
|
166
|
-
const {
|
|
167
|
-
recorder.record("ActionStart", {
|
|
170
|
+
const { query, describe } = action;
|
|
171
|
+
recorder.record("ActionStart", { description: describe(input) });
|
|
168
172
|
const fn = query({ kubectl });
|
|
173
|
+
let queryErr: unknown;
|
|
169
174
|
try {
|
|
170
175
|
return await retryUntil(() => fn(input), { ...options, recorder });
|
|
171
176
|
} catch (error) {
|
|
177
|
+
queryErr = error;
|
|
178
|
+
throw error;
|
|
179
|
+
} finally {
|
|
172
180
|
recorder.record("ActionEnd", {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
ok: false,
|
|
176
|
-
error: error as Error,
|
|
181
|
+
ok: queryErr === undefined,
|
|
182
|
+
error: queryErr as Error,
|
|
177
183
|
});
|
|
178
|
-
throw error;
|
|
179
184
|
}
|
|
180
185
|
};
|
|
181
186
|
|
|
182
187
|
const createNewNamespaceFn =
|
|
183
188
|
(scenarioDeps: CreateScenarioOptions) =>
|
|
184
189
|
async (
|
|
185
|
-
name?:
|
|
190
|
+
name?: ApplyNamespaceInput,
|
|
186
191
|
options?: undefined | ActionOptions
|
|
187
192
|
): Promise<Namespace> => {
|
|
188
193
|
const namespaceName = await createMutateFn(scenarioDeps, applyNamespace)(
|
|
@@ -193,11 +198,15 @@ const createNewNamespaceFn =
|
|
|
193
198
|
const namespacedKubectl = kubectl.extends({ namespace: namespaceName });
|
|
194
199
|
const namespacedDeps = { ...scenarioDeps, kubectl: namespacedKubectl };
|
|
195
200
|
return {
|
|
201
|
+
name: namespaceName,
|
|
196
202
|
apply: createMutateFn(namespacedDeps, apply),
|
|
203
|
+
create: createMutateFn(namespacedDeps, create),
|
|
197
204
|
applyStatus: createOneWayMutateFn(namespacedDeps, applyStatus),
|
|
198
205
|
delete: createOneWayMutateFn(namespacedDeps, deleteResource),
|
|
206
|
+
label: createOneWayMutateFn(namespacedDeps, label),
|
|
199
207
|
get: createQueryFn(namespacedDeps, get),
|
|
200
208
|
assert: createQueryFn(namespacedDeps, assert),
|
|
209
|
+
assertAbsence: createQueryFn(namespacedDeps, assertAbsence),
|
|
201
210
|
assertList: createQueryFn(namespacedDeps, assertList),
|
|
202
211
|
};
|
|
203
212
|
};
|
|
@@ -214,10 +223,13 @@ const createUseClusterFn =
|
|
|
214
223
|
const clusterDeps = { ...scenarioDeps, kubectl: clusterKubectl };
|
|
215
224
|
return {
|
|
216
225
|
apply: createMutateFn(clusterDeps, apply),
|
|
226
|
+
create: createMutateFn(clusterDeps, create),
|
|
217
227
|
applyStatus: createOneWayMutateFn(clusterDeps, applyStatus),
|
|
218
228
|
delete: createOneWayMutateFn(clusterDeps, deleteResource),
|
|
229
|
+
label: createOneWayMutateFn(clusterDeps, label),
|
|
219
230
|
get: createQueryFn(clusterDeps, get),
|
|
220
231
|
assert: createQueryFn(clusterDeps, assert),
|
|
232
|
+
assertAbsence: createQueryFn(clusterDeps, assertAbsence),
|
|
221
233
|
assertList: createQueryFn(clusterDeps, assertList),
|
|
222
234
|
newNamespace: createNewNamespaceFn(clusterDeps),
|
|
223
235
|
};
|
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
|