Skip to content

Commit f3b95cb

Browse files
nturinskiNathan TurinskialexweiningerCopilot
authored
Migrate tests to use testApi with esbuild (#4920)
* Migrate tests to use testApi with esbuild * Small clean up * Fix build --------- Co-authored-by: Nathan Turinski <naturins@microsoft.comm> Co-authored-by: alexweininger <alex.weininger@live.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a0fd2a0 commit f3b95cb

24 files changed

+500
-128
lines changed

src/commands/createFunctionApp/AuthenticationPromptStep.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ export class AuthenticationPromptStep<T extends IFunctionAppWizardContext> exten
2121
}
2222

2323
public shouldPrompt(context: T): boolean {
24-
// don't need to prompt if the user has already selected a managed identity
25-
return !context.managedIdentity;
24+
// don't need to prompt if the user has already chosen an authentication type
25+
return context.useManagedIdentity === undefined;
2626
}
2727

2828
public async getSubWizard(context: T): Promise<IWizardOptions<T> | undefined> {

src/commands/createFunctionApp/containerImage/detectDockerfile.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,18 @@ export async function detectDockerfile(context: ICreateFunctionAppContext): Prom
1818
return undefined;
1919
}
2020

21-
context.workspaceFolder ??= await getRootWorkspaceFolder() as WorkspaceFolder;
21+
try {
22+
context.workspaceFolder ??= await getRootWorkspaceFolder() as WorkspaceFolder;
23+
} catch {
24+
// If the user cancels workspace folder selection (e.g., in a multi-root workspace),
25+
// skip dockerfile detection rather than failing the entire creation flow
26+
return undefined;
27+
}
28+
29+
if (!context.workspaceFolder) {
30+
return undefined;
31+
}
32+
2233
context.rootPath ??= await tryGetFunctionProjectRoot(context, context.workspaceFolder, 'prompt') ?? context.workspaceFolder.uri.fsPath;
2334

2435
const pattern: RelativePattern = new RelativePattern(context.rootPath, `**/${dockerfileGlobPattern}`);

src/commands/createNewProject/pythonSteps/PythonVenvCreateStep.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class PythonVenvCreateStep extends AzureWizardExecuteStepWithActivityOutp
5050
}
5151

5252
public configureBeforeExecute(context: IPythonVenvWizardContext): void | Promise<void> {
53-
if (!context.venvName) {
53+
if (!context.venvName && this.shouldExecute(context)) {
5454
context.venvName = '.venv';
5555
}
5656
}

src/commands/deploy/deploy.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -201,31 +201,37 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
201201
Object.assign(context, await getStorageConnectionIfNeeded(Object.assign(context, subscriptionContext), appSettings, site, context.projectPath));
202202

203203
const deploymentWarningMessages: string[] = [];
204-
const connectionStringWarningMessage = await getWarningsForConnectionSettings(context, {
205-
appSettings,
206-
node,
207-
projectPath: context.projectPath
208-
});
209-
210-
if (connectionStringWarningMessage) {
211-
deploymentWarningMessages.push(connectionStringWarningMessage);
212-
}
204+
let eolWarningMessage: string | undefined;
205+
206+
// Skip all deployment warnings for newly created apps since the app was
207+
// just configured during creation and warnings would be noise
208+
if (!context.isNewApp) {
209+
const connectionStringWarningMessage = await getWarningsForConnectionSettings(context, {
210+
appSettings,
211+
node,
212+
projectPath: context.projectPath
213+
});
214+
215+
if (connectionStringWarningMessage) {
216+
deploymentWarningMessages.push(connectionStringWarningMessage);
217+
}
213218

214-
const eolWarningMessage = await getEolWarningMessages({ ...context, ...subscriptionContext }, {
215-
site: site.rawSite,
216-
isLinux: client.isLinux,
217-
isFlex: isFlexConsumption,
218-
client
219-
});
219+
eolWarningMessage = await getEolWarningMessages({ ...context, ...subscriptionContext }, {
220+
site: site.rawSite,
221+
isLinux: client.isLinux,
222+
isFlex: isFlexConsumption,
223+
client
224+
});
220225

221-
if (eolWarningMessage) {
222-
deploymentWarningMessages.push(eolWarningMessage);
223-
}
226+
if (eolWarningMessage) {
227+
deploymentWarningMessages.push(eolWarningMessage);
228+
}
224229

225-
const extensionBundleWarningMessage: string | undefined = await getWarningForExtensionBundle(context);
230+
const extensionBundleWarningMessage: string | undefined = await getWarningForExtensionBundle(context);
226231

227-
if (extensionBundleWarningMessage) {
228-
deploymentWarningMessages.push(extensionBundleWarningMessage);
232+
if (extensionBundleWarningMessage) {
233+
deploymentWarningMessages.push(extensionBundleWarningMessage);
234+
}
229235
}
230236

231237
if ((getWorkspaceSetting<boolean>('showDeployConfirmation', context.workspaceFolder.uri.fsPath) && !context.isNewApp && isZipDeploy) ||

src/extension.ts

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import { registerAppServiceExtensionVariables } from '@microsoft/vscode-azext-azureappservice';
99
import { registerAzureUtilsExtensionVariables, type AzureAccountTreeItemBase } from '@microsoft/vscode-azext-azureutils';
10-
import { callWithTelemetryAndErrorHandling, createApiProvider, createAzExtOutputChannel, createExperimentationService, registerErrorHandler, registerEvent, registerReportIssueCommand, registerUIExtensionVariables, type IActionContext, type apiUtils } from '@microsoft/vscode-azext-utils';
10+
import { callWithTelemetryAndErrorHandling, createApiProvider, createAzExtOutputChannel, createExperimentationService, registerErrorHandler, registerEvent, registerOnActionStartHandler, registerReportIssueCommand, registerUIExtensionVariables, type apiUtils, type IActionContext } from '@microsoft/vscode-azext-utils';
1111
import { AzExtResourceType, getAzureResourcesExtensionApi } from '@microsoft/vscode-azureresources-api';
1212
import * as vscode from 'vscode';
1313
import { FunctionAppResolver } from './FunctionAppResolver';
@@ -16,7 +16,14 @@ import { createFunctionFromApi } from './commands/api/createFunctionFromApi';
1616
import { downloadAppSettingsFromApi } from './commands/api/downloadAppSettingsFromApi';
1717
import { revealTreeItem } from './commands/api/revealTreeItem';
1818
import { uploadAppSettingsFromApi } from './commands/api/uploadAppSettingsFromApi';
19+
import { copyFunctionUrl } from './commands/copyFunctionUrl';
1920
import { runPostFunctionCreateStepsFromCache } from './commands/createFunction/FunctionCreateStepBase';
21+
import { createFunctionInternal } from './commands/createFunction/createFunction';
22+
import { createFunctionApp, createFunctionAppAdvanced } from './commands/createFunctionApp/createFunctionApp';
23+
import { createNewProjectInternal } from './commands/createNewProject/createNewProject';
24+
import { deleteFunctionApp } from './commands/deleteFunctionApp';
25+
import { deployProductionSlot } from './commands/deploy/deploy';
26+
import { initProjectForVSCode } from './commands/initProjectForVSCode/initProjectForVSCode';
2027
import { startFuncProcessFromApi } from './commands/pickFuncProcess';
2128
import { registerCommands } from './commands/registerCommands';
2229
import { func } from './constants';
@@ -27,12 +34,13 @@ import { NodeDebugProvider } from './debug/NodeDebugProvider';
2734
import { PowerShellDebugProvider } from './debug/PowerShellDebugProvider';
2835
import { PythonDebugProvider } from './debug/PythonDebugProvider';
2936
import { handleUri } from './downloadAzureProject/handleUri';
30-
import { ext } from './extensionVariables';
37+
import { ext, TemplateSource } from './extensionVariables';
3138
import { registerFuncHostTaskEvents } from './funcCoreTools/funcHostTask';
3239
import { validateFuncCoreToolsInstalled } from './funcCoreTools/validateFuncCoreToolsInstalled';
3340
import { validateFuncCoreToolsIsLatest } from './funcCoreTools/validateFuncCoreToolsIsLatest';
3441
import { getResourceGroupsApi } from './getExtensionApi';
3542
import { CentralTemplateProvider } from './templates/CentralTemplateProvider';
43+
import type { TestApi } from './testApi';
3644
import { ShellContainerClient } from './tree/durableTaskScheduler/ContainerClient';
3745
import { HttpDurableTaskSchedulerClient } from './tree/durableTaskScheduler/DurableTaskSchedulerClient';
3846
import { DurableTaskSchedulerDataBranchProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider';
@@ -137,24 +145,81 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta
137145
new DurableTaskSchedulerWorkspaceDataBranchProvider(emulatorClient));
138146
});
139147

140-
console.log('Activated Azure Functions extension...');
141-
console.log('**********************************************');
148+
const apis: (AzureFunctionsExtensionApi | TestApi)[] = [
149+
<AzureFunctionsExtensionApi>{
150+
revealTreeItem,
151+
createFunction: createFunctionFromApi,
152+
downloadAppSettings: downloadAppSettingsFromApi,
153+
uploadAppSettings: uploadAppSettingsFromApi,
154+
listLocalProjects: listLocalProjects,
155+
listLocalFunctions: listLocalFunctions,
156+
isFuncCoreToolsInstalled: async (message: string) => {
157+
return await callWithTelemetryAndErrorHandling('azureFunctions.api.isFuncCoreToolsInstalled', async (context: IActionContext) => {
158+
return await validateFuncCoreToolsInstalled(context, message, undefined);
159+
});
160+
},
161+
startFuncProcess: startFuncProcessFromApi,
162+
apiVersion: '1.10.0'
163+
}
164+
];
165+
166+
// Add test API when running tests
167+
// This allows tests to access and override internal extension state without changing the public API.
168+
if (process.env.VSCODE_RUNNING_TESTS) {
169+
// Cache of template providers keyed by source, for use across test runs
170+
const testTemplateProviders = new Map<string, CentralTemplateProvider>();
171+
172+
apis.push(<TestApi>{
173+
apiVersion: '99.0.0',
174+
extensionVariables: {
175+
getOutputChannel: () => ext.outputChannel,
176+
getContext: () => ext.context,
177+
getRgApi: () => ext.rgApi,
178+
getIgnoreBundle: () => ext.ignoreBundle
179+
},
180+
testing: {
181+
setIgnoreBundle: (ignoreBundle) => {
182+
ext.ignoreBundle = ignoreBundle;
183+
},
184+
registerOnActionStartHandler,
185+
},
186+
commands: {
187+
createFunctionApp,
188+
createFunctionAppAdvanced,
189+
deleteFunctionApp,
190+
deployProductionSlot,
191+
copyFunctionUrl,
192+
createNewProjectInternal,
193+
createFunctionInternal,
194+
initProjectForVSCode,
195+
registerTemplateSource: (context, source) => {
196+
let cached = testTemplateProviders.get(source);
197+
if (!cached) {
198+
cached = new CentralTemplateProvider(source as TemplateSource);
199+
testTemplateProviders.set(source, cached);
200+
}
201+
ext.templateProvider.registerActionVariable(cached, context);
202+
},
203+
getFunctionTemplates: async (context, projectPath, language, languageModel, version, templateFilter, projectTemplateKey, source) => {
204+
let provider: CentralTemplateProvider;
205+
if (source) {
206+
let cached = testTemplateProviders.get(source);
207+
if (!cached) {
208+
cached = new CentralTemplateProvider(source as TemplateSource);
209+
testTemplateProviders.set(source, cached);
210+
}
211+
provider = cached;
212+
ext.templateProvider.registerActionVariable(provider, context);
213+
} else {
214+
provider = ext.templateProvider.get(context);
215+
}
216+
return provider.getFunctionTemplates(context, projectPath, language, languageModel, version, templateFilter, projectTemplateKey);
217+
}
218+
}
219+
});
220+
}
142221

143-
return createApiProvider([<AzureFunctionsExtensionApi>{
144-
revealTreeItem,
145-
createFunction: createFunctionFromApi,
146-
downloadAppSettings: downloadAppSettingsFromApi,
147-
uploadAppSettings: uploadAppSettingsFromApi,
148-
listLocalProjects: listLocalProjects,
149-
listLocalFunctions: listLocalFunctions,
150-
isFuncCoreToolsInstalled: async (message: string) => {
151-
return await callWithTelemetryAndErrorHandling('azureFunctions.api.isFuncCoreToolsInstalled', async (context: IActionContext) => {
152-
return await validateFuncCoreToolsInstalled(context, message, undefined);
153-
});
154-
},
155-
startFuncProcess: startFuncProcessFromApi,
156-
apiVersion: '1.10.0'
157-
}]);
222+
return createApiProvider(apis);
158223
}
159224

160225
export async function deactivateInternal(): Promise<void> {

src/testApi.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import type { IActionContext, IAzExtOutputChannel } from '@microsoft/vscode-azext-utils';
7+
import type { AzureHostExtensionApi } from '@microsoft/vscode-azext-utils/hostapi';
8+
import type { Disposable } from 'vscode';
9+
import type * as vscode from 'vscode';
10+
import type { ProjectLanguage, TemplateFilter } from './constants';
11+
import type { FuncVersion } from './FuncVersion';
12+
import type { FunctionTemplateBase } from './templates/IFunctionTemplate';
13+
import type { ICreateFunctionOptions } from './vscode-azurefunctions.api';
14+
15+
/**
16+
* Test-only API for accessing internal extension state.
17+
* This API is only available when VSCODE_RUNNING_TESTS environment variable is set.
18+
* It should NEVER be used in production code.
19+
*/
20+
export interface TestApi {
21+
/**
22+
* API version for the test API
23+
*/
24+
apiVersion: '99.0.0';
25+
26+
/**
27+
* Access to select internal extension variables (exposed as functions to avoid accidental mutation).
28+
*/
29+
extensionVariables: {
30+
getOutputChannel(): IAzExtOutputChannel | undefined;
31+
getContext(): vscode.ExtensionContext | undefined;
32+
getRgApi(): AzureHostExtensionApi | undefined;
33+
getIgnoreBundle(): boolean | undefined;
34+
};
35+
36+
/**
37+
* Testing utilities for overriding internal state.
38+
*/
39+
testing: {
40+
setIgnoreBundle(ignoreBundle: boolean | undefined): void;
41+
/**
42+
* Register an onActionStartHandler in the BUNDLE's @microsoft/vscode-azext-utils instance.
43+
* This is needed because the test module and bundle each have their own copy of vscode-azext-utils.
44+
* Handlers registered in the test module do NOT apply to action contexts created within the bundle.
45+
*/
46+
registerOnActionStartHandler(handler: (context: IActionContext) => void): Disposable;
47+
};
48+
49+
/**
50+
* Commands exposed for testing.
51+
*/
52+
commands: {
53+
createFunctionApp(context: IActionContext, ...args: unknown[]): Promise<string>;
54+
createFunctionAppAdvanced(context: IActionContext, ...args: unknown[]): Promise<string | undefined>;
55+
deleteFunctionApp(context: IActionContext, ...args: unknown[]): Promise<void>;
56+
deployProductionSlot(context: IActionContext, ...args: unknown[]): Promise<void>;
57+
copyFunctionUrl(context: IActionContext, ...args: unknown[]): Promise<void>;
58+
createNewProjectInternal(context: IActionContext, options?: ICreateFunctionOptions): Promise<void>;
59+
createFunctionInternal(context: IActionContext, ...args: unknown[]): Promise<void>;
60+
initProjectForVSCode(context: IActionContext, fsPath?: string, language?: string): Promise<void>;
61+
62+
/**
63+
* Register a template source for the given action context.
64+
* This creates/caches a per-source CentralTemplateProvider in the bundle and registers it
65+
* as the action variable on the bundle's ext.templateProvider for the given context.
66+
*/
67+
registerTemplateSource(context: IActionContext, source: string): void;
68+
69+
/**
70+
* Get function templates from the bundled extension's CentralTemplateProvider.
71+
* This crosses the esbuild module boundary, using the real provider registered during activation.
72+
* @param source - Optional template source (Backup, Latest, Staging). If undefined, uses the default provider.
73+
*/
74+
getFunctionTemplates(
75+
context: IActionContext,
76+
projectPath: string | undefined,
77+
language: ProjectLanguage,
78+
languageModel: number | undefined,
79+
version: FuncVersion,
80+
templateFilter: TemplateFilter,
81+
projectTemplateKey: string | undefined,
82+
source?: string
83+
): Promise<FunctionTemplateBase[]>;
84+
};
85+
}

0 commit comments

Comments
 (0)