diff --git a/src/client/pythonEnvironments/collection/environmentsResolver.ts b/src/client/pythonEnvironments/collection/environmentsResolver.ts index 25181bddda03..6dd71e9ac5e5 100644 --- a/src/client/pythonEnvironments/collection/environmentsResolver.ts +++ b/src/client/pythonEnvironments/collection/environmentsResolver.ts @@ -6,9 +6,7 @@ import { Event, EventEmitter } from 'vscode'; import { traceVerbose } from '../../common/logger'; import { areSameEnvironment, PythonEnvInfo } from '../base/info'; import { InterpreterInformation } from '../base/info/interpreter'; -import { - ILocator, IPythonEnvsIterator, PythonEnvUpdatedEvent, QueryForEvent, -} from '../base/locator'; +import { ILocator, IPythonEnvsIterator, PythonEnvUpdatedEvent, QueryForEvent } from '../base/locator'; import { PythonEnvsChangedEvent } from '../base/watcher'; import { IEnvironmentInfoService } from '../info/environmentInfoService'; @@ -19,11 +17,19 @@ export class PythonEnvsResolver implements ILocator { constructor( private readonly pythonEnvsReducer: ILocator, - private readonly environmentInfoService: IEnvironmentInfoService, + private readonly environmentInfoService: IEnvironmentInfoService ) {} - public resolveEnv(env: string | PythonEnvInfo): Promise { - return this.pythonEnvsReducer.resolveEnv(env); + public async resolveEnv(env: string | PythonEnvInfo): Promise { + const environment = await this.pythonEnvsReducer.resolveEnv(env); + if (!environment) { + return undefined; + } + const interpreterInfo = await this.environmentInfoService.getEnvironmentInfo(environment.executable.filename); + if (!interpreterInfo) { + return undefined; + } + return getResolvedEnv(interpreterInfo, environment); } public iterEnvs(query?: QueryForEvent): IPythonEnvsIterator { @@ -34,13 +40,13 @@ export class PythonEnvsResolver implements ILocator { return iterator; } - private async* iterEnvsIterator( + private async *iterEnvsIterator( iterator: IPythonEnvsIterator, - didUpdate: EventEmitter, + didUpdate: EventEmitter ): AsyncIterator { const state = { done: false, - pending: 0, + pending: 0 }; const seen: PythonEnvInfo[] = []; @@ -84,10 +90,10 @@ export class PythonEnvsResolver implements ILocator { envIndex: number, state: { done: boolean; pending: number }, didUpdate: EventEmitter, - seen: PythonEnvInfo[], + seen: PythonEnvInfo[] ) { const interpreterInfo = await this.environmentInfoService.getEnvironmentInfo( - seen[envIndex].executable.filename, + seen[envIndex].executable.filename ); if (interpreterInfo && seen[envIndex]) { const resolvedEnv = getResolvedEnv(interpreterInfo, seen[envIndex]); @@ -106,7 +112,7 @@ export class PythonEnvsResolver implements ILocator { */ function checkIfFinishedAndNotify( state: { done: boolean; pending: number }, - didUpdate: EventEmitter, + didUpdate: EventEmitter ) { if (state.done && state.pending === 0) { didUpdate.fire(null); diff --git a/src/test/pythonEnvironments/collection/environmentsResolver.unit.test.ts b/src/test/pythonEnvironments/collection/environmentsResolver.unit.test.ts index 5042f85ad7db..14bf747fb9af 100644 --- a/src/test/pythonEnvironments/collection/environmentsResolver.unit.test.ts +++ b/src/test/pythonEnvironments/collection/environmentsResolver.unit.test.ts @@ -14,20 +14,28 @@ import { PythonEnvUpdatedEvent } from '../../../client/pythonEnvironments/base/l import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher'; import { PythonEnvsResolver } from '../../../client/pythonEnvironments/collection/environmentsResolver'; import * as ExternalDep from '../../../client/pythonEnvironments/common/externalDependencies'; -import { - EnvironmentInfoService, - IEnvironmentInfoService, -} from '../../../client/pythonEnvironments/info/environmentInfoService'; +import { EnvironmentInfoService } from '../../../client/pythonEnvironments/info/environmentInfoService'; import { sleep } from '../../core'; import { createEnv, getEnvs, SimpleLocator } from '../base/common'; suite('Environments Resolver', () => { + /** + * Returns the expected environment to be returned by Environment info service + */ + function createExpectedEnvInfo(env: PythonEnvInfo): PythonEnvInfo { + const updatedEnv = cloneDeep(env); + updatedEnv.version = { + ...parseVersion('3.8.3-final'), + sysVersion: '3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]', + }; + updatedEnv.executable.filename = env.executable.filename; + updatedEnv.executable.sysPrefix = 'path'; + updatedEnv.arch = Architecture.x64; + return updatedEnv; + } suite('iterEnvs()', () => { let stubShellExec: sinon.SinonStub; - let envService: IEnvironmentInfoService; - setup(() => { - envService = new EnvironmentInfoService(); stubShellExec = ImportMock.mockFunction( ExternalDep, 'shellExecute', @@ -44,23 +52,14 @@ suite('Environments Resolver', () => { stubShellExec.restore(); }); - function createExpectedEnvInfo(env: PythonEnvInfo): PythonEnvInfo { - const updatedEnv = cloneDeep(env); - updatedEnv.version = { ...parseVersion('3.8.3-final'), sysVersion: '3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]' }; - updatedEnv.executable.filename = env.executable.filename; - updatedEnv.executable.sysPrefix = 'path'; - updatedEnv.arch = Architecture.x64; - return updatedEnv; - } - test('Iterator only yields as-is', async () => { const env1 = createEnv('env1', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); const env2 = createEnv('env2', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); const env3 = createEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); const env4 = createEnv('env4', '3.9.0rc2', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); const environmentsToBeIterated = [env1, env2, env3, env4]; - const pythonEnvManager = new SimpleLocator(environmentsToBeIterated); - const reducer = new PythonEnvsResolver(pythonEnvManager, envService); + const pythonEnvReducer = new SimpleLocator(environmentsToBeIterated); + const reducer = new PythonEnvsResolver(pythonEnvReducer, new EnvironmentInfoService()); const iterator = reducer.iterEnvs(); const envs = await getEnvs(iterator); @@ -73,9 +72,9 @@ suite('Environments Resolver', () => { const env1 = createEnv('env1', '3.5.12b1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec1')); const env2 = createEnv('env2', '3.8.1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); const environmentsToBeIterated = [env1, env2]; - const pythonEnvManager = new SimpleLocator(environmentsToBeIterated); + const pythonEnvReducer = new SimpleLocator(environmentsToBeIterated); const onUpdatedEvents: (PythonEnvUpdatedEvent | null)[] = []; - const reducer = new PythonEnvsResolver(pythonEnvManager, envService); + const reducer = new PythonEnvsResolver(pythonEnvReducer, new EnvironmentInfoService()); const iterator = reducer.iterEnvs(); // Act @@ -108,9 +107,9 @@ suite('Environments Resolver', () => { const updatedEnv = createEnv('env1', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); const environmentsToBeIterated = [env]; const didUpdate = new EventEmitter(); - const pythonEnvManager = new SimpleLocator(environmentsToBeIterated, { onUpdated: didUpdate.event }); + const pythonEnvReducer = new SimpleLocator(environmentsToBeIterated, { onUpdated: didUpdate.event }); const onUpdatedEvents: (PythonEnvUpdatedEvent | null)[] = []; - const reducer = new PythonEnvsResolver(pythonEnvManager, envService); + const reducer = new PythonEnvsResolver(pythonEnvReducer, new EnvironmentInfoService()); const iterator = reducer.iterEnvs(); // Act @@ -134,43 +133,112 @@ suite('Environments Resolver', () => { // Assert // The updates can be anything, even the number of updates, but they should lead to the same final state const { length } = onUpdatedEvents; - assert.deepEqual(onUpdatedEvents[length - 2]?.new, createExpectedEnvInfo(updatedEnv), 'The final update to environment is incorrect'); + assert.deepEqual( + onUpdatedEvents[length - 2]?.new, + createExpectedEnvInfo(updatedEnv), + 'The final update to environment is incorrect', + ); assert.equal(onUpdatedEvents[length - 1], null, 'Last update should be null'); didUpdate.dispose(); }); }); test('onChanged fires iff onChanged from reducer fires', () => { - const pythonEnvManager = new SimpleLocator([]); + const pythonEnvReducer = new SimpleLocator([]); const event1: PythonEnvsChangedEvent = {}; const event2: PythonEnvsChangedEvent = { kind: PythonEnvKind.Unknown }; const expected = [event1, event2]; - const reducer = new PythonEnvsResolver(pythonEnvManager, new EnvironmentInfoService()); + const reducer = new PythonEnvsResolver(pythonEnvReducer, new EnvironmentInfoService()); const events: PythonEnvsChangedEvent[] = []; reducer.onChanged((e) => events.push(e)); - pythonEnvManager.fire(event1); - pythonEnvManager.fire(event2); + pythonEnvReducer.fire(event1); + pythonEnvReducer.fire(event2); assert.deepEqual(events, expected); }); - test('Calls reducer to resolves environments', async () => { - const env = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const resolvedEnv = createEnv('env1', '3.8.1', PythonEnvKind.Conda, 'resolved/path/to/exec'); - const pythonEnvManager = new SimpleLocator([], { - resolve: async (e: PythonEnvInfo) => { - if (e === env) { - return resolvedEnv; - } - return undefined; - }, + suite('resolveEnv()', () => { + let stubShellExec: sinon.SinonStub; + setup(() => { + stubShellExec = ImportMock.mockFunction( + ExternalDep, + 'shellExecute', + new Promise>((resolve) => { + resolve({ + stdout: + '{"versionInfo": [3, 8, 3, "final", 0], "sysPrefix": "path", "sysVersion": "3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]", "is64Bit": true}', + }); + }), + ); + }); + + teardown(() => { + stubShellExec.restore(); + }); + + test('Calls into reducer to get resolved environment, then calls environnment service to resolve environment further and return it', async () => { + const env = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const resolvedEnvReturnedByReducer = createEnv( + 'env1', + '3.8.1', + PythonEnvKind.Conda, + 'resolved/path/to/exec', + ); + const pythonEnvReducer = new SimpleLocator([], { + resolve: async (e: PythonEnvInfo) => { + if (e === env) { + return resolvedEnvReturnedByReducer; + } + throw new Error('Incorrect environment sent to the reducer'); + }, + }); + const reducer = new PythonEnvsResolver(pythonEnvReducer, new EnvironmentInfoService()); + + const expected = await reducer.resolveEnv(env); + + assert.deepEqual(expected, createExpectedEnvInfo(resolvedEnvReturnedByReducer)); }); - const reducer = new PythonEnvsResolver(pythonEnvManager, new EnvironmentInfoService()); - const expected = await reducer.resolveEnv(env); + test('If the reducer resolves environment, but fetching interpreter info returns undefined, return undefined', async () => { + stubShellExec.returns( + new Promise>((_resolve, reject) => { + reject(); + }), + ); + const env = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const resolvedEnvReturnedByReducer = createEnv( + 'env1', + '3.8.1', + PythonEnvKind.Conda, + 'resolved/path/to/exec', + ); + const pythonEnvReducer = new SimpleLocator([], { + resolve: async (e: PythonEnvInfo) => { + if (e === env) { + return resolvedEnvReturnedByReducer; + } + throw new Error('Incorrect environment sent to the reducer'); + }, + }); + const reducer = new PythonEnvsResolver(pythonEnvReducer, new EnvironmentInfoService()); + + const expected = await reducer.resolveEnv(env); + + assert.deepEqual(expected, undefined); + }); - assert.deepEqual(expected, resolvedEnv); + test("If the reducer isn't able to resolve environment, return undefined", async () => { + const env = createEnv('env', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const pythonEnvReducer = new SimpleLocator([], { + resolve: async () => undefined, + }); + const reducer = new PythonEnvsResolver(pythonEnvReducer, new EnvironmentInfoService()); + + const expected = await reducer.resolveEnv(env); + + assert.deepEqual(expected, undefined); + }); }); });