microsoft/typespec

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
4d92e0825d81d6432d29b0fade7b161a2e2aafb7

Branches

Tags

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

Clone

HTTPS

Download ZIP

packages/compiler/testing/test-host.ts

321lines · modecode

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