@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/dist/src/actions/serve.js +3 -3
  3. package/dist/src/actions/serve.js.map +1 -1
  4. package/dist/src/actions/server-test.d.ts +5 -0
  5. package/dist/src/actions/server-test.d.ts.map +1 -1
  6. package/dist/src/actions/server-test.js +35 -1
  7. package/dist/src/actions/server-test.js.map +1 -1
  8. package/dist/src/actions/upload-coverage-report.d.ts +2 -1
  9. package/dist/src/actions/upload-coverage-report.d.ts.map +1 -1
  10. package/dist/src/actions/upload-coverage-report.js +6 -2
  11. package/dist/src/actions/upload-coverage-report.js.map +1 -1
  12. package/dist/src/actions/upload-scenario-manifest.d.ts +4 -2
  13. package/dist/src/actions/upload-scenario-manifest.d.ts.map +1 -1
  14. package/dist/src/actions/upload-scenario-manifest.js +17 -7
  15. package/dist/src/actions/upload-scenario-manifest.js.map +1 -1
  16. package/dist/src/cli/cli.js +28 -11
  17. package/dist/src/cli/cli.js.map +1 -1
  18. package/dist/src/coverage/common.d.ts.map +1 -1
  19. package/dist/src/coverage/common.js +1 -0
  20. package/dist/src/coverage/common.js.map +1 -1
  21. package/dist/src/coverage/scenario-manifest.d.ts +2 -2
  22. package/dist/src/coverage/scenario-manifest.d.ts.map +1 -1
  23. package/dist/src/coverage/scenario-manifest.js +4 -5
  24. package/dist/src/coverage/scenario-manifest.js.map +1 -1
  25. package/dist/src/lib/decorators.d.ts.map +1 -1
  26. package/dist/src/lib/decorators.js +0 -2
  27. package/dist/src/lib/decorators.js.map +1 -1
  28. package/dist/src/server/server.js +1 -1
  29. package/dist/src/server/server.js.map +1 -1
  30. package/dist/src/utils/body-utils.d.ts.map +1 -1
  31. package/dist/src/utils/request-utils.d.ts.map +1 -1
  32. package/docs/decorators.md +24 -0
  33. package/docs/using-spector.md +99 -0
  34. package/docs/writing-mock-apis.md +116 -0
  35. package/docs/writing-scenario-spec.md +49 -0
  36. package/package.json +19 -19
  37. package/src/actions/serve.ts +3 -3
  38. package/src/actions/server-test.ts +42 -1
  39. package/src/actions/upload-coverage-report.ts +7 -1
  40. package/src/actions/upload-scenario-manifest.ts +22 -9
  41. package/src/cli/cli.ts +28 -11
  42. package/src/coverage/common.ts +1 -0
  43. package/src/coverage/scenario-manifest.ts +5 -3
  44. package/src/lib/decorators.ts +0 -2
  45. package/src/server/server.ts +1 -1
  46. 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,YAAa,OAAO,KAAG,MACL,CAAC"}
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.1-dev.1",
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.4.1",
29
+ "@azure/identity": "~4.7.0",
30
30
  "@types/js-yaml": "^4.0.5",
31
- "@typespec/compiler": "~0.61.2 || >=0.62.0-dev <0.62.0",
32
- "@typespec/versioning": "~0.61.0 || >=0.62.0-dev <0.62.0",
33
- "@typespec/http": "~0.61.0 || >=0.62.0-dev <0.62.0",
34
- "@typespec/rest": "~0.61.0 || >=0.62.0-dev <0.62.0",
35
- "@typespec/spec-api": "~0.1.0-alpha.0 || >=0.1.0-alpha.1-dev <0.1.0-alpha.1",
36
- "@typespec/spec-coverage-sdk": "~0.1.0-alpha.0 || >=0.1.0-alpha.1-dev <0.1.0-alpha.1",
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.7.5",
38
+ "axios": "^1.8.1",
39
39
  "body-parser": "^1.20.3",
40
40
  "deep-equal": "^2.2.0",
41
- "express": "^4.21.1",
41
+ "express": "^4.21.2",
42
42
  "express-promise-router": "^4.1.1",
43
- "form-data": "^4.0.1",
44
- "globby": "~14.0.2",
45
- "jackspeak": "4.0.2",
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.0",
50
+ "picocolors": "~1.1.1",
51
51
  "source-map-support": "~0.5.21",
52
- "winston": "^3.15.0",
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.7.5",
63
- "@types/node-fetch": "^2.6.3",
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.6.3",
68
- "@typespec/tspd": "~0.46.0"
67
+ "typescript": "~5.8.2",
68
+ "@typespec/tspd": "^0.46.0"
69
69
  },
70
70
  "peerDependencies": {},
71
71
  "scripts": {
@@ -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.join(" ")
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
- await obj.executeScenario();
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, new AzureCliCredential());
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
- scenariosPath: string;
10
+ scenariosPaths: string[];
10
11
  storageAccountName: string;
12
+ setNames: string[];
13
+ containerName: string;
11
14
  }
12
15
 
13
16
  export async function uploadScenarioManifest({
14
- scenariosPath,
17
+ scenariosPaths,
15
18
  storageAccountName,
19
+ setNames,
20
+ containerName,
16
21
  }: UploadScenarioManifestConfig) {
17
- const [manifest, diagnostics] = await computeScenarioManifest(scenariosPath);
18
- if (manifest === undefined || diagnostics.length > 0) {
19
- process.exit(-1);
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
- await writeFile("manifest.json", JSON.stringify(manifest, null, 2));
23
- const client = new SpecCoverageClient(storageAccountName, new AzureCliCredential());
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(manifest);
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 <scenariosPath>",
95
+ "start <scenariosPaths..>",
96
96
  "Start the server in the background.",
97
97
  (cmd) => {
98
98
  return cmd
99
- .positional("scenariosPath", {
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: resolve(process.cwd(), args.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
- for (const scenariosPath of args.scenariosPaths) {
298
- logger.info(`Uploading scenario manifest for scenarios at ${scenariosPath}`);
299
- await uploadScenarioManifest({
300
- scenariosPath: resolve(process.cwd(), scenariosPath),
301
- storageAccountName: args.storageAccountName,
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
  )
@@ -6,5 +6,6 @@ export async function getScenarioMetadata(scenariosPath: string): Promise<Scenar
6
6
  return {
7
7
  commit: getCommit(scenariosPath),
8
8
  version: pkg?.version ?? "?",
9
+ packageName: pkg?.name ?? "@typespec/http-specs",
9
10
  };
10
11
  }
@@ -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, GeneratorMode } from "@typespec/spec-coverage-sdk";
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
- modes: GeneratorMode,
43
+ setName
42
44
  };
43
45
  }