Skip to content

Commit 5ebedcd

Browse files
DonJayamanneAman Agarwal
authored andcommitted
Add support to use experimental debugger when debugging python unit tests (#1046)
* ✨ unit test debugging using experimental debugger * 🐛 add injectable attribute * 🔨 separate test launcher for experimental debugger * 🔨 update links * 📝 change log * Fixes #906
1 parent a568f92 commit 5ebedcd

16 files changed

Lines changed: 328 additions & 32 deletions

File tree

news/1 Enhancements/906.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for expermental debugger when debugging Python Unit Tests.

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,12 @@
13971397
"description": "Pattern used to exclude files and folders from ctags See http://ctags.sourceforge.net/ctags.html.",
13981398
"scope": "resource"
13991399
},
1400+
"python.unitTest.useExperimentalDebugger": {
1401+
"type": "boolean",
1402+
"default": false,
1403+
"description": "Use the experimental debugger when debugging unit tests.",
1404+
"scope": "resource"
1405+
},
14001406
"python.unitTest.promptToConfigure": {
14011407
"type": "boolean",
14021408
"default": true,

pythonFiles/experimental/ptvsd_launcher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
traceback.print_exc()
8181
print('''
8282
Internal error detected. Please copy the above traceback and report at
83-
https://go.microsoft.com/fwlink/?LinkId=293415
83+
https://github.com/Microsoft/vscode-python/issues/new
8484
8585
Press Enter to close. . .''')
8686
try:
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import os
5+
import sys
6+
7+
8+
def parse_argv():
9+
"""Parses arguments for use with the test launcher.
10+
Arguments are:
11+
1. Working directory.
12+
2. Test runner, `pytest` or `nose`
13+
3. Rest of the arguments are passed into the test runner.
14+
"""
15+
16+
return (sys.argv[1], sys.argv[2], sys.argv[3:])
17+
18+
19+
def exclude_current_file_from_debugger():
20+
# Load the debugger package
21+
try:
22+
import ptvsd
23+
import ptvsd.debugger as vspd
24+
vspd.DONT_DEBUG.append(os.path.normcase(__file__))
25+
except:
26+
traceback.print_exc()
27+
print('''
28+
Internal error detected. Please copy the above traceback and report at
29+
https://github.com/Microsoft/vscode-python/issues/new
30+
31+
Press Enter to close. . .''')
32+
try:
33+
raw_input()
34+
except NameError:
35+
input()
36+
sys.exit(1)
37+
38+
39+
def run(cwd, testRunner, args):
40+
"""Runs the test
41+
cwd -- the current directory to be set
42+
testRuner -- test runner to be used `pytest` or `nose`
43+
args -- arguments passed into the test runner
44+
"""
45+
46+
sys.path[0] = os.getcwd()
47+
os.chdir(cwd)
48+
49+
try:
50+
if testRunner == 'pytest':
51+
import pytest
52+
pytest.main(args)
53+
else:
54+
import nose
55+
nose.run(argv=args)
56+
sys.exit(0)
57+
finally:
58+
pass
59+
60+
61+
if __name__ == '__main__':
62+
exclude_current_file_from_debugger()
63+
cwd, testRunner, args = parse_argv()
64+
run(cwd, testRunner, args)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { injectable } from 'inversify';
7+
import { debug, DebugConfiguration, WorkspaceFolder } from 'vscode';
8+
import { IDebugService } from './types';
9+
10+
@injectable()
11+
export class DebugService implements IDebugService {
12+
public startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration): Thenable<boolean> {
13+
return debug.startDebugging(folder, nameOrConfiguration);
14+
}
15+
}

src/client/common/application/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,12 @@ export interface IWorkspaceService {
434434
* An event that is emitted when the [configuration](#WorkspaceConfiguration) changed.
435435
*/
436436
readonly onDidChangeConfiguration: Event<ConfigurationChangeEvent>;
437+
/**
438+
* Whether a workspace folder exists
439+
* @type {boolean}
440+
* @memberof IWorkspaceService
441+
*/
442+
readonly hasWorkspaceFolders: boolean;
437443

438444
/**
439445
* Returns the [workspace folder](#WorkspaceFolder) that contains a given uri.
@@ -524,3 +530,19 @@ export interface ITerminalManager {
524530
*/
525531
createTerminal(options: TerminalOptions): Terminal;
526532
}
533+
534+
export const IDebugService = Symbol('IDebugManager');
535+
536+
export interface IDebugService {
537+
/**
538+
* Start debugging by using either a named launch or named compound configuration,
539+
* or by directly passing a [DebugConfiguration](#DebugConfiguration).
540+
* The named configurations are looked up in '.vscode/launch.json' found in the given folder.
541+
* Before debugging starts, all unsaved files are saved and the launch configurations are brought up-to-date.
542+
* Folder specific variables used in the configuration (e.g. '${workspaceFolder}') are resolved against the given folder.
543+
* @param folder The [workspace folder](#WorkspaceFolder) for looking up named configurations and resolving variables or `undefined` for a non-folder setup.
544+
* @param nameOrConfiguration Either the name of a debug or compound configuration or a [DebugConfiguration](#DebugConfiguration) object.
545+
* @return A thenable that resolves when debugging could be successfully started.
546+
*/
547+
startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | vscode.DebugConfiguration): Thenable<boolean>;
548+
}

src/client/common/application/workspace.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export class WorkspaceService implements IWorkspaceService {
2020
public get onDidChangeWorkspaceFolders(): vscode.Event<vscode.WorkspaceFoldersChangeEvent> {
2121
return vscode.workspace.onDidChangeWorkspaceFolders;
2222
}
23+
public get hasWorkspaceFolders() {
24+
return Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0;
25+
}
2326
public getConfiguration(section?: string, resource?: vscode.Uri): vscode.WorkspaceConfiguration {
2427
return vscode.workspace.getConfiguration(section, resource);
2528
}

src/client/common/serviceRegistry.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import { IServiceManager } from '../ioc/types';
55
import { ApplicationShell } from './application/applicationShell';
66
import { CommandManager } from './application/commandManager';
7+
import { DebugService } from './application/debugService';
78
import { DocumentManager } from './application/documentManager';
89
import { TerminalManager } from './application/terminalManager';
9-
import { IApplicationShell, ICommandManager, IDocumentManager, ITerminalManager, IWorkspaceService } from './application/types';
10+
import { IApplicationShell, ICommandManager, IDebugService, IDocumentManager, ITerminalManager, IWorkspaceService } from './application/types';
1011
import { WorkspaceService } from './application/workspace';
1112
import { ConfigurationService } from './configuration/service';
1213
import { ProductInstaller } from './installer/productInstaller';
@@ -38,6 +39,7 @@ export function registerTypes(serviceManager: IServiceManager) {
3839
serviceManager.addSingleton<IWorkspaceService>(IWorkspaceService, WorkspaceService);
3940
serviceManager.addSingleton<IDocumentManager>(IDocumentManager, DocumentManager);
4041
serviceManager.addSingleton<ITerminalManager>(ITerminalManager, TerminalManager);
42+
serviceManager.addSingleton<IDebugService>(IDebugService, DebugService);
4143

4244
serviceManager.addSingleton<ITerminalHelper>(ITerminalHelper, TerminalHelper);
4345
serviceManager.addSingleton<ITerminalActivationCommandProvider>(ITerminalActivationCommandProvider, Bash, 'bashCShellFish');

src/client/common/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export interface IUnitTestSettings {
131131
readonly unittestEnabled: boolean;
132132
unittestArgs: string[];
133133
cwd?: string;
134+
readonly useExperimentalDebugger?: boolean;
134135
}
135136
export interface IPylintCategorySeverity {
136137
readonly convention: DiagnosticSeverity;
Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,72 @@
1-
import { injectable } from 'inversify';
2-
import { debug, Uri, workspace } from 'vscode';
3-
import { ITestDebugLauncher, launchOptions } from './types';
1+
import { inject, injectable } from 'inversify';
2+
import * as path from 'path';
3+
import { Uri } from 'vscode';
4+
import { IDebugService, IWorkspaceService } from '../../common/application/types';
5+
import { EXTENSION_ROOT_DIR } from '../../common/constants';
6+
import { IConfigurationService } from '../../common/types';
7+
import { IServiceContainer } from '../../ioc/types';
8+
import { ITestDebugLauncher, LaunchOptions, TestProvider } from './types';
49

510
@injectable()
611
export class DebugLauncher implements ITestDebugLauncher {
7-
public async launchDebugger(options: launchOptions) {
12+
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { }
13+
public async launchDebugger(options: LaunchOptions) {
814
if (options.token && options.token!.isCancellationRequested) {
915
return;
1016
}
1117
const cwdUri = options.cwd ? Uri.file(options.cwd) : undefined;
12-
13-
if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) {
18+
const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService);
19+
if (!workspaceService.hasWorkspaceFolders) {
1420
throw new Error('Please open a workspace');
1521
}
16-
let workspaceFolder = workspace.getWorkspaceFolder(cwdUri!);
22+
let workspaceFolder = workspaceService.getWorkspaceFolder(cwdUri!);
1723
if (!workspaceFolder) {
18-
workspaceFolder = workspace.workspaceFolders[0];
24+
workspaceFolder = workspaceService.workspaceFolders![0];
1925
}
20-
const args = options.args.slice();
21-
const program = args.shift();
22-
return debug.startDebugging(workspaceFolder, {
26+
27+
const cwd = cwdUri ? cwdUri.fsPath : workspaceFolder.uri.fsPath;
28+
const configurationService = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(Uri.file(cwd));
29+
const useExperimentalDebugger = configurationService.unitTest.useExperimentalDebugger === true;
30+
const debugManager = this.serviceContainer.get<IDebugService>(IDebugService);
31+
const debuggerType = useExperimentalDebugger ? 'pythonExperimental' : 'python';
32+
const debugArgs = this.fixArgs(options.args, options.testProvider, useExperimentalDebugger);
33+
const program = this.getTestLauncherScript(options.testProvider, useExperimentalDebugger);
34+
35+
return debugManager.startDebugging(workspaceFolder, {
2336
name: 'Debug Unit Test',
24-
type: 'python',
37+
type: debuggerType,
2538
request: 'launch',
2639
program,
27-
cwd: cwdUri ? cwdUri.fsPath : workspaceFolder.uri.fsPath,
28-
args,
40+
cwd,
41+
args: debugArgs,
2942
console: 'none',
3043
debugOptions: ['RedirectOutput']
3144
}).then(() => void (0));
3245
}
46+
private fixArgs(args: string[], testProvider: TestProvider, useExperimentalDebugger: boolean): string[] {
47+
if (testProvider === 'unittest' && useExperimentalDebugger) {
48+
return args.filter(item => item !== '--debug');
49+
} else {
50+
return args;
51+
}
52+
}
53+
private getTestLauncherScript(testProvider: TestProvider, useExperimentalDebugger: boolean) {
54+
switch (testProvider) {
55+
case 'unittest': {
56+
return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'PythonTools', 'visualstudio_py_testlauncher.py');
57+
}
58+
case 'pytest':
59+
case 'nosetest': {
60+
if (useExperimentalDebugger) {
61+
return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'testlauncher.py');
62+
} else {
63+
return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'PythonTools', 'testlauncher.py');
64+
}
65+
66+
}
67+
default: {
68+
throw new Error(`Unknown test provider '${testProvider}'`);
69+
}
70+
}
71+
}
3372
}

0 commit comments

Comments
 (0)