diff --git a/.chronus/changes/tester-v2-2025-4-14-20-23-17.md b/.chronus/changes/tester-v2-2025-4-14-20-23-17.md new file mode 100644 index 00000000000..00481206246 --- /dev/null +++ b/.chronus/changes/tester-v2-2025-4-14-20-23-17.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: feature +packages: + - "@typespec/compiler" +--- + +[API] Addition of a new testing framework. See https://typespec.io/docs/extending-typespec/testing diff --git a/.chronus/changes/tester-v2-2025-4-15-17-52-11.md b/.chronus/changes/tester-v2-2025-4-15-17-52-11.md new file mode 100644 index 00000000000..c155ee4c604 --- /dev/null +++ b/.chronus/changes/tester-v2-2025-4-15-17-52-11.md @@ -0,0 +1,15 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: internal +packages: + - "@typespec/http" + - "@typespec/openapi" + - "@typespec/openapi3" + - "@typespec/rest" + - "@typespec/sse" + - "@typespec/streams" + - "@typespec/versioning" + - "@typespec/xml" +--- + +Migrated to new tester diff --git a/packages/compiler/src/testing/fourslash.ts b/packages/compiler/src/testing/fourslash.ts new file mode 100644 index 00000000000..44c02e04494 --- /dev/null +++ b/packages/compiler/src/testing/fourslash.ts @@ -0,0 +1,26 @@ +/** + * PositionedMarker represents a marker in the code with its name and position. + */ +export interface PositionedMarker { + /** Marker name */ + readonly name: string; + /** Position of the marker */ + readonly pos: number; +} + +/** + * Extract TypeScript fourslash-style markers: /\*markerName*\/ + * @param code + * @returns an array of Marker objects with name, pos, and end + */ +export function extractMarkers(code: string): PositionedMarker[] { + const markerRegex = /\/\*([a-zA-Z0-9_]+)\*\//g; + const markers: PositionedMarker[] = []; + let match: RegExpExecArray | null; + while ((match = markerRegex.exec(code)) !== null) { + const markerName = match[1]; + const pos = markerRegex.lastIndex; + markers.push({ name: markerName, pos }); + } + return markers; +} diff --git a/packages/compiler/src/testing/fs.ts b/packages/compiler/src/testing/fs.ts new file mode 100644 index 00000000000..73a4c6f3bfb --- /dev/null +++ b/packages/compiler/src/testing/fs.ts @@ -0,0 +1,156 @@ +import { readdir, readFile, stat } from "fs/promises"; +import { join } from "path"; +import { pathToFileURL } from "url"; +import { getAnyExtensionFromPath, resolvePath } from "../core/path-utils.js"; +import { createStringMap } from "../utils/misc.js"; +import { createTestCompilerHost, TestHostOptions } from "./test-compiler-host.js"; +import { findFilesFromPattern } from "./test-host.js"; +import type { JsFile, MockFile, TestFileSystem, TypeSpecTestLibrary } from "./types.js"; + +export function resolveVirtualPath(path: string, ...paths: string[]) { + // NB: We should always resolve an absolute path, and there is no absolute + // path that works across OSes. This ensures that we can still rely on API + // like pathToFileURL in tests. + const rootDir = process.platform === "win32" ? "Z:/test" : "/test"; + return resolvePath(rootDir, path, ...paths); +} + +/** + * Constructor for various mock files. + */ +export const mockFile = { + /** Define a JS file with the given named exports */ + js: (exports: Record): JsFile => { + return { kind: "js", exports }; + }, +}; + +export function createTestFileSystem(options?: TestHostOptions): TestFileSystem { + const virtualFs = createStringMap(!!options?.caseInsensitiveFileSystem); + const jsImports = createStringMap>(!!options?.caseInsensitiveFileSystem); + return createTestFileSystemInternal(virtualFs, jsImports, options); +} + +function createTestFileSystemInternal( + virtualFs: Map, + jsImports: Map>, + options?: TestHostOptions, +): TestFileSystem { + const compilerHost = createTestCompilerHost(virtualFs, jsImports, options); + + let frozen = false; + return { + add, + addTypeSpecFile, + addJsFile, + addRealTypeSpecFile, + addRealJsFile, + addRealFolder, + addTypeSpecLibrary, + fs: virtualFs, + compilerHost, + freeze, + clone, + }; + + function assertNotFrozen() { + if (frozen) { + throw new Error("Cannot modify the file system after it has been frozen."); + } + } + + function add(path: string, contents: MockFile) { + assertNotFrozen(); + if (typeof contents === "string") { + addRaw(path, contents); + } else { + addJsFile(path, contents.exports); + } + } + + function addRaw(path: string, contents: string) { + assertNotFrozen(); + virtualFs.set(resolveVirtualPath(path), contents); + } + + function addJsFile(path: string, contents: Record) { + assertNotFrozen(); + + const key = resolveVirtualPath(path); + virtualFs.set(key, ""); // don't need contents + jsImports.set(key, new Promise((r) => r(contents))); + } + + function addTypeSpecFile(path: string, contents: string) { + assertNotFrozen(); + virtualFs.set(resolveVirtualPath(path), contents); + } + + async function addRealTypeSpecFile(path: string, existingPath: string) { + assertNotFrozen(); + + virtualFs.set(resolveVirtualPath(path), await readFile(existingPath, "utf8")); + } + + async function addRealFolder(folder: string, existingFolder: string) { + assertNotFrozen(); + + const entries = await readdir(existingFolder); + for (const entry of entries) { + const existingPath = join(existingFolder, entry); + const virtualPath = join(folder, entry); + const s = await stat(existingPath); + if (s.isFile()) { + if (existingPath.endsWith(".js")) { + await addRealJsFile(virtualPath, existingPath); + } else { + await addRealTypeSpecFile(virtualPath, existingPath); + } + } + if (s.isDirectory()) { + await addRealFolder(virtualPath, existingPath); + } + } + } + + async function addRealJsFile(path: string, existingPath: string) { + assertNotFrozen(); + + const key = resolveVirtualPath(path); + const exports = await import(pathToFileURL(existingPath).href); + + virtualFs.set(key, ""); + jsImports.set(key, exports); + } + + async function addTypeSpecLibrary(testLibrary: TypeSpecTestLibrary) { + assertNotFrozen(); + + for (const { realDir, pattern, virtualPath } of testLibrary.files) { + const lookupDir = resolvePath(testLibrary.packageRoot, realDir); + const entries = await findFilesFromPattern(lookupDir, pattern); + for (const entry of entries) { + const fileRealPath = resolvePath(lookupDir, entry); + const fileVirtualPath = resolveVirtualPath(virtualPath, entry); + switch (getAnyExtensionFromPath(fileRealPath)) { + case ".tsp": + case ".json": + const contents = await readFile(fileRealPath, "utf-8"); + addTypeSpecFile(fileVirtualPath, contents); + break; + case ".js": + case ".mjs": + await addRealJsFile(fileVirtualPath, fileRealPath); + break; + } + } + } + } + function freeze() { + frozen = true; + } + + function clone() { + return createTestFileSystemInternal(new Map(virtualFs), new Map(jsImports), options); + } +} diff --git a/packages/compiler/src/testing/index.ts b/packages/compiler/src/testing/index.ts index edcd6c82f9a..80fbe8465c0 100644 --- a/packages/compiler/src/testing/index.ts +++ b/packages/compiler/src/testing/index.ts @@ -1,5 +1,12 @@ +export { + /** @deprecated Using this should be a noop. Prefer new test framework*/ + StandardTestLibrary, +} from "./test-compiler-host.js"; + export { expectCodeFixOnAst } from "./code-fix-testing.js"; export { expectDiagnosticEmpty, expectDiagnostics, type DiagnosticMatch } from "./expect.js"; +export { createTestFileSystem, mockFile } from "./fs.js"; +export { t } from "./marked-template.js"; export { createLinterRuleTester, type ApplyCodeFixExpect, @@ -7,14 +14,8 @@ export { type LinterRuleTester, } from "./rule-tester.js"; export { extractCursor, extractSquiggles } from "./source-utils.js"; -export { - StandardTestLibrary, - createTestFileSystem, - createTestHost, - createTestRunner, - findFilesFromPattern, - type TestHostOptions, -} from "./test-host.js"; +export type { TestHostOptions } from "./test-compiler-host.js"; +export { createTestHost, createTestRunner, findFilesFromPattern } from "./test-host.js"; export { createTestLibrary, createTestWrapper, @@ -24,13 +25,23 @@ export { trimBlankLines, type TestWrapperOptions, } from "./test-utils.js"; +export { createTester } from "./tester.js"; export type { BasicTestRunner, - TestFileSystem, + EmitterTester, + EmitterTesterInstance, + JsFile, + MockFile, + TestCompileOptions, + TestCompileResult, + TestEmitterCompileResult, + TestFileSystem as TestFileSystem, TestFiles, TestHost, TestHostConfig, TestHostError, + Tester, + TesterInstance, TypeSpecTestLibrary, TypeSpecTestLibraryInit, } from "./types.js"; diff --git a/packages/compiler/src/testing/marked-template.ts b/packages/compiler/src/testing/marked-template.ts new file mode 100644 index 00000000000..2f285bed9fd --- /dev/null +++ b/packages/compiler/src/testing/marked-template.ts @@ -0,0 +1,198 @@ +import type { + ArrayValue, + BooleanLiteral, + BooleanValue, + Entity, + Enum, + EnumMember, + EnumValue, + Interface, + Model, + ModelProperty, + Namespace, + NumericLiteral, + NumericValue, + ObjectValue, + Operation, + Scalar, + ScalarValue, + StringLiteral, + StringValue, + Type, + Union, + UnionVariant, + Value, +} from "../core/types.js"; + +export type Marker = T extends Type + ? TypeMarker + : T extends Value + ? ValueMarker + : never; + +export interface TypeMarker { + readonly entityKind: "Type"; + readonly kind?: T["kind"]; + readonly name: N; +} + +export interface ValueMarker { + readonly entityKind: "Value"; + readonly valueKind?: T["valueKind"]; + readonly name: N; +} + +export type MarkerConfig> = { + [K in keyof T]: Marker; +}; + +export interface TemplateWithMarkers> { + readonly isTemplateWithMarkers: true; + readonly code: string; + readonly markers: MarkerConfig; +} + +export const TemplateWithMarkers = { + is: (value: unknown): value is TemplateWithMarkers => { + return typeof value === "object" && value !== null && "isTemplateWithMarkers" in value; + }, +}; + +/** Specify that this value is dynamic and needs to be interpolated with the given keys */ +function code | string)[]>( + strings: TemplateStringsArray, + ...keys: T +): TemplateWithMarkers>> { + const markers: MarkerConfig = {}; + const result: string[] = [strings[0]]; + keys.forEach((key, i) => { + if (typeof key === "string") { + result.push(key); + } else { + result.push(`/*${key.name}*/${key.name}`); + markers[key.name] = { + entityKind: key.entityKind, + name: key.name, + kind: (key as any).kind, + valueKind: (key as any).valueKind, + }; + } + result.push(strings[i + 1]); + }); + return { + isTemplateWithMarkers: true, + code: result.join(""), + markers: markers as any, + }; +} + +function typeMarker(kind?: T["kind"]) { + return (name: N): TypeMarker => { + return { + entityKind: "Type", + kind, + name, + }; + }; +} + +function valueMarker(valueKind?: T["valueKind"]) { + return (name: N): ValueMarker => { + return { + entityKind: "Value", + valueKind, + name, + }; + }; +} + +/** TypeSpec template marker */ +export const t = { + /** + * Define a marked code block + * + * @example + * ```ts + * const code = t.code`model ${t.model("Foo")} { bar: string }`; + * ``` + */ + code: code, + + // -- Types -- + + /** Mark any type */ + type: typeMarker(), + /** Mark a model */ + model: typeMarker("Model"), + /** Mark an enum */ + enum: typeMarker("Enum"), + /** Mark an union */ + union: typeMarker("Union"), + /** Mark an interface */ + interface: typeMarker("Interface"), + /** Mark an operation */ + op: typeMarker("Operation"), + /** Mark an enum member */ + enumMember: typeMarker("EnumMember"), + /** Mark a model property */ + modelProperty: typeMarker("ModelProperty"), + /** Mark a namespace */ + namespace: typeMarker("Namespace"), + /** Mark a scalar */ + scalar: typeMarker("Scalar"), + /** Mark a union variant */ + unionVariant: typeMarker("UnionVariant"), + /** Mark a boolean literal */ + boolean: typeMarker("Boolean"), + /** Mark a number literal */ + number: typeMarker("Number"), + /** Mark a string literal */ + string: typeMarker("String"), + + // -- Values -- + + /** Mark any value */ + value: valueMarker(), + /** Mark an object value */ + object: valueMarker("ObjectValue"), + /** Mark an array value */ + array: valueMarker("ArrayValue"), + /** Mark a numeric value */ + numericValue: valueMarker("NumericValue"), + /** Mark a string value */ + stringValue: valueMarker("StringValue"), + /** Mark a boolean value */ + booleanValue: valueMarker("BooleanValue"), + /** Mark a scalar value */ + scalarValue: valueMarker("ScalarValue"), + /** Mark an enum value */ + enumValue: valueMarker("EnumValue"), +}; + +type Prettify> = { + [K in keyof T]: T[K] & Entity; +} & {}; + +type InferType = T extends Marker ? K : never; +type CollectType | string>> = { + [K in T[number] as K extends Marker ? N : never]: InferType; +}; + +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void + ? I + : never; + +type FlattenRecord> = UnionToIntersection; + +type FlattenTemplates>> = FlattenRecord<{ + [K in keyof M]: M[K] extends TemplateWithMarkers ? T : never; +}>; + +export type GetMarkedEntities< + M extends string | TemplateWithMarkers | Record>, +> = + M extends Record> + ? FlattenTemplates + : M extends string | TemplateWithMarkers + ? R + : never; diff --git a/packages/compiler/src/testing/rule-tester.ts b/packages/compiler/src/testing/rule-tester.ts index 3ec2a9f3a62..4bb3be9e2ca 100644 --- a/packages/compiler/src/testing/rule-tester.ts +++ b/packages/compiler/src/testing/rule-tester.ts @@ -11,7 +11,7 @@ import { } from "../core/types.js"; import { DiagnosticMatch, expectDiagnosticEmpty, expectDiagnostics } from "./expect.js"; import { resolveVirtualPath, trimBlankLines } from "./test-utils.js"; -import { BasicTestRunner } from "./types.js"; +import { BasicTestRunner, TesterInstance } from "./types.js"; export interface LinterRuleTester { expect(code: string): LinterRuleTestExpect; @@ -28,7 +28,7 @@ export interface ApplyCodeFixExpect { } export function createLinterRuleTester( - runner: BasicTestRunner, + runner: BasicTestRunner | TesterInstance, ruleDef: LinterRuleDefinition, libraryName: string, ): LinterRuleTester { @@ -71,7 +71,8 @@ export function createLinterRuleTester( await applyCodeFixReal(host, codefix); ok(content, "No content was written to the host."); - const offset = runner.fs.get(resolveVirtualPath("./main.tsp"))?.indexOf(code); + const fs = "keys" in runner.fs ? runner.fs : runner.fs.fs; + const offset = fs.get(resolveVirtualPath("./main.tsp"))?.indexOf(code); strictEqual(trimBlankLines(content.slice(offset)), trimBlankLines(expectedCode)); } } diff --git a/packages/compiler/src/testing/test-compiler-host.ts b/packages/compiler/src/testing/test-compiler-host.ts new file mode 100644 index 00000000000..067224cff2a --- /dev/null +++ b/packages/compiler/src/testing/test-compiler-host.ts @@ -0,0 +1,166 @@ +import { RmOptions } from "fs"; +import { fileURLToPath, pathToFileURL } from "url"; +import { CompilerPackageRoot, NodeHost } from "../core/node-host.js"; +import { createSourceFile, getSourceFileKindFromExt } from "../core/source-file.js"; +import { CompilerHost, StringLiteral, Type } from "../core/types.js"; +import { resolveVirtualPath } from "./fs.js"; +import { TestFileSystem, TestHostError, TypeSpecTestLibrary } from "./types.js"; + +export const StandardTestLibrary: TypeSpecTestLibrary = { + name: "@typespec/compiler", + packageRoot: CompilerPackageRoot, + files: [ + { virtualPath: "./.tsp/dist/src/lib", realDir: "./dist/src/lib", pattern: "**" }, + { virtualPath: "./.tsp/lib", realDir: "./lib", pattern: "**" }, + ], +}; + +export interface TestHostOptions { + caseInsensitiveFileSystem?: boolean; + excludeTestLib?: boolean; + compilerHostOverrides?: Partial; +} + +export function createTestCompilerHost( + virtualFs: Map, + jsImports: Map>, + options?: TestHostOptions, +): CompilerHost { + const libDirs = [resolveVirtualPath(".tsp/lib/std")]; + if (!options?.excludeTestLib) { + libDirs.push(resolveVirtualPath(".tsp/test-lib")); + } + + return { + async readUrl(url: string) { + const contents = virtualFs.get(url); + if (contents === undefined) { + throw new TestHostError(`File ${url} not found.`, "ENOENT"); + } + return createSourceFile(contents, url); + }, + async readFile(path: string) { + path = resolveVirtualPath(path); + const contents = virtualFs.get(path); + if (contents === undefined) { + throw new TestHostError(`File ${path} not found.`, "ENOENT"); + } + return createSourceFile(contents, path); + }, + + async writeFile(path: string, content: string) { + path = resolveVirtualPath(path); + virtualFs.set(path, content); + }, + + async readDir(path: string) { + path = resolveVirtualPath(path); + const fileFolder = [...virtualFs.keys()] + .filter((x) => x.startsWith(`${path}/`)) + .map((x) => x.replace(`${path}/`, "")) + .map((x) => { + const index = x.indexOf("/"); + return index !== -1 ? x.substring(0, index) : x; + }); + return [...new Set(fileFolder)]; + }, + + async rm(path: string, options: RmOptions) { + path = resolveVirtualPath(path); + + if (options.recursive && !virtualFs.has(path)) { + for (const key of virtualFs.keys()) { + if (key.startsWith(`${path}/`)) { + virtualFs.delete(key); + } + } + } else { + virtualFs.delete(path); + } + }, + + getLibDirs() { + return libDirs; + }, + + getExecutionRoot() { + return resolveVirtualPath(".tsp"); + }, + + async getJsImport(path) { + path = resolveVirtualPath(path); + const module = jsImports.get(path); + if (module === undefined) { + throw new TestHostError(`Module ${path} not found`, "ERR_MODULE_NOT_FOUND"); + } + return module; + }, + + async stat(path: string) { + path = resolveVirtualPath(path); + + if (virtualFs.has(path)) { + return { + isDirectory() { + return false; + }, + isFile() { + return true; + }, + }; + } + + for (const fsPath of virtualFs.keys()) { + if (fsPath.startsWith(path) && fsPath !== path) { + return { + isDirectory() { + return true; + }, + isFile() { + return false; + }, + }; + } + } + + throw new TestHostError(`File ${path} not found`, "ENOENT"); + }, + + // symlinks not supported in test-host + async realpath(path) { + return path; + }, + getSourceFileKind: getSourceFileKindFromExt, + + logSink: { log: NodeHost.logSink.log }, + mkdirp: async (path: string) => path, + fileURLToPath, + pathToFileURL(path: string) { + return pathToFileURL(path).href; + }, + + ...options?.compilerHostOverrides, + }; +} + +export function addTestLib(fs: TestFileSystem): Record { + const testTypes: Record = {}; + // add test decorators + fs.add(".tsp/test-lib/main.tsp", 'import "./test.js";'); + fs.addJsFile(".tsp/test-lib/test.js", { + namespace: "TypeSpec", + $test(_: any, target: Type, nameLiteral?: StringLiteral) { + let name = nameLiteral?.value; + if (!name) { + if ("name" in target && typeof target.name === "string") { + name = target.name; + } else { + throw new Error("Need to specify a name for test type"); + } + } + + testTypes[name] = target; + }, + }); + return testTypes; +} diff --git a/packages/compiler/src/testing/test-host.ts b/packages/compiler/src/testing/test-host.ts index eb851dad263..a62e5b33787 100644 --- a/packages/compiler/src/testing/test-host.ts +++ b/packages/compiler/src/testing/test-host.ts @@ -1,246 +1,17 @@ import assert from "assert"; -import type { RmOptions } from "fs"; -import { readdir, readFile, stat } from "fs/promises"; import { globby } from "globby"; -import { join } from "path"; -import { fileURLToPath, pathToFileURL } from "url"; import { logDiagnostics, logVerboseTestOutput } from "../core/diagnostics.js"; import { createLogger } from "../core/logger/logger.js"; -import { NodeHost } from "../core/node-host.js"; import { CompilerOptions } from "../core/options.js"; -import { getAnyExtensionFromPath, resolvePath } from "../core/path-utils.js"; import { compile as compileProgram, Program } from "../core/program.js"; -import type { CompilerHost, Diagnostic, StringLiteral, Type } from "../core/types.js"; -import { createSourceFile, getSourceFileKindFromExt } from "../index.js"; -import { createStringMap } from "../utils/misc.js"; +import type { Diagnostic, Type } from "../core/types.js"; import { expectDiagnosticEmpty } from "./expect.js"; -import { createTestWrapper, findTestPackageRoot, resolveVirtualPath } from "./test-utils.js"; -import { - BasicTestRunner, - TestFileSystem, - TestHost, - TestHostConfig, - TestHostError, - TypeSpecTestLibrary, -} from "./types.js"; - -export interface TestHostOptions { - caseInsensitiveFileSystem?: boolean; - excludeTestLib?: boolean; - compilerHostOverrides?: Partial; -} - -function createTestCompilerHost( - virtualFs: Map, - jsImports: Map>, - options?: TestHostOptions, -): CompilerHost { - const libDirs = [resolveVirtualPath(".tsp/lib/std")]; - if (!options?.excludeTestLib) { - libDirs.push(resolveVirtualPath(".tsp/test-lib")); - } - - return { - async readUrl(url: string) { - const contents = virtualFs.get(url); - if (contents === undefined) { - throw new TestHostError(`File ${url} not found.`, "ENOENT"); - } - return createSourceFile(contents, url); - }, - async readFile(path: string) { - path = resolveVirtualPath(path); - const contents = virtualFs.get(path); - if (contents === undefined) { - throw new TestHostError(`File ${path} not found.`, "ENOENT"); - } - return createSourceFile(contents, path); - }, - - async writeFile(path: string, content: string) { - path = resolveVirtualPath(path); - virtualFs.set(path, content); - }, - - async readDir(path: string) { - path = resolveVirtualPath(path); - const fileFolder = [...virtualFs.keys()] - .filter((x) => x.startsWith(`${path}/`)) - .map((x) => x.replace(`${path}/`, "")) - .map((x) => { - const index = x.indexOf("/"); - return index !== -1 ? x.substring(0, index) : x; - }); - return [...new Set(fileFolder)]; - }, - - async rm(path: string, options: RmOptions) { - path = resolveVirtualPath(path); - - if (options.recursive && !virtualFs.has(path)) { - for (const key of virtualFs.keys()) { - if (key.startsWith(`${path}/`)) { - virtualFs.delete(key); - } - } - } else { - virtualFs.delete(path); - } - }, - - getLibDirs() { - return libDirs; - }, - - getExecutionRoot() { - return resolveVirtualPath(".tsp"); - }, - - async getJsImport(path) { - path = resolveVirtualPath(path); - const module = jsImports.get(path); - if (module === undefined) { - throw new TestHostError(`Module ${path} not found`, "ERR_MODULE_NOT_FOUND"); - } - return module; - }, - - async stat(path: string) { - path = resolveVirtualPath(path); - - if (virtualFs.has(path)) { - return { - isDirectory() { - return false; - }, - isFile() { - return true; - }, - }; - } - - for (const fsPath of virtualFs.keys()) { - if (fsPath.startsWith(path) && fsPath !== path) { - return { - isDirectory() { - return true; - }, - isFile() { - return false; - }, - }; - } - } - - throw new TestHostError(`File ${path} not found`, "ENOENT"); - }, - - // symlinks not supported in test-host - async realpath(path) { - return path; - }, - getSourceFileKind: getSourceFileKindFromExt, - - logSink: { log: NodeHost.logSink.log }, - mkdirp: async (path: string) => path, - fileURLToPath, - pathToFileURL(path: string) { - return pathToFileURL(path).href; - }, - - ...options?.compilerHostOverrides, - }; -} - -export async function createTestFileSystem(options?: TestHostOptions): Promise { - const virtualFs = createStringMap(!!options?.caseInsensitiveFileSystem); - const jsImports = createStringMap>(!!options?.caseInsensitiveFileSystem); - - const compilerHost = createTestCompilerHost(virtualFs, jsImports, options); - return { - addTypeSpecFile, - addJsFile, - addRealTypeSpecFile, - addRealJsFile, - addRealFolder, - addTypeSpecLibrary, - compilerHost, - fs: virtualFs, - }; - - function addTypeSpecFile(path: string, contents: string) { - virtualFs.set(resolveVirtualPath(path), contents); - } - - function addJsFile(path: string, contents: any) { - const key = resolveVirtualPath(path); - virtualFs.set(key, ""); // don't need contents - jsImports.set(key, new Promise((r) => r(contents))); - } - - async function addRealTypeSpecFile(path: string, existingPath: string) { - virtualFs.set(resolveVirtualPath(path), await readFile(existingPath, "utf8")); - } - - async function addRealFolder(folder: string, existingFolder: string) { - const entries = await readdir(existingFolder); - for (const entry of entries) { - const existingPath = join(existingFolder, entry); - const virtualPath = join(folder, entry); - const s = await stat(existingPath); - if (s.isFile()) { - if (existingPath.endsWith(".js")) { - await addRealJsFile(virtualPath, existingPath); - } else { - await addRealTypeSpecFile(virtualPath, existingPath); - } - } - if (s.isDirectory()) { - await addRealFolder(virtualPath, existingPath); - } - } - } - - async function addRealJsFile(path: string, existingPath: string) { - const key = resolveVirtualPath(path); - const exports = await import(pathToFileURL(existingPath).href); - - virtualFs.set(key, ""); - jsImports.set(key, exports); - } - - async function addTypeSpecLibrary(testLibrary: TypeSpecTestLibrary) { - for (const { realDir, pattern, virtualPath } of testLibrary.files) { - const lookupDir = resolvePath(testLibrary.packageRoot, realDir); - const entries = await findFilesFromPattern(lookupDir, pattern); - for (const entry of entries) { - const fileRealPath = resolvePath(lookupDir, entry); - const fileVirtualPath = resolveVirtualPath(virtualPath, entry); - switch (getAnyExtensionFromPath(fileRealPath)) { - case ".tsp": - case ".json": - const contents = await readFile(fileRealPath, "utf-8"); - addTypeSpecFile(fileVirtualPath, contents); - break; - case ".js": - case ".mjs": - await addRealJsFile(fileVirtualPath, fileRealPath); - break; - } - } - } - } -} - -export const StandardTestLibrary: TypeSpecTestLibrary = { - name: "@typespec/compiler", - packageRoot: await findTestPackageRoot(import.meta.url), - files: [ - { virtualPath: "./.tsp/dist/src/lib", realDir: "./dist/src/lib", pattern: "**" }, - { virtualPath: "./.tsp/lib", realDir: "./lib", pattern: "**" }, - ], -}; +import { createTestFileSystem } from "./fs.js"; +import { addTestLib, StandardTestLibrary } from "./test-compiler-host.js"; +import { createTestWrapper, resolveVirtualPath } from "./test-utils.js"; +import { BasicTestRunner, TestHost, TestHostConfig, TypeSpecTestLibrary } from "./types.js"; +/** Use {@link createTester} */ export async function createTestHost(config: TestHostConfig = {}): Promise { const testHost = await createTestHostInternal(); await testHost.addTypeSpecLibrary(StandardTestLibrary); @@ -252,6 +23,7 @@ export async function createTestHost(config: TestHostConfig = {}): Promise { const testHost = host ?? (await createTestHost()); return createTestWrapper(testHost); @@ -260,36 +32,8 @@ export async function createTestRunner(host?: TestHost): Promise { let program: Program | undefined; const libraries: TypeSpecTestLibrary[] = []; - const testTypes: Record = {}; const fileSystem = await createTestFileSystem(); - - // add test decorators - fileSystem.addTypeSpecFile(".tsp/test-lib/main.tsp", 'import "./test.js";'); - fileSystem.addJsFile(".tsp/test-lib/test.js", { - namespace: "TypeSpec", - $test(_: any, target: Type, nameLiteral?: StringLiteral) { - let name = nameLiteral?.value; - if (!name) { - if ( - target.kind === "Model" || - target.kind === "Scalar" || - target.kind === "Namespace" || - target.kind === "Enum" || - target.kind === "Operation" || - target.kind === "ModelProperty" || - target.kind === "EnumMember" || - target.kind === "Interface" || - (target.kind === "Union" && !target.expression) - ) { - name = target.name!; - } else { - throw new Error("Need to specify a name for test type"); - } - } - - testTypes[name] = target; - }, - }); + const testTypes = addTestLib(fileSystem); return { ...fileSystem, diff --git a/packages/compiler/src/testing/test-server-host.ts b/packages/compiler/src/testing/test-server-host.ts index 84e1d165a91..b028ee4de03 100644 --- a/packages/compiler/src/testing/test-server-host.ts +++ b/packages/compiler/src/testing/test-server-host.ts @@ -7,7 +7,8 @@ import { IdentifierNode, SyntaxKind } from "../core/types.js"; import { createClientConfigProvider } from "../server/client-config-provider.js"; import { Server, ServerHost, createServer } from "../server/index.js"; import { createStringMap } from "../utils/misc.js"; -import { StandardTestLibrary, TestHostOptions, createTestFileSystem } from "./test-host.js"; +import { createTestFileSystem } from "./fs.js"; +import { StandardTestLibrary, TestHostOptions } from "./test-compiler-host.js"; import { resolveVirtualPath } from "./test-utils.js"; import { TestFileSystem } from "./types.js"; diff --git a/packages/compiler/src/testing/tester.ts b/packages/compiler/src/testing/tester.ts new file mode 100644 index 00000000000..2b848a78073 --- /dev/null +++ b/packages/compiler/src/testing/tester.ts @@ -0,0 +1,468 @@ +import { readFile, realpath } from "fs/promises"; +import { pathToFileURL } from "url"; +import { compilerAssert } from "../core/diagnostics.js"; +import { getEntityName } from "../core/helpers/type-name-utils.js"; +import { NodeHost } from "../core/node-host.js"; +import { CompilerOptions } from "../core/options.js"; +import { getIdentifierContext, getNodeAtPosition } from "../core/parser.js"; +import { getRelativePathFromDirectory, joinPaths, resolvePath } from "../core/path-utils.js"; +import { Program, compile as coreCompile } from "../core/program.js"; +import { createSourceLoader } from "../core/source-loader.js"; +import { CompilerHost, Diagnostic, Entity, NoTarget, SourceFile } from "../core/types.js"; +import { resolveModule } from "../module-resolver/module-resolver.js"; +import { expectDiagnosticEmpty } from "./expect.js"; +import { PositionedMarker, extractMarkers } from "./fourslash.js"; +import { createTestFileSystem } from "./fs.js"; +import { GetMarkedEntities, Marker, TemplateWithMarkers } from "./marked-template.js"; +import { StandardTestLibrary, addTestLib } from "./test-compiler-host.js"; +import { resolveVirtualPath } from "./test-utils.js"; +import type { + EmitterTester, + EmitterTesterInstance, + MockFile, + TestCompileOptions, + TestCompileResult, + TestEmitterCompileResult, + TestFileSystem, + Tester, + TesterBuilder, + TesterInstance, +} from "./types.js"; + +export interface TesterOptions { + libraries: string[]; +} +export function createTester(base: string, options: TesterOptions): Tester { + return createTesterInternal({ + fs: once(() => createTesterFs(base, options)), + libraries: options.libraries, + }); +} + +function once(fn: () => Promise): () => Promise { + let load: Promise | undefined; + return () => { + if (load) return load; + load = fn(); + return load; + }; +} + +async function createTesterFs(base: string, options: TesterOptions) { + const fs = createTestFileSystem(); + + const host: CompilerHost = { + ...NodeHost, + // We want to keep the original path in the file map but we do still want to resolve the full path when loading JS to prevent duplicate imports. + realpath: async (x: string) => x, + getJsImport: async (path: string) => { + return await import(pathToFileURL(await realpath(path)).href); + }, + }; + + const sl = await createSourceLoader(host); + const selfName = JSON.parse(await readFile(resolvePath(base, "package.json"), "utf8")).name; + for (const lib of options.libraries) { + await sl.importPath(lib, NoTarget, base); + + const resolved = await resolveModule( + { + realpath: async (x) => x, + stat: NodeHost.stat, + readFile: async (path) => { + const file = await NodeHost.readFile(path); + return file.text; + }, + }, + lib, + { baseDir: base, conditions: ["import", "default"] }, + ); + if (resolved.type === "module") { + const virtualPath = computeRelativePath(lib, resolved.mainFile); + fs.addJsFile(virtualPath, host.getJsImport(resolved.mainFile)); + } + } + + await fs.addTypeSpecLibrary(StandardTestLibrary); + + function computeVirtualPath(file: SourceFile): string { + const context = sl.resolution.locationContexts.get(file); + compilerAssert( + context?.type === "library", + `Unexpected: all source files should be in a library but ${file.path} was in '${context?.type}'`, + ); + return computeRelativePath(context.metadata.name, file.path); + } + + function computeRelativePath(libName: string, realPath: string): string { + const relativePath = getRelativePathFromDirectory(base, realPath, false); + if (libName === selfName) { + return joinPaths("node_modules", selfName, relativePath); + } else { + return relativePath; + } + } + + for (const file of sl.resolution.sourceFiles.values()) { + const relativePath = computeVirtualPath(file.file); + fs.add(resolveVirtualPath(relativePath), file.file.text); + } + for (const file of sl.resolution.jsSourceFiles.values()) { + const relativePath = computeVirtualPath(file.file); + fs.addJsFile(resolveVirtualPath(relativePath), file.esmExports); + } + for (const [path, lib] of sl.resolution.loadedLibraries) { + fs.add(resolvePath("node_modules", path, "package.json"), (lib.manifest as any).file.text); + } + fs.freeze(); + return fs; +} + +interface TesterInternalParams { + fs: () => Promise; + libraries: string[]; + wraps?: ((code: string) => string)[]; + imports?: string[]; + usings?: string[]; + compilerOptions?: CompilerOptions; +} + +interface EmitterTesterInternalParams extends TesterInternalParams { + outputProcess?: (result: any) => any; + emitter: string; +} + +function createTesterBuilder< + const I extends TesterInternalParams, + const O extends TesterBuilder, +>(params: I, create: (values: I) => O): TesterBuilder { + return { + files, + wrap, + importLibraries, + import: importFn, + using, + }; + + function files(files: Record): O { + const fs = async () => { + const fs = (await params.fs()).clone(); + for (const [name, value] of Object.entries(files)) { + fs.add(name, value); + } + fs.freeze(); + return fs; + }; + return create({ + ...params, + fs, + }); + } + function wrap(fn: (x: string) => string): O { + return create({ + ...params, + wraps: [...(params.wraps ?? []), fn], + }); + } + + function importLibraries(): O { + return create({ + ...params, + imports: [...(params.imports ?? []), ...params.libraries], + }); + } + + function importFn(...imports: string[]): O { + return create({ + ...params, + imports: [...(params.imports ?? []), ...imports], + }); + } + + function using(...usings: string[]): O { + return create({ + ...params, + usings: [...(params.usings ?? []), ...usings], + }); + } +} + +function createTesterInternal(params: TesterInternalParams): Tester { + return { + ...createCompilable(async (...args) => { + const instance = await createTesterInstance(params); + return instance.compileAndDiagnose(...args); + }), + ...createTesterBuilder(params, createTesterInternal), + emit, + createInstance, + }; + + function emit(emitter: string, options?: Record): EmitterTester { + return createEmitterTesterInternal({ + ...params, + emitter, + compilerOptions: options + ? { + ...params.compilerOptions, + options: { + ...params.compilerOptions?.options, + [emitter]: options, + }, + } + : params.compilerOptions, + }); + } + + function createInstance(): Promise { + return createTesterInstance(params); + } +} + +function createEmitterTesterInternal( + params: EmitterTesterInternalParams, +): EmitterTester { + return { + ...createCompilable(async (...args) => { + const instance = await createEmitterTesterInstance(params); + return instance.compileAndDiagnose(...args); + }), + ...createTesterBuilder>( + params, + createEmitterTesterInternal, + ), + pipe: (cb: (previous: Result) => O): EmitterTester => { + return createEmitterTesterInternal({ + ...params, + outputProcess: async (result) => { + return params.outputProcess ? cb(params.outputProcess(result)) : cb(result); + }, + }); + }, + createInstance: () => createEmitterTesterInstance(params), + }; +} + +async function createEmitterTesterInstance( + params: EmitterTesterInternalParams, +): Promise> { + const tester = await createTesterInstance(params); + return { + fs: tester.fs, + ...createCompilable(compileAndDiagnose), + get program() { + return tester.program; + }, + }; + + async function compileAndDiagnose( + code: string | Record, + options?: TestCompileOptions, + ): Promise<[Result, readonly Diagnostic[]]> { + if (options?.compilerOptions?.emit !== undefined) { + throw new Error("Cannot set emit in options."); + } + const resolvedOptions: TestCompileOptions = { + ...options, + compilerOptions: { + ...params.compilerOptions, + ...options?.compilerOptions, + outputDir: "tsp-output", + emit: [params.emitter], + }, + }; + const [result, diagnostics] = await tester.compileAndDiagnose(code, resolvedOptions); + const outputs: Record = {}; + const outputDir = + resolvedOptions.compilerOptions?.options?.[params.emitter]?.["emitter-output-dir"] ?? + resolveVirtualPath(resolvePath("tsp-output", params.emitter)); + for (const [name, value] of result.fs.fs) { + if (name.startsWith(outputDir)) { + const relativePath = name.slice(outputDir.length + 1); + outputs[relativePath] = value; + } + } + + const prep = { + ...result, + outputs, + }; + const final = params.outputProcess ? params.outputProcess(prep) : prep; + return [final, diagnostics]; + } +} + +async function createTesterInstance(params: TesterInternalParams): Promise { + let savedProgram: Program | undefined; + const fs = (await params.fs()).clone(); + + return { + ...createCompilable(compileAndDiagnose), + fs, + get program() { + if (!savedProgram) { + throw new Error("Program not initialized. Call compile first."); + } + return savedProgram; + }, + }; + + function applyWraps(code: string, wraps: ((code: string) => string)[]): string { + for (const wrap of wraps) { + code = wrap(code); + } + return code; + } + + function addCode( + fs: TestFileSystem, + code: string | TemplateWithMarkers | Record>, + ): { + markerPositions: PositionedMarkerInFile[]; + markerConfigs: Record>; + } { + const markerPositions: PositionedMarkerInFile[] = []; + const markerConfigs: Record> = {}; + + function addTsp(filename: string, value: string | TemplateWithMarkers) { + const codeStr = TemplateWithMarkers.is(value) ? value.code : value; + + const actualCode = filename === "main.tsp" ? wrapMain(codeStr) : codeStr; + if (TemplateWithMarkers.is(value)) { + const markers = extractMarkers(actualCode); + for (const marker of markers) { + markerPositions.push({ ...marker, filename }); + } + for (const [markerName, markerConfig] of Object.entries(value.markers)) { + if (markerConfig) { + markerConfigs[markerName] = markerConfig; + } + } + } + fs.addTypeSpecFile(filename, actualCode); + } + + const files = + typeof code === "string" || TemplateWithMarkers.is(code) ? { "main.tsp": code } : code; + for (const [name, value] of Object.entries(files)) { + addTsp(name, value); + } + + return { markerPositions, markerConfigs }; + } + + function wrapMain(code: string): string { + const imports = (params.imports ?? []).map((x) => `import "${x}";`); + const usings = (params.usings ?? []).map((x) => `using ${x};`); + + const actualCode = [ + ...imports, + ...usings, + params.wraps ? applyWraps(code, params.wraps) : code, + ].join("\n"); + return actualCode; + } + + async function compileAndDiagnose< + T extends string | TemplateWithMarkers | Record>, + >( + code: T, + options?: TestCompileOptions, + ): Promise<[TestCompileResult>, readonly Diagnostic[]]> { + const typesCollected = addTestLib(fs); + const { markerPositions, markerConfigs } = addCode(fs, code); + + const program = await coreCompile( + fs.compilerHost, + resolveVirtualPath("main.tsp"), + options?.compilerOptions, + ); + savedProgram = program; + + const entities = extractMarkedEntities(program, markerPositions, markerConfigs); + return [{ program, fs, ...typesCollected, ...entities } as any, program.diagnostics]; + } +} + +interface PositionedMarkerInFile extends PositionedMarker { + /** The file where the marker is located */ + readonly filename: string; +} + +function extractMarkedEntities( + program: Program, + markerPositions: PositionedMarkerInFile[], + markerConfigs: Record>, +) { + const entities: Record = {}; + for (const marker of markerPositions) { + const file = program.sourceFiles.get(resolveVirtualPath(marker.filename)); + if (!file) { + throw new Error(`Couldn't find ${resolveVirtualPath(marker.filename)} in program`); + } + const { name, pos } = marker; + const markerConfig = markerConfigs[name]; + const node = getNodeAtPosition(file, pos); + if (!node) { + throw new Error(`Could not find node at ${pos}`); + } + const { node: contextNode } = getIdentifierContext(node as any); + if (contextNode === undefined) { + throw new Error( + `Could not find context node for ${name} at ${pos}. File content: ${file.file.text}`, + ); + } + const entity = program.checker.getTypeOrValueForNode(contextNode); + if (entity === null) { + throw new Error( + `Expected ${name} to be of entity kind ${markerConfig?.entityKind} but got null (Means a value failed to resolve) at ${pos}`, + ); + } + if (markerConfig) { + const { entityKind, kind, valueKind } = markerConfig as any; + if (entity.entityKind !== entityKind) { + throw new Error( + `Expected ${name} to be of entity kind ${entityKind} but got (${entity?.entityKind}) ${getEntityName(entity)} at ${pos}`, + ); + } + if (entity.entityKind === "Type" && kind !== undefined && entity.kind !== kind) { + throw new Error( + `Expected ${name} to be of kind ${kind} but got (${entity.kind}) ${getEntityName(entity)} at ${pos}`, + ); + } else if ( + entity?.entityKind === "Value" && + valueKind !== undefined && + entity.valueKind !== valueKind + ) { + throw new Error( + `Expected ${name} to be of value kind ${valueKind} but got (${entity.valueKind}) ${getEntityName(entity)} at ${pos}`, + ); + } + } + + entities[name] = entity; + } + return entities; +} + +export interface Compilable { + compileAndDiagnose(...args: A): Promise<[R, readonly Diagnostic[]]>; + compile(...args: A): Promise; + diagnose(...args: A): Promise; +} +function createCompilable( + fn: (...args: A) => Promise<[R, readonly Diagnostic[]]>, +): Compilable { + return { + compileAndDiagnose: fn, + compile: async (...args: A) => { + const [result, diagnostics] = await fn(...args); + expectDiagnosticEmpty(diagnostics); + return result; + }, + diagnose: async (...args: A) => { + const [_, diagnostics] = await fn(...args); + return diagnostics; + }, + }; +} diff --git a/packages/compiler/src/testing/types.ts b/packages/compiler/src/testing/types.ts index 0ae7aa45068..30391ea8596 100644 --- a/packages/compiler/src/testing/types.ts +++ b/packages/compiler/src/testing/types.ts @@ -1,20 +1,222 @@ import type { CompilerOptions } from "../core/options.js"; import type { Program } from "../core/program.js"; -import type { CompilerHost, Diagnostic, Type } from "../core/types.js"; +import type { CompilerHost, Diagnostic, Entity, Type } from "../core/types.js"; +import { GetMarkedEntities, TemplateWithMarkers } from "./marked-template.js"; + +// #region Test file system + +/** Represent a mock file. Use `mockFile` function to construct */ +export type MockFile = string | JsFile; + +export interface JsFile { + readonly kind: "js"; + readonly exports: Record; +} export interface TestFileSystem { - readonly compilerHost: CompilerHost; + /** Raw files */ readonly fs: Map; + /** Compiler host */ + readonly compilerHost: CompilerHost; + /** + * Add a mock test file + * @example + * ```ts + * fs.add("foo.tsp", "model Foo {}"); + * fs.add("foo.js", mockFile.js({ Foo: { bar: 1 } })); + * ``` + */ + add(path: string, content: MockFile): void; + + /** Prefer using {@link add} */ addTypeSpecFile(path: string, contents: string): void; + /** Prefer using {@link add} */ addJsFile(path: string, contents: Record): void; addRealTypeSpecFile(path: string, realPath: string): Promise; addRealJsFile(path: string, realPath: string): Promise; addRealFolder(path: string, realPath: string): Promise; addTypeSpecLibrary(testLibrary: TypeSpecTestLibrary): Promise; + + /** @internal */ + freeze(): void; + + /** @internal */ + clone(): TestFileSystem; } -export interface TestHost extends TestFileSystem { +//#endregion + +// #region Tester +export type TestCompileResult> = T & { + /** The program created in this test compilation. */ + readonly program: Program; + + /** File system */ + readonly fs: TestFileSystem; +} & Record; + +export interface TestCompileOptions { + /** Optional compiler options */ + readonly compilerOptions?: CompilerOptions; +} + +interface Testable { + /** + * Compile the given code and validate no diagnostics(error or warnings) are present. + * Use {@link compileAndDiagnose} to get the compiler result and manage diagnostics yourself. + * + * @param code Can be the content of the `main.tsp` file or a record of files(MUST contains a main.tsp). + * @param options Optional test options. + * @returns {@link TestCompileResult} with the program and collected entities. + * + * @example + * ```ts + * const result = await tester.compile(t.code`model ${t.model("Foo")} { bar: string }`); + * // result.program is the program created + * // result.Foo is the model Foo created + * ``` + */ + compile< + T extends string | TemplateWithMarkers | Record>, + >( + code: T, + options?: TestCompileOptions, + ): Promise>>; + /** + * Compile the given code and return the list of diagnostics emitted. + * @param code Can be the content of the `main.tsp` file or a record of files(MUST contains a main.tsp). + * @param options Optional test options. + * @returns List of diagnostics emitted. + * + * @example + * ```ts + * const diagnostics = await tester.diagnose("model Foo {}"); + * expectDiagnostics(diagnostics, { + * code: "no-foo", + * message: "Do not use Foo as a model name", + * }); + * ``` + */ + diagnose(main: string, options?: TestCompileOptions): Promise; + + /** + * Compile the given code and return the collected entities and diagnostics. + * + * @param code Can be the content of the `main.tsp` file or a record of files(MUST contains a main.tsp). + * @param options Optional test options. + * @returns {@link TestCompileResult} with the program and collected entities with the list of diagnostics emitted. + * + * @example + * ```ts + * const [result, diagnostics] = await tester.compileAndDiagnose(t.code`model ${t.model("Foo")} { bar: string }`); + * // result.program is the program created + * // result.Foo is the model Foo created + * ``` + */ + compileAndDiagnose< + T extends string | TemplateWithMarkers | Record>, + >( + code: T, + options?: TestCompileOptions, + ): Promise<[TestCompileResult>, readonly Diagnostic[]]>; +} + +export interface TesterBuilder { + /** Extend with the given list of files */ + files(files: Record): T; + /** Auto import all libraries defined in this tester. */ + importLibraries(): T; + /** Import the given paths */ + import(...imports: string[]): T; + /** Add using statement for the given namespaces. */ + using(...names: string[]): T; + /** Wrap the code of the `main.tsp` file */ + wrap(fn: (x: string) => string): T; +} + +// Immutable structure meant to be reused +export interface Tester extends Testable, TesterBuilder { + /** + * Create an emitter tester + * @param options - Options to pass to the emitter + */ + emit(emitter: string, options?: Record): EmitterTester; + /** Create an instance of the tester */ + createInstance(): Promise; +} + +export interface TestEmitterCompileResult { + /** The program created in this test compilation. */ + readonly program: Program; + + /** Files written to the emitter output dir. */ + readonly outputs: Record; +} + +export interface OutputTestable { + compile(code: string | Record, options?: TestCompileOptions): Promise; + compileAndDiagnose( + code: string | Record, + options?: TestCompileOptions, + ): Promise<[Result, readonly Diagnostic[]]>; + diagnose( + code: string | Record, + options?: TestCompileOptions, + ): Promise; +} + +/** Alternate version of the tester which runs the configured emitter */ +export interface EmitterTester + extends OutputTestable, + TesterBuilder> { + /** + * Pipe the output of the emitter into a different structure + * + * @example + * ```ts + * const MyTester = Tester.emit("my-emitter").pipe((result) => { + * return JSON.parse(result.outputs["output.json"]); + * }); + * + * const result = await MyTester.compile("model Foo { bar: string }"); + * // result is the parsed JSON from the output.json file + * ``` + */ + pipe(cb: (result: Result) => O): EmitterTester; + + /** Create a mutable instance of the tester */ + createInstance(): Promise>; +} + +export interface TesterInstanceBase { + /** Program created. Only available after calling `compile`, `diagnose` or `compileAndDiagnose` */ + get program(): Program; + + /** File system used */ + readonly fs: TestFileSystem; +} +/** Instance of a tester. */ +export interface TesterInstance extends TesterInstanceBase, Testable {} + +/** Instance of an emitter tester */ +export interface EmitterTesterInstance extends TesterInstanceBase, OutputTestable {} + +// #endregion + +// #region Legacy Test host +export interface TestHost + extends Pick< + TestFileSystem, + | "addTypeSpecFile" + | "addJsFile" + | "addRealTypeSpecFile" + | "addRealJsFile" + | "addRealFolder" + | "addTypeSpecLibrary" + | "compilerHost" + | "fs" + > { program: Program; libraries: TypeSpecTestLibrary[]; testTypes: Record; @@ -93,3 +295,4 @@ export interface BasicTestRunner { options?: CompilerOptions, ): Promise<[Record, readonly Diagnostic[]]>; } +// #endregion diff --git a/packages/compiler/test/testing/fourslash.test.ts b/packages/compiler/test/testing/fourslash.test.ts new file mode 100644 index 00000000000..63246ace6d9 --- /dev/null +++ b/packages/compiler/test/testing/fourslash.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { extractMarkers } from "../../src/testing/fourslash.js"; + +describe("extractMarkers", () => { + it("marks the pos right after the fourslash syntax", () => { + const code = `model /*foo*/Foo {}`; + const markers = extractMarkers(code); + expect(markers).toHaveLength(1); + expect(markers[0]).toMatchObject({ name: "foo" }); + expect(code.slice(markers[0].pos, markers[0].pos + 3)).toBe("Foo"); + }); + + it("extracts multiple markers", () => { + const code = `model /*foo*/Foo {}\nmodel /*bar*/Bar {}`; + const markers = extractMarkers(code); + expect(markers).toHaveLength(2); + expect(markers[0].name).toBe("foo"); + expect(code.slice(markers[0].pos, markers[0].pos + 3)).toBe("Foo"); + expect(markers[1].name).toBe("bar"); + expect(code.slice(markers[1].pos, markers[1].pos + 3)).toBe("Bar"); + }); + + it("extracts marker with identifier containing numbers and underscores", () => { + const code = `model /*foo*/Foo_123 {}`; + const markers = extractMarkers(code); + expect(markers).toHaveLength(1); + expect(markers[0].name).toBe("foo"); + expect(code.slice(markers[0].pos, markers[0].pos + 7)).toBe("Foo_123"); + }); +}); diff --git a/packages/compiler/test/testing/tester.test.ts b/packages/compiler/test/testing/tester.test.ts new file mode 100644 index 00000000000..308ebc8f0b9 --- /dev/null +++ b/packages/compiler/test/testing/tester.test.ts @@ -0,0 +1,308 @@ +// TODO: rename? + +import { strictEqual } from "assert"; +import { describe, expect, expectTypeOf, it } from "vitest"; +import { resolvePath } from "../../src/core/path-utils.js"; +import { + EmitContext, + emitFile, + Enum, + getLocationContext, + Model, + navigateProgram, + ObjectValue, + Program, +} from "../../src/index.js"; +import { mockFile } from "../../src/testing/fs.js"; +import { t } from "../../src/testing/marked-template.js"; +import { createTester } from "../../src/testing/tester.js"; + +const Tester = createTester(resolvePath(import.meta.dirname, "../.."), { libraries: [] }); + +it("generic type", async () => { + const res = await Tester.compile(t.code` + model ${t.model("Foo")} {} + enum ${t.enum("Bar")} {} + union /*Baz*/Baz {} + `); + expect(res.Foo.kind).toBe("Model"); + + expectTypeOf({ + Foo: res.Foo, + Bar: res.Bar, + Baz: res.Baz, + program: res.program, + }).toExtend<{ + Foo: Model; + Bar: Enum; + program: Program; + }>(); +}); + +describe("extract types", () => { + it("generic type", async () => { + const res = await Tester.compile(t.code` + model ${t.type("Foo")} {} + enum ${t.type("Bar")} {} + `); + expect(res.Foo.kind).toBe("Model"); + expect(res.Bar.kind).toBe("Enum"); + }); + + it("extract with fourslash syntax", async () => { + const res = await Tester.compile(t.code` + model /*ExtractedFoo*/Foo {} + `); + strictEqual(res.ExtractedFoo.entityKind, "Type"); + expect(res.ExtractedFoo.kind).toBe("Model"); + }); + + it("model", async () => { + const res = await Tester.compile(t.code` + model ${t.model("Foo")} {} + `); + expectTypeOf(res.Foo).toExtend(); + expect(res.Foo.kind).toBe("Model"); + }); + + it("alias", async () => { + const res = await Tester.compile(t.code` + model Foo {} + alias ${t.model("Bar")} = Foo; + `); + expect(res.Bar.kind).toBe("Model"); + }); + + it("enum", async () => { + const res = await Tester.compile(t.code` + enum ${t.enum("Foo")} {} + `); + expect(res.Foo.kind).toBe("Enum"); + }); + + it("union", async () => { + const res = await Tester.compile(t.code` + union ${t.union("Foo")} {} + `); + expect(res.Foo.kind).toBe("Union"); + }); + + it("interface", async () => { + const res = await Tester.compile(t.code` + interface ${t.interface("Foo")} {} + `); + expect(res.Foo.kind).toBe("Interface"); + }); + + it("operation", async () => { + const res = await Tester.compile(t.code` + op ${t.op("Foo")}(): void; + `); + expect(res.Foo.kind).toBe("Operation"); + }); + + it("namespace", async () => { + const res = await Tester.compile(t.code` + namespace ${t.namespace("Foo")} {} + `); + expect(res.Foo.kind).toBe("Namespace"); + }); + + it("scalar", async () => { + const res = await Tester.compile(t.code` + scalar ${t.scalar("Foo")}; + `); + expect(res.Foo.kind).toBe("Scalar"); + }); + + it("model property", async () => { + const res = await Tester.compile(t.code` + model Bar { + ${t.modelProperty("prop")}: string; + } + `); + expect(res.prop.kind).toBe("ModelProperty"); + }); + + it("model property in operation", async () => { + const res = await Tester.compile(t.code` + op test( + ${t.modelProperty("prop")}: string; + ): void; + `); + expect(res.prop.kind).toBe("ModelProperty"); + }); + + it("union variant", async () => { + const res = await Tester.compile(t.code` + union Bar { + ${t.unionVariant("A")}: string; + } + `); + expect(res.A.kind).toBe("UnionVariant"); + }); + + it("enum member", async () => { + const res = await Tester.compile(t.code` + enum Bar { + ${t.enumMember("A")} + } + `); + expect(res.A.kind).toBe("EnumMember"); + }); + + it("validate type match", async () => { + await expect(() => + Tester.compile(t.code` + enum ${t.model("Foo")} {} + `), + ).rejects.toThrowError("Expected Foo to be of kind Model but got (Enum) Foo at 21"); + }); +}); + +describe("extract values", () => { + it("generic value", async () => { + const res = await Tester.compile(t.code` + const ${t.value("a")} = "foo"; + const ${t.value("b")} = 123; + `); + expect(res.a.valueKind).toBe("StringValue"); + expect(res.b.valueKind).toBe("NumericValue"); + }); + + it("object", async () => { + const res = await Tester.compile(t.code` + const ${t.object("foo")} = #{}; + `); + expect(res.foo.valueKind).toBe("ObjectValue"); + expectTypeOf(res.foo).toExtend(); + }); + + it("array", async () => { + const res = await Tester.compile(t.code` + const ${t.array("foo")} = #[]; + `); + expect(res.foo.valueKind).toBe("ArrayValue"); + }); + + it("validate value match", async () => { + await expect(() => + Tester.compile(t.code` + const ${t.object("foo")} = 123; + `), + ).rejects.toThrowError( + "Expected foo to be of value kind ObjectValue but got (NumericValue) 123 at 22", + ); + }); +}); + +it("still extract with additional using", async () => { + const res = await Tester.using("TypeSpec").compile(t.code` + model ${t.model("Foo")} {} + `); + expect(res.Foo.kind).toBe("Model"); +}); + +it("still extract with wrappers", async () => { + const res = await Tester.wrap((x) => `model Test {}\n${x}\nmodel Test2 {}`).compile(t.code` + model ${t.model("Foo")} {} + `); + expect(res.Foo.kind).toBe("Model"); +}); + +it("still extract with multiple files", async () => { + const res = await Tester.compile({ + "main.tsp": t.code` + import "./b.tsp"; + model ${t.model("A")} {} + `, + "b.tsp": t.code` + enum ${t.enum("B")} {} + `, + }); + + expectTypeOf(res.A).toExtend(); + expectTypeOf(res.B).toExtend(); + expect(res.A.kind).toBe("Model"); + expect(res.B.kind).toBe("Enum"); +}); + +it("add extra files via fs api", async () => { + const tester = await Tester.createInstance(); + tester.fs.add("foo.tsp", "model Foo {}"); + await tester.compile( + ` + import "./foo.tsp"; + model Bar {} + `, + ); +}); + +describe("emitter", () => { + const EmitterTester = Tester.files({ + "node_modules/dummy-emitter/package.json": JSON.stringify({ + name: "dummy-emitter", + version: "1.0.0", + exports: { ".": "./index.js" }, + }), + "node_modules/dummy-emitter/index.js": mockFile.js({ + $onEmit: (context: EmitContext) => { + navigateProgram(context.program, { + model: (model) => { + if (getLocationContext(context.program, model).type !== "project") return; + emitFile(context.program, { + path: resolvePath(context.emitterOutputDir, `${model.name}.model`), + content: model.name, + }); + }, + }); + }, + }), + }).emit("dummy-emitter"); + + it("return output", async () => { + const res = await EmitterTester.compile( + ` + model Foo {} + model Bar {} + `, + ); + expect(res.outputs).toEqual({ + "Foo.model": "Foo", + "Bar.model": "Bar", + }); + }); + + it("can use same chai methods", async () => { + const res = await await EmitterTester.wrap( + (x) => `model Test {}\n${x}\nmodel Test2 {}`, + ).compile(`model Foo {}`); + expect(res.outputs).toEqual({ + "Foo.model": "Foo", + "Test.model": "Test", + "Test2.model": "Test2", + }); + }); + + it("pipe outputs", async () => { + const res = await await EmitterTester.pipe((x) => x.outputs["Foo.model"]).compile( + `model Foo {}`, + ); + expect(res).toEqual("Foo"); + }); + + it("add extra files via fs api", async () => { + const tester = await EmitterTester.createInstance(); + tester.fs.add("foo.tsp", "model Foo {}"); + const res = await tester.compile( + ` + import "./foo.tsp"; + model Bar {} + `, + ); + expect(res.outputs).toEqual({ + "Foo.model": "Foo", + "Bar.model": "Bar", + }); + }); +}); diff --git a/packages/http/test/auth.test.ts b/packages/http/test/auth.test.ts index a95355df986..47d769a874d 100644 --- a/packages/http/test/auth.test.ts +++ b/packages/http/test/auth.test.ts @@ -1,23 +1,18 @@ -import { Operation } from "@typespec/compiler"; -import { BasicTestRunner } from "@typespec/compiler/testing"; import { ok, strictEqual } from "assert"; -import { beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { getAuthenticationForOperation } from "../src/auth.js"; -import { createHttpTestRunner } from "./test-host.js"; - -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createHttpTestRunner(); -}); +import { Tester } from "./test-host.js"; describe("per operation authentication", () => { /** Test function that will expect api key auth only and return the name of the one selected */ async function getTestOperationApiKeyAuthName(code: string) { - const { test } = (await runner.compile(code)) as { test: Operation }; + const { test, program } = await Tester.compile(code); - ok(test, "Should have operation called test marked with @test"); - const auth = getAuthenticationForOperation(runner.program, test); + ok( + test.entityKind === "Type" && test.kind === "Operation", + "Should have operation called test marked with @test", + ); + const auth = getAuthenticationForOperation(program, test); const scheme = auth?.options[0].schemes[0]; strictEqual(scheme?.type, "apiKey"); return scheme.name; diff --git a/packages/http/test/experimental/typekit/http-operation.test.ts b/packages/http/test/experimental/typekit/http-operation.test.ts index 0b7034571c4..d33f742fb13 100644 --- a/packages/http/test/experimental/typekit/http-operation.test.ts +++ b/packages/http/test/experimental/typekit/http-operation.test.ts @@ -1,22 +1,15 @@ -import { Model, Operation } from "@typespec/compiler"; -import { BasicTestRunner, expectDiagnostics } from "@typespec/compiler/testing"; +import { expectDiagnostics, t } from "@typespec/compiler/testing"; import { $ } from "@typespec/compiler/typekit"; -import { assert, beforeEach, describe, expect, it } from "vitest"; -import { createHttpTestRunner } from "./../../test-host.js"; +import { assert, describe, expect, it } from "vitest"; +import { Tester } from "./../../test-host.js"; // Activate Http TypeKit augmentation import "../../../src/experimental/typekit/index.js"; -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createHttpTestRunner(); -}); - describe("httpOperation:getResponses", () => { it("should get responses", async () => { - const { getFoo } = (await runner.compile(` - @test model Foo { + const { getFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { @visibility(Lifecycle.Create) id: int32; age: int32; @@ -24,18 +17,18 @@ describe("httpOperation:getResponses", () => { } @error - @test model Error { + model ${t.model("Error")} { message: string; code: int32 } @route("/foo") @get - @test op getFoo(): Foo | Error; - `)) as { getFoo: Operation; Foo: Model; Error: Model }; + op ${t.op("getFoo")}(): Foo | Error; + `); - const httpOperation = $(runner.program).httpOperation.get(getFoo); - const responses = $(runner.program).httpOperation.flattenResponses(httpOperation); + const httpOperation = $(program).httpOperation.get(getFoo); + const responses = $(program).httpOperation.flattenResponses(httpOperation); expect(responses).toHaveLength(2); expect(responses[0].statusCode).toBe(200); expect(responses[0].contentType).toBe("application/json"); @@ -44,8 +37,8 @@ describe("httpOperation:getResponses", () => { }); it("should get responses with multiple status codes", async () => { - const { getFoo } = (await runner.compile(` - @test model Foo { + const { getFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { @visibility(Lifecycle.Create) id: int32; age: int32; @@ -54,11 +47,11 @@ describe("httpOperation:getResponses", () => { @route("/foo") @get - @test op getFoo(): Foo | void; - `)) as { getFoo: Operation; Foo: Model; Error: Model }; + op ${t.op("getFoo")}(): Foo | void; + `); - const httpOperation = $(runner.program).httpOperation.get(getFoo); - const responses = $(runner.program).httpOperation.flattenResponses(httpOperation); + const httpOperation = $(program).httpOperation.get(getFoo); + const responses = $(program).httpOperation.flattenResponses(httpOperation); expect(responses).toHaveLength(2); expect(responses[0].statusCode).toBe(200); expect(responses[0].contentType).toBe("application/json"); @@ -67,8 +60,8 @@ describe("httpOperation:getResponses", () => { }); it("should get responses with multiple status codes and contentTypes", async () => { - const { getFoo } = (await runner.compile(` - @test model Foo { + const { getFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { @visibility(Lifecycle.Create) id: int32; age: int32; @@ -76,18 +69,18 @@ describe("httpOperation:getResponses", () => { } @error - @test model Error { + model ${t.model("Error")} { message: string; code: int32 } @route("/foo") @get - @test op getFoo(): Foo | {...Foo, @header contentType: "text/plain"} | Error; - `)) as { getFoo: Operation; Foo: Model; Error: Model }; + op ${t.op("getFoo")}(): Foo | {...Foo, @header contentType: "text/plain"} | Error; + `); - const httpOperation = $(runner.program).httpOperation.get(getFoo); - const responses = $(runner.program).httpOperation.flattenResponses(httpOperation); + const httpOperation = $(program).httpOperation.get(getFoo); + const responses = $(program).httpOperation.flattenResponses(httpOperation); expect(responses).toHaveLength(3); expect(responses[0].statusCode).toBe(200); expect(responses[0].contentType).toBe("application/json"); @@ -99,15 +92,15 @@ describe("httpOperation:getResponses", () => { }); it("should get diagnostics from httpOperation.get", async () => { - const [{ getFoo }] = await runner.compileAndDiagnose(` + const [{ getFoo, program }, _] = await Tester.compileAndDiagnose(t.code` @route("/foo/{missing-param}") @get - @test op getFoo(): Foo | Error; + op ${t.op("getFoo")}(): void; `); assert.ok(getFoo.kind === "Operation"); - const [httpOperation, diagnostics] = $(runner.program).httpOperation.get.withDiagnostics(getFoo); + const [httpOperation, diagnostics] = $(program).httpOperation.get.withDiagnostics(getFoo); expect(httpOperation).toBeDefined(); expectDiagnostics(diagnostics, { diff --git a/packages/http/test/experimental/typekit/http-request.test.ts b/packages/http/test/experimental/typekit/http-request.test.ts index 559f898bed1..896b5b18c96 100644 --- a/packages/http/test/experimental/typekit/http-request.test.ts +++ b/packages/http/test/experimental/typekit/http-request.test.ts @@ -1,22 +1,15 @@ -import { Model, Operation } from "@typespec/compiler"; -import { BasicTestRunner } from "@typespec/compiler/testing"; +import { t } from "@typespec/compiler/testing"; import { $ } from "@typespec/compiler/typekit"; -import { beforeEach, describe, expect, it } from "vitest"; -import { createHttpTestRunner } from "./../../test-host.js"; +import { describe, expect, it } from "vitest"; +import { Tester } from "./../../test-host.js"; // Activate Http TypeKit augmentation import "../../../src/experimental/typekit/index.js"; -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createHttpTestRunner(); -}); - describe("HttpRequest Body Parameters", () => { it("should get the body parameters model when spread", async () => { - const { createFoo } = (await runner.compile(` - @test model Foo { + const { createFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { id: int32; age: int32; name: string; @@ -24,24 +17,24 @@ describe("HttpRequest Body Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)!; expect(body).toBeDefined(); expect(tk.model.is(body)).toBe(true); - expect((body as Model).properties.size).toBe(3); + expect((body as any).properties.size).toBe(3); }); it("should get the body model params when body is defined explicitly as a property", async () => { - const { createFoo } = (await runner.compile(` + const { createFoo, program } = await Tester.compile(t.code` @route("/foo") @post - @test op createFoo(@body foo: int32): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); + op ${t.op("createFoo")}(@body foo: int32): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)!; @@ -52,8 +45,8 @@ describe("HttpRequest Body Parameters", () => { }); it("should get the body when spread and nested", async () => { - const { createFoo } = (await runner.compile(` - @test model Foo { + const { createFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { @path id: int32; age: int32; name: string; @@ -65,22 +58,22 @@ describe("HttpRequest Body Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)!; expect(body).toBeDefined(); - expect((body as Model).properties.size).toBe(3); + expect((body as any).properties.size).toBe(3); const properties = Array.from(body.properties.values()) - .map((p) => p.name) + .map((p: any) => p.name) .join(","); expect(properties).toBe("age,name,options"); - const optionsParam = (body as Model).properties.get("options")!.type as Model; + const optionsParam = (body as any).properties.get("options").type; const optionsProps = Array.from(optionsParam.properties.values()) - .map((p) => p.name) + .map((p: any) => p.name) .join(","); // TODO: Why do we get the path property token here? @@ -88,8 +81,8 @@ describe("HttpRequest Body Parameters", () => { }); it("should get the body when named body model", async () => { - const { createFoo } = (await runner.compile(` - @test model Foo { + const { createFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { id: int32; age: int32; name: string; @@ -97,9 +90,9 @@ describe("HttpRequest Body Parameters", () => { @route("/foo") @post - @test op createFoo(@body foo: Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); + op ${t.op("createFoo")}(@body foo: Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)!; @@ -107,12 +100,12 @@ describe("HttpRequest Body Parameters", () => { expect(tk.model.is(body)).toBe(true); // Should have a single property called foo expect(body.properties.size).toBe(1); - expect((body.properties.get("foo")?.type as Model).name).toBe("Foo"); + expect((body.properties.get("foo")?.type as any).name).toBe("Foo"); }); it("should get the named body body when combined", async () => { - const { createFoo } = (await runner.compile(` - @test model Foo { + const { createFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { @path id: int32; age: int32; name: string; @@ -120,23 +113,23 @@ describe("HttpRequest Body Parameters", () => { @route("/foo") @post - @test op createFoo(foo: Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); + op ${t.op("createFoo")}(foo: Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)!; expect(body).toBeDefined(); expect(tk.model.is(body)).toBe(true); - expect((body as Model).properties.size).toBe(1); - expect(((body as Model).properties.get("foo")?.type as any).name).toBe("Foo"); + expect((body as any).properties.size).toBe(1); + expect(((body as any).properties.get("foo")?.type as any).name).toBe("Foo"); }); }); describe("HttpRequest Get Parameters", () => { it("should only have body parameters", async () => { - const { createFoo } = (await runner.compile(` - @test model Foo { + const { createFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { id: int32; age: int32; name: string; @@ -144,9 +137,9 @@ describe("HttpRequest Get Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)!; @@ -160,8 +153,8 @@ describe("HttpRequest Get Parameters", () => { }); it("should be able to get parameter options", async () => { - const { createFoo } = (await runner.compile(` - @test model Foo { + const { createFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { @path(#{allowReserved: true}) id: string; # suppress "deprecated" "Test" @header(#{explode: true}) requestId: string[]; @@ -170,9 +163,9 @@ describe("HttpRequest Get Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const headers = tk.httpRequest.getParameters(httpOperation, "header"); @@ -205,8 +198,8 @@ describe("HttpRequest Get Parameters", () => { }); it("should only have header parameters", async () => { - const { createFoo } = (await runner.compile(` - @test model Foo { + const { createFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { @path id: int32; age: int32; name: string; @@ -214,12 +207,12 @@ describe("HttpRequest Get Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); - const body = tk.httpRequest.getBodyParameters(httpOperation)! as Model; + const body = tk.httpRequest.getBodyParameters(httpOperation)! as any; const headers = tk.httpRequest.getParameters(httpOperation, "header"); const path = tk.httpRequest.getParameters(httpOperation, "path")!; const query = tk.httpRequest.getParameters(httpOperation, "query"); @@ -233,8 +226,8 @@ describe("HttpRequest Get Parameters", () => { }); it("should only have path parameters", async () => { - const { createFoo } = (await runner.compile(` - @test model Foo { + const { createFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { @header id: int32; @header age: int32; name: string; @@ -242,12 +235,12 @@ describe("HttpRequest Get Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); - const body = tk.httpRequest.getBodyParameters(httpOperation)! as Model; + const body = tk.httpRequest.getBodyParameters(httpOperation)! as any; const headers = tk.httpRequest.getParameters(httpOperation, "header")!; const path = tk.httpRequest.getParameters(httpOperation, "path"); const query = tk.httpRequest.getParameters(httpOperation, "query"); @@ -262,8 +255,8 @@ describe("HttpRequest Get Parameters", () => { }); it("should only have query parameters", async () => { - const { createFoo } = (await runner.compile(` - @test model Foo { + const { createFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { @query id: int32; @query age: int32; name: string; @@ -271,12 +264,12 @@ describe("HttpRequest Get Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); - const body = tk.httpRequest.getBodyParameters(httpOperation)! as Model; + const body = tk.httpRequest.getBodyParameters(httpOperation)! as any; const headers = tk.httpRequest.getParameters(httpOperation, "header"); const path = tk.httpRequest.getParameters(httpOperation, "path"); const query = tk.httpRequest.getParameters(httpOperation, "query")!; @@ -291,8 +284,8 @@ describe("HttpRequest Get Parameters", () => { }); it("should have query and header parameters", async () => { - const { createFoo } = (await runner.compile(` - @test model Foo { + const { createFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { @query id: int32; @header age: int32; name: string; @@ -300,9 +293,9 @@ describe("HttpRequest Get Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const headerAndQuery = tk.httpRequest.getParameters(httpOperation, ["header", "query"]); diff --git a/packages/http/test/experimental/typekit/http-response.test.ts b/packages/http/test/experimental/typekit/http-response.test.ts index d3ce22b1ea9..ca79a3d9b53 100644 --- a/packages/http/test/experimental/typekit/http-response.test.ts +++ b/packages/http/test/experimental/typekit/http-response.test.ts @@ -1,37 +1,30 @@ -import { Model, Operation } from "@typespec/compiler"; -import { BasicTestRunner } from "@typespec/compiler/testing"; +import { t } from "@typespec/compiler/testing"; import { $ } from "@typespec/compiler/typekit"; -import { beforeEach, expect, it } from "vitest"; -import { createHttpTestRunner } from "./../../test-host.js"; +import { expect, it } from "vitest"; +import { Tester } from "./../../test-host.js"; // Activate Http TypeKit augmentation import "../../../src/experimental/typekit/index.js"; -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createHttpTestRunner(); -}); - it("should return true for an error response", async () => { - const { getFoo } = (await runner.compile(` - @test model Foo { + const { getFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { id: int32; age: int32; name: string; } @error - @test model Error { + model ${t.model("Error")} { message: string; code: int32 } @route("/foo") @get - @test op getFoo(): Foo | Error; - `)) as { getFoo: Operation; Foo: Model; Error: Model }; - const tk = $(runner.program); + op ${t.op("getFoo")}(): Foo | Error; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(getFoo); const responses = tk.httpOperation.flattenResponses(httpOperation); @@ -41,24 +34,24 @@ it("should return true for an error response", async () => { }); it("should identify a single and default status code", async () => { - const { getFoo } = (await runner.compile(` - @test model Foo { + const { getFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { id: int32; age: int32; name: string; } @error - @test model Error { + model ${t.model("Error")} { message: string; code: int32 } @route("/foo") @get - @test op getFoo(): Foo | Error; - `)) as { getFoo: Operation; Foo: Model; Error: Model }; - const tk = $(runner.program); + op ${t.op("getFoo")}(): Foo | Error; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(getFoo); const responses = tk.httpOperation.flattenResponses(httpOperation); @@ -69,8 +62,8 @@ it("should identify a single and default status code", async () => { }); it("should identify a range status code", async () => { - const { getFoo } = (await runner.compile(` - @test model Foo { + const { getFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { id: int32; age: int32; name: string; @@ -78,16 +71,16 @@ it("should identify a range status code", async () => { } @error - @test model Error { + model ${t.model("Error")} { message: string; code: int32 } @route("/foo") @get - @test op getFoo(): Foo | Error; - `)) as { getFoo: Operation; Foo: Model; Error: Model }; - const tk = $(runner.program); + op ${t.op("getFoo")}(): Foo | Error; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(getFoo); const responses = tk.httpOperation.flattenResponses(httpOperation); diff --git a/packages/http/test/file.test.ts b/packages/http/test/file.test.ts index 68976c43ee7..4710323bcb4 100644 --- a/packages/http/test/file.test.ts +++ b/packages/http/test/file.test.ts @@ -656,7 +656,7 @@ describe("custom file model", () => { ], }, ], - runner, + program, } = await compileOperationsFull(` ${makeFileModel("x-filename")} op example(...SpecFile): SpecFile; @@ -670,7 +670,7 @@ describe("custom file model", () => { (p) => p.type === "header" && p.name === "x-filename", ); ok(requestXFilename); - ok(isHeader(runner.program, requestBody.type.properties.get("filename")!)); + ok(isHeader(program, requestBody.type.properties.get("filename")!)); strictEqual(responseBody?.bodyKind, "file"); expect(responseBody?.property).toStrictEqual(undefined); @@ -680,7 +680,7 @@ describe("custom file model", () => { (p) => p.kind === "header" && p.options.name === "x-filename", ); ok(responseXFilename); - ok(isHeader(runner.program, responseBody.type.properties.get("filename")!)); + ok(isHeader(program, responseBody.type.properties.get("filename")!)); }); it("extends aliased File with header", async () => { @@ -695,7 +695,7 @@ describe("custom file model", () => { ], }, ], - runner, + program, } = await compileOperationsFull(` model StringFile is Http.File; model JsonFile extends StringFile<"application/json"> { @@ -712,7 +712,7 @@ describe("custom file model", () => { (p) => p.type === "header" && p.name === "x-filename", ); ok(requestXFilename); - ok(isHeader(runner.program, requestBody.type.properties.get("filename")!)); + ok(isHeader(program, requestBody.type.properties.get("filename")!)); strictEqual(responseBody?.bodyKind, "file"); expect(responseBody?.property).toStrictEqual(undefined); @@ -722,7 +722,7 @@ describe("custom file model", () => { (p) => p.kind === "header" && p.options.name === "x-filename", ); ok(responseXFilename); - ok(isHeader(runner.program, responseBody.type.properties.get("filename")!)); + ok(isHeader(program, responseBody.type.properties.get("filename")!)); }); it("intersected payload upload and download", async () => { @@ -808,7 +808,7 @@ describe("custom file model", () => { }); it("allows interior metadata using bodyRoot", async () => { - const { operations, runner, diagnostics } = await compileOperationsFull(` + const { operations, program, diagnostics } = await compileOperationsFull(` ${makeFileModel("x-filename")} op example(@bodyRoot specFile: SpecFile): { @bodyRoot specFile: SpecFile }; `); @@ -835,7 +835,7 @@ describe("custom file model", () => { (p) => p.type === "header" && p.name === "x-filename", ); ok(requestXFilename); - ok(isHeader(runner.program, requestBody.type.properties.get("filename")!)); + ok(isHeader(program, requestBody.type.properties.get("filename")!)); strictEqual(responseBody?.bodyKind, "file"); ok(responseBody.property); @@ -846,7 +846,7 @@ describe("custom file model", () => { (p) => p.kind === "header" && p.options.name === "x-filename", ); ok(responseXFilename); - ok(isHeader(runner.program, responseBody.type.properties.get("filename")!)); + ok(isHeader(program, responseBody.type.properties.get("filename")!)); }); describe("multipart", () => { @@ -863,7 +863,7 @@ describe("custom file model", () => { }, ], diagnostics, - runner, + program, } = await compileOperationsFull(` ${makeFileModel("x-filename")} op example(@multipartBody fields: { file: HttpPart }) : { @multipartBody fields: { file: HttpPart}}; @@ -884,7 +884,7 @@ describe("custom file model", () => { (p) => p.options.name === "x-filename", ); ok(requestXFilename); - ok(isHeader(runner.program, requestPartBody.type.properties.get("filename")!)); + ok(isHeader(program, requestPartBody.type.properties.get("filename")!)); strictEqual(multipartResponseBody?.bodyKind, "multipart"); ok(multipartResponseBody?.property); @@ -899,7 +899,7 @@ describe("custom file model", () => { (p) => p.options.name === "x-filename", ); ok(responseXFilename); - ok(isHeader(runner.program, responsePartBody.type.properties.get("filename")!)); + ok(isHeader(program, responsePartBody.type.properties.get("filename")!)); }); it("intersect payload form-data upload and download", async () => { @@ -915,7 +915,7 @@ describe("custom file model", () => { }, ], diagnostics, - runner, + program, } = await compileOperationsFull(` ${makeFileModel("x-filename")} op example(@multipartBody fields: { file: HttpPart }) : { @multipartBody fields: { file: HttpPart};}; @@ -936,12 +936,12 @@ describe("custom file model", () => { (p) => p.options.name === "x-filename", ); ok(requestXFilename); - ok(isHeader(runner.program, requestPartBody.type.properties.get("filename")!)); + ok(isHeader(program, requestPartBody.type.properties.get("filename")!)); const requestXFoo = multipartRequestBody?.parts[0].headers.find( (p) => p.options.name === "x-foo", ); ok(requestXFoo); - ok(isHeader(runner.program, requestPartBody.type.properties.get("xFoo")!)); + ok(isHeader(program, requestPartBody.type.properties.get("xFoo")!)); strictEqual(multipartResponseBody?.bodyKind, "multipart"); ok(multipartResponseBody?.property); @@ -956,12 +956,12 @@ describe("custom file model", () => { (p) => p.options.name === "x-filename", ); ok(responseXFilename); - ok(isHeader(runner.program, responsePartBody.type.properties.get("filename")!)); + ok(isHeader(program, responsePartBody.type.properties.get("filename")!)); const responseXBar = multipartResponseBody?.parts[0].headers.find( (p) => p.options.name === "x-bar", ); ok(responseXBar); - ok(isHeader(runner.program, responsePartBody.type.properties.get("xBar")!)); + ok(isHeader(program, responsePartBody.type.properties.get("xBar")!)); }); }); }); diff --git a/packages/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts index 926b1270a3d..1e88a9f7c63 100644 --- a/packages/http/test/http-decorators.test.ts +++ b/packages/http/test/http-decorators.test.ts @@ -1,11 +1,7 @@ -import { ModelProperty, Namespace } from "@typespec/compiler"; -import { - BasicTestRunner, - expectDiagnosticEmpty, - expectDiagnostics, -} from "@typespec/compiler/testing"; +import { ModelProperty } from "@typespec/compiler"; +import { expectDiagnosticEmpty, expectDiagnostics, t } from "@typespec/compiler/testing"; import { deepStrictEqual, ok, strictEqual } from "assert"; -import { beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { getAuthentication, getCookieParamOptions, @@ -27,18 +23,13 @@ import { isStatusCode, } from "../src/decorators.js"; import { includeInapplicableMetadataInPayload } from "../src/private.decorators.js"; -import { createHttpTestRunner } from "./test-host.js"; -describe("http: decorators", () => { - let runner: BasicTestRunner; - - beforeEach(async () => { - runner = await createHttpTestRunner(); - }); +import { Tester } from "./test-host.js"; +describe("http: decorators", () => { describe("emit diagnostic if passing arguments to verb decorators", () => { ["get", "post", "put", "delete", "head"].forEach((verb) => { it(`@${verb}`, async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @${verb}("/test") op test(): string; `); @@ -50,7 +41,7 @@ describe("http: decorators", () => { }); it(`@patch`, async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @patch("/test") op test(): string; `); @@ -63,7 +54,7 @@ describe("http: decorators", () => { describe("@header", () => { it("emit diagnostics when @header is not used on model property", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @header op test(): string; @header model Foo {} @@ -84,7 +75,7 @@ describe("http: decorators", () => { }); it("emit diagnostics when header name is not a string or of value HeaderOptions", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` op test(@header(123) MyHeader: string): string; op test2(@header(#{ name: 123 }) MyHeader: string): string; op test3(@header(#{ format: "invalid" }) MyHeader: string): string; @@ -108,51 +99,51 @@ describe("http: decorators", () => { }); it("generate header name from property name", async () => { - const { MyHeader } = await runner.compile(` - op test(@test @header MyHeader: string): string; + const { MyHeader, program } = await Tester.compile(t.code` + op test(@header ${t.modelProperty("MyHeader")}: string): string; `); - ok(isHeader(runner.program, MyHeader)); - strictEqual(getHeaderFieldName(runner.program, MyHeader), "my-header"); + ok(isHeader(program, MyHeader)); + strictEqual(getHeaderFieldName(program, MyHeader), "my-header"); }); it("override header name with 1st parameter", async () => { - const { MyHeader } = await runner.compile(` - op test(@test @header("x-my-header") MyHeader: string): string; + const { MyHeader, program } = await Tester.compile(t.code` + op test( @header("x-my-header") ${t.modelProperty("MyHeader")}: string): string; `); - strictEqual(getHeaderFieldName(runner.program, MyHeader), "x-my-header"); + strictEqual(getHeaderFieldName(program, MyHeader), "x-my-header"); }); it("override header with HeaderOptions", async () => { - const { SingleString } = await runner.compile(` - @put op test(@test @header(#{name: "x-single-string"}) SingleString: string): string; + const { SingleString, program } = await Tester.compile(t.code` + @put op test(@header(#{name: "x-single-string"}) ${t.modelProperty("SingleString")}: string): string; `); - deepStrictEqual(getHeaderFieldOptions(runner.program, SingleString), { + deepStrictEqual(getHeaderFieldOptions(program, SingleString), { type: "header", name: "x-single-string", }); - strictEqual(getHeaderFieldName(runner.program, SingleString), "x-single-string"); + strictEqual(getHeaderFieldName(program, SingleString), "x-single-string"); }); it("specify explode", async () => { - const { MyHeader } = await runner.compile(` - @put op test(@test @header(#{ explode: true }) MyHeader: string): string; + const { MyHeader, program } = await Tester.compile(t.code` + @put op test(@header(#{ explode: true }) ${t.modelProperty("MyHeader")}: string): string; `); - deepStrictEqual(getHeaderFieldOptions(runner.program, MyHeader), { + deepStrictEqual(getHeaderFieldOptions(program, MyHeader), { type: "header", name: "my-header", explode: true, }); - strictEqual(getHeaderFieldName(runner.program, MyHeader), "my-header"); + strictEqual(getHeaderFieldName(program, MyHeader), "my-header"); }); }); describe("@cookie", () => { it("emit diagnostics when @cookie is not used on model property", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @cookie op test(): string; @cookie model Foo {} @@ -173,10 +164,10 @@ describe("http: decorators", () => { }); it("emit diagnostics when cookie name is not a string or of type CookieOptions", async () => { - const diagnostics = await runner.diagnose(` - op test(@cookie(123) MyCookie: string): string; - op test2(@cookie(#{ name: 123 }) MyCookie: string): string; - op test3(@cookie(#{ format: "invalid" }) MyCookie: string): string; + const diagnostics = await Tester.diagnose(` + op test(@cookie(123) myCookie: string): string; + op test2(@cookie(#{ name: 123 })myCookie: string): string; + op test3(@cookie(#{ format: "invalid" }) myCookie: string): string; `); expectDiagnostics(diagnostics, [ @@ -193,34 +184,34 @@ describe("http: decorators", () => { }); it("generate cookie name from property name", async () => { - const { myCookie } = await runner.compile(` - op test(@test @cookie myCookie: string): string; + const { myCookie, program } = await Tester.compile(t.code` + op test(@cookie ${t.modelProperty("myCookie")}: string): string; `); - ok(isCookieParam(runner.program, myCookie)); - strictEqual(getCookieParamOptions(runner.program, myCookie)?.name, "my_cookie"); + ok(isCookieParam(program, myCookie)); + strictEqual(getCookieParamOptions(program, myCookie)?.name, "my_cookie"); }); it("override cookie name with 1st parameter", async () => { - const { myCookie } = await runner.compile(` - op test(@test @cookie("my-cookie") myCookie: string): string; + const { myCookie, program } = await Tester.compile(t.code` + op test(@cookie("my-cookie") ${t.modelProperty("myCookie")}: string): string; `); - strictEqual(getCookieParamOptions(runner.program, myCookie)?.name, "my-cookie"); + strictEqual(getCookieParamOptions(program, myCookie)?.name, "my-cookie"); }); it("override cookie with CookieOptions", async () => { - const { myCookie } = await runner.compile(` - op test(@test @cookie(#{name: "my-cookie"}) myCookie: string): string; + const { myCookie, program } = await Tester.compile(t.code` + op test(@cookie(#{name: "my-cookie"}) ${t.modelProperty("myCookie")}: string): string; `); - strictEqual(getCookieParamOptions(runner.program, myCookie)?.name, "my-cookie"); + strictEqual(getCookieParamOptions(program, myCookie)?.name, "my-cookie"); }); }); describe("@query", () => { it("emit diagnostics when @query is not used on model property", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @query op test(): string; @query model Foo {} @@ -241,7 +232,7 @@ describe("http: decorators", () => { }); it("emit diagnostics when query name is not a string or of type QueryOptions", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` op test(@query(123) MyQuery: string): string; op test2(@query(#{name: 123}) MyQuery: string): string; op test3(@query(#{format: "invalid"}) MyQuery: string): string; @@ -261,27 +252,27 @@ describe("http: decorators", () => { }); it("generate query name from property name", async () => { - const { select } = await runner.compile(` - op test(@test @query select: string): string; + const { select, program } = await Tester.compile(t.code` + op test(@query ${t.modelProperty("select")}: string): string; `); - ok(isQueryParam(runner.program, select)); - strictEqual(getQueryParamName(runner.program, select), "select"); + ok(isQueryParam(program, select)); + strictEqual(getQueryParamName(program, select), "select"); }); it("override query name with 1st parameter", async () => { - const { select } = await runner.compile(` - op test(@test @query("$select") select: string): string; + const { select, program } = await Tester.compile(t.code` + op test(@query("$select") ${t.modelProperty("select")}: string): string; `); - strictEqual(getQueryParamName(runner.program, select), "$select"); + strictEqual(getQueryParamName(program, select), "$select"); }); it("specify explode: true", async () => { - const { selects } = await runner.compile(` - op test(@test @query(#{ explode: true }) selects: string[]): string; + const { selects, program } = await Tester.compile(t.code` + op test(@query(#{ explode: true }) ${t.modelProperty("selects")}: string[]): string; `); - expect(getQueryParamOptions(runner.program, selects)).toEqual({ + expect(getQueryParamOptions(program, selects)).toEqual({ type: "query", name: "selects", explode: true, @@ -291,7 +282,7 @@ describe("http: decorators", () => { describe("@route", () => { it("emit diagnostics when duplicated unshared routes are applied", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @route("/test") op test(): string; @route("/test") op test2(): string; `); @@ -309,7 +300,7 @@ describe("http: decorators", () => { }); it("emit diagnostics when not all duplicated routes are declared shared on each op conflicting", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @route("/test") @sharedRoute op test(): string; @route("/test") @sharedRoute op test2(): string; @route("/test") op test3(): string; @@ -331,7 +322,7 @@ describe("http: decorators", () => { }); it("do not emit diagnostics when duplicated shared routes are applied", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @route("/test") @sharedRoute op test(): string; @route("/test") @sharedRoute op test2(): string; `); @@ -340,7 +331,7 @@ describe("http: decorators", () => { }); it("do not emit diagnostics routes sharing path but not same verb", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @route("/test") @sharedRoute op test(): string; @route("/test") @sharedRoute op test2(): string; @route("/test") @post op test3(): string; @@ -351,7 +342,7 @@ describe("http: decorators", () => { describe("@path", () => { it("emit diagnostics when @path is not used on model property", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @path op test(): string; @path model Foo {} @@ -372,7 +363,7 @@ describe("http: decorators", () => { }); it("accept optional path when specified at the root of @route", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @route("{/myPath}") op test(@path myPath?: string): string; `); @@ -380,7 +371,7 @@ describe("http: decorators", () => { }); it("accept optional path when specified in route", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @route("base{/myPath}") op test(@path myPath?: string): string; `); @@ -388,7 +379,7 @@ describe("http: decorators", () => { }); it("accept optional path when not used as operation parameter", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @route("/") op test(): {@path myPath?: string}; `); @@ -396,7 +387,7 @@ describe("http: decorators", () => { }); it("emit diagnostics when path name is not a string", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` op test(@path(123) MyPath: string): string; `); @@ -408,33 +399,33 @@ describe("http: decorators", () => { }); it("generate path name from property name", async () => { - const { select } = await runner.compile(` - op test(@test @path select: string): string; + const { select, program } = await Tester.compile(t.code` + op test(@path ${t.modelProperty("select")}: string): string; `); - ok(isPathParam(runner.program, select)); - strictEqual(getPathParamName(runner.program, select), "select"); + ok(isPathParam(program, select)); + strictEqual(getPathParamName(program, select), "select"); }); it("override path name with 1st parameter", async () => { - const { select } = await runner.compile(` - op test(@test @path("$select") select: string): string; + const { select, program } = await Tester.compile(t.code` + op test(@path("$select") ${t.modelProperty("select")}: string): string; `); - deepStrictEqual(getPathParamOptions(runner.program, select), { + deepStrictEqual(getPathParamOptions(program, select), { type: "path", name: "$select", allowReserved: false, explode: false, style: "simple", }); - strictEqual(getPathParamName(runner.program, select), "$select"); + strictEqual(getPathParamName(program, select), "$select"); }); }); describe("@body", () => { it("emit diagnostics when @body is not used on model property", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @body op test(): string; @body model Foo {} @@ -455,17 +446,17 @@ describe("http: decorators", () => { }); it("set the body with @body", async () => { - const { body } = await runner.compile(` - @post op test(@test @body body: string): string; + const { body, program } = await Tester.compile(t.code` + @post op test(@body ${t.modelProperty("body")}: string): string; `); - ok(isBody(runner.program, body)); + ok(isBody(program, body)); }); }); describe("@bodyRoot", () => { it("emit diagnostics when @body is not used on model property", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @bodyRoot op test(): string; @bodyRoot model Foo {} @@ -486,17 +477,17 @@ describe("http: decorators", () => { }); it("set the body root with @bodyRoot", async () => { - const { body } = (await runner.compile(` - @post op test(@test @bodyRoot body: string): string; - `)) as { body: ModelProperty }; + const { body, program } = await Tester.compile(t.code` + @post op test(@bodyRoot ${t.modelProperty("body")}: string): string; + `); - ok(isBodyRoot(runner.program, body)); + ok(isBodyRoot(program, body)); }); }); describe("@bodyIgnore", () => { it("emit diagnostics when @body is not used on model property", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @bodyIgnore op test(): string; @bodyIgnore model Foo {} @@ -517,17 +508,17 @@ describe("http: decorators", () => { }); it("isBodyIgnore returns true on property decorated", async () => { - const { body } = await runner.compile(` - @post op test(@test @bodyIgnore body: string): string; + const { body, program } = await Tester.compile(t.code` + @post op test(@bodyIgnore ${t.modelProperty("body")}: string): string; `); - ok(isBodyIgnore(runner.program, body as ModelProperty)); + ok(isBodyIgnore(program, body)); }); }); describe("@statusCode", () => { it("emit diagnostics when @statusCode is not used on model property", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @statusCode op test(): string; @statusCode model Foo {} @@ -548,7 +539,7 @@ describe("http: decorators", () => { }); it("emits error if multiple properties are decorated with `@statusCode` in return type", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` model CreatedOrUpdatedResponse { @statusCode ok: "200"; @@ -567,7 +558,7 @@ describe("http: decorators", () => { }); it("emits error if multiple `@statusCode` decorators are composed together", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` model CustomUnauthorizedResponse { @statusCode _: 401; @@ -590,30 +581,30 @@ describe("http: decorators", () => { }); it("set numeric statusCode with @statusCode", async () => { - const { code } = (await runner.compile(` + const { code, program } = await Tester.compile(t.code` op test(): { - @test @statusCode code: 201 + @statusCode ${t.modelProperty("code")}: 201 }; - `)) as { code: ModelProperty }; + `); - ok(isStatusCode(runner.program, code)); - deepStrictEqual(getStatusCodes(runner.program, code), [201]); + ok(isStatusCode(program, code)); + deepStrictEqual(getStatusCodes(program, code), [201]); }); it("set range statusCode with @statusCode", async () => { - const { code } = (await runner.compile(` + const { code, program } = await Tester.compile(t.code` op test(): { - @test @statusCode @minValue(200) @maxValue(299) code: int32; + @statusCode @minValue(200) @maxValue(299) ${t.modelProperty("code")}: int32; }; - `)) as { code: ModelProperty }; + `); - ok(isStatusCode(runner.program, code)); - deepStrictEqual(getStatusCodes(runner.program, code), [{ start: 200, end: 299 }]); + ok(isStatusCode(program, code)); + deepStrictEqual(getStatusCodes(program, code), [{ start: 200, end: 299 }]); }); describe("invalid status codes", () => { async function checkInvalid(code: string, message: string) { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` op test(): { @statusCode code: ${code} }; @@ -633,7 +624,7 @@ describe("http: decorators", () => { describe("@server", () => { it("emit diagnostics when @server is not used on namespace", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @server("https://example.com", "MyServer") op test(): string; @server("https://example.com", "MyServer") model Foo {} @@ -652,7 +643,7 @@ describe("http: decorators", () => { }); it("emit diagnostics when url is not a string", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @server(123, "MyServer") namespace MyService {} `); @@ -663,7 +654,7 @@ describe("http: decorators", () => { }); it("emit diagnostics when description is not a string", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @server("https://example.com", 123) namespace MyService {} `); @@ -674,7 +665,7 @@ describe("http: decorators", () => { }); it("emit diagnostics when parameters is not a model", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @server("https://example.com", "My service url", 123) namespace MyService {} `); @@ -685,7 +676,7 @@ describe("http: decorators", () => { }); it("emit diagnostics if url has parameters that is not specified in model", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @server("https://example.com/{name}/foo", "My service url", {other: string}) namespace MyService {} `); @@ -697,12 +688,12 @@ describe("http: decorators", () => { }); it("define a simple server without description", async () => { - const { MyService } = (await runner.compile(` + const { MyService, program } = await Tester.compile(t.code` @server("https://example.com") - @test namespace MyService {} - `)) as { MyService: Namespace }; + namespace ${t.namespace("MyService")} {} + `); - const servers = getServers(runner.program, MyService); + const servers = getServers(program, MyService); deepStrictEqual(servers, [ { description: undefined, @@ -713,12 +704,12 @@ describe("http: decorators", () => { }); it("define a simple server with a fixed url", async () => { - const { MyService } = (await runner.compile(` + const { MyService, program } = await Tester.compile(t.code` @server("https://example.com", "My service url") - @test namespace MyService {} - `)) as { MyService: Namespace }; + namespace ${t.namespace("MyService")} {} + `); - const servers = getServers(runner.program, MyService); + const servers = getServers(program, MyService); deepStrictEqual(servers, [ { description: "My service url", @@ -729,16 +720,16 @@ describe("http: decorators", () => { }); it("define a server with parameters", async () => { - const { MyService, NameParam } = (await runner.compile(` + const { MyService, NameParam, program } = await Tester.compile(t.code` @server("https://example.com/{name}/foo", "My service url", {@test("NameParam") name: string }) - @test namespace MyService {} - `)) as { MyService: Namespace; NameParam: ModelProperty }; + namespace ${t.namespace("MyService")} {} + `); - const servers = getServers(runner.program, MyService); + const servers = getServers(program, MyService); deepStrictEqual(servers, [ { description: "My service url", - parameters: new Map([["name", NameParam]]), + parameters: new Map([["name", NameParam as any]]), url: "https://example.com/{name}/foo", }, ]); @@ -747,7 +738,7 @@ describe("http: decorators", () => { describe("@useAuth", () => { it("emit diagnostics when config is not a model, tuple or union", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @useAuth(anOp) namespace Foo {} @@ -760,7 +751,7 @@ describe("http: decorators", () => { }); it("emit diagnostic when OAuth2 flow is not a valid model", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @useAuth(OAuth2Auth<["foo"]>) namespace Foo {} @@ -780,12 +771,12 @@ describe("http: decorators", () => { }); it("can specify BasicAuth", async () => { - const { Foo } = (await runner.compile(` + const { Foo, program } = await Tester.compile(t.code` @useAuth(BasicAuth) - @test namespace Foo {} - `)) as { Foo: Namespace }; + namespace ${t.namespace("Foo")} {} + `); - expect(getAuthentication(runner.program, Foo)).toEqual({ + expect(getAuthentication(program, Foo)).toEqual({ options: [ { schemes: [ @@ -802,14 +793,14 @@ describe("http: decorators", () => { }); it("can specify custom auth name with description", async () => { - const { Foo } = (await runner.compile(` + const { Foo, program } = await Tester.compile(t.code` @doc("My custom basic auth") model MyAuth is BasicAuth; @useAuth(MyAuth) - @test namespace Foo {} - `)) as { Foo: Namespace }; + namespace ${t.namespace("Foo")} {} + `); - expect(getAuthentication(runner.program, Foo)).toEqual({ + expect(getAuthentication(program, Foo)).toEqual({ options: [ { schemes: [ @@ -827,12 +818,12 @@ describe("http: decorators", () => { }); it("can specify BearerAuth", async () => { - const { Foo } = (await runner.compile(` + const { Foo, program } = await Tester.compile(t.code` @useAuth(BearerAuth) - @test namespace Foo {} - `)) as { Foo: Namespace }; + namespace ${t.namespace("Foo")} {} + `); - expect(getAuthentication(runner.program, Foo)).toEqual({ + expect(getAuthentication(program, Foo)).toEqual({ options: [ { schemes: [ @@ -849,12 +840,12 @@ describe("http: decorators", () => { }); it("can specify ApiKeyAuth", async () => { - const { Foo } = (await runner.compile(` + const { Foo, program } = await Tester.compile(t.code` @useAuth(ApiKeyAuth) - @test namespace Foo {} - `)) as { Foo: Namespace }; + namespace ${t.namespace("Foo")} {} + `); - expect(getAuthentication(runner.program, Foo)).toEqual({ + expect(getAuthentication(program, Foo)).toEqual({ options: [ { schemes: [ @@ -872,7 +863,7 @@ describe("http: decorators", () => { }); it("can specify OAuth2", async () => { - const { Foo } = (await runner.compile(` + const { Foo, program } = await Tester.compile(t.code` model MyFlow { type: OAuth2FlowType.implicit; authorizationUrl: "https://api.example.com/oauth2/authorize"; @@ -880,10 +871,10 @@ describe("http: decorators", () => { scopes: ["read", "write"]; } @useAuth(OAuth2Auth<[MyFlow]>) - @test namespace Foo {} - `)) as { Foo: Namespace }; + namespace ${t.namespace("Foo")} {} + `); - expect(getAuthentication(runner.program, Foo)).toEqual({ + expect(getAuthentication(program, Foo)).toEqual({ options: [ { schemes: [ @@ -907,7 +898,7 @@ describe("http: decorators", () => { }); it("can specify OAuth2 with scopes, which are default for every flow", async () => { - const { Foo } = (await runner.compile(` + const { Foo, program } = await Tester.compile(t.code` alias MyAuth = OAuth2Auth { }], Scopes=T>; @useAuth(MyAuth<["read", "write"]>) - @test namespace Foo {} - `)) as { Foo: Namespace }; + namespace ${t.namespace("Foo")} {} + `); - expect(getAuthentication(runner.program, Foo)).toEqual({ + expect(getAuthentication(program, Foo)).toEqual({ options: [ { schemes: [ @@ -942,12 +933,12 @@ describe("http: decorators", () => { }); it("can specify NoAuth", async () => { - const { Foo } = (await runner.compile(` + const { Foo, program } = await Tester.compile(t.code` @useAuth(NoAuth) - @test namespace Foo {} - `)) as { Foo: Namespace }; + namespace ${t.namespace("Foo")} {} + `); - expect(getAuthentication(runner.program, Foo)).toEqual({ + expect(getAuthentication(program, Foo)).toEqual({ options: [ { schemes: [ @@ -963,12 +954,12 @@ describe("http: decorators", () => { }); it("can specify multiple auth options", async () => { - const { Foo } = (await runner.compile(` + const { Foo, program } = await Tester.compile(t.code` @useAuth(BasicAuth | BearerAuth) - @test namespace Foo {} - `)) as { Foo: Namespace }; + namespace ${t.namespace("Foo")} {} + `); - expect(getAuthentication(runner.program, Foo)).toEqual({ + expect(getAuthentication(program, Foo)).toEqual({ options: [ { schemes: [ @@ -995,12 +986,12 @@ describe("http: decorators", () => { }); it("can specify multiple auth schemes to be used together", async () => { - const { Foo } = (await runner.compile(` + const { Foo, program } = await Tester.compile(t.code` @useAuth([BasicAuth, BearerAuth]) - @test namespace Foo {} - `)) as { Foo: Namespace }; + namespace ${t.namespace("Foo")} {} + `); - expect(getAuthentication(runner.program, Foo)).toEqual({ + expect(getAuthentication(program, Foo)).toEqual({ options: [ { schemes: [ @@ -1023,12 +1014,12 @@ describe("http: decorators", () => { }); it("can specify multiple auth schemes to be used together and multiple options", async () => { - const { Foo } = (await runner.compile(` + const { Foo, program } = await Tester.compile(t.code` @useAuth(BearerAuth | [ApiKeyAuth, BasicAuth]) - @test namespace Foo {} - `)) as { Foo: Namespace }; + namespace ${t.namespace("Foo")} {} + `); - expect(getAuthentication(runner.program, Foo)).toEqual({ + expect(getAuthentication(program, Foo)).toEqual({ options: [ { schemes: [ @@ -1062,16 +1053,16 @@ describe("http: decorators", () => { }); it("can override auth schemes on interface", async () => { - const { Foo } = (await runner.compile(` + const { Foo, program } = await Tester.compile(t.code` alias ServiceKeyAuth = ApiKeyAuth; @useAuth(ServiceKeyAuth) - @test namespace Foo { + namespace ${t.namespace("Foo")} { @useAuth(BasicAuth | BearerAuth) interface Bar { } } - `)) as { Foo: Namespace }; + `); - expect(getAuthentication(runner.program, Foo.interfaces.get("Bar")!)).toEqual({ + expect(getAuthentication(program, Foo.interfaces.get("Bar")!)).toEqual({ options: [ { schemes: [ @@ -1098,16 +1089,16 @@ describe("http: decorators", () => { }); it("can override auth schemes on operation", async () => { - const { Foo } = (await runner.compile(` + const { Foo, program } = await Tester.compile(t.code` alias ServiceKeyAuth = ApiKeyAuth; @useAuth(ServiceKeyAuth) - @test namespace Foo { + namespace ${t.namespace("Foo")} { @useAuth([BasicAuth, BearerAuth]) op bar(): void; } - `)) as { Foo: Namespace }; + `); - expect(getAuthentication(runner.program, Foo.operations.get("bar")!)).toEqual({ + expect(getAuthentication(program, Foo.operations.get("bar")!)).toEqual({ options: [ { schemes: [ @@ -1132,67 +1123,52 @@ describe("http: decorators", () => { describe("@includeInapplicableMetadataInPayload", () => { it("defaults to true", async () => { - const { M } = await runner.compile(` + const { M, program } = await Tester.compile(t.code` namespace Foo; - @test model M {p: string; } + model ${t.model("M")} {p: string; } `); strictEqual(M.kind, "Model" as const); - strictEqual( - includeInapplicableMetadataInPayload(runner.program, M.properties.get("p")!), - true, - ); + strictEqual(includeInapplicableMetadataInPayload(program, M.properties.get("p")!), true); }); it("can specify at namespace level", async () => { - const { M } = await runner.compile(` + const { M, program } = await Tester.compile(t.code` @Private.includeInapplicableMetadataInPayload(false) namespace Foo; - @test model M {p: string; } + model ${t.model("M")} {p: string; } `); strictEqual(M.kind, "Model" as const); - strictEqual( - includeInapplicableMetadataInPayload(runner.program, M.properties.get("p")!), - false, - ); + strictEqual(includeInapplicableMetadataInPayload(program, M.properties.get("p")!), false); }); it("can specify at model level", async () => { - const { M } = await runner.compile(` + const { M, program } = await Tester.compile(t.code` namespace Foo; - @Private.includeInapplicableMetadataInPayload(false) @test model M { p: string; } + @Private.includeInapplicableMetadataInPayload(false) model ${t.model("M")} { p: string; } `); strictEqual(M.kind, "Model" as const); - strictEqual( - includeInapplicableMetadataInPayload(runner.program, M.properties.get("p")!), - false, - ); + strictEqual(includeInapplicableMetadataInPayload(program, M.properties.get("p")!), false); }); it("can specify at property level", async () => { - const { M } = await runner.compile(` + const { M, program } = await Tester.compile(t.code` namespace Foo; - @test model M { @Private.includeInapplicableMetadataInPayload(false) p: string; } + model ${t.model("M")} { @Private.includeInapplicableMetadataInPayload(false) p: string; } `); strictEqual(M.kind, "Model" as const); - strictEqual( - includeInapplicableMetadataInPayload(runner.program, M.properties.get("p")!), - false, - ); + strictEqual(includeInapplicableMetadataInPayload(program, M.properties.get("p")!), false); }); it("can be overridden", async () => { - const { M } = await runner.compile(` + const { M, program } = await Tester.compile(t.code` @Private.includeInapplicableMetadataInPayload(false) namespace Foo; - @Private.includeInapplicableMetadataInPayload(true) @test model M { p: string; } + @Private.includeInapplicableMetadataInPayload(true) model ${t.model("M")} { p: string; } `); strictEqual(M.kind, "Model" as const); - strictEqual( - includeInapplicableMetadataInPayload(runner.program, M.properties.get("p")!), - true, - ); + strictEqual(includeInapplicableMetadataInPayload(program, M.properties.get("p")!), true); }); }); }); diff --git a/packages/http/test/merge-patch.test.ts b/packages/http/test/merge-patch.test.ts index 646e182c405..948ca8729c0 100644 --- a/packages/http/test/merge-patch.test.ts +++ b/packages/http/test/merge-patch.test.ts @@ -1,8 +1,8 @@ import { Diagnostic, Model, ModelProperty, Program, Type, TypeKind } from "@typespec/compiler"; import { - BasicTestRunner, expectDiagnosticEmpty, expectDiagnostics, + TesterInstance, } from "@typespec/compiler/testing"; import { $ } from "@typespec/compiler/typekit"; import { deepStrictEqual, ok } from "assert"; @@ -14,15 +14,11 @@ import { } from "../src/experimental/merge-patch/helpers.js"; import { getAllHttpServices } from "../src/operations.js"; import { HttpOperation, RouteResolutionOptions } from "../src/types.js"; -import { - createHttpTestRunner, - diagnoseOperations, - getOperationsWithServiceNamespace, -} from "./test-host.js"; +import { diagnoseOperations, getOperationsWithServiceNamespace, Tester } from "./test-host.js"; -let runner: BasicTestRunner; +let runner: TesterInstance; beforeEach(async () => { - runner = await createHttpTestRunner(); + runner = await Tester.createInstance(); }); function checkNullableUnion(program: Program, union: Type): boolean { @@ -69,16 +65,13 @@ function isNullableUnion(property: ModelProperty) { return property; } async function compileAndDiagnoseWithRunner( - runner: BasicTestRunner, + runner: TesterInstance, code: string, options?: RouteResolutionOptions, ): Promise<[HttpOperation[], readonly Diagnostic[]]> { await runner.compileAndDiagnose( `@service(#{title: "Test Service"}) namespace TestService; ${code}`, - { - noEmit: true, - }, ); const [services] = getAllHttpServices(runner.program, options); return [services[0].operations, runner.program.diagnostics]; diff --git a/packages/http/test/overloads.test.ts b/packages/http/test/overloads.test.ts index 6211e6c078f..19b6681be4a 100644 --- a/packages/http/test/overloads.test.ts +++ b/packages/http/test/overloads.test.ts @@ -1,30 +1,23 @@ -import { Operation } from "@typespec/compiler"; -import { BasicTestRunner, expectDiagnostics } from "@typespec/compiler/testing"; +import { expectDiagnostics, t } from "@typespec/compiler/testing"; import { strictEqual } from "assert"; -import { beforeEach, describe, it } from "vitest"; +import { describe, it } from "vitest"; import { getHttpOperation, listHttpOperationsIn } from "../src/index.js"; -import { createHttpTestRunner } from "./test-host.js"; +import { Tester } from "./test-host.js"; describe("http: overloads", () => { - let runner: BasicTestRunner; - - beforeEach(async () => { - runner = await createHttpTestRunner(); - }); - it("overloads inherit base overload route and verb", async () => { - const { uploadString, uploadBytes } = (await runner.compile(` + const { uploadString, uploadBytes, program } = await Tester.compile(t.code` @route("/upload") @put op upload(data: string | bytes, @header contentType: "text/plain" | "application/octet-stream"): void; @overload(upload) - @test op uploadString(data: string, @header contentType: "text/plain" ): void; + op ${t.op("uploadString")}(data: string, @header contentType: "text/plain" ): void; @overload(upload) - @test op uploadBytes(data: bytes, @header contentType: "application/octet-stream"): void; - `)) as { uploadString: Operation; uploadBytes: Operation }; + op ${t.op("uploadBytes")}(data: bytes, @header contentType: "application/octet-stream"): void; + `); - const [uploadStringHttp] = getHttpOperation(runner.program, uploadString); - const [uploadBytesHttp] = getHttpOperation(runner.program, uploadBytes); + const [uploadStringHttp] = getHttpOperation(program, uploadString); + const [uploadBytesHttp] = getHttpOperation(program, uploadBytes); strictEqual(uploadStringHttp.path, "/upload"); strictEqual(uploadStringHttp.verb, "put"); @@ -33,20 +26,20 @@ describe("http: overloads", () => { }); it("overloads can change their route or verb", async () => { - const { upload, uploadString, uploadBytes } = (await runner.compile(` + const { upload, uploadString, uploadBytes, program } = await Tester.compile(t.code` @route("/upload") @put - @test op upload(data: string | bytes, @header contentType: "text/plain" | "application/octet-stream"): void; + op ${t.op("upload")}(data: string | bytes, @header contentType: "text/plain" | "application/octet-stream"): void; @overload(upload) @route("/uploadString") - @test op uploadString(data: string, @header contentType: "text/plain" ): void; + op ${t.op("uploadString")}(data: string, @header contentType: "text/plain" ): void; @overload(upload) - @post @test op uploadBytes(data: bytes, @header contentType: "application/octet-stream"): void; - `)) as { upload: Operation; uploadString: Operation; uploadBytes: Operation }; + @post op ${t.op("uploadBytes")}(data: bytes, @header contentType: "application/octet-stream"): void; + `); - const [uploadHttp] = getHttpOperation(runner.program, upload); - const [uploadStringHttp] = getHttpOperation(runner.program, uploadString); - const [uploadBytesHttp] = getHttpOperation(runner.program, uploadBytes); + const [uploadHttp] = getHttpOperation(program, upload); + const [uploadStringHttp] = getHttpOperation(program, uploadString); + const [uploadBytesHttp] = getHttpOperation(program, uploadBytes); strictEqual(uploadHttp.path, "/upload"); strictEqual(uploadHttp.verb, "put"); @@ -61,7 +54,7 @@ describe("http: overloads", () => { }); it("links overloads", async () => { - await runner.compile(` + const { program } = await Tester.compile(t.code` @route("/upload") @put op upload(data: string | bytes, @header contentType: "text/plain" | "application/octet-stream"): void; @@ -72,8 +65,8 @@ describe("http: overloads", () => { `); const [[overload, uploadString, uploadBytes]] = listHttpOperationsIn( - runner.program, - runner.program.getGlobalNamespaceType(), + program, + program.getGlobalNamespaceType(), ); strictEqual(uploadString.overloading, overload); @@ -83,7 +76,7 @@ describe("http: overloads", () => { }); it("overload base route should still be unique with other operations", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @route("/upload") op otherUpload(data: bytes): void; @@ -107,7 +100,7 @@ describe("http: overloads", () => { }); it("overloads route should still be unique with other operations", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @route("/uploadString") op otherUploadString(data: string): void; diff --git a/packages/http/test/plaindata.test.ts b/packages/http/test/plaindata.test.ts index 18ad55dd53e..a041a91ce68 100644 --- a/packages/http/test/plaindata.test.ts +++ b/packages/http/test/plaindata.test.ts @@ -1,62 +1,37 @@ -import { TestHost } from "@typespec/compiler/testing"; +import { t } from "@typespec/compiler/testing"; import { ok, strictEqual } from "assert"; -import { beforeEach, describe, it } from "vitest"; +import { describe, it } from "vitest"; import { isBody, isHeader, isPathParam, isQueryParam } from "../src/decorators.js"; -import { createHttpTestHost } from "./test-host.js"; +import { Tester } from "./test-host.js"; describe("http: plain data", () => { - let testHost: TestHost; - - beforeEach(async () => { - testHost = await createHttpTestHost(); - }); - it("removes header/query/body/path", async () => { - testHost.addTypeSpecFile( - "main.tsp", - ` - import "@typespec/http"; - using Http; - - @test - model Before { + const { Before, After, Spread, program } = await Tester.compile(t.code` + model ${t.model("Before")} { @header a: string; @query b: string; @path c: string; @body d: string; } - @test - model After is PlainData {} + model ${t.model("After")} is PlainData {} - @test - model Spread { + model ${t.model("Spread")} { ...After } - `, - ); - - const { Before, After, Spread } = await testHost.compile("main.tsp"); - const program = testHost.program; + `); - strictEqual(Before.kind, "Model" as const); ok(isHeader(program, Before.properties.get("a")!), "header expected"); ok(isBody(program, Before.properties.get("d")!), "body expected"); - ok(isQueryParam(testHost.program, Before.properties.get("b")!), "query expected"); - ok(isPathParam(testHost.program, Before.properties.get("c")!), "path expected"); + ok(isQueryParam(program, Before.properties.get("b")!), "query expected"); + ok(isPathParam(program, Before.properties.get("c")!), "path expected"); for (const model of [After, Spread]) { strictEqual(model.kind, "Model" as const); ok(!isHeader(program, model.properties.get("a")!), `header not expected in ${model.name}`); ok(!isBody(program, model.properties.get("d")!), `body not expected in ${model.name}`); - ok( - !isQueryParam(testHost.program, model.properties.get("b")!), - `query not expected in ${model.name}`, - ); - ok( - !isPathParam(testHost.program, model.properties.get("c")!), - `path not expected in ${model.name}`, - ); + ok(!isQueryParam(program, model.properties.get("b")!), `query not expected in ${model.name}`); + ok(!isPathParam(program, model.properties.get("c")!), `path not expected in ${model.name}`); } }); }); diff --git a/packages/http/test/routes.test.ts b/packages/http/test/routes.test.ts index 92ba6312054..93775129fc2 100644 --- a/packages/http/test/routes.test.ts +++ b/packages/http/test/routes.test.ts @@ -1,15 +1,14 @@ -import { Operation } from "@typespec/compiler"; -import { expectDiagnosticEmpty, expectDiagnostics } from "@typespec/compiler/testing"; +import { expectDiagnosticEmpty, expectDiagnostics, t } from "@typespec/compiler/testing"; import { deepStrictEqual, ok, strictEqual } from "assert"; import { describe, expect, it } from "vitest"; import { PathOptions } from "../generated-defs/TypeSpec.Http.js"; -import { HttpOperation, HttpOperationParameter, getRoutePath } from "../src/index.js"; +import { getRoutePath, HttpOperation, HttpOperationParameter } from "../src/index.js"; import { compileOperations, - createHttpTestRunner, diagnoseOperations, getOperations, getRoutesFor, + Tester, } from "./test-host.js"; describe("http: routes", () => { @@ -462,26 +461,23 @@ describe("http: routes", () => { describe("shared routes", () => { it("@sharedRoute decorator makes routes shared", async () => { - const runner = await createHttpTestRunner(); - const { get1, get2 } = (await runner.compile(` + const { program, get1, get2 } = await Tester.compile(t.code` @route("/test") namespace Foo { - @test @sharedRoute @route("/get1") - op get1(): string; + op ${t.op("get1")}(): string; } @route("/test") namespace Foo { - @test @route("/get2") - op get2(): string; + op ${t.op("get2")}(): string; } - `)) as { get1: Operation; get2: Operation }; + `); - strictEqual(getRoutePath(runner.program, get1)?.shared, true); - strictEqual(getRoutePath(runner.program, get2)?.shared, false); + strictEqual(getRoutePath(program, get1)?.shared, true); + strictEqual(getRoutePath(program, get2)?.shared, false); }); }); }); diff --git a/packages/http/test/rules/op-reference-container-route.test.ts b/packages/http/test/rules/op-reference-container-route.test.ts index 3cfc198b555..1c7b4aee3e9 100644 --- a/packages/http/test/rules/op-reference-container-route.test.ts +++ b/packages/http/test/rules/op-reference-container-route.test.ts @@ -1,12 +1,12 @@ -import { LinterRuleTester, createLinterRuleTester } from "@typespec/compiler/testing"; +import { createLinterRuleTester, LinterRuleTester } from "@typespec/compiler/testing"; import { beforeEach, describe, it } from "vitest"; import { opReferenceContainerRouteRule } from "../../src/rules/op-reference-container-route.js"; -import { createHttpTestRunner } from "../test-host.js"; +import { Tester } from "../test-host.js"; describe("operation reference route container rule", () => { let ruleTester: LinterRuleTester; beforeEach(async () => { - const runner = await createHttpTestRunner(); + const runner = await Tester.createInstance(); ruleTester = createLinterRuleTester(runner, opReferenceContainerRouteRule, "@typespec/http"); }); diff --git a/packages/http/test/test-host.ts b/packages/http/test/test-host.ts index ba92f6870cf..203cd332a87 100644 --- a/packages/http/test/test-host.ts +++ b/packages/http/test/test-host.ts @@ -1,29 +1,18 @@ -import { createDiagnosticCollector, Diagnostic } from "@typespec/compiler"; -import { - BasicTestRunner, - createTestHost, - createTestWrapper, - expectDiagnosticEmpty, - TestHost, -} from "@typespec/compiler/testing"; +import { createDiagnosticCollector, Diagnostic, Program, resolvePath } from "@typespec/compiler"; +import { createTester, expectDiagnosticEmpty } from "@typespec/compiler/testing"; import { getAllHttpServices, HttpOperation, HttpOperationParameter, HttpVerb, } from "../src/index.js"; -import { HttpTestLibrary } from "../src/testing/index.js"; import { RouteResolutionOptions } from "../src/types.js"; -export async function createHttpTestHost(): Promise { - return createTestHost({ - libraries: [HttpTestLibrary], - }); -} -export async function createHttpTestRunner(): Promise { - const host = await createHttpTestHost(); - return createTestWrapper(host, { autoUsings: ["TypeSpec.Http"] }); -} +export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: ["@typespec/http"], +}) + .importLibraries() + .using("Http"); export interface RouteDetails { path: string; @@ -82,7 +71,7 @@ export async function compileOperations( } export interface CompileOperationsResult { - runner: BasicTestRunner; + program: Program; operations: HttpOperation[]; diagnostics: readonly Diagnostic[]; } @@ -91,19 +80,15 @@ export async function compileOperationsFull( code: string, routeOptions?: RouteResolutionOptions, ): Promise { - const runner = await createHttpTestRunner(); const diagnostics = createDiagnosticCollector(); - diagnostics.pipe( - await runner.compileAndDiagnose( + const { program } = diagnostics.pipe( + await Tester.compileAndDiagnose( `@service(#{title: "Test Service"}) namespace TestService; ${code}`, - { - noEmit: true, - }, ), ); - const services = diagnostics.pipe(getAllHttpServices(runner.program, routeOptions)); - return { runner, operations: services[0].operations, diagnostics: diagnostics.diagnostics }; + const services = diagnostics.pipe(getAllHttpServices(program, routeOptions)); + return { operations: services[0].operations, diagnostics: diagnostics.diagnostics, program }; } export async function diagnoseOperations( @@ -118,22 +103,17 @@ export async function getOperationsWithServiceNamespace( code: string, routeOptions?: RouteResolutionOptions, ): Promise<[HttpOperation[], readonly Diagnostic[]]> { - const runner = await createHttpTestRunner(); - await runner.compileAndDiagnose( + const [{ program }, _] = await Tester.compileAndDiagnose( `@service(#{title: "Test Service"}) namespace TestService; ${code}`, - { - noEmit: true, - }, ); - const [services] = getAllHttpServices(runner.program, routeOptions); - return [services[0].operations, runner.program.diagnostics]; + const [services] = getAllHttpServices(program, routeOptions); + return [services[0].operations, program.diagnostics]; } export async function getOperations(code: string): Promise { - const runner = await createHttpTestRunner(); - await runner.compile(code); - const [services, diagnostics] = getAllHttpServices(runner.program); + const { program } = await Tester.compile(code); + const [services, diagnostics] = getAllHttpServices(program); expectDiagnosticEmpty(diagnostics); return services[0].operations; diff --git a/packages/http/test/typekit/http-opperation.test.ts b/packages/http/test/typekit/http-opperation.test.ts index eb0f462756b..26215429264 100644 --- a/packages/http/test/typekit/http-opperation.test.ts +++ b/packages/http/test/typekit/http-opperation.test.ts @@ -1,20 +1,13 @@ -import { Model, Operation } from "@typespec/compiler"; -import { BasicTestRunner } from "@typespec/compiler/testing"; +import { t } from "@typespec/compiler/testing"; import { $ } from "@typespec/compiler/typekit"; -import { beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import "../../src/experimental/typekit/index.js"; -import { createHttpTestRunner } from "./../test-host.js"; - -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createHttpTestRunner(); -}); +import { Tester } from "./../test-host.js"; describe("httpOperation:getResponses", () => { it("should get responses", async () => { - const { getFoo } = (await runner.compile(` - @test model Foo { + const { getFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { @visibility(Lifecycle.Create) id: int32; age: int32; @@ -22,16 +15,16 @@ describe("httpOperation:getResponses", () => { } @error - @test model Error { + model ${t.model("Error")} { message: string; code: int32 } @route("/foo") @get - @test op getFoo(): Foo | Error; - `)) as { getFoo: Operation; Foo: Model; Error: Model }; - const tk = $(runner.program); + op ${t.op("getFoo")}(): Foo | Error; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(getFoo); const responses = tk.httpOperation.flattenResponses(httpOperation); @@ -43,8 +36,8 @@ describe("httpOperation:getResponses", () => { }); it("should get responses with multiple status codes", async () => { - const { getFoo } = (await runner.compile(` - @test model Foo { + const { getFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { @visibility(Lifecycle.Create) id: int32; age: int32; @@ -53,9 +46,9 @@ describe("httpOperation:getResponses", () => { @route("/foo") @get - @test op getFoo(): Foo | void; - `)) as { getFoo: Operation; Foo: Model; Error: Model }; - const tk = $(runner.program); + op ${t.op("getFoo")}(): Foo | void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(getFoo); const responses = tk.httpOperation.flattenResponses(httpOperation); @@ -67,8 +60,8 @@ describe("httpOperation:getResponses", () => { }); it("should get responses with multiple status codes and contentTypes", async () => { - const { getFoo } = (await runner.compile(` - @test model Foo { + const { getFoo, program } = await Tester.compile(t.code` + model ${t.model("Foo")} { @visibility(Lifecycle.Create) id: int32; age: int32; @@ -76,16 +69,16 @@ describe("httpOperation:getResponses", () => { } @error - @test model Error { + model ${t.model("Error")} { message: string; code: int32 } @route("/foo") @get - @test op getFoo(): Foo | {...Foo, @header contentType: "text/plain"} | Error; - `)) as { getFoo: Operation; Foo: Model; Error: Model }; - const tk = $(runner.program); + op ${t.op("getFoo")}(): Foo | {...Foo, @header contentType: "text/plain"} | Error; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(getFoo); const responses = tk.httpOperation.flattenResponses(httpOperation); diff --git a/packages/http/test/typekit/http-request.test.ts b/packages/http/test/typekit/http-request.test.ts index 8ab36677fea..f81bf451f7c 100644 --- a/packages/http/test/typekit/http-request.test.ts +++ b/packages/http/test/typekit/http-request.test.ts @@ -1,25 +1,18 @@ -import { Model, Operation } from "@typespec/compiler"; -import { BasicTestRunner } from "@typespec/compiler/testing"; +import { Model } from "@typespec/compiler"; +import { t } from "@typespec/compiler/testing"; import { $ } from "@typespec/compiler/typekit"; -import { beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import "../../src/experimental/typekit/index.js"; -import { createHttpTestRunner } from "./../test-host.js"; - -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createHttpTestRunner(); -}); +import { Tester } from "./../test-host.js"; describe("HttpRequest Body Parameters", () => { it("should handle model is array response", async () => { - const { get } = (await runner.compile(` - model EmbeddingVector is Array; - - @test op get(): EmbeddingVector; - `)) as { get: Operation; Foo: Model }; - const tk = $(runner.program); + const { program, get } = await Tester.compile(t.code` + model EmbeddingVector is Array; + op ${t.op("get")}(): EmbeddingVector; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(get); const body = tk.httpOperation.getReturnType(httpOperation)!; expect(body).toBeDefined(); @@ -29,7 +22,7 @@ describe("HttpRequest Body Parameters", () => { }); it("should get the body parameters model when spread", async () => { - const { createFoo } = (await runner.compile(` + const { program, createFoo } = await Tester.compile(t.code` @test model Foo { id: int32; age: int32; @@ -38,10 +31,9 @@ describe("HttpRequest Body Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); - + @test op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)!; expect(body).toBeDefined(); @@ -51,13 +43,12 @@ describe("HttpRequest Body Parameters", () => { }); it("should get the body model params when body is defined explicitly as a property", async () => { - const { createFoo } = (await runner.compile(` + const { program, createFoo } = await Tester.compile(t.code` @route("/foo") @post - @test op createFoo(@body foo: int32): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); - + op ${t.op("createFoo")}(@body foo: int32): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)!; expect(body).toBeDefined(); @@ -68,7 +59,7 @@ describe("HttpRequest Body Parameters", () => { }); it("should get the body when spread and nested", async () => { - const { createFoo } = (await runner.compile(` + const { program, createFoo } = await Tester.compile(t.code` @test model Foo { @path id: int32; age: int32; @@ -81,10 +72,9 @@ describe("HttpRequest Body Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); - + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)!; expect(body).toBeDefined(); @@ -104,7 +94,7 @@ describe("HttpRequest Body Parameters", () => { }); it("should get the body when named body model", async () => { - const { createFoo } = (await runner.compile(` + const { program, createFoo } = await Tester.compile(t.code` @test model Foo { id: int32; age: int32; @@ -113,10 +103,9 @@ describe("HttpRequest Body Parameters", () => { @route("/foo") @post - @test op createFoo(@body foo: Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); - + op ${t.op("createFoo")}(@body foo: Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)!; expect(body).toBeDefined(); @@ -127,7 +116,7 @@ describe("HttpRequest Body Parameters", () => { }); it("should get the named body body when combined", async () => { - const { createFoo } = (await runner.compile(` + const { program, createFoo } = await Tester.compile(t.code` @test model Foo { @path id: int32; age: int32; @@ -136,10 +125,9 @@ describe("HttpRequest Body Parameters", () => { @route("/foo") @post - @test op createFoo(foo: Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); - + op ${t.op("createFoo")}(foo: Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)!; expect(body).toBeDefined(); @@ -153,7 +141,7 @@ describe("HttpRequest Body Parameters", () => { describe("HttpRequest Get Parameters", () => { it("should only have body parameters", async () => { - const { createFoo } = (await runner.compile(` + const { program, createFoo } = await Tester.compile(t.code` @test model Foo { id: int32; age: int32; @@ -162,10 +150,9 @@ describe("HttpRequest Get Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); - + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)!; const headers = tk.httpRequest.getParameters(httpOperation, "header"); @@ -178,7 +165,7 @@ describe("HttpRequest Get Parameters", () => { }); it("should be able to get parameter options", async () => { - const { createFoo } = (await runner.compile(` + const { program, createFoo } = await Tester.compile(t.code` @test model Foo { @path(#{allowReserved: true}) id: string; @header(#{explode: true}) requestId: string[]; @@ -187,10 +174,9 @@ describe("HttpRequest Get Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); - + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const headers = tk.httpRequest.getParameters(httpOperation, "header"); const path = tk.httpRequest.getParameters(httpOperation, "path"); @@ -222,7 +208,7 @@ describe("HttpRequest Get Parameters", () => { }); it("should only have header parameters", async () => { - const { createFoo } = (await runner.compile(` + const { program, createFoo } = await Tester.compile(t.code` @test model Foo { @path id: int32; age: int32; @@ -231,10 +217,9 @@ describe("HttpRequest Get Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); - + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)! as Model; const headers = tk.httpRequest.getParameters(httpOperation, "header"); @@ -250,7 +235,7 @@ describe("HttpRequest Get Parameters", () => { }); it("should only have path parameters", async () => { - const { createFoo } = (await runner.compile(` + const { program, createFoo } = await Tester.compile(t.code` @test model Foo { @header id: int32; @header age: int32; @@ -259,10 +244,9 @@ describe("HttpRequest Get Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); - + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)! as Model; const headers = tk.httpRequest.getParameters(httpOperation, "header")!; @@ -279,7 +263,7 @@ describe("HttpRequest Get Parameters", () => { }); it("should only have query parameters", async () => { - const { createFoo } = (await runner.compile(` + const { program, createFoo } = await Tester.compile(t.code` @test model Foo { @query id: int32; @query age: int32; @@ -288,10 +272,9 @@ describe("HttpRequest Get Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); - + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const body = tk.httpRequest.getBodyParameters(httpOperation)! as Model; const headers = tk.httpRequest.getParameters(httpOperation, "header"); @@ -308,7 +291,7 @@ describe("HttpRequest Get Parameters", () => { }); it("should have query and header parameters", async () => { - const { createFoo } = (await runner.compile(` + const { program, createFoo } = await Tester.compile(t.code` @test model Foo { @query id: int32; @header age: int32; @@ -317,10 +300,9 @@ describe("HttpRequest Get Parameters", () => { @route("/foo") @post - @test op createFoo(...Foo): void; - `)) as { createFoo: Operation; Foo: Model }; - const tk = $(runner.program); - + op ${t.op("createFoo")}(...Foo): void; + `); + const tk = $(program); const httpOperation = tk.httpOperation.get(createFoo); const headerAndQuery = tk.httpRequest.getParameters(httpOperation, ["header", "query"]); expect(headerAndQuery).toBeDefined(); diff --git a/packages/openapi/src/testing/index.ts b/packages/openapi/src/testing/index.ts index 0659d03b35f..706829790f7 100644 --- a/packages/openapi/src/testing/index.ts +++ b/packages/openapi/src/testing/index.ts @@ -4,6 +4,7 @@ import { TypeSpecTestLibrary, } from "@typespec/compiler/testing"; +/** @deprecated use new Tester */ export const OpenAPITestLibrary: TypeSpecTestLibrary = createTestLibrary({ name: "@typespec/openapi", packageRoot: await findTestPackageRoot(import.meta.url), diff --git a/packages/openapi/test/decorators.test.ts b/packages/openapi/test/decorators.test.ts index e19b5342c15..d3472f32048 100644 --- a/packages/openapi/test/decorators.test.ts +++ b/packages/openapi/test/decorators.test.ts @@ -1,7 +1,6 @@ -import { Namespace } from "@typespec/compiler"; -import { BasicTestRunner, expectDiagnostics } from "@typespec/compiler/testing"; +import { expectDiagnostics, t } from "@typespec/compiler/testing"; import { deepStrictEqual } from "assert"; -import { beforeEach, describe, it } from "vitest"; +import { describe, it } from "vitest"; import { getExtensions, getExternalDocs, @@ -10,18 +9,12 @@ import { resolveInfo, setInfo, } from "../src/decorators.js"; -import { createOpenAPITestRunner } from "./test-host.js"; +import { Tester } from "./test-host.js"; describe("openapi: decorators", () => { - let runner: BasicTestRunner; - - beforeEach(async () => { - runner = await createOpenAPITestRunner(); - }); - describe("@operationId", () => { it("emit diagnostic if use on non operation", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @operationId("foo") model Foo {} `); @@ -34,7 +27,7 @@ describe("openapi: decorators", () => { }); it("emit diagnostic if operation id is not a string", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @operationId(123) op foo(): string; `); @@ -47,29 +40,27 @@ describe("openapi: decorators", () => { describe("@extension", () => { it("apply extension on model", async () => { - const { Foo } = await runner.compile(` + const { program, Foo } = await Tester.compile(t.code` @extension("x-custom", "Bar") - @test - model Foo { + model ${t.model("Foo")} { prop: string } `); - deepStrictEqual(Object.fromEntries(getExtensions(runner.program, Foo)), { + deepStrictEqual(Object.fromEntries(getExtensions(program, Foo)), { "x-custom": "Bar", }); }); it("apply extension with complex value", async () => { - const { Foo } = await runner.compile(` + const { program, Foo } = await Tester.compile(t.code` @extension("x-custom", #{foo: 123, bar: "string"}) - @test - model Foo { + model ${t.model("Foo")} { prop: string } `); - deepStrictEqual(Object.fromEntries(getExtensions(runner.program, Foo)), { + deepStrictEqual(Object.fromEntries(getExtensions(program, Foo)), { "x-custom": { foo: 123, bar: "string" }, }); }); @@ -83,33 +74,31 @@ describe("openapi: decorators", () => { { value: `"hi"`, expected: "hi" }, { value: `null`, expected: null }, ])("treats value $value as raw value", async ({ value, expected }) => { - const { Foo } = await runner.compile(` + const { program, Foo } = await Tester.compile(t.code` @extension("x-custom", ${value}) - @test - model Foo{} + model ${t.model("Foo")} {} `); - deepStrictEqual(Object.fromEntries(getExtensions(runner.program, Foo)), { + deepStrictEqual(Object.fromEntries(getExtensions(program, Foo)), { "x-custom": expected, }); }); it("supports extension key not starting with `x-`", async () => { - const { Foo } = await runner.compile(` + const { program, Foo } = await Tester.compile(t.code` @extension("foo", "Bar") - @test - model Foo { + model ${t.model("Foo")} { prop: string } `); - deepStrictEqual(Object.fromEntries(getExtensions(runner.program, Foo)), { + deepStrictEqual(Object.fromEntries(getExtensions(program, Foo)), { foo: "Bar", }); }); it("emit diagnostics when passing non string extension key", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @extension(123, "Bar") @test model Foo { @@ -125,7 +114,7 @@ describe("openapi: decorators", () => { describe("@externalDocs", () => { it("emit diagnostic if url is not a string", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @externalDocs(123) model Foo {} `); @@ -136,10 +125,9 @@ describe("openapi: decorators", () => { }); it("emit diagnostic if description is not a string", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @externalDocs("https://example.com", 123) model Foo {} - `); expectDiagnostics(diagnostics, { @@ -148,23 +136,21 @@ describe("openapi: decorators", () => { }); it("set the external url", async () => { - const { Foo } = await runner.compile(` + const { program, Foo } = await Tester.compile(t.code` @externalDocs("https://example.com") - @test - model Foo {} + model ${t.model("Foo")} {} `); - deepStrictEqual(getExternalDocs(runner.program, Foo), { url: "https://example.com" }); + deepStrictEqual(getExternalDocs(program, Foo), { url: "https://example.com" }); }); it("set the external url with description", async () => { - const { Foo } = await runner.compile(` + const { program, Foo } = await Tester.compile(t.code` @externalDocs("https://example.com", "More info there") - @test - model Foo {} + model ${t.model("Foo")} {} `); - deepStrictEqual(getExternalDocs(runner.program, Foo), { + deepStrictEqual(getExternalDocs(program, Foo), { url: "https://example.com", description: "More info there", }); @@ -179,7 +165,7 @@ describe("openapi: decorators", () => { ["contact", `#{ contact: #{ foo:"Bar"} }`], ["complex", `#{ contact: #{ \`x-custom\`: "string" }, foo:"Bar" }`], ])("%s", async (_, code) => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @info(${code}) @test namespace Service; `); @@ -191,7 +177,7 @@ describe("openapi: decorators", () => { }); it("multiple", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @info(#{ license: #{ name: "Apache 2.0", foo1:"Bar"}, contact: #{ \`x-custom\`: "string", foo2:"Bar" }, @@ -218,9 +204,9 @@ describe("openapi: decorators", () => { }); it("emit diagnostic if termsOfService is not a valid url", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @info(#{termsOfService:"notvalidurl"}) - @test namespace Service {} + namespace Service {} `); expectDiagnostics(diagnostics, { @@ -230,7 +216,7 @@ describe("openapi: decorators", () => { }); it("emit diagnostic if use on non namespace", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @info(#{}) model Foo {} `); @@ -242,7 +228,7 @@ describe("openapi: decorators", () => { }); it("emit diagnostic if info parameter is not an object", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @info(123) namespace Service {} `); @@ -253,7 +239,7 @@ describe("openapi: decorators", () => { }); it("set all properties", async () => { - const { Service } = (await runner.compile(` + const { program, Service } = await Tester.compile(t.code` @info(#{ title: "My API", version: "1.0.0", @@ -269,10 +255,10 @@ describe("openapi: decorators", () => { url: "http://www.apache.org/licenses/LICENSE-2.0.html" }, }) - @test namespace Service {} - `)) as { Service: Namespace }; + namespace ${t.namespace("Service")} {} + `); - deepStrictEqual(getInfo(runner.program, Service), { + deepStrictEqual(getInfo(program, Service), { title: "My API", version: "1.0.0", summary: "My API summary", @@ -290,8 +276,7 @@ describe("openapi: decorators", () => { }); it("resolveInfo() merge with data from @service and @summary", async () => { - const { Service } = (await runner.compile(` - #suppress "deprecated" "Test" + const { program, Service } = await Tester.compile(t.code` @service(#{ title: "Service API", }) @@ -300,10 +285,10 @@ describe("openapi: decorators", () => { version: "1.0.0", termsOfService: "http://example.com/terms/", }) - @test namespace Service {} - `)) as { Service: Namespace }; + namespace ${t.namespace("Service")} {} + `); - deepStrictEqual(resolveInfo(runner.program, Service), { + deepStrictEqual(resolveInfo(program, Service), { title: "Service API", version: "1.0.0", summary: "My summary", @@ -312,18 +297,18 @@ describe("openapi: decorators", () => { }); it("resolveInfo() returns empty object if nothing is provided", async () => { - const { Service } = (await runner.compile(` - @test namespace Service {} - `)) as { Service: Namespace }; + const { program, Service } = await Tester.compile(t.code` + namespace ${t.namespace("Service")} {} + `); - deepStrictEqual(resolveInfo(runner.program, Service), {}); + deepStrictEqual(resolveInfo(program, Service), {}); }); it("setInfo() function for setting info object directly", async () => { - const { Service } = (await runner.compile(` - @test namespace Service {} - `)) as { Service: Namespace }; - setInfo(runner.program, Service, { + const { program, Service } = await Tester.compile(t.code` + namespace ${t.namespace("Service")} {} + `); + setInfo(program, Service, { title: "My API", version: "1.0.0", summary: "My API summary", @@ -339,7 +324,7 @@ describe("openapi: decorators", () => { }, "x-custom": "Bar", }); - deepStrictEqual(getInfo(runner.program, Service), { + deepStrictEqual(getInfo(program, Service), { title: "My API", version: "1.0.0", summary: "My API summary", @@ -360,7 +345,7 @@ describe("openapi: decorators", () => { describe("@tagMetadata", () => { it("emit an error if a non-service namespace", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` @tagMetadata("tagName", #{}) namespace Test {} @@ -379,7 +364,7 @@ describe("openapi: decorators", () => { ["description is not a string", `@tagMetadata("tagName", #{ description: 123, })`], ["externalDocs is not an object", `@tagMetadata("tagName", #{ externalDocs: 123, })`], ])("%s", async (_, code) => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` ${code} namespace PetStore{}; @@ -392,7 +377,7 @@ describe("openapi: decorators", () => { }); it("emit diagnostic if dup tagName", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` @service() @tagMetadata("tagName", #{}) @@ -415,7 +400,7 @@ describe("openapi: decorators", () => { `#{ externalDocs: #{ url: "https://example.com", \`x-custom\`: "string" }, foo:"Bar" }`, ], ])("%s", async (_, code) => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` @service() @tagMetadata("tagName", ${code}) @@ -430,7 +415,7 @@ describe("openapi: decorators", () => { }); it("multiple", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` @service() @tagMetadata("tagName", #{ @@ -455,13 +440,13 @@ describe("openapi: decorators", () => { }); it("emit diagnostic if externalDocs.url is not a valid url", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` - @service() + @service @tagMetadata("tagName", #{ externalDocs: #{ url: "notvalidurl"}, }) - @test namespace Service {} + namespace Service {} `, ); @@ -472,7 +457,7 @@ describe("openapi: decorators", () => { }); it("emit diagnostic if use on non namespace", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` @tagMetadata("tagName", #{}) model Foo {} @@ -542,16 +527,12 @@ describe("openapi: decorators", () => { ], ]; it.each(testCases)("%s", async (_, tagMetaDecorator, expected) => { - const runner = await createOpenAPITestRunner(); - const { PetStore } = await runner.compile( - ` + const { program, PetStore } = await Tester.compile(t.code` @service() ${tagMetaDecorator} - @test - namespace PetStore {} - `, - ); - deepStrictEqual(getTagsMetadata(runner.program, PetStore), expected); + namespace ${t.namespace("PetStore")} {} + `); + deepStrictEqual(getTagsMetadata(program, PetStore), expected); }); }); }); diff --git a/packages/openapi/test/helpers.test.ts b/packages/openapi/test/helpers.test.ts index 4328ba0eeb5..bee423b4ca3 100644 --- a/packages/openapi/test/helpers.test.ts +++ b/packages/openapi/test/helpers.test.ts @@ -3,6 +3,7 @@ import { BasicTestRunner, createTestRunner } from "@typespec/compiler/testing"; import { strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { resolveOperationId } from "../src/helpers.js"; + describe("openapi: helpers", () => { let runner: BasicTestRunner; diff --git a/packages/openapi/test/test-host.ts b/packages/openapi/test/test-host.ts index c3656a17113..521899c6e4e 100644 --- a/packages/openapi/test/test-host.ts +++ b/packages/openapi/test/test-host.ts @@ -1,27 +1,8 @@ -import { createTestHost, createTestWrapper } from "@typespec/compiler/testing"; -import { HttpTestLibrary } from "@typespec/http/testing"; -import { RestTestLibrary } from "@typespec/rest/testing"; -import { OpenAPITestLibrary } from "../src/testing/index.js"; +import { resolvePath } from "@typespec/compiler"; +import { createTester } from "@typespec/compiler/testing"; -export async function createOpenAPITestHost() { - return createTestHost({ - libraries: [HttpTestLibrary, RestTestLibrary, OpenAPITestLibrary], - }); -} -export async function createOpenAPITestRunner() { - const host = await createOpenAPITestHost(); - return createTestWrapper(host, { autoUsings: ["TypeSpec.OpenAPI"] }); -} - -export async function createOpenAPITestRunnerWithDecorators(decorators: Record) { - const host = await createOpenAPITestHost(); - host.addJsFile("dec.js", decorators); - return createTestWrapper(host, { - wrapper(code) { - return ` - import "./dec.js"; - using OpenAPI; - ${code}`; - }, - }); -} +export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: ["@typespec/http", "@typespec/rest", "@typespec/openapi"], +}) + .importLibraries() + .using("OpenAPI"); diff --git a/packages/openapi3/src/testing/index.ts b/packages/openapi3/src/testing/index.ts index 32aa459b660..be67df9601c 100644 --- a/packages/openapi3/src/testing/index.ts +++ b/packages/openapi3/src/testing/index.ts @@ -4,6 +4,7 @@ import { findTestPackageRoot, } from "@typespec/compiler/testing"; +/** @deprecated use new Tester */ export const OpenAPI3TestLibrary: TypeSpecTestLibrary = createTestLibrary({ name: "@typespec/openapi3", packageRoot: await findTestPackageRoot(import.meta.url), diff --git a/packages/openapi3/test/decorators.test.ts b/packages/openapi3/test/decorators.test.ts index 98f1f31ff6d..2068efdf083 100644 --- a/packages/openapi3/test/decorators.test.ts +++ b/packages/openapi3/test/decorators.test.ts @@ -1,22 +1,13 @@ -import { - BasicTestRunner, - expectDiagnosticEmpty, - expectDiagnostics, -} from "@typespec/compiler/testing"; +import { expectDiagnostics, t } from "@typespec/compiler/testing"; import { strictEqual } from "assert"; -import { beforeEach, describe, it } from "vitest"; +import { describe, it } from "vitest"; import { getRef } from "../src/decorators.js"; -import { createOpenAPITestRunner } from "./test-host.js"; +import { ApiTester, SimpleTester } from "./test-host.js"; describe("openapi3: decorators", () => { - let runner: BasicTestRunner; - - beforeEach(async () => { - runner = await createOpenAPITestRunner(); - }); describe("@useRef", () => { it("emit diagnostic if use on non model or property", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await SimpleTester.diagnose(` @useRef("foo") op foo(): string; `); @@ -29,7 +20,7 @@ describe("openapi3: decorators", () => { }); it("emit diagnostic if ref is not a string", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await SimpleTester.diagnose(` @useRef(123) model Foo {} `); @@ -40,7 +31,7 @@ describe("openapi3: decorators", () => { }); it("emit diagnostic if ref is not passed", async () => { - const diagnostics = await runner.diagnose(` + const diagnostics = await SimpleTester.diagnose(` @useRef model Foo {} `); @@ -54,14 +45,15 @@ describe("openapi3: decorators", () => { }); it("set external reference", async () => { - const [{ Foo }, diagnostics] = await runner.compileAndDiagnose(` - @test @useRef("../common.json#/definitions/Foo") - model Foo {} - `); + const { Foo, program } = await ApiTester.compile(t.code` + import "@typespec/openapi3"; + using OpenAPI; - expectDiagnosticEmpty(diagnostics); + @useRef("../common.json#/definitions/Foo") + model ${t.model("Foo")} {} + `); - strictEqual(getRef(runner.program, Foo), "../common.json#/definitions/Foo"); + strictEqual(getRef(program, Foo), "../common.json#/definitions/Foo"); }); }); }); diff --git a/packages/openapi3/test/examples.test.ts b/packages/openapi3/test/examples.test.ts index b9c01ed79d2..df10f71f725 100644 --- a/packages/openapi3/test/examples.test.ts +++ b/packages/openapi3/test/examples.test.ts @@ -287,7 +287,6 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => { @route("/") op getColors(${param}): void; `, - undefined, { "experimental-parameter-examples": "data" }, ); expect((res.paths[path].get?.parameters[0] as OpenAPI3Parameter).example).toEqual( @@ -419,7 +418,6 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => { @route("/") op getColors(${param}): void; `, - undefined, { "experimental-parameter-examples": "serialized" }, ); expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual( @@ -567,7 +565,6 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => { @route("${route}") op getColors(${param}): void; `, - undefined, { "experimental-parameter-examples": "serialized" }, ); expect((res.paths[`/{color}`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual( @@ -625,7 +622,6 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => { @route("/") op getColors(${param}): void; `, - undefined, { "experimental-parameter-examples": "serialized" }, ); expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual( @@ -671,7 +667,6 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => { @route("/") op getColors(${param}): void; `, - undefined, { "experimental-parameter-examples": "serialized" }, ); expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual( @@ -696,7 +691,6 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => { @route("/") op getColors(@query color: string): void; `, - undefined, { "experimental-parameter-examples": "serialized" }, ); expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).examples).toEqual({ @@ -727,7 +721,6 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => { @route("/") op getColors(@query color: string): void; `, - undefined, { "experimental-parameter-examples": "data" }, ); expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).examples).toEqual({ @@ -781,7 +774,6 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => { encodedHeader: utcDateTime; } `, - undefined, { "experimental-parameter-examples": "data" }, ); expect((res.components.parameters["Test.dob"] as OpenAPI3Parameter).examples).toEqual({ @@ -847,7 +839,6 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => { encodedHeader: utcDateTime; } `, - undefined, { "experimental-parameter-examples": "data" }, ); expect((res.components.parameters["Test.dob"] as OpenAPI3Parameter).example).toEqual( diff --git a/packages/openapi3/test/get-openapi.test.ts b/packages/openapi3/test/get-openapi.test.ts index 7503dfbed27..9b389fd099d 100644 --- a/packages/openapi3/test/get-openapi.test.ts +++ b/packages/openapi3/test/get-openapi.test.ts @@ -2,19 +2,14 @@ import { expectDiagnostics } from "@typespec/compiler/testing"; import { ok, strictEqual } from "assert"; import { it } from "vitest"; import { getOpenAPI3 } from "../src/openapi.js"; -import { createOpenAPITestHost } from "./test-host.js"; +import { ApiTester } from "./test-host.js"; it("can get openapi as an object", async () => { - const host = await createOpenAPITestHost(); - host.addTypeSpecFile( - "./main.tsp", - `import "@typespec/http"; - import "@typespec/rest"; + const { program } = await ApiTester.compile(` + import "@typespec/http"; import "@typespec/openapi"; import "@typespec/openapi3"; - using Rest; using Http; - using OpenAPI; @service namespace Foo; @@ -23,35 +18,24 @@ it("can get openapi as an object", async () => { model Item { x: true } model Bar { }; // unreachable - `, - ); - await host.compile("main.tsp"); - const output = await getOpenAPI3(host.program, { "omit-unreachable-types": false }); + `); + const output = await getOpenAPI3(program, { "omit-unreachable-types": false }); const documentRecord = output[0]; ok(!documentRecord.versioned, "should not be versioned"); strictEqual(documentRecord.document.components!.schemas!["Item"].type, "object"); }); it("has diagnostics", async () => { - const host = await createOpenAPITestHost(); - host.addTypeSpecFile( - "./main.tsp", - `import "@typespec/http"; - import "@typespec/rest"; - import "@typespec/openapi"; - import "@typespec/openapi3"; - using Rest; + const { program } = await ApiTester.compile(` + import "@typespec/http"; using Http; - using OpenAPI; @service namespace Foo; op read(): {@minValue(455) @maxValue(495) @statusCode _: int32, content: string}; - `, - ); - await host.compile("main.tsp"); - const output = await getOpenAPI3(host.program, { "omit-unreachable-types": false }); + `); + const output = await getOpenAPI3(program, { "omit-unreachable-types": false }); const documentRecord = output[0]; ok(!documentRecord.versioned, "should not be versioned"); expectDiagnostics(documentRecord.diagnostics, [ diff --git a/packages/openapi3/test/merge-patch.test.ts b/packages/openapi3/test/merge-patch.test.ts index 45c319f42c8..93412b9aae6 100644 --- a/packages/openapi3/test/merge-patch.test.ts +++ b/packages/openapi3/test/merge-patch.test.ts @@ -18,7 +18,6 @@ export async function oapiForPatchRequest( @patch op update(${body}): void; } `, - undefined, options, ); diff --git a/packages/openapi3/test/metadata.test.ts b/packages/openapi3/test/metadata.test.ts index 4ab93228386..8d26eea79d0 100644 --- a/packages/openapi3/test/metadata.test.ts +++ b/packages/openapi3/test/metadata.test.ts @@ -388,7 +388,6 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => { @delete op delete(...U): void; } `, - undefined, { "omit-unreachable-types": true }, ); diff --git a/packages/openapi3/test/output-file.test.ts b/packages/openapi3/test/output-file.test.ts index 0faba31d3a7..90ffd7165d9 100644 --- a/packages/openapi3/test/output-file.test.ts +++ b/packages/openapi3/test/output-file.test.ts @@ -1,13 +1,13 @@ import { resolvePath } from "@typespec/compiler"; import { - BasicTestRunner, expectDiagnosticEmpty, resolveVirtualPath, + TesterInstance, } from "@typespec/compiler/testing"; import { ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { OpenAPI3EmitterOptions } from "../src/lib.js"; -import { createOpenAPITestRunner } from "./test-host.js"; +import { ApiTester } from "./test-host.js"; describe("openapi3: output file", () => { const expectedJsonEmptySpec = [ @@ -36,15 +36,16 @@ describe("openapi3: output file", () => { ]; const outputDir = resolveVirtualPath("test-output"); - let runner: BasicTestRunner; + let runner: TesterInstance; beforeEach(async () => { - runner = await createOpenAPITestRunner(); + runner = await ApiTester.importLibraries().createInstance(); }); async function compileOpenAPI(options: OpenAPI3EmitterOptions, code: string = ""): Promise { const diagnostics = await runner.diagnose(code, { - noEmit: false, - emit: ["@typespec/openapi3"], - options: { "@typespec/openapi3": { ...options, "emitter-output-dir": outputDir } }, + compilerOptions: { + emit: ["@typespec/openapi3"], + options: { "@typespec/openapi3": { ...options, "emitter-output-dir": outputDir } }, + }, }); expectDiagnosticEmpty(diagnostics); @@ -56,14 +57,14 @@ describe("openapi3: output file", () => { newLine: "\n" | "\r\n" = "\n", ) { const outPath = resolvePath(outputDir, filename); - const content = runner.fs.get(outPath); + const content = runner.fs.fs.get(outPath); ok(content, `Expected ${outPath} to exist.`); strictEqual(content, lines.join(newLine)); } function expectHasOutput(filename: string) { const outPath = resolvePath(outputDir, filename); - const content = runner.fs.get(outPath); + const content = runner.fs.fs.get(outPath); ok(content, `Expected ${outPath} to exist.`); } diff --git a/packages/openapi3/test/output-spec-versions.test.ts b/packages/openapi3/test/output-spec-versions.test.ts index 2ced94b0a3f..d98fdc41d3b 100644 --- a/packages/openapi3/test/output-spec-versions.test.ts +++ b/packages/openapi3/test/output-spec-versions.test.ts @@ -1,25 +1,26 @@ import { resolvePath } from "@typespec/compiler"; import { - BasicTestRunner, expectDiagnosticEmpty, resolveVirtualPath, + TesterInstance, } from "@typespec/compiler/testing"; import { ok } from "assert"; import { beforeEach, expect, it } from "vitest"; import { OpenAPI3EmitterOptions } from "../src/lib.js"; -import { createOpenAPITestRunner } from "./test-host.js"; +import { ApiTester } from "./test-host.js"; const outputDir = resolveVirtualPath("test-output"); -let runner: BasicTestRunner; +let runner: TesterInstance; beforeEach(async () => { - runner = await createOpenAPITestRunner(); + runner = await ApiTester.createInstance(); }); async function compileOpenAPI(options: OpenAPI3EmitterOptions, code: string = ""): Promise { const diagnostics = await runner.diagnose(code, { - noEmit: false, - emit: ["@typespec/openapi3"], - options: { "@typespec/openapi3": { ...options, "emitter-output-dir": outputDir } }, + compilerOptions: { + emit: ["@typespec/openapi3"], + options: { "@typespec/openapi3": { ...options, "emitter-output-dir": outputDir } }, + }, }); expectDiagnosticEmpty(diagnostics); @@ -27,7 +28,7 @@ async function compileOpenAPI(options: OpenAPI3EmitterOptions, code: string = "" function expectHasOutput(filename: string) { const outPath = resolvePath(outputDir, filename); - const content = runner.fs.get(outPath); + const content = runner.fs.fs.get(outPath); ok(content, `Expected ${outPath} to exist.`); } @@ -45,7 +46,7 @@ it("does not create nested directory if only 1 spec version is specified", async it("defaults to 3.0.0 if not specified", async () => { await compileOpenAPI({ "file-type": "json" }); const outPath = resolvePath(outputDir, "openapi.json"); - const content = runner.fs.get(outPath); + const content = runner.fs.fs.get(outPath); ok(content, `Expected ${outPath} to exist.`); const doc = JSON.parse(content); expect(doc.openapi).toBe("3.0.0"); @@ -54,7 +55,7 @@ it("defaults to 3.0.0 if not specified", async () => { it("supports 3.1.0", async () => { await compileOpenAPI({ "openapi-versions": ["3.1.0"], "file-type": "json" }); const outPath = resolvePath(outputDir, "openapi.json"); - const content = runner.fs.get(outPath); + const content = runner.fs.fs.get(outPath); ok(content, `Expected ${outPath} to exist.`); const doc = JSON.parse(content); expect(doc.openapi).toBe("3.1.0"); diff --git a/packages/openapi3/test/primitive-types.test.ts b/packages/openapi3/test/primitive-types.test.ts index b54dd4990bc..5720882727f 100644 --- a/packages/openapi3/test/primitive-types.test.ts +++ b/packages/openapi3/test/primitive-types.test.ts @@ -53,7 +53,6 @@ worksFor(["3.0.0", "3.1.0"], ({ oapiForModel, openApiFor }) => { ` model Pet { name: safeint }; `, - undefined, { "safeint-strategy": "double-int" }, ); @@ -66,7 +65,6 @@ worksFor(["3.0.0", "3.1.0"], ({ oapiForModel, openApiFor }) => { ` model Pet { name: safeint }; `, - undefined, { "safeint-strategy": "int64" }, ); diff --git a/packages/openapi3/test/test-host.ts b/packages/openapi3/test/test-host.ts index 8c22ee40af4..31f95a26618 100644 --- a/packages/openapi3/test/test-host.ts +++ b/packages/openapi3/test/test-host.ts @@ -1,132 +1,97 @@ import { Diagnostic, interpolatePath, resolvePath } from "@typespec/compiler"; import { - createTestHost, - createTestWrapper, + createTester, expectDiagnosticEmpty, resolveVirtualPath, } from "@typespec/compiler/testing"; -import { HttpTestLibrary } from "@typespec/http/testing"; -import { JsonSchemaTestLibrary } from "@typespec/json-schema/testing"; -import { OpenAPITestLibrary } from "@typespec/openapi/testing"; -import { RestTestLibrary } from "@typespec/rest/testing"; -import { VersioningTestLibrary } from "@typespec/versioning/testing"; -import { XmlTestLibrary } from "@typespec/xml/testing"; import { ok } from "assert"; import { parse } from "yaml"; import { OpenAPI3EmitterOptions } from "../src/lib.js"; -import { OpenAPI3TestLibrary } from "../src/testing/index.js"; import { OpenAPI3Document } from "../src/types.js"; -export async function createOpenAPITestHost() { - return createTestHost({ - libraries: [ - HttpTestLibrary, - JsonSchemaTestLibrary, - RestTestLibrary, - VersioningTestLibrary, - XmlTestLibrary, - OpenAPITestLibrary, - OpenAPI3TestLibrary, - ], - }); -} +export const ApiTester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: [ + "@typespec/http", + "@typespec/json-schema", + "@typespec/rest", + "@typespec/versioning", + "@typespec/openapi", + "@typespec/xml", + "@typespec/openapi3", + ], +}); -export async function createOpenAPITestRunner({ - emitterOptions, - withVersioning, -}: { withVersioning?: boolean; emitterOptions?: OpenAPI3EmitterOptions } = {}) { - const host = await createOpenAPITestHost(); - const importAndUsings = ` - import "@typespec/http"; - import "@typespec/rest"; - import "@typespec/json-schema"; - import "@typespec/openapi"; - import "@typespec/openapi3"; - import "@typespec/xml"; - ${withVersioning ? `import "@typespec/versioning"` : ""}; - using Rest; - using Http; - using OpenAPI; - using TypeSpec.Xml; - ${withVersioning ? "using Versioning;" : ""} -`; - return createTestWrapper(host, { - wrapper: (code) => `${importAndUsings} ${code}`, - compilerOptions: { - emit: ["@typespec/openapi3"], - options: { - "@typespec/openapi3": { ...emitterOptions }, - }, - }, - }); -} +export const SimpleTester = ApiTester.import( + "@typespec/http", + "@typespec/json-schema", + "@typespec/rest", + "@typespec/openapi", + "@typespec/xml", + "@typespec/openapi3", +) + .using("Http", "Rest", "OpenAPI", "Xml") + .emit("@typespec/openapi3"); + +export const TesterWithVersioning = ApiTester.importLibraries() + .using("Http", "Rest", "OpenAPI", "Xml", "Versioning") + .emit("@typespec/openapi3"); export async function emitOpenApiWithDiagnostics( code: string, options: OpenAPI3EmitterOptions = {}, ): Promise<[OpenAPI3Document, readonly Diagnostic[], string]> { - const runner = await createOpenAPITestRunner(); + const runner = await SimpleTester.createInstance(); const fileType = options["file-type"] || "yaml"; const outputFile = resolveVirtualPath("openapi" + fileType === "json" ? ".json" : ".yaml"); const diagnostics = await runner.diagnose(code, { - emit: ["@typespec/openapi3"], - options: { - "@typespec/openapi3": { ...options, "output-file": outputFile }, + compilerOptions: { + options: { + "@typespec/openapi3": { ...options, "output-file": outputFile }, + }, }, }); - const content = runner.fs.get(outputFile); + const content = runner.fs.fs.get(outputFile); ok(content, "Expected to have found openapi output"); const doc = fileType === "json" ? JSON.parse(content) : parse(content); return [doc, diagnostics, content]; } export async function diagnoseOpenApiFor(code: string, options: OpenAPI3EmitterOptions = {}) { - const runner = await createOpenAPITestRunner(); - const diagnostics = await runner.diagnose(code, { - emit: ["@typespec/openapi3"], - options: { "@typespec/openapi3": options as any }, + const diagnostics = await SimpleTester.diagnose(code, { + compilerOptions: { options: { "@typespec/openapi3": options as any } }, }); return diagnostics; } -export async function openApiFor( - code: string, - versions?: string[], - options: OpenAPI3EmitterOptions = {}, -) { - const host = await createOpenAPITestHost(); - const outPath = resolveVirtualPath("{version}.openapi.json"); - host.addTypeSpecFile( - "./main.tsp", - `import "@typespec/http"; import "@typespec/json-schema"; import "@typespec/rest"; import "@typespec/openapi"; import "@typespec/openapi3";import "@typespec/xml"; ${ - versions ? `import "@typespec/versioning"; using Versioning;` : "" - }using Rest;using Http;using OpenAPI;using TypeSpec.Xml;${code}`, - ); - const diagnostics = await host.diagnose("./main.tsp", { - noEmit: false, - emit: ["@typespec/openapi3"], - options: { "@typespec/openapi3": { ...options, "output-file": outPath } }, +export async function openApiFor(code: string, options: OpenAPI3EmitterOptions = {}) { + const host = await SimpleTester.createInstance(); + const outPath = "{emitter-output-dir}/openapi.json"; + const { outputs } = await host.compile(code, { + compilerOptions: { + options: { "@typespec/openapi3": { ...options, "output-file": outPath } }, + }, }); - expectDiagnosticEmpty(diagnostics); - if (!versions) { - return JSON.parse(host.fs.get(resolveVirtualPath("openapi.json"))!); - } else { - const output: any = {}; - for (const version of versions) { - output[version] = JSON.parse(host.fs.get(interpolatePath(outPath, { version: version }))!); - } - return output; - } + return JSON.parse(outputs["openapi.json"]); } -export async function checkFor(code: string, options: OpenAPI3EmitterOptions = {}) { - const host = await createOpenAPITestRunner(); - return await host.diagnose(code, { - dryRun: true, - emit: ["@typespec/openapi3"], - options: { "@typespec/openapi3": { ...options } }, +export async function openApiForVersions( + code: string, + versions: T[], +): Promise> { + const host = await TesterWithVersioning.createInstance(); + const outPath = "{emitter-output-dir}/{version}.openapi.json"; + const { outputs } = await host.compile(code, { + compilerOptions: { + options: { "@typespec/openapi3": { "output-file": outPath } }, + }, }); + + const output: Record = {} as any; + for (const version of versions) { + output[version] = JSON.parse(outputs[interpolatePath(outPath, { version: version })]!); + } + return output; } export async function oapiForModel( @@ -146,7 +111,6 @@ export async function oapiForModel( }; } `, - undefined, options, ); @@ -163,17 +127,15 @@ export async function openapiWithOptions( code: string, options: OpenAPI3EmitterOptions, ): Promise { - const runner = await createOpenAPITestRunner(); - const outPath = resolvePath("/openapi.json"); + const runner = await SimpleTester.createInstance(); const diagnostics = await runner.diagnose(code, { - emit: ["@typespec/openapi3"], - options: { "@typespec/openapi3": { ...options, "output-file": outPath } }, + compilerOptions: { options: { "@typespec/openapi3": { ...options, "output-file": outPath } } }, }); expectDiagnosticEmpty(diagnostics); - const content = runner.fs.get(outPath)!; + const content = runner.fs.fs.get(outPath)!; return JSON.parse(content); } diff --git a/packages/openapi3/test/versioning.test.ts b/packages/openapi3/test/versioning.test.ts index f29a92e004e..4a9ccbbc4f7 100644 --- a/packages/openapi3/test/versioning.test.ts +++ b/packages/openapi3/test/versioning.test.ts @@ -1,13 +1,16 @@ -import { DecoratorContext, getNamespaceFullName, Namespace } from "@typespec/compiler"; -import { createTestWrapper, expectDiagnostics } from "@typespec/compiler/testing"; -import { deepStrictEqual, strictEqual } from "assert"; +import { expectDiagnostics } from "@typespec/compiler/testing"; +import { deepStrictEqual, ok, strictEqual } from "assert"; import { describe, it } from "vitest"; -import { createOpenAPITestHost, createOpenAPITestRunner } from "./test-host.js"; +import { ApiTester, openApiForVersions } from "./test-host.js"; import { worksFor } from "./works-for.js"; worksFor(["3.0.0", "3.1.0"], ({ openApiFor, version: specVersion }) => { + const TesterWithVersioning = ApiTester.importLibraries() + .using("Http", "Rest", "Versioning") + .emit("@typespec/openapi3", { "openapi-versions": [specVersion] }); + it("works with models", async () => { - const { v1, v2, v3 } = await openApiFor( + const { v1, v2, v3 } = await openApiForVersions( ` @versioned(Versions) @service(#{title: "My Service"}) @@ -47,6 +50,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor, version: specVersion }) => { ); strictEqual(v1.info.version, "v1"); + ok(v1.components?.schemas); deepStrictEqual(v1.components.schemas.Test, { type: "object", properties: { @@ -67,6 +71,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor, version: specVersion }) => { }); strictEqual(v2.info.version, "v2"); + ok(v2.components?.schemas); deepStrictEqual(v2.components.schemas.Test, { type: "object", properties: { @@ -85,7 +90,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor, version: specVersion }) => { }, required: ["prop1", "prop2"], }); - + ok(v3.components?.schemas); strictEqual(v3.info.version, "v3"); deepStrictEqual(v3.components.schemas.Test, { type: "object", @@ -108,59 +113,12 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor, version: specVersion }) => { }); }); - it("doesn't lose parent namespace", async () => { - const host = await createOpenAPITestHost(); - - let storedNamespace: string | undefined = undefined; - host.addJsFile("test.js", { - $armNamespace(context: DecoratorContext, entity: Namespace) { - storedNamespace = getNamespaceFullName(entity); - }, - }); - - const runner = createTestWrapper(host, { - autoImports: [...host.libraries.map((x) => x.name), "./test.js"], - autoUsings: ["TypeSpec.Rest", "TypeSpec.Http", "TypeSpec.OpenAPI", "TypeSpec.Versioning"], - compilerOptions: { - emit: ["@typespec/openapi3"], - options: { "@typespec/openapi3": { "openapi-versions": [specVersion] } }, - }, - }); - - await runner.compile(` - @versioned(Contoso.Library.Versions) - namespace Contoso.Library { - namespace Blah { } - enum Versions { v1 }; - } - @armNamespace - @service(#{title: "Widgets 'r' Us"}) - @useDependency(Contoso.Library.Versions.v1) - namespace Contoso.WidgetService { - model Widget { - @key - @segment("widgets") - id: string; - } - interface Operations { - @test - op get(id: string): Widget; - } - } - `); - - strictEqual(storedNamespace, "Contoso.WidgetService"); - }); - // Test for https://github.com/microsoft/typespec/issues/812 it("doesn't throw errors when using UpdateableProperties", async () => { // if this test throws a duplicate name diagnostic, check that getEffectiveType // is returning the projected type. - const runner = await createOpenAPITestRunner({ - withVersioning: true, - emitterOptions: { "openapi-versions": [specVersion] }, - }); - await runner.compile(` + await TesterWithVersioning.compile( + ` @versioned(Library.Versions) namespace Library { enum Versions { @@ -181,16 +139,14 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor, version: specVersion }) => { oops(...UpdateableProperties): Widget; } } - `); + `, + ); }); describe("versioned resource", () => { it("reports diagnostic without crashing for mismatched versions", async () => { - const runner = await createOpenAPITestRunner({ - withVersioning: true, - emitterOptions: { "openapi-versions": [specVersion] }, - }); - const diagnostics = await runner.diagnose(` + const diagnostics = await TesterWithVersioning.diagnose( + ` @versioned(Versions) @service namespace DemoService; @@ -219,18 +175,16 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor, version: specVersion }) => { @route("/widgets") interface Widgets extends Resource.ResourceOperations {} - `); + `, + ); expectDiagnostics(diagnostics, { code: "@typespec/versioning/incompatible-versioned-reference", }); }); it("succeeds for aligned versions", async () => { - const runner = await createOpenAPITestRunner({ - withVersioning: true, - emitterOptions: { "openapi-versions": [specVersion] }, - }); - await runner.compile(` + await TesterWithVersioning.compile( + ` @versioned(Versions) @service namespace DemoService; @@ -260,7 +214,8 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor, version: specVersion }) => { @added(Versions.v2) @route("/widgets") interface Widgets extends Resource.ResourceOperations {} - `); + `, + ); }); }); }); diff --git a/packages/openapi3/test/works-for.ts b/packages/openapi3/test/works-for.ts index f1c04a1bfbd..a4dfa9e64a9 100644 --- a/packages/openapi3/test/works-for.ts +++ b/packages/openapi3/test/works-for.ts @@ -1,7 +1,6 @@ import { describe } from "vitest"; import { OpenAPIVersion } from "../src/lib.js"; import { - checkFor, diagnoseOpenApiFor, emitOpenApiWithDiagnostics, oapiForModel, @@ -21,7 +20,7 @@ export type SpecHelper = { oapiForModel: typeof oapiForModel; openApiFor: typeof openApiFor; openapiWithOptions: typeof openapiWithOptions; - checkFor: typeof checkFor; + checkFor: typeof diagnoseOpenApiFor; diagnoseOpenApiFor: typeof diagnoseOpenApiFor; emitOpenApiWithDiagnostics: typeof emitOpenApiWithDiagnostics; objectSchemaIndexer: ObjectSchemaIndexer; @@ -34,12 +33,12 @@ function createSpecHelpers(version: OpenAPIVersion): SpecHelper { version, oapiForModel: (...[name, modelDef, options]: Parameters) => oapiForModel(name, modelDef, { ...options, "openapi-versions": [version] }), - openApiFor: (...[code, versions, options]: Parameters) => - openApiFor(code, versions, { ...options, "openapi-versions": [version] }), + openApiFor: (...[code, options]: Parameters) => + openApiFor(code, { ...options, "openapi-versions": [version] }), openapiWithOptions: (...[code, options]: Parameters) => openapiWithOptions(code, { ...options, "openapi-versions": [version] }), - checkFor: (...[code, options]: Parameters) => - checkFor(code, { ...options, "openapi-versions": [version] }), + checkFor: (...[code, options]: Parameters) => + diagnoseOpenApiFor(code, { ...options, "openapi-versions": [version] }), diagnoseOpenApiFor: (...[code, options]: Parameters) => diagnoseOpenApiFor(code, { ...options, "openapi-versions": [version] }), emitOpenApiWithDiagnostics: ( diff --git a/packages/openapi3/test/xml-models.test.ts b/packages/openapi3/test/xml-models.test.ts index 907afd224a1..e5ce5eb56ed 100644 --- a/packages/openapi3/test/xml-models.test.ts +++ b/packages/openapi3/test/xml-models.test.ts @@ -1,7 +1,7 @@ import { expectDiagnostics } from "@typespec/compiler/testing"; import { deepStrictEqual } from "assert"; import { describe, expect, it } from "vitest"; -import { createOpenAPITestRunner } from "./test-host.js"; +import { SimpleTester } from "./test-host.js"; import { worksFor } from "./works-for.js"; worksFor(["3.0.0", "3.1.0"], ({ emitOpenApiWithDiagnostics, oapiForModel }) => { @@ -229,8 +229,7 @@ worksFor(["3.0.0", "3.1.0"], ({ emitOpenApiWithDiagnostics, oapiForModel }) => { describe("@unwrapped", () => { it("warning if unwrapped not array", async () => { - const runner = await createOpenAPITestRunner(); - const diagnostics = await runner.diagnose( + const diagnostics = await SimpleTester.diagnose( `model Book { @unwrapped id: string; diff --git a/packages/rest/test/resource.test.ts b/packages/rest/test/resource.test.ts index b2192257546..e3663762d2a 100644 --- a/packages/rest/test/resource.test.ts +++ b/packages/rest/test/resource.test.ts @@ -1,10 +1,9 @@ -import { Model } from "@typespec/compiler"; -import { expectDiagnostics } from "@typespec/compiler/testing"; +import { expectDiagnostics, t } from "@typespec/compiler/testing"; import { deepStrictEqual, ok, strictEqual } from "assert"; import { describe, it } from "vitest"; import { getResourceTypeKey } from "../src/resource.js"; import { getSegment } from "../src/rest.js"; -import { compileOperations, createRestTestRunner, getRoutesFor } from "./test-host.js"; +import { Tester, compileOperations, getRoutesFor } from "./test-host.js"; describe("rest: resources", () => { it("@resource decorator emits a diagnostic when a @key property is not found", async () => { @@ -13,7 +12,7 @@ describe("rest: resources", () => { model Thing { id: string; } - `); + `); expectDiagnostics(diagnostics, { code: "@typespec/rest/resource-missing-key", @@ -23,50 +22,41 @@ describe("rest: resources", () => { }); it("getResourceTypeKey works for base classes", async () => { - const runner = await createRestTestRunner(); - const { Thing } = (await runner.compile(` - + const { Thing, program } = await Tester.compile(t.code` model BaseThing { @key id: string; } - @test @resource("things") - model Thing extends BaseThing { + model ${t.model("Thing")} extends BaseThing { extra: string; } - `)) as { Thing: Model }; + `); - // Check the key property to ensure the segment got added - const key = getResourceTypeKey(runner.program, Thing); + const key = getResourceTypeKey(program, Thing); ok(key, "No key property found."); - strictEqual(getSegment(runner.program, key.keyProperty), "things"); + strictEqual(getSegment(program, key.keyProperty), "things"); }); it("@resource decorator applies @segment decorator on the @key property", async () => { - const runner = await createRestTestRunner(); - const { Thing } = (await runner.compile(` - @test + const { Thing, program } = await Tester.compile(t.code` @resource("things") - model Thing { + model ${t.model("Thing")} { @key id: string; } - `)) as { Thing: Model }; + `); - // Check the key property to ensure the segment got added - const key = getResourceTypeKey(runner.program, Thing); + const key = getResourceTypeKey(program, Thing); ok(key, "No key property found."); - strictEqual(getSegment(runner.program, key.keyProperty), "things"); + strictEqual(getSegment(program, key.keyProperty), "things"); }); it("@resource decorator applies @segment decorator that reaches route generation", async () => { - const routes = await getRoutesFor( - ` + const routes = await getRoutesFor(` using Rest.Resource; - @test @resource("things") model Thing { @key("thingId") @@ -76,8 +66,7 @@ describe("rest: resources", () => { @error model Error {} interface Things extends ResourceRead {} - `, - ); + `); deepStrictEqual(routes, [ { @@ -89,8 +78,7 @@ describe("rest: resources", () => { }); it("resources: generates standard operations for resource types and their children", async () => { - const routes = await getRoutesFor( - ` + const routes = await getRoutesFor(` using Rest.Resource; namespace Things { @@ -112,8 +100,7 @@ describe("rest: resources", () => { interface Things extends ResourceOperations {} interface Subthings extends ResourceOperations {} } - `, - ); + `); deepStrictEqual(routes, [ { @@ -170,8 +157,7 @@ describe("rest: resources", () => { }); it("resources: collection action paths are generated correctly", async () => { - const routes = await getRoutesFor( - ` + const routes = await getRoutesFor(` using Rest.Resource; model Thing { @@ -191,8 +177,7 @@ describe("rest: resources", () => { @actionSeparator(":") op exportThingWithColon2(): {}; } - `, - ); + `); deepStrictEqual(routes, [ { @@ -219,7 +204,7 @@ describe("rest: resources", () => { @key("anotherId") secondId: string; } - `); + `); expectDiagnostics(diagnostics, [ { @@ -261,7 +246,7 @@ describe("rest: resources", () => { subSubthingId: string; } } - `); + `); expectDiagnostics(diagnostics, [ { @@ -280,8 +265,7 @@ describe("rest: resources", () => { }); it("resources: standard lifecycle operations have expected paths and verbs", async () => { - const routes = await getRoutesFor( - ` + const routes = await getRoutesFor(` using Rest.Resource; model Thing { @@ -294,8 +278,7 @@ describe("rest: resources", () => { interface Things extends ResourceOperations, ResourceCreateOrReplace { } - `, - ); + `); deepStrictEqual(routes, [ { @@ -332,8 +315,7 @@ describe("rest: resources", () => { }); it("singleton resource: generates standard operations", async () => { - const routes = await getRoutesFor( - ` + const routes = await getRoutesFor(` using Rest.Resource; namespace Things { @@ -353,8 +335,7 @@ describe("rest: resources", () => { interface Things extends ResourceRead {} interface ThingsSingleton extends SingletonResourceOperations {} } - `, - ); + `); deepStrictEqual(routes, [ { @@ -376,8 +357,7 @@ describe("rest: resources", () => { }); it("extension resources: generates standard operations for extensions on parent and child resources", async () => { - const routes = await getRoutesFor( - ` + const routes = await getRoutesFor(` using Rest.Resource; namespace Things { @@ -405,8 +385,7 @@ describe("rest: resources", () => { interface ThingsExtension extends ExtensionResourceOperations {} interface SubthingsExtension extends ExtensionResourceOperations {} } - `, - ); + `); deepStrictEqual(routes, [ { @@ -463,17 +442,14 @@ describe("rest: resources", () => { }); it("emit diagnostic if missing @key decorator on resource", async () => { - const runner = await createRestTestRunner(); - const diagnostics = await runner.diagnose( - ` + const diagnostics = await Tester.diagnose(` using Rest.Resource; interface Dogs extends ResourceOperations {} model Dog {} @error model Error {code: string} - `, - ); + `); expectDiagnostics(diagnostics, { code: "@typespec/rest/resource-missing-key", message: @@ -482,9 +458,7 @@ describe("rest: resources", () => { }); it("emit diagnostic if missing @error decorator on error", async () => { - const runner = await createRestTestRunner(); - const diagnostics = await runner.diagnose( - ` + const diagnostics = await Tester.diagnose(` using Rest.Resource; interface Dogs extends ResourceOperations {} @@ -493,8 +467,7 @@ describe("rest: resources", () => { @key foo: string } model Error {code: string} - `, - ); + `); expectDiagnostics(diagnostics, { code: "@typespec/rest/resource-missing-error", message: diff --git a/packages/rest/test/rest-decorators.test.ts b/packages/rest/test/rest-decorators.test.ts index e14a67993d4..1bea0e3abef 100644 --- a/packages/rest/test/rest-decorators.test.ts +++ b/packages/rest/test/rest-decorators.test.ts @@ -1,27 +1,20 @@ -import { Scalar } from "@typespec/compiler"; -import { BasicTestRunner, expectDiagnostics } from "@typespec/compiler/testing"; +import { expectDiagnostics, t } from "@typespec/compiler/testing"; import { ok, strictEqual } from "assert"; -import { beforeEach, describe, it } from "vitest"; +import { describe, it } from "vitest"; import { getResourceLocationType } from "../src/rest.js"; -import { createRestTestRunner } from "./test-host.js"; +import { Tester } from "./test-host.js"; describe("rest: rest decorators", () => { - let runner: BasicTestRunner; - - beforeEach(async () => { - runner = await createRestTestRunner(); - }); - describe("@resourceLocation", () => { it("emit diagnostic when used on non-model", async () => { - const diagnostics = await runner.diagnose(` - model Widget {}; + const diagnostics = await Tester.diagnose(` + model Widget {}; - @TypeSpec.Rest.Private.resourceLocation(Widget) - op test(): string; + @TypeSpec.Rest.Private.resourceLocation(Widget) + op test(): string; - scalar WidgetLocation extends ResourceLocation; - `); + scalar WidgetLocation extends ResourceLocation; + `); expectDiagnostics(diagnostics, [ { @@ -33,14 +26,13 @@ describe("rest: rest decorators", () => { }); it("marks a model type as a resource location for a specific type", async () => { - const { WidgetLocation } = (await runner.compile(` - model Widget {}; + const { WidgetLocation, program } = await Tester.compile(t.code` + model Widget {}; - @test - scalar WidgetLocation extends ResourceLocation; -`)) as { WidgetLocation: Scalar }; + scalar ${t.scalar("WidgetLocation")} extends ResourceLocation; + `); - const resourceType = getResourceLocationType(runner.program, WidgetLocation.baseScalar!); + const resourceType = getResourceLocationType(program, WidgetLocation.baseScalar!); ok(resourceType); strictEqual(resourceType!.name, "Widget"); }); diff --git a/packages/rest/test/routes.test.ts b/packages/rest/test/routes.test.ts index d63baf40dd3..3841570f663 100644 --- a/packages/rest/test/routes.test.ts +++ b/packages/rest/test/routes.test.ts @@ -1,14 +1,9 @@ import { ModelProperty, Operation } from "@typespec/compiler"; -import { expectDiagnostics } from "@typespec/compiler/testing"; +import { expectDiagnostics, t } from "@typespec/compiler/testing"; import { isSharedRoute } from "@typespec/http"; import { deepStrictEqual, strictEqual } from "assert"; import { describe, expect, it } from "vitest"; -import { - compileOperations, - createRestTestRunner, - getOperations, - getRoutesFor, -} from "./test-host.js"; +import { Tester, compileOperations, getOperations, getRoutesFor } from "./test-host.js"; describe("rest: routes", () => { it("always produces a route starting with /", async () => { @@ -222,9 +217,11 @@ describe("rest: routes", () => { }); it("emit diagnostic if passing arguments to autoroute decorators", async () => { - const [_, diagnostics] = await compileOperations(` + const [_, diagnostics] = await compileOperations( + ` @autoRoute("/test") op test(): string; - `); + `, + ); expectDiagnostics(diagnostics, { code: "invalid-argument-count", @@ -234,7 +231,8 @@ describe("rest: routes", () => { describe("use of @route with @autoRoute", () => { it("can override library operation route in service", async () => { - const ops = await getOperations(` + const ops = await getOperations( + ` namespace Lib { @route("one") op action(): void; @@ -246,7 +244,8 @@ describe("rest: routes", () => { @route("my") op my2 is Lib.action; } - `); + `, + ); strictEqual(ops[0].verb, "get"); strictEqual(ops[0].path, "/one"); strictEqual(ops[1].verb, "get"); @@ -254,7 +253,8 @@ describe("rest: routes", () => { }); it("can override library interface route in service", async () => { - const ops = await getOperations(` + const ops = await getOperations( + ` namespace Lib { @route("one") interface Ops { @@ -269,7 +269,8 @@ describe("rest: routes", () => { @route("my") interface Mys2 extends Lib.Ops {} } - `); + `, + ); strictEqual(ops[0].verb, "get"); strictEqual(ops[0].path, "/"); strictEqual(ops[1].verb, "get"); @@ -277,7 +278,8 @@ describe("rest: routes", () => { }); it("can override library interface route in service without changing library", async () => { - const ops = await getOperations(` + const ops = await getOperations( + ` namespace Lib { @route("one") interface Ops { @@ -291,7 +293,8 @@ describe("rest: routes", () => { op op2 is Lib.Ops.action; } - `); + `, + ); strictEqual(ops[1].verb, "get"); strictEqual(ops[1].path, "/my"); strictEqual(ops[1].container.kind, "Interface"); @@ -301,7 +304,8 @@ describe("rest: routes", () => { }); it("prepends @route in service when library operation uses @autoRoute", async () => { - const ops = await getOperations(` + const ops = await getOperations( + ` namespace Lib { @autoRoute op action(@path @segment("pets") id: string): void; @@ -314,7 +318,8 @@ describe("rest: routes", () => { @route("my") op my2 is Lib.action; } - `); + `, + ); strictEqual(ops[0].verb, "get"); strictEqual(ops[0].path, "/pets/{id}"); strictEqual(ops[1].verb, "get"); @@ -322,7 +327,8 @@ describe("rest: routes", () => { }); it("prepends @route in service when library interface operation uses @autoRoute", async () => { - const ops = await getOperations(` + const ops = await getOperations( + ` namespace Lib { interface Ops { @autoRoute @@ -336,7 +342,8 @@ describe("rest: routes", () => { @route("my") interface Mys2 extends Lib.Ops {}; } - `); + `, + ); strictEqual(ops[0].verb, "get"); strictEqual(ops[0].path, "/pets/{id}"); strictEqual(ops[1].verb, "get"); @@ -344,7 +351,8 @@ describe("rest: routes", () => { }); it("prepends @route in service when library interface uses @autoRoute", async () => { - const ops = await getOperations(` + const ops = await getOperations( + ` namespace Lib { @autoRoute interface Ops { @@ -358,7 +366,8 @@ describe("rest: routes", () => { @route("my") interface Mys2 extends Lib.Ops {}; } - `); + `, + ); strictEqual(ops[0].verb, "get"); strictEqual(ops[0].path, "/pets/{id}"); strictEqual(ops[1].verb, "get"); @@ -491,20 +500,17 @@ describe("rest: routes", () => { }); it("@autoRoute operations can also be shared routes", async () => { - const runner = await createRestTestRunner(); - const { get1, get2 } = (await runner.compile(` - @test + const { get1, get2, program } = await Tester.compile(t.code` @autoRoute @sharedRoute - op get1(@segment("get1") @path name: string): string; + op ${t.op("get1")}(@segment("get1") @path name: string): string; - @test @autoRoute - op get2(@segment("get2") @path name: string): string; - `)) as { get1: Operation; get2: Operation }; + op ${t.op("get2")}(@segment("get2") @path name: string): string; + `); - strictEqual(isSharedRoute(runner.program, get1), true); - strictEqual(isSharedRoute(runner.program, get2), false); + strictEqual(isSharedRoute(program, get1), true); + strictEqual(isSharedRoute(program, get2), false); }); it("emits a diagnostic when @sharedRoute is used on action without explicit name", async () => { diff --git a/packages/rest/test/test-host.ts b/packages/rest/test/test-host.ts index d72f21388d9..e1e75a0f87d 100644 --- a/packages/rest/test/test-host.ts +++ b/packages/rest/test/test-host.ts @@ -1,11 +1,6 @@ -import { Diagnostic } from "@typespec/compiler"; -import { - BasicTestRunner, - createTestHost, - createTestWrapper, - expectDiagnosticEmpty, - TestHost, -} from "@typespec/compiler/testing"; +import type { Diagnostic } from "@typespec/compiler"; +import { resolvePath } from "@typespec/compiler"; +import { createTester, expectDiagnosticEmpty } from "@typespec/compiler/testing"; import { getAllHttpServices, HttpOperation, @@ -13,18 +8,12 @@ import { HttpVerb, } from "@typespec/http"; import { unsafe_RouteResolutionOptions as RouteResolutionOptions } from "@typespec/http/experimental"; -import { HttpTestLibrary } from "@typespec/http/testing"; -import { RestTestLibrary } from "../src/testing/index.js"; -export async function createRestTestHost(): Promise { - return createTestHost({ - libraries: [HttpTestLibrary, RestTestLibrary], - }); -} -export async function createRestTestRunner(): Promise { - const host = await createRestTestHost(); - return createTestWrapper(host, { autoUsings: ["TypeSpec.Http", "TypeSpec.Rest"] }); -} +export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: ["@typespec/http", "@typespec/rest"], +}) + .importLibraries() + .using("Http", "Rest"); export interface RouteDetails { path: string; @@ -51,9 +40,6 @@ export interface SimpleOperationDetails { path: string; params: { params: Array<{ name: string; type: HttpOperationParameter["type"] }>; - /** - * name of explicit `@body` parameter or array of unannotated parameter names that make up the body. - */ body?: string | string[]; }; } @@ -86,22 +72,17 @@ export async function getOperationsWithServiceNamespace( code: string, routeOptions?: RouteResolutionOptions, ): Promise<[HttpOperation[], readonly Diagnostic[]]> { - const runner = await createRestTestRunner(); - await runner.compileAndDiagnose( + const [result, diagnostics] = await Tester.compileAndDiagnose( `@service(#{title: "Test Service"}) namespace TestService; ${code}`, - { - noEmit: true, - }, ); - const [services] = getAllHttpServices(runner.program, routeOptions); - return [services[0].operations, runner.program.diagnostics]; + const [services] = getAllHttpServices(result.program, routeOptions); + return [services[0].operations, diagnostics]; } export async function getOperations(code: string): Promise { - const runner = await createRestTestRunner(); - await runner.compile(code); - const [services, diagnostics] = getAllHttpServices(runner.program); + const { program } = await Tester.compile(code); + const [services, diagnostics] = getAllHttpServices(program); expectDiagnosticEmpty(diagnostics); return services[0].operations; diff --git a/packages/sse/test/decorators.test.ts b/packages/sse/test/decorators.test.ts index 45babde4edc..df513cd9fc3 100644 --- a/packages/sse/test/decorators.test.ts +++ b/packages/sse/test/decorators.test.ts @@ -1,19 +1,13 @@ import type { UnionVariant } from "@typespec/compiler"; -import { expectDiagnostics, type BasicTestRunner } from "@typespec/compiler/testing"; -import { beforeEach, describe, expect, it } from "vitest"; +import { expectDiagnostics, t } from "@typespec/compiler/testing"; +import { describe, expect, it } from "vitest"; import { isTerminalEvent } from "../src/decorators.js"; -import { createSSETestRunner } from "./test-host.js"; - -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createSSETestRunner(); -}); +import { Tester } from "./test-host.js"; describe("@terminalEvent", () => { it("marks the model as a terminal event", async () => { - const { TerminalEvent } = await runner.compile( - ` + const { TerminalEvent, program } = await Tester.compile( + t.code` @events union TestEvents { { done: false, @data message: string}, @@ -25,11 +19,11 @@ union TestEvents { `, ); - expect(isTerminalEvent(runner.program, TerminalEvent as UnionVariant)).toBe(true); + expect(isTerminalEvent(program, TerminalEvent as UnionVariant)).toBe(true); }); it("can only be applied to union variants within a union decorated with @events", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` union TestEvents { { done: false, @data message: string}, diff --git a/packages/sse/test/models.test.ts b/packages/sse/test/models.test.ts index 57e0db242c4..35bb1999972 100644 --- a/packages/sse/test/models.test.ts +++ b/packages/sse/test/models.test.ts @@ -1,32 +1,22 @@ -import type { BasicTestRunner } from "@typespec/compiler/testing"; +import { t } from "@typespec/compiler/testing"; import { getContentTypes } from "@typespec/http"; import { getStreamOf } from "@typespec/streams"; -import { assert, beforeEach, describe, expect, it } from "vitest"; -import { createSSETestRunner } from "./test-host.js"; - -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createSSETestRunner(); -}); +import { describe, expect, it } from "vitest"; +import { Tester } from "./test-host.js"; describe("SSEStream", () => { it("sets streamOf, contentType ('text/event-stream'), and body", async () => { - const { Foo, TestEvents } = await runner.compile(` - @test + const { Foo, TestEvents, program } = await Tester.compile(t.code` @events - union TestEvents { + union ${t.union("TestEvents")} { foo: string, bar: string, } - @test model Foo is SSEStream; + model ${t.model("Foo")} is SSEStream; `); - assert(Foo.kind === "Model"); - assert(TestEvents.kind === "Union"); - - expect(getStreamOf(runner.program, Foo)).toBe(TestEvents); + expect(getStreamOf(program, Foo)).toBe(TestEvents); expect(getContentTypes(Foo.properties.get("contentType")!)[0]).toEqual(["text/event-stream"]); expect(Foo.properties.get("body")!.type).toMatchObject({ kind: "Scalar", diff --git a/packages/sse/test/test-host.ts b/packages/sse/test/test-host.ts index 0f27e4efb6f..622aae21b25 100644 --- a/packages/sse/test/test-host.ts +++ b/packages/sse/test/test-host.ts @@ -1,16 +1,8 @@ -import { createTestHost, createTestWrapper } from "@typespec/compiler/testing"; -import { EventsTestLibrary } from "@typespec/events/testing"; -import { HttpTestLibrary } from "@typespec/http/testing"; -import { StreamsTestLibrary } from "@typespec/streams/testing"; -import { SSETestLibrary } from "../src/testing/index.js"; +import { resolvePath } from "@typespec/compiler"; +import { createTester } from "@typespec/compiler/testing"; -export async function createSSETestHost() { - return createTestHost({ - libraries: [EventsTestLibrary, HttpTestLibrary, StreamsTestLibrary, SSETestLibrary], - }); -} - -export async function createSSETestRunner() { - const host = await createSSETestHost(); - return createTestWrapper(host, { autoUsings: ["TypeSpec.Events", "TypeSpec.SSE"] }); -} +export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: ["@typespec/events", "@typespec/http", "@typespec/streams", "@typespec/sse"], +}) + .importLibraries() + .using("Events", "SSE"); diff --git a/packages/streams/test/decorators.test.ts b/packages/streams/test/decorators.test.ts index 4c3465d059c..ea6fe6ee3ae 100644 --- a/packages/streams/test/decorators.test.ts +++ b/packages/streams/test/decorators.test.ts @@ -1,38 +1,36 @@ import type { Model } from "@typespec/compiler"; -import type { BasicTestRunner } from "@typespec/compiler/testing"; -import { beforeEach, describe, expect, it } from "vitest"; +import { t } from "@typespec/compiler/testing"; +import { describe, expect, it } from "vitest"; import { getStreamOf } from "../src/decorators.js"; -import { createStreamsTestRunner } from "./test-host.js"; - -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createStreamsTestRunner(); -}); +import { Tester } from "./test-host.js"; describe("@streamOf", () => { it("provides stream protocol type", async () => { - const { Blob } = await runner.compile(`@test @streamOf(string) model Blob {}`); + const { Blob, program } = await Tester.compile(t.code` + @streamOf(string) + model ${t.model("Blob")} {} + `); - expect(getStreamOf(runner.program, Blob as Model)).toMatchObject({ + expect(getStreamOf(program, Blob as Model)).toMatchObject({ kind: "Scalar", name: "string", }); }); it("returns undefined if model is not decorated", async () => { - const { Blob } = await runner.compile(`@test model Blob {}`); + const { Blob, program } = await Tester.compile(t.code` + model ${t.model("Blob")} {} + `); - expect(getStreamOf(runner.program, Blob as Model)).toBeUndefined(); + expect(getStreamOf(program, Blob as Model)).toBeUndefined(); }); it("is automatically set on the Stream model", async () => { - const { CustomStream, Message } = await runner.compile( - ` - @test model Message { id: string, text: string } - @test model CustomStream is Stream {}`, - ); + const { CustomStream, Message, program } = await Tester.compile(t.code` + model ${t.model("Message")} { id: string, text: string } + model ${t.model("CustomStream")} is Stream {} + `); - expect(getStreamOf(runner.program, CustomStream as Model)).toBe(Message); + expect(getStreamOf(program, CustomStream as Model)).toBe(Message); }); }); diff --git a/packages/streams/test/test-host.ts b/packages/streams/test/test-host.ts index 1bb51f7ac3a..09b8ee81b96 100644 --- a/packages/streams/test/test-host.ts +++ b/packages/streams/test/test-host.ts @@ -1,13 +1,8 @@ -import { createTestHost, createTestWrapper } from "@typespec/compiler/testing"; -import { StreamsTestLibrary } from "../src/testing/index.js"; +import { resolvePath } from "@typespec/compiler"; +import { createTester } from "@typespec/compiler/testing"; -export async function createStreamsTestHost() { - return createTestHost({ - libraries: [StreamsTestLibrary], - }); -} - -export async function createStreamsTestRunner() { - const host = await createStreamsTestHost(); - return createTestWrapper(host, { autoUsings: ["TypeSpec.Streams"] }); -} +export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: ["@typespec/streams"], +}) + .importLibraries() + .using("Streams"); diff --git a/packages/versioning/test/incompatible-versioning.test.ts b/packages/versioning/test/incompatible-versioning.test.ts index a36eb881bec..383d1ce4bbb 100644 --- a/packages/versioning/test/incompatible-versioning.test.ts +++ b/packages/versioning/test/incompatible-versioning.test.ts @@ -1,30 +1,19 @@ import { - createTestWrapper, expectDiagnosticEmpty, expectDiagnostics, - type BasicTestRunner, - type TestHost, + type TesterInstance, } from "@typespec/compiler/testing"; import { ok } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { createVersioningTestHost, createVersioningTestRunner } from "./test-host.js"; +import { Tester } from "./test-host.js"; describe("versioning: incompatible use of decorators", () => { - let runner: BasicTestRunner; - let host: TestHost; + let runner: TesterInstance; const imports: string[] = []; beforeEach(async () => { - host = await createVersioningTestHost(); - runner = createTestWrapper(host, { - wrapper: (code) => ` - import "@typespec/versioning"; - ${imports.map((i) => `import "${i}";`).join("\n")} - using Versioning; - ${code}`, - }); + runner = await Tester.import(...imports).createInstance(); }); - it("emit diagnostic when version enum has duplicate values", async () => { const diagnostics = await runner.diagnose(` @versioned(Versions) @@ -64,24 +53,17 @@ describe("versioning: incompatible use of decorators", () => { }); describe("versioning: validate incompatible references", () => { - let runner: BasicTestRunner; - let host: TestHost; - const imports: string[] = []; + let runner: TesterInstance; beforeEach(async () => { - host = await createVersioningTestHost(); - runner = createTestWrapper(host, { - wrapper: (code) => ` - import "@typespec/versioning"; - ${imports.map((i) => `import "${i}";`).join("\n")} - using Versioning; - + runner = await Tester.wrap( + (code) => ` @versioned(Versions) namespace TestService { enum Versions {v1, v2, v3, v4} ${code} }`, - }); + ).createInstance(); }); describe("operation", () => { @@ -850,18 +832,27 @@ describe("versioning: validate incompatible references", () => { }); describe("interface templates", () => { - beforeEach(() => { - imports.push("./lib.tsp"); - host.addTypeSpecFile( - "lib.tsp", - ` - namespace Lib; - interface Ops { - get(): T[]; - } + beforeEach(async () => { + runner = await Tester.import("./lib.tsp") + .files({ + "lib.tsp": ` + namespace Lib; + interface Ops { + get(): T[]; + } `, - ); + }) + .wrap( + (code) => ` + @versioned(Versions) + namespace TestService { + enum Versions {v1, v2, v3, v4} + ${code} + }`, + ) + .createInstance(); }); + it("emit diagnostic when extending interface with versioned type argument from unversioned interface", async () => { const diagnostics = await runner.diagnose( ` @@ -915,15 +906,9 @@ describe("versioning: validate incompatible references", () => { }); describe("with @useDependency", () => { - let runner: BasicTestRunner; - - beforeEach(async () => { - runner = await createVersioningTestRunner(); - }); - it("emit diagnostic when referencing incompatible version addition via version dependency", async () => { // Here Foo was added in v2 which makes it only available in 1 & 2. - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @versioned(Versions) namespace VersionedLib { enum Versions {l1, l2} @@ -957,7 +942,7 @@ describe("versioning: validate incompatible references", () => { it("emit diagnostic when referencing incompatible version removal via version dependency", async () => { // Here Foo was added in v2 which makes it only available in 1 & 2. - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @versioned(Versions) namespace VersionedLib { enum Versions {l1, l2, l3} @@ -991,7 +976,7 @@ describe("versioning: validate incompatible references", () => { it("doesn't emit diagnostic if all version use the same one", async () => { // Here Foo was added in v2 which makes it only available in 1 & 2. - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @versioned(Versions) namespace VersionedLib { enum Versions {l1, l2} @@ -1019,7 +1004,7 @@ describe("versioning: validate incompatible references", () => { it("emit diagnostic when using item that was added in a later version of library", async () => { // Here Foo was added in v2 but version 1 was selected. - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @versioned(Versions) namespace VersionedLib { enum Versions {l1, l2} @@ -1041,7 +1026,7 @@ describe("versioning: validate incompatible references", () => { it("emit diagnostic when using item that was removed in an earlier version of library", async () => { // Here Foo was removed in v2 but version 2 was selected. - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.diagnose(` @versioned(Versions) namespace VersionedLib { enum Versions {l1, l2} diff --git a/packages/versioning/test/mutations/apply-snapshot-versioning.test.ts b/packages/versioning/test/mutations/apply-snapshot-versioning.test.ts index 20c0a3c2c34..421f43ebc65 100644 --- a/packages/versioning/test/mutations/apply-snapshot-versioning.test.ts +++ b/packages/versioning/test/mutations/apply-snapshot-versioning.test.ts @@ -3,7 +3,7 @@ import { unsafe_mutateSubgraphWithNamespace } from "@typespec/compiler/experimen import { strictEqual } from "assert"; import { describe, expect, it } from "vitest"; import { getVersioningMutators } from "../../src/mutator.js"; -import { createVersioningTestRunner } from "../test-host.js"; +import { Tester } from "../test-host.js"; const baseCode = ` @versioned(Versions) @@ -15,13 +15,14 @@ const baseCode = ` async function testMutationLogic( code: string, ): Promise<{ v1: Namespace; v2: Namespace; v3: Namespace }> { - const runner = await createVersioningTestRunner(); + const runner = await Tester.createInstance(); const fullCode = baseCode + "\n" + code; - const { Service } = (await runner.compile(fullCode)) as { Service: Namespace }; - const mutators = getVersioningMutators(runner.program, Service); + const { Service } = await runner.compile(fullCode); + const mutators = getVersioningMutators(runner.program, Service as Namespace); strictEqual(mutators?.kind, "versioned"); const [v1, v2, v3] = mutators.snapshots.map( - (x) => unsafe_mutateSubgraphWithNamespace(runner.program, [x.mutator], Service).type, + (x) => + unsafe_mutateSubgraphWithNamespace(runner.program, [x.mutator], Service as Namespace).type, ); return { v1, v2, v3 } as any; } diff --git a/packages/versioning/test/test-host.ts b/packages/versioning/test/test-host.ts index 098e1b76af4..2740ddb0fd7 100644 --- a/packages/versioning/test/test-host.ts +++ b/packages/versioning/test/test-host.ts @@ -1,17 +1,8 @@ -import { - createTestHost, - createTestWrapper, - type BasicTestRunner, - type TestHost, -} from "@typespec/compiler/testing"; -import { VersioningTestLibrary } from "../src/testing/index.js"; +import { resolvePath } from "@typespec/compiler"; +import { createTester } from "@typespec/compiler/testing"; -export async function createVersioningTestHost(): Promise { - return createTestHost({ - libraries: [VersioningTestLibrary], - }); -} -export async function createVersioningTestRunner(): Promise { - const host = await createVersioningTestHost(); - return createTestWrapper(host, { autoUsings: ["TypeSpec.Versioning"] }); -} +export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: ["@typespec/versioning"], +}) + .importLibraries() + .using("Versioning"); diff --git a/packages/versioning/test/versioning-timeline.test.ts b/packages/versioning/test/versioning-timeline.test.ts index 9c6194b64b9..93906958791 100644 --- a/packages/versioning/test/versioning-timeline.test.ts +++ b/packages/versioning/test/versioning-timeline.test.ts @@ -3,7 +3,7 @@ import { deepStrictEqual } from "assert"; import { describe, it } from "vitest"; import { VersioningTimeline } from "../src/versioning-timeline.js"; import { resolveVersions } from "../src/versioning.js"; -import { createVersioningTestRunner } from "./test-host.js"; +import { Tester } from "./test-host.js"; describe("versioning: VersioningTimeline", () => { function generateLibraryNamespace(name: string, versions: string[]) { @@ -22,7 +22,6 @@ describe("versioning: VersioningTimeline", () => { } const libNamespaceNames = libraryVersions.map((_, i) => `TestLibNs_${i}`); - const runner = await createVersioningTestRunner(); const content = [ `@versioned(Versions) namespace TestServiceNs { enum Versions { @@ -33,17 +32,15 @@ describe("versioning: VersioningTimeline", () => { }`, ...libraryVersions.map((x, i) => generateLibraryNamespace(libNamespaceNames[i], x)), ].join("\n"); - await runner.compile(content); + const { program } = await Tester.compile(content); - const serviceNamespace = runner.program - .getGlobalNamespaceType() - .namespaces.get("TestServiceNs")!; + const serviceNamespace = program.getGlobalNamespaceType().namespaces.get("TestServiceNs")!; const libNamespaces: Namespace[] = libNamespaceNames.map( - (x) => runner.program.getGlobalNamespaceType().namespaces.get(x)!, + (x) => program.getGlobalNamespaceType().namespaces.get(x)!, ); - const resolutions = resolveVersions(runner.program, serviceNamespace); + const resolutions = resolveVersions(program, serviceNamespace); const timeline = new VersioningTimeline( - runner.program, + program, resolutions.map((x) => x.versions), ); const timelineMatrix: string[][] = []; diff --git a/packages/xml/test/decorators.test.ts b/packages/xml/test/decorators.test.ts index 8c181074547..591c27662f3 100644 --- a/packages/xml/test/decorators.test.ts +++ b/packages/xml/test/decorators.test.ts @@ -1,13 +1,13 @@ -import { resolveEncodedName, type Model, type ModelProperty } from "@typespec/compiler"; -import { expectDiagnostics, type BasicTestRunner } from "@typespec/compiler/testing"; +import { resolveEncodedName, type Model } from "@typespec/compiler"; +import { expectDiagnostics, t, type TesterInstance } from "@typespec/compiler/testing"; import { beforeEach, describe, expect, it } from "vitest"; import { getNs, isAttribute, isUnwrapped } from "../src/decorators.js"; -import { createXmlTestRunner } from "./test-host.js"; +import { Tester } from "./test-host.js"; -let runner: BasicTestRunner; +let runner: TesterInstance; beforeEach(async () => { - runner = await createXmlTestRunner(); + runner = await Tester.createInstance(); }); describe("@name", () => { @@ -16,7 +16,7 @@ describe("@name", () => { ["model prop", `model Blob {@Xml.name("XmlName") @test title:string}`], ["scalar", `@Xml.name("XmlName") @test scalar Blob extends string;`], ])("%s", async (_, code) => { - const result = await runner.compile(`${code}`); + const result = await runner.compile(t.code`${code}`); const curr = (result.Blob || result.title) as Model; expect(resolveEncodedName(runner.program, curr, "application/xml")).toEqual("XmlName"); }); @@ -24,17 +24,17 @@ describe("@name", () => { describe("@attribute", () => { it("mark property as being an attribute", async () => { - const { id } = (await runner.compile(`model Blob { - @test @Xml.attribute id : string - }`)) as { id: ModelProperty }; + const { id } = await runner.compile(t.code`model Blob { + @Xml.attribute ${t.modelProperty("id")} : string + }`); expect(isAttribute(runner.program, id)).toBe(true); }); it("returns false if property is not decorated", async () => { - const { id } = (await runner.compile(`model Blob { - @test id : string - }`)) as { id: ModelProperty }; + const { id } = await runner.compile(t.code`model Blob { + ${t.modelProperty("id")} : string + }`); expect(isAttribute(runner.program, id)).toBe(false); }); @@ -42,17 +42,17 @@ describe("@attribute", () => { describe("@unwrapped", () => { it("mark property as to not be wrapped", async () => { - const { id } = (await runner.compile(`model Blob { - @test @Xml.unwrapped id : string - }`)) as { id: ModelProperty }; + const { id } = await runner.compile(t.code`model Blob { + @Xml.unwrapped ${t.modelProperty("id")} : string + }`); expect(isUnwrapped(runner.program, id)).toBe(true); }); it("returns false if property is not decorated", async () => { - const { id } = (await runner.compile(`model Blob { - @test id : string - }`)) as { id: ModelProperty }; + const { id } = await runner.compile(t.code`model Blob { + ${t.modelProperty("id")} : string + }`); expect(isUnwrapped(runner.program, id)).toBe(false); }); @@ -60,9 +60,9 @@ describe("@unwrapped", () => { describe("@ns", () => { it("provide the namespace and prefix using string", async () => { - const { id } = await runner.compile(` + const { id } = await runner.compile(t.code` model Blob { - @test @Xml.ns("https://example.com/ns1", "ns1") id : string; + @Xml.ns("https://example.com/ns1", "ns1") ${t.modelProperty("id")} : string; } `); @@ -73,10 +73,10 @@ describe("@ns", () => { }); it("doesn't carry over to children", async () => { - const { id } = await runner.compile(` + const { id } = await runner.compile(t.code` @Xml.ns("https://example.com/ns1", "ns1") model Blob { - @test id : string; + ${t.modelProperty("id")} : string; } `); @@ -84,7 +84,7 @@ describe("@ns", () => { }); it("provide the namespace using enum declaration", async () => { - const { id } = await runner.compile(` + const { id } = await runner.compile(t.code` @Xml.nsDeclarations enum Namespaces { ns1: "https://example.com/ns1", @@ -92,7 +92,7 @@ describe("@ns", () => { } model Blob { - @test @Xml.ns(Namespaces.ns2) id : string; + @Xml.ns(Namespaces.ns2) ${t.modelProperty("id")} : string; } `); diff --git a/packages/xml/test/encoding.test.ts b/packages/xml/test/encoding.test.ts index 3ff3d757ffa..1f754384094 100644 --- a/packages/xml/test/encoding.test.ts +++ b/packages/xml/test/encoding.test.ts @@ -1,14 +1,7 @@ -import type { ModelProperty } from "@typespec/compiler"; -import type { BasicTestRunner } from "@typespec/compiler/testing"; -import { beforeEach, describe, expect, it } from "vitest"; +import { t } from "@typespec/compiler/testing"; +import { describe, expect, it } from "vitest"; import { getXmlEncoding } from "../src/encoding.js"; -import { createXmlTestRunner } from "./test-host.js"; - -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createXmlTestRunner(); -}); +import { Tester } from "./test-host.js"; describe("default encodings", () => { it.each([ @@ -19,19 +12,19 @@ describe("default encodings", () => { ["plainTime", "TypeSpec.Xml.Encoding.xmlTime"], ["bytes", "TypeSpec.Xml.Encoding.xmlBase64Binary"], ])("%s", async (type, expectedEncoding) => { - const { prop } = (await runner.compile(`model Foo { - @test prop: ${type} - }`)) as { prop: ModelProperty }; - const encoding = getXmlEncoding(runner.program, prop); + const { prop, program } = await Tester.compile(t.code`model Foo { + ${t.modelProperty("prop")}: ${type} + }`); + const encoding = getXmlEncoding(program, prop); expect(encoding?.encoding).toEqual(expectedEncoding); }); }); it("override encoding", async () => { - const { prop } = (await runner.compile(`model Foo { + const { prop, program } = await Tester.compile(t.code`model Foo { @encode("rfc3339") - @test prop: utcDateTime; - }`)) as { prop: ModelProperty }; - const encoding = getXmlEncoding(runner.program, prop); + ${t.modelProperty("prop")}: utcDateTime; + }`); + const encoding = getXmlEncoding(program, prop); expect(encoding?.encoding).toEqual("rfc3339"); }); diff --git a/packages/xml/test/test-host.ts b/packages/xml/test/test-host.ts index ada33dbc4c9..225791a8689 100644 --- a/packages/xml/test/test-host.ts +++ b/packages/xml/test/test-host.ts @@ -1,12 +1,8 @@ -import { createTestHost, createTestWrapper } from "@typespec/compiler/testing"; -import { XmlTestLibrary } from "../src/testing/index.js"; +import { resolvePath } from "@typespec/compiler"; +import { createTester } from "@typespec/compiler/testing"; -export async function createXmlTestHost() { - return createTestHost({ - libraries: [XmlTestLibrary], - }); -} -export async function createXmlTestRunner() { - const host = await createXmlTestHost(); - return createTestWrapper(host, { autoUsings: ["TypeSpec.Xml"] }); -} +export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: ["@typespec/xml"], +}) + .importLibraries() + .using("Xml"); diff --git a/website/src/content/current-sidebar.ts b/website/src/content/current-sidebar.ts index 4cd9a1cce2e..6da32097370 100644 --- a/website/src/content/current-sidebar.ts +++ b/website/src/content/current-sidebar.ts @@ -238,6 +238,7 @@ const sidebar: SidebarItem[] = [ "extending-typespec/create-decorators", "extending-typespec/linters", "extending-typespec/codefixes", + "extending-typespec/testing", "extending-typespec/emitters-basics", "extending-typespec/emitter-framework", "extending-typespec/emitter-metadata-handling", diff --git a/website/src/content/docs/docs/extending-typespec/basics.md b/website/src/content/docs/docs/extending-typespec/basics.md index c8674acee39..60a536e537d 100644 --- a/website/src/content/docs/docs/extending-typespec/basics.md +++ b/website/src/content/docs/docs/extending-typespec/basics.md @@ -215,160 +215,7 @@ TypeSpec libraries are defined using `peerDependencies` to avoid having multiple ## Step 4: Testing your TypeSpec library -TypeSpec provides a testing framework to assist in testing libraries. The examples here are shown using Node.js's built-in test framework (available in Node 20+), but any other JS test framework can be used that will provide more advanced features like vitest, which is used in this project. - -### a. Add devDependencies - -Ensure that you have the following in your `package.json`: - -```json -"devDependencies": { - "@types/node": "~18.11.9", - "source-map-support": "^0.5.21" -} -``` - -Also add a `vitest.config.ts` file at the root of your project. - -```ts -import { defineConfig, mergeConfig } from "vitest/config"; - -export default defineConfig({ - test: { - environment: "node", - // testTimeout: 10000, // Uncomment to increase the default timeout - isolate: false, // Your test shouldn't have side effects to this will improve performance. - }, -}); -``` - -### b. Define the testing library - -The first step is to define how your library can be loaded from the test framework. This will allow your library to be reused by other library tests. - -1. Create a new file `./src/testing/index.ts` with the following content - -```ts -import { createTestLibrary, findTestPackageRoot } from "@typespec/compiler/testing"; - -export const MyTestLibrary = createTestLibrary({ - name: "", - // Set this to the absolute path to the root of the package. (e.g. in this case this file would be compiled to ./dist/src/testing/index.js) - packageRoot: await findTestPackageRoot(import.meta.url), -}); -``` - -2. Add an `exports` for the `testing` endpoint to `package.json` (update with correct paths) - -```jsonc -{ - // ... - "main": "dist/src/index.js", - "exports": { - ".": { - "default": "./dist/src/index.js", - "types": "./dist/src/index.d.ts", - }, - "./testing": { - "default": "./dist/src/testing/index.js", - "types": "./dist/src/testing/index.d.ts", - }, - }, -} -``` - -### c. Define the test host and test runner for your library - -Define some of the test framework base pieces that will be used in the tests. There are 2 functions: - -- `createTestHost`: This is a lower-level API that provides a virtual file system. -- `createTestRunner`: This is a wrapper on top of the test host that will automatically add a `main.tsp` file and automatically import libraries. - -Create a new file `test/test-host.js` (change `test` to be your test folder) - -```ts -import { createTestHost, createTestWrapper } from "@typespec/compiler/testing"; -import { RestTestLibrary } from "@typespec/rest/testing"; -import { MyTestLibrary } from "../src/testing/index.js"; - -export async function createMyTestHost() { - return createTestHost({ - libraries: [RestTestLibrary, MyTestLibrary], // Add other libraries you depend on in your tests - }); -} -export async function createMyTestRunner() { - const host = await createMyTestHost(); - return createTestWrapper(host, { autoUsings: ["My"] }); -} -``` - -### d. Write tests - -After setting up that infrastructure you can start writing tests. By default Node.js will run all files matching these patterns: - -``` -**/*.test.?(c|m)js -**/*-test.?(c|m)js -**/*_test.?(c|m)js -**/test-*.?(c|m)js -**/test.?(c|m)js -**/test/**/*.?(c|m)js -``` - -[See nodejs doc](https://nodejs.org/api/test.html) - -```ts -import { createMyTestRunner } from "./test-host.js"; -import { describe, beforeEach, it } from "node:test"; - -describe("my library", () => { - let runner: BasicTestRunner; - - beforeEach(async () => { - runner = await createMyTestRunner(); - }); - - // Check everything works fine - it("does this", async () => { - const { Foo } = await runner.compile(` - @test model Foo {} - `); - strictEqual(Foo.kind, "Model"); - }); - - // Check diagnostics are emitted - it("errors", async () => { - const diagnostics = await runner.diagnose(` - model Bar {} - `); - expectDiagnostics(diagnostics, { code: "...", message: "..." }); - }); -}); -``` - -#### e. `@test` decorator - -The `@test` decorator is a decorator loaded in the test environment. It can be used to collect any decorable type. -When using the `compile` method it will return a `Record` which is a map of all the types annotated with the `@test` decorator. - -```ts -const { Foo, CustomName } = await runner.compile(` - @test model Foo {} - - model Bar { - @test("CustomName") name: string - } -`); - -Foo; // type of: model Foo {} -CustomName; // type of : Bar.name -``` - -#### f. Install VS Code extension for the test framework - -If you are using VS Code, you can install the [Node test runner](https://marketplace.visualstudio.com/items?itemName=connor4312.nodejs-testing) to run your tests from the editor. This will also allow you to easily debug your tests. - -After installing the extension, you should be able to discover, run, and debug your tests from the test explorer. +[Testing](./testing.mdx) see documentation for adding tests to your library. ## Step 5: Publishing your TypeSpec library diff --git a/website/src/content/docs/docs/extending-typespec/testing.mdx b/website/src/content/docs/docs/extending-typespec/testing.mdx new file mode 100644 index 00000000000..675c57520cf --- /dev/null +++ b/website/src/content/docs/docs/extending-typespec/testing.mdx @@ -0,0 +1,322 @@ +--- +title: Testing +tableOfContents: + maxHeadingLevel: 4 +--- + +import { Steps } from '@astrojs/starlight/components'; + +TypeSpec provides a testing framework to assist in testing libraries. The examples here are shown using vitest, but any other JS test framework can be used that will provide more advanced features like vitest, which is used in this project. + +:::note +This is a documentation for the new Testing framework. To migrate from the old one see the [migration guide](#migrate-from-test-host) +::: + +## Setting up vitest + +This step is a basic explanation of how to setup vitest. Please refer to the [vitest documentation](https://vitest.dev/) for more details. + + + +1. Add vitest to your dependencies + + ```diff lang=json title="package.json" + { + "name": "my-library", + "scripts": { + + "test": "vitest run", + + "test:watch": "vitest" + }, + "devDependencies": { + + "vitest": "^3.1.4" + } + } + ``` + +2. Add a `vitest.config.ts` file at the root of your project. + + ```ts title="vitest.config.ts" + import { defineConfig, mergeConfig } from "vitest/config"; + + export default defineConfig({ + test: { + environment: "node", + // testTimeout: 10000, // Uncomment to increase the default timeout + isolate: false, // Your test shouldn't have side effects doing this will improve performance. + }, + }); + ``` + + + +## Quick start + +### Define the tester + +Define a tester for your library. This should be a root level file. It will ensure that file system calls are cached in between tests. + +```ts title="test/tester.ts" +import { createTester } from "@typespec/compiler/testing"; + +const MyTester = createTester({ + libraries: ["@typespec/http", "@typespec/openapi", "my-library"], // Add other libraries you depend on in your tests +}); +``` + +:::note +Unlike the old test wrapper this will not auto import anything. You can pipe with .importLibraries() to import all the libraries you defined in the `createTester` call. +::: + +### Write your first test + +```ts title="test/my-library.test.ts" +import { t } from "@typespec/compiler/testing"; +import { MyTester } from "./tester.js"; +import { it } from "vitest"; + +// Check everything works fine +it("does this", async () => { + const { Foo } = await MyTester.compile(t.code` + model ${t.model("Foo")} {} + `); + strictEqual(Foo.name, "Foo"); +}); + +// Check diagnostics are emitted +it("errors", async () => { + const diagnostics = await MyTester.diagnose(` + model Bar {} + `); + expectDiagnostics(diagnostics, { code: "...", message: "..." }); +}); +``` + +## Tester API + +### `compile` + +Compile the given code and assert no diagnostics were emitted. + +```ts title="test/my-library.test.ts" +// Check everything works fine +it("does this", async () => { + const { Foo } = await MyTester.compile(t.code` + model ${t.model("Foo")} {} + `); + strictEqual(Foo.name, "Foo"); +}); +``` + +### `diagnose` + +Compile the given code and return the diagnostics. + +```ts title="test/my-library.test.ts" +it("errors", async () => { + const diagnostics = await MyTester.diagnose(` + model Bar {} + `); + expectDiagnostics(diagnostics, { code: "...", message: "..." }); +}); +``` + +### `compileAndDiagnose` + +Returns a tuple of the result (same as `compile`) and the diagnostics (same as `diagnose`). + +```ts title="test/my-library.test.ts" +it("does this", async () => { + const [diagnostics, { Foo }] = await MyTester.compileAndDiagnose(t.code` + model ${t.model("Foo")} {} + `); + strictEqual(Foo.name, "Foo"); + expectDiagnostics(diagnostics, { code: "...", message: "..." }); +}); +``` + +## Tester chains + +The tester uses a builder pattern to allow you to configure a tester. Each pipe provides a clone of the tester allowing you to create different testers without modifying the original one. + +### `files` + +This will inject the given files in the tester. + +```ts +import { mockFile } from "@typespec/compiler/testing"; + +const TesterWithFoo = MyTester.files({ + "foo.tsp": ` + model Foo {} + `, + "bar.js": mockFile.js({ + $myDec: () => {}, + }), +}); + +await TesterWithFoo.compile(` + import "./foo.tsp"; + import "./bar.js"; +`); +``` + +### `import` + +Import the given path or libraries + +```ts +import { mockFile } from "@typespec/compiler/testing"; + +const TesterWithFoo = MyTester.import("my-library", "./foo.tsp"); + +await TesterWithFoo.compile(` + model Bar is Foo; +`); +``` + +Example combining with `files` + +```ts +import { mockFile } from "@typespec/compiler/testing"; + +const TesterWithFoo = MyTester.files({ + "foo.tsp": ` + model Foo {} + `, +}).import("./foo.tsp"); + +await TesterWithFoo.compile(` + model Bar is Foo; +`); +``` + +### `importLibraries` + +Import all the libraries originally defined in the `createTester` call. + +```ts +const MyTester = createTester({ + libraries: ["@typespec/http", "@typespec/openapi", "my-library"], // Add other libraries you depend on in your tests +}); + +MyTester.importLibraries(); + +// equivalent to +MyTester.import("@typespec/http", "@typespec/openapi", "my-library"); +``` + +### `using` + +Add the given using + +```ts +import { mockFile } from "@typespec/compiler/testing"; + +const TesterWithFoo = MyTester.using("Http", "MyOrg.MyLibrary"); +``` + +### `wrap` + +Wrap the source of the main file. + +```ts +import { mockFile } from "@typespec/compiler/testing"; + +const TesterWithFoo = MyTester.wrap(x=> ` + model Common {} + ${x} + `); +}); + +await TesterWithFoo.compile(` + model Bar is Common; +`); +``` + +## Collecting types + +The base tester provides a way to easily collect types from the test code in order to use them in the test. There are 3 ways this can be achieved: + +| Option | Type inferred/validated | +| -------------------------------------------- | ----------------------- | +| 1. `t` helper with `t.code` and `t.` | ✅ | +| 2. Flourslash syntax (`/*foo*/`) | | +| 3. `@test` decorator | | + +1. Using the `t` helper with `t.code` and `t.` + +```ts +const { Foo } = await MyTester.compile(t.code` + model ${t.model("Foo")} {} +`); // type of Foo is automatically inferred and validated to be a Model +strictEqual(Foo.name, "Foo"); +``` + +2. Using flourslash syntax to mark the types you want to collect (`/*foo*/`) + +```ts +const { Foo } = await MyTester.compile(t.code` + model /*foo*/Foo {} +`); // Foo is typed as an Entity +strictEqual(Foo.entityKind, "Type"); +strictEqual(Foo.type, "Model"); +strictEqual(Foo.name, "Foo"); +``` + +3. Using the `@test` decorator + +This is mostly kept for backwards compatibility with the old test host. It has the limitation of only being to target decorable types. +It is preferable to use the `t` helper when possible or the flourslash syntax for more complex cases. + +```ts +const { Foo } = await MyTester.compile(t.code` + @test model Foo {} +`); // Foo is typed as an Entity +strictEqual(Foo.entityKind, "Type"); +strictEqual(Foo.type, "Model"); +strictEqual(Foo.name, "Foo"); +``` + +## Migrate from test host + +PR with examples https://github.com/microsoft/typespec/pull/7151 + +```diff lang=ts title="test-host.ts" +- import { createTestHost, createTestWrapper } from "@typespec/compiler/testing"; +- import { HttpTestLibrary } from "@typespec/http/testing"; +- import { RestTestLibrary } from "@typespec/rest/testing"; +- import { MyTestLibrary } from "../src/testing/index.js"; +- +- export async function createMyTestHost() { +- return createTestHost({ +- libraries: [HttpTestLibrary, RestTestLibrary, MyTestLibrary], +- }); +- } +- export async function createMyTestRunner() { +- const host = await createOpenAPITestHost(); +- return createTestWrapper(host, { autoUsings: ["TypeSpec.My"] }); +- } + ++ import { resolvePath } from "@typespec/compiler"; ++ import { createTester } from "@typespec/compiler/testing"; ++ ++ export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { ++ libraries: ["@typespec/http", "@typespec/rest", "@typespec/my"], ++ }) ++ .importLibraries() ++ .using("My"); +``` + +In test files + +```diff lang=ts title="test/my-library.test.ts" + it("mark property as being an attribute", async () => { +- const { id } = (await runner.compile(`model Blob { +- @test @Xml.attribute id : string +- }`)) as { id: ModelProperty }; ++ const { id } = await Tester.compile(t.code`model Blob { ++ @Xml.attribute ${t.modelProperty("id")} : string ++ }`); + expect(isAttribute(runner.program, id)).toBe(true); + }); +```