From 8fe0dd7b64e058aa393ba7550d99c1bf59094df0 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 14 Sep 2020 16:10:26 -0700 Subject: [PATCH] Add copy of Python Extension API service for functional tests (#33) --- src/client/api/pythonApi.ts | 4 - src/client/interpreter/contracts.ts | 1 - .../datascience/dataScienceIocContainer.ts | 19 +++- .../datascience/notebook.functional.test.ts | 21 ---- src/test/interpreters/envActivation.ts | 19 ++++ src/test/interpreters/index.ts | 95 +++++++++++++++---- src/test/interpreters/interpreterService.ts | 18 +++- src/test/interpreters/selector.ts | 23 +++++ src/test/interpreters/winStoreInterpreter.ts | 24 +++++ 9 files changed, 175 insertions(+), 49 deletions(-) create mode 100644 src/test/interpreters/envActivation.ts create mode 100644 src/test/interpreters/selector.ts create mode 100644 src/test/interpreters/winStoreInterpreter.ts diff --git a/src/client/api/pythonApi.ts b/src/client/api/pythonApi.ts index b3929cc7461..3fef5d13d3f 100644 --- a/src/client/api/pythonApi.ts +++ b/src/client/api/pythonApi.ts @@ -134,8 +134,4 @@ export class InterpreterService implements IInterpreterService { public getInterpreterDetails(pythonPath: string, resource?: Uri): Promise { return this.api.getApi().then((api) => api.getInterpreterDetails(pythonPath, resource)); } - - public initialize(): void { - // Noop. - } } diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index 93e5875e808..98b646e1436 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -7,5 +7,4 @@ export interface IInterpreterService { getInterpreters(resource?: Uri): Promise; getActiveInterpreter(resource?: Uri): Promise; getInterpreterDetails(pythonPath: string, resource?: Uri): Promise; - initialize(): void; } diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 63b3ff15739..42152ebba56 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -304,10 +304,17 @@ import { } from '../../client/datascience/types'; import { ProtocolParser } from '../../client/debugger/extension/helpers/protocolParser'; import { IProtocolParser } from '../../client/debugger/extension/types'; +import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; +import { IInterpreterSelector } from '../../client/interpreter/configuration/types'; import { IInterpreterService } from '../../client/interpreter/contracts'; +import { IWindowsStoreInterpreter } from '../../client/interpreter/locators/types'; import { PythonEnvironment } from '../../client/pythonEnvironments/info'; import { CodeExecutionHelper } from '../../client/terminals/codeExecution/helper'; import { ICodeExecutionHelper } from '../../client/terminals/types'; +import { EnvironmentActivationService } from '../interpreters/envActivation'; +import { InterpreterService } from '../interpreters/interpreterService'; +import { InterpreterSelector } from '../interpreters/selector'; +import { WindowsStoreInterpreter } from '../interpreters/winStoreInterpreter'; import { MockOutputChannel } from '../mockClasses'; import { UnitTestIocContainer } from '../testing/serviceRegistry'; import { MockCommandManager } from './mockCommandManager'; @@ -905,6 +912,16 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.addSingletonInstance(KernelService, instance(this.kernelServiceMock)); } else { this.serviceManager.addSingleton(IInstaller, ProductInstaller); + this.serviceManager.addSingleton(IInterpreterService, InterpreterService); + this.serviceManager.addSingleton(IInterpreterSelector, InterpreterSelector); + this.serviceManager.addSingleton( + IWindowsStoreInterpreter, + WindowsStoreInterpreter + ); + this.serviceManager.addSingleton( + IEnvironmentActivationService, + EnvironmentActivationService + ); this.serviceManager.addSingleton(KernelService, KernelService); this.serviceManager.addSingleton(IProcessServiceFactory, ProcessServiceFactory); this.serviceManager.addSingleton(IPythonExecutionFactory, PythonExecutionFactory); @@ -975,7 +992,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { when(this.applicationShell.onDidChangeWindowState).thenReturn(eventCallback); when(this.applicationShell.withProgress(anything(), anything())).thenCall((_o, c) => c()); - const interpreterManager = this.serviceContainer.get(IInterpreterService); + const interpreterManager = this.serviceContainer.get(IInterpreterService); interpreterManager.initialize(); if (this.mockJupyter) { diff --git a/src/test/datascience/notebook.functional.test.ts b/src/test/datascience/notebook.functional.test.ts index f7048c9cf8c..14199204660 100644 --- a/src/test/datascience/notebook.functional.test.ts +++ b/src/test/datascience/notebook.functional.test.ts @@ -560,12 +560,6 @@ suite('DataScience notebook tests', () => { // Rewire our data we use to search for processes @injectable() class EmptyInterpreterService implements IInterpreterService { - public get hasInterpreters(): Promise { - return Promise.resolve(true); - } - public onDidChangeInterpreterConfiguration(): Disposable { - return { dispose: noop }; - } public onDidChangeInterpreter( _listener: (e: void) => any, _thisArgs?: any, @@ -577,27 +571,12 @@ suite('DataScience notebook tests', () => { public getInterpreters(_resource?: Uri): Promise { return Promise.resolve([]); } - public autoSetInterpreter(): Promise { - throw new Error('Method not implemented'); - } public getActiveInterpreter(_resource?: Uri): Promise { return Promise.resolve(undefined); } public getInterpreterDetails(_pythonPath: string, _resoure?: Uri): Promise { throw new Error('Method not implemented'); } - public refresh(_resource: Uri): Promise { - throw new Error('Method not implemented'); - } - public initialize(): void { - throw new Error('Method not implemented'); - } - public getDisplayName(_interpreter: Partial): Promise { - throw new Error('Method not implemented'); - } - public shouldAutoSetInterpreter(): Promise { - throw new Error('Method not implemented'); - } } ioc.serviceManager.rebind(IInterpreterService, EmptyInterpreterService); await createNotebook(undefined, undefined, true); diff --git a/src/test/interpreters/envActivation.ts b/src/test/interpreters/envActivation.ts new file mode 100644 index 00000000000..2c501c8b5b1 --- /dev/null +++ b/src/test/interpreters/envActivation.ts @@ -0,0 +1,19 @@ +import { injectable } from 'inversify'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { getActivatedEnvVariables } from '.'; +import { Resource } from '../../client/common/types'; +import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; + +@injectable() +export class EnvironmentActivationService implements IEnvironmentActivationService { + public getActivatedEnvironmentVariables( + _resource: Resource, + interpreter?: PythonEnvironment, + _allowExceptions?: boolean + ): Promise { + return getActivatedEnvVariables(interpreter?.path || process.env.CI_PYTHON_PATH || 'python'); + } +} diff --git a/src/test/interpreters/index.ts b/src/test/interpreters/index.ts index c6f3a904d04..191d78d163b 100644 --- a/src/test/interpreters/index.ts +++ b/src/test/interpreters/index.ts @@ -2,38 +2,95 @@ // Licensed under the MIT License. import * as path from 'path'; +import '../../client/common/extensions'; import { traceError } from '../../client/common/logger'; import { BufferDecoder } from '../../client/common/process/decoder'; import { PythonEnvInfo } from '../../client/common/process/internal/scripts'; import { ProcessService } from '../../client/common/process/proc'; import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { getOSType, OSType } from '../common'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; import { isCondaEnvironment } from './condaLocator'; import { getCondaEnvironment, getCondaFile, isCondaAvailable } from './condaService'; import { parsePythonVersion } from './pythonVersion'; const SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'pythonFiles'); +const defaultShells = { + [OSType.Windows]: 'cmd', + [OSType.OSX]: 'bash', + [OSType.Linux]: 'bash', + [OSType.Unknown]: undefined +}; +const defaultShell = defaultShells[getOSType()]; + +const interpreterInfoCache = new Map>(); export async function getInterpreterInfo(pythonPath: string): Promise { - const cli = await getPythonCli(pythonPath); - const processService = new ProcessService(new BufferDecoder()); - const argv = [...cli, path.join(SCRIPTS_DIR, 'interpreterInfo.py')]; - // Concat these together to make a set of quoted strings - const quoted = argv.reduce((p, c) => (p ? `${p} "${c}"` : `"${c.replace('\\', '\\\\')}"`), ''); + if (interpreterInfoCache.has(pythonPath)) { + return interpreterInfoCache.get(pythonPath); + } + + const promise = (async () => { + try { + const cli = await getPythonCli(pythonPath); + const processService = new ProcessService(new BufferDecoder()); + const argv = [...cli, path.join(SCRIPTS_DIR, 'interpreterInfo.py').fileToCommandArgument()]; + const cmd = argv.reduce((p, c) => (p ? `${p} "${c}"` : `"${c.replace('\\', '/')}"`), ''); + const result = await processService.shellExec(cmd, { + timeout: 1_500, + env: process.env, + shell: defaultShell + }); + if (result.stderr && result.stderr.length) { + traceError(`Failed to parse interpreter information for ${argv} stderr: ${result.stderr}`); + return; + } + const json: PythonEnvInfo = JSON.parse(result.stdout.trim()); + const rawVersion = `${json.versionInfo.slice(0, 3).join('.')}-${json.versionInfo[3]}`; + return { + path: pythonPath, + version: parsePythonVersion(rawVersion), + sysVersion: json.sysVersion, + sysPrefix: json.sysPrefix + }; + } catch (ex) { + traceError('Failed to get Activated env Variables'); + return undefined; + } + })(); + interpreterInfoCache.set(pythonPath, promise); + return promise; +} - const result = await processService.shellExec(quoted, { timeout: 1_500 }); - if (result.stderr) { - traceError(`Failed to parse interpreter information for ${argv} stderr: ${result.stderr}`); - return; +const envVariables = new Map>(); +export async function getActivatedEnvVariables(pythonPath: string): Promise { + if (envVariables.has(pythonPath)) { + return envVariables.get(pythonPath); } - const json: PythonEnvInfo = JSON.parse(result.stdout.trim()); - const rawVersion = `${json.versionInfo.slice(0, 3).join('.')}-${json.versionInfo[3]}`; - return { - path: pythonPath, - version: parsePythonVersion(rawVersion), - sysVersion: json.sysVersion, - sysPrefix: json.sysPrefix - }; + const promise = (async () => { + const cli = await getPythonCli(pythonPath); + const processService = new ProcessService(new BufferDecoder()); + const argv = [...cli, path.join(SCRIPTS_DIR, 'printEnvVariables.py')]; + const cmd = argv.reduce((p, c) => (p ? `${p} "${c}"` : `"${c.replace('\\', '/')}"`), ''); + const result = await processService.shellExec(cmd, { + timeout: 1_500, + maxBuffer: 1000 * 1000, + throwOnStdErr: false, + env: process.env, + shell: defaultShell + }); + if (result.stderr && result.stderr.length) { + traceError(`Failed to parse interpreter information for ${argv} stderr: ${result.stderr}`); + return; + } + try { + return JSON.parse(result.stdout.trim()); + } catch (ex) { + traceError(`Failed to parse interpreter information for ${argv}`, ex); + } + })(); + envVariables.set(pythonPath, promise); + return promise; } async function getPythonCli(pythonPath: string) { @@ -55,11 +112,11 @@ async function getPythonCli(pythonPath: string) { } const condaFile = await getCondaFile(); - return [condaFile, ...runArgs, 'python']; + return [condaFile.fileToCommandArgument(), ...runArgs, 'python']; } catch { // Noop. } traceError('Using Conda Interpreter, but no conda'); } - return [pythonPath]; + return [pythonPath.fileToCommandArgument()]; } diff --git a/src/test/interpreters/interpreterService.ts b/src/test/interpreters/interpreterService.ts index 7ee86b3380c..0b303e1b87a 100644 --- a/src/test/interpreters/interpreterService.ts +++ b/src/test/interpreters/interpreterService.ts @@ -1,3 +1,4 @@ +import { injectable } from 'inversify'; // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -6,12 +7,23 @@ import { getInterpreterInfo } from '.'; import { IInterpreterService } from '../../client/interpreter/contracts'; import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +@injectable() export class InterpreterService implements IInterpreterService { public readonly onDidChangeInterpreter = new EventEmitter().event; - public async getInterpreters(resource?: Uri): Promise { - const active = await this.getActiveInterpreter(resource); - return active ? [active] : []; + public async getInterpreters(_resource?: Uri): Promise { + const [active, globalInterpreter] = await Promise.all([ + getInterpreterInfo(process.env.CI_PYTHON_PATH as string), + getInterpreterInfo('python') + ]); + const interpreters: PythonEnvironment[] = []; + if (active) { + interpreters.push(active); + } + if (globalInterpreter) { + interpreters.push(globalInterpreter); + } + return interpreters; } public async getActiveInterpreter(_resource?: Uri): Promise { diff --git a/src/test/interpreters/selector.ts b/src/test/interpreters/selector.ts new file mode 100644 index 00000000000..7d3aea350da --- /dev/null +++ b/src/test/interpreters/selector.ts @@ -0,0 +1,23 @@ +import { inject, injectable } from 'inversify'; +import { Resource } from '../../client/common/types'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IInterpreterQuickPickItem, IInterpreterSelector } from '../../client/interpreter/configuration/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; + +@injectable() +export class InterpreterSelector implements IInterpreterSelector { + constructor(@inject(IInterpreterService) private readonly interpreterService: IInterpreterService) {} + + public async getSuggestions(resource: Resource): Promise { + const interpreters = await this.interpreterService.getInterpreters(resource); + return interpreters.map((item) => ({ + label: item.displayName || item.path, + description: item.displayName || item.path, + detail: item.displayName || item.path, + path: item.path, + interpreter: item + })); + } +} diff --git a/src/test/interpreters/winStoreInterpreter.ts b/src/test/interpreters/winStoreInterpreter.ts new file mode 100644 index 00000000000..13ba04e2b5e --- /dev/null +++ b/src/test/interpreters/winStoreInterpreter.ts @@ -0,0 +1,24 @@ +import { injectable } from 'inversify'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IWindowsStoreInterpreter } from '../../client/interpreter/locators/types'; + +@injectable() +export class WindowsStoreInterpreter implements IWindowsStoreInterpreter { + /** + * Whether this is a Windows Store/App Interpreter. + * + * @param {string} pythonPath + * @returns {boolean} + * @memberof WindowsStoreInterpreter + */ + public async isWindowsStoreInterpreter(pythonPath: string): Promise { + const pythonPathToCompare = pythonPath.toUpperCase().replace(/\//g, '\\'); + return ( + pythonPathToCompare.includes('\\Microsoft\\WindowsApps\\'.toUpperCase()) || + pythonPathToCompare.includes('\\Program Files\\WindowsApps\\'.toUpperCase()) || + pythonPathToCompare.includes('\\Microsoft\\WindowsApps\\PythonSoftwareFoundation'.toUpperCase()) + ); + } +}