diff --git a/news/3 Code Health/7696.md b/news/3 Code Health/7696.md new file mode 100644 index 000000000000..31014c414703 --- /dev/null +++ b/news/3 Code Health/7696.md @@ -0,0 +1,2 @@ +Use "conda run" (instead of using the "python.pythonPath" setting directly) when executing +Python and an Anaconda environment is selected. diff --git a/src/client/common/process/condaExecutionService.ts b/src/client/common/process/condaExecutionService.ts new file mode 100644 index 000000000000..c0554161e2e4 --- /dev/null +++ b/src/client/common/process/condaExecutionService.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { injectable } from 'inversify'; +import { CondaEnvironmentInfo } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { PythonExecutionService } from './pythonProcess'; +import { IProcessService, PythonExecutionInfo } from './types'; + +@injectable() +export class CondaExecutionService extends PythonExecutionService { + constructor( + serviceContainer: IServiceContainer, + procService: IProcessService, + pythonPath: string, + private readonly condaFile: string, + private readonly condaEnvironment: CondaEnvironmentInfo + ) { + super(serviceContainer, procService, pythonPath); + } + + public getExecutionInfo(args: string[]): PythonExecutionInfo { + const executionArgs = this.condaEnvironment.name !== '' ? ['-n', this.condaEnvironment.name] : ['-p', this.condaEnvironment.path]; + + return { command: this.condaFile, args: ['run', ...executionArgs, 'python', ...args] }; + } +} diff --git a/src/client/common/process/pythonDaemon.ts b/src/client/common/process/pythonDaemon.ts index 8f6f09a90336..2220db15e49b 100644 --- a/src/client/common/process/pythonDaemon.ts +++ b/src/client/common/process/pythonDaemon.ts @@ -21,6 +21,7 @@ import { IPythonExecutionService, ObservableExecutionResult, Output, + PythonExecutionInfo, PythonVersionInfo, SpawnOptions, StdErrError @@ -96,6 +97,9 @@ export class PythonDaemonExecutionService implements IPythonDaemonExecutionServi return this.pythonExecutionService.getExecutablePath(); } } + public getExecutionInfo(args: string[]): PythonExecutionInfo { + return this.pythonExecutionService.getExecutionInfo(args); + } public async isModuleInstalled(moduleName: string): Promise { this.throwIfRPCConnectionIsDead(); try { diff --git a/src/client/common/process/pythonDaemonPool.ts b/src/client/common/process/pythonDaemonPool.ts index 945ccccd4320..124c86594bee 100644 --- a/src/client/common/process/pythonDaemonPool.ts +++ b/src/client/common/process/pythonDaemonPool.ts @@ -22,6 +22,7 @@ import { IPythonDaemonExecutionService, IPythonExecutionService, ObservableExecutionResult, + PythonExecutionInfo, SpawnOptions } from './types'; @@ -71,6 +72,9 @@ export class PythonDaemonExecutionServicePool implements IPythonDaemonExecutionS this.logger.logProcess(`${this.pythonPath} (daemon)`, ['getExecutablePath']); return this.wrapCall(daemon => daemon.getExecutablePath()); } + public getExecutionInfo(args: string[]): PythonExecutionInfo { + return this.pythonExecutionService.getExecutionInfo(args); + } public async isModuleInstalled(moduleName: string): Promise { this.logger.logProcess(`${this.pythonPath} (daemon)`, ['-m', moduleName]); return this.wrapCall(daemon => daemon.isModuleInstalled(moduleName)); diff --git a/src/client/common/process/pythonExecutionFactory.ts b/src/client/common/process/pythonExecutionFactory.ts index 2b9f81c93296..703ac3ab31e2 100644 --- a/src/client/common/process/pythonExecutionFactory.ts +++ b/src/client/common/process/pythonExecutionFactory.ts @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { inject, injectable } from 'inversify'; +import { gte } from 'semver'; +import { Uri } from 'vscode'; import { IEnvironmentActivationService } from '../../interpreter/activation/types'; -import { IInterpreterService } from '../../interpreter/contracts'; +import { ICondaService, IInterpreterService } from '../../interpreter/contracts'; import { WindowsStoreInterpreter } from '../../interpreter/locators/services/windowsStoreInterpreter'; import { IWindowsStoreInterpreter } from '../../interpreter/locators/types'; import { IServiceContainer } from '../../ioc/types'; @@ -11,6 +13,7 @@ import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { traceError } from '../logger'; import { IConfigurationService, IDisposableRegistry } from '../types'; +import { CondaExecutionService } from './condaExecutionService'; import { ProcessService } from './proc'; import { PythonDaemonExecutionServicePool } from './pythonDaemonPool'; import { PythonExecutionService } from './pythonProcess'; @@ -28,6 +31,9 @@ import { } from './types'; import { WindowsStorePythonProcess } from './windowsStorePythonProcess'; +// Minimum version number of conda required to be able to use 'conda run' +export const CONDA_RUN_VERSION = '4.6.0'; + @injectable() export class PythonExecutionFactory implements IPythonExecutionFactory { private readonly daemonsPerPythonService = new Map>(); @@ -36,6 +42,7 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { @inject(IEnvironmentActivationService) private readonly activationHelper: IEnvironmentActivationService, @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, @inject(IConfigurationService) private readonly configService: IConfigurationService, + @inject(ICondaService) private readonly condaService: ICondaService, @inject(IBufferDecoder) private readonly decoder: IBufferDecoder, @inject(WindowsStoreInterpreter) private readonly windowsStoreInterpreter: IWindowsStoreInterpreter ) {} @@ -44,6 +51,18 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { const processService: IProcessService = await this.processServiceFactory.create(options.resource); const processLogger = this.serviceContainer.get(IProcessLogger); processService.on('exec', processLogger.logProcess.bind(processLogger)); + + // Don't bother getting a conda execution service instance if we haven't fetched the list of interpreters yet. + // Also, without this hasInterpreters check smoke tests will time out + const interpreterService = this.serviceContainer.get(IInterpreterService); + const hasInterpreters = await interpreterService.hasInterpreters; + if (hasInterpreters) { + const condaExecutionService = await this.createCondaExecutionService(pythonPath, processService); + if (condaExecutionService) { + return condaExecutionService; + } + } + if (this.windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath)) { return new WindowsStorePythonProcess(this.serviceContainer, processService, pythonPath, this.windowsStoreInterpreter); } @@ -98,6 +117,33 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { const processLogger = this.serviceContainer.get(IProcessLogger); processService.on('exec', processLogger.logProcess.bind(processLogger)); this.serviceContainer.get(IDisposableRegistry).push(processService); + + const condaExecutionService = await this.createCondaExecutionService(pythonPath, processService); + if (condaExecutionService) { + return condaExecutionService; + } + return new PythonExecutionService(this.serviceContainer, processService, pythonPath); } + public async createCondaExecutionService(pythonPath: string, processService?: IProcessService, resource?: Uri): Promise { + const processServicePromise = processService ? Promise.resolve(processService) : this.processServiceFactory.create(resource); + const [condaVersion, condaEnvironment, condaFile, procService] = await Promise.all([ + this.condaService.getCondaVersion(), + this.condaService.getCondaEnvironment(pythonPath), + this.condaService.getCondaFile(), + processServicePromise + ]); + + if (condaVersion && gte(condaVersion, CONDA_RUN_VERSION) && condaEnvironment && condaFile && procService) { + // Add logging to the newly created process service + if (!processService) { + const processLogger = this.serviceContainer.get(IProcessLogger); + procService.on('exec', processLogger.logProcess.bind(processLogger)); + this.serviceContainer.get(IDisposableRegistry).push(procService); + } + return new CondaExecutionService(this.serviceContainer, procService, pythonPath, condaFile, condaEnvironment); + } + + return Promise.resolve(undefined); + } } diff --git a/src/client/common/process/pythonProcess.ts b/src/client/common/process/pythonProcess.ts index c6f9d4701a17..aa628daa8260 100644 --- a/src/client/common/process/pythonProcess.ts +++ b/src/client/common/process/pythonProcess.ts @@ -18,6 +18,7 @@ import { IProcessService, IPythonExecutionService, ObservableExecutionResult, + PythonExecutionInfo, PythonVersionInfo, SpawnOptions } from './types'; @@ -41,7 +42,8 @@ export class PythonExecutionService implements IPythonExecutionService { // See these two bugs: // https://github.com/microsoft/vscode-python/issues/7569 // https://github.com/microsoft/vscode-python/issues/7760 - const jsonValue = await waitForPromise(this.procService.exec(this.pythonPath, [file], { mergeStdOutErr: true }), 5000) + const { command, args } = this.getExecutionInfo([file]); + const jsonValue = await waitForPromise(this.procService.exec(command, args, { mergeStdOutErr: true }), 5000) .then(output => output ? output.stdout.trim() : '--timed out--'); // --timed out-- should cause an exception let json: { versionInfo: PythonVersionInfo; sysPrefix: string; sysVersion: string; is64Bit: boolean }; @@ -69,29 +71,41 @@ export class PythonExecutionService implements IPythonExecutionService { if (await this.fileSystem.fileExists(this.pythonPath)) { return this.pythonPath; } - return this.procService.exec(this.pythonPath, ['-c', 'import sys;print(sys.executable)'], { throwOnStdErr: true }) - .then(output => output.stdout.trim()); + + const { command, args } = this.getExecutionInfo(['-c', 'import sys;print(sys.executable)']); + return this.procService.exec(command, args, { throwOnStdErr: true }).then(output => output.stdout.trim()); } public async isModuleInstalled(moduleName: string): Promise { - return this.procService.exec(this.pythonPath, ['-c', `import ${moduleName}`], { throwOnStdErr: true }) + const { command, args } = this.getExecutionInfo(['-c', `import ${moduleName}`]); + return this.procService.exec(command, args, { throwOnStdErr: true }) .then(() => true).catch(() => false); } + public getExecutionInfo(args: string[]): PythonExecutionInfo { + return { command: this.pythonPath, args }; + } + public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult { const opts: SpawnOptions = { ...options }; + // Cannot use this.getExecutionInfo() until 'conda run' can be run without buffering output. + // See https://github.com/microsoft/vscode-python/issues/8473 return this.procService.execObservable(this.pythonPath, args, opts); } public execModuleObservable(moduleName: string, args: string[], options: SpawnOptions): ObservableExecutionResult { const opts: SpawnOptions = { ...options }; + // Cannot use this.getExecutionInfo() until 'conda run' can be run without buffering output. + // See https://github.com/microsoft/vscode-python/issues/8473 return this.procService.execObservable(this.pythonPath, ['-m', moduleName, ...args], opts); } public async exec(args: string[], options: SpawnOptions): Promise> { const opts: SpawnOptions = { ...options }; - return this.procService.exec(this.pythonPath, args, opts); + const executable = this.getExecutionInfo(args); + return this.procService.exec(executable.command, executable.args, opts); } public async execModule(moduleName: string, args: string[], options: SpawnOptions): Promise> { const opts: SpawnOptions = { ...options }; - const result = await this.procService.exec(this.pythonPath, ['-m', moduleName, ...args], opts); + const executable = this.getExecutionInfo(['-m', moduleName, ...args]); + const result = await this.procService.exec(executable.command, executable.args, opts); // If a module is not installed we'll have something in stderr. if (moduleName && ErrorUtils.outputHasModuleNotInstalledError(moduleName!, result.stderr)) { diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index 2e360846b83f..f3b08a5b0ded 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -9,6 +9,7 @@ import { Newable } from '../../ioc/types'; import { ExecutionInfo, IDisposable, Version } from '../types'; import { Architecture } from '../utils/platform'; import { EnvironmentVariables } from '../variables/types'; +import { CondaExecutionService } from './condaExecutionService'; export const IBufferDecoder = Symbol('IBufferDecoder'); export interface IBufferDecoder { @@ -115,6 +116,7 @@ export interface IPythonExecutionFactory { */ createDaemon(options: DaemonExecutionFactoryCreationOptions): Promise; createActivatedEnvironment(options: ExecutionFactoryCreateWithEnvironmentOptions): Promise; + createCondaExecutionService(pythonPath: string, processService?: IProcessService, resource?: Uri): Promise; } export type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final' | 'unknown'; export type PythonVersionInfo = [number, number, number, ReleaseLevel]; @@ -132,6 +134,7 @@ export interface IPythonExecutionService { getInterpreterInformation(): Promise; getExecutablePath(): Promise; isModuleInstalled(moduleName: string): Promise; + getExecutionInfo(args: string[]): PythonExecutionInfo; execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult; execModuleObservable(moduleName: string, args: string[], options: SpawnOptions): ObservableExecutionResult; @@ -140,6 +143,10 @@ export interface IPythonExecutionService { execModule(moduleName: string, args: string[], options: SpawnOptions): Promise>; } +export type PythonExecutionInfo = { + command: string; + args: string[]; +}; /** * Identical to the PythonExecutionService, but with a `dispose` method. * This is a daemon process that lives on until it is disposed, hence the `IDisposable`. diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index 20865aa16094..32837df1b8bd 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -43,6 +43,11 @@ export type CondaInfo = { conda_version?: string; }; +export type CondaEnvironmentInfo = { + name: string; + path: string; +}; + export const ICondaService = Symbol('ICondaService'); export interface ICondaService { @@ -51,11 +56,11 @@ export interface ICondaService { isCondaAvailable(): Promise; getCondaVersion(): Promise; getCondaInfo(): Promise; - getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string; path: string }[]) | undefined>; + getCondaEnvironments(ignoreCache: boolean): Promise; getInterpreterPath(condaEnvironmentPath: string): string; getCondaFileFromInterpreter(interpreterPath?: string, envName?: string): Promise; isCondaEnvironment(interpreterPath: string): Promise; - getCondaEnvironment(interpreterPath: string): Promise<{ name: string; path: string } | undefined>; + getCondaEnvironment(interpreterPath: string): Promise; } export enum InterpreterType { diff --git a/src/client/interpreter/locators/index.ts b/src/client/interpreter/locators/index.ts index da725e1f4825..9bb0753cb343 100644 --- a/src/client/interpreter/locators/index.ts +++ b/src/client/interpreter/locators/index.ts @@ -51,7 +51,7 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi return new EventEmitter>().event; } public get hasInterpreters(): Promise { - return this._hasInterpreters.promise; + return this._hasInterpreters.completed ? this._hasInterpreters.promise : Promise.resolve(false); } /** diff --git a/src/client/interpreter/locators/services/cacheableLocatorService.ts b/src/client/interpreter/locators/services/cacheableLocatorService.ts index 6d6e76580c03..8fcdeb0c39da 100644 --- a/src/client/interpreter/locators/services/cacheableLocatorService.ts +++ b/src/client/interpreter/locators/services/cacheableLocatorService.ts @@ -34,7 +34,7 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ return this.locating.event; } public get hasInterpreters(): Promise { - return this._hasInterpreters.promise; + return this._hasInterpreters.completed ? this._hasInterpreters.promise : Promise.resolve(false); } public abstract dispose(): void; @traceDecorators.verbose('Get Interpreters in CacheableLocatorService') diff --git a/src/client/interpreter/locators/services/condaService.ts b/src/client/interpreter/locators/services/condaService.ts index d002ca086fe8..e522b6c77eed 100644 --- a/src/client/interpreter/locators/services/condaService.ts +++ b/src/client/interpreter/locators/services/condaService.ts @@ -10,6 +10,7 @@ import { IProcessServiceFactory } from '../../../common/process/types'; import { IConfigurationService, IDisposableRegistry, ILogger, IPersistentStateFactory } from '../../../common/types'; import { cache } from '../../../common/utils/decorators'; import { + CondaEnvironmentInfo, CondaInfo, ICondaService, IInterpreterLocatorService, @@ -219,10 +220,10 @@ export class CondaService implements ICondaService { * Return the list of conda envs (by name, interpreter filename). */ @traceDecorators.verbose('Get Conda environments') - public async getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string; path: string }[]) | undefined> { + public async getCondaEnvironments(ignoreCache: boolean): Promise { // Global cache. // tslint:disable-next-line:no-any - const globalPersistence = this.persistentStateFactory.createGlobalPersistentState<{ data: { name: string; path: string }[] | undefined }>('CONDA_ENVIRONMENTS', undefined as any); + const globalPersistence = this.persistentStateFactory.createGlobalPersistentState<{ data: CondaEnvironmentInfo[] | undefined }>('CONDA_ENVIRONMENTS', undefined as any); if (!ignoreCache && globalPersistence.value) { return globalPersistence.value.data; } diff --git a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts index 3066ec27fb71..e1b7d23d7bab 100644 --- a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts +++ b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts @@ -9,6 +9,7 @@ import { Disposable, Uri } from 'vscode'; import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types'; import '../../common/extensions'; import { IFileSystem, IPlatformService } from '../../common/platform/types'; +import { IPythonExecutionFactory, PythonExecutionInfo } from '../../common/process/types'; import { ITerminalServiceFactory } from '../../common/terminal/types'; import { IConfigurationService, IDisposableRegistry } from '../../common/types'; import { DjangoContextInitializer } from './djangoContext'; @@ -16,31 +17,38 @@ import { TerminalCodeExecutionProvider } from './terminalCodeExecution'; @injectable() export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvider { - constructor(@inject(ITerminalServiceFactory) terminalServiceFactory: ITerminalServiceFactory, + constructor( + @inject(ITerminalServiceFactory) terminalServiceFactory: ITerminalServiceFactory, @inject(IConfigurationService) configurationService: IConfigurationService, @inject(IWorkspaceService) workspace: IWorkspaceService, @inject(IDocumentManager) documentManager: IDocumentManager, @inject(IPlatformService) platformService: IPlatformService, @inject(ICommandManager) commandManager: ICommandManager, @inject(IFileSystem) fileSystem: IFileSystem, - @inject(IDisposableRegistry) disposableRegistry: Disposable[]) { - - super(terminalServiceFactory, configurationService, workspace, disposableRegistry, platformService); + @inject(IPythonExecutionFactory) pythonExecFactory: IPythonExecutionFactory, + @inject(IDisposableRegistry) disposableRegistry: Disposable[] + ) { + super(terminalServiceFactory, configurationService, workspace, disposableRegistry, platformService, pythonExecFactory); this.terminalTitle = 'Django Shell'; disposableRegistry.push(new DjangoContextInitializer(documentManager, workspace, fileSystem, commandManager)); } - public getReplCommandArgs(resource?: Uri): { command: string; args: string[] } { - const pythonSettings = this.configurationService.getSettings(resource); - const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath; - const args = pythonSettings.terminal.launchArgs.slice(); + + public async getExecutableInfo(resource?: Uri, args: string[] = []): Promise { + const { command, args: executableArgs } = await super.getExecutableInfo(resource, args); const workspaceUri = resource ? this.workspace.getWorkspaceFolder(resource) : undefined; const defaultWorkspace = Array.isArray(this.workspace.workspaceFolders) && this.workspace.workspaceFolders.length > 0 ? this.workspace.workspaceFolders[0].uri.fsPath : ''; const workspaceRoot = workspaceUri ? workspaceUri.uri.fsPath : defaultWorkspace; const managePyPath = workspaceRoot.length === 0 ? 'manage.py' : path.join(workspaceRoot, 'manage.py'); - args.push(managePyPath.fileToCommandArgument()); - args.push('shell'); - return { command, args }; + executableArgs.push(managePyPath.fileToCommandArgument()); + executableArgs.push('shell'); + return { command, args: executableArgs }; + } + + public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise { + // We need the executable info but not the 'manage.py shell' args + const { command, args } = await super.getExecutableInfo(resource); + return { command, args: args.concat(executeArgs) }; } } diff --git a/src/client/terminals/codeExecution/repl.ts b/src/client/terminals/codeExecution/repl.ts index c40cc3792995..816b3ea3df6b 100644 --- a/src/client/terminals/codeExecution/repl.ts +++ b/src/client/terminals/codeExecution/repl.ts @@ -7,6 +7,7 @@ import { inject, injectable } from 'inversify'; import { Disposable } from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; import { IPlatformService } from '../../common/platform/types'; +import { IPythonExecutionFactory } from '../../common/process/types'; import { ITerminalServiceFactory } from '../../common/terminal/types'; import { IConfigurationService, IDisposableRegistry } from '../../common/types'; import { TerminalCodeExecutionProvider } from './terminalCodeExecution'; @@ -17,11 +18,11 @@ export class ReplProvider extends TerminalCodeExecutionProvider { @inject(ITerminalServiceFactory) terminalServiceFactory: ITerminalServiceFactory, @inject(IConfigurationService) configurationService: IConfigurationService, @inject(IWorkspaceService) workspace: IWorkspaceService, + @inject(IPythonExecutionFactory) pythonExecFactory: IPythonExecutionFactory, @inject(IDisposableRegistry) disposableRegistry: Disposable[], @inject(IPlatformService) platformService: IPlatformService ) { - - super(terminalServiceFactory, configurationService, workspace, disposableRegistry, platformService); + super(terminalServiceFactory, configurationService, workspace, disposableRegistry, platformService, pythonExecFactory); this.terminalTitle = 'REPL'; } } diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index c80089d196f4..d60dacc17565 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -9,6 +9,7 @@ import { Disposable, Uri } from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; import '../../common/extensions'; import { IPlatformService } from '../../common/platform/types'; +import { IPythonExecutionFactory, PythonExecutionInfo } from '../../common/process/types'; import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types'; import { IConfigurationService, IDisposableRegistry } from '../../common/types'; import { ICodeExecutionService } from '../../terminals/types'; @@ -18,22 +19,20 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { protected terminalTitle!: string; private _terminalService!: ITerminalService; private replActive?: Promise; - constructor(@inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, + constructor( + @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, @inject(IConfigurationService) protected readonly configurationService: IConfigurationService, @inject(IWorkspaceService) protected readonly workspace: IWorkspaceService, @inject(IDisposableRegistry) protected readonly disposables: Disposable[], - @inject(IPlatformService) protected readonly platformService: IPlatformService) { + @inject(IPlatformService) protected readonly platformService: IPlatformService, + @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory + ) {} - } public async executeFile(file: Uri) { - const pythonSettings = this.configurationService.getSettings(file); - await this.setCwdForFileExecution(file); + const { command, args } = await this.getExecuteFileArgs(file, [file.fsPath.fileToCommandArgument()]); - const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath; - const launchArgs = pythonSettings.terminal.launchArgs; - - await this.getTerminalService(file).sendCommand(command, launchArgs.concat(file.fsPath.fileToCommandArgument())); + await this.getTerminalService(file).sendCommand(command, args); } public async execute(code: string, resource?: Uri): Promise { @@ -50,7 +49,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { return; } this.replActive = new Promise(async resolve => { - const replCommandArgs = this.getReplCommandArgs(resource); + const replCommandArgs = await this.getExecutableInfo(resource); await this.getTerminalService(resource).sendCommand(replCommandArgs.command, replCommandArgs.args); // Give python repl time to start before we start sending text. @@ -59,11 +58,28 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { await this.replActive; } - public getReplCommandArgs(resource?: Uri): { command: string; args: string[] } { + + public async getExecutableInfo(resource?: Uri, args: string[] = []): Promise { const pythonSettings = this.configurationService.getSettings(resource); - const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath; - const args = pythonSettings.terminal.launchArgs.slice(); - return { command, args }; + const command = pythonSettings.pythonPath; + const launchArgs = pythonSettings.terminal.launchArgs; + + const condaExecutionService = await this.pythonExecFactory.createCondaExecutionService(command, undefined, resource); + if (condaExecutionService) { + return condaExecutionService.getExecutionInfo([...launchArgs, ...args]); + } + + const isWindows = this.platformService.isWindows; + + return { + command: isWindows ? command.replace(/\\/g, '/') : command, + args: [...launchArgs, ...args] + }; + } + + // Overridden in subclasses, see djangoShellCodeExecution.ts + public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise { + return this.getExecutableInfo(resource, executeArgs); } private getTerminalService(resource?: Uri): ITerminalService { if (!this._terminalService) { diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index db31b0e52461..c50c46fef0b2 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -21,6 +21,8 @@ import { ITerminalHelper } from '../../client/common/terminal/types'; import { IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IsWindows, ModuleNamePurpose, Product, ProductType } from '../../client/common/types'; import { createDeferred } from '../../client/common/utils/async'; import { getNamesAndValues } from '../../client/common/utils/enum'; +import { ICondaService } from '../../client/interpreter/contracts'; +import { CondaService } from '../../client/interpreter/locators/services/condaService'; import { InterpreterHashProvider } from '../../client/interpreter/locators/services/hashProvider'; import { InterpeterHashProviderFactory } from '../../client/interpreter/locators/services/hashProviderFactory'; import { InterpreterFilter } from '../../client/interpreter/locators/services/interpreterFilter'; @@ -71,7 +73,9 @@ suite('Installer', () => { ioc.serviceManager.addSingletonInstance(IApplicationShell, TypeMoq.Mock.ofType().object); ioc.serviceManager.addSingleton(IConfigurationService, ConfigurationService); ioc.serviceManager.addSingleton(IWorkspaceService, WorkspaceService); + ioc.serviceManager.addSingleton(ICondaService, CondaService); + ioc.registerMockInterpreterTypes(); ioc.registerMockProcessTypes(); ioc.serviceManager.addSingletonInstance(IsWindows, false); ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); diff --git a/src/test/common/process/condaExecutionService.unit.test.ts b/src/test/common/process/condaExecutionService.unit.test.ts new file mode 100644 index 000000000000..8ef07c408684 --- /dev/null +++ b/src/test/common/process/condaExecutionService.unit.test.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { CondaExecutionService } from '../../../client/common/process/condaExecutionService'; +import { IProcessService } from '../../../client/common/process/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('CondaExecutionService', () => { + let serviceContainer: TypeMoq.IMock; + let processService: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + let executionService: CondaExecutionService; + const args = ['-a', 'b', '-c']; + const pythonPath = 'path/to/python'; + const condaFile = 'path/to/conda'; + + setup(() => { + processService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + serviceContainer = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + fileSystem = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + + serviceContainer.setup(s => s.get(IFileSystem)).returns(() => fileSystem.object); + }); + + test('getExecutionInfo with a named environment should return execution info using the environment name', () => { + const environment = { name: 'foo', path: 'bar' }; + executionService = new CondaExecutionService(serviceContainer.object, processService.object, pythonPath, condaFile, environment); + + const result = executionService.getExecutionInfo(args); + + expect(result).to.deep.equal({ command: condaFile, args: ['run', '-n', environment.name, 'python', ...args] }); + }); + + test('getExecutionInfo with a non-named environment should return execution info using the environment path', async () => { + const environment = { name: '', path: 'bar' }; + executionService = new CondaExecutionService(serviceContainer.object, processService.object, pythonPath, condaFile, environment); + + const result = executionService.getExecutionInfo(args); + + expect(result).to.deep.equal({ command: condaFile, args: ['run', '-p', environment.path, 'python', ...args] }); + }); +}); diff --git a/src/test/common/process/pythonExecutionFactory.unit.test.ts b/src/test/common/process/pythonExecutionFactory.unit.test.ts index 73268bea9ff6..bbd712fd0103 100644 --- a/src/test/common/process/pythonExecutionFactory.unit.test.ts +++ b/src/test/common/process/pythonExecutionFactory.unit.test.ts @@ -5,30 +5,34 @@ import * as assert from 'assert'; import { expect } from 'chai'; import { SemVer } from 'semver'; import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; import { Uri } from 'vscode'; import { PythonSettings } from '../../../client/common/configSettings'; import { ConfigurationService } from '../../../client/common/configuration/service'; +import { CondaExecutionService } from '../../../client/common/process/condaExecutionService'; import { BufferDecoder } from '../../../client/common/process/decoder'; import { ProcessLogger } from '../../../client/common/process/logger'; -import { ProcessService } from '../../../client/common/process/proc'; import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; import { PythonDaemonExecutionServicePool } from '../../../client/common/process/pythonDaemonPool'; -import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; +import { CONDA_RUN_VERSION, PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; import { PythonExecutionService } from '../../../client/common/process/pythonProcess'; import { ExecutionFactoryCreationOptions, IBufferDecoder, IProcessLogger, + IProcessService, IProcessServiceFactory, IPythonExecutionService } from '../../../client/common/process/types'; +import { WindowsStorePythonProcess } from '../../../client/common/process/windowsStorePythonProcess'; import { IConfigurationService, IDisposableRegistry } from '../../../client/common/types'; import { Architecture } from '../../../client/common/utils/platform'; import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; -import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts'; +import { ICondaService, IInterpreterService, InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts'; import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { CondaService } from '../../../client/interpreter/locators/services/condaService'; import { WindowsStoreInterpreter } from '../../../client/interpreter/locators/services/windowsStoreInterpreter'; import { IWindowsStoreInterpreter } from '../../../client/interpreter/locators/types'; import { ServiceContainer } from '../../../client/ioc/container'; @@ -71,47 +75,58 @@ suite('Process - PythonExecutionFactory', () => { let factory: PythonExecutionFactory; let activationHelper: IEnvironmentActivationService; let bufferDecoder: IBufferDecoder; - let procecssFactory: IProcessServiceFactory; + let processFactory: IProcessServiceFactory; let configService: IConfigurationService; + let condaService: ICondaService; let processLogger: IProcessLogger; - let processService: ProcessService; + let processService: typemoq.IMock; let windowsStoreInterpreter: IWindowsStoreInterpreter; + let interpreterService: IInterpreterService; setup(() => { bufferDecoder = mock(BufferDecoder); activationHelper = mock(EnvironmentActivationService); - procecssFactory = mock(ProcessServiceFactory); + processFactory = mock(ProcessServiceFactory); configService = mock(ConfigurationService); + condaService = mock(CondaService); processLogger = mock(ProcessLogger); windowsStoreInterpreter = mock(WindowsStoreInterpreter); when(processLogger.logProcess('', [], {})).thenReturn(); - processService = mock(ProcessService); - when(processService.on('exec', () => { return; })).thenReturn(processService); - const interpreterService = mock(InterpreterService); + processService = typemoq.Mock.ofType(); + processService.setup(p => p.on('exec', () => { return; })).returns(() => processService.object); + processService.setup((p: any) => p.then).returns(() => undefined); + interpreterService = mock(InterpreterService); when(interpreterService.getInterpreterDetails(anything())).thenResolve({} as any); const serviceContainer = mock(ServiceContainer); when(serviceContainer.get(IDisposableRegistry)).thenReturn([]); when(serviceContainer.get(IProcessLogger)).thenReturn(processLogger); when(serviceContainer.get(IInterpreterService)).thenReturn(instance(interpreterService)); factory = new PythonExecutionFactory(instance(serviceContainer), - instance(activationHelper), instance(procecssFactory), - instance(configService), instance(bufferDecoder), - instance(windowsStoreInterpreter)); + instance(activationHelper), instance(processFactory), + instance(configService), instance(condaService), + instance(bufferDecoder), instance(windowsStoreInterpreter)); }); teardown(() => sinon.restore()); test('Ensure PythonExecutionService is created', async () => { const pythonSettings = mock(PythonSettings); - when(procecssFactory.create(resource)).thenResolve(instance(processService)); + when(processFactory.create(resource)).thenResolve(processService.object); when(activationHelper.getActivatedEnvironmentVariables(resource)).thenResolve({ x: '1' }); when(pythonSettings.pythonPath).thenReturn('HELLO'); when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); const service = await factory.create({ resource }); - verify(procecssFactory.create(resource)).once(); + verify(processFactory.create(resource)).once(); verify(pythonSettings.pythonPath).once(); expect(service).instanceOf(PythonExecutionService); }); test('Ensure we use an existing `create` method if there are no environment variables for the activated env', async () => { + const pythonPath = 'path/to/python'; + const pythonSettings = mock(PythonSettings); + + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + let createInvoked = false; const mockExecService = 'something'; factory.create = async (_options: ExecutionFactoryCreationOptions) => { @@ -124,6 +139,13 @@ suite('Process - PythonExecutionFactory', () => { assert.equal(createInvoked, true); }); test('Ensure we use an existing `create` method if there are no environment variables (0 length) for the activated env', async () => { + const pythonPath = 'path/to/python'; + const pythonSettings = mock(PythonSettings); + + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + let createInvoked = false; const mockExecService = 'something'; factory.create = async (_options: ExecutionFactoryCreationOptions) => { @@ -156,6 +178,172 @@ suite('Process - PythonExecutionFactory', () => { expect(service).instanceOf(PythonExecutionService); assert.equal(createInvoked, false); }); + + test('Ensure `create` returns a WindowsStorePythonProcess instance if it\'s a windows store intepreter path', async () => { + const pythonPath = 'path/to/python'; + const pythonSettings = mock(PythonSettings); + + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + when(windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath)).thenReturn(true); + + const service = await factory.create({ resource }); + + verify(processFactory.create(resource)).once(); + verify(pythonSettings.pythonPath).once(); + verify(windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath)).once(); + expect(service).instanceOf(WindowsStorePythonProcess); + }); + + test('Ensure `create` returns a CondaExecutionService instance if createCondaExecutionService() returns a valid object', async () => { + const pythonPath = 'path/to/python'; + const pythonSettings = mock(PythonSettings); + + when(interpreterService.hasInterpreters).thenResolve(true); + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + when(condaService.getCondaVersion()).thenResolve(new SemVer(CONDA_RUN_VERSION)); + when(condaService.getCondaEnvironment(pythonPath)).thenResolve({ name: 'foo', path: 'path/to/foo/env' }); + when(condaService.getCondaFile()).thenResolve('conda'); + + const service = await factory.create({ resource }); + + verify(processFactory.create(resource)).once(); + verify(pythonSettings.pythonPath).once(); + verify(condaService.getCondaVersion()).once(); + verify(condaService.getCondaEnvironment(pythonPath)).once(); + verify(condaService.getCondaFile()).once(); + expect(service).instanceOf(CondaExecutionService); + }); + + test('Ensure `create` returns a PythonExecutionService instance if createCondaExecutionService() returns undefined', async () => { + const pythonPath = 'path/to/python'; + const pythonSettings = mock(PythonSettings); + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + when(condaService.getCondaVersion()).thenResolve(new SemVer('1.0.0')); + when(interpreterService.hasInterpreters).thenResolve(true); + + const service = await factory.create({ resource }); + + verify(processFactory.create(resource)).once(); + verify(pythonSettings.pythonPath).once(); + verify(condaService.getCondaVersion()).once(); + verify(condaService.getCondaEnvironment(pythonPath)).once(); + verify(condaService.getCondaFile()).once(); + expect(service).instanceOf(PythonExecutionService); + }); + + test('Ensure `createActivatedEnvironment` returns a CondaExecutionService instance if createCondaExecutionService() returns a valid object', async () => { + const pythonPath = 'path/to/python'; + const pythonSettings = mock(PythonSettings); + + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve({ x: '1' }); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + when(condaService.getCondaVersion()).thenResolve(new SemVer(CONDA_RUN_VERSION)); + when(condaService.getCondaEnvironment(anyString())).thenResolve({ name: 'foo', path: 'path/to/foo/env' }); + when(condaService.getCondaFile()).thenResolve('conda'); + + const service = await factory.createActivatedEnvironment({ resource, interpreter }); + + verify(condaService.getCondaFile()).once(); + if (!interpreter) { + verify(pythonSettings.pythonPath).once(); + verify(condaService.getCondaEnvironment(pythonPath)).once(); + } else { + verify(condaService.getCondaEnvironment(interpreter.path)).once(); + } + + expect(service).instanceOf(CondaExecutionService); + }); + + test('Ensure `createActivatedEnvironment` returns a PythonExecutionService instance if createCondaExecutionService() returns undefined', async () => { + let createInvoked = false; + const pythonPath = 'path/to/python'; + const mockExecService = 'mockService'; + factory.create = async (_options: ExecutionFactoryCreationOptions) => { + createInvoked = true; + return Promise.resolve((mockExecService as any) as IPythonExecutionService); + }; + + const pythonSettings = mock(PythonSettings); + when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve({ x: '1' }); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + when(condaService.getCondaVersion()).thenResolve(new SemVer('1.0.0')); + + const service = await factory.createActivatedEnvironment({ resource, interpreter }); + + verify(condaService.getCondaFile()).once(); + verify(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).once(); + verify(condaService.getCondaVersion()).once(); + if (!interpreter) { + verify(pythonSettings.pythonPath).once(); + } + + expect(service).instanceOf(PythonExecutionService); + assert.equal(createInvoked, false); + }); + + test('Ensure `createCondaExecutionService` creates a CondaExecutionService instance if there is a conda environment', async () => { + const pythonPath = 'path/to/python'; + when(condaService.getCondaEnvironment(pythonPath)).thenResolve({ name: 'foo', path: 'path/to/foo/env' }); + when(condaService.getCondaVersion()).thenResolve(new SemVer(CONDA_RUN_VERSION)); + when(condaService.getCondaFile()).thenResolve('conda'); + + const result = await factory.createCondaExecutionService(pythonPath, processService.object, resource); + + expect(result).instanceOf(CondaExecutionService); + verify(condaService.getCondaVersion()).once(); + verify(condaService.getCondaEnvironment(pythonPath)).once(); + verify(condaService.getCondaFile()).once(); + }); + + test('Ensure `createCondaExecutionService` instantiates a ProcessService instance if the process argument is undefined', async () => { + const pythonPath = 'path/to/python'; + when(processFactory.create(resource)).thenResolve(processService.object); + when(condaService.getCondaEnvironment(pythonPath)).thenResolve({ name: 'foo', path: 'path/to/foo/env' }); + when(condaService.getCondaVersion()).thenResolve(new SemVer(CONDA_RUN_VERSION)); + when(condaService.getCondaFile()).thenResolve('conda'); + + const result = await factory.createCondaExecutionService(pythonPath, undefined, resource); + + expect(result).instanceOf(CondaExecutionService); + verify(processFactory.create(resource)).once(); + verify(condaService.getCondaVersion()).once(); + verify(condaService.getCondaEnvironment(pythonPath)).once(); + verify(condaService.getCondaFile()).once(); + }); + + test('Ensure `createCondaExecutionService` returns undefined if there is no conda environment', async () => { + const pythonPath = 'path/to/python'; + when(condaService.getCondaEnvironment(pythonPath)).thenResolve(undefined); + when(condaService.getCondaVersion()).thenResolve(new SemVer(CONDA_RUN_VERSION)); + + const result = await factory.createCondaExecutionService(pythonPath, processService.object); + + expect(result).to.be.equal(undefined, 'createCondaExecutionService should return undefined if not in a conda environment'); + verify(condaService.getCondaVersion()).once(); + verify(condaService.getCondaEnvironment(pythonPath)).once(); + verify(condaService.getCondaFile()).once(); + }); + + test('Ensure `createCondaExecutionService` returns undefined if the conda version does not support conda run', async () => { + const pythonPath = 'path/to/python'; + when(condaService.getCondaVersion()).thenResolve(new SemVer('1.0.0')); + + const result = await factory.createCondaExecutionService(pythonPath, processService.object); + + expect(result).to.be.equal(undefined, 'createCondaExecutionService should return undefined if not in a conda environment'); + verify(condaService.getCondaVersion()).once(); + verify(condaService.getCondaEnvironment(pythonPath)).once(); + verify(condaService.getCondaFile()).once(); + }); test('Create Daemon Service an invoke initialize', async () => { const pythonSettings = mock(PythonSettings); when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve({ x: '1' }); diff --git a/src/test/common/process/pythonProcess.unit.test.ts b/src/test/common/process/pythonProcess.unit.test.ts index c1bfce6c9c80..6e5fb6189217 100644 --- a/src/test/common/process/pythonProcess.unit.test.ts +++ b/src/test/common/process/pythonProcess.unit.test.ts @@ -249,4 +249,12 @@ suite('PythonExecutionService', () => { expect(result).to.eventually.be.rejectedWith(`Module '${moduleName}' not installed`); }); + + test('getExecutionInfo should return pythonPath and the execution arguments as is', () => { + const args = ['-a', 'b', '-c']; + + const result = executionService.getExecutionInfo(args); + + expect(result).to.deep.equal({ command: pythonPath, args }, 'getExecutionInfo should return pythonPath and the command and execution arguments as is'); + }); }); diff --git a/src/test/datascience/executionServiceMock.ts b/src/test/datascience/executionServiceMock.ts index aefb095b50cb..1fa043a04068 100644 --- a/src/test/datascience/executionServiceMock.ts +++ b/src/test/datascience/executionServiceMock.ts @@ -67,4 +67,7 @@ export class MockPythonExecutionService implements IPythonExecutionService { return result; } + public getExecutionInfo(args: string[]) { + return { command: this.pythonPath, args }; + } } diff --git a/src/test/datascience/mockPythonService.ts b/src/test/datascience/mockPythonService.ts index 39293dce94a1..1f8dd7212049 100644 --- a/src/test/datascience/mockPythonService.ts +++ b/src/test/datascience/mockPythonService.ts @@ -64,4 +64,8 @@ export class MockPythonService implements IPythonExecutionService { public setDelay(timeout: number | undefined) { this.procService.setDelay(timeout); } + + public getExecutionInfo(args: string[]) { + return { command: this.interpreter.path, args }; + } } diff --git a/src/test/format/extension.format.test.ts b/src/test/format/extension.format.test.ts index d0abe5b3eaaf..ed0986d8e188 100644 --- a/src/test/format/extension.format.test.ts +++ b/src/test/format/extension.format.test.ts @@ -105,10 +105,11 @@ suite('Formatting - General', () => { ioc.serviceManager.addSingleton(InterpreterHashProvider, InterpreterHashProvider); ioc.serviceManager.addSingleton(InterpeterHashProviderFactory, InterpeterHashProviderFactory); ioc.serviceManager.addSingleton(InterpreterFilter, InterpreterFilter); + ioc.serviceManager.addSingleton(ICondaService, CondaService); // Mocks. ioc.registerMockProcessTypes(); - ioc.serviceManager.addSingleton(ICondaService, CondaService); + ioc.registerMockInterpreterTypes(); } async function injectFormatOutput(outputFileName: string) { diff --git a/src/test/linters/lint.functional.test.ts b/src/test/linters/lint.functional.test.ts index 9672612ab842..4bcba475a609 100644 --- a/src/test/linters/lint.functional.test.ts +++ b/src/test/linters/lint.functional.test.ts @@ -36,6 +36,7 @@ import { import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; +import { ICondaService, IInterpreterService } from '../../client/interpreter/contracts'; import { WindowsStoreInterpreter } from '../../client/interpreter/locators/services/windowsStoreInterpreter'; import { IServiceContainer } from '../../client/ioc/types'; import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; @@ -246,6 +247,15 @@ class TestFixture extends BaseTestFixture { serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IBufferDecoder), TypeMoq.It.isAny())) .returns(() => decoder); + const interpreterService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + interpreterService.setup(i => i.hasInterpreters).returns(() => Promise.resolve(true)); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())).returns(() => interpreterService.object); + + const condaService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(undefined)); + condaService.setup(c => c.getCondaVersion()).returns(() => Promise.resolve(undefined)); + condaService.setup(c => c.getCondaFile()).returns(() => Promise.resolve('conda')); + const processLogger = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); processLogger .setup(p => p.logProcess(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) @@ -259,6 +269,7 @@ class TestFixture extends BaseTestFixture { envActivationService.object, procServiceFactory, configService, + condaService.object, decoder, instance(windowsStoreInterpreter) ); diff --git a/src/test/refactor/extension.refactor.extract.var.test.ts b/src/test/refactor/extension.refactor.extract.var.test.ts index 7652d5398326..acfa756ae500 100644 --- a/src/test/refactor/extension.refactor.extract.var.test.ts +++ b/src/test/refactor/extension.refactor.extract.var.test.ts @@ -5,6 +5,8 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import { commands, Position, Range, Selection, TextEditorCursorStyle, TextEditorLineNumbersStyle, TextEditorOptions, Uri, window, workspace } from 'vscode'; import { getTextEditsFromPatch } from '../../client/common/editor'; +import { ICondaService } from '../../client/interpreter/contracts'; +import { CondaService } from '../../client/interpreter/locators/services/condaService'; import { extractVariable } from '../../client/providers/simpleRefactorProvider'; import { RefactorProxy } from '../../client/refactor/proxy'; import { getExtensionSettings, isPythonVersion } from '../common'; @@ -51,6 +53,8 @@ suite('Variable Extraction', () => { ioc.registerCommonTypes(); ioc.registerProcessTypes(); ioc.registerVariableTypes(); + + ioc.serviceManager.addSingleton(ICondaService, CondaService); } async function testingVariableExtraction(shouldError: boolean, startPos: Position, endPos: Position): Promise { diff --git a/src/test/refactor/rename.test.ts b/src/test/refactor/rename.test.ts index c7e9ae0c91e6..c1d633f552de 100644 --- a/src/test/refactor/rename.test.ts +++ b/src/test/refactor/rename.test.ts @@ -17,6 +17,7 @@ import { PythonExecutionFactory } from '../../client/common/process/pythonExecut import { IProcessLogger, IProcessServiceFactory, IPythonExecutionFactory } from '../../client/common/process/types'; import { IConfigurationService, IPythonSettings } from '../../client/common/types'; import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; +import { ICondaService, IInterpreterService } from '../../client/interpreter/contracts'; import { WindowsStoreInterpreter } from '../../client/interpreter/locators/services/windowsStoreInterpreter'; import { IServiceContainer } from '../../client/ioc/types'; import { RefactorProxy } from '../../client/refactor/proxy'; @@ -39,21 +40,34 @@ suite('Refactor Rename', () => { pythonSettings.setup(p => p.pythonPath).returns(() => PYTHON_PATH); const configService = typeMoq.Mock.ofType(); configService.setup(c => c.getSettings(typeMoq.It.isAny())).returns(() => pythonSettings.object); + const condaService = typeMoq.Mock.ofType(); const processServiceFactory = typeMoq.Mock.ofType(); processServiceFactory.setup(p => p.create(typeMoq.It.isAny())).returns(() => Promise.resolve(new ProcessService(new BufferDecoder()))); + const interpreterService = typeMoq.Mock.ofType(); + interpreterService.setup(i => i.hasInterpreters).returns (() => Promise.resolve(true)); const envActivationService = typeMoq.Mock.ofType(); envActivationService.setup(e => e.getActivatedEnvironmentVariables(typeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); serviceContainer = typeMoq.Mock.ofType(); serviceContainer.setup(s => s.get(typeMoq.It.isValue(IConfigurationService), typeMoq.It.isAny())).returns(() => configService.object); serviceContainer.setup(s => s.get(typeMoq.It.isValue(IProcessServiceFactory), typeMoq.It.isAny())).returns(() => processServiceFactory.object); + serviceContainer.setup(s => s.get(typeMoq.It.isValue(IInterpreterService), typeMoq.It.isAny())).returns(() => interpreterService.object); serviceContainer.setup(s => s.get(typeMoq.It.isValue(IEnvironmentActivationService), typeMoq.It.isAny())) .returns(() => envActivationService.object); const windowsStoreInterpreter = mock(WindowsStoreInterpreter); serviceContainer .setup(s => s.get(typeMoq.It.isValue(IPythonExecutionFactory), typeMoq.It.isAny())) - .returns(() => new PythonExecutionFactory(serviceContainer.object, - undefined as any, processServiceFactory.object, - configService.object, undefined as any, instance(windowsStoreInterpreter))); + .returns( + () => + new PythonExecutionFactory( + serviceContainer.object, + undefined as any, + processServiceFactory.object, + configService.object, + condaService.object, + undefined as any, + instance(windowsStoreInterpreter) + ) + ); const processLogger = typeMoq.Mock.ofType(); processLogger.setup(p => p.logProcess(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())).returns(() => { return; }); serviceContainer.setup(s => s.get(typeMoq.It.isValue(IProcessLogger), typeMoq.It.isAny())).returns(() => processLogger.object); diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index 6c390f9cdb10..11335a1ba111 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -11,8 +11,9 @@ import { IS_WINDOWS } from '../client/common/platform/constants'; import { FileSystem } from '../client/common/platform/fileSystem'; import { PathUtils } from '../client/common/platform/pathUtils'; import { PlatformService } from '../client/common/platform/platformService'; +import { RegistryImplementation } from '../client/common/platform/registry'; import { registerTypes as platformRegisterTypes } from '../client/common/platform/serviceRegistry'; -import { IFileSystem, IPlatformService } from '../client/common/platform/types'; +import { IFileSystem, IPlatformService, IRegistry } from '../client/common/platform/types'; import { BufferDecoder } from '../client/common/process/decoder'; import { ProcessService } from '../client/common/process/proc'; import { PythonExecutionFactory } from '../client/common/process/pythonExecutionFactory'; @@ -26,10 +27,24 @@ import { registerTypes as formattersRegisterTypes } from '../client/formatters/s import { EnvironmentActivationService } from '../client/interpreter/activation/service'; import { IEnvironmentActivationService } from '../client/interpreter/activation/types'; import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../client/interpreter/autoSelection/types'; +import { CONDA_ENV_FILE_SERVICE, CONDA_ENV_SERVICE, CURRENT_PATH_SERVICE, GLOBAL_VIRTUAL_ENV_SERVICE, IInterpreterLocatorHelper, IInterpreterLocatorService, IInterpreterService, INTERPRETER_LOCATOR_SERVICE, IPipEnvService, KNOWN_PATH_SERVICE, PIPENV_SERVICE, WINDOWS_REGISTRY_SERVICE, WORKSPACE_VIRTUAL_ENV_SERVICE } from '../client/interpreter/contracts'; +import { InterpreterService } from '../client/interpreter/interpreterService'; +import { PythonInterpreterLocatorService } from '../client/interpreter/locators'; +import { InterpreterLocatorHelper } from '../client/interpreter/locators/helpers'; +import { CondaEnvFileService } from '../client/interpreter/locators/services/condaEnvFileService'; +import { CondaEnvService } from '../client/interpreter/locators/services/condaEnvService'; +import { CurrentPathService } from '../client/interpreter/locators/services/currentPathService'; +import { GlobalVirtualEnvService } from '../client/interpreter/locators/services/globalVirtualEnvService'; import { InterpreterHashProvider } from '../client/interpreter/locators/services/hashProvider'; import { InterpeterHashProviderFactory } from '../client/interpreter/locators/services/hashProviderFactory'; import { InterpreterFilter } from '../client/interpreter/locators/services/interpreterFilter'; +import { KnownPathsService } from '../client/interpreter/locators/services/KnownPathsService'; +import { PipEnvService } from '../client/interpreter/locators/services/pipEnvService'; +import { PipEnvServiceHelper } from '../client/interpreter/locators/services/pipEnvServiceHelper'; +import { WindowsRegistryService } from '../client/interpreter/locators/services/windowsRegistryService'; import { WindowsStoreInterpreter } from '../client/interpreter/locators/services/windowsStoreInterpreter'; +import { WorkspaceVirtualEnvService } from '../client/interpreter/locators/services/workspaceVirtualEnvService'; +import { IPipEnvServiceHelper } from '../client/interpreter/locators/types'; import { registerTypes as interpretersRegisterTypes } from '../client/interpreter/serviceRegistry'; import { ServiceContainer } from '../client/ioc/container'; import { ServiceManager } from '../client/ioc/serviceManager'; @@ -135,6 +150,24 @@ export class IocContainer { this.serviceManager.rebindInstance(IEnvironmentActivationService, instance(mockEnvironmentActivationService)); } + public registerMockInterpreterTypes() { + this.serviceManager.addSingleton(IInterpreterService, InterpreterService); + this.serviceManager.addSingleton(IInterpreterLocatorService, PythonInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); + this.serviceManager.addSingleton(IInterpreterLocatorService, CondaEnvFileService, CONDA_ENV_FILE_SERVICE); + this.serviceManager.addSingleton(IInterpreterLocatorService, CondaEnvService, CONDA_ENV_SERVICE); + this.serviceManager.addSingleton(IInterpreterLocatorService, CurrentPathService, CURRENT_PATH_SERVICE); + this.serviceManager.addSingleton(IInterpreterLocatorService, GlobalVirtualEnvService, GLOBAL_VIRTUAL_ENV_SERVICE); + this.serviceManager.addSingleton(IInterpreterLocatorService, WorkspaceVirtualEnvService, WORKSPACE_VIRTUAL_ENV_SERVICE); + this.serviceManager.addSingleton(IInterpreterLocatorService, PipEnvService, PIPENV_SERVICE); + this.serviceManager.addSingleton(IInterpreterLocatorService, WindowsRegistryService, WINDOWS_REGISTRY_SERVICE); + this.serviceManager.addSingleton(IInterpreterLocatorService, KnownPathsService, KNOWN_PATH_SERVICE); + this.serviceManager.addSingleton(IPipEnvService, PipEnvService); + + this.serviceManager.addSingleton(IInterpreterLocatorHelper, InterpreterLocatorHelper); + this.serviceManager.addSingleton(IPipEnvServiceHelper, PipEnvServiceHelper); + this.serviceManager.addSingleton(IRegistry, RegistryImplementation); + } + public registerMockProcess() { this.serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); diff --git a/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts b/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts index e85da9496e88..c0caba42a3d7 100644 --- a/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts +++ b/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts @@ -9,8 +9,11 @@ import * as TypeMoq from 'typemoq'; import { Disposable, Uri, WorkspaceFolder } from 'vscode'; import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { CondaExecutionService } from '../../../client/common/process/condaExecutionService'; +import { IProcessService, IPythonExecutionFactory } from '../../../client/common/process/types'; import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; import { DjangoShellCodeExecutionProvider } from '../../../client/terminals/codeExecution/djangoShellCodeExecution'; import { ICodeExecutionService } from '../../../client/terminals/types'; import { PYTHON_PATH } from '../../common'; @@ -23,6 +26,7 @@ suite('Terminal - Django Shell Code Execution', () => { let workspace: TypeMoq.IMock; let platform: TypeMoq.IMock; let settings: TypeMoq.IMock; + let pythonExecutionFactory: TypeMoq.IMock; let disposables: Disposable[] = []; setup(() => { const terminalFactory = TypeMoq.Mock.ofType(); @@ -30,7 +34,9 @@ suite('Terminal - Django Shell Code Execution', () => { terminalService = TypeMoq.Mock.ofType(); const configService = TypeMoq.Mock.ofType(); workspace = TypeMoq.Mock.ofType(); - workspace.setup(c => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { + workspace + .setup(c => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { return { dispose: () => void 0 }; @@ -39,8 +45,18 @@ suite('Terminal - Django Shell Code Execution', () => { const documentManager = TypeMoq.Mock.ofType(); const commandManager = TypeMoq.Mock.ofType(); const fileSystem = TypeMoq.Mock.ofType(); - executor = new DjangoShellCodeExecutionProvider(terminalFactory.object, configService.object, - workspace.object, documentManager.object, platform.object, commandManager.object, fileSystem.object, disposables); + pythonExecutionFactory = TypeMoq.Mock.ofType(); + executor = new DjangoShellCodeExecutionProvider( + terminalFactory.object, + configService.object, + workspace.object, + documentManager.object, + platform.object, + commandManager.object, + fileSystem.object, + pythonExecutionFactory.object, + disposables + ); terminalFactory.setup(f => f.getTerminalService(TypeMoq.It.isAny())).returns(() => terminalService.object); @@ -58,13 +74,12 @@ suite('Terminal - Django Shell Code Execution', () => { disposables = []; }); - function testReplCommandArguments(isWindows: boolean, pythonPath: string, expectedPythonPath: string, - terminalArgs: string[], expectedTerminalArgs: string[], resource?: Uri) { + async function testReplCommandArguments(isWindows: boolean, pythonPath: string, expectedPythonPath: string, terminalArgs: string[], expectedTerminalArgs: string[], resource?: Uri) { platform.setup(p => p.isWindows).returns(() => isWindows); settings.setup(s => s.pythonPath).returns(() => pythonPath); terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); - const replCommandArgs = (executor as DjangoShellCodeExecutionProvider).getReplCommandArgs(resource); + const replCommandArgs = await (executor as DjangoShellCodeExecutionProvider).getExecutableInfo(resource); expect(replCommandArgs).not.to.be.an('undefined', 'Command args is undefined'); expect(replCommandArgs.command).to.be.equal(expectedPythonPath, 'Incorrect python path'); expect(replCommandArgs.args).to.be.deep.equal(expectedTerminalArgs, 'Incorrect arguments'); @@ -75,7 +90,7 @@ suite('Terminal - Django Shell Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); - testReplCommandArguments(true, pythonPath, 'c:/program files/python/python.exe', terminalArgs, expectedTerminalArgs); + await testReplCommandArguments(true, pythonPath, 'c:/program files/python/python.exe', terminalArgs, expectedTerminalArgs); }); test('Ensure fully qualified python path is returned as is, when building repl args on Windows', async () => { @@ -83,7 +98,7 @@ suite('Terminal - Django Shell Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); }); test('Ensure python path is returned as is, when building repl args on Windows', async () => { @@ -91,7 +106,7 @@ suite('Terminal - Django Shell Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); }); test('Ensure fully qualified python path is returned as is, on non Windows', async () => { @@ -99,7 +114,7 @@ suite('Terminal - Django Shell Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); }); test('Ensure python path is returned as is, on non Windows', async () => { @@ -107,7 +122,7 @@ suite('Terminal - Django Shell Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); }); test('Ensure current workspace folder (containing spaces) is used to prefix manage.py', async () => { @@ -118,7 +133,7 @@ suite('Terminal - Django Shell Code Execution', () => { workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); const expectedTerminalArgs = terminalArgs.concat(`${path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument()}`, 'shell'); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); }); test('Ensure current workspace folder (without spaces) is used to prefix manage.py', async () => { @@ -129,7 +144,7 @@ suite('Terminal - Django Shell Code Execution', () => { workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); const expectedTerminalArgs = terminalArgs.concat(path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument(), 'shell'); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); }); test('Ensure default workspace folder (containing spaces) is used to prefix manage.py', async () => { @@ -141,7 +156,7 @@ suite('Terminal - Django Shell Code Execution', () => { workspace.setup(w => w.workspaceFolders).returns(() => [workspaceFolder]); const expectedTerminalArgs = terminalArgs.concat(`${path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument()}`, 'shell'); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); }); test('Ensure default workspace folder (without spaces) is used to prefix manage.py', async () => { @@ -153,7 +168,42 @@ suite('Terminal - Django Shell Code Execution', () => { workspace.setup(w => w.workspaceFolders).returns(() => [workspaceFolder]); const expectedTerminalArgs = terminalArgs.concat(path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument(), 'shell'); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); }); + async function testReplCondaCommandArguments(pythonPath: string, terminalArgs: string[], condaEnv: { name: string; path: string }, resource?: Uri) { + settings.setup(s => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + + const condaFile = 'conda'; + const serviceContainer = TypeMoq.Mock.ofType(); + const processService = TypeMoq.Mock.ofType(); + const condaExecutionService = new CondaExecutionService(serviceContainer.object, processService.object, pythonPath, condaFile, condaEnv); + const hasEnvName = condaEnv.name !== ''; + const condaArgs = ['run', ...(hasEnvName ? ['-n', condaEnv.name] : ['-p', condaEnv.path]), 'python']; + const expectedTerminalArgs = [...condaArgs, ...terminalArgs, 'manage.py', 'shell']; + pythonExecutionFactory.setup(p => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(condaExecutionService)); + + const replCommandArgs = await (executor as DjangoShellCodeExecutionProvider).getExecutableInfo(resource); + + expect(replCommandArgs).not.to.be.an('undefined', 'Conda command args are undefined'); + expect(replCommandArgs.command).to.be.equal(condaFile, 'Incorrect conda path'); + expect(replCommandArgs.args).to.be.deep.equal(expectedTerminalArgs, 'Incorrect conda arguments'); + } + + test('Ensure conda args including env name are passed when using a conda environment with a name', async () => { + const pythonPath = 'c:/program files/python/python.exe'; + const condaPath = { name: 'foo-env', path: 'path/to/foo-env' }; + const terminalArgs = ['-a', 'b', '-c']; + + await testReplCondaCommandArguments(pythonPath, terminalArgs, condaPath); + }); + + test('Ensure conda args including env path are passed when using a conda environment with an empty name', async () => { + const pythonPath = 'c:/program files/python/python.exe'; + const condaPath = { name: '', path: 'path/to/foo-env' }; + const terminalArgs = ['-a', 'b', '-c']; + + await testReplCondaCommandArguments(pythonPath, terminalArgs, condaPath); + }); }); diff --git a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts index 35bffee23ddb..771390fe712b 100644 --- a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts +++ b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts @@ -9,9 +9,12 @@ import * as TypeMoq from 'typemoq'; import { Disposable, Uri, WorkspaceFolder } from 'vscode'; import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { CondaExecutionService } from '../../../client/common/process/condaExecutionService'; +import { IProcessService, IPythonExecutionFactory } from '../../../client/common/process/types'; import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../../client/common/types'; import { noop } from '../../../client/common/utils/misc'; +import { IServiceContainer } from '../../../client/ioc/types'; import { DjangoShellCodeExecutionProvider } from '../../../client/terminals/codeExecution/djangoShellCodeExecution'; import { ReplProvider } from '../../../client/terminals/codeExecution/repl'; import { TerminalCodeExecutionProvider } from '../../../client/terminals/codeExecution/terminalCodeExecution'; @@ -33,6 +36,7 @@ suite('Terminal - Code Execution', () => { let documentManager: TypeMoq.IMock; let commandManager: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; + let pythonExecutionFactory: TypeMoq.IMock; let isDjangoRepl: boolean; teardown(() => { @@ -56,18 +60,25 @@ suite('Terminal - Code Execution', () => { documentManager = TypeMoq.Mock.ofType(); commandManager = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); - + pythonExecutionFactory = TypeMoq.Mock.ofType(); settings = TypeMoq.Mock.ofType(); settings.setup(s => s.terminal).returns(() => terminalSettings.object); configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); switch (testSuiteName) { case 'Terminal Execution': { - executor = new TerminalCodeExecutionProvider(terminalFactory.object, configService.object, workspace.object, disposables, platform.object); + executor = new TerminalCodeExecutionProvider( + terminalFactory.object, + configService.object, + workspace.object, + disposables, + platform.object, + pythonExecutionFactory.object + ); break; } case 'Repl Execution': { - executor = new ReplProvider(terminalFactory.object, configService.object, workspace.object, disposables, platform.object); + executor = new ReplProvider(terminalFactory.object, configService.object, workspace.object, pythonExecutionFactory.object, disposables, platform.object); expectedTerminalTitle = 'REPL'; break; } @@ -76,8 +87,17 @@ suite('Terminal - Code Execution', () => { workspace.setup(w => w.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { return { dispose: noop }; }); - executor = new DjangoShellCodeExecutionProvider(terminalFactory.object, configService.object, workspace.object, documentManager.object, - platform.object, commandManager.object, fileSystem.object, disposables); + executor = new DjangoShellCodeExecutionProvider( + terminalFactory.object, + configService.object, + workspace.object, + documentManager.object, + platform.object, + commandManager.object, + fileSystem.object, + pythonExecutionFactory.object, + disposables + ); expectedTerminalTitle = 'Django Shell'; break; } @@ -209,11 +229,13 @@ suite('Terminal - Code Execution', () => { terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); terminalSettings.setup(t => t.executeInFileDir).returns(() => false); workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + pythonExecutionFactory.setup(p => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); await executor.executeFile(file); const expectedPythonPath = isWindows ? pythonPath.replace(/\\/g, '/') : pythonPath; const expectedArgs = terminalArgs.concat(file.fsPath.fileToCommandArgument()); terminalService.verify(async t => t.sendCommand(TypeMoq.It.isValue(expectedPythonPath), TypeMoq.It.isValue(expectedArgs)), TypeMoq.Times.once()); + pythonExecutionFactory.verify(async p => p.createCondaExecutionService(pythonPath, undefined, file), TypeMoq.Times.once()); } test('Ensure python file execution script is sent to terminal on windows', async () => { @@ -236,51 +258,116 @@ suite('Terminal - Code Execution', () => { await testFileExecution(false, PYTHON_PATH, ['-a', '-b', '-c'], file); }); - function testReplCommandArguments(isWindows: boolean, pythonPath: string, expectedPythonPath: string, terminalArgs: string[]) { + async function testCondaFileExecution(pythonPath: string, terminalArgs: string[], file: Uri, condaEnv: { name: string; path: string }): Promise { + settings.setup(s => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + terminalSettings.setup(t => t.executeInFileDir).returns(() => false); + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + + const condaFile = 'conda'; + const serviceContainer = TypeMoq.Mock.ofType(); + const processService = TypeMoq.Mock.ofType(); + const condaExecutionService = new CondaExecutionService(serviceContainer.object, processService.object, pythonPath, condaFile, condaEnv); + pythonExecutionFactory.setup(p => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(condaExecutionService)); + + await executor.executeFile(file); + + const hasEnvName = condaEnv.name !== ''; + const condaArgs = ['run', ...(hasEnvName ? ['-n', condaEnv.name] : ['-p', condaEnv.path]), 'python']; + const expectedArgs = [...condaArgs, ...terminalArgs, file.fsPath.fileToCommandArgument()]; + + pythonExecutionFactory.verify(async p => p.createCondaExecutionService(pythonPath, undefined, file), TypeMoq.Times.once()); + terminalService.verify(async t => t.sendCommand(TypeMoq.It.isValue(condaFile), TypeMoq.It.isValue(expectedArgs)), TypeMoq.Times.once()); + } + + test('Ensure conda args with conda env name are sent to terminal if there is a conda environment with a name', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + await testCondaFileExecution(PYTHON_PATH, ['-a', '-b', '-c'], file, { name: 'foo-env', path: 'path/to/foo-env' }); + }); + + test('Ensure conda args with conda env path are sent to terminal if there is a conda environment without a name', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + await testCondaFileExecution(PYTHON_PATH, ['-a', '-b', '-c'], file, { name: '', path: 'path/to/foo-env' }); + }); + + async function testReplCommandArguments(isWindows: boolean, pythonPath: string, expectedPythonPath: string, terminalArgs: string[]) { + pythonExecutionFactory.setup(p => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); platform.setup(p => p.isWindows).returns(() => isWindows); settings.setup(s => s.pythonPath).returns(() => pythonPath); terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); const expectedTerminalArgs = isDjangoRepl ? terminalArgs.concat(['manage.py', 'shell']) : terminalArgs; - const replCommandArgs = (executor as TerminalCodeExecutionProvider).getReplCommandArgs(); + const replCommandArgs = await (executor as TerminalCodeExecutionProvider).getExecutableInfo(); expect(replCommandArgs).not.to.be.an('undefined', 'Command args is undefined'); expect(replCommandArgs.command).to.be.equal(expectedPythonPath, 'Incorrect python path'); expect(replCommandArgs.args).to.be.deep.equal(expectedTerminalArgs, 'Incorrect arguments'); + pythonExecutionFactory.verify(async p => p.createCondaExecutionService(pythonPath, undefined, undefined), TypeMoq.Times.once()); } - test('Ensure fully qualified python path is escaped when building repl args on Windows', () => { + test('Ensure fully qualified python path is escaped when building repl args on Windows', async () => { const pythonPath = 'c:\\program files\\python\\python.exe'; const terminalArgs = ['-a', 'b', 'c']; - testReplCommandArguments(true, pythonPath, 'c:/program files/python/python.exe', terminalArgs); + await testReplCommandArguments(true, pythonPath, 'c:/program files/python/python.exe', terminalArgs); }); - test('Ensure fully qualified python path is returned as is, when building repl args on Windows', () => { + test('Ensure fully qualified python path is returned as is, when building repl args on Windows', async () => { const pythonPath = 'c:/program files/python/python.exe'; const terminalArgs = ['-a', 'b', 'c']; - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); }); - test('Ensure python path is returned as is, when building repl args on Windows', () => { + test('Ensure python path is returned as is, when building repl args on Windows', async () => { const pythonPath = PYTHON_PATH; const terminalArgs = ['-a', 'b', 'c']; - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); }); - test('Ensure fully qualified python path is returned as is, on non Windows', () => { + test('Ensure fully qualified python path is returned as is, on non Windows', async () => { const pythonPath = 'usr/bin/python'; const terminalArgs = ['-a', 'b', 'c']; - testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); + await testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); }); - test('Ensure python path is returned as is, on non Windows', () => { + test('Ensure python path is returned as is, on non Windows', async () => { const pythonPath = PYTHON_PATH; const terminalArgs = ['-a', 'b', 'c']; - testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); + await testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); + }); + + async function testReplCondaCommandArguments(pythonPath: string, terminalArgs: string[], condaEnv: { name: string; path: string }) { + settings.setup(s => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + + const condaFile = 'conda'; + const serviceContainer = TypeMoq.Mock.ofType(); + const processService = TypeMoq.Mock.ofType(); + const condaExecutionService = new CondaExecutionService(serviceContainer.object, processService.object, pythonPath, condaFile, condaEnv); + pythonExecutionFactory.setup(p => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(condaExecutionService)); + + const hasEnvName = condaEnv.name !== ''; + const condaArgs = ['run', ...(hasEnvName ? ['-n', condaEnv.name] : ['-p', condaEnv.path]), 'python']; + const djangoArgs = isDjangoRepl ? ['manage.py', 'shell'] : []; + const expectedTerminalArgs = [...condaArgs, ...terminalArgs, ...djangoArgs]; + + const replCommandArgs = await (executor as TerminalCodeExecutionProvider).getExecutableInfo(); + + expect(replCommandArgs).not.to.be.an('undefined', 'Conda command args are undefined'); + expect(replCommandArgs.command).to.be.equal('conda', 'Incorrect conda path'); + expect(replCommandArgs.args).to.be.deep.equal(expectedTerminalArgs, 'Incorrect conda arguments'); + pythonExecutionFactory.verify(async p => p.createCondaExecutionService(pythonPath, undefined, undefined), TypeMoq.Times.once()); + } + + test('Ensure conda args with env name are returned when building repl args with a conda env with a name', async () => { + await testReplCondaCommandArguments(PYTHON_PATH, ['-a', 'b', 'c'], { name: 'foo-env', path: 'path/to/foo-env' }); + }); + + test('Ensure conda args with env path are returned when building repl args with a conda env without a name', async () => { + await testReplCondaCommandArguments(PYTHON_PATH, ['-a', 'b', 'c'], { name: '', path: 'path/to/foo-env' }); }); test('Ensure nothing happens when blank text is sent to the terminal', async () => { diff --git a/src/test/testing/nosetest/nosetest.run.test.ts b/src/test/testing/nosetest/nosetest.run.test.ts index 963e4d9a01b7..13e8512d17e8 100644 --- a/src/test/testing/nosetest/nosetest.run.test.ts +++ b/src/test/testing/nosetest/nosetest.run.test.ts @@ -7,8 +7,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import { IProcessServiceFactory } from '../../../client/common/process/types'; -import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { ICondaService } from '../../../client/interpreter/contracts'; import { CondaService } from '../../../client/interpreter/locators/services/condaService'; import { InterpreterHashProvider } from '../../../client/interpreter/locators/services/hashProvider'; import { InterpeterHashProviderFactory } from '../../../client/interpreter/locators/services/hashProviderFactory'; @@ -66,8 +65,8 @@ suite('Unit Tests - nose - run against actual python process', () => { ioc.registerVariableTypes(); ioc.registerMockProcessTypes(); + ioc.registerMockInterpreterTypes(); ioc.serviceManager.addSingleton(ICondaService, CondaService); - ioc.serviceManager.addSingleton(IInterpreterService, InterpreterService); ioc.serviceManager.addSingleton(WindowsStoreInterpreter, WindowsStoreInterpreter); ioc.serviceManager.addSingleton(InterpreterHashProvider, InterpreterHashProvider); diff --git a/src/test/testing/nosetest/nosetest.test.ts b/src/test/testing/nosetest/nosetest.test.ts index b601d8d5b81f..67725aa2e44e 100644 --- a/src/test/testing/nosetest/nosetest.test.ts +++ b/src/test/testing/nosetest/nosetest.test.ts @@ -3,8 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { ICondaService } from '../../../client/interpreter/contracts'; import { CondaService } from '../../../client/interpreter/locators/services/condaService'; import { CommandSource } from '../../../client/testing/common/constants'; import { ITestManagerFactory } from '../../../client/testing/common/types'; @@ -57,8 +56,8 @@ suite('Unit Tests - nose - discovery against actual python process', () => { ioc.registerProcessTypes(); ioc.registerUnitTestTypes(); ioc.registerVariableTypes(); + ioc.registerMockInterpreterTypes(); ioc.serviceManager.addSingleton(ICondaService, CondaService); - ioc.serviceManager.addSingleton(IInterpreterService, InterpreterService); } test('Discover Tests (single test file)', async () => { diff --git a/src/test/testing/pytest/pytest.discovery.test.ts b/src/test/testing/pytest/pytest.discovery.test.ts index 2cda90f3f3a8..e2bb6fffb547 100644 --- a/src/test/testing/pytest/pytest.discovery.test.ts +++ b/src/test/testing/pytest/pytest.discovery.test.ts @@ -47,9 +47,10 @@ suite('Unit Tests - pytest - discovery with mocked process output', () => { @inject(IEnvironmentActivationService) activationHelper: IEnvironmentActivationService, @inject(IProcessServiceFactory) processServiceFactory: IProcessServiceFactory, @inject(IConfigurationService) private readonly _configService: IConfigurationService, + @inject(ICondaService) condaService: ICondaService, @inject(WindowsStoreInterpreter) windowsStoreInterpreter: WindowsStoreInterpreter, @inject(IBufferDecoder) decoder: IBufferDecoder) { - super(_serviceContainer, activationHelper, processServiceFactory, _configService, decoder, windowsStoreInterpreter); + super(_serviceContainer, activationHelper, processServiceFactory, _configService, condaService, decoder, windowsStoreInterpreter); } public async createActivatedEnvironment(options: ExecutionFactoryCreateWithEnvironmentOptions): Promise { const pythonPath = options.interpreter ? options.interpreter.path : this._configService.getSettings(options.resource).pythonPath; diff --git a/src/test/testing/pytest/pytest.run.test.ts b/src/test/testing/pytest/pytest.run.test.ts index 79adf2ac7286..a3435669c020 100644 --- a/src/test/testing/pytest/pytest.run.test.ts +++ b/src/test/testing/pytest/pytest.run.test.ts @@ -316,9 +316,10 @@ suite('Unit Tests - pytest - run with mocked process output', () => { @inject(IEnvironmentActivationService) activationHelper: IEnvironmentActivationService, @inject(IProcessServiceFactory) processServiceFactory: IProcessServiceFactory, @inject(IConfigurationService) private readonly _configService: IConfigurationService, + @inject(ICondaService) condaService: ICondaService, @inject(WindowsStoreInterpreter) windowsStoreInterpreter: WindowsStoreInterpreter, @inject(IBufferDecoder) decoder: IBufferDecoder) { - super(_serviceContainer, activationHelper, processServiceFactory, _configService, decoder, windowsStoreInterpreter); + super(_serviceContainer, activationHelper, processServiceFactory, _configService, condaService, decoder, windowsStoreInterpreter); } public async createActivatedEnvironment(options: ExecutionFactoryCreateWithEnvironmentOptions): Promise { const pythonPath = options.interpreter ? options.interpreter.path : this._configService.getSettings(options.resource).pythonPath; diff --git a/src/test/testing/pytest/pytest.test.ts b/src/test/testing/pytest/pytest.test.ts index e580d22b8824..c7debe5b6249 100644 --- a/src/test/testing/pytest/pytest.test.ts +++ b/src/test/testing/pytest/pytest.test.ts @@ -2,8 +2,7 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { ICondaService } from '../../../client/interpreter/contracts'; import { CondaService } from '../../../client/interpreter/locators/services/condaService'; import { CommandSource } from '../../../client/testing/common/constants'; import { ITestManagerFactory } from '../../../client/testing/common/types'; @@ -36,8 +35,8 @@ suite('Unit Tests - pytest - discovery against actual python process', () => { ioc.registerProcessTypes(); ioc.registerUnitTestTypes(); ioc.registerVariableTypes(); + ioc.registerMockInterpreterTypes(); ioc.serviceManager.addSingleton(ICondaService, CondaService); - ioc.serviceManager.addSingleton(IInterpreterService, InterpreterService); } test('Discover Tests (single test file)', async () => {