microsoft/typespec

Public

mirrored from https://github.com/microsoft/typespecAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e966efd30e552f4e84d5ae7224515199738ddd8e

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

packages/compiler/testing/test-host.ts

294lines · modecode

1import assert from "assert";
2import { readFile } from "fs/promises";
3import { globby } from "globby";
4import { fileURLToPath, pathToFileURL } from "url";
5import { createSourceFile, logDiagnostics, logVerboseTestOutput } from "../core/diagnostics.js";
6import { NodeHost } from "../core/node-host.js";
7import { CompilerOptions } from "../core/options.js";
8import { getAnyExtensionFromPath, resolvePath } from "../core/path-utils.js";
9import { createProgram, Program } from "../core/program.js";
10import { CompilerHost, Diagnostic, Type } from "../core/types.js";
11import { expectDiagnosticEmpty } from "./expect.js";
12import { BasicTestRunner, createTestWrapper } from "./test-utils.js";
13import {
14 CadlTestLibrary,
15 TestFileSystem,
16 TestHost,
17 TestHostConfig,
18 TestHostError,
19} from "./types.js";
20
21export function resolveVirtualPath(path: string, ...paths: string[]) {
22 // NB: We should always resolve an absolute path, and there is no absolute
23 // path that works across OSes. This ensures that we can still rely on API
24 // like pathToFileURL in tests.
25 const rootDir = process.platform === "win32" ? "Z:/test" : "/test";
26 return resolvePath(rootDir, path, ...paths);
27}
28
29function createTestCompilerHost(
30 virtualFs: Map<string, string>,
31 jsImports: Map<string, Record<string, any>>
32): CompilerHost {
33 return {
34 async readUrl(url: string) {
35 const contents = virtualFs.get(url);
36 if (contents === undefined) {
37 throw new TestHostError(`File ${url} not found.`, "ENOENT");
38 }
39 return createSourceFile(contents, url);
40 },
41 async readFile(path: string) {
42 path = resolveVirtualPath(path);
43 const contents = virtualFs.get(path);
44 if (contents === undefined) {
45 throw new TestHostError(`File ${path} not found.`, "ENOENT");
46 }
47 return createSourceFile(contents, path);
48 },
49
50 async writeFile(path: string, content: string) {
51 path = resolveVirtualPath(path);
52 virtualFs.set(path, content);
53 },
54
55 async readDir(path: string) {
56 path = resolveVirtualPath(path);
57 return [...virtualFs.keys()]
58 .filter((x) => x.startsWith(`${path}/`))
59 .map((x) => x.replace(`${path}/`, ""));
60 },
61
62 async removeDir(path: string) {
63 path = resolveVirtualPath(path);
64
65 for (const key of virtualFs.keys()) {
66 if (key.startsWith(`${path}/`)) {
67 virtualFs.delete(key);
68 }
69 }
70 },
71
72 getLibDirs() {
73 return [resolveVirtualPath(".cadl/lib"), resolveVirtualPath(".cadl/test-lib")];
74 },
75
76 getExecutionRoot() {
77 return resolveVirtualPath(".cadl");
78 },
79
80 async getJsImport(path) {
81 path = resolveVirtualPath(path);
82 const module = jsImports.get(path);
83 if (module === undefined) {
84 throw new TestHostError(`Module ${path} not found`, "ERR_MODULE_NOT_FOUND");
85 }
86 return module;
87 },
88
89 async stat(path: string) {
90 path = resolveVirtualPath(path);
91
92 if (virtualFs.has(path)) {
93 return {
94 isDirectory() {
95 return false;
96 },
97 isFile() {
98 return true;
99 },
100 };
101 }
102
103 for (const fsPath of virtualFs.keys()) {
104 if (fsPath.startsWith(path) && fsPath !== path) {
105 return {
106 isDirectory() {
107 return true;
108 },
109 isFile() {
110 return false;
111 },
112 };
113 }
114 }
115
116 throw new TestHostError(`File ${path} not found`, "ENOENT");
117 },
118
119 // symlinks not supported in test-host
120 async realpath(path) {
121 return path;
122 },
123
124 logSink: NodeHost.logSink,
125 mkdirp: async (path: string) => path,
126 fileURLToPath,
127 pathToFileURL(path: string) {
128 return pathToFileURL(path).href;
129 },
130 };
131}
132
133export async function createTestFileSystem(): Promise<TestFileSystem> {
134 const virtualFs = new Map<string, string>();
135 const jsImports = new Map<string, Promise<any>>();
136
137 const compilerHost = createTestCompilerHost(virtualFs, jsImports);
138 return {
139 addCadlFile,
140 addJsFile,
141 addRealCadlFile,
142 addRealJsFile,
143 addCadlLibrary,
144 compilerHost,
145 fs: virtualFs,
146 };
147
148 function addCadlFile(path: string, contents: string) {
149 virtualFs.set(resolveVirtualPath(path), contents);
150 }
151
152 function addJsFile(path: string, contents: any) {
153 const key = resolveVirtualPath(path);
154 virtualFs.set(key, ""); // don't need contents
155 jsImports.set(key, new Promise((r) => r(contents)));
156 }
157
158 async function addRealCadlFile(path: string, existingPath: string) {
159 virtualFs.set(resolveVirtualPath(path), await readFile(existingPath, "utf8"));
160 }
161
162 async function addRealJsFile(path: string, existingPath: string) {
163 const key = resolveVirtualPath(path);
164 const exports = await import(pathToFileURL(existingPath).href);
165
166 virtualFs.set(key, "");
167 jsImports.set(key, exports);
168 }
169
170 async function addCadlLibrary(testLibrary: CadlTestLibrary) {
171 for (const { realDir, pattern, virtualPath } of testLibrary.files) {
172 const lookupDir = resolvePath(testLibrary.packageRoot, realDir);
173 const entries = await findFilesFromPattern(lookupDir, pattern);
174 for (const entry of entries) {
175 const fileRealPath = resolvePath(lookupDir, entry);
176 const fileVirtualPath = resolveVirtualPath(virtualPath, entry);
177 switch (getAnyExtensionFromPath(fileRealPath)) {
178 case ".cadl":
179 case ".json":
180 const contents = await readFile(fileRealPath, "utf-8");
181 addCadlFile(fileVirtualPath, contents);
182 break;
183 case ".js":
184 case ".mjs":
185 await addRealJsFile(fileVirtualPath, fileRealPath);
186 break;
187 }
188 }
189 }
190 }
191}
192
193export const StandardTestLibrary: CadlTestLibrary = {
194 name: "@cadl-lang/compiler",
195 packageRoot: resolvePath(fileURLToPath(import.meta.url), "../../../"),
196 files: [
197 { virtualPath: "./.cadl/dist/lib", realDir: "./dist/lib", pattern: "*" },
198 { virtualPath: "./.cadl/lib", realDir: "./lib", pattern: "*" },
199 ],
200};
201
202export async function createTestHost(config: TestHostConfig = {}): Promise<TestHost> {
203 const testHost = await createTestHostInternal();
204 await testHost.addCadlLibrary(StandardTestLibrary);
205 if (config.libraries) {
206 for (const library of config.libraries) {
207 await testHost.addCadlLibrary(library);
208 }
209 }
210 return testHost;
211}
212
213export async function createTestRunner(): Promise<BasicTestRunner> {
214 const testHost = await createTestHost();
215 return createTestWrapper(testHost, (code) => code);
216}
217
218async function createTestHostInternal(): Promise<TestHost> {
219 let program: Program | undefined;
220 const testTypes: Record<string, Type> = {};
221 const fileSystem = await createTestFileSystem();
222
223 // add test decorators
224 fileSystem.addCadlFile(".cadl/test-lib/main.cadl", 'import "./test.js";');
225 fileSystem.addJsFile(".cadl/test-lib/test.js", {
226 $test(_: any, target: Type, name?: string) {
227 if (!name) {
228 if (
229 target.kind === "Model" ||
230 target.kind === "Namespace" ||
231 target.kind === "Enum" ||
232 target.kind === "Operation" ||
233 target.kind === "ModelProperty" ||
234 target.kind === "EnumMember" ||
235 target.kind === "Interface" ||
236 (target.kind === "Union" && !target.expression)
237 ) {
238 name = target.name!;
239 } else {
240 throw new Error("Need to specify a name for test type");
241 }
242 }
243
244 testTypes[name] = target;
245 },
246 });
247
248 return {
249 ...fileSystem,
250 compile,
251 diagnose,
252 compileAndDiagnose,
253 testTypes,
254 get program() {
255 assert(
256 program,
257 "Program cannot be accessed without calling compile, diagnose, or compileAndDiagnose."
258 );
259 return program;
260 },
261 };
262
263 async function compile(main: string, options: CompilerOptions = {}) {
264 const [testTypes, diagnostics] = await compileAndDiagnose(main, options);
265 expectDiagnosticEmpty(diagnostics);
266 return testTypes;
267 }
268
269 async function diagnose(main: string, options: CompilerOptions = {}) {
270 const [, diagnostics] = await compileAndDiagnose(main, options);
271 return diagnostics;
272 }
273
274 async function compileAndDiagnose(
275 mainFile: string,
276 options: CompilerOptions = {}
277 ): Promise<[Record<string, Type>, readonly Diagnostic[]]> {
278 if (options.noEmit === undefined) {
279 // default for tests is noEmit
280 options = { ...options, noEmit: true };
281 }
282 const p = await createProgram(fileSystem.compilerHost, mainFile, options);
283 program = p;
284 logVerboseTestOutput((log) => logDiagnostics(p.diagnostics, p.logger));
285 return [testTypes, p.diagnostics];
286 }
287}
288
289export async function findFilesFromPattern(directory: string, pattern: string): Promise<string[]> {
290 return globby(pattern, {
291 cwd: directory,
292 onlyFiles: true,
293 });
294}
295