Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function executeOnAzurite(context: IActionContext, command: string,
const azuriteExtension = extensions.getExtension(azuriteExtensionId);

if (azuriteExtension?.isActive) {
vscode.commands.executeCommand(command, {
await vscode.commands.executeCommand(command, {
...args,
});
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { autoStartAzuriteSetting, localEmulatorConnectionString } from '../../../constants';
import { validateFuncCoreToolsInstalled } from '../../commands/funcCoreTools/validateFuncCoreToolsInstalled';
import { getAzureWebJobsStorage } from '../../utils/appSettings/localSettings';
import { getWorkspaceSetting } from '../../utils/vsCodeConfig/settings';
import { preDebugValidate, validateEmulatorIsRunning } from '../validatePreDebug';

vi.mock('azure-storage', () => ({
createBlobService: vi.fn(() => ({
doesContainerExist: (_container: string, callback: (err?: Error) => void) => callback(new Error('connection refused')),
})),
}));

vi.mock('../../commands/funcCoreTools/validateFuncCoreToolsInstalled', () => ({
validateFuncCoreToolsInstalled: vi.fn(),
}));

vi.mock('../../utils/appSettings/localSettings', () => ({
getAzureWebJobsStorage: vi.fn(),
setLocalAppSetting: vi.fn(),
}));

vi.mock('../../utils/vsCodeConfig/settings', () => ({
getFunctionsWorkerRuntime: vi.fn(),
getWorkspaceSetting: vi.fn(),
}));

describe('validatePreDebug', () => {
const projectPath = 'D:\\workspace\\LogicApp';
const context = {
telemetry: {
properties: {},
measurements: {},
},
ui: {
showWarningMessage: vi.fn(),
},
} as any;

beforeEach(() => {
vi.clearAllMocks();
context.telemetry.properties = {};
vi.mocked(getAzureWebJobsStorage).mockResolvedValue(localEmulatorConnectionString);
});

it('does not offer Debug anyway when auto-started Azurite cannot be reached', async () => {
vi.mocked(validateFuncCoreToolsInstalled).mockResolvedValue(true);
vi.mocked(getWorkspaceSetting).mockImplementation((key: string) => {
return key === autoStartAzuriteSetting ? true : undefined;
});

const result = await preDebugValidate(context, projectPath);

expect(result).toBe(false);
expect(context.ui.showWarningMessage).toHaveBeenCalledWith(
expect.stringContaining('Failed to verify "AzureWebJobsStorage"'),
expect.objectContaining({ modal: true })
);
});

it('blocks debug when AzureWebJobsStorage is missing', async () => {
vi.mocked(validateFuncCoreToolsInstalled).mockResolvedValue(true);
vi.mocked(getAzureWebJobsStorage).mockResolvedValue(undefined);

const result = await preDebugValidate(context, projectPath);

expect(result).toBe(false);
expect(context.ui.showWarningMessage).toHaveBeenCalledWith(
expect.stringContaining('Missing required "AzureWebJobsStorage"'),
expect.objectContaining({ modal: true })
);
});

it('keeps Debug anyway available when explicitly allowed', async () => {
context.ui.showWarningMessage.mockImplementation(async (_message: string, _options: unknown, debugAnyway: unknown) => debugAnyway);

const result = await validateEmulatorIsRunning(context, projectPath, { allowDebugAnyway: true });

expect(result).toBe(true);
expect(context.ui.showWarningMessage).toHaveBeenCalledWith(
expect.stringContaining('Failed to verify "AzureWebJobsStorage"'),
expect.objectContaining({ modal: true }),
expect.objectContaining({ title: 'Debug anyway' })
);
});
});
38 changes: 35 additions & 3 deletions apps/vs-code-designer/src/app/debug/validatePreDebug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
autoStartAzuriteSetting,
projectLanguageSetting,
workerRuntimeKey,
localEmulatorConnectionString,
Expand All @@ -20,6 +21,11 @@ import { MismatchBehavior, Platform } from '@microsoft/vscode-extension-logic-ap
import * as azureStorage from 'azure-storage';
import * as vscode from 'vscode';

export interface ValidateEmulatorOptions {
promptWarningMessage?: boolean;
allowDebugAnyway?: boolean;
}

/**
* Validates functions core tools is installed and azure emulator is running
* @param {IActionContext} context - Command context.
Expand All @@ -45,7 +51,15 @@ export async function preDebugValidate(context: IActionContext, projectPath: str
await validateWorkerRuntime(context, projectLanguage, projectPath);

context.telemetry.properties.lastValidateStep = 'emulatorRunning';
shouldContinue = await validateEmulatorIsRunning(context, projectPath);
const azureWebJobsStorage: string | undefined = await getAzureWebJobsStorage(context, projectPath);
if (azureWebJobsStorage?.trim()) {
const autoStartAzurite = !!getWorkspaceSetting<boolean>(autoStartAzuriteSetting);
shouldContinue = await validateEmulatorIsRunning(context, projectPath, {
allowDebugAnyway: !autoStartAzurite,
});
} else {
shouldContinue = await showMissingAzureWebJobsStorageWarning(context);
}
}
} catch (error) {
if (parseError(error).isUserCancelledError) {
Expand Down Expand Up @@ -104,18 +118,31 @@ async function validateWorkerRuntime(context: IActionContext, projectLanguage: s
}
}

async function showMissingAzureWebJobsStorageWarning(context: IActionContext): Promise<boolean> {
const message: string = localize(
'missingAzureWebJobsStorage',
'Missing required "{0}" connection in "{1}". Add a storage connection string before debugging this project.',
azureWebJobsStorageKey,
localSettingsFileName
);
await context.ui.showWarningMessage(message, { modal: true });
return false;
}

/**
* If AzureWebJobsStorage is set, pings the emulator to make sure it's actually running
* @param {IActionContext} context - Command context.
* @param {string} projectPath - Project path.
* @param {boolean} promptWarningMessage - Boolean to determine whether prompt a message to ask user if emulator is running.
* @param {boolean | ValidateEmulatorOptions} options - Options for prompting and allowing debug continuation.
* @returns {boolean} Returns true if a valid emulator is running, otherwise returns false.
*/
export async function validateEmulatorIsRunning(
context: IActionContext,
projectPath: string,
promptWarningMessage = true
options: boolean | ValidateEmulatorOptions = true
): Promise<boolean> {
const promptWarningMessage = typeof options === 'boolean' ? options : (options.promptWarningMessage ?? true);
const allowDebugAnyway = typeof options === 'boolean' ? true : (options.allowDebugAnyway ?? true);
const azureWebJobsStorage: string | undefined = await getAzureWebJobsStorage(context, projectPath);

if (azureWebJobsStorage && azureWebJobsStorage.toLowerCase() === localEmulatorConnectionString.toLowerCase()) {
Expand Down Expand Up @@ -143,6 +170,11 @@ export async function validateEmulatorIsRunning(
);

const learnMoreLink: string = process.platform === Platform.windows ? 'https://aka.ms/AA4ym56' : 'https://aka.ms/AA4yef8';
if (!allowDebugAnyway) {
await context.ui.showWarningMessage(message, { learnMoreLink, modal: true });
return false;
}

const debugAnyway: vscode.MessageItem = { title: localize('debugAnyway', 'Debug anyway') };
const result: vscode.MessageItem = await context.ui.showWarningMessage(message, { learnMoreLink, modal: true }, debugAnyway);
return result === debugAnyway;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as vscode from 'vscode';
import {
autoStartAzuriteSetting,
azuriteBinariesLocationSetting,
azuriteExtensionPrefix,
azuriteLocationSetting,
defaultAzuritePathValue,
showAutoStartAzuriteWarning,
} from '../../../../constants';
import { executeOnAzurite } from '../../../azuriteExtension/executeOnAzuriteExt';
import { validateEmulatorIsRunning } from '../../../debug/validatePreDebug';
import { updateWorkspaceSetting } from '../../vsCodeConfig/settings';
import { activateAzurite } from '../activateAzurite';

vi.mock('../../../azuriteExtension/executeOnAzuriteExt', () => ({
executeOnAzurite: vi.fn(),
}));

vi.mock('../../../debug/validatePreDebug', () => ({
validateEmulatorIsRunning: vi.fn(),
}));

vi.mock('../../delay', () => ({
delay: vi.fn(),
}));

vi.mock('../../verifyIsProject', () => ({
tryGetLogicAppProjectRoot: vi.fn(),
}));

vi.mock('../../workspace', () => ({
getWorkspaceFolder: vi.fn(),
}));

vi.mock('../../vsCodeConfig/settings', () => ({
getWorkspaceSetting: vi.fn((key: string, _projectPath?: string, prefix?: string) => {
if (key === azuriteLocationSetting && prefix === azuriteExtensionPrefix) {
return undefined;
}

if (key === azuriteBinariesLocationSetting) {
return defaultAzuritePathValue;
}

if (key === showAutoStartAzuriteWarning) {
return false;
}

if (key === autoStartAzuriteSetting) {
return true;
}

return undefined;
}),
updateGlobalSetting: vi.fn(),
updateWorkspaceSetting: vi.fn(),
}));

describe('activateAzurite', () => {
const projectPath = 'D:\\workspace\\LogicApp';
const context = {
telemetry: {
properties: {},
measurements: {},
},
ui: {
showWarningMessage: vi.fn(),
showInputBox: vi.fn(),
},
} as any;

beforeEach(() => {
vi.clearAllMocks();
(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: projectPath } }];
context.telemetry.properties = {};
});

it('waits for Azurite to become ready after starting it', async () => {
vi.mocked(validateEmulatorIsRunning).mockResolvedValueOnce(false).mockResolvedValueOnce(false).mockResolvedValueOnce(true);

await activateAzurite(context, projectPath);

expect(updateWorkspaceSetting).toHaveBeenCalledWith(
azuriteLocationSetting,
defaultAzuritePathValue,
projectPath,
azuriteExtensionPrefix
);
expect(executeOnAzurite).toHaveBeenCalledTimes(1);
expect(validateEmulatorIsRunning).toHaveBeenNthCalledWith(1, context, projectPath, false);
expect(validateEmulatorIsRunning).toHaveBeenNthCalledWith(2, context, projectPath, false);
expect(validateEmulatorIsRunning).toHaveBeenNthCalledWith(3, context, projectPath, false);
expect(context.telemetry.properties.azuriteReady).toBe('true');
});
});
32 changes: 31 additions & 1 deletion apps/vs-code-designer/src/app/utils/azurite/activateAzurite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ import {
import { localize } from '../../../localize';
import { executeOnAzurite } from '../../azuriteExtension/executeOnAzuriteExt';
import { validateEmulatorIsRunning } from '../../debug/validatePreDebug';
import { delay } from '../delay';
import { tryGetLogicAppProjectRoot } from '../verifyIsProject';
import { getWorkspaceSetting, updateGlobalSetting, updateWorkspaceSetting } from '../vsCodeConfig/settings';
import { getWorkspaceFolder } from '../workspace';
import { DialogResponses, type IActionContext } from '@microsoft/vscode-azext-utils';
import * as vscode from 'vscode';
import type { MessageItem } from 'vscode';

const azuriteStartupRetryCount = 10;
const azuriteStartupRetryDelayMs = 500;

/**
* Prompts user to set azurite.location and Start Azurite.
* If azurite extension location was not set:
Expand All @@ -42,7 +46,7 @@ export async function activateAzurite(context: IActionContext, projectPath?: str

const showAutoStartAzuriteWarningSetting = !!getWorkspaceSetting<boolean>(showAutoStartAzuriteWarning);

const autoStartAzurite = !!getWorkspaceSetting<boolean>(autoStartAzuriteSetting);
let autoStartAzurite = !!getWorkspaceSetting<boolean>(autoStartAzuriteSetting);
context.telemetry.properties.autoStartAzurite = `${autoStartAzurite}`;

if (showAutoStartAzuriteWarningSetting) {
Expand All @@ -60,6 +64,8 @@ export async function activateAzurite(context: IActionContext, projectPath?: str
} else if (result === enableMessage) {
await updateGlobalSetting(showAutoStartAzuriteWarning, false);
await updateGlobalSetting(autoStartAzuriteSetting, true);
autoStartAzurite = true;
context.telemetry.properties.autoStartAzurite = 'true';

// User has not configured workspace azurite.location.
if (!azuriteLocationExtSetting) {
Expand Down Expand Up @@ -92,7 +98,31 @@ export async function activateAzurite(context: IActionContext, projectPath?: str
await executeOnAzurite(context, extensionCommand.azureAzuriteStart);
context.telemetry.properties.azuriteStart = 'true';
context.telemetry.properties.azuriteLocation = azuriteLocation;
await waitForAzuriteReady(context, projectPath);
}
}
}
}

async function waitForAzuriteReady(context: IActionContext, projectPath: string): Promise<void> {
for (let attempt = 1; attempt <= azuriteStartupRetryCount; attempt++) {
context.telemetry.properties.azuriteStartupAttempt = attempt.toString();
if (await validateEmulatorIsRunning(context, projectPath, false)) {
context.telemetry.properties.azuriteReady = 'true';
return;
}

if (attempt < azuriteStartupRetryCount) {
await delay(azuriteStartupRetryDelayMs);
}
Comment on lines +108 to +117
}

context.telemetry.properties.azuriteReady = 'false';
throw new Error(
localize(
'azuriteFailedToStart',
'Azurite did not become ready within "{0}" seconds. Make sure the Azurite extension is installed and running, then try debugging again.',
(azuriteStartupRetryCount * azuriteStartupRetryDelayMs) / 1000
Comment on lines +124 to +125
)
);
}
Loading