Skip to content

Commit 24d8613

Browse files
Copiloteleanorjboyd
andcommitted
Use environment extension API for project detection
- Removed custom projectRootFinder.ts utility - Use environment extension's getPythonProjects() API - Query projects from env ext when available - Fall back to workspace as single project if env ext not available - Maintain per-project adapter architecture for discovery and execution Co-authored-by: eleanorjboyd <[email protected]>
1 parent b497a41 commit 24d8613

3 files changed

Lines changed: 61 additions & 388 deletions

File tree

src/client/testing/testController/common/projectRootFinder.ts

Lines changed: 0 additions & 137 deletions
This file was deleted.

src/client/testing/testController/controller.ts

Lines changed: 61 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -52,26 +52,27 @@ import { ITestDebugLauncher } from '../common/types';
5252
import { PythonResultResolver } from './common/resultResolver';
5353
import { onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis';
5454
import { IEnvironmentVariablesProvider } from '../../common/variables/types';
55-
import { findProjectRoots, ProjectRoot, isFileInProject } from './common/projectRootFinder';
55+
import { useEnvExtension, getEnvExtApi } from '../../envExt/api.internal';
56+
import { PythonProject } from '../../envExt/types';
5657

5758
// Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types.
5859
type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER];
5960
type TriggerKeyType = keyof EventPropertyType;
6061
type TriggerType = EventPropertyType[TriggerKeyType];
6162

6263
/**
63-
* Represents a project-specific test adapter with its project root information
64+
* Represents a project-specific test adapter with its project information
6465
*/
6566
interface ProjectTestAdapter {
6667
adapter: WorkspaceTestAdapter;
67-
projectRoot: ProjectRoot;
68+
project: PythonProject;
6869
}
6970

7071
@injectable()
7172
export class PythonTestController implements ITestController, IExtensionSingleActivationService {
7273
public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false };
7374

74-
// Map of workspace URI -> array of project-specific test adapters
75+
// Map of workspace URI string -> array of project-specific test adapters
7576
private readonly testAdapters: Map<string, ProjectTestAdapter[]> = new Map();
7677

7778
private readonly triggerTypes: TriggerType[] = [];
@@ -186,22 +187,15 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
186187
}
187188

188189
/**
189-
* Creates a test adapter for a specific project root within a workspace.
190-
* @param _workspaceUri The workspace folder URI (for future use)
191-
* @param projectRoot The project root to create adapter for
190+
* Creates a test adapter for a specific project within a workspace.
191+
* @param project The project to create adapter for
192192
* @param testProvider The test provider (pytest or unittest)
193193
* @returns A ProjectTestAdapter instance
194194
*/
195-
private createProjectAdapter(
196-
_workspaceUri: Uri,
197-
projectRoot: ProjectRoot,
198-
testProvider: TestProvider,
199-
): ProjectTestAdapter {
200-
traceVerbose(
201-
`Creating ${testProvider} adapter for project at ${projectRoot.uri.fsPath} (marker: ${projectRoot.markerFile})`,
202-
);
195+
private createProjectAdapter(project: PythonProject, testProvider: TestProvider): ProjectTestAdapter {
196+
traceVerbose(`Creating ${testProvider} adapter for project at ${project.uri.fsPath}`);
203197

204-
const resultResolver = new PythonResultResolver(this.testController, testProvider, projectRoot.uri);
198+
const resultResolver = new PythonResultResolver(this.testController, testProvider, project.uri);
205199

206200
let discoveryAdapter: ITestDiscoveryAdapter;
207201
let executionAdapter: ITestExecutionAdapter;
@@ -234,13 +228,13 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
234228
testProvider,
235229
discoveryAdapter,
236230
executionAdapter,
237-
projectRoot.uri, // Use project root URI instead of workspace URI
231+
project.uri, // Use project URI instead of workspace URI
238232
resultResolver,
239233
);
240234

241235
return {
242236
adapter: workspaceTestAdapter,
243-
projectRoot,
237+
project,
244238
};
245239
}
246240

@@ -339,32 +333,59 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
339333

340334
/**
341335
* Discovers tests for a specific test provider (pytest or unittest).
342-
* Detects all project roots within the workspace and runs discovery for each project.
336+
* Detects all projects within the workspace and runs discovery for each project.
343337
*/
344338
private async discoverTestsForProvider(workspaceUri: Uri, expectedProvider: TestProvider): Promise<void> {
345339
const workspaceKey = workspaceUri.toString();
346340

347-
// Step 1: Detect all project roots in the workspace
348-
traceVerbose(`Detecting ${expectedProvider} projects in workspace: ${workspaceUri.fsPath}`);
349-
const projectRoots = await findProjectRoots(workspaceUri, expectedProvider);
350-
traceVerbose(`Found ${projectRoots.length} project(s) in workspace ${workspaceUri.fsPath}`);
341+
// Step 1: Get Python projects from the environment extension
342+
let projects: PythonProject[] = [];
343+
344+
if (useEnvExtension()) {
345+
try {
346+
const envExtApi = await getEnvExtApi();
347+
const allProjects = envExtApi.getPythonProjects();
348+
349+
// Filter projects that belong to this workspace
350+
projects = allProjects.filter((project) => {
351+
const projectWorkspace = this.workspaceService.getWorkspaceFolder(project.uri);
352+
return projectWorkspace?.uri.toString() === workspaceKey;
353+
});
354+
355+
traceVerbose(`Found ${projects.length} Python project(s) in workspace ${workspaceUri.fsPath}`);
356+
} catch (error) {
357+
traceError(`Error getting projects from environment extension: ${error}`);
358+
// Fall back to using workspace as single project
359+
projects = [];
360+
}
361+
}
362+
363+
// If no projects found or env extension not available, treat workspace as single project
364+
if (projects.length === 0) {
365+
traceVerbose(`No projects detected, using workspace root as single project: ${workspaceUri.fsPath}`);
366+
projects = [{
367+
name: this.workspaceService.getWorkspaceFolder(workspaceUri)?.name || 'workspace',
368+
uri: workspaceUri,
369+
}];
370+
}
351371

352372
// Step 2: Create or reuse adapters for each project
353373
const existingAdapters = this.testAdapters.get(workspaceKey) || [];
354374
const newAdapters: ProjectTestAdapter[] = [];
355375

356-
for (const projectRoot of projectRoots) {
376+
for (const project of projects) {
357377
// Check if we already have an adapter for this project
358378
const existingAdapter = existingAdapters.find(
359-
(a) => a.projectRoot.uri.fsPath === projectRoot.uri.fsPath && a.adapter.getTestProvider() === expectedProvider,
379+
(a) => a.project.uri.toString() === project.uri.toString() &&
380+
a.adapter.getTestProvider() === expectedProvider,
360381
);
361382

362383
if (existingAdapter) {
363-
traceVerbose(`Reusing existing adapter for project at ${projectRoot.uri.fsPath}`);
384+
traceVerbose(`Reusing existing adapter for project at ${project.uri.fsPath}`);
364385
newAdapters.push(existingAdapter);
365386
} else {
366387
// Create new adapter for this project
367-
const projectAdapter = this.createProjectAdapter(workspaceUri, projectRoot, expectedProvider);
388+
const projectAdapter = this.createProjectAdapter(project, expectedProvider);
368389
newAdapters.push(projectAdapter);
369390
}
370391
}
@@ -375,10 +396,8 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
375396
// Step 3: Run discovery for each project
376397
const interpreter = await this.interpreterService.getActiveInterpreter(workspaceUri);
377398
await Promise.all(
378-
newAdapters.map(async ({ adapter, projectRoot }) => {
379-
traceVerbose(
380-
`Running ${expectedProvider} discovery for project at ${projectRoot.uri.fsPath}`,
381-
);
399+
newAdapters.map(async ({ adapter, project }) => {
400+
traceVerbose(`Running ${expectedProvider} discovery for project at ${project.uri.fsPath}`);
382401
try {
383402
await adapter.discoverTests(
384403
this.testController,
@@ -387,13 +406,10 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
387406
interpreter,
388407
);
389408
} catch (error) {
390-
traceError(
391-
`Error during ${expectedProvider} discovery for project at ${projectRoot.uri.fsPath}:`,
392-
error,
393-
);
409+
traceError(`Error during ${expectedProvider} discovery for project at ${project.uri.fsPath}:`, error);
394410
this.surfaceErrorNode(
395-
projectRoot.uri,
396-
`Error discovering tests in project at ${projectRoot.uri.fsPath}: ${error}`,
411+
project.uri,
412+
`Error discovering tests in project at ${project.uri.fsPath}: ${error}`,
397413
expectedProvider,
398414
);
399415
}
@@ -506,6 +522,9 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
506522
return Array.from(this.workspaceService.workspaceFolders || []);
507523
}
508524

525+
/**
526+
* Runs tests for a single workspace.
527+
*/
509528
/**
510529
* Runs tests for a single workspace.
511530
* Groups tests by project and executes them using the appropriate project adapter.
@@ -548,16 +567,17 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
548567
const testItemsByProject = new Map<string, TestItem[]>();
549568
for (const testItem of testItems) {
550569
// Find which project this test belongs to
551-
const projectAdapter = projectAdapters.find(({ projectRoot }) => {
570+
const projectAdapter = projectAdapters.find(({ project }) => {
552571
const testPath = testItem.uri?.fsPath;
553572
if (!testPath) {
554573
return false;
555574
}
556-
return isFileInProject(testPath, projectRoot);
575+
// Check if test path is within project path
576+
return testPath === project.uri.fsPath || testPath.startsWith(project.uri.fsPath + '/');
557577
});
558578

559579
if (projectAdapter) {
560-
const projectKey = projectAdapter.projectRoot.uri.fsPath;
580+
const projectKey = projectAdapter.project.uri.toString();
561581
if (!testItemsByProject.has(projectKey)) {
562582
testItemsByProject.set(projectKey, []);
563583
}
@@ -572,7 +592,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
572592
await Promise.all(
573593
Array.from(testItemsByProject.entries()).map(async ([projectPath, projectTestItems]) => {
574594
const projectAdapter = projectAdapters.find(
575-
({ projectRoot }) => projectRoot.uri.fsPath === projectPath,
595+
({ project }) => project.uri.toString() === projectPath,
576596
);
577597

578598
if (!projectAdapter) {

0 commit comments

Comments
 (0)