@@ -52,26 +52,27 @@ import { ITestDebugLauncher } from '../common/types';
5252import { PythonResultResolver } from './common/resultResolver' ;
5353import { onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis' ;
5454import { 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.
5859type EventPropertyType = IEventNamePropertyMapping [ EventName . UNITTEST_DISCOVERY_TRIGGER ] ;
5960type TriggerKeyType = keyof EventPropertyType ;
6061type 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 */
6566interface ProjectTestAdapter {
6667 adapter : WorkspaceTestAdapter ;
67- projectRoot : ProjectRoot ;
68+ project : PythonProject ;
6869}
6970
7071@injectable ( )
7172export 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