Skip to content

Commit 6146c8c

Browse files
author
Kartik Raj
committed
Added tests
1 parent 5314a97 commit 6146c8c

File tree

3 files changed

+90
-78
lines changed

3 files changed

+90
-78
lines changed

src/client/pythonEnvironments/collection/environmentsResolver.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,11 @@ export class PythonEnvsResolver implements ILocator {
5050
state.done = true;
5151
checkIfFinishedAndNotify(state, didUpdate);
5252
} else {
53-
const old = seen.find((s) => areSameEnvironment(s, event.old));
54-
if (old !== undefined) {
55-
didUpdate.fire({ old, new: event.new });
56-
seen[seen.indexOf(old)] = event.new;
53+
const oldIndex = seen.findIndex((s) => areSameEnvironment(s, event.old));
54+
if (oldIndex !== -1) {
55+
seen[oldIndex] = event.new;
5756
state.pending += 1;
58-
this.resolveInBackground(event.new, state, didUpdate, seen).ignoreErrors();
57+
this.resolveInBackground(oldIndex, state, didUpdate, seen).ignoreErrors();
5958
} else {
6059
// This implies a problem in a downstream locator
6160
traceVerbose(`Expected already iterated env in resolver, got ${event.old}`);
@@ -71,7 +70,7 @@ export class PythonEnvsResolver implements ILocator {
7170
seen.push(currEnv);
7271
yield currEnv;
7372
state.pending += 1;
74-
this.resolveInBackground(currEnv, state, didUpdate, seen).ignoreErrors();
73+
this.resolveInBackground(seen.indexOf(currEnv), state, didUpdate, seen).ignoreErrors();
7574
// eslint-disable-next-line no-await-in-loop
7675
result = await iterator.next();
7776
}
@@ -82,18 +81,18 @@ export class PythonEnvsResolver implements ILocator {
8281
}
8382

8483
private async resolveInBackground(
85-
env: PythonEnvInfo,
84+
envIndex: number,
8685
state: { done: boolean; pending: number },
8786
didUpdate: EventEmitter<PythonEnvUpdatedEvent | null>,
8887
seen: PythonEnvInfo[],
8988
) {
90-
const interpreterInfo = await this.environmentInfoService.getEnvironmentInfo(env.executable.filename);
91-
// Old environment to be fired might have changed until now, search for it again
92-
const seenEnv = seen.find((s) => areSameEnvironment(s, env));
93-
if (interpreterInfo && seenEnv) {
94-
const resolvedEnv = getResolvedEnv(interpreterInfo, seenEnv);
95-
didUpdate.fire({ old: seenEnv, new: resolvedEnv });
96-
seen[seen.indexOf(seenEnv)] = resolvedEnv;
89+
const interpreterInfo = await this.environmentInfoService.getEnvironmentInfo(
90+
seen[envIndex].executable.filename,
91+
);
92+
if (interpreterInfo && seen[envIndex]) {
93+
const resolvedEnv = getResolvedEnv(interpreterInfo, seen[envIndex]);
94+
didUpdate.fire({ old: seen[envIndex], new: resolvedEnv });
95+
seen[envIndex] = resolvedEnv;
9796
}
9897
state.pending -= 1;
9998
checkIfFinishedAndNotify(state, didUpdate);

src/test/pythonEnvironments/base/common.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
import { Event } from 'vscode';
45
import { createDeferred, flattenIterator, iterable, mapToIterator } from '../../../client/common/utils/async';
56
import { Architecture } from '../../../client/common/utils/platform';
7+
import { EMPTY_VERSION, parseBasicVersionInfo } from '../../../client/common/utils/version';
68
import {
79
PythonEnvInfo,
810
PythonEnvKind,
11+
PythonReleaseLevel,
12+
PythonVersion
913
} from '../../../client/pythonEnvironments/base/info';
10-
import { parseVersion } from '../../../client/pythonEnvironments/base/info/pythonVersion';
11-
import { IPythonEnvsIterator, Locator, PythonLocatorQuery } from '../../../client/pythonEnvironments/base/locator';
14+
import { IPythonEnvsIterator, Locator, PythonEnvUpdatedEvent, PythonLocatorQuery } from '../../../client/pythonEnvironments/base/locator';
1215
import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher';
1316

1417
export function createEnv(
@@ -43,6 +46,36 @@ export function createEnv(
4346
};
4447
}
4548

49+
function parseVersion(versionStr: string): PythonVersion {
50+
const parsed = parseBasicVersionInfo<PythonVersion>(versionStr);
51+
if (!parsed) {
52+
if (versionStr === '') {
53+
return EMPTY_VERSION as PythonVersion;
54+
}
55+
throw Error(`invalid version ${versionStr}`);
56+
}
57+
const { version, after } = parsed;
58+
const match = after.match(/^(a|b|rc)(\d+)$/);
59+
if (match) {
60+
const [, levelStr, serialStr ] = match;
61+
let level: PythonReleaseLevel;
62+
if (levelStr === 'a') {
63+
level = PythonReleaseLevel.Alpha;
64+
} else if (levelStr === 'b') {
65+
level = PythonReleaseLevel.Beta;
66+
} else if (levelStr === 'rc') {
67+
level = PythonReleaseLevel.Candidate;
68+
} else {
69+
throw Error('unreachable!');
70+
}
71+
version.release = {
72+
level,
73+
serial: parseInt(serialStr, 10)
74+
};
75+
}
76+
return version;
77+
}
78+
4679
export function createLocatedEnv(
4780
location: string,
4881
versionStr: string,
@@ -66,6 +99,7 @@ export class SimpleLocator extends Locator {
6699
resolve?: null | ((env: PythonEnvInfo) => Promise<PythonEnvInfo | undefined>);
67100
before?: Promise<void>;
68101
after?: Promise<void>;
102+
onUpdated?: Event<PythonEnvUpdatedEvent | null>;
69103
beforeEach?(e: PythonEnvInfo): Promise<void>;
70104
afterEach?(e: PythonEnvInfo): Promise<void>;
71105
onQuery?(query: PythonLocatorQuery | undefined, envs: PythonEnvInfo[]): Promise<PythonEnvInfo[]>;
@@ -83,7 +117,7 @@ export class SimpleLocator extends Locator {
83117
const deferred = this.deferred;
84118
const callbacks = this.callbacks;
85119
let envs = this.envs;
86-
async function* iterator() {
120+
const iterator: IPythonEnvsIterator = async function*() {
87121
if (callbacks?.onQuery !== undefined) {
88122
envs = await callbacks.onQuery(query, envs);
89123
}
@@ -114,8 +148,9 @@ export class SimpleLocator extends Locator {
114148
await callbacks.after;
115149
}
116150
deferred.resolve();
117-
}
118-
return iterator();
151+
}();
152+
iterator.onUpdated = this.callbacks?.onUpdated;
153+
return iterator;
119154
}
120155
public async resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
121156
const envInfo: PythonEnvInfo = typeof env === 'string' ? createEnv('', '', undefined, env) : env;

src/test/pythonEnvironments/collection/environmentsResolver.unit.test.ts

Lines changed: 37 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,26 @@
22
// Licensed under the MIT License.
33

44
import { assert, expect } from 'chai';
5+
import { cloneDeep } from 'lodash';
56
import * as path from 'path';
67
import { ImportMock } from 'ts-mock-imports';
78
import { EventEmitter } from 'vscode';
89
import { ExecutionResult } from '../../../client/common/process/types';
10+
import { Architecture } from '../../../client/common/utils/platform';
911
import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments/base/info';
12+
import { parseVersion } from '../../../client/pythonEnvironments/base/info/pythonVersion';
1013
import { PythonEnvUpdatedEvent } from '../../../client/pythonEnvironments/base/locator';
1114
import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher';
1215
import { PythonEnvsResolver } from '../../../client/pythonEnvironments/collection/environmentsResolver';
1316
import * as ExternalDep from '../../../client/pythonEnvironments/common/externalDependencies';
14-
import { EnvironmentInfoService, IEnvironmentInfoService } from '../../../client/pythonEnvironments/info/environmentInfoService';
17+
import {
18+
EnvironmentInfoService,
19+
IEnvironmentInfoService,
20+
} from '../../../client/pythonEnvironments/info/environmentInfoService';
1521
import { sleep } from '../../core';
1622
import { createEnv, getEnvs, SimpleLocator } from '../base/common';
1723

18-
suite('Environments Reducer', () => {
24+
suite('Environments Resolver', () => {
1925
suite('iterEnvs()', () => {
2026
let stubShellExec: sinon.SinonStub;
2127
let envService: IEnvironmentInfoService;
@@ -28,40 +34,45 @@ suite('Environments Reducer', () => {
2834
new Promise<ExecutionResult<string>>((resolve) => {
2935
resolve({
3036
stdout:
31-
'{"versionInfo": [3, 8, 3, "final", 0], "sysPrefix": "path", "version": "3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]", "is64Bit": true}',
37+
'{"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}',
3238
});
3339
}),
3440
);
3541
});
42+
3643
teardown(() => {
3744
stubShellExec.restore();
3845
});
3946

40-
test('Iterator only yields unique environments', async () => {
47+
function createExpectedEnvInfo(env: PythonEnvInfo): PythonEnvInfo {
48+
const updatedEnv = cloneDeep(env);
49+
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)]' };
50+
updatedEnv.executable.filename = env.executable.filename;
51+
updatedEnv.executable.sysPrefix = 'path';
52+
updatedEnv.arch = Architecture.x64;
53+
return updatedEnv;
54+
}
55+
56+
test('Iterator only yields as-is', async () => {
4157
const env1 = createEnv('env1', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1'));
4258
const env2 = createEnv('env2', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2'));
4359
const env3 = createEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3'));
44-
const env4 = createEnv('env4', '3.9.0rc2', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); // Same as env2
45-
const env5 = createEnv('env5', '3.8', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); // Same as env1
46-
const environmentsToBeIterated = [env1, env2, env3, env4, env5]; // Contains 3 unique environments
60+
const env4 = createEnv('env4', '3.9.0rc2', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2'));
61+
const environmentsToBeIterated = [env1, env2, env3, env4];
4762
const pythonEnvManager = new SimpleLocator(environmentsToBeIterated);
4863
const reducer = new PythonEnvsResolver(pythonEnvManager, envService);
4964

5065
const iterator = reducer.iterEnvs();
5166
const envs = await getEnvs(iterator);
5267

53-
const expected = [env1, env2, env3];
54-
assert.deepEqual(envs, expected);
68+
assert.deepEqual(envs, environmentsToBeIterated);
5569
});
5670

57-
test('Single updates for multiple environments are sent correctly followed by the null event', async () => {
71+
test('Updates for environments are sent correctly followed by the null event', async () => {
5872
// Arrange
5973
const env1 = createEnv('env1', '3.5.12b1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec1'));
6074
const env2 = createEnv('env2', '3.8.1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2'));
61-
const env3 = createEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3'));
62-
const env4 = createEnv('env4', '3.9.0rc2', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); // Same as env2;
63-
const env5 = createEnv('env5', '3.8', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); // Same as env1;
64-
const environmentsToBeIterated = [env1, env2, env3, env4, env5]; // Contains 3 unique environments
75+
const environmentsToBeIterated = [env1, env2];
6576
const pythonEnvManager = new SimpleLocator(environmentsToBeIterated);
6677
const onUpdatedEvents: (PythonEnvUpdatedEvent | null)[] = [];
6778
const reducer = new PythonEnvsResolver(pythonEnvManager, envService);
@@ -84,54 +95,18 @@ suite('Environments Reducer', () => {
8495

8596
// Assert
8697
const expectedUpdates = [
87-
{ old: env2, new: mergeEnvironments(env2, env4) },
88-
{ old: env1, new: mergeEnvironments(env1, env5) },
98+
{ old: env1, new: createExpectedEnvInfo(env1) },
99+
{ old: env2, new: createExpectedEnvInfo(env2) },
89100
null,
90101
];
91102
assert.deepEqual(expectedUpdates, onUpdatedEvents);
92103
});
93104

94-
test('Multiple updates for the same environment are sent correctly followed by the null event', async () => {
105+
test('Updates to environments from the incoming iterator are sent correctly followed by the null event', async () => {
95106
// Arrange
96-
const env1 = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec'));
97-
const env2 = createEnv('env2', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec'));
98-
const env3 = createEnv('env3', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec'));
99-
const environmentsToBeIterated = [env1, env2, env3]; // All refer to the same environment
100-
const pythonEnvManager = new SimpleLocator(environmentsToBeIterated);
101-
const onUpdatedEvents: (PythonEnvUpdatedEvent | null)[] = [];
102-
const reducer = new PythonEnvsResolver(pythonEnvManager, envService);
103-
104-
const iterator = reducer.iterEnvs(); // Act
105-
106-
// Assert
107-
let { onUpdated } = iterator;
108-
expect(onUpdated).to.not.equal(undefined, '');
109-
110-
// Arrange
111-
onUpdated = onUpdated!;
112-
onUpdated((e) => {
113-
onUpdatedEvents.push(e);
114-
});
115-
116-
// Act
117-
await getEnvs(iterator);
118-
await sleep(1); // Resolve pending calls in the background
119-
120-
// Assert
121-
const env12 = mergeEnvironments(env1, env2);
122-
const expectedUpdates = [
123-
{ old: env1, new: env12 },
124-
{ old: env12, new: mergeEnvironments(env12, env3) },
125-
null,
126-
];
127-
assert.deepEqual(expectedUpdates, onUpdatedEvents);
128-
});
129-
130-
test('Updates to environments from the incoming iterator are passed on correctly followed by the null event', async () => {
131-
// Arrange
132-
const env1 = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec'));
133-
const env2 = createEnv('env2', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec'));
134-
const environmentsToBeIterated = [env1];
107+
const env = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec'));
108+
const updatedEnv = createEnv('env1', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec'));
109+
const environmentsToBeIterated = [env];
135110
const didUpdate = new EventEmitter<PythonEnvUpdatedEvent | null>();
136111
const pythonEnvManager = new SimpleLocator(environmentsToBeIterated, { onUpdated: didUpdate.event });
137112
const onUpdatedEvents: (PythonEnvUpdatedEvent | null)[] = [];
@@ -151,13 +126,16 @@ suite('Environments Reducer', () => {
151126

152127
// Act
153128
await getEnvs(iterator);
154-
didUpdate.fire({ old: env1, new: env2 });
129+
await sleep(1);
130+
didUpdate.fire({ old: env, new: updatedEnv });
155131
didUpdate.fire(null); // It is essential for the incoming iterator to fire "null" event signifying it's done
156132
await sleep(1);
157133

158134
// Assert
159-
const expectedUpdates = [{ old: env1, new: mergeEnvironments(env1, env2) }, null];
160-
assert.deepEqual(expectedUpdates, onUpdatedEvents);
135+
// The updates can be anything, even the number of updates, but they should lead to the same final state
136+
const { length } = onUpdatedEvents;
137+
assert.deepEqual(onUpdatedEvents[length - 2]?.new, createExpectedEnvInfo(updatedEnv), 'The final update to environment is incorrect');
138+
assert.equal(onUpdatedEvents[length - 1], null, 'Last update should be null');
161139
didUpdate.dispose();
162140
});
163141
});

0 commit comments

Comments
 (0)