diff --git a/.chronus/changes/migrate-tester-v2-libs-2026-2-4-17-51-22.md b/.chronus/changes/migrate-tester-v2-libs-2026-2-4-17-51-22.md new file mode 100644 index 00000000000..f98f0d1ea29 --- /dev/null +++ b/.chronus/changes/migrate-tester-v2-libs-2026-2-4-17-51-22.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: internal +packages: + - "@typespec/html-program-viewer" +--- + +Migrate all libraries except compiler to tester v2 diff --git a/.chronus/config.yaml b/.chronus/config.yaml index 88efb045be2..9d6a6af5911 100644 --- a/.chronus/config.yaml +++ b/.chronus/config.yaml @@ -77,4 +77,5 @@ additionalPackages: changedFiles: - "!**/*.md" - "!**/*.test.ts" + - "!**/packages/*/test/**/*" - "!**/*.e2e.ts" diff --git a/packages/asset-emitter/test/host.ts b/packages/asset-emitter/test/host.ts index 0b54ff19914..3e931bf04ec 100644 --- a/packages/asset-emitter/test/host.ts +++ b/packages/asset-emitter/test/host.ts @@ -1,37 +1,22 @@ import { resolvePath } from "@typespec/compiler"; -import { createTestHost, type TypeSpecTestLibrary } from "@typespec/compiler/testing"; -import { fileURLToPath } from "url"; +import { createTester, mockFile } from "@typespec/compiler/testing"; import { expect, type MockInstance, vi } from "vitest"; import { createAssetEmitter, TypeEmitter } from "../src/index.js"; -export const lib: TypeSpecTestLibrary = { - name: "typespec-ts-interface-emitter", - packageRoot: resolvePath(fileURLToPath(import.meta.url), "../../../"), - files: [ - { - realDir: "", - pattern: "package.json", - virtualPath: "./node_modules/typespec-ts-interface-emitter", - }, - { - realDir: "dist/src", - pattern: "*.js", - virtualPath: "./node_modules/typespec-ts-interface-emitter/dist/src", - }, - ], -}; +const Tester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: [], +}); export async function getHostForTypeSpecFile(contents: string, decorators?: Record) { - const host = await createTestHost(); + let tester = Tester; if (decorators) { - await host.addJsFile("dec.js", decorators); + tester = tester.files({ "dec.js": mockFile.js(decorators) }); contents = `import "./dec.js";\n` + contents; } - await host.addTypeSpecFile("main.tsp", contents); - await host.compile("main.tsp", { - outputDir: "tsp-output", + const [result] = await tester.compileAndDiagnose(contents, { + compilerOptions: { outputDir: "tsp-output" }, }); - return host; + return { program: result.program, compilerHost: result.fs.compilerHost }; } export async function emitTypeSpec( diff --git a/packages/events/test/decorators.test.ts b/packages/events/test/decorators.test.ts index 3a03a41b5a7..e577e55b231 100644 --- a/packages/events/test/decorators.test.ts +++ b/packages/events/test/decorators.test.ts @@ -1,32 +1,23 @@ -import type { Model, Union } from "@typespec/compiler"; -import { - expectDiagnosticEmpty, - expectDiagnostics, - type BasicTestRunner, -} from "@typespec/compiler/testing"; -import { assert, beforeEach, describe, expect, it } from "vitest"; +import type { Model } from "@typespec/compiler"; +import { expectDiagnosticEmpty, expectDiagnostics, t } from "@typespec/compiler/testing"; +import { describe, expect, it } from "vitest"; import { getContentType, isEventData, isEvents } from "../src/decorators.js"; import { unsafe_getEventDefinitions as getEventDefinitions } from "../src/experimental/index.js"; -import { createEventsTestRunner } from "./test-host.js"; - -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createEventsTestRunner(); -}); +import { Tester } from "./test-host.js"; describe("@events", () => { it("marks the union as containing event definitions", async () => { - const { MixedEvents } = await runner.compile(`@test @events union MixedEvents {}`); + const { MixedEvents, program } = await Tester.compile( + t.code`@events union ${t.union("MixedEvents")} {}`, + ); - expect(isEvents(runner.program, MixedEvents as Union)).toBe(true); + expect(isEvents(program, MixedEvents)).toBe(true); }); it("can contain multiple event definitions", async () => { - const { MixedEvents, JsonEvent, StringEvent } = await runner.compile( - ` -@test -model StringEvent { + const { MixedEvents, JsonEvent, StringEvent, program } = await Tester.compile( + t.code` +model ${t.model("StringEvent")} { payload: { @Events.contentType("text/plain") @Events.data @@ -34,8 +25,7 @@ model StringEvent { }; } -@test -model JsonEvent { +model ${t.model("JsonEvent")} { @Events.data @Events.contentType("application/json") payload: { @@ -44,9 +34,8 @@ model JsonEvent { }; } -@test @events -union MixedEvents { +union ${t.union("MixedEvents")} { @Events.contentType("application/json") stringEvent: StringEvent, @@ -58,14 +47,10 @@ union MixedEvents { `, ); - assert(MixedEvents.kind === "Union"); - assert(JsonEvent.kind === "Model"); - assert(StringEvent.kind === "Model"); - const variants = Array.from(MixedEvents.variants.values()); - expect(isEvents(runner.program, MixedEvents)).toBe(true); - const [eventDefinitions, diagnostics] = getEventDefinitions(runner.program, MixedEvents); + expect(isEvents(program, MixedEvents)).toBe(true); + const [eventDefinitions, diagnostics] = getEventDefinitions(program, MixedEvents); expectDiagnosticEmpty(diagnostics); expect(eventDefinitions.length).toBe(3); @@ -101,14 +86,15 @@ union MixedEvents { describe("@data", () => { it("marks a model property as being the event payload", async () => { - const { Event } = await runner.compile(`@test model Event { @data foo: string }`); - assert(Event.kind === "Model"); + const { Event, program } = await Tester.compile( + t.code`model ${t.model("Event")} { @data foo: string }`, + ); - expect(isEventData(runner.program, Event.properties.get("foo")!)).toBe(true); + expect(isEventData(program, Event.properties.get("foo")!)).toBe(true); }); it("can be applied directly only once in an event model", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` model SampleEvent { @data @@ -131,7 +117,7 @@ union SampleEvents { }); it("cannot be applied directly more than once in an event model", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` model SampleEvent { @data @@ -155,7 +141,7 @@ union SampleEvents { }); it("cannot be applied indirectly more than once in an event model", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` model Foo { @data @@ -181,7 +167,7 @@ union SampleEvents { }); it("cannot be applied in a Record", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` model Foo { @data @@ -206,7 +192,7 @@ union SampleEvents { }); it("cannot be applied in an Array", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` model Foo { @data @@ -231,7 +217,7 @@ union SampleEvents { }); it("detects multiple event payloads nested in tuples", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` model Foo { @data @@ -265,41 +251,34 @@ union SampleEvents { describe("@contentType", () => { it("can set the top-level event's content-type", async () => { - const { MixedEvents, TargetEvent } = await runner.compile( - ` + const { stringEvent, program } = await Tester.compile( + t.code` model StringEvent { @Events.contentType("text/plain") @Events.data payload: string; } -@test @events union MixedEvents { - @test("TargetEvent") @Events.contentType("application/json") - stringEvent: StringEvent, + ${t.unionVariant("stringEvent")}: StringEvent, } `, ); - assert(MixedEvents.kind === "Union"); - assert(TargetEvent.kind === "UnionVariant"); - - expect(getContentType(runner.program, TargetEvent)).toBe("application/json"); + expect(getContentType(program, stringEvent)).toBe("application/json"); }); it("can set the event payload's content-type", async () => { - const { MixedEvents, EventPayload } = await runner.compile( - ` + const { payload, program } = await Tester.compile( + t.code` model StringEvent { - @test("EventPayload") @Events.contentType("text/plain") @Events.data - payload: string; + ${t.modelProperty("payload")}: string; } -@test @events union MixedEvents { @Events.contentType("application/json") @@ -308,22 +287,17 @@ union MixedEvents { `, ); - assert(MixedEvents.kind === "Union"); - assert(EventPayload.kind === "ModelProperty"); - - expect(getContentType(runner.program, EventPayload)).toBe("text/plain"); + expect(getContentType(program, payload)).toBe("text/plain"); }); it("cannot be set on a non-payload property", async () => { - const diagnostics = await runner.diagnose( + const diagnostics = await Tester.diagnose( ` model StringEvent { - @test("EventPayload") @Events.contentType("text/plain") payload: string; } -@test @events union MixedEvents { stringEvent: StringEvent, diff --git a/packages/events/test/test-host.ts b/packages/events/test/test-host.ts index 41e474b99bf..6681a5def7e 100644 --- a/packages/events/test/test-host.ts +++ b/packages/events/test/test-host.ts @@ -1,13 +1,8 @@ -import { createTestHost, createTestWrapper } from "@typespec/compiler/testing"; -import { EventsTestLibrary } from "../src/testing/index.js"; +import { resolvePath } from "@typespec/compiler"; +import { createTester } from "@typespec/compiler/testing"; -export async function createEventsTestHost() { - return createTestHost({ - libraries: [EventsTestLibrary], - }); -} - -export async function createEventsTestRunner() { - const host = await createEventsTestHost(); - return createTestWrapper(host, { autoUsings: ["TypeSpec.Events"] }); -} +export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: ["@typespec/events"], +}) + .importLibraries() + .using("Events"); diff --git a/packages/html-program-viewer/src/react/type-graph.test.tsx b/packages/html-program-viewer/src/react/type-graph.test.tsx index bb08113212a..f46cb8422a2 100644 --- a/packages/html-program-viewer/src/react/type-graph.test.tsx +++ b/packages/html-program-viewer/src/react/type-graph.test.tsx @@ -1,12 +1,11 @@ import { render } from "@testing-library/react"; import { it } from "vitest"; -import { createViewerTestRunner } from "../../test/test-host.js"; +import { Tester } from "../../test/test-host.js"; import { TypeGraph } from "./index.js"; async function renderTypeGraphFor(code: string) { - const runner = await createViewerTestRunner(); - await runner.compile(code); - render(); + const { program } = await Tester.compile(code); + render(); } it("operation", async () => { diff --git a/packages/html-program-viewer/test/emitter.test.ts b/packages/html-program-viewer/test/emitter.test.ts index 5f2a74e1dd4..17196dcbe07 100644 --- a/packages/html-program-viewer/test/emitter.test.ts +++ b/packages/html-program-viewer/test/emitter.test.ts @@ -1,15 +1,8 @@ -import type { BasicTestRunner } from "@typespec/compiler/testing"; -import { beforeEach, it } from "vitest"; -import { createViewerTestRunner } from "./test-host.js"; +import { it } from "vitest"; +import { Tester } from "./test-host.js"; -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createViewerTestRunner(); -}); +const EmitterTester = Tester.emit("@typespec/html-program-viewer"); it("runs emitter", async () => { - await runner.compile(`op foo(): string;`, { - emit: ["@typespec/html-program-viewer"], - }); + await EmitterTester.compile(`op foo(): string;`); }); diff --git a/packages/html-program-viewer/test/test-host.ts b/packages/html-program-viewer/test/test-host.ts index 0842d203836..b5d1197b89f 100644 --- a/packages/html-program-viewer/test/test-host.ts +++ b/packages/html-program-viewer/test/test-host.ts @@ -1,15 +1,6 @@ -import { createTestHost, createTestWrapper } from "@typespec/compiler/testing"; -import { ProgramViewerTestLibrary } from "../src/testing/index.js"; +import { resolvePath } from "@typespec/compiler"; +import { createTester } from "@typespec/compiler/testing"; -export async function createViewerTestHost() { - return createTestHost({ - libraries: [ProgramViewerTestLibrary], - }); -} - -export async function createViewerTestRunner() { - const host = await createViewerTestHost(); - return createTestWrapper(host, { - autoImports: [], - }); -} +export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: ["@typespec/html-program-viewer"], +}); diff --git a/packages/http-client-js/test/test-host.ts b/packages/http-client-js/test/test-host.ts index c97b25744bc..619bdf74d79 100644 --- a/packages/http-client-js/test/test-host.ts +++ b/packages/http-client-js/test/test-host.ts @@ -1,15 +1,5 @@ import { Diagnostic, resolvePath } from "@typespec/compiler"; -import { - BasicTestRunner, - createTester, - createTestHost, - createTestWrapper, - expectDiagnosticEmpty, -} from "@typespec/compiler/testing"; -import { HttpTestLibrary } from "@typespec/http/testing"; -import { RestTestLibrary } from "@typespec/rest/testing"; -import { join, relative } from "path"; -import { HttpClientJavascriptEmitterTestLibrary } from "../src/testing/index.js"; +import { createTester, expectDiagnosticEmpty } from "@typespec/compiler/testing"; const ApiTester = createTester(resolvePath(import.meta.dirname, ".."), { libraries: ["@typespec/http", "@typespec/rest", "@typespec/http-client-js"], @@ -17,63 +7,11 @@ const ApiTester = createTester(resolvePath(import.meta.dirname, ".."), { export const Tester = ApiTester.emit("@typespec/http-client-js"); -export async function createHttpClientJsTestHost() { - return createTestHost({ - libraries: [HttpClientJavascriptEmitterTestLibrary, HttpTestLibrary, RestTestLibrary], - }); -} - -export async function createHttpClientJavascriptEmitterTestRunner() { - const host = await createHttpClientJsTestHost(); - - return createTestWrapper(host, { - autoImports: ["@typespec/http", "@typespec/rest"], - autoUsings: ["TypeSpec.Http", "TypeSpec.Rest"], - compilerOptions: { - noEmit: false, - emit: ["@typespec/http-client-js"], - }, - }); -} - -const emitterOutputDir = join("tsp-output", "http-client-js"); - export async function emitWithDiagnostics( code: string, ): Promise<[Record, readonly Diagnostic[]]> { - const runner = await createHttpClientJavascriptEmitterTestRunner(); - await runner.compileAndDiagnose(code, { - outputDir: "tsp-output", - }); - const result = await readFilesRecursively(emitterOutputDir, runner); - return [result, runner.program.diagnostics]; -} - -async function readFilesRecursively( - dir: string, - runner: BasicTestRunner, -): Promise> { - const entries = await runner.program.host.readDir(dir); - const result: Record = {}; - - for (const entry of entries) { - const fullPath = join(dir, entry); - const stat = await runner.program.host.stat(fullPath); - - if (stat.isDirectory()) { - // Recursively read files in the directory - const nestedFiles = await readFilesRecursively(fullPath, runner); - Object.assign(result, nestedFiles); - } else if (stat.isFile()) { - // Read the file - // Read the file and store it with a relative path - const relativePath = relative(emitterOutputDir, fullPath); - const fileContent = await runner.program.host.readFile(fullPath); - result[relativePath] = fileContent.text; - } - } - - return result; + const [result, diagnostics] = await Tester.compileAndDiagnose(code); + return [result.outputs, diagnostics]; } export async function emit(code: string): Promise> { diff --git a/packages/http/test/streams/get-stream-metadata.test.ts b/packages/http/test/streams/get-stream-metadata.test.ts index 82c62dc8698..5f8f53a76bd 100644 --- a/packages/http/test/streams/get-stream-metadata.test.ts +++ b/packages/http/test/streams/get-stream-metadata.test.ts @@ -1,42 +1,19 @@ -import { Model, Program } from "@typespec/compiler"; -import { - createTestHost, - createTestWrapper, - expectDiagnosticEmpty, - type BasicTestRunner, -} from "@typespec/compiler/testing"; -import { StreamsTestLibrary } from "@typespec/streams/testing"; -import { assert, beforeEach, describe, expect, it } from "vitest"; +import { expectDiagnosticEmpty, t } from "@typespec/compiler/testing"; +import { describe, expect, it } from "vitest"; import { getStreamMetadata } from "../../src/experimental/index.js"; import { getAllHttpServices } from "../../src/operations.js"; -import { HttpTestLibrary } from "../../src/testing/index.js"; -import { HttpService } from "../../src/types.js"; - -let runner: BasicTestRunner; -let getHttpServiceWithProgram: ( - code: string, -) => Promise<{ service: HttpService; Thing: Model; program: Program }>; - -beforeEach(async () => { - const host = await createTestHost({ - libraries: [StreamsTestLibrary, HttpTestLibrary], - }); - runner = createTestWrapper(host, { - autoImports: [`@typespec/http/streams`, "@typespec/streams"], - autoUsings: ["TypeSpec.Http", "TypeSpec.Http.Streams", "TypeSpec.Streams"], - }); - getHttpServiceWithProgram = async (code) => { - const { Thing } = await runner.compile(` - @test model Thing { id: string } - ${code} - `); - assert(Thing.kind === "Model"); - const [services, diagnostics] = getAllHttpServices(runner.program); - - expectDiagnosticEmpty(diagnostics); - return { service: services[0], Thing, program: runner.program }; - }; -}); +import { StreamsTester } from "./tester.js"; + +async function getHttpServiceWithProgram(code: string) { + const { Thing, program } = await StreamsTester.compile(t.code` + model ${t.model("Thing")} { id: string } + ${code} + `); + const [services, diagnostics] = getAllHttpServices(program); + + expectDiagnosticEmpty(diagnostics); + return { service: services[0], Thing, program }; +} describe("Operation Responses", () => { it("can get stream metadata from HttpStream", async () => { diff --git a/packages/http/test/streams/streams.test.ts b/packages/http/test/streams/streams.test.ts index b67121b1633..f1a77dff834 100644 --- a/packages/http/test/streams/streams.test.ts +++ b/packages/http/test/streams/streams.test.ts @@ -1,38 +1,18 @@ -import { - createTestHost, - createTestWrapper, - type BasicTestRunner, -} from "@typespec/compiler/testing"; +import { t } from "@typespec/compiler/testing"; import { getStreamOf } from "@typespec/streams"; -import { StreamsTestLibrary } from "@typespec/streams/testing"; -import { assert, beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { getContentTypes } from "../../src/content-types.js"; -import { HttpTestLibrary } from "../../src/testing/index.js"; - -let runner: BasicTestRunner; - -beforeEach(async () => { - const host = await createTestHost({ - libraries: [StreamsTestLibrary, HttpTestLibrary], - }); - runner = createTestWrapper(host, { - autoImports: [`@typespec/http/streams`], - autoUsings: ["TypeSpec.Http.Streams"], - }); -}); +import { StreamsTester } from "./tester.js"; describe("HttpStream", () => { it("sets streamOf, contentType, and body", async () => { - const { Foo, Message } = await runner.compile(` - @test - model Message { id: string, text: string } + const { Foo, Message, program } = await StreamsTester.compile(t.code` + model ${t.model("Message")} { id: string, text: string } - @test model Foo is HttpStream; + model ${t.model("Foo")} is HttpStream; `); - assert(Foo.kind === "Model"); - assert(Message.kind === "Model"); - expect(getStreamOf(runner.program, Foo)).toBe(Message); + expect(getStreamOf(program, Foo)).toBe(Message); expect(getContentTypes(Foo.properties.get("contentType")!)[0]).toEqual(["application/jsonl"]); expect(Foo.properties.get("body")!.type).toMatchObject({ kind: "Scalar", @@ -43,16 +23,13 @@ describe("HttpStream", () => { describe("JsonlStream", () => { it("sets streamOf, contentType ('application/jsonl'), and body", async () => { - const { Foo, Message } = await runner.compile(` - @test - model Message { id: string, text: string } + const { Foo, Message, program } = await StreamsTester.compile(t.code` + model ${t.model("Message")} { id: string, text: string } - @test model Foo is JsonlStream; + model ${t.model("Foo")} is JsonlStream; `); - assert(Foo.kind === "Model"); - assert(Message.kind === "Model"); - expect(getStreamOf(runner.program, Foo)).toBe(Message); + expect(getStreamOf(program, Foo)).toBe(Message); expect(getContentTypes(Foo.properties.get("contentType")!)[0]).toEqual(["application/jsonl"]); expect(Foo.properties.get("body")!.type).toMatchObject({ kind: "Scalar", diff --git a/packages/http/test/streams/tester.ts b/packages/http/test/streams/tester.ts new file mode 100644 index 00000000000..b7539760b79 --- /dev/null +++ b/packages/http/test/streams/tester.ts @@ -0,0 +1,8 @@ +import { resolvePath } from "@typespec/compiler"; +import { createTester } from "@typespec/compiler/testing"; + +export const StreamsTester = createTester(resolvePath(import.meta.dirname, "../.."), { + libraries: ["@typespec/http", "@typespec/streams"], +}) + .import("@typespec/http", "@typespec/http/streams", "@typespec/streams") + .using("TypeSpec.Http", "TypeSpec.Http.Streams", "TypeSpec.Streams"); diff --git a/packages/library-linter/test/linter.test.ts b/packages/library-linter/test/linter.test.ts index c4e0fe9b8be..f37b9dd2cab 100644 --- a/packages/library-linter/test/linter.test.ts +++ b/packages/library-linter/test/linter.test.ts @@ -1,25 +1,12 @@ import { setTypeSpecNamespace } from "@typespec/compiler"; -import { - BasicTestRunner, - TestHost, - createTestWrapper, - expectDiagnostics, -} from "@typespec/compiler/testing"; -import { beforeEach, describe, it } from "vitest"; -import { createLibraryLinterTestHost } from "./test-host.js"; +import { expectDiagnostics, mockFile } from "@typespec/compiler/testing"; +import { describe, it } from "vitest"; +import { Tester } from "./test-host.js"; describe("library-linter", () => { - let runner: BasicTestRunner; - let host: TestHost; - - beforeEach(async () => { - host = await createLibraryLinterTestHost(); - runner = createTestWrapper(host); - }); - describe("missing namespace", () => { it("emit diagnostics when model is missing namespace", async () => { - const diagnostics = await runner.diagnose("model Foo {}"); + const diagnostics = await Tester.diagnose("model Foo {}"); expectDiagnostics(diagnostics, { code: "@typespec/library-linter/missing-namespace", message: "Model 'Foo' is not in a namespace. This is bad practice for a published library.", @@ -28,7 +15,7 @@ describe("library-linter", () => { }); it("emit diagnostics when operation is missing namespace", async () => { - const diagnostics = await runner.diagnose("op test(): string;"); + const diagnostics = await Tester.diagnose("op test(): string;"); expectDiagnostics(diagnostics, { code: "@typespec/library-linter/missing-namespace", message: @@ -38,7 +25,7 @@ describe("library-linter", () => { }); it("emit diagnostics when interface is missing namespace", async () => { - const diagnostics = await runner.diagnose("interface Foo {}"); + const diagnostics = await Tester.diagnose("interface Foo {}"); expectDiagnostics(diagnostics, { code: "@typespec/library-linter/missing-namespace", message: @@ -48,10 +35,9 @@ describe("library-linter", () => { }); it("emit diagnostics when decorator is missing namespace", async () => { - host.addJsFile("./mylib.js", { - $myDec: () => null, - }); - const diagnostics = await runner.diagnose(` + const diagnostics = await Tester.files({ + "./mylib.js": mockFile.js({ $myDec: () => null }), + }).diagnose(` import "./mylib.js"; extern dec myDec(target: unknown); namespace Foo { model Bar {}} @@ -71,8 +57,9 @@ describe("library-linter", () => { $foo: (...args: unknown[]) => null, }; setTypeSpecNamespace("Testing", decorators.$foo); - host.addJsFile("dec.js", decorators); - const diagnostics = await runner.diagnose(`import "./dec.js";`); + const diagnostics = await Tester.files({ + "dec.js": mockFile.js(decorators), + }).diagnose(`import "./dec.js";`); expectDiagnostics(diagnostics, { code: "@typespec/library-linter/missing-signature", message: `Decorator function $foo is missing a decorator declaration. Add "extern dec foo(...args);" to the library tsp.`, diff --git a/packages/library-linter/test/test-host.ts b/packages/library-linter/test/test-host.ts index 6d0d2425606..739f6138a4c 100644 --- a/packages/library-linter/test/test-host.ts +++ b/packages/library-linter/test/test-host.ts @@ -1,13 +1,6 @@ -import { createTestHost, createTestWrapper } from "@typespec/compiler/testing"; -import { LibraryLinterTestLibrary } from "../src/testing/index.js"; +import { resolvePath } from "@typespec/compiler"; +import { createTester } from "@typespec/compiler/testing"; -export async function createLibraryLinterTestHost() { - return createTestHost({ - libraries: [LibraryLinterTestLibrary], - }); -} - -export async function createLibraryLinterTestRunner() { - const host = await createLibraryLinterTestHost(); - return createTestWrapper(host); -} +export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: ["@typespec/library-linter"], +}).importLibraries(); diff --git a/packages/openapi3/test/tsp-openapi3/utils/tsp-for-openapi3.ts b/packages/openapi3/test/tsp-openapi3/utils/tsp-for-openapi3.ts index 455aa01f71f..25976118482 100644 --- a/packages/openapi3/test/tsp-openapi3/utils/tsp-for-openapi3.ts +++ b/packages/openapi3/test/tsp-openapi3/utils/tsp-for-openapi3.ts @@ -1,13 +1,8 @@ +import { ApiTester } from "#test/test-host.js"; import { Diagnostic, Namespace, Program } from "@typespec/compiler"; -import { - createTestHost as coreCreateTestHost, - expectDiagnosticEmpty, -} from "@typespec/compiler/testing"; -import { HttpTestLibrary } from "@typespec/http/testing"; -import { OpenAPITestLibrary } from "@typespec/openapi/testing"; +import { expectDiagnosticEmpty } from "@typespec/compiler/testing"; import assert from "node:assert"; import { convertOpenAPI3Document } from "../../../src/index.js"; -import { OpenAPI3TestLibrary } from "../../../src/testing/index.js"; import { OpenAPI3Document, OpenAPI3Header, @@ -19,9 +14,8 @@ import { } from "../../../src/types.js"; function wrapCodeInTest(code: string): string { - // Find the 1st namespace declaration and decorate it - const serviceIndex = code.indexOf("@service"); - return `${code.slice(0, serviceIndex)}@test\n${code.slice(serviceIndex)}`; + // Place a fourslash marker before the namespace identifier so we can extract it + return code.replace("namespace TestService", "namespace /*TestService*/TestService"); } export interface OpenAPI3Options extends Partial { @@ -32,16 +26,8 @@ export interface OpenAPI3Options extends Partial { parameters?: Record>; } -async function createTestHost() { - return coreCreateTestHost({ - libraries: [HttpTestLibrary, OpenAPITestLibrary, OpenAPI3TestLibrary], - }); -} - export async function validateTsp(code: string) { - const host = await createTestHost(); - host.addTypeSpecFile("main.tsp", code); - const [, diagnostics] = await host.compileAndDiagnose("main.tsp"); + const diagnostics = await ApiTester.diagnose(code); expectDiagnosticEmpty(diagnostics); } @@ -60,20 +46,19 @@ export async function compileForOpenAPI3(props: OpenAPI3Options): Promise<{ const code = await convertOpenAPI3Document(openApi3Doc); const testableCode = wrapCodeInTest(code); - const host = await createTestHost(); - host.addTypeSpecFile("main.tsp", testableCode); - const [types, diagnostics] = await host.compileAndDiagnose("main.tsp"); - const { TestService } = types; + const [result, diagnostics] = await ApiTester.compileAndDiagnose(testableCode); + const TestService = result.TestService; + const { program } = result; assert( - TestService?.kind === "Namespace", - `Expected TestService to be a namespace, instead got ${TestService?.kind}`, + TestService?.entityKind === "Type" && TestService?.kind === "Namespace", + `Expected TestService to be a namespace, instead got ${TestService?.entityKind}/${(TestService as any)?.kind}`, ); return { - namespace: TestService, + namespace: TestService as Namespace, diagnostics, - program: host.program, + program, }; } diff --git a/packages/protobuf/test/scenarios.test.ts b/packages/protobuf/test/scenarios.test.ts index b27c687f24a..8a0ad5692c2 100644 --- a/packages/protobuf/test/scenarios.test.ts +++ b/packages/protobuf/test/scenarios.test.ts @@ -5,12 +5,7 @@ import { describe, it } from "vitest"; import micromatch from "micromatch"; import { formatDiagnostic, resolvePath } from "@typespec/compiler"; -import { - TypeSpecTestLibrary, - createTestHost, - findTestPackageRoot, - resolveVirtualPath, -} from "@typespec/compiler/testing"; +import { createTester, findTestPackageRoot } from "@typespec/compiler/testing"; import { readdirSync, statSync } from "fs"; import { mkdir, readFile, readdir, rm, stat, writeFile } from "fs/promises"; import { ProtobufEmitterOptions } from "../src/lib.js"; @@ -21,19 +16,9 @@ const SCENARIOS_DIRECTORY = resolvePath(pkgRoot, "test/scenarios"); const shouldRecord = process.env.RECORD === "true"; const patternsToRun = process.env.RUN_SCENARIOS?.split(",") ?? ["*"]; -const TypeSpecProtobufTestLibrary: TypeSpecTestLibrary = { - name: "@typespec/protobuf", - packageRoot: await findTestPackageRoot(import.meta.url), - files: [ - { realDir: "", pattern: "package.json", virtualPath: "./node_modules/@typespec/protobuf" }, - { - realDir: "dist/src", - pattern: "*.js", - virtualPath: "./node_modules/@typespec/protobuf/dist/src", - }, - { realDir: "lib/", pattern: "*.tsp", virtualPath: "./node_modules/@typespec/protobuf/lib" }, - ], -}; +const ProtobufTester = createTester(resolvePath(pkgRoot), { + libraries: ["@typespec/protobuf"], +}); describe("protobuf scenarios", function () { const scenarios = readdirSync(SCENARIOS_DIRECTORY) @@ -144,31 +129,21 @@ async function doEmit( files: Record, options: ProtobufEmitterOptions, ): Promise { - const baseOutputPath = resolveVirtualPath("test-output/"); - - const host = await createTestHost({ - libraries: [TypeSpecProtobufTestLibrary], - }); - - for (const [fileName, content] of Object.entries(files)) { - host.addTypeSpecFile(fileName, content); + const emitterTester = ProtobufTester.emit( + "@typespec/protobuf", + options as Record, + ); + const [result, diagnostics] = await emitterTester.compileAndDiagnose(files); + + // The EmitterTester strips the emitter output dir prefix, but the expected files + // include the emitter package name prefix (e.g., "@typespec/protobuf/main.proto") + const prefixedOutputs: Record = {}; + for (const [name, value] of Object.entries(result.outputs)) { + prefixedOutputs[`@typespec/protobuf/${name}`] = value; } - const [, diagnostics] = await host.compileAndDiagnose("main.tsp", { - outputDir: baseOutputPath, - noEmit: false, - emit: ["@typespec/protobuf"], - options: { - "@typespec/protobuf": options as Record, - }, - }); - return { - files: Object.fromEntries( - [...host.fs.entries()] - .filter(([name]) => name.startsWith(baseOutputPath)) - .map(([name, value]) => [name.replace(baseOutputPath, ""), value]), - ), + files: prefixedOutputs, diagnostics: diagnostics.map((x) => formatDiagnostic(x)), }; } diff --git a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts index 2b7a405940e..27ff5c8eb74 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -1,29 +1,25 @@ -import { definePackageFlags } from "@typespec/compiler"; -import { createTestHost, expectDiagnosticEmpty } from "@typespec/compiler/testing"; +import { definePackageFlags, resolvePath } from "@typespec/compiler"; +import { createTester, expectDiagnosticEmpty, mockFile } from "@typespec/compiler/testing"; import { describe, expect, it } from "vitest"; import { generateExternDecorators } from "../../src/gen-extern-signatures/gen-extern-signatures.js"; +const Tester = createTester(resolvePath(import.meta.dirname, "../.."), { + libraries: [], +}) + .files({ + "lib.js": mockFile.js({ $flags: definePackageFlags({}) }), + }) + .import("./lib.js") + .using("TypeSpec.Reflection"); + async function generateDecoratorSignatures(code: string) { - const host = await createTestHost(); - host.addTypeSpecFile( - "main.tsp", - ` - import "./lib.js"; - using TypeSpec.Reflection; - ${code}`, - ); - host.addJsFile("lib.js", { - $flags: definePackageFlags({}), - }); - await host.diagnose("main.tsp", { - parseOptions: { comments: true, docs: true }, + const [{ program }] = await Tester.compileAndDiagnose(code, { + compilerOptions: { parseOptions: { comments: true, docs: true } }, }); - expectDiagnosticEmpty( - host.program.diagnostics.filter((x) => x.code !== "missing-implementation"), - ); + expectDiagnosticEmpty(program.diagnostics.filter((x) => x.code !== "missing-implementation")); - const result = await generateExternDecorators(host.program, "test-lib", { + const result = await generateExternDecorators(program, "test-lib", { prettierConfig: { printWidth: 160, // So there is no inconsistency in the .each test with different parameter length plugins: [], diff --git a/packages/tspd/test/test-utils.ts b/packages/tspd/test/test-utils.ts index 84ab503b278..e6d40517f0b 100644 --- a/packages/tspd/test/test-utils.ts +++ b/packages/tspd/test/test-utils.ts @@ -1,16 +1,18 @@ -import { Diagnostic } from "@typespec/compiler"; -import { createTestHost, expectDiagnosticEmpty } from "@typespec/compiler/testing"; +import { Diagnostic, resolvePath } from "@typespec/compiler"; +import { createTester, expectDiagnosticEmpty } from "@typespec/compiler/testing"; import { MarkdownRenderer } from "../src/ref-doc/emitters/markdown.js"; import { extractRefDocs } from "../src/ref-doc/extractor.js"; import { TypeSpecRefDocBase } from "../src/ref-doc/types.js"; +const Tester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: [], +}); + export async function extractTestRefDoc( code: string, ): Promise<[TypeSpecRefDocBase, readonly Diagnostic[]]> { - const host = await createTestHost(); - host.addTypeSpecFile("main.tsp", code); - await host.compile("main.tsp"); - return extractRefDocs(host.program); + const [{ program }] = await Tester.compileAndDiagnose(code); + return extractRefDocs(program); } export async function createMarkdownRenderer(code: string) { diff --git a/website/src/content/docs/docs/extending-typespec/testing.mdx b/website/src/content/docs/docs/extending-typespec/testing.mdx index 675c57520cf..521322bd8de 100644 --- a/website/src/content/docs/docs/extending-typespec/testing.mdx +++ b/website/src/content/docs/docs/extending-typespec/testing.mdx @@ -281,6 +281,8 @@ strictEqual(Foo.name, "Foo"); PR with examples https://github.com/microsoft/typespec/pull/7151 +### Replace test host setup + ```diff lang=ts title="test-host.ts" - import { createTestHost, createTestWrapper } from "@typespec/compiler/testing"; - import { HttpTestLibrary } from "@typespec/http/testing"; @@ -307,16 +309,69 @@ PR with examples https://github.com/microsoft/typespec/pull/7151 + .using("My"); ``` -In test files +### Update test files + +Remove the `beforeEach` runner setup and use `Tester` directly. The `program` is available on the compile result. ```diff lang=ts title="test/my-library.test.ts" +- import { createMyTestRunner } from "./test-host.js"; ++ import { Tester } from "./test-host.js"; + +- let runner: BasicTestRunner; +- beforeEach(async () => { +- runner = await createMyTestRunner(); +- }); + 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 { +- expect(isAttribute(runner.program, id)).toBe(true); ++ const { id, program } = await Tester.compile(t.code`model Blob { + @Xml.attribute ${t.modelProperty("id")} : string + }`); - expect(isAttribute(runner.program, id)).toBe(true); ++ expect(isAttribute(program, id)).toBe(true); }); ``` + +### Injecting files + +Replace `host.addTypeSpecFile()` / `host.addJsFile()` with the `.files()` chain and `mockFile.js()`: + +```diff lang=ts +- host.addJsFile("./dec.js", { $myDec: () => null }); +- const diagnostics = await runner.diagnose(` +- import "./dec.js"; +- extern dec myDec(target: unknown); +- `); ++ import { mockFile } from "@typespec/compiler/testing"; ++ const diagnostics = await Tester.files({ ++ "./dec.js": mockFile.js({ $myDec: () => null }), ++ }).diagnose(` ++ import "./dec.js"; ++ extern dec myDec(target: unknown); ++ `); +``` + +### Emitter testing + +For emitter tests, use `.emit()` to create an `EmitterTester`. The compile result includes `outputs` with the emitted files: + +```diff lang=ts title="test-host.ts" +- export async function createMyEmitterTestRunner() { +- const host = await createMyTestHost(); +- return createTestWrapper(host, { +- compilerOptions: { noEmit: false, emit: ["@typespec/my-emitter"] }, +- }); +- } + ++ export const EmitterTester = Tester.emit("@typespec/my-emitter"); +``` + +```diff lang=ts title="test/emitter.test.ts" +- const runner = await createMyEmitterTestRunner(); +- await runner.compileAndDiagnose(code, { outputDir: "tsp-output" }); + ++ const [result, diagnostics] = await EmitterTester.compileAndDiagnose(code); ++ // result.outputs contains the emitted files as Record +```