diff --git a/src/managers/conda/condaEnvManager.ts b/src/managers/conda/condaEnvManager.ts index 1654bc89..6eaeac91 100644 --- a/src/managers/conda/condaEnvManager.ts +++ b/src/managers/conda/condaEnvManager.ts @@ -95,7 +95,8 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { title: CondaStrings.condaDiscovering, }, async () => { - this.collection = (await refreshCondaEnvs(false, this.nativeFinder, this.api, this.log, this)) ?? []; + this.collection = + (await refreshCondaEnvs(false, this.nativeFinder, this.api, this.log, this)) ?? []; await this.loadEnvMap(); this._onDidChangeEnvironments.fire( @@ -273,7 +274,8 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { resolve: (p) => resolveCondaPath(p, this.nativeFinder, this.api, this.log, this), startBackgroundInit: () => withProgress({ location: ProgressLocation.Window, title: CondaStrings.condaDiscovering }, async () => { - this.collection = (await refreshCondaEnvs(false, this.nativeFinder, this.api, this.log, this)) ?? []; + this.collection = + (await refreshCondaEnvs(false, this.nativeFinder, this.api, this.log, this)) ?? []; await this.loadEnvMap(); this._onDidChangeEnvironments.fire( this.collection.map((e) => ({ diff --git a/src/test/managers/conda/condaEnvManager.findEnvironmentByPath.unit.test.ts b/src/test/managers/conda/condaEnvManager.findEnvironmentByPath.unit.test.ts index 364e7d03..9e215035 100644 --- a/src/test/managers/conda/condaEnvManager.findEnvironmentByPath.unit.test.ts +++ b/src/test/managers/conda/condaEnvManager.findEnvironmentByPath.unit.test.ts @@ -1,33 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import assert from 'assert'; import * as sinon from 'sinon'; -import { Uri } from 'vscode'; import { PythonEnvironment, PythonEnvironmentApi } from '../../../api'; import { isWindows } from '../../../common/utils/platformUtils'; -import { PythonEnvironmentImpl } from '../../../internal.api'; import { CondaEnvManager } from '../../../managers/conda/condaEnvManager'; import { NativePythonFinder } from '../.././../managers/common/nativePythonFinder'; - -/** - * Helper to create a minimal PythonEnvironment stub with required fields. - * Only `name`, `environmentPath`, and `version` matter for findEnvironmentByPath. - */ -function makeEnv(name: string, envPath: string, version: string = '3.12.0'): PythonEnvironment { - return new PythonEnvironmentImpl( - { id: `${name}-test`, managerId: 'ms-python.python:conda' }, - { - name, - displayName: `${name} (${version})`, - displayPath: envPath, - version, - environmentPath: Uri.file(envPath), - sysPrefix: envPath, - execInfo: { - run: { executable: 'python' }, - }, - }, - ); -} +import { makeMockPythonEnvironment as makeEnv } from '../../mocks/pythonEnvironment'; /** * Creates a CondaEnvManager with a given collection, bypassing initialization. diff --git a/src/test/managers/conda/condaEnvManager.setEvents.unit.test.ts b/src/test/managers/conda/condaEnvManager.setEvents.unit.test.ts index ddc2e37a..a2ef150c 100644 --- a/src/test/managers/conda/condaEnvManager.setEvents.unit.test.ts +++ b/src/test/managers/conda/condaEnvManager.setEvents.unit.test.ts @@ -2,29 +2,12 @@ import assert from 'assert'; import * as sinon from 'sinon'; import { Uri } from 'vscode'; -import { DidChangeEnvironmentEventArgs, PythonEnvironment, PythonEnvironmentApi, PythonProject } from '../../../api'; +import { DidChangeEnvironmentEventArgs, PythonEnvironmentApi, PythonProject } from '../../../api'; import { normalizePath } from '../../../common/utils/pathUtils'; -import { PythonEnvironmentImpl } from '../../../internal.api'; import { CondaEnvManager } from '../../../managers/conda/condaEnvManager'; import * as condaUtils from '../../../managers/conda/condaUtils'; import { NativePythonFinder } from '../../../managers/common/nativePythonFinder'; - -function makeEnv(name: string, envPath: string, version: string = '3.12.0'): PythonEnvironment { - return new PythonEnvironmentImpl( - { id: `${name}-test`, managerId: 'ms-python.python:conda' }, - { - name, - displayName: `${name} (${version})`, - displayPath: envPath, - version, - environmentPath: Uri.file(envPath), - sysPrefix: envPath, - execInfo: { - run: { executable: 'python' }, - }, - }, - ); -} +import { makeMockPythonEnvironment as makeEnv } from '../../mocks/pythonEnvironment'; function createManager(apiOverrides?: Partial): CondaEnvManager { const api = { diff --git a/src/test/managers/conda/condaEnvManager.setGlobal.unit.test.ts b/src/test/managers/conda/condaEnvManager.setGlobal.unit.test.ts new file mode 100644 index 00000000..5c6dd881 --- /dev/null +++ b/src/test/managers/conda/condaEnvManager.setGlobal.unit.test.ts @@ -0,0 +1,92 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import assert from 'assert'; +import * as sinon from 'sinon'; +import { PythonEnvironmentApi } from '../../../api'; +import { CondaEnvManager } from '../../../managers/conda/condaEnvManager'; +import * as condaUtils from '../../../managers/conda/condaUtils'; +import { NativePythonFinder } from '../../../managers/common/nativePythonFinder'; +import { makeMockPythonEnvironment as makeEnv } from '../../mocks/pythonEnvironment'; + +function createManager(): CondaEnvManager { + const manager = new CondaEnvManager( + {} as NativePythonFinder, + {} as PythonEnvironmentApi, + { info: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } as any, + ); + // Bypass initialization + (manager as any)._initialized = { completed: true, promise: Promise.resolve() }; + (manager as any).collection = []; + return manager; +} + +suite('CondaEnvManager.set - globalEnv update', () => { + let setCondaForGlobalStub: sinon.SinonStub; + let checkNoPythonStub: sinon.SinonStub; + + setup(() => { + setCondaForGlobalStub = sinon.stub(condaUtils, 'setCondaForGlobal').resolves(); + checkNoPythonStub = sinon.stub(condaUtils, 'checkForNoPythonCondaEnvironment'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('set(undefined, env) updates globalEnv in memory', async () => { + const manager = createManager(); + const oldEnv = makeEnv('base', '/miniconda3', '3.11.0'); + const newEnv = makeEnv('myenv', '/miniconda3/envs/myenv', '3.12.0'); + (manager as any).globalEnv = oldEnv; + + // checkForNoPythonCondaEnvironment returns the env as-is (has Python) + checkNoPythonStub.resolves(newEnv); + + await manager.set(undefined, newEnv); + + // globalEnv should now be updated in memory + const result = await manager.get(undefined); + assert.strictEqual(result, newEnv, 'get(undefined) should return the newly set environment'); + assert.notStrictEqual(result, oldEnv, 'get(undefined) should NOT return the old environment'); + }); + + test('set(undefined, env) persists to disk', async () => { + const manager = createManager(); + const newEnv = makeEnv('myenv', '/miniconda3/envs/myenv', '3.12.0'); + checkNoPythonStub.resolves(newEnv); + + await manager.set(undefined, newEnv); + + assert.ok(setCondaForGlobalStub.calledOnce, 'setCondaForGlobal should be called'); + assert.strictEqual( + setCondaForGlobalStub.firstCall.args[0], + newEnv.environmentPath.fsPath, + 'should persist the correct path', + ); + }); + + test('set(undefined, undefined) clears globalEnv', async () => { + const manager = createManager(); + const oldEnv = makeEnv('base', '/miniconda3', '3.11.0'); + (manager as any).globalEnv = oldEnv; + + await manager.set(undefined, undefined); + + const result = await manager.get(undefined); + assert.strictEqual(result, undefined, 'get(undefined) should return undefined after clearing'); + }); + + test('set(undefined, noPythonEnv) where user declines install clears globalEnv', async () => { + const manager = createManager(); + const oldEnv = makeEnv('base', '/miniconda3', '3.11.0'); + const noPythonEnv = makeEnv('nopy', '/miniconda3/envs/nopy', 'no-python'); + (manager as any).globalEnv = oldEnv; + + // User declined to install Python + checkNoPythonStub.resolves(undefined); + + await manager.set(undefined, noPythonEnv); + + const result = await manager.get(undefined); + assert.strictEqual(result, undefined, 'globalEnv should be cleared when checkedEnv is undefined'); + }); +}); diff --git a/src/test/mocks/pythonEnvironment.ts b/src/test/mocks/pythonEnvironment.ts new file mode 100644 index 00000000..2a14dfbc --- /dev/null +++ b/src/test/mocks/pythonEnvironment.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { PythonEnvironment } from '../../api'; +import { PythonEnvironmentImpl } from '../../internal.api'; + +/** + * Options for {@link createMockPythonEnvironment}. + */ +export interface MockPythonEnvironmentOptions { + /** Environment name, e.g. `myenv`. Defaults to `test-env`. */ + name?: string; + /** Filesystem path for `environmentPath`, `displayPath`, and `sysPrefix`. */ + envPath: string; + /** Version string. Defaults to `3.12.0`. */ + version?: string; + /** Manager id. Defaults to `ms-python.python:conda`. */ + managerId?: string; + /** Environment id. Defaults to `-test`. */ + id?: string; + /** Optional description. */ + description?: string; + /** Optional display name. Defaults to ` ()`. */ + displayName?: string; + /** If true, includes an `activation` entry in `execInfo`. */ + hasActivation?: boolean; +} + +/** + * Create a minimal {@link PythonEnvironment} for use in unit tests. + * + * Shared across manager and view tests so they agree on the shape of a mock + * environment. Only fields that tests commonly need are populated; extend this + * helper if additional fields become required. + */ +export function createMockPythonEnvironment(options: MockPythonEnvironmentOptions): PythonEnvironment { + const { + name = 'test-env', + envPath, + version = '3.12.0', + managerId = 'ms-python.python:conda', + id = `${name}-test`, + description, + displayName = `${name} (${version})`, + hasActivation = false, + } = options; + + return new PythonEnvironmentImpl( + { id, managerId }, + { + name, + displayName, + displayPath: envPath, + version, + description, + environmentPath: Uri.file(envPath), + sysPrefix: envPath, + execInfo: { + run: { executable: 'python' }, + ...(hasActivation && { + activation: [{ executable: envPath.replace('python', 'activate') }], + }), + }, + }, + ); +} + +/** + * Positional shorthand for {@link createMockPythonEnvironment} used by conda + * manager unit tests. Prefer {@link createMockPythonEnvironment} for new tests + * that need to customize additional fields. + */ +export function makeMockPythonEnvironment( + name: string, + envPath: string, + version: string = '3.12.0', +): PythonEnvironment { + return createMockPythonEnvironment({ name, envPath, version }); +}