From 22c77f02e05497d839f165ff620143e84c5848be Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:46:39 -0700 Subject: [PATCH 01/13] first draft --- .../commands/reportIssueCommand.ts | 5 +- src/extension/common/python.ts | 184 ++- src/extension/common/settings.ts | 2 +- src/extension/debugger/adapter/factory.ts | 31 +- src/extension/envExtApi.ts | 1289 +++++++++++++++++ .../unittest/adapter/factory.unit.test.ts | 686 ++++----- 6 files changed, 1763 insertions(+), 434 deletions(-) create mode 100644 src/extension/envExtApi.ts diff --git a/src/extension/common/application/commands/reportIssueCommand.ts b/src/extension/common/application/commands/reportIssueCommand.ts index 104b5225..33ccd38a 100644 --- a/src/extension/common/application/commands/reportIssueCommand.ts +++ b/src/extension/common/application/commands/reportIssueCommand.ts @@ -8,7 +8,6 @@ import * as path from 'path'; import { executeCommand } from '../../vscodeapi'; import { getActiveEnvironmentPath, resolveEnvironment } from '../../python'; import { EXTENSION_ROOT_DIR } from '../../constants'; -import { getRawVersion } from '../../settings'; import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; @@ -22,9 +21,9 @@ export async function openReportIssue(): Promise { const userTemplate = await fs.readFile(userDataTemplatePath, 'utf8'); const interpreterPath = await getActiveEnvironmentPath(); const interpreter = await resolveEnvironment(interpreterPath); - const virtualEnvKind = interpreter?.environment?.type || 'Unknown'; + const virtualEnvKind = interpreter?.envId.managerId ?? 'N/A'; - const pythonVersion = getRawVersion(interpreter?.version); + const pythonVersion = interpreter?.version ?? 'unknown'; await executeCommand('workbench.action.openIssueReporter', { extensionId: 'ms-python.debugpy', issueBody: template, diff --git a/src/extension/common/python.ts b/src/extension/common/python.ts index 4ec05b86..9735a6bd 100644 --- a/src/extension/common/python.ts +++ b/src/extension/common/python.ts @@ -11,19 +11,16 @@ import { ResolvedEnvironment, Resource, } from '@vscode/python-extension'; -import { commands, EventEmitter, extensions, Uri, Event, Disposable, Extension } from 'vscode'; -import { createDeferred } from './utils/async'; +import { commands, EventEmitter, extensions, Uri, Event, Disposable } from 'vscode'; import { traceError, traceLog } from './log/logging'; +import { PythonEnvironment, PythonEnvironmentApi, PythonEnvsExtension } from '../envExtApi'; -/** - * Interface for the Python extension API. - */ -interface IExtensionApi { - ready: Promise; - settings: { - getExecutionDetails(resource?: Resource): { execCommand: string[] | undefined }; - }; -} +// interface IExtensionApi { +// ready: Promise; +// settings: { +// getExecutionDetails(resource?: Resource): { execCommand: string[] | undefined }; +// }; +// } /** * Details about a Python interpreter. @@ -70,20 +67,18 @@ async function activateEnvsExtension(): Promise | undefined> { return extension; } -/** - * Gets the Python extension's API interface. - * @returns The Python extension API or undefined if not available - */ -async function getPythonExtensionAPI(): Promise { - const extension = await activateExtension(); - return extension?.exports as IExtensionApi; +async function getPythonEnviromentExtensionAPI(): Promise { + // Load the Python extension API + await activateEnvsExtension(); + return await PythonEnvsExtension.api(); } -/** - * Gets the Python extension's environment API. - * @returns The Python extension environment API - */ -async function getPythonExtensionEnviromentAPI(): Promise { +// async function getLegacyPythonExtensionAPI(): Promise { +// const extension = await activateExtension(); +// return extension?.exports as IExtensionApi; +// } + +async function getLegacyPythonExtensionEnviromentAPI(): Promise { // Load the Python extension API await activateExtension(); return await PythonExtension.api(); @@ -95,7 +90,7 @@ async function getPythonExtensionEnviromentAPI(): Promise { */ export async function initializePython(disposables: Disposable[]): Promise { try { - const api = await getPythonExtensionEnviromentAPI(); + const api = await getLegacyPythonExtensionEnviromentAPI(); if (api) { disposables.push( @@ -140,51 +135,80 @@ export async function runPythonExtensionCommand(command: string, ...rest: any[]) * @returns Array of command components or undefined if not available */ export async function getSettingsPythonPath(resource?: Uri): Promise { - const api = await getPythonExtensionAPI(); - return api?.settings.getExecutionDetails(resource).execCommand; + // const api = await getLegacyPythonExtensionAPI(); + // return api?.settings.getExecutionDetails(resource).execCommand; + + const apiNew = await getPythonEnviromentExtensionAPI(); + const abc: PythonEnvironment[] = await apiNew.getEnvironments(resource || 'all'); + console.log('Python envs:', abc); + return undefined; } -/** - * Returns the environment variables used by the extension for a resource, which includes the custom - * variables configured by user in `.env` files. - * @param resource Optional workspace resource to get environment variables for - * @returns Environment variables object - */ -export async function getEnvironmentVariables(resource?: Resource): Promise { - const api = await getPythonExtensionEnviromentAPI(); - return Promise.resolve(api.environments.getEnvironmentVariables(resource)); +export async function getEnvironmentVariables(resource?: Resource) { + const api = await getLegacyPythonExtensionEnviromentAPI(); + return api.environments.getEnvironmentVariables(resource); } -/** - * Returns details for the given environment, or `undefined` if the env is invalid. - * @param env Environment to resolve (can be Environment object, path, or string) - * @returns Resolved environment details - */ export async function resolveEnvironment( env: Environment | EnvironmentPath | string, +): Promise { + // const api = await getLegacyPythonExtensionEnviromentAPI(); + // return api.environments.resolveEnvironment(env); + + const apiNew = await getPythonEnviromentExtensionAPI(); + + // Handle different input types for the new API + if (typeof env === 'string') { + // Convert string path to Uri for the new API + return apiNew.resolveEnvironment(Uri.file(env)); + } else if (typeof env === 'object' && 'path' in env) { + // EnvironmentPath has a uri property + return apiNew.resolveEnvironment(Uri.file(env.path)); + } else { + return undefined; + } +} + +export async function legacyResolveEnvironment( + env: Environment | EnvironmentPath | string, ): Promise { - const api = await getPythonExtensionEnviromentAPI(); + const api = await getLegacyPythonExtensionEnviromentAPI(); return api.environments.resolveEnvironment(env); } -/** - * Returns the environment configured by user in settings. Note that this can be an invalid environment, use - * resolve the environment to get full details. - * @param resource Optional workspace resource to get active environment for - * @returns Path to the active environment - */ -export async function getActiveEnvironmentPath(resource?: Resource): Promise { - const api = await getPythonExtensionEnviromentAPI(); +export async function getLegacyActiveEnvironmentPath(resource?: Resource) { + const api = await getLegacyPythonExtensionEnviromentAPI(); return api.environments.getActiveEnvironmentPath(resource); } +export async function getActiveEnvironmentPath(resource?: Resource): Promise { + const api = await getPythonEnviromentExtensionAPI(); + + // Convert Resource to Uri if it exists + let resourceUri: Uri | undefined; + if (resource instanceof Uri) { + resourceUri = resource; + } else if (resource && 'uri' in resource) { + // WorkspaceFolder type + resourceUri = resource.uri; + } + + return api.getEnvironment(resourceUri); +} + /** - * Gets detailed information about the active Python interpreter. - * @param resource Optional workspace resource to get interpreter details for - * @returns Interpreter details including path and resource information + * Gets Python interpreter details using the legacy Python extension API. + * + * This function retrieves the active Python environment for a given resource using the + * legacy @vscode/python-extension API. It resolves the environment to get the executable + * path and handles path quoting for paths containing spaces. + * + * @param resource Optional URI to specify the workspace/folder context for interpreter selection + * @returns Promise resolving to interpreter details containing the executable path and resource */ -export async function getInterpreterDetails(resource?: Uri): Promise { - const api = await getPythonExtensionEnviromentAPI(); +export async function getLegacyInterpreterDetails(resource?: Uri): Promise { + const api = await getLegacyPythonExtensionEnviromentAPI(); + const environment = await api.environments.resolveEnvironment(api.environments.getActiveEnvironmentPath(resource)); if (environment?.executable.uri) { return { path: [environment?.executable.uri.fsPath], resource }; @@ -192,34 +216,34 @@ export async function getInterpreterDetails(resource?: Uri): Promise { - const api = await getPythonExtensionEnviromentAPI(); - const onAddedToCollection = createDeferred(); - api.environments.onDidChangeEnvironments(async () => { - if (api.environments.known) { - onAddedToCollection.resolve(); - } - }); - const initialEnvs = api.environments.known; - if (initialEnvs.length > 0) { - return true; +export function quoteStringIfNecessary(arg: string): string { + // Always return if already quoted to avoid double-quoting + if (arg.startsWith('"') && arg.endsWith('"')) { + return arg; } - // Initiates a refresh of Python environments within the specified scope. - await Promise.race([onAddedToCollection.promise, api?.environments.refreshEnvironments()]); - return api.environments.known.length > 0; + // Quote if contains common shell special characters that are problematic across multiple shells + // Includes: space, &, |, <, >, ;, ', ", `, (, ), [, ], {, }, $ + const needsQuoting = /[\s&|<>;'"`()\[\]{}$]/.test(arg); + + return needsQuoting ? `"${arg}"` : arg; } -/** - * Gets environments known to the extension at the time of fetching the property. Note this may not - * contain all environments in the system as a refresh might be going on. - * @returns Array of known Python environments - */ -export async function getInterpreters(): Promise { - const api = await getPythonExtensionEnviromentAPI(); - return api.environments.known || []; +export async function getInterpreterDetails(resource?: Uri): Promise { + const api = await getPythonEnviromentExtensionAPI(); + + // A promise that resolves to the current Python environment, or undefined if none is set. + const env: PythonEnvironment | undefined = await api.getEnvironment(resource); + // resolve the environment to get full details + const resolvedEnv = env ? await api.resolveEnvironment(env?.environmentPath) : undefined; + const executablePath = resolvedEnv?.execInfo.activatedRun?.executable + ? resolvedEnv.execInfo.activatedRun.executable + : resolvedEnv?.execInfo.run.executable; + + // Quote the executable path if necessary + const a: IInterpreterDetails = { + path: executablePath ? [quoteStringIfNecessary(executablePath)] : undefined, + resource, + }; + return a; } diff --git a/src/extension/common/settings.ts b/src/extension/common/settings.ts index 9239a4f8..d4db2567 100644 --- a/src/extension/common/settings.ts +++ b/src/extension/common/settings.ts @@ -2,10 +2,10 @@ // Licensed under the MIT License. import { ConfigurationChangeEvent, ConfigurationTarget, Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; -import { getInterpreterDetails } from './python'; import { getConfiguration, getWorkspaceFolder, getWorkspaceFolders } from './vscodeapi'; import { isUnitTestExecution } from './constants'; import { VersionInfo } from '@vscode/python-extension'; +import { getInterpreterDetails } from './python'; export interface ISettings { workspace: string; diff --git a/src/extension/debugger/adapter/factory.ts b/src/extension/debugger/adapter/factory.ts index aa83b3bd..7f07d41a 100644 --- a/src/extension/debugger/adapter/factory.ts +++ b/src/extension/debugger/adapter/factory.ts @@ -23,8 +23,8 @@ import { getInterpreterDetails, resolveEnvironment, runPythonExtensionCommand } import { Commands, EXTENSION_ROOT_DIR } from '../../common/constants'; import { Common, DebugConfigStrings, Interpreters } from '../../common/utils/localize'; import { IPersistentStateFactory } from '../../common/types'; -import { ResolvedEnvironment } from '@vscode/python-extension'; import { fileToCommandArgumentForPythonExt } from '../../common/stringUtils'; +import { PythonEnvironment } from '../../envExtApi'; // persistent state names, exported to make use of in testing export enum debugStateKeys { @@ -177,15 +177,32 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac } } - private async getExecutableCommand(interpreter: ResolvedEnvironment | undefined): Promise { + /** + * Extracts the executable command from a resolved Python environment. + * + * This function takes a resolved Python environment and returns the path to the Python + * executable as a string array suitable for spawning processes. It also performs version + * validation, showing a deprecation warning if the Python version is below 3.9. + * + * @param interpreter The resolved Python environment containing executable path and version info + * @returns Promise resolving to an array containing the Python executable path, or empty array if no interpreter + */ + private async getExecutableCommand(interpreter: PythonEnvironment | undefined): Promise { if (interpreter) { - if ( - (interpreter.version?.major ?? 0) < 3 || - ((interpreter.version?.major ?? 0) <= 3 && (interpreter.version?.minor ?? 0) < 9) - ) { + const executablePath = interpreter.execInfo.activatedRun?.executable ?? interpreter.execInfo.run.executable; + const version = interpreter.version; + + // Parse version string (e.g., "3.8.10" -> major: 3, minor: 8) + const parseMajorMinor = (v: string) => { + const m = v.match(/^(\d+)(?:\.(\d+))?/); + return { major: m ? Number(m[1]) : 0, minor: m && m[2] ? Number(m[2]) : 0 }; + }; + const { major, minor } = parseMajorMinor(version || ''); + + if (major < 3 || (major <= 3 && minor < 9)) { this.showDeprecatedPythonMessage(); } - return interpreter.path.length > 0 ? [interpreter.path] : []; + return executablePath ? [executablePath] : []; } return []; } diff --git a/src/extension/envExtApi.ts b/src/extension/envExtApi.ts new file mode 100644 index 00000000..afd83e55 --- /dev/null +++ b/src/extension/envExtApi.ts @@ -0,0 +1,1289 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Disposable, + Event, + extensions, + FileChangeType, + LogOutputChannel, + MarkdownString, + TaskExecution, + Terminal, + TerminalOptions, + ThemeIcon, + Uri, +} from 'vscode'; + +/** + * The path to an icon, or a theme-specific configuration of icons. + */ +export type IconPath = + | Uri + | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + } + | ThemeIcon; + +/** + * Options for executing a Python executable. + */ +export interface PythonCommandRunConfiguration { + /** + * Path to the binary like `python.exe` or `python3` to execute. This should be an absolute path + * to an executable that can be spawned. + */ + executable: string; + + /** + * Arguments to pass to the python executable. These arguments will be passed on all execute calls. + * This is intended for cases where you might want to do interpreter specific flags. + */ + args?: string[]; +} + +/** + * Contains details on how to use a particular python environment + * + * Running In Terminal: + * 1. If {@link PythonEnvironmentExecutionInfo.activatedRun} is provided, then that will be used. + * 2. If {@link PythonEnvironmentExecutionInfo.activatedRun} is not provided, then: + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then: + * - 'unknown' will be used if provided. + * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then {@link PythonEnvironmentExecutionInfo.activation} will be used. + * - If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. + * + * Creating a Terminal: + * 1. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. + * 2. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then {@link PythonEnvironmentExecutionInfo.activation} will be used. + * 3. If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then: + * - 'unknown' will be used if provided. + * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. + * 4. If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. + * + */ +export interface PythonEnvironmentExecutionInfo { + /** + * Details on how to run the python executable. + */ + run: PythonCommandRunConfiguration; + + /** + * Details on how to run the python executable after activating the environment. + * If set this will overrides the {@link PythonEnvironmentExecutionInfo.run} command. + */ + activatedRun?: PythonCommandRunConfiguration; + + /** + * Details on how to activate an environment. + */ + activation?: PythonCommandRunConfiguration[]; + + /** + * Details on how to activate an environment using a shell specific command. + * If set this will override the {@link PythonEnvironmentExecutionInfo.activation}. + * 'unknown' is used if shell type is not known. + * If 'unknown' is not provided and shell type is not known then + * {@link PythonEnvironmentExecutionInfo.activation} if set. + */ + shellActivation?: Map; + + /** + * Details on how to deactivate an environment. + */ + deactivation?: PythonCommandRunConfiguration[]; + + /** + * Details on how to deactivate an environment using a shell specific command. + * If set this will override the {@link PythonEnvironmentExecutionInfo.deactivation} property. + * 'unknown' is used if shell type is not known. + * If 'unknown' is not provided and shell type is not known then + * {@link PythonEnvironmentExecutionInfo.deactivation} if set. + */ + shellDeactivation?: Map; +} + +/** + * Interface representing the ID of a Python environment. + */ +export interface PythonEnvironmentId { + /** + * The unique identifier of the Python environment. + */ + id: string; + + /** + * The ID of the manager responsible for the Python environment. + */ + managerId: string; +} + +/** + * Display information for an environment group. + */ +export interface EnvironmentGroupInfo { + /** + * The name of the environment group. This is used as an identifier for the group. + * + * Note: The first instance of the group with the given name will be used in the UI. + */ + readonly name: string; + + /** + * The description of the environment group. + */ + readonly description?: string; + + /** + * The tooltip for the environment group, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the environment group, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; +} + +/** + * Interface representing information about a Python environment. + */ +export interface PythonEnvironmentInfo { + /** + * The name of the Python environment. + */ + readonly name: string; + + /** + * The display name of the Python environment. + */ + readonly displayName: string; + + /** + * The short display name of the Python environment. + */ + readonly shortDisplayName?: string; + + /** + * The display path of the Python environment. + */ + readonly displayPath: string; + + /** + * The version of the Python environment. + */ + readonly version: string; + + /** + * Path to the python binary or environment folder. + */ + readonly environmentPath: Uri; + + /** + * The description of the Python environment. + */ + readonly description?: string; + + /** + * The tooltip for the Python environment, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the Python environment, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * Information on how to execute the Python environment. This is required for executing Python code in the environment. + */ + readonly execInfo: PythonEnvironmentExecutionInfo; + + /** + * `sys.prefix` is the path to the base directory of the Python installation. Typically obtained by executing `sys.prefix` in the Python interpreter. + * This is required by extension like Jupyter, Pylance, and other extensions to provide better experience with python. + */ + readonly sysPrefix: string; + + /** + * Optional `group` for this environment. This is used to group environments in the Environment Manager UI. + */ + readonly group?: string | EnvironmentGroupInfo; +} + +/** + * Interface representing a Python environment. + */ +export interface PythonEnvironment extends PythonEnvironmentInfo { + /** + * The ID of the Python environment. + */ + readonly envId: PythonEnvironmentId; +} + +/** + * Type representing the scope for setting a Python environment. + * Can be undefined or a URI. + */ +export type SetEnvironmentScope = undefined | Uri | Uri[]; + +/** + * Type representing the scope for getting a Python environment. + * Can be undefined or a URI. + */ +export type GetEnvironmentScope = undefined | Uri; + +/** + * Type representing the scope for creating a Python environment. + * Can be a Python project or 'global'. + */ +export type CreateEnvironmentScope = Uri | Uri[] | 'global'; +/** + * The scope for which environments are to be refreshed. + * - `undefined`: Search for environments globally and workspaces. + * - {@link Uri}: Environments in the workspace/folder or associated with the Uri. + */ +export type RefreshEnvironmentsScope = Uri | undefined; + +/** + * The scope for which environments are required. + * - `"all"`: All environments. + * - `"global"`: Python installations that are usually a base for creating virtual environments. + * - {@link Uri}: Environments for the workspace/folder/file pointed to by the Uri. + */ +export type GetEnvironmentsScope = Uri | 'all' | 'global'; + +/** + * Event arguments for when the current Python environment changes. + */ +export type DidChangeEnvironmentEventArgs = { + /** + * The URI of the environment that changed. + */ + readonly uri: Uri | undefined; + + /** + * The old Python environment before the change. + */ + readonly old: PythonEnvironment | undefined; + + /** + * The new Python environment after the change. + */ + readonly new: PythonEnvironment | undefined; +}; + +/** + * Enum representing the kinds of environment changes. + */ +export enum EnvironmentChangeKind { + /** + * Indicates that an environment was added. + */ + add = 'add', + + /** + * Indicates that an environment was removed. + */ + remove = 'remove', +} + +/** + * Event arguments for when the list of Python environments changes. + */ +export type DidChangeEnvironmentsEventArgs = { + /** + * The kind of change that occurred (add or remove). + */ + kind: EnvironmentChangeKind; + + /** + * The Python environment that was added or removed. + */ + environment: PythonEnvironment; +}[]; + +/** + * Type representing the context for resolving a Python environment. + */ +export type ResolveEnvironmentContext = Uri; + +export interface QuickCreateConfig { + /** + * The description of the quick create step. + */ + readonly description: string; + + /** + * The detail of the quick create step. + */ + readonly detail?: string; +} + +/** + * Interface representing an environment manager. + */ +export interface EnvironmentManager { + /** + * The name of the environment manager. Allowed characters (a-z, A-Z, 0-9, -, _). + */ + readonly name: string; + + /** + * The display name of the environment manager. + */ + readonly displayName?: string; + + /** + * The preferred package manager ID for the environment manager. This is a combination + * of publisher id, extension id, and {@link EnvironmentManager.name package manager name}. + * `.:` + * + * @example + * 'ms-python.python:pip' + */ + readonly preferredPackageManagerId: string; + + /** + * The description of the environment manager. + */ + readonly description?: string; + + /** + * The tooltip for the environment manager, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the environment manager, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * The log output channel for the environment manager. + */ + readonly log?: LogOutputChannel; + + /** + * The quick create details for the environment manager. Having this method also enables the quick create feature + * for the environment manager. Should Implement {@link EnvironmentManager.create} to support quick create. + */ + quickCreateConfig?(): QuickCreateConfig | undefined; + + /** + * Creates a new Python environment within the specified scope. Create should support adding a .gitignore file if it creates a folder within the workspace. If a manager does not support environment creation, do not implement this method; the UI disables "create" options when `this.manager.create === undefined`. + * @param scope - The scope within which to create the environment. + * @param options - Optional parameters for creating the Python environment. + * @returns A promise that resolves to the created Python environment, or undefined if creation failed. + */ + create?(scope: CreateEnvironmentScope, options?: CreateEnvironmentOptions): Promise; + + /** + * Removes the specified Python environment. + * @param environment - The Python environment to remove. + * @returns A promise that resolves when the environment is removed. + */ + remove?(environment: PythonEnvironment): Promise; + + /** + * Refreshes the list of Python environments within the specified scope. + * @param scope - The scope within which to refresh environments. + * @returns A promise that resolves when the refresh is complete. + */ + refresh(scope: RefreshEnvironmentsScope): Promise; + + /** + * Retrieves a list of Python environments within the specified scope. + * @param scope - The scope within which to retrieve environments. + * @returns A promise that resolves to an array of Python environments. + */ + getEnvironments(scope: GetEnvironmentsScope): Promise; + + /** + * Event that is fired when the list of Python environments changes. + */ + onDidChangeEnvironments?: Event; + + /** + * Sets the current Python environment within the specified scope. + * @param scope - The scope within which to set the environment. + * @param environment - The Python environment to set. If undefined, the environment is unset. + * @returns A promise that resolves when the environment is set. + */ + set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise; + + /** + * Retrieves the current Python environment within the specified scope. + * @param scope - The scope within which to retrieve the environment. + * @returns A promise that resolves to the current Python environment, or undefined if none is set. + */ + get(scope: GetEnvironmentScope): Promise; + + /** + * Event that is fired when the current Python environment changes. + */ + onDidChangeEnvironment?: Event; + + /** + * Resolves the specified Python environment. The environment can be either a {@link PythonEnvironment} or a {@link Uri} context. + * + * This method is used to obtain a fully detailed {@link PythonEnvironment} object. The input can be: + * - A {@link PythonEnvironment} object, which might be missing key details such as {@link PythonEnvironment.execInfo}. + * - A {@link Uri} object, which typically represents either: + * - A folder that contains the Python environment. + * - The path to a Python executable. + * + * @param context - The context for resolving the environment, which can be a {@link PythonEnvironment} or a {@link Uri}. + * @returns A promise that resolves to the fully detailed {@link PythonEnvironment}, or `undefined` if the environment cannot be resolved. + */ + resolve(context: ResolveEnvironmentContext): Promise; + + /** + * Clears the environment manager's cache. + * + * @returns A promise that resolves when the cache is cleared. + */ + clearCache?(): Promise; +} + +/** + * Interface representing a package ID. + */ +export interface PackageId { + /** + * The ID of the package. + */ + id: string; + + /** + * The ID of the package manager. + */ + managerId: string; + + /** + * The ID of the environment in which the package is installed. + */ + environmentId: string; +} + +/** + * Interface representing package information. + */ +export interface PackageInfo { + /** + * The name of the package. + */ + readonly name: string; + + /** + * The display name of the package. + */ + readonly displayName: string; + + /** + * The version of the package. + */ + readonly version?: string; + + /** + * The description of the package. + */ + readonly description?: string; + + /** + * The tooltip for the package, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the package, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * The URIs associated with the package. + */ + readonly uris?: readonly Uri[]; +} + +/** + * Interface representing a package. + */ +export interface Package extends PackageInfo { + /** + * The ID of the package. + */ + readonly pkgId: PackageId; +} + +/** + * Enum representing the kinds of package changes. + */ +export enum PackageChangeKind { + /** + * Indicates that a package was added. + */ + add = 'add', + + /** + * Indicates that a package was removed. + */ + remove = 'remove', +} + +/** + * Event arguments for when packages change. + */ +export interface DidChangePackagesEventArgs { + /** + * The Python environment in which the packages changed. + */ + environment: PythonEnvironment; + + /** + * The package manager responsible for the changes. + */ + manager: PackageManager; + + /** + * The list of changes, each containing the kind of change and the package affected. + */ + changes: { kind: PackageChangeKind; pkg: Package }[]; +} + +/** + * Interface representing a package manager. + */ +export interface PackageManager { + /** + * The name of the package manager. Allowed characters (a-z, A-Z, 0-9, -, _). + */ + name: string; + + /** + * The display name of the package manager. + */ + displayName?: string; + + /** + * The description of the package manager. + */ + description?: string; + + /** + * The tooltip for the package manager, which can be a string or a Markdown string. + */ + tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the package manager, which can be a string, Uri, or an object with light and dark theme paths. + */ + iconPath?: IconPath; + + /** + * The log output channel for the package manager. + */ + log?: LogOutputChannel; + + /** + * Installs/Uninstall packages in the specified Python environment. + * @param environment - The Python environment in which to install packages. + * @param options - Options for managing packages. + * @returns A promise that resolves when the installation is complete. + */ + manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise; + + /** + * Refreshes the package list for the specified Python environment. + * @param environment - The Python environment for which to refresh the package list. + * @returns A promise that resolves when the refresh is complete. + */ + refresh(environment: PythonEnvironment): Promise; + + /** + * Retrieves the list of packages for the specified Python environment. + * @param environment - The Python environment for which to retrieve packages. + * @returns An array of packages, or undefined if the packages could not be retrieved. + */ + getPackages(environment: PythonEnvironment): Promise; + + /** + * Event that is fired when packages change. + */ + onDidChangePackages?: Event; + + /** + * Clears the package manager's cache. + * @returns A promise that resolves when the cache is cleared. + */ + clearCache?(): Promise; +} + +/** + * Interface representing a Python project. + */ +export interface PythonProject { + /** + * The name of the Python project. + */ + readonly name: string; + + /** + * The URI of the Python project. + */ + readonly uri: Uri; + + /** + * The description of the Python project. + */ + readonly description?: string; + + /** + * The tooltip for the Python project, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; +} + +/** + * Options for creating a Python project. + */ +export interface PythonProjectCreatorOptions { + /** + * The name of the Python project. + */ + name: string; + + /** + * Path provided as the root for the project. + */ + rootUri: Uri; + + /** + * Boolean indicating whether the project should be created without any user input. + */ + quickCreate?: boolean; +} + +/** + * Interface representing a creator for Python projects. + */ +export interface PythonProjectCreator { + /** + * The name of the Python project creator. + */ + readonly name: string; + + /** + * The display name of the Python project creator. + */ + readonly displayName?: string; + + /** + * The description of the Python project creator. + */ + readonly description?: string; + + /** + * The tooltip for the Python project creator, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * Creates a new Python project(s) or, if files are not a project, returns Uri(s) to the created files. + * Anything that needs its own python environment constitutes a project. + * @param options Optional parameters for creating the Python project. + * @returns A promise that resolves to one of the following: + * - PythonProject or PythonProject[]: when a single or multiple projects are created. + * - Uri or Uri[]: when files are created that do not constitute a project. + * - undefined: if project creation fails. + */ + create(options?: PythonProjectCreatorOptions): Promise; + + /** + * A flag indicating whether the project creator supports quick create where no user input is required. + */ + readonly supportsQuickCreate?: boolean; +} + +/** + * Event arguments for when Python projects change. + */ +export interface DidChangePythonProjectsEventArgs { + /** + * The list of Python projects that were added. + */ + added: PythonProject[]; + + /** + * The list of Python projects that were removed. + */ + removed: PythonProject[]; +} + +export type PackageManagementOptions = + | { + /** + * Upgrade the packages if they are already installed. + */ + upgrade?: boolean; + + /** + * Show option to skip package installation or uninstallation. + */ + showSkipOption?: boolean; + /** + * The list of packages to install. + */ + install: string[]; + + /** + * The list of packages to uninstall. + */ + uninstall?: string[]; + } + | { + /** + * Upgrade the packages if they are already installed. + */ + upgrade?: boolean; + + /** + * Show option to skip package installation or uninstallation. + */ + showSkipOption?: boolean; + /** + * The list of packages to install. + */ + install?: string[]; + + /** + * The list of packages to uninstall. + */ + uninstall: string[]; + }; + +/** + * Options for creating a Python environment. + */ +export interface CreateEnvironmentOptions { + /** + * Provides some context about quick create based on user input. + * - if true, the environment should be created without any user input or prompts. + * - if false, the environment creation can show user input or prompts. + * This also means user explicitly skipped the quick create option. + * - if undefined, the environment creation can show user input or prompts. + * You can show quick create option to the user if you support it. + */ + quickCreate?: boolean; + /** + * Packages to install in addition to the automatically picked packages as a part of creating environment. + */ + additionalPackages?: string[]; +} + +/** + * Object representing the process started using run in background API. + */ +export interface PythonProcess { + /** + * The process ID of the Python process. + */ + readonly pid?: number; + + /** + * The standard input of the Python process. + */ + readonly stdin: NodeJS.WritableStream; + + /** + * The standard output of the Python process. + */ + readonly stdout: NodeJS.ReadableStream; + + /** + * The standard error of the Python process. + */ + readonly stderr: NodeJS.ReadableStream; + + /** + * Kills the Python process. + */ + kill(): void; + + /** + * Event that is fired when the Python process exits. + */ + onExit(listener: (code: number | null, signal: NodeJS.Signals | null) => void): void; +} + +export interface PythonEnvironmentManagerRegistrationApi { + /** + * Register an environment manager implementation. + * + * @param manager Environment Manager implementation to register. + * @returns A disposable that can be used to unregister the environment manager. + * @see {@link EnvironmentManager} + */ + registerEnvironmentManager(manager: EnvironmentManager): Disposable; +} + +export interface PythonEnvironmentItemApi { + /** + * Create a Python environment item from the provided environment info. This item is used to interact + * with the environment. + * + * @param info Some details about the environment like name, version, etc. needed to interact with the environment. + * @param manager The environment manager to associate with the environment. + * @returns The Python environment. + */ + createPythonEnvironmentItem(info: PythonEnvironmentInfo, manager: EnvironmentManager): PythonEnvironment; +} + +export interface PythonEnvironmentManagementApi { + /** + * Create a Python environment using environment manager associated with the scope. + * + * @param scope Where the environment is to be created. + * @param options Optional parameters for creating the Python environment. + * @returns The Python environment created. `undefined` if not created. + */ + createEnvironment( + scope: CreateEnvironmentScope, + options?: CreateEnvironmentOptions, + ): Promise; + + /** + * Remove a Python environment. + * + * @param environment The Python environment to remove. + * @returns A promise that resolves when the environment has been removed. + */ + removeEnvironment(environment: PythonEnvironment): Promise; +} + +export interface PythonEnvironmentsApi { + /** + * Initiates a refresh of Python environments within the specified scope. + * @param scope - The scope within which to search for environments. + * @returns A promise that resolves when the search is complete. + */ + refreshEnvironments(scope: RefreshEnvironmentsScope): Promise; + + /** + * Retrieves a list of Python environments within the specified scope. + * @param scope - The scope within which to retrieve environments. + * @returns A promise that resolves to an array of Python environments. + */ + getEnvironments(scope: GetEnvironmentsScope): Promise; + + /** + * Event that is fired when the list of Python environments changes. + * @see {@link DidChangeEnvironmentsEventArgs} + */ + onDidChangeEnvironments: Event; + + /** + * This method is used to get the details missing from a PythonEnvironment. Like + * {@link PythonEnvironment.execInfo} and other details. + * + * @param context : The PythonEnvironment or Uri for which details are required. + */ + resolveEnvironment(context: ResolveEnvironmentContext): Promise; +} + +export interface PythonProjectEnvironmentApi { + /** + * Sets the current Python environment within the specified scope. + * @param scope - The scope within which to set the environment. + * @param environment - The Python environment to set. If undefined, the environment is unset. + */ + setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise; + + /** + * Retrieves the current Python environment within the specified scope. + * @param scope - The scope within which to retrieve the environment. + * @returns A promise that resolves to the current Python environment, or undefined if none is set. + */ + getEnvironment(scope: GetEnvironmentScope): Promise; + + /** + * Event that is fired when the selected Python environment changes for Project, Folder or File. + * @see {@link DidChangeEnvironmentEventArgs} + */ + onDidChangeEnvironment: Event; +} + +export interface PythonEnvironmentManagerApi + extends PythonEnvironmentManagerRegistrationApi, + PythonEnvironmentItemApi, + PythonEnvironmentManagementApi, + PythonEnvironmentsApi, + PythonProjectEnvironmentApi {} + +export interface PythonPackageManagerRegistrationApi { + /** + * Register a package manager implementation. + * + * @param manager Package Manager implementation to register. + * @returns A disposable that can be used to unregister the package manager. + * @see {@link PackageManager} + */ + registerPackageManager(manager: PackageManager): Disposable; +} + +export interface PythonPackageGetterApi { + /** + * Refresh the list of packages in a Python Environment. + * + * @param environment The Python Environment for which the list of packages is to be refreshed. + * @returns A promise that resolves when the list of packages has been refreshed. + */ + refreshPackages(environment: PythonEnvironment): Promise; + + /** + * Get the list of packages in a Python Environment. + * + * @param environment The Python Environment for which the list of packages is required. + * @returns The list of packages in the Python Environment. + */ + getPackages(environment: PythonEnvironment): Promise; + + /** + * Event raised when the list of packages in a Python Environment changes. + * @see {@link DidChangePackagesEventArgs} + */ + onDidChangePackages: Event; +} + +export interface PythonPackageItemApi { + /** + * Create a package item from the provided package info. + * + * @param info The package info. + * @param environment The Python Environment in which the package is installed. + * @param manager The package manager that installed the package. + * @returns The package item. + */ + createPackageItem(info: PackageInfo, environment: PythonEnvironment, manager: PackageManager): Package; +} + +export interface PythonPackageManagementApi { + /** + * Install/Uninstall packages into a Python Environment. + * + * @param environment The Python Environment into which packages are to be installed. + * @param packages The packages to install. + * @param options Options for installing packages. + */ + managePackages(environment: PythonEnvironment, options: PackageManagementOptions): Promise; +} + +export interface PythonPackageManagerApi + extends PythonPackageManagerRegistrationApi, + PythonPackageGetterApi, + PythonPackageManagementApi, + PythonPackageItemApi {} + +export interface PythonProjectCreationApi { + /** + * Register a Python project creator. + * + * @param creator The project creator to register. + * @returns A disposable that can be used to unregister the project creator. + * @see {@link PythonProjectCreator} + */ + registerPythonProjectCreator(creator: PythonProjectCreator): Disposable; +} +export interface PythonProjectGetterApi { + /** + * Get all python projects. + */ + getPythonProjects(): readonly PythonProject[]; + + /** + * Get the python project for a given URI. + * + * @param uri The URI of the project + * @returns The project or `undefined` if not found. + */ + getPythonProject(uri: Uri): PythonProject | undefined; +} + +export interface PythonProjectModifyApi { + /** + * Add a python project or projects to the list of projects. + * + * @param projects The project or projects to add. + */ + addPythonProject(projects: PythonProject | PythonProject[]): void; + + /** + * Remove a python project from the list of projects. + * + * @param project The project to remove. + */ + removePythonProject(project: PythonProject): void; + + /** + * Event raised when python projects are added or removed. + * @see {@link DidChangePythonProjectsEventArgs} + */ + onDidChangePythonProjects: Event; +} + +/** + * The API for interacting with Python projects. A project in python is any folder or file that is a contained + * in some manner. For example, a PEP-723 compliant file can be treated as a project. A folder with a `pyproject.toml`, + * or just python files can be treated as a project. All this allows you to do is set a python environment for that project. + * + * By default all `vscode.workspace.workspaceFolders` are treated as projects. + */ +export interface PythonProjectApi extends PythonProjectCreationApi, PythonProjectGetterApi, PythonProjectModifyApi {} + +export interface PythonTerminalCreateOptions extends TerminalOptions { + /** + * Whether to disable activation on create. + */ + disableActivation?: boolean; +} + +export interface PythonTerminalCreateApi { + /** + * Creates a terminal and activates any (activatable) environment for the terminal. + * + * @param environment The Python environment to activate. + * @param options Options for creating the terminal. + * + * Note: Non-activatable environments have no effect on the terminal. + */ + createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise; +} + +/** + * Options for running a Python script or module in a terminal. + * + * Example: + * * Running Script: `python myscript.py --arg1` + * ```typescript + * { + * args: ["myscript.py", "--arg1"] + * } + * ``` + * * Running a module: `python -m my_module --arg1` + * ```typescript + * { + * args: ["-m", "my_module", "--arg1"] + * } + * ``` + */ +export interface PythonTerminalExecutionOptions { + /** + * Current working directory for the terminal. This in only used to create the terminal. + */ + cwd: string | Uri; + + /** + * Arguments to pass to the python executable. + */ + args?: string[]; + + /** + * Set `true` to show the terminal. + */ + show?: boolean; +} + +export interface PythonTerminalRunApi { + /** + * Runs a Python script or module in a terminal. This API will create a terminal if one is not available to use. + * If a terminal is available, it will be used to run the script or module. + * + * Note: + * - If you restart VS Code, this will create a new terminal, this is a limitation of VS Code. + * - If you close the terminal, this will create a new terminal. + * - In cases of multi-root/project scenario, it will create a separate terminal for each project. + */ + runInTerminal(environment: PythonEnvironment, options: PythonTerminalExecutionOptions): Promise; + + /** + * Runs a Python script or module in a dedicated terminal. This API will create a terminal if one is not available to use. + * If a terminal is available, it will be used to run the script or module. This terminal will be dedicated to the script, + * and selected based on the `terminalKey`. + * + * @param terminalKey A unique key to identify the terminal. For scripts you can use the Uri of the script file. + */ + runInDedicatedTerminal( + terminalKey: Uri | string, + environment: PythonEnvironment, + options: PythonTerminalExecutionOptions, + ): Promise; +} + +/** + * Options for running a Python task. + * + * Example: + * * Running Script: `python myscript.py --arg1` + * ```typescript + * { + * args: ["myscript.py", "--arg1"] + * } + * ``` + * * Running a module: `python -m my_module --arg1` + * ```typescript + * { + * args: ["-m", "my_module", "--arg1"] + * } + * ``` + */ +export interface PythonTaskExecutionOptions { + /** + * Name of the task to run. + */ + name: string; + + /** + * Arguments to pass to the python executable. + */ + args: string[]; + + /** + * The Python project to use for the task. + */ + project?: PythonProject; + + /** + * Current working directory for the task. Default is the project directory for the script being run. + */ + cwd?: string; + + /** + * Environment variables to set for the task. + */ + env?: { [key: string]: string }; +} + +export interface PythonTaskRunApi { + /** + * Run a Python script or module as a task. + * + */ + runAsTask(environment: PythonEnvironment, options: PythonTaskExecutionOptions): Promise; +} + +/** + * Options for running a Python script or module in the background. + */ +export interface PythonBackgroundRunOptions { + /** + * The Python environment to use for running the script or module. + */ + args: string[]; + + /** + * Current working directory for the script or module. Default is the project directory for the script being run. + */ + cwd?: string; + + /** + * Environment variables to set for the script or module. + */ + env?: { [key: string]: string | undefined }; +} +export interface PythonBackgroundRunApi { + /** + * Run a Python script or module in the background. This API will create a new process to run the script or module. + */ + runInBackground(environment: PythonEnvironment, options: PythonBackgroundRunOptions): Promise; +} + +export interface PythonExecutionApi + extends PythonTerminalCreateApi, + PythonTerminalRunApi, + PythonTaskRunApi, + PythonBackgroundRunApi {} + +/** + * Event arguments for when the monitored `.env` files or any other sources change. + */ +export interface DidChangeEnvironmentVariablesEventArgs { + /** + * The URI of the file that changed. No `Uri` means a non-file source of environment variables changed. + */ + uri?: Uri; + + /** + * The type of change that occurred. + */ + changeType: FileChangeType; +} + +export interface PythonEnvironmentVariablesApi { + /** + * Get environment variables for a workspace. This picks up `.env` file from the root of the + * workspace. + * + * Order of overrides: + * 1. `baseEnvVar` if given or `process.env` + * 2. `.env` file from the "python.envFile" setting in the workspace. + * 3. `.env` file at the root of the python project. + * 4. `overrides` in the order provided. + * + * @param uri The URI of the project, workspace or a file in a for which environment variables are required. + * @param overrides Additional environment variables to override the defaults. + * @param baseEnvVar The base environment variables that should be used as a starting point. + */ + getEnvironmentVariables( + uri: Uri, + overrides?: ({ [key: string]: string | undefined } | Uri)[], + baseEnvVar?: { [key: string]: string | undefined }, + ): Promise<{ [key: string]: string | undefined }>; + + /** + * Event raised when `.env` file changes or any other monitored source of env variable changes. + */ + onDidChangeEnvironmentVariables: Event; +} + +/** + * The API for interacting with Python environments, package managers, and projects. + */ +export interface PythonEnvironmentApi + extends PythonEnvironmentManagerApi, + PythonPackageManagerApi, + PythonProjectApi, + PythonExecutionApi, + PythonEnvironmentVariablesApi {} + +export const PEVSC_EXTENSION_ID = 'ms-python.vscode-python-envs'; + +// export interface PythonEnvsExtension { +// api: () => Promise; +// } + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PythonEnvsExtension { + /** + * Returns the API exposed by the Python extension in VS Code. + */ + export async function api(): Promise { + const extension = extensions.getExtension(PEVSC_EXTENSION_ID); + if (extension === undefined) { + throw new Error(`Python extension is not installed or is disabled`); + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonApi: PythonEnvironmentApi = extension.exports; + return pythonApi; + } +} diff --git a/src/test/unittest/adapter/factory.unit.test.ts b/src/test/unittest/adapter/factory.unit.test.ts index 86342d2c..2bbf455b 100644 --- a/src/test/unittest/adapter/factory.unit.test.ts +++ b/src/test/unittest/adapter/factory.unit.test.ts @@ -1,343 +1,343 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import { SemVer } from 'semver'; -import { instance, mock, when } from 'ts-mockito'; -import { DebugAdapterExecutable, DebugAdapterServer, DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; -import { IPersistentStateFactory } from '../../../extension/common/types'; -import { DebugAdapterDescriptorFactory, debugStateKeys } from '../../../extension/debugger/adapter/factory'; -import { IDebugAdapterDescriptorFactory } from '../../../extension/debugger/types'; -import { EventName } from '../../../extension/telemetry/constants'; -import { PersistentState, PersistentStateFactory } from '../../../extension/common/persistentState'; -import { EXTENSION_ROOT_DIR } from '../../../extension/common/constants'; -import { Architecture } from '../../../extension/common/platform'; -import * as pythonApi from '../../../extension/common/python'; -import * as telemetry from '../../../extension/telemetry'; -import * as telemetryReporter from '../../../extension/telemetry/reporter'; -import * as vscodeApi from '../../../extension/common/vscodeapi'; -import { DebugConfigStrings } from '../../../extension/common/utils/localize'; - -use(chaiAsPromised); - -suite('Debugging - Adapter Factory', () => { - let factory: IDebugAdapterDescriptorFactory; - let stateFactory: IPersistentStateFactory; - let state: PersistentState; - let showErrorMessageStub: sinon.SinonStub; - let resolveEnvironmentStub: sinon.SinonStub; - let getInterpretersStub: sinon.SinonStub; - let getInterpreterDetailsStub: sinon.SinonStub; - let hasInterpretersStub: sinon.SinonStub; - let getTelemetryReporterStub: sinon.SinonStub; - let reporter: any; - - const nodeExecutable = undefined; - const debugAdapterPath = path.join(EXTENSION_ROOT_DIR, 'bundled', 'libs', 'debugpy', 'adapter'); - const pythonPath = 'path/to/python/interpreter'; - const interpreter = { - architecture: Architecture.Unknown, - path: pythonPath, - sysPrefix: '', - sysVersion: '', - envType: 'Unknow', - version: new SemVer('3.7.4-test'), - }; - const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; - const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; - - class Reporter { - public static eventNames: string[] = []; - public static properties: Record[] = []; - public static measures: {}[] = []; - public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { - Reporter.eventNames.push(eventName); - Reporter.properties.push(properties!); - Reporter.measures.push(measures!); - } - } - - setup(() => { - process.env.VSC_PYTHON_UNIT_TEST = undefined; - process.env.VSC_PYTHON_CI_TEST = undefined; - reporter = new Reporter(); - - stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState) as PersistentState; - showErrorMessageStub = sinon.stub(vscodeApi, 'showErrorMessage'); - resolveEnvironmentStub = sinon.stub(pythonApi, 'resolveEnvironment'); - getInterpretersStub = sinon.stub(pythonApi, 'getInterpreters'); - getInterpreterDetailsStub = sinon.stub(pythonApi, 'getInterpreterDetails'); - hasInterpretersStub = sinon.stub(pythonApi, 'hasInterpreters'); - getTelemetryReporterStub = sinon.stub(telemetryReporter, 'getTelemetryReporter'); - - when( - stateFactory.createGlobalPersistentState(debugStateKeys.doNotShowAgain, false), - ).thenReturn(instance(state)); - getInterpretersStub.returns([interpreter]); - hasInterpretersStub.returns(true); - getTelemetryReporterStub.returns(reporter); - factory = new DebugAdapterDescriptorFactory(instance(stateFactory)); - }); - - teardown(() => { - process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; - process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; - Reporter.properties = []; - Reporter.eventNames = []; - Reporter.measures = []; - telemetry.clearTelemetryReporter(); - sinon.restore(); - }); - - function createSession(config: Partial, workspaceFolder?: WorkspaceFolder): DebugSession { - return { - configuration: { name: '', request: 'launch', type: 'python', ...config }, - id: '', - name: 'python', - type: 'python', - workspaceFolder, - customRequest: () => Promise.resolve(), - getDebugProtocolBreakpoint: () => Promise.resolve(undefined), - }; - } - - test('Return the value of configuration.pythonPath as the current python path if it exists', async () => { - const session = createSession({ pythonPath }); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - - resolveEnvironmentStub.withArgs(pythonPath).resolves(interpreter); - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test('Return the path of the active interpreter as the current python path, it exists and configuration.pythonPath is not defined', async () => { - const session = createSession({}); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.resolves(interpreter); - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test('Display a message if no python interpreter is set', async () => { - getInterpreterDetailsStub.resolves(undefined); - const session = createSession({}); - const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); - - await expect(promise).to.eventually.be.rejectedWith(DebugConfigStrings.debugStopped); - - //check error message - sinon.assert.calledOnce(showErrorMessageStub); - }); - - test('Display a message if python version is less than 3.7', async () => { - getInterpretersStub.returns([]); - const session = createSession({}); - const deprecatedInterpreter = { - architecture: Architecture.Unknown, - path: pythonPath, - sysPrefix: '', - sysVersion: '', - envType: 'Unknown', - version: new SemVer('3.6.12-test'), - }; - when(state.value).thenReturn(false); - getInterpreterDetailsStub.resolves({ path: [deprecatedInterpreter.path] }); - resolveEnvironmentStub.resolves(deprecatedInterpreter); - - await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - sinon.assert.calledOnce(showErrorMessageStub); - }); - - test('Return Debug Adapter server if request is "attach", and port is specified directly', async () => { - const session = createSession({ request: 'attach', port: 5678, host: 'localhost' }); - const debugServer = new DebugAdapterServer(session.configuration.port, session.configuration.host); - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - // Interpreter not needed for host/port - sinon.assert.neverCalledWith(getInterpretersStub); - - assert.deepStrictEqual(descriptor, debugServer); - }); - - test('Return Debug Adapter server if request is "attach", and connect is specified', async () => { - const session = createSession({ request: 'attach', connect: { port: 5678, host: 'localhost' } }); - const debugServer = new DebugAdapterServer( - session.configuration.connect.port, - session.configuration.connect.host, - ); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - // Interpreter not needed for connect - sinon.assert.neverCalledWith(getInterpretersStub); - assert.deepStrictEqual(descriptor, debugServer); - }); - - test('Return Debug Adapter server if request is "attach", and connect is specified with port as string', async () => { - const session = createSession({ request: 'attach', connect: { port: '5678', host: 'localhost' } }); - const debugServer = new DebugAdapterServer(5678, session.configuration.connect.host); - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - // Interpreter not needed for connect - sinon.assert.neverCalledWith(getInterpretersStub); - assert.deepStrictEqual(descriptor, debugServer); - }); - - test('Return Debug Adapter executable if request is "attach", and listen is specified', async () => { - const session = createSession({ request: 'attach', listen: { port: 5678, host: 'localhost' } }); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.resolves(interpreter); - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test('Throw error if request is "attach", and neither port, processId, listen, nor connect is specified', async () => { - const session = createSession({ - request: 'attach', - port: undefined, - processId: undefined, - listen: undefined, - connect: undefined, - }); - - const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); - - await expect(promise).to.eventually.be.rejectedWith( - '"request":"attach" requires either "connect", "listen", or "processId"', - ); - }); - - test('Pass the --log-dir argument to debug adapter if configuration.logToFile is set', async () => { - const session = createSession({ logToFile: true }); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [ - debugAdapterPath, - '--log-dir', - EXTENSION_ROOT_DIR, - ]); - - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test("Don't pass the --log-dir argument to debug adapter if configuration.logToFile is not set", async () => { - const session = createSession({}); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test("Don't pass the --log-dir argument to debugger if configuration.logToFile is set to false", async () => { - const session = createSession({ logToFile: false }); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test('Send attach to local process telemetry if attaching to a local process', async () => { - const session = createSession({ request: 'attach', processId: 1234 }); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); - - await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.ok(Reporter.eventNames.includes(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS)); - }); - - test("Don't send any telemetry if not attaching to a local process", async () => { - const session = createSession({}); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); - - await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.ok(Reporter.eventNames.includes(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH)); - }); - - test('Use "debugAdapterPath" when specified', async () => { - const customAdapterPath = 'custom/debug/adapter/path'; - const session = createSession({ debugAdapterPath: customAdapterPath }); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [customAdapterPath]); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - test('Add quotes to interpreter path with spaces', async () => { - const customAdapterPath = 'custom/debug/adapter/customAdapterPath'; - const session = createSession({ debugAdapterPath: customAdapterPath }); - const interpreterPathSpaces = 'path/to/python interpreter with spaces'; - const interpreterPathSpacesQuoted = `"${interpreterPathSpaces}"`; - const debugExecutable = new DebugAdapterExecutable(interpreterPathSpacesQuoted, [customAdapterPath]); - - getInterpreterDetailsStub.resolves({ path: [interpreterPathSpaces] }); - const interpreterSpacePath = { - architecture: Architecture.Unknown, - path: interpreterPathSpaces, - sysPrefix: '', - sysVersion: '', - envType: 'Unknow', - version: new SemVer('3.7.4-test'), - }; - resolveEnvironmentStub.withArgs(interpreterPathSpaces).resolves(interpreterSpacePath); - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test('Use "debugAdapterPython" when specified', async () => { - const session = createSession({ debugAdapterPython: '/bin/custompy' }); - const debugExecutable = new DebugAdapterExecutable('/bin/custompy', [debugAdapterPath]); - const customInterpreter = { - architecture: Architecture.Unknown, - path: '/bin/custompy', - sysPrefix: '', - sysVersion: '', - envType: 'unknow', - version: new SemVer('3.7.4-test'), - }; - - resolveEnvironmentStub.withArgs('/bin/custompy').resolves(customInterpreter); - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test('Do not use "python" to spawn the debug adapter', async () => { - const session = createSession({ python: '/bin/custompy' }); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); -}); +// /* eslint-disable @typescript-eslint/naming-convention */ +// // Copyright (c) Microsoft Corporation. All rights reserved. +// // Licensed under the MIT License. + +// 'use strict'; + +// import * as assert from 'assert'; +// import { expect, use } from 'chai'; +// import * as chaiAsPromised from 'chai-as-promised'; +// import * as path from 'path'; +// import * as sinon from 'sinon'; +// import { SemVer } from 'semver'; +// import { instance, mock, when } from 'ts-mockito'; +// import { DebugAdapterExecutable, DebugAdapterServer, DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; +// import { IPersistentStateFactory } from '../../../extension/common/types'; +// import { DebugAdapterDescriptorFactory, debugStateKeys } from '../../../extension/debugger/adapter/factory'; +// import { IDebugAdapterDescriptorFactory } from '../../../extension/debugger/types'; +// import { EventName } from '../../../extension/telemetry/constants'; +// import { PersistentState, PersistentStateFactory } from '../../../extension/common/persistentState'; +// import { EXTENSION_ROOT_DIR } from '../../../extension/common/constants'; +// import { Architecture } from '../../../extension/common/platform'; +// import * as pythonApi from '../../../extension/common/python'; +// import * as telemetry from '../../../extension/telemetry'; +// import * as telemetryReporter from '../../../extension/telemetry/reporter'; +// import * as vscodeApi from '../../../extension/common/vscodeapi'; +// import { DebugConfigStrings } from '../../../extension/common/utils/localize'; + +// use(chaiAsPromised); + +// suite('Debugging - Adapter Factory', () => { +// let factory: IDebugAdapterDescriptorFactory; +// let stateFactory: IPersistentStateFactory; +// let state: PersistentState; +// let showErrorMessageStub: sinon.SinonStub; +// let resolveEnvironmentStub: sinon.SinonStub; +// let getInterpretersStub: sinon.SinonStub; +// let getInterpreterDetailsStub: sinon.SinonStub; +// let hasInterpretersStub: sinon.SinonStub; +// let getTelemetryReporterStub: sinon.SinonStub; +// let reporter: any; + +// const nodeExecutable = undefined; +// const debugAdapterPath = path.join(EXTENSION_ROOT_DIR, 'bundled', 'libs', 'debugpy', 'adapter'); +// const pythonPath = 'path/to/python/interpreter'; +// const interpreter = { +// architecture: Architecture.Unknown, +// path: pythonPath, +// sysPrefix: '', +// sysVersion: '', +// envType: 'Unknow', +// version: new SemVer('3.7.4-test'), +// }; +// const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; +// const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + +// class Reporter { +// public static eventNames: string[] = []; +// public static properties: Record[] = []; +// public static measures: {}[] = []; +// public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { +// Reporter.eventNames.push(eventName); +// Reporter.properties.push(properties!); +// Reporter.measures.push(measures!); +// } +// } + +// setup(() => { +// process.env.VSC_PYTHON_UNIT_TEST = undefined; +// process.env.VSC_PYTHON_CI_TEST = undefined; +// reporter = new Reporter(); + +// stateFactory = mock(PersistentStateFactory); +// state = mock(PersistentState) as PersistentState; +// showErrorMessageStub = sinon.stub(vscodeApi, 'showErrorMessage'); +// resolveEnvironmentStub = sinon.stub(pythonApi, 'resolveEnvironment'); +// getInterpretersStub = sinon.stub(pythonApi, 'getInterpreters'); +// getInterpreterDetailsStub = sinon.stub(pythonApi, 'getInterpreterDetails'); +// hasInterpretersStub = sinon.stub(pythonApi, 'hasInterpreters'); +// getTelemetryReporterStub = sinon.stub(telemetryReporter, 'getTelemetryReporter'); + +// when( +// stateFactory.createGlobalPersistentState(debugStateKeys.doNotShowAgain, false), +// ).thenReturn(instance(state)); +// getInterpretersStub.returns([interpreter]); +// hasInterpretersStub.returns(true); +// getTelemetryReporterStub.returns(reporter); +// factory = new DebugAdapterDescriptorFactory(instance(stateFactory)); +// }); + +// teardown(() => { +// process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; +// process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; +// Reporter.properties = []; +// Reporter.eventNames = []; +// Reporter.measures = []; +// telemetry.clearTelemetryReporter(); +// sinon.restore(); +// }); + +// function createSession(config: Partial, workspaceFolder?: WorkspaceFolder): DebugSession { +// return { +// configuration: { name: '', request: 'launch', type: 'python', ...config }, +// id: '', +// name: 'python', +// type: 'python', +// workspaceFolder, +// customRequest: () => Promise.resolve(), +// getDebugProtocolBreakpoint: () => Promise.resolve(undefined), +// }; +// } + +// test('Return the value of configuration.pythonPath as the current python path if it exists', async () => { +// const session = createSession({ pythonPath }); +// const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + +// resolveEnvironmentStub.withArgs(pythonPath).resolves(interpreter); +// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// assert.deepStrictEqual(descriptor, debugExecutable); +// }); + +// test('Return the path of the active interpreter as the current python path, it exists and configuration.pythonPath is not defined', async () => { +// const session = createSession({}); +// const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); +// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); +// resolveEnvironmentStub.resolves(interpreter); +// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// assert.deepStrictEqual(descriptor, debugExecutable); +// }); + +// test('Display a message if no python interpreter is set', async () => { +// getInterpreterDetailsStub.resolves(undefined); +// const session = createSession({}); +// const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// await expect(promise).to.eventually.be.rejectedWith(DebugConfigStrings.debugStopped); + +// //check error message +// sinon.assert.calledOnce(showErrorMessageStub); +// }); + +// test('Display a message if python version is less than 3.7', async () => { +// getInterpretersStub.returns([]); +// const session = createSession({}); +// const deprecatedInterpreter = { +// architecture: Architecture.Unknown, +// path: pythonPath, +// sysPrefix: '', +// sysVersion: '', +// envType: 'Unknown', +// version: new SemVer('3.6.12-test'), +// }; +// when(state.value).thenReturn(false); +// getInterpreterDetailsStub.resolves({ path: [deprecatedInterpreter.path] }); +// resolveEnvironmentStub.resolves(deprecatedInterpreter); + +// await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// sinon.assert.calledOnce(showErrorMessageStub); +// }); + +// test('Return Debug Adapter server if request is "attach", and port is specified directly', async () => { +// const session = createSession({ request: 'attach', port: 5678, host: 'localhost' }); +// const debugServer = new DebugAdapterServer(session.configuration.port, session.configuration.host); +// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// // Interpreter not needed for host/port +// sinon.assert.neverCalledWith(getInterpretersStub); + +// assert.deepStrictEqual(descriptor, debugServer); +// }); + +// test('Return Debug Adapter server if request is "attach", and connect is specified', async () => { +// const session = createSession({ request: 'attach', connect: { port: 5678, host: 'localhost' } }); +// const debugServer = new DebugAdapterServer( +// session.configuration.connect.port, +// session.configuration.connect.host, +// ); + +// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// // Interpreter not needed for connect +// sinon.assert.neverCalledWith(getInterpretersStub); +// assert.deepStrictEqual(descriptor, debugServer); +// }); + +// test('Return Debug Adapter server if request is "attach", and connect is specified with port as string', async () => { +// const session = createSession({ request: 'attach', connect: { port: '5678', host: 'localhost' } }); +// const debugServer = new DebugAdapterServer(5678, session.configuration.connect.host); +// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// // Interpreter not needed for connect +// sinon.assert.neverCalledWith(getInterpretersStub); +// assert.deepStrictEqual(descriptor, debugServer); +// }); + +// test('Return Debug Adapter executable if request is "attach", and listen is specified', async () => { +// const session = createSession({ request: 'attach', listen: { port: 5678, host: 'localhost' } }); +// const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); +// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); +// resolveEnvironmentStub.resolves(interpreter); +// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// assert.deepStrictEqual(descriptor, debugExecutable); +// }); + +// test('Throw error if request is "attach", and neither port, processId, listen, nor connect is specified', async () => { +// const session = createSession({ +// request: 'attach', +// port: undefined, +// processId: undefined, +// listen: undefined, +// connect: undefined, +// }); + +// const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// await expect(promise).to.eventually.be.rejectedWith( +// '"request":"attach" requires either "connect", "listen", or "processId"', +// ); +// }); + +// test('Pass the --log-dir argument to debug adapter if configuration.logToFile is set', async () => { +// const session = createSession({ logToFile: true }); +// const debugExecutable = new DebugAdapterExecutable(pythonPath, [ +// debugAdapterPath, +// '--log-dir', +// EXTENSION_ROOT_DIR, +// ]); + +// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); +// resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + +// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// assert.deepStrictEqual(descriptor, debugExecutable); +// }); + +// test("Don't pass the --log-dir argument to debug adapter if configuration.logToFile is not set", async () => { +// const session = createSession({}); +// const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + +// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); +// resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + +// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// assert.deepStrictEqual(descriptor, debugExecutable); +// }); + +// test("Don't pass the --log-dir argument to debugger if configuration.logToFile is set to false", async () => { +// const session = createSession({ logToFile: false }); +// const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + +// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); +// resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + +// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// assert.deepStrictEqual(descriptor, debugExecutable); +// }); + +// test('Send attach to local process telemetry if attaching to a local process', async () => { +// const session = createSession({ request: 'attach', processId: 1234 }); +// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); +// resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + +// await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// assert.ok(Reporter.eventNames.includes(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS)); +// }); + +// test("Don't send any telemetry if not attaching to a local process", async () => { +// const session = createSession({}); +// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); +// resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + +// await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// assert.ok(Reporter.eventNames.includes(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH)); +// }); + +// test('Use "debugAdapterPath" when specified', async () => { +// const customAdapterPath = 'custom/debug/adapter/path'; +// const session = createSession({ debugAdapterPath: customAdapterPath }); +// const debugExecutable = new DebugAdapterExecutable(pythonPath, [customAdapterPath]); +// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); +// resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); +// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// assert.deepStrictEqual(descriptor, debugExecutable); +// }); +// test('Add quotes to interpreter path with spaces', async () => { +// const customAdapterPath = 'custom/debug/adapter/customAdapterPath'; +// const session = createSession({ debugAdapterPath: customAdapterPath }); +// const interpreterPathSpaces = 'path/to/python interpreter with spaces'; +// const interpreterPathSpacesQuoted = `"${interpreterPathSpaces}"`; +// const debugExecutable = new DebugAdapterExecutable(interpreterPathSpacesQuoted, [customAdapterPath]); + +// getInterpreterDetailsStub.resolves({ path: [interpreterPathSpaces] }); +// const interpreterSpacePath = { +// architecture: Architecture.Unknown, +// path: interpreterPathSpaces, +// sysPrefix: '', +// sysVersion: '', +// envType: 'Unknow', +// version: new SemVer('3.7.4-test'), +// }; +// resolveEnvironmentStub.withArgs(interpreterPathSpaces).resolves(interpreterSpacePath); +// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// assert.deepStrictEqual(descriptor, debugExecutable); +// }); + +// test('Use "debugAdapterPython" when specified', async () => { +// const session = createSession({ debugAdapterPython: '/bin/custompy' }); +// const debugExecutable = new DebugAdapterExecutable('/bin/custompy', [debugAdapterPath]); +// const customInterpreter = { +// architecture: Architecture.Unknown, +// path: '/bin/custompy', +// sysPrefix: '', +// sysVersion: '', +// envType: 'unknow', +// version: new SemVer('3.7.4-test'), +// }; + +// resolveEnvironmentStub.withArgs('/bin/custompy').resolves(customInterpreter); +// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// assert.deepStrictEqual(descriptor, debugExecutable); +// }); + +// test('Do not use "python" to spawn the debug adapter', async () => { +// const session = createSession({ python: '/bin/custompy' }); +// const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); +// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); +// resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); +// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + +// assert.deepStrictEqual(descriptor, debugExecutable); +// }); +// }); From 5d749ebfd2eda85343eeaca16fb0258a613e5ce6 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 23 Oct 2025 15:22:57 -0700 Subject: [PATCH 02/13] tests and split legacy python from python --- .../commands/reportIssueCommand.ts | 13 +- src/extension/common/legacyPython.ts | 218 ++++++ src/extension/common/python.ts | 315 ++++----- src/extension/common/utilities.ts | 20 + src/extension/envExtApi.ts | 2 +- .../commands/reportIssueCommand.unit.test.ts | 16 +- src/test/unittest/common/helpers.ts | 30 + .../unittest/common/pythonFalse.unit.test.ts | 621 ++++++++++++++++++ .../unittest/common/pythonTrue.unit.test.ts | 553 ++++++++++++++++ .../resolvers/launch.unit.test.ts | 13 +- 10 files changed, 1627 insertions(+), 174 deletions(-) create mode 100644 src/extension/common/legacyPython.ts create mode 100644 src/test/unittest/common/helpers.ts create mode 100644 src/test/unittest/common/pythonFalse.unit.test.ts create mode 100644 src/test/unittest/common/pythonTrue.unit.test.ts diff --git a/src/extension/common/application/commands/reportIssueCommand.ts b/src/extension/common/application/commands/reportIssueCommand.ts index 33ccd38a..d24ae726 100644 --- a/src/extension/common/application/commands/reportIssueCommand.ts +++ b/src/extension/common/application/commands/reportIssueCommand.ts @@ -10,6 +10,7 @@ import { getActiveEnvironmentPath, resolveEnvironment } from '../../python'; import { EXTENSION_ROOT_DIR } from '../../constants'; import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; +import { PythonEnvironment } from '../../../envExtApi'; /** * Allows the user to report an issue related to the Python Debugger extension using our template. @@ -19,11 +20,17 @@ export async function openReportIssue(): Promise { const userDataTemplatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_user_data_template.md'); const template = await fs.readFile(templatePath, 'utf8'); const userTemplate = await fs.readFile(userDataTemplatePath, 'utf8'); + // get active environment and resolve it const interpreterPath = await getActiveEnvironmentPath(); - const interpreter = await resolveEnvironment(interpreterPath); - const virtualEnvKind = interpreter?.envId.managerId ?? 'N/A'; - + let interpreter: PythonEnvironment | undefined = undefined; + if (interpreterPath && 'environmentPath' in interpreterPath) { + interpreter = interpreterPath ? await resolveEnvironment(interpreterPath.environmentPath.fsPath) : undefined; + } else if (interpreterPath && 'path' in interpreterPath) { + interpreter = interpreterPath ? await resolveEnvironment(interpreterPath.path) : undefined; + } + const virtualEnvKind = interpreter && interpreter.envId ? interpreter.envId.managerId : 'Unknown'; const pythonVersion = interpreter?.version ?? 'unknown'; + await executeCommand('workbench.action.openIssueReporter', { extensionId: 'ms-python.debugpy', issueBody: template, diff --git a/src/extension/common/legacyPython.ts b/src/extension/common/legacyPython.ts new file mode 100644 index 00000000..a0ae5ed5 --- /dev/null +++ b/src/extension/common/legacyPython.ts @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/naming-convention */ +import { + ActiveEnvironmentPathChangeEvent, + Environment, + EnvironmentPath, + EnvironmentVariables, + PythonExtension, + ResolvedEnvironment, + Resource, +} from '@vscode/python-extension'; +import { EventEmitter, extensions, Uri, Disposable, Extension } from 'vscode'; +import { createDeferred } from './utils/async'; +import { traceError, traceLog } from './log/logging'; + +/** + * Interface for the Python extension API. + */ +interface LegacyIExtensionApi { + ready: Promise; + settings: { + getExecutionDetails(resource?: Resource): { execCommand: string[] | undefined }; + }; +} + +/** + * Details about a Python interpreter. + */ +export interface LegacyIInterpreterDetails { + /** Array of path components to the Python executable */ + path?: string[]; + /** The workspace resource associated with this interpreter */ + resource?: Uri; +} + +// /** Event emitter for Python interpreter changes */ +// const legacyOnDidChangePythonInterpreterEvent = new EventEmitter(); + +// /** Event that fires when the active Python interpreter changes */ +// export const legacyOnDidChangePythonInterpreter: Event = +// legacyOnDidChangePythonInterpreterEvent.event; +/** + * Activates the Python extension and ensures it's ready for use. + * @returns The activated Python extension instance + */ +async function legacyActivateExtension(): Promise | undefined> { + console.log('Activating Python extension...'); + activateEnvsExtension(); + const extension = extensions.getExtension('ms-python.python'); + if (extension) { + if (!extension.isActive) { + await extension.activate(); + } + } + console.log('Python extension activated.'); + return extension; +} +/** + * Activates the Python environments extension. + * @returns The activated Python environments extension instance + */ +async function activateEnvsExtension(): Promise | undefined> { + const extension = extensions.getExtension('ms-python.vscode-python-envs'); + if (extension) { + if (!extension.isActive) { + await extension.activate(); + } + } + return extension; +} + +/** + * Gets the Python extension's API interface. + * @returns The Python extension API or undefined if not available + */ +async function legacyGetPythonExtensionAPI(): Promise { + const extension = await legacyActivateExtension(); + return extension?.exports as LegacyIExtensionApi; +} + +/** + * Gets the Python extension's environment API. + * @returns The Python extension environment API + */ +async function legacyGetPythonExtensionEnviromentAPI(): Promise { + // Load the Python extension API + await legacyActivateExtension(); + return await PythonExtension.api(); +} + +/** + * Initializes Python integration by setting up event listeners and getting initial interpreter details. + * @param disposables Array to store disposable resources for cleanup + */ +export async function legacyInitializePython( + disposables: Disposable[], + onDidChangePythonInterpreterEvent: EventEmitter, +): Promise { + try { + const api = await legacyGetPythonExtensionEnviromentAPI(); + + if (api) { + disposables.push( + // This event is triggered when the active environment setting changes. + api.environments.onDidChangeActiveEnvironmentPath((e: ActiveEnvironmentPathChangeEvent) => { + let resourceUri: Uri | undefined; + if (e.resource instanceof Uri) { + resourceUri = e.resource; + } + if (e.resource && 'uri' in e.resource) { + // WorkspaceFolder type + resourceUri = e.resource.uri; + } + onDidChangePythonInterpreterEvent.fire({ path: [e.path], resource: resourceUri }); + }), + ); + + traceLog('Waiting for interpreter from python extension.'); + onDidChangePythonInterpreterEvent.fire(await legacyGetInterpreterDetails()); + } + } catch (error) { + traceError('Error initializing python: ', error); + } +} + +/** + * Returns all the details needed to execute code within the selected environment, + * corresponding to the specified resource taking into account any workspace-specific settings + * for the workspace to which this resource belongs. + * @param resource Optional workspace resource to get settings for + * @returns Array of command components or undefined if not available + */ +export async function legacyGetSettingsPythonPath(resource?: Uri): Promise { + const api = await legacyGetPythonExtensionAPI(); + return api?.settings.getExecutionDetails(resource).execCommand; +} + +/** + * Returns the environment variables used by the extension for a resource, which includes the custom + * variables configured by user in `.env` files. + * @param resource Optional workspace resource to get environment variables for + * @returns Environment variables object + */ +export async function legacyGetEnvironmentVariables(resource?: Resource): Promise { + const api = await legacyGetPythonExtensionEnviromentAPI(); + return Promise.resolve(api.environments.getEnvironmentVariables(resource)); +} + +/** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param env Environment to resolve (can be Environment object, path, or string) + * @returns Resolved environment details + */ +export async function legacyResolveEnvironment( + env: Environment | EnvironmentPath | string, +): Promise { + const api = await legacyGetPythonExtensionEnviromentAPI(); + return api.environments.resolveEnvironment(env); +} + +/** + * Returns the environment configured by user in settings. Note that this can be an invalid environment, use + * resolve the environment to get full details. + * @param resource Optional workspace resource to get active environment for + * @returns Path to the active environment + */ +export async function legacyGetActiveEnvironmentPath(resource?: Resource): Promise { + const api = await legacyGetPythonExtensionEnviromentAPI(); + return api.environments.getActiveEnvironmentPath(resource); +} + +/** + * Gets detailed information about the active Python interpreter. + * @param resource Optional workspace resource to get interpreter details for + * @returns Interpreter details including path and resource information + */ +export async function legacyGetInterpreterDetails(resource?: Uri): Promise { + const api = await legacyGetPythonExtensionEnviromentAPI(); + const environment = await api.environments.resolveEnvironment(api.environments.getActiveEnvironmentPath(resource)); + if (environment?.executable.uri) { + return { path: [environment?.executable.uri.fsPath], resource }; + } + return { path: undefined, resource }; +} + +/** + * Checks if any Python interpreters are available in the system. + * @returns True if interpreters are found, false otherwise + */ +export async function legacyHasInterpreters(): Promise { + const api = await legacyGetPythonExtensionEnviromentAPI(); + const onAddedToCollection = createDeferred(); + api.environments.onDidChangeEnvironments(async () => { + if (api.environments.known) { + onAddedToCollection.resolve(); + } + }); + const initialEnvs = api.environments.known; + if (initialEnvs.length > 0) { + return true; + } + // Initiates a refresh of Python environments within the specified scope. + await Promise.race([onAddedToCollection.promise, api?.environments.refreshEnvironments()]); + + return api.environments.known.length > 0; +} + +/** + * Gets environments known to the extension at the time of fetching the property. Note this may not + * contain all environments in the system as a refresh might be going on. + * @returns Array of known Python environments + */ +export async function legacyGetInterpreters(): Promise { + const api = await legacyGetPythonExtensionEnviromentAPI(); + return api.environments.known || []; +} diff --git a/src/extension/common/python.ts b/src/extension/common/python.ts index 9735a6bd..b50f5ff8 100644 --- a/src/extension/common/python.ts +++ b/src/extension/common/python.ts @@ -2,25 +2,19 @@ // Licensed under the MIT License. /* eslint-disable @typescript-eslint/naming-convention */ -import { - ActiveEnvironmentPathChangeEvent, - Environment, - EnvironmentPath, - EnvironmentVariables, - PythonExtension, - ResolvedEnvironment, - Resource, -} from '@vscode/python-extension'; +import { Environment, EnvironmentPath, ResolvedEnvironment, Resource } from '@vscode/python-extension'; import { commands, EventEmitter, extensions, Uri, Event, Disposable } from 'vscode'; -import { traceError, traceLog } from './log/logging'; +import { traceError, traceLog, traceWarn } from './log/logging'; import { PythonEnvironment, PythonEnvironmentApi, PythonEnvsExtension } from '../envExtApi'; - -// interface IExtensionApi { -// ready: Promise; -// settings: { -// getExecutionDetails(resource?: Resource): { execCommand: string[] | undefined }; -// }; -// } +import { + legacyGetActiveEnvironmentPath, + legacyGetEnvironmentVariables, + legacyGetInterpreterDetails, + legacyGetSettingsPythonPath, + legacyInitializePython, + legacyResolveEnvironment, +} from './legacyPython'; +import { useEnvExtension } from './utilities'; /** * Details about a Python interpreter. @@ -37,20 +31,21 @@ const onDidChangePythonInterpreterEvent = new EventEmitter( /** Event that fires when the active Python interpreter changes */ export const onDidChangePythonInterpreter: Event = onDidChangePythonInterpreterEvent.event; -/** - * Activates the Python extension and ensures it's ready for use. - * @returns The activated Python extension instance - */ -async function activateExtension(): Promise | undefined> { - console.log('Activating Python extension...'); - activateEnvsExtension(); + +async function activateExtensions() { + traceWarn('Value during activateExtensions of useEnvExtension(): ', useEnvExtension()); + await activatePythonExtension(); + await activateEnvsExtension(); +} + +async function activatePythonExtension() { const extension = extensions.getExtension('ms-python.python'); if (extension) { if (!extension.isActive) { + console.log('Activating Python extension...'); await extension.activate(); } } - console.log('Python extension activated.'); return extension; } /** @@ -61,69 +56,44 @@ async function activateEnvsExtension(): Promise | undefined> { const extension = extensions.getExtension('ms-python.vscode-python-envs'); if (extension) { if (!extension.isActive) { + console.log('Activating Python Environments extension...'); await extension.activate(); } } return extension; } -async function getPythonEnviromentExtensionAPI(): Promise { - // Load the Python extension API +export async function getPythonEnvironmentExtensionAPI(): Promise { await activateEnvsExtension(); return await PythonEnvsExtension.api(); } -// async function getLegacyPythonExtensionAPI(): Promise { -// const extension = await activateExtension(); -// return extension?.exports as IExtensionApi; -// } - -async function getLegacyPythonExtensionEnviromentAPI(): Promise { - // Load the Python extension API - await activateExtension(); - return await PythonExtension.api(); -} - -/** - * Initializes Python integration by setting up event listeners and getting initial interpreter details. - * @param disposables Array to store disposable resources for cleanup - */ export async function initializePython(disposables: Disposable[]): Promise { - try { - const api = await getLegacyPythonExtensionEnviromentAPI(); - - if (api) { - disposables.push( - // This event is triggered when the active environment setting changes. - api.environments.onDidChangeActiveEnvironmentPath((e: ActiveEnvironmentPathChangeEvent) => { - let resourceUri: Uri | undefined; - if (e.resource instanceof Uri) { - resourceUri = e.resource; - } - if (e.resource && 'uri' in e.resource) { - // WorkspaceFolder type - resourceUri = e.resource.uri; - } - onDidChangePythonInterpreterEvent.fire({ path: [e.path], resource: resourceUri }); - }), - ); - - traceLog('Waiting for interpreter from python extension.'); - onDidChangePythonInterpreterEvent.fire(await getInterpreterDetails()); + if (!useEnvExtension()) { + await legacyInitializePython(disposables, onDidChangePythonInterpreterEvent); + } else { + try { + const api = await getPythonEnvironmentExtensionAPI(); + if (api) { + disposables.push( + api.onDidChangeEnvironments(async () => { + // not sure if this is the right event.... + onDidChangePythonInterpreterEvent.fire(await getInterpreterDetails()); + traceLog('Python environments changed.'); + }), + ); + + traceLog('Waiting for interpreter from python environments extension.'); + onDidChangePythonInterpreterEvent.fire(await getInterpreterDetails()); + } + } catch (error) { + traceError('Error initializing python: ', error); } - } catch (error) { - traceError('Error initializing python: ', error); } } -/** - * Executes a command from the Python extension. - * @param command The command identifier to execute - * @param rest Additional arguments to pass to the command - * @returns The result of the command execution - */ -export async function runPythonExtensionCommand(command: string, ...rest: any[]): Promise { - await activateExtension(); +export async function runPythonExtensionCommand(command: string, ...rest: any[]) { + await activateExtensions(); return await commands.executeCommand(command, ...rest); } @@ -135,69 +105,120 @@ export async function runPythonExtensionCommand(command: string, ...rest: any[]) * @returns Array of command components or undefined if not available */ export async function getSettingsPythonPath(resource?: Uri): Promise { - // const api = await getLegacyPythonExtensionAPI(); - // return api?.settings.getExecutionDetails(resource).execCommand; + // this one is only called if getInterpreterDetails(workspaceFolder) doesn't return somethinig with r.path + if (!useEnvExtension()) { + return legacyGetSettingsPythonPath(resource); + } else { + const api = await getPythonEnvironmentExtensionAPI(); + let pyEnv = await api.getEnvironment(resource); - const apiNew = await getPythonEnviromentExtensionAPI(); - const abc: PythonEnvironment[] = await apiNew.getEnvironments(resource || 'all'); - console.log('Python envs:', abc); - return undefined; -} + if (!pyEnv) { + return undefined; + } + + // Resolve environment if execution info is not available + if (!pyEnv.execInfo) { + pyEnv = await api.resolveEnvironment(pyEnv.environmentPath); + } + + // Extract execution command from resolved environment + const execInfo = pyEnv?.execInfo; + if (!execInfo) { + return undefined; + } + + const runConfig = execInfo.activatedRun ?? execInfo.run; + return runConfig.args ? [runConfig.executable, ...runConfig.args] : [runConfig.executable]; + } +} // should I make this more async? rn it just becomes sync export async function getEnvironmentVariables(resource?: Resource) { - const api = await getLegacyPythonExtensionEnviromentAPI(); - return api.environments.getEnvironmentVariables(resource); + if (!useEnvExtension()) { + return legacyGetEnvironmentVariables(resource); + } else { + const api = await getPythonEnvironmentExtensionAPI(); + + // Convert resource to Uri or undefined + const resourceUri = + resource instanceof Uri ? resource : resource && 'uri' in resource ? resource.uri : undefined; + + return api.getEnvironmentVariables(resourceUri); + } } export async function resolveEnvironment( env: Environment | EnvironmentPath | string, ): Promise { - // const api = await getLegacyPythonExtensionEnviromentAPI(); - // return api.environments.resolveEnvironment(env); - - const apiNew = await getPythonEnviromentExtensionAPI(); - - // Handle different input types for the new API - if (typeof env === 'string') { - // Convert string path to Uri for the new API - return apiNew.resolveEnvironment(Uri.file(env)); - } else if (typeof env === 'object' && 'path' in env) { - // EnvironmentPath has a uri property - return apiNew.resolveEnvironment(Uri.file(env.path)); + if (!useEnvExtension()) { + const legacyResolvedEnv: ResolvedEnvironment | undefined = await legacyResolveEnvironment(env); + // if its a legacy path, convert to new python environment + const pythonVersion = legacyResolvedEnv?.version + ? `${legacyResolvedEnv.version.major}.${legacyResolvedEnv.version.minor}.${legacyResolvedEnv.version.micro}` + : 'Unknown'; + const execUri = legacyResolvedEnv?.executable.uri; + if (execUri === undefined) { + // Should return undefined for invalid environment + return undefined; + } + if (legacyResolvedEnv) { + const pythonEnv: PythonEnvironment = { + envId: { + id: execUri.fsPath, + managerId: legacyResolvedEnv.environment?.type ?? 'Venv', + }, + name: legacyResolvedEnv.environment?.name ?? `Python ${pythonVersion ?? 'Unknown'}`, + displayName: legacyResolvedEnv.environment?.name ?? `Python ${pythonVersion ?? 'Unknown'}`, + displayPath: execUri.fsPath, + version: pythonVersion, + environmentPath: execUri, + execInfo: { + run: { + executable: execUri.fsPath, + args: [], + }, + }, + sysPrefix: legacyResolvedEnv.executable.sysPrefix ?? '', + }; + return pythonEnv; + } } else { - return undefined; + const api = await getPythonEnvironmentExtensionAPI(); + + // Handle different input types for the new API + if (typeof env === 'string') { + // Convert string path to Uri for the new API + return api.resolveEnvironment(Uri.file(env)); + } else if (typeof env === 'object' && 'path' in env) { + // EnvironmentPath has a uri property + return api.resolveEnvironment(Uri.file(env.path)); + } else { + return undefined; + } } } -export async function legacyResolveEnvironment( - env: Environment | EnvironmentPath | string, -): Promise { - const api = await getLegacyPythonExtensionEnviromentAPI(); - return api.environments.resolveEnvironment(env); -} - -export async function getLegacyActiveEnvironmentPath(resource?: Resource) { - const api = await getLegacyPythonExtensionEnviromentAPI(); - return api.environments.getActiveEnvironmentPath(resource); -} +export async function getActiveEnvironmentPath( + resource?: Resource, +): Promise { + // if I add environmentPath. or there needs to be some conversion between the two here + //TODO: fix this return type?? + if (!useEnvExtension()) { + const envPath: EnvironmentPath = await legacyGetActiveEnvironmentPath(resource); + return envPath; + } else { + const api = await getPythonEnvironmentExtensionAPI(); -export async function getActiveEnvironmentPath(resource?: Resource): Promise { - const api = await getPythonEnviromentExtensionAPI(); + // Convert resource to Uri | undefined from Resource | undefined + const resourceUri = + resource instanceof Uri ? resource : resource && 'uri' in resource ? resource.uri : undefined; - // Convert Resource to Uri if it exists - let resourceUri: Uri | undefined; - if (resource instanceof Uri) { - resourceUri = resource; - } else if (resource && 'uri' in resource) { - // WorkspaceFolder type - resourceUri = resource.uri; + const env = await api.getEnvironment(resourceUri); + return env; } - - return api.getEnvironment(resourceUri); } /** - * Gets Python interpreter details using the legacy Python extension API. + * Gets Python interpreter details using the Python Envs extension API. * * This function retrieves the active Python environment for a given resource using the * legacy @vscode/python-extension API. It resolves the environment to get the executable @@ -206,44 +227,24 @@ export async function getActiveEnvironmentPath(resource?: Resource): Promise { - const api = await getLegacyPythonExtensionEnviromentAPI(); - - const environment = await api.environments.resolveEnvironment(api.environments.getActiveEnvironmentPath(resource)); - if (environment?.executable.uri) { - return { path: [environment?.executable.uri.fsPath], resource }; - } - return { path: undefined, resource }; -} - -export function quoteStringIfNecessary(arg: string): string { - // Always return if already quoted to avoid double-quoting - if (arg.startsWith('"') && arg.endsWith('"')) { - return arg; - } - - // Quote if contains common shell special characters that are problematic across multiple shells - // Includes: space, &, |, <, >, ;, ', ", `, (, ), [, ], {, }, $ - const needsQuoting = /[\s&|<>;'"`()\[\]{}$]/.test(arg); - - return needsQuoting ? `"${arg}"` : arg; -} - export async function getInterpreterDetails(resource?: Uri): Promise { - const api = await getPythonEnviromentExtensionAPI(); - - // A promise that resolves to the current Python environment, or undefined if none is set. - const env: PythonEnvironment | undefined = await api.getEnvironment(resource); - // resolve the environment to get full details - const resolvedEnv = env ? await api.resolveEnvironment(env?.environmentPath) : undefined; - const executablePath = resolvedEnv?.execInfo.activatedRun?.executable - ? resolvedEnv.execInfo.activatedRun.executable - : resolvedEnv?.execInfo.run.executable; - - // Quote the executable path if necessary - const a: IInterpreterDetails = { - path: executablePath ? [quoteStringIfNecessary(executablePath)] : undefined, - resource, - }; - return a; + if (!useEnvExtension()) { + return legacyGetInterpreterDetails(resource); + } else { + const api = await getPythonEnvironmentExtensionAPI(); + + // A promise that resolves to the current Python environment, or undefined if none is set. + const env: PythonEnvironment | undefined = await api.getEnvironment(resource); + // resolve the environment to get full details + const resolvedEnv = env ? await api.resolveEnvironment(env?.environmentPath) : undefined; + const executablePath = resolvedEnv?.execInfo.activatedRun?.executable + ? resolvedEnv.execInfo.activatedRun.executable + : resolvedEnv?.execInfo.run.executable; + + const a: IInterpreterDetails = { + path: executablePath ? [executablePath] : undefined, + resource, + }; + return a; + } } diff --git a/src/extension/common/utilities.ts b/src/extension/common/utilities.ts index c58f5a08..675d5fe7 100644 --- a/src/extension/common/utilities.ts +++ b/src/extension/common/utilities.ts @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { Extension, extensions } from 'vscode'; +import { getConfiguration } from './vscodeapi'; + /** * Returns the elements of an array that meet the condition specified in an async callback function. * @param asyncPredicate The filter method calls the async predicate function one time for each element in the array. @@ -9,3 +12,20 @@ export async function asyncFilter(arr: T[], asyncPredicate: (value: T) => Pro const results = await Promise.all(arr.map(asyncPredicate)); return arr.filter((_v, index) => results[index]); } + +export function getExtension(extensionId: string): Extension | undefined { + return extensions.getExtension(extensionId); +} +let _useExt: boolean | undefined; +export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; + +export function useEnvExtension(): boolean { + if (_useExt !== undefined) { + return _useExt; + } + const inExpSetting = getConfiguration('python').get('useEnvironmentsExtension', false); + + // If extension is installed and in experiment, then use it. + _useExt = !!getExtension(ENVS_EXTENSION_ID) && inExpSetting; + return _useExt; +} diff --git a/src/extension/envExtApi.ts b/src/extension/envExtApi.ts index afd83e55..dcb3da44 100644 --- a/src/extension/envExtApi.ts +++ b/src/extension/envExtApi.ts @@ -1243,7 +1243,7 @@ export interface PythonEnvironmentVariablesApi { * @param baseEnvVar The base environment variables that should be used as a starting point. */ getEnvironmentVariables( - uri: Uri, + uri: Uri | undefined, overrides?: ({ [key: string]: string | undefined } | Uri)[], baseEnvVar?: { [key: string]: string | undefined }, ): Promise<{ [key: string]: string | undefined }>; diff --git a/src/test/unittest/common/application/commands/reportIssueCommand.unit.test.ts b/src/test/unittest/common/application/commands/reportIssueCommand.unit.test.ts index 1434d718..cc954842 100644 --- a/src/test/unittest/common/application/commands/reportIssueCommand.unit.test.ts +++ b/src/test/unittest/common/application/commands/reportIssueCommand.unit.test.ts @@ -14,7 +14,7 @@ import * as vscodeapi from '../../../../../extension/common/vscodeapi'; import * as pythonApi from '../../../../../extension/common/python'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../../constants'; import { openReportIssue } from '../../../../../extension/common/application/commands/reportIssueCommand'; -import { PythonEnvironment } from '../../../../../extension/debugger/adapter/types'; +import { PythonEnvironment } from '../../../../../extension/envExtApi'; suite('Report Issue Command', () => { let executeCommandStub: sinon.SinonStub; @@ -23,21 +23,21 @@ suite('Report Issue Command', () => { setup(async () => { executeCommandStub = sinon.stub(vscodeapi, 'executeCommand'); resolveEnvironmentStub = sinon.stub(pythonApi, 'resolveEnvironment'); + // Ensure useEnvironmentsExtension is false for these tests + (pythonApi as any).useEnvironmentsExtension = false; const interpreter = { - environment: { - type: 'Venv', - }, - version: { - major: 3, - minor: 9, - micro: 0, + envId: { + id: '/path/to/interpreter', + managerId: 'Venv', }, + version: '3.9.0', } as unknown as PythonEnvironment; resolveEnvironmentStub.resolves(interpreter); }); teardown(() => { sinon.restore(); + (pythonApi as any).useEnvironmentsExtension = undefined; }); test('Test if issue body is filled correctly when including all the settings', async () => { diff --git a/src/test/unittest/common/helpers.ts b/src/test/unittest/common/helpers.ts new file mode 100644 index 00000000..6ce8d848 --- /dev/null +++ b/src/test/unittest/common/helpers.ts @@ -0,0 +1,30 @@ +import { Uri } from 'vscode'; +import { PythonEnvironment } from '../../../extension/envExtApi'; + +/** + * Helper to build a simple PythonEnvironment object for tests. + * @param execPath string - path to the python executable + * @param version string - python version string (e.g. '3.9.0') + * @param sysPrefix string - sysPrefix value (optional) + */ +export function buildPythonEnvironment(execPath: string, version: string, sysPrefix: string = ''): PythonEnvironment { + const execUri = Uri.file(execPath); + return { + envId: { + id: execUri.fsPath, + managerId: 'Venv', + }, + name: `Python ${version}`, + displayName: `Python ${version}`, + displayPath: execUri.fsPath, + version: version, + environmentPath: execUri, + execInfo: { + run: { + executable: execUri.fsPath, + args: [], + }, + }, + sysPrefix, + } as PythonEnvironment; +} diff --git a/src/test/unittest/common/pythonFalse.unit.test.ts b/src/test/unittest/common/pythonFalse.unit.test.ts new file mode 100644 index 00000000..ee728282 --- /dev/null +++ b/src/test/unittest/common/pythonFalse.unit.test.ts @@ -0,0 +1,621 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +('use strict'); + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { Uri, Disposable, Extension, commands, extensions } from 'vscode'; +import * as pythonApi from '../../../extension/common/python'; +import * as utilities from '../../../extension/common/utilities'; +import { + PythonExtension, + Environment, + EnvironmentPath, + ResolvedEnvironment, + ActiveEnvironmentPathChangeEvent, +} from '@vscode/python-extension'; +import { buildPythonEnvironment } from './helpers'; + +suite('Python API Tests - useEnvironmentsExtension:false', () => { + let getExtensionStub: sinon.SinonStub; + let executeCommandStub: sinon.SinonStub; + let mockPythonExtension: Extension; + let mockEnvsExtension: Extension; + let mockPythonExtensionApi: any; + let mockPythonEnvApi: any; + + setup(() => { + // Stub extensions.getExtension + getExtensionStub = sinon.stub(extensions, 'getExtension'); + executeCommandStub = sinon.stub(commands, 'executeCommand'); + + // Mock useEnvExtension to return false for this test suite + sinon.stub(utilities, 'useEnvExtension').returns(false); + + // Create mock Python extension + mockPythonExtension = { + id: 'ms-python.python', + extensionUri: Uri.file('/mock/path'), + extensionPath: '/mock/path', + isActive: true, + packageJSON: {}, + exports: undefined, + activate: sinon.stub().resolves(), + extensionKind: 1, + } as any; + + // Create mock Python Envs extension + mockEnvsExtension = { + id: 'ms-python.vscode-python-envs', + extensionUri: Uri.file('/mock/path'), + extensionPath: '/mock/path', + isActive: true, + packageJSON: {}, + exports: undefined, + activate: sinon.stub().resolves(), + extensionKind: 1, + } as any; + + // Create mock Python extension API + mockPythonExtensionApi = { + ready: Promise.resolve(), + settings: { + getExecutionDetails: sinon.stub().returns({ execCommand: undefined }), + }, + }; + + // Create mock Python environment API + mockPythonEnvApi = { + environments: { + known: [], + getActiveEnvironmentPath: sinon.stub(), + resolveEnvironment: sinon.stub(), + getEnvironmentVariables: sinon.stub(), + onDidChangeActiveEnvironmentPath: sinon.stub().returns({ dispose: sinon.stub() }), + onDidChangeEnvironments: sinon.stub().returns({ dispose: sinon.stub() }), + refreshEnvironments: sinon.stub().resolves(), + }, + }; + + // Setup default behavior + getExtensionStub.withArgs('ms-python.python').returns(mockPythonExtension); + getExtensionStub.withArgs('ms-python.vscode-python-envs').returns(mockEnvsExtension); + (pythonApi as any)._useExt = false; + }); + + teardown(() => { + sinon.restore(); + // Reset useEnvironmentsExtension after each test + (pythonApi as any)._useExt = undefined; + }); + + suite('initializePython', () => { + test('Should initialize python and set up event listeners', async () => { + const disposables: Disposable[] = []; + (mockPythonExtension as any).exports = mockPythonExtensionApi; + mockPythonEnvApi.environments.onDidChangeActiveEnvironmentPath.returns({ + dispose: sinon.stub(), + }); + mockPythonEnvApi.environments.resolveEnvironment.resolves({ + executable: { uri: Uri.file('/usr/bin/python3') }, + } as ResolvedEnvironment); + // Stub PythonExtension.api() + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + await pythonApi.initializePython(disposables); + expect(disposables.length).to.be.greaterThan(0); + expect(mockPythonEnvApi.environments.onDidChangeActiveEnvironmentPath.called).to.be.true; + }); + + test('Should handle errors gracefully when python extension is not available', async () => { + const disposables: Disposable[] = []; + sinon.stub(PythonExtension, 'api').rejects(new Error('Extension not found')); + + await pythonApi.initializePython(disposables); + + // Should not throw, just handle error internally + expect(disposables.length).to.equal(0); + }); + + test('Should fire onDidChangePythonInterpreter event after initialization', async () => { + const disposables: Disposable[] = []; + const mockEventHandler = sinon.stub(); + + (mockPythonExtension as any).exports = mockPythonExtensionApi; + mockPythonEnvApi.environments.resolveEnvironment.resolves({ + executable: { uri: Uri.file('/usr/bin/python3') }, + } as ResolvedEnvironment); + + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + pythonApi.onDidChangePythonInterpreter(mockEventHandler); + + await pythonApi.initializePython(disposables); + + // Event should be fired during initialization + sinon.assert.called(mockEventHandler); + }); + }); + + suite('runPythonExtensionCommand', () => { + test('Should execute command through VS Code commands API', async () => { + (mockPythonExtension as any).isActive = true; + executeCommandStub.resolves('result'); + + const result = await pythonApi.runPythonExtensionCommand('python.test.command', 'arg1', 'arg2'); + + expect(result).to.equal('result'); + sinon.assert.calledWith(executeCommandStub, 'python.test.command', 'arg1', 'arg2'); + }); + + test('Should activate extension before executing command if not active', async () => { + (mockPythonExtension as any).isActive = false; + const activateStub = mockPythonExtension.activate as sinon.SinonStub; + executeCommandStub.resolves('result'); + + await pythonApi.runPythonExtensionCommand('python.test.command'); + + sinon.assert.called(activateStub); + sinon.assert.called(executeCommandStub); + }); + }); + + suite('getSettingsPythonPath', () => { + test('Should return execution details from Python extension API', async () => { + const expectedPath = ['/usr/bin/python3']; + mockPythonExtensionApi.settings.getExecutionDetails.returns({ execCommand: expectedPath }); + (mockPythonExtension as any).exports = mockPythonExtensionApi; + (mockPythonExtension as any).isActive = true; + + const result = await pythonApi.getSettingsPythonPath(); + + expect(result).to.deep.equal(expectedPath); + }); + + test('Should return execution details for specific resource', async () => { + const resource = Uri.file('/workspace/file.py'); + const expectedPath = ['/usr/bin/python3']; + mockPythonExtensionApi.settings.getExecutionDetails.returns({ execCommand: expectedPath }); + (mockPythonExtension as any).exports = mockPythonExtensionApi; + (mockPythonExtension as any).isActive = true; + + const result = await pythonApi.getSettingsPythonPath(resource); + + expect(result).to.deep.equal(expectedPath); + sinon.assert.calledWith(mockPythonExtensionApi.settings.getExecutionDetails, resource); + }); + + test('Should return undefined when execCommand is not available', async () => { + mockPythonExtensionApi.settings.getExecutionDetails.returns({ execCommand: undefined }); + (mockPythonExtension as any).exports = mockPythonExtensionApi; + (mockPythonExtension as any).isActive = true; + + const result = await pythonApi.getSettingsPythonPath(); + + expect(result).to.be.undefined; + }); + }); + + suite('getEnvironmentVariables', () => { + test('Should return environment variables from Python extension API', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const expectedVars = { PATH: '/usr/bin', PYTHONPATH: '/usr/lib/python3' }; + mockPythonEnvApi.environments.getEnvironmentVariables.returns(Promise.resolve(expectedVars)); + + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + const result = await pythonApi.getEnvironmentVariables(); + + expect(result).to.deep.equal(expectedVars); + sinon.assert.calledWith(mockPythonEnvApi.environments.getEnvironmentVariables, sinon.match.any); + }); + + test('Should get environment variables for specific resource', async () => { + const resource = Uri.file('/workspace/file.py'); + // eslint-disable-next-line @typescript-eslint/naming-convention + const expectedVars = { PATH: '/usr/bin' }; + mockPythonEnvApi.environments.getEnvironmentVariables.returns(Promise.resolve(expectedVars)); + + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + const result = await pythonApi.getEnvironmentVariables(resource); + + expect(result).to.deep.equal(expectedVars); + sinon.assert.calledWith(mockPythonEnvApi.environments.getEnvironmentVariables, resource); + }); + + test('Should handle undefined resource and return workspace environment variables', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const expectedVars = { PATH: '/usr/bin', PYTHONPATH: '/workspace/python' }; + mockPythonEnvApi.environments.getEnvironmentVariables.returns(Promise.resolve(expectedVars)); + + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + const result = await pythonApi.getEnvironmentVariables(undefined); + + expect(result).to.deep.equal(expectedVars); + sinon.assert.calledWith(mockPythonEnvApi.environments.getEnvironmentVariables, undefined); + }); + }); + + suite('resolveEnvironment', () => { + test('Should resolve environment from path string', async () => { + const envPath = '/usr/bin/python3'; + const mockResolvedEnv: ResolvedEnvironment = { + id: 'test-env', + version: { major: 3, minor: 9, micro: 0 }, + executable: { uri: Uri.file(envPath) }, + } as ResolvedEnvironment; + const expectedEnv = buildPythonEnvironment(envPath, '3.9.0'); + + mockPythonEnvApi.environments.resolveEnvironment.resolves(mockResolvedEnv); + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + const result = await pythonApi.resolveEnvironment(envPath); + + expect(result).to.deep.equal(expectedEnv); + sinon.assert.calledWith(mockPythonEnvApi.environments.resolveEnvironment, envPath); + }); + + test('Should resolve environment from Environment object', async () => { + let pythonPath = '/usr/bin/python3'; + const env: Environment = { + id: 'test-env', + path: pythonPath, + } as Environment; + const mockResolvedEnv: ResolvedEnvironment = { + id: 'test-env', + version: { major: 3, minor: 9, micro: 0 }, + executable: { uri: Uri.file(pythonPath) }, + } as ResolvedEnvironment; + const expectedEnv = buildPythonEnvironment(pythonPath, '3.9.0'); + + mockPythonEnvApi.environments.resolveEnvironment.resolves(mockResolvedEnv); + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + const result = await pythonApi.resolveEnvironment(env); + + expect(result).to.deep.equal(expectedEnv); + }); + + test('Should return undefined for invalid environment', async () => { + const envPath = '/invalid/path'; + mockPythonEnvApi.environments.resolveEnvironment.resolves(undefined); + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + const result = await pythonApi.resolveEnvironment(envPath); + + expect(result).to.be.undefined; + }); + }); + + suite('getActiveEnvironmentPath', () => { + test('Should return active environment path', async () => { + const expectedPath: EnvironmentPath = { + id: 'test-env', + path: '/usr/bin/python3', + }; + mockPythonEnvApi.environments.getActiveEnvironmentPath.returns(expectedPath); + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + const result = await pythonApi.getActiveEnvironmentPath(); + + expect(result).to.deep.equal(expectedPath); + }); + + test('Should return active environment path for specific resource', async () => { + const resource = Uri.file('/workspace/file.py'); + const expectedPath: EnvironmentPath = { + id: 'test-env', + path: '/usr/bin/python3', + }; + mockPythonEnvApi.environments.getActiveEnvironmentPath.returns(expectedPath); + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + const result = await pythonApi.getActiveEnvironmentPath(resource); + + expect(result).to.deep.equal(expectedPath); + sinon.assert.calledWith(mockPythonEnvApi.environments.getActiveEnvironmentPath, resource); + }); + }); + + suite('getInterpreterDetails', () => { + test('Should return interpreter details with path', async () => { + const pythonPath = '/usr/bin/python3'; + const mockEnv: ResolvedEnvironment = { + id: 'test-env', + executable: { uri: Uri.file(pythonPath) }, + } as ResolvedEnvironment; + + mockPythonEnvApi.environments.getActiveEnvironmentPath.returns({ path: pythonPath }); + mockPythonEnvApi.environments.resolveEnvironment.resolves(mockEnv); + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + const result = await pythonApi.getInterpreterDetails(); + + // Use Uri.file().fsPath to get platform-normalized path for comparison + expect(result.path).to.deep.equal([Uri.file(pythonPath).fsPath]); + expect(result.resource).to.be.undefined; + }); + + test('Should return interpreter details with resource', async () => { + const resource = Uri.file('/workspace/file.py'); + const pythonPath = '/usr/bin/python3'; + const mockEnv: ResolvedEnvironment = { + id: 'test-env', + executable: { uri: Uri.file(pythonPath) }, + } as ResolvedEnvironment; + + mockPythonEnvApi.environments.getActiveEnvironmentPath.returns({ path: pythonPath }); + mockPythonEnvApi.environments.resolveEnvironment.resolves(mockEnv); + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + const result = await pythonApi.getInterpreterDetails(resource); + + // Use Uri.file().fsPath to get platform-normalized path for comparison + expect(result.path).to.deep.equal([Uri.file(pythonPath).fsPath]); + expect(result.resource).to.deep.equal(resource); + }); + + test('Should not quote path with spaces', async () => { + // this should be updated when we fix the quoting logic in getInterpreterDetails + const pythonPath = '/path with spaces/python3'; + const mockUri = { + fsPath: pythonPath, + }; + const mockEnv: ResolvedEnvironment = { + id: 'test-env', + executable: { uri: mockUri }, + } as ResolvedEnvironment; + + mockPythonEnvApi.environments.getActiveEnvironmentPath.returns({ path: pythonPath }); + mockPythonEnvApi.environments.resolveEnvironment.resolves(mockEnv); + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + const result = await pythonApi.getInterpreterDetails(); + + expect(result.path).to.deep.equal([`${pythonPath}`]); + }); + + test('Should not double-quote already quoted path', async () => { + const quotedPythonPath = '"/path with spaces/python3"'; + // Create a mock Uri that when accessed via fsPath returns the already quoted path + const mockUri = { + fsPath: quotedPythonPath, + } as Uri; + const mockEnv: ResolvedEnvironment = { + id: 'test-env', + executable: { uri: mockUri }, + } as ResolvedEnvironment; + + mockPythonEnvApi.environments.getActiveEnvironmentPath.returns({ path: quotedPythonPath }); + mockPythonEnvApi.environments.resolveEnvironment.resolves(mockEnv); + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + const result = await pythonApi.getInterpreterDetails(); + + expect(result.path).to.deep.equal([quotedPythonPath]); + }); + + test('Should return undefined path when environment is not resolved', async () => { + mockPythonEnvApi.environments.getActiveEnvironmentPath.returns({ path: '/usr/bin/python3' }); + mockPythonEnvApi.environments.resolveEnvironment.resolves(undefined); + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + const result = await pythonApi.getInterpreterDetails(); + + expect(result.path).to.be.undefined; + expect(result.resource).to.be.undefined; + }); + + test('Should return undefined path when executable uri is not available', async () => { + const mockEnv: ResolvedEnvironment = { + id: 'test-env', + executable: { uri: undefined }, + } as any; + + mockPythonEnvApi.environments.getActiveEnvironmentPath.returns({ path: '/usr/bin/python3' }); + mockPythonEnvApi.environments.resolveEnvironment.resolves(mockEnv); + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + const result = await pythonApi.getInterpreterDetails(); + + expect(result.path).to.be.undefined; + }); + }); + + suite('onDidChangePythonInterpreter event', () => { + test('Should fire event when active environment path changes', async () => { + const disposables: Disposable[] = []; + let eventCallback: any; + const mockEventHandler = sinon.stub(); + + mockPythonEnvApi.environments.onDidChangeActiveEnvironmentPath = (callback: any) => { + eventCallback = callback; + return { dispose: sinon.stub() }; + }; + mockPythonEnvApi.environments.resolveEnvironment.resolves({ + executable: { uri: Uri.file('/usr/bin/python3') }, + } as ResolvedEnvironment); + + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + pythonApi.onDidChangePythonInterpreter(mockEventHandler); + + await pythonApi.initializePython(disposables); + + // Simulate environment path change + const changeEvent: ActiveEnvironmentPathChangeEvent = { + id: 'test-env', + path: '/usr/bin/python3.9', + resource: Uri.file('/workspace'), + }; + eventCallback(changeEvent); + + // Should be called at least twice: once during init, once from the event + expect(mockEventHandler.callCount).to.be.greaterThan(1); + const lastCall = mockEventHandler.lastCall.args[0]; + expect(lastCall.path).to.deep.equal(['/usr/bin/python3.9']); + expect(lastCall.resource).to.deep.equal(Uri.file('/workspace')); + }); + + test('Should handle WorkspaceFolder resource in event', async () => { + const disposables: Disposable[] = []; + let eventCallback: any; + const mockEventHandler = sinon.stub(); + + mockPythonEnvApi.environments.onDidChangeActiveEnvironmentPath = (callback: any) => { + eventCallback = callback; + return { dispose: sinon.stub() }; + }; + mockPythonEnvApi.environments.resolveEnvironment.resolves({ + executable: { uri: Uri.file('/usr/bin/python3') }, + } as ResolvedEnvironment); + + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + pythonApi.onDidChangePythonInterpreter(mockEventHandler); + + await pythonApi.initializePython(disposables); + + // Simulate environment path change with WorkspaceFolder resource + const workspaceFolderUri = Uri.file('/workspace'); + const changeEvent: any = { + id: 'test-env', + path: '/usr/bin/python3.9', + resource: { uri: workspaceFolderUri, name: 'workspace', index: 0 }, + }; + eventCallback(changeEvent); + + const lastCall = mockEventHandler.lastCall.args[0]; + expect(lastCall.resource).to.deep.equal(workspaceFolderUri); + }); + + test('Should handle null resource in event', async () => { + const disposables: Disposable[] = []; + let eventCallback: any; + const mockEventHandler = sinon.stub(); + + mockPythonEnvApi.environments.onDidChangeActiveEnvironmentPath = (callback: any) => { + eventCallback = callback; + return { dispose: sinon.stub() }; + }; + mockPythonEnvApi.environments.resolveEnvironment.resolves({ + executable: { uri: Uri.file('/usr/bin/python3') }, + } as ResolvedEnvironment); + + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + pythonApi.onDidChangePythonInterpreter(mockEventHandler); + + await pythonApi.initializePython(disposables); + + // Simulate environment path change with null resource + const changeEvent: any = { + id: 'test-env', + path: '/usr/bin/python3.9', + resource: null, + }; + eventCallback(changeEvent); + + const lastCall = mockEventHandler.lastCall.args[0]; + expect(lastCall.path).to.deep.equal(['/usr/bin/python3.9']); + expect(lastCall.resource).to.be.undefined; // null gets converted to undefined + }); + + test('Should handle undefined resource in event', async () => { + const disposables: Disposable[] = []; + let eventCallback: any; + const mockEventHandler = sinon.stub(); + + mockPythonEnvApi.environments.onDidChangeActiveEnvironmentPath = (callback: any) => { + eventCallback = callback; + return { dispose: sinon.stub() }; + }; + mockPythonEnvApi.environments.resolveEnvironment.resolves({ + executable: { uri: Uri.file('/usr/bin/python3') }, + } as ResolvedEnvironment); + + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + pythonApi.onDidChangePythonInterpreter(mockEventHandler); + + await pythonApi.initializePython(disposables); + + // Simulate environment path change with undefined resource + const changeEvent: ActiveEnvironmentPathChangeEvent = { + id: 'test-env', + path: '/usr/bin/python3.9', + resource: undefined, + }; + eventCallback(changeEvent); + + const lastCall = mockEventHandler.lastCall.args[0]; + expect(lastCall.path).to.deep.equal(['/usr/bin/python3.9']); + expect(lastCall.resource).to.be.undefined; + }); + + test('Should handle event with missing id', async () => { + const disposables: Disposable[] = []; + let eventCallback: any; + const mockEventHandler = sinon.stub(); + + mockPythonEnvApi.environments.onDidChangeActiveEnvironmentPath = (callback: any) => { + eventCallback = callback; + return { dispose: sinon.stub() }; + }; + mockPythonEnvApi.environments.resolveEnvironment.resolves({ + executable: { uri: Uri.file('/usr/bin/python3') }, + } as ResolvedEnvironment); + + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + pythonApi.onDidChangePythonInterpreter(mockEventHandler); + + await pythonApi.initializePython(disposables); + + // Simulate environment path change with missing id + const changeEvent: any = { + path: '/usr/bin/python3.9', + resource: Uri.file('/workspace'), + }; + eventCallback(changeEvent); + + const lastCall = mockEventHandler.lastCall.args[0]; + expect(lastCall.path).to.deep.equal(['/usr/bin/python3.9']); + expect(lastCall.resource).to.deep.equal(Uri.file('/workspace')); + }); + + test('Should handle event with null path', async () => { + const disposables: Disposable[] = []; + let eventCallback: any; + const mockEventHandler = sinon.stub(); + + mockPythonEnvApi.environments.onDidChangeActiveEnvironmentPath = (callback: any) => { + eventCallback = callback; + return { dispose: sinon.stub() }; + }; + mockPythonEnvApi.environments.resolveEnvironment.resolves({ + executable: { uri: Uri.file('/usr/bin/python3') }, + } as ResolvedEnvironment); + + sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); + + pythonApi.onDidChangePythonInterpreter(mockEventHandler); + + await pythonApi.initializePython(disposables); + + // Simulate environment path change with null path + const changeEvent: any = { + id: 'test-env', + path: null, + resource: Uri.file('/workspace'), + }; + eventCallback(changeEvent); + + const lastCall = mockEventHandler.lastCall.args[0]; + expect(lastCall.path).to.deep.equal([null]); + expect(lastCall.resource).to.deep.equal(Uri.file('/workspace')); + }); + }); +}); diff --git a/src/test/unittest/common/pythonTrue.unit.test.ts b/src/test/unittest/common/pythonTrue.unit.test.ts new file mode 100644 index 00000000..bf6f9fd8 --- /dev/null +++ b/src/test/unittest/common/pythonTrue.unit.test.ts @@ -0,0 +1,553 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { Uri, Disposable, Extension, extensions } from 'vscode'; +import * as pythonApi from '../../../extension/common/python'; +import * as utilities from '../../../extension/common/utilities'; +import { Environment, EnvironmentPath } from '@vscode/python-extension'; +import { buildPythonEnvironment } from './helpers'; + +suite('Python API Tests- useEnvironmentsExtension:true', () => { + let getExtensionStub: sinon.SinonStub; + let mockPythonExtension: Extension; + let mockEnvsExtension: Extension; + let mockPythonEnvApi: any; + + setup(() => { + // Stub extensions.getExtension + getExtensionStub = sinon.stub(extensions, 'getExtension'); + + // Mock useEnvExtension to return true for this test suite + sinon.stub(utilities, 'useEnvExtension').returns(true); + + // Create mock Python extension + mockPythonExtension = { + id: 'ms-python.python', + extensionUri: Uri.file('/mock/path'), + extensionPath: '/mock/path', + isActive: true, + packageJSON: {}, + exports: undefined, + activate: sinon.stub().resolves(), + extensionKind: 1, + } as any; + + // Create mock Python Envs extension + mockEnvsExtension = { + id: 'ms-python.vscode-python-envs', + extensionUri: Uri.file('/mock/path'), + extensionPath: '/mock/path', + isActive: true, + packageJSON: {}, + exports: undefined, + activate: sinon.stub().resolves(), + extensionKind: 1, + } as any; + + // Create mock Python environment API - for new environments extension + mockPythonEnvApi = { + getEnvironment: sinon.stub(), + setEnvironment: sinon.stub(), + resolveEnvironment: sinon.stub(), + getEnvironmentVariables: sinon.stub(), + onDidChangeEnvironment: sinon.stub().returns({ dispose: sinon.stub() }), + onDidChangeEnvironments: sinon.stub().returns({ dispose: sinon.stub() }), + refreshEnvironments: sinon.stub().resolves(), + getEnvironments: sinon.stub(), + }; + + // Setup default behavior + getExtensionStub.withArgs('ms-python.python').returns(mockPythonExtension); + getExtensionStub.withArgs('ms-python.vscode-python-envs').returns(mockEnvsExtension); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('initializePython', () => { + test('Should initialize python and set up event listeners', async () => { + const disposables: Disposable[] = []; + (mockEnvsExtension as any).exports = mockPythonEnvApi; + const mockPythonEnv = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); + mockPythonEnvApi.getEnvironment.resolves(mockPythonEnv); + mockPythonEnvApi.resolveEnvironment.resolves(mockPythonEnv); + mockPythonEnvApi.onDidChangeEnvironments.returns({ + dispose: sinon.stub(), + }); + + await pythonApi.initializePython(disposables); + expect(disposables.length).to.be.greaterThan(0); + expect(mockPythonEnvApi.onDidChangeEnvironments.called).to.be.true; + }); + + test('Should handle errors gracefully when python extension is not available', async () => { + const disposables: Disposable[] = []; + // Return undefined extension to simulate extension not found + getExtensionStub.withArgs('ms-python.vscode-python-envs').returns(undefined); + + await pythonApi.initializePython(disposables); + + // Should not throw, just handle error internally + expect(disposables.length).to.equal(0); + }); + + test('Should fire onDidChangePythonInterpreter event after initialization', async () => { + const disposables: Disposable[] = []; + const mockEventHandler = sinon.stub(); + + (mockEnvsExtension as any).exports = mockPythonEnvApi; + const mockPythonEnv = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); + mockPythonEnvApi.getEnvironment.resolves(mockPythonEnv); + mockPythonEnvApi.resolveEnvironment.resolves(mockPythonEnv); + + pythonApi.onDidChangePythonInterpreter(mockEventHandler); + + await pythonApi.initializePython(disposables); + + // Event should be fired during initialization + sinon.assert.called(mockEventHandler); + }); + }); + + suite('getSettingsPythonPath', () => { + test('Should return execution details from Python extension API', async () => { + const expectedPath = ['/usr/bin/python3']; + // OLD API: Using getEnvironment() + resolveEnvironment() instead of settings.getExecutionDetails + const mockPythonEnv = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironment.resolves(mockPythonEnv); + mockPythonEnvApi.resolveEnvironment.resolves(mockPythonEnv); + + const result = await pythonApi.getSettingsPythonPath(); + + expect(result).to.deep.equal(expectedPath); + }); + + test('Should return execution details for specific resource', async () => { + const resource = Uri.file('/workspace/file.py'); + const expectedPath = ['/usr/bin/python3']; + // OLD API: Using getEnvironment() + resolveEnvironment() instead of settings.getExecutionDetails + const mockPythonEnv = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironment.resolves(mockPythonEnv); + mockPythonEnvApi.resolveEnvironment.resolves(mockPythonEnv); + + const result = await pythonApi.getSettingsPythonPath(resource); + + expect(result).to.deep.equal(expectedPath); + // OLD API: Using getEnvironment() instead of settings.getExecutionDetails + sinon.assert.calledWith(mockPythonEnvApi.getEnvironment, resource); + }); + + test('Should return undefined when execCommand is not available', async () => { + // OLD API: Using getEnvironment() instead of settings.getExecutionDetails + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironment.resolves(undefined); + + const result = await pythonApi.getSettingsPythonPath(); + + expect(result).to.be.undefined; + }); + }); + + suite('getEnvironmentVariables', () => { + test('Should return environment variables from Python extension API', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const expectedVars = { PATH: '/usr/bin', PYTHONPATH: '/usr/lib/python3' }; + // OLD API: Using getEnvironmentVariables() instead of environments.getEnvironmentVariables + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironmentVariables.resolves(expectedVars); + + const result = await pythonApi.getEnvironmentVariables(); + + expect(result).to.deep.equal(expectedVars); + // OLD API: Using getEnvironmentVariables() instead of environments.getEnvironmentVariables + sinon.assert.calledWith(mockPythonEnvApi.getEnvironmentVariables, sinon.match.any); + }); + + test('Should get environment variables for specific resource', async () => { + const resource = Uri.file('/workspace/file.py'); + // eslint-disable-next-line @typescript-eslint/naming-convention + const expectedVars = { PATH: '/usr/bin' }; + // OLD API: Using getEnvironmentVariables() instead of environments.getEnvironmentVariables + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironmentVariables.resolves(expectedVars); + + const result = await pythonApi.getEnvironmentVariables(resource); + + expect(result).to.deep.equal(expectedVars); + // OLD API: Using getEnvironmentVariables() instead of environments.getEnvironmentVariables + sinon.assert.calledWith(mockPythonEnvApi.getEnvironmentVariables, resource); + }); + + test('Should handle undefined resource and return workspace environment variables', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const expectedVars = { PATH: '/usr/bin', PYTHONPATH: '/workspace/python' }; + // OLD API: Using getEnvironmentVariables() instead of environments.getEnvironmentVariables + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironmentVariables.resolves(expectedVars); + + const result = await pythonApi.getEnvironmentVariables(undefined); + + expect(result).to.deep.equal(expectedVars); + // OLD API: Using getEnvironmentVariables() instead of environments.getEnvironmentVariables + sinon.assert.calledWith(mockPythonEnvApi.getEnvironmentVariables, undefined); + }); + }); + + suite('resolveEnvironment', () => { + test('Should resolve environment from path string', async () => { + const envPath = '/usr/bin/python3'; + // Use buildPythonEnvironment for realistic mock + const expectedEnv = buildPythonEnvironment(envPath, '3.9.0'); + + // OLD API: Using resolveEnvironment() instead of environments.resolveEnvironment + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.resolveEnvironment.resolves(expectedEnv); + + const result = await pythonApi.resolveEnvironment(envPath); + + expect(result).to.deep.equal(expectedEnv); + // OLD API: Using resolveEnvironment() instead of environments.resolveEnvironment + // sinon.assert.calledWith(mockPythonEnvApi.resolveEnvironment, envPath); + }); + + test('Should resolve environment from Environment object', async () => { + const env: Environment = { + id: 'test-env', + path: '/usr/bin/python3', + version: { major: 3, minor: 9, micro: 0 }, + } as Environment; + // Use buildPythonEnvironment for realistic mock + const expectedEnv = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); + + // OLD API: Using resolveEnvironment() instead of environments.resolveEnvironment + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.resolveEnvironment.resolves(expectedEnv); + + const result = await pythonApi.resolveEnvironment(expectedEnv.environmentPath.fsPath); + + expect(result).to.deep.equal(expectedEnv); + }); + + test('Should return undefined for invalid environment', async () => { + const envPath = '/invalid/path'; + // OLD API: Using resolveEnvironment() instead of environments.resolveEnvironment + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.resolveEnvironment.resolves(undefined); + + const result = await pythonApi.resolveEnvironment(envPath); + + expect(result).to.be.undefined; + }); + }); + + suite('getActiveEnvironmentPath', () => { + test('Should return active environment path', async () => { + const expectedPath: EnvironmentPath = { + id: 'test-env', + path: '/usr/bin/python3', + }; + // OLD API: Using getEnvironment() instead of environments.getActiveEnvironmentPath + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironment.returns(expectedPath); + + const result = await pythonApi.getActiveEnvironmentPath(); + + expect(result).to.deep.equal(expectedPath); + }); + + test('Should return active environment path for specific resource', async () => { + const resource = Uri.file('/workspace/file.py'); + const expectedPath: EnvironmentPath = { + id: 'test-env', + path: '/usr/bin/python3', + }; + // OLD API: Using getEnvironment() instead of environments.getActiveEnvironmentPath + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironment.returns(expectedPath); + + const result = await pythonApi.getActiveEnvironmentPath(resource); + + expect(result).to.deep.equal(expectedPath); + // OLD API: Using getEnvironment() instead of environments.getActiveEnvironmentPath + sinon.assert.calledWith(mockPythonEnvApi.getEnvironment, resource); + }); + }); + + suite('getInterpreterDetails', () => { + test('Should return interpreter details without resource', async () => { + const pythonPath = '/usr/bin/python3'; + const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); + + // OLD API: Using getEnvironment() and resolveEnvironment() instead of environments.* + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file(pythonPath) }); + mockPythonEnvApi.resolveEnvironment.resolves(mockEnv); + + const result = await pythonApi.getInterpreterDetails(); + + // Use Uri.file().fsPath to get platform-normalized path for comparison + expect(result.path).to.deep.equal([Uri.file(pythonPath).fsPath]); + expect(result.resource).to.be.undefined; + }); + + test('Should return interpreter details with resource', async () => { + const resource = Uri.file('/workspace/file.py'); + const pythonPath = '/usr/bin/python3'; + const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); + + // OLD API: Using getEnvironment() and resolveEnvironment() instead of environments.* + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file(pythonPath) }); + mockPythonEnvApi.resolveEnvironment.resolves(mockEnv); + + const result = await pythonApi.getInterpreterDetails(resource); + + // Use Uri.file().fsPath to get platform-normalized path for comparison + expect(result.path).to.deep.equal([Uri.file(pythonPath).fsPath]); + expect(result.resource).to.deep.equal(resource); + }); + + test('Should not quote path with spaces', async () => { + // this should be updated when we fix the quoting logic in getInterpreterDetails + const pythonPath = '/path with spaces/python3'; + const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); + + // OLD API: Using getEnvironment() and resolveEnvironment() instead of environments.* + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file(pythonPath) }); + mockPythonEnvApi.resolveEnvironment.resolves(mockEnv); + + const result = await pythonApi.getInterpreterDetails(); + + expect(result.path).to.deep.equal([`${pythonPath}`]); + }); + + test('Should not double-quote already quoted path', async () => { + const quotedPython = Uri.file('"/path with spaces/python3"'); + const quotedPythonPath = quotedPython.fsPath; + const mockEnv = buildPythonEnvironment(quotedPythonPath, '3.9.0'); + + // OLD API: Using getEnvironment() and resolveEnvironment() instead of environments.* + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironment.returns({ environmentPath: quotedPython }); + mockPythonEnvApi.resolveEnvironment.resolves(mockEnv); + + const result = await pythonApi.getInterpreterDetails(); + + expect(result.path).to.deep.equal([quotedPythonPath]); + }); + + test('Should return undefined path when environment is not resolved', async () => { + // OLD API: Using getEnvironment() and resolveEnvironment() instead of environments.* + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file('/usr/bin/python3') }); + mockPythonEnvApi.resolveEnvironment.resolves(undefined); + + const result = await pythonApi.getInterpreterDetails(); + + expect(result.path).to.be.undefined; + expect(result.resource).to.be.undefined; + }); + + test('Should return undefined path when executable uri is not available', async () => { + const mockEnv = { + id: 'test-env', + execInfo: { + run: { executable: undefined, args: [] }, + }, + } as any; + + // OLD API: Using getEnvironment() and resolveEnvironment() instead of environments.* + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file('/usr/bin/python3') }); + mockPythonEnvApi.resolveEnvironment.resolves(mockEnv); + + const result = await pythonApi.getInterpreterDetails(); + + expect(result.path).to.be.undefined; + }); + }); + + suite('onDidChangePythonInterpreter event', () => { + test('Should fire event when active environment path changes', async () => { + const disposables: Disposable[] = []; + let eventCallback: any; + const mockEventHandler = sinon.stub(); + const pythonPath = '/usr/bin/python3.9'; + const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); + + // OLD API: Using onDidChangeEnvironments instead of onDidChangeActiveEnvironmentPath + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.onDidChangeEnvironments = (callback: any) => { + eventCallback = callback; + return { dispose: sinon.stub() }; + }; + // Set up mocks for getInterpreterDetails() call in event handler + mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file(pythonPath) }); + mockPythonEnvApi.resolveEnvironment.resolves(mockEnv); + + pythonApi.onDidChangePythonInterpreter(mockEventHandler); + + await pythonApi.initializePython(disposables); + + // Trigger the environment change event + await eventCallback(); + + // Should be called at least twice: once during init, once from the event + expect(mockEventHandler.callCount).to.be.greaterThan(1); + const lastCall = mockEventHandler.lastCall.args[0]; + expect(lastCall.path).to.deep.equal([pythonPath]); + expect(lastCall.resource).to.be.undefined; + }); + + test('Should handle WorkspaceFolder resource in event', async () => { + const disposables: Disposable[] = []; + let eventCallback: any; + const mockEventHandler = sinon.stub(); + const pythonPath = '/usr/bin/python3.9'; + const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); + + // OLD API: Using onDidChangeEnvironments instead of onDidChangeActiveEnvironmentPath + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.onDidChangeEnvironments = (callback: any) => { + eventCallback = callback; + return { dispose: sinon.stub() }; + }; + // Set up mocks for getInterpreterDetails() call in event handler + mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file(pythonPath) }); + mockPythonEnvApi.resolveEnvironment.resolves(mockEnv); + + pythonApi.onDidChangePythonInterpreter(mockEventHandler); + + await pythonApi.initializePython(disposables); + + // Trigger the environment change event + await eventCallback(); + + const lastCall = mockEventHandler.lastCall.args[0]; + expect(lastCall.path).to.deep.equal([pythonPath]); + expect(lastCall.resource).to.be.undefined; + }); + + test('Should handle null resource in event', async () => { + const disposables: Disposable[] = []; + let eventCallback: any; + const mockEventHandler = sinon.stub(); + const pythonPath = '/usr/bin/python3.9'; + const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); + + // OLD API: Using onDidChangeEnvironments instead of onDidChangeActiveEnvironmentPath + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.onDidChangeEnvironments = (callback: any) => { + eventCallback = callback; + return { dispose: sinon.stub() }; + }; + // Set up mocks for getInterpreterDetails() call in event handler + mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file(pythonPath) }); + mockPythonEnvApi.resolveEnvironment.resolves(mockEnv); + + pythonApi.onDidChangePythonInterpreter(mockEventHandler); + + await pythonApi.initializePython(disposables); + + // Trigger the environment change event + await eventCallback(); + + const lastCall = mockEventHandler.lastCall.args[0]; + expect(lastCall.path).to.deep.equal([pythonPath]); + expect(lastCall.resource).to.be.undefined; + }); + + test('Should handle undefined resource in event', async () => { + const disposables: Disposable[] = []; + let eventCallback: any; + const mockEventHandler = sinon.stub(); + const pythonPath = '/usr/bin/python3.9'; + const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); + + // OLD API: Using onDidChangeEnvironments instead of onDidChangeActiveEnvironmentPath + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.onDidChangeEnvironments = (callback: any) => { + eventCallback = callback; + return { dispose: sinon.stub() }; + }; + // Set up mocks for getInterpreterDetails() call in event handler + mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file(pythonPath) }); + mockPythonEnvApi.resolveEnvironment.resolves(mockEnv); + + pythonApi.onDidChangePythonInterpreter(mockEventHandler); + + await pythonApi.initializePython(disposables); + + // Trigger the environment change event + await eventCallback(); + + const lastCall = mockEventHandler.lastCall.args[0]; + expect(lastCall.path).to.deep.equal([pythonPath]); + expect(lastCall.resource).to.be.undefined; + }); + + test('Should handle event with missing id', async () => { + const disposables: Disposable[] = []; + let eventCallback: any; + const mockEventHandler = sinon.stub(); + const pythonPath = '/usr/bin/python3.9'; + const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); + + // OLD API: Using onDidChangeEnvironments instead of onDidChangeActiveEnvironmentPath + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.onDidChangeEnvironments = (callback: any) => { + eventCallback = callback; + return { dispose: sinon.stub() }; + }; + // Set up mocks for getInterpreterDetails() call in event handler + mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file(pythonPath) }); + mockPythonEnvApi.resolveEnvironment.resolves(mockEnv); + + pythonApi.onDidChangePythonInterpreter(mockEventHandler); + + await pythonApi.initializePython(disposables); + + // Trigger the environment change event + await eventCallback(); + + const lastCall = mockEventHandler.lastCall.args[0]; + expect(lastCall.path).to.deep.equal([pythonPath]); + expect(lastCall.resource).to.be.undefined; + }); + + test('Should handle event with null path', async () => { + const disposables: Disposable[] = []; + let eventCallback: any; + const mockEventHandler = sinon.stub(); + + // OLD API: Using onDidChangeEnvironments instead of onDidChangeActiveEnvironmentPath + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.onDidChangeEnvironments = (callback: any) => { + eventCallback = callback; + return { dispose: sinon.stub() }; + }; + // Test case where getEnvironment returns no environment (null path case) + mockPythonEnvApi.getEnvironment.returns(undefined); + mockPythonEnvApi.resolveEnvironment.resolves(undefined); + + pythonApi.onDidChangePythonInterpreter(mockEventHandler); + + await pythonApi.initializePython(disposables); + + // Trigger the environment change event + await eventCallback(); + + const lastCall = mockEventHandler.lastCall.args[0]; + expect(lastCall.path).to.be.undefined; + expect(lastCall.resource).to.be.undefined; + }); + }); +}); diff --git a/src/test/unittest/configuration/resolvers/launch.unit.test.ts b/src/test/unittest/configuration/resolvers/launch.unit.test.ts index d15e2cdf..68dd5003 100644 --- a/src/test/unittest/configuration/resolvers/launch.unit.test.ts +++ b/src/test/unittest/configuration/resolvers/launch.unit.test.ts @@ -123,16 +123,19 @@ getInfoPerOS().forEach(([osName, osType, path]) => { if (config === undefined || config === null) { return config; } - const interpreterPath = await pythonApi.getActiveEnvironmentPath( + const activePythonEnv = await pythonApi.getActiveEnvironmentPath( workspaceFolder ? workspaceFolder.uri : undefined, ); + let interpreterPath = ''; + if (activePythonEnv && 'environmentPath' in activePythonEnv) { + interpreterPath = activePythonEnv.environmentPath.fsPath; + } else if (activePythonEnv && 'path' in activePythonEnv) { + interpreterPath = activePythonEnv.path; + } for (const key of Object.keys(config)) { const value = config[key]; if (typeof value === 'string') { - config[key] = value.replace( - '${command:python.interpreterPath}', - interpreterPath ? interpreterPath.path : '', - ); + config[key] = value.replace('${command:python.interpreterPath}', interpreterPath); } } From cec4929c89ea4f5d775949538426d2a6cfc4fc6b Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:49:01 -0700 Subject: [PATCH 03/13] rebased and fixed --- .../commands/reportIssueCommand.ts | 2 +- src/extension/common/python.ts | 2 +- src/extension/debugger/adapter/factory.ts | 2 +- src/test/unittest/common/python.unit.test.ts | 76 ------------------- .../unittest/common/pythonTrue.unit.test.ts | 7 +- 5 files changed, 4 insertions(+), 85 deletions(-) diff --git a/src/extension/common/application/commands/reportIssueCommand.ts b/src/extension/common/application/commands/reportIssueCommand.ts index d24ae726..b687d25e 100644 --- a/src/extension/common/application/commands/reportIssueCommand.ts +++ b/src/extension/common/application/commands/reportIssueCommand.ts @@ -6,11 +6,11 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import { executeCommand } from '../../vscodeapi'; -import { getActiveEnvironmentPath, resolveEnvironment } from '../../python'; import { EXTENSION_ROOT_DIR } from '../../constants'; import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { PythonEnvironment } from '../../../envExtApi'; +import { getActiveEnvironmentPath, resolveEnvironment } from '../../python'; /** * Allows the user to report an issue related to the Python Debugger extension using our template. diff --git a/src/extension/common/python.ts b/src/extension/common/python.ts index b50f5ff8..c2490047 100644 --- a/src/extension/common/python.ts +++ b/src/extension/common/python.ts @@ -3,7 +3,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Environment, EnvironmentPath, ResolvedEnvironment, Resource } from '@vscode/python-extension'; -import { commands, EventEmitter, extensions, Uri, Event, Disposable } from 'vscode'; +import { commands, EventEmitter, extensions, Uri, Event, Disposable, Extension } from 'vscode'; import { traceError, traceLog, traceWarn } from './log/logging'; import { PythonEnvironment, PythonEnvironmentApi, PythonEnvsExtension } from '../envExtApi'; import { diff --git a/src/extension/debugger/adapter/factory.ts b/src/extension/debugger/adapter/factory.ts index 7f07d41a..bd360267 100644 --- a/src/extension/debugger/adapter/factory.ts +++ b/src/extension/debugger/adapter/factory.ts @@ -19,12 +19,12 @@ import { executeCommand, showErrorMessage } from '../../common/vscodeapi'; import { traceLog, traceVerbose } from '../../common/log/logging'; import { EventName } from '../../telemetry/constants'; import { sendTelemetryEvent } from '../../telemetry'; -import { getInterpreterDetails, resolveEnvironment, runPythonExtensionCommand } from '../../common/python'; import { Commands, EXTENSION_ROOT_DIR } from '../../common/constants'; import { Common, DebugConfigStrings, Interpreters } from '../../common/utils/localize'; import { IPersistentStateFactory } from '../../common/types'; import { fileToCommandArgumentForPythonExt } from '../../common/stringUtils'; import { PythonEnvironment } from '../../envExtApi'; +import { resolveEnvironment, getInterpreterDetails, runPythonExtensionCommand } from '../../common/python'; // persistent state names, exported to make use of in testing export enum debugStateKeys { diff --git a/src/test/unittest/common/python.unit.test.ts b/src/test/unittest/common/python.unit.test.ts index 9b12e5c4..902a3171 100644 --- a/src/test/unittest/common/python.unit.test.ts +++ b/src/test/unittest/common/python.unit.test.ts @@ -415,82 +415,6 @@ suite('Python API Tests', () => { }); }); - suite('hasInterpreters', () => { - test('Should return true when interpreters are available initially', async () => { - mockPythonEnvApi.environments.known = [{ id: 'env1', path: '/usr/bin/python3' } as Environment]; - sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); - - const result = await pythonApi.hasInterpreters(); - - expect(result).to.be.true; - }); - - test('Should return false when no interpreters are available', async () => { - mockPythonEnvApi.environments.known = []; - mockPythonEnvApi.environments.refreshEnvironments.resolves(); - sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); - - const result = await pythonApi.hasInterpreters(); - - expect(result).to.be.false; - }); - - test('Should wait for environments to be added after refresh', async () => { - mockPythonEnvApi.environments.known = []; - let onDidChangeCallback: any; - mockPythonEnvApi.environments.onDidChangeEnvironments = (callback: any) => { - onDidChangeCallback = callback; - return { dispose: sinon.stub() }; - }; - mockPythonEnvApi.environments.refreshEnvironments = async () => { - // Simulate environments being added - mockPythonEnvApi.environments.known = [{ id: 'env1', path: '/usr/bin/python3' } as Environment]; - if (onDidChangeCallback) { - onDidChangeCallback(); - } - }; - - sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); - - const result = await pythonApi.hasInterpreters(); - - expect(result).to.be.true; - }); - }); - - suite('getInterpreters', () => { - test('Should return list of known interpreters', async () => { - const expectedEnvs: readonly Environment[] = [ - { id: 'env1', path: '/usr/bin/python3' } as Environment, - { id: 'env2', path: '/usr/bin/python2' } as Environment, - ]; - mockPythonEnvApi.environments.known = expectedEnvs; - sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); - - const result = await pythonApi.getInterpreters(); - - expect(result).to.deep.equal(expectedEnvs); - }); - - test('Should return empty array when no interpreters are available', async () => { - mockPythonEnvApi.environments.known = []; - sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); - - const result = await pythonApi.getInterpreters(); - - expect(result).to.deep.equal([]); - }); - - test('Should return empty array when known is null', async () => { - mockPythonEnvApi.environments.known = null; - sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); - - const result = await pythonApi.getInterpreters(); - - expect(result).to.deep.equal([]); - }); - }); - suite('onDidChangePythonInterpreter event', () => { test('Should fire event when active environment path changes', async () => { const disposables: Disposable[] = []; diff --git a/src/test/unittest/common/pythonTrue.unit.test.ts b/src/test/unittest/common/pythonTrue.unit.test.ts index bf6f9fd8..8fa57011 100644 --- a/src/test/unittest/common/pythonTrue.unit.test.ts +++ b/src/test/unittest/common/pythonTrue.unit.test.ts @@ -8,7 +8,7 @@ import * as sinon from 'sinon'; import { Uri, Disposable, Extension, extensions } from 'vscode'; import * as pythonApi from '../../../extension/common/python'; import * as utilities from '../../../extension/common/utilities'; -import { Environment, EnvironmentPath } from '@vscode/python-extension'; +import { EnvironmentPath } from '@vscode/python-extension'; import { buildPythonEnvironment } from './helpers'; suite('Python API Tests- useEnvironmentsExtension:true', () => { @@ -218,11 +218,6 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { }); test('Should resolve environment from Environment object', async () => { - const env: Environment = { - id: 'test-env', - path: '/usr/bin/python3', - version: { major: 3, minor: 9, micro: 0 }, - } as Environment; // Use buildPythonEnvironment for realistic mock const expectedEnv = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); From 464d6e2587fa936b5706615683909c703dd7bf84 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:42:42 -0700 Subject: [PATCH 04/13] support per file debug with correct interpreter --- .../debugConfigurationService.ts | 17 +++++++++++++++-- .../debugger/configuration/resolvers/base.ts | 19 ++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/extension/debugger/configuration/debugConfigurationService.ts b/src/extension/debugger/configuration/debugConfigurationService.ts index 8776f9b6..900f911a 100644 --- a/src/extension/debugger/configuration/debugConfigurationService.ts +++ b/src/extension/debugger/configuration/debugConfigurationService.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - +import * as fs from 'fs'; import { cloneDeep } from 'lodash'; -import { CancellationToken, DebugConfiguration, QuickPickItem, WorkspaceFolder } from 'vscode'; +import { CancellationToken, DebugConfiguration, QuickPickItem, Uri, WorkspaceFolder } from 'vscode'; import { DebugConfigStrings } from '../../common/utils/localize'; import { IMultiStepInputFactory, InputStep, IQuickPickParameters, MultiStepInput } from '../../common/multiStepInput'; import { AttachRequestArguments, DebugConfigurationArguments, LaunchRequestArguments } from '../../types'; @@ -17,6 +17,7 @@ import { buildPyramidLaunchConfiguration } from './providers/pyramidLaunch'; import { buildRemoteAttachConfiguration } from './providers/remoteAttach'; import { IDebugConfigurationResolver } from './types'; import { buildFileWithArgsLaunchDebugConfiguration } from './providers/fileLaunchWithArgs'; +import { getInterpreterDetails } from '../../common/python'; export class PythonDebugConfigurationService implements IDebugConfigurationService { private cacheDebugConfig: DebugConfiguration | undefined = undefined; @@ -90,6 +91,18 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi debugConfiguration: DebugConfiguration, token?: CancellationToken, ): Promise { + // now that ${file} is resolved, we can use it to get the interpreter for that file + if (debugConfiguration.program !== undefined) { + if (debugConfiguration.python === undefined) { + // If program is a valid file, get interpreter for that file + if (fs.existsSync(debugConfiguration.program) && fs.statSync(debugConfiguration.program).isFile()) { + const interpreter = await getInterpreterDetails(Uri.file(debugConfiguration.program)); + if (interpreter?.path && interpreter.path.length > 0) { + debugConfiguration.python = interpreter.path[0]; + } + } + } + } function resolve(resolver: IDebugConfigurationResolver) { return resolver.resolveDebugConfigurationWithSubstitutedVariables(folder, debugConfiguration as T, token); } diff --git a/src/extension/debugger/configuration/resolvers/base.ts b/src/extension/debugger/configuration/resolvers/base.ts index 8b887107..8dc477a6 100644 --- a/src/extension/debugger/configuration/resolvers/base.ts +++ b/src/extension/debugger/configuration/resolvers/base.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. 'use strict'; - +import * as fs from 'fs'; import * as path from 'path'; import { CancellationToken, DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; import { sendTelemetryEvent } from '../../../telemetry'; @@ -137,9 +137,22 @@ export abstract class BaseConfigurationResolver if (!debugConfiguration) { return; } + let interpreterDetailsTarget = workspaceFolder; - // get the interpreter details in the context of the workspace folder - const interpreterDetail = await getInterpreterDetails(workspaceFolder); + if (debugConfiguration.program !== undefined) { + if (debugConfiguration.python === undefined) { + if (debugConfiguration.program === '${file}') { + // If program is ${file}, we cannot determine the interpreter yet + return; + } + // If program is a valid file, get interpreter for that file + if (fs.existsSync(debugConfiguration.program) && fs.statSync(debugConfiguration.program).isFile()) { + interpreterDetailsTarget = Uri.file(debugConfiguration.program); + } + } + } + // get the interpreter details in the context of either the workspace folder or the program file + const interpreterDetail = await getInterpreterDetails(interpreterDetailsTarget); const interpreterPath = interpreterDetail?.path ?? (await getSettingsPythonPath(workspaceFolder)); const resolvedInterpreterPath = interpreterPath ? interpreterPath[0] : interpreterPath; From 332d2ec37ab9bb7af755a5283047d075f4eff374 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:04:23 -0700 Subject: [PATCH 05/13] fix tests --- .../unittest/adapter/factory.unit.test.ts | 676 +++++++++--------- 1 file changed, 333 insertions(+), 343 deletions(-) diff --git a/src/test/unittest/adapter/factory.unit.test.ts b/src/test/unittest/adapter/factory.unit.test.ts index 2bbf455b..1616872a 100644 --- a/src/test/unittest/adapter/factory.unit.test.ts +++ b/src/test/unittest/adapter/factory.unit.test.ts @@ -1,343 +1,333 @@ -// /* eslint-disable @typescript-eslint/naming-convention */ -// // Copyright (c) Microsoft Corporation. All rights reserved. -// // Licensed under the MIT License. - -// 'use strict'; - -// import * as assert from 'assert'; -// import { expect, use } from 'chai'; -// import * as chaiAsPromised from 'chai-as-promised'; -// import * as path from 'path'; -// import * as sinon from 'sinon'; -// import { SemVer } from 'semver'; -// import { instance, mock, when } from 'ts-mockito'; -// import { DebugAdapterExecutable, DebugAdapterServer, DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; -// import { IPersistentStateFactory } from '../../../extension/common/types'; -// import { DebugAdapterDescriptorFactory, debugStateKeys } from '../../../extension/debugger/adapter/factory'; -// import { IDebugAdapterDescriptorFactory } from '../../../extension/debugger/types'; -// import { EventName } from '../../../extension/telemetry/constants'; -// import { PersistentState, PersistentStateFactory } from '../../../extension/common/persistentState'; -// import { EXTENSION_ROOT_DIR } from '../../../extension/common/constants'; -// import { Architecture } from '../../../extension/common/platform'; -// import * as pythonApi from '../../../extension/common/python'; -// import * as telemetry from '../../../extension/telemetry'; -// import * as telemetryReporter from '../../../extension/telemetry/reporter'; -// import * as vscodeApi from '../../../extension/common/vscodeapi'; -// import { DebugConfigStrings } from '../../../extension/common/utils/localize'; - -// use(chaiAsPromised); - -// suite('Debugging - Adapter Factory', () => { -// let factory: IDebugAdapterDescriptorFactory; -// let stateFactory: IPersistentStateFactory; -// let state: PersistentState; -// let showErrorMessageStub: sinon.SinonStub; -// let resolveEnvironmentStub: sinon.SinonStub; -// let getInterpretersStub: sinon.SinonStub; -// let getInterpreterDetailsStub: sinon.SinonStub; -// let hasInterpretersStub: sinon.SinonStub; -// let getTelemetryReporterStub: sinon.SinonStub; -// let reporter: any; - -// const nodeExecutable = undefined; -// const debugAdapterPath = path.join(EXTENSION_ROOT_DIR, 'bundled', 'libs', 'debugpy', 'adapter'); -// const pythonPath = 'path/to/python/interpreter'; -// const interpreter = { -// architecture: Architecture.Unknown, -// path: pythonPath, -// sysPrefix: '', -// sysVersion: '', -// envType: 'Unknow', -// version: new SemVer('3.7.4-test'), -// }; -// const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; -// const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; - -// class Reporter { -// public static eventNames: string[] = []; -// public static properties: Record[] = []; -// public static measures: {}[] = []; -// public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { -// Reporter.eventNames.push(eventName); -// Reporter.properties.push(properties!); -// Reporter.measures.push(measures!); -// } -// } - -// setup(() => { -// process.env.VSC_PYTHON_UNIT_TEST = undefined; -// process.env.VSC_PYTHON_CI_TEST = undefined; -// reporter = new Reporter(); - -// stateFactory = mock(PersistentStateFactory); -// state = mock(PersistentState) as PersistentState; -// showErrorMessageStub = sinon.stub(vscodeApi, 'showErrorMessage'); -// resolveEnvironmentStub = sinon.stub(pythonApi, 'resolveEnvironment'); -// getInterpretersStub = sinon.stub(pythonApi, 'getInterpreters'); -// getInterpreterDetailsStub = sinon.stub(pythonApi, 'getInterpreterDetails'); -// hasInterpretersStub = sinon.stub(pythonApi, 'hasInterpreters'); -// getTelemetryReporterStub = sinon.stub(telemetryReporter, 'getTelemetryReporter'); - -// when( -// stateFactory.createGlobalPersistentState(debugStateKeys.doNotShowAgain, false), -// ).thenReturn(instance(state)); -// getInterpretersStub.returns([interpreter]); -// hasInterpretersStub.returns(true); -// getTelemetryReporterStub.returns(reporter); -// factory = new DebugAdapterDescriptorFactory(instance(stateFactory)); -// }); - -// teardown(() => { -// process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; -// process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; -// Reporter.properties = []; -// Reporter.eventNames = []; -// Reporter.measures = []; -// telemetry.clearTelemetryReporter(); -// sinon.restore(); -// }); - -// function createSession(config: Partial, workspaceFolder?: WorkspaceFolder): DebugSession { -// return { -// configuration: { name: '', request: 'launch', type: 'python', ...config }, -// id: '', -// name: 'python', -// type: 'python', -// workspaceFolder, -// customRequest: () => Promise.resolve(), -// getDebugProtocolBreakpoint: () => Promise.resolve(undefined), -// }; -// } - -// test('Return the value of configuration.pythonPath as the current python path if it exists', async () => { -// const session = createSession({ pythonPath }); -// const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - -// resolveEnvironmentStub.withArgs(pythonPath).resolves(interpreter); -// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// assert.deepStrictEqual(descriptor, debugExecutable); -// }); - -// test('Return the path of the active interpreter as the current python path, it exists and configuration.pythonPath is not defined', async () => { -// const session = createSession({}); -// const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); -// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); -// resolveEnvironmentStub.resolves(interpreter); -// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// assert.deepStrictEqual(descriptor, debugExecutable); -// }); - -// test('Display a message if no python interpreter is set', async () => { -// getInterpreterDetailsStub.resolves(undefined); -// const session = createSession({}); -// const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// await expect(promise).to.eventually.be.rejectedWith(DebugConfigStrings.debugStopped); - -// //check error message -// sinon.assert.calledOnce(showErrorMessageStub); -// }); - -// test('Display a message if python version is less than 3.7', async () => { -// getInterpretersStub.returns([]); -// const session = createSession({}); -// const deprecatedInterpreter = { -// architecture: Architecture.Unknown, -// path: pythonPath, -// sysPrefix: '', -// sysVersion: '', -// envType: 'Unknown', -// version: new SemVer('3.6.12-test'), -// }; -// when(state.value).thenReturn(false); -// getInterpreterDetailsStub.resolves({ path: [deprecatedInterpreter.path] }); -// resolveEnvironmentStub.resolves(deprecatedInterpreter); - -// await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// sinon.assert.calledOnce(showErrorMessageStub); -// }); - -// test('Return Debug Adapter server if request is "attach", and port is specified directly', async () => { -// const session = createSession({ request: 'attach', port: 5678, host: 'localhost' }); -// const debugServer = new DebugAdapterServer(session.configuration.port, session.configuration.host); -// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// // Interpreter not needed for host/port -// sinon.assert.neverCalledWith(getInterpretersStub); - -// assert.deepStrictEqual(descriptor, debugServer); -// }); - -// test('Return Debug Adapter server if request is "attach", and connect is specified', async () => { -// const session = createSession({ request: 'attach', connect: { port: 5678, host: 'localhost' } }); -// const debugServer = new DebugAdapterServer( -// session.configuration.connect.port, -// session.configuration.connect.host, -// ); - -// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// // Interpreter not needed for connect -// sinon.assert.neverCalledWith(getInterpretersStub); -// assert.deepStrictEqual(descriptor, debugServer); -// }); - -// test('Return Debug Adapter server if request is "attach", and connect is specified with port as string', async () => { -// const session = createSession({ request: 'attach', connect: { port: '5678', host: 'localhost' } }); -// const debugServer = new DebugAdapterServer(5678, session.configuration.connect.host); -// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// // Interpreter not needed for connect -// sinon.assert.neverCalledWith(getInterpretersStub); -// assert.deepStrictEqual(descriptor, debugServer); -// }); - -// test('Return Debug Adapter executable if request is "attach", and listen is specified', async () => { -// const session = createSession({ request: 'attach', listen: { port: 5678, host: 'localhost' } }); -// const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); -// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); -// resolveEnvironmentStub.resolves(interpreter); -// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// assert.deepStrictEqual(descriptor, debugExecutable); -// }); - -// test('Throw error if request is "attach", and neither port, processId, listen, nor connect is specified', async () => { -// const session = createSession({ -// request: 'attach', -// port: undefined, -// processId: undefined, -// listen: undefined, -// connect: undefined, -// }); - -// const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// await expect(promise).to.eventually.be.rejectedWith( -// '"request":"attach" requires either "connect", "listen", or "processId"', -// ); -// }); - -// test('Pass the --log-dir argument to debug adapter if configuration.logToFile is set', async () => { -// const session = createSession({ logToFile: true }); -// const debugExecutable = new DebugAdapterExecutable(pythonPath, [ -// debugAdapterPath, -// '--log-dir', -// EXTENSION_ROOT_DIR, -// ]); - -// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); -// resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); - -// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// assert.deepStrictEqual(descriptor, debugExecutable); -// }); - -// test("Don't pass the --log-dir argument to debug adapter if configuration.logToFile is not set", async () => { -// const session = createSession({}); -// const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - -// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); -// resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); - -// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// assert.deepStrictEqual(descriptor, debugExecutable); -// }); - -// test("Don't pass the --log-dir argument to debugger if configuration.logToFile is set to false", async () => { -// const session = createSession({ logToFile: false }); -// const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - -// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); -// resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); - -// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// assert.deepStrictEqual(descriptor, debugExecutable); -// }); - -// test('Send attach to local process telemetry if attaching to a local process', async () => { -// const session = createSession({ request: 'attach', processId: 1234 }); -// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); -// resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); - -// await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// assert.ok(Reporter.eventNames.includes(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS)); -// }); - -// test("Don't send any telemetry if not attaching to a local process", async () => { -// const session = createSession({}); -// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); -// resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); - -// await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// assert.ok(Reporter.eventNames.includes(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH)); -// }); - -// test('Use "debugAdapterPath" when specified', async () => { -// const customAdapterPath = 'custom/debug/adapter/path'; -// const session = createSession({ debugAdapterPath: customAdapterPath }); -// const debugExecutable = new DebugAdapterExecutable(pythonPath, [customAdapterPath]); -// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); -// resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); -// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// assert.deepStrictEqual(descriptor, debugExecutable); -// }); -// test('Add quotes to interpreter path with spaces', async () => { -// const customAdapterPath = 'custom/debug/adapter/customAdapterPath'; -// const session = createSession({ debugAdapterPath: customAdapterPath }); -// const interpreterPathSpaces = 'path/to/python interpreter with spaces'; -// const interpreterPathSpacesQuoted = `"${interpreterPathSpaces}"`; -// const debugExecutable = new DebugAdapterExecutable(interpreterPathSpacesQuoted, [customAdapterPath]); - -// getInterpreterDetailsStub.resolves({ path: [interpreterPathSpaces] }); -// const interpreterSpacePath = { -// architecture: Architecture.Unknown, -// path: interpreterPathSpaces, -// sysPrefix: '', -// sysVersion: '', -// envType: 'Unknow', -// version: new SemVer('3.7.4-test'), -// }; -// resolveEnvironmentStub.withArgs(interpreterPathSpaces).resolves(interpreterSpacePath); -// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// assert.deepStrictEqual(descriptor, debugExecutable); -// }); - -// test('Use "debugAdapterPython" when specified', async () => { -// const session = createSession({ debugAdapterPython: '/bin/custompy' }); -// const debugExecutable = new DebugAdapterExecutable('/bin/custompy', [debugAdapterPath]); -// const customInterpreter = { -// architecture: Architecture.Unknown, -// path: '/bin/custompy', -// sysPrefix: '', -// sysVersion: '', -// envType: 'unknow', -// version: new SemVer('3.7.4-test'), -// }; - -// resolveEnvironmentStub.withArgs('/bin/custompy').resolves(customInterpreter); -// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// assert.deepStrictEqual(descriptor, debugExecutable); -// }); - -// test('Do not use "python" to spawn the debug adapter', async () => { -// const session = createSession({ python: '/bin/custompy' }); -// const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); -// getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); -// resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); -// const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - -// assert.deepStrictEqual(descriptor, debugExecutable); -// }); -// }); +/* eslint-disable @typescript-eslint/naming-convention */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { SemVer } from 'semver'; +import { instance, mock, when } from 'ts-mockito'; +import { DebugAdapterExecutable, DebugAdapterServer, DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; +import { IPersistentStateFactory } from '../../../extension/common/types'; +import { DebugAdapterDescriptorFactory, debugStateKeys } from '../../../extension/debugger/adapter/factory'; +import { IDebugAdapterDescriptorFactory } from '../../../extension/debugger/types'; +import { EventName } from '../../../extension/telemetry/constants'; +import { PersistentState, PersistentStateFactory } from '../../../extension/common/persistentState'; +import { EXTENSION_ROOT_DIR } from '../../../extension/common/constants'; +import { Architecture } from '../../../extension/common/platform'; +import * as pythonApi from '../../../extension/common/python'; +import * as telemetry from '../../../extension/telemetry'; +import * as telemetryReporter from '../../../extension/telemetry/reporter'; +import * as vscodeApi from '../../../extension/common/vscodeapi'; +import { DebugConfigStrings } from '../../../extension/common/utils/localize'; + +use(chaiAsPromised); + +suite('Debugging - Adapter Factory', () => { + let factory: IDebugAdapterDescriptorFactory; + let stateFactory: IPersistentStateFactory; + let state: PersistentState; + let showErrorMessageStub: sinon.SinonStub; + let resolveEnvironmentStub: sinon.SinonStub; + let getInterpreterDetailsStub: sinon.SinonStub; + let getTelemetryReporterStub: sinon.SinonStub; + let reporter: any; + + const nodeExecutable = undefined; + const debugAdapterPath = path.join(EXTENSION_ROOT_DIR, 'bundled', 'libs', 'debugpy', 'adapter'); + const pythonPath = 'path/to/python/interpreter'; + const interpreter = { + architecture: Architecture.Unknown, + path: pythonPath, + sysPrefix: '', + sysVersion: '', + envType: 'Unknow', + version: new SemVer('3.7.4-test'), + }; + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + + class Reporter { + public static eventNames: string[] = []; + public static properties: Record[] = []; + public static measures: {}[] = []; + public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { + Reporter.eventNames.push(eventName); + Reporter.properties.push(properties!); + Reporter.measures.push(measures!); + } + } + + setup(() => { + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + reporter = new Reporter(); + + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState) as PersistentState; + showErrorMessageStub = sinon.stub(vscodeApi, 'showErrorMessage'); + resolveEnvironmentStub = sinon.stub(pythonApi, 'resolveEnvironment'); + getInterpreterDetailsStub = sinon.stub(pythonApi, 'getInterpreterDetails'); + getTelemetryReporterStub = sinon.stub(telemetryReporter, 'getTelemetryReporter'); + + when( + stateFactory.createGlobalPersistentState(debugStateKeys.doNotShowAgain, false), + ).thenReturn(instance(state)); + getTelemetryReporterStub.returns(reporter); + factory = new DebugAdapterDescriptorFactory(instance(stateFactory)); + }); + + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + Reporter.properties = []; + Reporter.eventNames = []; + Reporter.measures = []; + telemetry.clearTelemetryReporter(); + sinon.restore(); + }); + + function createSession(config: Partial, workspaceFolder?: WorkspaceFolder): DebugSession { + return { + configuration: { name: '', request: 'launch', type: 'python', ...config }, + id: '', + name: 'python', + type: 'python', + workspaceFolder, + customRequest: () => Promise.resolve(), + getDebugProtocolBreakpoint: () => Promise.resolve(undefined), + }; + } + + test('Return the value of configuration.pythonPath as the current python path if it exists', async () => { + const session = createSession({ pythonPath }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + resolveEnvironmentStub.withArgs(pythonPath).resolves(interpreter); + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Return the path of the active interpreter as the current python path, it exists and configuration.pythonPath is not defined', async () => { + const session = createSession({}); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); + resolveEnvironmentStub.resolves(interpreter); + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Display a message if no python interpreter is set', async () => { + getInterpreterDetailsStub.resolves(undefined); + const session = createSession({}); + const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); + + await expect(promise).to.eventually.be.rejectedWith(DebugConfigStrings.debugStopped); + + //check error message + sinon.assert.calledOnce(showErrorMessageStub); + }); + + test('Display a message if python version is less than 3.7', async () => { + const session = createSession({}); + const deprecatedInterpreter = { + architecture: Architecture.Unknown, + path: pythonPath, + sysPrefix: '', + sysVersion: '', + envType: 'Unknown', + version: new SemVer('3.6.12-test'), + }; + when(state.value).thenReturn(false); + getInterpreterDetailsStub.resolves({ path: [deprecatedInterpreter.path] }); + resolveEnvironmentStub.resolves(deprecatedInterpreter); + + await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + sinon.assert.calledOnce(showErrorMessageStub); + }); + + test('Return Debug Adapter server if request is "attach", and port is specified directly', async () => { + const session = createSession({ request: 'attach', port: 5678, host: 'localhost' }); + const debugServer = new DebugAdapterServer(session.configuration.port, session.configuration.host); + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + // Interpreter not needed for host/port + + assert.deepStrictEqual(descriptor, debugServer); + }); + + test('Return Debug Adapter server if request is "attach", and connect is specified', async () => { + const session = createSession({ request: 'attach', connect: { port: 5678, host: 'localhost' } }); + const debugServer = new DebugAdapterServer( + session.configuration.connect.port, + session.configuration.connect.host, + ); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + // Interpreter not needed for connect + assert.deepStrictEqual(descriptor, debugServer); + }); + + test('Return Debug Adapter server if request is "attach", and connect is specified with port as string', async () => { + const session = createSession({ request: 'attach', connect: { port: '5678', host: 'localhost' } }); + const debugServer = new DebugAdapterServer(5678, session.configuration.connect.host); + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + // Interpreter not needed for connect + assert.deepStrictEqual(descriptor, debugServer); + }); + + test('Return Debug Adapter executable if request is "attach", and listen is specified', async () => { + const session = createSession({ request: 'attach', listen: { port: 5678, host: 'localhost' } }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); + resolveEnvironmentStub.resolves(interpreter); + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Throw error if request is "attach", and neither port, processId, listen, nor connect is specified', async () => { + const session = createSession({ + request: 'attach', + port: undefined, + processId: undefined, + listen: undefined, + connect: undefined, + }); + + const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); + + await expect(promise).to.eventually.be.rejectedWith( + '"request":"attach" requires either "connect", "listen", or "processId"', + ); + }); + + test('Pass the --log-dir argument to debug adapter if configuration.logToFile is set', async () => { + const session = createSession({ logToFile: true }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [ + debugAdapterPath, + '--log-dir', + EXTENSION_ROOT_DIR, + ]); + + getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); + resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test("Don't pass the --log-dir argument to debug adapter if configuration.logToFile is not set", async () => { + const session = createSession({}); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); + resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test("Don't pass the --log-dir argument to debugger if configuration.logToFile is set to false", async () => { + const session = createSession({ logToFile: false }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); + resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Send attach to local process telemetry if attaching to a local process', async () => { + const session = createSession({ request: 'attach', processId: 1234 }); + getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); + resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + + await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.ok(Reporter.eventNames.includes(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS)); + }); + + test("Don't send any telemetry if not attaching to a local process", async () => { + const session = createSession({}); + getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); + resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + + await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.ok(Reporter.eventNames.includes(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH)); + }); + + test('Use "debugAdapterPath" when specified', async () => { + const customAdapterPath = 'custom/debug/adapter/path'; + const session = createSession({ debugAdapterPath: customAdapterPath }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [customAdapterPath]); + getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); + resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + test('Add quotes to interpreter path with spaces', async () => { + const customAdapterPath = 'custom/debug/adapter/customAdapterPath'; + const session = createSession({ debugAdapterPath: customAdapterPath }); + const interpreterPathSpaces = 'path/to/python interpreter with spaces'; + const interpreterPathSpacesQuoted = `"${interpreterPathSpaces}"`; + const debugExecutable = new DebugAdapterExecutable(interpreterPathSpacesQuoted, [customAdapterPath]); + + getInterpreterDetailsStub.resolves({ path: [interpreterPathSpaces] }); + const interpreterSpacePath = { + architecture: Architecture.Unknown, + path: interpreterPathSpaces, + sysPrefix: '', + sysVersion: '', + envType: 'Unknow', + version: new SemVer('3.7.4-test'), + }; + resolveEnvironmentStub.withArgs(interpreterPathSpaces).resolves(interpreterSpacePath); + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Use "debugAdapterPython" when specified', async () => { + const session = createSession({ debugAdapterPython: '/bin/custompy' }); + const debugExecutable = new DebugAdapterExecutable('/bin/custompy', [debugAdapterPath]); + const customInterpreter = { + architecture: Architecture.Unknown, + path: '/bin/custompy', + sysPrefix: '', + sysVersion: '', + envType: 'unknow', + version: new SemVer('3.7.4-test'), + }; + + resolveEnvironmentStub.withArgs('/bin/custompy').resolves(customInterpreter); + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Do not use "python" to spawn the debug adapter', async () => { + const session = createSession({ python: '/bin/custompy' }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); + resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); +}); From 1896cdebbc9acff4701ea8f5e47a61dd3a1994ec Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 28 Oct 2025 15:05:50 -0700 Subject: [PATCH 06/13] add extra logging --- .../commands/reportIssueCommand.ts | 4 +++ src/extension/common/legacyPython.ts | 34 +++++++++++++++---- src/extension/common/python.ts | 31 +++++++++++++++-- src/extension/debugger/adapter/factory.ts | 5 +++ .../debugConfigurationService.ts | 7 ++++ 5 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/extension/common/application/commands/reportIssueCommand.ts b/src/extension/common/application/commands/reportIssueCommand.ts index b687d25e..f370e9cc 100644 --- a/src/extension/common/application/commands/reportIssueCommand.ts +++ b/src/extension/common/application/commands/reportIssueCommand.ts @@ -10,12 +10,14 @@ import { EXTENSION_ROOT_DIR } from '../../constants'; import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { PythonEnvironment } from '../../../envExtApi'; +import { traceLog } from '../../log/logging'; import { getActiveEnvironmentPath, resolveEnvironment } from '../../python'; /** * Allows the user to report an issue related to the Python Debugger extension using our template. */ export async function openReportIssue(): Promise { + traceLog('openReportIssue: Starting report issue flow'); const templatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_template.md'); const userDataTemplatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_user_data_template.md'); const template = await fs.readFile(templatePath, 'utf8'); @@ -30,6 +32,7 @@ export async function openReportIssue(): Promise { } const virtualEnvKind = interpreter && interpreter.envId ? interpreter.envId.managerId : 'Unknown'; const pythonVersion = interpreter?.version ?? 'unknown'; + traceLog(`openReportIssue: Resolved pythonVersion='${pythonVersion}' envKind='${virtualEnvKind}'`); await executeCommand('workbench.action.openIssueReporter', { extensionId: 'ms-python.debugpy', @@ -37,4 +40,5 @@ export async function openReportIssue(): Promise { data: userTemplate.replace('{0}', pythonVersion).replace('{1}', virtualEnvKind), }); sendTelemetryEvent(EventName.USE_REPORT_ISSUE_COMMAND, undefined, {}); + traceLog('openReportIssue: Issue reporter command executed'); } diff --git a/src/extension/common/legacyPython.ts b/src/extension/common/legacyPython.ts index a0ae5ed5..f8d01526 100644 --- a/src/extension/common/legacyPython.ts +++ b/src/extension/common/legacyPython.ts @@ -99,12 +99,14 @@ export async function legacyInitializePython( onDidChangePythonInterpreterEvent: EventEmitter, ): Promise { try { + traceLog('legacyInitializePython: Starting initialization'); const api = await legacyGetPythonExtensionEnviromentAPI(); if (api) { disposables.push( // This event is triggered when the active environment setting changes. api.environments.onDidChangeActiveEnvironmentPath((e: ActiveEnvironmentPathChangeEvent) => { + traceLog(`legacyInitializePython: Active environment path changed to '${e.path}'`); let resourceUri: Uri | undefined; if (e.resource instanceof Uri) { resourceUri = e.resource; @@ -119,6 +121,7 @@ export async function legacyInitializePython( traceLog('Waiting for interpreter from python extension.'); onDidChangePythonInterpreterEvent.fire(await legacyGetInterpreterDetails()); + traceLog('legacyInitializePython: Initial interpreter details fired'); } } catch (error) { traceError('Error initializing python: ', error); @@ -134,7 +137,9 @@ export async function legacyInitializePython( */ export async function legacyGetSettingsPythonPath(resource?: Uri): Promise { const api = await legacyGetPythonExtensionAPI(); - return api?.settings.getExecutionDetails(resource).execCommand; + const execCommand = api?.settings.getExecutionDetails(resource).execCommand; + traceLog(`legacyGetSettingsPythonPath: execCommand='${execCommand?.join(' ')}' resource='${resource?.fsPath}'`); + return execCommand; } /** @@ -157,7 +162,10 @@ export async function legacyResolveEnvironment( env: Environment | EnvironmentPath | string, ): Promise { const api = await legacyGetPythonExtensionEnviromentAPI(); - return api.environments.resolveEnvironment(env); + traceLog(`legacyResolveEnvironment: Resolving environment '${typeof env === 'string' ? env : (env as any).path}'`); + const resolved = api.environments.resolveEnvironment(env); + resolved.then((r) => traceLog(`legacyResolveEnvironment: Resolved executable='${r?.executable.uri?.fsPath}'`)); + return resolved; } /** @@ -168,7 +176,13 @@ export async function legacyResolveEnvironment( */ export async function legacyGetActiveEnvironmentPath(resource?: Resource): Promise { const api = await legacyGetPythonExtensionEnviromentAPI(); - return api.environments.getActiveEnvironmentPath(resource); + const active = api.environments.getActiveEnvironmentPath(resource); + traceLog( + `legacyGetActiveEnvironmentPath: activePath='${active.path}' resource='${ + (resource as any)?.uri?.fsPath || (resource as Uri)?.fsPath || '' + }'`, + ); + return active; } /** @@ -180,8 +194,12 @@ export async function legacyGetInterpreterDetails(resource?: Uri): Promise { }); const initialEnvs = api.environments.known; if (initialEnvs.length > 0) { + traceLog(`legacyHasInterpreters: Found ${initialEnvs.length} initial environments`); return true; } // Initiates a refresh of Python environments within the specified scope. await Promise.race([onAddedToCollection.promise, api?.environments.refreshEnvironments()]); - - return api.environments.known.length > 0; + const has = api.environments.known.length > 0; + traceLog(`legacyHasInterpreters: After refresh count='${api.environments.known.length}' result='${has}'`); + return has; } /** @@ -214,5 +234,7 @@ export async function legacyHasInterpreters(): Promise { */ export async function legacyGetInterpreters(): Promise { const api = await legacyGetPythonExtensionEnviromentAPI(); - return api.environments.known || []; + const known = api.environments.known || []; + traceLog(`legacyGetInterpreters: returning ${known.length} environments`); + return known; } diff --git a/src/extension/common/python.ts b/src/extension/common/python.ts index c2490047..0fa462d6 100644 --- a/src/extension/common/python.ts +++ b/src/extension/common/python.ts @@ -69,6 +69,7 @@ export async function getPythonEnvironmentExtensionAPI(): Promise { + traceLog(`initializePython: usingEnvExt='${useEnvExtension()}'`); if (!useEnvExtension()) { await legacyInitializePython(disposables, onDidChangePythonInterpreterEvent); } else { @@ -78,13 +79,16 @@ export async function initializePython(disposables: Disposable[]): Promise disposables.push( api.onDidChangeEnvironments(async () => { // not sure if this is the right event.... - onDidChangePythonInterpreterEvent.fire(await getInterpreterDetails()); - traceLog('Python environments changed.'); + const details = await getInterpreterDetails(); + traceLog(`initializePython:onDidChangeEnvironments fired executable='${details.path?.[0]}'`); + onDidChangePythonInterpreterEvent.fire(details); + traceLog('Python environments changed event processed.'); }), ); traceLog('Waiting for interpreter from python environments extension.'); onDidChangePythonInterpreterEvent.fire(await getInterpreterDetails()); + traceLog('initializePython: Initial interpreter details fired (env extension path)'); } } catch (error) { traceError('Error initializing python: ', error); @@ -94,6 +98,7 @@ export async function initializePython(disposables: Disposable[]): Promise export async function runPythonExtensionCommand(command: string, ...rest: any[]) { await activateExtensions(); + traceLog(`runPythonExtensionCommand: executing command='${command}' argsCount='${rest.length}'`); return await commands.executeCommand(command, ...rest); } @@ -113,21 +118,33 @@ export async function getSettingsPythonPath(resource?: Uri): Promise { + traceLog(`createDebugAdapterDescriptor: request='${session.configuration.request}' name='${session.name}'`); const configuration = session.configuration as LaunchRequestArguments | AttachRequestArguments; // There are four distinct scenarios here: @@ -67,9 +68,11 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac } else if (configuration.listen === undefined && configuration.processId === undefined) { throw new Error('"request":"attach" requires either "connect", "listen", or "processId"'); } + traceLog('createDebugAdapterDescriptor: attach scenario using spawned adapter'); } const command = await this.getDebugAdapterPython(configuration, session.workspaceFolder); + traceLog(`createDebugAdapterDescriptor: python command parts='${command.join(' ')}'`); if (command.length !== 0) { if (configuration.request === 'attach' && configuration.processId !== undefined) { sendTelemetryEvent(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS); @@ -114,6 +117,7 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac configuration: LaunchRequestArguments | AttachRequestArguments, workspaceFolder?: WorkspaceFolder, ): Promise { + traceVerbose('getDebugAdapterPython: Resolving interpreter for debug adapter'); if (configuration.debugAdapterPython !== undefined) { return this.getExecutableCommand(await resolveEnvironment(configuration.debugAdapterPython)); } else if (configuration.pythonPath) { @@ -202,6 +206,7 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac if (major < 3 || (major <= 3 && minor < 9)) { this.showDeprecatedPythonMessage(); } + traceLog(`getExecutableCommand: executable='${executablePath}' version='${version}'`); return executablePath ? [executablePath] : []; } return []; diff --git a/src/extension/debugger/configuration/debugConfigurationService.ts b/src/extension/debugger/configuration/debugConfigurationService.ts index 900f911a..9e4e196e 100644 --- a/src/extension/debugger/configuration/debugConfigurationService.ts +++ b/src/extension/debugger/configuration/debugConfigurationService.ts @@ -18,6 +18,7 @@ import { buildRemoteAttachConfiguration } from './providers/remoteAttach'; import { IDebugConfigurationResolver } from './types'; import { buildFileWithArgsLaunchDebugConfiguration } from './providers/fileLaunchWithArgs'; import { getInterpreterDetails } from '../../common/python'; +import { traceLog } from '../../common/log/logging'; export class PythonDebugConfigurationService implements IDebugConfigurationService { private cacheDebugConfig: DebugConfiguration | undefined = undefined; @@ -96,8 +97,14 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi if (debugConfiguration.python === undefined) { // If program is a valid file, get interpreter for that file if (fs.existsSync(debugConfiguration.program) && fs.statSync(debugConfiguration.program).isFile()) { + traceLog( + `resolveDebugConfigurationWithSubstitutedVariables: resolving interpreter for program='${debugConfiguration.program}'`, + ); const interpreter = await getInterpreterDetails(Uri.file(debugConfiguration.program)); if (interpreter?.path && interpreter.path.length > 0) { + traceLog( + `resolveDebugConfigurationWithSubstitutedVariables: setting debugConfiguration.python='${interpreter.path[0]}'`, + ); debugConfiguration.python = interpreter.path[0]; } } From d37728d14f8e7d6f9e0ec3c48346dca17c5a3efb Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 29 Oct 2025 09:45:35 -0700 Subject: [PATCH 07/13] fix tests and add type --- src/extension/common/python.ts | 3 +- .../unittest/adapter/factory.unit.test.ts | 110 ++++++++++-------- src/test/unittest/common/python.unit.test.ts | 29 ++--- .../unittest/common/pythonTrue.unit.test.ts | 24 ++-- 4 files changed, 86 insertions(+), 80 deletions(-) diff --git a/src/extension/common/python.ts b/src/extension/common/python.ts index 0fa462d6..baa3a552 100644 --- a/src/extension/common/python.ts +++ b/src/extension/common/python.ts @@ -15,6 +15,7 @@ import { legacyResolveEnvironment, } from './legacyPython'; import { useEnvExtension } from './utilities'; +import { EnvironmentVariables } from './variables/types'; /** * Details about a Python interpreter. @@ -149,7 +150,7 @@ export async function getSettingsPythonPath(resource?: Uri): Promise { if (!useEnvExtension()) { return legacyGetEnvironmentVariables(resource); } else { diff --git a/src/test/unittest/adapter/factory.unit.test.ts b/src/test/unittest/adapter/factory.unit.test.ts index 1616872a..df17088d 100644 --- a/src/test/unittest/adapter/factory.unit.test.ts +++ b/src/test/unittest/adapter/factory.unit.test.ts @@ -11,7 +11,14 @@ import * as path from 'path'; import * as sinon from 'sinon'; import { SemVer } from 'semver'; import { instance, mock, when } from 'ts-mockito'; -import { DebugAdapterExecutable, DebugAdapterServer, DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; +import { + DebugAdapterExecutable, + DebugAdapterServer, + DebugConfiguration, + DebugSession, + Uri, + WorkspaceFolder, +} from 'vscode'; import { IPersistentStateFactory } from '../../../extension/common/types'; import { DebugAdapterDescriptorFactory, debugStateKeys } from '../../../extension/debugger/adapter/factory'; import { IDebugAdapterDescriptorFactory } from '../../../extension/debugger/types'; @@ -24,6 +31,7 @@ import * as telemetry from '../../../extension/telemetry'; import * as telemetryReporter from '../../../extension/telemetry/reporter'; import * as vscodeApi from '../../../extension/common/vscodeapi'; import { DebugConfigStrings } from '../../../extension/common/utils/localize'; +import { PythonEnvironment } from '../../../extension/envExtApi'; use(chaiAsPromised); @@ -40,14 +48,29 @@ suite('Debugging - Adapter Factory', () => { const nodeExecutable = undefined; const debugAdapterPath = path.join(EXTENSION_ROOT_DIR, 'bundled', 'libs', 'debugpy', 'adapter'); const pythonPath = 'path/to/python/interpreter'; - const interpreter = { - architecture: Architecture.Unknown, - path: pythonPath, - sysPrefix: '', - sysVersion: '', - envType: 'Unknow', - version: new SemVer('3.7.4-test'), - }; + function createInterpreter(executable: string, version: string): PythonEnvironment { + return { + envId: { id: executable, managerId: 'Venv' }, + name: `Python ${version}`, + displayName: `Python ${version}`, + displayPath: executable, + version, + environmentPath: Uri.file(executable), + execInfo: { + run: { + executable, + args: [], + }, + activatedRun: { + executable, + args: [], + }, + }, + sysPrefix: '', + }; + } + + const interpreter: PythonEnvironment = createInterpreter(pythonPath, '3.7.4-test'); const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; @@ -116,7 +139,7 @@ suite('Debugging - Adapter Factory', () => { test('Return the path of the active interpreter as the current python path, it exists and configuration.pythonPath is not defined', async () => { const session = createSession({}); const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); + getInterpreterDetailsStub.resolves({ path: [interpreter.execInfo.run.executable] }); resolveEnvironmentStub.resolves(interpreter); const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); @@ -136,16 +159,15 @@ suite('Debugging - Adapter Factory', () => { test('Display a message if python version is less than 3.7', async () => { const session = createSession({}); - const deprecatedInterpreter = { + const deprecatedInterpreter: PythonEnvironment = { + ...createInterpreter(pythonPath, '3.6.12-test'), + // Provide semver-like object for version check path while keeping string version for our helper. architecture: Architecture.Unknown, - path: pythonPath, - sysPrefix: '', - sysVersion: '', - envType: 'Unknown', - version: new SemVer('3.6.12-test'), - }; + // Keep a SemVer instance separately if code relies on it (factory only parses string). + semVer: new SemVer('3.6.12-test'), + } as any; when(state.value).thenReturn(false); - getInterpreterDetailsStub.resolves({ path: [deprecatedInterpreter.path] }); + getInterpreterDetailsStub.resolves({ path: [deprecatedInterpreter.execInfo.run.executable] }); resolveEnvironmentStub.resolves(deprecatedInterpreter); await factory.createDebugAdapterDescriptor(session, nodeExecutable); @@ -188,7 +210,7 @@ suite('Debugging - Adapter Factory', () => { test('Return Debug Adapter executable if request is "attach", and listen is specified', async () => { const session = createSession({ request: 'attach', listen: { port: 5678, host: 'localhost' } }); const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); + getInterpreterDetailsStub.resolves({ path: [interpreter.execInfo.run.executable] }); resolveEnvironmentStub.resolves(interpreter); const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); @@ -219,8 +241,8 @@ suite('Debugging - Adapter Factory', () => { EXTENSION_ROOT_DIR, ]); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + getInterpreterDetailsStub.resolves({ path: [interpreter.execInfo.run.executable] }); + resolveEnvironmentStub.withArgs(interpreter.execInfo.run.executable).resolves(interpreter); const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); @@ -231,8 +253,8 @@ suite('Debugging - Adapter Factory', () => { const session = createSession({}); const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + getInterpreterDetailsStub.resolves({ path: [interpreter.execInfo.run.executable] }); + resolveEnvironmentStub.withArgs(interpreter.execInfo.run.executable).resolves(interpreter); const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); @@ -243,8 +265,8 @@ suite('Debugging - Adapter Factory', () => { const session = createSession({ logToFile: false }); const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + getInterpreterDetailsStub.resolves({ path: [interpreter.execInfo.run.executable] }); + resolveEnvironmentStub.withArgs(interpreter.execInfo.run.executable).resolves(interpreter); const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); @@ -253,8 +275,8 @@ suite('Debugging - Adapter Factory', () => { test('Send attach to local process telemetry if attaching to a local process', async () => { const session = createSession({ request: 'attach', processId: 1234 }); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + getInterpreterDetailsStub.resolves({ path: [interpreter.execInfo.run.executable] }); + resolveEnvironmentStub.withArgs(interpreter.execInfo.run.executable).resolves(interpreter); await factory.createDebugAdapterDescriptor(session, nodeExecutable); @@ -263,8 +285,8 @@ suite('Debugging - Adapter Factory', () => { test("Don't send any telemetry if not attaching to a local process", async () => { const session = createSession({}); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + getInterpreterDetailsStub.resolves({ path: [interpreter.execInfo.run.executable] }); + resolveEnvironmentStub.withArgs(interpreter.execInfo.run.executable).resolves(interpreter); await factory.createDebugAdapterDescriptor(session, nodeExecutable); @@ -275,8 +297,8 @@ suite('Debugging - Adapter Factory', () => { const customAdapterPath = 'custom/debug/adapter/path'; const session = createSession({ debugAdapterPath: customAdapterPath }); const debugExecutable = new DebugAdapterExecutable(pythonPath, [customAdapterPath]); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + getInterpreterDetailsStub.resolves({ path: [interpreter.execInfo.run.executable] }); + resolveEnvironmentStub.withArgs(interpreter.execInfo.run.executable).resolves(interpreter); const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); assert.deepStrictEqual(descriptor, debugExecutable); @@ -289,14 +311,9 @@ suite('Debugging - Adapter Factory', () => { const debugExecutable = new DebugAdapterExecutable(interpreterPathSpacesQuoted, [customAdapterPath]); getInterpreterDetailsStub.resolves({ path: [interpreterPathSpaces] }); - const interpreterSpacePath = { - architecture: Architecture.Unknown, - path: interpreterPathSpaces, - sysPrefix: '', - sysVersion: '', - envType: 'Unknow', - version: new SemVer('3.7.4-test'), - }; + const interpreterSpacePath: PythonEnvironment = createInterpreter(interpreterPathSpaces, '3.7.4-test'); + // Add architecture for completeness. + (interpreterSpacePath as any).architecture = Architecture.Unknown; resolveEnvironmentStub.withArgs(interpreterPathSpaces).resolves(interpreterSpacePath); const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); @@ -306,15 +323,8 @@ suite('Debugging - Adapter Factory', () => { test('Use "debugAdapterPython" when specified', async () => { const session = createSession({ debugAdapterPython: '/bin/custompy' }); const debugExecutable = new DebugAdapterExecutable('/bin/custompy', [debugAdapterPath]); - const customInterpreter = { - architecture: Architecture.Unknown, - path: '/bin/custompy', - sysPrefix: '', - sysVersion: '', - envType: 'unknow', - version: new SemVer('3.7.4-test'), - }; - + const customInterpreter: PythonEnvironment = createInterpreter('/bin/custompy', '3.7.4-test'); + (customInterpreter as any).architecture = Architecture.Unknown; resolveEnvironmentStub.withArgs('/bin/custompy').resolves(customInterpreter); const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); @@ -324,8 +334,8 @@ suite('Debugging - Adapter Factory', () => { test('Do not use "python" to spawn the debug adapter', async () => { const session = createSession({ python: '/bin/custompy' }); const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - getInterpreterDetailsStub.resolves({ path: [interpreter.path] }); - resolveEnvironmentStub.withArgs(interpreter.path).resolves(interpreter); + getInterpreterDetailsStub.resolves({ path: [interpreter.execInfo.run.executable] }); + resolveEnvironmentStub.withArgs(interpreter.execInfo.run.executable).resolves(interpreter); const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); assert.deepStrictEqual(descriptor, debugExecutable); diff --git a/src/test/unittest/common/python.unit.test.ts b/src/test/unittest/common/python.unit.test.ts index 902a3171..7bd433b4 100644 --- a/src/test/unittest/common/python.unit.test.ts +++ b/src/test/unittest/common/python.unit.test.ts @@ -237,18 +237,21 @@ suite('Python API Tests', () => { suite('resolveEnvironment', () => { test('Should resolve environment from path string', async () => { const envPath = '/usr/bin/python3'; - const expectedEnv: ResolvedEnvironment = { + // Legacy branch returns a PythonEnvironment shape, not the minimal ResolvedEnvironment. + // Assert critical fields instead of deep equality with a minimal object. + mockPythonEnvApi.environments.resolveEnvironment.resolves({ id: 'test-env', executable: { uri: Uri.file(envPath) }, - } as ResolvedEnvironment; - - mockPythonEnvApi.environments.resolveEnvironment.resolves(expectedEnv); + }); sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); const result = await pythonApi.resolveEnvironment(envPath); - - expect(result).to.deep.equal(expectedEnv); - sinon.assert.calledWith(mockPythonEnvApi.environments.resolveEnvironment, envPath); + expect(result).to.not.be.undefined; + // PythonEnvironment generated by legacyResolveEnvironment -> converted shape + expect((result as any).execInfo?.run?.executable).to.equal(envPath); + expect((result as any).environmentPath?.fsPath).to.equal(envPath); + expect((result as any).displayPath).to.equal(envPath); + expect((result as any).version).to.equal('Unknown'); }); test('Should resolve environment from Environment object', async () => { @@ -256,17 +259,17 @@ suite('Python API Tests', () => { id: 'test-env', path: '/usr/bin/python3', } as Environment; - const expectedEnv: ResolvedEnvironment = { + mockPythonEnvApi.environments.resolveEnvironment.resolves({ id: 'test-env', executable: { uri: Uri.file('/usr/bin/python3') }, - } as ResolvedEnvironment; - - mockPythonEnvApi.environments.resolveEnvironment.resolves(expectedEnv); + }); sinon.stub(PythonExtension, 'api').resolves(mockPythonEnvApi); const result = await pythonApi.resolveEnvironment(env); - - expect(result).to.deep.equal(expectedEnv); + expect(result).to.not.be.undefined; + expect((result as any).execInfo?.run?.executable).to.equal('/usr/bin/python3'); + expect((result as any).environmentPath?.fsPath).to.equal('/usr/bin/python3'); + expect((result as any).displayPath).to.equal('/usr/bin/python3'); }); test('Should return undefined for invalid environment', async () => { diff --git a/src/test/unittest/common/pythonTrue.unit.test.ts b/src/test/unittest/common/pythonTrue.unit.test.ts index 8fa57011..4c90245d 100644 --- a/src/test/unittest/common/pythonTrue.unit.test.ts +++ b/src/test/unittest/common/pythonTrue.unit.test.ts @@ -8,7 +8,6 @@ import * as sinon from 'sinon'; import { Uri, Disposable, Extension, extensions } from 'vscode'; import * as pythonApi from '../../../extension/common/python'; import * as utilities from '../../../extension/common/utilities'; -import { EnvironmentPath } from '@vscode/python-extension'; import { buildPythonEnvironment } from './helpers'; suite('Python API Tests- useEnvironmentsExtension:true', () => { @@ -244,33 +243,26 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { suite('getActiveEnvironmentPath', () => { test('Should return active environment path', async () => { - const expectedPath: EnvironmentPath = { - id: 'test-env', - path: '/usr/bin/python3', - }; - // OLD API: Using getEnvironment() instead of environments.getActiveEnvironmentPath + // Match production shape: getEnvironment() returns a PythonEnvironment-like object + const envObj = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); (mockEnvsExtension as any).exports = mockPythonEnvApi; - mockPythonEnvApi.getEnvironment.returns(expectedPath); + mockPythonEnvApi.getEnvironment.returns(envObj); const result = await pythonApi.getActiveEnvironmentPath(); - expect(result).to.deep.equal(expectedPath); + expect((result as any).environmentPath.fsPath).to.equal('/usr/bin/python3'); + expect((result as any).execInfo.run.executable).to.equal('/usr/bin/python3'); }); test('Should return active environment path for specific resource', async () => { const resource = Uri.file('/workspace/file.py'); - const expectedPath: EnvironmentPath = { - id: 'test-env', - path: '/usr/bin/python3', - }; - // OLD API: Using getEnvironment() instead of environments.getActiveEnvironmentPath + const envObj = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); (mockEnvsExtension as any).exports = mockPythonEnvApi; - mockPythonEnvApi.getEnvironment.returns(expectedPath); + mockPythonEnvApi.getEnvironment.returns(envObj); const result = await pythonApi.getActiveEnvironmentPath(resource); - expect(result).to.deep.equal(expectedPath); - // OLD API: Using getEnvironment() instead of environments.getActiveEnvironmentPath + expect((result as any).environmentPath.fsPath).to.equal('/usr/bin/python3'); sinon.assert.calledWith(mockPythonEnvApi.getEnvironment, resource); }); }); From 21c601164ccf623680c7503e2e8f94247723a73a Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:20:37 -0700 Subject: [PATCH 08/13] remove extra comments --- src/extension/common/python.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/extension/common/python.ts b/src/extension/common/python.ts index baa3a552..3a934a68 100644 --- a/src/extension/common/python.ts +++ b/src/extension/common/python.ts @@ -17,20 +17,14 @@ import { import { useEnvExtension } from './utilities'; import { EnvironmentVariables } from './variables/types'; -/** - * Details about a Python interpreter. - */ export interface IInterpreterDetails { - /** Array of path components to the Python executable */ path?: string[]; - /** The workspace resource associated with this interpreter */ resource?: Uri; } /** Event emitter for Python interpreter changes */ const onDidChangePythonInterpreterEvent = new EventEmitter(); -/** Event that fires when the active Python interpreter changes */ export const onDidChangePythonInterpreter: Event = onDidChangePythonInterpreterEvent.event; async function activateExtensions() { From 00ed7160743eb936c994d30d23bc52f424f25ba6 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:37:44 -0700 Subject: [PATCH 09/13] path fix --- .../unittest/common/pythonTrue.unit.test.ts | 86 +++++++++++-------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/src/test/unittest/common/pythonTrue.unit.test.ts b/src/test/unittest/common/pythonTrue.unit.test.ts index 4c90245d..cccb587e 100644 --- a/src/test/unittest/common/pythonTrue.unit.test.ts +++ b/src/test/unittest/common/pythonTrue.unit.test.ts @@ -6,10 +6,24 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; import { Uri, Disposable, Extension, extensions } from 'vscode'; +import * as path from 'path'; import * as pythonApi from '../../../extension/common/python'; import * as utilities from '../../../extension/common/utilities'; import { buildPythonEnvironment } from './helpers'; +// Platform-specific path constants using path.join so tests assert using native separators. +// Leading root '/' preserved; on Windows this yields a leading backslash (e.g. '\\usr\\bin'). +const PYTHON_PATH = path.join('/', 'usr', 'bin', 'python3'); +const PYTHON_PATH_39 = path.join('/', 'usr', 'bin', 'python3.9'); +const PYTHON_PATH_WITH_SPACES = path.join('/', 'path with spaces', 'python3'); +const QUOTED_PYTHON_PATH = `"${PYTHON_PATH_WITH_SPACES}"`; +const PYTHON_PATH_DIR = path.join('/', 'usr', 'bin'); +const PYTHON_LIB_PYTHON3_DIR = path.join('/', 'usr', 'lib', 'python3'); +const WORKSPACE_FILE = path.join('/', 'workspace', 'file.py'); +const WORKSPACE_PYTHON_DIR = path.join('/', 'workspace', 'python'); +const INVALID_PATH = path.join('/', 'invalid', 'path'); +const MOCK_PATH = path.join('/', 'mock', 'path'); + suite('Python API Tests- useEnvironmentsExtension:true', () => { let getExtensionStub: sinon.SinonStub; let mockPythonExtension: Extension; @@ -26,8 +40,8 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { // Create mock Python extension mockPythonExtension = { id: 'ms-python.python', - extensionUri: Uri.file('/mock/path'), - extensionPath: '/mock/path', + extensionUri: Uri.file(MOCK_PATH), + extensionPath: MOCK_PATH, isActive: true, packageJSON: {}, exports: undefined, @@ -38,8 +52,8 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { // Create mock Python Envs extension mockEnvsExtension = { id: 'ms-python.vscode-python-envs', - extensionUri: Uri.file('/mock/path'), - extensionPath: '/mock/path', + extensionUri: Uri.file(MOCK_PATH), + extensionPath: MOCK_PATH, isActive: true, packageJSON: {}, exports: undefined, @@ -72,7 +86,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { test('Should initialize python and set up event listeners', async () => { const disposables: Disposable[] = []; (mockEnvsExtension as any).exports = mockPythonEnvApi; - const mockPythonEnv = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); + const mockPythonEnv = buildPythonEnvironment(PYTHON_PATH, '3.9.0'); mockPythonEnvApi.getEnvironment.resolves(mockPythonEnv); mockPythonEnvApi.resolveEnvironment.resolves(mockPythonEnv); mockPythonEnvApi.onDidChangeEnvironments.returns({ @@ -100,7 +114,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { const mockEventHandler = sinon.stub(); (mockEnvsExtension as any).exports = mockPythonEnvApi; - const mockPythonEnv = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); + const mockPythonEnv = buildPythonEnvironment(PYTHON_PATH, '3.9.0'); mockPythonEnvApi.getEnvironment.resolves(mockPythonEnv); mockPythonEnvApi.resolveEnvironment.resolves(mockPythonEnv); @@ -115,9 +129,9 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { suite('getSettingsPythonPath', () => { test('Should return execution details from Python extension API', async () => { - const expectedPath = ['/usr/bin/python3']; + const expectedPath = [PYTHON_PATH]; // OLD API: Using getEnvironment() + resolveEnvironment() instead of settings.getExecutionDetails - const mockPythonEnv = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); + const mockPythonEnv = buildPythonEnvironment(PYTHON_PATH, '3.9.0'); (mockEnvsExtension as any).exports = mockPythonEnvApi; mockPythonEnvApi.getEnvironment.resolves(mockPythonEnv); mockPythonEnvApi.resolveEnvironment.resolves(mockPythonEnv); @@ -128,10 +142,10 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { }); test('Should return execution details for specific resource', async () => { - const resource = Uri.file('/workspace/file.py'); - const expectedPath = ['/usr/bin/python3']; + const resource = Uri.file(WORKSPACE_FILE); + const expectedPath = [PYTHON_PATH]; // OLD API: Using getEnvironment() + resolveEnvironment() instead of settings.getExecutionDetails - const mockPythonEnv = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); + const mockPythonEnv = buildPythonEnvironment(PYTHON_PATH, '3.9.0'); (mockEnvsExtension as any).exports = mockPythonEnvApi; mockPythonEnvApi.getEnvironment.resolves(mockPythonEnv); mockPythonEnvApi.resolveEnvironment.resolves(mockPythonEnv); @@ -157,7 +171,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { suite('getEnvironmentVariables', () => { test('Should return environment variables from Python extension API', async () => { // eslint-disable-next-line @typescript-eslint/naming-convention - const expectedVars = { PATH: '/usr/bin', PYTHONPATH: '/usr/lib/python3' }; + const expectedVars = { PATH: PYTHON_PATH_DIR, PYTHONPATH: PYTHON_LIB_PYTHON3_DIR }; // OLD API: Using getEnvironmentVariables() instead of environments.getEnvironmentVariables (mockEnvsExtension as any).exports = mockPythonEnvApi; mockPythonEnvApi.getEnvironmentVariables.resolves(expectedVars); @@ -170,9 +184,9 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { }); test('Should get environment variables for specific resource', async () => { - const resource = Uri.file('/workspace/file.py'); + const resource = Uri.file(WORKSPACE_FILE); // eslint-disable-next-line @typescript-eslint/naming-convention - const expectedVars = { PATH: '/usr/bin' }; + const expectedVars = { PATH: PYTHON_PATH_DIR }; // OLD API: Using getEnvironmentVariables() instead of environments.getEnvironmentVariables (mockEnvsExtension as any).exports = mockPythonEnvApi; mockPythonEnvApi.getEnvironmentVariables.resolves(expectedVars); @@ -186,7 +200,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { test('Should handle undefined resource and return workspace environment variables', async () => { // eslint-disable-next-line @typescript-eslint/naming-convention - const expectedVars = { PATH: '/usr/bin', PYTHONPATH: '/workspace/python' }; + const expectedVars = { PATH: PYTHON_PATH_DIR, PYTHONPATH: WORKSPACE_PYTHON_DIR }; // OLD API: Using getEnvironmentVariables() instead of environments.getEnvironmentVariables (mockEnvsExtension as any).exports = mockPythonEnvApi; mockPythonEnvApi.getEnvironmentVariables.resolves(expectedVars); @@ -201,7 +215,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { suite('resolveEnvironment', () => { test('Should resolve environment from path string', async () => { - const envPath = '/usr/bin/python3'; + const envPath = PYTHON_PATH; // Use buildPythonEnvironment for realistic mock const expectedEnv = buildPythonEnvironment(envPath, '3.9.0'); @@ -218,7 +232,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { test('Should resolve environment from Environment object', async () => { // Use buildPythonEnvironment for realistic mock - const expectedEnv = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); + const expectedEnv = buildPythonEnvironment(PYTHON_PATH, '3.9.0'); // OLD API: Using resolveEnvironment() instead of environments.resolveEnvironment (mockEnvsExtension as any).exports = mockPythonEnvApi; @@ -230,7 +244,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { }); test('Should return undefined for invalid environment', async () => { - const envPath = '/invalid/path'; + const envPath = INVALID_PATH; // OLD API: Using resolveEnvironment() instead of environments.resolveEnvironment (mockEnvsExtension as any).exports = mockPythonEnvApi; mockPythonEnvApi.resolveEnvironment.resolves(undefined); @@ -244,32 +258,32 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { suite('getActiveEnvironmentPath', () => { test('Should return active environment path', async () => { // Match production shape: getEnvironment() returns a PythonEnvironment-like object - const envObj = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); + const envObj = buildPythonEnvironment(PYTHON_PATH, '3.9.0'); (mockEnvsExtension as any).exports = mockPythonEnvApi; mockPythonEnvApi.getEnvironment.returns(envObj); const result = await pythonApi.getActiveEnvironmentPath(); - expect((result as any).environmentPath.fsPath).to.equal('/usr/bin/python3'); - expect((result as any).execInfo.run.executable).to.equal('/usr/bin/python3'); + expect((result as any).environmentPath.fsPath).to.equal(PYTHON_PATH); + expect((result as any).execInfo.run.executable).to.equal(PYTHON_PATH); }); test('Should return active environment path for specific resource', async () => { - const resource = Uri.file('/workspace/file.py'); - const envObj = buildPythonEnvironment('/usr/bin/python3', '3.9.0'); + const resource = Uri.file(WORKSPACE_FILE); + const envObj = buildPythonEnvironment(PYTHON_PATH, '3.9.0'); (mockEnvsExtension as any).exports = mockPythonEnvApi; mockPythonEnvApi.getEnvironment.returns(envObj); const result = await pythonApi.getActiveEnvironmentPath(resource); - expect((result as any).environmentPath.fsPath).to.equal('/usr/bin/python3'); + expect((result as any).environmentPath.fsPath).to.equal(PYTHON_PATH); sinon.assert.calledWith(mockPythonEnvApi.getEnvironment, resource); }); }); suite('getInterpreterDetails', () => { test('Should return interpreter details without resource', async () => { - const pythonPath = '/usr/bin/python3'; + const pythonPath = PYTHON_PATH; const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); // OLD API: Using getEnvironment() and resolveEnvironment() instead of environments.* @@ -285,8 +299,8 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { }); test('Should return interpreter details with resource', async () => { - const resource = Uri.file('/workspace/file.py'); - const pythonPath = '/usr/bin/python3'; + const resource = Uri.file(WORKSPACE_FILE); + const pythonPath = PYTHON_PATH; const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); // OLD API: Using getEnvironment() and resolveEnvironment() instead of environments.* @@ -303,7 +317,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { test('Should not quote path with spaces', async () => { // this should be updated when we fix the quoting logic in getInterpreterDetails - const pythonPath = '/path with spaces/python3'; + const pythonPath = PYTHON_PATH_WITH_SPACES; const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); // OLD API: Using getEnvironment() and resolveEnvironment() instead of environments.* @@ -317,7 +331,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { }); test('Should not double-quote already quoted path', async () => { - const quotedPython = Uri.file('"/path with spaces/python3"'); + const quotedPython = Uri.file(QUOTED_PYTHON_PATH); const quotedPythonPath = quotedPython.fsPath; const mockEnv = buildPythonEnvironment(quotedPythonPath, '3.9.0'); @@ -334,7 +348,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { test('Should return undefined path when environment is not resolved', async () => { // OLD API: Using getEnvironment() and resolveEnvironment() instead of environments.* (mockEnvsExtension as any).exports = mockPythonEnvApi; - mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file('/usr/bin/python3') }); + mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file(PYTHON_PATH) }); mockPythonEnvApi.resolveEnvironment.resolves(undefined); const result = await pythonApi.getInterpreterDetails(); @@ -353,7 +367,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { // OLD API: Using getEnvironment() and resolveEnvironment() instead of environments.* (mockEnvsExtension as any).exports = mockPythonEnvApi; - mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file('/usr/bin/python3') }); + mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file(PYTHON_PATH) }); mockPythonEnvApi.resolveEnvironment.resolves(mockEnv); const result = await pythonApi.getInterpreterDetails(); @@ -367,7 +381,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { const disposables: Disposable[] = []; let eventCallback: any; const mockEventHandler = sinon.stub(); - const pythonPath = '/usr/bin/python3.9'; + const pythonPath = PYTHON_PATH_39; const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); // OLD API: Using onDidChangeEnvironments instead of onDidChangeActiveEnvironmentPath @@ -398,7 +412,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { const disposables: Disposable[] = []; let eventCallback: any; const mockEventHandler = sinon.stub(); - const pythonPath = '/usr/bin/python3.9'; + const pythonPath = PYTHON_PATH_39; const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); // OLD API: Using onDidChangeEnvironments instead of onDidChangeActiveEnvironmentPath @@ -427,7 +441,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { const disposables: Disposable[] = []; let eventCallback: any; const mockEventHandler = sinon.stub(); - const pythonPath = '/usr/bin/python3.9'; + const pythonPath = PYTHON_PATH_39; const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); // OLD API: Using onDidChangeEnvironments instead of onDidChangeActiveEnvironmentPath @@ -456,7 +470,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { const disposables: Disposable[] = []; let eventCallback: any; const mockEventHandler = sinon.stub(); - const pythonPath = '/usr/bin/python3.9'; + const pythonPath = PYTHON_PATH_39; const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); // OLD API: Using onDidChangeEnvironments instead of onDidChangeActiveEnvironmentPath @@ -485,7 +499,7 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { const disposables: Disposable[] = []; let eventCallback: any; const mockEventHandler = sinon.stub(); - const pythonPath = '/usr/bin/python3.9'; + const pythonPath = PYTHON_PATH_39; const mockEnv = buildPythonEnvironment(pythonPath, '3.9.0'); // OLD API: Using onDidChangeEnvironments instead of onDidChangeActiveEnvironmentPath From 331c9f1620a065ac88fe256fa1020fde83a5f647 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:26:52 -0800 Subject: [PATCH 10/13] test fixes --- src/test/unittest/common/python.unit.test.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/test/unittest/common/python.unit.test.ts b/src/test/unittest/common/python.unit.test.ts index 7bd433b4..be94ea23 100644 --- a/src/test/unittest/common/python.unit.test.ts +++ b/src/test/unittest/common/python.unit.test.ts @@ -248,9 +248,10 @@ suite('Python API Tests', () => { const result = await pythonApi.resolveEnvironment(envPath); expect(result).to.not.be.undefined; // PythonEnvironment generated by legacyResolveEnvironment -> converted shape - expect((result as any).execInfo?.run?.executable).to.equal(envPath); - expect((result as any).environmentPath?.fsPath).to.equal(envPath); - expect((result as any).displayPath).to.equal(envPath); + const expectedPath = Uri.file(envPath).fsPath; + expect((result as any).execInfo?.run?.executable).to.equal(expectedPath); + expect((result as any).environmentPath?.fsPath).to.equal(expectedPath); + expect((result as any).displayPath).to.equal(expectedPath); expect((result as any).version).to.equal('Unknown'); }); @@ -267,9 +268,10 @@ suite('Python API Tests', () => { const result = await pythonApi.resolveEnvironment(env); expect(result).to.not.be.undefined; - expect((result as any).execInfo?.run?.executable).to.equal('/usr/bin/python3'); - expect((result as any).environmentPath?.fsPath).to.equal('/usr/bin/python3'); - expect((result as any).displayPath).to.equal('/usr/bin/python3'); + const expectedPath = Uri.file('/usr/bin/python3').fsPath; + expect((result as any).execInfo?.run?.executable).to.equal(expectedPath); + expect((result as any).environmentPath?.fsPath).to.equal(expectedPath); + expect((result as any).displayPath).to.equal(expectedPath); }); test('Should return undefined for invalid environment', async () => { From 728877c3de814dc27c21dbe79534fafa08362a0a Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:38:21 -0800 Subject: [PATCH 11/13] fixes --- .../common/application/commands/reportIssueCommand.ts | 4 ++-- src/extension/common/python.ts | 6 ++---- src/extension/debugger/adapter/factory.ts | 2 +- src/test/unittest/common/pythonFalse.unit.test.ts | 2 -- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/extension/common/application/commands/reportIssueCommand.ts b/src/extension/common/application/commands/reportIssueCommand.ts index f370e9cc..7cdd88df 100644 --- a/src/extension/common/application/commands/reportIssueCommand.ts +++ b/src/extension/common/application/commands/reportIssueCommand.ts @@ -26,9 +26,9 @@ export async function openReportIssue(): Promise { const interpreterPath = await getActiveEnvironmentPath(); let interpreter: PythonEnvironment | undefined = undefined; if (interpreterPath && 'environmentPath' in interpreterPath) { - interpreter = interpreterPath ? await resolveEnvironment(interpreterPath.environmentPath.fsPath) : undefined; + interpreter = await resolveEnvironment(interpreterPath.environmentPath.fsPath); } else if (interpreterPath && 'path' in interpreterPath) { - interpreter = interpreterPath ? await resolveEnvironment(interpreterPath.path) : undefined; + interpreter = await resolveEnvironment(interpreterPath.path); } const virtualEnvKind = interpreter && interpreter.envId ? interpreter.envId.managerId : 'Unknown'; const pythonVersion = interpreter?.version ?? 'unknown'; diff --git a/src/extension/common/python.ts b/src/extension/common/python.ts index 3a934a68..b142771c 100644 --- a/src/extension/common/python.ts +++ b/src/extension/common/python.ts @@ -28,7 +28,7 @@ const onDidChangePythonInterpreterEvent = new EventEmitter( export const onDidChangePythonInterpreter: Event = onDidChangePythonInterpreterEvent.event; async function activateExtensions() { - traceWarn('Value during activateExtensions of useEnvExtension(): ', useEnvExtension()); + traceLog('Value during activateExtensions of useEnvExtension(): ', useEnvExtension()); await activatePythonExtension(); await activateEnvsExtension(); } @@ -73,7 +73,6 @@ export async function initializePython(disposables: Disposable[]): Promise if (api) { disposables.push( api.onDidChangeEnvironments(async () => { - // not sure if this is the right event.... const details = await getInterpreterDetails(); traceLog(`initializePython:onDidChangeEnvironments fired executable='${details.path?.[0]}'`); onDidChangePythonInterpreterEvent.fire(details); @@ -217,8 +216,7 @@ export async function resolveEnvironment( export async function getActiveEnvironmentPath( resource?: Resource, ): Promise { - // if I add environmentPath. or there needs to be some conversion between the two here - //TODO: fix this return type?? + if (!useEnvExtension()) { const envPath: EnvironmentPath = await legacyGetActiveEnvironmentPath(resource); traceLog(`getActiveEnvironmentPath: legacy active path='${envPath.path}'`); diff --git a/src/extension/debugger/adapter/factory.ts b/src/extension/debugger/adapter/factory.ts index 3240fc98..5666cd0a 100644 --- a/src/extension/debugger/adapter/factory.ts +++ b/src/extension/debugger/adapter/factory.ts @@ -199,7 +199,7 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac // Parse version string (e.g., "3.8.10" -> major: 3, minor: 8) const parseMajorMinor = (v: string) => { const m = v.match(/^(\d+)(?:\.(\d+))?/); - return { major: m ? Number(m[1]) : 0, minor: m && m[2] ? Number(m[2]) : 0 }; + return { major: m && m[1] ? Number(m[1]) : 0, minor: m && m[2] ? Number(m[2]) : 0 }; }; const { major, minor } = parseMajorMinor(version || ''); diff --git a/src/test/unittest/common/pythonFalse.unit.test.ts b/src/test/unittest/common/pythonFalse.unit.test.ts index ee728282..f5ff31ee 100644 --- a/src/test/unittest/common/pythonFalse.unit.test.ts +++ b/src/test/unittest/common/pythonFalse.unit.test.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -('use strict'); - import { expect } from 'chai'; import * as sinon from 'sinon'; import { Uri, Disposable, Extension, commands, extensions } from 'vscode'; From b821396438cc378ac6abbe08f043e1972b8d4e20 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:43:31 -0800 Subject: [PATCH 12/13] error --- src/extension/common/python.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension/common/python.ts b/src/extension/common/python.ts index b142771c..76ea8186 100644 --- a/src/extension/common/python.ts +++ b/src/extension/common/python.ts @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Environment, EnvironmentPath, ResolvedEnvironment, Resource } from '@vscode/python-extension'; import { commands, EventEmitter, extensions, Uri, Event, Disposable, Extension } from 'vscode'; -import { traceError, traceLog, traceWarn } from './log/logging'; +import { traceError, traceLog } from './log/logging'; import { PythonEnvironment, PythonEnvironmentApi, PythonEnvsExtension } from '../envExtApi'; import { legacyGetActiveEnvironmentPath, From 24af4933bfbf9176f65a160a8ab1d37aa3bef6c2 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:53:01 -0800 Subject: [PATCH 13/13] format --- src/extension/common/python.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/extension/common/python.ts b/src/extension/common/python.ts index 76ea8186..2fadbf18 100644 --- a/src/extension/common/python.ts +++ b/src/extension/common/python.ts @@ -216,7 +216,6 @@ export async function resolveEnvironment( export async function getActiveEnvironmentPath( resource?: Resource, ): Promise { - if (!useEnvExtension()) { const envPath: EnvironmentPath = await legacyGetActiveEnvironmentPath(resource); traceLog(`getActiveEnvironmentPath: legacy active path='${envPath.path}'`);