Add python-environment's project support for pytest execution#25772
Add python-environment's project support for pytest execution#25772eleanorjboyd wants to merge 10 commits intomicrosoft:test-project-supportfrom
Conversation
src/client/testing/testController/common/projectTestExecution.ts
Outdated
Show resolved
Hide resolved
src/client/testing/testController/common/projectTestExecution.ts
Outdated
Show resolved
Hide resolved
src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
This PR adds project-aware pytest execution support to the test controller flow by wiring Python Environments “projects” into execution/debugging, and introducing shared helpers + unit tests to validate the new behavior in multi-project workspaces.
Changes:
- Add project-based test execution orchestration (
executeTestsForProjects, grouping test items by owning project, per-project execution). - Pass project context into pytest execution and debugging (env var
PROJECT_ROOT_PATH,LaunchOptions.project, project-derived Python path/session naming). - Add centralized test mocks and new/updated unit tests covering project execution and debug-session lifecycle handling.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/client/testing/testController/common/projectTestExecution.ts | New project-based execution utilities (grouping, project lookup, coverage callback setup). |
| src/client/testing/testController/controller.ts | Routes pytest runs through project-based execution when projects are registered. |
| src/client/testing/testController/pytest/pytestExecutionAdapter.ts | Sets project execution environment (PROJECT_ROOT_PATH) and passes project into debug launch options; uses project environment when available. |
| src/client/testing/testController/unittest/testExecutionAdapter.ts | Updates execution adapter signature and adds logging around new parameters. |
| src/client/testing/common/debugLauncher.ts | Supports concurrent debug sessions via a per-launch marker; uses project name/python path when available. |
| src/client/testing/common/types.ts | Extends LaunchOptions to include optional project metadata. |
| src/test/testing/testController/testMocks.ts | Adds reusable mocks for project adapters, dependencies, and test items. |
| src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts | Adds unit tests validating project env var + debug launch options behavior for pytest. |
| src/test/testing/testController/common/projectTestExecution.unit.test.ts | Adds comprehensive unit tests for grouping, project resolution, execution orchestration, and coverage callback behavior. |
| src/test/testing/common/debugLauncher.unit.test.ts | Updates and adds tests for multi-session debug lifecycle tracking and project-specific config naming. |
| // Check if the item's path starts with the project's path | ||
| if (itemPath.startsWith(projectPath) && projectPath.length > bestMatchLength) { | ||
| bestMatch = project; | ||
| bestMatchLength = projectPath.length; | ||
| } |
There was a problem hiding this comment.
Path matching via startsWith() is not path-boundary safe (e.g. /workspace/proj10/... matches /workspace/proj). Use the existing isParentPath() helper (handles separator + case normalization) or otherwise ensure safe parent/child path checks.
| const testCaseIds: string[] = []; | ||
|
|
||
| // Mark items as started and collect test IDs | ||
| for (const item of testItems) { | ||
| // Recursively get test case nodes if this is a parent node | ||
| const testCaseNodes = getTestCaseNodesRecursive(item); | ||
| for (const node of testCaseNodes) { | ||
| runInstance.started(node); | ||
| const runId = project.resultResolver.vsIdToRunId.get(node.id); | ||
| if (runId) { | ||
| testCaseIds.push(runId); | ||
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
testCaseIds is collected in an array without deduplication. If selected items overlap (e.g. class + method), leaf nodes (and their run IDs) can be included multiple times, resulting in duplicate runInstance.started() calls and duplicate IDs passed to the runner. Consider using a Set like WorkspaceTestAdapter.executeTests() does.
| const testCaseIds: string[] = []; | |
| // Mark items as started and collect test IDs | |
| for (const item of testItems) { | |
| // Recursively get test case nodes if this is a parent node | |
| const testCaseNodes = getTestCaseNodesRecursive(item); | |
| for (const node of testCaseNodes) { | |
| runInstance.started(node); | |
| const runId = project.resultResolver.vsIdToRunId.get(node.id); | |
| if (runId) { | |
| testCaseIds.push(runId); | |
| } | |
| } | |
| } | |
| const processedTestItemIds = new Set<string>(); | |
| const uniqueTestCaseIds = new Set<string>(); | |
| // Mark items as started and collect test IDs | |
| for (const item of testItems) { | |
| // Recursively get test case nodes if this is a parent node | |
| const testCaseNodes = getTestCaseNodesRecursive(item); | |
| for (const node of testCaseNodes) { | |
| if (processedTestItemIds.has(node.id)) { | |
| continue; | |
| } | |
| processedTestItemIds.add(node.id); | |
| runInstance.started(node); | |
| const runId = project.resultResolver.vsIdToRunId.get(node.id); | |
| if (runId) { | |
| uniqueTestCaseIds.add(runId); | |
| } | |
| } | |
| } | |
| const testCaseIds = Array.from(uniqueTestCaseIds); |
| // Unique marker to identify this session among concurrent debug sessions | ||
| const sessionMarker = `test-${Date.now()}-${Math.random().toString(36).substring(7)}`; | ||
| launchArgs[TEST_SESSION_MARKER_KEY] = sessionMarker; |
There was a problem hiding this comment.
The debug session marker uses Math.random().toString(36).substring(7), which can produce an empty/very short suffix and reduces uniqueness (and can make tests flaky). Prefer a stable approach like Math.random().toString(36).slice(2) or crypto.randomUUID() (if available) to ensure a non-empty, sufficiently unique marker.
| console.log( | ||
| 'interpreter, project parameters are currently unused in UnittestTestExecutionAdapter, they will be used in a future implementation of project-based unittest execution.:', | ||
| { | ||
| interpreter, | ||
| project, |
There was a problem hiding this comment.
runTests() now uses console.log() (and logs interpreter/project objects). This is noisy in production and can leak details; also the message says params are unused even though project is used later (e.g. PROJECT_ROOT_PATH). Please remove this console.log and use existing traceVerbose/traceInfo only if needed, with an accurate message.
| // Set PROJECT_ROOT_PATH for project-based testing | ||
| // This tells the Python side where to root the test tree for multi-project workspaces | ||
| if (project) { | ||
| mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; | ||
| traceInfo( | ||
| `[test-by-project] Setting PROJECT_ROOT_PATH=${project.projectUri.fsPath} for ${project.projectName}`, | ||
| ); | ||
| } | ||
|
|
There was a problem hiding this comment.
PROJECT_ROOT_PATH is set again here, but it’s already set earlier in runTestsNew(). Please remove this duplicate block and keep a single source of truth for setting/logging PROJECT_ROOT_PATH to avoid divergence.
| // Set PROJECT_ROOT_PATH for project-based testing | |
| // This tells the Python side where to root the test tree for multi-project workspaces | |
| if (project) { | |
| mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; | |
| traceInfo( | |
| `[test-by-project] Setting PROJECT_ROOT_PATH=${project.projectUri.fsPath} for ${project.projectName}`, | |
| ); | |
| } |
| // Check if we're in project-based mode and should use project-specific execution | ||
| if (this.projectRegistry.hasProjects(workspace.uri) && settings.testing.pytestEnabled) { | ||
| const projects = this.projectRegistry.getProjectsArray(workspace.uri); | ||
| await executeTestsForProjects(projects, testItems, runInstance, request, token, { | ||
| projectRegistry: this.projectRegistry, |
There was a problem hiding this comment.
Project-based execution is gated on pytestEnabled. In project-based mode (useEnvExtension()), legacy WorkspaceTestAdapters are not initialized, so if only unittest is enabled this will fall through and testAdapter can be undefined (runtime error). Consider either supporting project-based execution for unittest too, or initializing legacy adapters in project mode / gating project mode by provider.
| // Setup coverage for this project if needed | ||
| if (request.profile?.kind === TestRunProfileKind.Coverage) { | ||
| setupCoverageForProject(request, project); | ||
| } |
There was a problem hiding this comment.
setupCoverageForProject() mutates request.profile.loadDetailedCoverage. When executing multiple projects, this is called once per project and the last one wins, so coverage lookups will only consult a single project's detailedCoverageMap. Please set a single callback for the whole run that can route by file path to the correct project (or merge maps) rather than overwriting it per project.
No description provided.