Skip to content

Add python-environment's project support for pytest execution#25772

Open
eleanorjboyd wants to merge 10 commits intomicrosoft:test-project-supportfrom
eleanorjboyd:instant-eagle-pytest-execution
Open

Add python-environment's project support for pytest execution#25772
eleanorjboyd wants to merge 10 commits intomicrosoft:test-project-supportfrom
eleanorjboyd:instant-eagle-pytest-execution

Conversation

@eleanorjboyd
Copy link
Member

No description provided.

@eleanorjboyd eleanorjboyd self-assigned this Feb 6, 2026
@eleanorjboyd eleanorjboyd added feature-request Request for new features or functionality skip tests Updates to tests unnecessary skip-issue-check labels Feb 6, 2026
@eleanorjboyd eleanorjboyd marked this pull request as ready for review February 6, 2026 23:59
@eleanorjboyd eleanorjboyd requested a review from Copilot February 7, 2026 00:02
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-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.

Comment on lines +223 to +227
// Check if the item's path starts with the project's path
if (itemPath.startsWith(projectPath) && projectPath.length > bestMatchLength) {
bestMatch = project;
bestMatchLength = projectPath.length;
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +243 to +257
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);
}
}
}

Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +93
// 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;
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +60
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,
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +128
// 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}`,
);
}

Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
// 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}`,
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +788 to +792
// 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,
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +81
// Setup coverage for this project if needed
if (request.profile?.kind === TestRunProfileKind.Coverage) {
setupCoverageForProject(request, project);
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

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.

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 skip tests Updates to tests unnecessary skip-issue-check

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant