Skip to content

Commit c29567b

Browse files
authored
feat: switch to event-driven approach for environment retrieval in integration tests (#1311)
should help with CI flakiness
1 parent 3932262 commit c29567b

File tree

1 file changed

+87
-88
lines changed

1 file changed

+87
-88
lines changed

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)