Skip to content

Commit b0ebc9b

Browse files
eleanorjboydaidoskanapyanovkarthiknadig
authored
Enable debug pytest (#21228)
fixes #21147 --------- Co-authored-by: Aidos Kanapyanov <[email protected]> Co-authored-by: Karthik Nadig <[email protected]>
1 parent be9662f commit b0ebc9b

File tree

8 files changed

+80
-25
lines changed

8 files changed

+80
-25
lines changed

.vscode/launch.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
// Enable this to log telemetry to the output during debugging
2323
"XVSC_PYTHON_LOG_TELEMETRY": "1",
2424
// Enable this to log debugger output. Directory must exist ahead of time
25-
"XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output_Ex"
25+
"XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output_Ex",
26+
"ENABLE_PYTHON_TESTING_REWRITE": "1"
2627
}
2728
},
2829
{

pythonFiles/vscode_pytest/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,12 @@ def pytest_sessionfinish(session, exitstatus):
179179
4: Pytest encountered an internal error or exception during test execution.
180180
5: Pytest was unable to find any tests to run.
181181
"""
182+
print(
183+
"pytest session has finished, exit status: ",
184+
exitstatus,
185+
"in discovery? ",
186+
IS_DISCOVERY,
187+
)
182188
cwd = pathlib.Path.cwd()
183189
if IS_DISCOVERY:
184190
try:
@@ -209,7 +215,6 @@ def pytest_sessionfinish(session, exitstatus):
209215
f"Pytest exited with error status: {exitstatus}, {ERROR_MESSAGE_CONST[exitstatus]}"
210216
)
211217
exitstatus_bool = "error"
212-
213218
execution_post(
214219
os.fsdecode(cwd),
215220
exitstatus_bool,

src/client/testing/common/debugLauncher.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ITestDebugLauncher, LaunchOptions } from './types';
1515
import { getConfigurationsForWorkspace } from '../../debugger/extension/configuration/launch.json/launchJsonReader';
1616
import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis/workspaceApis';
1717
import { showErrorMessage } from '../../common/vscodeApis/windowApis';
18+
import { createDeferred } from '../../common/utils/async';
1819

1920
@injectable()
2021
export class DebugLauncher implements ITestDebugLauncher {
@@ -42,16 +43,12 @@ export class DebugLauncher implements ITestDebugLauncher {
4243
);
4344
const debugManager = this.serviceContainer.get<IDebugService>(IDebugService);
4445

45-
return debugManager.startDebugging(workspaceFolder, launchArgs).then(
46-
// Wait for debug session to be complete.
47-
() =>
48-
new Promise<void>((resolve) => {
49-
debugManager.onDidTerminateDebugSession(() => {
50-
resolve();
51-
});
52-
}),
53-
(ex) => traceError('Failed to start debugging tests', ex),
54-
);
46+
const deferred = createDeferred<void>();
47+
debugManager.onDidTerminateDebugSession(() => {
48+
deferred.resolve();
49+
});
50+
debugManager.startDebugging(workspaceFolder, launchArgs);
51+
return deferred.promise;
5552
}
5653

5754
private static resolveWorkspaceFolder(cwd: string): WorkspaceFolder {
@@ -181,6 +178,12 @@ export class DebugLauncher implements ITestDebugLauncher {
181178
const args = script(testArgs);
182179
const [program] = args;
183180
configArgs.program = program;
181+
// if the test provider is pytest, then use the pytest module instead of using a program
182+
const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE;
183+
if (options.testProvider === 'pytest' && rewriteTestingEnabled) {
184+
configArgs.module = 'pytest';
185+
configArgs.program = undefined;
186+
}
184187
configArgs.args = args.slice(1);
185188
// We leave configArgs.request as "test" so it will be sent in telemetry.
186189

@@ -201,6 +204,21 @@ export class DebugLauncher implements ITestDebugLauncher {
201204
throw Error(`Invalid debug config "${debugConfig.name}"`);
202205
}
203206
launchArgs.request = 'launch';
207+
if (options.testProvider === 'pytest' && rewriteTestingEnabled) {
208+
if (options.pytestPort && options.pytestUUID) {
209+
launchArgs.env = {
210+
...launchArgs.env,
211+
TEST_PORT: options.pytestPort,
212+
TEST_UUID: options.pytestUUID,
213+
};
214+
} else {
215+
throw Error(
216+
`Missing value for debug setup, both port and uuid need to be defined. port: "${options.pytestPort}" uuid: "${options.pytestUUID}"`,
217+
);
218+
}
219+
const pluginPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles');
220+
launchArgs.env.PYTHONPATH = pluginPath;
221+
}
204222

205223
// Clear out purpose so we can detect if the configuration was used to
206224
// run via F5 style debugging.
@@ -210,13 +228,19 @@ export class DebugLauncher implements ITestDebugLauncher {
210228
}
211229

212230
private static getTestLauncherScript(testProvider: TestProvider) {
231+
const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE;
213232
switch (testProvider) {
214233
case 'unittest': {
234+
if (rewriteTestingEnabled) {
235+
return internalScripts.execution_py_testlauncher; // this is the new way to run unittest execution, debugger
236+
}
215237
return internalScripts.visualstudio_py_testlauncher; // old way unittest execution, debugger
216-
// return internalScripts.execution_py_testlauncher; // this is the new way to run unittest execution, debugger
217238
}
218239
case 'pytest': {
219-
return internalScripts.testlauncher;
240+
if (rewriteTestingEnabled) {
241+
return (testArgs: string[]) => testArgs;
242+
}
243+
return internalScripts.testlauncher; // old way pytest execution, debugger
220244
}
221245
default: {
222246
throw new Error(`Unknown test provider '${testProvider}'`);

src/client/testing/common/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export type LaunchOptions = {
2525
testProvider: TestProvider;
2626
token?: CancellationToken;
2727
outChannel?: OutputChannel;
28+
pytestPort?: string;
29+
pytestUUID?: string;
2830
};
2931

3032
export type ParserOptions = TestDiscoveryOptions;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
Uri,
1313
WorkspaceFolder,
1414
} from 'vscode';
15-
import { TestDiscoveryOptions } from '../../common/types';
15+
import { ITestDebugLauncher, TestDiscoveryOptions } from '../../common/types';
1616
import { IPythonExecutionFactory } from '../../../common/process/types';
1717

1818
export type TestRunInstanceOptions = TestRunOptions & {
@@ -193,6 +193,7 @@ export interface ITestExecutionAdapter {
193193
testIds: string[],
194194
debugBool?: boolean,
195195
executionFactory?: IPythonExecutionFactory,
196+
debugLauncher?: ITestDebugLauncher,
196197
): Promise<ExecutionTestPayload>;
197198
}
198199

src/client/testing/testController/controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
409409
token,
410410
request.profile?.kind === TestRunProfileKind.Debug,
411411
this.pythonExecFactory,
412+
this.debugLauncher,
412413
);
413414
}
414415
return this.pytest.runTests(
@@ -438,6 +439,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
438439
testItems,
439440
token,
440441
request.profile?.kind === TestRunProfileKind.Debug,
442+
this.pythonExecFactory,
441443
);
442444
}
443445
// below is old way of running unittest execution

src/client/testing/testController/pytest/pytestExecutionAdapter.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ import {
1212
IPythonExecutionFactory,
1313
SpawnOptions,
1414
} from '../../../common/process/types';
15-
import { EXTENSION_ROOT_DIR } from '../../../constants';
1615
import { removePositionalFoldersAndFiles } from './arguments';
16+
import { ITestDebugLauncher, LaunchOptions } from '../../common/types';
17+
import { PYTEST_PROVIDER } from '../../common/constants';
18+
import { EXTENSION_ROOT_DIR } from '../../../common/constants';
1719

1820
// eslint-disable-next-line @typescript-eslint/no-explicit-any
19-
(global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR;
21+
// (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR;
2022
/**
2123
* Wrapper Class for pytest test execution. This is where we call `runTestCommand`?
2224
*/
@@ -47,11 +49,12 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter {
4749
testIds: string[],
4850
debugBool?: boolean,
4951
executionFactory?: IPythonExecutionFactory,
52+
debugLauncher?: ITestDebugLauncher,
5053
): Promise<ExecutionTestPayload> {
5154
traceVerbose(uri, testIds, debugBool);
5255
if (executionFactory !== undefined) {
5356
// ** new version of run tests.
54-
return this.runTestsNew(uri, testIds, debugBool, executionFactory);
57+
return this.runTestsNew(uri, testIds, debugBool, executionFactory, debugLauncher);
5558
}
5659
// if executionFactory is undefined, we are using the old method signature of run tests.
5760
this.outputChannel.appendLine('Running tests.');
@@ -64,6 +67,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter {
6467
testIds: string[],
6568
debugBool?: boolean,
6669
executionFactory?: IPythonExecutionFactory,
70+
debugLauncher?: ITestDebugLauncher,
6771
): Promise<ExecutionTestPayload> {
6872
const deferred = createDeferred<ExecutionTestPayload>();
6973
const relativePathToPytest = 'pythonFiles';
@@ -106,16 +110,29 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter {
106110
testArgs.splice(0, 0, '--rootdir', uri.fsPath);
107111
}
108112

113+
// why is this needed?
109114
if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) {
110115
testArgs.push('--capture', 'no');
111116
}
112-
113-
console.debug(`Running test with arguments: ${testArgs.join(' ')}\r\n`);
114-
console.debug(`Current working directory: ${uri.fsPath}\r\n`);
115-
116-
const argArray = ['-m', 'pytest', '-p', 'vscode_pytest'].concat(testArgs).concat(testIds);
117-
console.debug('argArray', argArray);
118-
execService?.exec(argArray, spawnOptions);
117+
const pluginArgs = ['-p', 'vscode_pytest', '-v'].concat(testArgs).concat(testIds);
118+
if (debugBool) {
119+
const pytestPort = this.testServer.getPort().toString();
120+
const pytestUUID = uuid.toString();
121+
const launchOptions: LaunchOptions = {
122+
cwd: uri.fsPath,
123+
args: pluginArgs,
124+
token: spawnOptions.token,
125+
testProvider: PYTEST_PROVIDER,
126+
pytestPort,
127+
pytestUUID,
128+
};
129+
console.debug(`Running debug test with arguments: ${pluginArgs.join(' ')}\r\n`);
130+
await debugLauncher!.launchDebugger(launchOptions);
131+
} else {
132+
const runArgs = ['-m', 'pytest'].concat(pluginArgs);
133+
console.debug(`Running test with arguments: ${runArgs.join(' ')}\r\n`);
134+
execService?.exec(runArgs, spawnOptions);
135+
}
119136
} catch (ex) {
120137
console.debug(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`);
121138
return Promise.reject(ex);

src/client/testing/testController/workspaceTestAdapter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
} from './common/types';
3939
import { fixLogLines } from './common/utils';
4040
import { IPythonExecutionFactory } from '../../common/process/types';
41+
import { ITestDebugLauncher } from '../common/types';
4142

4243
/**
4344
* This class exposes a test-provider-agnostic way of discovering tests.
@@ -77,6 +78,7 @@ export class WorkspaceTestAdapter {
7778
token?: CancellationToken,
7879
debugBool?: boolean,
7980
executionFactory?: IPythonExecutionFactory,
81+
debugLauncher?: ITestDebugLauncher,
8082
): Promise<void> {
8183
if (this.executing) {
8284
return this.executing.promise;
@@ -110,6 +112,7 @@ export class WorkspaceTestAdapter {
110112
testCaseIds,
111113
debugBool,
112114
executionFactory,
115+
debugLauncher,
113116
);
114117
traceVerbose('executionFactory defined');
115118
} else {

0 commit comments

Comments
 (0)