Skip to content

Commit eea39d2

Browse files
edvilmeCopilot
andcommitted
fix: make integration tests fully event-driven with setEnvironmentAndWait
Add setEnvironmentAndWait() helper that subscribes to onDidChangeEnvironment before calling setEnvironment, then drains the setImmediate event chain (up to 5 event loop ticks) to ensure all async side-effects settle before proceeding. All setEnvironment calls (including teardown) now use this helper, eliminating race conditions where stale events from a previous call leak into subsequent test steps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 63d8baa commit eea39d2

1 file changed

Lines changed: 72 additions & 42 deletions

File tree

src/test/integration/interpreterSelection.integration.test.ts

Lines changed: 72 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,54 @@
2323

2424
import * as assert from 'assert';
2525
import * as vscode from 'vscode';
26-
import { DidChangeEnvironmentEventArgs, PythonEnvironment, PythonEnvironmentApi } from '../../api';
26+
import { DidChangeEnvironmentEventArgs, PythonEnvironment, PythonEnvironmentApi, SetEnvironmentScope } from '../../api';
2727
import { ENVS_EXTENSION_ID } from '../constants';
2828
import { TestEventHandler, waitForCondition } from '../testUtils';
2929

30+
/**
31+
* Calls setEnvironment and waits for the async event chain to fully settle.
32+
*
33+
* setEnvironment fires onDidChangeActiveEnvironment via setImmediate, which
34+
* can trigger refreshEnvironment (another setImmediate), which may fire
35+
* again (a third setImmediate). This helper subscribes to the event BEFORE
36+
* the call and yields enough event loop iterations to drain all pending
37+
* setImmediate callbacks — ensuring no stale events leak into subsequent
38+
* test steps.
39+
*
40+
* This is NOT a timeout — each iteration is a deterministic event loop tick
41+
* that completes in microseconds. For idempotent sets (env already matches),
42+
* no events fire and the loop exits immediately after the ticks drain.
43+
*/
44+
async function setEnvironmentAndWait(
45+
api: PythonEnvironmentApi,
46+
scope: SetEnvironmentScope,
47+
env: PythonEnvironment | undefined,
48+
): Promise<void> {
49+
const handler = new TestEventHandler<DidChangeEnvironmentEventArgs>(
50+
api.onDidChangeEnvironment,
51+
'setEnvironmentAndWait',
52+
);
53+
54+
try {
55+
await api.setEnvironment(scope, env);
56+
57+
// Drain the setImmediate event chain. Each tick processes one
58+
// pending callback. 5 ticks covers the full chain:
59+
// tick 1: _onDidChangeActiveEnvironment.fire()
60+
// tick 2: refreshEnvironment() from manager callback
61+
// tick 3: _onDidChangeActiveEnvironment.fire() from refresh
62+
// ticks 4-5: safety margin for edge cases
63+
for (let tick = 0; tick < 5; tick++) {
64+
await new Promise<void>((resolve) => setImmediate(resolve));
65+
if (handler.fired) {
66+
break;
67+
}
68+
}
69+
} finally {
70+
handler.dispose();
71+
}
72+
}
73+
3074
suite('Integration: Interpreter Selection Priority', function () {
3175
this.timeout(60_000);
3276

@@ -51,14 +95,12 @@ suite('Integration: Interpreter Selection Priority', function () {
5195
originalEnv = await api.getEnvironment(undefined);
5296
});
5397

54-
// Reset to original state after each test to prevent state pollution
98+
// Reset to original state after each test to prevent state pollution.
99+
// Uses setEnvironmentAndWait to ensure all async events drain before
100+
// the next test starts — prevents stale events from leaking.
55101
teardown(async function () {
56102
try {
57-
if (originalEnv) {
58-
await api.setEnvironment(undefined, originalEnv);
59-
} else {
60-
await api.setEnvironment(undefined, undefined);
61-
}
103+
await setEnvironmentAndWait(api, undefined, originalEnv);
62104
} catch {
63105
// Ignore errors during reset
64106
}
@@ -96,11 +138,9 @@ suite('Integration: Interpreter Selection Priority', function () {
96138

97139
const envToSet = environments[0];
98140

99-
// Set environment globally
100-
await api.setEnvironment(undefined, envToSet);
141+
// Set environment and wait for async event chain to settle
142+
await setEnvironmentAndWait(api, undefined, envToSet);
101143

102-
// getEnvironment calls manager.get() directly (no cache),
103-
// so it returns the correct value immediately after set.
104144
const retrieved = await api.getEnvironment(undefined);
105145

106146
assert.ok(retrieved, 'Should have environment after setting');
@@ -130,14 +170,11 @@ suite('Integration: Interpreter Selection Priority', function () {
130170
const projectEnv = environments[1];
131171
const project = projects[0];
132172

133-
// Set global environment
134-
await api.setEnvironment(undefined, globalEnv);
135-
136-
// Set different environment for project
137-
await api.setEnvironment(project.uri, projectEnv);
173+
// Set global environment and wait for event chain to settle
174+
await setEnvironmentAndWait(api, undefined, globalEnv);
138175

139-
// getEnvironment calls manager.get() directly (no cache),
140-
// so it returns the correct value immediately after set.
176+
// Set different environment for project and wait
177+
await setEnvironmentAndWait(api, project.uri, projectEnv);
141178

142179
// Verify global is unchanged
143180
const globalRetrieved = await api.getEnvironment(undefined);
@@ -175,29 +212,22 @@ suite('Integration: Interpreter Selection Priority', function () {
175212
const oldEnv = environments[0];
176213
const newEnv = environments[1];
177214

178-
// Subscribe BEFORE any setEnvironment calls. The handler captures
179-
// all events — from both sets and any async refreshEnvironment
180-
// side-effects. We then search for the specific event we need.
181-
// This eliminates drain races entirely: no matter how many
182-
// setImmediate ticks the event chain takes, the handler sees
183-
// everything and we pick the right event by envId match.
215+
// Set initial environment and wait for all async events to drain.
216+
// This ensures no stale events from this call leak into the handler.
217+
await setEnvironmentAndWait(api, undefined, oldEnv);
218+
219+
// Now subscribe a fresh handler for the second set
184220
const handler = new TestEventHandler<DidChangeEnvironmentEventArgs>(
185221
api.onDidChangeEnvironment,
186222
'onDidChangeEnvironment',
187223
);
188224

189225
try {
190-
// Set initial environment (may or may not fire events)
191-
await api.setEnvironment(undefined, oldEnv);
192-
193226
// Change to new environment — this MUST fire an event since
194-
// oldEnv.envId.id !== newEnv.envId.id, so the _activeSelection
195-
// idempotency check passes.
227+
// oldEnv.envId.id !== newEnv.envId.id
196228
await api.setEnvironment(undefined, newEnv);
197229

198-
// Wait for the specific event whose new env matches newEnv.
199-
// Events from the first set (new=oldEnv) are captured but
200-
// won't match this condition, so they're harmlessly ignored.
230+
// Wait for the event whose new env matches what we set
201231
await waitForCondition(
202232
() => handler.all.some((e) => e.new?.envId.id === newEnv.envId.id),
203233
15_000,
@@ -237,8 +267,8 @@ suite('Integration: Interpreter Selection Priority', function () {
237267
const project = projects[0];
238268
const env = environments[0];
239269

240-
// Set project environment
241-
await api.setEnvironment(project.uri, env);
270+
// Set project environment and wait for event chain to settle
271+
await setEnvironmentAndWait(api, project.uri, env);
242272

243273
// Query for a file inside the project
244274
const fileUri = vscode.Uri.joinPath(project.uri, 'subdir', 'script.py');
@@ -292,11 +322,11 @@ suite('Integration: Interpreter Selection Priority', function () {
292322

293323
const env = environments[0];
294324

295-
// Set environment first time
296-
await api.setEnvironment(undefined, env);
325+
// Set environment first time and wait for event chain to settle
326+
await setEnvironmentAndWait(api, undefined, env);
297327

298-
// Set same environment again
299-
await api.setEnvironment(undefined, env);
328+
// Set same environment again and wait for any events to drain
329+
await setEnvironmentAndWait(api, undefined, env);
300330

301331
// Verify functional idempotency: after setting the same environment
302332
// twice, getEnvironment should still return the same environment.
@@ -327,13 +357,13 @@ suite('Integration: Interpreter Selection Priority', function () {
327357
return;
328358
}
329359

330-
// Set an explicit environment
331-
await api.setEnvironment(undefined, environments[0]);
360+
// Set an explicit environment and wait for event chain to settle
361+
await setEnvironmentAndWait(api, undefined, environments[0]);
332362
const beforeClear = await api.getEnvironment(undefined);
333363
assert.ok(beforeClear, 'Should have environment before clearing');
334364

335-
// Clear the selection
336-
await api.setEnvironment(undefined, undefined);
365+
// Clear the selection and wait for event chain to settle
366+
await setEnvironmentAndWait(api, undefined, undefined);
337367

338368
// Get environment - should return auto-discovered environment
339369
const autoEnv = await api.getEnvironment(undefined);

0 commit comments

Comments
 (0)