Skip to content

Commit dab9ee5

Browse files
author
Kartik Raj
committed
Modify resolveEnv()
1 parent 65094df commit dab9ee5

File tree

3 files changed

+378
-3
lines changed

3 files changed

+378
-3
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { cloneDeep } from 'lodash';
5+
import { Event, EventEmitter } from 'vscode';
6+
import { traceVerbose } from '../../common/logger';
7+
import { areSameEnvironment, PythonEnvInfo } from '../base/info';
8+
import { InterpreterInformation } from '../base/info/interpreter';
9+
import {
10+
ILocator, IPythonEnvsIterator, PythonEnvUpdatedEvent, QueryForEvent,
11+
} from '../base/locator';
12+
import { PythonEnvsChangedEvent } from '../base/watcher';
13+
import { IEnvironmentInfoService } from '../info/environmentInfoService';
14+
15+
export class PythonEnvsResolver implements ILocator {
16+
public get onChanged(): Event<PythonEnvsChangedEvent> {
17+
return this.pythonEnvsReducer.onChanged;
18+
}
19+
20+
constructor(
21+
private readonly pythonEnvsReducer: ILocator,
22+
private readonly environmentInfoService: IEnvironmentInfoService,
23+
) {}
24+
25+
public async resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
26+
const environment = await this.pythonEnvsReducer.resolveEnv(env);
27+
if (!environment) {
28+
return undefined;
29+
}
30+
const interpreterInfo = await this.environmentInfoService.getEnvironmentInfo(environment.executable.filename);
31+
if (!interpreterInfo) {
32+
return undefined;
33+
}
34+
return getResolvedEnv(interpreterInfo, environment);
35+
}
36+
37+
public iterEnvs(query?: QueryForEvent<PythonEnvsChangedEvent>): IPythonEnvsIterator {
38+
const didUpdate = new EventEmitter<PythonEnvUpdatedEvent | null>();
39+
const incomingIterator = this.pythonEnvsReducer.iterEnvs(query);
40+
const iterator: IPythonEnvsIterator = this.iterEnvsIterator(incomingIterator, didUpdate);
41+
iterator.onUpdated = didUpdate.event;
42+
return iterator;
43+
}
44+
45+
private async* iterEnvsIterator(
46+
iterator: IPythonEnvsIterator,
47+
didUpdate: EventEmitter<PythonEnvUpdatedEvent | null>,
48+
): AsyncIterator<PythonEnvInfo, void> {
49+
const state = {
50+
done: false,
51+
pending: 0,
52+
};
53+
const seen: PythonEnvInfo[] = [];
54+
55+
if (iterator.onUpdated !== undefined) {
56+
iterator.onUpdated((event) => {
57+
if (event === null) {
58+
state.done = true;
59+
checkIfFinishedAndNotify(state, didUpdate);
60+
} else {
61+
const oldIndex = seen.findIndex((s) => areSameEnvironment(s, event.old));
62+
if (oldIndex !== -1) {
63+
seen[oldIndex] = event.new;
64+
state.pending += 1;
65+
this.resolveInBackground(oldIndex, state, didUpdate, seen).ignoreErrors();
66+
} else {
67+
// This implies a problem in a downstream locator
68+
traceVerbose(`Expected already iterated env in resolver, got ${event.old}`);
69+
}
70+
}
71+
});
72+
}
73+
74+
let result = await iterator.next();
75+
while (!result.done) {
76+
const currEnv = result.value;
77+
// Resolver only expects unique environments, so store & yield as-is.
78+
seen.push(currEnv);
79+
yield currEnv;
80+
state.pending += 1;
81+
this.resolveInBackground(seen.indexOf(currEnv), state, didUpdate, seen).ignoreErrors();
82+
// eslint-disable-next-line no-await-in-loop
83+
result = await iterator.next();
84+
}
85+
if (iterator.onUpdated === undefined) {
86+
state.done = true;
87+
checkIfFinishedAndNotify(state, didUpdate);
88+
}
89+
}
90+
91+
private async resolveInBackground(
92+
envIndex: number,
93+
state: { done: boolean; pending: number },
94+
didUpdate: EventEmitter<PythonEnvUpdatedEvent | null>,
95+
seen: PythonEnvInfo[],
96+
) {
97+
const interpreterInfo = await this.environmentInfoService.getEnvironmentInfo(
98+
seen[envIndex].executable.filename,
99+
);
100+
if (interpreterInfo) {
101+
const resolvedEnv = getResolvedEnv(interpreterInfo, seen[envIndex]);
102+
didUpdate.fire({ old: seen[envIndex], new: resolvedEnv });
103+
seen[envIndex] = resolvedEnv;
104+
}
105+
state.pending -= 1;
106+
checkIfFinishedAndNotify(state, didUpdate);
107+
}
108+
}
109+
110+
/**
111+
* When all info from incoming iterator has been received and all background calls finishes, notify that we're done
112+
* @param state Carries the current state of progress
113+
* @param didUpdate Used to notify when finished
114+
*/
115+
function checkIfFinishedAndNotify(
116+
state: { done: boolean; pending: number },
117+
didUpdate: EventEmitter<PythonEnvUpdatedEvent | null>,
118+
) {
119+
if (state.done && state.pending === 0) {
120+
didUpdate.fire(null);
121+
didUpdate.dispose();
122+
}
123+
}
124+
125+
function getResolvedEnv(interpreterInfo: InterpreterInformation, environment: PythonEnvInfo) {
126+
// Deep copy into a new object
127+
const resolvedEnv = cloneDeep(environment);
128+
resolvedEnv.version = interpreterInfo.version;
129+
resolvedEnv.executable.filename = interpreterInfo.executable.filename;
130+
resolvedEnv.executable.sysPrefix = interpreterInfo.executable.sysPrefix;
131+
resolvedEnv.arch = interpreterInfo.arch;
132+
return resolvedEnv;
133+
}

src/test/pythonEnvironments/base/common.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ import {
1111
PythonEnvKind,
1212
} from '../../../client/pythonEnvironments/base/info';
1313
import { parseVersion } from '../../../client/pythonEnvironments/base/info/pythonVersion';
14-
import {
15-
IPythonEnvsIterator, Locator, PythonEnvUpdatedEvent, PythonLocatorQuery,
16-
} from '../../../client/pythonEnvironments/base/locator';
14+
import { IPythonEnvsIterator, Locator, PythonEnvUpdatedEvent, PythonLocatorQuery } from '../../../client/pythonEnvironments/base/locator';
1715
import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher';
1816

1917
export function createEnv(
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { assert, expect } from 'chai';
5+
import { cloneDeep } from 'lodash';
6+
import * as path from 'path';
7+
import { ImportMock } from 'ts-mock-imports';
8+
import { EventEmitter } from 'vscode';
9+
import { ExecutionResult } from '../../../client/common/process/types';
10+
import { Architecture } from '../../../client/common/utils/platform';
11+
import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments/base/info';
12+
import { parseVersion } from '../../../client/pythonEnvironments/base/info/pythonVersion';
13+
import { PythonEnvUpdatedEvent } from '../../../client/pythonEnvironments/base/locator';
14+
import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher';
15+
import { PythonEnvsResolver } from '../../../client/pythonEnvironments/collection/environmentsResolver';
16+
import * as ExternalDep from '../../../client/pythonEnvironments/common/externalDependencies';
17+
import { EnvironmentInfoService } from '../../../client/pythonEnvironments/info/environmentInfoService';
18+
import { sleep } from '../../core';
19+
import { createEnv, getEnvs, SimpleLocator } from '../base/common';
20+
21+
suite('Environments Resolver', () => {
22+
/**
23+
* Returns the expected environment to be returned by Environment info service
24+
*/
25+
function createExpectedEnvInfo(env: PythonEnvInfo): PythonEnvInfo {
26+
const updatedEnv = cloneDeep(env);
27+
updatedEnv.version = {
28+
...parseVersion('3.8.3-final'),
29+
sysVersion: '3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]',
30+
};
31+
updatedEnv.executable.filename = env.executable.filename;
32+
updatedEnv.executable.sysPrefix = 'path';
33+
updatedEnv.arch = Architecture.x64;
34+
return updatedEnv;
35+
}
36+
suite('iterEnvs()', () => {
37+
let stubShellExec: sinon.SinonStub;
38+
setup(() => {
39+
stubShellExec = ImportMock.mockFunction(
40+
ExternalDep,
41+
'shellExecute',
42+
new Promise<ExecutionResult<string>>((resolve) => {
43+
resolve({
44+
stdout:
45+
'{"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}',
46+
});
47+
}),
48+
);
49+
});
50+
51+
teardown(() => {
52+
stubShellExec.restore();
53+
});
54+
55+
test('Iterator yields environments as-is', async () => {
56+
const env1 = createEnv('env1', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1'));
57+
const env2 = createEnv('env2', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2'));
58+
const env3 = createEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3'));
59+
const env4 = createEnv('env4', '3.9.0rc2', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2'));
60+
const environmentsToBeIterated = [env1, env2, env3, env4];
61+
const pythonEnvReducer = new SimpleLocator(environmentsToBeIterated);
62+
const reducer = new PythonEnvsResolver(pythonEnvReducer, new EnvironmentInfoService());
63+
64+
const iterator = reducer.iterEnvs();
65+
const envs = await getEnvs(iterator);
66+
67+
assert.deepEqual(envs, environmentsToBeIterated);
68+
});
69+
70+
test('Updates for environments are sent correctly followed by the null event', async () => {
71+
// Arrange
72+
const env1 = createEnv('env1', '3.5.12b1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec1'));
73+
const env2 = createEnv('env2', '3.8.1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2'));
74+
const environmentsToBeIterated = [env1, env2];
75+
const pythonEnvReducer = new SimpleLocator(environmentsToBeIterated);
76+
const onUpdatedEvents: (PythonEnvUpdatedEvent | null)[] = [];
77+
const reducer = new PythonEnvsResolver(pythonEnvReducer, new EnvironmentInfoService());
78+
79+
const iterator = reducer.iterEnvs(); // Act
80+
81+
// Assert
82+
let { onUpdated } = iterator;
83+
expect(onUpdated).to.not.equal(undefined, '');
84+
85+
// Arrange
86+
onUpdated = onUpdated!;
87+
onUpdated((e) => {
88+
onUpdatedEvents.push(e);
89+
});
90+
91+
// Act
92+
await getEnvs(iterator);
93+
await sleep(1); // Resolve pending calls in the background
94+
95+
// Assert
96+
const expectedUpdates = [
97+
{ old: env1, new: createExpectedEnvInfo(env1) },
98+
{ old: env2, new: createExpectedEnvInfo(env2) },
99+
null,
100+
];
101+
assert.deepEqual(expectedUpdates, onUpdatedEvents);
102+
});
103+
104+
test('Updates to environments from the incoming iterator are sent correctly followed by the null event', async () => {
105+
// Arrange
106+
const env = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec'));
107+
const updatedEnv = createEnv('env1', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec'));
108+
const environmentsToBeIterated = [env];
109+
const didUpdate = new EventEmitter<PythonEnvUpdatedEvent | null>();
110+
const pythonEnvReducer = new SimpleLocator(environmentsToBeIterated, { onUpdated: didUpdate.event });
111+
const onUpdatedEvents: (PythonEnvUpdatedEvent | null)[] = [];
112+
const reducer = new PythonEnvsResolver(pythonEnvReducer, new EnvironmentInfoService());
113+
114+
const iterator = reducer.iterEnvs(); // Act
115+
116+
// Assert
117+
let { onUpdated } = iterator;
118+
expect(onUpdated).to.not.equal(undefined, '');
119+
120+
// Arrange
121+
onUpdated = onUpdated!;
122+
onUpdated((e) => {
123+
onUpdatedEvents.push(e);
124+
});
125+
126+
// Act
127+
await getEnvs(iterator);
128+
await sleep(1);
129+
didUpdate.fire({ old: env, new: updatedEnv });
130+
didUpdate.fire(null); // It is essential for the incoming iterator to fire "null" event signifying it's done
131+
await sleep(1);
132+
133+
// Assert
134+
// The updates can be anything, even the number of updates, but they should lead to the same final state
135+
const { length } = onUpdatedEvents;
136+
assert.deepEqual(
137+
onUpdatedEvents[length - 2]?.new,
138+
createExpectedEnvInfo(updatedEnv),
139+
'The final update to environment is incorrect',
140+
);
141+
assert.equal(onUpdatedEvents[length - 1], null, 'Last update should be null');
142+
didUpdate.dispose();
143+
});
144+
});
145+
146+
test('onChanged fires iff onChanged from reducer fires', () => {
147+
const pythonEnvReducer = new SimpleLocator([]);
148+
const event1: PythonEnvsChangedEvent = {};
149+
const event2: PythonEnvsChangedEvent = { kind: PythonEnvKind.Unknown };
150+
const expected = [event1, event2];
151+
const reducer = new PythonEnvsResolver(pythonEnvReducer, new EnvironmentInfoService());
152+
153+
const events: PythonEnvsChangedEvent[] = [];
154+
reducer.onChanged((e) => events.push(e));
155+
156+
pythonEnvReducer.fire(event1);
157+
pythonEnvReducer.fire(event2);
158+
159+
assert.deepEqual(events, expected);
160+
});
161+
162+
suite('resolveEnv()', () => {
163+
let stubShellExec: sinon.SinonStub;
164+
setup(() => {
165+
stubShellExec = ImportMock.mockFunction(
166+
ExternalDep,
167+
'shellExecute',
168+
new Promise<ExecutionResult<string>>((resolve) => {
169+
resolve({
170+
stdout:
171+
'{"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}',
172+
});
173+
}),
174+
);
175+
});
176+
177+
teardown(() => {
178+
stubShellExec.restore();
179+
});
180+
181+
test('Calls into reducer to get resolved environment, then calls environnment service to resolve environment further and return it', async () => {
182+
const env = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec'));
183+
const resolvedEnvReturnedByReducer = createEnv(
184+
'env1',
185+
'3.8.1',
186+
PythonEnvKind.Conda,
187+
'resolved/path/to/exec',
188+
);
189+
const pythonEnvReducer = new SimpleLocator([], {
190+
resolve: async (e: PythonEnvInfo) => {
191+
if (e === env) {
192+
return resolvedEnvReturnedByReducer;
193+
}
194+
throw new Error('Incorrect environment sent to the reducer');
195+
},
196+
});
197+
const reducer = new PythonEnvsResolver(pythonEnvReducer, new EnvironmentInfoService());
198+
199+
const expected = await reducer.resolveEnv(env);
200+
201+
assert.deepEqual(expected, createExpectedEnvInfo(resolvedEnvReturnedByReducer));
202+
});
203+
204+
test('If the reducer resolves environment, but fetching interpreter info returns undefined, return undefined', async () => {
205+
stubShellExec.returns(
206+
new Promise<ExecutionResult<string>>((_resolve, reject) => {
207+
reject();
208+
}),
209+
);
210+
const env = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec'));
211+
const resolvedEnvReturnedByReducer = createEnv(
212+
'env1',
213+
'3.8.1',
214+
PythonEnvKind.Conda,
215+
'resolved/path/to/exec',
216+
);
217+
const pythonEnvReducer = new SimpleLocator([], {
218+
resolve: async (e: PythonEnvInfo) => {
219+
if (e === env) {
220+
return resolvedEnvReturnedByReducer;
221+
}
222+
throw new Error('Incorrect environment sent to the reducer');
223+
},
224+
});
225+
const reducer = new PythonEnvsResolver(pythonEnvReducer, new EnvironmentInfoService());
226+
227+
const expected = await reducer.resolveEnv(env);
228+
229+
assert.deepEqual(expected, undefined);
230+
});
231+
232+
test("If the reducer isn't able to resolve environment, return undefined", async () => {
233+
const env = createEnv('env', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec'));
234+
const pythonEnvReducer = new SimpleLocator([], {
235+
resolve: async () => undefined,
236+
});
237+
const reducer = new PythonEnvsResolver(pythonEnvReducer, new EnvironmentInfoService());
238+
239+
const expected = await reducer.resolveEnv(env);
240+
241+
assert.deepEqual(expected, undefined);
242+
});
243+
});
244+
});

0 commit comments

Comments
 (0)