-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Add python-environment's project support for pytest execution #25772
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: test-project-support
Are you sure you want to change the base?
Changes from all commits
e797064
9222587
ecc92c4
2cd8bc7
00a9264
51f14be
a0c557d
18208e0
d4767df
84d5f98
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import { inject, injectable, named } from 'inversify'; | ||
| import * as path from 'path'; | ||
| import { DebugConfiguration, l10n, Uri, WorkspaceFolder, DebugSession, DebugSessionOptions } from 'vscode'; | ||
| import { DebugConfiguration, l10n, Uri, WorkspaceFolder, DebugSession, DebugSessionOptions, Disposable } from 'vscode'; | ||
| import { IApplicationShell, IDebugService } from '../../common/application/types'; | ||
| import { EXTENSION_ROOT_DIR } from '../../common/constants'; | ||
| import * as internalScripts from '../../common/process/internal/scripts'; | ||
|
|
@@ -17,6 +17,14 @@ import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis | |
| import { showErrorMessage } from '../../common/vscodeApis/windowApis'; | ||
| import { createDeferred } from '../../common/utils/async'; | ||
| import { addPathToPythonpath } from './helpers'; | ||
| import * as envExtApi from '../../envExt/api.internal'; | ||
|
|
||
| /** | ||
| * Key used to mark debug configurations with a unique session identifier. | ||
| * This allows us to track which debug session belongs to which launchDebugger() call | ||
| * when multiple debug sessions are launched in parallel. | ||
| */ | ||
| const TEST_SESSION_MARKER_KEY = '__vscodeTestSessionMarker'; | ||
|
|
||
| @injectable() | ||
| export class DebugLauncher implements ITestDebugLauncher { | ||
|
|
@@ -31,25 +39,46 @@ export class DebugLauncher implements ITestDebugLauncher { | |
| this.configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); | ||
| } | ||
|
|
||
| /** | ||
| * Launches a debug session for test execution. | ||
| * Handles cancellation, multi-session support via unique markers, and cleanup. | ||
| */ | ||
| public async launchDebugger( | ||
| options: LaunchOptions, | ||
| callback?: () => void, | ||
| sessionOptions?: DebugSessionOptions, | ||
| ): Promise<void> { | ||
| const deferred = createDeferred<void>(); | ||
| let hasCallbackBeenCalled = false; | ||
|
|
||
| // Collect disposables for cleanup when debugging completes | ||
| const disposables: Disposable[] = []; | ||
|
|
||
| // Ensure callback is only invoked once, even if multiple termination paths fire | ||
| const callCallbackOnce = () => { | ||
| if (!hasCallbackBeenCalled) { | ||
| hasCallbackBeenCalled = true; | ||
| callback?.(); | ||
| } | ||
| }; | ||
|
|
||
| // Early exit if already cancelled before we start | ||
| if (options.token && options.token.isCancellationRequested) { | ||
| hasCallbackBeenCalled = true; | ||
| return undefined; | ||
| callCallbackOnce(); | ||
| deferred.resolve(); | ||
| callback?.(); | ||
| return deferred.promise; | ||
| } | ||
|
|
||
| options.token?.onCancellationRequested(() => { | ||
| deferred.resolve(); | ||
| callback?.(); | ||
| hasCallbackBeenCalled = true; | ||
| }); | ||
| // Listen for cancellation from the test run (e.g., user clicks stop in Test Explorer) | ||
| // This allows the caller to clean up resources even if the debug session is still running | ||
| if (options.token) { | ||
| disposables.push( | ||
| options.token.onCancellationRequested(() => { | ||
| deferred.resolve(); | ||
| callCallbackOnce(); | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| const workspaceFolder = DebugLauncher.resolveWorkspaceFolder(options.cwd); | ||
| const launchArgs = await this.getLaunchArgs( | ||
|
|
@@ -59,23 +88,49 @@ export class DebugLauncher implements ITestDebugLauncher { | |
| ); | ||
| const debugManager = this.serviceContainer.get<IDebugService>(IDebugService); | ||
|
|
||
| let activatedDebugSession: DebugSession | undefined; | ||
| debugManager.startDebugging(workspaceFolder, launchArgs, sessionOptions).then(() => { | ||
| // Save the debug session after it is started so we can check if it is the one that was terminated. | ||
| activatedDebugSession = debugManager.activeDebugSession; | ||
| }); | ||
| debugManager.onDidTerminateDebugSession((session) => { | ||
| traceVerbose(`Debug session terminated. sessionId: ${session.id}`); | ||
| // Only resolve no callback has been made and the session is the one that was started. | ||
| if ( | ||
| !hasCallbackBeenCalled && | ||
| activatedDebugSession !== undefined && | ||
| session.id === activatedDebugSession?.id | ||
| ) { | ||
| deferred.resolve(); | ||
| callback?.(); | ||
| } | ||
| // 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; | ||
|
Comment on lines
+91
to
+93
|
||
|
|
||
| let ourSession: DebugSession | undefined; | ||
|
|
||
| // Capture our specific debug session when it starts by matching the marker. | ||
| // This fires for ALL debug sessions, so we filter to only our marker. | ||
| disposables.push( | ||
| debugManager.onDidStartDebugSession((session) => { | ||
| if (session.configuration[TEST_SESSION_MARKER_KEY] === sessionMarker) { | ||
| ourSession = session; | ||
| traceVerbose(`[test-debug] Debug session started: ${session.name} (${session.id})`); | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| // Handle debug session termination (user stops debugging, or tests complete). | ||
| // Only react to OUR session terminating - other parallel sessions should | ||
| // continue running independently. | ||
| disposables.push( | ||
| debugManager.onDidTerminateDebugSession((session) => { | ||
| if (ourSession && session.id === ourSession.id) { | ||
| traceVerbose(`[test-debug] Debug session terminated: ${session.name} (${session.id})`); | ||
| deferred.resolve(); | ||
| callCallbackOnce(); | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| // 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()); | ||
| }); | ||
|
|
||
| return deferred.promise; | ||
| } | ||
|
|
||
|
|
@@ -108,6 +163,12 @@ export class DebugLauncher implements ITestDebugLauncher { | |
| subProcess: true, | ||
| }; | ||
| } | ||
|
|
||
| // Use project name in debug session name if provided | ||
| if (options.project) { | ||
| debugConfig.name = `Debug Tests: ${options.project.name}`; | ||
| } | ||
|
|
||
| if (!debugConfig.rules) { | ||
| debugConfig.rules = []; | ||
| } | ||
|
|
@@ -257,6 +318,23 @@ export class DebugLauncher implements ITestDebugLauncher { | |
| // run via F5 style debugging. | ||
| launchArgs.purpose = []; | ||
|
|
||
| // For project-based execution, get the Python path from the project's environment. | ||
| // Fallback: if env API unavailable or fails, LaunchConfigurationResolver already set | ||
| // launchArgs.python from the active interpreter, so debugging still works. | ||
| if (options.project && envExtApi.useEnvExtension()) { | ||
eleanorjboyd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| try { | ||
| const pythonEnv = await envExtApi.getEnvironment(options.project.uri); | ||
| if (pythonEnv?.execInfo?.run?.executable) { | ||
| launchArgs.python = pythonEnv.execInfo.run.executable; | ||
| traceVerbose( | ||
| `[test-by-project] Debug session using Python path from project: ${launchArgs.python}`, | ||
| ); | ||
| } | ||
| } catch (error) { | ||
| traceVerbose(`[test-by-project] Could not get environment for project, using default: ${error}`); | ||
| } | ||
| } | ||
|
|
||
| return launchArgs; | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.