-
Notifications
You must be signed in to change notification settings - Fork 39
Expand file tree
/
Copy pathpythonProjects.integration.test.ts
More file actions
360 lines (302 loc) · 13.1 KB
/
pythonProjects.integration.test.ts
File metadata and controls
360 lines (302 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
/**
* Integration Test: Python Projects
*
* PURPOSE:
* Verify that Python project management works correctly - adding projects,
* assigning environments, and persisting settings.
*
* WHAT THIS TESTS:
* 1. Adding projects via API
* 2. Removing projects via API
* 3. Project-environment associations
* 4. Events fire when projects change
* 5. Workspace folders are treated as default projects
*
*/
import * as assert from 'assert';
import * as vscode from 'vscode';
import { PythonEnvironment, PythonEnvironmentApi } from '../../api';
import { ENVS_EXTENSION_ID } from '../constants';
import { TestEventHandler, waitForCondition } from '../testUtils';
suite('Integration: Python Projects', function () {
this.timeout(60_000);
let api: PythonEnvironmentApi;
let originalProjectEnvs: Map<string, PythonEnvironment | undefined>;
suiteSetup(async function () {
this.timeout(30_000);
const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID);
assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`);
if (!extension.isActive) {
await extension.activate();
await waitForCondition(() => extension.isActive, 20_000, 'Extension did not activate');
}
api = extension.exports as PythonEnvironmentApi;
assert.ok(api, 'API not available');
assert.ok(typeof api.getPythonProjects === 'function', 'getPythonProjects method not available');
// Save original state for restoration
originalProjectEnvs = new Map();
const projects = api.getPythonProjects();
for (const project of projects) {
const env = await api.getEnvironment(project.uri);
originalProjectEnvs.set(project.uri.toString(), env);
}
});
suiteTeardown(async function () {
// Restore original state
for (const [uriStr, env] of originalProjectEnvs) {
try {
const uri = vscode.Uri.parse(uriStr);
await api.setEnvironment(uri, env);
} catch {
// Ignore errors during cleanup
}
}
});
/**
* Test: Workspace folders are default projects
*
* When a workspace is open, the workspace folder(s) should be
* automatically treated as Python projects.
*/
test('Workspace folders appear as default projects', async function () {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
this.skip();
return;
}
const projects = api.getPythonProjects();
assert.ok(Array.isArray(projects), 'getPythonProjects should return array');
// Each workspace folder should be a project
for (const folder of workspaceFolders) {
const found = projects.some(
(p) => p.uri.fsPath === folder.uri.fsPath || p.uri.toString() === folder.uri.toString(),
);
assert.ok(found, `Workspace folder ${folder.name} should be a project`);
}
});
/**
* Test: getPythonProject returns correct project for URI
*
* Given a URI within a project, getPythonProject should return
* the containing project.
*/
test('getPythonProject returns project for URI', async function () {
const projects = api.getPythonProjects();
if (projects.length === 0) {
this.skip();
return;
}
const project = projects[0];
const foundProject = api.getPythonProject(project.uri);
assert.ok(foundProject, 'Should find project by its URI');
assert.strictEqual(foundProject.uri.toString(), project.uri.toString(), 'Found project should match original');
});
/**
* Test: getPythonProject returns undefined for unknown URI
*
* Querying a path that's not within any project should return undefined.
*/
test('getPythonProject returns undefined for unknown URI', async function () {
// Use a path that definitely won't be a project
const unknownUri = vscode.Uri.file('/nonexistent/path/that/wont/exist');
const project = api.getPythonProject(unknownUri);
assert.strictEqual(project, undefined, 'Should return undefined for unknown path');
});
/**
* Test: Projects have required structure
*
* Each project should have the minimum required properties.
*/
test('Projects have valid structure', async function () {
const projects = api.getPythonProjects();
if (projects.length === 0) {
this.skip();
return;
}
for (const project of projects) {
assert.ok(typeof project.name === 'string', 'Project must have name');
assert.ok(project.name.length > 0, 'Project name should not be empty');
assert.ok(project.uri, 'Project must have URI');
assert.ok(project.uri instanceof vscode.Uri, 'Project URI must be a Uri');
}
});
/**
* Test: Environment can be set and retrieved for project
*
* After setting an environment for a project, getEnvironment should
* return that environment.
*/
test('setEnvironment and getEnvironment work for project', async function () {
const projects = api.getPythonProjects();
if (projects.length === 0) {
this.skip();
return;
}
const environments = await api.getEnvironments('all');
if (environments.length === 0) {
this.skip();
return;
}
const project = projects[0];
const env = environments[0];
// Wait for the change event, then verify getEnvironment.
// Using an event-driven approach instead of polling avoids a race condition where
// setEnvironment's async settings write hasn't landed by the time getEnvironment
// reads back the manager from settings.
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
subscription.dispose();
reject(
new Error(
`onDidChangeEnvironment did not fire for project within 15s. Expected envId: ${env.envId.id}`,
),
);
}, 15_000);
const subscription = api.onDidChangeEnvironment((e) => {
if (e.uri?.toString() === project.uri.toString() && e.new?.envId.id === env.envId.id) {
clearTimeout(timeout);
subscription.dispose();
resolve();
}
});
// Set environment after subscribing so we don't miss the event
api.setEnvironment(project.uri, env).catch((err) => {
clearTimeout(timeout);
subscription.dispose();
reject(err);
});
});
// Verify getEnvironment returns the correct value now that setEnvironment has fully completed
const retrievedEnv = await api.getEnvironment(project.uri);
assert.ok(retrievedEnv, 'Should get environment after setting');
assert.strictEqual(retrievedEnv.envId.id, env.envId.id, 'Retrieved environment should match set environment');
});
/**
* Test: onDidChangeEnvironment fires when project environment changes
*
* Setting an environment for a project should fire the change event.
*/
test('onDidChangeEnvironment fires on project environment change', async function () {
const projects = api.getPythonProjects();
const environments = await api.getEnvironments('all');
if (projects.length === 0 || environments.length < 2) {
// Need at least 2 environments to guarantee a change
this.skip();
return;
}
const project = projects[0];
// Get current environment to pick a different one
const currentEnv = await api.getEnvironment(project.uri);
// Pick an environment different from current
let targetEnv = environments[0];
if (currentEnv && currentEnv.envId.id === targetEnv.envId.id) {
targetEnv = environments[1];
}
// Register handler BEFORE making the change
const handler = new TestEventHandler(api.onDidChangeEnvironment, 'onDidChangeEnvironment');
try {
// Set environment - this should fire the event
await api.setEnvironment(project.uri, targetEnv);
// Wait for an event where event.new is defined (the actual change event)
// Use 15s timeout - CI runners can be slow
await waitForCondition(
() => handler.all.some((e) => e.new !== undefined),
15_000,
'onDidChangeEnvironment with new environment was not fired',
);
// Find the event with the new environment
const changeEvent = handler.all.find((e) => e.new !== undefined);
assert.ok(changeEvent, 'Should have change event with new environment');
assert.ok(changeEvent.new, 'Event should have new environment');
} finally {
handler.dispose();
}
});
/**
* Test: Environment can be unset for project
*
* Setting undefined as environment should clear the explicit association.
* After clearing, getEnvironment may return auto-discovered env or undefined.
*/
test('setEnvironment with undefined clears association', async function () {
const projects = api.getPythonProjects();
const environments = await api.getEnvironments('all');
if (projects.length === 0 || environments.length === 0) {
this.skip();
return;
}
const project = projects[0];
const env = environments[0];
// Set environment first
await api.setEnvironment(project.uri, env);
// Wait for it to be set
// Use 15s timeout - CI runners can be slow with settings persistence
await waitForCondition(
async () => {
const retrieved = await api.getEnvironment(project.uri);
return retrieved !== undefined && retrieved.envId.id === env.envId.id;
},
15_000,
'Environment was not set before clearing',
);
// Verify it was set
const beforeClear = await api.getEnvironment(project.uri);
assert.ok(beforeClear, 'Environment should be set before clearing');
assert.strictEqual(beforeClear.envId.id, env.envId.id, 'Should have the explicitly set environment');
// Clear environment
await api.setEnvironment(project.uri, undefined);
// After clearing, if there's still an environment, it should either be:
// 1. undefined (no auto-discovery)
// 2. Different from the explicitly set one (auto-discovered fallback)
// 3. Same as before if it happens to be auto-discovered too (edge case)
const afterClear = await api.getEnvironment(project.uri);
// The key assertion: the operation completed without error
// and the API behaves consistently (returns env or undefined)
if (afterClear) {
assert.ok(afterClear.envId, 'If environment returned, it must have valid envId');
assert.ok(afterClear.envId.id, 'If environment returned, envId must have id');
} else {
assert.strictEqual(
afterClear,
undefined,
'Cleared association should return undefined when no auto-discovery',
);
}
});
/**
* Test: File within project resolves to project environment
*
* A file path inside a project should resolve to that project's environment.
*/
test('File in project uses project environment', async function () {
const projects = api.getPythonProjects();
const environments = await api.getEnvironments('all');
if (projects.length === 0 || environments.length === 0) {
this.skip();
return;
}
const project = projects[0];
const env = environments[0];
// Set environment for project
await api.setEnvironment(project.uri, env);
// Wait for it to be set
// Use 15s timeout - CI runners can be slow with settings persistence
await waitForCondition(
async () => {
const retrieved = await api.getEnvironment(project.uri);
return retrieved !== undefined && retrieved.envId.id === env.envId.id;
},
15_000,
'Environment was not set for project',
);
// Create a hypothetical file path inside the project
const fileUri = vscode.Uri.joinPath(project.uri, 'some_script.py');
// Get environment for the file
const fileEnv = await api.getEnvironment(fileUri);
// Should inherit project's environment
assert.ok(fileEnv, 'File should get environment from project');
assert.strictEqual(fileEnv.envId.id, env.envId.id, 'File should use project environment');
});
});