From 2b7dc71e2a029453077891b6e1d253f041f6763e Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 28 Nov 2017 10:22:00 -0800 Subject: [PATCH] Fix debugging tests (#304) Fixes #89 - Get a random port if the one prescribed is not available - Perform handshake with support for data being streamed --- package.json | 2 ++ src/client/unittests/common/debugLauncher.ts | 23 +++++++++++++++----- src/client/unittests/common/types.ts | 3 ++- src/client/unittests/nosetest/runner.ts | 15 ++++++++----- src/client/unittests/pytest/runner.ts | 15 ++++++++----- src/client/unittests/unittest/runner.ts | 12 +++++----- src/test/unittests/mocks.ts | 7 ++++-- 7 files changed, 51 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 90e9d7efaaa3..877ec4a6a07a 100644 --- a/package.json +++ b/package.json @@ -1568,6 +1568,7 @@ "diff-match-patch": "^1.0.0", "fs-extra": "^4.0.2", "fuzzy": "^0.1.3", + "get-port": "^3.2.0", "line-by-line": "^0.1.5", "lodash": "^4.17.4", "minimatch": "^3.0.3", @@ -1591,6 +1592,7 @@ "@types/chai": "^4.0.4", "@types/chai-as-promised": "^7.1.0", "@types/fs-extra": "^4.0.2", + "@types/get-port": "^3.2.0", "@types/jquery": "^1.10.31", "@types/lodash": "^4.14.74", "@types/mocha": "^2.2.43", diff --git a/src/client/unittests/common/debugLauncher.ts b/src/client/unittests/common/debugLauncher.ts index 783dbc24fd7c..a5afa317f055 100644 --- a/src/client/unittests/common/debugLauncher.ts +++ b/src/client/unittests/common/debugLauncher.ts @@ -1,3 +1,4 @@ +import * as getFreePort from 'get-port'; import * as os from 'os'; import { CancellationToken, debug, OutputChannel, Uri, workspace } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; @@ -5,19 +6,31 @@ import { createDeferred } from './../../common/helpers'; import { execPythonFile } from './../../common/utils'; import { ITestDebugLauncher } from './types'; +const HAND_SHAKE = `READY${os.EOL}`; + export class DebugLauncher implements ITestDebugLauncher { - public async launchDebugger(rootDirectory: string, testArgs: string[], token?: CancellationToken, outChannel?: OutputChannel) { + public getPort(resource?: Uri): Promise { + const pythonSettings = PythonSettings.getInstance(resource); + const port = pythonSettings.unitTest.debugPort; + return new Promise((resolve, reject) => getFreePort({ host: 'localhost', port }).then(resolve, reject)); + } + public async launchDebugger(rootDirectory: string, testArgs: string[], port: number, token?: CancellationToken, outChannel?: OutputChannel) { const pythonSettings = PythonSettings.getInstance(rootDirectory ? Uri.file(rootDirectory) : undefined); // tslint:disable-next-line:no-any const def = createDeferred(); // tslint:disable-next-line:no-any const launchDef = createDeferred(); let outputChannelShown = false; + let accumulatedData: string = ''; execPythonFile(rootDirectory, pythonSettings.pythonPath, testArgs, rootDirectory, true, (data: string) => { - if (data.startsWith(`READY${os.EOL}`)) { - // debug socket server has started. + if (!launchDef.resolved) { + accumulatedData += data; + if (!accumulatedData.startsWith(HAND_SHAKE)) { + return; + } + // Socket server has started, lets start the debugger. launchDef.resolve(); - data = data.substring((`READY${os.EOL}`).length); + data = accumulatedData.substring(HAND_SHAKE.length); } if (!outputChannelShown) { @@ -53,7 +66,7 @@ export class DebugLauncher implements ITestDebugLauncher { request: 'attach', localRoot: rootDirectory, remoteRoot: rootDirectory, - port: pythonSettings.unitTest.debugPort, + port, secret: 'my_secret', host: 'localhost' }); diff --git a/src/client/unittests/common/types.ts b/src/client/unittests/common/types.ts index 5125b5a4c17c..9d993ece79e7 100644 --- a/src/client/unittests/common/types.ts +++ b/src/client/unittests/common/types.ts @@ -148,5 +148,6 @@ export interface ITestResultsService { } export interface ITestDebugLauncher { - launchDebugger(rootDirectory: string, testArgs: string[], token?: CancellationToken, outChannel?: OutputChannel): Promise; + getPort(resource?: Uri): Promise; + launchDebugger(rootDirectory: string, testArgs: string[], port: number, token?: CancellationToken, outChannel?: OutputChannel): Promise; } diff --git a/src/client/unittests/nosetest/runner.ts b/src/client/unittests/nosetest/runner.ts index ec67b2adc656..d4ab9ed8cb91 100644 --- a/src/client/unittests/nosetest/runner.ts +++ b/src/client/unittests/nosetest/runner.ts @@ -58,14 +58,17 @@ export function runTest(testResultsService: ITestResultsService, debugLauncher: } return promiseToGetXmlLogFile.then(() => { - const pythonSettings = PythonSettings.getInstance(Uri.file(rootDirectory)); if (debug === true) { - const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py'); - const nosetestlauncherargs = [rootDirectory, 'my_secret', pythonSettings.unitTest.debugPort.toString(), 'nose']; - const debuggerArgs = [testLauncherFile].concat(nosetestlauncherargs).concat(noseTestArgs.concat(testPaths)); - // tslint:disable-next-line:prefer-type-cast no-any - return debugLauncher.launchDebugger(rootDirectory, debuggerArgs, token, outChannel) as Promise; + return debugLauncher.getPort(Uri.file(rootDirectory)) + .then(debugPort => { + const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py'); + const nosetestlauncherargs = [rootDirectory, 'my_secret', debugPort.toString(), 'nose']; + const debuggerArgs = [testLauncherFile].concat(nosetestlauncherargs).concat(noseTestArgs.concat(testPaths)); + // tslint:disable-next-line:prefer-type-cast no-any + return debugLauncher.launchDebugger(rootDirectory, debuggerArgs, debugPort, token, outChannel) as Promise; + }); } else { + const pythonSettings = PythonSettings.getInstance(Uri.file(rootDirectory)); // tslint:disable-next-line:prefer-type-cast no-any return run(pythonSettings.unitTest.nosetestPath, noseTestArgs.concat(testPaths), rootDirectory, token, outChannel) as Promise; } diff --git a/src/client/unittests/pytest/runner.ts b/src/client/unittests/pytest/runner.ts index 3e314b305cab..2ec37a4b8239 100644 --- a/src/client/unittests/pytest/runner.ts +++ b/src/client/unittests/pytest/runner.ts @@ -33,14 +33,17 @@ export function runTest(testResultsService: ITestResultsService, debugLauncher: args = args.filter(arg => arg.trim().startsWith('-')); } const testArgs = testPaths.concat(args, [`--junitxml=${xmlLogFile}`]); - const pythonSettings = PythonSettings.getInstance(Uri.file(rootDirectory)); if (debug) { - const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py'); - const pytestlauncherargs = [rootDirectory, 'my_secret', pythonSettings.unitTest.debugPort.toString(), 'pytest']; - const debuggerArgs = [testLauncherFile].concat(pytestlauncherargs).concat(testArgs); - // tslint:disable-next-line:prefer-type-cast no-any - return debugLauncher.launchDebugger(rootDirectory, debuggerArgs, token, outChannel) as Promise; + return debugLauncher.getPort(Uri.file(rootDirectory)) + .then(debugPort => { + const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py'); + const pytestlauncherargs = [rootDirectory, 'my_secret', debugPort.toString(), 'pytest']; + const debuggerArgs = [testLauncherFile].concat(pytestlauncherargs).concat(testArgs); + // tslint:disable-next-line:prefer-type-cast no-any + return debugLauncher.launchDebugger(rootDirectory, debuggerArgs, debugPort, token, outChannel) as Promise; + }); } else { + const pythonSettings = PythonSettings.getInstance(Uri.file(rootDirectory)); // tslint:disable-next-line:prefer-type-cast no-any return run(pythonSettings.unitTest.pyTestPath, testArgs, rootDirectory, token, outChannel) as Promise; } diff --git a/src/client/unittests/unittest/runner.ts b/src/client/unittests/unittest/runner.ts index db72c77b14ef..c8c2eb05d00d 100644 --- a/src/client/unittests/unittest/runner.ts +++ b/src/client/unittests/unittest/runner.ts @@ -85,10 +85,6 @@ export function runTest(testManager: BaseTestManager, testResultsService: ITestR testArgs = testArgs.filter(arg => arg !== '--uf'); testArgs.push(`--result-port=${port}`); - if (debug === true) { - const debugPort = PythonSettings.getInstance(Uri.file(rootDirectory)).unitTest.debugPort; - testArgs.push(...['--secret=my_secret', `--port=${debugPort}`]); - } testArgs.push(`--us=${startTestDiscoveryDirectory}`); if (testId.length > 0) { testArgs.push(`-t${testId}`); @@ -97,8 +93,12 @@ export function runTest(testManager: BaseTestManager, testResultsService: ITestR testArgs.push(`--testFile=${testFile}`); } if (debug === true) { - // tslint:disable-next-line:prefer-type-cast no-any - return debugLauncher.launchDebugger(rootDirectory, [testLauncherFile].concat(testArgs), token, outChannel); + return debugLauncher.getPort(Uri.file(rootDirectory)) + .then(debugPort => { + testArgs.push(...['--secret=my_secret', `--port=${debugPort}`]); + // tslint:disable-next-line:prefer-type-cast no-any + return debugLauncher.launchDebugger(rootDirectory, [testLauncherFile].concat(testArgs), debugPort, token, outChannel); + }); } else { // tslint:disable-next-line:prefer-type-cast no-any return run(PythonSettings.getInstance(Uri.file(rootDirectory)).pythonPath, [testLauncherFile].concat(testArgs), rootDirectory, token, outChannel); diff --git a/src/test/unittests/mocks.ts b/src/test/unittests/mocks.ts index fd9741543a78..5982f5b94c2b 100644 --- a/src/test/unittests/mocks.ts +++ b/src/test/unittests/mocks.ts @@ -1,4 +1,4 @@ -import { CancellationToken, Disposable, OutputChannel } from 'vscode'; +import { CancellationToken, Disposable, OutputChannel, Uri } from 'vscode'; import { createDeferred, Deferred } from '../../client/common/helpers'; import { Product } from '../../client/common/installer'; import { BaseTestManager } from '../../client/unittests/common/baseTestManager'; @@ -25,7 +25,10 @@ export class MockDebugLauncher implements ITestDebugLauncher, Disposable { constructor() { this._launched = createDeferred(); } - public async launchDebugger(rootDirectory: string, testArgs: string[], token?: CancellationToken, outChannel?: OutputChannel): Promise { + public async getPort(resource?: Uri): Promise { + return 0; + } + public async launchDebugger(rootDirectory: string, testArgs: string[], debugPort: number, token?: CancellationToken, outChannel?: OutputChannel): Promise { this._launched.resolve(true); // tslint:disable-next-line:no-non-null-assertion this._token = token!;