Skip to content

Commit 080efaf

Browse files
authored
Merge branch 'main' into copilot/fix-env-file-notification-bug
2 parents 6500790 + c29567b commit 080efaf

6 files changed

Lines changed: 549 additions & 111 deletions

File tree

src/extension.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,12 +447,40 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
447447
),
448448
commands.registerCommand('python-envs.reportIssue', async () => {
449449
try {
450+
// Prompt for issue title
451+
const rawTitle = await window.showInputBox({
452+
title: l10n.t('Report Issue - Title'),
453+
prompt: l10n.t('Enter a brief title for the issue'),
454+
placeHolder: l10n.t('e.g., Environment not detected, activation fails, etc.'),
455+
ignoreFocusOut: true,
456+
});
457+
const title = rawTitle?.trim();
458+
459+
if (!title) {
460+
// User cancelled or provided empty title
461+
return;
462+
}
463+
464+
// Prompt for issue description
465+
const rawDescription = await window.showInputBox({
466+
title: l10n.t('Report Issue - Description'),
467+
prompt: l10n.t('Describe the issue in more detail'),
468+
placeHolder: l10n.t('Provide additional context about what happened...'),
469+
ignoreFocusOut: true,
470+
});
471+
const description = rawDescription?.trim();
472+
473+
if (!description) {
474+
// User cancelled or provided empty description
475+
return;
476+
}
477+
450478
const issueData = await collectEnvironmentInfo(context, envManagers, projectManager);
451479

452480
await commands.executeCommand('workbench.action.openIssueReporter', {
453481
extensionId: 'ms-python.vscode-python-envs',
454-
issueTitle: '[Python Environments] ',
455-
issueBody: `<!-- Please describe the issue you're experiencing -->\n\n<!-- The following information was automatically generated -->\n\n<details>\n<summary>Environment Information</summary>\n\n\`\`\`\n${issueData}\n\`\`\`\n\n</details>`,
482+
issueTitle: `[Python Environments] ${title}`,
483+
issueBody: `## Description\n${description}\n\n## Steps to Reproduce\n1. \n2. \n3. \n\n## Expected Behavior\n\n\n## Actual Behavior\n\n\n<!-- The following information was automatically generated -->\n\n<details>\n<summary>Environment Information</summary>\n\n\`\`\`\n${issueData}\n\`\`\`\n\n</details>`,
456484
});
457485
} catch (error) {
458486
window.showErrorMessage(`Failed to open issue reporter: ${error}`);

src/managers/builtin/venvUtils.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,23 +61,32 @@ export async function clearVenvCache(): Promise<void> {
6161
}
6262

6363
export async function getVenvForWorkspace(fsPath: string): Promise<string | undefined> {
64-
if (process.env.VIRTUAL_ENV) {
65-
return process.env.VIRTUAL_ENV;
66-
}
67-
68-
const state = await getWorkspacePersistentState();
69-
const data: { [key: string]: string } | undefined = await state.get(VENV_WORKSPACE_KEY);
70-
if (data) {
71-
try {
64+
// Check persisted user selection first — this should always take priority
65+
// over process.env.VIRTUAL_ENV so that explicit selections survive reload.
66+
try {
67+
const state = await getWorkspacePersistentState();
68+
const data: { [key: string]: string } | undefined = await state.get(VENV_WORKSPACE_KEY);
69+
if (data) {
7270
const envPath = data[fsPath];
73-
if (await fsapi.pathExists(envPath)) {
71+
if (envPath && (await fsapi.pathExists(envPath))) {
7472
return envPath;
7573
}
76-
setVenvForWorkspace(fsPath, undefined);
77-
} catch {
78-
return undefined;
74+
if (envPath) {
75+
await setVenvForWorkspace(fsPath, undefined);
76+
}
77+
}
78+
} catch {
79+
// fall through to VIRTUAL_ENV check
80+
}
81+
82+
// Fall back to VIRTUAL_ENV only if it points to a path inside this workspace.
83+
if (process.env.VIRTUAL_ENV) {
84+
const rel = path.relative(fsPath, process.env.VIRTUAL_ENV);
85+
if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) {
86+
return process.env.VIRTUAL_ENV;
7987
}
8088
}
89+
8190
return undefined;
8291
}
8392

src/managers/conda/condaUtils.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,21 @@ export function getCondaPathSetting(): string | undefined {
8282
}
8383

8484
export async function getCondaForWorkspace(fsPath: string): Promise<string | undefined> {
85+
// Check persisted user selection first so explicit choices survive restarts
86+
const state = await getWorkspacePersistentState();
87+
const data = await state.get<unknown>(CONDA_WORKSPACE_KEY);
88+
if (data && typeof data === 'object') {
89+
const workspaceSelections = data as { [key: string]: string };
90+
if (Object.prototype.hasOwnProperty.call(workspaceSelections, fsPath)) {
91+
return workspaceSelections[fsPath];
92+
}
93+
}
94+
95+
// Fall back to CONDA_PREFIX only when no explicit selection exists
8596
if (process.env.CONDA_PREFIX) {
8697
return process.env.CONDA_PREFIX;
8798
}
8899

89-
const state = await getWorkspacePersistentState();
90-
const data: { [key: string]: string } | undefined = await state.get(CONDA_WORKSPACE_KEY);
91-
if (data) {
92-
try {
93-
return data[fsPath];
94-
} catch {
95-
return undefined;
96-
}
97-
}
98100
return undefined;
99101
}
100102

src/test/integration/pythonProjects.integration.test.ts

Lines changed: 87 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,47 @@ import { PythonEnvironment, PythonEnvironmentApi } from '../../api';
2323
import { ENVS_EXTENSION_ID } from '../constants';
2424
import { TestEventHandler, waitForCondition } from '../testUtils';
2525

26+
const ENV_CHANGE_TIMEOUT_MS = 15_000;
27+
28+
function getDifferentEnvironment(
29+
environments: PythonEnvironment[],
30+
currentEnv: PythonEnvironment | undefined,
31+
): PythonEnvironment | undefined {
32+
return environments.find((env) => env.envId.id !== currentEnv?.envId.id);
33+
}
34+
35+
async function setEnvironmentAndWaitForChange(
36+
api: PythonEnvironmentApi,
37+
projectUri: vscode.Uri,
38+
env: PythonEnvironment,
39+
timeoutMs = ENV_CHANGE_TIMEOUT_MS,
40+
): Promise<void> {
41+
await new Promise<void>((resolve, reject) => {
42+
let subscription: vscode.Disposable | undefined;
43+
const timeout = setTimeout(() => {
44+
subscription?.dispose();
45+
reject(
46+
new Error(`onDidChangeEnvironment did not fire within ${timeoutMs}ms. Expected envId: ${env.envId.id}`),
47+
);
48+
}, timeoutMs);
49+
50+
subscription = api.onDidChangeEnvironment((e) => {
51+
if (e.uri?.toString() === projectUri.toString() && e.new?.envId.id === env.envId.id) {
52+
clearTimeout(timeout);
53+
subscription?.dispose();
54+
resolve();
55+
}
56+
});
57+
58+
// Set environment after subscribing so we don't miss the event.
59+
api.setEnvironment(projectUri, env).catch((err) => {
60+
clearTimeout(timeout);
61+
subscription?.dispose();
62+
reject(err);
63+
});
64+
});
65+
}
66+
2667
suite('Integration: Python Projects', function () {
2768
this.timeout(60_000);
2869

@@ -162,54 +203,29 @@ suite('Integration: Python Projects', function () {
162203

163204
const environments = await api.getEnvironments('all');
164205

165-
if (environments.length === 0) {
206+
if (environments.length < 2) {
166207
this.skip();
167208
return;
168209
}
169210

170211
const project = projects[0];
171-
const env = environments[0];
172-
173-
// Diagnostic logging for CI debugging
174-
console.log(`[TEST DEBUG] Project URI: ${project.uri.fsPath}`);
175-
console.log(`[TEST DEBUG] Setting environment with envId: ${env.envId.id}`);
176-
console.log(`[TEST DEBUG] Environment path: ${env.environmentPath?.fsPath}`);
177-
console.log(`[TEST DEBUG] Total environments available: ${environments.length}`);
178-
environments.forEach((e, i) => {
179-
console.log(`[TEST DEBUG] env[${i}]: ${e.envId.id} (${e.displayName})`);
180-
});
181212

182-
// Set environment for project
183-
await api.setEnvironment(project.uri, env);
184-
185-
// Track what getEnvironment returns during polling for diagnostics
186-
let pollCount = 0;
187-
let lastRetrievedId: string | undefined;
188-
let lastRetrievedManagerId: string | undefined;
189-
190-
// Wait for the environment to be retrievable with the correct ID
191-
// This handles async persistence across platforms
192-
// Use 15s timeout - CI runners (especially macos) can be slow with settings persistence
193-
await waitForCondition(
194-
async () => {
195-
const retrieved = await api.getEnvironment(project.uri);
196-
pollCount++;
197-
const retrievedId = retrieved?.envId?.id;
198-
lastRetrievedManagerId = retrieved?.envId?.managerId;
199-
if (retrievedId !== lastRetrievedId) {
200-
console.log(
201-
`[TEST DEBUG] Poll #${pollCount}: getEnvironment returned envId=${retrievedId ?? 'undefined'}, managerId=${lastRetrievedManagerId ?? 'undefined'}`,
202-
);
203-
lastRetrievedId = retrievedId;
204-
}
205-
return retrieved !== undefined && retrieved.envId.id === env.envId.id;
206-
},
207-
15_000,
208-
() =>
209-
`Environment was not set correctly. Expected envId: ${env.envId.id} (manager: ${env.envId.managerId}), last retrieved: ${lastRetrievedId ?? 'undefined'} (manager: ${lastRetrievedManagerId ?? 'undefined'}) after ${pollCount} polls`,
210-
);
211-
212-
// Final verification
213+
// Pick an environment different from the current one so setEnvironment
214+
// actually triggers a change event. If all candidates map to the same env,
215+
// skip instead of hanging on an event that will never fire.
216+
const currentEnv = await api.getEnvironment(project.uri);
217+
const env = getDifferentEnvironment(environments, currentEnv);
218+
if (!env) {
219+
this.skip();
220+
return;
221+
}
222+
223+
// Using an event-driven approach instead of polling avoids a race condition where
224+
// setEnvironment's async settings write hasn't landed by the time getEnvironment
225+
// reads back the manager from settings.
226+
await setEnvironmentAndWaitForChange(api, project.uri, env);
227+
228+
// Verify getEnvironment returns the correct value now that setEnvironment has fully completed
213229
const retrievedEnv = await api.getEnvironment(project.uri);
214230
assert.ok(retrievedEnv, 'Should get environment after setting');
215231
assert.strictEqual(retrievedEnv.envId.id, env.envId.id, 'Retrieved environment should match set environment');
@@ -232,13 +248,12 @@ suite('Integration: Python Projects', function () {
232248

233249
const project = projects[0];
234250

235-
// Get current environment to pick a different one
251+
// Pick an environment different from the current one so a change event is guaranteed.
236252
const currentEnv = await api.getEnvironment(project.uri);
237-
238-
// Pick an environment different from current
239-
let targetEnv = environments[0];
240-
if (currentEnv && currentEnv.envId.id === targetEnv.envId.id) {
241-
targetEnv = environments[1];
253+
const targetEnv = getDifferentEnvironment(environments, currentEnv);
254+
if (!targetEnv) {
255+
this.skip();
256+
return;
242257
}
243258

244259
// Register handler BEFORE making the change
@@ -275,30 +290,23 @@ suite('Integration: Python Projects', function () {
275290
const projects = api.getPythonProjects();
276291
const environments = await api.getEnvironments('all');
277292

278-
if (projects.length === 0 || environments.length === 0) {
293+
if (projects.length === 0 || environments.length < 2) {
279294
this.skip();
280295
return;
281296
}
282297

283298
const project = projects[0];
284-
const env = environments[0];
285-
286-
// Set environment first
287-
await api.setEnvironment(project.uri, env);
288-
289-
// Wait for it to be set
290-
// Use 15s timeout - CI runners can be slow with settings persistence
291-
let clearTestLastId: string | undefined;
292-
await waitForCondition(
293-
async () => {
294-
const retrieved = await api.getEnvironment(project.uri);
295-
clearTestLastId = retrieved?.envId?.id;
296-
return retrieved !== undefined && retrieved.envId.id === env.envId.id;
297-
},
298-
15_000,
299-
() =>
300-
`Environment was not set before clearing. Expected: ${env.envId.id} (manager: ${env.envId.managerId}), got: ${clearTestLastId ?? 'undefined'}`,
301-
);
299+
300+
// Pick an environment different from the current one to guarantee a change event
301+
const currentEnv = await api.getEnvironment(project.uri);
302+
const env = getDifferentEnvironment(environments, currentEnv);
303+
if (!env) {
304+
this.skip();
305+
return;
306+
}
307+
308+
// Set environment first, using event-driven wait.
309+
await setEnvironmentAndWaitForChange(api, project.uri, env);
302310

303311
// Verify it was set
304312
const beforeClear = await api.getEnvironment(project.uri);
@@ -337,32 +345,23 @@ suite('Integration: Python Projects', function () {
337345
const projects = api.getPythonProjects();
338346
const environments = await api.getEnvironments('all');
339347

340-
if (projects.length === 0 || environments.length === 0) {
348+
if (projects.length === 0 || environments.length < 2) {
341349
this.skip();
342350
return;
343351
}
344352

345353
const project = projects[0];
346-
const env = environments[0];
347-
348-
// Set environment for project
349-
await api.setEnvironment(project.uri, env);
350-
351-
// Wait for it to be set
352-
// Use 15s timeout - CI runners can be slow with settings persistence
353-
let fileTestLastId: string | undefined;
354-
let fileTestLastManagerId: string | undefined;
355-
await waitForCondition(
356-
async () => {
357-
const retrieved = await api.getEnvironment(project.uri);
358-
fileTestLastId = retrieved?.envId?.id;
359-
fileTestLastManagerId = retrieved?.envId?.managerId;
360-
return retrieved !== undefined && retrieved.envId.id === env.envId.id;
361-
},
362-
15_000,
363-
() =>
364-
`Environment was not set for project. Expected: ${env.envId.id} (manager: ${env.envId.managerId}), got: ${fileTestLastId ?? 'undefined'} (manager: ${fileTestLastManagerId ?? 'undefined'})`,
365-
);
354+
355+
// Pick an environment different from the current one to guarantee a change event
356+
const currentEnv = await api.getEnvironment(project.uri);
357+
const env = getDifferentEnvironment(environments, currentEnv);
358+
if (!env) {
359+
this.skip();
360+
return;
361+
}
362+
363+
// Set environment for project, using event-driven wait.
364+
await setEnvironmentAndWaitForChange(api, project.uri, env);
366365

367366
// Create a hypothetical file path inside the project
368367
const fileUri = vscode.Uri.joinPath(project.uri, 'some_script.py');

0 commit comments

Comments
 (0)