diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts index ace2ae4775b4..2834b5642cfa 100644 --- a/src/client/pythonEnvironments/common/environmentIdentifier.ts +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -5,6 +5,7 @@ import { isCondaEnvironment } from '../discovery/locators/services/condaLocator' import { isPipenvEnvironment } from '../discovery/locators/services/pipEnvHelper'; import { isVenvEnvironment } from '../discovery/locators/services/venvLocator'; import { isVirtualenvEnvironment } from '../discovery/locators/services/virtualenvLocator'; +import { isVirtualenvwrapperEnvironment } from '../discovery/locators/services/virtualenvwrapperLocator'; import { isWindowsStoreEnvironment } from '../discovery/locators/services/windowsStoreLocator'; import { EnvironmentType } from '../info'; @@ -48,6 +49,10 @@ export async function identifyEnvironment(interpreterPath: string): Promise { + // The WORKON_HOME variable contains the path to the root directory of all virtualenvwrapper environments. + // If the interpreter path belongs to one of them then it is a virtualenvwrapper type of environment. + const workonHomeDir = getEnvironmentVariable('WORKON_HOME') || getDefaultVirtualenvwrapperDir(); + const environmentName = path.basename(path.dirname(path.dirname(interpreterPath))); + + let environmentDir = path.join(workonHomeDir, environmentName); + + if (getOSType() === OSType.Windows) { + environmentDir = environmentDir.toUpperCase(); + } + + return await pathExists(environmentDir) && interpreterPath.startsWith(`${environmentDir}${path.sep}`); +} diff --git a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts index 7027a05704f8..6f6bd4cbc818 100644 --- a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts @@ -8,6 +8,7 @@ import * as platformApis from '../../../client/common/utils/platform'; import { identifyEnvironment } from '../../../client/pythonEnvironments/common/environmentIdentifier'; import * as externalDependencies from '../../../client/pythonEnvironments/common/externalDependencies'; import { EnvironmentType } from '../../../client/pythonEnvironments/info'; +import { getOSType as getOSTypeForTest, OSType } from '../../common'; import { TEST_LAYOUT_ROOT } from './commonTestConstants'; suite('Environment Identifier', () => { @@ -145,6 +146,69 @@ suite('Environment Identifier', () => { }); }); + suite('Virtualenvwrapper', () => { + let getEnvVarStub: sinon.SinonStub; + let getOsTypeStub: sinon.SinonStub; + let getUserHomeDirStub: sinon.SinonStub; + + suiteSetup(() => { + getEnvVarStub = sinon.stub(platformApis, 'getEnvironmentVariable'); + getOsTypeStub = sinon.stub(platformApis, 'getOSType'); + getUserHomeDirStub = sinon.stub(platformApis, 'getUserHomeDir'); + + getUserHomeDirStub.returns(path.join(TEST_LAYOUT_ROOT, 'virtualenvwrapper1')); + }); + + suiteTeardown(() => { + getEnvVarStub.restore(); + getOsTypeStub.restore(); + getUserHomeDirStub.restore(); + }); + + test('WORKON_HOME is set to its default value ~/.virtualenvs on non-Windows', async function () { + if (getOSTypeForTest() === OSType.Windows) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + + const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'virtualenvwrapper1', '.virtualenvs', 'myenv', 'bin', 'python'); + + getEnvVarStub.withArgs('WORKON_HOME').returns(undefined); + + const envType = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, EnvironmentType.VirtualEnvWrapper); + + return undefined; + }); + + test('WORKON_HOME is set to its default value %USERPROFILE%\\Envs on Windows', async function () { + if (getOSTypeForTest() !== OSType.Windows) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + + const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'virtualenvwrapper1', 'Envs', 'myenv', 'Scripts', 'python'); + + getEnvVarStub.withArgs('WORKON_HOME').returns(undefined); + getOsTypeStub.returns(platformApis.OSType.Windows); + + const envType = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, EnvironmentType.VirtualEnvWrapper); + + return undefined; + }); + + test('WORKON_HOME is set to a custom value', async () => { + const workonHomeDir = path.join(TEST_LAYOUT_ROOT, 'virtualenvwrapper2'); + const interpreterPath = path.join(workonHomeDir, 'myenv', 'bin', 'python'); + + getEnvVarStub.withArgs('WORKON_HOME').returns(workonHomeDir); + + const envType = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, EnvironmentType.VirtualEnvWrapper); + }); + }); + suite('Virtualenv', () => { const activateFiles = [ { folder: 'virtualenv1', file: 'activate' }, diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/.virtualenvs/myenv/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/.virtualenvs/myenv/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/Envs/myenv/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/Envs/myenv/Scripts/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper2/myenv/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper2/myenv/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/virtualenvwrapperUtils.unit.test.ts b/src/test/pythonEnvironments/common/virtualenvwrapperUtils.unit.test.ts new file mode 100644 index 000000000000..77daf8aeeb7e --- /dev/null +++ b/src/test/pythonEnvironments/common/virtualenvwrapperUtils.unit.test.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as platformUtils from '../../../client/common/utils/platform'; +import { getDefaultVirtualenvwrapperDir } from '../../../client/pythonEnvironments/common/virtualenvwrapperUtils'; + +suite('Virtualenvwrapper Utils tests', () => { + const homeDir = path.join('path', 'to', 'home'); + + let getOsTypeStub: sinon.SinonStub; + let getHomeDirStub: sinon.SinonStub; + + setup(() => { + getOsTypeStub = sinon.stub(platformUtils, 'getOSType'); + getHomeDirStub = sinon.stub(platformUtils, 'getUserHomeDir'); + + getHomeDirStub.returns(homeDir); + }); + + teardown(() => { + getOsTypeStub.restore(); + getHomeDirStub.restore(); + }); + + test('Default virtualenvwrapper directory on non-Windows should be ~/.virtualenvs', () => { + getOsTypeStub.returns(platformUtils.OSType.Linux); + + const directory = getDefaultVirtualenvwrapperDir(); + + assert.deepStrictEqual(directory, path.join(homeDir, '.virtualenvs')); + }); + + test('Default virtualenvwrapper directory on Windows should be %USERPROFILE%\\Envs', () => { + getOsTypeStub.returns(platformUtils.OSType.Windows); + + const directory = getDefaultVirtualenvwrapperDir(); + + assert.deepStrictEqual(directory, path.join(homeDir, 'Envs')); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/virtualenvwrapperLocator.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/virtualenvwrapperLocator.unit.test.ts new file mode 100644 index 000000000000..05d73758e2a4 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/virtualenvwrapperLocator.unit.test.ts @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as platformUtils from '../../../../client/common/utils/platform'; +import * as fileUtils from '../../../../client/pythonEnvironments/common/externalDependencies'; +import * as virtualenvwrapperUtils from '../../../../client/pythonEnvironments/common/virtualenvwrapperUtils'; +import { isVirtualenvwrapperEnvironment } from '../../../../client/pythonEnvironments/discovery/locators/services/virtualenvwrapperLocator'; + +suite('Virtualenvwrapper Locator Tests', () => { + const envDirectory = 'myenv'; + const homeDir = path.join('path', 'to', 'home'); + + let getEnvVariableStub: sinon.SinonStub; + let pathExistsStub:sinon.SinonStub; + let getDefaultDirStub:sinon.SinonStub; + + setup(() => { + getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); + pathExistsStub = sinon.stub(fileUtils, 'pathExists'); + getDefaultDirStub = sinon.stub(virtualenvwrapperUtils, 'getDefaultVirtualenvwrapperDir'); + + pathExistsStub.withArgs(path.join(homeDir, envDirectory)).resolves(true); + pathExistsStub.resolves(false); + }); + + teardown(() => { + getEnvVariableStub.restore(); + pathExistsStub.restore(); + getDefaultDirStub.restore(); + }); + + test('WORKON_HOME is not set, and the interpreter is is in a subfolder', async () => { + const interpreter = path.join(homeDir, envDirectory, 'bin', 'python'); + + getEnvVariableStub.withArgs('WORKON_HOME').returns(undefined); + getDefaultDirStub.returns(homeDir); + + assert.ok(await isVirtualenvwrapperEnvironment(interpreter)); + }); + + test('WORKON_HOME is set to a custom value, and the interpreter is is in a subfolder', async () => { + const workonHomeDirectory = path.join('path', 'to', 'workonHome'); + const interpreter = path.join(workonHomeDirectory, envDirectory, 'bin', 'python'); + + getEnvVariableStub.withArgs('WORKON_HOME').returns(workonHomeDirectory); + pathExistsStub.withArgs(path.join(workonHomeDirectory, envDirectory)).resolves(true); + + assert.ok(await isVirtualenvwrapperEnvironment(interpreter)); + }); + + test('The interpreter is not in a subfolder of WORKON_HOME', async () => { + const workonHomeDirectory = path.join('path', 'to', 'workonHome'); + const interpreter = path.join('some', 'path', envDirectory, 'bin', 'python'); + + getEnvVariableStub.withArgs('WORKON_HOME').returns(workonHomeDirectory); + pathExistsStub.withArgs(path.join(workonHomeDirectory, envDirectory)).resolves(false); + + assert.deepStrictEqual(await isVirtualenvwrapperEnvironment(interpreter), false); + }); +});