@typespec/spector 0.1.0-alpha.1-dev.1 → 0.1.0-alpha.10-dev.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.
- package/CHANGELOG.md +52 -1
- package/dist/src/actions/serve.js +3 -3
- package/dist/src/actions/serve.js.map +1 -1
- package/dist/src/actions/server-test.d.ts +5 -0
- package/dist/src/actions/server-test.d.ts.map +1 -1
- package/dist/src/actions/server-test.js +35 -1
- package/dist/src/actions/server-test.js.map +1 -1
- package/dist/src/actions/upload-coverage-report.d.ts +2 -1
- package/dist/src/actions/upload-coverage-report.d.ts.map +1 -1
- package/dist/src/actions/upload-coverage-report.js +6 -2
- package/dist/src/actions/upload-coverage-report.js.map +1 -1
- package/dist/src/actions/upload-scenario-manifest.d.ts +4 -2
- package/dist/src/actions/upload-scenario-manifest.d.ts.map +1 -1
- package/dist/src/actions/upload-scenario-manifest.js +17 -7
- package/dist/src/actions/upload-scenario-manifest.js.map +1 -1
- package/dist/src/cli/cli.js +28 -11
- package/dist/src/cli/cli.js.map +1 -1
- package/dist/src/coverage/common.d.ts.map +1 -1
- package/dist/src/coverage/common.js +1 -0
- package/dist/src/coverage/common.js.map +1 -1
- package/dist/src/coverage/scenario-manifest.d.ts +2 -2
- package/dist/src/coverage/scenario-manifest.d.ts.map +1 -1
- package/dist/src/coverage/scenario-manifest.js +4 -5
- package/dist/src/coverage/scenario-manifest.js.map +1 -1
- package/dist/src/lib/decorators.d.ts.map +1 -1
- package/dist/src/lib/decorators.js +0 -2
- package/dist/src/lib/decorators.js.map +1 -1
- package/dist/src/server/server.js +1 -1
- package/dist/src/server/server.js.map +1 -1
- package/dist/src/utils/body-utils.d.ts.map +1 -1
- package/dist/src/utils/request-utils.d.ts.map +1 -1
- package/docs/decorators.md +24 -0
- package/docs/using-spector.md +99 -0
- package/docs/writing-mock-apis.md +116 -0
- package/docs/writing-scenario-spec.md +49 -0
- package/package.json +19 -19
- package/src/actions/serve.ts +3 -3
- package/src/actions/server-test.ts +42 -1
- package/src/actions/upload-coverage-report.ts +7 -1
- package/src/actions/upload-scenario-manifest.ts +22 -9
- package/src/cli/cli.ts +28 -11
- package/src/coverage/common.ts +1 -0
- package/src/coverage/scenario-manifest.ts +5 -3
- package/src/lib/decorators.ts +0 -2
- package/src/server/server.ts +1 -1
- package/temp/.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-utils.d.ts","sourceRoot":"","sources":["../../../src/utils/request-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,eAAO,MAAM,iBAAiB,
|
|
1
|
+
{"version":3,"file":"request-utils.d.ts","sourceRoot":"","sources":["../../../src/utils/request-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,eAAO,MAAM,iBAAiB,GAAI,SAAS,OAAO,KAAG,MACL,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## Spector decorators
|
|
2
|
+
|
|
3
|
+
### `@scenarioService`
|
|
4
|
+
|
|
5
|
+
Decorator setting up the boilerplate for specs service namespace. Will automatically set:
|
|
6
|
+
|
|
7
|
+
- `@service{title: '<namespace>', value: '1.0.0'}` using the namespace as a value
|
|
8
|
+
- `@server` to `localhost:3000`
|
|
9
|
+
- `@route` using the parameter passed.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
|
|
13
|
+
```tsp
|
|
14
|
+
@scenarioSpec("/my-spec")
|
|
15
|
+
namespace MySpec;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### `@scenario`
|
|
19
|
+
|
|
20
|
+
Mark an operation, interface or namespace as a scenario. Optionally can provide the name of the scenario.
|
|
21
|
+
|
|
22
|
+
### `@scenarioDoc`
|
|
23
|
+
|
|
24
|
+
Specify how to implement this scenario. Value is markdown. Differ from @doc which describe the scenario to the end user.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Using spector
|
|
2
|
+
|
|
3
|
+
Install `spector` CLI globally or as a local dependency:
|
|
4
|
+
|
|
5
|
+
- **Globally**: run `npm install -g @typespec/spector`. You can then use `tsp-spector` as a CLI tool.
|
|
6
|
+
- **Locally**: run `npm install @typespec/spector` which should add the dependency to package.json. You can then use `tsp-spector` with `npx tsp-spector`.
|
|
7
|
+
|
|
8
|
+
**NOTE** With local install you'll need to prefix `tsp-spector` with `npx`(unless commands are written directly as npm scripts)
|
|
9
|
+
|
|
10
|
+
## Test against scenarios
|
|
11
|
+
|
|
12
|
+
### Spector Config
|
|
13
|
+
|
|
14
|
+
Spector supports a config file where you can configure scenarios that are not supported by your generator.
|
|
15
|
+
|
|
16
|
+
Create a `spector-config.yaml` file.
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
# List of unsupported scenarios
|
|
20
|
+
unsupportedScenarios:
|
|
21
|
+
- Foo_Bar
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Run mock api server
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Minimal
|
|
28
|
+
tsp-spector serve ./path/to/scenarios
|
|
29
|
+
|
|
30
|
+
# Change the port
|
|
31
|
+
tsp-spector serve ./path/to/scenarios --port 1234
|
|
32
|
+
|
|
33
|
+
# Specify where the coverage file should go
|
|
34
|
+
tsp-spector serve ./path/to/scenarios --coverageFile ./path/to/spector-coverage.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Alternative to start in background
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
tsp-spector server start ./path/to/scenarios # Takes the same arguments as serve
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Stop running server
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
tsp-spector server stop # Stop at the default port
|
|
47
|
+
tsp-spector server stop --port 1234 # If started the server at another port
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Validate and merge coverage
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Minimal
|
|
54
|
+
tsp-spector check-coverage ./path/to/scenarios
|
|
55
|
+
|
|
56
|
+
# Path to tsp-spector config file for generator
|
|
57
|
+
tsp-spector check-coverage ./path/to/scenarios --configFile ./spector-config.yaml
|
|
58
|
+
|
|
59
|
+
# In case where there was multiple serve instance each creating their own coverage file
|
|
60
|
+
tsp-spector check-coverage ./path/to/scenarios --coverageFiles ./path/to/*-coverage.json --coverageFiles ./other/to/*-coverage.json
|
|
61
|
+
|
|
62
|
+
# Specify where the merged coverage file should go
|
|
63
|
+
tsp-spector check-coverage ./path/to/scenarios --mergedCoverageFile ./path/to/spector-final-coverage.json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Upload coverage
|
|
67
|
+
|
|
68
|
+
Upload the coverage. Upload from the `main` branch. DO NOT upload on PR this WILL override the latest index.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Minimal
|
|
72
|
+
tsp-spector upload-coverage --generatorName typescript --version=0.1.0
|
|
73
|
+
|
|
74
|
+
# Specify Coverage file
|
|
75
|
+
tsp-spector upload-coverage --generatorName typescript --version=0.1.0 --coverageFile ./path/to/spector-final-coverage.json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
|
|
80
|
+
- `--storageAccountName`: Name of the storage account to publish coverage. Use `typespec` for Spector/Azure Spector dashboard.
|
|
81
|
+
- `--generatorName`: Name of the generator. Must be one of `"@typespec/http-client-python", "@typespec/http-client-csharp", "@azure-tools/typespec-ts-rlc", "@azure-tools/typespec-ts-modular", "@typespec/http-client-js", "@typespec/http-client-java"`.
|
|
82
|
+
- `--generatorVersion`: Version of the generator
|
|
83
|
+
- `--coverageFile`: Path to the coverage file
|
|
84
|
+
|
|
85
|
+
#### Upload in azure devops
|
|
86
|
+
|
|
87
|
+
**This is applicable in the azure-sdk/internal ado project only.**
|
|
88
|
+
|
|
89
|
+
Add the following step
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
- task: AzureCLI@2
|
|
93
|
+
displayName: Upload scenario coverage
|
|
94
|
+
inputs:
|
|
95
|
+
azureSubscription: "Typespec Storage"
|
|
96
|
+
scriptType: "bash"
|
|
97
|
+
scriptLocation: "inlineScript"
|
|
98
|
+
inlineScript: `tsp-spector upload-coverage --storageAccountName typespec --containerName coverages --generatorMode standard ... FILL options fitting your generator here as described above...`
|
|
99
|
+
```
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Writing mock apis
|
|
2
|
+
|
|
3
|
+
1. Create a `mockapi.ts` file next to the `main.tsp` of the scenario
|
|
4
|
+
2. Create and export a variable called `Scenarios` with a `Record<string, ScenarioMockApi>` type
|
|
5
|
+
3. For each of the scenario assign a new property to the `Scenarios` variable. The value use one of the following:
|
|
6
|
+
- `passOnSuccess`: This will take one or multiple routes and will only pass teh scenario if all the routes gets called with a 2xx success code.
|
|
7
|
+
|
|
8
|
+
## Example
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { passOnSuccess, mockapi } from "@typespec/spec-api";
|
|
12
|
+
import { ScenarioMockApi } from "@typespec/spec-api";
|
|
13
|
+
|
|
14
|
+
export const Scenarios: Record<string, ScenarioMockApi> = {};
|
|
15
|
+
|
|
16
|
+
Scenarios.Hello_world = passOnSuccess(
|
|
17
|
+
mockapi.get("/hello/world", () => {
|
|
18
|
+
return {
|
|
19
|
+
status: 200,
|
|
20
|
+
body: {
|
|
21
|
+
contentType: "application/json",
|
|
22
|
+
rawContent: `"Hello World!"`,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## How to build response
|
|
30
|
+
|
|
31
|
+
Return the response object. [See type](../../spec-api/src/types.ts)
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// Minimum requirement is the status code.
|
|
35
|
+
return {
|
|
36
|
+
status: 200,
|
|
37
|
+
};
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Return a body
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// Return json
|
|
44
|
+
return {
|
|
45
|
+
status: 200,
|
|
46
|
+
body: json({ foo: 123 }),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Return raw content
|
|
50
|
+
return {
|
|
51
|
+
status: 200,
|
|
52
|
+
body: {
|
|
53
|
+
contentType: "application/text",
|
|
54
|
+
rawContent: "foobar",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Return headers
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// Return json
|
|
63
|
+
return {
|
|
64
|
+
status: 200,
|
|
65
|
+
headers: {
|
|
66
|
+
MyHeader: "value-1"
|
|
67
|
+
MyHeaderOther: req.headers.MyRequestHeader
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## How to validate the request:
|
|
74
|
+
|
|
75
|
+
All built-in validation tools can be accessed using `req.expect.`
|
|
76
|
+
|
|
77
|
+
### Validate the body
|
|
78
|
+
|
|
79
|
+
- With `req.expect.bodyEquals`
|
|
80
|
+
|
|
81
|
+
This will do a deep equals of the body to make sure it match.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
app.post("/example", "Example", (req) => {
|
|
85
|
+
req.bodyEquals({ foo: "123", bar: "456" });
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- With `req.expect.rawBodyEquals`
|
|
90
|
+
|
|
91
|
+
This will compare the raw body sent.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
app.post("/example", "Example", (req) => {
|
|
95
|
+
req.rawBodyEquals('"foo"');
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Custom validation
|
|
100
|
+
|
|
101
|
+
You can do any kind of validation accessing the `req: MockRequest` object and deciding to return a different response in some cases.
|
|
102
|
+
You can also always `throw` a `ValidationError`
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
app.post("/example", "Example", (req) => {
|
|
108
|
+
if (req.headers.MyCustomHeader.startsWith("x-foo")) {
|
|
109
|
+
throw new ValidationError(
|
|
110
|
+
"MyCustomHeader shouldn't start with x-foo",
|
|
111
|
+
null,
|
|
112
|
+
req.headers.MyCustomHeader,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
## Defining scenario
|
|
2
|
+
|
|
3
|
+
The goal of the testserver is to define scenarios that needs to be supported in client generators. This means to have a meaningful coverage we need scenarios to:
|
|
4
|
+
|
|
5
|
+
- have a specific behavior that is meant to be tested
|
|
6
|
+
- ❌ DO NOT use the same scenario for testing multiple things.
|
|
7
|
+
- have a meaningful name that can be used in a compatibility table to see what a given generator support(e.g. `get_string`)
|
|
8
|
+
- a description of what the scenario is validating(e.g. `"Support passing a simple string as JSON"`)
|
|
9
|
+
- have a good description on what the client is expecting to generate/receive/send(e.g `Validate that this operation returns a JSON string that match "abc"`)
|
|
10
|
+
- ✅ DO describe how to validate that this scenario is working from the client point of view
|
|
11
|
+
|
|
12
|
+
When naming scenario always think about what would it look like in the [compatibility table](#compatibility-table) and would that name be meaningful to someone looking to see what is supported.
|
|
13
|
+
|
|
14
|
+
```cadl
|
|
15
|
+
import "@typespec/spector";
|
|
16
|
+
|
|
17
|
+
@scenarioService("/strings")
|
|
18
|
+
namespace String;
|
|
19
|
+
|
|
20
|
+
@scenario("get_string")
|
|
21
|
+
@doc("Support passing a simple string as JSON")
|
|
22
|
+
@scenarioDoc("In this scenario the Client should expect a string matching 'abc' to be returned.")
|
|
23
|
+
@get
|
|
24
|
+
@route("/simple")
|
|
25
|
+
op returnString(): string;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Decorators that should be provided in this test library `@typespec/spector`:
|
|
29
|
+
|
|
30
|
+
- `@scenario`: Specify that this operation, interface or namespace is a scenario. Optionally take a scenario name otherwise default to the namespace name + operation/interface name
|
|
31
|
+
- `@scenarioDoc`: Specify how to implement this scenario. Differ from `@doc` which describe the scenario to the end user.
|
|
32
|
+
- `@supportedBy`: Specify if something is supported only by some kind of SDK. Option: `arm`, `dpg`. By default everything.
|
|
33
|
+
|
|
34
|
+
## Compatibility table
|
|
35
|
+
|
|
36
|
+
With all this information, a detailed compatibility table should be able to be produced by compiling each one of the scenarios and extracting the cases. Providing something like
|
|
37
|
+
|
|
38
|
+
| Scenario | CSharp | Python | Go | Java | TS/JS |
|
|
39
|
+
| ---------------------------- | ------ | ------ | --- | ---- | ----- |
|
|
40
|
+
| Vanilla |
|
|
41
|
+
| `get_string` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
42
|
+
| `put_string` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
43
|
+
| Azure |
|
|
44
|
+
| `pageable_nextLink` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
45
|
+
| `pageable_continuationToken` | ❌ | ✅ | ✅ | ✅ | ✅ |
|
|
46
|
+
|
|
47
|
+
## Writing the mock api
|
|
48
|
+
|
|
49
|
+
Each scenario should be accompanied by a mock api. See [writing a mock api docs](./writing-mock-apis.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typespec/spector",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.10-dev.0",
|
|
4
4
|
"description": "Typespec Core Tool to validate, run mock api, collect coverage.",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -26,30 +26,30 @@
|
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/microsoft/typespec#readme",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@azure/identity": "~4.
|
|
29
|
+
"@azure/identity": "~4.7.0",
|
|
30
30
|
"@types/js-yaml": "^4.0.5",
|
|
31
|
-
"@typespec/compiler": "
|
|
32
|
-
"@typespec/
|
|
33
|
-
"@typespec/
|
|
34
|
-
"@typespec/
|
|
35
|
-
"@typespec/spec-
|
|
36
|
-
"@typespec/
|
|
31
|
+
"@typespec/compiler": "^0.67.0 || >=0.68.0-dev <0.68.0",
|
|
32
|
+
"@typespec/http": "^0.67.0 || >=0.68.0-dev <0.68.0",
|
|
33
|
+
"@typespec/rest": "^0.67.0 || >=0.68.0-dev <0.68.0",
|
|
34
|
+
"@typespec/spec-api": "^0.1.0-alpha.2 || >=0.1.0-alpha.3-dev <0.1.0-alpha.3",
|
|
35
|
+
"@typespec/spec-coverage-sdk": "^0.1.0-alpha.4 || >=0.1.0-alpha.5-dev <0.1.0-alpha.5",
|
|
36
|
+
"@typespec/versioning": "^0.67.0 || >=0.68.0-dev <0.68.0",
|
|
37
37
|
"ajv": "~8.17.1",
|
|
38
|
-
"axios": "^1.
|
|
38
|
+
"axios": "^1.8.1",
|
|
39
39
|
"body-parser": "^1.20.3",
|
|
40
40
|
"deep-equal": "^2.2.0",
|
|
41
|
-
"express": "^4.21.
|
|
41
|
+
"express": "^4.21.2",
|
|
42
42
|
"express-promise-router": "^4.1.1",
|
|
43
|
-
"form-data": "^4.0.
|
|
44
|
-
"globby": "~14.0
|
|
45
|
-
"jackspeak": "4.0
|
|
43
|
+
"form-data": "^4.0.2",
|
|
44
|
+
"globby": "~14.1.0",
|
|
45
|
+
"jackspeak": "4.1.0",
|
|
46
46
|
"js-yaml": "^4.1.0",
|
|
47
47
|
"morgan": "^1.10.0",
|
|
48
48
|
"multer": "^1.4.5-lts.1",
|
|
49
49
|
"node-fetch": "^3.3.1",
|
|
50
|
-
"picocolors": "~1.1.
|
|
50
|
+
"picocolors": "~1.1.1",
|
|
51
51
|
"source-map-support": "~0.5.21",
|
|
52
|
-
"winston": "^3.
|
|
52
|
+
"winston": "^3.17.0",
|
|
53
53
|
"xml2js": "^0.6.2",
|
|
54
54
|
"yargs": "~17.7.2"
|
|
55
55
|
},
|
|
@@ -59,13 +59,13 @@
|
|
|
59
59
|
"@types/express": "^5.0.0",
|
|
60
60
|
"@types/morgan": "^1.9.4",
|
|
61
61
|
"@types/multer": "^1.4.10",
|
|
62
|
-
"@types/node": "~22.
|
|
63
|
-
"@types/node-fetch": "^2.6.
|
|
62
|
+
"@types/node": "~22.13.9",
|
|
63
|
+
"@types/node-fetch": "^2.6.12",
|
|
64
64
|
"@types/xml2js": "^0.4.11",
|
|
65
65
|
"@types/yargs": "~17.0.33",
|
|
66
66
|
"rimraf": "~6.0.1",
|
|
67
|
-
"typescript": "~5.
|
|
68
|
-
"@typespec/tspd": "
|
|
67
|
+
"typescript": "~5.8.2",
|
|
68
|
+
"@typespec/tspd": "^0.46.0"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {},
|
|
71
71
|
"scripts": {
|
package/src/actions/serve.ts
CHANGED
|
@@ -39,14 +39,14 @@ export async function startInBackground(config: ServeConfig) {
|
|
|
39
39
|
const [nodeExe, entrypoint] = process.argv;
|
|
40
40
|
logger.info(`Starting server in background at port ${config.port}`);
|
|
41
41
|
const scenariosPath = Array.isArray(config.scenariosPath)
|
|
42
|
-
? config.scenariosPath
|
|
43
|
-
: config.scenariosPath;
|
|
42
|
+
? config.scenariosPath
|
|
43
|
+
: [config.scenariosPath];
|
|
44
44
|
const cp = spawn(
|
|
45
45
|
nodeExe,
|
|
46
46
|
[
|
|
47
47
|
entrypoint,
|
|
48
48
|
"serve",
|
|
49
|
-
scenariosPath,
|
|
49
|
+
...scenariosPath,
|
|
50
50
|
"--port",
|
|
51
51
|
config.port.toString(),
|
|
52
52
|
"--coverageFile",
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { MockApiDefinition } from "@typespec/spec-api";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
+
import pc from "picocolors";
|
|
4
5
|
import { logger } from "../logger.js";
|
|
5
6
|
import { loadScenarioMockApis } from "../scenarios-resolver.js";
|
|
6
7
|
import { makeServiceCall, uint8ArrayToString } from "./helper.js";
|
|
7
8
|
|
|
8
9
|
const DEFAULT_BASE_URL = "http://localhost:3000";
|
|
9
10
|
|
|
11
|
+
export interface ServerTestDiagnostics {
|
|
12
|
+
scenario_name: string;
|
|
13
|
+
status: "success" | "failure";
|
|
14
|
+
message: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
class ServerTestsGenerator {
|
|
11
18
|
private name: string = "";
|
|
12
19
|
private mockApiDefinition: MockApiDefinition;
|
|
@@ -184,6 +191,9 @@ export async function serverTest(scenariosPath: string, options: ServerTestOptio
|
|
|
184
191
|
}
|
|
185
192
|
// 2. Load all the scenarios
|
|
186
193
|
const scenarios = await loadScenarioMockApis(scenariosPath);
|
|
194
|
+
const success_diagnostics: ServerTestDiagnostics[] = [];
|
|
195
|
+
const failure_diagnostics: ServerTestDiagnostics[] = [];
|
|
196
|
+
|
|
187
197
|
// 3. Execute each scenario
|
|
188
198
|
for (const [name, scenario] of Object.entries(scenarios)) {
|
|
189
199
|
if (!Array.isArray(scenario.apis)) continue;
|
|
@@ -191,8 +201,39 @@ export async function serverTest(scenariosPath: string, options: ServerTestOptio
|
|
|
191
201
|
if (api.kind !== "MockApiDefinition") continue;
|
|
192
202
|
if (testCasesToRun.length === 0 || testCasesToRun.includes(name)) {
|
|
193
203
|
const obj: ServerTestsGenerator = new ServerTestsGenerator(name, api, baseUrl);
|
|
194
|
-
|
|
204
|
+
try {
|
|
205
|
+
await obj.executeScenario();
|
|
206
|
+
success_diagnostics.push({
|
|
207
|
+
scenario_name: name,
|
|
208
|
+
status: "success",
|
|
209
|
+
message: "executed successfully",
|
|
210
|
+
});
|
|
211
|
+
} catch (e: any) {
|
|
212
|
+
failure_diagnostics.push({
|
|
213
|
+
scenario_name: name,
|
|
214
|
+
status: "failure",
|
|
215
|
+
message: `code = ${e.code} \n message = ${e.message} \n name = ${e.name} \n stack = ${e.stack} \n status = ${e.status}`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
195
218
|
}
|
|
196
219
|
}
|
|
197
220
|
}
|
|
221
|
+
|
|
222
|
+
// 4. Print diagnostics
|
|
223
|
+
logger.info("Server Tests Diagnostics Summary");
|
|
224
|
+
|
|
225
|
+
if (success_diagnostics.length > 0) logger.info("Success Scenarios");
|
|
226
|
+
success_diagnostics.forEach((diagnostic) => {
|
|
227
|
+
logger.info(`${pc.green("✓")} Scenario: ${diagnostic.scenario_name} - ${diagnostic.message}`);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (failure_diagnostics.length > 0) logger.error("Failure Scenarios");
|
|
231
|
+
if (failure_diagnostics.length > 0) {
|
|
232
|
+
logger.error("Failed Scenario details");
|
|
233
|
+
failure_diagnostics.forEach((diagnostic) => {
|
|
234
|
+
logger.error(`${pc.red("✘")} Scenario: ${diagnostic.scenario_name}`);
|
|
235
|
+
logger.error(`${diagnostic.message}`);
|
|
236
|
+
});
|
|
237
|
+
process.exit(-1);
|
|
238
|
+
}
|
|
198
239
|
}
|
|
@@ -11,6 +11,7 @@ export interface UploadCoverageReportConfig {
|
|
|
11
11
|
generatorVersion: string;
|
|
12
12
|
generatorCommit?: string;
|
|
13
13
|
generatorMode: string;
|
|
14
|
+
containerName: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export async function uploadCoverageReport({
|
|
@@ -20,11 +21,16 @@ export async function uploadCoverageReport({
|
|
|
20
21
|
generatorVersion,
|
|
21
22
|
generatorCommit: geenratorCommit,
|
|
22
23
|
generatorMode,
|
|
24
|
+
containerName,
|
|
23
25
|
}: UploadCoverageReportConfig) {
|
|
24
26
|
const content = await readFile(coverageFile);
|
|
25
27
|
const coverage: CoverageReport = JSON.parse(content.toString());
|
|
26
28
|
|
|
27
|
-
const client = new SpecCoverageClient(storageAccountName,
|
|
29
|
+
const client = new SpecCoverageClient(storageAccountName, {
|
|
30
|
+
credential: new AzureCliCredential(),
|
|
31
|
+
containerName,
|
|
32
|
+
});
|
|
33
|
+
await client.createIfNotExists();
|
|
28
34
|
const generatorMetadata: GeneratorMetadata = {
|
|
29
35
|
name: generatorName,
|
|
30
36
|
version: generatorVersion,
|
|
@@ -1,28 +1,41 @@
|
|
|
1
1
|
import { AzureCliCredential } from "@azure/identity";
|
|
2
2
|
import { SpecCoverageClient } from "@typespec/spec-coverage-sdk";
|
|
3
3
|
import { writeFile } from "fs/promises";
|
|
4
|
+
import { resolve } from "path";
|
|
4
5
|
import pc from "picocolors";
|
|
5
6
|
import { computeScenarioManifest } from "../coverage/scenario-manifest.js";
|
|
6
7
|
import { logger } from "../logger.js";
|
|
7
8
|
|
|
8
9
|
export interface UploadScenarioManifestConfig {
|
|
9
|
-
|
|
10
|
+
scenariosPaths: string[];
|
|
10
11
|
storageAccountName: string;
|
|
12
|
+
setNames: string[];
|
|
13
|
+
containerName: string;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export async function uploadScenarioManifest({
|
|
14
|
-
|
|
17
|
+
scenariosPaths,
|
|
15
18
|
storageAccountName,
|
|
19
|
+
setNames,
|
|
20
|
+
containerName,
|
|
16
21
|
}: UploadScenarioManifestConfig) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
process.
|
|
22
|
+
const manifests = [];
|
|
23
|
+
for (let idx = 0; idx < scenariosPaths.length; idx++) {
|
|
24
|
+
const path = resolve(process.cwd(), scenariosPaths[idx]);
|
|
25
|
+
logger.info(`Computing scenario manifest for ${path}`);
|
|
26
|
+
const [manifest, diagnostics] = await computeScenarioManifest(path, setNames[idx]);
|
|
27
|
+
if (manifest === undefined || diagnostics.length > 0) {
|
|
28
|
+
process.exit(-1);
|
|
29
|
+
}
|
|
30
|
+
manifests.push(manifest);
|
|
20
31
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
await writeFile("manifest.json", JSON.stringify(manifests, null, 2));
|
|
33
|
+
const client = new SpecCoverageClient(storageAccountName, {
|
|
34
|
+
credential: new AzureCliCredential(),
|
|
35
|
+
containerName,
|
|
36
|
+
});
|
|
24
37
|
await client.createIfNotExists();
|
|
25
|
-
await client.manifest.upload(
|
|
38
|
+
await client.manifest.upload(manifests);
|
|
26
39
|
|
|
27
40
|
logger.info(
|
|
28
41
|
`${pc.green("✓")} Scenario manifest uploaded to ${storageAccountName} storage account.`,
|
package/src/cli/cli.ts
CHANGED
|
@@ -92,13 +92,14 @@ async function main() {
|
|
|
92
92
|
.command("server", "Server management", (cmd) => {
|
|
93
93
|
cmd
|
|
94
94
|
.command(
|
|
95
|
-
"start <
|
|
95
|
+
"start <scenariosPaths..>",
|
|
96
96
|
"Start the server in the background.",
|
|
97
97
|
(cmd) => {
|
|
98
98
|
return cmd
|
|
99
|
-
.positional("
|
|
100
|
-
description: "Path to the scenarios and mock apis",
|
|
99
|
+
.positional("scenariosPaths", {
|
|
100
|
+
description: "Path(s) to the scenarios and mock apis",
|
|
101
101
|
type: "string",
|
|
102
|
+
array: true,
|
|
102
103
|
demandOption: true,
|
|
103
104
|
})
|
|
104
105
|
.option("port", {
|
|
@@ -115,7 +116,7 @@ async function main() {
|
|
|
115
116
|
},
|
|
116
117
|
async (args) =>
|
|
117
118
|
startInBackground({
|
|
118
|
-
scenariosPath:
|
|
119
|
+
scenariosPath: args.scenariosPaths,
|
|
119
120
|
port: args.port,
|
|
120
121
|
coverageFile: args.coverageFile,
|
|
121
122
|
}),
|
|
@@ -287,20 +288,30 @@ async function main() {
|
|
|
287
288
|
array: true,
|
|
288
289
|
demandOption: true,
|
|
289
290
|
})
|
|
291
|
+
.option("setName", {
|
|
292
|
+
type: "string",
|
|
293
|
+
description: "Set used to generate the manifest.",
|
|
294
|
+
array: true,
|
|
295
|
+
demandOption: true,
|
|
296
|
+
})
|
|
290
297
|
.option("storageAccountName", {
|
|
291
298
|
type: "string",
|
|
292
299
|
description: "Name of the storage account",
|
|
293
300
|
})
|
|
301
|
+
.option("containerName", {
|
|
302
|
+
type: "string",
|
|
303
|
+
description: "Name of the Container",
|
|
304
|
+
demandOption: true,
|
|
305
|
+
})
|
|
294
306
|
.demandOption("storageAccountName");
|
|
295
307
|
},
|
|
296
308
|
async (args) => {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
309
|
+
await uploadScenarioManifest({
|
|
310
|
+
scenariosPaths: args.scenariosPaths,
|
|
311
|
+
storageAccountName: args.storageAccountName,
|
|
312
|
+
setNames: args.setName,
|
|
313
|
+
containerName: args.containerName,
|
|
314
|
+
});
|
|
304
315
|
},
|
|
305
316
|
)
|
|
306
317
|
.command(
|
|
@@ -339,6 +350,11 @@ async function main() {
|
|
|
339
350
|
type: "string",
|
|
340
351
|
description: "Mode of generator to upload.",
|
|
341
352
|
})
|
|
353
|
+
.option("containerName", {
|
|
354
|
+
type: "string",
|
|
355
|
+
description: "Name of the Container",
|
|
356
|
+
demandOption: true,
|
|
357
|
+
})
|
|
342
358
|
.demandOption("generatorMode");
|
|
343
359
|
},
|
|
344
360
|
async (args) => {
|
|
@@ -349,6 +365,7 @@ async function main() {
|
|
|
349
365
|
generatorVersion: args.generatorVersion,
|
|
350
366
|
generatorCommit: args.generatorCommit ?? getCommit(process.cwd()),
|
|
351
367
|
generatorMode: args.generatorMode,
|
|
368
|
+
containerName: args.containerName,
|
|
352
369
|
});
|
|
353
370
|
},
|
|
354
371
|
)
|
package/src/coverage/common.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { loadScenarios } from "../scenarios-resolver.js";
|
|
2
2
|
import { Diagnostic } from "../utils/diagnostic-reporter.js";
|
|
3
3
|
import { getCommit, getPackageJson } from "../utils/misc-utils.js";
|
|
4
|
-
import { ScenarioLocation, ScenarioManifest
|
|
4
|
+
import { ScenarioLocation, ScenarioManifest } from "@typespec/spec-coverage-sdk";
|
|
5
5
|
import { getSourceLocation, normalizePath } from "@typespec/compiler";
|
|
6
6
|
import { relative } from "path";
|
|
7
7
|
import type { Scenario } from "../lib/decorators.js";
|
|
8
8
|
|
|
9
9
|
export async function computeScenarioManifest(
|
|
10
10
|
scenariosPath: string,
|
|
11
|
+
setName: string
|
|
11
12
|
): Promise<[ScenarioManifest | undefined, readonly Diagnostic[]]> {
|
|
12
13
|
const [scenarios, diagnostics] = await loadScenarios(scenariosPath);
|
|
13
14
|
if (diagnostics.length > 0) {
|
|
@@ -16,7 +17,7 @@ export async function computeScenarioManifest(
|
|
|
16
17
|
|
|
17
18
|
const commit = getCommit(scenariosPath);
|
|
18
19
|
const pkg = await getPackageJson(scenariosPath);
|
|
19
|
-
return [createScenarioManifest(scenariosPath, pkg?.version ?? "?", commit, scenarios), []];
|
|
20
|
+
return [createScenarioManifest(scenariosPath, pkg?.version ?? "?", commit, scenarios, setName), []];
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export function createScenarioManifest(
|
|
@@ -24,6 +25,7 @@ export function createScenarioManifest(
|
|
|
24
25
|
version: string,
|
|
25
26
|
commit: string,
|
|
26
27
|
scenarios: Scenario[],
|
|
28
|
+
setName: string
|
|
27
29
|
): ScenarioManifest {
|
|
28
30
|
const sortedScenarios = [...scenarios].sort((a, b) => a.name.localeCompare(b.name));
|
|
29
31
|
return {
|
|
@@ -38,6 +40,6 @@ export function createScenarioManifest(
|
|
|
38
40
|
};
|
|
39
41
|
return { name, scenarioDoc, location };
|
|
40
42
|
}),
|
|
41
|
-
|
|
43
|
+
setName
|
|
42
44
|
};
|
|
43
45
|
}
|