diff --git a/package.json b/package.json index 399be812f06e..1dd275cf44ca 100644 --- a/package.json +++ b/package.json @@ -376,6 +376,12 @@ ], "configuration": { "properties": { + "python.activeStateToolPath": { + "default": "state", + "description": "%python.activeStateToolPath.description%", + "scope": "machine-overridable", + "type": "string" + }, "python.autoComplete.extraPaths": { "default": [], "description": "%python.autoComplete.extraPaths.description%", diff --git a/package.nls.json b/package.nls.json index 459346744336..02eaad9f6a07 100644 --- a/package.nls.json +++ b/package.nls.json @@ -25,6 +25,7 @@ "python.command.python.launchTensorBoard.title": "Launch TensorBoard", "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", "python.menu.createNewFile.title": "Python File", + "python.activeStateToolPath.description": "Path to the State Tool executable for ActiveState runtimes (version 0.36+).", "python.autoComplete.extraPaths.description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", "python.condaPath.description": "Path to the conda executable to use for activation (version 4.4+).", "python.defaultInterpreterPath.description": "Path to default Python to use when extension loads up for the first time, no longer used once an interpreter is selected for the workspace. See [here](https://aka.ms/AAfekmf) to understand when this is used", diff --git a/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json index c8d5d743275e..778434c5cf0d 100644 --- a/resources/report_issue_user_settings.json +++ b/resources/report_issue_user_settings.json @@ -7,6 +7,7 @@ "envFile": "placeholder", "venvPath": "placeholder", "venvFolders": "placeholder", + "activeStateToolPath": "placeholder", "condaPath": "placeholder", "pipenvPath": "placeholder", "poetryPath": "placeholder", diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 6c31de4361c8..3e3525d5b2a4 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -98,6 +98,8 @@ export class PythonSettings implements IPythonSettings { public venvFolders: string[] = []; + public activeStateToolPath = ''; + public condaPath = ''; public pipenvPath = ''; @@ -254,6 +256,11 @@ export class PythonSettings implements IPythonSettings { this.venvPath = systemVariables.resolveAny(pythonSettings.get('venvPath'))!; this.venvFolders = systemVariables.resolveAny(pythonSettings.get('venvFolders'))!; + const activeStateToolPath = systemVariables.resolveAny(pythonSettings.get('activeStateToolPath'))!; + this.activeStateToolPath = + activeStateToolPath && activeStateToolPath.length > 0 + ? getAbsolutePath(activeStateToolPath, workspaceRoot) + : activeStateToolPath; const condaPath = systemVariables.resolveAny(pythonSettings.get('condaPath'))!; this.condaPath = condaPath && condaPath.length > 0 ? getAbsolutePath(condaPath, workspaceRoot) : condaPath; const pipenvPath = systemVariables.resolveAny(pythonSettings.get('pipenvPath'))!; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 886d34a1914b..8f340c3e01e2 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -184,6 +184,7 @@ export interface IPythonSettings { readonly pythonPath: string; readonly venvPath: string; readonly venvFolders: string[]; + readonly activeStateToolPath: string; readonly condaPath: string; readonly pipenvPath: string; readonly poetryPath: string; diff --git a/src/client/interpreter/configuration/environmentTypeComparer.ts b/src/client/interpreter/configuration/environmentTypeComparer.ts index 992e910d8fc5..0631bb594bfd 100644 --- a/src/client/interpreter/configuration/environmentTypeComparer.ts +++ b/src/client/interpreter/configuration/environmentTypeComparer.ts @@ -4,6 +4,7 @@ import { injectable, inject } from 'inversify'; import { Resource } from '../../common/types'; import { Architecture } from '../../common/utils/platform'; +import { isActiveStateEnvironmentForWorkspace } from '../../pythonEnvironments/common/environmentManagers/activestate'; import { isParentPath } from '../../pythonEnvironments/common/externalDependencies'; import { EnvironmentType, PythonEnvironment, virtualEnvTypes } from '../../pythonEnvironments/info'; import { PythonVersion } from '../../pythonEnvironments/info/pythonVersion'; @@ -93,6 +94,14 @@ export class EnvironmentTypeComparer implements IInterpreterComparer { if (isProblematicCondaEnvironment(i)) { return false; } + if ( + i.envType === EnvironmentType.ActiveState && + (!i.path || + !workspaceUri || + !isActiveStateEnvironmentForWorkspace(i.path, workspaceUri.folderUri.fsPath)) + ) { + return false; + } if (getEnvLocationHeuristic(i, workspaceUri?.folderUri.fsPath || '') === EnvLocationHeuristic.Local) { return true; } @@ -237,6 +246,7 @@ function getPrioritizedEnvironmentType(): EnvironmentType[] { EnvironmentType.VirtualEnvWrapper, EnvironmentType.Venv, EnvironmentType.VirtualEnv, + EnvironmentType.ActiveState, EnvironmentType.Conda, EnvironmentType.Pyenv, EnvironmentType.MicrosoftStore, diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 717e943bae84..4dd1a93b95ee 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -66,6 +66,7 @@ export namespace EnvGroups { export const Venv = 'Venv'; export const Poetry = 'Poetry'; export const VirtualEnvWrapper = 'VirtualEnvWrapper'; + export const ActiveState = 'ActiveState'; export const Recommended = Common.recommended; } diff --git a/src/client/pythonEnvironments/base/info/envKind.ts b/src/client/pythonEnvironments/base/info/envKind.ts index 09490725e960..8828003c5ce7 100644 --- a/src/client/pythonEnvironments/base/info/envKind.ts +++ b/src/client/pythonEnvironments/base/info/envKind.ts @@ -22,6 +22,7 @@ export function getKindDisplayName(kind: PythonEnvKind): string { [PythonEnvKind.VirtualEnvWrapper, 'virtualenv'], [PythonEnvKind.Pipenv, 'pipenv'], [PythonEnvKind.Conda, 'conda'], + [PythonEnvKind.ActiveState, 'ActiveState'], // For now we treat OtherVirtual like Unknown. ] as [PythonEnvKind, string][]) { if (kind === candidate) { @@ -63,6 +64,7 @@ export function getPrioritizedEnvKinds(): PythonEnvKind[] { PythonEnvKind.Venv, PythonEnvKind.VirtualEnvWrapper, PythonEnvKind.VirtualEnv, + PythonEnvKind.ActiveState, PythonEnvKind.OtherVirtual, PythonEnvKind.OtherGlobal, PythonEnvKind.System, diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts index 4ef512c56ed6..e55031fe8078 100644 --- a/src/client/pythonEnvironments/base/info/index.ts +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -15,6 +15,7 @@ export enum PythonEnvKind { MicrosoftStore = 'global-microsoft-store', Pyenv = 'global-pyenv', Poetry = 'poetry', + ActiveState = 'activestate', Custom = 'global-custom', OtherGlobal = 'global-other', // "virtual" diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index 4bfcfac7fc87..0cca49e2b4c5 100644 --- a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -30,6 +30,7 @@ import { parseVersionFromExecutable } from '../../info/executable'; import { traceError, traceWarn } from '../../../../logging'; import { isVirtualEnvironment } from '../../../common/environmentManagers/simplevirtualenvs'; import { getWorkspaceFolderPaths } from '../../../../common/vscodeApis/workspaceApis'; +import { ActiveState } from '../../../common/environmentManagers/activestate'; function getResolvers(): Map Promise> { const resolvers = new Map Promise>(); @@ -42,6 +43,7 @@ function getResolvers(): Map Promise { return envInfo; } +async function resolveActiveStateEnv(env: BasicEnvInfo): Promise { + const info = buildEnvInfo({ + kind: env.kind, + executable: env.executablePath, + }); + const projects = await ActiveState.getState().then((v) => v?.getProjects()); + if (projects) { + for (const project of projects) { + for (const dir of project.executables) { + if (arePathsSame(dir, path.dirname(env.executablePath))) { + info.name = `${project.organization}/${project.name}`; + return info; + } + } + } + } + return info; +} + async function isBaseCondaPyenvEnvironment(executablePath: string) { if (!(await isCondaEnvironment(executablePath))) { return false; diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts new file mode 100644 index 000000000000..315f8e283d85 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ActiveState } from '../../../common/environmentManagers/activestate'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { traceError, traceVerbose } from '../../../../logging'; +import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; +import { findInterpretersInDir } from '../../../common/commonUtils'; + +export class ActiveStateLocator extends LazyResourceBasedLocator { + public readonly providerId: string = 'activestate'; + + // eslint-disable-next-line class-methods-use-this + public async *doIterEnvs(): IPythonEnvsIterator { + const state = await ActiveState.getState(); + if (state === undefined) { + traceVerbose(`Couldn't locate the state binary.`); + return; + } + const projects = await state.getProjects(); + if (projects === undefined) { + traceVerbose(`Couldn't fetch State Tool projects.`); + return; + } + for (const project of projects) { + if (project.executables) { + for (const dir of project.executables) { + try { + traceVerbose(`Looking for Python in: ${project.name}`); + for await (const exe of findInterpretersInDir(dir)) { + traceVerbose(`Found Python executable: ${exe.filename}`); + yield { kind: PythonEnvKind.ActiveState, executablePath: exe.filename }; + } + } catch (ex) { + traceError(`Failed to process State Tool project: ${JSON.stringify(project)}`, ex); + } + } + } + } + } +} diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts index 957321ed8e61..2dbc8b2b93d9 100644 --- a/src/client/pythonEnvironments/common/environmentIdentifier.ts +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -15,6 +15,7 @@ import { isVirtualenvwrapperEnvironment as isVirtualEnvWrapperEnvironment, } from './environmentManagers/simplevirtualenvs'; import { isMicrosoftStoreEnvironment } from './environmentManagers/microsoftStoreEnv'; +import { isActiveStateEnvironment } from './environmentManagers/activestate'; function getIdentifiers(): Map Promise> { const notImplemented = () => Promise.resolve(false); @@ -32,6 +33,7 @@ function getIdentifiers(): Map Promise identifier.set(PythonEnvKind.Venv, isVenvEnvironment); identifier.set(PythonEnvKind.VirtualEnvWrapper, isVirtualEnvWrapperEnvironment); identifier.set(PythonEnvKind.VirtualEnv, isVirtualEnvEnvironment); + identifier.set(PythonEnvKind.ActiveState, isActiveStateEnvironment); identifier.set(PythonEnvKind.Unknown, defaultTrue); identifier.set(PythonEnvKind.OtherGlobal, isGloballyInstalledEnv); return identifier; diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts new file mode 100644 index 000000000000..75b34f41176c --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { dirname } from 'path'; +import { + arePathsSame, + getPythonSetting, + onDidChangePythonSetting, + pathExists, + shellExecute, +} from '../externalDependencies'; +import { cache } from '../../../common/utils/decorators'; +import { traceError, traceVerbose } from '../../../logging'; +import { getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; + +export const ACTIVESTATETOOLPATH_SETTING_KEY = 'activeStateToolPath'; + +const STATE_GENERAL_TIMEOUT = 5000; + +export type ProjectInfo = { + name: string; + organization: string; + local_checkouts: string[]; // eslint-disable-line camelcase + executables: string[]; +}; + +export async function isActiveStateEnvironment(interpreterPath: string): Promise { + const execDir = path.dirname(interpreterPath); + const runtimeDir = path.dirname(execDir); + return pathExists(path.join(runtimeDir, '_runtime_store')); +} + +export class ActiveState { + private static statePromise: Promise | undefined; + + public static async getState(): Promise { + if (ActiveState.statePromise === undefined) { + ActiveState.statePromise = ActiveState.locate(); + } + return ActiveState.statePromise; + } + + constructor() { + onDidChangePythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY, () => { + ActiveState.statePromise = undefined; + }); + } + + public static getStateToolDir(): string | undefined { + const home = getUserHomeDir(); + if (!home) { + return undefined; + } + return getOSType() === OSType.Windows + ? path.join(home, 'AppData', 'Local', 'ActiveState', 'StateTool') + : path.join(home, '.local', 'ActiveState', 'StateTool'); + } + + private static async locate(): Promise { + const stateToolDir = this.getStateToolDir(); + const stateCommand = + getPythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand; + if (stateToolDir && ((await pathExists(stateToolDir)) || stateCommand !== this.defaultStateCommand)) { + return new ActiveState(); + } + return undefined; + } + + public async getProjects(): Promise { + return this.getProjectsCached(); + } + + private static readonly defaultStateCommand: string = 'state'; + + @cache(30_000, true, 10_000) + // eslint-disable-next-line class-methods-use-this + private async getProjectsCached(): Promise { + try { + const stateCommand = + getPythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand; + const result = await shellExecute(`${stateCommand} projects -o editor`, { + timeout: STATE_GENERAL_TIMEOUT, + }); + if (!result) { + return undefined; + } + let output = result.stdout.trimEnd(); + if (output[output.length - 1] === '\0') { + // '\0' is a record separator. + output = output.substring(0, output.length - 1); + } + traceVerbose(`${stateCommand} projects -o editor: ${output}`); + const projects = JSON.parse(output); + ActiveState.setCachedProjectInfo(projects); + return projects; + } catch (ex) { + traceError(ex); + return undefined; + } + } + + // Stored copy of known projects. isActiveStateEnvironmentForWorkspace() is + // not async, so getProjects() cannot be used. ActiveStateLocator sets this + // when it resolves project info. + private static cachedProjectInfo: ProjectInfo[] = []; + + public static getCachedProjectInfo(): ProjectInfo[] { + return this.cachedProjectInfo; + } + + private static setCachedProjectInfo(projects: ProjectInfo[]): void { + this.cachedProjectInfo = projects; + } +} + +export function isActiveStateEnvironmentForWorkspace(interpreterPath: string, workspacePath: string): boolean { + const interpreterDir = dirname(interpreterPath); + for (const project of ActiveState.getCachedProjectInfo()) { + if (project.executables) { + for (const [i, dir] of project.executables.entries()) { + // Note multiple checkouts for the same interpreter may exist. + // Check them all. + if (arePathsSame(dir, interpreterDir) && arePathsSame(workspacePath, project.local_checkouts[i])) { + return true; + } + } + } + } + return false; +} diff --git a/src/client/pythonEnvironments/index.ts b/src/client/pythonEnvironments/index.ts index 3f7bac7d670e..8d6a8cb7d4ba 100644 --- a/src/client/pythonEnvironments/index.ts +++ b/src/client/pythonEnvironments/index.ts @@ -36,6 +36,7 @@ import { import { EnvsCollectionService } from './base/locators/composite/envsCollectionService'; import { IDisposable } from '../common/types'; import { traceError } from '../logging'; +import { ActiveStateLocator } from './base/locators/lowLevel/activestateLocator'; /** * Set up the Python environments component (during extension activation).' @@ -137,6 +138,7 @@ function createNonWorkspaceLocators(ext: ExtensionState): ILocator // OS-independent locators go here. new PyenvLocator(), new CondaEnvironmentLocator(), + new ActiveStateLocator(), new GlobalVirtualEnvironmentLocator(), new CustomVirtualEnvironmentLocator(), ); diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index d0f41c45a5b1..70abbb0fad76 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -19,6 +19,7 @@ export enum EnvironmentType { MicrosoftStore = 'MicrosoftStore', Poetry = 'Poetry', VirtualEnvWrapper = 'VirtualEnvWrapper', + ActiveState = 'ActiveState', Global = 'Global', System = 'System', } @@ -114,6 +115,9 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string case EnvironmentType.VirtualEnvWrapper: { return 'virtualenvwrapper'; } + case EnvironmentType.ActiveState: { + return 'activestate'; + } default: { return ''; } diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index a3b8c3f0aaf7..ce9bfb4caf11 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -35,6 +35,7 @@ const convertedKinds = new Map( [PythonEnvKind.Poetry]: EnvironmentType.Poetry, [PythonEnvKind.Venv]: EnvironmentType.Venv, [PythonEnvKind.VirtualEnvWrapper]: EnvironmentType.VirtualEnvWrapper, + [PythonEnvKind.ActiveState]: EnvironmentType.ActiveState, }), ); diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index 7d2d6230f05b..eeaed6aa996b 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -81,6 +81,7 @@ suite('Python Settings', async () => { for (const name of [ 'pythonPath', 'venvPath', + 'activeStateToolPath', 'condaPath', 'pipenvPath', 'envFile', @@ -139,11 +140,17 @@ suite('Python Settings', async () => { } suite('String settings', async () => { - ['venvPath', 'condaPath', 'pipenvPath', 'envFile', 'poetryPath', 'defaultInterpreterPath'].forEach( - async (settingName) => { - testIfValueIsUpdated(settingName, 'stringValue'); - }, - ); + [ + 'venvPath', + 'activeStateToolPath', + 'condaPath', + 'pipenvPath', + 'envFile', + 'poetryPath', + 'defaultInterpreterPath', + ].forEach(async (settingName) => { + testIfValueIsUpdated(settingName, 'stringValue'); + }); }); suite('Boolean settings', async () => { diff --git a/src/test/pythonEnvironments/base/info/envKind.unit.test.ts b/src/test/pythonEnvironments/base/info/envKind.unit.test.ts index c12777d3e653..fdf174b4c551 100644 --- a/src/test/pythonEnvironments/base/info/envKind.unit.test.ts +++ b/src/test/pythonEnvironments/base/info/envKind.unit.test.ts @@ -20,6 +20,7 @@ const KIND_NAMES: [PythonEnvKind, string][] = [ [PythonEnvKind.VirtualEnvWrapper, 'virtualenvWrapper'], [PythonEnvKind.Pipenv, 'pipenv'], [PythonEnvKind.Conda, 'conda'], + [PythonEnvKind.ActiveState, 'activestate'], [PythonEnvKind.OtherVirtual, 'otherVirtual'], ]; diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts new file mode 100644 index 000000000000..5bdbd22def0f --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as fsapi from 'fs-extra'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { ActiveStateLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/activestateLocator'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { ExecutionResult } from '../../../../../client/common/process/types'; +import { createBasicEnv } from '../../common'; +import * as platform from '../../../../../client/common/utils/platform'; +import { ActiveState } from '../../../../../client/pythonEnvironments/common/environmentManagers/activestate'; +import { replaceAll } from '../../../../../client/common/stringUtils'; + +suite('ActiveState Locator', () => { + const testActiveStateDir = path.join(TEST_LAYOUT_ROOT, 'activestate'); + let locator: ActiveStateLocator; + + setup(() => { + locator = new ActiveStateLocator(); + + let homeDir: string; + switch (platform.getOSType()) { + case platform.OSType.Windows: + homeDir = 'C:\\Users\\user'; + break; + case platform.OSType.OSX: + homeDir = '/Users/user'; + break; + default: + homeDir = '/home/user'; + } + sinon.stub(platform, 'getUserHomeDir').returns(homeDir); + + const stateToolDir = ActiveState.getStateToolDir(); + if (stateToolDir) { + sinon.stub(fsapi, 'pathExists').callsFake((dir: string) => dir === stateToolDir); + } + + sinon.stub(externalDependencies, 'getPythonSetting').returns(undefined); + + sinon.stub(externalDependencies, 'shellExecute').callsFake((command: string) => { + if (command === 'state projects -o editor') { + return Promise.resolve>({ + stdout: `[{"name":"test","organization":"test-org","local_checkouts":["does-not-matter"],"executables":["${replaceAll( + path.join(testActiveStateDir, 'c09080d1', 'exec'), + '\\', + '\\\\', + )}"]},{"name":"test2","organization":"test-org","local_checkouts":["does-not-matter2"],"executables":["${replaceAll( + path.join(testActiveStateDir, '2af6390a', 'exec'), + '\\', + '\\\\', + )}"]}]\n\0`, + }); + } + return Promise.reject(new Error('Command failed')); + }); + }); + + teardown(() => sinon.restore()); + + test('iterEnvs()', async () => { + const actualEnvs = await getEnvs(locator.iterEnvs()); + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.ActiveState, + path.join( + testActiveStateDir, + 'c09080d1', + 'exec', + platform.getOSType() === platform.OSType.Windows ? 'python3.exe' : 'python3', + ), + ), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts new file mode 100644 index 000000000000..23eebc5fee07 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import { getOSType, OSType } from '../../../../client/common/utils/platform'; +import { isActiveStateEnvironment } from '../../../../client/pythonEnvironments/common/environmentManagers/activestate'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +suite('isActiveStateEnvironment Tests', () => { + const testActiveStateDir = path.join(TEST_LAYOUT_ROOT, 'activestate'); + + test('Return true if runtime is set up', async () => { + const result = await isActiveStateEnvironment( + path.join( + testActiveStateDir, + 'c09080d1', + 'exec', + getOSType() === OSType.Windows ? 'python3.exe' : 'python3', + ), + ); + expect(result).to.equal(true); + }); + + test(`Return false if the runtime is not set up`, async () => { + const result = await isActiveStateEnvironment( + path.join( + testActiveStateDir, + 'b6a0705d', + 'exec', + getOSType() === OSType.Windows ? 'python3.exe' : 'python3', + ), + ); + expect(result).to.equal(false); + }); +}); diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/_runtime_store/completed b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/_runtime_store/completed new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3 b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3.exe b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 new file mode 100644 index 000000000000..0800f9b4dfd2 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 @@ -0,0 +1 @@ +invalid python interpreter: missing _runtime_store diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe new file mode 100644 index 000000000000..0800f9b4dfd2 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe @@ -0,0 +1 @@ +invalid python interpreter: missing _runtime_store diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/_runtime_store/completed b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/_runtime_store/completed new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3 b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3.exe b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3.exe new file mode 100644 index 000000000000..e69de29bb2d1