Skip to content

Commit 190b9c2

Browse files
CopilotJoshLove-msfttimotheeguerin
authored
Add per-table emitter configuration for spec-dashboard (#9054)
Some languages like C# require different emitters for management plane vs data plane scenarios. Currently, `emitterNames` is only configurable globally in `CoverageFromAzureStorageOptions`, preventing mixed-emitter table configurations. ## Changes - Added optional `emitterNames?: string[]` field to `TableDefinition` interface - Modified `splitManifestByTables()` to propagate table-specific emitter names through the pipeline - Updated `getCoverageSummaries()` to: - Collect union of global and table-specific emitters - Load all required reports in single batch - Filter reports per table using table-specific emitters when present, falling back to global `options.emitterNames` otherwise - Refactored `loadReports()` to accept emitter names as parameter ## Example ```typescript const options: CoverageFromAzureStorageOptions = { storageAccountName: "account", containerName: "coverage", manifestContainerName: "manifests", emitterNames: ["@typespec/default"], tables: [ { name: "Management Plane", packageName: "my-package", prefixes: ["mgmt_"], emitterNames: ["@azure-tools/typespec-csharp-mgmt"] // Override for mgmt scenarios }, { name: "Data Plane", packageName: "my-package", prefixes: ["dataplane_"], emitterNames: ["@azure-tools/typespec-csharp"] // Override for data plane } ] }; ``` <!-- START COPILOT CODING AGENT SUFFIX --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>Add ability to specify emitters per table</issue_title> > <issue_description>In some languages, like C#, we have different emitters for mgmt vs data plane scenarios. We should be able to specify the emitterNames in the TableDefinitions in CoverageFromAzureStorageOptions. If a table has emitter names we use that, otherwise fall back to global defaults.</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> - Fixes #9053 <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/microsoft/typespec/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: JoshLove-msft <[email protected]> Co-authored-by: jolov <[email protected]> Co-authored-by: Timothee Guerin <[email protected]>
1 parent 99a775b commit 190b9c2

File tree

2 files changed

+110
-17
lines changed

2 files changed

+110
-17
lines changed

packages/spec-dashboard/src/apis.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,4 +224,66 @@ describe("splitManifestByTables", () => {
224224
expect(defaultTable!.manifest.scenarios).toHaveLength(1);
225225
expect(defaultTable!.manifest.scenarios[0].name).toBe("unique_scenario");
226226
});
227+
228+
it("should include emitterNames from table definition", () => {
229+
const manifest = createManifest("test-package", "Display Name", ["scenario1"]);
230+
const tables: TableDefinition[] = [
231+
{
232+
name: "Test Table",
233+
packageName: "test-package",
234+
emitterNames: ["@typespec/emitter-1", "@typespec/emitter-2"],
235+
},
236+
];
237+
238+
const result = splitManifestByTables(manifest, tables);
239+
240+
expect(result).toHaveLength(1);
241+
expect(result[0].emitterNames).toEqual(["@typespec/emitter-1", "@typespec/emitter-2"]);
242+
});
243+
244+
it("should return undefined emitterNames when not specified in table definition", () => {
245+
const manifest = createManifest("test-package", "Display Name", ["scenario1"]);
246+
const tables: TableDefinition[] = [
247+
{
248+
name: "Test Table",
249+
packageName: "test-package",
250+
},
251+
];
252+
253+
const result = splitManifestByTables(manifest, tables);
254+
255+
expect(result).toHaveLength(1);
256+
expect(result[0].emitterNames).toBeUndefined();
257+
});
258+
259+
it("should handle different emitterNames for different tables", () => {
260+
const manifest = createManifest("test-package", "Display Name", [
261+
"mgmt_scenario1",
262+
"dataplane_scenario1",
263+
]);
264+
const tables: TableDefinition[] = [
265+
{
266+
name: "Management Table",
267+
packageName: "test-package",
268+
prefixes: ["mgmt_"],
269+
emitterNames: ["@azure-tools/typespec-csharp-mgmt"],
270+
},
271+
{
272+
name: "Data Plane Table",
273+
packageName: "test-package",
274+
prefixes: ["dataplane_"],
275+
emitterNames: ["@azure-tools/typespec-csharp"],
276+
},
277+
];
278+
279+
const result = splitManifestByTables(manifest, tables);
280+
281+
expect(result).toHaveLength(2);
282+
283+
const mgmtTable = result.find((r) => r.tableName === "Management Table");
284+
expect(mgmtTable!.emitterNames).toEqual(["@azure-tools/typespec-csharp-mgmt"]);
285+
286+
const dataPlaneTable = result.find((r) => r.tableName === "Data Plane Table");
287+
expect(dataPlaneTable!.emitterNames).toEqual(["@azure-tools/typespec-csharp"]);
288+
});
227289
});

packages/spec-dashboard/src/apis.ts

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export interface TableDefinition {
1414
packageName: string;
1515
/** Prefixes to filter the coverage data. Any scenarios starting with this prefix will be included in this table */
1616
prefixes?: string[];
17+
/** Optional emitter names specific to this table. If not provided, falls back to global emitterNames */
18+
emitterNames?: string[];
1719
}
1820

1921
export interface CoverageFromAzureStorageOptions {
@@ -70,7 +72,7 @@ function matchesPrefixes(scenarioName: string, prefixes: string[]): boolean {
7072
export function splitManifestByTables(
7173
manifest: ScenarioManifest,
7274
tableDefinitions: TableDefinition[],
73-
): Array<{ manifest: ScenarioManifest; tableName: string }> {
75+
): Array<{ manifest: ScenarioManifest; tableName: string; emitterNames?: string[] }> {
7476
const packageName = manifest.packageName ?? "";
7577
const defaultTableName = manifest.displayName || packageName;
7678

@@ -79,10 +81,11 @@ export function splitManifestByTables(
7981

8082
if (applicableTables.length === 0) {
8183
// No table definitions for this manifest, return as-is with a default name
82-
return [{ manifest, tableName: defaultTableName }];
84+
return [{ manifest, tableName: defaultTableName, emitterNames: undefined }];
8385
}
8486

85-
const result: Array<{ manifest: ScenarioManifest; tableName: string }> = [];
87+
const result: Array<{ manifest: ScenarioManifest; tableName: string; emitterNames?: string[] }> =
88+
[];
8689
const usedScenarios = new Set<string>();
8790

8891
// Separate tables with prefixes from catch-all tables (no prefixes)
@@ -109,6 +112,7 @@ export function splitManifestByTables(
109112
scenarios: filteredScenarios,
110113
},
111114
tableName: table.name,
115+
emitterNames: table.emitterNames,
112116
});
113117
}
114118
}
@@ -129,6 +133,7 @@ export function splitManifestByTables(
129133
scenarios: remainingScenarios,
130134
},
131135
tableName: table.name,
136+
emitterNames: table.emitterNames,
132137
});
133138
}
134139
}
@@ -145,6 +150,7 @@ export function splitManifestByTables(
145150
scenarios: unmatchedScenarios,
146151
},
147152
tableName: defaultTableName,
153+
emitterNames: undefined,
148154
});
149155
}
150156

@@ -156,18 +162,14 @@ export async function getCoverageSummaries(
156162
): Promise<CoverageSummary[]> {
157163
const coverageClient = getCoverageClient(options);
158164
const manifestClient = getManifestClient(options);
159-
const [manifests, generatorReports] = await Promise.all([
160-
manifestClient.manifest.get(),
161-
loadReports(coverageClient, options),
162-
]);
163165

164-
const reports = Object.values(generatorReports)[0] as Record<
165-
string,
166-
ResolvedCoverageReport | undefined
167-
>;
168-
169-
// Split manifests into tables based on configuration
170-
const allManifests: Array<{ manifest: ScenarioManifest; tableName: string }> = [];
166+
// First, split manifests to determine which emitters we need
167+
const manifests = await manifestClient.manifest.get();
168+
const allManifests: Array<{
169+
manifest: ScenarioManifest;
170+
tableName: string;
171+
emitterNames?: string[];
172+
}> = [];
171173

172174
for (const manifest of manifests) {
173175
if (options.tables && options.tables.length > 0) {
@@ -179,14 +181,42 @@ export async function getCoverageSummaries(
179181
allManifests.push({
180182
manifest,
181183
tableName: manifest.displayName || manifest.packageName || "",
184+
emitterNames: undefined,
182185
});
183186
}
184187
}
185188

186-
return allManifests.map(({ manifest, tableName }) => {
189+
// Collect all unique emitter names needed
190+
const allEmitterNames = new Set<string>(options.emitterNames);
191+
for (const { emitterNames } of allManifests) {
192+
if (emitterNames) {
193+
emitterNames.forEach((name) => allEmitterNames.add(name));
194+
}
195+
}
196+
197+
// Load reports for all needed emitters
198+
const generatorReports = await loadReports(coverageClient, options, Array.from(allEmitterNames));
199+
200+
const reports = Object.values(generatorReports)[0] as Record<
201+
string,
202+
ResolvedCoverageReport | undefined
203+
>;
204+
205+
return allManifests.map(({ manifest, tableName, emitterNames }) => {
206+
// Use table-specific emitters if provided, otherwise use global emitters
207+
const effectiveEmitters = emitterNames ?? options.emitterNames;
208+
209+
// Filter reports to only include the emitters for this table
210+
const filteredReports: Record<string, ResolvedCoverageReport | undefined> = {};
211+
for (const emitterName of effectiveEmitters) {
212+
if (reports[emitterName]) {
213+
filteredReports[emitterName] = reports[emitterName];
214+
}
215+
}
216+
187217
return {
188218
manifest,
189-
generatorReports: processReports(reports, manifest),
219+
generatorReports: processReports(filteredReports, manifest),
190220
tableName,
191221
};
192222
});
@@ -230,14 +260,15 @@ function getSuiteReportForManifest(
230260
async function loadReports(
231261
coverageClient: SpecCoverageClient,
232262
options: CoverageFromAzureStorageOptions,
263+
emitterNames: string[],
233264
): Promise<{
234265
[mode: string]: Record<string, ResolvedCoverageReport | undefined>;
235266
}> {
236267
const results = await Promise.all(
237268
(options.modes ?? ["standard"]).map(
238269
async (mode): Promise<[string, Record<string, ResolvedCoverageReport | undefined>]> => {
239270
const items = await Promise.all(
240-
options.emitterNames.map(
271+
emitterNames.map(
241272
async (emitterName): Promise<[string, ResolvedCoverageReport | undefined]> => {
242273
try {
243274
const report = await coverageClient.coverage.getLatestCoverageFor(

0 commit comments

Comments
 (0)