-
Notifications
You must be signed in to change notification settings - Fork 284
New testing framework #7151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
timotheeguerin
wants to merge
59
commits into
microsoft:main
Choose a base branch
from
timotheeguerin:tester-v2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
New testing framework #7151
Changes from 7 commits
Commits
Show all changes
59 commits
Select commit
Hold shift + click to select a range
dec22bd
Initial host builder
timotheeguerin 9a669a7
wrap
timotheeguerin 69b55ff
Clone fs
timotheeguerin 4452fcd
cleanup
timotheeguerin 97e51a7
tweaks
timotheeguerin a534b40
auto import and using
timotheeguerin 48fc2c2
.
timotheeguerin 8242a64
Simplify
timotheeguerin 4af3ea0
marked template v1
timotheeguerin 5d22b31
Simpler?
timotheeguerin 7b57e21
Merge branch 'main' of https://github.com/Microsoft/typespec into tes…
timotheeguerin 219a2d9
connect marker
timotheeguerin a6885a4
works with values
timotheeguerin dcf71e2
Better
timotheeguerin 595ff1b
.
timotheeguerin fd4c0ce
type check
timotheeguerin e01aec9
update openapi
timotheeguerin 59779c7
remove log
timotheeguerin 555512a
.
timotheeguerin e0dd02b
simplify
timotheeguerin ccc79ca
test
timotheeguerin 9e16b20
.
timotheeguerin ab0380e
.
timotheeguerin 342c388
Works with multi file
timotheeguerin ad30cbf
fix
timotheeguerin e8b3792
simplify
timotheeguerin 776bf33
emitter testing support
timotheeguerin f2d9522
Merge branch 'main' of https://github.com/Microsoft/typespec into tes…
timotheeguerin 9205a98
Separate FS and add generic functions
timotheeguerin afc3463
try
timotheeguerin f287465
Create tester
timotheeguerin a64c1e0
Missing
timotheeguerin 9f3c4a3
fix
timotheeguerin 12402d2
fixup and basic connect to openapi3
timotheeguerin 37d93a2
More migration openapi3
timotheeguerin b512337
more fixes
timotheeguerin a306d02
Simplify openapiFor
timotheeguerin d342f06
fix
timotheeguerin 627d219
Migrate xml
timotheeguerin 8acb8dd
Migrate sse and streams
timotheeguerin 61f8c52
migrate rest
timotheeguerin 600686c
migrate http and better node resolution
timotheeguerin d9bdc80
migrate versioning
timotheeguerin 8f941cb
Handle change to output dir
timotheeguerin d8d3596
Merge branch 'main' into tester-v2
timotheeguerin 4d7ecd3
Create tester-v2-2025-4-14-20-23-17.md
timotheeguerin 36814a8
Create tester-v2-2025-4-15-17-52-11.md
timotheeguerin 7ca1450
Merge branch 'main' of https://github.com/Microsoft/typespec into tes…
timotheeguerin 9acf7db
Rename
timotheeguerin 92c65ab
parity and add files
timotheeguerin 3b379e2
piping
timotheeguerin 7c8a7ec
Fix values
timotheeguerin 79a1e38
works
timotheeguerin ac12908
Merge branch 'main' into tester-v2
timotheeguerin e19dbaa
Merge branch 'main' into tester-v2
timotheeguerin c59c9ec
add docs for marked template
timotheeguerin 6f6fd7a
add docs
timotheeguerin d96573f
add back for back compat
timotheeguerin 1db6ad0
Apply suggestions from code review
timotheeguerin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,257 @@ | ||
import { readFile } from "fs/promises"; | ||
import { compilerAssert } from "../core/diagnostics.js"; | ||
import { NodeHost } from "../core/node-host.js"; | ||
import { CompilerOptions } from "../core/options.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 { Diagnostic, NoTarget, SourceFile, StringLiteral, Type } from "../core/types.js"; | ||
import { expectDiagnosticEmpty } from "./expect.js"; | ||
import { StandardTestLibrary, createTestFileSystem } from "./test-host.js"; | ||
import { resolveVirtualPath } from "./test-utils.js"; | ||
import { TestFileSystem } from "./types.js"; | ||
|
||
// Need a way to combine that with `program` | ||
export type TestCompileResult = Record<string, Type>; | ||
|
||
export interface JsFileDef { | ||
[key: string]: string | unknown; | ||
} | ||
|
||
interface TestCompileOptions { | ||
readonly files?: Record<string, string | JsFileDef>; | ||
readonly options?: CompilerOptions; | ||
} | ||
|
||
interface Testable { | ||
compile(main: string, options?: TestCompileOptions): Promise<TestCompileResult>; | ||
diagnose(main: string, options?: TestCompileOptions): Promise<readonly Diagnostic[]>; | ||
compileAndDiagnose( | ||
main: string, | ||
options?: TestCompileOptions, | ||
): Promise<[TestCompileResult, readonly Diagnostic[]]>; | ||
} | ||
|
||
// Immutable structure meant to be reused | ||
export interface Tester extends Testable { | ||
/** Auto import all libraries defined in this tester. */ | ||
importLibraries(): Tester; | ||
import(...imports: string[]): Tester; | ||
using(...names: string[]): Tester; | ||
wrap(fn: (x: string) => string): Tester; | ||
createInstance(): TesterInstance; | ||
} | ||
|
||
export interface TesterInstance extends Testable { | ||
readonly program: Program; | ||
} | ||
|
||
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<T>(fn: () => Promise<T>): () => Promise<T> { | ||
let load: Promise<T> | undefined; | ||
return () => { | ||
if (load) return load; | ||
load = fn(); | ||
return load; | ||
}; | ||
} | ||
|
||
async function createTesterFs(base: string, options: TesterOptions) { | ||
const fs = createTestFileSystem(); | ||
|
||
const sl = await createSourceLoader({ ...NodeHost, realpath: async (x) => x }); | ||
const selfName = JSON.parse(await readFile(resolvePath(base, "package.json"), "utf8")).name; | ||
for (const lib of options.libraries) { | ||
await sl.importPath(lib, NoTarget, base); | ||
} | ||
|
||
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}'`, | ||
); | ||
const relativePath = getRelativePathFromDirectory(base, file.path, false); | ||
if (context.metadata.name === selfName) { | ||
return joinPaths("node_modules", selfName, relativePath); | ||
} else { | ||
return relativePath; | ||
} | ||
} | ||
|
||
for (const file of sl.resolution.sourceFiles.values()) { | ||
const relativePath = computeVirtualPath(file.file); | ||
fs.addTypeSpecFile(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.addTypeSpecFile( | ||
resolvePath("node_modules", path, "package.json"), | ||
(lib.manifest as any).file.text, | ||
); | ||
} | ||
fs.freeze(); | ||
return fs; | ||
} | ||
|
||
interface TesterInternalParams { | ||
fs: () => Promise<TestFileSystem>; | ||
libraries: string[]; | ||
wraps?: ((code: string) => string)[]; | ||
imports?: string[]; | ||
usings?: string[]; | ||
} | ||
|
||
function createTesterInternal(params: TesterInternalParams): Tester { | ||
const { compile, compileAndDiagnose, diagnose } = createInstance(); | ||
return { | ||
compile, | ||
compileAndDiagnose, | ||
diagnose, | ||
wrap, | ||
importLibraries, | ||
import: importFn, | ||
using, | ||
createInstance, | ||
}; | ||
|
||
function wrap(fn: (x: string) => string): Tester { | ||
return createTesterInternal({ | ||
...params, | ||
wraps: [...(params.wraps ?? []), fn], | ||
}); | ||
} | ||
|
||
function importLibraries(): Tester { | ||
return createTesterInternal({ | ||
...params, | ||
imports: [...(params.imports ?? []), ...params.libraries], | ||
}); | ||
} | ||
|
||
function importFn(...imports: string[]): Tester { | ||
return createTesterInternal({ | ||
...params, | ||
imports: [...(params.imports ?? []), ...imports], | ||
}); | ||
} | ||
|
||
function using(...usings: string[]): Tester { | ||
return createTesterInternal({ | ||
...params, | ||
usings: [...(params.usings ?? []), ...usings], | ||
}); | ||
} | ||
|
||
function createInstance(): TesterInstance { | ||
return createTesterInstance({ | ||
...params, | ||
fs: async () => { | ||
const fs = await params.fs(); | ||
return fs.clone(); | ||
}, | ||
}); | ||
} | ||
} | ||
|
||
function createTesterInstance(params: TesterInternalParams): TesterInstance { | ||
let savedProgram: Program | undefined; | ||
|
||
return { | ||
compileAndDiagnose, | ||
compile, | ||
diagnose, | ||
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; | ||
} | ||
async function compileAndDiagnose( | ||
code: string, | ||
options?: TestCompileOptions, | ||
): Promise<[TestCompileResult, readonly Diagnostic[]]> { | ||
const fs = await params.fs(); | ||
const types = await addTestLib(fs); | ||
|
||
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"); | ||
fs.addTypeSpecFile("main.tsp", actualCode); | ||
const program = await coreCompile(fs.compilerHost, resolveVirtualPath("main.tsp")); | ||
savedProgram = program; | ||
return [{ program, ...types } as any, program.diagnostics]; | ||
} | ||
|
||
async function compile(code: string, options?: TestCompileOptions): Promise<TestCompileResult> { | ||
const [result, diagnostics] = await compileAndDiagnose(code, options); | ||
expectDiagnosticEmpty(diagnostics); | ||
return result; | ||
} | ||
async function diagnose( | ||
code: string, | ||
options?: TestCompileOptions, | ||
): Promise<readonly Diagnostic[]> { | ||
const [_, diagnostics] = await compileAndDiagnose(code, options); | ||
return diagnostics; | ||
} | ||
} | ||
|
||
function addTestLib(fs: TestFileSystem): Record<string, Type> { | ||
const testTypes: Record<string, Type> = {}; | ||
// add test decorators | ||
fs.addTypeSpecFile(".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 ( | ||
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; | ||
}, | ||
}); | ||
return testTypes; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.