@tsonic/emitter 0.0.1
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/package.json +34 -0
- package/scripts/update-golden-tests.ts +119 -0
- package/src/adapter-generator.ts +112 -0
- package/src/array.test.ts +301 -0
- package/src/constants.ts +32 -0
- package/src/core/exports.ts +36 -0
- package/src/core/imports.test.ts +83 -0
- package/src/core/imports.ts +243 -0
- package/src/core/index.ts +9 -0
- package/src/core/module-emitter/assembly.ts +83 -0
- package/src/core/module-emitter/header.ts +19 -0
- package/src/core/module-emitter/index.ts +17 -0
- package/src/core/module-emitter/namespace.ts +39 -0
- package/src/core/module-emitter/orchestrator.ts +98 -0
- package/src/core/module-emitter/separation.ts +41 -0
- package/src/core/module-emitter/static-container.ts +75 -0
- package/src/core/module-emitter.test.ts +154 -0
- package/src/core/module-emitter.ts +6 -0
- package/src/core/module-map.ts +218 -0
- package/src/core/options.ts +16 -0
- package/src/core/type-params.ts +34 -0
- package/src/emitter-types/context.ts +91 -0
- package/src/emitter-types/core.ts +138 -0
- package/src/emitter-types/csharp-types.ts +42 -0
- package/src/emitter-types/formatting.ts +13 -0
- package/src/emitter-types/fqn.ts +81 -0
- package/src/emitter-types/index.ts +31 -0
- package/src/emitter.ts +107 -0
- package/src/expression-emitter.ts +112 -0
- package/src/expressions/access.ts +104 -0
- package/src/expressions/calls.ts +264 -0
- package/src/expressions/collections.ts +354 -0
- package/src/expressions/functions.ts +71 -0
- package/src/expressions/identifiers.ts +125 -0
- package/src/expressions/index.test.ts +515 -0
- package/src/expressions/index.ts +38 -0
- package/src/expressions/literals.ts +138 -0
- package/src/expressions/operators.ts +211 -0
- package/src/expressions/other.ts +63 -0
- package/src/generator-exchange.ts +120 -0
- package/src/generator.test.ts +193 -0
- package/src/golden-tests/config-parser.ts +67 -0
- package/src/golden-tests/discovery.ts +61 -0
- package/src/golden-tests/index.ts +10 -0
- package/src/golden-tests/registration.ts +26 -0
- package/src/golden-tests/runner.ts +131 -0
- package/src/golden-tests/tree-builder.ts +43 -0
- package/src/golden-tests/types.ts +21 -0
- package/src/golden.test.ts +40 -0
- package/src/hierarchical-bindings.test.ts +258 -0
- package/src/index.ts +14 -0
- package/src/integration.test.ts +303 -0
- package/src/specialization/call-site-rewriting.test.ts +99 -0
- package/src/specialization/collection/expressions.ts +184 -0
- package/src/specialization/collection/index.ts +7 -0
- package/src/specialization/collection/orchestrator.ts +25 -0
- package/src/specialization/collection/statements.ts +91 -0
- package/src/specialization/collection.ts +10 -0
- package/src/specialization/generation.ts +189 -0
- package/src/specialization/generic-classes.test.ts +59 -0
- package/src/specialization/generic-functions.test.ts +292 -0
- package/src/specialization/helpers.ts +39 -0
- package/src/specialization/index.ts +28 -0
- package/src/specialization/interfaces.test.ts +151 -0
- package/src/specialization/naming.ts +34 -0
- package/src/specialization/substitution.ts +186 -0
- package/src/specialization/type-aliases.test.ts +134 -0
- package/src/specialization/types.ts +19 -0
- package/src/specialization-generator.ts +23 -0
- package/src/statement-emitter.ts +117 -0
- package/src/statements/blocks.ts +115 -0
- package/src/statements/classes/helpers.ts +9 -0
- package/src/statements/classes/index.ts +13 -0
- package/src/statements/classes/inline-types.ts +79 -0
- package/src/statements/classes/members/constructors.ts +123 -0
- package/src/statements/classes/members/index.ts +8 -0
- package/src/statements/classes/members/methods.ts +137 -0
- package/src/statements/classes/members/orchestrator.ts +33 -0
- package/src/statements/classes/members/properties.ts +62 -0
- package/src/statements/classes/members.ts +6 -0
- package/src/statements/classes/parameters.ts +69 -0
- package/src/statements/classes/properties.ts +95 -0
- package/src/statements/classes.ts +14 -0
- package/src/statements/control/conditionals.ts +134 -0
- package/src/statements/control/exceptions.ts +59 -0
- package/src/statements/control/index.ts +11 -0
- package/src/statements/control/loops.ts +250 -0
- package/src/statements/control.ts +14 -0
- package/src/statements/declarations/classes.ts +89 -0
- package/src/statements/declarations/enums.ts +32 -0
- package/src/statements/declarations/functions.ts +147 -0
- package/src/statements/declarations/index.ts +10 -0
- package/src/statements/declarations/interfaces.ts +116 -0
- package/src/statements/declarations/structs.test.ts +182 -0
- package/src/statements/declarations/type-aliases.ts +104 -0
- package/src/statements/declarations/variables.ts +159 -0
- package/src/statements/declarations.ts +13 -0
- package/src/statements/index.test.ts +258 -0
- package/src/statements/index.ts +43 -0
- package/src/type-assertion.test.ts +143 -0
- package/src/type-emitter.ts +18 -0
- package/src/types/arrays.ts +21 -0
- package/src/types/dictionaries.ts +52 -0
- package/src/types/emitter.ts +76 -0
- package/src/types/functions.ts +45 -0
- package/src/types/index.test.ts +116 -0
- package/src/types/index.ts +14 -0
- package/src/types/intersections.ts +19 -0
- package/src/types/literals.ts +26 -0
- package/src/types/objects.ts +27 -0
- package/src/types/parameters.test.ts +146 -0
- package/src/types/parameters.ts +95 -0
- package/src/types/primitives.ts +24 -0
- package/src/types/references.ts +187 -0
- package/src/types/unions.test.ts +397 -0
- package/src/types/unions.ts +62 -0
- package/src/types.ts +33 -0
- package/testcases/README.md +213 -0
- package/testcases/arrays/basic/ArrayLiteral.ts +4 -0
- package/testcases/arrays/basic/config.yaml +1 -0
- package/testcases/arrays/destructuring/ArrayDestructure.ts +4 -0
- package/testcases/arrays/destructuring/config.yaml +1 -0
- package/testcases/arrays/methods/ArrayMethods.ts +6 -0
- package/testcases/arrays/methods/config.yaml +1 -0
- package/testcases/arrays/multidimensional/MultiDimensional.ts +10 -0
- package/testcases/arrays/multidimensional/config.yaml +1 -0
- package/testcases/arrays/spread/ArraySpread.ts +3 -0
- package/testcases/arrays/spread/config.yaml +1 -0
- package/testcases/async/basic/AsyncFunction.ts +5 -0
- package/testcases/async/basic/config.yaml +1 -0
- package/testcases/classes/abstract/AbstractClasses.ts +53 -0
- package/testcases/classes/abstract/config.yaml +1 -0
- package/testcases/classes/basic/Person.ts +12 -0
- package/testcases/classes/basic/config.yaml +1 -0
- package/testcases/classes/constructor/User.ts +11 -0
- package/testcases/classes/constructor/config.yaml +1 -0
- package/testcases/classes/field-inference/Counter.ts +11 -0
- package/testcases/classes/field-inference/config.yaml +1 -0
- package/testcases/classes/inheritance/Inheritance.ts +24 -0
- package/testcases/classes/inheritance/config.yaml +1 -0
- package/testcases/classes/static-members/MathHelper.ts +12 -0
- package/testcases/classes/static-members/config.yaml +1 -0
- package/testcases/control-flow/error-handling/ErrorHandling.ts +13 -0
- package/testcases/control-flow/error-handling/config.yaml +1 -0
- package/testcases/control-flow/loops/Loops.ts +21 -0
- package/testcases/control-flow/loops/config.yaml +1 -0
- package/testcases/control-flow/switch/SwitchStatement.ts +15 -0
- package/testcases/control-flow/switch/config.yaml +1 -0
- package/testcases/edge-cases/complex-expressions/ComplexExpressions.ts +10 -0
- package/testcases/edge-cases/complex-expressions/config.yaml +1 -0
- package/testcases/edge-cases/nested-scopes/NestedScopes.ts +10 -0
- package/testcases/edge-cases/nested-scopes/config.yaml +1 -0
- package/testcases/edge-cases/shadowing/Shadowing.ts +16 -0
- package/testcases/edge-cases/shadowing/config.yaml +1 -0
- package/testcases/functions/arrow/ArrowFunction.ts +5 -0
- package/testcases/functions/arrow/config.yaml +1 -0
- package/testcases/functions/basic/Greet.ts +3 -0
- package/testcases/functions/basic/config.yaml +1 -0
- package/testcases/functions/closures/Closures.ts +11 -0
- package/testcases/functions/closures/config.yaml +1 -0
- package/testcases/functions/default-params/DefaultParams.ts +7 -0
- package/testcases/functions/default-params/config.yaml +1 -0
- package/testcases/functions/rest-params/RestParams.ts +7 -0
- package/testcases/functions/rest-params/config.yaml +1 -0
- package/testcases/functions/type-guards/TypeGuards.ts +52 -0
- package/testcases/functions/type-guards/config.yaml +1 -0
- package/testcases/operators/logical/LogicalOperators.ts +11 -0
- package/testcases/operators/logical/config.yaml +1 -0
- package/testcases/operators/nullish-coalescing/NullishCoalescing.ts +7 -0
- package/testcases/operators/nullish-coalescing/config.yaml +1 -0
- package/testcases/operators/optional-chaining/OptionalChaining.ts +15 -0
- package/testcases/operators/optional-chaining/config.yaml +1 -0
- package/testcases/real-world/advanced-generics/advanced-generics.ts +116 -0
- package/testcases/real-world/advanced-generics/config.yaml +1 -0
- package/testcases/real-world/async-ops/async-ops.ts +67 -0
- package/testcases/real-world/async-ops/config.yaml +1 -0
- package/testcases/real-world/business-logic/business-logic.ts +215 -0
- package/testcases/real-world/business-logic/config.yaml +1 -0
- package/testcases/real-world/calculator/calculator.ts +29 -0
- package/testcases/real-world/calculator/config.yaml +1 -0
- package/testcases/real-world/data-structures/config.yaml +1 -0
- package/testcases/real-world/data-structures/data-structures.ts +133 -0
- package/testcases/real-world/functional/config.yaml +1 -0
- package/testcases/real-world/functional/functional.ts +116 -0
- package/testcases/real-world/shapes/config.yaml +1 -0
- package/testcases/real-world/shapes/shapes.ts +87 -0
- package/testcases/real-world/string-utils/config.yaml +1 -0
- package/testcases/real-world/string-utils/string-utils.ts +47 -0
- package/testcases/real-world/todo-list/config.yaml +1 -0
- package/testcases/real-world/todo-list/todo-list.ts +52 -0
- package/testcases/real-world/type-guards/config.yaml +1 -0
- package/testcases/real-world/type-guards/type-guards.ts +71 -0
- package/testcases/structs/basic/Point.ts +9 -0
- package/testcases/structs/basic/config.yaml +1 -0
- package/testcases/types/conditional/ConditionalTypes.ts +35 -0
- package/testcases/types/conditional/config.yaml +1 -0
- package/testcases/types/constants/ModuleConstants.ts +6 -0
- package/testcases/types/constants/config.yaml +1 -0
- package/testcases/types/generics/Generics.ts +15 -0
- package/testcases/types/generics/config.yaml +1 -0
- package/testcases/types/interfaces/Interfaces.ts +14 -0
- package/testcases/types/interfaces/config.yaml +1 -0
- package/testcases/types/mapped/MappedTypes.ts +27 -0
- package/testcases/types/mapped/config.yaml +1 -0
- package/testcases/types/tuples-intersections/TuplesAndIntersections.ts +46 -0
- package/testcases/types/tuples-intersections/config.yaml +1 -0
- package/testcases/types/unions/UnionTypes.ts +11 -0
- package/testcases/types/unions/config.yaml +1 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for generator emission
|
|
3
|
+
* Per spec/13-generators.md
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it } from "mocha";
|
|
7
|
+
import { expect } from "chai";
|
|
8
|
+
import { emitModule } from "./emitter.js";
|
|
9
|
+
import { IrModule } from "@tsonic/frontend";
|
|
10
|
+
|
|
11
|
+
describe("Generator Emission", () => {
|
|
12
|
+
it("should generate exchange class for simple generator", () => {
|
|
13
|
+
const module: IrModule = {
|
|
14
|
+
kind: "module",
|
|
15
|
+
filePath: "/test/counter.ts",
|
|
16
|
+
namespace: "Test",
|
|
17
|
+
className: "counter",
|
|
18
|
+
isStaticContainer: true,
|
|
19
|
+
imports: [],
|
|
20
|
+
exports: [],
|
|
21
|
+
body: [
|
|
22
|
+
{
|
|
23
|
+
kind: "functionDeclaration",
|
|
24
|
+
name: "counter",
|
|
25
|
+
parameters: [],
|
|
26
|
+
returnType: {
|
|
27
|
+
kind: "referenceType",
|
|
28
|
+
name: "Generator",
|
|
29
|
+
typeArguments: [
|
|
30
|
+
{ kind: "primitiveType", name: "number" },
|
|
31
|
+
{ kind: "primitiveType", name: "undefined" },
|
|
32
|
+
{ kind: "primitiveType", name: "undefined" },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
body: {
|
|
36
|
+
kind: "blockStatement",
|
|
37
|
+
statements: [
|
|
38
|
+
{
|
|
39
|
+
kind: "variableDeclaration",
|
|
40
|
+
declarationKind: "let",
|
|
41
|
+
isExported: false,
|
|
42
|
+
declarations: [
|
|
43
|
+
{
|
|
44
|
+
kind: "variableDeclarator",
|
|
45
|
+
name: { kind: "identifierPattern", name: "i" },
|
|
46
|
+
type: { kind: "primitiveType", name: "number" },
|
|
47
|
+
initializer: { kind: "literal", value: 0 },
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
kind: "whileStatement",
|
|
53
|
+
condition: { kind: "literal", value: true },
|
|
54
|
+
body: {
|
|
55
|
+
kind: "blockStatement",
|
|
56
|
+
statements: [
|
|
57
|
+
{
|
|
58
|
+
kind: "expressionStatement",
|
|
59
|
+
expression: {
|
|
60
|
+
kind: "yield",
|
|
61
|
+
expression: { kind: "identifier", name: "i" },
|
|
62
|
+
delegate: false,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
kind: "expressionStatement",
|
|
67
|
+
expression: {
|
|
68
|
+
kind: "assignment",
|
|
69
|
+
operator: "+=",
|
|
70
|
+
left: { kind: "identifier", name: "i" },
|
|
71
|
+
right: { kind: "literal", value: 1 },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
isAsync: false,
|
|
80
|
+
isGenerator: true,
|
|
81
|
+
isExported: true,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const code = emitModule(module);
|
|
87
|
+
|
|
88
|
+
// Should contain exchange class
|
|
89
|
+
expect(code).to.include("public sealed class counter_exchange");
|
|
90
|
+
expect(code).to.include("public object? Input { get; set; }");
|
|
91
|
+
expect(code).to.include("public double Output { get; set; }");
|
|
92
|
+
|
|
93
|
+
// Should have IEnumerable return type
|
|
94
|
+
expect(code).to.include(
|
|
95
|
+
"public static IEnumerable<counter_exchange> counter()"
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Should use System.Collections.Generic
|
|
99
|
+
expect(code).to.include("using System.Collections.Generic");
|
|
100
|
+
|
|
101
|
+
// Should initialize exchange variable
|
|
102
|
+
expect(code).to.include("var exchange = new counter_exchange()");
|
|
103
|
+
|
|
104
|
+
// Should emit yield with exchange object pattern
|
|
105
|
+
expect(code).to.include("exchange.Output = i");
|
|
106
|
+
expect(code).to.include("yield return exchange");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should handle async generator", () => {
|
|
110
|
+
const module: IrModule = {
|
|
111
|
+
kind: "module",
|
|
112
|
+
filePath: "/test/asyncCounter.ts",
|
|
113
|
+
namespace: "Test",
|
|
114
|
+
className: "asyncCounter",
|
|
115
|
+
isStaticContainer: true,
|
|
116
|
+
imports: [],
|
|
117
|
+
exports: [],
|
|
118
|
+
body: [
|
|
119
|
+
{
|
|
120
|
+
kind: "functionDeclaration",
|
|
121
|
+
name: "asyncCounter",
|
|
122
|
+
parameters: [],
|
|
123
|
+
returnType: {
|
|
124
|
+
kind: "referenceType",
|
|
125
|
+
name: "AsyncGenerator",
|
|
126
|
+
typeArguments: [
|
|
127
|
+
{ kind: "primitiveType", name: "number" },
|
|
128
|
+
{ kind: "primitiveType", name: "undefined" },
|
|
129
|
+
{ kind: "primitiveType", name: "undefined" },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
body: {
|
|
133
|
+
kind: "blockStatement",
|
|
134
|
+
statements: [
|
|
135
|
+
{
|
|
136
|
+
kind: "variableDeclaration",
|
|
137
|
+
declarationKind: "let",
|
|
138
|
+
isExported: false,
|
|
139
|
+
declarations: [
|
|
140
|
+
{
|
|
141
|
+
kind: "variableDeclarator",
|
|
142
|
+
name: { kind: "identifierPattern", name: "i" },
|
|
143
|
+
type: { kind: "primitiveType", name: "number" },
|
|
144
|
+
initializer: { kind: "literal", value: 0 },
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
kind: "whileStatement",
|
|
150
|
+
condition: { kind: "literal", value: true },
|
|
151
|
+
body: {
|
|
152
|
+
kind: "blockStatement",
|
|
153
|
+
statements: [
|
|
154
|
+
{
|
|
155
|
+
kind: "expressionStatement",
|
|
156
|
+
expression: {
|
|
157
|
+
kind: "yield",
|
|
158
|
+
expression: { kind: "identifier", name: "i" },
|
|
159
|
+
delegate: false,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
kind: "expressionStatement",
|
|
164
|
+
expression: {
|
|
165
|
+
kind: "assignment",
|
|
166
|
+
operator: "+=",
|
|
167
|
+
left: { kind: "identifier", name: "i" },
|
|
168
|
+
right: { kind: "literal", value: 1 },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
isAsync: true,
|
|
177
|
+
isGenerator: true,
|
|
178
|
+
isExported: true,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const code = emitModule(module);
|
|
184
|
+
|
|
185
|
+
// Should contain exchange class
|
|
186
|
+
expect(code).to.include("public sealed class asyncCounter_exchange");
|
|
187
|
+
|
|
188
|
+
// Should have IAsyncEnumerable return type with async
|
|
189
|
+
expect(code).to.include(
|
|
190
|
+
"public static async IAsyncEnumerable<asyncCounter_exchange> asyncCounter()"
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config.yaml parser for golden tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import YAML from "yaml";
|
|
6
|
+
import { TestEntry } from "./types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse config.yaml and extract test entries
|
|
10
|
+
*/
|
|
11
|
+
export const parseConfigYaml = (yamlContent: string): readonly TestEntry[] => {
|
|
12
|
+
const parsed = YAML.parse(yamlContent);
|
|
13
|
+
|
|
14
|
+
if (!Array.isArray(parsed)) {
|
|
15
|
+
throw new Error("config.yaml must be an array of test entries");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const entries: TestEntry[] = [];
|
|
19
|
+
|
|
20
|
+
for (const item of parsed) {
|
|
21
|
+
if (typeof item === "object" && item !== null) {
|
|
22
|
+
// Check if it's the simple YAML format: { "File.ts": "title" }
|
|
23
|
+
const keys = Object.keys(item);
|
|
24
|
+
|
|
25
|
+
if (keys.length === 1 && keys[0] && keys[0].endsWith(".ts")) {
|
|
26
|
+
// Simple format parsed as object
|
|
27
|
+
const input = keys[0];
|
|
28
|
+
const title = item[input];
|
|
29
|
+
|
|
30
|
+
if (typeof title !== "string") {
|
|
31
|
+
throw new Error(`Title must be a string for ${input}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
entries.push({ input, title });
|
|
35
|
+
} else if (item.input && item.title) {
|
|
36
|
+
// Explicit format: { input: "File.ts", title: "..." }
|
|
37
|
+
const input = item.input;
|
|
38
|
+
const title = item.title;
|
|
39
|
+
|
|
40
|
+
if (typeof input !== "string" || typeof title !== "string") {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"Each test entry must have 'input' and 'title' fields"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
entries.push({ input, title });
|
|
47
|
+
} else {
|
|
48
|
+
throw new Error(`Invalid test entry: ${JSON.stringify(item)}`);
|
|
49
|
+
}
|
|
50
|
+
} else if (typeof item === "string") {
|
|
51
|
+
// Quoted string format: "File.ts: title here"
|
|
52
|
+
const match = item.match(/^(\S+\.ts):\s*(.+)$/);
|
|
53
|
+
if (!match || !match[1] || !match[2]) {
|
|
54
|
+
throw new Error(`Invalid test entry format: ${item}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
entries.push({
|
|
58
|
+
input: match[1],
|
|
59
|
+
title: match[2].trim(),
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
throw new Error(`Invalid test entry: ${JSON.stringify(item)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return entries;
|
|
67
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test scenario discovery
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { Scenario } from "./types.js";
|
|
8
|
+
import { parseConfigYaml } from "./config-parser.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Discover all test scenarios by walking the testcases directory (synchronous)
|
|
12
|
+
*/
|
|
13
|
+
export const discoverScenarios = (baseDir: string): readonly Scenario[] => {
|
|
14
|
+
const scenarios: Scenario[] = [];
|
|
15
|
+
|
|
16
|
+
const walk = (dir: string, pathParts: string[]): void => {
|
|
17
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
18
|
+
|
|
19
|
+
// Check if this directory contains config.yaml
|
|
20
|
+
const hasConfig = entries.some((e) => e.name === "config.yaml");
|
|
21
|
+
|
|
22
|
+
if (hasConfig) {
|
|
23
|
+
// This is a test directory - read config
|
|
24
|
+
const configPath = path.join(dir, "config.yaml");
|
|
25
|
+
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
26
|
+
const testEntries = parseConfigYaml(configContent);
|
|
27
|
+
|
|
28
|
+
// Create scenarios for each test entry
|
|
29
|
+
for (const entry of testEntries) {
|
|
30
|
+
const inputPath = path.join(dir, entry.input);
|
|
31
|
+
const baseName = path.basename(entry.input, ".ts");
|
|
32
|
+
const expectedPath = path.join(dir, `${baseName}.cs`);
|
|
33
|
+
|
|
34
|
+
// Verify files exist
|
|
35
|
+
if (!fs.existsSync(inputPath)) {
|
|
36
|
+
throw new Error(`Input file not found: ${inputPath}`);
|
|
37
|
+
}
|
|
38
|
+
if (!fs.existsSync(expectedPath)) {
|
|
39
|
+
throw new Error(`Expected file not found: ${expectedPath}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
scenarios.push({
|
|
43
|
+
pathParts,
|
|
44
|
+
title: entry.title,
|
|
45
|
+
inputPath,
|
|
46
|
+
expectedPath,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Recurse into subdirectories
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
walk(path.join(dir, entry.name), [...pathParts, entry.name]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
walk(baseDir, []);
|
|
60
|
+
return scenarios;
|
|
61
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Golden test harness - Public API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type { TestEntry, Scenario, DescribeNode } from "./types.js";
|
|
6
|
+
export { parseConfigYaml } from "./config-parser.js";
|
|
7
|
+
export { discoverScenarios } from "./discovery.js";
|
|
8
|
+
export { buildDescribeTree } from "./tree-builder.js";
|
|
9
|
+
export { normalizeCs, runScenario } from "./runner.js";
|
|
10
|
+
export { registerNode } from "./registration.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test registration for Mocha
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it } from "mocha";
|
|
6
|
+
import { DescribeNode } from "./types.js";
|
|
7
|
+
import { runScenario } from "./runner.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Register describe blocks recursively
|
|
11
|
+
*/
|
|
12
|
+
export const registerNode = (node: DescribeNode): void => {
|
|
13
|
+
describe(node.name, () => {
|
|
14
|
+
// Register child describe blocks
|
|
15
|
+
for (const child of node.children.values()) {
|
|
16
|
+
registerNode(child);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Register tests at this level
|
|
20
|
+
for (const scenario of node.tests) {
|
|
21
|
+
it(scenario.title, async () => {
|
|
22
|
+
await runScenario(scenario);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test scenario runner
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { expect } from "chai";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { compile, buildIr } from "@tsonic/frontend";
|
|
9
|
+
import { emitCSharpFiles } from "../emitter.js";
|
|
10
|
+
import { generateFileHeader } from "../constants.js";
|
|
11
|
+
import { Scenario } from "./types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Normalize C# output for comparison
|
|
15
|
+
*/
|
|
16
|
+
export const normalizeCs = (code: string): string => {
|
|
17
|
+
return (
|
|
18
|
+
code
|
|
19
|
+
.trim()
|
|
20
|
+
// Normalize line endings
|
|
21
|
+
.replace(/\r\n/g, "\n")
|
|
22
|
+
// Remove trailing whitespace
|
|
23
|
+
.replace(/\s+$/gm, "")
|
|
24
|
+
// Normalize timestamp line (make comparison timestamp-agnostic)
|
|
25
|
+
.replace(/\/\/ Generated at: .+/, "// Generated at: TIMESTAMP")
|
|
26
|
+
// Normalize file path to just filename (strip directory path)
|
|
27
|
+
.replace(/\/\/ Generated from: .+\/([^/]+)$/, "// Generated from: $1")
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run a single test scenario
|
|
33
|
+
*/
|
|
34
|
+
export const runScenario = async (scenario: Scenario): Promise<void> => {
|
|
35
|
+
// Read expected output (without header)
|
|
36
|
+
const expectedCsBody = fs.readFileSync(scenario.expectedPath, "utf-8");
|
|
37
|
+
|
|
38
|
+
// Determine source root (parent of input file)
|
|
39
|
+
const sourceRoot = path.dirname(scenario.inputPath);
|
|
40
|
+
|
|
41
|
+
// Build namespace from path parts (case-preserved, hyphens stripped per spec)
|
|
42
|
+
// e.g., ['types', 'interfaces'] → 'TestCases.types.interfaces'
|
|
43
|
+
// Note: pathParts contains directory path only (no filename), so no slicing needed
|
|
44
|
+
const namespaceParts = scenario.pathParts.map((part) =>
|
|
45
|
+
part.replace(/-/g, "")
|
|
46
|
+
); // Strip hyphens
|
|
47
|
+
const rootNamespace = ["TestCases", ...namespaceParts].join(".");
|
|
48
|
+
|
|
49
|
+
// Step 1: Compile TypeScript → Program
|
|
50
|
+
// Use standard lib for golden tests (they don't have BCL bindings)
|
|
51
|
+
const compileResult = compile([scenario.inputPath], {
|
|
52
|
+
sourceRoot,
|
|
53
|
+
rootNamespace,
|
|
54
|
+
useStandardLib: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!compileResult.ok) {
|
|
58
|
+
// Show diagnostics if compilation failed
|
|
59
|
+
const errors = compileResult.error.diagnostics
|
|
60
|
+
.map((d) => `${d.code}: ${d.message}`)
|
|
61
|
+
.join("\n");
|
|
62
|
+
throw new Error(`Compilation failed:\n${errors}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Step 2: Build IR from Program
|
|
66
|
+
const irResult = buildIr(compileResult.value.program, {
|
|
67
|
+
sourceRoot,
|
|
68
|
+
rootNamespace,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!irResult.ok) {
|
|
72
|
+
const errors = irResult.error
|
|
73
|
+
.map((d) => `${d.code}: ${d.message}`)
|
|
74
|
+
.join("\n");
|
|
75
|
+
throw new Error(`IR build failed:\n${errors}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Step 3: Emit IR → C#
|
|
79
|
+
// Note: Don't set entryPointPath - golden tests are NOT entry points
|
|
80
|
+
const emitResult = emitCSharpFiles(irResult.value, {
|
|
81
|
+
rootNamespace,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!emitResult.ok) {
|
|
85
|
+
const errors = emitResult.errors
|
|
86
|
+
.map((d) => `${d.code}: ${d.message}`)
|
|
87
|
+
.join("\n");
|
|
88
|
+
throw new Error(`Emit failed:\n${errors}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const csharpFiles = emitResult.files;
|
|
92
|
+
|
|
93
|
+
// Find the generated file for our input
|
|
94
|
+
// The key should be the class name derived from the input file
|
|
95
|
+
const className = path.basename(scenario.inputPath, ".ts");
|
|
96
|
+
const generatedKey = Array.from(csharpFiles.keys()).find((key) =>
|
|
97
|
+
key.endsWith(`${className}.cs`)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (!generatedKey) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Could not find generated C# file for ${scenario.inputPath}. Available: ${Array.from(csharpFiles.keys()).join(", ")}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const actualCs = csharpFiles.get(generatedKey);
|
|
107
|
+
if (!actualCs) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Generated file key exists but content is missing: ${generatedKey}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Generate expected header using shared constant (with TIMESTAMP placeholder)
|
|
114
|
+
// Use just the filename for comparison (actual files may have full paths)
|
|
115
|
+
const fileName = path.basename(scenario.inputPath);
|
|
116
|
+
const expectedHeader = generateFileHeader(fileName, {
|
|
117
|
+
timestamp: "TIMESTAMP",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Combine header with expected body
|
|
121
|
+
const expectedCs = expectedHeader + expectedCsBody;
|
|
122
|
+
|
|
123
|
+
// Normalize and compare
|
|
124
|
+
const normalizedActual = normalizeCs(actualCs);
|
|
125
|
+
const normalizedExpected = normalizeCs(expectedCs);
|
|
126
|
+
|
|
127
|
+
expect(normalizedActual).to.equal(
|
|
128
|
+
normalizedExpected,
|
|
129
|
+
`C# output mismatch for ${scenario.pathParts.join("/")}`
|
|
130
|
+
);
|
|
131
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build describe tree structure for nested tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Scenario, DescribeNode } from "./types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a tree structure for nested describe blocks
|
|
9
|
+
*/
|
|
10
|
+
export const buildDescribeTree = (
|
|
11
|
+
scenarios: readonly Scenario[]
|
|
12
|
+
): DescribeNode | null => {
|
|
13
|
+
if (scenarios.length === 0) return null;
|
|
14
|
+
|
|
15
|
+
const root: DescribeNode = {
|
|
16
|
+
name: "Golden Tests",
|
|
17
|
+
children: new Map(),
|
|
18
|
+
tests: [],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
for (const scenario of scenarios) {
|
|
22
|
+
let current = root;
|
|
23
|
+
|
|
24
|
+
// Navigate/create tree nodes for each path part
|
|
25
|
+
for (const part of scenario.pathParts) {
|
|
26
|
+
let node = current.children.get(part);
|
|
27
|
+
if (!node) {
|
|
28
|
+
node = {
|
|
29
|
+
name: part,
|
|
30
|
+
children: new Map(),
|
|
31
|
+
tests: [],
|
|
32
|
+
};
|
|
33
|
+
current.children.set(part, node);
|
|
34
|
+
}
|
|
35
|
+
current = node;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Add test to the leaf node
|
|
39
|
+
current.tests.push(scenario);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return root;
|
|
43
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Golden test types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type TestEntry = {
|
|
6
|
+
readonly input: string;
|
|
7
|
+
readonly title: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type Scenario = {
|
|
11
|
+
readonly pathParts: readonly string[];
|
|
12
|
+
readonly title: string;
|
|
13
|
+
readonly inputPath: string;
|
|
14
|
+
readonly expectedPath: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type DescribeNode = {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly children: Map<string, DescribeNode>;
|
|
20
|
+
tests: Scenario[];
|
|
21
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Golden Test Harness for Tsonic Emitter
|
|
3
|
+
*
|
|
4
|
+
* Automatically discovers and runs test cases from testcases/ directory.
|
|
5
|
+
* Each directory with config.yaml defines tests:
|
|
6
|
+
* - config.yaml: List of tests (input.ts → expected output)
|
|
7
|
+
* - FileName.ts: TypeScript source
|
|
8
|
+
* - FileName.cs: Expected C# output
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import {
|
|
14
|
+
discoverScenarios,
|
|
15
|
+
buildDescribeTree,
|
|
16
|
+
registerNode,
|
|
17
|
+
} from "./golden-tests/index.js";
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
const TESTCASES_DIR = path.join(__dirname, "../testcases");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Main test suite setup (synchronous discovery for Mocha compatibility)
|
|
25
|
+
*/
|
|
26
|
+
try {
|
|
27
|
+
const scenarios = discoverScenarios(TESTCASES_DIR);
|
|
28
|
+
|
|
29
|
+
if (scenarios.length === 0) {
|
|
30
|
+
console.warn("⚠️ No golden test cases found in testcases/");
|
|
31
|
+
} else {
|
|
32
|
+
const tree = buildDescribeTree(scenarios);
|
|
33
|
+
if (tree) {
|
|
34
|
+
registerNode(tree);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("❌ Failed to setup golden tests:", error);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|