Skip to content

Commit ece0479

Browse files
authored
Debugging unit tests without having to attach to the debugger (#990)
Fixes #983
1 parent ae7a342 commit ece0479

8 files changed

Lines changed: 48 additions & 131 deletions

File tree

news/3 Code Health/983.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Launch the unit tests in debug mode as opposed to running and attaching the debugger.

pythonFiles/PythonTools/testlauncher.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,15 @@ def main():
22
import os
33
import sys
44
from ptvsd.visualstudio_py_debugger import DONT_DEBUG, DEBUG_ENTRYPOINTS, get_code
5-
from ptvsd.attach_server import DEFAULT_PORT, enable_attach, wait_for_attach
65

76
sys.path[0] = os.getcwd()
87
os.chdir(sys.argv[1])
9-
secret = sys.argv[2]
10-
port = int(sys.argv[3])
11-
testFx = sys.argv[4]
12-
args = sys.argv[5:]
8+
testFx = sys.argv[2]
9+
args = sys.argv[3:]
1310

1411
DONT_DEBUG.append(os.path.normcase(__file__))
1512
DEBUG_ENTRYPOINTS.add(get_code(main))
1613

17-
enable_attach(secret, ('127.0.0.1', port), redirect_output = False)
18-
sys.stdout.flush()
19-
print('READY')
20-
sys.stdout.flush()
21-
wait_for_attach()
22-
2314
try:
2415
if testFx == 'pytest':
2516
import pytest
@@ -32,4 +23,4 @@ def main():
3223
pass
3324

3425
if __name__ == '__main__':
35-
main()
26+
main()

pythonFiles/PythonTools/visualstudio_py_testlauncher.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,7 @@ def main():
199199
global _channel
200200

201201
parser = OptionParser(prog = 'visualstudio_py_testlauncher', usage = 'Usage: %prog [<option>] <test names>... ')
202-
parser.add_option('-s', '--secret', metavar='<secret>', help='restrict server to only allow clients that specify <secret> when connecting')
203-
parser.add_option('-p', '--port', type='int', metavar='<port>', help='listen for debugger connections on <port>')
202+
parser.add_option('--debug', action='store_true', help='Whether debugging the unit tests')
204203
parser.add_option('-x', '--mixed-mode', action='store_true', help='wait for mixed-mode debugger to attach')
205204
parser.add_option('-t', '--test', type='str', dest='tests', action='append', help='specifies a test to run')
206205
parser.add_option('--testFile', type='str', help='Fully qualitified path to file name')
@@ -214,9 +213,8 @@ def main():
214213
parser.add_option('--uc', '--catch', type='str', help='Catch control-C and display results')
215214
(opts, _) = parser.parse_args()
216215

217-
if opts.secret and opts.port:
216+
if opts.debug:
218217
from ptvsd.visualstudio_py_debugger import DONT_DEBUG, DEBUG_ENTRYPOINTS, get_code
219-
from ptvsd.attach_server import DEFAULT_PORT, enable_attach, wait_for_attach
220218

221219
sys.path[0] = os.getcwd()
222220
if opts.result_port:
@@ -231,15 +229,11 @@ def main():
231229
sys.stdout = _TestOutput(sys.stdout, is_stdout = True)
232230
sys.stderr = _TestOutput(sys.stderr, is_stdout = False)
233231

234-
if opts.secret and opts.port:
232+
if opts.debug:
235233
DONT_DEBUG.append(os.path.normcase(__file__))
236234
DEBUG_ENTRYPOINTS.add(get_code(main))
237235

238-
enable_attach(opts.secret, ('127.0.0.1', getattr(opts, 'port', DEFAULT_PORT)), redirect_output = True)
239-
sys.stdout.flush()
240-
print('READY')
241-
sys.stdout.flush()
242-
wait_for_attach()
236+
pass
243237
elif opts.mixed_mode:
244238
# For mixed-mode attach, there's no ptvsd and hence no wait_for_attach(),
245239
# so we have to use Win32 API in a loop to do the same thing.
@@ -340,4 +334,4 @@ def main():
340334
pass
341335

342336
if __name__ == '__main__':
343-
main()
337+
main()
Lines changed: 23 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,33 @@
1-
import * as getFreePort from 'get-port';
2-
import { inject, injectable } from 'inversify';
3-
import * as os from 'os';
1+
import { injectable } from 'inversify';
42
import { debug, Uri, workspace } from 'vscode';
5-
import { PythonSettings } from '../../common/configSettings';
6-
import { IPythonExecutionFactory } from '../../common/process/types';
7-
import { createDeferred } from './../../common/helpers';
83
import { ITestDebugLauncher, launchOptions } from './types';
94

10-
const HAND_SHAKE = `READY${os.EOL}`;
11-
125
@injectable()
136
export class DebugLauncher implements ITestDebugLauncher {
14-
constructor( @inject(IPythonExecutionFactory) private pythonExecutionFactory: IPythonExecutionFactory) { }
15-
public async getLaunchOptions(resource?: Uri): Promise<{ port: number, host: string }> {
16-
const pythonSettings = PythonSettings.getInstance(resource);
17-
const port = await getFreePort({ host: 'localhost', port: pythonSettings.unitTest.debugPort });
18-
const host = typeof pythonSettings.unitTest.debugHost === 'string' && pythonSettings.unitTest.debugHost.trim().length > 0 ? pythonSettings.unitTest.debugHost.trim() : 'localhost';
19-
return { port, host };
20-
}
217
public async launchDebugger(options: launchOptions) {
8+
if (options.token && options.token!.isCancellationRequested) {
9+
return;
10+
}
2211
const cwdUri = options.cwd ? Uri.file(options.cwd) : undefined;
23-
return this.pythonExecutionFactory.create(cwdUri)
24-
.then(executionService => {
25-
// tslint:disable-next-line:no-any
26-
const def = createDeferred<void>();
27-
// tslint:disable-next-line:no-any
28-
const launchDef = createDeferred<void>();
29-
30-
let outputChannelShown = false;
31-
let accumulatedData: string = '';
32-
const result = executionService.execObservable(options.args, { cwd: options.cwd, mergeStdOutErr: true, token: options.token });
33-
result.out.subscribe(output => {
34-
let data = output.out;
35-
if (!launchDef.resolved) {
36-
accumulatedData += output.out;
37-
if (!accumulatedData.startsWith(HAND_SHAKE)) {
38-
return;
39-
}
40-
// Socket server has started, lets start the vs debugger.
41-
launchDef.resolve();
42-
data = accumulatedData.substring(HAND_SHAKE.length);
43-
}
44-
45-
if (!outputChannelShown) {
46-
outputChannelShown = true;
47-
options.outChannel!.show();
48-
}
49-
options.outChannel!.append(data);
50-
}, error => {
51-
if (!def.completed) {
52-
def.reject(error);
53-
}
54-
}, () => {
55-
// Complete only when the process has completed.
56-
if (!def.completed) {
57-
def.resolve();
58-
}
59-
});
60-
61-
launchDef.promise
62-
.then(() => {
63-
if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) {
64-
throw new Error('Please open a workspace');
65-
}
66-
let workspaceFolder = workspace.getWorkspaceFolder(cwdUri!);
67-
if (!workspaceFolder) {
68-
workspaceFolder = workspace.workspaceFolders[0];
69-
}
70-
return debug.startDebugging(workspaceFolder, {
71-
name: 'Debug Unit Test',
72-
type: 'python',
73-
request: 'attach',
74-
localRoot: options.cwd,
75-
remoteRoot: options.cwd,
76-
port: options.port,
77-
secret: 'my_secret',
78-
host: options.host
79-
});
80-
})
81-
.catch(reason => {
82-
if (!def.completed) {
83-
def.reject(reason);
84-
}
85-
});
8612

87-
return def.promise;
88-
});
13+
if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) {
14+
throw new Error('Please open a workspace');
15+
}
16+
let workspaceFolder = workspace.getWorkspaceFolder(cwdUri!);
17+
if (!workspaceFolder) {
18+
workspaceFolder = workspace.workspaceFolders[0];
19+
}
20+
const args = options.args.slice();
21+
const program = args.shift();
22+
return debug.startDebugging(workspaceFolder, {
23+
name: 'Debug Unit Test',
24+
type: 'python',
25+
request: 'launch',
26+
program,
27+
cwd: cwdUri ? cwdUri.fsPath : workspaceFolder.uri.fsPath,
28+
args,
29+
console: 'none',
30+
debugOptions: ['RedirectOutput']
31+
}).then(() => void (0));
8932
}
9033
}

src/client/unittests/common/types.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,11 @@ export type launchOptions = {
190190
args: string[];
191191
token?: CancellationToken;
192192
outChannel?: OutputChannel;
193-
port: number;
194-
host: string;
195193
};
196194

197195
export const ITestDebugLauncher = Symbol('ITestDebugLauncher');
198196

199197
export interface ITestDebugLauncher {
200-
getLaunchOptions(resource?: Uri): Promise<{ port: number, host: string }>;
201198
launchDebugger(options: launchOptions): Promise<void>;
202199
}
203200

src/client/unittests/nosetest/runner.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,12 @@ export function runTest(serviceContainer: IServiceContainer, testResultsService:
5959
return promiseToGetXmlLogFile.then(() => {
6060
if (options.debug === true) {
6161
const debugLauncher = serviceContainer.get<ITestDebugLauncher>(ITestDebugLauncher);
62-
return debugLauncher.getLaunchOptions(options.workspaceFolder)
63-
.then(debugPortAndHost => {
64-
const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py');
65-
const nosetestlauncherargs = [options.cwd, 'my_secret', debugPortAndHost.port.toString(), 'nose'];
66-
const debuggerArgs = [testLauncherFile].concat(nosetestlauncherargs).concat(noseTestArgs.concat(testPaths));
67-
const launchOptions = { cwd: options.cwd, args: debuggerArgs, token: options.token, outChannel: options.outChannel, port: debugPortAndHost.port, host: debugPortAndHost.host };
68-
// tslint:disable-next-line:prefer-type-cast no-any
69-
return debugLauncher.launchDebugger(launchOptions) as Promise<any>;
70-
});
62+
const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py');
63+
const nosetestlauncherargs = [options.cwd, 'nose'];
64+
const debuggerArgs = [testLauncherFile].concat(nosetestlauncherargs).concat(noseTestArgs.concat(testPaths));
65+
const launchOptions = { cwd: options.cwd, args: debuggerArgs, token: options.token, outChannel: options.outChannel };
66+
// tslint:disable-next-line:prefer-type-cast no-any
67+
return debugLauncher.launchDebugger(launchOptions) as Promise<any>;
7168
} else {
7269
// tslint:disable-next-line:prefer-type-cast no-any
7370
const runOptions: Options = {

src/client/unittests/pytest/runner.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,12 @@ export function runTest(serviceContainer: IServiceContainer, testResultsService:
3535
const testArgs = testPaths.concat(args, [`--junitxml=${xmlLogFile}`]);
3636
if (options.debug) {
3737
const debugLauncher = serviceContainer.get<ITestDebugLauncher>(ITestDebugLauncher);
38-
return debugLauncher.getLaunchOptions(options.workspaceFolder)
39-
.then(debugPortAndHost => {
40-
const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py');
41-
const pytestlauncherargs = [options.cwd, 'my_secret', debugPortAndHost.port.toString(), 'pytest'];
42-
const debuggerArgs = [testLauncherFile].concat(pytestlauncherargs).concat(testArgs);
43-
const launchOptions = { cwd: options.cwd, args: debuggerArgs, token: options.token, outChannel: options.outChannel, port: debugPortAndHost.port, host: debugPortAndHost.host };
44-
// tslint:disable-next-line:prefer-type-cast no-any
45-
return debugLauncher.launchDebugger(launchOptions) as Promise<any>;
46-
});
38+
const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py');
39+
const pytestlauncherargs = [options.cwd, 'pytest'];
40+
const debuggerArgs = [testLauncherFile].concat(pytestlauncherargs).concat(testArgs);
41+
const launchOptions = { cwd: options.cwd, args: debuggerArgs, token: options.token, outChannel: options.outChannel };
42+
// tslint:disable-next-line:prefer-type-cast no-any
43+
return debugLauncher.launchDebugger(launchOptions) as Promise<any>;
4744
} else {
4845
const runOptions: Options = {
4946
args: testArgs,

src/client/unittests/unittest/runner.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,10 @@ export async function runTest(serviceContainer: IServiceContainer, testManager:
9393
}
9494
if (options.debug === true) {
9595
const debugLauncher = serviceContainer.get<ITestDebugLauncher>(ITestDebugLauncher);
96-
return debugLauncher.getLaunchOptions(options.workspaceFolder)
97-
.then(debugPortAndHost => {
98-
testArgs.push(...['--secret=my_secret', `--port=${debugPortAndHost.port}`]);
99-
const launchOptions = { cwd: options.cwd, args: [testLauncherFile].concat(testArgs), token: options.token, outChannel: options.outChannel, port: debugPortAndHost.port, host: debugPortAndHost.host };
100-
// tslint:disable-next-line:prefer-type-cast no-any
101-
return debugLauncher.launchDebugger(launchOptions);
102-
});
96+
testArgs.push(...['--debug']);
97+
const launchOptions = { cwd: options.cwd, args: [testLauncherFile].concat(testArgs), token: options.token, outChannel: options.outChannel};
98+
// tslint:disable-next-line:prefer-type-cast no-any
99+
return debugLauncher.launchDebugger(launchOptions);
103100
} else {
104101
// tslint:disable-next-line:prefer-type-cast no-any
105102
const runOptions: Options = {

0 commit comments

Comments
 (0)