@appthrust/kest 0.13.0 → 0.14.0
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.
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
bun test v1.3.8 (b64edcb4)
|
|
2
|
+
|
|
3
|
+
example/example.test.ts:
|
|
4
|
+
# Example: applies ConfigMap using YAML, file import, and object literal
|
|
5
|
+
|
|
6
|
+
## Scenario Overview
|
|
7
|
+
|
|
8
|
+
| # | Action | Status | Duration |
|
|
9
|
+
|---|--------|--------|----------|
|
|
10
|
+
| 1 | Create `Namespace` with auto-generated name | ✅ | 186ms |
|
|
11
|
+
| 2 | Apply `ConfigMap` "my-config-1" | ✅ | 55ms |
|
|
12
|
+
| 3 | Apply a resource | ✅ | 117ms |
|
|
13
|
+
| 4 | Apply `ConfigMap` "my-config-3" | ✅ | 135ms |
|
|
14
|
+
| 5 | Assert `ConfigMap` "my-config-1" | ✅ | 63ms |
|
|
15
|
+
|
|
16
|
+
## Scenario Details
|
|
17
|
+
|
|
18
|
+
### Given: a new namespace exists
|
|
19
|
+
|
|
20
|
+
**✅ Create `Namespace` with auto-generated name**
|
|
21
|
+
|
|
22
|
+
```shell
|
|
23
|
+
kubectl create -f - <<EOF
|
|
24
|
+
apiVersion: v1
|
|
25
|
+
kind: Namespace
|
|
26
|
+
metadata:
|
|
27
|
+
name: kest-x173q
|
|
28
|
+
EOF
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```text title="stdout"
|
|
32
|
+
namespace/kest-x173q created
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### When: I apply ConfigMaps using different formats
|
|
36
|
+
|
|
37
|
+
**✅ Apply `ConfigMap` "my-config-1"**
|
|
38
|
+
|
|
39
|
+
```shell
|
|
40
|
+
kubectl apply -f - -n kest-x173q <<EOF
|
|
41
|
+
apiVersion: v1
|
|
42
|
+
kind: ConfigMap
|
|
43
|
+
metadata:
|
|
44
|
+
name: my-config-1
|
|
45
|
+
data:
|
|
46
|
+
mode: demo-1
|
|
47
|
+
EOF
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```text title="stdout"
|
|
51
|
+
configmap/my-config-1 created
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**✅ Apply a resource**
|
|
55
|
+
|
|
56
|
+
```shell
|
|
57
|
+
kubectl apply -f - -n kest-x173q <<EOF
|
|
58
|
+
apiVersion: v1
|
|
59
|
+
kind: ConfigMap
|
|
60
|
+
metadata:
|
|
61
|
+
name: my-config-2
|
|
62
|
+
data:
|
|
63
|
+
mode: demo-2
|
|
64
|
+
EOF
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```text title="stdout"
|
|
68
|
+
configmap/my-config-2 created
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**✅ Apply `ConfigMap` "my-config-3"**
|
|
72
|
+
|
|
73
|
+
```shell
|
|
74
|
+
kubectl apply -f - -n kest-x173q <<EOF
|
|
75
|
+
apiVersion: v1
|
|
76
|
+
kind: ConfigMap
|
|
77
|
+
metadata:
|
|
78
|
+
name: my-config-3
|
|
79
|
+
data:
|
|
80
|
+
mode: demo-3
|
|
81
|
+
EOF
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```text title="stdout"
|
|
85
|
+
configmap/my-config-3 created
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Then: the ConfigMap should have the expected data
|
|
89
|
+
|
|
90
|
+
**✅ Assert `ConfigMap` "my-config-1"**
|
|
91
|
+
|
|
92
|
+
```shell
|
|
93
|
+
kubectl get ConfigMap my-config-1 -o yaml -n kest-x173q
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```yaml title="stdout"
|
|
97
|
+
apiVersion: v1
|
|
98
|
+
data:
|
|
99
|
+
mode: demo-1
|
|
100
|
+
kind: ConfigMap
|
|
101
|
+
metadata:
|
|
102
|
+
annotations:
|
|
103
|
+
kubectl.kubernetes.io/last-applied-configuration: |
|
|
104
|
+
{"apiVersion":"v1","data":{"mode":"demo-1"},"kind":"ConfigMap","metadata":{"annotations":{},"name":"my-config-1","namespace":"kest-x173q"}}
|
|
105
|
+
creationTimestamp: "2026-03-02T02:39:15Z"
|
|
106
|
+
name: my-config-1
|
|
107
|
+
namespace: kest-x173q
|
|
108
|
+
resourceVersion: "585"
|
|
109
|
+
uid: 25339d11-cf6b-44a5-a680-1548d1e80252
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Cleanup
|
|
113
|
+
|
|
114
|
+
| # | Action | Status | Duration |
|
|
115
|
+
|---|--------|--------|----------|
|
|
116
|
+
| 1 | Apply `ConfigMap` "my-config-3" | ✅ | 75ms |
|
|
117
|
+
| 2 | Apply a resource | ✅ | 41ms |
|
|
118
|
+
| 3 | Apply `ConfigMap` "my-config-1" | ✅ | 36ms |
|
|
119
|
+
| 4 | Create `Namespace` with auto-generated name | ✅ | 5.103s |
|
|
120
|
+
|
|
121
|
+
```shellsession
|
|
122
|
+
$ kubectl delete ConfigMap/my-config-3 --ignore-not-found -n kest-x173q
|
|
123
|
+
configmap "my-config-3" deleted
|
|
124
|
+
|
|
125
|
+
$ kubectl delete ConfigMap/my-config-2 --ignore-not-found -n kest-x173q
|
|
126
|
+
configmap "my-config-2" deleted
|
|
127
|
+
|
|
128
|
+
$ kubectl delete ConfigMap/my-config-1 --ignore-not-found -n kest-x173q
|
|
129
|
+
configmap "my-config-1" deleted
|
|
130
|
+
|
|
131
|
+
$ kubectl delete Namespace/kest-x173q --ignore-not-found
|
|
132
|
+
namespace "kest-x173q" deleted
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Total: 5.815s** (Actions: 556ms, Cleanup: 5.255s)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
1 pass
|
|
139
|
+
7 filtered out
|
|
140
|
+
0 fail
|
|
141
|
+
1 expect() calls
|
|
142
|
+
Ran 1 test across 1 file. [6.04s]
|
package/package.json
CHANGED
package/ts/recording/index.ts
CHANGED
|
@@ -87,13 +87,14 @@ interface BaseEvent<
|
|
|
87
87
|
> {
|
|
88
88
|
readonly kind: Kind;
|
|
89
89
|
readonly data: Data;
|
|
90
|
+
readonly timestamp: number;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
export class Recorder {
|
|
93
94
|
private readonly events: Array<Event> = [];
|
|
94
95
|
|
|
95
96
|
record<T extends Event>(kind: T["kind"], data: T["data"]) {
|
|
96
|
-
this.events.push({ kind, data } as T);
|
|
97
|
+
this.events.push({ kind, data, timestamp: Date.now() } as T);
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
getEvents(): Array<Event> {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Duration } from "../../duration";
|
|
2
|
+
|
|
1
3
|
export interface Report {
|
|
2
4
|
scenarios: Array<Scenario>;
|
|
3
5
|
}
|
|
@@ -8,6 +10,7 @@ export interface Scenario {
|
|
|
8
10
|
details: Array<Tagged<"BDDSection", BDDSection> | Tagged<"Action", Action>>;
|
|
9
11
|
cleanup: Array<CleanupItem>;
|
|
10
12
|
cleanupSkipped?: boolean;
|
|
13
|
+
duration?: undefined | Duration;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
type Tagged<Tag extends string, Target extends object> = Target & {
|
|
@@ -17,6 +20,7 @@ type Tagged<Tag extends string, Target extends object> = Target & {
|
|
|
17
20
|
export interface OverviewItem {
|
|
18
21
|
name: string;
|
|
19
22
|
status: "pending" | "success" | "failure";
|
|
23
|
+
duration?: undefined | Duration;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
export interface BDDSection {
|
|
@@ -30,6 +34,7 @@ export interface Action {
|
|
|
30
34
|
attempts?: undefined | number;
|
|
31
35
|
commands: Array<Command>;
|
|
32
36
|
error?: undefined | Error;
|
|
37
|
+
duration?: undefined | Duration;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
export interface Command {
|
|
@@ -55,6 +60,7 @@ export interface CleanupItem {
|
|
|
55
60
|
resource?: undefined | string;
|
|
56
61
|
status: "success" | "failure";
|
|
57
62
|
command: CleanupCommand;
|
|
63
|
+
duration?: undefined | Duration;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
export interface CleanupCommand {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Duration } from "../../../duration";
|
|
1
2
|
import type { Event } from "../../../recording";
|
|
2
3
|
import type {
|
|
3
4
|
Action,
|
|
@@ -30,6 +31,8 @@ interface ParseState {
|
|
|
30
31
|
currentAction: Action | undefined;
|
|
31
32
|
currentOverviewIndex: number | undefined;
|
|
32
33
|
currentCleanup: CleanupItem | undefined;
|
|
34
|
+
currentActionStartTimestamp: number | undefined;
|
|
35
|
+
scenarioStartTimestamp: number | undefined;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export function parseEvents(events: ReadonlyArray<Event>): Report {
|
|
@@ -41,6 +44,8 @@ export function parseEvents(events: ReadonlyArray<Event>): Report {
|
|
|
41
44
|
currentAction: undefined,
|
|
42
45
|
currentOverviewIndex: undefined,
|
|
43
46
|
currentCleanup: undefined,
|
|
47
|
+
currentActionStartTimestamp: undefined,
|
|
48
|
+
scenarioStartTimestamp: undefined,
|
|
44
49
|
};
|
|
45
50
|
|
|
46
51
|
for (const event of events) {
|
|
@@ -64,9 +69,19 @@ function handleNonBDDEvent(state: ParseState, event: Event): void {
|
|
|
64
69
|
case "ScenarioStart":
|
|
65
70
|
handleScenarioStart(state, event);
|
|
66
71
|
return;
|
|
67
|
-
case "ScenarioEnd":
|
|
72
|
+
case "ScenarioEnd": {
|
|
73
|
+
if (state.currentScenario) {
|
|
74
|
+
const duration = computeDuration(
|
|
75
|
+
state.scenarioStartTimestamp,
|
|
76
|
+
event.timestamp
|
|
77
|
+
);
|
|
78
|
+
if (duration) {
|
|
79
|
+
state.currentScenario.duration = duration;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
68
82
|
clearScenarioProgressState(state);
|
|
69
83
|
return;
|
|
84
|
+
}
|
|
70
85
|
case "RevertingsStart":
|
|
71
86
|
state.inCleanup = true;
|
|
72
87
|
clearCurrentActionState(state);
|
|
@@ -109,6 +124,7 @@ function handleActionStart(
|
|
|
109
124
|
state: ParseState,
|
|
110
125
|
event: Extract<Event, { kind: "ActionStart" }>
|
|
111
126
|
): void {
|
|
127
|
+
state.currentActionStartTimestamp = event.timestamp;
|
|
112
128
|
const scenario = ensureScenario(state.currentScenario, state.report);
|
|
113
129
|
if (state.inCleanup) {
|
|
114
130
|
const cleanup: CleanupItem = {
|
|
@@ -156,10 +172,17 @@ function applyRegularActionEnd(
|
|
|
156
172
|
return;
|
|
157
173
|
}
|
|
158
174
|
|
|
175
|
+
const duration = computeDuration(
|
|
176
|
+
state.currentActionStartTimestamp,
|
|
177
|
+
event.timestamp
|
|
178
|
+
);
|
|
179
|
+
currentAction.duration = duration;
|
|
180
|
+
|
|
159
181
|
if (state.currentOverviewIndex !== undefined) {
|
|
160
182
|
const overviewItem = currentScenario.overview[state.currentOverviewIndex];
|
|
161
183
|
if (overviewItem) {
|
|
162
184
|
overviewItem.status = event.data.ok ? "success" : "failure";
|
|
185
|
+
overviewItem.duration = duration;
|
|
163
186
|
}
|
|
164
187
|
}
|
|
165
188
|
|
|
@@ -177,6 +200,7 @@ function applyRegularActionEnd(
|
|
|
177
200
|
|
|
178
201
|
state.currentAction = undefined;
|
|
179
202
|
state.currentOverviewIndex = undefined;
|
|
203
|
+
state.currentActionStartTimestamp = undefined;
|
|
180
204
|
}
|
|
181
205
|
|
|
182
206
|
function handleCommandResult(
|
|
@@ -247,6 +271,7 @@ function handleScenarioStart(
|
|
|
247
271
|
};
|
|
248
272
|
state.report.scenarios.push(state.currentScenario);
|
|
249
273
|
clearScenarioProgressState(state);
|
|
274
|
+
state.scenarioStartTimestamp = event.timestamp;
|
|
250
275
|
}
|
|
251
276
|
|
|
252
277
|
function handleActionEnd(
|
|
@@ -270,8 +295,16 @@ function handleCleanupActionEnd(
|
|
|
270
295
|
|
|
271
296
|
if (state.currentCleanup) {
|
|
272
297
|
state.currentCleanup.status = event.data.ok ? "success" : "failure";
|
|
298
|
+
const duration = computeDuration(
|
|
299
|
+
state.currentActionStartTimestamp,
|
|
300
|
+
event.timestamp
|
|
301
|
+
);
|
|
302
|
+
if (duration) {
|
|
303
|
+
state.currentCleanup.duration = duration;
|
|
304
|
+
}
|
|
273
305
|
}
|
|
274
306
|
state.currentCleanup = undefined;
|
|
307
|
+
state.currentActionStartTimestamp = undefined;
|
|
275
308
|
return true;
|
|
276
309
|
}
|
|
277
310
|
|
|
@@ -344,9 +377,24 @@ function ensureScenario(
|
|
|
344
377
|
return created;
|
|
345
378
|
}
|
|
346
379
|
|
|
380
|
+
function computeDuration(
|
|
381
|
+
start: number | undefined,
|
|
382
|
+
end: number | undefined
|
|
383
|
+
): Duration | undefined {
|
|
384
|
+
if (typeof start !== "number" || typeof end !== "number") {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
const ms = end - start;
|
|
388
|
+
if (ms < 0 || !Number.isFinite(ms) || !Number.isInteger(ms)) {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
return new Duration(ms);
|
|
392
|
+
}
|
|
393
|
+
|
|
347
394
|
function clearScenarioProgressState(state: ParseState): void {
|
|
348
395
|
state.currentBDDSection = undefined;
|
|
349
396
|
state.inCleanup = false;
|
|
397
|
+
state.scenarioStartTimestamp = undefined;
|
|
350
398
|
clearCurrentActionState(state);
|
|
351
399
|
}
|
|
352
400
|
|
|
@@ -354,6 +402,7 @@ function clearCurrentActionState(state: ParseState): void {
|
|
|
354
402
|
state.currentAction = undefined;
|
|
355
403
|
state.currentOverviewIndex = undefined;
|
|
356
404
|
state.currentCleanup = undefined;
|
|
405
|
+
state.currentActionStartTimestamp = undefined;
|
|
357
406
|
}
|
|
358
407
|
|
|
359
408
|
function bddFromEvent(event: Event): BDDSection | undefined {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Duration } from "../../../duration";
|
|
1
2
|
import { codeToANSIForcedColors } from "../../shiki";
|
|
2
3
|
import type { MarkdownReporterOptions } from "../index";
|
|
3
4
|
import type { Action, Report } from "../model";
|
|
@@ -174,14 +175,26 @@ export async function renderReport(
|
|
|
174
175
|
lines.push("");
|
|
175
176
|
|
|
176
177
|
// Overview
|
|
178
|
+
const overviewHasDuration = scenario.overview.some((o) => o.duration);
|
|
177
179
|
lines.push("## Scenario Overview");
|
|
178
180
|
lines.push("");
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
if (overviewHasDuration) {
|
|
182
|
+
lines.push("| # | Action | Status | Duration |");
|
|
183
|
+
lines.push("|---|--------|--------|----------|");
|
|
184
|
+
} else {
|
|
185
|
+
lines.push("| # | Action | Status |");
|
|
186
|
+
lines.push("|---|--------|--------|");
|
|
187
|
+
}
|
|
181
188
|
for (const [i, item] of scenario.overview.entries()) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
189
|
+
if (overviewHasDuration) {
|
|
190
|
+
lines.push(
|
|
191
|
+
`| ${i + 1} | ${stripAnsi(item.name)} | ${statusEmoji(item.status)} | ${item.duration?.toString() ?? ""} |`
|
|
192
|
+
);
|
|
193
|
+
} else {
|
|
194
|
+
lines.push(
|
|
195
|
+
`| ${i + 1} | ${stripAnsi(item.name)} | ${statusEmoji(item.status)} |`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
185
198
|
}
|
|
186
199
|
lines.push("");
|
|
187
200
|
|
|
@@ -308,14 +321,26 @@ export async function renderReport(
|
|
|
308
321
|
"To clean up manually, run the revert commands from a passing test run."
|
|
309
322
|
);
|
|
310
323
|
} else if (scenario.cleanup.length > 0) {
|
|
324
|
+
const cleanupHasDuration = scenario.cleanup.some((c) => c.duration);
|
|
311
325
|
lines.push("### Cleanup");
|
|
312
326
|
lines.push("");
|
|
313
|
-
|
|
314
|
-
|
|
327
|
+
if (cleanupHasDuration) {
|
|
328
|
+
lines.push("| # | Action | Status | Duration |");
|
|
329
|
+
lines.push("|---|--------|--------|----------|");
|
|
330
|
+
} else {
|
|
331
|
+
lines.push("| # | Action | Status |");
|
|
332
|
+
lines.push("|---|--------|--------|");
|
|
333
|
+
}
|
|
315
334
|
for (const [i, item] of scenario.cleanup.entries()) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
335
|
+
if (cleanupHasDuration) {
|
|
336
|
+
lines.push(
|
|
337
|
+
`| ${i + 1} | ${stripAnsi(item.action)} | ${item.status === "success" ? "✅" : "❌"} | ${item.duration?.toString() ?? ""} |`
|
|
338
|
+
);
|
|
339
|
+
} else {
|
|
340
|
+
lines.push(
|
|
341
|
+
`| ${i + 1} | ${stripAnsi(item.action)} | ${item.status === "success" ? "✅" : "❌"} |`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
319
344
|
}
|
|
320
345
|
lines.push("");
|
|
321
346
|
|
|
@@ -334,6 +359,33 @@ export async function renderReport(
|
|
|
334
359
|
lines.push("```");
|
|
335
360
|
}
|
|
336
361
|
|
|
362
|
+
// Total duration summary
|
|
363
|
+
if (scenario.duration) {
|
|
364
|
+
lines.push("");
|
|
365
|
+
const actionsDurationMs = scenario.overview.reduce(
|
|
366
|
+
(sum, o) => sum + (o.duration?.milliseconds ?? 0),
|
|
367
|
+
0
|
|
368
|
+
);
|
|
369
|
+
const cleanupDurationMs = scenario.cleanup.reduce(
|
|
370
|
+
(sum, c) => sum + (c.duration?.milliseconds ?? 0),
|
|
371
|
+
0
|
|
372
|
+
);
|
|
373
|
+
const parts: Array<string> = [];
|
|
374
|
+
if (actionsDurationMs > 0) {
|
|
375
|
+
parts.push(`Actions: ${new Duration(actionsDurationMs).toString()}`);
|
|
376
|
+
}
|
|
377
|
+
if (cleanupDurationMs > 0) {
|
|
378
|
+
parts.push(`Cleanup: ${new Duration(cleanupDurationMs).toString()}`);
|
|
379
|
+
}
|
|
380
|
+
if (parts.length > 0) {
|
|
381
|
+
lines.push(
|
|
382
|
+
`**Total: ${scenario.duration.toString()}** (${parts.join(", ")})`
|
|
383
|
+
);
|
|
384
|
+
} else {
|
|
385
|
+
lines.push(`**Total: ${scenario.duration.toString()}**`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
337
389
|
renderedScenarios.push(lines.join("\n"));
|
|
338
390
|
}
|
|
339
391
|
|
package/ts/test.ts
CHANGED
|
@@ -17,8 +17,7 @@ interface TestOptions {
|
|
|
17
17
|
timeout?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
setDefaultTimeout(defaultTimeout);
|
|
20
|
+
setDefaultTimeout(60_000);
|
|
22
21
|
|
|
23
22
|
type Callback = (scenario: Scenario) => Promise<unknown>;
|
|
24
23
|
|
|
@@ -98,12 +97,12 @@ function makeScenarioTest(runner: BunTestRunner): TestFunction {
|
|
|
98
97
|
function convertTestOptions(
|
|
99
98
|
options?: undefined | TestOptions
|
|
100
99
|
): undefined | BunTestOptions {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
if (!options?.timeout) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
104
103
|
return {
|
|
105
104
|
...options,
|
|
106
|
-
timeout,
|
|
105
|
+
timeout: parseDuration(options.timeout).toMilliseconds(),
|
|
107
106
|
};
|
|
108
107
|
}
|
|
109
108
|
|