-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Add support for Projects in Testing #25780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
## Summary This PR implements **project-based test discovery** for pytest, enabling multi-project workspace support. When the Python Environments API is available, the extension now discovers Python projects within workspaces and creates separate test tree roots for each project with its own Python environment. ## What's New ### Project-Based Testing Architecture - **TestProjectRegistry**: Manages the lifecycle of Python test projects, including: - Discovering projects via Python Environments API - Creating ProjectAdapter instances per workspace - Computing nested project relationships for ignore lists - Fallback to "legacy" single-adapter mode when API unavailable - **ProjectAdapter**: Interface representing a single Python project with test infrastructure: - Project identity (ID, name, URI) - Python environment from the environments API - Test framework adapters (discovery/execution) - Nested project ignore paths ### Key Features - ✅ **Multi-project workspaces**: Each Python project gets its own test tree root - ✅ **Nested project handling**: Parent projects automatically ignore nested child projects via `--ignore` flags - ✅ **Graceful fallback**: Falls back to legacy single-adapter mode if Python Environments API is unavailable - ✅ **Project root path**: Python-side `get_test_root_path()` function returns appropriate root for test tree ### Code Improvements - Standardized logging prefixes to `[test-by-project]` across all files - Centralized adapter creation via `createTestAdapters()` helper method - Extracted reusable methods for discovery, execution, and file watching ## Scope & Limitations > **⚠️ Important: unittest is NOT supported in this PR** > > This PR focuses exclusively on **pytest**. unittest support for project-based testing will be implemented in a future PR. ## Testing - Added unit tests for `TestProjectRegistry` class - Added unit tests for Python-side `get_test_root_path()` function - Manual testing with multi-project workspaces ## Related Issues first step in: microsoft/vscode-python-environments#987 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This adds a place within the github workspace that developers can store any AI related artifacts they create that could be useful but they don't want to commit. Things like issue summarization, plans for features, code analysis etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds project-based (multi-project) testing support to the VS Code Python extension’s Test Explorer integration, leveraging the Python Environments API so each project can be discovered/run/debugged with its own environment (fixes microsoft/vscode-python-environments#987).
Changes:
- Introduces a project registry + project adapter model to discover/register multiple Python projects per workspace and run discovery/execution per project.
- Scopes VS Code test IDs by project (via a project ID separator) and threads project context through discovery/execution adapters (including
PROJECT_ROOT_PATHsupport in Python runner scripts). - Adds extensive unit tests (TS + Python) covering project-based discovery/execution, nested project behavior, and debug session handling.
Reviewed changes
Copilot reviewed 36 out of 36 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/client/testing/testController/controller.ts | Adds project-based activation/discovery/execution flow and project-change handling. |
| src/client/testing/testController/common/testProjectRegistry.ts | New registry for discovering projects, creating project-specific adapters/resolvers, and nested ignore computation. |
| src/client/testing/testController/common/projectAdapter.ts | Defines per-project adapter shape (env + adapters + resolver + lifecycle). |
| src/client/testing/testController/common/projectUtils.ts | Adds project ID scoping utilities + shared adapter creation helper. |
| src/client/testing/testController/common/projectTestExecution.ts | Executes selected tests grouped by owning project with project-specific environment/debug/coverage behavior. |
| src/client/testing/testController/common/utils.ts | Adds project-scoped IDs in populateTestTree() and project-aware error labels. |
| src/client/testing/testController/common/testDiscoveryHandler.ts | Routes discovery into project-scoped IDs and project-scoped error nodes. |
| src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts | Threads ProjectAdapter through discovery; adds nested --ignore support; sets PROJECT_ROOT_PATH. |
| src/client/testing/testController/pytest/pytestExecutionAdapter.ts | Threads ProjectAdapter through execution; uses project env when available; sets PROJECT_ROOT_PATH. |
| src/client/testing/testController/unittest/testDiscoveryAdapter.ts | Threads ProjectAdapter through discovery; uses project env when available; sets PROJECT_ROOT_PATH. |
| src/client/testing/testController/unittest/testExecutionAdapter.ts | Threads ProjectAdapter through execution/debug; uses project env when available; sets PROJECT_ROOT_PATH. |
| src/client/testing/testController/workspaceTestAdapter.ts | Allows passing a ProjectAdapter to execution for project-scoped runs. |
| src/client/testing/common/debugLauncher.ts | Adds multi-session tracking via markers; supports project-based debug naming + Python path resolution. |
| python_files/vscode_pytest/init.py | Uses PROJECT_ROOT_PATH to root discovery/execution payload cwd/test-tree root for project mode. |
| python_files/unittestadapter/discovery.py | Supports PROJECT_ROOT_PATH to override cwd/top-level behavior for project-rooted discovery. |
| python_files/unittestadapter/execution.py | Supports PROJECT_ROOT_PATH to override cwd in execution payloads. |
| src/test/vscode-mock.ts | Adds vscode.tests.createTestController() mock for unit tests. |
| src/test/testing/testController/** | Adds unit tests for project registry, project execution grouping, controller behavior, adapters, and utilities. |
| python_files/tests/** | Adds Python-side tests for PROJECT_ROOT_PATH behavior for both unittest + pytest. |
| .github/instructions/testing_feature_area.instructions.md | Documents the new project-based testing architecture and key files/tests. |
src/client/testing/testController/common/testProjectRegistry.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 36 out of 36 changed files in this pull request and generated 3 comments.
| let node = testController.items.get(child.path); | ||
|
|
||
| if (!node) { | ||
| node = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); | ||
| // Create project-scoped ID for non-test nodes | ||
| const nodeId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${child.id_}` : child.id_; | ||
| node = testController.createTestItem(nodeId, child.name, Uri.file(child.path)); |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In project-scoped mode, folder/non-test nodes are looked up via testController.items.get(child.path), but they are created with an ID that includes the project scope (${projectId}${PROJECT_ID_SEPARATOR}...). This lookup will never find existing nodes (and can also accidentally pick up an unrelated root item whose ID happens to equal child.path in multi-root/nested workspaces). Prefer looking up by the same scoped ID you create (and ideally against testRoot.children, not the global testController.items) before deciding to create a new node.
See below for a potential fix:
// Use project-scoped ID for non-test nodes and look up within the current root
const nodeId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${child.id_}` : child.id_;
let node = testRoot!.children.get(nodeId);
if (!node) {
| testProvider, | ||
| workspaceUri, | ||
| projectId, | ||
| pythonProject.name, // Use simple project name for test tree label (without version) |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ProjectAdapter.projectName is computed as a display name including the Python version (createProjectDisplayName(...)), but PythonResultResolver is initialized with pythonProject.name (without version). This makes the Test Explorer root label / discovery error labels / debug session naming (via project.pythonProject) inconsistent with the adapter’s display name and can make it hard to distinguish projects that share a name but differ by interpreter. Consider passing the computed projectDisplayName into PythonResultResolver (and/or using it when building the LaunchOptions.project object) so UI labels are consistent and disambiguated.
| pythonProject.name, // Use simple project name for test tree label (without version) | |
| projectDisplayName, // Use display name (including version) for test tree label |
| // Track completion for progress logging | ||
| const projectsCompleted = new Set<string>(); | ||
|
|
||
| // Run discovery for all projects in parallel | ||
| await Promise.all(projects.map((project) => this.discoverTestsForProject(project, projectsCompleted))); | ||
|
|
||
| traceInfo( | ||
| `[test-by-project] Discovery complete: ${projectsCompleted.size}/${projects.length} projects succeeded`, | ||
| ); |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
projectsCompleted is incremented for both successful and failed project discoveries (see catch block in discoverTestsForProject()), but the summary log says "projects succeeded". This makes the completion log inaccurate; either track successes separately or change the message to reflect "completed"/"attempted" rather than "succeeded".
fixes microsoft/vscode-python-environments#987