diff --git a/src/client/common/process/pythonExecutionFactory.ts b/src/client/common/process/pythonExecutionFactory.ts index d8401a603d03..efb05c3c9d12 100644 --- a/src/client/common/process/pythonExecutionFactory.ts +++ b/src/client/common/process/pythonExecutionFactory.ts @@ -25,7 +25,7 @@ import { import { IInterpreterAutoSelectionService } from '../../interpreter/autoSelection/types'; import { sleep } from '../utils/async'; import { traceError } from '../../logging'; -import { getPixiEnvironmentFromInterpreter } from '../../pythonEnvironments/common/environmentManagers/pixi'; +import { getPixi, getPixiEnvironmentFromInterpreter } from '../../pythonEnvironments/common/environmentManagers/pixi'; @injectable() export class PythonExecutionFactory implements IPythonExecutionFactory { @@ -80,16 +80,18 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { } const processService: IProcessService = await this.processServiceFactory.create(options.resource); + if (await getPixi()) { + const pixiExecutionService = await this.createPixiExecutionService(pythonPath, processService); + if (pixiExecutionService) { + return pixiExecutionService; + } + } + const condaExecutionService = await this.createCondaExecutionService(pythonPath, processService); if (condaExecutionService) { return condaExecutionService; } - const pixiExecutionService = await this.createPixiExecutionService(pythonPath, processService); - if (pixiExecutionService) { - return pixiExecutionService; - } - const windowsStoreInterpreterCheck = this.pyenvs.isMicrosoftStoreInterpreter.bind(this.pyenvs); const env = (await windowsStoreInterpreterCheck(pythonPath)) @@ -122,16 +124,18 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { processService.on('exec', this.logger.logProcess.bind(this.logger)); this.disposables.push(processService); + if (await getPixi()) { + const pixiExecutionService = await this.createPixiExecutionService(pythonPath, processService); + if (pixiExecutionService) { + return pixiExecutionService; + } + } + const condaExecutionService = await this.createCondaExecutionService(pythonPath, processService); if (condaExecutionService) { return condaExecutionService; } - const pixiExecutionService = await this.createPixiExecutionService(pythonPath, processService); - if (pixiExecutionService) { - return pixiExecutionService; - } - const env = createPythonEnv(pythonPath, processService, this.fileSystem); return createPythonService(processService, env); } diff --git a/src/client/common/terminal/environmentActivationProviders/pixiActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pixiActivationProvider.ts index f9110f6be60c..1deaa56dd8ae 100644 --- a/src/client/common/terminal/environmentActivationProviders/pixiActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/pixiActivationProvider.ts @@ -8,13 +8,7 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; import { IInterpreterService } from '../../../interpreter/contracts'; import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; -import { traceError } from '../../../logging'; -import { - getPixiEnvironmentFromInterpreter, - isNonDefaultPixiEnvironmentName, -} from '../../../pythonEnvironments/common/environmentManagers/pixi'; -import { exec } from '../../../pythonEnvironments/common/externalDependencies'; -import { splitLines } from '../../stringUtils'; +import { getPixiActivationCommands } from '../../../pythonEnvironments/common/environmentManagers/pixi'; @injectable() export class PixiActivationCommandProvider implements ITerminalActivationCommandProvider { @@ -37,38 +31,11 @@ export class PixiActivationCommandProvider implements ITerminalActivationCommand return this.getActivationCommandsForInterpreter(interpreter.path, targetShell); } - public async getActivationCommandsForInterpreter( + public getActivationCommandsForInterpreter( pythonPath: string, targetShell: TerminalShellType, ): Promise { - const pixiEnv = await getPixiEnvironmentFromInterpreter(pythonPath); - if (!pixiEnv) { - return undefined; - } - - const command = ['shell-hook', '--manifest-path', pixiEnv.manifestPath]; - if (isNonDefaultPixiEnvironmentName(pixiEnv.envName)) { - command.push('--environment'); - command.push(pixiEnv.envName); - } - - const pixiTargetShell = shellTypeToPixiShell(targetShell); - if (pixiTargetShell) { - command.push('--shell'); - command.push(pixiTargetShell); - } - - const shellHookOutput = await exec(pixiEnv.pixi.command, command, { - throwOnStdErr: false, - }).catch(traceError); - if (!shellHookOutput) { - return undefined; - } - - return splitLines(shellHookOutput.stdout, { - removeEmptyEntries: true, - trim: true, - }); + return getPixiActivationCommands(pythonPath, targetShell); } } diff --git a/src/client/common/terminal/helper.ts b/src/client/common/terminal/helper.ts index 9fcdd98bd289..d2b3bb7879af 100644 --- a/src/client/common/terminal/helper.ts +++ b/src/client/common/terminal/helper.ts @@ -22,6 +22,7 @@ import { TerminalActivationProviders, TerminalShellType, } from './types'; +import { isPixiEnvironment } from '../../pythonEnvironments/common/environmentManagers/pixi'; @injectable() export class TerminalHelper implements ITerminalHelper { @@ -143,6 +144,19 @@ export class TerminalHelper implements ITerminalHelper { ): Promise { const settings = this.configurationService.getSettings(resource); + const isPixiEnv = interpreter + ? interpreter.envType === EnvironmentType.Pixi + : await isPixiEnvironment(settings.pythonPath); + if (isPixiEnv) { + const activationCommands = interpreter + ? await this.pixi.getActivationCommandsForInterpreter(interpreter.path, terminalShellType) + : await this.pixi.getActivationCommands(resource, terminalShellType); + + if (Array.isArray(activationCommands)) { + return activationCommands; + } + } + const condaService = this.serviceContainer.get(IComponentAdapter); // If we have a conda environment, then use that. const isCondaEnvironment = interpreter diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index a4da35c88b9b..38f2d6a56277 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -54,6 +54,7 @@ import { StopWatch } from './common/utils/stopWatch'; import { registerReplCommands, registerReplExecuteOnEnter, registerStartNativeReplCommand } from './repl/replCommands'; import { registerTriggerForTerminalREPL } from './terminals/codeExecution/terminalReplWatcher'; import { registerPythonStartup } from './terminals/pythonStartup'; +import { registerPixiFeatures } from './pythonEnvironments/common/environmentManagers/pixi'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -100,6 +101,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components): IInterpreterService, ); const pathUtils = ext.legacyIOC.serviceContainer.get(IPathUtils); + registerPixiFeatures(ext.disposables); registerAllCreateEnvironmentFeatures( ext.disposables, interpreterQuickPick, diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index 586bad0d765c..6b49444b3b3d 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -39,6 +39,7 @@ import { StopWatch } from '../../common/utils/stopWatch'; import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { cache } from '../../common/utils/decorators'; +import { getRunPixiPythonCommand } from '../../pythonEnvironments/common/environmentManagers/pixi'; const ENVIRONMENT_PREFIX = 'e8b39361-0157-4923-80e1-22d70d46dee6'; const CACHE_DURATION = 10 * 60 * 1000; @@ -252,6 +253,11 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi // Using environment prefix isn't needed as the marker script already takes care of it. command = [...pythonArgv, ...args].map((arg) => arg.toCommandArgumentForPythonExt()).join(' '); } + } else if (interpreter?.envType === EnvironmentType.Pixi) { + const pythonArgv = await getRunPixiPythonCommand(interpreter.path); + if (pythonArgv) { + command = [...pythonArgv, ...args].map((arg) => arg.toCommandArgumentForPythonExt()).join(' '); + } } if (!command) { const activationCommands = await this.helper.getEnvironmentActivationShellCommands( diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/pixiLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/pixiLocator.ts index 7cdc78ec6f10..f4a3886a2120 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/pixiLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/pixiLocator.ts @@ -6,17 +6,17 @@ import { asyncFilter } from '../../../../common/utils/arrayUtils'; import { chain, iterable } from '../../../../common/utils/async'; import { traceError, traceVerbose } from '../../../../logging'; import { getCondaInterpreterPath } from '../../../common/environmentManagers/conda'; -import { Pixi } from '../../../common/environmentManagers/pixi'; import { pathExists } from '../../../common/externalDependencies'; import { PythonEnvKind } from '../../info'; import { IPythonEnvsIterator, BasicEnvInfo } from '../../locator'; import { FSWatcherKind, FSWatchingLocator } from './fsWatchingLocator'; +import { getPixi } from '../../../common/environmentManagers/pixi'; /** * Returns all virtual environment locations to look for in a workspace. */ async function getVirtualEnvDirs(root: string): Promise { - const pixi = await Pixi.getPixi(); + const pixi = await getPixi(); const envDirs = (await pixi?.getEnvList(root)) ?? []; return asyncFilter(envDirs, pathExists); } diff --git a/src/client/pythonEnvironments/common/environmentManagers/pixi.ts b/src/client/pythonEnvironments/common/environmentManagers/pixi.ts index 32db66932385..6abf26f830fb 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/pixi.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/pixi.ts @@ -5,12 +5,17 @@ import * as path from 'path'; import { readJSON } from 'fs-extra'; -import { OSType, getOSType, getUserHomeDir } from '../../../common/utils/platform'; -import { exec, getPythonSetting, onDidChangePythonSetting, pathExists, pathExistsSync } from '../externalDependencies'; +import which from 'which'; +import { getUserHomeDir } from '../../../common/utils/platform'; +import { exec, getPythonSetting, onDidChangePythonSetting, pathExists } from '../externalDependencies'; import { cache } from '../../../common/utils/decorators'; -import { isTestExecution } from '../../../common/constants'; import { traceVerbose, traceWarn } from '../../../logging'; import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; +import { isWindows } from '../../../common/platform/platformService'; +import { IDisposableRegistry } from '../../../common/types'; +import { getWorkspaceFolderPaths } from '../../../common/vscodeApis/workspaceApis'; +import { isTestExecution } from '../../../common/constants'; +import { TerminalShellType } from '../../../common/terminal/types'; export const PIXITOOLPATH_SETTING_KEY = 'pixiToolPath'; @@ -63,94 +68,31 @@ export async function isPixiEnvironment(interpreterPath: string): Promise { + try { + return await which('pixi', { all: true }); + } catch { + // Ignore errors + } + return []; +} + /** Wraps the "pixi" utility, and exposes its functionality. */ export class Pixi { - /** - * Locating pixi binary can be expensive, since it potentially involves spawning or - * trying to spawn processes; so we only do it once per session. - */ - private static pixiPromise: Promise | undefined; - /** * Creates a Pixi service corresponding to the corresponding "pixi" command. * * @param command - Command used to run pixi. This has the same meaning as the * first argument of spawn() - i.e. it can be a full path, or just a binary name. */ - constructor(public readonly command: string) { - onDidChangePythonSetting(PIXITOOLPATH_SETTING_KEY, () => { - Pixi.pixiPromise = undefined; - }); - } - - /** - * Returns a Pixi instance corresponding to the binary which can be used to run commands for the cwd. - * - * Pixi commands can be slow and so can be bottleneck to overall discovery time. So trigger command - * execution as soon as possible. To do that we need to ensure the operations before the command are - * performed synchronously. - */ - public static async getPixi(): Promise { - if (Pixi.pixiPromise === undefined || isTestExecution()) { - Pixi.pixiPromise = Pixi.locate(); - } - return Pixi.pixiPromise; - } - - private static async locate(): Promise { - // First thing this method awaits on should be pixi command execution, hence perform all operations - // before that synchronously. - - traceVerbose(`Getting pixi`); - // Produce a list of candidate binaries to be probed by exec'ing them. - function* getCandidates() { - // Read the pixi location from the settings. - try { - const customPixiToolPath = getPythonSetting(PIXITOOLPATH_SETTING_KEY); - if (customPixiToolPath && customPixiToolPath !== 'pixi') { - // If user has specified a custom pixi path, use it first. - yield customPixiToolPath; - } - } catch (ex) { - traceWarn(`Failed to get pixi setting`, ex); - } - - // Check unqualified filename, in case it's on PATH. - yield 'pixi'; - - // Check the default installation location - const home = getUserHomeDir(); - if (home) { - const defaultpixiToolPath = path.join(home, '.pixi', 'bin', 'pixi'); - if (pathExistsSync(defaultpixiToolPath)) { - yield defaultpixiToolPath; - } - } - } - - // Probe the candidates, and pick the first one that exists and does what we need. - for (const pixiToolPath of getCandidates()) { - traceVerbose(`Probing pixi binary: ${pixiToolPath}`); - const pixi = new Pixi(pixiToolPath); - const pixiVersion = await pixi.getVersion(); - if (pixiVersion !== undefined) { - traceVerbose(`Found pixi ${pixiVersion} via filesystem probing: ${pixiToolPath}`); - return pixi; - } - traceVerbose(`Failed to find pixi: ${pixiToolPath}`); - } - - // Didn't find anything. - traceVerbose(`No pixi binary found`); - return undefined; - } + constructor(public readonly command: string) {} /** * Retrieves list of Python environments known to this pixi for the specified directory. @@ -187,29 +129,6 @@ export class Pixi { } } - /** - * Runs `pixi --version` and returns the version part of the output. - */ - @cache(30_000, true, 10_000) - public async getVersion(): Promise { - try { - const versionOutput = await exec(this.command, ['--version'], { - throwOnStdErr: false, - }); - if (!versionOutput || !versionOutput.stdout) { - return undefined; - } - const versionParts = versionOutput.stdout.split(' '); - if (versionParts.length < 2) { - return undefined; - } - return versionParts[1].trim(); - } catch (error) { - traceVerbose(`Failed to get pixi version`, error); - return undefined; - } - } - /** * Returns the command line arguments to run `python` within a specific pixi environment. * @param manifestPath The path to the manifest file used by pixi. @@ -240,9 +159,62 @@ export class Pixi { // eslint-disable-next-line class-methods-use-this async getPixiEnvironmentMetadata(envDir: string): Promise { const pixiPath = path.join(envDir, 'conda-meta/pixi'); - const result: PixiEnvMetadata | undefined = await readJSON(pixiPath).catch(traceVerbose); - return result; + try { + const result: PixiEnvMetadata | undefined = await readJSON(pixiPath); + return result; + } catch (e) { + traceVerbose(`Failed to get pixi environment metadata for ${envDir}`, e); + } + return undefined; + } +} + +async function getPixiTool(): Promise { + let pixi = getPythonSetting(PIXITOOLPATH_SETTING_KEY); + + if (!pixi || pixi === 'pixi' || !(await pathExists(pixi))) { + pixi = undefined; + const paths = await findPixiOnPath(); + for (const p of paths) { + if (await pathExists(p)) { + pixi = p; + break; + } + } + } + + if (!pixi) { + // Check the default installation location + const home = getUserHomeDir(); + if (home) { + const pixiToolPath = path.join(home, '.pixi', 'bin', isWindows() ? 'pixi.exe' : 'pixi'); + if (await pathExists(pixiToolPath)) { + pixi = pixiToolPath; + } + } + } + + return pixi ? new Pixi(pixi) : undefined; +} + +/** + * Locating pixi binary can be expensive, since it potentially involves spawning or + * trying to spawn processes; so we only do it once per session. + */ +let _pixi: Promise | undefined; + +/** + * Returns a Pixi instance corresponding to the binary which can be used to run commands for the cwd. + * + * Pixi commands can be slow and so can be bottleneck to overall discovery time. So trigger command + * execution as soon as possible. To do that we need to ensure the operations before the command are + * performed synchronously. + */ +export function getPixi(): Promise { + if (_pixi === undefined || isTestExecution()) { + _pixi = getPixiTool(); } + return _pixi; } export type PixiEnvironmentInfo = { @@ -253,6 +225,12 @@ export type PixiEnvironmentInfo = { envName?: string; }; +function isPixiProjectDir(pixiProjectDir: string): boolean { + const paths = getWorkspaceFolderPaths().map((f) => path.normalize(f)); + const normalized = path.normalize(pixiProjectDir); + return paths.some((p) => p === normalized); +} + /** * Given the location of an interpreter, try to deduce information about the environment in which it * resides. @@ -262,16 +240,13 @@ export type PixiEnvironmentInfo = { */ export async function getPixiEnvironmentFromInterpreter( interpreterPath: string, - pixi?: Pixi, ): Promise { if (!interpreterPath) { return undefined; } const prefix = getPrefixFromInterpreterPath(interpreterPath); - - // Find the pixi executable for the project - pixi = pixi || (await Pixi.getPixi()); + const pixi = await getPixi(); if (!pixi) { traceVerbose(`could not find a pixi interpreter for the interpreter at ${interpreterPath}`); return undefined; @@ -304,30 +279,108 @@ export async function getPixiEnvironmentFromInterpreter( envsDir = path.dirname(prefix); dotPixiDir = path.dirname(envsDir); pixiProjectDir = path.dirname(dotPixiDir); + if (!isPixiProjectDir(pixiProjectDir)) { + traceVerbose(`could not determine the pixi project directory for the interpreter at ${interpreterPath}`); + return undefined; + } // Invoke pixi to get information about the pixi project pixiInfo = await pixi.getPixiInfo(pixiProjectDir); + + if (!pixiInfo || !pixiInfo.project_info) { + traceWarn(`failed to determine pixi project information for the interpreter at ${interpreterPath}`); + return undefined; + } + + return { + interpreterPath, + pixi, + pixiVersion: pixiInfo.version, + manifestPath: pixiInfo.project_info.manifest_path, + envName, + }; } catch (error) { traceWarn('Error processing paths or getting Pixi Info:', error); } - if (!pixiInfo || !pixiInfo.project_info) { - traceWarn(`failed to determine pixi project information for the interpreter at ${interpreterPath}`); - return undefined; - } - - return { - interpreterPath, - pixi, - pixiVersion: pixiInfo.version, - manifestPath: pixiInfo.project_info.manifest_path, - envName, - }; + return undefined; } /** * Returns true if the given environment name is *not* the default environment. */ export function isNonDefaultPixiEnvironmentName(envName?: string): envName is string { - return envName !== undefined && envName !== 'default'; + return envName !== 'default'; +} + +export function registerPixiFeatures(disposables: IDisposableRegistry): void { + disposables.push( + onDidChangePythonSetting(PIXITOOLPATH_SETTING_KEY, () => { + _pixi = getPixiTool(); + }), + ); +} + +/** + * Returns the `pixi run` command + */ +export async function getRunPixiPythonCommand(pythonPath: string): Promise { + const pixiEnv = await getPixiEnvironmentFromInterpreter(pythonPath); + if (!pixiEnv) { + return undefined; + } + + const args = [ + pixiEnv.pixi.command.toCommandArgumentForPythonExt(), + 'run', + '--manifest-path', + pixiEnv.manifestPath.toCommandArgumentForPythonExt(), + ]; + if (isNonDefaultPixiEnvironmentName(pixiEnv.envName)) { + args.push('--environment'); + args.push(pixiEnv.envName.toCommandArgumentForPythonExt()); + } + + args.push('python'); + return args; +} + +export async function getPixiActivationCommands( + pythonPath: string, + _targetShell?: TerminalShellType, +): Promise { + const pixiEnv = await getPixiEnvironmentFromInterpreter(pythonPath); + if (!pixiEnv) { + return undefined; + } + + const args = [ + pixiEnv.pixi.command.toCommandArgumentForPythonExt(), + 'shell', + '--manifest-path', + pixiEnv.manifestPath.toCommandArgumentForPythonExt(), + ]; + if (isNonDefaultPixiEnvironmentName(pixiEnv.envName)) { + args.push('--environment'); + args.push(pixiEnv.envName.toCommandArgumentForPythonExt()); + } + + // const pixiTargetShell = shellTypeToPixiShell(targetShell); + // if (pixiTargetShell) { + // args.push('--shell'); + // args.push(pixiTargetShell); + // } + + // const shellHookOutput = await exec(pixiEnv.pixi.command, args, { + // throwOnStdErr: false, + // }).catch(traceError); + // if (!shellHookOutput) { + // return undefined; + // } + + // return splitLines(shellHookOutput.stdout, { + // removeEmptyEntries: true, + // trim: true, + // }); + return [args.join(' ')]; } diff --git a/src/test/common/process/pythonExecutionFactory.unit.test.ts b/src/test/common/process/pythonExecutionFactory.unit.test.ts index dd0061b79d63..0981c59e78bb 100644 --- a/src/test/common/process/pythonExecutionFactory.unit.test.ts +++ b/src/test/common/process/pythonExecutionFactory.unit.test.ts @@ -89,6 +89,7 @@ suite('Process - PythonExecutionFactory', () => { let autoSelection: IInterpreterAutoSelectionService; let interpreterPathExpHelper: IInterpreterPathService; let getPixiEnvironmentFromInterpreterStub: sinon.SinonStub; + let getPixiStub: sinon.SinonStub; const pythonPath = 'path/to/python'; setup(() => { sinon.stub(Conda, 'getConda').resolves(new Conda('conda')); @@ -97,6 +98,9 @@ suite('Process - PythonExecutionFactory', () => { getPixiEnvironmentFromInterpreterStub = sinon.stub(pixi, 'getPixiEnvironmentFromInterpreter'); getPixiEnvironmentFromInterpreterStub.resolves(undefined); + getPixiStub = sinon.stub(pixi, 'getPixi'); + getPixiStub.resolves(undefined); + activationHelper = mock(EnvironmentActivationService); processFactory = mock(ProcessServiceFactory); configService = mock(ConfigurationService); @@ -142,6 +146,9 @@ suite('Process - PythonExecutionFactory', () => { when(serviceContainer.tryGet(IInterpreterService)).thenReturn( instance(interpreterService), ); + when(serviceContainer.get(IConfigurationService)).thenReturn( + instance(configService), + ); factory = new PythonExecutionFactory( instance(serviceContainer), instance(activationHelper), diff --git a/src/test/common/terminals/helper.unit.test.ts b/src/test/common/terminals/helper.unit.test.ts index e4a0ab9bd3e8..864188b7c7b4 100644 --- a/src/test/common/terminals/helper.unit.test.ts +++ b/src/test/common/terminals/helper.unit.test.ts @@ -211,8 +211,8 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.equal(condaActivationCommands); - verify(pythonSettings.pythonPath).once(); - verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); verify(condaActivationProvider.getActivationCommands(resource, anything())).once(); }); test('Activation command must return undefined if none of the proivders support the shell', async () => { @@ -231,8 +231,8 @@ suite('Terminal Service helpers', () => { ); expect(cmd).to.equal(undefined, 'Command must be undefined'); - verify(pythonSettings.pythonPath).once(); - verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); @@ -255,8 +255,8 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.deep.equal(expectCommand); - verify(pythonSettings.pythonPath).once(); - verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(bashActivationProvider.getActivationCommands(resource, anything())).once(); verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); @@ -287,7 +287,7 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.deep.equal(expectCommand); - verify(pythonSettings.pythonPath).once(); + verify(pythonSettings.pythonPath).atLeast(1); verify(condaService.isCondaEnvironment(pythonPath)).once(); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(bashActivationProvider.getActivationCommands(resource, anything())).never(); @@ -313,7 +313,7 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.deep.equal(expectCommand); - verify(pythonSettings.pythonPath).once(); + verify(pythonSettings.pythonPath).atLeast(1); verify(condaService.isCondaEnvironment(pythonPath)).once(); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); @@ -340,7 +340,7 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.deep.equal(expectCommand); - verify(pythonSettings.pythonPath).once(); + verify(pythonSettings.pythonPath).atLeast(1); verify(condaService.isCondaEnvironment(pythonPath)).once(); verify(bashActivationProvider.getActivationCommands(resource, anything())).once(); verify(cmdActivationProvider.getActivationCommands(resource, anything())).once(); @@ -387,8 +387,13 @@ suite('Terminal Service helpers', () => { ); expect(cmd).to.equal(undefined, 'Command must be undefined'); - verify(pythonSettings.pythonPath).times(interpreter ? 0 : 1); - verify(condaService.isCondaEnvironment(pythonPath)).times(interpreter ? 0 : 1); + if (interpreter) { + verify(pythonSettings.pythonPath).never(); + verify(condaService.isCondaEnvironment(pythonPath)).never(); + } else { + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); + } verify(bashActivationProvider.isShellSupported(shellToExpect)).atLeast(1); verify(pyenvActivationProvider.isShellSupported(anything())).never(); verify(pipenvActivationProvider.isShellSupported(anything())).never(); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/pixiLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/pixiLocator.unit.test.ts index 6bb147b41832..b55f61c3a771 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/pixiLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/pixiLocator.unit.test.ts @@ -17,12 +17,15 @@ suite('Pixi Locator', () => { let getPythonSetting: sinon.SinonStub; let getOSType: sinon.SinonStub; let locator: PixiLocator; + let pathExistsStub: sinon.SinonStub; suiteSetup(() => { getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); getPythonSetting.returns('pixi'); getOSType = sinon.stub(platformUtils, 'getOSType'); exec = sinon.stub(externalDependencies, 'exec'); + pathExistsStub = sinon.stub(externalDependencies, 'pathExists'); + pathExistsStub.resolves(true); }); suiteTeardown(() => sinon.restore()); @@ -38,7 +41,7 @@ suite('Pixi Locator', () => { getOSType.returns(osType); locator = new PixiLocator(projectDir); - exec.callsFake(makeExecHandler({ pixiPath: 'pixi', cwd: projectDir })); + exec.callsFake(makeExecHandler({ cwd: projectDir })); const iterator = locator.iterEnvs(); const actualEnvs = await getEnvs(iterator); @@ -66,26 +69,15 @@ suite('Pixi Locator', () => { test('project with multiple environments', async () => { getOSType.returns(platformUtils.OSType.Linux); - exec.callsFake(makeExecHandler({ pixiPath: 'pixi', cwd: projectDirs.multiEnv.path })); + exec.callsFake(makeExecHandler({ cwd: projectDirs.multiEnv.path })); locator = new PixiLocator(projectDirs.multiEnv.path); const iterator = locator.iterEnvs(); const actualEnvs = await getEnvs(iterator); - const expectedEnvs = [ - createBasicEnv( - PythonEnvKind.Pixi, - path.join(projectDirs.multiEnv.info.environments_info[1].prefix, 'bin/python'), - undefined, - projectDirs.multiEnv.info.environments_info[1].prefix, - ), - createBasicEnv( - PythonEnvKind.Pixi, - path.join(projectDirs.multiEnv.info.environments_info[2].prefix, 'bin/python'), - undefined, - projectDirs.multiEnv.info.environments_info[2].prefix, - ), - ]; + const expectedEnvs = projectDirs.multiEnv.info.environments_info.map((info) => + createBasicEnv(PythonEnvKind.Pixi, path.join(info.prefix, 'bin/python'), undefined, info.prefix), + ); assertBasicEnvsEqual(actualEnvs, expectedEnvs); }); }); diff --git a/src/test/pythonEnvironments/common/environmentManagers/pixi.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/pixi.unit.test.ts index d6b283c69fd3..0cbc6b25145c 100644 --- a/src/test/pythonEnvironments/common/environmentManagers/pixi.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentManagers/pixi.unit.test.ts @@ -4,7 +4,7 @@ import * as sinon from 'sinon'; import { ExecutionResult, ShellOptions } from '../../../../client/common/process/types'; import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; -import { Pixi } from '../../../../client/pythonEnvironments/common/environmentManagers/pixi'; +import { getPixi } from '../../../../client/pythonEnvironments/common/environmentManagers/pixi'; export type PixiCommand = { cmd: 'info --json' } | { cmd: '--version' } | { cmd: null }; @@ -105,10 +105,12 @@ export function makeExecHandler(verify: VerifyOptions = {}) { suite('Pixi binary is located correctly', async () => { let exec: sinon.SinonStub; let getPythonSetting: sinon.SinonStub; + let pathExists: sinon.SinonStub; setup(() => { getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); exec = sinon.stub(externalDependencies, 'exec'); + pathExists = sinon.stub(externalDependencies, 'pathExists'); }); teardown(() => { @@ -117,10 +119,16 @@ suite('Pixi binary is located correctly', async () => { const testPath = async (pixiPath: string, verify = true) => { getPythonSetting.returns(pixiPath); + pathExists.returns(pixiPath !== 'pixi'); // If `verify` is false, don’t verify that the command has been called with that path exec.callsFake(makeExecHandler(verify ? { pixiPath } : undefined)); - const pixi = await Pixi.getPixi(); - expect(pixi?.command).to.equal(pixiPath); + const pixi = await getPixi(); + + if (pixiPath === 'pixi') { + expect(pixi).to.equal(undefined); + } else { + expect(pixi?.command).to.equal(pixiPath); + } }; test('Return a Pixi instance in an empty directory', () => testPath('pixiPath', false)); @@ -133,7 +141,7 @@ suite('Pixi binary is located correctly', async () => { exec.callsFake((_file: string, _args: string[], _options: ShellOptions) => Promise.reject(new Error('Command failed')), ); - const pixi = await Pixi.getPixi(); + const pixi = await getPixi(); expect(pixi?.command).to.equal(undefined); }); }); diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index dcd45b2e56bc..8a1891962429 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -7,6 +7,7 @@ import * as path from 'path'; import * as assert from 'assert'; import * as fs from 'fs'; import * as os from 'os'; +import * as sinon from 'sinon'; import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; import { ITestController, ITestResultResolver } from '../../../client/testing/testController/common/types'; import { IPythonExecutionFactory } from '../../../client/common/process/types'; @@ -22,6 +23,7 @@ import { TestProvider } from '../../../client/testing/types'; import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; import { createTypeMoq } from '../../mocks/helper'; +import * as pixi from '../../../client/pythonEnvironments/common/environmentManagers/pixi'; suite('End to End Tests: test adapters', () => { let resultResolver: ITestResultResolver; @@ -32,6 +34,7 @@ suite('End to End Tests: test adapters', () => { let workspaceUri: Uri; let testOutputChannel: typeMoq.IMock; let testController: TestController; + let getPixiStub: sinon.SinonStub; const unittestProvider: TestProvider = UNITTEST_PROVIDER; const pytestProvider: TestProvider = PYTEST_PROVIDER; const rootPathSmallWorkspace = path.join( @@ -104,6 +107,9 @@ suite('End to End Tests: test adapters', () => { }); setup(async () => { + getPixiStub = sinon.stub(pixi, 'getPixi'); + getPixiStub.resolves(undefined); + // create objects that were injected configService = serviceContainer.get(IConfigurationService); pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); @@ -130,6 +136,9 @@ suite('End to End Tests: test adapters', () => { // Whatever you need to return }); }); + teardown(() => { + sinon.restore(); + }); suiteTeardown(async () => { // remove symlink const dest = rootPathDiscoverySymlink;