Skip to content

Commit 1a169ea

Browse files
Copilotanthonykim1
andcommitted
Add comprehensive unit tests for getEnvironment scope resolution
Co-authored-by: anthonykim1 <62267334+anthonykim1@users.noreply.github.com>
1 parent 76dce84 commit 1a169ea

File tree

1 file changed

+259
-0
lines changed

1 file changed

+259
-0
lines changed
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
/**
5+
* Python API getEnvironment Unit Tests
6+
*
7+
* This test suite validates the getEnvironment API functionality including:
8+
* - Returning environment for specific scope (Uri)
9+
* - Smart scope detection when scope is undefined:
10+
* - Using active text editor's document URI
11+
* - Using single workspace folder URI
12+
* - Falling back to global environment
13+
*/
14+
15+
import * as assert from 'assert';
16+
import * as sinon from 'sinon';
17+
import * as typeMoq from 'typemoq';
18+
import { EventEmitter, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode';
19+
import {
20+
DidChangeEnvironmentEventArgs,
21+
DidChangeEnvironmentVariablesEventArgs,
22+
PythonEnvironment,
23+
} from '../../api';
24+
import * as extensionApis from '../../common/extension.apis';
25+
import * as windowApis from '../../common/window.apis';
26+
import * as workspaceApis from '../../common/workspace.apis';
27+
import { PythonEnvironmentManagers } from '../../features/envManagers';
28+
import { getPythonApi, setPythonApi } from '../../features/pythonApi';
29+
import { TerminalManager } from '../../features/terminal/terminalManager';
30+
import { EnvVarManager } from '../../features/execution/envVariableManager';
31+
import * as managerReady from '../../features/common/managerReady';
32+
import { ProjectCreators, PythonProjectManager } from '../../internal.api';
33+
import { setupNonThenable } from '../mocks/helper';
34+
35+
suite('PythonApi.getEnvironment Tests', () => {
36+
let envManagers: typeMoq.IMock<PythonEnvironmentManagers>;
37+
let projectManager: typeMoq.IMock<PythonProjectManager>;
38+
let projectCreators: typeMoq.IMock<ProjectCreators>;
39+
let terminalManager: typeMoq.IMock<TerminalManager>;
40+
let envVarManager: typeMoq.IMock<EnvVarManager>;
41+
let mockEnvironment: typeMoq.IMock<PythonEnvironment>;
42+
let getExtensionStub: sinon.SinonStub;
43+
let activeTextEditorStub: sinon.SinonStub;
44+
let getWorkspaceFoldersStub: sinon.SinonStub;
45+
46+
setup(() => {
47+
// Mock extension APIs
48+
const mockPythonExtension = {
49+
id: 'ms-python.python',
50+
extensionPath: '/mock/python/extension',
51+
};
52+
const mockEnvsExtension = {
53+
id: 'ms-python.vscode-python-envs',
54+
extensionPath: '/mock/envs/extension',
55+
};
56+
57+
getExtensionStub = sinon.stub(extensionApis, 'getExtension');
58+
getExtensionStub.withArgs('ms-python.python').returns(mockPythonExtension as any);
59+
getExtensionStub.withArgs('ms-python.vscode-python-envs').returns(mockEnvsExtension as any);
60+
61+
sinon.stub(extensionApis, 'allExtensions').returns([mockPythonExtension, mockEnvsExtension] as any);
62+
63+
// Stub the manager ready functions to avoid hanging
64+
sinon.stub(managerReady, 'waitForEnvManager').resolves();
65+
sinon.stub(managerReady, 'waitForEnvManagerId').resolves();
66+
sinon.stub(managerReady, 'waitForAllEnvManagers').resolves();
67+
68+
// Create mocks
69+
envManagers = typeMoq.Mock.ofType<PythonEnvironmentManagers>();
70+
projectManager = typeMoq.Mock.ofType<PythonProjectManager>();
71+
projectCreators = typeMoq.Mock.ofType<ProjectCreators>();
72+
terminalManager = typeMoq.Mock.ofType<TerminalManager>();
73+
envVarManager = typeMoq.Mock.ofType<EnvVarManager>();
74+
75+
// Setup event emitters
76+
const onDidChangeEnvironmentEmitter = new EventEmitter<DidChangeEnvironmentEventArgs>();
77+
78+
envManagers
79+
.setup((e) => e.onDidChangeEnvironmentFiltered)
80+
.returns(() => onDidChangeEnvironmentEmitter.event);
81+
setupNonThenable(envManagers);
82+
setupNonThenable(projectManager);
83+
setupNonThenable(projectCreators);
84+
setupNonThenable(terminalManager);
85+
86+
const onDidChangeEnvVarsEmitter = new EventEmitter<DidChangeEnvironmentVariablesEventArgs>();
87+
envVarManager
88+
.setup((e) => e.onDidChangeEnvironmentVariables)
89+
.returns(() => onDidChangeEnvVarsEmitter.event);
90+
setupNonThenable(envVarManager);
91+
92+
// Mock environment
93+
mockEnvironment = typeMoq.Mock.ofType<PythonEnvironment>();
94+
mockEnvironment.setup((e) => e.envId).returns(() => ({ id: 'test-env', managerId: 'test-mgr' }));
95+
mockEnvironment.setup((e) => e.displayName).returns(() => 'Test Environment');
96+
setupNonThenable(mockEnvironment);
97+
98+
// Setup a default return for all getEnvironment calls
99+
envManagers
100+
.setup((e) => e.getEnvironment(typeMoq.It.isAny()))
101+
.returns(() => Promise.resolve(mockEnvironment.object));
102+
103+
// Stub window and workspace APIs
104+
activeTextEditorStub = sinon.stub(windowApis, 'activeTextEditor');
105+
getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders');
106+
107+
// Initialize API
108+
setPythonApi(
109+
envManagers.object,
110+
projectManager.object,
111+
projectCreators.object,
112+
terminalManager.object,
113+
envVarManager.object,
114+
);
115+
});
116+
117+
teardown(() => {
118+
sinon.restore();
119+
});
120+
121+
test('getEnvironment with explicit URI scope returns environment for that scope', async () => {
122+
const testUri = Uri.file('/test/workspace/file.py');
123+
124+
envManagers
125+
.setup((e) => e.getEnvironment(testUri))
126+
.returns(() => Promise.resolve(mockEnvironment.object))
127+
.verifiable(typeMoq.Times.once());
128+
129+
const api = await getPythonApi();
130+
const result = await api.getEnvironment(testUri);
131+
132+
assert.strictEqual(result, mockEnvironment.object);
133+
envManagers.verifyAll();
134+
});
135+
136+
test('getEnvironment with undefined scope uses active text editor URI when available', async () => {
137+
const testUri = Uri.file('/test/workspace/file.py');
138+
const mockDoc: Partial<TextDocument> = {
139+
uri: testUri,
140+
isUntitled: false,
141+
};
142+
143+
const mockEditor: Partial<TextEditor> = {
144+
document: mockDoc as TextDocument,
145+
};
146+
147+
activeTextEditorStub.returns(mockEditor as TextEditor);
148+
149+
const api = await getPythonApi();
150+
const result = await api.getEnvironment(undefined);
151+
152+
assert.strictEqual(result, mockEnvironment.object);
153+
// Verify the stub was called with a non-undefined value (should be testUri)
154+
sinon.assert.called(activeTextEditorStub);
155+
});
156+
157+
test('getEnvironment with undefined scope uses workspace folder when no active editor', async () => {
158+
const workspaceUri = Uri.file('/test/workspace');
159+
const mockWorkspaceFolder: Partial<WorkspaceFolder> = {
160+
uri: workspaceUri,
161+
name: 'test-workspace',
162+
index: 0,
163+
};
164+
165+
activeTextEditorStub.returns(undefined);
166+
getWorkspaceFoldersStub.returns([mockWorkspaceFolder as WorkspaceFolder]);
167+
168+
const api = await getPythonApi();
169+
const result = await api.getEnvironment(undefined);
170+
171+
assert.strictEqual(result, mockEnvironment.object);
172+
sinon.assert.called(getWorkspaceFoldersStub);
173+
});
174+
175+
test('getEnvironment with undefined scope falls back to global when no editor or workspace', async () => {
176+
activeTextEditorStub.returns(undefined);
177+
getWorkspaceFoldersStub.returns(undefined);
178+
179+
const api = await getPythonApi();
180+
const result = await api.getEnvironment(undefined);
181+
182+
assert.strictEqual(result, mockEnvironment.object);
183+
});
184+
185+
test('getEnvironment with undefined scope ignores untitled documents', async () => {
186+
const workspaceUri = Uri.file('/test/workspace');
187+
const mockWorkspaceFolder: Partial<WorkspaceFolder> = {
188+
uri: workspaceUri,
189+
name: 'test-workspace',
190+
index: 0,
191+
};
192+
193+
const mockDoc: Partial<TextDocument> = {
194+
isUntitled: true,
195+
};
196+
197+
const mockEditor: Partial<TextEditor> = {
198+
document: mockDoc as TextDocument,
199+
};
200+
201+
activeTextEditorStub.returns(mockEditor as TextEditor);
202+
getWorkspaceFoldersStub.returns([mockWorkspaceFolder as WorkspaceFolder]);
203+
204+
const api = await getPythonApi();
205+
const result = await api.getEnvironment(undefined);
206+
207+
assert.strictEqual(result, mockEnvironment.object);
208+
sinon.assert.called(getWorkspaceFoldersStub);
209+
});
210+
211+
test('getEnvironment with undefined scope ignores non-file scheme documents', async () => {
212+
const workspaceUri = Uri.file('/test/workspace');
213+
const mockWorkspaceFolder: Partial<WorkspaceFolder> = {
214+
uri: workspaceUri,
215+
name: 'test-workspace',
216+
index: 0,
217+
};
218+
219+
const mockDoc: Partial<TextDocument> = {
220+
uri: Uri.parse('git:/test/file.py'),
221+
isUntitled: false,
222+
};
223+
224+
const mockEditor: Partial<TextEditor> = {
225+
document: mockDoc as TextDocument,
226+
};
227+
228+
activeTextEditorStub.returns(mockEditor as TextEditor);
229+
getWorkspaceFoldersStub.returns([mockWorkspaceFolder as WorkspaceFolder]);
230+
231+
const api = await getPythonApi();
232+
const result = await api.getEnvironment(undefined);
233+
234+
assert.strictEqual(result, mockEnvironment.object);
235+
sinon.assert.called(getWorkspaceFoldersStub);
236+
});
237+
238+
test('getEnvironment with undefined scope falls back to global when multiple workspaces', async () => {
239+
const workspace1: Partial<WorkspaceFolder> = {
240+
uri: Uri.file('/workspace1'),
241+
name: 'workspace1',
242+
index: 0,
243+
};
244+
245+
const workspace2: Partial<WorkspaceFolder> = {
246+
uri: Uri.file('/workspace2'),
247+
name: 'workspace2',
248+
index: 1,
249+
};
250+
251+
activeTextEditorStub.returns(undefined);
252+
getWorkspaceFoldersStub.returns([workspace1 as WorkspaceFolder, workspace2 as WorkspaceFolder]);
253+
254+
const api = await getPythonApi();
255+
const result = await api.getEnvironment(undefined);
256+
257+
assert.strictEqual(result, mockEnvironment.object);
258+
});
259+
});

0 commit comments

Comments
 (0)