Skip to content

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

Merged
merged 62 commits into from
Jun 24, 2025
Merged
Changes from 1 commit
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
dec22bd
Initial host builder
timotheeguerin Apr 26, 2025
9a669a7
wrap
timotheeguerin Apr 27, 2025
69b55ff
Clone fs
timotheeguerin Apr 27, 2025
4452fcd
cleanup
timotheeguerin Apr 27, 2025
97e51a7
tweaks
timotheeguerin Apr 28, 2025
a534b40
auto import and using
timotheeguerin Apr 28, 2025
48fc2c2
.
timotheeguerin Apr 28, 2025
8242a64
Simplify
timotheeguerin Apr 28, 2025
4af3ea0
marked template v1
timotheeguerin Apr 29, 2025
5d22b31
Simpler?
timotheeguerin Apr 29, 2025
7b57e21
Merge branch 'main' of https://github.com/Microsoft/typespec into tes…
timotheeguerin May 9, 2025
219a2d9
connect marker
timotheeguerin May 9, 2025
a6885a4
works with values
timotheeguerin May 9, 2025
dcf71e2
Better
timotheeguerin May 9, 2025
595ff1b
.
timotheeguerin May 9, 2025
fd4c0ce
type check
timotheeguerin May 9, 2025
e01aec9
update openapi
timotheeguerin May 9, 2025
59779c7
remove log
timotheeguerin May 9, 2025
555512a
.
timotheeguerin May 9, 2025
e0dd02b
simplify
timotheeguerin May 9, 2025
ccc79ca
test
timotheeguerin May 9, 2025
9e16b20
.
timotheeguerin May 9, 2025
ab0380e
.
timotheeguerin May 9, 2025
342c388
Works with multi file
timotheeguerin May 9, 2025
ad30cbf
fix
timotheeguerin May 9, 2025
e8b3792
simplify
timotheeguerin May 11, 2025
776bf33
emitter testing support
timotheeguerin May 12, 2025
f2d9522
Merge branch 'main' of https://github.com/Microsoft/typespec into tes…
timotheeguerin May 13, 2025
9205a98
Separate FS and add generic functions
timotheeguerin May 13, 2025
afc3463
try
timotheeguerin May 13, 2025
f287465
Create tester
timotheeguerin May 13, 2025
a64c1e0
Missing
timotheeguerin May 13, 2025
9f3c4a3
fix
timotheeguerin May 13, 2025
12402d2
fixup and basic connect to openapi3
timotheeguerin May 13, 2025
37d93a2
More migration openapi3
timotheeguerin May 13, 2025
b512337
more fixes
timotheeguerin May 13, 2025
a306d02
Simplify openapiFor
timotheeguerin May 13, 2025
d342f06
fix
timotheeguerin May 14, 2025
627d219
Migrate xml
timotheeguerin May 14, 2025
8acb8dd
Migrate sse and streams
timotheeguerin May 14, 2025
61f8c52
migrate rest
timotheeguerin May 14, 2025
600686c
migrate http and better node resolution
timotheeguerin May 14, 2025
d9bdc80
migrate versioning
timotheeguerin May 14, 2025
8f941cb
Handle change to output dir
timotheeguerin May 14, 2025
d8d3596
Merge branch 'main' into tester-v2
timotheeguerin May 14, 2025
4d7ecd3
Create tester-v2-2025-4-14-20-23-17.md
timotheeguerin May 15, 2025
36814a8
Create tester-v2-2025-4-15-17-52-11.md
timotheeguerin May 15, 2025
7ca1450
Merge branch 'main' of https://github.com/Microsoft/typespec into tes…
timotheeguerin May 23, 2025
9acf7db
Rename
timotheeguerin May 23, 2025
92c65ab
parity and add files
timotheeguerin May 23, 2025
3b379e2
piping
timotheeguerin May 23, 2025
7c8a7ec
Fix values
timotheeguerin May 23, 2025
79a1e38
works
timotheeguerin May 23, 2025
ac12908
Merge branch 'main' into tester-v2
timotheeguerin May 29, 2025
e19dbaa
Merge branch 'main' into tester-v2
timotheeguerin Jun 3, 2025
c59c9ec
add docs for marked template
timotheeguerin Jun 3, 2025
6f6fd7a
add docs
timotheeguerin Jun 3, 2025
d96573f
add back for back compat
timotheeguerin Jun 3, 2025
1db6ad0
Apply suggestions from code review
timotheeguerin Jun 9, 2025
46e607e
Merge branch 'main' of https://github.com/Microsoft/typespec into tes…
timotheeguerin Jun 21, 2025
729e5eb
handle review comments
timotheeguerin Jun 21, 2025
983c2e4
.
timotheeguerin Jun 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Better
  • Loading branch information
timotheeguerin committed May 9, 2025
commit dcf71e281aba49bde48d1c2fbb58eceb38219a68
1 change: 1 addition & 0 deletions packages/compiler/src/testing/index.ts
Original file line number Diff line number Diff line change
@@ -36,4 +36,5 @@ export type {
} from "./types.js";

// TODO: use named imports
export { t } from "./marked-template.js";
export * from "./test-host-v2.js";
70 changes: 32 additions & 38 deletions packages/compiler/src/testing/marked-template.ts
Original file line number Diff line number Diff line change
@@ -50,71 +50,41 @@ function valueMarker<const T extends Value>(valueKind?: T["valueKind"]) {
};
}

export const m = {
// Types
type: typeMarker<Type>(),
model: typeMarker<Model>("Model"),
enum: typeMarker<Enum>("Enum"),
union: typeMarker<Union>("Union"),
interface: typeMarker<Interface>("Interface"),
op: typeMarker<Operation>("Operation"),
enumMember: typeMarker<EnumMember>("EnumMember"),
modelProperty: typeMarker<ModelProperty>("ModelProperty"),
namespace: typeMarker<Namespace>("Namespace"),
scalar: typeMarker<Type>("Scalar"),
unionVariant: typeMarker<Type>("UnionVariant"),
boolean: typeMarker<Type>("Boolean"),
number: typeMarker<Type>("Number"),
string: typeMarker<Type>("String"),

// Values
value: valueMarker<Value>(),
object: valueMarker<Value>("ObjectValue"),
array: valueMarker<Value>("ArrayValue"),
};

export type MarkerConfig<T extends Record<string, Entity>> = {
[K in keyof T]: {
pos: number;
end: number;
} & Marker<T[K], K & string>;
[K in keyof T]: Marker<T[K], K & string>;
};

export interface TemplateWithMarkers<T extends Record<string, Entity>> {
readonly code: string;
readonly markers: MarkerConfig<T>;
}

type Prettify<T> = {
[K in keyof T]: T[K];
type Prettify<T extends Record<string, Entity>> = {
[K in keyof T]: T[K] & Entity;
} & {};

type InferType<T> = T extends Marker<infer K, infer _> ? K : never;
type CollectType<T extends ReadonlyArray<Marker<Entity, string> | string>> = {
[K in T[number] as K extends Marker<infer _K, infer N> ? N : never]: InferType<K>;
};
/** Specify that this value is dynamic and needs to be interpolated with the given keys */
export function extract<const T extends (Marker<Entity, string> | string)[]>(
function extract<const T extends (Marker<Entity, string> | string)[]>(
strings: TemplateStringsArray,
...keys: T
): TemplateWithMarkers<Prettify<CollectType<T>> & Record<string, Entity>> {
): TemplateWithMarkers<Prettify<CollectType<T>>> {
const markers: MarkerConfig<any> = {};
const result: string[] = [strings[0]];
let pos = strings[0].length;
keys.forEach((key, i) => {
if (typeof key === "string") {
result.push(key);
pos += key.length;
} else {
result.push(key.name);
result.push(`/*${key.name}*/${key.name}`);
markers[key.name] = {
pos,
end: pos + key.name.length,
entityKind: key.entityKind,
name: key.name,
kind: (key as any).kind,
valueKind: (key as any).valueKind,
};
pos += key.name.length;
}
result.push(strings[i + 1]);
});
@@ -124,4 +94,28 @@ export function extract<const T extends (Marker<Entity, string> | string)[]>(
};
}

const a = extract`foo ${m.model("bar")} ${"regular"} ${m.enum("def")}`;
/** TypeSpec template marker */
export const t = {
code: extract,

// Types
type: typeMarker<Type>(),
model: typeMarker<Model>("Model"),
enum: typeMarker<Enum>("Enum"),
union: typeMarker<Union>("Union"),
interface: typeMarker<Interface>("Interface"),
op: typeMarker<Operation>("Operation"),
enumMember: typeMarker<EnumMember>("EnumMember"),
modelProperty: typeMarker<ModelProperty>("ModelProperty"),
namespace: typeMarker<Namespace>("Namespace"),
scalar: typeMarker<Type>("Scalar"),
unionVariant: typeMarker<Type>("UnionVariant"),
boolean: typeMarker<Type>("Boolean"),
number: typeMarker<Type>("Number"),
string: typeMarker<Type>("String"),

// Values
value: valueMarker<Value>(),
object: valueMarker<Value>("ObjectValue"),
array: valueMarker<Value>("ArrayValue"),
};
71 changes: 55 additions & 16 deletions packages/compiler/src/testing/test-host-v2.ts
Original file line number Diff line number Diff line change
@@ -213,6 +213,9 @@ function createTesterInstance(params: TesterInternalParams): TesterInstance {
...usings,
params.wraps ? applyWraps(codeStr, params.wraps) : codeStr,
].join("\n");

const markerPositions = extractMarkers(actualCode);

fs.addTypeSpecFile("main.tsp", actualCode);
const program = await coreCompile(fs.compilerHost, resolveVirtualPath("main.tsp"));
savedProgram = program;
@@ -222,8 +225,9 @@ function createTesterInstance(params: TesterInternalParams): TesterInstance {
if (!file) {
throw new Error(`Couldn't find main.tsp in program`);
}
for (const marker of Object.values(code.markers)) {
const { pos, end, name, kind, entityKind, valueKind } = marker;
for (const marker of markerPositions) {
const { name, pos, end } = marker;
const markerConfig = code.markers[name];
const node = getNodeAtPosition(file, pos);
if (!node) {
throw new Error(`Could not find node at ${pos}-${end}`);
@@ -235,23 +239,31 @@ function createTesterInstance(params: TesterInternalParams): TesterInstance {
const entity = program.checker.getTypeOrValueForNode(getSymNode(sym));
if (entity === null) {
throw new Error(
`Expected ${name} to be of entity kind ${entityKind} but got null (Means a value failed to resolve) at ${pos}-${end}`,
`Expected ${name} to be of entity kind ${markerConfig?.entityKind} but got null (Means a value failed to resolve) at ${pos}-${end}`,
);
}
if (entity.entityKind !== entityKind) {
throw new Error(
`Expected ${name} to be of entity kind ${entityKind} but got (${entity?.entityKind}) ${getEntityName(entity)} at ${pos}-${end}`,
);
}
if (entity.entityKind === "Type" && entity.kind !== kind) {
throw new Error(
`Expected ${name} to be of kind ${kind} but got (${entity.kind}) ${getEntityName(entity)} at ${pos}-${end}`,
);
} else if (entity?.entityKind === "Value" && entity.valueKind !== valueKind) {
throw new Error(
`Expected ${name} to be of value kind ${valueKind} but got (${entity.valueKind}) ${getEntityName(entity)} at ${pos}-${end}`,
);
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}-${end}`,
);
}
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}-${end}`,
);
} 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}-${end}`,
);
}
}

(types as any)[name] = entity;
}
}
@@ -306,3 +318,30 @@ function addTestLib(fs: TestFileSystem): Record<string, Type> {
});
return testTypes;
}

export interface PositionedMarker {
name: string;
pos: number;
end: number;
}
function extractMarkers(code: string): PositionedMarker[] {
// Extract TypeScript fourslash-style markers: /*markerName*/
// Returns an array of Marker objects with name, pos, and end
const markerRegex = /\/\*([a-zA-Z0-9_]+)\*\//g;
const markers: PositionedMarker[] = [];
let match: RegExpExecArray | null;
while ((match = markerRegex.exec(code)) !== null) {
const markerName = match[1];
// The marker is immediately followed by the identifier
// Find the next word after the marker
const afterMarker = code.slice(markerRegex.lastIndex);
const idMatch = /([a-zA-Z0-9_]+)/.exec(afterMarker);
if (idMatch) {
const id = idMatch[1];
const pos = markerRegex.lastIndex;
const end = pos + id.length;
markers.push({ name: markerName, pos, end });
}
}
return markers;
}
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.