diff --git a/src/client/pythonEnvironments/common/commonUtils.ts b/src/client/pythonEnvironments/common/commonUtils.ts new file mode 100644 index 000000000000..b679410bab73 --- /dev/null +++ b/src/client/pythonEnvironments/common/commonUtils.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fsapi from 'fs-extra'; +import * as path from 'path'; +import { chain, iterable } from '../../common/utils/async'; +import { getOSType, OSType } from '../../common/utils/platform'; +import { isPosixPythonBin } from './posixUtils'; +import { isWindowsPythonExe } from './windowsUtils'; + +export async function* findInterpretersInDir(root:string, recurseLevels?:number): AsyncIterableIterator { + const dirContents = (await fsapi.readdir(root)).map((c) => path.join(root, c)); + const os = getOSType(); + const checkBin = os === OSType.Windows ? isWindowsPythonExe : isPosixPythonBin; + const generators = dirContents.map((item) => { + async function* generator() { + const stat = await fsapi.lstat(item); + + if (stat.isDirectory()) { + if (recurseLevels && recurseLevels > 0) { + const subItems = findInterpretersInDir(item, recurseLevels - 1); + + for await (const subItem of subItems) { + yield subItem; + } + } + } else if (checkBin(item)) { + yield item; + } + } + + return generator(); + }); + + yield* iterable(chain(generators)); +} diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts index 64d62721986f..430fc4e7033d 100644 --- a/src/client/pythonEnvironments/common/environmentIdentifier.ts +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -4,9 +4,7 @@ import { isCondaEnvironment } from '../discovery/locators/services/condaLocator'; import { isPipenvEnvironment } from '../discovery/locators/services/pipEnvHelper'; import { isPyenvEnvironment } from '../discovery/locators/services/pyenvLocator'; -import { isVenvEnvironment } from '../discovery/locators/services/venvLocator'; -import { isVirtualenvEnvironment } from '../discovery/locators/services/virtualenvLocator'; -import { isVirtualenvwrapperEnvironment } from '../discovery/locators/services/virtualenvwrapperLocator'; +import { isVenvEnvironment, isVirtualenvEnvironment, isVirtualenvwrapperEnvironment } from '../discovery/locators/services/virtualEnvironmentIdentifier'; import { isWindowsStoreEnvironment } from '../discovery/locators/services/windowsStoreLocator'; import { EnvironmentType } from '../info'; diff --git a/src/client/pythonEnvironments/common/virtualenvwrapperUtils.ts b/src/client/pythonEnvironments/common/virtualenvwrapperUtils.ts deleted file mode 100644 index 1849934ff59e..000000000000 --- a/src/client/pythonEnvironments/common/virtualenvwrapperUtils.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import * as path from 'path'; -import { getOSType, getUserHomeDir, OSType } from '../../common/utils/platform'; - -export function getDefaultVirtualenvwrapperDir(): string { - const homeDir = getUserHomeDir() || ''; - - // In Windows, the default path for WORKON_HOME is %USERPROFILE%\Envs. - if (getOSType() === OSType.Windows) { - return path.join(homeDir, 'Envs'); - } - return path.join(homeDir, '.virtualenvs'); -} diff --git a/src/client/pythonEnvironments/discovery/locators/services/globalVirtualEnvronmentLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/globalVirtualEnvronmentLocator.ts new file mode 100644 index 000000000000..30fbc2dd5b2d --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/globalVirtualEnvronmentLocator.ts @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fsapi from 'fs-extra'; +import { toUpper, uniq } from 'lodash'; +import * as path from 'path'; +import { traceVerbose } from '../../../../common/logger'; +import { chain, iterable } from '../../../../common/utils/async'; +import { + getEnvironmentVariable, getOSType, getUserHomeDir, OSType, +} from '../../../../common/utils/platform'; +import { + PythonEnvInfo, PythonEnvKind, UNKNOWN_PYTHON_VERSION, +} from '../../../base/info'; +import { buildEnvInfo } from '../../../base/info/env'; +import { ILocator, IPythonEnvsIterator } from '../../../base/locator'; +import { PythonEnvsWatcher } from '../../../base/watcher'; +import { findInterpretersInDir } from '../../../common/commonUtils'; +import { getFileInfo, pathExists } from '../../../common/externalDependencies'; +import { isVenvEnvironment, isVirtualenvEnvironment, isVirtualenvwrapperEnvironment } from './virtualEnvironmentIdentifier'; + +const DEFAULT_SEARCH_DEPTH = 2; + +/** + * Gets all default virtual environment locations. This uses WORKON_HOME, + * and user home directory to find some known locations where global virtual + * environments are often created. + */ +async function getGlobalVirtualEnvDirs(): Promise { + const venvDirs:string[] = []; + + const workOnHome = getEnvironmentVariable('WORKON_HOME'); + if (workOnHome && await pathExists(workOnHome)) { + venvDirs.push(workOnHome); + } + + const homeDir = getUserHomeDir(); + if (homeDir && await pathExists(homeDir)) { + const os = getOSType(); + let subDirs = ['Envs', 'envs', '.direnv', '.venvs', '.virtualenvs']; + if (os === OSType.Windows) { + subDirs = uniq(subDirs.map(toUpper)); + } + + (await fsapi.readdir(homeDir)) + .filter((d) => subDirs.includes(os === OSType.Windows ? d.toUpperCase() : d)) + .forEach((d) => venvDirs.push(path.join(homeDir, d))); + } + + return venvDirs; +} + +/** + * Gets the virtual environment kind for a given interpreter path. + * This only checks for environments created using venv, virtualenv, + * and virtualenvwrapper based environments. + * @param interpreterPath: Absolute path to the interpreter paths. + */ +async function getVirtualEnvKind(interpreterPath:string): Promise { + if (await isVenvEnvironment(interpreterPath)) { + return PythonEnvKind.Venv; + } + + if (await isVirtualenvwrapperEnvironment(interpreterPath)) { + return PythonEnvKind.VirtualEnvWrapper; + } + + if (await isVirtualenvEnvironment(interpreterPath)) { + return PythonEnvKind.VirtualEnv; + } + + return PythonEnvKind.Unknown; +} + +/** + * Finds and resolves virtual environments created in known global locations. + */ +export class GlobalVirtualEnvironmentLocator extends PythonEnvsWatcher implements ILocator { + private virtualEnvKinds = [ + PythonEnvKind.Venv, + PythonEnvKind.VirtualEnv, + PythonEnvKind.VirtualEnvWrapper, + ]; + + public constructor(private readonly searchDepth?:number) { + super(); + } + + public iterEnvs(): IPythonEnvsIterator { + // Number of levels of sub-directories to recurse when looking for + // interpreters + const searchDepth = this.searchDepth ?? DEFAULT_SEARCH_DEPTH; + + async function* iterator(virtualEnvKinds:PythonEnvKind[]) { + const envRootDirs = await getGlobalVirtualEnvDirs(); + const envGenerators = envRootDirs.map((envRootDir) => { + async function* generator() { + traceVerbose(`Searching for global virtual envs in: ${envRootDir}`); + + const envGenerator = findInterpretersInDir(envRootDir, searchDepth); + + for await (const env of envGenerator) { + // We only care about python.exe (on windows) and python (on linux/mac) + // Other version like python3.exe or python3.8 are often symlinks to + // python.exe or python in the same directory in the case of virtual + // environments. + const name = path.basename(env).toLowerCase(); + if (name === 'python.exe' || name === 'python') { + // We should extract the kind here to avoid doing is*Environment() + // check multiple times. Those checks are file system heavy and + // we can use the kind to determine this anyway. + const kind = await getVirtualEnvKind(env); + + const timeData = await getFileInfo(env); + if (virtualEnvKinds.includes(kind)) { + traceVerbose(`Global Virtual Environment: [added] ${env}`); + const envInfo = buildEnvInfo({ + kind, + executable: env, + version: UNKNOWN_PYTHON_VERSION, + }); + envInfo.executable.ctime = timeData.ctime; + envInfo.executable.mtime = timeData.mtime; + yield envInfo; + } else { + traceVerbose(`Global Virtual Environment: [skipped] ${env}`); + } + } else { + traceVerbose(`Global Virtual Environment: [skipped] ${env}`); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + } + + return iterator(this.virtualEnvKinds); + } + + public async resolveEnv(env: string | PythonEnvInfo): Promise { + const executablePath = typeof env === 'string' ? env : env.executable.filename; + if (await pathExists(executablePath)) { + // We should extract the kind here to avoid doing is*Environment() + // check multiple times. Those checks are file system heavy and + // we can use the kind to determine this anyway. + const kind = await getVirtualEnvKind(executablePath); + if (this.virtualEnvKinds.includes(kind)) { + const timeData = await getFileInfo(executablePath); + const envInfo = buildEnvInfo({ + kind, + version: UNKNOWN_PYTHON_VERSION, + executable: executablePath, + }); + envInfo.executable.ctime = timeData.ctime; + envInfo.executable.mtime = timeData.mtime; + return envInfo; + } + } + return undefined; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/venvLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/venvLocator.ts deleted file mode 100644 index ea3e6a859ba0..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/venvLocator.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as path from 'path'; -import { pathExists } from '../../../common/externalDependencies'; - - -/** - * Checks if the given interpreter belongs to a venv based environment. - * @param {string} interpreterPath: Absolute path to the python interpreter. - * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. - */ -export async function isVenvEnvironment(interpreterPath:string): Promise{ - const pyvenvConfigFile = 'pyvenv.cfg'; - - // Check if the pyvenv.cfg file is in the directory as the interpreter. - // env - // |__ pyvenv.cfg <--- check if this file exists - // |__ python <--- interpreterPath - const venvPath1 = path.join(path.dirname(interpreterPath), pyvenvConfigFile); - - // Check if the pyvenv.cfg file is in the parent directory relative to the interpreter. - // env - // |__ pyvenv.cfg <--- check if this file exists - // |__ bin or Scripts - // |__ python <--- interpreterPath - const venvPath2 = path.join(path.dirname(path.dirname(interpreterPath)), pyvenvConfigFile); - - return [await pathExists(venvPath1), await pathExists(venvPath2)].includes(true); -} diff --git a/src/client/pythonEnvironments/discovery/locators/services/virtualEnvironmentIdentifier.ts b/src/client/pythonEnvironments/discovery/locators/services/virtualEnvironmentIdentifier.ts new file mode 100644 index 000000000000..a3f9628c32a8 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/virtualEnvironmentIdentifier.ts @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fsapi from 'fs-extra'; +import * as path from 'path'; +import { + getEnvironmentVariable, getOSType, getUserHomeDir, OSType, +} from '../../../../common/utils/platform'; +import { pathExists } from '../../../common/externalDependencies'; + +/** + * Checks if the given interpreter belongs to a venv based environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. + */ +export async function isVenvEnvironment(interpreterPath:string): Promise { + const pyvenvConfigFile = 'pyvenv.cfg'; + + // Check if the pyvenv.cfg file is in the parent directory relative to the interpreter. + // env + // |__ pyvenv.cfg <--- check if this file exists + // |__ bin or Scripts + // |__ python <--- interpreterPath + const venvPath1 = path.join(path.dirname(path.dirname(interpreterPath)), pyvenvConfigFile); + + // Check if the pyvenv.cfg file is in the directory as the interpreter. + // env + // |__ pyvenv.cfg <--- check if this file exists + // |__ python <--- interpreterPath + const venvPath2 = path.join(path.dirname(interpreterPath), pyvenvConfigFile); + + // The paths are ordered in the most common to least common + const venvPaths = [venvPath1, venvPath2]; + + // We don't need to test all at once, testing each one here + for (const venvPath of venvPaths) { + if (await pathExists(venvPath)) { + return true; + } + } + return false; +} + +/** + * Checks if the given interpreter belongs to a virtualenv based environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a virtualenv environment. + */ +export async function isVirtualenvEnvironment(interpreterPath:string): Promise { + // Check if there are any activate.* files in the same directory as the interpreter. + // + // env + // |__ activate, activate.* <--- check if any of these files exist + // |__ python <--- interpreterPath + const directory = path.dirname(interpreterPath); + const files = await fsapi.readdir(directory); + const regex = /^activate(\.([A-z]|\d)+)?$/i; + + return files.find((file) => regex.test(file)) !== undefined; +} + +async function getDefaultVirtualenvwrapperDir(): Promise { + const homeDir = getUserHomeDir() || ''; + + // In Windows, the default path for WORKON_HOME is %USERPROFILE%\Envs. + // If 'Envs' is not available we should default to '.virtualenvs'. Since that + // is also valid for windows. + if (getOSType() === OSType.Windows) { + // ~/Envs with uppercase 'E' is the default home dir for + // virtualEnvWrapper. + const envs = path.join(homeDir, 'Envs'); + if (await pathExists(envs)) { + return envs; + } + } + return path.join(homeDir, '.virtualenvs'); +} + +function getWorkOnHome(): 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 workOnHome = getEnvironmentVariable('WORKON_HOME'); + if (workOnHome) { + return Promise.resolve(workOnHome); + } + return getDefaultVirtualenvwrapperDir(); +} + +/** + * Checks if the given interpreter belongs to a virtualenvWrapper based environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean}: Returns true if the interpreter belongs to a virtualenvWrapper environment. + */ +export async function isVirtualenvwrapperEnvironment(interpreterPath:string): Promise { + const workOnHomeDir = await getWorkOnHome(); + let pathToCheck = interpreterPath; + let workOnRoot = workOnHomeDir; + + if (getOSType() === OSType.Windows) { + workOnRoot = workOnHomeDir.toUpperCase(); + pathToCheck = interpreterPath.toUpperCase(); + } + + // For environment to be a virtualenvwrapper based it has to follow these two rules: + // 1. It should be in a sub-directory under the WORKON_HOME + // 2. It should be a valid virtualenv environment + return await pathExists(workOnHomeDir) + && pathToCheck.startsWith(`${workOnRoot}${path.sep}`) + && isVirtualenvEnvironment(interpreterPath); +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/virtualenvLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/virtualenvLocator.ts deleted file mode 100644 index 056fd3a97dab..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/virtualenvLocator.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as fsapi from 'fs-extra'; -import * as path from 'path'; - -/** - * Checks if the given interpreter belongs to a virtualenv based environment. - * @param {string} interpreterPath: Absolute path to the python interpreter. - * @returns {boolean} : Returns true if the interpreter belongs to a virtualenv environment. - */ -export async function isVirtualenvEnvironment(interpreterPath:string): Promise { - // Check if there are any activate.* files in the same directory as the interpreter. - // - // env - // |__ activate, activate.* <--- check if any of these files exist - // |__ python <--- interpreterPath - const directory = path.dirname(interpreterPath); - const files = await fsapi.readdir(directory); - const regex = /^activate(\.([A-z]|\d)+)?$/; - - return files.find((file) => regex.test(file)) !== undefined; -} diff --git a/src/client/pythonEnvironments/discovery/locators/services/virtualenvwrapperLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/virtualenvwrapperLocator.ts deleted file mode 100644 index 8e89c7ad7e46..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/virtualenvwrapperLocator.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as path from 'path'; -import { - getEnvironmentVariable, getOSType, OSType, -} from '../../../../common/utils/platform'; -import { pathExists } from '../../../common/externalDependencies'; -import { getDefaultVirtualenvwrapperDir } from '../../../common/virtualenvwrapperUtils'; - -/** - * Checks if the given interpreter belongs to a virtualenvWrapper based environment. - * @param {string} interpreterPath: Absolute path to the python interpreter. - * @returns {boolean}: Returns true if the interpreter belongs to a virtualenvWrapper environment. - */ -export async function isVirtualenvwrapperEnvironment(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); - let pathToCheck = interpreterPath; - - if (getOSType() === OSType.Windows) { - environmentDir = environmentDir.toUpperCase(); - pathToCheck = interpreterPath.toUpperCase(); - } - - return await pathExists(environmentDir) && pathToCheck.startsWith(`${environmentDir}${path.sep}`); -} diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/.virtualenvs/myenv/bin/activate b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/.virtualenvs/myenv/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/Envs/myenv/Scripts/activate b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/Envs/myenv/Scripts/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper2/myenv/bin/activate b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper2/myenv/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/nonvenv/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/nonvenv/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/nonvenv/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/nonvenv/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix1/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix1/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix1/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix2/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix2/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix2/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix2/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win1/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win2/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win2/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win2/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win2/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/nonvenv/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/nonvenv/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/nonvenv/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/nonvenv/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/nonvenv/python3 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/nonvenv/python3 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/nonvenv/python3.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/nonvenv/python3.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix2/bin/activate.sh b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix2/bin/activate.sh new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix2/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix2/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win2/bin/activate.ps1 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win2/bin/activate.ps1 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win2/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win2/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win2/bin/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win2/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win2/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win2/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/nonvenv/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/nonvenv/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/nonvenv/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/nonvenv/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix2/bin/activate.sh b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix2/bin/activate.sh new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix2/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix2/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win2/bin/activate.ps1 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win2/bin/activate.ps1 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win2/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win2/bin/python.exe 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 deleted file mode 100644 index 77daf8aeeb7e..000000000000 --- a/src/test/pythonEnvironments/common/virtualenvwrapperUtils.unit.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -// 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/envTestUtils.ts b/src/test/pythonEnvironments/discovery/locators/envTestUtils.ts index 4ac34ad2edeb..b484ad957329 100644 --- a/src/test/pythonEnvironments/discovery/locators/envTestUtils.ts +++ b/src/test/pythonEnvironments/discovery/locators/envTestUtils.ts @@ -26,6 +26,7 @@ export function assertEnvsEqual( actualEnvs:(PythonEnvInfo | undefined)[], expectedEnvs: (PythonEnvInfo | undefined)[], ):void{ + assert.deepStrictEqual(actualEnvs.length, expectedEnvs.length, 'Number of envs'); zip(actualEnvs, expectedEnvs).forEach((value) => { const [actual, expected] = value; assertEnvEqual(actual, expected); diff --git a/src/test/pythonEnvironments/discovery/locators/globalVirtualEnvironmentLocator.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/globalVirtualEnvironmentLocator.unit.test.ts new file mode 100644 index 000000000000..a950b43a89d2 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/globalVirtualEnvironmentLocator.unit.test.ts @@ -0,0 +1,244 @@ +// 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 { PythonEnvInfo, PythonEnvKind, UNKNOWN_PYTHON_VERSION } from '../../../../client/pythonEnvironments/base/info'; +import { getEnvs } from '../../../../client/pythonEnvironments/base/locatorUtils'; +import { GlobalVirtualEnvironmentLocator } from '../../../../client/pythonEnvironments/discovery/locators/services/globalVirtualEnvronmentLocator'; +import { TEST_LAYOUT_ROOT } from '../../common/commonTestConstants'; +import { assertEnvEqual, assertEnvsEqual } from './envTestUtils'; + +suite('GlobalVirtualEnvironment Locator', () => { + const testVirtualHomeDir = path.join(TEST_LAYOUT_ROOT, 'virtualhome'); + const testWorkOnHomePath = path.join(testVirtualHomeDir, 'workonhome'); + let getEnvVariableStub: sinon.SinonStub; + let getUserHomeDirStub: sinon.SinonStub; + let getOSTypeStub: sinon.SinonStub; + + function createExpectedEnvInfo(interpreterPath:string, kind:PythonEnvKind): PythonEnvInfo { + return { + name: '', + location: '', + kind, + executable: { + filename: interpreterPath, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + defaultDisplayName: undefined, + version: UNKNOWN_PYTHON_VERSION, + arch: platformUtils.Architecture.Unknown, + distro: { org: '' }, + searchLocation: undefined, + }; + } + + function comparePaths(actual:PythonEnvInfo[], expected:PythonEnvInfo[]) { + const actualPaths = actual.map((a) => a.executable.filename); + const expectedPaths = expected.map((a) => a.executable.filename); + assert.deepStrictEqual(actualPaths, expectedPaths); + } + + setup(() => { + getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); + getEnvVariableStub.withArgs('WORKON_HOME').returns(testWorkOnHomePath); + + getUserHomeDirStub = sinon.stub(platformUtils, 'getUserHomeDir'); + getUserHomeDirStub.returns(testVirtualHomeDir); + + getOSTypeStub = sinon.stub(platformUtils, 'getOSType'); + getOSTypeStub.returns(platformUtils.OSType.Linux); + }); + teardown(() => { + getEnvVariableStub.restore(); + getUserHomeDirStub.restore(); + getOSTypeStub.restore(); + }); + + test('iterEnvs(): Windows', async () => { + getOSTypeStub.returns(platformUtils.OSType.Windows); + const expectedEnvs = [ + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), PythonEnvKind.Venv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.venvs', 'win2', 'bin', 'python.exe'), PythonEnvKind.Venv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.virtualenvs', 'win1', 'python.exe'), PythonEnvKind.VirtualEnv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.virtualenvs', 'win2', 'bin', 'python.exe'), PythonEnvKind.VirtualEnv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, 'Envs', 'wrapper_win1', 'python.exe'), PythonEnvKind.VirtualEnv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, 'Envs', 'wrapper_win2', 'bin', 'python.exe'), PythonEnvKind.VirtualEnv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, 'workonhome', 'win1', 'python.exe'), PythonEnvKind.VirtualEnvWrapper), + createExpectedEnvInfo(path.join(testVirtualHomeDir, 'workonhome', 'win2', 'bin', 'python.exe'), PythonEnvKind.VirtualEnvWrapper), + ].sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + const locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = (await getEnvs(iterator)) + .sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + comparePaths(actualEnvs, expectedEnvs); + assertEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Windows (WORKON_HOME NOT set)', async () => { + getOSTypeStub.returns(platformUtils.OSType.Windows); + getEnvVariableStub.withArgs('WORKON_HOME').returns(undefined); + const expectedEnvs = [ + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), PythonEnvKind.Venv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.venvs', 'win2', 'bin', 'python.exe'), PythonEnvKind.Venv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.virtualenvs', 'win1', 'python.exe'), PythonEnvKind.VirtualEnv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.virtualenvs', 'win2', 'bin', 'python.exe'), PythonEnvKind.VirtualEnv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, 'Envs', 'wrapper_win1', 'python.exe'), PythonEnvKind.VirtualEnvWrapper), + createExpectedEnvInfo(path.join(testVirtualHomeDir, 'Envs', 'wrapper_win2', 'bin', 'python.exe'), PythonEnvKind.VirtualEnvWrapper), + ].sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + const locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = (await getEnvs(iterator)) + .sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + comparePaths(actualEnvs, expectedEnvs); + assertEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Non-Windows', async () => { + const expectedEnvs = [ + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.venvs', 'posix1', 'python'), PythonEnvKind.Venv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.venvs', 'posix2', 'bin', 'python'), PythonEnvKind.Venv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.virtualenvs', 'posix1', 'python'), PythonEnvKind.VirtualEnv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.virtualenvs', 'posix2', 'bin', 'python'), PythonEnvKind.VirtualEnv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, 'workonhome', 'posix1', 'python'), PythonEnvKind.VirtualEnvWrapper), + createExpectedEnvInfo(path.join(testVirtualHomeDir, 'workonhome', 'posix2', 'bin', 'python'), PythonEnvKind.VirtualEnvWrapper), + ].sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + const locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = (await getEnvs(iterator)) + .sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + comparePaths(actualEnvs, expectedEnvs); + assertEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): with depth set', async () => { + const expectedEnvs = [ + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.venvs', 'posix1', 'python'), PythonEnvKind.Venv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.virtualenvs', 'posix1', 'python'), PythonEnvKind.VirtualEnv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, 'workonhome', 'posix1', 'python'), PythonEnvKind.VirtualEnvWrapper), + ].sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + const locator = new GlobalVirtualEnvironmentLocator(1); + const iterator = locator.iterEnvs(); + const actualEnvs = (await getEnvs(iterator)) + .sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + comparePaths(actualEnvs, expectedEnvs); + assertEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Non-Windows (WORKON_HOME not set)', async () => { + getEnvVariableStub.withArgs('WORKON_HOME').returns(undefined); + const expectedEnvs = [ + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.venvs', 'posix1', 'python'), PythonEnvKind.Venv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.venvs', 'posix2', 'bin', 'python'), PythonEnvKind.Venv), + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.virtualenvs', 'posix1', 'python'), PythonEnvKind.VirtualEnvWrapper), + createExpectedEnvInfo(path.join(testVirtualHomeDir, '.virtualenvs', 'posix2', 'bin', 'python'), PythonEnvKind.VirtualEnvWrapper), + ].sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + const locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = (await getEnvs(iterator)) + .sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + comparePaths(actualEnvs, expectedEnvs); + assertEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): No User home dir set', async () => { + getUserHomeDirStub.returns(undefined); + const expectedEnvs = [ + createExpectedEnvInfo(path.join(testVirtualHomeDir, 'workonhome', 'posix1', 'python'), PythonEnvKind.VirtualEnvWrapper), + createExpectedEnvInfo(path.join(testVirtualHomeDir, 'workonhome', 'posix2', 'bin', 'python'), PythonEnvKind.VirtualEnvWrapper), + ].sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + const locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = (await getEnvs(iterator)) + .sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + comparePaths(actualEnvs, expectedEnvs); + assertEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): No default virtual environment dirs ', async () => { + // We can simulate that by pointing the user home dir to some random directory + getUserHomeDirStub.returns(path.join('some', 'random', 'directory')); + const expectedEnvs = [ + createExpectedEnvInfo(path.join(testVirtualHomeDir, 'workonhome', 'posix1', 'python'), PythonEnvKind.VirtualEnvWrapper), + createExpectedEnvInfo(path.join(testVirtualHomeDir, 'workonhome', 'posix2', 'bin', 'python'), PythonEnvKind.VirtualEnvWrapper), + ].sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + const locator = new GlobalVirtualEnvironmentLocator(2); + const iterator = locator.iterEnvs(); + const actualEnvs = (await getEnvs(iterator)) + .sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + comparePaths(actualEnvs, expectedEnvs); + assertEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('resolveEnv(string)', async () => { + const interpreterPath = path.join(testVirtualHomeDir, '.venvs', 'posix1', 'python'); + const expected = createExpectedEnvInfo(interpreterPath, PythonEnvKind.Venv); + + const locator = new GlobalVirtualEnvironmentLocator(); + const actual = await locator.resolveEnv(interpreterPath); + + assertEnvEqual(actual, expected); + }); + + test('resolveEnv(PythonEnvInfo)', async () => { + const interpreterPath = path.join(testVirtualHomeDir, 'workonhome', 'posix1', 'python'); + const expected = createExpectedEnvInfo(interpreterPath, PythonEnvKind.VirtualEnvWrapper); + + // Partially filled in env info object + const input:PythonEnvInfo = { + name: '', + location: '', + kind: PythonEnvKind.Unknown, + distro: { org: '' }, + arch: platformUtils.Architecture.Unknown, + executable: { + filename: interpreterPath, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + version: UNKNOWN_PYTHON_VERSION, + }; + + const locator = new GlobalVirtualEnvironmentLocator(); + const actual = await locator.resolveEnv(input); + + assertEnvEqual(actual, expected); + }); + + test('resolveEnv(string): not venv based environment', async () => { + const interpreterPath = path.join(testVirtualHomeDir, '.virtualenvs', 'nonvenv', 'python'); + + const locator = new GlobalVirtualEnvironmentLocator(); + const actual = await locator.resolveEnv(interpreterPath); + + assert.deepStrictEqual(actual, undefined); + }); + + test('resolveEnv(string): non existent path', async () => { + const interpreterPath = path.join('some', 'random', 'nonvenv', 'python'); + + const locator = new GlobalVirtualEnvironmentLocator(); + const actual = await locator.resolveEnv(interpreterPath); + + assert.deepStrictEqual(actual, undefined); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/venvLocator.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/venvLocator.unit.test.ts deleted file mode 100644 index c2f4b0278f8e..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/venvLocator.unit.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -// 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 { ImportMock } from 'ts-mock-imports'; -import * as fileapis from '../../../../client/pythonEnvironments/common/externalDependencies'; -import { isVenvEnvironment } from '../../../../client/pythonEnvironments/discovery/locators/services/venvLocator'; - -suite('Venv Locator Tests', () => { - suite('Venv identifier Tests', () => { - const pyvenvCfg = 'pyvenv.cfg'; - const envRoot = path.join('path', 'to', 'env'); - const configPath = path.join('env', pyvenvCfg); - let fileExistsStub:sinon.SinonStub; - - setup(() => { - fileExistsStub = ImportMock.mockFunction(fileapis, 'pathExists'); - }); - - teardown(() => { - fileExistsStub.restore(); - }); - - test('pyvenv.cfg does not exist', async () => { - const interpreter = path.join(envRoot, 'python'); - fileExistsStub.callsFake(() => Promise.resolve(false)); - assert.ok(!(await isVenvEnvironment(interpreter))); - }); - - test('pyvenv.cfg exists in the current folder', async () => { - const interpreter = path.join(envRoot, 'python'); - - fileExistsStub.callsFake((p:string) => { - if (p.endsWith(configPath)) { - return Promise.resolve(true); - } - return Promise.resolve(false); - }); - - assert.ok(await isVenvEnvironment(interpreter)); - }); - - test('pyvenv.cfg exists in the parent folder', async () => { - const interpreter = path.join(envRoot, 'bin', 'python'); - - fileExistsStub.callsFake((p:string) => { - if (p.endsWith(configPath)) { - return Promise.resolve(true); - } - return Promise.resolve(false); - }); - - assert.ok(await isVenvEnvironment(interpreter)); - }); - }); -}); diff --git a/src/test/pythonEnvironments/discovery/locators/virtualEnvironmentIdentifier.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/virtualEnvironmentIdentifier.unit.test.ts new file mode 100644 index 000000000000..e8c102730e00 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/virtualEnvironmentIdentifier.unit.test.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as fsapi from 'fs-extra'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { ImportMock } from 'ts-mock-imports'; +import * as platformUtils from '../../../../client/common/utils/platform'; +import * as fileUtils from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { isVenvEnvironment, isVirtualenvEnvironment, isVirtualenvwrapperEnvironment } from '../../../../client/pythonEnvironments/discovery/locators/services/virtualEnvironmentIdentifier'; +import { TEST_LAYOUT_ROOT } from '../../common/commonTestConstants'; + +suite('isVenvEnvironment Tests', () => { + const pyvenvCfg = 'pyvenv.cfg'; + const envRoot = path.join('path', 'to', 'env'); + const configPath = path.join('env', pyvenvCfg); + let fileExistsStub:sinon.SinonStub; + + setup(() => { + fileExistsStub = ImportMock.mockFunction(fileUtils, 'pathExists'); + }); + + teardown(() => { + fileExistsStub.restore(); + }); + + test('pyvenv.cfg does not exist', async () => { + const interpreter = path.join(envRoot, 'python'); + fileExistsStub.callsFake(() => Promise.resolve(false)); + assert.ok(!(await isVenvEnvironment(interpreter))); + }); + + test('pyvenv.cfg exists in the current folder', async () => { + const interpreter = path.join(envRoot, 'python'); + + fileExistsStub.callsFake((p:string) => { + if (p.endsWith(configPath)) { + return Promise.resolve(true); + } + return Promise.resolve(false); + }); + + assert.ok(await isVenvEnvironment(interpreter)); + }); + + test('pyvenv.cfg exists in the parent folder', async () => { + const interpreter = path.join(envRoot, 'bin', 'python'); + + fileExistsStub.callsFake((p:string) => { + if (p.endsWith(configPath)) { + return Promise.resolve(true); + } + return Promise.resolve(false); + }); + + assert.ok(await isVenvEnvironment(interpreter)); + }); +}); + +suite('isVirtualenvEnvironment Tests', () => { + const envRoot = path.join('path', 'to', 'env'); + const interpreter = path.join(envRoot, 'python'); + let readDirStub: sinon.SinonStub; + + setup(() => { + readDirStub = sinon.stub(fsapi, 'readdir'); + }); + + teardown(() => { + readDirStub.restore(); + }); + + test('Interpreter folder contains an activate file', async () => { + readDirStub.resolves(['activate', 'python']); + + assert.ok(await isVirtualenvEnvironment(interpreter)); + }); + + test('Interpreter folder does not contain any activate.* files', async () => { + readDirStub.resolves(['mymodule', 'python']); + + assert.strictEqual(await isVirtualenvEnvironment(interpreter), false); + }); +}); + +suite('isVirtualenvwrapperEnvironment Tests', () => { + const homeDir = path.join(TEST_LAYOUT_ROOT, 'virutalhome'); + + let getEnvVariableStub: sinon.SinonStub; + let getUserHomeDirStub: sinon.SinonStub; + let pathExistsStub:sinon.SinonStub; + let readDirStub: sinon.SinonStub; + + setup(() => { + getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); + getUserHomeDirStub = sinon.stub(platformUtils, 'getUserHomeDir'); + + readDirStub = sinon.stub(fsapi, 'readdir'); + readDirStub.resolves(['activate', 'python']); + + pathExistsStub = sinon.stub(fileUtils, 'pathExists'); + pathExistsStub.resolves(true); + // This is windows specific path. For test purposes we will use the common path + // that works on all OS. So, fail the path check for windows specific default route. + pathExistsStub.withArgs(path.join(homeDir, 'Envs')).resolves(false); + }); + + teardown(() => { + getEnvVariableStub.restore(); + getUserHomeDirStub.restore(); + pathExistsStub.restore(); + readDirStub.restore(); + }); + + test('WORKON_HOME is not set, and the interpreter is in a sub-folder of virtualenvwrapper', async () => { + const interpreter = path.join(homeDir, '.virtualenvs', 'win2', 'bin', 'python.exe'); + + getEnvVariableStub.withArgs('WORKON_HOME').returns(undefined); + getUserHomeDirStub.returns(homeDir); + + assert.ok(await isVirtualenvwrapperEnvironment(interpreter)); + }); + + test('WORKON_HOME is set to a custom value, and the interpreter is is in a sub-folder', async () => { + const workonHomeDirectory = path.join(homeDir, 'workonhome'); + const interpreter = path.join(workonHomeDirectory, 'win2', 'bin', 'python.exe'); + + getEnvVariableStub.withArgs('WORKON_HOME').returns(workonHomeDirectory); + pathExistsStub.withArgs(path.join(workonHomeDirectory)).resolves(true); + + assert.ok(await isVirtualenvwrapperEnvironment(interpreter)); + }); + + test('The interpreter is not in a sub-folder of WORKON_HOME', async () => { + const workonHomeDirectory = path.join('path', 'to', 'workonhome'); + const interpreter = path.join('some', 'path', 'env', 'bin', 'python'); + + getEnvVariableStub.withArgs('WORKON_HOME').returns(workonHomeDirectory); + + assert.deepStrictEqual(await isVirtualenvwrapperEnvironment(interpreter), false); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/virtualenvLocator.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/virtualenvLocator.unit.test.ts deleted file mode 100644 index a16a5c2139c9..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/virtualenvLocator.unit.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as assert from 'assert'; -import * as fsapi from 'fs-extra'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import { isVirtualenvEnvironment } from '../../../../client/pythonEnvironments/discovery/locators/services/virtualenvLocator'; - -suite('Virtualenv Locator Tests', () => { - const envRoot = path.join('path', 'to', 'env'); - const interpreter = path.join(envRoot, 'python'); - let readDirStub: sinon.SinonStub; - - setup(() => { - readDirStub = sinon.stub(fsapi, 'readdir'); - }); - - teardown(() => { - readDirStub.restore(); - }); - - test('Interpreter folder contains an activate file', async () => { - readDirStub.resolves(['activate', 'python']); - - assert.ok(await isVirtualenvEnvironment(interpreter)); - }); - - test('Interpreter folder does not contain any activate.* files', async () => { - readDirStub.resolves(['mymodule', 'python']); - - assert.strictEqual(await isVirtualenvEnvironment(interpreter), false); - }); -}); diff --git a/src/test/pythonEnvironments/discovery/locators/virtualenvwrapperLocator.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/virtualenvwrapperLocator.unit.test.ts deleted file mode 100644 index 2221e109a1ae..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/virtualenvwrapperLocator.unit.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -// 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.resolves(true); - }); - - teardown(() => { - getEnvVariableStub.restore(); - pathExistsStub.restore(); - getDefaultDirStub.restore(); - }); - - test('WORKON_HOME is not set, and the interpreter is in a subfolder of virtualenvwrapper', 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); - - assert.deepStrictEqual(await isVirtualenvwrapperEnvironment(interpreter), false); - }); -});