diff --git a/package.json b/package.json index 92962be54..dd6de4874 100644 --- a/package.json +++ b/package.json @@ -1373,6 +1373,11 @@ "description": "%azureFunctions.validateFuncCoreTools%", "default": true }, + "azureFunctions.forceEmulatorValidation": { + "type": "boolean", + "description": "%azureFunctions.forceEmulatorValidation%", + "default": false + }, "azureFunctions.requestTimeout": { "type": "number", "description": "%azureFunctions.requestTimeout%", diff --git a/package.nls.json b/package.nls.json index 07c2f44c0..d782788a1 100644 --- a/package.nls.json +++ b/package.nls.json @@ -111,6 +111,7 @@ "azureFunctions.toggleAppSettingVisibility": "Toggle App Setting Visibility.", "azureFunctions.uninstallFuncCoreTools": "Uninstall Azure Functions Core Tools", "azureFunctions.validateFuncCoreTools": "Validate the Azure Functions Core Tools is installed before debugging.", + "azureFunctions.forceEmulatorValidation": "Force the extension to validate emulator connections before debugging. Overrides the default behavior of skipping emulator setup when a reference to an emulator is detected in the existing pre-launch task chain.", "azureFunctions.viewCommitInGitHub": "View Commit in GitHub", "azureFunctions.viewDeploymentLogs": "View Deployment Logs", "azureFunctions.viewProperties": "View Properties", diff --git a/src/debug/getPreLaunchTaskChain.ts b/src/debug/getPreLaunchTaskChain.ts new file mode 100644 index 000000000..ed04c6f73 --- /dev/null +++ b/src/debug/getPreLaunchTaskChain.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITask } from '../vsCodeConfig/tasks'; + +/** + * Resolves the full chain of tasks associated with a given `preLaunchTask`. + * Recursively follows the `dependsOn` references found in the `tasks.json`. + */ +export function getPreLaunchTaskChain(tasks: ITask[], preLaunchTask: string): string[] { + const tasksMap = new Map(); + + for (const task of tasks) { + if (task.label) { + tasksMap.set(task.label, task); + } + } + + const dependentTasks = new Set(); + + function getDependentTasks(name: string): void { + const task = tasksMap.get(name); + if (!task || dependentTasks.has(name)) { + return; + } + dependentTasks.add(name); + + const dependsOn: unknown = task?.dependsOn; + if (typeof dependsOn === 'string') { + getDependentTasks(dependsOn); + } else if (Array.isArray(dependsOn)) { + for (const dep of dependsOn) { + if (typeof dep === 'string') { + getDependentTasks(dep); + } + } + } + } + + if (!tasksMap.has(preLaunchTask)) { + return []; + } + + getDependentTasks(preLaunchTask); + return Array.from(dependentTasks.values()); +} diff --git a/src/debug/validatePreDebug.ts b/src/debug/validatePreDebug.ts index e5dce1551..457d6a7c2 100644 --- a/src/debug/validatePreDebug.ts +++ b/src/debug/validatePreDebug.ts @@ -18,6 +18,8 @@ import { durableUtils } from '../utils/durableUtils'; import { isPythonV2Plus } from '../utils/programmingModelUtils'; import { getDebugConfigs, isDebugConfigEqual } from '../vsCodeConfig/launch'; import { getWorkspaceSetting, tryGetFunctionsWorkerRuntimeForProject } from "../vsCodeConfig/settings"; +import { getTasks } from '../vsCodeConfig/tasks'; +import { getPreLaunchTaskChain } from './getPreLaunchTaskChain'; import { validateDTSConnectionPreDebug } from './storageProviders/validateDTSConnectionPreDebug'; import { validateNetheriteConnectionPreDebug } from './storageProviders/validateNetheriteConnectionPreDebug'; import { validateSQLConnectionPreDebug } from './storageProviders/validateSQLConnectionPreDebug'; @@ -32,12 +34,23 @@ export interface IPreDebugContext extends Omit { const context: IPreDebugContext = Object.assign(actionContext, { action: CodeAction.Debug }); const workspace: vscode.WorkspaceFolder = getMatchingWorkspace(debugConfig); let shouldContinue: boolean; context.telemetry.properties.debugType = debugConfig.type; + // If one of the `preLaunchTasks` already handles starting emulators, assume the user is already on top of automating this part of the setup + const preLaunchTaskName: string | undefined = debugConfig.preLaunchTask; + const preLaunchTaskChain: string[] = typeof preLaunchTaskName === 'string' ? getPreLaunchTaskChain(getTasks(workspace), preLaunchTaskName) : []; + const hasEmulatorTask: boolean = preLaunchTaskChain.some(label => emulatorTaskRegExp.test(label)); + const forceEmulatorValidation: boolean = !!getWorkspaceSetting('forceEmulatorValidation', workspace.uri.fsPath); + const skipEmulatorValidation: boolean = hasEmulatorTask && !forceEmulatorValidation; + context.telemetry.properties.hasEmulatorTask = String(hasEmulatorTask); + context.telemetry.properties.forceEmulatorValidation = String(forceEmulatorValidation); + try { context.telemetry.properties.lastValidateStep = 'funcInstalled'; const message: string = localize('installFuncTools', 'You must have the Azure Functions Core Tools installed to debug your local functions.'); @@ -62,28 +75,30 @@ export async function preDebugValidate(actionContext: IActionContext, debugConfi context.telemetry.properties.lastValidateStep = 'workerRuntime'; await validateWorkerRuntime(context, projectLanguage, context.projectPath); - switch (durableStorageType) { - case DurableBackend.DTS: - context.telemetry.properties.lastValidateStep = 'dtsConnection'; - await validateDTSConnectionPreDebug(context, context.projectPath); - break; - case DurableBackend.Netherite: - context.telemetry.properties.lastValidateStep = 'netheriteConnection'; - await validateNetheriteConnectionPreDebug(context, context.projectPath); - break; - case DurableBackend.SQL: - context.telemetry.properties.lastValidateStep = 'sqlDbConnection'; - await validateSQLConnectionPreDebug(context, context.projectPath); - break; - case DurableBackend.Storage: - default: + if (!skipEmulatorValidation) { + switch (durableStorageType) { + case DurableBackend.DTS: + context.telemetry.properties.lastValidateStep = 'dtsConnection'; + await validateDTSConnectionPreDebug(context, context.projectPath); + break; + case DurableBackend.Netherite: + context.telemetry.properties.lastValidateStep = 'netheriteConnection'; + await validateNetheriteConnectionPreDebug(context, context.projectPath); + break; + case DurableBackend.SQL: + context.telemetry.properties.lastValidateStep = 'sqlDbConnection'; + await validateSQLConnectionPreDebug(context, context.projectPath); + break; + case DurableBackend.Storage: + default: + } + + context.telemetry.properties.lastValidateStep = 'azureWebJobsStorage'; + await validateAzureWebJobsStorage(context, context.projectPath); + + context.telemetry.properties.lastValidateStep = 'emulatorRunning'; + shouldContinue = await validateEmulatorIsRunning(context, context.projectPath); } - - context.telemetry.properties.lastValidateStep = 'azureWebJobsStorage'; - await validateAzureWebJobsStorage(context, context.projectPath); - - context.telemetry.properties.lastValidateStep = 'emulatorRunning'; - shouldContinue = await validateEmulatorIsRunning(context, context.projectPath); } } } catch (error) { diff --git a/test/getPreLaunchTaskChain.test.ts b/test/getPreLaunchTaskChain.test.ts new file mode 100644 index 000000000..a32f9d962 --- /dev/null +++ b/test/getPreLaunchTaskChain.test.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { getPreLaunchTaskChain } from '../src/debug/getPreLaunchTaskChain'; +import type { ITask } from '../src/vsCodeConfig/tasks'; + +suite('getPreLaunchTaskChain', () => { + test('returns only the preLaunchTask when it has no dependencies', () => { + const tasks: ITask[] = [ + { type: 'shell', label: 'build', command: 'npm run build' } + ]; + const result = getPreLaunchTaskChain(tasks, 'build'); + assert.deepStrictEqual(result, ['build']); + }); + + test('returns empty array when preLaunchTask is not found in tasks list', () => { + const result = getPreLaunchTaskChain([], 'nonexistent'); + assert.deepStrictEqual(result, []); + }); + + test('resolves a single string dependsOn', () => { + const tasks: ITask[] = [ + { type: 'shell', label: 'build', command: 'npm run build', dependsOn: 'compile' }, + { type: 'shell', label: 'compile', command: 'tsc' } + ]; + const result = getPreLaunchTaskChain(tasks, 'build'); + assert.deepStrictEqual(result, ['build', 'compile']); + }); + + test('resolves an array dependsOn', () => { + const tasks: ITask[] = [ + { type: 'shell', label: 'build', command: 'npm run build', dependsOn: ['compile', 'lint'] }, + { type: 'shell', label: 'compile', command: 'tsc' }, + { type: 'shell', label: 'lint', command: 'eslint .' } + ]; + const result = getPreLaunchTaskChain(tasks, 'build'); + assert.deepStrictEqual(result, ['build', 'compile', 'lint']); + }); + + test('resolves chained dependencies', () => { + const tasks: ITask[] = [ + { type: 'shell', label: 'build', dependsOn: 'compile' }, + { type: 'shell', label: 'compile', dependsOn: 'clean' }, + { type: 'shell', label: 'clean', command: 'rm -rf dist' } + ]; + const result = getPreLaunchTaskChain(tasks, 'build'); + assert.deepStrictEqual(result, ['build', 'compile', 'clean']); + }); + + test('handles circular dependencies without infinite loop', () => { + const tasks: ITask[] = [ + { type: 'shell', label: 'a', dependsOn: 'b' }, + { type: 'shell', label: 'b', dependsOn: 'a' } + ]; + const result = getPreLaunchTaskChain(tasks, 'a'); + assert.deepStrictEqual(result, ['a', 'b']); + }); + + test('excludes dependency names when they are not defined tasks', () => { + const tasks: ITask[] = [ + { type: 'shell', label: 'build', dependsOn: 'unknown-task' } + ]; + const result = getPreLaunchTaskChain(tasks, 'build'); + assert.deepStrictEqual(result, ['build']); + }); + + test('skips non-string values in dependsOn array', () => { + const tasks: ITask[] = [ + { type: 'shell', label: 'build', dependsOn: ['compile', 42, null, 'lint'] } as unknown as ITask, + { type: 'shell', label: 'compile', command: 'tsc' }, + { type: 'shell', label: 'lint', command: 'eslint .' } + ]; + const result = getPreLaunchTaskChain(tasks, 'build'); + assert.deepStrictEqual(result, ['build', 'compile', 'lint']); + }); + + test('skips tasks without labels when building task map', () => { + const tasks: ITask[] = [ + { type: 'shell', label: 'build', dependsOn: 'compile' }, + { type: 'shell', command: 'tsc' } // no label - should not be in the task map + ]; + const result = getPreLaunchTaskChain(tasks, 'build'); + assert.deepStrictEqual(result, ['build']); + }); + + test('returns empty array for empty string preLaunchTask', () => { + const result = getPreLaunchTaskChain([], ''); + assert.deepStrictEqual(result, []); + }); +});