Skip to content

Conversation

@eleanorjboyd
Copy link
Member

@eleanorjboyd eleanorjboyd commented Feb 9, 2026

eleanorjboyd and others added 6 commits February 5, 2026 21:37
## 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
@eleanorjboyd eleanorjboyd added the feature-request Request for new features or functionality label Feb 9, 2026
@eleanorjboyd eleanorjboyd self-assigned this Feb 9, 2026
@eleanorjboyd eleanorjboyd requested a review from Copilot February 9, 2026 21:54
@eleanorjboyd eleanorjboyd marked this pull request as ready for review February 9, 2026 21:54
@vs-code-engineering vs-code-engineering bot added this to the February 2026 milestone Feb 9, 2026
Copy link

Copilot AI left a 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_PATH support 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.

Comment on lines +121 to +133
// Start the debug session
const started = await debugManager.startDebugging(workspaceFolder, launchArgs, sessionOptions);
if (!started) {
traceError('Failed to start debug session');
deferred.resolve();
callCallbackOnce();
}

// Clean up event subscriptions when debugging completes (success, failure, or cancellation)
deferred.promise.finally(() => {
disposables.forEach((d) => d.dispose());
});

Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

launchDebugger() awaits debugManager.startDebugging(...) without a try/catch. If startDebugging rejects/throws, this method will throw and never resolve deferred, and the event subscriptions in disposables won’t be disposed (potential leak + hung callers). Wrap the startDebugging await in a try/catch, resolve/reject the deferred appropriately, and ensure disposables are always cleaned up (e.g., in a finally block).

Suggested change
// Start the debug session
const started = await debugManager.startDebugging(workspaceFolder, launchArgs, sessionOptions);
if (!started) {
traceError('Failed to start debug session');
deferred.resolve();
callCallbackOnce();
}
// Clean up event subscriptions when debugging completes (success, failure, or cancellation)
deferred.promise.finally(() => {
disposables.forEach((d) => d.dispose());
});
// Clean up event subscriptions when debugging completes (success, failure, or cancellation)
deferred.promise.finally(() => {
disposables.forEach((d) => d.dispose());
});
// Start the debug session
let started = false;
try {
started = await debugManager.startDebugging(workspaceFolder, launchArgs, sessionOptions);
} catch (error) {
traceError('Error starting debug session', error);
deferred.reject(error);
callCallbackOnce();
return deferred.promise;
}
if (!started) {
traceError('Failed to start debug session');
deferred.resolve();
callCallbackOnce();
}

Copilot uses AI. Check for mistakes.
const parentPath = parent.projectUri.fsPath;
const childPath = child.projectUri.fsPath;

if (childPath.startsWith(parentPath + path.sep)) {
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested project detection uses childPath.startsWith(parentPath + path.sep), which is not case-normalized and can mis-detect relationships on Windows (and can be tripped up by path normalization differences). Since this file already imports isParentPath, use that helper (and keep the self-check) to compute nested projects reliably across platforms.

Suggested change
if (childPath.startsWith(parentPath + path.sep)) {
if (isParentPath(parentPath, childPath)) {

Copilot uses AI. Check for mistakes.
Comment on lines +309 to +314
// Clear registry and re-discover all projects for the workspace
this.projectRegistry.clearWorkspace(workspace.uri);
await this.projectRegistry.discoverAndRegisterProjects(workspace.uri);

// Re-run test discovery for the workspace to populate fresh test items
await this.discoverTestsInWorkspace(workspace.uri);
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleProjectChanges() clears and re-discovers projects via projectRegistry.discoverAndRegisterProjects(...), then calls discoverTestsInWorkspace(), which (in project mode) calls discoverAllProjectsInWorkspace() and re-discovers projects again (it clears + re-registers internally). This duplicates work and can cause unnecessary churn (extra API calls + extra test item cleanup). Consider letting discoverAllProjectsInWorkspace() be the single place that clears/re-registers, and in handleProjectChanges() just do the minimal cleanup + call discoverAllProjectsInWorkspace(workspace.uri) directly (or skip the pre-registration step).

Suggested change
// Clear registry and re-discover all projects for the workspace
this.projectRegistry.clearWorkspace(workspace.uri);
await this.projectRegistry.discoverAndRegisterProjects(workspace.uri);
// Re-run test discovery for the workspace to populate fresh test items
await this.discoverTestsInWorkspace(workspace.uri);
// Re-discover all projects and tests for the workspace in a single pass.
// discoverAllProjectsInWorkspace is responsible for clearing/re-registering
// projects and performing test discovery for the workspace.
await this.discoverAllProjectsInWorkspace(workspace.uri);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature-request Request for new features or functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

VSCode Python test discovery not activating the environment

1 participant