@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.
- package/CHANGELOG.md +18 -0
- 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 +20 -12
- 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
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 @@
|
|
|
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
|
|
1
|
+
import { type EmitterTester } from "@typespec/compiler/testing";
|
|
2
2
|
import type { LanguageConfiguration, SnippetExtractor } from "./snippet-extractor.js";
|
|
3
|
-
export declare function executeScenarios(
|
|
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,
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
25
|
-
const
|
|
26
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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(
|
|
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
|
-
|
|
42
|
+
...file,
|
|
139
43
|
scenarios: []
|
|
140
44
|
};
|
|
141
45
|
for (const section of sections) {
|
|
142
|
-
const scenarioContent = parseScenario(section.content
|
|
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
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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,
|
|
209
|
-
const scenarios = scenarioFiles.map(f => parseFile(f
|
|
108
|
+
function describeScenarios(scenarioFiles, tester, languageConfiguration, snippetExtractor) {
|
|
109
|
+
const scenarios = scenarioFiles.map(f => parseFile(f));
|
|
210
110
|
for (const scenarioFile of scenarios) {
|
|
211
|
-
describe(
|
|
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 =
|
|
136
|
+
testBlock.content = await languageConfiguration.format(result);
|
|
226
137
|
} else {
|
|
227
|
-
const expected = await languageConfiguration.format(testBlock.content
|
|
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(
|
|
166
|
+
newContent.push(line.content);
|
|
256
167
|
newContent.push("```");
|
|
257
168
|
}
|
|
258
169
|
}
|
|
@@ -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
|
|
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"}
|
|
@@ -22,5 +22,5 @@ export interface LanguageConfiguration {
|
|
|
22
22
|
enumNodeType?: string;
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
-
export declare function
|
|
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
|
|
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,
|
|
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 =
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 =
|
|
125
|
+
extractor = createSnippetExtractor(await createTypeScriptExtractorConfig());
|
|
126
126
|
});
|
|
127
127
|
it("should extract a basic enum", async () => {
|
|
128
128
|
const content = d`
|