diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index c4a3d13ecd73..1dc762e3e811 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -37,8 +37,8 @@ export type ExecutionResult = { export const IProcessService = Symbol('IProcessService'); export interface IProcessService { - execObservable(file: string, args: string[], options: SpawnOptions): ObservableExecutionResult; - exec(file: string, args: string[], options: SpawnOptions): Promise>; + execObservable(file: string, args: string[], options?: SpawnOptions): ObservableExecutionResult; + exec(file: string, args: string[], options?: SpawnOptions): Promise>; } export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory'); diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 2ad393ec1ff6..cbac6002fb58 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -58,7 +58,8 @@ export enum Product { mypy = 11, unittest = 12, ctags = 13, - rope = 14 + rope = 14, + isort = 15 } export enum ModuleNamePurpose { diff --git a/src/client/common/utils.ts b/src/client/common/utils.ts index 0f6d4ea06635..3e803b388466 100644 --- a/src/client/common/utils.ts +++ b/src/client/common/utils.ts @@ -1,7 +1,6 @@ 'use strict'; // tslint:disable: no-any one-line no-suspicious-comment prefer-template prefer-const no-unnecessary-callback-wrapper no-function-expression no-string-literal no-control-regex no-shadowed-variable -import * as child_process from 'child_process'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -30,18 +29,6 @@ export function fsReaddirAsync(root: string): Promise { }); } -export async function getPathFromPythonCommand(pythonPath: string): Promise { - return await new Promise((resolve, reject) => { - child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { - if (stdout) { - const lines = stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); - resolve(lines.length > 0 ? lines[0] : ''); - } else { - reject(); - } - }); - }); -} export function formatErrorForLogging(error: Error | string): string { let message: string = ''; if (typeof error === 'string') { diff --git a/src/client/extension.ts b/src/client/extension.ts index 61a6c5b74d5d..35e53f2c1cc6 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -14,6 +14,7 @@ import { STANDARD_OUTPUT_CHANNEL } from './common/constants'; import { FeatureDeprecationManager } from './common/featureDeprecationManager'; import { createDeferred } from './common/helpers'; import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; +import { IProcessService, IPythonExecutionFactory } from './common/process/types'; import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; import { GLOBAL_MEMENTO, IDisposableRegistry, ILogger, IMemento, IOutputChannel, IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types'; import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; @@ -85,7 +86,7 @@ export async function activate(context: vscode.ExtensionContext) { const pythonSettings = settings.PythonSettings.getInstance(); sendStartupTelemetry(activated, serviceContainer); - sortImports.activate(context, standardOutputChannel); + sortImports.activate(context, standardOutputChannel, serviceContainer); const interpreterManager = new InterpreterManager(serviceContainer); // This must be completed before we can continue. await interpreterManager.autoSetInterpreter(); @@ -93,15 +94,16 @@ export async function activate(context: vscode.ExtensionContext) { interpreterManager.refresh() .catch(ex => console.error('Python Extension: interpreterManager.refresh', ex)); context.subscriptions.push(interpreterManager); + const processService = serviceContainer.get(IProcessService); const interpreterVersionService = serviceContainer.get(IInterpreterVersionService); - context.subscriptions.push(new SetInterpreterProvider(interpreterManager, interpreterVersionService)); + context.subscriptions.push(new SetInterpreterProvider(interpreterManager, interpreterVersionService, processService)); context.subscriptions.push(...activateExecInTerminalProvider()); context.subscriptions.push(activateUpdateSparkLibraryProvider()); activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceContainer); const jediFactory = new JediFactory(context.asAbsolutePath('.'), serviceContainer); context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); - context.subscriptions.push(new ReplProvider()); + context.subscriptions.push(new ReplProvider(serviceContainer.get(IPythonExecutionFactory))); // Enable indentAction // tslint:disable-next-line:no-non-null-assertion @@ -130,7 +132,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.languages.registerHoverProvider(PYTHON, new PythonHoverProvider(jediFactory))); context.subscriptions.push(vscode.languages.registerReferenceProvider(PYTHON, new PythonReferenceProvider(jediFactory))); context.subscriptions.push(vscode.languages.registerCompletionItemProvider(PYTHON, new PythonCompletionItemProvider(jediFactory), '.')); - context.subscriptions.push(vscode.languages.registerCodeLensProvider(PYTHON, new ShebangCodeLensProvider())); + context.subscriptions.push(vscode.languages.registerCodeLensProvider(PYTHON, new ShebangCodeLensProvider(processService))); const symbolProvider = new PythonSymbolProvider(jediFactory); context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(PYTHON, symbolProvider)); diff --git a/src/client/interpreter/configuration/setInterpreterProvider.ts b/src/client/interpreter/configuration/setInterpreterProvider.ts index 22a776f97941..a359473c41cd 100644 --- a/src/client/interpreter/configuration/setInterpreterProvider.ts +++ b/src/client/interpreter/configuration/setInterpreterProvider.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import { commands, ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; import { InterpreterManager } from '../'; import * as settings from '../../common/configSettings'; +import { IProcessService } from '../../common/process/types'; import { IInterpreterVersionService, PythonInterpreter, WorkspacePythonPath } from '../contracts'; import { ShebangCodeLensProvider } from '../display/shebangCodeLensProvider'; import { PythonPathUpdaterService } from './pythonPathUpdaterService'; @@ -15,7 +16,9 @@ interface PythonPathQuickPickItem extends QuickPickItem { export class SetInterpreterProvider implements Disposable { private disposables: Disposable[] = []; private pythonPathUpdaterService: PythonPathUpdaterService; - constructor(private interpreterManager: InterpreterManager, interpreterVersionService: IInterpreterVersionService) { + constructor(private interpreterManager: InterpreterManager, + interpreterVersionService: IInterpreterVersionService, + private processService: IProcessService) { this.disposables.push(commands.registerCommand('python.setInterpreter', this.setInterpreter.bind(this))); this.disposables.push(commands.registerCommand('python.setShebangInterpreter', this.setShebangInterpreter.bind(this))); this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), interpreterVersionService); @@ -88,7 +91,7 @@ export class SetInterpreterProvider implements Disposable { } private async setShebangInterpreter(): Promise { - const shebang = await ShebangCodeLensProvider.detectShebang(window.activeTextEditor!.document); + const shebang = await new ShebangCodeLensProvider(this.processService).detectShebang(window.activeTextEditor!.document); if (!shebang) { return; } diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index 74efb2fd11fa..8ef1f093e1d7 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -1,11 +1,11 @@ -import * as child_process from 'child_process'; import { EOL } from 'os'; import * as path from 'path'; import { Disposable, StatusBarItem, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; +import { IProcessService } from '../../common/process/types'; import * as utils from '../../common/utils'; import { IInterpreterLocatorService, IInterpreterVersionService } from '../contracts'; -import { getActiveWorkspaceUri, getFirstNonEmptyLineFromMultilineString } from '../helpers'; +import { getActiveWorkspaceUri } from '../helpers'; import { IVirtualEnvironmentManager } from '../virtualEnvs/types'; // tslint:disable-next-line:completed-docs @@ -13,7 +13,8 @@ export class InterpreterDisplay implements Disposable { constructor(private statusBar: StatusBarItem, private interpreterLocator: IInterpreterLocatorService, private virtualEnvMgr: IVirtualEnvironmentManager, - private versionProvider: IInterpreterVersionService) { + private versionProvider: IInterpreterVersionService, + private processService: IProcessService) { this.statusBar.command = 'python.setInterpreter'; } @@ -69,11 +70,8 @@ export class InterpreterDisplay implements Disposable { .then(env => env ? env.name : ''); } private async getFullyQualifiedPathToInterpreter(pythonPath: string) { - return new Promise(resolve => { - child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { - resolve(getFirstNonEmptyLineFromMultilineString(stdout)); - }); - }) + return this.processService.exec(pythonPath, ['-c', 'import sys;print(sys.executable)']) + .then(output => output.stdout.trim()) .then(value => value.length === 0 ? pythonPath : value) .catch(() => pythonPath); } diff --git a/src/client/interpreter/display/shebangCodeLensProvider.ts b/src/client/interpreter/display/shebangCodeLensProvider.ts index c7cd0e19534e..134e3fa9e68b 100644 --- a/src/client/interpreter/display/shebangCodeLensProvider.ts +++ b/src/client/interpreter/display/shebangCodeLensProvider.ts @@ -1,16 +1,14 @@ -'use strict'; -import * as child_process from 'child_process'; import * as vscode from 'vscode'; import { CancellationToken, CodeLens, TextDocument } from 'vscode'; import * as settings from '../../common/configSettings'; +import { IProcessService } from '../../common/process/types'; import { IS_WINDOWS } from '../../common/utils'; -import { getFirstNonEmptyLineFromMultilineString } from '../../interpreter/helpers'; export class ShebangCodeLensProvider implements vscode.CodeLensProvider { - // tslint:disable-next-line:prefer-type-cast no-any + // tslint:disable-next-line:no-any public onDidChangeCodeLenses: vscode.Event = vscode.workspace.onDidChangeConfiguration as any as vscode.Event; - // tslint:disable-next-line:function-name - public static async detectShebang(document: TextDocument): Promise { + constructor(private processService: IProcessService) { } + public async detectShebang(document: TextDocument): Promise { const firstLine = document.lineAt(0); if (firstLine.isEmptyOrWhitespace) { return; @@ -21,40 +19,30 @@ export class ShebangCodeLensProvider implements vscode.CodeLensProvider { } const shebang = firstLine.text.substr(2).trim(); - const pythonPath = await ShebangCodeLensProvider.getFullyQualifiedPathToInterpreter(shebang); + const pythonPath = await this.getFullyQualifiedPathToInterpreter(shebang); return typeof pythonPath === 'string' && pythonPath.length > 0 ? pythonPath : undefined; } - private static async getFullyQualifiedPathToInterpreter(pythonPath: string) { - if (pythonPath.indexOf('bin/env ') >= 0 && !IS_WINDOWS) { - // In case we have pythonPath as '/usr/bin/env python' - return new Promise(resolve => { - const command = child_process.exec(`${pythonPath} -c 'import sys;print(sys.executable)'`); - let result = ''; - command.stdout.on('data', (data) => { - result += data.toString(); - }); - command.on('close', () => { - resolve(getFirstNonEmptyLineFromMultilineString(result)); - }); - }); - } else { - return new Promise(resolve => { - child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { - resolve(getFirstNonEmptyLineFromMultilineString(stdout)); - }); - }); - } - } - public async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { const codeLenses = await this.createShebangCodeLens(document); return Promise.resolve(codeLenses); } - + private async getFullyQualifiedPathToInterpreter(pythonPath: string) { + let cmdFile = pythonPath; + let args = ['-c', 'import sys;print(sys.executable)']; + if (pythonPath.indexOf('bin/env ') >= 0 && !IS_WINDOWS) { + // In case we have pythonPath as '/usr/bin/env python'. + const parts = pythonPath.split(' ').map(part => part.trim()).filter(part => part.length > 0); + cmdFile = parts.shift()!; + args = parts.concat(args); + } + return this.processService.exec(cmdFile, args) + .then(output => output.stdout.trim()) + .catch(() => ''); + } private async createShebangCodeLens(document: TextDocument) { - const shebang = await ShebangCodeLensProvider.detectShebang(document); + const shebang = await this.detectShebang(document); const pythonPath = settings.PythonSettings.getInstance(document.uri).pythonPath; - const resolvedPythonPath = await ShebangCodeLensProvider.getFullyQualifiedPathToInterpreter(pythonPath); + const resolvedPythonPath = await this.getFullyQualifiedPathToInterpreter(pythonPath); if (!shebang || shebang === resolvedPythonPath) { return []; } diff --git a/src/client/interpreter/index.ts b/src/client/interpreter/index.ts index c346dc1dfee7..9a120f6895a5 100644 --- a/src/client/interpreter/index.ts +++ b/src/client/interpreter/index.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import { ConfigurationTarget, Disposable, StatusBarAlignment, Uri, window, workspace } from 'vscode'; import { PythonSettings } from '../common/configSettings'; +import { IProcessService } from '../common/process/types'; import { IServiceContainer } from '../ioc/types'; import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; @@ -21,7 +22,8 @@ export class InterpreterManager implements Disposable { const statusBar = window.createStatusBarItem(StatusBarAlignment.Left); this.interpreterProvider = serviceContainer.get(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); const versionService = serviceContainer.get(IInterpreterVersionService); - this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr, versionService); + const processService = serviceContainer.get(IProcessService); + this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr, versionService, processService); this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), versionService); PythonSettings.getInstance().addListener('change', () => this.onConfigChanged()); this.disposables.push(window.onDidChangeActiveTextEditor(() => this.refresh())); diff --git a/src/client/interpreter/locators/services/condaEnvService.ts b/src/client/interpreter/locators/services/condaEnvService.ts index 370ac3df7ccb..eb67c1c5a044 100644 --- a/src/client/interpreter/locators/services/condaEnvService.ts +++ b/src/client/interpreter/locators/services/condaEnvService.ts @@ -1,8 +1,8 @@ -import * as child_process from 'child_process'; import * as fs from 'fs-extra'; import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; +import { IProcessService } from '../../../common/process/types'; import { VersionUtils } from '../../../common/versionUtils'; import { ICondaLocatorService, IInterpreterLocatorService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts'; import { AnacondaCompanyName, AnacondaCompanyNames, CONDA_RELATIVE_PY_PATH, CondaInfo } from './conda'; @@ -12,7 +12,8 @@ import { CondaHelper } from './condaHelper'; export class CondaEnvService implements IInterpreterLocatorService { private readonly condaHelper = new CondaHelper(); constructor( @inject(ICondaLocatorService) private condaLocator: ICondaLocatorService, - @inject(IInterpreterVersionService) private versionService: IInterpreterVersionService) { + @inject(IInterpreterVersionService) private versionService: IInterpreterVersionService, + @inject(IProcessService) private processService: IProcessService) { } public async getInterpreters(resource?: Uri) { return this.getSuggestionsFromConda(); @@ -99,32 +100,24 @@ export class CondaEnvService implements IInterpreterLocatorService { } private async getSuggestionsFromConda(): Promise { return this.condaLocator.getCondaFile() - .then(async condaFile => { - return new Promise((resolve, reject) => { - // interrogate conda (if it's on the path) to find all environments. - child_process.execFile(condaFile, ['info', '--json'], (_, stdout) => { - if (stdout.length === 0) { - resolve([]); - return; - } + .then(condaFile => this.processService.exec(condaFile, ['info', '--json'])) + .then(output => output.stdout) + .then(stdout => { + if (stdout.length === 0) { + return []; + } - try { - // tslint:disable-next-line:prefer-type-cast - const info = JSON.parse(stdout) as CondaInfo; - resolve(this.parseCondaInfo(info)); - } catch (e) { - // Failed because either: - // 1. conda is not installed. - // 2. `conda info --json` has changed signature. - // 3. output of `conda info --json` has changed in structure. - // In all cases, we can't offer conda pythonPath suggestions. - resolve([]); - } - }); - }).catch((err) => { - console.error('Python Extension (getSuggestionsFromConda):', err); + try { + const info = JSON.parse(stdout) as CondaInfo; + return this.parseCondaInfo(info); + } catch { + // Failed because either: + // 1. conda is not installed. + // 2. `conda info --json` has changed signature. + // 3. output of `conda info --json` has changed in structure. + // In all cases, we can't offer conda pythonPath suggestions. return []; - }); - }); + } + }).catch(() => []); } } diff --git a/src/client/interpreter/locators/services/condaLocator.ts b/src/client/interpreter/locators/services/condaLocator.ts index b866cf3727e3..0e28c2424238 100644 --- a/src/client/interpreter/locators/services/condaLocator.ts +++ b/src/client/interpreter/locators/services/condaLocator.ts @@ -1,4 +1,3 @@ -import * as child_process from 'child_process'; import * as fs from 'fs-extra'; import { inject, injectable, named, optional } from 'inversify'; import * as path from 'path'; @@ -70,15 +69,9 @@ export class CondaLocatorService implements ICondaLocatorService { } } public async isCondaInCurrentPath() { - return new Promise((resolve, reject) => { - child_process.execFile('conda', ['--version'], (_, stdout) => { - if (stdout && stdout.length > 0) { - resolve(true); - } else { - resolve(false); - } - }); - }); + return this.processService.exec('conda', ['--version']) + .then(output => output.stdout.length > 0) + .catch(() => false); } private async getCondaFileFromKnownLocations(): Promise { const condaFiles = await Promise.all(KNOWN_CONDA_LOCATIONS diff --git a/src/client/interpreter/locators/services/currentPathService.ts b/src/client/interpreter/locators/services/currentPathService.ts index 31f9dd070cbc..fb1940e8aee9 100644 --- a/src/client/interpreter/locators/services/currentPathService.ts +++ b/src/client/interpreter/locators/services/currentPathService.ts @@ -1,17 +1,17 @@ -import * as child_process from 'child_process'; import { inject, injectable } from 'inversify'; import * as _ from 'lodash'; import * as path from 'path'; import { Uri } from 'vscode'; import { PythonSettings } from '../../../common/configSettings'; +import { IProcessService } from '../../../common/process/types'; import { IInterpreterLocatorService, IInterpreterVersionService, InterpreterType } from '../../contracts'; -import { getFirstNonEmptyLineFromMultilineString } from '../../helpers'; import { IVirtualEnvironmentManager } from '../../virtualEnvs/types'; @injectable() export class CurrentPathService implements IInterpreterLocatorService { public constructor( @inject(IVirtualEnvironmentManager) private virtualEnvMgr: IVirtualEnvironmentManager, - @inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService) { } + @inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService, + @inject(IProcessService) private processService: IProcessService) { } public async getInterpreters(resource?: Uri) { return this.suggestionsFromKnownPaths(); } @@ -44,12 +44,8 @@ export class CurrentPathService implements IInterpreterLocatorService { }); } private async getInterpreter(pythonPath: string, defaultValue: string) { - return new Promise(resolve => { - // tslint:disable-next-line:variable-name - child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_err, stdout) => { - resolve(getFirstNonEmptyLineFromMultilineString(stdout)); - }); - }) + return this.processService.exec(pythonPath, ['-c', 'import sys;print(sys.executable)'], {}) + .then(output => output.stdout.trim()) .then(value => value.length === 0 ? defaultValue : value) .catch(() => defaultValue); // Ignore exceptions in getting the executable. } diff --git a/src/client/providers/importSortProvider.ts b/src/client/providers/importSortProvider.ts index 99c8961b672b..7033796f95f9 100644 --- a/src/client/providers/importSortProvider.ts +++ b/src/client/providers/importSortProvider.ts @@ -1,17 +1,18 @@ -'use strict'; -import * as child_process from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -import * as vscode from 'vscode'; +import { TextDocument, TextEdit } from 'vscode'; import { PythonSettings } from '../common/configSettings'; import { getTempFileWithDocumentContents, getTextEditsFromPatch } from '../common/editor'; +import { ExecutionResult, IProcessService, IPythonExecutionFactory } from '../common/process/types'; import { captureTelemetry } from '../telemetry'; import { FORMAT_SORT_IMPORTS } from '../telemetry/constants'; // tslint:disable-next-line:completed-docs export class PythonImportSortProvider { + constructor(private pythonExecutionFactory: IPythonExecutionFactory, + private processService: IProcessService) { } @captureTelemetry(FORMAT_SORT_IMPORTS) - public async sortImports(extensionDir: string, document: vscode.TextDocument): Promise { + public async sortImports(extensionDir: string, document: TextDocument): Promise { if (document.lineCount === 1) { return []; } @@ -23,35 +24,25 @@ export class PythonImportSortProvider { const tmpFileCreated = document.isDirty; const filePath = tmpFileCreated ? await getTempFileWithDocumentContents(document) : document.fileName; const settings = PythonSettings.getInstance(document.uri); - const pythonPath = settings.pythonPath; const isort = settings.sortImports.path; - const args = settings.sortImports.args.join(' '); - let isortCmd = ''; + const args = [filePath, '--diff'].concat(settings.sortImports.args); + let promise: Promise>; + if (typeof isort === 'string' && isort.length > 0) { - if (isort.indexOf(' ') > 0) { - isortCmd = `"${isort}" "${filePath}" --diff ${args}`; - } else { - isortCmd = `${isort} "${filePath}" --diff ${args}`; - } + // Lets just treat this as a standard tool. + promise = this.processService.exec(isort, args, { throwOnStdErr: true }); } else { - if (pythonPath.indexOf(' ') > 0) { - isortCmd = `"${pythonPath}" "${importScript}" "${filePath}" --diff ${args}`; - } else { - isortCmd = `${pythonPath} "${importScript}" "${filePath}" --diff ${args}`; + promise = this.pythonExecutionFactory.create(document.uri) + .then(executionService => executionService.exec([importScript].concat(args), { throwOnStdErr: true })); + } + + try { + const result = await promise; + return getTextEditsFromPatch(document.getText(), result.stdout); + } finally { + if (tmpFileCreated) { + fs.unlink(filePath); } } - // tslint:disable-next-line:promise-must-complete - return await new Promise((resolve, reject) => { - child_process.exec(isortCmd, (error, stdout, stderr) => { - if (tmpFileCreated) { - fs.unlink(filePath); - } - if (error || (stderr && stderr.length > 0)) { - reject(error ? error : stderr); - } else { - resolve(getTextEditsFromPatch(document.getText(), stdout)); - } - }); - }); } } diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts index 9c0f52d11862..38293ece0e34 100644 --- a/src/client/providers/jediProxy.ts +++ b/src/client/providers/jediProxy.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as child_process from 'child_process'; +import { ChildProcess } from 'child_process'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as vscode from 'vscode'; @@ -127,7 +127,7 @@ commandNames.set(CommandType.Usages, 'usages'); commandNames.set(CommandType.Symbols, 'names'); export class JediProxy implements vscode.Disposable { - private proc: child_process.ChildProcess | null; + private proc: ChildProcess | null; private pythonSettings: PythonSettings; private cmdId: number = 0; private lastKnownPythonInterpreter: string; diff --git a/src/client/providers/replProvider.ts b/src/client/providers/replProvider.ts index 753c261744ba..2928da9b06e7 100644 --- a/src/client/providers/replProvider.ts +++ b/src/client/providers/replProvider.ts @@ -1,13 +1,12 @@ -import { commands, Disposable, window, workspace } from 'vscode'; -import { PythonSettings } from '../common/configSettings'; +import { commands, Disposable, Uri, window } from 'vscode'; import { Commands } from '../common/constants'; -import { getPathFromPythonCommand } from '../common/utils'; +import { IPythonExecutionFactory } from '../common/process/types'; import { captureTelemetry } from '../telemetry'; import { REPL } from '../telemetry/constants'; export class ReplProvider implements Disposable { private readonly disposables: Disposable[] = []; - constructor() { + constructor(private pythonExecutionFactory: IPythonExecutionFactory) { this.registerCommand(); } public dispose() { @@ -19,31 +18,12 @@ export class ReplProvider implements Disposable { } @captureTelemetry(REPL) private async commandHandler() { - const pythonPath = await this.getPythonPath(); - if (!pythonPath) { - return; - } - let pythonInterpreterPath: string; - try { - pythonInterpreterPath = await getPathFromPythonCommand(pythonPath).catch(() => pythonPath); - // tslint:disable-next-line:variable-name - } catch (_ex) { - pythonInterpreterPath = pythonPath; - } + // If we have any active window open, then use that as the uri + const resource: Uri | undefined = window.activeTextEditor ? window.activeTextEditor!.document.uri : undefined; + const executionFactory = await this.pythonExecutionFactory.create(resource); + const pythonInterpreterPath = await executionFactory.getExecutablePath().catch(() => 'python'); const term = window.createTerminal('Python', pythonInterpreterPath); term.show(); this.disposables.push(term); } - private async getPythonPath(): Promise { - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { - return PythonSettings.getInstance().pythonPath; - } - if (workspace.workspaceFolders.length === 1) { - return PythonSettings.getInstance(workspace.workspaceFolders[0].uri).pythonPath; - } - - // tslint:disable-next-line:no-any prefer-type-cast - const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); - return workspace ? PythonSettings.getInstance(workspaceFolder.uri).pythonPath : undefined; - } } diff --git a/src/client/refactor/proxy.ts b/src/client/refactor/proxy.ts index 9cc5b684a642..93b94553d8df 100644 --- a/src/client/refactor/proxy.ts +++ b/src/client/refactor/proxy.ts @@ -1,6 +1,6 @@ // tslint:disable:no-any no-empty member-ordering prefer-const prefer-template no-var-self -import * as child_process from 'child_process'; +import { ChildProcess } from 'child_process'; import * as path from 'path'; import * as vscode from 'vscode'; import { Uri } from 'vscode'; @@ -12,7 +12,7 @@ import { getWindowsLineEndingCount, IS_WINDOWS } from '../common/utils'; import { IServiceContainer } from '../ioc/types'; export class RefactorProxy extends vscode.Disposable { - private _process?: child_process.ChildProcess; + private _process?: ChildProcess; private _extensionDir: string; private _previousOutData: string = ''; private _previousStdErrData: string = ''; diff --git a/src/client/sortImports.ts b/src/client/sortImports.ts index 9ba64f0a0dbf..53fadc8769a9 100644 --- a/src/client/sortImports.ts +++ b/src/client/sortImports.ts @@ -1,13 +1,13 @@ -'use strict'; - +import * as os from 'os'; import * as vscode from 'vscode'; +import { IProcessService, IPythonExecutionFactory } from './common/process/types'; +import { IServiceContainer } from './ioc/types'; import * as sortProvider from './providers/importSortProvider'; -import * as os from 'os'; -export function activate(context: vscode.ExtensionContext, outChannel: vscode.OutputChannel) { - let rootDir = context.asAbsolutePath('.'); - let disposable = vscode.commands.registerCommand('python.sortImports', () => { - let activeEditor = vscode.window.activeTextEditor; +export function activate(context: vscode.ExtensionContext, outChannel: vscode.OutputChannel, serviceContainer: IServiceContainer) { + const rootDir = context.asAbsolutePath('.'); + const disposable = vscode.commands.registerCommand('python.sortImports', () => { + const activeEditor = vscode.window.activeTextEditor; if (!activeEditor || activeEditor.document.languageId !== 'python') { vscode.window.showErrorMessage('Please open a Python source file to sort the imports.'); return Promise.resolve(); @@ -21,6 +21,7 @@ export function activate(context: vscode.ExtensionContext, outChannel: vscode.Ou const lastLine = activeEditor.document.lineAt(activeEditor.document.lineCount - 1); let emptyLineAdded = Promise.resolve(true); if (lastLine.text.trim().length > 0) { + // tslint:disable-next-line:no-any emptyLineAdded = new Promise((resolve, reject) => { activeEditor.edit(builder => { builder.insert(lastLine.range.end, os.EOL); @@ -28,17 +29,17 @@ export function activate(context: vscode.ExtensionContext, outChannel: vscode.Ou }); } return emptyLineAdded.then(() => { - return new sortProvider.PythonImportSortProvider().sortImports(rootDir, activeEditor.document); + const processService = serviceContainer.get(IProcessService); + const pythonExecutionFactory = serviceContainer.get(IPythonExecutionFactory); + return new sortProvider.PythonImportSortProvider(pythonExecutionFactory, processService).sortImports(rootDir, activeEditor.document); }).then(changes => { - if (changes.length === 0) { + if (!changes || changes!.length === 0) { return; } - return activeEditor.edit(builder => { - changes.forEach(change => builder.replace(change.range, change.newText)); - }); + return new Promise((resolve, reject) => activeEditor.edit(builder => changes.forEach(change => builder.replace(change.range, change.newText))).then(resolve, reject)); }).catch(error => { - let message = typeof error === 'string' ? error : (error.message ? error.message : error); + const message = typeof error === 'string' ? error : (error.message ? error.message : error); outChannel.appendLine(error); outChannel.show(); vscode.window.showErrorMessage(message); diff --git a/src/client/workspaceSymbols/generator.ts b/src/client/workspaceSymbols/generator.ts index 0f8876c1a8e7..2696e8c6f237 100644 --- a/src/client/workspaceSymbols/generator.ts +++ b/src/client/workspaceSymbols/generator.ts @@ -1,8 +1,8 @@ -import * as child_process from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; import { IPythonSettings, PythonSettings } from '../common/configSettings'; +import { IProcessService } from '../common/process/types'; import { captureTelemetry } from '../telemetry'; import { WORKSPACE_SYMBOLS_BUILD } from '../telemetry/constants'; @@ -16,7 +16,8 @@ export class Generator implements vscode.Disposable { public get enabled(): boolean { return this.pythonSettings.workspaceSymbols.enabled; } - constructor(public readonly workspaceFolder: vscode.Uri, private output: vscode.OutputChannel) { + constructor(public readonly workspaceFolder: vscode.Uri, private output: vscode.OutputChannel, + private processService: IProcessService) { this.disposables = []; this.optionsFile = path.join(__dirname, '..', '..', '..', 'resources', 'ctagOptions'); this.pythonSettings = PythonSettings.getInstance(workspaceFolder); @@ -61,33 +62,22 @@ export class Generator implements vscode.Disposable { this.output.appendLine(`${'-'.repeat(10)}Generating Tags${'-'.repeat(10)}`); this.output.appendLine(`${cmd} ${args.join(' ')}`); const promise = new Promise((resolve, reject) => { - const options: child_process.SpawnOptions = { - cwd: source.directory - }; - - let hasErrors = false; + const result = this.processService.execObservable(cmd, args, { cwd: source.directory }); let errorMsg = ''; - const proc = child_process.spawn(cmd, args, options); - proc.stderr.setEncoding('utf8'); - proc.stdout.setEncoding('utf8'); - proc.on('error', (error: Error) => { - reject(error); - }); - proc.stderr.on('data', (data: string) => { - hasErrors = true; - errorMsg += data; - this.output.append(data); - }); - proc.stdout.on('data', (data: string) => { - this.output.append(data); - }); - proc.on('exit', () => { - if (hasErrors) { - reject(errorMsg); - } else { - resolve(); + result.out.subscribe(output => { + if (output.source === 'stderr') { + errorMsg += output.out; } - }); + this.output.append(output.out); + }, + reject, + () => { + if (errorMsg.length > 0) { + reject(new Error(errorMsg)); + } else { + resolve(); + } + }); }); vscode.window.setStatusBarMessage('Generating Tags', promise); diff --git a/src/client/workspaceSymbols/main.ts b/src/client/workspaceSymbols/main.ts index b84b2c1682f5..1690ede4f38c 100644 --- a/src/client/workspaceSymbols/main.ts +++ b/src/client/workspaceSymbols/main.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { OutputChannel, workspace } from 'vscode'; import { Commands, PythonLanguage, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; import { isNotInstalledError } from '../common/helpers'; +import { IProcessService } from '../common/process/types'; import { IInstaller, InstallerResponse, IOutputChannel, Product } from '../common/types'; import { fsExistsAsync } from '../common/utils'; import { IServiceContainer } from '../ioc/types'; @@ -36,7 +37,8 @@ export class WorkspaceSymbols implements vscode.Disposable { if (Array.isArray(vscode.workspace.workspaceFolders)) { vscode.workspace.workspaceFolders.forEach(wkSpc => { - this.generators.push(new Generator(wkSpc.uri, this.outputChannel)); + const processService = this.serviceContainer.get(IProcessService); + this.generators.push(new Generator(wkSpc.uri, this.outputChannel, processService)); }); } } diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 3354150afade..267e6cfa2962 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -83,7 +83,7 @@ suite('Installer', () => { test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async () => { ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('one', false)); ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('two', true)); - if (prod.value === Product.ctags || prod.value === Product.unittest) { + if (prod.value === Product.ctags || prod.value === Product.unittest || prod.value === Product.isort) { return; } await testCheckingIfProductIsInstalled(prod.value); @@ -109,7 +109,7 @@ suite('Installer', () => { test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async () => { ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('one', false)); ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('two', true)); - if (prod.value === Product.unittest || prod.value === Product.ctags) { + if (prod.value === Product.unittest || prod.value === Product.ctags || prod.value === Product.isort) { return; } await testInstallingProduct(prod.value); diff --git a/src/test/format/extension.sort.test.ts b/src/test/format/extension.sort.test.ts index c8c2f1c6ffc3..af88891e6e91 100644 --- a/src/test/format/extension.sort.test.ts +++ b/src/test/format/extension.sort.test.ts @@ -3,9 +3,11 @@ import * as fs from 'fs'; import { EOL } from 'os'; import * as path from 'path'; import { commands, ConfigurationTarget, Position, Range, Uri, window, workspace } from 'vscode'; +import { IProcessService, IPythonExecutionFactory } from '../../client/common/process/types'; import { PythonImportSortProvider } from '../../client/providers/importSortProvider'; import { updateSetting } from '../common'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { UnitTestIocContainer } from '../unittests/serviceRegistry'; const sortingPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'sorting'); const fileToFormatWithoutConfig = path.join(sortingPath, 'noconfig', 'before.py'); @@ -18,9 +20,10 @@ const extensionDir = path.join(__dirname, '..', '..', '..'); // tslint:disable-next-line:max-func-body-length suite('Sorting', () => { + let ioc: UnitTestIocContainer; + let sorter: PythonImportSortProvider; const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; suiteSetup(initialize); - setup(initializeTest); suiteTeardown(async () => { fs.writeFileSync(fileToFormatWithConfig, fs.readFileSync(originalFileToFormatWithConfig)); fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); @@ -29,17 +32,30 @@ suite('Sorting', () => { await closeActiveWindows(); }); setup(async () => { + await initializeTest(); + initializeDI(); fs.writeFileSync(fileToFormatWithConfig, fs.readFileSync(originalFileToFormatWithConfig)); fs.writeFileSync(fileToFormatWithoutConfig, fs.readFileSync(originalFileToFormatWithoutConfig)); fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); await updateSetting('sortImports.args', [], Uri.file(sortingPath), configTarget); await closeActiveWindows(); + const pythonExecutionFactory = ioc.serviceContainer.get(IPythonExecutionFactory); + const processService = ioc.serviceContainer.get(IProcessService); + sorter = new PythonImportSortProvider(pythonExecutionFactory, processService); }); - + teardown(async () => { + ioc.dispose(); + await closeActiveWindows(); + }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + } test('Without Config', async () => { const textDocument = await workspace.openTextDocument(fileToFormatWithoutConfig); await window.showTextDocument(textDocument); - const sorter = new PythonImportSortProvider(); const edits = await sorter.sortImports(extensionDir, textDocument); assert.equal(edits.filter(value => value.newText === EOL && value.range.isEqual(new Range(2, 0, 2, 0))).length, 1, 'EOL not found'); assert.equal(edits.filter(value => value.newText === '' && value.range.isEqual(new Range(3, 0, 4, 0))).length, 1, '"" not found'); @@ -58,7 +74,6 @@ suite('Sorting', () => { test('With Config', async () => { const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); await window.showTextDocument(textDocument); - const sorter = new PythonImportSortProvider(); const edits = await sorter.sortImports(extensionDir, textDocument); const newValue = `from third_party import lib2${EOL}from third_party import lib3${EOL}from third_party import lib4${EOL}from third_party import lib5${EOL}from third_party import lib6${EOL}from third_party import lib7${EOL}from third_party import lib8${EOL}from third_party import lib9${EOL}`; assert.equal(edits.filter(value => value.newText === newValue && value.range.isEqual(new Range(0, 0, 3, 0))).length, 1, 'New Text not found'); @@ -79,7 +94,6 @@ suite('Sorting', () => { await editor.edit(builder => { builder.insert(new Position(0, 0), `from third_party import lib0${EOL}`); }); - const sorter = new PythonImportSortProvider(); const edits = await sorter.sortImports(extensionDir, textDocument); assert.notEqual(edits.length, 0, 'No edits'); }); diff --git a/src/test/initialize.ts b/src/test/initialize.ts index 1c0518399614..76d5a711b605 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -1,4 +1,3 @@ -import * as assert from 'assert'; import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; @@ -85,8 +84,6 @@ export async function closeActiveWindows(): Promise { }); }, 50); - }).then(() => { - assert.equal(vscode.window.visibleTextEditors.length, 0); }); } diff --git a/src/test/interpreters/condaEnvService.test.ts b/src/test/interpreters/condaEnvService.test.ts index 8f6142c77662..3ece985b08f8 100644 --- a/src/test/interpreters/condaEnvService.test.ts +++ b/src/test/interpreters/condaEnvService.test.ts @@ -19,8 +19,11 @@ suite('Interpreters from Conda Environments', () => { let ioc: UnitTestIocContainer; let processService: IProcessService; suiteSetup(initialize); - setup(initializeTest); - + setup(async () => { + await initializeTest(); + initializeDI(); + }); + teardown(() => ioc.dispose()); function initializeDI() { ioc = new UnitTestIocContainer(); ioc.registerCommonTypes(); @@ -30,13 +33,13 @@ suite('Interpreters from Conda Environments', () => { } test('Must return an empty list for empty json', async () => { - const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider('')); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider(''), processService); // tslint:disable-next-line:no-any prefer-type-cast const interpreters = await condaProvider.parseCondaInfo({} as any); assert.equal(interpreters.length, 0, 'Incorrect number of entries'); }); test('Must extract display name from version info', async () => { - const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider('')); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider(''), processService); const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), path.join(environmentsPath, 'conda', 'envs', 'scipy')], @@ -57,7 +60,7 @@ suite('Interpreters from Conda Environments', () => { assert.equal(interpreters[1].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); }); test('Must use the default display name if sys.version is invalid', async () => { - const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider('')); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider(''), processService); const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')], default_prefix: '', @@ -72,7 +75,7 @@ suite('Interpreters from Conda Environments', () => { assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); }); test('Must use the default display name if sys.version is empty', async () => { - const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider('')); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider(''), processService); const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')] }; @@ -85,7 +88,7 @@ suite('Interpreters from Conda Environments', () => { assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); }); test('Must include the default_prefix into the list of interpreters', async () => { - const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider('')); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider(''), processService); const info = { default_prefix: path.join(environmentsPath, 'conda', 'envs', 'numpy') }; @@ -98,7 +101,7 @@ suite('Interpreters from Conda Environments', () => { assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); }); test('Must exclude interpreters that do not exist on disc', async () => { - const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider('')); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider(''), processService); const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), path.join(environmentsPath, 'path0', 'one.exe'), @@ -130,7 +133,7 @@ suite('Interpreters from Conda Environments', () => { { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new CondaEnvService(new CondaLocatorService(true, processService, mockRegistryProvider), new MockInterpreterVersionProvider('')); + const condaProvider = new CondaEnvService(new CondaLocatorService(true, processService, mockRegistryProvider), new MockInterpreterVersionProvider(''), processService); assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[0]), false, '1. Identified environment incorrectly'); assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[1]), false, '2. Identified environment incorrectly'); @@ -153,7 +156,7 @@ suite('Interpreters from Conda Environments', () => { { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new CondaEnvService(new CondaLocatorService(true, processService, mockRegistryProvider), new MockInterpreterVersionProvider('')); + const condaProvider = new CondaEnvService(new CondaLocatorService(true, processService, mockRegistryProvider), new MockInterpreterVersionProvider(''), processService); // tslint:disable-next-line:no-non-null-assertion assert.equal(condaProvider.getLatestVersion(registryInterpreters)!.displayName, 'Two', 'Failed to identify latest version'); @@ -171,7 +174,7 @@ suite('Interpreters from Conda Environments', () => { { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new CondaEnvService(new CondaLocatorService(true, processService, mockRegistryProvider), new MockInterpreterVersionProvider('')); + const condaProvider = new CondaEnvService(new CondaLocatorService(true, processService, mockRegistryProvider), new MockInterpreterVersionProvider(''), processService); // tslint:disable-next-line:no-non-null-assertion assert.equal(condaProvider.getLatestVersion(registryInterpreters)!.displayName, 'Two', 'Failed to identify latest version'); diff --git a/src/test/interpreters/display.multiroot.test.ts b/src/test/interpreters/display.multiroot.test.ts index c2710638b23e..cee12255a878 100644 --- a/src/test/interpreters/display.multiroot.test.ts +++ b/src/test/interpreters/display.multiroot.test.ts @@ -2,11 +2,13 @@ import * as assert from 'assert'; import * as path from 'path'; import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; +import { IProcessService } from '../../client/common/process/types'; import { InterpreterDisplay } from '../../client/interpreter/display'; import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; import { clearPythonPathInWorkspaceFolder } from '../common'; import { closeActiveWindows, initialize, initializePython, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; import { MockStatusBarItem } from '../mockClasses'; +import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { MockInterpreterVersionProvider } from './mocks'; import { MockProvider } from './mocks'; @@ -16,6 +18,7 @@ const fileToOpen = path.join(workspace3Uri.fsPath, 'file.py'); // tslint:disable-next-line:max-func-body-length suite('Multiroot Interpreters Display', () => { + let ioc: UnitTestIocContainer; suiteSetup(async function () { if (!IS_MULTI_ROOT_TEST) { // tslint:disable-next-line:no-invalid-this @@ -23,13 +26,23 @@ suite('Multiroot Interpreters Display', () => { } await initialize(); }); - setup(initializeTest); + setup(async () => { + await initializeTest(); + initializeDI(); + }); suiteTeardown(initializePython); teardown(async () => { + ioc.dispose(); await clearPythonPathInWorkspaceFolder(fileToOpen); await initialize(); await closeActiveWindows(); }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + } test('Must get display name from workspace folder interpreter and not from interpreter in workspace', async () => { const settings = workspace.getConfiguration('python', Uri.file(fileToOpen)); @@ -44,7 +57,8 @@ suite('Multiroot Interpreters Display', () => { const provider = new MockProvider([]); const displayName = `${path.basename(pythonPath)} [Environment]`; const displayNameProvider = new MockInterpreterVersionProvider(displayName); - const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); + const processService = ioc.serviceContainer.get(IProcessService); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider, processService); await display.refresh(); assert.equal(statusBar.text, displayName, 'Incorrect display name'); diff --git a/src/test/interpreters/display.test.ts b/src/test/interpreters/display.test.ts index e82bc5ebe66a..8cfd46524922 100644 --- a/src/test/interpreters/display.test.ts +++ b/src/test/interpreters/display.test.ts @@ -4,6 +4,7 @@ import { EOL } from 'os'; import * as path from 'path'; import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; +import { IProcessService } from '../../client/common/process/types'; import { InterpreterType } from '../../client/interpreter/contracts'; import { InterpreterDisplay } from '../../client/interpreter/display'; import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; @@ -11,6 +12,7 @@ import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs' import { clearPythonPathInWorkspaceFolder, rootWorkspaceUri, updateSetting } from '../common'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; import { MockStatusBarItem } from '../mockClasses'; +import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { MockInterpreterVersionProvider } from './mocks'; import { MockProvider, MockVirtualEnv } from './mocks'; @@ -18,24 +20,35 @@ const fileInNonRootWorkspace = path.join(__dirname, '..', '..', '..', 'src', 'te // tslint:disable-next-line:max-func-body-length suite('Interpreters Display', () => { + let ioc: UnitTestIocContainer; + let processService: IProcessService; const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; suiteSetup(initialize); setup(async () => { + initializeDI(); await initializeTest(); if (IS_MULTI_ROOT_TEST) { await initializeMultiRoot(); } }); teardown(async () => { + ioc.dispose(); await clearPythonPathInWorkspaceFolder(fileInNonRootWorkspace); await initialize(); await closeActiveWindows(); }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + processService = ioc.serviceContainer.get(IProcessService); + } test('Must have command name', () => { const statusBar = new MockStatusBarItem(); const displayNameProvider = new MockInterpreterVersionProvider(''); // tslint:disable-next-line:no-unused-expression - new InterpreterDisplay(statusBar, new MockProvider([]), new VirtualEnvironmentManager([]), displayNameProvider); + new InterpreterDisplay(statusBar, new MockProvider([]), new VirtualEnvironmentManager([]), displayNameProvider, processService); assert.equal(statusBar.command, 'python.setInterpreter', 'Incorrect command name'); }); test('Must get display name from interpreter itself', async () => { @@ -43,7 +56,7 @@ suite('Interpreters Display', () => { const provider = new MockProvider([]); const displayName = 'Mock Display Name'; const displayNameProvider = new MockInterpreterVersionProvider(displayName); - const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider, processService); await display.refresh(); assert.equal(statusBar.text, displayName, 'Incorrect display name'); @@ -56,7 +69,7 @@ suite('Interpreters Display', () => { const env3 = new MockVirtualEnv(true, 'Mock 3'); const displayName = 'Mock Display Name'; const displayNameProvider = new MockInterpreterVersionProvider(displayName); - const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([env1, env2, env3]), displayNameProvider); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([env1, env2, env3]), displayNameProvider, processService); await display.refresh(); assert.equal(statusBar.text, `${displayName} (${env2.name})`, 'Incorrect display name'); }); @@ -65,7 +78,7 @@ suite('Interpreters Display', () => { const provider = new MockProvider([]); const displayName = 'Mock Display Name'; const displayNameProvider = new MockInterpreterVersionProvider(displayName, true); - const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider, processService); // Change interpreter to an invalid value const pythonPath = 'UnknownInterpreter'; await updateSetting('pythonPath', pythonPath, rootWorkspaceUri, configTarget); @@ -89,7 +102,7 @@ suite('Interpreters Display', () => { const provider = new MockProvider(interpreters); const displayName = 'Mock Display Name'; const displayNameProvider = new MockInterpreterVersionProvider(displayName, true); - const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider, processService); await display.refresh(); assert.equal(statusBar.text, interpreters[1].displayName, 'Incorrect display name'); @@ -109,7 +122,7 @@ suite('Interpreters Display', () => { ]; const provider = new MockProvider(interpreters); const displayNameProvider = new MockInterpreterVersionProvider(''); - const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider, processService); await display.refresh(); assert.equal(statusBar.text, interpreters[1].displayName, 'Incorrect display name'); @@ -124,7 +137,7 @@ suite('Interpreters Display', () => { ]; const provider = new MockProvider(interpreters); const displayNameProvider = new MockInterpreterVersionProvider('', true); - const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider, processService); // Change interpreter to an invalid value const pythonPath = 'UnknownInterpreter'; await updateSetting('pythonPath', pythonPath, rootWorkspaceUri, configTarget); diff --git a/src/test/providers/shebangCodeLenseProvider.test.ts b/src/test/providers/shebangCodeLenseProvider.test.ts index 158f6e6db987..31581af2d14e 100644 --- a/src/test/providers/shebangCodeLenseProvider.test.ts +++ b/src/test/providers/shebangCodeLenseProvider.test.ts @@ -2,10 +2,13 @@ import * as assert from 'assert'; import * as child_process from 'child_process'; import * as path from 'path'; import * as vscode from 'vscode'; +import { CancellationTokenSource } from 'vscode'; import { IS_WINDOWS, PythonSettings } from '../../client/common/configSettings'; +import { IProcessService } from '../../client/common/process/types'; import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider'; import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { UnitTestIocContainer } from '../unittests/serviceRegistry'; const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'shebang'); const fileShebang = path.join(autoCompPath, 'shebang.py'); @@ -14,13 +17,23 @@ const fileShebangInvalid = path.join(autoCompPath, 'shebangInvalid.py'); const filePlain = path.join(autoCompPath, 'plain.py'); suite('Shebang detection', () => { + let ioc: UnitTestIocContainer; suiteSetup(initialize); suiteTeardown(async () => { await initialize(); await closeActiveWindows(); }); - setup(initializeTest); - + setup(async () => { + initializeDI(); + await initializeTest(); + }); + teardown(() => ioc.dispose()); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + } test('A code lens will appear when sheban python and python in settings are different', async () => { const pythonPath = 'someUnknownInterpreter'; const editor = await openFile(fileShebang); @@ -30,14 +43,14 @@ suite('Shebang detection', () => { assert.equal(codeLenses.length, 1, 'No CodeLens available'); const codeLens = codeLenses[0]; assert(codeLens.range.isSingleLine, 'Invalid CodeLens Range'); - assert.equal(codeLens.command.command, 'python.setShebangInterpreter'); + assert.equal(codeLens.command!.command, 'python.setShebangInterpreter'); }); test('Code lens will not appear when sheban python and python in settings are the same', async () => { PythonSettings.dispose(); const pythonPath = await getFullyQualifiedPathToInterpreter('python'); const editor = await openFile(fileShebang); - PythonSettings.getInstance(editor.document.uri).pythonPath = pythonPath; + PythonSettings.getInstance(editor.document.uri).pythonPath = pythonPath!; const codeLenses = await setupCodeLens(editor); assert.equal(codeLenses.length, 0, 'CodeLens available although interpreters are equal'); @@ -58,14 +71,14 @@ suite('Shebang detection', () => { assert.equal(codeLenses.length, 1, 'No CodeLens available'); const codeLens = codeLenses[0]; assert(codeLens.range.isSingleLine, 'Invalid CodeLens Range'); - assert.equal(codeLens.command.command, 'python.setShebangInterpreter'); + assert.equal(codeLens.command!.command, 'python.setShebangInterpreter'); }); test('Code lens will not appear even when shebang python uses env and python settings are the same', async () => { const pythonPath = await getFullyQualifiedPathToInterpreter('python'); const editor = await openFile(fileShebangEnv); - PythonSettings.getInstance(editor.document.uri).pythonPath = pythonPath; + PythonSettings.getInstance(editor.document.uri).pythonPath = pythonPath!; const codeLenses = await setupCodeLens(editor); assert.equal(codeLenses.length, 0, 'CodeLens available although interpreters are equal'); }); @@ -93,7 +106,8 @@ suite('Shebang detection', () => { async function setupCodeLens(editor: vscode.TextEditor) { const document = editor.document; - const codeLensProvider = new ShebangCodeLensProvider(); - return await codeLensProvider.provideCodeLenses(document, null); + const processService = ioc.serviceContainer.get(IProcessService); + const codeLensProvider = new ShebangCodeLensProvider(processService); + return await codeLensProvider.provideCodeLenses(document, new CancellationTokenSource().token); } }); diff --git a/src/test/workspaceSymbols/multiroot.test.ts b/src/test/workspaceSymbols/multiroot.test.ts index 9d4de7940274..ccbf64b3adcb 100644 --- a/src/test/workspaceSymbols/multiroot.test.ts +++ b/src/test/workspaceSymbols/multiroot.test.ts @@ -1,15 +1,19 @@ import * as assert from 'assert'; import * as path from 'path'; import { CancellationTokenSource, ConfigurationTarget, Uri } from 'vscode'; +import { IProcessService } from '../../client/common/process/types'; import { Generator } from '../../client/workspaceSymbols/generator'; import { WorkspaceSymbolProvider } from '../../client/workspaceSymbols/provider'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; import { MockOutputChannel } from '../mockClasses'; +import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { updateSetting } from './../common'; const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); suite('Multiroot Workspace Symbols', () => { + let ioc: UnitTestIocContainer; + let processService: IProcessService; suiteSetup(function () { if (!IS_MULTI_ROOT_TEST) { // tslint:disable-next-line:no-invalid-this @@ -17,13 +21,24 @@ suite('Multiroot Workspace Symbols', () => { } return initialize(); }); - setup(initializeTest); + setup(async () => { + initializeDI(); + await initializeTest(); + }); suiteTeardown(closeActiveWindows); teardown(async () => { + ioc.dispose(); await closeActiveWindows(); await updateSetting('workspaceSymbols.enabled', false, Uri.file(path.join(multirootPath, 'parent', 'child')), ConfigurationTarget.WorkspaceFolder); await updateSetting('workspaceSymbols.enabled', false, Uri.file(path.join(multirootPath, 'workspace2')), ConfigurationTarget.WorkspaceFolder); }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + processService = ioc.serviceContainer.get(IProcessService); + } test('symbols should be returned when enabeld and vice versa', async () => { const childWorkspaceUri = Uri.file(path.join(multirootPath, 'parent', 'child')); @@ -31,7 +46,7 @@ suite('Multiroot Workspace Symbols', () => { await updateSetting('workspaceSymbols.enabled', false, childWorkspaceUri, ConfigurationTarget.WorkspaceFolder); - let generator = new Generator(childWorkspaceUri, outputChannel); + let generator = new Generator(childWorkspaceUri, outputChannel, processService); let provider = new WorkspaceSymbolProvider([generator], outputChannel); let symbols = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); assert.equal(symbols.length, 0, 'Symbols returned even when workspace symbols are turned off'); @@ -39,7 +54,7 @@ suite('Multiroot Workspace Symbols', () => { await updateSetting('workspaceSymbols.enabled', true, childWorkspaceUri, ConfigurationTarget.WorkspaceFolder); - generator = new Generator(childWorkspaceUri, outputChannel); + generator = new Generator(childWorkspaceUri, outputChannel, processService); provider = new WorkspaceSymbolProvider([generator], outputChannel); symbols = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); assert.notEqual(symbols.length, 0, 'Symbols should be returned when workspace symbols are turned on'); @@ -53,8 +68,8 @@ suite('Multiroot Workspace Symbols', () => { await updateSetting('workspaceSymbols.enabled', true, workspace2Uri, ConfigurationTarget.WorkspaceFolder); const generators = [ - new Generator(childWorkspaceUri, outputChannel), - new Generator(workspace2Uri, outputChannel)]; + new Generator(childWorkspaceUri, outputChannel, processService), + new Generator(workspace2Uri, outputChannel, processService)]; const provider = new WorkspaceSymbolProvider(generators, outputChannel); const symbols = await provider.provideWorkspaceSymbols('meth1Of', new CancellationTokenSource().token); diff --git a/src/test/workspaceSymbols/standard.test.ts b/src/test/workspaceSymbols/standard.test.ts index b2859b0fc428..5d264e7fd35a 100644 --- a/src/test/workspaceSymbols/standard.test.ts +++ b/src/test/workspaceSymbols/standard.test.ts @@ -1,35 +1,50 @@ import * as assert from 'assert'; import * as path from 'path'; import { CancellationTokenSource, ConfigurationTarget, Uri } from 'vscode'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { PythonSettings } from '../../client/common/configSettings'; +import { IProcessService } from '../../client/common/process/types'; import { Generator } from '../../client/workspaceSymbols/generator'; -import { MockOutputChannel } from '../mockClasses'; import { WorkspaceSymbolProvider } from '../../client/workspaceSymbols/provider'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { MockOutputChannel } from '../mockClasses'; +import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { updateSetting } from './../common'; -import { PythonSettings } from '../../client/common/configSettings'; const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); const configUpdateTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; suite('Workspace Symbols', () => { - suiteSetup(() => initialize()); - suiteTeardown(() => closeActiveWindows()); - setup(() => initializeTest()); + let ioc: UnitTestIocContainer; + let processService: IProcessService; + suiteSetup(initialize); + suiteTeardown(closeActiveWindows); + setup(async () => { + initializeDI(); + await initializeTest(); + }); teardown(async () => { + ioc.dispose(); await closeActiveWindows(); await updateSetting('workspaceSymbols.enabled', false, workspaceUri, configUpdateTarget); }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + processService = ioc.serviceContainer.get(IProcessService); + } - test(`symbols should be returned when enabeld and vice versa`, async () => { + test('symbols should be returned when enabeld and vice versa', async () => { const outputChannel = new MockOutputChannel('Output'); await updateSetting('workspaceSymbols.enabled', false, workspaceUri, configUpdateTarget); // The workspace will be in the output test folder // So lets modify the settings so it sees the source test folder let settings = PythonSettings.getInstance(workspaceUri); - settings.workspaceSymbols.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags') + settings.workspaceSymbols.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags'); - let generator = new Generator(workspaceUri, outputChannel); + let generator = new Generator(workspaceUri, outputChannel, processService); let provider = new WorkspaceSymbolProvider([generator], outputChannel); let symbols = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); assert.equal(symbols.length, 0, 'Symbols returned even when workspace symbols are turned off'); @@ -40,14 +55,14 @@ suite('Workspace Symbols', () => { // The workspace will be in the output test folder // So lets modify the settings so it sees the source test folder settings = PythonSettings.getInstance(workspaceUri); - settings.workspaceSymbols.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags') + settings.workspaceSymbols.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags'); - generator = new Generator(workspaceUri, outputChannel); + generator = new Generator(workspaceUri, outputChannel, processService); provider = new WorkspaceSymbolProvider([generator], outputChannel); symbols = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); assert.notEqual(symbols.length, 0, 'Symbols should be returned when workspace symbols are turned on'); }); - test(`symbols should be filtered correctly`, async () => { + test('symbols should be filtered correctly', async () => { const outputChannel = new MockOutputChannel('Output'); await updateSetting('workspaceSymbols.enabled', true, workspaceUri, configUpdateTarget); @@ -55,9 +70,9 @@ suite('Workspace Symbols', () => { // The workspace will be in the output test folder // So lets modify the settings so it sees the source test folder const settings = PythonSettings.getInstance(workspaceUri); - settings.workspaceSymbols.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags') + settings.workspaceSymbols.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags'); - const generators = [new Generator(workspaceUri, outputChannel)]; + const generators = [new Generator(workspaceUri, outputChannel, processService)]; const provider = new WorkspaceSymbolProvider(generators, outputChannel); const symbols = await provider.provideWorkspaceSymbols('meth1Of', new CancellationTokenSource().token);