Skip to content

Commit a0c557d

Browse files
committed
addressed comments
1 parent 51f14be commit a0c557d

File tree

3 files changed

+20
-58
lines changed

3 files changed

+20
-58
lines changed

src/client/testing/common/debugLauncher.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,7 @@ export class DebugLauncher implements ITestDebugLauncher {
4141

4242
/**
4343
* Launches a debug session for test execution.
44-
*
45-
* **Cancellation handling:**
46-
* Cancellation can occur from multiple sources, all properly handled:
47-
* 1. **Pre-check**: If already cancelled before starting, returns immediately
48-
* 2. **Token cancellation**: If the parent CancellationToken fires during debugging,
49-
* the deferred resolves and the callback is invoked to clean up resources
50-
* 3. **Session termination**: When the user stops debugging (via UI or completes),
51-
* the onDidTerminateDebugSession event fires and we resolve
52-
*
53-
* **Multi-session support:**
54-
* When debugging tests from multiple projects simultaneously, each launchDebugger()
55-
* call needs to track its own debug session independently. We use a unique marker
56-
* in the launch configuration to identify which session belongs to which call,
57-
* avoiding race conditions with the global `activeDebugSession` property.
58-
*
59-
* @param options Launch configuration including test provider, args, and optional project info
60-
* @param callback Called when the debug session ends (for cleanup like closing named pipes)
61-
* @param sessionOptions VS Code debug session options (e.g., testRun association)
44+
* Handles cancellation, multi-session support via unique markers, and cleanup.
6245
*/
6346
public async launchDebugger(
6447
options: LaunchOptions,
@@ -105,12 +88,7 @@ export class DebugLauncher implements ITestDebugLauncher {
10588
);
10689
const debugManager = this.serviceContainer.get<IDebugService>(IDebugService);
10790

108-
// Generate a unique marker for this debug session.
109-
// When multiple debug sessions start in parallel (e.g., debugging tests from
110-
// multiple projects), we can't rely on debugManager.activeDebugSession because
111-
// it's a global that could be overwritten by another concurrent session start.
112-
// Instead, we embed a unique marker in our launch configuration and match it
113-
// when the session starts to identify which session is ours.
91+
// Unique marker to identify this session among concurrent debug sessions
11492
const sessionMarker = `test-${Date.now()}-${Math.random().toString(36).substring(7)}`;
11593
launchArgs[TEST_SESSION_MARKER_KEY] = sessionMarker;
11694

@@ -341,7 +319,8 @@ export class DebugLauncher implements ITestDebugLauncher {
341319
launchArgs.purpose = [];
342320

343321
// For project-based execution, get the Python path from the project's environment.
344-
// This ensures debug sessions use the correct interpreter for each project.
322+
// Fallback: if env API unavailable or fails, LaunchConfigurationResolver already set
323+
// launchArgs.python from the active interpreter, so debugging still works.
345324
if (options.project && envExtApi.useEnvExtension()) {
346325
try {
347326
const pythonEnv = await envExtApi.getEnvironment(options.project.uri);

src/client/testing/common/types.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,7 @@ export type LaunchOptions = {
2727
pytestPort?: string;
2828
pytestUUID?: string;
2929
runTestIdsPort?: string;
30-
/**
31-
* Optional Python project for project-based execution.
32-
* When provided, the debug launcher will:
33-
* - Use the project's associated Python environment
34-
* - Name the debug session after the project
35-
*/
30+
/** Optional Python project for project-based execution. */
3631
project?: PythonProject;
3732
};
3833

src/client/testing/testController/common/projectTestExecution.ts

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License.
33

44
import { CancellationToken, FileCoverageDetail, TestItem, TestRun, TestRunProfileKind, TestRunRequest } from 'vscode';
5-
import { traceError, traceInfo, traceVerbose } from '../../../logging';
5+
import { traceError, traceInfo, traceVerbose, traceWarn } from '../../../logging';
66
import { sendTelemetryEvent } from '../../../telemetry';
77
import { EventName } from '../../../telemetry/constants';
88
import { IPythonExecutionFactory } from '../../../common/process/types';
@@ -71,7 +71,7 @@ export async function executeTestsForProjects(
7171
traceInfo(`[test-by-project] Executing ${items.length} test item(s) for project: ${project.projectName}`);
7272

7373
sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, {
74-
tool: 'pytest',
74+
tool: project.testProvider,
7575
debugging: isDebugMode,
7676
});
7777

@@ -100,10 +100,7 @@ export async function executeTestsForProjects(
100100
}
101101

102102
/**
103-
* Lookup context for project resolution during a single test run.
104-
* Maps file paths to their resolved ProjectAdapter to avoid
105-
* repeated API calls and linear searches.
106-
* Created fresh per run and discarded after grouping completes.
103+
* Lookup context for avoiding redundant API calls within a single test run.
107104
*/
108105
interface ProjectLookupContext {
109106
/** Maps file URI fsPath → resolved ProjectAdapter (or undefined if no match) */
@@ -113,15 +110,8 @@ interface ProjectLookupContext {
113110
}
114111

115112
/**
116-
* Groups test items by their owning project using the Python Environment API.
117-
* Each test item's URI is matched to a project via the API's getPythonProject method.
118-
* Falls back to path-based matching when the extension API is not available.
119-
*
120-
* Uses a per-run cache to avoid redundant API calls for test items sharing the same file.
121-
*
122-
* Time complexity: O(n + p) amortized, where n = test items, p = projects
123-
* - Building adapter lookup map: O(p)
124-
* - Each test item: O(1) amortized (cached after first lookup per unique file)
113+
* Groups test items by their owning project. Uses env API when available, else falls back to path matching.
114+
* Time complexity: O(n + p) amortized via per-run caching.
125115
*/
126116
export async function groupTestItemsByProject(
127117
testItems: TestItem[],
@@ -134,7 +124,8 @@ export async function groupTestItemsByProject(
134124
result.set(getProjectId(project.projectUri), { project, items: [] });
135125
}
136126

137-
// Build lookup context for this run - O(p) setup, enables O(1) lookups
127+
// Build lookup context for this run - O(p) one-time setup, enables O(1) lookups per item.
128+
// When tests are from a single project, most lookups hit the cache after the first item.
138129
const lookupContext: ProjectLookupContext = {
139130
uriToAdapter: new Map(),
140131
projectPathToAdapter: new Map(projects.map((p) => [p.projectUri.fsPath, p])),
@@ -150,7 +141,7 @@ export async function groupTestItemsByProject(
150141
}
151142
} else {
152143
// If no project matches, log it
153-
traceVerbose(`[test-by-project] Could not match test item ${item.id} to a project`);
144+
traceWarn(`[test-by-project] Could not match test item ${item.id} to a project`);
154145
}
155146
}
156147

@@ -165,12 +156,7 @@ export async function groupTestItemsByProject(
165156
}
166157

167158
/**
168-
* Finds the project that owns a test item based on the test item's URI.
169-
* Uses the Python Environment extension API when available, falling back
170-
* to path-based matching (longest matching path prefix).
171-
*
172-
* Results are stored in the lookup context to avoid redundant API calls for items in the same file.
173-
* Time complexity: O(1) amortized with context, O(p) worst case on context miss.
159+
* Finds the project that owns a test item. Uses env API when available, else path-based matching.
174160
*/
175161
export async function findProjectForTestItem(
176162
item: TestItem,
@@ -188,7 +174,9 @@ export async function findProjectForTestItem(
188174

189175
let result: ProjectAdapter | undefined;
190176

191-
// Try using the Python Environment extension API first
177+
// Try using the Python Environment extension API first.
178+
// Legacy path: when useEnvExtension() is false, this block is skipped and we go
179+
// directly to findProjectByPath() below (path-based matching).
192180
if (useEnvExtension()) {
193181
try {
194182
const envExtApi = await getEnvExtApi();
@@ -206,7 +194,8 @@ export async function findProjectForTestItem(
206194
}
207195
}
208196

209-
// Fallback: path-based matching (most specific/longest path wins)
197+
// Fallback: path-based matching when env API unavailable or didn't find a match.
198+
// O(p) time complexity where p = number of projects.
210199
if (!result) {
211200
result = findProjectByPath(item, projects);
212201
}
@@ -220,8 +209,7 @@ export async function findProjectForTestItem(
220209
}
221210

222211
/**
223-
* Finds the project that owns a test item using path-based matching.
224-
* Returns the most specific (longest path) matching project.
212+
* Fallback: finds project using path-based matching. O(p) time complexity.
225213
*/
226214
function findProjectByPath(item: TestItem, projects: ProjectAdapter[]): ProjectAdapter | undefined {
227215
if (!item.uri) return undefined;

0 commit comments

Comments
 (0)