Skip to content

Commit dea449f

Browse files
edvilmeCopilot
andauthored
Add automated tests for interpreter resolution (#645)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 501b182 commit dea449f

File tree

2 files changed

+166
-0
lines changed

2 files changed

+166
-0
lines changed

src/common/python.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,8 @@ export function checkVersion(resolved: ResolvedEnvironment | undefined): boolean
235235
traceError(`Supported versions are ${PYTHON_VERSION} and above.`);
236236
return false;
237237
}
238+
239+
export function resetCachedApis(): void {
240+
_api = undefined;
241+
_envsApi = undefined;
242+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { assert } from 'chai';
5+
import * as sinon from 'sinon';
6+
import { EventEmitter, extensions, Uri } from 'vscode';
7+
import { PythonExtension } from '@vscode/python-extension';
8+
import { getInterpreterDetails, resetCachedApis } from '../../../../common/python';
9+
10+
suite('Python Interpreter Resolution Tests', () => {
11+
let getExtensionStub: sinon.SinonStub;
12+
let pythonExtensionApiStub: sinon.SinonStub;
13+
14+
setup(() => {
15+
resetCachedApis();
16+
getExtensionStub = sinon.stub(extensions, 'getExtension');
17+
pythonExtensionApiStub = sinon.stub(PythonExtension, 'api');
18+
});
19+
20+
teardown(() => {
21+
sinon.restore();
22+
});
23+
24+
test('Finds interpreter using Python Environments extension', async () => {
25+
const mockEnvsApi = {
26+
getEnvironment: sinon.stub().resolves({
27+
version: '3.12.0',
28+
execInfo: {
29+
run: { executable: '/usr/bin/python3.12', args: [] },
30+
},
31+
envId: { id: 'test-env', managerId: 'test-manager' },
32+
sysPrefix: '/usr',
33+
name: 'test',
34+
displayName: 'Python 3.12',
35+
environmentPath: Uri.file('/usr/bin/python3.12'),
36+
}),
37+
resolveEnvironment: sinon.stub(),
38+
onDidChangeEnvironment: new EventEmitter().event,
39+
};
40+
41+
getExtensionStub.withArgs('ms-python.vscode-python-envs').returns({
42+
isActive: true,
43+
activate: sinon.stub().resolves(),
44+
exports: mockEnvsApi,
45+
});
46+
47+
const result = await getInterpreterDetails(Uri.file('/test/workspace'));
48+
49+
assert.isDefined(result.path);
50+
assert.strictEqual(result.path![0], '/usr/bin/python3.12');
51+
assert.isTrue(mockEnvsApi.getEnvironment.calledOnce);
52+
// Legacy API should not be called
53+
assert.isTrue(pythonExtensionApiStub.notCalled);
54+
});
55+
56+
test('Finds interpreter using legacy ms-python.python extension', async () => {
57+
// Envs extension not installed
58+
getExtensionStub.withArgs('ms-python.vscode-python-envs').returns(undefined);
59+
60+
const interpreterUri = Uri.file('/usr/bin/python3.10');
61+
const mockLegacyApi = {
62+
environments: {
63+
getActiveEnvironmentPath: sinon.stub().returns('/usr/bin/python3.10'),
64+
resolveEnvironment: sinon.stub().resolves({
65+
executable: {
66+
uri: interpreterUri,
67+
bitness: '64-bit',
68+
sysPrefix: '/usr',
69+
},
70+
version: {
71+
major: 3,
72+
minor: 10,
73+
micro: 0,
74+
release: { level: 'final', serial: 0 },
75+
sysVersion: '3.10.0',
76+
},
77+
}),
78+
onDidChangeActiveEnvironmentPath: new EventEmitter().event,
79+
},
80+
debug: {
81+
getDebuggerPackagePath: sinon.stub(),
82+
},
83+
};
84+
85+
pythonExtensionApiStub.resolves(mockLegacyApi);
86+
87+
const result = await getInterpreterDetails(Uri.file('/test/workspace'));
88+
89+
assert.isDefined(result.path);
90+
assert.strictEqual(result.path![0], interpreterUri.fsPath);
91+
assert.isTrue(mockLegacyApi.environments.resolveEnvironment.calledOnce);
92+
});
93+
94+
test('Rejects unsupported Python version from Environments extension', async () => {
95+
const mockEnvsApi = {
96+
getEnvironment: sinon.stub().resolves({
97+
version: '3.8.0',
98+
execInfo: {
99+
run: { executable: '/usr/bin/python3.8', args: [] },
100+
},
101+
envId: { id: 'old-env', managerId: 'test-manager' },
102+
sysPrefix: '/usr',
103+
name: 'old-python',
104+
displayName: 'Python 3.8',
105+
environmentPath: Uri.file('/usr/bin/python3.8'),
106+
}),
107+
resolveEnvironment: sinon.stub(),
108+
onDidChangeEnvironment: new EventEmitter().event,
109+
};
110+
111+
getExtensionStub.withArgs('ms-python.vscode-python-envs').returns({
112+
isActive: true,
113+
activate: sinon.stub().resolves(),
114+
exports: mockEnvsApi,
115+
});
116+
117+
const result = await getInterpreterDetails(Uri.file('/test/workspace'));
118+
119+
assert.isUndefined(result.path);
120+
assert.isTrue(mockEnvsApi.getEnvironment.calledOnce);
121+
// Should not fall through to legacy when version is explicitly unsupported
122+
assert.isTrue(pythonExtensionApiStub.notCalled);
123+
});
124+
125+
test('Rejects unsupported Python version from legacy extension', async () => {
126+
// Envs extension not installed
127+
getExtensionStub.withArgs('ms-python.vscode-python-envs').returns(undefined);
128+
129+
const interpreterUri = Uri.file('/usr/bin/python3.8');
130+
const mockLegacyApi = {
131+
environments: {
132+
getActiveEnvironmentPath: sinon.stub().returns('/usr/bin/python3.8'),
133+
resolveEnvironment: sinon.stub().resolves({
134+
executable: {
135+
uri: interpreterUri,
136+
bitness: '64-bit',
137+
sysPrefix: '/usr',
138+
},
139+
version: {
140+
major: 3,
141+
minor: 8,
142+
micro: 0,
143+
release: { level: 'final', serial: 0 },
144+
sysVersion: '3.8.0',
145+
},
146+
}),
147+
onDidChangeActiveEnvironmentPath: new EventEmitter().event,
148+
},
149+
debug: {
150+
getDebuggerPackagePath: sinon.stub(),
151+
},
152+
};
153+
154+
pythonExtensionApiStub.resolves(mockLegacyApi);
155+
156+
const result = await getInterpreterDetails(Uri.file('/test/workspace'));
157+
158+
assert.isUndefined(result.path);
159+
assert.isTrue(mockLegacyApi.environments.resolveEnvironment.calledOnce);
160+
});
161+
});

0 commit comments

Comments
 (0)