Skip to content

Commit 33939f1

Browse files
committed
Fix pytest discovery using the workspace-root interpreter after missed startup environment events
Subscribe to Python Environments extension project/environment changes before the initial test project discovery, and queue re-discovery for workspaces whose initial project registration is still in flight when an environment is assigned. Previously, environment assignments raised while initial discovery was running were missed entirely, leaving the workspace stuck on the fallback default project that discovers tests with the workspace-root (e.g. system) interpreter instead of the project's environment. Fixes #25718
1 parent 712d8a7 commit 33939f1

2 files changed

Lines changed: 66 additions & 8 deletions

File tree

‎src/client/testing/testController/controller.ts‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,16 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
230230
if (useEnvExtension()) {
231231
traceInfo('[test-by-project] Activating project-based testing mode');
232232

233+
// Subscribe to project and environment changes BEFORE the initial project discovery.
234+
// Acquiring the environments API during discovery activates the environments extension
235+
// and kicks off its initial refresh; project registrations and environment assignments
236+
// raised before these subscriptions are attached would be missed entirely. A missed
237+
// environment assignment leaves the workspace stuck on the fallback default project,
238+
// which discovers tests with the workspace-root interpreter instead of the
239+
// project-specific environment (e.g. a monorepo sub-project venv).
240+
await this.subscribeToProjectChanges();
241+
await this.subscribeToEnvironmentChanges();
242+
233243
// Discover projects in parallel across all workspaces
234244
// Promise.allSettled ensures one workspace failure doesn't block others
235245
const results = await Promise.allSettled(
@@ -254,11 +264,6 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
254264
this.activateLegacyWorkspace(workspace);
255265
}
256266
});
257-
// Subscribe to project changes to update test tree when projects are added/removed
258-
await this.subscribeToProjectChanges();
259-
// Subscribe to environment changes so projects that had no resolved environment at
260-
// activation get discovered once the environments extension assigns one.
261-
await this.subscribeToEnvironmentChanges();
262267
return;
263268
}
264269

@@ -340,8 +345,13 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
340345

341346
let queued = false;
342347
for (const workspace of affected) {
343-
// Only workspaces already in project-based mode can be re-discovered this way.
344-
if (this.projectRegistry.hasProjects(workspace.uri)) {
348+
// Workspaces in legacy mode (a legacy adapter was registered for them) are not
349+
// re-discovered this way. All other workspaces are: both workspaces with registered
350+
// projects and workspaces whose initial project registration is still in flight.
351+
// The latter ensures environment assignments raised during startup discovery (before
352+
// the workspace had any projects registered) still trigger re-discovery once the
353+
// environments extension resolves them.
354+
if (this.projectRegistry.hasProjects(workspace.uri) || !this.testAdapters.has(workspace.uri)) {
345355
this.pendingEnvChangeWorkspaces.set(workspace.uri.toString(), workspace);
346356
queued = true;
347357
}

‎src/test/testing/testController/controller.unit.test.ts‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,8 +409,11 @@ suite('PythonTestController', () => {
409409
assert.strictEqual(triggerStub.notCalled, true);
410410
});
411411

412-
test('ignores workspaces that are not in project-based mode', () => {
412+
test('ignores workspaces in legacy mode (legacy adapter registered)', () => {
413413
const { controller, triggerStub } = setupController(false);
414+
// A registered legacy adapter marks the workspace as legacy mode, so environment
415+
// changes must not queue a project-based re-discovery for it.
416+
(controller as any).testAdapters.set(workspaceUri, {});
414417

415418
(controller as any).handleEnvironmentChange({ uri: workspaceUri, old: undefined, new: newEnv });
416419

@@ -419,6 +422,21 @@ suite('PythonTestController', () => {
419422
assert.strictEqual(triggerStub.notCalled, true);
420423
});
421424

425+
test('queues workspaces whose initial project registration is still in flight', () => {
426+
// Startup race: the environments extension assigns an environment while the initial
427+
// project discovery for the workspace is still running. No projects are registered yet
428+
// (hasProjects === false) and no legacy adapter exists. The assignment must still queue
429+
// a re-discovery, otherwise the workspace stays on the fallback default project (using
430+
// the workspace-root interpreter) until a manual refresh.
431+
const { controller, triggerStub } = setupController(false);
432+
433+
(controller as any).handleEnvironmentChange({ uri: workspaceUri, old: undefined, new: newEnv });
434+
435+
const pending = (controller as any).pendingEnvChangeWorkspaces as Map<string, vscode.WorkspaceFolder>;
436+
assert.strictEqual(pending.has(workspaceUri.toString()), true);
437+
assert.strictEqual(triggerStub.calledOnce, true);
438+
});
439+
422440
test('re-discovers each pending workspace and clears the queue', async () => {
423441
const { controller, rediscoverStub } = setupController(true);
424442

@@ -430,5 +448,35 @@ suite('PythonTestController', () => {
430448
assert.strictEqual(rediscoverStub.calledOnceWithExactly(workspaceUri), true);
431449
assert.strictEqual(pending.size, 0);
432450
});
451+
452+
test('activate() subscribes to project and environment changes before initial project discovery', async () => {
453+
// Subscribing after the initial discovery misses project registrations and environment
454+
// assignments raised by the environments extension while discovery is running (its
455+
// initial refresh is kicked off by acquiring its API during discovery).
456+
sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true);
457+
sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({
458+
getPythonProjects: () => [],
459+
getEnvironment: sandbox.stub().resolves(undefined),
460+
onDidChangePythonProjects: sandbox.stub().returns({ dispose: () => undefined }),
461+
onDidChangeEnvironment: sandbox.stub().returns({ dispose: () => undefined }),
462+
} as any);
463+
464+
const controller = createController({
465+
workspaceService: ({
466+
workspaceFolders: [workspaceFolder],
467+
getWorkspaceFolder: () => workspaceFolder,
468+
} as unknown) as any,
469+
});
470+
471+
const subscribeProjectsSpy = sandbox.spy(controller as any, 'subscribeToProjectChanges');
472+
const subscribeEnvSpy = sandbox.spy(controller as any, 'subscribeToEnvironmentChanges');
473+
const discoverSpy = sandbox.spy((controller as any).projectRegistry, 'discoverAndRegisterProjects');
474+
475+
await controller.activate();
476+
477+
assert.strictEqual(discoverSpy.calledOnceWithExactly(workspaceUri), true);
478+
assert.strictEqual(subscribeProjectsSpy.calledBefore(discoverSpy), true);
479+
assert.strictEqual(subscribeEnvSpy.calledBefore(discoverSpy), true);
480+
});
433481
});
434482
});

0 commit comments

Comments
 (0)