@typespec/emitter-framework 0.9.0-dev.8 → 0.9.0-dev.9
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/dist/src/testing/scenario-test/code-block-expectation.d.ts +33 -0
- package/dist/src/testing/scenario-test/code-block-expectation.d.ts.map +1 -0
- package/dist/src/testing/scenario-test/code-block-expectation.js +69 -0
- package/dist/src/testing/scenario-test/code-block-expectation.test.d.ts +2 -0
- package/dist/src/testing/scenario-test/code-block-expectation.test.d.ts.map +1 -0
- package/dist/src/testing/scenario-test/code-block-expectation.test.js +80 -0
- package/dist/src/testing/scenario-test/harness.d.ts +2 -2
- package/dist/src/testing/scenario-test/harness.d.ts.map +1 -1
- package/dist/src/testing/scenario-test/harness.js +69 -158
- package/dist/src/testing/scenario-test/index.d.ts +0 -1
- package/dist/src/testing/scenario-test/index.d.ts.map +1 -1
- package/dist/src/testing/scenario-test/index.js +1 -2
- package/dist/src/testing/scenario-test/snippet-extractor.d.ts +1 -1
- package/dist/src/testing/scenario-test/snippet-extractor.js +1 -1
- package/dist/test/testing/snippet-extractor-csharp.test.js +3 -3
- package/dist/test/testing/snippet-extractor-java.test.js +3 -3
- package/dist/test/testing/snippet-extractor-python.test.js +2 -2
- package/dist/test/testing/snippet-extractor-typescript.test.js +3 -3
- package/package.json +2 -3
- package/src/testing/scenario-test/code-block-expectation.test.ts +95 -0
- package/src/testing/scenario-test/code-block-expectation.ts +115 -0
- package/src/testing/scenario-test/harness.ts +91 -236
- package/src/testing/scenario-test/index.ts +0 -1
- package/src/testing/scenario-test/snippet-extractor.ts +1 -1
- package/test/testing/snippet-extractor-csharp.test.ts +3 -3
- package/test/testing/snippet-extractor-java.test.ts +3 -3
- package/test/testing/snippet-extractor-python.test.ts +2 -2
- package/test/testing/snippet-extractor-typescript.test.ts +3 -3
- package/dist/src/testing/scenario-test/test-host.d.ts +0 -8
- package/dist/src/testing/scenario-test/test-host.d.ts.map +0 -1
- package/dist/src/testing/scenario-test/test-host.js +0 -49
- package/src/testing/scenario-test/test-host.ts +0 -83
|
@@ -0,0 +1,95 @@
|
|
|
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 {
|
|
5
|
+
createSnippetExtractor,
|
|
6
|
+
createTypeScriptExtractorConfig,
|
|
7
|
+
type SnippetExtractor,
|
|
8
|
+
} from "./snippet-extractor.js";
|
|
9
|
+
|
|
10
|
+
describe("parseCodeBlockHeading", () => {
|
|
11
|
+
it("parse whole file expectation", () => {
|
|
12
|
+
expect(parseCodeBlockHeading("ts path/to/file.ts")).toEqual({
|
|
13
|
+
lang: "ts",
|
|
14
|
+
file: "path/to/file.ts",
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("throws error when no file is provided", () => {
|
|
19
|
+
expect(() => parseCodeBlockHeading("ts")).toThrow(
|
|
20
|
+
'Invalid code block heading: "ts". Missing file path. Expected format: "<lang> <path>"',
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("parse parse with type and name", () => {
|
|
25
|
+
expect(parseCodeBlockHeading("ts path/to/file.ts interface foo")).toEqual({
|
|
26
|
+
lang: "ts",
|
|
27
|
+
file: "path/to/file.ts",
|
|
28
|
+
query: {
|
|
29
|
+
type: "interface",
|
|
30
|
+
name: "foo",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("throws error when using type but no name is provided", () => {
|
|
36
|
+
expect(() => parseCodeBlockHeading("ts path/to/file.ts interface")).toThrow(
|
|
37
|
+
'Invalid code block heading: "ts path/to/file.ts interface". Missing name when using type. Expected format: "<lang> <path> [type] [name]"',
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("getExcerptForQuery", () => {
|
|
43
|
+
let snippetExtractor: SnippetExtractor;
|
|
44
|
+
beforeAll(async () => {
|
|
45
|
+
const tsExtractorConfig = await createTypeScriptExtractorConfig();
|
|
46
|
+
snippetExtractor = createSnippetExtractor(tsExtractorConfig);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("gets a whole file", async () => {
|
|
50
|
+
const expectation = {
|
|
51
|
+
lang: "ts",
|
|
52
|
+
file: "file.ts",
|
|
53
|
+
};
|
|
54
|
+
const outputs = {
|
|
55
|
+
"file.ts": d`
|
|
56
|
+
interface bar {
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
interface foo {
|
|
60
|
+
bar: string;
|
|
61
|
+
}
|
|
62
|
+
`,
|
|
63
|
+
};
|
|
64
|
+
const excerpt = getExcerptForQuery(snippetExtractor, expectation, outputs);
|
|
65
|
+
expect(excerpt).toBe(outputs["file.ts"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("gets an interface for typescript", async () => {
|
|
69
|
+
const expectation = {
|
|
70
|
+
lang: "ts",
|
|
71
|
+
file: "file.ts",
|
|
72
|
+
query: {
|
|
73
|
+
type: "interface",
|
|
74
|
+
name: "foo",
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const outputs = {
|
|
78
|
+
"file.ts": d`
|
|
79
|
+
interface bar {
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface foo {
|
|
84
|
+
bar: string;
|
|
85
|
+
}
|
|
86
|
+
`,
|
|
87
|
+
};
|
|
88
|
+
const excerpt = getExcerptForQuery(snippetExtractor, expectation, outputs);
|
|
89
|
+
expect(excerpt).toBe(d`
|
|
90
|
+
interface foo {
|
|
91
|
+
bar: string;
|
|
92
|
+
}
|
|
93
|
+
`);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { SnippetExtractor } from "./snippet-extractor.js";
|
|
2
|
+
|
|
3
|
+
export interface ElementQuery {
|
|
4
|
+
/** Type to query */
|
|
5
|
+
type: string;
|
|
6
|
+
|
|
7
|
+
/** Name of the type to query */
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CodeBlockQuery {
|
|
12
|
+
/** Language */
|
|
13
|
+
lang: string;
|
|
14
|
+
|
|
15
|
+
/** File path */
|
|
16
|
+
file: string;
|
|
17
|
+
|
|
18
|
+
/** Query for content in the file */
|
|
19
|
+
query?: ElementQuery;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CodeBlockExpectation extends CodeBlockQuery {
|
|
23
|
+
/** Expected content of the code block */
|
|
24
|
+
expected: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse a markdown code block following the expectation syntax
|
|
29
|
+
*/
|
|
30
|
+
export function parseCodeblockExpectation(heading: string, content: string): CodeBlockExpectation {
|
|
31
|
+
const query = parseCodeBlockHeading(heading);
|
|
32
|
+
return {
|
|
33
|
+
...query,
|
|
34
|
+
expected: content,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse the codeblock heading for what it should validate.
|
|
40
|
+
* Expected format
|
|
41
|
+
* ```
|
|
42
|
+
* ts path/to/file.ts {type} {name}
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function parseCodeBlockHeading(heading: string): CodeBlockQuery {
|
|
46
|
+
const [lang, file, type, name] = heading.split(" ");
|
|
47
|
+
if (!file) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Invalid code block heading: "${heading}". Missing file path. Expected format: "<lang> <path>"`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (type && !name) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Invalid code block heading: "${heading}". Missing name when using type. Expected format: "<lang> <path> [type] [name]"`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { lang, file, query: type ? { type, name } : undefined };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getExcerptForQuery(
|
|
63
|
+
snippetExtractor: SnippetExtractor,
|
|
64
|
+
expectation: CodeBlockQuery,
|
|
65
|
+
outputs: Record<string, string>,
|
|
66
|
+
): string {
|
|
67
|
+
const content = outputs[expectation.file];
|
|
68
|
+
|
|
69
|
+
if (!content) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`File ${expectation.file} not found in emitted files:\n ${Object.keys(outputs).join("\n")}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return getExcerptInFile(snippetExtractor, expectation, content);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getExcerptInFile(
|
|
79
|
+
snippetExtractor: SnippetExtractor,
|
|
80
|
+
expectation: CodeBlockQuery,
|
|
81
|
+
sourceFile: string,
|
|
82
|
+
): string {
|
|
83
|
+
if (expectation.query) {
|
|
84
|
+
const excerpt = tryGetExcerptInFile(snippetExtractor, expectation.query, sourceFile);
|
|
85
|
+
if (!excerpt) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Could not find ${expectation.query.type} "${expectation.query.name}" in file "${expectation.file}".`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return excerpt;
|
|
91
|
+
} else {
|
|
92
|
+
return sourceFile;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function tryGetExcerptInFile(
|
|
97
|
+
snippetExtractor: SnippetExtractor,
|
|
98
|
+
query: ElementQuery,
|
|
99
|
+
sourceFile: string,
|
|
100
|
+
): string | null {
|
|
101
|
+
switch (query.type) {
|
|
102
|
+
case "interface":
|
|
103
|
+
return snippetExtractor.getInterface(sourceFile, query.name);
|
|
104
|
+
case "type":
|
|
105
|
+
return snippetExtractor.getTypeAlias(sourceFile, query.name);
|
|
106
|
+
case "enum":
|
|
107
|
+
return snippetExtractor.getEnum(sourceFile, query.name);
|
|
108
|
+
case "function":
|
|
109
|
+
return snippetExtractor.getFunction(sourceFile, query.name);
|
|
110
|
+
case "class":
|
|
111
|
+
return snippetExtractor.getClass(sourceFile, query.name);
|
|
112
|
+
default:
|
|
113
|
+
throw new Error("Unsupported type in code block expectation: " + query.type);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -1,176 +1,59 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
1
|
+
import { logDiagnostics, NodeHost } from "@typespec/compiler";
|
|
2
|
+
import { expectDiagnosticEmpty, type EmitterTester } from "@typespec/compiler/testing";
|
|
3
3
|
import { readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
4
|
-
import
|
|
5
|
-
import path from "path";
|
|
4
|
+
import { join } from "pathe";
|
|
6
5
|
import { format } from "prettier";
|
|
7
|
-
import { afterAll, describe, expect, it } from "vitest";
|
|
6
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
getExcerptForQuery,
|
|
9
|
+
parseCodeblockExpectation,
|
|
10
|
+
type CodeBlockExpectation,
|
|
11
|
+
} from "./code-block-expectation.js";
|
|
8
12
|
import type { LanguageConfiguration, SnippetExtractor } from "./snippet-extractor.js";
|
|
9
|
-
import { emitWithDiagnostics } from "./test-host.js";
|
|
10
|
-
|
|
11
|
-
const rawArgs = process.env.TEST_ARGS ? process.env.TEST_ARGS.split(" ") : [];
|
|
12
|
-
|
|
13
|
-
// Parse command-line arguments with minimist
|
|
14
|
-
const args = minimist(rawArgs, {
|
|
15
|
-
alias: {
|
|
16
|
-
filter: "f", // Short alias for `--filter`
|
|
17
|
-
},
|
|
18
|
-
default: {
|
|
19
|
-
filter: undefined, // Default to undefined if no filter is provided
|
|
20
|
-
},
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// Extract the filter paths from the parsed arguments
|
|
24
|
-
const filterPaths = args.filter
|
|
25
|
-
? Array.isArray(args.filter) // Handle single or multiple file paths
|
|
26
|
-
? args.filter
|
|
27
|
-
: [args.filter]
|
|
28
|
-
: undefined;
|
|
29
13
|
|
|
30
14
|
const SCENARIOS_UPDATE =
|
|
31
15
|
process.env["RECORD"] === "true" || process.env["SCENARIOS_UPDATE"] === "true";
|
|
32
16
|
|
|
33
|
-
type EmitterFunction = (tsp: string, namedArgs: Record<string, string>) => Promise<string>;
|
|
34
|
-
|
|
35
|
-
async function assertGetEmittedFile(
|
|
36
|
-
testLibrary: TypeSpecTestLibrary,
|
|
37
|
-
emitterOutputDir: string,
|
|
38
|
-
file: string,
|
|
39
|
-
code: string,
|
|
40
|
-
) {
|
|
41
|
-
const [emittedFiles, diagnostics] = await emitWithDiagnostics(
|
|
42
|
-
testLibrary,
|
|
43
|
-
emitterOutputDir,
|
|
44
|
-
code,
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
48
|
-
const warnings = diagnostics.filter((d) => d.severity === "warning");
|
|
49
|
-
if (warnings.length > 0) {
|
|
50
|
-
// eslint-disable-next-line no-console
|
|
51
|
-
console.warn(`Warning compiling code:\n ${warnings.map((x) => x.message).join("\n")}`);
|
|
52
|
-
}
|
|
53
|
-
if (errors.length > 0) {
|
|
54
|
-
throw new Error(`Error compiling code:\n ${errors.map((x) => x.message).join("\n")}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const normalizedTarget = normalizePath(file);
|
|
58
|
-
const sourceFile = emittedFiles.find((x) => normalizePath(x.path) === normalizedTarget);
|
|
59
|
-
|
|
60
|
-
if (!sourceFile) {
|
|
61
|
-
throw new Error(
|
|
62
|
-
`File ${file} not found in emitted files:\n ${emittedFiles.map((f) => f.path).join("\n")}`,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
return sourceFile;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Mapping of different snapshot types to how to get them.
|
|
70
|
-
* Snapshot types can take single-word string arguments templated in curly braces {} and are otherwise regex
|
|
71
|
-
*/
|
|
72
|
-
function getCodeBlockTypes(
|
|
73
|
-
testLibrary: TypeSpecTestLibrary,
|
|
74
|
-
languageConfiguration: LanguageConfiguration,
|
|
75
|
-
emitterOutputDir: string,
|
|
76
|
-
snippetExtractor: SnippetExtractor,
|
|
77
|
-
): Record<string, EmitterFunction> {
|
|
78
|
-
const languageTags = languageConfiguration.codeBlockTypes.join("|");
|
|
79
|
-
return {
|
|
80
|
-
// Snapshot of a particular interface named {name} in the models file
|
|
81
|
-
[`(${languageTags}) {file} interface {name}`]: async (code, { file, name }) => {
|
|
82
|
-
const sourceFile = await assertGetEmittedFile(testLibrary, emitterOutputDir, file, code);
|
|
83
|
-
const snippet = snippetExtractor.getInterface(sourceFile.content, name);
|
|
84
|
-
|
|
85
|
-
if (!snippet) {
|
|
86
|
-
throw new Error(`Interface ${name} not found in ${file}`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return snippet;
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
[`(${languageTags}) {file} type {name}`]: async (code, { file, name }) => {
|
|
93
|
-
const sourceFile = await assertGetEmittedFile(testLibrary, emitterOutputDir, file, code);
|
|
94
|
-
const snippet = snippetExtractor.getTypeAlias(sourceFile.content, name);
|
|
95
|
-
|
|
96
|
-
if (!snippet) {
|
|
97
|
-
throw new Error(`Type alias ${name} not found in ${file}`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return snippet;
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
// Snapshot of a particular function named {name} in the models file
|
|
104
|
-
[`(${languageTags}) {file} function {name}`]: async (code, { file, name }) => {
|
|
105
|
-
const sourceFile = await assertGetEmittedFile(testLibrary, emitterOutputDir, file, code);
|
|
106
|
-
const snippet = snippetExtractor.getFunction(sourceFile.content, name);
|
|
107
|
-
|
|
108
|
-
if (!snippet) {
|
|
109
|
-
throw new Error(`Function ${name} not found in ${file}`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return snippet;
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
// Snapshot of a particular class named {name} in the models file
|
|
116
|
-
[`(${languageTags}) {file} class {name}`]: async (code, { file, name }) => {
|
|
117
|
-
const sourceFile = await assertGetEmittedFile(testLibrary, emitterOutputDir, file, code);
|
|
118
|
-
const snippet = snippetExtractor.getClass(sourceFile.content, name);
|
|
119
|
-
|
|
120
|
-
if (!snippet) {
|
|
121
|
-
throw new Error(`Class ${name} not found in ${file}`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return snippet;
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
// Snapshot of the entire file
|
|
128
|
-
[`(${languageTags}) {file}`]: async (code, { file }) => {
|
|
129
|
-
const sourceFile = await assertGetEmittedFile(testLibrary, emitterOutputDir, file, code);
|
|
130
|
-
return sourceFile.content;
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
17
|
export async function executeScenarios(
|
|
136
|
-
|
|
18
|
+
tester: EmitterTester,
|
|
137
19
|
languageConfiguration: LanguageConfiguration,
|
|
138
20
|
scenariosLocation: string,
|
|
139
|
-
emitterOutputDir: string,
|
|
140
21
|
snippetExtractor: SnippetExtractor,
|
|
141
22
|
) {
|
|
142
|
-
const scenarioList =
|
|
143
|
-
// eslint-disable-next-line no-console
|
|
144
|
-
scenarioList.length && console.log("Filtering scenarios: ", scenarioList);
|
|
23
|
+
const scenarioList = discoverAllScenarios(scenariosLocation);
|
|
145
24
|
|
|
146
|
-
|
|
147
|
-
// Add all scenarios.
|
|
148
|
-
discoverAllScenarios(scenariosLocation, scenarioList);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
describeScenarios(
|
|
152
|
-
scenarioList,
|
|
153
|
-
testLibrary,
|
|
154
|
-
languageConfiguration,
|
|
155
|
-
emitterOutputDir,
|
|
156
|
-
snippetExtractor,
|
|
157
|
-
);
|
|
25
|
+
describeScenarios(scenarioList, tester, languageConfiguration, snippetExtractor);
|
|
158
26
|
}
|
|
159
27
|
|
|
160
|
-
function discoverAllScenarios(
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
28
|
+
function discoverAllScenarios(dir: string): ScenarioFileId[] {
|
|
29
|
+
const scenarios: ScenarioFileId[] = [];
|
|
30
|
+
|
|
31
|
+
function recurse(current: string) {
|
|
32
|
+
const children = readdirSync(join(dir, current));
|
|
33
|
+
for (const child of children) {
|
|
34
|
+
const fullPath = join(dir, current, child);
|
|
35
|
+
const stat = statSync(fullPath);
|
|
36
|
+
if (stat.isDirectory()) {
|
|
37
|
+
recurse(join(current, child));
|
|
38
|
+
} else {
|
|
39
|
+
scenarios.push({ path: fullPath, relativePath: join(current, child) });
|
|
40
|
+
}
|
|
169
41
|
}
|
|
170
42
|
}
|
|
171
43
|
|
|
44
|
+
recurse("");
|
|
172
45
|
return scenarios;
|
|
173
46
|
}
|
|
47
|
+
|
|
48
|
+
interface ScenarioFileId {
|
|
49
|
+
path: string;
|
|
50
|
+
relativePath: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ScenarioFile extends ScenarioFileId {
|
|
54
|
+
scenarios: Scenario[];
|
|
55
|
+
}
|
|
56
|
+
|
|
174
57
|
interface Scenario {
|
|
175
58
|
// The title of the scenario delimited by H1
|
|
176
59
|
title: string;
|
|
@@ -186,54 +69,32 @@ interface ScenarioContents {
|
|
|
186
69
|
|
|
187
70
|
interface SpecCodeBlock {
|
|
188
71
|
kind: "spec" | "test";
|
|
189
|
-
content: string
|
|
72
|
+
content: string;
|
|
190
73
|
}
|
|
191
74
|
|
|
192
75
|
interface TestCodeBlock {
|
|
193
76
|
kind: "test";
|
|
194
77
|
heading: string;
|
|
195
|
-
content: string
|
|
196
|
-
|
|
197
|
-
template: string;
|
|
198
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
199
|
-
fn: Function;
|
|
200
|
-
namedArgs: Record<string, string> | null;
|
|
201
|
-
};
|
|
78
|
+
content: string;
|
|
79
|
+
expectation: CodeBlockExpectation;
|
|
202
80
|
}
|
|
203
81
|
|
|
204
82
|
type ScenarioCodeBlock = SpecCodeBlock | TestCodeBlock;
|
|
205
83
|
|
|
206
|
-
|
|
207
|
-
path: string;
|
|
208
|
-
scenarios: Scenario[];
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function parseFile(
|
|
212
|
-
path: string,
|
|
213
|
-
testLibrary: TypeSpecTestLibrary,
|
|
214
|
-
languageConfiguration: LanguageConfiguration,
|
|
215
|
-
emitterOutputDir: string,
|
|
216
|
-
snippetExtractor: SnippetExtractor,
|
|
217
|
-
): ScenarioFile {
|
|
84
|
+
function parseFile(file: ScenarioFileId): ScenarioFile {
|
|
218
85
|
// Read the whole file
|
|
219
|
-
const rawContent = readFileSync(path, { encoding: "utf-8" });
|
|
86
|
+
const rawContent = readFileSync(file.path, { encoding: "utf-8" });
|
|
220
87
|
|
|
221
88
|
// Split the content by H1
|
|
222
89
|
const sections = splitByH1(rawContent);
|
|
223
90
|
|
|
224
91
|
const scenarioFile: ScenarioFile = {
|
|
225
|
-
|
|
92
|
+
...file,
|
|
226
93
|
scenarios: [],
|
|
227
94
|
};
|
|
228
95
|
|
|
229
96
|
for (const section of sections) {
|
|
230
|
-
const scenarioContent = parseScenario(
|
|
231
|
-
section.content,
|
|
232
|
-
testLibrary,
|
|
233
|
-
languageConfiguration,
|
|
234
|
-
emitterOutputDir,
|
|
235
|
-
snippetExtractor,
|
|
236
|
-
);
|
|
97
|
+
const scenarioContent = parseScenario(section.content);
|
|
237
98
|
const scenario: Scenario = {
|
|
238
99
|
title: section.title,
|
|
239
100
|
content: scenarioContent,
|
|
@@ -249,59 +110,43 @@ function isTestCodeBlock(codeBlock: ScenarioCodeBlock): codeBlock is TestCodeBlo
|
|
|
249
110
|
return codeBlock.kind === "test";
|
|
250
111
|
}
|
|
251
112
|
|
|
252
|
-
function parseScenario(
|
|
253
|
-
content: string,
|
|
254
|
-
testLibrary: TypeSpecTestLibrary,
|
|
255
|
-
languageConfiguration: LanguageConfiguration,
|
|
256
|
-
emitterOutputDir: string,
|
|
257
|
-
snippetExtractor: SnippetExtractor,
|
|
258
|
-
): ScenarioContents {
|
|
113
|
+
function parseScenario(content: string): ScenarioContents {
|
|
259
114
|
const rawLines = content.split("\n");
|
|
260
115
|
const scenario: ScenarioContents = {
|
|
261
116
|
lines: [],
|
|
262
|
-
specBlock: { kind: "spec", content:
|
|
117
|
+
specBlock: { kind: "spec", content: "" },
|
|
263
118
|
testBlocks: [],
|
|
264
119
|
};
|
|
265
120
|
|
|
266
|
-
let currentCodeBlock:
|
|
267
|
-
|
|
268
|
-
// Precompute output code block types once
|
|
269
|
-
const outputCodeBlockTypes = getCodeBlockTypes(
|
|
270
|
-
testLibrary,
|
|
271
|
-
languageConfiguration,
|
|
272
|
-
emitterOutputDir,
|
|
273
|
-
snippetExtractor,
|
|
274
|
-
);
|
|
121
|
+
let currentCodeBlock: { heading: string; content: string[] } | null = null;
|
|
275
122
|
|
|
276
123
|
for (const line of rawLines) {
|
|
277
124
|
if (line.startsWith("```") && currentCodeBlock) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
125
|
+
const heading = currentCodeBlock.heading;
|
|
126
|
+
const codeBlockKind =
|
|
127
|
+
heading.includes("tsp") || heading.includes("typespec") ? "spec" : "test";
|
|
128
|
+
const content = currentCodeBlock.content.join("\n");
|
|
129
|
+
if (codeBlockKind === "spec") {
|
|
130
|
+
const codeblock: SpecCodeBlock = {
|
|
131
|
+
kind: "spec",
|
|
132
|
+
content,
|
|
133
|
+
};
|
|
134
|
+
scenario.lines.push(codeblock);
|
|
135
|
+
scenario.specBlock.content = content;
|
|
282
136
|
} else {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
template,
|
|
292
|
-
fn,
|
|
293
|
-
namedArgs: match.groups ?? null,
|
|
294
|
-
};
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
scenario.testBlocks.push(currentCodeBlock);
|
|
137
|
+
const codeblock: TestCodeBlock = {
|
|
138
|
+
kind: "test",
|
|
139
|
+
heading: currentCodeBlock.heading,
|
|
140
|
+
content,
|
|
141
|
+
expectation: parseCodeblockExpectation(currentCodeBlock.heading, content),
|
|
142
|
+
};
|
|
143
|
+
scenario.lines.push(codeblock);
|
|
144
|
+
scenario.testBlocks.push(codeblock);
|
|
299
145
|
}
|
|
300
146
|
currentCodeBlock = null;
|
|
301
147
|
} else if (line.startsWith("```")) {
|
|
302
|
-
const codeBlockKind = line.includes("tsp") || line.includes("typespec") ? "spec" : "test";
|
|
303
148
|
// Start a new code block
|
|
304
|
-
currentCodeBlock = {
|
|
149
|
+
currentCodeBlock = { heading: line.substring(3), content: [] };
|
|
305
150
|
} else if (currentCodeBlock) {
|
|
306
151
|
// Append to code block content
|
|
307
152
|
currentCodeBlock.content.push(line);
|
|
@@ -315,37 +160,47 @@ function parseScenario(
|
|
|
315
160
|
}
|
|
316
161
|
|
|
317
162
|
function describeScenarios(
|
|
318
|
-
scenarioFiles:
|
|
319
|
-
|
|
163
|
+
scenarioFiles: ScenarioFileId[],
|
|
164
|
+
tester: EmitterTester,
|
|
320
165
|
languageConfiguration: LanguageConfiguration,
|
|
321
|
-
emitterOutputDir: string,
|
|
322
166
|
snippetExtractor: SnippetExtractor,
|
|
323
167
|
) {
|
|
324
|
-
const scenarios = scenarioFiles.map((f) =>
|
|
325
|
-
parseFile(f, testLibrary, languageConfiguration, emitterOutputDir, snippetExtractor),
|
|
326
|
-
);
|
|
168
|
+
const scenarios = scenarioFiles.map((f) => parseFile(f));
|
|
327
169
|
|
|
328
170
|
for (const scenarioFile of scenarios) {
|
|
329
|
-
describe(
|
|
171
|
+
describe(`${scenarioFile.relativePath}`, () => {
|
|
330
172
|
for (const scenario of scenarioFile.scenarios) {
|
|
331
173
|
const isOnly = scenario.title.includes("only:");
|
|
332
174
|
const isSkip = scenario.title.includes("skip:");
|
|
333
|
-
|
|
334
175
|
const describeFn = isSkip ? describe.skip : isOnly ? describe.only : describe;
|
|
335
176
|
|
|
177
|
+
let outputFiles: Record<string, string>;
|
|
178
|
+
beforeAll(async () => {
|
|
179
|
+
const code = scenario.content.specBlock.content;
|
|
180
|
+
const [{ outputs }, diagnostics] = await tester.compileAndDiagnose(code);
|
|
181
|
+
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
182
|
+
const warnings = diagnostics.filter((d) => d.severity === "warning");
|
|
183
|
+
if (warnings.length > 0) {
|
|
184
|
+
// TODO: this should ideally fail the test or be part of the expectation.
|
|
185
|
+
logDiagnostics(warnings, NodeHost.logSink);
|
|
186
|
+
}
|
|
187
|
+
expectDiagnosticEmpty(errors);
|
|
188
|
+
outputFiles = outputs;
|
|
189
|
+
});
|
|
190
|
+
|
|
336
191
|
describeFn(`Scenario: ${scenario.title}`, () => {
|
|
337
192
|
for (const testBlock of scenario.content.testBlocks) {
|
|
338
193
|
it(`Test: ${testBlock.heading}`, async () => {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
194
|
+
const result = getExcerptForQuery(
|
|
195
|
+
snippetExtractor,
|
|
196
|
+
testBlock.expectation,
|
|
197
|
+
outputFiles,
|
|
343
198
|
);
|
|
344
199
|
|
|
345
200
|
if (SCENARIOS_UPDATE) {
|
|
346
|
-
testBlock.content =
|
|
201
|
+
testBlock.content = await languageConfiguration.format(result);
|
|
347
202
|
} else {
|
|
348
|
-
const expected = await languageConfiguration.format(testBlock.content
|
|
203
|
+
const expected = await languageConfiguration.format(testBlock.content);
|
|
349
204
|
const actual = await languageConfiguration.format(result);
|
|
350
205
|
expect(actual).toBe(expected);
|
|
351
206
|
}
|
|
@@ -375,7 +230,7 @@ async function updateFile(scenarioFile: ScenarioFile) {
|
|
|
375
230
|
} else {
|
|
376
231
|
const heading = isTestCodeBlock(line) ? line.heading : "tsp";
|
|
377
232
|
newContent.push("```" + heading);
|
|
378
|
-
newContent.push(
|
|
233
|
+
newContent.push(line.content);
|
|
379
234
|
newContent.push("```");
|
|
380
235
|
}
|
|
381
236
|
}
|
|
@@ -92,7 +92,7 @@ export interface LanguageConfiguration {
|
|
|
92
92
|
|
|
93
93
|
await Parser.init();
|
|
94
94
|
|
|
95
|
-
export function
|
|
95
|
+
export function createSnippetExtractor(
|
|
96
96
|
languageConfiguration: LanguageConfiguration,
|
|
97
97
|
): SnippetExtractor {
|
|
98
98
|
return new SnippetExtractorImpl(languageConfiguration);
|
|
@@ -2,14 +2,14 @@ import { d } from "@alloy-js/core/testing";
|
|
|
2
2
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
3
3
|
import {
|
|
4
4
|
createCSharpExtractorConfig,
|
|
5
|
-
|
|
5
|
+
createSnippetExtractor,
|
|
6
6
|
type SnippetExtractor,
|
|
7
7
|
} from "../../src/testing/index.js";
|
|
8
8
|
describe("C# Snippet Extractor", () => {
|
|
9
9
|
let extractor: SnippetExtractor;
|
|
10
10
|
|
|
11
11
|
beforeEach(async () => {
|
|
12
|
-
extractor =
|
|
12
|
+
extractor = createSnippetExtractor(await createCSharpExtractorConfig());
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
it("should extract a class", () => {
|
|
@@ -66,7 +66,7 @@ describe("C# Snippet Extractor - Enums", () => {
|
|
|
66
66
|
let extractor: SnippetExtractor;
|
|
67
67
|
|
|
68
68
|
beforeEach(async () => {
|
|
69
|
-
extractor =
|
|
69
|
+
extractor = createSnippetExtractor(await createCSharpExtractorConfig());
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
it("should extract a basic enum", async () => {
|