Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
80ec2cf
feat: Implement modular test generation with clean test names
Jul 27, 2025
4dc77e8
feat: Add response validation assertions to generated tests
Jul 27, 2025
cb11a98
feat: Add comprehensive unit tests for test generation functionality
Jul 27, 2025
f518b0e
Update modular test generation and scenarios
Jul 27, 2025
b9ac0ba
Add LRO test support and improve paging test assertions
Jul 27, 2025
037d338
Add more unit test cases
Jul 27, 2025
93ca81f
Fix the lro issue
Jul 27, 2025
c4b8cb7
Fix the issues in parameter test generation
MaryGao Jul 28, 2025
7c216db
Merge branch 'main' into generate-test-modular
MaryGao Jul 31, 2025
f4b4f34
Merge branch 'main' into generate-test-modular
MaryGao Aug 18, 2025
d7bb665
Remove the useless generated codes
MaryGao Aug 18, 2025
d0dc4bd
Regenerate the test cases for UTs
MaryGao Aug 18, 2025
caee79c
Fix the client-level parameter missing issues
MaryGao Aug 18, 2025
19d22ab
Fix the parameter without normalization issue
MaryGao Aug 18, 2025
054cbb4
Fix the comma
MaryGao Aug 18, 2025
123361e
Merge branch 'main' into generate-test-modular
MaryGao Aug 18, 2025
049750a
Fix the bytes and additional properties issues in sample generation
MaryGao Aug 19, 2025
270dbbe
Merge branch 'generate-test-modular' of https://github.com/marygao/au…
MaryGao Aug 19, 2025
8752cc4
Fix the additional issues in testings
MaryGao Aug 19, 2025
ad54af1
Format codes
MaryGao Aug 19, 2025
0405fd4
Fix the response types in bytes
MaryGao Aug 19, 2025
6983f01
Merge branch 'main' into generate-test-modular
MaryGao Aug 26, 2025
636419c
Merge branch 'main' into generate-test-modular
MaryGao Aug 26, 2025
bdfea4c
refactor: extract common utilities for samples and tests generation
Aug 26, 2025
f7b0a08
feat: Complete refactoring of emitSamples and emitTests with shared u…
Aug 26, 2025
4e36c0b
Format code
Aug 26, 2025
b3a441c
Merge branch 'main' into generate-test-modular
MaryGao Aug 28, 2025
de93d3f
Fix the lint issues
Aug 28, 2025
f5b1b2a
Revert the change in generated samples
Aug 28, 2025
7ce7d01
Revert change in smoke testing
Aug 28, 2025
cdb25a7
Rename the interface a little and improve the import with binder
MaryGao Sep 8, 2025
5d0da7a
Fix the import issues and regenerate in the UTs
MaryGao Sep 8, 2025
b107897
Refactor the recorder create helper
MaryGao Sep 9, 2025
297835f
Fix the UTs
MaryGao Sep 9, 2025
0a31b02
Fix the un-used files removed issue
MaryGao Sep 9, 2025
c486f60
Update the UTs
Sep 9, 2025
eb9aebe
add ut for sample parameter order
v-jiaodi Sep 16, 2025
358c652
update
v-jiaodi Sep 16, 2025
dbde475
Merge branch 'main' into generate-test-modular
MaryGao Sep 17, 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
19 changes: 15 additions & 4 deletions packages/typespec-ts/src/framework/hooks/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export interface Binder {
sourceFile: SourceFile
): string;
resolveReference(refkey: unknown): string;
resolveAllReferences(sourceRoot: string): void;
resolveAllReferences(sourceRoot: string, testRoot?: string): void;
}

const PLACEHOLDER_PREFIX = "__PLACEHOLDER_";
Expand Down Expand Up @@ -226,7 +226,7 @@ class BinderImp implements Binder {
/**
* Applies all tracked imports to their respective source files.
*/
resolveAllReferences(sourceRoot: string): void {
resolveAllReferences(sourceRoot: string, testRoot?: string): void {
this.project.getSourceFiles().map((file) => {
this.resolveDeclarationReferences(file);
this.resolveDependencyReferences(file);
Expand All @@ -242,7 +242,7 @@ class BinderImp implements Binder {
}
});

this.cleanUnreferencedHelpers(sourceRoot);
this.cleanUnreferencedHelpers(sourceRoot, testRoot);
}

private resolveDependencyReferences(file: SourceFile) {
Expand Down Expand Up @@ -302,7 +302,7 @@ class BinderImp implements Binder {
this.references.get(refkey)!.add(sourceFile);
}

private cleanUnreferencedHelpers(sourceRoot: string) {
private cleanUnreferencedHelpers(sourceRoot: string, testRoot?: string) {
const usedHelperFiles = new Set<SourceFile>();
for (const helper of this.staticHelpers.values()) {
const sourceFile = helper[SourceFileSymbol];
Expand All @@ -319,13 +319,24 @@ class BinderImp implements Binder {
}
}

// delete unused helper files
this.project
//normalizae the final path to adapt to different systems
.getSourceFiles(
normalizePath(path.join(sourceRoot, "static-helpers/**/*.ts"))
)
.filter((helperFile) => !usedHelperFiles.has(helperFile))
.forEach((helperFile) => helperFile.delete());
if (!testRoot) {
return;
}
this.project
//normalizae the final path to adapt to different systems
.getSourceFiles(
normalizePath(path.join(testRoot, "test/generated/util/**/*.ts"))
)
.filter((helperFile) => !usedHelperFiles.has(helperFile))
.forEach((helperFile) => helperFile.delete());
}
}

Expand Down
122 changes: 70 additions & 52 deletions packages/typespec-ts/src/framework/load-static-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,76 +35,97 @@ export function isStaticHelperMetadata(

export type StaticHelpers = Record<string, StaticHelperMetadata>;

const DEFAULT_STATIC_HELPERS_PATH = "static/static-helpers";
const DEFAULT_SOURCES_STATIC_HELPERS_PATH = "static/static-helpers";
const DEFAULT_SOURCES_TESTING_HELPERS_PATH = "static/test-helpers";

export interface LoadStaticHelpersOptions
extends Partial<ModularEmitterOptions> {
helpersAssetDirectory?: string;
sourcesDir?: string;
rootDir?: string;
}

interface FileMetadata {
source: string;
target: string;
}

export async function loadStaticHelpers(
project: Project,
helpers: StaticHelpers,
options: LoadStaticHelpersOptions = {}
): Promise<Map<string, StaticHelperMetadata>> {
const sourcesDir = options.sourcesDir ?? "";
const helpersMap = new Map<string, StaticHelperMetadata>();
// Load static helpers used in sources code
const defaultStaticHelpersPath = path.join(
resolveProjectRoot(),
DEFAULT_STATIC_HELPERS_PATH
DEFAULT_SOURCES_STATIC_HELPERS_PATH
);
const files = await traverseDirectory(
const filesInSources = await traverseDirectory(
options.helpersAssetDirectory ?? defaultStaticHelpersPath
);
await loadFiles(filesInSources, options.sourcesDir ?? "");
// Load static helpers used in testing code
const defaultTestingHelpersPath = path.join(
resolveProjectRoot(),
DEFAULT_SOURCES_TESTING_HELPERS_PATH
);
const filesInTestings = await traverseDirectory(
defaultTestingHelpersPath,
[],
"",
"test/generated/util"
);
await loadFiles(filesInTestings, options.rootDir ?? "");
return assertAllHelpersLoadedPresent(helpersMap);

for (const file of files) {
const targetPath = path.join(sourcesDir, file.target);
const contents = await readFile(file.source, "utf-8");
const addedFile = project.createSourceFile(targetPath, contents, {
overwrite: true
});
addedFile.getImportDeclarations().map((i) => {
if (!isAzurePackage({ options: options.options })) {
if (
i
.getModuleSpecifier()
.getFullText()
.includes("@azure/core-rest-pipeline")
) {
i.setModuleSpecifier("@typespec/ts-http-runtime");
async function loadFiles(files: FileMetadata[], generateDir: string) {
for (const file of files) {
const targetPath = path.join(generateDir, file.target);
const contents = await readFile(file.source, "utf-8");
const addedFile = project.createSourceFile(targetPath, contents, {
overwrite: true
});
addedFile.getImportDeclarations().map((i) => {
if (!isAzurePackage({ options: options.options })) {
if (
i
.getModuleSpecifier()
.getFullText()
.includes("@azure/core-rest-pipeline")
) {
i.setModuleSpecifier("@typespec/ts-http-runtime");
}
if (
i
.getModuleSpecifier()
.getFullText()
.includes("@azure-rest/core-client")
) {
i.setModuleSpecifier("@typespec/ts-http-runtime");
}
}
if (
i
.getModuleSpecifier()
.getFullText()
.includes("@azure-rest/core-client")
) {
i.setModuleSpecifier("@typespec/ts-http-runtime");
});

for (const entry of Object.values(helpers)) {
if (!addedFile.getFilePath().endsWith(entry.location)) {
continue;
}
}
});

for (const entry of Object.values(helpers)) {
if (!addedFile.getFilePath().endsWith(entry.location)) {
continue;
}
const declaration = getDeclarationByMetadata(addedFile, entry);
if (!declaration) {
throw new Error(
`Declaration ${
entry.name
} not found in file ${addedFile.getFilePath()}\n This is an Emitter bug, make sure that the map of static helpers passed to loadStaticHelpers matches what is in the file.`
);
}

const declaration = getDeclarationByMetadata(addedFile, entry);
if (!declaration) {
throw new Error(
`Declaration ${
entry.name
} not found in file ${addedFile.getFilePath()}\n This is an Emitter bug, make sure that the map of static helpers passed to loadStaticHelpers matches what is in the file.`
);
entry[SourceFileSymbol] = addedFile;
helpersMap.set(refkey(entry), entry);
}

entry[SourceFileSymbol] = addedFile;
helpersMap.set(refkey(entry), entry);
}
}

return assertAllHelpersLoadedPresent(helpersMap);
}

function assertAllHelpersLoadedPresent(
Expand Down Expand Up @@ -158,11 +179,11 @@ function getDeclarationByMetadata(
}
}

const _targetStaticHelpersBaseDir = "static-helpers";
async function traverseDirectory(
directory: string,
result: { source: string; target: string }[] = [],
relativePath: string = ""
relativePath: string = "",
targetBaseDir: string = "static-helpers"
): Promise<{ source: string; target: string }[]> {
try {
const files = await readdir(directory);
Expand All @@ -176,18 +197,15 @@ async function traverseDirectory(
await traverseDirectory(
filePath,
result,
path.join(relativePath, file)
path.join(relativePath, file),
targetBaseDir
);
} else if (
fileStat.isFile() &&
!file.endsWith(".d.ts") &&
file.endsWith(".ts")
) {
const target = path.join(
_targetStaticHelpersBaseDir,
relativePath,
file
);
const target = path.join(targetBaseDir, relativePath, file);
result.push({ source: filePath, target });
}
})
Expand Down
24 changes: 20 additions & 4 deletions packages/typespec-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import {
AzureCoreDependencies,
AzureIdentityDependencies,
AzurePollingDependencies,
DefaultCoreDependencies
DefaultCoreDependencies,
AzureTestDependencies
} from "./modular/external-dependencies.js";
import { EmitContext, Program } from "@typespec/compiler";
import { GenerationDirDetail, SdkContext } from "./utils/interfaces.js";
import {
CloudSettingHelpers,
CreateRecorderHelpers,
MultipartHelpers,
PagingHelpers,
PollingHelpers,
Expand Down Expand Up @@ -93,6 +95,7 @@ import { provideSdkTypes } from "./framework/hooks/sdkTypes.js";
import { transformRLCModel } from "./transform/transform.js";
import { transformRLCOptions } from "./transform/transfromRLCOptions.js";
import { emitSamples } from "./modular/emitSamples.js";
import { emitTests } from "./modular/emitTests.js";

export * from "./lib.js";

Expand Down Expand Up @@ -133,10 +136,12 @@ export async function $onEmit(context: EmitContext) {
...PollingHelpers,
...UrlTemplateHelpers,
...MultipartHelpers,
...CloudSettingHelpers
...CloudSettingHelpers,
...CreateRecorderHelpers
},
{
sourcesDir: dpgContext.generationPathDetail?.modularSourcesDir,
rootDir: dpgContext.generationPathDetail?.rootDir,
options: rlcOptions
}
);
Expand All @@ -145,7 +150,8 @@ export async function $onEmit(context: EmitContext) {
? {
...AzurePollingDependencies,
...AzureCoreDependencies,
...AzureIdentityDependencies
...AzureIdentityDependencies,
...AzureTestDependencies
}
: { ...DefaultCoreDependencies };
console.time("onEmit: provide binder");
Expand Down Expand Up @@ -355,8 +361,18 @@ export async function $onEmit(context: EmitContext) {
}
}

// Enable modular test generation when explicitly set to true
if (emitterOptions["experimental-generate-test-files"] === true) {
console.time("onEmit: emit tests");
await emitTests(dpgContext);
console.timeEnd("onEmit: emit tests");
}

console.time("onEmit: resolve references");
binder.resolveAllReferences(modularSourcesRoot);
binder.resolveAllReferences(
modularSourcesRoot,
dpgContext.generationPathDetail?.rootDir ?? ""
);
if (program.compilerOptions.noEmit || program.hasError()) {
return;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/typespec-ts/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export interface EmitterOptions {
"default-value-object"?: boolean;
//TODO should remove this after finish the release tool test
"should-use-pnpm-dep"?: boolean;
"experimental-generate-test-files"?: boolean;
}

export const RLCOptionsSchema: JSONSchemaType<EmitterOptions> = {
Expand Down Expand Up @@ -335,6 +336,11 @@ export const RLCOptionsSchema: JSONSchemaType<EmitterOptions> = {
type: "boolean",
nullable: true,
description: "Internal option for test."
},
"experimental-generate-test-files": {
type: "boolean",
nullable: true,
description: "Whether to generate test files for the client."
}
},
required: []
Expand Down
Loading
Loading