@typespec/emitter-framework 0.9.0-dev.8 → 0.10.0-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 (33) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/src/testing/scenario-test/code-block-expectation.d.ts +33 -0
  3. package/dist/src/testing/scenario-test/code-block-expectation.d.ts.map +1 -0
  4. package/dist/src/testing/scenario-test/code-block-expectation.js +69 -0
  5. package/dist/src/testing/scenario-test/code-block-expectation.test.d.ts +2 -0
  6. package/dist/src/testing/scenario-test/code-block-expectation.test.d.ts.map +1 -0
  7. package/dist/src/testing/scenario-test/code-block-expectation.test.js +80 -0
  8. package/dist/src/testing/scenario-test/harness.d.ts +2 -2
  9. package/dist/src/testing/scenario-test/harness.d.ts.map +1 -1
  10. package/dist/src/testing/scenario-test/harness.js +69 -158
  11. package/dist/src/testing/scenario-test/index.d.ts +0 -1
  12. package/dist/src/testing/scenario-test/index.d.ts.map +1 -1
  13. package/dist/src/testing/scenario-test/index.js +1 -2
  14. package/dist/src/testing/scenario-test/snippet-extractor.d.ts +1 -1
  15. package/dist/src/testing/scenario-test/snippet-extractor.js +1 -1
  16. package/dist/test/testing/snippet-extractor-csharp.test.js +3 -3
  17. package/dist/test/testing/snippet-extractor-java.test.js +3 -3
  18. package/dist/test/testing/snippet-extractor-python.test.js +2 -2
  19. package/dist/test/testing/snippet-extractor-typescript.test.js +3 -3
  20. package/package.json +20 -12
  21. package/src/testing/scenario-test/code-block-expectation.test.ts +95 -0
  22. package/src/testing/scenario-test/code-block-expectation.ts +115 -0
  23. package/src/testing/scenario-test/harness.ts +91 -236
  24. package/src/testing/scenario-test/index.ts +0 -1
  25. package/src/testing/scenario-test/snippet-extractor.ts +1 -1
  26. package/test/testing/snippet-extractor-csharp.test.ts +3 -3
  27. package/test/testing/snippet-extractor-java.test.ts +3 -3
  28. package/test/testing/snippet-extractor-python.test.ts +2 -2
  29. package/test/testing/snippet-extractor-typescript.test.ts +3 -3
  30. package/dist/src/testing/scenario-test/test-host.d.ts +0 -8
  31. package/dist/src/testing/scenario-test/test-host.d.ts.map +0 -1
  32. package/dist/src/testing/scenario-test/test-host.js +0 -49
  33. package/src/testing/scenario-test/test-host.ts +0 -83
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog - @typespec/emitter-framework
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Features
6
+
7
+ - [#7566](https://github.com/microsoft/typespec/pull/7566) Add csharp support: class, enum
8
+ - [#7815](https://github.com/microsoft/typespec/pull/7815) Migrate the scenario tester to use the new tester framework(Remove explicit relation with http/rest libraries in scenario framework)
9
+ - [#7655](https://github.com/microsoft/typespec/pull/7655) [C#] Add support for JsonPropertyName attributes on properties
10
+ - [#7655](https://github.com/microsoft/typespec/pull/7655) [C#] Add support for nullable properties
11
+
12
+ ### Bump dependencies
13
+
14
+ - [#7655](https://github.com/microsoft/typespec/pull/7655) Upgrade to alloy 0.18.0
15
+
16
+ ### Bug Fixes
17
+
18
+ - [#7650](https://github.com/microsoft/typespec/pull/7650) Adds subpath export for csharp emitter-framework components
19
+
20
+
3
21
  ## 0.8.0
4
22
 
5
23
  ### Features
@@ -0,0 +1,33 @@
1
+ import type { SnippetExtractor } from "./snippet-extractor.js";
2
+ export interface ElementQuery {
3
+ /** Type to query */
4
+ type: string;
5
+ /** Name of the type to query */
6
+ name: string;
7
+ }
8
+ export interface CodeBlockQuery {
9
+ /** Language */
10
+ lang: string;
11
+ /** File path */
12
+ file: string;
13
+ /** Query for content in the file */
14
+ query?: ElementQuery;
15
+ }
16
+ export interface CodeBlockExpectation extends CodeBlockQuery {
17
+ /** Expected content of the code block */
18
+ expected: string;
19
+ }
20
+ /**
21
+ * Parse a markdown code block following the expectation syntax
22
+ */
23
+ export declare function parseCodeblockExpectation(heading: string, content: string): CodeBlockExpectation;
24
+ /**
25
+ * Parse the codeblock heading for what it should validate.
26
+ * Expected format
27
+ * ```
28
+ * ts path/to/file.ts {type} {name}
29
+ * ```
30
+ */
31
+ export declare function parseCodeBlockHeading(heading: string): CodeBlockQuery;
32
+ export declare function getExcerptForQuery(snippetExtractor: SnippetExtractor, expectation: CodeBlockQuery, outputs: Record<string, string>): string;
33
+ //# sourceMappingURL=code-block-expectation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-block-expectation.d.ts","sourceRoot":"","sources":["../../../../src/testing/scenario-test/code-block-expectation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IAEb,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,eAAe;IACf,IAAI,EAAE,MAAM,CAAC;IAEb,gBAAgB;IAChB,IAAI,EAAE,MAAM,CAAC;IAEb,oCAAoC;IACpC,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,oBAAqB,SAAQ,cAAc;IAC1D,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,oBAAoB,CAMhG;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAerE;AAED,wBAAgB,kBAAkB,CAChC,gBAAgB,EAAE,gBAAgB,EAClC,WAAW,EAAE,cAAc,EAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,MAAM,CAUR"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Parse a markdown code block following the expectation syntax
3
+ */
4
+ export function parseCodeblockExpectation(heading, content) {
5
+ const query = parseCodeBlockHeading(heading);
6
+ return {
7
+ ...query,
8
+ expected: content
9
+ };
10
+ }
11
+
12
+ /**
13
+ * Parse the codeblock heading for what it should validate.
14
+ * Expected format
15
+ * ```
16
+ * ts path/to/file.ts {type} {name}
17
+ * ```
18
+ */
19
+ export function parseCodeBlockHeading(heading) {
20
+ const [lang, file, type, name] = heading.split(" ");
21
+ if (!file) {
22
+ throw new Error(`Invalid code block heading: "${heading}". Missing file path. Expected format: "<lang> <path>"`);
23
+ }
24
+ if (type && !name) {
25
+ throw new Error(`Invalid code block heading: "${heading}". Missing name when using type. Expected format: "<lang> <path> [type] [name]"`);
26
+ }
27
+ return {
28
+ lang,
29
+ file,
30
+ query: type ? {
31
+ type,
32
+ name
33
+ } : undefined
34
+ };
35
+ }
36
+ export function getExcerptForQuery(snippetExtractor, expectation, outputs) {
37
+ const content = outputs[expectation.file];
38
+ if (!content) {
39
+ throw new Error(`File ${expectation.file} not found in emitted files:\n ${Object.keys(outputs).join("\n")}`);
40
+ }
41
+ return getExcerptInFile(snippetExtractor, expectation, content);
42
+ }
43
+ function getExcerptInFile(snippetExtractor, expectation, sourceFile) {
44
+ if (expectation.query) {
45
+ const excerpt = tryGetExcerptInFile(snippetExtractor, expectation.query, sourceFile);
46
+ if (!excerpt) {
47
+ throw new Error(`Could not find ${expectation.query.type} "${expectation.query.name}" in file "${expectation.file}".`);
48
+ }
49
+ return excerpt;
50
+ } else {
51
+ return sourceFile;
52
+ }
53
+ }
54
+ function tryGetExcerptInFile(snippetExtractor, query, sourceFile) {
55
+ switch (query.type) {
56
+ case "interface":
57
+ return snippetExtractor.getInterface(sourceFile, query.name);
58
+ case "type":
59
+ return snippetExtractor.getTypeAlias(sourceFile, query.name);
60
+ case "enum":
61
+ return snippetExtractor.getEnum(sourceFile, query.name);
62
+ case "function":
63
+ return snippetExtractor.getFunction(sourceFile, query.name);
64
+ case "class":
65
+ return snippetExtractor.getClass(sourceFile, query.name);
66
+ default:
67
+ throw new Error("Unsupported type in code block expectation: " + query.type);
68
+ }
69
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=code-block-expectation.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-block-expectation.test.d.ts","sourceRoot":"","sources":["../../../../src/testing/scenario-test/code-block-expectation.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,80 @@
1
+ import { d } from "@alloy-js/core/testing";
2
+ import { beforeAll, describe, expect, it } from "vitest";
3
+ import { getExcerptForQuery, parseCodeBlockHeading } from "./code-block-expectation.js";
4
+ import { createSnippetExtractor, createTypeScriptExtractorConfig } from "./snippet-extractor.js";
5
+ describe("parseCodeBlockHeading", () => {
6
+ it("parse whole file expectation", () => {
7
+ expect(parseCodeBlockHeading("ts path/to/file.ts")).toEqual({
8
+ lang: "ts",
9
+ file: "path/to/file.ts"
10
+ });
11
+ });
12
+ it("throws error when no file is provided", () => {
13
+ expect(() => parseCodeBlockHeading("ts")).toThrow('Invalid code block heading: "ts". Missing file path. Expected format: "<lang> <path>"');
14
+ });
15
+ it("parse parse with type and name", () => {
16
+ expect(parseCodeBlockHeading("ts path/to/file.ts interface foo")).toEqual({
17
+ lang: "ts",
18
+ file: "path/to/file.ts",
19
+ query: {
20
+ type: "interface",
21
+ name: "foo"
22
+ }
23
+ });
24
+ });
25
+ it("throws error when using type but no name is provided", () => {
26
+ expect(() => parseCodeBlockHeading("ts path/to/file.ts interface")).toThrow('Invalid code block heading: "ts path/to/file.ts interface". Missing name when using type. Expected format: "<lang> <path> [type] [name]"');
27
+ });
28
+ });
29
+ describe("getExcerptForQuery", () => {
30
+ let snippetExtractor;
31
+ beforeAll(async () => {
32
+ const tsExtractorConfig = await createTypeScriptExtractorConfig();
33
+ snippetExtractor = createSnippetExtractor(tsExtractorConfig);
34
+ });
35
+ it("gets a whole file", async () => {
36
+ const expectation = {
37
+ lang: "ts",
38
+ file: "file.ts"
39
+ };
40
+ const outputs = {
41
+ "file.ts": d`
42
+ interface bar {
43
+
44
+ }
45
+ interface foo {
46
+ bar: string;
47
+ }
48
+ `
49
+ };
50
+ const excerpt = getExcerptForQuery(snippetExtractor, expectation, outputs);
51
+ expect(excerpt).toBe(outputs["file.ts"]);
52
+ });
53
+ it("gets an interface for typescript", async () => {
54
+ const expectation = {
55
+ lang: "ts",
56
+ file: "file.ts",
57
+ query: {
58
+ type: "interface",
59
+ name: "foo"
60
+ }
61
+ };
62
+ const outputs = {
63
+ "file.ts": d`
64
+ interface bar {
65
+
66
+ }
67
+
68
+ interface foo {
69
+ bar: string;
70
+ }
71
+ `
72
+ };
73
+ const excerpt = getExcerptForQuery(snippetExtractor, expectation, outputs);
74
+ expect(excerpt).toBe(d`
75
+ interface foo {
76
+ bar: string;
77
+ }
78
+ `);
79
+ });
80
+ });
@@ -1,4 +1,4 @@
1
- import type { TypeSpecTestLibrary } from "@typespec/compiler/testing";
1
+ import { type EmitterTester } from "@typespec/compiler/testing";
2
2
  import type { LanguageConfiguration, SnippetExtractor } from "./snippet-extractor.js";
3
- export declare function executeScenarios(testLibrary: TypeSpecTestLibrary, languageConfiguration: LanguageConfiguration, scenariosLocation: string, emitterOutputDir: string, snippetExtractor: SnippetExtractor): Promise<void>;
3
+ export declare function executeScenarios(tester: EmitterTester, languageConfiguration: LanguageConfiguration, scenariosLocation: string, snippetExtractor: SnippetExtractor): Promise<void>;
4
4
  //# sourceMappingURL=harness.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../../../src/testing/scenario-test/harness.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAMtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AA+HtF,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,mBAAmB,EAChC,qBAAqB,EAAE,qBAAqB,EAC5C,iBAAiB,EAAE,MAAM,EACzB,gBAAgB,EAAE,MAAM,EACxB,gBAAgB,EAAE,gBAAgB,iBAkBnC"}
1
+ {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../../../src/testing/scenario-test/harness.ts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAUvF,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAKtF,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,aAAa,EACrB,qBAAqB,EAAE,qBAAqB,EAC5C,iBAAiB,EAAE,MAAM,EACzB,gBAAgB,EAAE,gBAAgB,iBAKnC"}
@@ -1,145 +1,49 @@
1
- import { normalizePath } from "@typespec/compiler";
1
+ import { logDiagnostics, NodeHost } from "@typespec/compiler";
2
+ import { expectDiagnosticEmpty } from "@typespec/compiler/testing";
2
3
  import { readdirSync, readFileSync, statSync, writeFileSync } from "fs";
3
- import minimist from "minimist";
4
- import path from "path";
4
+ import { join } from "pathe";
5
5
  import { format } from "prettier";
6
- import { afterAll, describe, expect, it } from "vitest";
7
- import { emitWithDiagnostics } from "./test-host.js";
8
- const rawArgs = process.env.TEST_ARGS ? process.env.TEST_ARGS.split(" ") : [];
9
-
10
- // Parse command-line arguments with minimist
11
- const args = minimist(rawArgs, {
12
- alias: {
13
- filter: "f" // Short alias for `--filter`
14
- },
15
- default: {
16
- filter: undefined // Default to undefined if no filter is provided
17
- }
18
- });
19
-
20
- // Extract the filter paths from the parsed arguments
21
- const filterPaths = args.filter ? Array.isArray(args.filter) // Handle single or multiple file paths
22
- ? args.filter : [args.filter] : undefined;
6
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
7
+ import { getExcerptForQuery, parseCodeblockExpectation } from "./code-block-expectation.js";
23
8
  const SCENARIOS_UPDATE = process.env["RECORD"] === "true" || process.env["SCENARIOS_UPDATE"] === "true";
24
- async function assertGetEmittedFile(testLibrary, emitterOutputDir, file, code) {
25
- const [emittedFiles, diagnostics] = await emitWithDiagnostics(testLibrary, emitterOutputDir, code);
26
- const errors = diagnostics.filter(d => d.severity === "error");
27
- const warnings = diagnostics.filter(d => d.severity === "warning");
28
- if (warnings.length > 0) {
29
- // eslint-disable-next-line no-console
30
- console.warn(`Warning compiling code:\n ${warnings.map(x => x.message).join("\n")}`);
31
- }
32
- if (errors.length > 0) {
33
- throw new Error(`Error compiling code:\n ${errors.map(x => x.message).join("\n")}`);
34
- }
35
- const normalizedTarget = normalizePath(file);
36
- const sourceFile = emittedFiles.find(x => normalizePath(x.path) === normalizedTarget);
37
- if (!sourceFile) {
38
- throw new Error(`File ${file} not found in emitted files:\n ${emittedFiles.map(f => f.path).join("\n")}`);
39
- }
40
- return sourceFile;
9
+ export async function executeScenarios(tester, languageConfiguration, scenariosLocation, snippetExtractor) {
10
+ const scenarioList = discoverAllScenarios(scenariosLocation);
11
+ describeScenarios(scenarioList, tester, languageConfiguration, snippetExtractor);
41
12
  }
42
-
43
- /**
44
- * Mapping of different snapshot types to how to get them.
45
- * Snapshot types can take single-word string arguments templated in curly braces {} and are otherwise regex
46
- */
47
- function getCodeBlockTypes(testLibrary, languageConfiguration, emitterOutputDir, snippetExtractor) {
48
- const languageTags = languageConfiguration.codeBlockTypes.join("|");
49
- return {
50
- // Snapshot of a particular interface named {name} in the models file
51
- [`(${languageTags}) {file} interface {name}`]: async (code, {
52
- file,
53
- name
54
- }) => {
55
- const sourceFile = await assertGetEmittedFile(testLibrary, emitterOutputDir, file, code);
56
- const snippet = snippetExtractor.getInterface(sourceFile.content, name);
57
- if (!snippet) {
58
- throw new Error(`Interface ${name} not found in ${file}`);
59
- }
60
- return snippet;
61
- },
62
- [`(${languageTags}) {file} type {name}`]: async (code, {
63
- file,
64
- name
65
- }) => {
66
- const sourceFile = await assertGetEmittedFile(testLibrary, emitterOutputDir, file, code);
67
- const snippet = snippetExtractor.getTypeAlias(sourceFile.content, name);
68
- if (!snippet) {
69
- throw new Error(`Type alias ${name} not found in ${file}`);
70
- }
71
- return snippet;
72
- },
73
- // Snapshot of a particular function named {name} in the models file
74
- [`(${languageTags}) {file} function {name}`]: async (code, {
75
- file,
76
- name
77
- }) => {
78
- const sourceFile = await assertGetEmittedFile(testLibrary, emitterOutputDir, file, code);
79
- const snippet = snippetExtractor.getFunction(sourceFile.content, name);
80
- if (!snippet) {
81
- throw new Error(`Function ${name} not found in ${file}`);
82
- }
83
- return snippet;
84
- },
85
- // Snapshot of a particular class named {name} in the models file
86
- [`(${languageTags}) {file} class {name}`]: async (code, {
87
- file,
88
- name
89
- }) => {
90
- const sourceFile = await assertGetEmittedFile(testLibrary, emitterOutputDir, file, code);
91
- const snippet = snippetExtractor.getClass(sourceFile.content, name);
92
- if (!snippet) {
93
- throw new Error(`Class ${name} not found in ${file}`);
13
+ function discoverAllScenarios(dir) {
14
+ const scenarios = [];
15
+ function recurse(current) {
16
+ const children = readdirSync(join(dir, current));
17
+ for (const child of children) {
18
+ const fullPath = join(dir, current, child);
19
+ const stat = statSync(fullPath);
20
+ if (stat.isDirectory()) {
21
+ recurse(join(current, child));
22
+ } else {
23
+ scenarios.push({
24
+ path: fullPath,
25
+ relativePath: join(current, child)
26
+ });
94
27
  }
95
- return snippet;
96
- },
97
- // Snapshot of the entire file
98
- [`(${languageTags}) {file}`]: async (code, {
99
- file
100
- }) => {
101
- const sourceFile = await assertGetEmittedFile(testLibrary, emitterOutputDir, file, code);
102
- return sourceFile.content;
103
- }
104
- };
105
- }
106
- export async function executeScenarios(testLibrary, languageConfiguration, scenariosLocation, emitterOutputDir, snippetExtractor) {
107
- const scenarioList = filterPaths ?? [];
108
- // eslint-disable-next-line no-console
109
- scenarioList.length && console.log("Filtering scenarios: ", scenarioList);
110
- if (!scenarioList.length) {
111
- // Add all scenarios.
112
- discoverAllScenarios(scenariosLocation, scenarioList);
113
- }
114
- describeScenarios(scenarioList, testLibrary, languageConfiguration, emitterOutputDir, snippetExtractor);
115
- }
116
- function discoverAllScenarios(location, scenarios) {
117
- const children = readdirSync(location);
118
- for (const child of children) {
119
- const fullPath = path.join(location, child);
120
- const stat = statSync(fullPath);
121
- if (stat.isDirectory()) {
122
- discoverAllScenarios(fullPath, scenarios);
123
- } else {
124
- scenarios.push(fullPath);
125
28
  }
126
29
  }
30
+ recurse("");
127
31
  return scenarios;
128
32
  }
129
- function parseFile(path, testLibrary, languageConfiguration, emitterOutputDir, snippetExtractor) {
33
+ function parseFile(file) {
130
34
  // Read the whole file
131
- const rawContent = readFileSync(path, {
35
+ const rawContent = readFileSync(file.path, {
132
36
  encoding: "utf-8"
133
37
  });
134
38
 
135
39
  // Split the content by H1
136
40
  const sections = splitByH1(rawContent);
137
41
  const scenarioFile = {
138
- path,
42
+ ...file,
139
43
  scenarios: []
140
44
  };
141
45
  for (const section of sections) {
142
- const scenarioContent = parseScenario(section.content, testLibrary, languageConfiguration, emitterOutputDir, snippetExtractor);
46
+ const scenarioContent = parseScenario(section.content);
143
47
  const scenario = {
144
48
  title: section.title,
145
49
  content: scenarioContent
@@ -151,47 +55,43 @@ function parseFile(path, testLibrary, languageConfiguration, emitterOutputDir, s
151
55
  function isTestCodeBlock(codeBlock) {
152
56
  return codeBlock.kind === "test";
153
57
  }
154
- function parseScenario(content, testLibrary, languageConfiguration, emitterOutputDir, snippetExtractor) {
58
+ function parseScenario(content) {
155
59
  const rawLines = content.split("\n");
156
60
  const scenario = {
157
61
  lines: [],
158
62
  specBlock: {
159
63
  kind: "spec",
160
- content: []
64
+ content: ""
161
65
  },
162
66
  testBlocks: []
163
67
  };
164
68
  let currentCodeBlock = null;
165
-
166
- // Precompute output code block types once
167
- const outputCodeBlockTypes = getCodeBlockTypes(testLibrary, languageConfiguration, emitterOutputDir, snippetExtractor);
168
69
  for (const line of rawLines) {
169
70
  if (line.startsWith("```") && currentCodeBlock) {
170
- // Close the code block
171
- scenario.lines.push(currentCodeBlock);
172
- if (!isTestCodeBlock(currentCodeBlock)) {
173
- scenario.specBlock.content = currentCodeBlock.content;
71
+ const heading = currentCodeBlock.heading;
72
+ const codeBlockKind = heading.includes("tsp") || heading.includes("typespec") ? "spec" : "test";
73
+ const content = currentCodeBlock.content.join("\n");
74
+ if (codeBlockKind === "spec") {
75
+ const codeblock = {
76
+ kind: "spec",
77
+ content
78
+ };
79
+ scenario.lines.push(codeblock);
80
+ scenario.specBlock.content = content;
174
81
  } else {
175
- for (const [template, fn] of Object.entries(outputCodeBlockTypes)) {
176
- const templateRegex = new RegExp("^" + template.replace(/\{(\w+)\}/g, "(?<$1>[^\\s]+)") + "$");
177
- const match = currentCodeBlock.heading.match(templateRegex);
178
- if (match) {
179
- currentCodeBlock.matchedTemplate = {
180
- template,
181
- fn,
182
- namedArgs: match.groups ?? null
183
- };
184
- break;
185
- }
186
- }
187
- scenario.testBlocks.push(currentCodeBlock);
82
+ const codeblock = {
83
+ kind: "test",
84
+ heading: currentCodeBlock.heading,
85
+ content,
86
+ expectation: parseCodeblockExpectation(currentCodeBlock.heading, content)
87
+ };
88
+ scenario.lines.push(codeblock);
89
+ scenario.testBlocks.push(codeblock);
188
90
  }
189
91
  currentCodeBlock = null;
190
92
  } else if (line.startsWith("```")) {
191
- const codeBlockKind = line.includes("tsp") || line.includes("typespec") ? "spec" : "test";
192
93
  // Start a new code block
193
94
  currentCodeBlock = {
194
- kind: codeBlockKind,
195
95
  heading: line.substring(3),
196
96
  content: []
197
97
  };
@@ -205,26 +105,37 @@ function parseScenario(content, testLibrary, languageConfiguration, emitterOutpu
205
105
  }
206
106
  return scenario;
207
107
  }
208
- function describeScenarios(scenarioFiles, testLibrary, languageConfiguration, emitterOutputDir, snippetExtractor) {
209
- const scenarios = scenarioFiles.map(f => parseFile(f, testLibrary, languageConfiguration, emitterOutputDir, snippetExtractor));
108
+ function describeScenarios(scenarioFiles, tester, languageConfiguration, snippetExtractor) {
109
+ const scenarios = scenarioFiles.map(f => parseFile(f));
210
110
  for (const scenarioFile of scenarios) {
211
- describe(`Scenario File: ${scenarioFile.path}`, () => {
111
+ describe(`${scenarioFile.relativePath}`, () => {
212
112
  for (const scenario of scenarioFile.scenarios) {
213
113
  const isOnly = scenario.title.includes("only:");
214
114
  const isSkip = scenario.title.includes("skip:");
215
115
  const describeFn = isSkip ? describe.skip : isOnly ? describe.only : describe;
116
+ let outputFiles;
117
+ beforeAll(async () => {
118
+ const code = scenario.content.specBlock.content;
119
+ const [{
120
+ outputs
121
+ }, diagnostics] = await tester.compileAndDiagnose(code);
122
+ const errors = diagnostics.filter(d => d.severity === "error");
123
+ const warnings = diagnostics.filter(d => d.severity === "warning");
124
+ if (warnings.length > 0) {
125
+ // TODO: this should ideally fail the test or be part of the expectation.
126
+ logDiagnostics(warnings, NodeHost.logSink);
127
+ }
128
+ expectDiagnosticEmpty(errors);
129
+ outputFiles = outputs;
130
+ });
216
131
  describeFn(`Scenario: ${scenario.title}`, () => {
217
132
  for (const testBlock of scenario.content.testBlocks) {
218
133
  it(`Test: ${testBlock.heading}`, async () => {
219
- const {
220
- fn,
221
- namedArgs
222
- } = testBlock.matchedTemplate;
223
- const result = await fn(scenario.content.specBlock.content.join("\n"), namedArgs ?? {});
134
+ const result = getExcerptForQuery(snippetExtractor, testBlock.expectation, outputFiles);
224
135
  if (SCENARIOS_UPDATE) {
225
- testBlock.content = (await languageConfiguration.format(result)).split("\n");
136
+ testBlock.content = await languageConfiguration.format(result);
226
137
  } else {
227
- const expected = await languageConfiguration.format(testBlock.content.join("\n"));
138
+ const expected = await languageConfiguration.format(testBlock.content);
228
139
  const actual = await languageConfiguration.format(result);
229
140
  expect(actual).toBe(expected);
230
141
  }
@@ -252,7 +163,7 @@ async function updateFile(scenarioFile) {
252
163
  } else {
253
164
  const heading = isTestCodeBlock(line) ? line.heading : "tsp";
254
165
  newContent.push("```" + heading);
255
- newContent.push(...line.content);
166
+ newContent.push(line.content);
256
167
  newContent.push("```");
257
168
  }
258
169
  }
@@ -1,4 +1,3 @@
1
1
  export * from "./harness.js";
2
2
  export * from "./snippet-extractor.js";
3
- export * from "./test-host.js";
4
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/testing/scenario-test/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/testing/scenario-test/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC"}
@@ -1,3 +1,2 @@
1
1
  export * from "./harness.js";
2
- export * from "./snippet-extractor.js";
3
- export * from "./test-host.js";
2
+ export * from "./snippet-extractor.js";
@@ -22,5 +22,5 @@ export interface LanguageConfiguration {
22
22
  enumNodeType?: string;
23
23
  };
24
24
  }
25
- export declare function createSnipperExtractor(languageConfiguration: LanguageConfiguration): SnippetExtractor;
25
+ export declare function createSnippetExtractor(languageConfiguration: LanguageConfiguration): SnippetExtractor;
26
26
  //# sourceMappingURL=snippet-extractor.d.ts.map
@@ -67,7 +67,7 @@ export async function createTypeScriptExtractorConfig() {
67
67
  };
68
68
  }
69
69
  await Parser.init();
70
- export function createSnipperExtractor(languageConfiguration) {
70
+ export function createSnippetExtractor(languageConfiguration) {
71
71
  return new SnippetExtractorImpl(languageConfiguration);
72
72
  }
73
73
  class SnippetExtractorImpl {
@@ -1,10 +1,10 @@
1
1
  import { d } from "@alloy-js/core/testing";
2
2
  import { beforeEach, describe, expect, it } from "vitest";
3
- import { createCSharpExtractorConfig, createSnipperExtractor } from "../../src/testing/index.js";
3
+ import { createCSharpExtractorConfig, createSnippetExtractor } from "../../src/testing/index.js";
4
4
  describe("C# Snippet Extractor", () => {
5
5
  let extractor;
6
6
  beforeEach(async () => {
7
- extractor = createSnipperExtractor(await createCSharpExtractorConfig());
7
+ extractor = createSnippetExtractor(await createCSharpExtractorConfig());
8
8
  });
9
9
  it("should extract a class", () => {
10
10
  const content = d`
@@ -53,7 +53,7 @@ describe("C# Snippet Extractor", () => {
53
53
  describe("C# Snippet Extractor - Enums", () => {
54
54
  let extractor;
55
55
  beforeEach(async () => {
56
- extractor = createSnipperExtractor(await createCSharpExtractorConfig());
56
+ extractor = createSnippetExtractor(await createCSharpExtractorConfig());
57
57
  });
58
58
  it("should extract a basic enum", async () => {
59
59
  const content = d`
@@ -1,10 +1,10 @@
1
1
  import { d } from "@alloy-js/core/testing";
2
2
  import { beforeEach, describe, expect, it } from "vitest";
3
- import { createJavaExtractorConfig, createSnipperExtractor } from "../../src/testing/index.js";
3
+ import { createJavaExtractorConfig, createSnippetExtractor } from "../../src/testing/index.js";
4
4
  describe("Java Snippet Extractor", () => {
5
5
  let extractor;
6
6
  beforeEach(async () => {
7
- extractor = createSnipperExtractor(await createJavaExtractorConfig());
7
+ extractor = createSnippetExtractor(await createJavaExtractorConfig());
8
8
  });
9
9
  it("should extract a class", () => {
10
10
  const content = d`
@@ -53,7 +53,7 @@ describe("Java Snippet Extractor", () => {
53
53
  describe("Java Snippet Extractor - Enums", () => {
54
54
  let extractor;
55
55
  beforeEach(async () => {
56
- extractor = createSnipperExtractor(await createJavaExtractorConfig());
56
+ extractor = createSnippetExtractor(await createJavaExtractorConfig());
57
57
  });
58
58
  it("should extract a basic enum", async () => {
59
59
  const content = d`
@@ -1,10 +1,10 @@
1
1
  import { d } from "@alloy-js/core/testing";
2
2
  import { beforeEach, describe, expect, it } from "vitest";
3
- import { createPythonExtractorConfig, createSnipperExtractor } from "../../src/testing/index.js";
3
+ import { createPythonExtractorConfig, createSnippetExtractor } from "../../src/testing/index.js";
4
4
  describe("Python Snippet Extractor", () => {
5
5
  let extractor;
6
6
  beforeEach(async () => {
7
- extractor = createSnipperExtractor(await createPythonExtractorConfig());
7
+ extractor = createSnippetExtractor(await createPythonExtractorConfig());
8
8
  });
9
9
  it("should extract a class", () => {
10
10
  const content = d`
@@ -1,10 +1,10 @@
1
1
  import { d } from "@alloy-js/core/testing";
2
2
  import { beforeEach, describe, expect, it } from "vitest";
3
- import { createSnipperExtractor, createTypeScriptExtractorConfig } from "../../src/testing/index.js";
3
+ import { createSnippetExtractor, createTypeScriptExtractorConfig } from "../../src/testing/index.js";
4
4
  describe("TypeScript Snippet Extractor", () => {
5
5
  let extractor;
6
6
  beforeEach(async () => {
7
- extractor = createSnipperExtractor(await createTypeScriptExtractorConfig());
7
+ extractor = createSnippetExtractor(await createTypeScriptExtractorConfig());
8
8
  });
9
9
  it("should extract a class", async () => {
10
10
  const content = d`
@@ -122,7 +122,7 @@ describe("TypeScript Snippet Extractor", () => {
122
122
  describe("TypeScript Snippet Extractor - Enums", () => {
123
123
  let extractor;
124
124
  beforeEach(async () => {
125
- extractor = createSnipperExtractor(await createTypeScriptExtractorConfig());
125
+ extractor = createSnippetExtractor(await createTypeScriptExtractorConfig());
126
126
  });
127
127
  it("should extract a basic enum", async () => {
128
128
  const content = d`