@appthrust/kest 0.3.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 +17 -1
- package/package.json +2 -1
- package/ts/actions/apply-namespace.ts +9 -0
- 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/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 +29 -29
- package/ts/test.ts +2 -1
- package/ts/reporter/index.ts +0 -0
- package/ts/reporter/markdown.ts +0 -962
package/README.md
CHANGED
|
@@ -119,6 +119,21 @@ await ns.assert(
|
|
|
119
119
|
);
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
### Create Resources
|
|
123
|
+
|
|
124
|
+
Use `kubectl create` instead of `kubectl apply` when you need to ensure a resource is freshly created (e.g. the resource must not already exist, or you want to use `generateName`):
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
await ns.create({
|
|
128
|
+
apiVersion: "v1",
|
|
129
|
+
kind: "ConfigMap",
|
|
130
|
+
metadata: { name: "my-config" },
|
|
131
|
+
data: { mode: "demo" },
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Like `apply`, `create` registers a cleanup handler that deletes the resource when the test ends. The key difference is that `kubectl create` fails if the resource already exists, whereas `kubectl apply` performs an upsert.
|
|
136
|
+
|
|
122
137
|
### Multiple Manifest Formats
|
|
123
138
|
|
|
124
139
|
Apply resources using whichever format is most convenient:
|
|
@@ -398,6 +413,7 @@ The top-level API surface available in every test callback.
|
|
|
398
413
|
| Method | Description |
|
|
399
414
|
| ----------------------------------------------------------------------- | ------------------------------------------------- |
|
|
400
415
|
| `apply(manifest, options?)` | Apply a Kubernetes manifest and register cleanup |
|
|
416
|
+
| `create(manifest, options?)` | Create a Kubernetes resource and register cleanup |
|
|
401
417
|
| `applyStatus(manifest, options?)` | Apply a status subresource (server-side apply) |
|
|
402
418
|
| `delete(resource, options?)` | Delete a resource by API version, kind, and name |
|
|
403
419
|
| `label(input, options?)` | Add, update, or remove labels on a resource |
|
|
@@ -412,7 +428,7 @@ The top-level API surface available in every test callback.
|
|
|
412
428
|
|
|
413
429
|
### Namespace / Cluster
|
|
414
430
|
|
|
415
|
-
Returned by `newNamespace()` and `useCluster()` respectively. They expose the same core methods (`apply`, `applyStatus`, `delete`, `label`, `get`, `assert`, `assertAbsence`, `assertList`) scoped to their namespace or cluster context. `Cluster` additionally supports `newNamespace`.
|
|
431
|
+
Returned by `newNamespace()` and `useCluster()` respectively. They expose the same core methods (`apply`, `create`, `applyStatus`, `delete`, `label`, `get`, `assert`, `assertAbsence`, `assertList`) scoped to their namespace or cluster context. `Cluster` additionally supports `newNamespace`.
|
|
416
432
|
|
|
417
433
|
### Action Options
|
|
418
434
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appthrust/kest",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Kubernetes E2E testing framework designed for humans and AI alike",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "ts/index.ts",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@biomejs/biome": "^2.3.13",
|
|
54
54
|
"@suin/biome.json": "^0.1.0",
|
|
55
|
+
"@taml/encoder": "^1.0.0",
|
|
55
56
|
"@tsconfig/bun": "^1.0.10",
|
|
56
57
|
"@tsconfig/strictest": "^2.0.8",
|
|
57
58
|
"@types/bun": "^1.3.7",
|
|
@@ -30,6 +30,15 @@ export const applyNamespace = {
|
|
|
30
30
|
});
|
|
31
31
|
return { revert, output: name };
|
|
32
32
|
},
|
|
33
|
+
describe: (input) => {
|
|
34
|
+
if (input === undefined) {
|
|
35
|
+
return "Apply `Namespace` with auto-generated name";
|
|
36
|
+
}
|
|
37
|
+
if (typeof input === "string") {
|
|
38
|
+
return `Apply \`Namespace\` "${input}"`;
|
|
39
|
+
}
|
|
40
|
+
return `Apply \`Namespace\` with prefix "${input.generateName}"`;
|
|
41
|
+
},
|
|
33
42
|
} satisfies MutateDef<ApplyNamespaceInput, string>;
|
|
34
43
|
|
|
35
44
|
function resolveNamespaceName(input: ApplyNamespaceInput): string {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ApplyingManifest } from "../apis";
|
|
2
|
-
import { parseK8sResourceAny } from "../k8s-resource";
|
|
2
|
+
import { getResourceMeta, parseK8sResourceAny } from "../k8s-resource";
|
|
3
3
|
import type { OneWayMutateDef } from "./types";
|
|
4
4
|
|
|
5
5
|
export const applyStatus = {
|
|
@@ -20,4 +20,11 @@ export const applyStatus = {
|
|
|
20
20
|
await kubectl.applyStatus(result.value);
|
|
21
21
|
return undefined;
|
|
22
22
|
},
|
|
23
|
+
describe: (manifest) => {
|
|
24
|
+
const meta = getResourceMeta(manifest);
|
|
25
|
+
if (meta === undefined) {
|
|
26
|
+
return "Apply status of a resource";
|
|
27
|
+
}
|
|
28
|
+
return `Apply status of \`${meta.kind}\` "${meta.name}"`;
|
|
29
|
+
},
|
|
23
30
|
} satisfies OneWayMutateDef<ApplyingManifest, void>;
|
package/ts/actions/apply.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ApplyingManifest } from "../apis";
|
|
2
|
-
import { parseK8sResourceAny } from "../k8s-resource";
|
|
2
|
+
import { getResourceMeta, parseK8sResourceAny } from "../k8s-resource";
|
|
3
3
|
import type { MutateDef } from "./types";
|
|
4
4
|
|
|
5
5
|
export const apply = {
|
|
@@ -24,4 +24,11 @@ export const apply = {
|
|
|
24
24
|
output: undefined,
|
|
25
25
|
};
|
|
26
26
|
},
|
|
27
|
+
describe: (manifest) => {
|
|
28
|
+
const meta = getResourceMeta(manifest);
|
|
29
|
+
if (meta === undefined) {
|
|
30
|
+
return "Apply a resource";
|
|
31
|
+
}
|
|
32
|
+
return `Apply \`${meta.kind}\` "${meta.name}"`;
|
|
33
|
+
},
|
|
27
34
|
} satisfies MutateDef<ApplyingManifest, void>;
|
|
@@ -22,6 +22,8 @@ export const assertAbsence = {
|
|
|
22
22
|
`Expected ${resource.kind} "${resource.name}" to be absent, but it exists`
|
|
23
23
|
);
|
|
24
24
|
},
|
|
25
|
+
describe: (resource) =>
|
|
26
|
+
`Assert that \`${resource.kind}\` "${resource.name}" is absent`,
|
|
25
27
|
} satisfies QueryDef<K8sResourceReference, void>;
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -28,6 +28,9 @@ export const assertList = {
|
|
|
28
28
|
await condition.test.call(typed, typed);
|
|
29
29
|
return typed;
|
|
30
30
|
},
|
|
31
|
+
describe: <T extends K8sResource>(condition: ResourceListTest<T>): string => {
|
|
32
|
+
return `Assert a list of \`${condition.kind}\` resources`;
|
|
33
|
+
},
|
|
31
34
|
} satisfies QueryDef<ResourceListTest, Array<K8sResource>>;
|
|
32
35
|
|
|
33
36
|
function isSameGVK<T extends K8sResource>(
|
package/ts/actions/assert.ts
CHANGED
|
@@ -19,6 +19,9 @@ export const assert = {
|
|
|
19
19
|
await condition.test.call(fetched, fetched);
|
|
20
20
|
return fetched;
|
|
21
21
|
},
|
|
22
|
+
describe: <T extends K8sResource>(condition: ResourceTest<T>): string => {
|
|
23
|
+
return `Assert \`${condition.kind}\` "${condition.name}"`;
|
|
24
|
+
},
|
|
22
25
|
} satisfies QueryDef<ResourceTest, K8sResource>;
|
|
23
26
|
|
|
24
27
|
function toKubectlType<T extends K8sResource>(
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ApplyingManifest } from "../apis";
|
|
2
|
+
import { getResourceMeta, parseK8sResourceAny } from "../k8s-resource";
|
|
3
|
+
import type { MutateDef } from "./types";
|
|
4
|
+
|
|
5
|
+
export const create = {
|
|
6
|
+
type: "mutate",
|
|
7
|
+
name: "Create",
|
|
8
|
+
mutate:
|
|
9
|
+
({ kubectl }) =>
|
|
10
|
+
async (manifest) => {
|
|
11
|
+
const result = await parseK8sResourceAny(manifest);
|
|
12
|
+
if (!result.ok) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Invalid Kubernetes resource: ${result.violations.join(", ")}`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
await kubectl.create(result.value);
|
|
18
|
+
return {
|
|
19
|
+
async revert() {
|
|
20
|
+
await kubectl.delete(result.value.kind, result.value.metadata.name, {
|
|
21
|
+
ignoreNotFound: true,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
output: undefined,
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
describe: (manifest) => {
|
|
28
|
+
const meta = getResourceMeta(manifest);
|
|
29
|
+
if (meta === undefined) {
|
|
30
|
+
return "Create a resource";
|
|
31
|
+
}
|
|
32
|
+
return `Create \`${meta.kind}\` "${meta.name}"`;
|
|
33
|
+
},
|
|
34
|
+
} satisfies MutateDef<ApplyingManifest, void>;
|
package/ts/actions/delete.ts
CHANGED
|
@@ -11,4 +11,7 @@ export const deleteResource = {
|
|
|
11
11
|
await kubectl.delete(toKubectlType(resource), resource.name);
|
|
12
12
|
return undefined;
|
|
13
13
|
},
|
|
14
|
+
describe: (resource) => {
|
|
15
|
+
return `Delete \`${resource.kind}\` "${resource.name}"`;
|
|
16
|
+
},
|
|
14
17
|
} satisfies OneWayMutateDef<K8sResourceReference, void>;
|
package/ts/actions/exec.ts
CHANGED
|
@@ -17,5 +17,8 @@ export const exec = {
|
|
|
17
17
|
revert: revert ? () => revert(context) : noopRevert,
|
|
18
18
|
};
|
|
19
19
|
},
|
|
20
|
+
describe: () => {
|
|
21
|
+
return "Execute arbitrary processing";
|
|
22
|
+
},
|
|
20
23
|
// biome-ignore lint/suspicious/noExplicitAny: 本当はunknownにしたいが、createMutateFnとの噛み合せが難しいためanyにしている
|
|
21
24
|
} satisfies MutateDef<ExecInput<any>, unknown>;
|
package/ts/actions/get.ts
CHANGED
|
@@ -15,6 +15,9 @@ export const get = {
|
|
|
15
15
|
...finding,
|
|
16
16
|
test: (fetched) => assertSameGVK<T>(finding, fetched),
|
|
17
17
|
}),
|
|
18
|
+
describe: (finding) => {
|
|
19
|
+
return `Get \`${finding.kind}\` "${finding.name}"`;
|
|
20
|
+
},
|
|
18
21
|
} satisfies QueryDef<K8sResourceReference, K8sResource>;
|
|
19
22
|
|
|
20
23
|
function isSameGVK<T extends K8sResource>(
|
package/ts/actions/label.ts
CHANGED
package/ts/actions/types.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface MutateDef<Input, Output> {
|
|
|
6
6
|
readonly type: "mutate";
|
|
7
7
|
readonly name: string;
|
|
8
8
|
readonly mutate: Mutate<Input, Output>;
|
|
9
|
+
readonly describe: (input: Input) => string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export type Mutate<Input, Output> = (
|
|
@@ -27,6 +28,7 @@ export interface OneWayMutateDef<Input, Output> {
|
|
|
27
28
|
readonly type: "oneWayMutate";
|
|
28
29
|
readonly name: string;
|
|
29
30
|
readonly mutate: OneWayMutate<Input, Output>;
|
|
31
|
+
readonly describe: (input: Input) => string;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export type OneWayMutate<Input, Output> = (
|
|
@@ -37,6 +39,7 @@ export interface QueryDef<Input, Output> {
|
|
|
37
39
|
readonly type: "query";
|
|
38
40
|
readonly name: string;
|
|
39
41
|
readonly query: Query<Input, Output>;
|
|
42
|
+
readonly describe: (input: Input) => string;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
export type Query<Input, Output> = (
|
package/ts/apis/index.ts
CHANGED
|
@@ -54,6 +54,39 @@ export interface Scenario {
|
|
|
54
54
|
options?: undefined | ActionOptions
|
|
55
55
|
): Promise<void>;
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Creates a Kubernetes resource with `kubectl create`.
|
|
59
|
+
*
|
|
60
|
+
* The manifest is validated and then created. When the action succeeds, Kest
|
|
61
|
+
* registers a cleanup handler that deletes the resource using
|
|
62
|
+
* `kubectl delete <kind>/<metadata.name>` during scenario cleanup.
|
|
63
|
+
*
|
|
64
|
+
* Unlike {@link Scenario.apply}, this action uses `kubectl create` which
|
|
65
|
+
* fails if the resource already exists. Use this when you need to ensure the
|
|
66
|
+
* resource is freshly created (e.g. for resources that use `generateName` or
|
|
67
|
+
* when you want to guarantee no prior state).
|
|
68
|
+
*
|
|
69
|
+
* This action is retried when it throws.
|
|
70
|
+
*
|
|
71
|
+
* @template T - The expected Kubernetes resource shape.
|
|
72
|
+
* @param manifest - YAML string, resource object, or imported YAML module.
|
|
73
|
+
* @param options - Retry options such as timeout and polling interval.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* await s.create({
|
|
78
|
+
* apiVersion: "v1",
|
|
79
|
+
* kind: "ConfigMap",
|
|
80
|
+
* metadata: { name: "my-config" },
|
|
81
|
+
* data: { mode: "demo" },
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
create<T extends K8sResource>(
|
|
86
|
+
manifest: ApplyingManifest<T>,
|
|
87
|
+
options?: undefined | ActionOptions
|
|
88
|
+
): Promise<void>;
|
|
89
|
+
|
|
57
90
|
/**
|
|
58
91
|
* Applies the `status` subresource using server-side apply.
|
|
59
92
|
*
|
|
@@ -442,6 +475,30 @@ export interface Cluster {
|
|
|
442
475
|
options?: undefined | ActionOptions
|
|
443
476
|
): Promise<void>;
|
|
444
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Creates a Kubernetes resource with `kubectl create` and registers cleanup.
|
|
480
|
+
*
|
|
481
|
+
* Unlike {@link Cluster.apply}, this uses `kubectl create` which fails if the
|
|
482
|
+
* resource already exists.
|
|
483
|
+
*
|
|
484
|
+
* @template T - The expected Kubernetes resource shape.
|
|
485
|
+
* @param manifest - YAML string, resource object, or imported YAML module.
|
|
486
|
+
* @param options - Retry options such as timeout and polling interval.
|
|
487
|
+
*
|
|
488
|
+
* @example
|
|
489
|
+
* ```ts
|
|
490
|
+
* await cluster.create({
|
|
491
|
+
* apiVersion: "v1",
|
|
492
|
+
* kind: "Namespace",
|
|
493
|
+
* metadata: { name: "my-team" },
|
|
494
|
+
* });
|
|
495
|
+
* ```
|
|
496
|
+
*/
|
|
497
|
+
create<T extends K8sResource>(
|
|
498
|
+
manifest: ApplyingManifest<T>,
|
|
499
|
+
options?: undefined | ActionOptions
|
|
500
|
+
): Promise<void>;
|
|
501
|
+
|
|
445
502
|
/**
|
|
446
503
|
* Applies the `status` subresource using server-side apply.
|
|
447
504
|
*
|
|
@@ -683,6 +740,37 @@ export interface Namespace {
|
|
|
683
740
|
options?: undefined | ActionOptions
|
|
684
741
|
): Promise<void>;
|
|
685
742
|
|
|
743
|
+
/**
|
|
744
|
+
* Creates a Kubernetes resource in this namespace with `kubectl create` and
|
|
745
|
+
* registers cleanup.
|
|
746
|
+
*
|
|
747
|
+
* The target namespace is controlled by this {@link Namespace} instance.
|
|
748
|
+
* Prefer omitting `manifest.metadata.namespace`; if it is set, it must match
|
|
749
|
+
* this namespace (otherwise `kubectl` fails).
|
|
750
|
+
*
|
|
751
|
+
* Unlike {@link Namespace.apply}, this uses `kubectl create` which fails if
|
|
752
|
+
* the resource already exists.
|
|
753
|
+
*
|
|
754
|
+
* @template T - The expected Kubernetes resource shape.
|
|
755
|
+
* @param manifest - YAML string, resource object, or imported YAML module.
|
|
756
|
+
* @param options - Retry options such as timeout and polling interval.
|
|
757
|
+
*
|
|
758
|
+
* @example
|
|
759
|
+
* ```ts
|
|
760
|
+
* const ns = await s.newNamespace("my-ns");
|
|
761
|
+
* await ns.create({
|
|
762
|
+
* apiVersion: "v1",
|
|
763
|
+
* kind: "ConfigMap",
|
|
764
|
+
* metadata: { name: "my-config" },
|
|
765
|
+
* data: { mode: "demo" },
|
|
766
|
+
* });
|
|
767
|
+
* ```
|
|
768
|
+
*/
|
|
769
|
+
create<T extends K8sResource>(
|
|
770
|
+
manifest: ApplyingManifest<T>,
|
|
771
|
+
options?: undefined | ActionOptions
|
|
772
|
+
): Promise<void>;
|
|
773
|
+
|
|
686
774
|
/**
|
|
687
775
|
* Applies the `status` subresource in this namespace using server-side apply.
|
|
688
776
|
*
|
package/ts/k8s-resource/index.ts
CHANGED
|
@@ -118,3 +118,25 @@ export interface ESM {
|
|
|
118
118
|
function isESM(value: unknown): value is ESM {
|
|
119
119
|
return typeof value === "object" && value !== null && "default" in value;
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
export function getResourceMeta(
|
|
123
|
+
value: unknown
|
|
124
|
+
): undefined | { kind: string; name: string } {
|
|
125
|
+
if (typeof value === "string") {
|
|
126
|
+
const result = parseK8sResourceYaml(value);
|
|
127
|
+
if (result.ok) {
|
|
128
|
+
return { kind: result.value.kind, name: result.value.metadata.name };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (isESM(value)) {
|
|
132
|
+
const result = parseK8sResourceFromESM(value);
|
|
133
|
+
if (result.ok) {
|
|
134
|
+
return { kind: result.value.kind, name: result.value.metadata.name };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const result = parseK8sResource(value);
|
|
138
|
+
if (result.ok) {
|
|
139
|
+
return { kind: result.value.kind, name: result.value.metadata.name };
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
package/ts/recording/index.ts
CHANGED
|
@@ -1,126 +1,92 @@
|
|
|
1
|
-
interface BaseEvent<
|
|
2
|
-
Kind extends string,
|
|
3
|
-
Data extends Record<string, unknown> = Record<string, never>,
|
|
4
|
-
> {
|
|
5
|
-
readonly kind: Kind;
|
|
6
|
-
readonly data: Data;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
1
|
export type Event =
|
|
10
|
-
|
|
|
11
|
-
| CommandRunEvent
|
|
12
|
-
| CommandResultEvent
|
|
13
|
-
| RetryEvent
|
|
2
|
+
| ScenarioEvent
|
|
14
3
|
| ActionEvent
|
|
4
|
+
| CommandEvent
|
|
5
|
+
| RetryEvent
|
|
15
6
|
| RevertingsEvent
|
|
16
7
|
| BDDEvent;
|
|
17
8
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
|
29
|
-
|
|
9
|
+
type ScenarioEvent =
|
|
10
|
+
| BaseEvent<
|
|
11
|
+
"ScenarioStart",
|
|
12
|
+
{
|
|
13
|
+
readonly name: string;
|
|
14
|
+
}
|
|
15
|
+
>
|
|
16
|
+
| BaseEvent<"ScenarioEnd", Record<string, never>>;
|
|
17
|
+
|
|
18
|
+
type ActionEvent =
|
|
19
|
+
| BaseEvent<
|
|
20
|
+
"ActionStart",
|
|
21
|
+
{
|
|
22
|
+
readonly description: string;
|
|
23
|
+
}
|
|
24
|
+
>
|
|
25
|
+
| BaseEvent<
|
|
26
|
+
"ActionEnd",
|
|
27
|
+
{
|
|
28
|
+
readonly ok: boolean;
|
|
29
|
+
readonly error?: undefined | Error;
|
|
30
|
+
}
|
|
31
|
+
>;
|
|
32
|
+
|
|
33
|
+
type CommandEvent =
|
|
34
|
+
| BaseEvent<
|
|
35
|
+
"CommandRun",
|
|
36
|
+
{
|
|
37
|
+
readonly cmd: string;
|
|
38
|
+
readonly args: ReadonlyArray<string>;
|
|
39
|
+
readonly stdin?: undefined | string;
|
|
40
|
+
readonly stdinLanguage?: undefined | string;
|
|
41
|
+
}
|
|
42
|
+
>
|
|
43
|
+
| BaseEvent<
|
|
44
|
+
"CommandResult",
|
|
45
|
+
{
|
|
46
|
+
readonly exitCode: number;
|
|
47
|
+
readonly stdout: string;
|
|
48
|
+
readonly stderr: string;
|
|
49
|
+
readonly stdoutLanguage?: undefined | string;
|
|
50
|
+
readonly stderrLanguage?: undefined | string;
|
|
51
|
+
}
|
|
52
|
+
>;
|
|
53
|
+
|
|
54
|
+
type RetryEvent =
|
|
55
|
+
| BaseEvent<"RetryStart", Record<string, never>>
|
|
56
|
+
| BaseEvent<
|
|
57
|
+
"RetryEnd",
|
|
58
|
+
| {
|
|
59
|
+
readonly attempts: number;
|
|
60
|
+
readonly success: true;
|
|
61
|
+
readonly reason: "success";
|
|
62
|
+
}
|
|
63
|
+
| {
|
|
64
|
+
readonly attempts: number;
|
|
65
|
+
readonly success: false;
|
|
66
|
+
readonly reason: "timeout";
|
|
67
|
+
readonly error: Error;
|
|
68
|
+
}
|
|
69
|
+
>;
|
|
70
|
+
|
|
71
|
+
type RevertingsEvent =
|
|
72
|
+
| BaseEvent<"RevertingsStart">
|
|
73
|
+
| BaseEvent<"RevertingsEnd">;
|
|
74
|
+
|
|
75
|
+
type BDDEvent =
|
|
76
|
+
| BaseEvent<"BDDGiven", { readonly description: string }>
|
|
77
|
+
| BaseEvent<"BDDWhen", { readonly description: string }>
|
|
78
|
+
| BaseEvent<"BDDThen", { readonly description: string }>
|
|
79
|
+
| BaseEvent<"BDDAnd", { readonly description: string }>
|
|
80
|
+
| BaseEvent<"BDBut", { readonly description: string }>;
|
|
30
81
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
82
|
+
interface BaseEvent<
|
|
83
|
+
Kind extends string,
|
|
84
|
+
Data extends Record<string, unknown> = Record<string, never>,
|
|
85
|
+
> {
|
|
86
|
+
readonly kind: Kind;
|
|
87
|
+
readonly data: Data;
|
|
34
88
|
}
|
|
35
89
|
|
|
36
|
-
export type RetryStartEvent = BaseEvent<"RetryStart", Record<string, never>>;
|
|
37
|
-
|
|
38
|
-
export type RetryAttemptEvent = BaseEvent<
|
|
39
|
-
"RetryAttempt",
|
|
40
|
-
{
|
|
41
|
-
readonly attempt: number;
|
|
42
|
-
}
|
|
43
|
-
>;
|
|
44
|
-
|
|
45
|
-
export type RetryFailureEvent = BaseEvent<
|
|
46
|
-
"RetryFailure",
|
|
47
|
-
{
|
|
48
|
-
readonly attempt: number;
|
|
49
|
-
readonly error: ErrorSummary;
|
|
50
|
-
}
|
|
51
|
-
>;
|
|
52
|
-
|
|
53
|
-
export type RetryEndEvent = BaseEvent<
|
|
54
|
-
"RetryEnd",
|
|
55
|
-
| {
|
|
56
|
-
readonly attempts: number;
|
|
57
|
-
readonly success: true;
|
|
58
|
-
readonly reason: "success";
|
|
59
|
-
}
|
|
60
|
-
| {
|
|
61
|
-
readonly attempts: number;
|
|
62
|
-
readonly success: false;
|
|
63
|
-
readonly reason: "timeout";
|
|
64
|
-
readonly error: ErrorSummary;
|
|
65
|
-
}
|
|
66
|
-
>;
|
|
67
|
-
|
|
68
|
-
export type CommandRunEvent = BaseEvent<
|
|
69
|
-
"CommandRun",
|
|
70
|
-
{
|
|
71
|
-
readonly cmd: string;
|
|
72
|
-
readonly args: ReadonlyArray<string>;
|
|
73
|
-
readonly stdin?: undefined | string;
|
|
74
|
-
readonly stdinLanguage?: undefined | string;
|
|
75
|
-
}
|
|
76
|
-
>;
|
|
77
|
-
|
|
78
|
-
export type CommandResultEvent = BaseEvent<
|
|
79
|
-
"CommandResult",
|
|
80
|
-
{
|
|
81
|
-
readonly exitCode: number;
|
|
82
|
-
readonly stdout: string;
|
|
83
|
-
readonly stderr: string;
|
|
84
|
-
readonly stdoutLanguage?: undefined | string;
|
|
85
|
-
readonly stderrLanguage?: undefined | string;
|
|
86
|
-
}
|
|
87
|
-
>;
|
|
88
|
-
|
|
89
|
-
export type ActionEvent = ActionStartEvent | ActionEndEvent;
|
|
90
|
-
|
|
91
|
-
export type ActionPhase = "mutate" | "revert" | "query";
|
|
92
|
-
|
|
93
|
-
export type ActionStartEvent = BaseEvent<
|
|
94
|
-
"ActionStart",
|
|
95
|
-
{
|
|
96
|
-
readonly action: string; // e.g. "CreateNamespaceAction"
|
|
97
|
-
readonly phase: ActionPhase;
|
|
98
|
-
readonly input?: undefined | Readonly<Record<string, unknown>>;
|
|
99
|
-
}
|
|
100
|
-
>;
|
|
101
|
-
|
|
102
|
-
export type ActionEndEvent = BaseEvent<
|
|
103
|
-
"ActionEnd",
|
|
104
|
-
{
|
|
105
|
-
readonly action: string; // e.g. "CreateNamespaceAction"
|
|
106
|
-
readonly phase: ActionPhase;
|
|
107
|
-
readonly ok: boolean;
|
|
108
|
-
readonly error?: undefined | ErrorSummary;
|
|
109
|
-
readonly output?: undefined | Readonly<Record<string, unknown>>;
|
|
110
|
-
}
|
|
111
|
-
>;
|
|
112
|
-
|
|
113
|
-
export type RevertingsEvent = RevertingsStartEvent | RevertingsEndEvent;
|
|
114
|
-
export type RevertingsStartEvent = BaseEvent<"RevertingsStart">;
|
|
115
|
-
export type RevertingsEndEvent = BaseEvent<"RevertingsEnd">;
|
|
116
|
-
|
|
117
|
-
export type BDDEvent = BaseEvent<
|
|
118
|
-
"BDDGiven" | "BDDWhen" | "BDDThen" | "BDDAnd" | "BDBut",
|
|
119
|
-
{
|
|
120
|
-
readonly description: string;
|
|
121
|
-
}
|
|
122
|
-
>;
|
|
123
|
-
|
|
124
90
|
export class Recorder {
|
|
125
91
|
private readonly events: Array<Event> = [];
|
|
126
92
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Event } from "../../recording";
|
|
2
|
+
import type { Reporter } from "../interface";
|
|
3
|
+
import { parseEvents } from "./parser";
|
|
4
|
+
import { renderReport } from "./renderer";
|
|
5
|
+
|
|
6
|
+
export interface MarkdownReporterOptions {
|
|
7
|
+
/**
|
|
8
|
+
* If true, keep ANSI escape codes (colors) in error messages.
|
|
9
|
+
* If false (default), remove ANSI escape codes.
|
|
10
|
+
*/
|
|
11
|
+
enableANSI?: undefined | boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function newMarkdownReporter(
|
|
15
|
+
options: MarkdownReporterOptions = {}
|
|
16
|
+
): Reporter {
|
|
17
|
+
return {
|
|
18
|
+
report(events: ReadonlyArray<Event>): Promise<string> {
|
|
19
|
+
const report = parseEvents(events);
|
|
20
|
+
return renderReport(report, options);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface Report {
|
|
2
|
+
scenarios: Array<Scenario>;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface Scenario {
|
|
6
|
+
name: string;
|
|
7
|
+
overview: Array<OverviewItem>;
|
|
8
|
+
details: Array<Tagged<"BDDSection", BDDSection> | Tagged<"Action", Action>>;
|
|
9
|
+
cleanup: Array<CleanupItem>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Tagged<Tag extends string, Target extends object> = Target & {
|
|
13
|
+
readonly type: Tag;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface OverviewItem {
|
|
17
|
+
name: string;
|
|
18
|
+
status: "pending" | "success" | "failure";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BDDSection {
|
|
22
|
+
keyword: "given" | "when" | "then" | "and" | "but";
|
|
23
|
+
description: string;
|
|
24
|
+
actions: Array<Action>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Action {
|
|
28
|
+
name: string;
|
|
29
|
+
attempts?: undefined | number;
|
|
30
|
+
command?: undefined | Command;
|
|
31
|
+
error?: undefined | Error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Command {
|
|
35
|
+
cmd: string;
|
|
36
|
+
args: Array<string>;
|
|
37
|
+
stdin?: Text;
|
|
38
|
+
stdout?: Text;
|
|
39
|
+
stderr?: Text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Text {
|
|
43
|
+
text: string;
|
|
44
|
+
language?: undefined | string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Error {
|
|
48
|
+
message: Text;
|
|
49
|
+
stack?: undefined | string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CleanupItem {
|
|
53
|
+
action: string;
|
|
54
|
+
resource?: undefined | string;
|
|
55
|
+
status: "success" | "failure";
|
|
56
|
+
command: CleanupCommand;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CleanupCommand {
|
|
60
|
+
cmd: string;
|
|
61
|
+
args: Array<string>;
|
|
62
|
+
output: string;
|
|
63
|
+
}
|