From b43ae94bf69028a7e1c0fc6882db228889b0ec5f Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 27 Mar 2018 13:31:51 -0700 Subject: [PATCH 01/24] split compilation for faster compilation on windows --- .vscode/settings.json | 2 +- .vscode/tasks.json | 37 ++++++++++++++++++++++++++++++++++++- gulpfile.js | 9 +++++---- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index be66f967c5c4..31866020cb46 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,7 +14,7 @@ "coverage": true }, "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version - "tslint.enable": true, + "tslint.enable": false, // We will run our own linting in gulp (& git commit hooks), else tslint extension just complains about unmodified files "python.linting.enabled": false, "python.unitTest.promptToConfigure": false, "python.workspaceSymbols.enabled": false, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f4384e2537bf..826c669b3eeb 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,6 +9,23 @@ "tasks": [ { "label": "Compile", + "type": "npm", + "script": "compile", + "isBackground": true, + "problemMatcher": [ + "$tsc-watch", + { + "base": "$tslint5", + "fileLocation": "relative" + } + ], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "Hygiene", "type": "gulp", "task": "watch", "isBackground": true, @@ -18,6 +35,24 @@ "focus": false, "panel": "dedicated" }, + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [] + }, + { + // Use this to run hygiene and display errors in problems window (very slow on Windows) + "label": "Hygiene (Problems Window)", + "type": "gulp", + "task": "watchProblems", + "isBackground": true, + "presentation": { + "echo": true, + "reveal": "never", + "focus": false, + "panel": "dedicated" + }, "problemMatcher": [ { "applyTo": "allDocuments", @@ -70,4 +105,4 @@ } } ] -} +} \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 6a8cbadb7e77..b8250803dbde 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -78,6 +78,9 @@ gulp.task('compile', () => run({ mode: 'compile', skipFormatCheck: true, skipInd gulp.task('watch', ['hygiene-modified', 'hygiene-watch']); +// Duplicate to allow duplicate task in tasks.json (one ith problem matching, and one without) +gulp.task('watchProblems', ['hygiene-modified', 'hygiene-watch']); + gulp.task('debugger-coverage', () => buildDebugAdapterCoverage()); gulp.task('hygiene-watch', () => gulp.watch(tsFilter, debounce(() => run({ mode: 'changes', skipFormatCheck: true, skipIndentationCheck: true, skipCopyrightCheck: true }), 100))); @@ -149,16 +152,14 @@ function getTsProject(options) { } let configuration; -let program; -let linter; /** * * @param {hygieneOptions} options */ function getLinter(options) { configuration = configuration ? configuration : tslint.Configuration.findConfiguration(null, '.'); - program = program ? program : tslint.Linter.createProgram('./tsconfig.json'); - linter = linter ? linter : new tslint.Linter({ formatter: 'json' }, program); + const program = tslint.Linter.createProgram('./tsconfig.json'); + const linter = new tslint.Linter({ formatter: 'json' }, program); return { linter, configuration }; } let compilationInProgress = false; From feda0dd8c1a99dfd57bae12c909756e30cc67650 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 27 Mar 2018 13:36:47 -0700 Subject: [PATCH 02/24] :memo: update docs and change log --- CONTRIBUTING.md | 2 +- news/3 Code Health/1216.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 news/3 Code Health/1216.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef238b941add..dc48f492daef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ You may see warnings that ```The engine "vscode" appears to be invalid.```, you ### Incremental Build -Run the `Compile` build Tasks from the [Command Palette](https://code.visualstudio.com/docs/editor/tasks) (short cut `CTRL+SHIFT+B` or `⇧⌘B`) +Run the `Compile` and `Hygiene` build Tasks from the [Command Palette](https://code.visualstudio.com/docs/editor/tasks) (short cut `CTRL+SHIFT+B` or `⇧⌘B`) ### Errors and Warnings diff --git a/news/3 Code Health/1216.md b/news/3 Code Health/1216.md new file mode 100644 index 000000000000..884f1ba6286a --- /dev/null +++ b/news/3 Code Health/1216.md @@ -0,0 +1 @@ +Improved developer experience of the Python Extension on Windows. \ No newline at end of file From 09add7ae9c5d759219d309386b53a53d8320c6a8 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 28 Mar 2018 13:22:07 -0700 Subject: [PATCH 03/24] :sparkles: experimental debugger attach --- package.json | 49 ++++- src/client/debugger/Common/Contracts.ts | 1 + .../DebugClients/RemoteDebugClient.ts | 14 +- .../DebugServers/RemoteDebugServerv2.ts | 44 ++++ .../debugger/configProviders/baseProvider.ts | 37 +++- .../configProviders/pythonV2Provider.ts | 16 +- src/client/debugger/mainV2.ts | 88 +++++--- .../configProvider/provider.attach.test.ts | 197 ++++++++++++++++++ 8 files changed, 401 insertions(+), 45 deletions(-) create mode 100644 src/client/debugger/DebugServers/RemoteDebugServerv2.ts create mode 100644 src/test/debugger/configProvider/provider.attach.test.ts diff --git a/package.json b/package.json index dfb0680cfcc7..9b8b709a0e2f 100644 --- a/package.json +++ b/package.json @@ -963,7 +963,54 @@ "default": false } } - } + }, + "attach": { + "required": [ + "port", + "remoteRoot" + ], + "properties": { + "localRoot": { + "type": "string", + "description": "Local source root that corrresponds to the 'remoteRoot'.", + "default": "${workspaceFolder}" + }, + "remoteRoot": { + "type": "string", + "description": "The source root of the remote host.", + "default": "" + }, + "port": { + "type": "number", + "description": "Debug port to attach", + "default": 0 + }, + "host": { + "type": "string", + "description": "IP Address of the of remote server (default is localhost or use 127.0.0.1).", + "default": "localhost" + }, + "debugOptions": { + "type": "array", + "description": "Advanced options, view read me for further details.", + "items": { + "type": "string", + "enum": [ + "RedirectOutput", + "DebugStdLib", + "Django", + "Jinja" + ] + }, + "default": [] + }, + "logToFile": { + "type": "boolean", + "description": "Enable logging of debugger events to a log file.", + "default": false + } + } + } }, "initialConfigurations": [ { diff --git a/src/client/debugger/Common/Contracts.ts b/src/client/debugger/Common/Contracts.ts index 7828d5685d7c..09dd275528a6 100644 --- a/src/client/debugger/Common/Contracts.ts +++ b/src/client/debugger/Common/Contracts.ts @@ -72,6 +72,7 @@ export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArgum } export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { + type?: DebuggerType; /** An absolute path to local directory with source. */ localRoot: string; remoteRoot: string; diff --git a/src/client/debugger/DebugClients/RemoteDebugClient.ts b/src/client/debugger/DebugClients/RemoteDebugClient.ts index 45aec00388a4..a7bee8e9f578 100644 --- a/src/client/debugger/DebugClients/RemoteDebugClient.ts +++ b/src/client/debugger/DebugClients/RemoteDebugClient.ts @@ -2,19 +2,25 @@ import { DebugSession } from 'vscode-debugadapter'; import { AttachRequestArguments, IPythonProcess } from '../Common/Contracts'; import { BaseDebugServer } from '../DebugServers/BaseDebugServer'; import { RemoteDebugServer } from '../DebugServers/RemoteDebugServer'; +import { RemoteDebugServerV2 } from '../DebugServers/RemoteDebugServerv2'; import { DebugClient, DebugType } from './DebugClient'; export class RemoteDebugClient extends DebugClient { - private pythonProcess: IPythonProcess; + private pythonProcess?: IPythonProcess; private debugServer?: BaseDebugServer; // tslint:disable-next-line:no-any - constructor(args: any, debugSession: DebugSession) { + constructor(args: AttachRequestArguments, debugSession: DebugSession) { super(args, debugSession); } public CreateDebugServer(pythonProcess?: IPythonProcess): BaseDebugServer { - this.pythonProcess = pythonProcess!; - this.debugServer = new RemoteDebugServer(this.debugSession, this.pythonProcess!, this.args); + if (this.args.type === 'pythonExperimental') { + // tslint:disable-next-line:no-any + this.debugServer = new RemoteDebugServerV2(this.debugSession, undefined as any, this.args); + } else { + this.pythonProcess = pythonProcess!; + this.debugServer = new RemoteDebugServer(this.debugSession, this.pythonProcess!, this.args); + } return this.debugServer!; } public get DebugType(): DebugType { diff --git a/src/client/debugger/DebugServers/RemoteDebugServerv2.ts b/src/client/debugger/DebugServers/RemoteDebugServerv2.ts new file mode 100644 index 000000000000..21b6b119e57c --- /dev/null +++ b/src/client/debugger/DebugServers/RemoteDebugServerv2.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:quotemark ordered-imports no-any no-empty curly member-ordering one-line max-func-body-length no-var-self prefer-const cyclomatic-complexity prefer-template + +import { DebugSession } from "vscode-debugadapter"; +import { IPythonProcess, IDebugServer, AttachRequestArguments } from "../Common/Contracts"; +import { connect, Socket } from "net"; +import { BaseDebugServer } from "./BaseDebugServer"; + +export class RemoteDebugServerV2 extends BaseDebugServer { + private args: AttachRequestArguments; + private socket?: Socket; + constructor(debugSession: DebugSession, pythonProcess: IPythonProcess, args: AttachRequestArguments) { + super(debugSession, pythonProcess); + this.args = args; + } + + public Stop() { + if (this.socket) { + this.socket.destroy(); + } + } + public Start(): Promise { + return new Promise((resolve, reject) => { + let portNumber = this.args.port; + let options = { port: portNumber! }; + if (typeof this.args.host === "string" && this.args.host.length > 0) { + (options).host = this.args.host; + } + try { + const socket = connect(options, () => { + this.socket = socket; + this.clientSocket.resolve(socket); + resolve(options); + }); + } catch (ex) { + reject(ex); + } + }); + } +} diff --git a/src/client/debugger/configProviders/baseProvider.ts b/src/client/debugger/configProviders/baseProvider.ts index f7786eca9b70..53329f185a18 100644 --- a/src/client/debugger/configProviders/baseProvider.ts +++ b/src/client/debugger/configProviders/baseProvider.ts @@ -11,17 +11,18 @@ import { PythonLanguage } from '../../common/constants'; import { IFileSystem, IPlatformService } from '../../common/platform/types'; import { IConfigurationService } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; -import { DebuggerType, DebugOptions, LaunchRequestArguments } from '../Common/Contracts'; +import { AttachRequestArguments, DebuggerType, DebugOptions, LaunchRequestArguments } from '../Common/Contracts'; // tslint:disable:no-invalid-template-strings -export type PythonDebugConfiguration = DebugConfiguration & LaunchRequestArguments; +export type PythonLaunchDebugConfiguration = DebugConfiguration & LaunchRequestArguments; +export type PythonAttachDebugConfiguration = DebugConfiguration & AttachRequestArguments; @injectable() export abstract class BaseConfigurationProvider implements DebugConfigurationProvider { constructor(@unmanaged() public debugType: DebuggerType, protected serviceContainer: IServiceContainer) { } public resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { - const config = debugConfiguration as PythonDebugConfiguration; + const config = debugConfiguration as PythonLaunchDebugConfiguration; const numberOfSettings = Object.keys(config); const workspaceFolder = this.getWorkspaceFolder(folder, config); @@ -35,10 +36,30 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro config.env = {}; } - this.provideDefaults(workspaceFolder, config); + if (config.request === 'attach') { + // tslint:disable-next-line:no-any + this.provideAttachDefaults(workspaceFolder, config as any as PythonAttachDebugConfiguration); + } else { + this.provideLaunchDefaults(workspaceFolder, config); + } return config; } - protected provideDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonDebugConfiguration): void { + protected provideAttachDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonAttachDebugConfiguration): void { + if (!Array.isArray(debugConfiguration.debugOptions)) { + debugConfiguration.debugOptions = []; + } + // Always redirect output. + if (debugConfiguration.debugOptions.indexOf(DebugOptions.RedirectOutput) === -1) { + debugConfiguration.debugOptions.push(DebugOptions.RedirectOutput); + } + if (!debugConfiguration.host) { + debugConfiguration.host = 'localhost'; + } + if (!debugConfiguration.localRoot && workspaceFolder) { + debugConfiguration.localRoot = workspaceFolder.fsPath; + } + } + protected provideLaunchDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonLaunchDebugConfiguration): void { this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); if (typeof debugConfiguration.cwd !== 'string' && workspaceFolder) { debugConfiguration.cwd = workspaceFolder.fsPath; @@ -75,7 +96,7 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro } } } - private getWorkspaceFolder(folder: WorkspaceFolder | undefined, config: PythonDebugConfiguration): Uri | undefined { + private getWorkspaceFolder(folder: WorkspaceFolder | undefined, config: PythonLaunchDebugConfiguration): Uri | undefined { if (folder) { return folder.uri; } @@ -94,14 +115,14 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro } } } - private getProgram(config: PythonDebugConfiguration): string | undefined { + private getProgram(config: PythonLaunchDebugConfiguration): string | undefined { const documentManager = this.serviceContainer.get(IDocumentManager); const editor = documentManager.activeTextEditor; if (editor && editor.document.languageId === PythonLanguage.language) { return editor.document.fileName; } } - private resolveAndUpdatePythonPath(workspaceFolder: Uri | undefined, debugConfiguration: PythonDebugConfiguration): void { + private resolveAndUpdatePythonPath(workspaceFolder: Uri | undefined, debugConfiguration: PythonLaunchDebugConfiguration): void { if (!debugConfiguration) { return; } diff --git a/src/client/debugger/configProviders/pythonV2Provider.ts b/src/client/debugger/configProviders/pythonV2Provider.ts index 0d95f539e80b..d356901f45ab 100644 --- a/src/client/debugger/configProviders/pythonV2Provider.ts +++ b/src/client/debugger/configProviders/pythonV2Provider.ts @@ -8,15 +8,15 @@ import { Uri } from 'vscode'; import { IPlatformService } from '../../common/platform/types'; import { IServiceContainer } from '../../ioc/types'; import { DebugOptions } from '../Common/Contracts'; -import { BaseConfigurationProvider, PythonDebugConfiguration } from './baseProvider'; +import { BaseConfigurationProvider, PythonAttachDebugConfiguration, PythonLaunchDebugConfiguration } from './baseProvider'; @injectable() export class PythonV2DebugConfigurationProvider extends BaseConfigurationProvider { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super('pythonExperimental', serviceContainer); } - protected provideDefaults(workspaceFolder: Uri, debugConfiguration: PythonDebugConfiguration): void { - super.provideDefaults(workspaceFolder, debugConfiguration); + protected provideLaunchDefaults(workspaceFolder: Uri, debugConfiguration: PythonLaunchDebugConfiguration): void { + super.provideLaunchDefaults(workspaceFolder, debugConfiguration); debugConfiguration.stopOnEntry = false; debugConfiguration.debugOptions = Array.isArray(debugConfiguration.debugOptions) ? debugConfiguration.debugOptions : []; @@ -30,4 +30,14 @@ export class PythonV2DebugConfigurationProvider extends BaseConfigurationProvide debugConfiguration.debugOptions.push(DebugOptions.Jinja); } } + protected provideAttachDefaults(workspaceFolder: Uri, debugConfiguration: PythonAttachDebugConfiguration): void { + super.provideAttachDefaults(workspaceFolder, debugConfiguration); + + debugConfiguration.debugOptions = Array.isArray(debugConfiguration.debugOptions) ? debugConfiguration.debugOptions : []; + + // Add PTVSD specific flags. + if (this.serviceContainer.get(IPlatformService).isWindows) { + debugConfiguration.debugOptions.push(DebugOptions.FixFilePathCase); + } + } } diff --git a/src/client/debugger/mainV2.ts b/src/client/debugger/mainV2.ts index 53f4d60d17cd..f7f78c585732 100644 --- a/src/client/debugger/mainV2.ts +++ b/src/client/debugger/mainV2.ts @@ -25,7 +25,7 @@ import { ICurrentProcess } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { AttachRequestArguments, LaunchRequestArguments } from './Common/Contracts'; import { DebugClient } from './DebugClients/DebugClient'; -import { CreateLaunchDebugClient } from './DebugClients/DebugFactory'; +import { CreateAttachDebugClient, CreateLaunchDebugClient } from './DebugClients/DebugFactory'; import { BaseDebugServer } from './DebugServers/BaseDebugServer'; import { initializeIoc } from './serviceRegistry'; import { IDebugStreamProvider, IProtocolLogger, IProtocolMessageWriter, IProtocolParser } from './types'; @@ -61,6 +61,11 @@ export class PythonDebugger extends DebugSession { } super.shutdown(); } + public async createAttachDebugServer(attachRequest: DebugProtocol.AttachRequest) { + const launcher = CreateAttachDebugClient(attachRequest.arguments as AttachRequestArguments, this); + this.debugServer = launcher.CreateDebugServer(undefined, this.serviceContainer); + await this.debugServer!.Start(); + } protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { const body = response.body!; @@ -87,7 +92,7 @@ export class PythonDebugger extends DebugSession { this.sendResponse(response); } protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { - this.sendResponse(response); + // We will let PTVSD send the response } protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { this.launchPTVSD(args) @@ -173,7 +178,7 @@ class DebugManager implements Disposable { private hasShutdown: boolean = false; private debugSession?: PythonDebugger; private ptvsdProcessId?: number; - private killPTVSDProcess: boolean = false; + private launchOrAttach?: 'launch'|'attach'; private terminatedEventSent: boolean = false; private readonly initializeRequestDeferred: Deferred; private get initializeRequest(): Promise { @@ -263,7 +268,7 @@ class DebugManager implements Disposable { this.terminatedEventSent = true; } - if (this.killPTVSDProcess && this.ptvsdProcessId) { + if (this.launchOrAttach === 'launch' && this.ptvsdProcessId) { logger.verbose('killing process'); try { // 1. Wait for some time, its possible the program has run to completion. @@ -273,7 +278,6 @@ class DebugManager implements Disposable { await sleep(100); killProcessTree(this.ptvsdProcessId!); } catch { } - this.killPTVSDProcess = false; this.ptvsdProcessId = undefined; } @@ -309,18 +313,37 @@ class DebugManager implements Disposable { // Keep track of the initialize and launch requests, we'll need to re-send these to ptvsd, for bootstrapping. this.inputProtocolParser.once('request_initialize', this.onRequestInitialize); this.inputProtocolParser.once('request_launch', this.onRequestLaunch); + this.inputProtocolParser.once('request_attach', this.onRequestAttach); this.outputProtocolParser.once('event_terminated', this.onEventTerminated); this.outputProtocolParser.once('response_disconnect', this.onResponseDisconnect); - this.outputProtocolParser.once('response_launch', this.connectVSCodeToPTVSD); + this.outputProtocolParser.once('response_launch', this.connectVSCodeToPTVSDForLaunch); + } + /** + * Connect PTVSD socket to VS Code. + * @private + * @memberof DebugManager + */ + private connectVSCodeToPTVSDForAttach = async (attachRequest: DebugProtocol.AttachRequest) => { + await this.debugSession!.createAttachDebugServer(attachRequest); + + await this.connectVSCodeToPTVSD(attachRequest, 'attach'); } /** * Once PTVSD process has been started (done by DebugSession), we need to connect PTVSD socket to VS Code. + * @private + * @memberof DebugManager + */ + private connectVSCodeToPTVSDForLaunch = async () => { + await this.connectVSCodeToPTVSD(await this.launchRequest, 'launch'); + } + /** + * Connect PTVSD socket to VS Code. * This allows PTVSD to communicate directly with VS Code. * @private * @memberof DebugManager */ - private connectVSCodeToPTVSD = async () => { + private connectVSCodeToPTVSD = async (attachOrLaunchRequest: DebugProtocol.AttachRequest | DebugProtocol.LaunchRequest, requestType: 'attach' | 'launch') => { // By now we're connected to the client. this.ptvsdSocket = await this.debugSession!.debugServer!.client; @@ -330,43 +353,50 @@ class DebugManager implements Disposable { this.ptvsdSocket.on('error', this.shutdown); const debugSoketProtocolParser = this.serviceContainer.get(IProtocolParser); debugSoketProtocolParser.connect(this.ptvsdSocket); + const initializedEventPromise = new Promise(resolve => debugSoketProtocolParser.once('event_initialized', resolve)); + const attachedOrLaunchedPromise = new Promise(resolve => debugSoketProtocolParser.once(`response_${requestType}`, resolve)); - // Send PTVSD the launch request (PTVSD needs to do its own initialization using launch arguments). - // E.g. redirectOutput & fixFilePathCase found in launch request are used to initialize the debugger. - this.sendMessage(await this.launchRequest, this.ptvsdSocket); - await new Promise(resolve => debugSoketProtocolParser.once('response_launch', resolve)); + // Keep track of processid for killing it. + if (requestType === 'launch') { + debugSoketProtocolParser.once('event_process', (proc: DebugProtocol.ProcessEvent) => { + this.ptvsdProcessId = proc.body.systemProcessId; + }); + } - // The PTVSD process has launched, now send the initialize request to it (required by PTVSD). + // Send the launch/attach request to PTVSD and wait for it to reply back. + this.sendMessage(attachOrLaunchRequest, this.ptvsdSocket); + await attachedOrLaunchedPromise; + + // Send the initialize request and wait for it to reply back with the initialized event this.sendMessage(await this.initializeRequest, this.ptvsdSocket); - // Keep track of processid for killing it. - debugSoketProtocolParser.once('event_process', (proc: DebugProtocol.ProcessEvent) => { - this.ptvsdProcessId = proc.body.systemProcessId; - }); + const initializedEvent = await initializedEventPromise; - // Wait for PTVSD to reply back with initialized event. - debugSoketProtocolParser.once('event_initialized', (initialized: DebugProtocol.InitializedEvent) => { - // Get ready for PTVSD to communicate directly with VS Code. - (this.inputStream as any as NodeJS.ReadStream).unpipe(this.debugSessionInputStream); - this.debugSessionOutputStream.unpipe(this.outputStream); + // Get ready for PTVSD to communicate directly with VS Code. + (this.inputStream as any as NodeJS.ReadStream).unpipe(this.debugSessionInputStream); + this.debugSessionOutputStream.unpipe(this.outputStream); - this.inputStream.pipe(this.ptvsdSocket!); - this.ptvsdSocket!.pipe(this.throughOutputStream); - this.ptvsdSocket!.pipe(this.outputStream); + this.inputStream.pipe(this.ptvsdSocket!); + this.ptvsdSocket!.pipe(this.throughOutputStream); + this.ptvsdSocket!.pipe(this.outputStream); - // Forward the initialized event sent by PTVSD onto VSCode. - // This is what will cause PTVSD to start the actualy work. - this.sendMessage(initialized, this.outputStream); - }); + // Forward the initialized event sent by PTVSD onto VSCode. + // This is what will cause PTVSD to start the actualy work. + this.sendMessage(initializedEvent, this.outputStream); } private onRequestInitialize = (request: DebugProtocol.InitializeRequest) => { this.initializeRequestDeferred.resolve(request); } private onRequestLaunch = (request: DebugProtocol.LaunchRequest) => { - this.killPTVSDProcess = true; + this.launchOrAttach = 'launch'; this.loggingEnabled = (request.arguments as LaunchRequestArguments).logToFile === true; this.launchRequestDeferred.resolve(request); } + private onRequestAttach = (request: DebugProtocol.AttachRequest) => { + this.launchOrAttach = 'attach'; + this.loggingEnabled = (request.arguments as AttachRequestArguments).logToFile === true; + this.connectVSCodeToPTVSDForAttach(request).ignoreErrors(); + } private onEventTerminated = async () => { logger.verbose('onEventTerminated'); this.terminatedEventSent = true; diff --git a/src/test/debugger/configProvider/provider.attach.test.ts b/src/test/debugger/configProvider/provider.attach.test.ts new file mode 100644 index 000000000000..596cc907c7dc --- /dev/null +++ b/src/test/debugger/configProvider/provider.attach.test.ts @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-invalid-template-strings no-any no-object-literal-type-assertion + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { PythonLanguage } from '../../../client/common/constants'; +import { EnumEx } from '../../../client/common/enumUtils'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { PythonDebugConfigurationProvider, PythonV2DebugConfigurationProvider } from '../../../client/debugger'; +import { DebugOptions } from '../../../client/debugger/Common/Contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; + +enum OS { + Windows, + Mac, + Linux +} +[ + { debugType: 'pythonExperimental', class: PythonV2DebugConfigurationProvider }, + { debugType: 'python', class: PythonDebugConfigurationProvider } +].forEach(provider => { + EnumEx.getNamesAndValues(OS).forEach(os => { + suite(`Debugging - Config Provider attach, ${provider.debugType}, OS = ${os.name}`, () => { + let serviceContainer: TypeMoq.IMock; + let debugProvider: DebugConfigurationProvider; + let platformService: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + const debugOptionsAvailable = [DebugOptions.RedirectOutput]; + if (os.value === OS.Windows && provider.debugType === 'pythonExperimental') { + debugOptionsAvailable.push(DebugOptions.FixFilePathCase); + } + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + platformService = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + platformService.setup(p => p.isWindows).returns(() => os.value === OS.Windows); + platformService.setup(p => p.isMac).returns(() => os.value === OS.Mac); + platformService.setup(p => p.isLinux).returns(() => os.value === OS.Linux); + debugProvider = new provider.class(serviceContainer.object); + }); + function createMoqWorkspaceFolder(folderPath: string) { + const folder = TypeMoq.Mock.ofType(); + folder.setup(f => f.uri).returns(() => Uri.file(folderPath)); + return folder.object; + } + function setupActiveEditor(fileName: string | undefined, languageId: string) { + const documentManager = TypeMoq.Mock.ofType(); + if (fileName) { + const textEditor = TypeMoq.Mock.ofType(); + const document = TypeMoq.Mock.ofType(); + document.setup(d => d.languageId).returns(() => languageId); + document.setup(d => d.fileName).returns(() => fileName); + textEditor.setup(t => t.document).returns(() => document.object); + documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); + } else { + documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + } + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager))).returns(() => documentManager.object); + } + function setupWorkspaces(folders: string[]) { + const workspaceService = TypeMoq.Mock.ofType(); + const workspaceFolders = folders.map(createMoqWorkspaceFolder); + workspaceService.setup(w => w.workspaceFolders).returns(() => workspaceFolders); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + } + test('Defaults should be returned when an empty object is passed with a Workspace Folder and active file', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PythonLanguage.language); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { request: 'attach' } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('localRoot'); + expect(debugConfig!.localRoot!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PythonLanguage.language); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + const filePath = Uri.file(path.dirname('')).fsPath; + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('localRoot'); + expect(debugConfig).to.have.property('host', 'localhost'); + expect(debugConfig!.localRoot!.toLowerCase()).to.be.equal(filePath.toLowerCase()); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { + setupActiveEditor(undefined, PythonLanguage.language); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.not.have.property('localRoot'); + expect(debugConfig).to.have.property('host', 'localhost'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and non python file', async () => { + const activeFile = 'xyz.js'; + + setupActiveEditor(activeFile, 'javascript'); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.not.have.property('localRoot'); + expect(debugConfig).to.have.property('host', 'localhost'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, with a workspace and an active python file', async () => { + const activeFile = 'xyz.py'; + setupActiveEditor(activeFile, PythonLanguage.language); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + const filePath = Uri.file(defaultWorkspace).fsPath; + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('localRoot'); + expect(debugConfig).to.have.property('host', 'localhost'); + expect(debugConfig!.localRoot!.toLowerCase()).to.be.equal(filePath.toLowerCase()); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + }); + test('Ensure \'localRoot\' is left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PythonLanguage.language); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('localRoot', localRoot); + }); + test('Ensure \'remoteRoot\' is left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PythonLanguage.language); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { remoteRoot, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('remoteRoot', remoteRoot); + }); + test('Ensure \'port\' is left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PythonLanguage.language); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const port = 12341234; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { port, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('port', port); + }); + test('Ensure \'debugOptions\' are left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PythonLanguage.language); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugOptions = debugOptionsAvailable.slice().concat(DebugOptions.Jinja, DebugOptions.Sudo); + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { debugOptions, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('debugOptions').to.be.deep.equal(debugOptions); + }); + }); + }); +}); From 459f842cf7f79ebb4753ddb6a4689faa01ae99ab Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 29 Mar 2018 10:19:44 -0700 Subject: [PATCH 04/24] :hammer: refactor --- .../DebugServers/RemoteDebugServerv2.ts | 8 ++ src/client/debugger/mainV2.ts | 78 ++++++++++--------- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/src/client/debugger/DebugServers/RemoteDebugServerv2.ts b/src/client/debugger/DebugServers/RemoteDebugServerv2.ts index 21b6b119e57c..5e52620fb57e 100644 --- a/src/client/debugger/DebugServers/RemoteDebugServerv2.ts +++ b/src/client/debugger/DebugServers/RemoteDebugServerv2.ts @@ -31,11 +31,19 @@ export class RemoteDebugServerV2 extends BaseDebugServer { (options).host = this.args.host; } try { + let connected = false; const socket = connect(options, () => { + connected = true; this.socket = socket; this.clientSocket.resolve(socket); resolve(options); }); + socket.on('error', ex => { + if (connected) { + return; + } + reject(ex); + }); } catch (ex) { reject(ex); } diff --git a/src/client/debugger/mainV2.ts b/src/client/debugger/mainV2.ts index f7f78c585732..295711ad6b5a 100644 --- a/src/client/debugger/mainV2.ts +++ b/src/client/debugger/mainV2.ts @@ -24,7 +24,6 @@ import { createDeferred, Deferred, isNotInstalledError } from '../common/helpers import { ICurrentProcess } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { AttachRequestArguments, LaunchRequestArguments } from './Common/Contracts'; -import { DebugClient } from './DebugClients/DebugClient'; import { CreateAttachDebugClient, CreateLaunchDebugClient } from './DebugClients/DebugFactory'; import { BaseDebugServer } from './DebugServers/BaseDebugServer'; import { initializeIoc } from './serviceRegistry'; @@ -44,7 +43,6 @@ const MIN_DEBUGGER_CONNECT_TIMEOUT = 5000; */ export class PythonDebugger extends DebugSession { public debugServer?: BaseDebugServer; - public debugClient?: DebugClient<{}>; public client = createDeferred(); private supportsRunInTerminalRequest: boolean = false; constructor(private readonly serviceContainer: IServiceContainer) { @@ -55,17 +53,8 @@ export class PythonDebugger extends DebugSession { this.debugServer.Stop(); this.debugServer = undefined; } - if (this.debugClient) { - this.debugClient.Stop(); - this.debugClient = undefined; - } super.shutdown(); } - public async createAttachDebugServer(attachRequest: DebugProtocol.AttachRequest) { - const launcher = CreateAttachDebugClient(attachRequest.arguments as AttachRequestArguments, this); - this.debugServer = launcher.CreateDebugServer(undefined, this.serviceContainer); - await this.debugServer!.Start(); - } protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { const body = response.body!; @@ -92,14 +81,24 @@ export class PythonDebugger extends DebugSession { this.sendResponse(response); } protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { - // We will let PTVSD send the response + const launcher = CreateAttachDebugClient(args as AttachRequestArguments, this); + this.debugServer = launcher.CreateDebugServer(undefined, this.serviceContainer); + this.debugServer!.Start() + .then(() => this.sendResponse(response)) + .catch(ex => { + logger.error('Attach failed'); + logger.error(`${ex}, ${ex.name}, ${ex.message}, ${ex.stack}`); + const message = this.getUserFriendlyAttachErrorMessage(ex) || 'Attach Failed'; + this.sendErrorResponse(response, { format: message, id: 1 }, undefined, undefined, ErrorDestination.User); + }); + } protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { this.launchPTVSD(args) .then(() => this.waitForPTVSDToConnect(args)) .then(() => this.sendResponse(response)) .catch(ex => { - const message = this.getErrorUserFriendlyMessage(args, ex) || 'Debug Error'; + const message = this.getUserFriendlyLaunchErrorMessage(args, ex) || 'Debug Error'; this.sendErrorResponse(response, { format: message, id: 1 }, undefined, undefined, ErrorDestination.User); }); } @@ -135,7 +134,7 @@ export class PythonDebugger extends DebugSession { const connectionTimeout = typeof (args as any).timeout === 'number' ? (args as any).timeout as number : DEBUGGER_CONNECT_TIMEOUT; return Math.max(connectionTimeout, MIN_DEBUGGER_CONNECT_TIMEOUT); } - private getErrorUserFriendlyMessage(launchArgs: LaunchRequestArguments, error: any): string | undefined { + private getUserFriendlyLaunchErrorMessage(launchArgs: LaunchRequestArguments, error: any): string | undefined { if (!error) { return; } @@ -146,6 +145,16 @@ export class PythonDebugger extends DebugSession { return errorMsg; } } + private getUserFriendlyAttachErrorMessage(error: any): string | undefined { + if (!error) { + return; + } + if (error.code === 'ECONNREFUSED' || error.errno === 'ECONNREFUSED') { + return `Failed to attach (${error.message})`; + } else { + return typeof error === 'string' ? error : ((error.message && error.message.length > 0) ? error.message : ''); + } + } } /** @@ -178,7 +187,7 @@ class DebugManager implements Disposable { private hasShutdown: boolean = false; private debugSession?: PythonDebugger; private ptvsdProcessId?: number; - private launchOrAttach?: 'launch'|'attach'; + private launchOrAttach?: 'launch' | 'attach'; private terminatedEventSent: boolean = false; private readonly initializeRequestDeferred: Deferred; private get initializeRequest(): Promise { @@ -189,6 +198,11 @@ class DebugManager implements Disposable { return this.launchRequestDeferred.promise; } + private readonly attachRequestDeferred: Deferred; + private get attachRequest(): Promise { + return this.attachRequestDeferred.promise; + } + private set loggingEnabled(value: boolean) { if (value) { logger.setup(LogLevel.Verbose, true); @@ -216,6 +230,7 @@ class DebugManager implements Disposable { this.initializeRequestDeferred = createDeferred(); this.launchRequestDeferred = createDeferred(); + this.attachRequestDeferred = createDeferred(); } public dispose() { this.shutdown().ignoreErrors(); @@ -317,25 +332,8 @@ class DebugManager implements Disposable { this.outputProtocolParser.once('event_terminated', this.onEventTerminated); this.outputProtocolParser.once('response_disconnect', this.onResponseDisconnect); - this.outputProtocolParser.once('response_launch', this.connectVSCodeToPTVSDForLaunch); - } - /** - * Connect PTVSD socket to VS Code. - * @private - * @memberof DebugManager - */ - private connectVSCodeToPTVSDForAttach = async (attachRequest: DebugProtocol.AttachRequest) => { - await this.debugSession!.createAttachDebugServer(attachRequest); - - await this.connectVSCodeToPTVSD(attachRequest, 'attach'); - } - /** - * Once PTVSD process has been started (done by DebugSession), we need to connect PTVSD socket to VS Code. - * @private - * @memberof DebugManager - */ - private connectVSCodeToPTVSDForLaunch = async () => { - await this.connectVSCodeToPTVSD(await this.launchRequest, 'launch'); + this.outputProtocolParser.once('response_launch', this.connectVSCodeToPTVSD); + this.outputProtocolParser.once('response_attach', this.connectVSCodeToPTVSD); } /** * Connect PTVSD socket to VS Code. @@ -343,7 +341,11 @@ class DebugManager implements Disposable { * @private * @memberof DebugManager */ - private connectVSCodeToPTVSD = async (attachOrLaunchRequest: DebugProtocol.AttachRequest | DebugProtocol.LaunchRequest, requestType: 'attach' | 'launch') => { + private connectVSCodeToPTVSD = async (response: DebugProtocol.AttachResponse | DebugProtocol.LaunchResponse) => { + if (!response || !response.success) { + return; + } + const attachOrLaunchRequest = await (this.launchOrAttach === 'attach' ? this.attachRequest : this.launchRequest); // By now we're connected to the client. this.ptvsdSocket = await this.debugSession!.debugServer!.client; @@ -354,10 +356,10 @@ class DebugManager implements Disposable { const debugSoketProtocolParser = this.serviceContainer.get(IProtocolParser); debugSoketProtocolParser.connect(this.ptvsdSocket); const initializedEventPromise = new Promise(resolve => debugSoketProtocolParser.once('event_initialized', resolve)); - const attachedOrLaunchedPromise = new Promise(resolve => debugSoketProtocolParser.once(`response_${requestType}`, resolve)); + const attachedOrLaunchedPromise = new Promise(resolve => debugSoketProtocolParser.once(`response_${this.launchOrAttach}`, resolve)); // Keep track of processid for killing it. - if (requestType === 'launch') { + if (this.launchOrAttach === 'launch') { debugSoketProtocolParser.once('event_process', (proc: DebugProtocol.ProcessEvent) => { this.ptvsdProcessId = proc.body.systemProcessId; }); @@ -395,7 +397,7 @@ class DebugManager implements Disposable { private onRequestAttach = (request: DebugProtocol.AttachRequest) => { this.launchOrAttach = 'attach'; this.loggingEnabled = (request.arguments as AttachRequestArguments).logToFile === true; - this.connectVSCodeToPTVSDForAttach(request).ignoreErrors(); + this.attachRequestDeferred.resolve(request); } private onEventTerminated = async () => { logger.verbose('onEventTerminated'); From 394bd96e683e4bb6766763e9e0d2fec0912c04d3 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 29 Mar 2018 16:14:21 -0700 Subject: [PATCH 05/24] :white_check_mark: tests for experimental debugger attach --- src/test/debugger/attach.ptvsd.test.ts | 136 ++++++++++++++++++ .../remoteDebugger-start-with-ptvsd.py | 14 ++ 2 files changed, 150 insertions(+) create mode 100644 src/test/debugger/attach.ptvsd.test.ts create mode 100644 src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.py diff --git a/src/test/debugger/attach.ptvsd.test.ts b/src/test/debugger/attach.ptvsd.test.ts new file mode 100644 index 000000000000..dda852d1d434 --- /dev/null +++ b/src/test/debugger/attach.ptvsd.test.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-this max-func-body-length no-empty no-increment-decrement + +import { expect } from 'chai'; +import { ChildProcess, spawn } from 'child_process'; +import * as getFreePort from 'get-port'; +import * as path from 'path'; +import { DebugClient } from 'vscode-debugadapter-testsupport'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import '../../client/common/extensions'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { sleep } from '../common'; +import { initialize, IS_APPVEYOR, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { DEBUGGER_TIMEOUT } from './common/constants'; +import { DebugClientEx } from './debugClient'; + +const fileToDebug = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'remoteDebugger-start-with-ptvsd.py'); +const ptvsdPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd'); +const DEBUG_ADAPTER = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'mainV2.js'); + +suite('Attach Debugger -Experimental', () => { + let debugClient: DebugClient; + let procToKill: ChildProcess; + suiteSetup(initialize); + + setup(async function () { + if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + this.skip(); + } + await sleep(1000); + debugClient = createDebugAdapter(); + debugClient.defaultTimeout = DEBUGGER_TIMEOUT; + await debugClient.start(); + }); + teardown(async () => { + // Wait for a second before starting another test (sometimes, sockets take a while to get closed). + await sleep(1000); + try { + await debugClient.stop().catch(() => { }); + } catch (ex) { } + if (procToKill) { + try { + procToKill.kill(); + } catch { } + } + }); + /** + * Creates the debug aimport { AttachRequestArguments } from '../../client/debugger/Common/Contracts'; + * We do not need to support code coverage on AppVeyor, lets use the standard test adapter. + * @returns {DebugClient} + */ + function createDebugAdapter(): DebugClient { + if (IS_WINDOWS) { + return new DebugClient('node', DEBUG_ADAPTER, 'pythonExperimental'); + } else { + const coverageDirectory = path.join(EXTENSION_ROOT_DIR, 'debug_coverage_attach_ptvsd'); + return new DebugClientEx(DEBUG_ADAPTER, 'pythonExperimental', coverageDirectory, { cwd: EXTENSION_ROOT_DIR }); + } + } + test('Confirm we are able to attach to a running program', async () => { + // Lets skip this test on AppVeyor (very flaky on AppVeyor). + if (IS_APPVEYOR) { + return; + } + + const port = await getFreePort({ host: 'localhost', port: 3000 }); + const customEnv = { ...process.env }; + + // Set the path for PTVSD to be picked up. + // tslint:disable-next-line:no-string-literal + customEnv['PYTHONPATH'] = ptvsdPath; + const pythonArgs = ['-m', 'ptvsd', '--server', '--port', `${port}`, '--file', fileToDebug.fileToCommandArgument()]; + procToKill = spawn('python', pythonArgs, { env: customEnv, cwd: path.dirname(fileToDebug) }); + + // Send initialize, attach + const initializePromise = debugClient.initializeRequest({ + adapterID: 'pythonExperimental', + linesStartAt1: true, + columnsStartAt1: true, + supportsRunInTerminalRequest: true, + pathFormat: 'path', + supportsVariableType: true, + supportsVariablePaging: true + }); + const attachPromise = debugClient.attachRequest({ + localRoot: path.dirname(fileToDebug), + remoteRoot: path.dirname(fileToDebug), + type: 'pythonExperimental', + port: port, + host: 'localhost', + logToFile: true, + debugOptions: ['RedirectOutput'] + }); + + await Promise.all([ + initializePromise, + attachPromise, + debugClient.waitForEvent('initialized') + ]); + + await debugClient.configurationDoneRequest(); + + const stdOutPromise = debugClient.assertOutput('stdout', 'this is stdout'); + const stdErrPromise = debugClient.assertOutput('stderr', 'this is stderr'); + + const breakpointLocation = { path: fileToDebug, column: 1, line: 12 }; + const breakpointPromise = debugClient.setBreakpointsRequest({ + lines: [breakpointLocation.line], + breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }], + source: { path: breakpointLocation.path } + }); + const exceptionBreakpointPromise = debugClient.setExceptionBreakpointsRequest({ filters: [] }); + await Promise.all([ + breakpointPromise, + exceptionBreakpointPromise, + stdOutPromise, stdErrPromise + ]); + + await debugClient.assertStoppedLocation('breakpoint', breakpointLocation); + + const threads = await debugClient.threadsRequest(); + expect(threads).to.be.not.equal(undefined, 'no threads response'); + expect(threads.body.threads).to.be.lengthOf(1); + + await Promise.all([ + debugClient.continueRequest({ threadId: threads.body.threads[0].id }), + debugClient.assertOutput('stdout', 'this is print'), + debugClient.waitForEvent('exited'), + debugClient.waitForEvent('terminated') + ]); + }); +}); diff --git a/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.py b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.py new file mode 100644 index 000000000000..ad8d7003cb6d --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.py @@ -0,0 +1,14 @@ +import sys +import time +time.sleep(2) +sys.stdout.write('this is stdout') +sys.stdout.flush() +sys.stderr.write('this is stderr') +sys.stderr.flush() +# Give the debugger some time to add a breakpoint. +time.sleep(5) +for i in range(1): + time.sleep(0.5) + pass + +print('this is print') From 22f6342667400ebf49543226325884de8339ebb3 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 29 Mar 2018 16:17:32 -0700 Subject: [PATCH 06/24] :hammer: increase timeout of this test --- src/test/debugger/attach.ptvsd.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/debugger/attach.ptvsd.test.ts b/src/test/debugger/attach.ptvsd.test.ts index dda852d1d434..bb9af518ea66 100644 --- a/src/test/debugger/attach.ptvsd.test.ts +++ b/src/test/debugger/attach.ptvsd.test.ts @@ -61,7 +61,8 @@ suite('Attach Debugger -Experimental', () => { return new DebugClientEx(DEBUG_ADAPTER, 'pythonExperimental', coverageDirectory, { cwd: EXTENSION_ROOT_DIR }); } } - test('Confirm we are able to attach to a running program', async () => { + test('Confirm we are able to attach to a running program', async function () { + this.timeout(20000); // Lets skip this test on AppVeyor (very flaky on AppVeyor). if (IS_APPVEYOR) { return; From bba9901861f963a93a335ae5a77adb1256662795 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 29 Mar 2018 16:34:22 -0700 Subject: [PATCH 07/24] :memo: change log [skip ci] --- news/1 Enhancements/1229.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/1 Enhancements/1229.md diff --git a/news/1 Enhancements/1229.md b/news/1 Enhancements/1229.md new file mode 100644 index 000000000000..1367edb3bbcc --- /dev/null +++ b/news/1 Enhancements/1229.md @@ -0,0 +1,2 @@ +Add prelimnary support for remote debugging using the experimental debugger. +Attach to a Python program started using the command `python -m ptvsd --server --port 9091 --file pythonFile.py` \ No newline at end of file From 020516322abc5c44048eb52ba11b9399f2efef00 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 29 Mar 2018 16:40:35 -0700 Subject: [PATCH 08/24] increase timeout for tests --- src/test/debugger/common/constants.ts | 2 +- src/test/debugger/misc.test.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/debugger/common/constants.ts b/src/test/debugger/common/constants.ts index 9be293352e34..a9bcc64f1a24 100644 --- a/src/test/debugger/common/constants.ts +++ b/src/test/debugger/common/constants.ts @@ -4,4 +4,4 @@ 'use strict'; // Sometimes PTVSD can take a while for thread & other events to be reported. -export const DEBUGGER_TIMEOUT = 10000; +export const DEBUGGER_TIMEOUT = 20000; diff --git a/src/test/debugger/misc.test.ts b/src/test/debugger/misc.test.ts index 90fc5669447f..00dc7cd9b402 100644 --- a/src/test/debugger/misc.test.ts +++ b/src/test/debugger/misc.test.ts @@ -445,7 +445,9 @@ let testCounter = 0; const pauseLocation = { path: path.join(debugFilesPath, 'sample3WithEx.py'), line: 5 }; await debugClient.assertStoppedLocation('exception', pauseLocation); }); - test('Test multi-threaded debugging', async () => { + test('Test multi-threaded debugging', async function () { + this.timeout(20000); + await Promise.all([ debugClient.configurationSequence(), debugClient.launch(buildLauncArgs('multiThread.py', false)), From e488c7c7516d7147d2e047a3f7aede4a3fd268d6 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 29 Mar 2018 23:48:28 -0700 Subject: [PATCH 09/24] add templates for attaching --- package.json | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b8b709a0e2f..6b26cd455fc6 100644 --- a/package.json +++ b/package.json @@ -874,7 +874,20 @@ "Pyramid" ] } - } + }, + { + "label": "%python.snippet.launch.attach.label%", + "description": "%python.snippet.launch.attach.description%", + "body": { + "name": "Attach (Remote Debug)", + "type": "pythonExperimental", + "request": "attach", + "localRoot": "^\"\\${workspaceFolder}\"", + "remoteRoot": "^\"\\${workspaceFolder}\"", + "port": 3000, + "host": "localhost" + } + } ], "configurationAttributes": { "launch": { @@ -1020,6 +1033,15 @@ "program": "${file}", "console": "integratedTerminal" }, + { + "name": "Python Experimental: Attach", + "type": "python", + "request": "pythonExperimental", + "localRoot": "${workspaceFolder}", + "remoteRoot": "${workspaceFolder}", + "port": 3000, + "host": "localhost" + }, { "name": "Python Experimental: Django", "type": "pythonExperimental", From 903b205335ddcd1942c60426c78ee461792bdcff Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 30 Mar 2018 00:04:26 -0700 Subject: [PATCH 10/24] change label [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b26cd455fc6..7ad2da532443 100644 --- a/package.json +++ b/package.json @@ -876,7 +876,7 @@ } }, { - "label": "%python.snippet.launch.attach.label%", + "label": "Python Experimental: Attach", "description": "%python.snippet.launch.attach.description%", "body": { "name": "Attach (Remote Debug)", From 7cce312c84d0505c2a3aa074b36abec5af2003bb Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 30 Mar 2018 00:09:07 -0700 Subject: [PATCH 11/24] chane type [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7ad2da532443..294fe364ba49 100644 --- a/package.json +++ b/package.json @@ -1035,7 +1035,7 @@ }, { "name": "Python Experimental: Attach", - "type": "python", + "type": "pythonExperimental", "request": "pythonExperimental", "localRoot": "${workspaceFolder}", "remoteRoot": "${workspaceFolder}", From 673ef14b6ff68eb4d9b154dcb28bda92906ba1fd Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 30 Mar 2018 16:42:17 -0700 Subject: [PATCH 12/24] improvements tests (temporary workaround due to PTVSD issues) --- src/test/debugger/misc.test.ts | 58 ++++++++++++++++--- src/test/pythonFiles/debugging/multiThread.py | 2 +- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/test/debugger/misc.test.ts b/src/test/debugger/misc.test.ts index 00dc7cd9b402..05b67f77898f 100644 --- a/src/test/debugger/misc.test.ts +++ b/src/test/debugger/misc.test.ts @@ -3,8 +3,7 @@ // tslint:disable:no-suspicious-comment max-func-body-length no-invalid-this no-var-requires no-require-imports no-any -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; +import { expect } from 'chai'; import * as path from 'path'; import { ThreadEvent } from 'vscode-debugadapter'; import { DebugClient } from 'vscode-debugadapter-testsupport'; @@ -22,8 +21,6 @@ import { DebugClientEx } from './debugClient'; const isProcessRunning = require('is-running') as (number) => boolean; -use(chaiAsPromised); - const debugFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'debugging'); const DEBUG_ADAPTER = path.join(__dirname, '..', '..', 'client', 'debugger', 'Main.js'); @@ -446,13 +443,19 @@ let testCounter = 0; await debugClient.assertStoppedLocation('exception', pauseLocation); }); test('Test multi-threaded debugging', async function () { - this.timeout(20000); - + if (debuggerType !== 'python') { + // See GitHub issue #1250 + this.skip(); + return; + } await Promise.all([ debugClient.configurationSequence(), debugClient.launch(buildLauncArgs('multiThread.py', false)), debugClient.waitForEvent('initialized') ]); + + // Add a delay for debugger to start (sometimes it takes a long time for new debugger to break). + await sleep(3000); const pythonFile = path.join(debugFilesPath, 'multiThread.py'); const breakpointLocation = { path: pythonFile, column: 1, line: 11 }; await debugClient.setBreakpointsRequest({ @@ -461,8 +464,49 @@ let testCounter = 0; source: { path: breakpointLocation.path } }); - // hit breakpoint. await debugClient.assertStoppedLocation('breakpoint', breakpointLocation); + const threads = await debugClient.threadsRequest(); + expect(threads.body.threads).of.lengthOf(2, 'incorrect number of threads'); + for (const thread of threads.body.threads) { + expect(thread.id).to.be.lessThan(MAX_SIGNED_INT32 + 1, 'ThreadId is not an integer'); + } + }); + test('Test multi-threaded debugging', async function () { + this.timeout(30000); + await Promise.all([ + debugClient.launch(buildLauncArgs('multiThread.py', false)), + debugClient.waitForEvent('initialized') + ]); + + const pythonFile = path.join(debugFilesPath, 'multiThread.py'); + const breakpointLocation = { path: pythonFile, column: 1, line: 11 }; + const breakpointRequestArgs = { + lines: [breakpointLocation.line], + breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }], + source: { path: breakpointLocation.path } + }; + + function waitForStoppedEventFromTwoThreads() { + return new Promise((resolve, reject) => { + let numberOfStops = 0; + debugClient.addListener('stopped', (event: DebugProtocol.StoppedEvent) => { + numberOfStops += 1; + if (numberOfStops < 2) { + return; + } + resolve(event); + }); + setTimeout(() => reject(new Error('Timeout waiting for two threads to stop at breakpoint')), DEBUGGER_TIMEOUT); + }); + } + + await Promise.all([ + debugClient.setBreakpointsRequest(breakpointRequestArgs), + debugClient.setExceptionBreakpointsRequest({ filters: [] }), + debugClient.configurationDoneRequest(), + waitForStoppedEventFromTwoThreads(), + debugClient.assertStoppedLocation('breakpoint', breakpointLocation) + ]); const threads = await debugClient.threadsRequest(); expect(threads.body.threads).of.lengthOf(2, 'incorrect number of threads'); diff --git a/src/test/pythonFiles/debugging/multiThread.py b/src/test/pythonFiles/debugging/multiThread.py index 707f2568a2da..588971ffb502 100644 --- a/src/test/pythonFiles/debugging/multiThread.py +++ b/src/test/pythonFiles/debugging/multiThread.py @@ -4,7 +4,7 @@ def bar(): time.sleep(2) - print("abcdef") + print('bar') def foo(x): while True: From 5d2c59a5b44dabf6ba897e703f175d5b6d9f6438 Mon Sep 17 00:00:00 2001 From: Mikhail Arkhipov Date: Wed, 28 Mar 2018 10:11:55 -0700 Subject: [PATCH 13/24] Fix spacing of general (non-specific) tokens + tests (#1222) Fixes #1096 --- src/client/formatters/lineFormatter.ts | 98 +++++++++++++++---- .../format/extension.lineFormatter.test.ts | 16 +++ .../format/extension.onEnterFormat.test.ts | 22 ++++- .../formatting/fileToFormatOnEnter.py | 4 + 4 files changed, 118 insertions(+), 22 deletions(-) diff --git a/src/client/formatters/lineFormatter.ts b/src/client/formatters/lineFormatter.ts index 4b3bff70aa8d..fc347235a525 100644 --- a/src/client/formatters/lineFormatter.ts +++ b/src/client/formatters/lineFormatter.ts @@ -5,14 +5,15 @@ import Char from 'typescript-char'; import { BraceCounter } from '../language/braceCounter'; import { TextBuilder } from '../language/textBuilder'; +import { TextRangeCollection } from '../language/textRangeCollection'; import { Tokenizer } from '../language/tokenizer'; import { ITextRangeCollection, IToken, TokenType } from '../language/types'; export class LineFormatter { - private builder: TextBuilder; - private tokens: ITextRangeCollection; - private braceCounter: BraceCounter; - private text: string; + private builder = new TextBuilder(); + private tokens: ITextRangeCollection = new TextRangeCollection([]); + private braceCounter = new BraceCounter(); + private text = ''; // tslint:disable-next-line:cyclomatic-complexity public formatLine(text: string): string { @@ -27,7 +28,7 @@ export class LineFormatter { const ws = this.text.substr(0, this.tokens.getItemAt(0).start); if (ws.length > 0) { - this.builder.append(ws); // Preserve leading indentation + this.builder.append(ws); // Preserve leading indentation. } for (let i = 0; i < this.tokens.count; i += 1) { @@ -55,24 +56,28 @@ export class LineFormatter { break; case TokenType.Colon: - // x: 1 if not in slice, x[1:y] if inside the slice + // x: 1 if not in slice, x[1:y] if inside the slice. this.builder.append(':'); if (!this.braceCounter.isOpened(TokenType.OpenBracket) && (next && next.type !== TokenType.Colon)) { - // Not inside opened [[ ... ] sequence + // Not inside opened [[ ... ] sequence. this.builder.softAppendSpace(); } break; case TokenType.Comment: - // add space before in-line comment + // Add space before in-line comment. if (prev) { this.builder.softAppendSpace(); } this.builder.append(this.text.substring(t.start, t.end)); break; + case TokenType.Semicolon: + this.builder.append(';'); + break; + default: - this.handleOther(t); + this.handleOther(t, i); break; } } @@ -85,7 +90,7 @@ export class LineFormatter { const opCode = this.text.charCodeAt(t.start); switch (opCode) { case Char.Equal: - if (index >= 2 && this.handleEqual(t, index)) { + if (this.handleEqual(t, index)) { return; } break; @@ -105,27 +110,66 @@ export class LineFormatter { } private handleEqual(t: IToken, index: number): boolean { - if (this.braceCounter.isOpened(TokenType.OpenBrace)) { - // Check if this is = in function arguments. If so, do not - // add spaces around it. - const prev = this.tokens.getItemAt(index - 1); - const prevPrev = this.tokens.getItemAt(index - 2); - if (prev.type === TokenType.Identifier && - (prevPrev.type === TokenType.Comma || prevPrev.type === TokenType.OpenBrace)) { - this.builder.append('='); - return true; - } + if (this.isMultipleStatements(index) && !this.braceCounter.isOpened(TokenType.OpenBrace)) { + return false; // x = 1; x, y = y, x + } + // Check if this is = in function arguments. If so, do not add spaces around it. + if (this.isEqualsInsideArguments(index)) { + this.builder.append('='); + return true; } return false; } - private handleOther(t: IToken): void { + private handleOther(t: IToken, index: number): void { if (this.isBraceType(t.type)) { this.braceCounter.countBrace(t); + this.builder.append(this.text.substring(t.start, t.end)); + return; } + + if (this.isEqualsInsideArguments(index - 1)) { + // Don't add space around = inside function arguments. + this.builder.append(this.text.substring(t.start, t.end)); + return; + } + + if (index > 0) { + const prev = this.tokens.getItemAt(index - 1); + if (this.isOpenBraceType(prev.type) || prev.type === TokenType.Colon) { + // Don't insert space after (, [ or { . + this.builder.append(this.text.substring(t.start, t.end)); + return; + } + } + + // In general, keep tokens separated. + this.builder.softAppendSpace(); this.builder.append(this.text.substring(t.start, t.end)); } + private isEqualsInsideArguments(index: number): boolean { + if (index < 1) { + return false; + } + const prev = this.tokens.getItemAt(index - 1); + if (prev.type === TokenType.Identifier) { + if (index >= 2) { + // (x=1 or ,x=1 + const prevPrev = this.tokens.getItemAt(index - 2); + return prevPrev.type === TokenType.Comma || prevPrev.type === TokenType.OpenBrace; + } else if (index < this.tokens.count - 2) { + const next = this.tokens.getItemAt(index + 1); + const nextNext = this.tokens.getItemAt(index + 2); + // x=1, or x=1) + if (this.isValueType(next.type)) { + return nextNext.type === TokenType.Comma || nextNext.type === TokenType.CloseBrace; + } + } + } + return false; + } + private isOpenBraceType(type: TokenType): boolean { return type === TokenType.OpenBrace || type === TokenType.OpenBracket || type === TokenType.OpenCurly; } @@ -135,4 +179,16 @@ export class LineFormatter { private isBraceType(type: TokenType): boolean { return this.isOpenBraceType(type) || this.isCloseBraceType(type); } + private isValueType(type: TokenType): boolean { + return type === TokenType.Identifier || type === TokenType.Unknown || + type === TokenType.Number || type === TokenType.String; + } + private isMultipleStatements(index: number): boolean { + for (let i = index; i >= 0; i -= 1) { + if (this.tokens.getItemAt(i).type === TokenType.Semicolon) { + return true; + } + } + return false; + } } diff --git a/src/test/format/extension.lineFormatter.test.ts b/src/test/format/extension.lineFormatter.test.ts index 842cb02d735d..79de72c5774a 100644 --- a/src/test/format/extension.lineFormatter.test.ts +++ b/src/test/format/extension.lineFormatter.test.ts @@ -65,4 +65,20 @@ suite('Formatting - line formatter', () => { const actual = formatter.formatLine(' # comment'); assert.equal(actual, ' # comment'); }); + test('Equals in first argument', () => { + const actual = formatter.formatLine('foo(x =0)'); + assert.equal(actual, 'foo(x=0)'); + }); + test('Equals in second argument', () => { + const actual = formatter.formatLine('foo(x,y= \"a\",'); + assert.equal(actual, 'foo(x, y=\"a\",'); + }); + test('Equals in multiline arguments', () => { + const actual = formatter.formatLine('x = 1,y =-2)'); + assert.equal(actual, 'x=1, y=-2)'); + }); + test('Equals in multiline arguments starting comma', () => { + const actual = formatter.formatLine(',x = 1,y =m)'); + assert.equal(actual, ', x=1, y=m)'); + }); }); diff --git a/src/test/format/extension.onEnterFormat.test.ts b/src/test/format/extension.onEnterFormat.test.ts index 74597ce19be7..8f594d5e2559 100644 --- a/src/test/format/extension.onEnterFormat.test.ts +++ b/src/test/format/extension.onEnterFormat.test.ts @@ -59,8 +59,28 @@ suite('Formatting - OnEnter provider', () => { assert.equal(text, 'x.y', 'Line ending with period was reformatted'); }); - test('Formatting line ending in string', async () => { + test('Formatting line with unknown neighboring tokens', async () => { const text = await formatAtPosition(9, 0); + assert.equal(text, 'if x <= 1:', 'Line with unknown neighboring tokens was not formatted'); + }); + + test('Formatting line with unknown neighboring tokens', async () => { + const text = await formatAtPosition(10, 0); + assert.equal(text, 'if 1 <= x:', 'Line with unknown neighboring tokens was not formatted'); + }); + + test('Formatting method definition with arguments', async () => { + const text = await formatAtPosition(11, 0); + assert.equal(text, 'def __init__(self, age=23)', 'Method definition with arguments was not formatted'); + }); + + test('Formatting space after open brace', async () => { + const text = await formatAtPosition(12, 0); + assert.equal(text, 'while(1)', 'Space after open brace was not formatted'); + }); + + test('Formatting line ending in string', async () => { + const text = await formatAtPosition(13, 0); assert.equal(text, 'x + """', 'Line ending in multiline string was not formatted'); }); diff --git a/src/test/pythonFiles/formatting/fileToFormatOnEnter.py b/src/test/pythonFiles/formatting/fileToFormatOnEnter.py index bbd025363098..3bbd1be73501 100644 --- a/src/test/pythonFiles/formatting/fileToFormatOnEnter.py +++ b/src/test/pythonFiles/formatting/fileToFormatOnEnter.py @@ -6,4 +6,8 @@ x+1 # @x x.y +if x<=1: +if 1<=x: +def __init__(self, age = 23) +while(1) x+""" From c0d254e6175066d173e27b22bf7273d182addc58 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 28 Mar 2018 10:17:03 -0700 Subject: [PATCH 14/24] RC update (#1220) --- CHANGELOG.md | 46 ++++++++++++++++++++++++++++++++++++++++++---- package.json | 2 +- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40bb6b4b5f3c..0e27347650ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 2018.3.0-beta (19 Mar 2018) +## 2018.3.0-rc (27 Mar 2018) ### Enhancements @@ -12,8 +12,19 @@ ([#1031](https://github.com/Microsoft/vscode-python/issues/1031)) 1. Add a Scrapy debug configuration for the experimental debugger. ([#1032](https://github.com/Microsoft/vscode-python/issues/1032)) +1. When using pipenv, install packages (such as linters, test frameworks) in dev-packages. + ([#1110](https://github.com/Microsoft/vscode-python/issues/1110)) +1. Added commands translation for italian locale. +(thanks [Dotpys](https://github.com/Dotpys/)) ([#1152](https://github.com/Microsoft/vscode-python/issues/1152)) +1. Add support for Django Template debugging in experimental debugger. + ([#1189](https://github.com/Microsoft/vscode-python/issues/1189)) +1. Add support for Flask Template debugging in experimental debugger. + ([#1190](https://github.com/Microsoft/vscode-python/issues/1190)) +1. Add support for Jinja template debugging. ([#1210](https://github.com/Microsoft/vscode-python/issues/1210)) 1. When debugging, use `Integrated Terminal` as the default console. ([#526](https://github.com/Microsoft/vscode-python/issues/526)) +1. Disable the display of errors messages when rediscovering of tests fail in response to changes to files, e.g. don't show a message if there's a syntax error in the test code. + ([#704](https://github.com/Microsoft/vscode-python/issues/704)) 1. Bundle python depedencies (PTVSD package) in the extension for the experimental debugger. ([#741](https://github.com/Microsoft/vscode-python/issues/741)) 1. Add support for expermental debugger when debugging Python Unit Tests. @@ -21,17 +32,28 @@ 1. Support `Debug Console` as a `console` option for the Experimental Debugger. ([#950](https://github.com/Microsoft/vscode-python/issues/950)) 1. Enable syntax highlighting for `requirements.in` files as used by -e.g. [pip-tools](https://github.com/jazzband/pip-tools). +e.g. [pip-tools](https://github.com/jazzband/pip-tools) +(thanks [Lorenzo Villani](https://github.com/lvillani)) ([#961](https://github.com/Microsoft/vscode-python/issues/961)) 1. Add support to read name of Pipfile from environment variable. ([#999](https://github.com/Microsoft/vscode-python/issues/999)) ### Fixes +1. Fixes issue that causes debugging of unit tests to hang indefinitely. ([#1009](https://github.com/Microsoft/vscode-python/issues/1009)) +1. Add ability to disable the check on memory usage of language server (Jedi) process. +To turn off this check, add `"python.jediMemoryLimit": -1` to your user or workspace settings (`settings.json`) file. + ([#1036](https://github.com/Microsoft/vscode-python/issues/1036)) 1. Ignore test results when debugging unit tests. ([#1043](https://github.com/Microsoft/vscode-python/issues/1043)) +1. Fixes auto formatting of conditional statements containing expressions with `<=` symbols. + ([#1096](https://github.com/Microsoft/vscode-python/issues/1096)) 1. Resolve debug configuration information in `launch.json` when debugging without opening a python file. ([#1098](https://github.com/Microsoft/vscode-python/issues/1098)) +1. Disables auto completion when editing text at the end of a comment string. + ([#1123](https://github.com/Microsoft/vscode-python/issues/1123)) +1. Ensures file paths are properly encoded when passing them as arguments to linters. + ([#199](https://github.com/Microsoft/vscode-python/issues/199)) 1. Fix occasionally having unverified breakpoints ([#87](https://github.com/Microsoft/vscode-python/issues/87)) 1. Ensure conda installer is not used for non-conda environments. @@ -44,6 +66,7 @@ e.g. [pip-tools](https://github.com/jazzband/pip-tools). 1. Exclude 'news' folder from getting packaged into the extension. ([#1020](https://github.com/Microsoft/vscode-python/issues/1020)) 1. Remove Jupyter commands. +(thanks [Yu Zhang](https://github.com/neilsustc)) ([#1034](https://github.com/Microsoft/vscode-python/issues/1034)) 1. Trigger incremental build compilation only when typescript files are modified. ([#1040](https://github.com/Microsoft/vscode-python/issues/1040)) @@ -52,22 +75,37 @@ e.g. [pip-tools](https://github.com/jazzband/pip-tools). 1. Enable unit testing of stdout and stderr redirection for the experimental debugger. ([#1048](https://github.com/Microsoft/vscode-python/issues/1048)) 1. Update npm package `vscode-extension-telemetry` to fix the warning 'os.tmpDir() deprecation'. +(thanks [osya](https://github.com/osya)) ([#1066](https://github.com/Microsoft/vscode-python/issues/1066)) -1. Prevent debugger stepping into js code, when debugging async TypeScript code. +1. Prevent the debugger stepping into JS code while developing the extension when debugging async TypeScript code. ([#1090](https://github.com/Microsoft/vscode-python/issues/1090)) 1. Increase timeouts for the debugger unit tests. ([#1094](https://github.com/Microsoft/vscode-python/issues/1094)) 1. Change the command used to install pip on AppVeyor to avoid installation errors. ([#1107](https://github.com/Microsoft/vscode-python/issues/1107)) +1. Check whether a document is active when detecthing changes in the active document. + ([#1114](https://github.com/Microsoft/vscode-python/issues/1114)) +1. Remove SIGINT handler in debugger adapter, thereby preventing it from shutting down the debugger. + ([#1122](https://github.com/Microsoft/vscode-python/issues/1122)) +1. Improve compilation speed of the extension's TypeScript code. + ([#1146](https://github.com/Microsoft/vscode-python/issues/1146)) +1. Changes to how debug options are passed into the experimental version of PTVSD (debugger). + ([#1168](https://github.com/Microsoft/vscode-python/issues/1168)) +1. Ensure file paths are not sent in telemetry when running unit tests. + ([#1180](https://github.com/Microsoft/vscode-python/issues/1180)) +1. Change `DjangoDebugging` to `Django` in `debugOptions` of launch.json. + ([#1198](https://github.com/Microsoft/vscode-python/issues/1198)) +1. Changed property name used to capture the trigger source of Unit Tests. ([#1213](https://github.com/Microsoft/vscode-python/issues/1213)) 1. Enable unit testing of the experimental debugger on CI servers ([#742](https://github.com/Microsoft/vscode-python/issues/742)) 1. Generate code coverage for debug adapter unit tests. ([#778](https://github.com/Microsoft/vscode-python/issues/778)) 1. Execute prospector as a module (using -m). ([#982](https://github.com/Microsoft/vscode-python/issues/982)) -1. Launch the unit tests in debug mode as opposed to running and attaching the debugger. +1. Launch unit tests in debug mode as opposed to running and attaching the debugger to the already-running interpreter. ([#983](https://github.com/Microsoft/vscode-python/issues/983)) + ## 2018.2.1 (09 Mar 2018) ### Fixes diff --git a/package.json b/package.json index 294fe364ba49..13f52168ad31 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Linting, Debugging (multi-threaded, remote), Intellisense, code formatting, refactoring, unit tests, snippets, and more.", - "version": "2018.3.0-beta", + "version": "2018.3.0-rc", "publisher": "ms-python", "author": { "name": "Microsoft Corporation" From 0ac5101e3638be5b57fb99c0a54dd10afe728d47 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 29 Mar 2018 10:18:58 -0700 Subject: [PATCH 15/24] 2018.3.0 release details (#1227) --- CHANGELOG.md | 3 +-- news/1 Enhancements/1029.md | 1 - news/1 Enhancements/1030.md | 1 - news/1 Enhancements/1031.md | 1 - news/1 Enhancements/1032.md | 1 - news/1 Enhancements/1110.md | 1 - news/1 Enhancements/1152.md | 2 -- news/1 Enhancements/1189.md | 1 - news/1 Enhancements/1190.md | 1 - news/1 Enhancements/1210.md | 1 - news/1 Enhancements/526.md | 1 - news/1 Enhancements/704.md | 1 - news/1 Enhancements/741.md | 1 - news/1 Enhancements/906.md | 1 - news/1 Enhancements/950.md | 1 - news/1 Enhancements/961.md | 3 --- news/1 Enhancements/999.md | 1 - news/2 Fixes/1009.md | 1 - news/2 Fixes/1036.md | 2 -- news/2 Fixes/1043.md | 1 - news/2 Fixes/1096.md | 1 - news/2 Fixes/1098.md | 1 - news/2 Fixes/1123.md | 1 - news/2 Fixes/199.md | 1 - news/2 Fixes/87.md | 1 - news/2 Fixes/969.md | 1 - news/2 Fixes/981.md | 1 - news/3 Code Health/1020.md | 1 - news/3 Code Health/1034.md | 2 -- news/3 Code Health/1040.md | 1 - news/3 Code Health/1042.md | 1 - news/3 Code Health/1048.md | 1 - news/3 Code Health/1066.md | 2 -- news/3 Code Health/1090.md | 1 - news/3 Code Health/1094.md | 1 - news/3 Code Health/1107.md | 1 - news/3 Code Health/1114.md | 1 - news/3 Code Health/1122.md | 1 - news/3 Code Health/1146.md | 1 - news/3 Code Health/1168.md | 1 - news/3 Code Health/1180.md | 1 - news/3 Code Health/1198.md | 1 - news/3 Code Health/1213.md | 1 - news/3 Code Health/742.md | 1 - news/3 Code Health/778.md | 1 - news/3 Code Health/982.md | 1 - news/3 Code Health/983.md | 1 - news/announce.py | 9 +++++++-- package.json | 2 +- 49 files changed, 9 insertions(+), 57 deletions(-) delete mode 100644 news/1 Enhancements/1029.md delete mode 100644 news/1 Enhancements/1030.md delete mode 100644 news/1 Enhancements/1031.md delete mode 100644 news/1 Enhancements/1032.md delete mode 100644 news/1 Enhancements/1110.md delete mode 100644 news/1 Enhancements/1152.md delete mode 100644 news/1 Enhancements/1189.md delete mode 100644 news/1 Enhancements/1190.md delete mode 100644 news/1 Enhancements/1210.md delete mode 100644 news/1 Enhancements/526.md delete mode 100644 news/1 Enhancements/704.md delete mode 100644 news/1 Enhancements/741.md delete mode 100644 news/1 Enhancements/906.md delete mode 100644 news/1 Enhancements/950.md delete mode 100644 news/1 Enhancements/961.md delete mode 100644 news/1 Enhancements/999.md delete mode 100644 news/2 Fixes/1009.md delete mode 100644 news/2 Fixes/1036.md delete mode 100644 news/2 Fixes/1043.md delete mode 100644 news/2 Fixes/1096.md delete mode 100644 news/2 Fixes/1098.md delete mode 100644 news/2 Fixes/1123.md delete mode 100644 news/2 Fixes/199.md delete mode 100644 news/2 Fixes/87.md delete mode 100644 news/2 Fixes/969.md delete mode 100644 news/2 Fixes/981.md delete mode 100644 news/3 Code Health/1020.md delete mode 100644 news/3 Code Health/1034.md delete mode 100644 news/3 Code Health/1040.md delete mode 100644 news/3 Code Health/1042.md delete mode 100644 news/3 Code Health/1048.md delete mode 100644 news/3 Code Health/1066.md delete mode 100644 news/3 Code Health/1090.md delete mode 100644 news/3 Code Health/1094.md delete mode 100644 news/3 Code Health/1107.md delete mode 100644 news/3 Code Health/1114.md delete mode 100644 news/3 Code Health/1122.md delete mode 100644 news/3 Code Health/1146.md delete mode 100644 news/3 Code Health/1168.md delete mode 100644 news/3 Code Health/1180.md delete mode 100644 news/3 Code Health/1198.md delete mode 100644 news/3 Code Health/1213.md delete mode 100644 news/3 Code Health/742.md delete mode 100644 news/3 Code Health/778.md delete mode 100644 news/3 Code Health/982.md delete mode 100644 news/3 Code Health/983.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e27347650ed..358776c756f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 2018.3.0-rc (27 Mar 2018) +## 2018.3.0 (28 Mar 2018) ### Enhancements @@ -105,7 +105,6 @@ To turn off this check, add `"python.jediMemoryLimit": -1` to your user or works 1. Launch unit tests in debug mode as opposed to running and attaching the debugger to the already-running interpreter. ([#983](https://github.com/Microsoft/vscode-python/issues/983)) - ## 2018.2.1 (09 Mar 2018) ### Fixes diff --git a/news/1 Enhancements/1029.md b/news/1 Enhancements/1029.md deleted file mode 100644 index 9da8383fe46d..000000000000 --- a/news/1 Enhancements/1029.md +++ /dev/null @@ -1 +0,0 @@ -Add a PySpark debug configuration for the experimental debugger. diff --git a/news/1 Enhancements/1030.md b/news/1 Enhancements/1030.md deleted file mode 100644 index 5822cd9b67ee..000000000000 --- a/news/1 Enhancements/1030.md +++ /dev/null @@ -1 +0,0 @@ -Add a Pyramid debug configuration for the experimental debugger. diff --git a/news/1 Enhancements/1031.md b/news/1 Enhancements/1031.md deleted file mode 100644 index 299a112ecfcf..000000000000 --- a/news/1 Enhancements/1031.md +++ /dev/null @@ -1 +0,0 @@ -Add a Watson debug configuration for the experimental debugger. diff --git a/news/1 Enhancements/1032.md b/news/1 Enhancements/1032.md deleted file mode 100644 index d7d5cb280e36..000000000000 --- a/news/1 Enhancements/1032.md +++ /dev/null @@ -1 +0,0 @@ -Add a Scrapy debug configuration for the experimental debugger. diff --git a/news/1 Enhancements/1110.md b/news/1 Enhancements/1110.md deleted file mode 100644 index 95572f4644ec..000000000000 --- a/news/1 Enhancements/1110.md +++ /dev/null @@ -1 +0,0 @@ -When using pipenv, install packages (such as linters, test frameworks) in dev-packages. diff --git a/news/1 Enhancements/1152.md b/news/1 Enhancements/1152.md deleted file mode 100644 index 25c2dcea67cb..000000000000 --- a/news/1 Enhancements/1152.md +++ /dev/null @@ -1,2 +0,0 @@ -Added commands translation for italian locale. -(thanks [Dotpys](https://github.com/Dotpys/)) \ No newline at end of file diff --git a/news/1 Enhancements/1189.md b/news/1 Enhancements/1189.md deleted file mode 100644 index 6d306e55189c..000000000000 --- a/news/1 Enhancements/1189.md +++ /dev/null @@ -1 +0,0 @@ -Add support for Django Template debugging in experimental debugger. diff --git a/news/1 Enhancements/1190.md b/news/1 Enhancements/1190.md deleted file mode 100644 index a1b144662fa5..000000000000 --- a/news/1 Enhancements/1190.md +++ /dev/null @@ -1 +0,0 @@ -Add support for Flask Template debugging in experimental debugger. diff --git a/news/1 Enhancements/1210.md b/news/1 Enhancements/1210.md deleted file mode 100644 index 68b06658e94b..000000000000 --- a/news/1 Enhancements/1210.md +++ /dev/null @@ -1 +0,0 @@ -Add support for Jinja template debugging. \ No newline at end of file diff --git a/news/1 Enhancements/526.md b/news/1 Enhancements/526.md deleted file mode 100644 index 829c4ac990f7..000000000000 --- a/news/1 Enhancements/526.md +++ /dev/null @@ -1 +0,0 @@ -When debugging, use `Integrated Terminal` as the default console. diff --git a/news/1 Enhancements/704.md b/news/1 Enhancements/704.md deleted file mode 100644 index 49f1c4d9a3b8..000000000000 --- a/news/1 Enhancements/704.md +++ /dev/null @@ -1 +0,0 @@ -Disable the display of errors messages when rediscovering of tests fail in response to changes to files, e.g. don't show a message if there's a syntax error in the test code. diff --git a/news/1 Enhancements/741.md b/news/1 Enhancements/741.md deleted file mode 100644 index 329ff07728af..000000000000 --- a/news/1 Enhancements/741.md +++ /dev/null @@ -1 +0,0 @@ -Bundle python depedencies (PTVSD package) in the extension for the experimental debugger. diff --git a/news/1 Enhancements/906.md b/news/1 Enhancements/906.md deleted file mode 100644 index 95569b479d9e..000000000000 --- a/news/1 Enhancements/906.md +++ /dev/null @@ -1 +0,0 @@ -Add support for expermental debugger when debugging Python Unit Tests. diff --git a/news/1 Enhancements/950.md b/news/1 Enhancements/950.md deleted file mode 100644 index bba2053af7fa..000000000000 --- a/news/1 Enhancements/950.md +++ /dev/null @@ -1 +0,0 @@ -Support `Debug Console` as a `console` option for the Experimental Debugger. diff --git a/news/1 Enhancements/961.md b/news/1 Enhancements/961.md deleted file mode 100644 index d2bf1576d4ab..000000000000 --- a/news/1 Enhancements/961.md +++ /dev/null @@ -1,3 +0,0 @@ -Enable syntax highlighting for `requirements.in` files as used by -e.g. [pip-tools](https://github.com/jazzband/pip-tools) -(thanks [Lorenzo Villani](https://github.com/lvillani)) diff --git a/news/1 Enhancements/999.md b/news/1 Enhancements/999.md deleted file mode 100644 index c215b7aadd31..000000000000 --- a/news/1 Enhancements/999.md +++ /dev/null @@ -1 +0,0 @@ -Add support to read name of Pipfile from environment variable. diff --git a/news/2 Fixes/1009.md b/news/2 Fixes/1009.md deleted file mode 100644 index 665fdfc9e709..000000000000 --- a/news/2 Fixes/1009.md +++ /dev/null @@ -1 +0,0 @@ -Fixes issue that causes debugging of unit tests to hang indefinitely. \ No newline at end of file diff --git a/news/2 Fixes/1036.md b/news/2 Fixes/1036.md deleted file mode 100644 index 55800cca1e6a..000000000000 --- a/news/2 Fixes/1036.md +++ /dev/null @@ -1,2 +0,0 @@ -Add ability to disable the check on memory usage of language server (Jedi) process. -To turn off this check, add `"python.jediMemoryLimit": -1` to your user or workspace settings (`settings.json`) file. diff --git a/news/2 Fixes/1043.md b/news/2 Fixes/1043.md deleted file mode 100644 index cf0541628a52..000000000000 --- a/news/2 Fixes/1043.md +++ /dev/null @@ -1 +0,0 @@ -Ignore test results when debugging unit tests. diff --git a/news/2 Fixes/1096.md b/news/2 Fixes/1096.md deleted file mode 100644 index d4a4500b38bc..000000000000 --- a/news/2 Fixes/1096.md +++ /dev/null @@ -1 +0,0 @@ -Fixes auto formatting of conditional statements containing expressions with `<=` symbols. diff --git a/news/2 Fixes/1098.md b/news/2 Fixes/1098.md deleted file mode 100644 index d7b5b5e35ef1..000000000000 --- a/news/2 Fixes/1098.md +++ /dev/null @@ -1 +0,0 @@ -Resolve debug configuration information in `launch.json` when debugging without opening a python file. diff --git a/news/2 Fixes/1123.md b/news/2 Fixes/1123.md deleted file mode 100644 index 1a23a845690b..000000000000 --- a/news/2 Fixes/1123.md +++ /dev/null @@ -1 +0,0 @@ -Disables auto completion when editing text at the end of a comment string. diff --git a/news/2 Fixes/199.md b/news/2 Fixes/199.md deleted file mode 100644 index 94f155e516b8..000000000000 --- a/news/2 Fixes/199.md +++ /dev/null @@ -1 +0,0 @@ -Ensures file paths are properly encoded when passing them as arguments to linters. diff --git a/news/2 Fixes/87.md b/news/2 Fixes/87.md deleted file mode 100644 index c024086fd1d3..000000000000 --- a/news/2 Fixes/87.md +++ /dev/null @@ -1 +0,0 @@ -Fix occasionally having unverified breakpoints diff --git a/news/2 Fixes/969.md b/news/2 Fixes/969.md deleted file mode 100644 index a991de1a919d..000000000000 --- a/news/2 Fixes/969.md +++ /dev/null @@ -1 +0,0 @@ -Ensure conda installer is not used for non-conda environments. diff --git a/news/2 Fixes/981.md b/news/2 Fixes/981.md deleted file mode 100644 index 8621551fbbd4..000000000000 --- a/news/2 Fixes/981.md +++ /dev/null @@ -1 +0,0 @@ -Fixes issue that display incorrect interpreter briefly before updating it to the right value. diff --git a/news/3 Code Health/1020.md b/news/3 Code Health/1020.md deleted file mode 100644 index ddd9b41bfb7e..000000000000 --- a/news/3 Code Health/1020.md +++ /dev/null @@ -1 +0,0 @@ -Exclude 'news' folder from getting packaged into the extension. diff --git a/news/3 Code Health/1034.md b/news/3 Code Health/1034.md deleted file mode 100644 index ca4bdd6fbe57..000000000000 --- a/news/3 Code Health/1034.md +++ /dev/null @@ -1,2 +0,0 @@ -Remove Jupyter commands. -(thanks [Yu Zhang](https://github.com/neilsustc)) diff --git a/news/3 Code Health/1040.md b/news/3 Code Health/1040.md deleted file mode 100644 index 88e881779fbd..000000000000 --- a/news/3 Code Health/1040.md +++ /dev/null @@ -1 +0,0 @@ -Trigger incremental build compilation only when typescript files are modified. diff --git a/news/3 Code Health/1042.md b/news/3 Code Health/1042.md deleted file mode 100644 index 94b355687cda..000000000000 --- a/news/3 Code Health/1042.md +++ /dev/null @@ -1 +0,0 @@ -Updated npm dependencies in devDependencies and fix TypeScript compilation issues. diff --git a/news/3 Code Health/1048.md b/news/3 Code Health/1048.md deleted file mode 100644 index c49a6f27e1a1..000000000000 --- a/news/3 Code Health/1048.md +++ /dev/null @@ -1 +0,0 @@ -Enable unit testing of stdout and stderr redirection for the experimental debugger. diff --git a/news/3 Code Health/1066.md b/news/3 Code Health/1066.md deleted file mode 100644 index 8279878538bb..000000000000 --- a/news/3 Code Health/1066.md +++ /dev/null @@ -1,2 +0,0 @@ -Update npm package `vscode-extension-telemetry` to fix the warning 'os.tmpDir() deprecation'. -(thanks [osya](https://github.com/osya)) diff --git a/news/3 Code Health/1090.md b/news/3 Code Health/1090.md deleted file mode 100644 index 222251dbfcf7..000000000000 --- a/news/3 Code Health/1090.md +++ /dev/null @@ -1 +0,0 @@ -Prevent the debugger stepping into JS code while developing the extension when debugging async TypeScript code. diff --git a/news/3 Code Health/1094.md b/news/3 Code Health/1094.md deleted file mode 100644 index 9da7257da17b..000000000000 --- a/news/3 Code Health/1094.md +++ /dev/null @@ -1 +0,0 @@ -Increase timeouts for the debugger unit tests. diff --git a/news/3 Code Health/1107.md b/news/3 Code Health/1107.md deleted file mode 100644 index 21e00e5d7662..000000000000 --- a/news/3 Code Health/1107.md +++ /dev/null @@ -1 +0,0 @@ -Change the command used to install pip on AppVeyor to avoid installation errors. diff --git a/news/3 Code Health/1114.md b/news/3 Code Health/1114.md deleted file mode 100644 index 0c77070b860d..000000000000 --- a/news/3 Code Health/1114.md +++ /dev/null @@ -1 +0,0 @@ -Check whether a document is active when detecthing changes in the active document. diff --git a/news/3 Code Health/1122.md b/news/3 Code Health/1122.md deleted file mode 100644 index 79587594dda4..000000000000 --- a/news/3 Code Health/1122.md +++ /dev/null @@ -1 +0,0 @@ -Remove SIGINT handler in debugger adapter, thereby preventing it from shutting down the debugger. diff --git a/news/3 Code Health/1146.md b/news/3 Code Health/1146.md deleted file mode 100644 index 26572fed17ea..000000000000 --- a/news/3 Code Health/1146.md +++ /dev/null @@ -1 +0,0 @@ -Improve compilation speed of the extension's TypeScript code. diff --git a/news/3 Code Health/1168.md b/news/3 Code Health/1168.md deleted file mode 100644 index 191f91bdd741..000000000000 --- a/news/3 Code Health/1168.md +++ /dev/null @@ -1 +0,0 @@ -Changes to how debug options are passed into the experimental version of PTVSD (debugger). diff --git a/news/3 Code Health/1180.md b/news/3 Code Health/1180.md deleted file mode 100644 index 9d27520795b0..000000000000 --- a/news/3 Code Health/1180.md +++ /dev/null @@ -1 +0,0 @@ -Ensure file paths are not sent in telemetry when running unit tests. diff --git a/news/3 Code Health/1198.md b/news/3 Code Health/1198.md deleted file mode 100644 index b14083165f25..000000000000 --- a/news/3 Code Health/1198.md +++ /dev/null @@ -1 +0,0 @@ -Change `DjangoDebugging` to `Django` in `debugOptions` of launch.json. diff --git a/news/3 Code Health/1213.md b/news/3 Code Health/1213.md deleted file mode 100644 index 85e1b6302812..000000000000 --- a/news/3 Code Health/1213.md +++ /dev/null @@ -1 +0,0 @@ -Changed property name used to capture the trigger source of Unit Tests. \ No newline at end of file diff --git a/news/3 Code Health/742.md b/news/3 Code Health/742.md deleted file mode 100644 index d35d1ffd391e..000000000000 --- a/news/3 Code Health/742.md +++ /dev/null @@ -1 +0,0 @@ -Enable unit testing of the experimental debugger on CI servers diff --git a/news/3 Code Health/778.md b/news/3 Code Health/778.md deleted file mode 100644 index 91eb90689389..000000000000 --- a/news/3 Code Health/778.md +++ /dev/null @@ -1 +0,0 @@ -Generate code coverage for debug adapter unit tests. diff --git a/news/3 Code Health/982.md b/news/3 Code Health/982.md deleted file mode 100644 index 00553e1194e9..000000000000 --- a/news/3 Code Health/982.md +++ /dev/null @@ -1 +0,0 @@ -Execute prospector as a module (using -m). diff --git a/news/3 Code Health/983.md b/news/3 Code Health/983.md deleted file mode 100644 index fe4e2a3e280a..000000000000 --- a/news/3 Code Health/983.md +++ /dev/null @@ -1 +0,0 @@ -Launch unit tests in debug mode as opposed to running and attaching the debugger to the already-running interpreter. diff --git a/news/announce.py b/news/announce.py index 7d1d12e22770..ce929b9900b2 100644 --- a/news/announce.py +++ b/news/announce.py @@ -88,8 +88,13 @@ def changelog_markdown(data): def git_rm(path): """Run git-rm on the path.""" status = subprocess.run(['git', 'rm', os.fspath(path.resolve())], - shell=True) - status.check_returncode() + shell=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + try: + status.check_returncode() + except Exception: + print(status.stdout, file=sys.stderr) + raise def cleanup(data): diff --git a/package.json b/package.json index 13f52168ad31..069f2be24b6e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Linting, Debugging (multi-threaded, remote), Intellisense, code formatting, refactoring, unit tests, snippets, and more.", - "version": "2018.3.0-rc", + "version": "2018.3.0", "publisher": "ms-python", "author": { "name": "Microsoft Corporation" From ba4375fb43b52b697b9e5d55fbd1a88ce80efbe9 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 29 Mar 2018 11:46:27 -0700 Subject: [PATCH 16/24] Bump version to 2018.4.0-alpha --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 069f2be24b6e..e7cc7e7291cc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Linting, Debugging (multi-threaded, remote), Intellisense, code formatting, refactoring, unit tests, snippets, and more.", - "version": "2018.3.0", + "version": "2018.4.0-alpha", "publisher": "ms-python", "author": { "name": "Microsoft Corporation" From 9eb88686d121c50601a7d3c5244129c323869e08 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 29 Mar 2018 16:20:58 -0700 Subject: [PATCH 17/24] Update on the removal of `closed` labels --- CONTRIBUTING.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc48f492daef..e9ce6ff30d39 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -112,25 +112,23 @@ To help actively track what stage issues are at, various labels are used. Which labels are expected to be set vary from when an issue is open to when an issue is closed. -#### Open issues - When an [issue is first opened](https://github.com/Microsoft/vscode-python/issues), -it is triaged to contain at least three types of labels: +it is triaged to contain at least two types of labels: 1. `needs` -1. `feature` 1. `type` -These labels cover what is blocking the issue from closing, what -feature(s) of the extension are related to the issue, and what type of -issue it is, respectively. +These labels cover what is blocking the issue from closing and what kind of +issue it is. We also add a `feature` label when appropriate for what the issue +relates to. #### Closed issues -When an -[issue is closed](https://github.com/Microsoft/vscode-python/issues?q=is%3Aissue+is%3Aclosed), -it should have an appropriate `closed-` label. +When an issue is closed by a pull request we add a +[`validate fix`](https://github.com/Microsoft/vscode-python/issues?q=label%3A%22validate+fix%22+is%3Aclosed) +label in order to request people help us test the fix to validate the issue was +resolved successfully. ### Pull request workflow @@ -150,8 +148,9 @@ it should have an appropriate `closed-` label. 1. Make sure all status checks are green (e.g. CLA check, CI, etc.) 1. Address any review comments 1. [Maintainers only] Merge the pull request -1. [Maintainers only] Update affected issues to be: - 1. Closed (with an appropriate `closed-` label) +1. [Maintainers only] Update affected issues: + 1. Add the [`validate fix`](https://github.com/Microsoft/vscode-python/issues?q=label%3A%22validate+fix%22+is%3Aclosed) + label 1. The issue(s) are attached to the current milestone 1. Register OSS usage 1. Email CELA about any 3rd-party usage changes From 24989f32316572d36981de71c4e9f9e19a0bc896 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 30 Mar 2018 09:28:29 -0700 Subject: [PATCH 18/24] Fixes issue that causes linter to fail when file path contains spaces. (#1241) (#1243) Forward-port from 2018.3.1 Fixes #1239 --- CHANGELOG.md | 8 +++++++- src/client/linters/flake8.ts | 2 +- src/client/linters/mypy.ts | 2 +- src/client/linters/pep8.ts | 2 +- src/client/linters/prospector.ts | 2 +- src/client/linters/pydocstyle.ts | 2 +- src/client/linters/pylama.ts | 2 +- src/client/linters/pylint.ts | 2 +- src/test/linters/lint.args.test.ts | 14 +++++++------- 9 files changed, 21 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 358776c756f8..0494063d280b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2018.3.1 (29 Mar 2018) + +### Fixes + +1. Fixes issue that causes linter to fail when file path contains spaces. +([#1239](https://github.com/Microsoft/vscode-python/issues/1239)) + ## 2018.3.0 (28 Mar 2018) ### Enhancements @@ -829,4 +836,3 @@ the following people who contributed code: ## Version 0.0.3 * Added support for debugging using PDB - diff --git a/src/client/linters/flake8.ts b/src/client/linters/flake8.ts index 494174e15e5d..d2c000a47fb0 100644 --- a/src/client/linters/flake8.ts +++ b/src/client/linters/flake8.ts @@ -13,7 +13,7 @@ export class Flake8 extends BaseLinter { } protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run(['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', document.uri.fsPath.fileToCommandArgument()], document, cancellation); + const messages = await this.run(['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', document.uri.fsPath], document, cancellation); messages.forEach(msg => { msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.flake8CategorySeverity); }); diff --git a/src/client/linters/mypy.ts b/src/client/linters/mypy.ts index 1064488700d5..5b0930e660bd 100644 --- a/src/client/linters/mypy.ts +++ b/src/client/linters/mypy.ts @@ -13,7 +13,7 @@ export class MyPy extends BaseLinter { } protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run([document.uri.fsPath.fileToCommandArgument()], document, cancellation, REGEX); + const messages = await this.run([document.uri.fsPath], document, cancellation, REGEX); messages.forEach(msg => { msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.mypyCategorySeverity); msg.code = msg.type; diff --git a/src/client/linters/pep8.ts b/src/client/linters/pep8.ts index e13d6c91c2b0..959923c6ad5e 100644 --- a/src/client/linters/pep8.ts +++ b/src/client/linters/pep8.ts @@ -13,7 +13,7 @@ export class Pep8 extends BaseLinter { } protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run(['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', document.uri.fsPath.fileToCommandArgument()], document, cancellation); + const messages = await this.run(['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', document.uri.fsPath], document, cancellation); messages.forEach(msg => { msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.pep8CategorySeverity); }); diff --git a/src/client/linters/prospector.ts b/src/client/linters/prospector.ts index 8bbef82c46a5..5642c5433848 100644 --- a/src/client/linters/prospector.ts +++ b/src/client/linters/prospector.ts @@ -28,7 +28,7 @@ export class Prospector extends BaseLinter { } protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - return this.run(['--absolute-paths', '--output-format=json', document.uri.fsPath.fileToCommandArgument()], document, cancellation); + return this.run(['--absolute-paths', '--output-format=json', document.uri.fsPath], document, cancellation); } protected async parseMessages(output: string, document: TextDocument, token: CancellationToken, regEx: string) { let parsedData: IProspectorResponse; diff --git a/src/client/linters/pydocstyle.ts b/src/client/linters/pydocstyle.ts index c22944f421d6..b23d52f66945 100644 --- a/src/client/linters/pydocstyle.ts +++ b/src/client/linters/pydocstyle.ts @@ -13,7 +13,7 @@ export class PyDocStyle extends BaseLinter { } protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run([document.uri.fsPath.fileToCommandArgument()], document, cancellation); + const messages = await this.run([document.uri.fsPath], document, cancellation); // All messages in pep8 are treated as warnings for now. messages.forEach(msg => { msg.severity = LintMessageSeverity.Warning; diff --git a/src/client/linters/pylama.ts b/src/client/linters/pylama.ts index ef66bc5446c3..edee2b44898f 100644 --- a/src/client/linters/pylama.ts +++ b/src/client/linters/pylama.ts @@ -14,7 +14,7 @@ export class PyLama extends BaseLinter { } protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run(['--format=parsable', document.uri.fsPath.fileToCommandArgument()], document, cancellation, REGEX); + const messages = await this.run(['--format=parsable', document.uri.fsPath], document, cancellation, REGEX); // All messages in pylama are treated as warnings for now. messages.forEach(msg => { msg.severity = LintMessageSeverity.Warning; diff --git a/src/client/linters/pylint.ts b/src/client/linters/pylint.ts index 4998fe22a46a..1e830283127d 100644 --- a/src/client/linters/pylint.ts +++ b/src/client/linters/pylint.ts @@ -70,7 +70,7 @@ export class Pylint extends BaseLinter { '--msg-template=\'{line},{column},{category},{msg_id}:{msg}\'', '--reports=n', '--output-format=text', - uri.fsPath.fileToCommandArgument() + uri.fsPath ]; const messages = await this.run(minArgs.concat(args), document, cancellation); messages.forEach(msg => { diff --git a/src/test/linters/lint.args.test.ts b/src/test/linters/lint.args.test.ts index 259f87e38ef7..780aefb2fe61 100644 --- a/src/test/linters/lint.args.test.ts +++ b/src/test/linters/lint.args.test.ts @@ -110,32 +110,32 @@ suite('Linting - Arguments', () => { [Uri.file(path.join('users', 'development path to', 'one.py')), Uri.file(path.join('users', 'development', 'one.py'))].forEach(fileUri => { test(`Flake8 (${fileUri.fsPath.indexOf(' ') > 0 ? 'with spaces' : 'without spaces'})`, async () => { const linter = new Flake8(outputChannel.object, serviceContainer); - const expectedArgs = ['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', fileUri.fsPath.fileToCommandArgument()]; + const expectedArgs = ['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', fileUri.fsPath]; await testLinter(linter, fileUri, expectedArgs); }); test(`Pep8 (${fileUri.fsPath.indexOf(' ') > 0 ? 'with spaces' : 'without spaces'})`, async () => { const linter = new Pep8(outputChannel.object, serviceContainer); - const expectedArgs = ['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', fileUri.fsPath.fileToCommandArgument()]; + const expectedArgs = ['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', fileUri.fsPath]; await testLinter(linter, fileUri, expectedArgs); }); test(`Prospector (${fileUri.fsPath.indexOf(' ') > 0 ? 'with spaces' : 'without spaces'})`, async () => { const linter = new Prospector(outputChannel.object, serviceContainer); - const expectedArgs = ['--absolute-paths', '--output-format=json', fileUri.fsPath.fileToCommandArgument()]; + const expectedArgs = ['--absolute-paths', '--output-format=json', fileUri.fsPath]; await testLinter(linter, fileUri, expectedArgs); }); test(`Pylama (${fileUri.fsPath.indexOf(' ') > 0 ? 'with spaces' : 'without spaces'})`, async () => { const linter = new PyLama(outputChannel.object, serviceContainer); - const expectedArgs = ['--format=parsable', fileUri.fsPath.fileToCommandArgument()]; + const expectedArgs = ['--format=parsable', fileUri.fsPath]; await testLinter(linter, fileUri, expectedArgs); }); test(`MyPy (${fileUri.fsPath.indexOf(' ') > 0 ? 'with spaces' : 'without spaces'})`, async () => { const linter = new MyPy(outputChannel.object, serviceContainer); - const expectedArgs = [fileUri.fsPath.fileToCommandArgument()]; + const expectedArgs = [fileUri.fsPath]; await testLinter(linter, fileUri, expectedArgs); }); test(`Pydocstyle (${fileUri.fsPath.indexOf(' ') > 0 ? 'with spaces' : 'without spaces'})`, async () => { const linter = new PyDocStyle(outputChannel.object, serviceContainer); - const expectedArgs = [fileUri.fsPath.fileToCommandArgument()]; + const expectedArgs = [fileUri.fsPath]; await testLinter(linter, fileUri, expectedArgs); }); test(`Pylint (${fileUri.fsPath.indexOf(' ') > 0 ? 'with spaces' : 'without spaces'})`, async () => { @@ -144,7 +144,7 @@ suite('Linting - Arguments', () => { let invoked = false; (linter as any).run = (args, doc, token) => { - expect(args[args.length - 1]).to.equal(fileUri.fsPath.fileToCommandArgument()); + expect(args[args.length - 1]).to.equal(fileUri.fsPath); invoked = true; return Promise.resolve([]); }; From 70600f68efa418d571c7ee74221b096a8d9848ea Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 30 Mar 2018 12:45:11 -0700 Subject: [PATCH 19/24] Use an existing method to identify the active interpreter (#1215) * :hammer: use getActiveInterpreter to get active interpreter * tests for module installer * :memo: news entry * Fixes #1015 --- news/2 Fixes/1015.md | 1 + .../common/installer/moduleInstaller.ts | 12 +-- src/client/interpreter/contracts.ts | 4 +- src/test/common/installer/installer.test.ts | 2 +- .../common/installer/moduleInstaller.test.ts | 78 +++++++++++++++++++ src/test/common/moduleInstaller.test.ts | 3 +- 6 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 news/2 Fixes/1015.md create mode 100644 src/test/common/installer/moduleInstaller.test.ts diff --git a/news/2 Fixes/1015.md b/news/2 Fixes/1015.md new file mode 100644 index 000000000000..c88fd33a17eb --- /dev/null +++ b/news/2 Fixes/1015.md @@ -0,0 +1 @@ +Use an existing method to identify the active interpreter. \ No newline at end of file diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index 39b07c97bed0..f69401faa9ff 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -8,12 +8,11 @@ import * as fs from 'fs'; import { injectable } from 'inversify'; import * as path from 'path'; import * as vscode from 'vscode'; -import { IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../interpreter/contracts'; +import { IInterpreterService, InterpreterType } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { PythonSettings } from '../configSettings'; import { STANDARD_OUTPUT_CHANNEL } from '../constants'; import { noop } from '../core.utils'; -import { IFileSystem } from '../platform/types'; import { ITerminalServiceFactory } from '../terminal/types'; import { ExecutionInfo, IOutputChannel } from '../types'; @@ -29,13 +28,8 @@ export abstract class ModuleInstaller { const args = ['-m', 'pip'].concat(executionInfo.args); const pythonPath = settings.pythonPath; - const locator = this.serviceContainer.get(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); - const fileSystem = this.serviceContainer.get(IFileSystem); - const interpreters = await locator.getInterpreters(resource); - - const currentInterpreter = interpreters.length > 1 - ? interpreters.filter(x => fileSystem.arePathsSame(x.path, pythonPath))[0] - : interpreters[0]; + const interpreterService = this.serviceContainer.get(IInterpreterService); + const currentInterpreter = await interpreterService.getActiveInterpreter(resource); if (!currentInterpreter || currentInterpreter.type !== InterpreterType.Unknown) { await terminalService.sendCommand(pythonPath, args); diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index b71c77eb5ccd..d2540595fa96 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -45,10 +45,10 @@ export interface ICondaService { isCondaAvailable(): Promise; getCondaVersion(): Promise; getCondaInfo(): Promise; - getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string, path: string }[]) | undefined>; + getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string; path: string }[]) | undefined>; getInterpreterPath(condaEnvironmentPath: string): string; isCondaEnvironment(interpreterPath: string): Promise; - getCondaEnvironment(interpreterPath: string): Promise<{ name: string, path: string } | undefined>; + getCondaEnvironment(interpreterPath: string): Promise<{ name: string; path: string } | undefined>; } export enum InterpreterType { diff --git a/src/test/common/installer/installer.test.ts b/src/test/common/installer/installer.test.ts index 84ef685e3d78..b1c37cf6bb85 100644 --- a/src/test/common/installer/installer.test.ts +++ b/src/test/common/installer/installer.test.ts @@ -14,7 +14,7 @@ import { IServiceContainer } from '../../../client/ioc/types'; use(chaiAsPromised); // tslint:disable-next-line:max-func-body-length -suite('Module Installerx', () => { +suite('Module Installer', () => { [undefined, Uri.file('resource')].forEach(resource => { EnumEx.getNamesAndValues(Product).forEach(product => { let disposables: Disposable[] = []; diff --git a/src/test/common/installer/moduleInstaller.test.ts b/src/test/common/installer/moduleInstaller.test.ts new file mode 100644 index 000000000000..ce0fd56cb567 --- /dev/null +++ b/src/test/common/installer/moduleInstaller.test.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Disposable} from 'vscode'; +import { CondaInstaller } from '../../../client/common/installer/condaInstaller'; +import { PipInstaller } from '../../../client/common/installer/pipInstaller'; +import { IInstallationChannelManager, IModuleInstaller } from '../../../client/common/installer/types'; +import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; +import { IConfigurationService, IDisposableRegistry, IPythonSettings } from '../../../client/common/types'; +import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { initialize } from '../../initialize'; + +// tslint:disable-next-line:max-func-body-length +suite('Module Installer', () => { + const pythonPath = path.join(__dirname, 'python'); + suiteSetup(initialize); + [CondaInstaller, PipInstaller].forEach(installerClass => { + let disposables: Disposable[] = []; + let installer: IModuleInstaller; + let installationChannel: TypeMoq.IMock; + let serviceContainer: TypeMoq.IMock; + let terminalService: TypeMoq.IMock; + let pythonSettings: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + + disposables = []; + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())).returns(() => disposables); + + installationChannel = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInstallationChannelManager), TypeMoq.It.isAny())).returns(() => installationChannel.object); + + const condaService = TypeMoq.Mock.ofType(); + condaService.setup(c => c.getCondaFile()).returns(() => Promise.resolve('conda')); + condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + const configService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => configService); + pythonSettings = TypeMoq.Mock.ofType(); + pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + + terminalService = TypeMoq.Mock.ofType(); + const terminalServiceFactory = TypeMoq.Mock.ofType(); + terminalServiceFactory.setup(f => f.getTerminalService(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => terminalService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory), TypeMoq.It.isAny())).returns(() => terminalServiceFactory.object); + + interpreterService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())).returns(() => interpreterService.object); + + installer = new installerClass(serviceContainer.object); + }); + teardown(() => { + disposables.forEach(disposable => { + if (disposable) { + disposable.dispose(); + } + }); + }); + test(`Ensure getActiveInterperter is used (${installerClass.name})`, async () => { + if (installer.displayName !== 'Pip') { + return; + } + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)).verifiable(); + try { + await installer.installModule('xyz'); + // tslint:disable-next-line:no-empty + } catch { } + interpreterService.verifyAll(); + }); + }); +}); diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index b1b4d5c52fcb..72201771d5fc 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -202,6 +202,8 @@ suite('Module Installer', () => { mockTerminalService .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) .returns((cmd: string, args: string[]) => { argsSent = args; return Promise.resolve(void 0); }); + // tslint:disable-next-line:no-any + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve({ type: InterpreterType.Unknown } as any)); await pipInstaller.installModule(moduleName); expect(argsSent.join(' ')).equal(`-m pip install -U ${moduleName} --user`, 'Invalid command sent to terminal for installation.'); @@ -225,7 +227,6 @@ suite('Module Installer', () => { mockTerminalService .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) .returns((cmd: string, args: string[]) => { argsSent = args; return Promise.resolve(void 0); }); - await pipInstaller.installModule(moduleName); expect(argsSent.join(' ')).equal(`-m pip install -U ${moduleName}`, 'Invalid command sent to terminal for installation.'); From 915d4950e26dc32dd80630629b615afa00a2d108 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 30 Mar 2018 12:46:11 -0700 Subject: [PATCH 20/24] Improved extension development experience on Windows (#1217) * split compilation for faster compilation on windows * :memo: update docs and change log * :hammer: rename linting (else auto fix is not available) [skip ci] * :hammer: minor changes [skip ci] * Fixes #1216 --- .vscode/settings.json | 2 +- gulpfile.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 31866020cb46..204fcc045003 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,4 +19,4 @@ "python.unitTest.promptToConfigure": false, "python.workspaceSymbols.enabled": false, "python.formatting.provider": "none" -} \ No newline at end of file +} diff --git a/gulpfile.js b/gulpfile.js index b8250803dbde..ba89b0a92e2f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -440,7 +440,7 @@ function getAddedFilesSync() { return out .split(/\r?\n/) .filter(l => !!l) - .filter(l => _.intersection(['A', '?'], l.substring(0, 2).trim().split()).length > 0) + .filter(l => _.intersection(['A', '?', 'U'], l.substring(0, 2).trim().split('')).length > 0) .map(l => path.join(__dirname, l.substring(2).trim())); } function getModifiedFilesSync() { @@ -448,7 +448,7 @@ function getModifiedFilesSync() { return out .split(/\r?\n/) .filter(l => !!l) - .filter(l => _.intersection(['M', 'A', 'R', 'C'], l.substring(0, 2).trim().split()).length > 0) + .filter(l => _.intersection(['M', 'A', 'R', 'C', 'U', '?'], l.substring(0, 2).trim().split('')).length > 0) .map(l => path.join(__dirname, l.substring(2).trim())); } From 4aff36bc4c123a20d00aa2a56fbd5ac9d06d7b24 Mon Sep 17 00:00:00 2001 From: Mikhail Arkhipov Date: Fri, 30 Mar 2018 13:44:07 -0700 Subject: [PATCH 21/24] VS Python analysis engine integration (#1231) * Undo changes * Test fixes * Increase timeout * Remove double event listening * Remove test * Revert "Remove test" This reverts commit e240c3fd117c38b9e6fdcbdd1ba2715789fefe48. * Revert "Remove double event listening" This reverts commit af573be27372a79d5589e2134002cc753bb54f2a. * Undo changes * Test fixes * .NET Core check * Better find dotnet * Fix pip test * Linting tests * Undo accidental changes * Add clone and build PTVS * Appveyor PTVS build * Fix slashes * Enable build * Try absolute path * Fix xcopy switch * Activate Analysis Engine test on Appveyor * Temporary only run new tests * Disable PEP hint tests * Test fix * Disable appveyor build and tests for PTVS for now * Remove analysis engine test from the set * Remove VS image for now * Build/sign VSXI project * Run vsce from cmd * Rename * Abs path vsce * Path * Move project * Ignore publishing project * Try csproj * Add framework * Ignore build output folder * Package before build * Try batch instead of PS * Fix path quotes * #1096 The if statement is automatically formatted incorrectly * Merge fix * Add more tests * More tests * Typo * Test * Also better handle multiline arguments * Changes lost on squash * More lost changes * Restore Jedi/PTVS setting * Update tests to new PTVS * Signature tests * Add PTVS tests task * Analysis Engine contribution * Add Mac/Linux info * Disable csproj build * Add unzip to dependencies * Minor fixes to doc * Change setting type to bool * Report progress on status bar * Simplify * CR feedback * Fix launching fx-independent code on Mac/Linux * Add title --- .gitignore | 2 + .vscode/launch.json | 22 +- .vscodeignore | 6 + CONTRIBUTING - PYTHON_ANALYSIS.md | 61 ++++ CONTRIBUTING.md | 3 +- appveyor.yml | 17 +- package.json | 9 + packageExtension.cmd | 1 + src/client/activation/analysis.ts | 270 ++++++++++++++++++ src/client/activation/analysisEngineHashes.ts | 9 + src/client/activation/classic.ts | 62 ++++ src/client/activation/downloader.ts | 129 +++++++++ src/client/activation/hashVerifier.ts | 28 ++ src/client/activation/platformData.ts | 49 ++++ src/client/activation/types.ts | 9 + src/client/common/configSettings.ts | 105 +++---- src/client/common/configuration/service.ts | 31 +- src/client/common/types.ts | 16 +- src/client/extension.ts | 140 ++++----- src/test/.vscode/settings.json | 10 +- src/test/analysisEngineTest.ts | 16 ++ src/test/autocomplete/base.test.ts | 42 ++- src/test/autocomplete/pep484.test.ts | 23 +- src/test/autocomplete/pep526.test.ts | 49 ++-- src/test/common/moduleInstaller.test.ts | 10 +- .../pythonProc.simple.multiroot.test.ts | 5 +- src/test/constants.ts | 7 +- .../{hover.test.ts => hover.jedi.test.ts} | 11 +- src/test/definitions/hover.ptvs.test.ts | 184 ++++++++++++ ...parallel.test.ts => parallel.jedi.test.ts} | 12 +- src/test/definitions/parallel.ptvs.test.ts | 57 ++++ src/test/linters/lint.commands.test.ts | 3 +- src/test/linters/lint.manager.test.ts | 2 + src/test/pythonFiles/autocomp/four.py | 2 +- src/test/pythonFiles/definition/four.py | 2 +- ...gnature.test.ts => signature.jedi.test.ts} | 9 +- src/test/signature/signature.ptvs.test.ts | 146 ++++++++++ vscode-python-signing.csproj | 20 ++ 38 files changed, 1373 insertions(+), 206 deletions(-) create mode 100644 CONTRIBUTING - PYTHON_ANALYSIS.md create mode 100644 packageExtension.cmd create mode 100644 src/client/activation/analysis.ts create mode 100644 src/client/activation/analysisEngineHashes.ts create mode 100644 src/client/activation/classic.ts create mode 100644 src/client/activation/downloader.ts create mode 100644 src/client/activation/hashVerifier.ts create mode 100644 src/client/activation/platformData.ts create mode 100644 src/client/activation/types.ts create mode 100644 src/test/analysisEngineTest.ts rename src/test/definitions/{hover.test.ts => hover.jedi.test.ts} (98%) create mode 100644 src/test/definitions/hover.ptvs.test.ts rename src/test/definitions/{parallel.test.ts => parallel.jedi.test.ts} (89%) create mode 100644 src/test/definitions/parallel.ptvs.test.ts rename src/test/signature/{signature.test.ts => signature.jedi.test.ts} (95%) create mode 100644 src/test/signature/signature.ptvs.test.ts create mode 100644 vscode-python-signing.csproj diff --git a/.gitignore b/.gitignore index cc941e968ccb..47c9018661ee 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ coverage/ pythonFiles/experimental/ptvsd/** debug_coverage*/** analysis/** +bin/** +obj/** diff --git a/.vscode/launch.json b/.vscode/launch.json index 4daf91a9249b..35f13ae91111 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -87,6 +87,26 @@ ], "preLaunchTask": "Compile" }, + { + "name": "Launch Analysis Engine Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/src/test", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "Compile", + "env": { + "VSC_PYTHON_ANALYSIS": "1" + } + }, { "name": "Launch Tests (with code coverage)", "type": "extensionHost", @@ -114,4 +134,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore index ae242dcf887d..0468d402749a 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -37,3 +37,9 @@ CODING_STANDARDS.md CONTRIBUTING.md news/** debug_coverage*/** +analysis/publish*.* +vscode-python-signing.* +packageExtension.cmd +bin/** +obj/** +BuildOutput/** diff --git a/CONTRIBUTING - PYTHON_ANALYSIS.md b/CONTRIBUTING - PYTHON_ANALYSIS.md new file mode 100644 index 000000000000..03563dac7efd --- /dev/null +++ b/CONTRIBUTING - PYTHON_ANALYSIS.md @@ -0,0 +1,61 @@ +# Contributing to Microsoft Python Analysis Engine +[![Contributing to Python Tools for Visual Studio](https://github.com/Microsoft/PTVS/blob/master/CONTRIBUTING.md)] + +[![Build Status (Travis)](https://travis-ci.org/Microsoft/vscode-python.svg?branch=master)](https://travis-ci.org/Microsoft/vscode-python) [![Build status (AppVeyor)](https://ci.appveyor.com/api/projects/status/s0pt8d79gqw222j7?svg=true)](https://ci.appveyor.com/project/DonJayamanne/vscode-python-v3vd6) [![codecov](https://codecov.io/gh/Microsoft/vscode-python/branch/master/graph/badge.svg)](https://codecov.io/gh/Microsoft/vscode-python) + + +## Contributing a pull request + +### Prerequisites + +1. .NET Core 2.0+ SDK + - [Windows](https://www.microsoft.com/net/learn/get-started/windows) + - [Mac OS](https://www.microsoft.com/net/learn/get-started/macos) + - [Linux](https://www.microsoft.com/net/learn/get-started/linux/rhel) +2. C# Extension to VS Code (all platforms) +3. Python 2.7 +4. Python 3.6 + +*Alternative:* [Visual Studio 2017](https://www.visualstudio.com/downloads/) (Windows only) with .NET Core and C# Workloads. Community Edition is free and is fully functional. + +### Setup + +```shell +git clone https://github.com/microsoft/ptvs +cd Python/Product/VsCode/AnalysisVsc +dotnet build +``` + +Visual Studio 2017: +1. Open solution in Python/Product/VsCode +2. Build AnalysisVsc project +3. Binaries arrive in *Python/BuildOutput/VsCode/raw* +4. Delete contents of the *analysis* folder in the Python Extension folder +5. Copy *.dll, *.pdb, *.json fron *Python/BuildOutput/VsCode/raw* to *analysis* + +### Debugging code in Python Extension to VS Code +Folow regular TypeScript debugging steps + +### Debugging C# code in Python Analysis Engine +1. Launch another instance of VS Code +2. Open Python/Product/VsCode/AnalysisVsc folder +3. Python Analysis Engine code is in *Python/Product/VsCode/Analysis* +4. Run extension from VS Code +5. In the instance with C# code select Dotnet Attach launch task. +6. Attach to *dotnet* process running *Microsoft.PythonTools.VsCode.dll* + +On Windows you can also attach from Visual Studio 2017. + +### Validate your changes + +1. Build C# code +2. Copy binaries to *analysis* folder +3. Use the `Launch Extension` launch option. + +### Unit Tests +1. Run the Unit Tests via the `Launch Analysis Engine Tests`. +2. On Windows you can also open complete PTVS solution in Visual Studio and run its tests (or at least the Analysis section). + + +### Coding Standards +See [![Contributing to Python Tools for Visual Studio](https://github.com/Microsoft/PTVS/blob/master/CONTRIBUTING.md)] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e9ce6ff30d39..9e066b582c40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,8 @@ [![Build Status (Travis)](https://travis-ci.org/Microsoft/vscode-python.svg?branch=master)](https://travis-ci.org/Microsoft/vscode-python) [![Build status (AppVeyor)](https://ci.appveyor.com/api/projects/status/s0pt8d79gqw222j7?svg=true)](https://ci.appveyor.com/project/DonJayamanne/vscode-python-v3vd6) [![codecov](https://codecov.io/gh/Microsoft/vscode-python/branch/master/graph/badge.svg)](https://codecov.io/gh/Microsoft/vscode-python) - +# Contributing to Microsoft Python Analysis Engine +[![Contributing to Python Analysis Engine](https://github.com/Microsoft/vscode-python/blob/master/CONTRIBUTING - PYTHON_ANALYSIS.md)] ## Contributing a pull request diff --git a/appveyor.yml b/appveyor.yml index 774b4d93ca18..3b80a7267a18 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,3 +1,6 @@ +#image: Visual Studio 2017 +#shallow_clone: true + environment: matrix: - PYTHON: "C:\\Python36" @@ -20,11 +23,23 @@ install: - python -m easy_install -U setuptools - "%PYTHON%/Scripts/pip.exe install --upgrade -r requirements.txt" +build: off +# build_script: +# - git clone https://github.com/MikhailArkhipov/PTVS.git c:/projects/PTVS +# - "cd c:\\projects\\PTVS" +# - git checkout origin/vsc +# - "cd Python\\Product\\VSCode\\AnalysisVsc" +# - "dotnet --info" +# - "dotnet build" +# - "cd c:\\projects\\vscode-python" +# - "xcopy /S /I c:\\projects\\PTVS\\BuildOutput\\VsCode\\raw analysis" + test_script: - yarn run clean - yarn run vscode:prepublish - yarn run testDebugger --silent - yarn run testSingleWorkspace --silent - yarn run testMultiWorkspace --silent + # - yarn run testAnalysisEngine --silent + -build: off diff --git a/package.json b/package.json index e7cc7e7291cc..7f4b5dc82d60 100644 --- a/package.json +++ b/package.json @@ -1121,6 +1121,12 @@ "default": "${workspaceFolder}/.env", "scope": "resource" }, + "python.jediEnabled": { + "type": "boolean", + "default": true, + "description": "Enables Jedi as IntelliSense engine instead of Microsoft Python Analysis Engine.", + "scope": "resource" + }, "python.jediPath": { "type": "string", "default": "", @@ -1796,6 +1802,7 @@ "testDebugger": "node ./out/test/debuggerTest.js", "testSingleWorkspace": "node ./out/test/standardTest.js", "testMultiWorkspace": "node ./out/test/multiRootTest.js", + "testAnalysisEngine": "node ./out/test/analysisEngineTest.js", "precommit": "node gulpfile.js", "lint-staged": "node gulpfile.js", "lint": "tslint src/**/*.ts -t verbose", @@ -1819,6 +1826,7 @@ "opn": "^5.1.0", "pidusage": "^1.2.0", "reflect-metadata": "^0.1.12", + "request-progress": "^3.0.0", "rxjs": "^5.5.2", "semver": "^5.4.1", "sudo-prompt": "^8.0.0", @@ -1828,6 +1836,7 @@ "uint64be": "^1.0.1", "unicode": "^10.0.0", "untildify": "^3.0.2", + "unzip": "^0.1.11", "vscode-debugadapter": "^1.0.1", "vscode-debugprotocol": "^1.0.1", "vscode-extension-telemetry": "^0.0.14", diff --git a/packageExtension.cmd b/packageExtension.cmd new file mode 100644 index 000000000000..dc3026c20f3e --- /dev/null +++ b/packageExtension.cmd @@ -0,0 +1 @@ +%1\vsce package --out %2 \ No newline at end of file diff --git a/src/client/activation/analysis.ts b/src/client/activation/analysis.ts new file mode 100644 index 000000000000..b0ef1b48d87a --- /dev/null +++ b/src/client/activation/analysis.ts @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { ExtensionContext, OutputChannel } from 'vscode'; +import { Disposable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient'; +import { IApplicationShell } from '../common/application/types'; +import { isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import '../common/extensions'; +import { IFileSystem, IPlatformService } from '../common/platform/types'; +import { IProcessService, IPythonExecutionFactory } from '../common/process/types'; +import { StopWatch } from '../common/stopWatch'; +import { IConfigurationService, IOutputChannel, IPythonSettings } from '../common/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { AnalysisEngineDownloader } from './downloader'; +import { PlatformData } from './platformData'; +import { IExtensionActivator } from './types'; + +const PYTHON = 'python'; +const dotNetCommand = 'dotnet'; +const languageClientName = 'Python Tools'; +const analysisEngineFolder = 'analysis'; + +class InterpreterData { + constructor(public readonly version: string, public readonly prefix: string) { } +} + +export class AnalysisExtensionActivator implements IExtensionActivator { + private readonly executionFactory: IPythonExecutionFactory; + private readonly configuration: IConfigurationService; + private readonly appShell: IApplicationShell; + private readonly output: OutputChannel; + private readonly fs: IFileSystem; + private readonly sw = new StopWatch(); + private readonly platformData: PlatformData; + private languageClient: LanguageClient | undefined; + + constructor(private readonly services: IServiceContainer, pythonSettings: IPythonSettings) { + this.executionFactory = this.services.get(IPythonExecutionFactory); + this.configuration = this.services.get(IConfigurationService); + this.appShell = this.services.get(IApplicationShell); + this.output = this.services.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.fs = this.services.get(IFileSystem); + this.platformData = new PlatformData(services.get(IPlatformService)); + } + + public async activate(context: ExtensionContext): Promise { + const clientOptions = await this.getAnalysisOptions(context); + if (!clientOptions) { + return false; + } + this.output.appendLine(`Options determined: ${this.sw.elapsedTime} ms`); + return this.startLanguageServer(context, clientOptions); + } + + public async deactivate(): Promise { + if (this.languageClient) { + await this.languageClient.stop(); + } + } + + private async startLanguageServer(context: ExtensionContext, clientOptions: LanguageClientOptions): Promise { + // Determine if we are running MSIL/Universal via dotnet or self-contained app. + const mscorlib = path.join(context.extensionPath, analysisEngineFolder, 'mscorlib.dll'); + let downloadPackage = false; + + if (!await this.fs.fileExistsAsync(mscorlib)) { + // Depends on .NET Runtime or SDK + this.languageClient = this.createSimpleLanguageClient(context, clientOptions); + const e = await this.tryStartLanguageClient(context, this.languageClient); + if (!e) { + return true; + } + if (await this.isDotNetInstalled()) { + this.appShell.showErrorMessage(`.NET Runtime appears to be installed but the language server did not start. Error ${e}`); + return false; + } + // No .NET Runtime, no mscorlib - need to download self-contained package. + downloadPackage = true; + } + + if (downloadPackage) { + const downloader = new AnalysisEngineDownloader(this.services, analysisEngineFolder); + await downloader.downloadAnalysisEngine(context); + } + + const serverModule = path.join(context.extensionPath, analysisEngineFolder, this.platformData.getEngineExecutableName()); + // Now try to start self-contained app + this.languageClient = this.createSelfContainedLanguageClient(context, serverModule, clientOptions); + const error = await this.tryStartLanguageClient(context, this.languageClient); + if (!error) { + return true; + } + this.appShell.showErrorMessage(`Language server failed to start. Error ${error}`); + return false; + } + + private async tryStartLanguageClient(context: ExtensionContext, lc: LanguageClient): Promise { + let disposable: Disposable | undefined; + try { + disposable = lc.start(); + await lc.onReady(); + this.output.appendLine(`Language server ready: ${this.sw.elapsedTime} ms`); + context.subscriptions.push(disposable); + } catch (ex) { + if (disposable) { + disposable.dispose(); + return ex; + } + } + } + + private createSimpleLanguageClient(context: ExtensionContext, clientOptions: LanguageClientOptions): LanguageClient { + const commandOptions = { stdio: 'pipe' }; + const serverModule = path.join(context.extensionPath, analysisEngineFolder, this.platformData.getEngineDllName()); + const serverOptions: ServerOptions = { + run: { command: dotNetCommand, args: [serverModule], options: commandOptions }, + debug: { command: dotNetCommand, args: [serverModule, '--debug'], options: commandOptions } + }; + return new LanguageClient(PYTHON, languageClientName, serverOptions, clientOptions); + } + + private createSelfContainedLanguageClient(context: ExtensionContext, serverModule: string, clientOptions: LanguageClientOptions): LanguageClient { + const options = { stdio: 'pipe' }; + const serverOptions: ServerOptions = { + run: { command: serverModule, rgs: [], options: options }, + debug: { command: serverModule, args: ['--debug'], options } + }; + return new LanguageClient(PYTHON, languageClientName, serverOptions, clientOptions); + } + + private async getAnalysisOptions(context: ExtensionContext): Promise { + // tslint:disable-next-line:no-any + const properties = new Map(); + + // Microsoft Python code analysis engine needs full path to the interpreter + const interpreterService = this.services.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(); + + if (interpreter) { + // tslint:disable-next-line:no-string-literal + properties['InterpreterPath'] = interpreter.path; + if (interpreter.displayName) { + // tslint:disable-next-line:no-string-literal + properties['Description'] = interpreter.displayName; + } + const interpreterData = await this.getInterpreterData(); + + // tslint:disable-next-line:no-string-literal + properties['Version'] = interpreterData.version; + // tslint:disable-next-line:no-string-literal + properties['PrefixPath'] = interpreterData.prefix; + // tslint:disable-next-line:no-string-literal + properties['DatabasePath'] = path.join(context.extensionPath, analysisEngineFolder); + + let searchPaths = await this.getSearchPaths(); + const settings = this.configuration.getSettings(); + if (settings.autoComplete) { + const extraPaths = settings.autoComplete.extraPaths; + if (extraPaths && extraPaths.length > 0) { + searchPaths = `${searchPaths};${extraPaths.join(';')}`; + } + } + // tslint:disable-next-line:no-string-literal + properties['SearchPaths'] = searchPaths; + + if (isTestExecution()) { + // tslint:disable-next-line:no-string-literal + properties['TestEnvironment'] = true; + } + } else { + const appShell = this.services.get(IApplicationShell); + const pythonPath = this.configuration.getSettings().pythonPath; + appShell.showErrorMessage(`Interpreter ${pythonPath} does not exist.`); + return; + } + + const selector: string[] = [PYTHON]; + // Options to control the language client + return { + // Register the server for Python documents + documentSelector: selector, + synchronize: { + configurationSection: PYTHON + }, + outputChannel: this.output, + initializationOptions: { + interpreter: { + properties + } + } + }; + } + + private async getInterpreterData(): Promise { + // Not appropriate for multiroot workspaces. + // See https://github.com/Microsoft/vscode-python/issues/1149 + const execService = await this.executionFactory.create(); + const result = await execService.exec(['-c', 'import sys; print(sys.version_info); print(sys.prefix)'], {}); + // 2.7.14 (v2.7.14:84471935ed, Sep 16 2017, 20:19:30) <> + // [MSC v.1500 32 bit (Intel)] + // C:\Python27 + if (!result.stdout) { + throw Error('Unable to determine Python interpreter version and system prefix.'); + } + const output = result.stdout.splitLines({ removeEmptyEntries: true, trim: true }); + if (!output || output.length < 2) { + throw Error('Unable to parse version and and system prefix from the Python interpreter output.'); + } + const majorMatches = output[0].match(/major=(\d*?),/); + const minorMatches = output[0].match(/minor=(\d*?),/); + if (!majorMatches || majorMatches.length < 2 || !minorMatches || minorMatches.length < 2) { + throw Error('Unable to parse interpreter version.'); + } + const prefix = output[output.length - 1]; + return new InterpreterData(`${majorMatches[1]}.${minorMatches[1]}`, prefix); + } + + private async getSearchPaths(): Promise { + // Not appropriate for multiroot workspaces. + // See https://github.com/Microsoft/vscode-python/issues/1149 + const execService = await this.executionFactory.create(); + const result = await execService.exec(['-c', 'import sys; print(sys.path);'], {}); + if (!result.stdout) { + throw Error('Unable to determine Python interpreter search paths.'); + } + // tslint:disable-next-line:no-unnecessary-local-variable + const paths = result.stdout.split(',') + .filter(p => this.isValidPath(p)) + .map(p => this.pathCleanup(p)); + return paths.join(';'); + } + + private pathCleanup(s: string): string { + s = s.trim(); + if (s[0] === '\'') { + s = s.substr(1); + } + if (s[s.length - 1] === ']') { + s = s.substr(0, s.length - 1); + } + if (s[s.length - 1] === '\'') { + s = s.substr(0, s.length - 1); + } + return s; + } + + private isValidPath(s: string): boolean { + return s.length > 0 && s[0] !== '['; + } + + // private async checkNetCoreRuntime(): Promise { + // if (!await this.isDotNetInstalled()) { + // const appShell = this.services.get(IApplicationShell); + // if (await appShell.showErrorMessage('Python Tools require .NET Core Runtime. Would you like to install it now?', 'Yes', 'No') === 'Yes') { + // appShell.openUrl('https://www.microsoft.com/net/download/core#/runtime'); + // appShell.showWarningMessage('Please restart VS Code after .NET Runtime installation is complete.'); + // } + // return false; + // } + // return true; + // } + + private async isDotNetInstalled(): Promise { + const ps = this.services.get(IProcessService); + const result = await ps.exec('dotnet', ['--version']).catch(() => { return { stdout: '' }; }); + return result.stdout.trim().startsWith('2.'); + } +} diff --git a/src/client/activation/analysisEngineHashes.ts b/src/client/activation/analysisEngineHashes.ts new file mode 100644 index 000000000000..bcf36993e3ff --- /dev/null +++ b/src/client/activation/analysisEngineHashes.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// This file will be replaced by a generated one during the release build +// with actual hashes of the uploaded packages. +export const analysis_engine_win_x86_sha512 = ''; +export const analysis_engine_win_x64_sha512 = ''; +export const analysis_engine_osx_x64_sha512 = ''; +export const analysis_engine_linux_x64_sha512 = ''; diff --git a/src/client/activation/classic.ts b/src/client/activation/classic.ts new file mode 100644 index 000000000000..ede966e94165 --- /dev/null +++ b/src/client/activation/classic.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { DocumentFilter, ExtensionContext, languages, OutputChannel } from 'vscode'; +import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import { IOutputChannel, IPythonSettings } from '../common/types'; +import { IShebangCodeLensProvider } from '../interpreter/contracts'; +import { IServiceManager } from '../ioc/types'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { PythonCompletionItemProvider } from '../providers/completionProvider'; +import { PythonDefinitionProvider } from '../providers/definitionProvider'; +import { PythonHoverProvider } from '../providers/hoverProvider'; +import { activateGoToObjectDefinitionProvider } from '../providers/objectDefinitionProvider'; +import { PythonReferenceProvider } from '../providers/referenceProvider'; +import { PythonRenameProvider } from '../providers/renameProvider'; +import { PythonSignatureProvider } from '../providers/signatureProvider'; +import { activateSimplePythonRefactorProvider } from '../providers/simpleRefactorProvider'; +import { PythonSymbolProvider } from '../providers/symbolProvider'; +import { TEST_OUTPUT_CHANNEL } from '../unittests/common/constants'; +import * as tests from '../unittests/main'; +import { IExtensionActivator } from './types'; + +const PYTHON: DocumentFilter = { language: 'python' }; + +export class ClassicExtensionActivator implements IExtensionActivator { + constructor(private serviceManager: IServiceManager, private pythonSettings: IPythonSettings) { + } + + public async activate(context: ExtensionContext): Promise { + const standardOutputChannel = this.serviceManager.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + activateSimplePythonRefactorProvider(context, standardOutputChannel, this.serviceManager); + + const jediFactory = new JediFactory(context.asAbsolutePath('.'), this.serviceManager); + context.subscriptions.push(jediFactory); + context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); + + context.subscriptions.push(jediFactory); + context.subscriptions.push(languages.registerRenameProvider(PYTHON, new PythonRenameProvider(this.serviceManager))); + const definitionProvider = new PythonDefinitionProvider(jediFactory); + + context.subscriptions.push(languages.registerDefinitionProvider(PYTHON, definitionProvider)); + context.subscriptions.push(languages.registerHoverProvider(PYTHON, new PythonHoverProvider(jediFactory))); + context.subscriptions.push(languages.registerReferenceProvider(PYTHON, new PythonReferenceProvider(jediFactory))); + context.subscriptions.push(languages.registerCompletionItemProvider(PYTHON, new PythonCompletionItemProvider(jediFactory, this.serviceManager), '.')); + context.subscriptions.push(languages.registerCodeLensProvider(PYTHON, this.serviceManager.get(IShebangCodeLensProvider))); + + const symbolProvider = new PythonSymbolProvider(jediFactory); + context.subscriptions.push(languages.registerDocumentSymbolProvider(PYTHON, symbolProvider)); + + if (this.pythonSettings.devOptions.indexOf('DISABLE_SIGNATURE') === -1) { + context.subscriptions.push(languages.registerSignatureHelpProvider(PYTHON, new PythonSignatureProvider(jediFactory), '(', ',')); + } + + const unitTestOutChannel = this.serviceManager.get(IOutputChannel, TEST_OUTPUT_CHANNEL); + tests.activate(context, unitTestOutChannel, symbolProvider, this.serviceManager); + + return true; + } + + // tslint:disable-next-line:no-empty + public async deactivate(): Promise { } +} diff --git a/src/client/activation/downloader.ts b/src/client/activation/downloader.ts new file mode 100644 index 000000000000..08d0719a2e2c --- /dev/null +++ b/src/client/activation/downloader.ts @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs'; +import * as path from 'path'; +import * as request from 'request'; +import * as requestProgress from 'request-progress'; +import * as unzip from 'unzip'; +import { ExtensionContext, OutputChannel, ProgressLocation, window } from 'vscode'; +import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import { noop } from '../common/core.utils'; +import { createDeferred, createTemporaryFile } from '../common/helpers'; +import { IPlatformService } from '../common/platform/types'; +import { IOutputChannel } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { HashVerifier } from './hashVerifier'; +import { PlatformData } from './platformData'; + +const downloadUriPrefix = 'https://pvsc.blob.core.windows.net/python-analysis'; +const downloadBaseFileName = 'python-analysis-vscode'; +const downloadVersion = '0.1.0'; +const downloadFileExtension = '.nupkg'; + +export class AnalysisEngineDownloader { + private readonly output: OutputChannel; + private readonly platform: IPlatformService; + private readonly platformData: PlatformData; + + constructor(private readonly services: IServiceContainer, private engineFolder: string) { + this.output = this.services.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.platform = this.services.get(IPlatformService); + this.platformData = new PlatformData(this.platform); + } + + public async downloadAnalysisEngine(context: ExtensionContext): Promise { + const localTempFilePath = await this.downloadFile(); + try { + await this.verifyDownload(localTempFilePath); + await this.unpackArchive(context.extensionPath, localTempFilePath); + } catch (err) { + this.output.appendLine('failed.'); + this.output.appendLine(err); + throw new Error(err); + } finally { + fs.unlink(localTempFilePath, noop); + } + } + + private async downloadFile(): Promise { + const platformString = this.platformData.getPlatformDesignator(); + const remoteFileName = `${downloadBaseFileName}-${platformString}.${downloadVersion}${downloadFileExtension}`; + const uri = `${downloadUriPrefix}/${remoteFileName}`; + this.output.append(`Downloading ${uri}... `); + const tempFile = await createTemporaryFile(downloadFileExtension); + + const deferred = createDeferred(); + const fileStream = fs.createWriteStream(tempFile.filePath); + fileStream.on('finish', () => { + fileStream.close(); + }).on('error', (err) => { + tempFile.cleanupCallback(); + deferred.reject(err); + }); + + const title = 'Downloading Python Analysis Engine... '; + await window.withProgress({ + location: ProgressLocation.Window, + title + }, (progress) => { + + requestProgress(request(uri)) + .on('progress', (state) => { + // https://www.npmjs.com/package/request-progress + const received = Math.round(state.size.transferred / 1024); + const total = Math.round(state.size.total / 1024); + const percentage = Math.round(100 * state.percent); + progress.report({ + message: `${title}${received} of ${total} KB (${percentage}%)` + }); + }) + .on('error', (err) => { + deferred.reject(err); + }) + .on('end', () => { + this.output.append('complete.'); + deferred.resolve(); + }) + .pipe(fileStream); + return deferred.promise; + }); + + return tempFile.filePath; + } + + private async verifyDownload(filePath: string): Promise { + this.output.appendLine(''); + this.output.append('Verifying download... '); + const verifier = new HashVerifier(); + if (!await verifier.verifyHash(filePath, this.platformData.getExpectedHash())) { + throw new Error('Hash of the downloaded file does not match.'); + } + this.output.append('valid.'); + } + + private async unpackArchive(extensionPath: string, tempFilePath: string): Promise { + this.output.appendLine(''); + this.output.append('Unpacking archive... '); + + const installFolder = path.join(extensionPath, this.engineFolder); + const deferred = createDeferred(); + + fs.createReadStream(tempFilePath) + .pipe(unzip.Extract({ path: installFolder })) + .on('finish', () => { + deferred.resolve(); + }) + .on('error', (err) => { + deferred.reject(err); + }); + await deferred.promise; + this.output.append('done.'); + + // Set file to executable + if (!this.platform.isWindows) { + const executablePath = path.join(installFolder, this.platformData.getEngineExecutableName()); + fs.chmodSync(executablePath, '0764'); // -rwxrw-r-- + } + } +} diff --git a/src/client/activation/hashVerifier.ts b/src/client/activation/hashVerifier.ts new file mode 100644 index 000000000000..6de248928aba --- /dev/null +++ b/src/client/activation/hashVerifier.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { createHash } from 'crypto'; +import * as fs from 'fs'; +import { createDeferred } from '../common/helpers'; + +export class HashVerifier { + public async verifyHash(filePath: string, expectedDigest: string): Promise { + const readStream = fs.createReadStream(filePath); + const deferred = createDeferred(); + const hash = createHash('sha512'); + hash.setEncoding('hex'); + readStream + .on('end', () => { + hash.end(); + deferred.resolve(); + }) + .on('error', (err) => { + deferred.reject(`Unable to calculate file hash. Error ${err}`); + }); + + readStream.pipe(hash); + await deferred.promise; + const actual = hash.read(); + return expectedDigest === '' ? true : actual === expectedDigest; + } +} diff --git a/src/client/activation/platformData.ts b/src/client/activation/platformData.ts new file mode 100644 index 000000000000..e6f56b951846 --- /dev/null +++ b/src/client/activation/platformData.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IPlatformService } from '../common/platform/types'; +import { + analysis_engine_linux_x64_sha512, + analysis_engine_osx_x64_sha512, + analysis_engine_win_x64_sha512, + analysis_engine_win_x86_sha512 +} from './analysisEngineHashes'; + +export class PlatformData { + constructor(private platform: IPlatformService) { } + public getPlatformDesignator(): string { + if (this.platform.isWindows) { + return this.platform.is64bit ? 'win-x64' : 'win-x86'; + } + if (this.platform.isMac) { + return 'osx-x64'; + } + if (this.platform.isLinux && this.platform.is64bit) { + return 'linux-x64'; + } + throw new Error('Python Analysis Engine does not support 32-bit Linux.'); + } + + public getEngineDllName(): string { + return 'Microsoft.PythonTools.VsCode.dll'; + } + + public getEngineExecutableName(): string { + return this.platform.isWindows + ? 'Microsoft.PythonTools.VsCode.exe' + : 'Microsoft.PythonTools.VsCode'; + } + + public getExpectedHash(): string { + if (this.platform.isWindows) { + return this.platform.is64bit ? analysis_engine_win_x64_sha512 : analysis_engine_win_x86_sha512; + } + if (this.platform.isMac) { + return analysis_engine_osx_x64_sha512; + } + if (this.platform.isLinux && this.platform.is64bit) { + return analysis_engine_linux_x64_sha512; + } + throw new Error('Unknown platform.'); + } +} diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts new file mode 100644 index 000000000000..68e0372c7c96 --- /dev/null +++ b/src/client/activation/types.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; + +export interface IExtensionActivator { + activate(context: vscode.ExtensionContext): Promise; + deactivate(): Promise; +} diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index d4a16e776b0c..f19f75e6ce97 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -3,8 +3,7 @@ import * as child_process from 'child_process'; import { EventEmitter } from 'events'; import * as path from 'path'; -import * as vscode from 'vscode'; -import { ConfigurationTarget, Uri } from 'vscode'; +import { ConfigurationTarget, DiagnosticSeverity, Disposable, Uri, workspace } from 'vscode'; import { isTestExecution } from './constants'; import { IAutoCompeteSettings, @@ -26,32 +25,33 @@ export const IS_WINDOWS = /^win/.test(process.platform); // tslint:disable-next-line:completed-docs export class PythonSettings extends EventEmitter implements IPythonSettings { private static pythonSettings: Map = new Map(); - - public jediPath: string; - public jediMemoryLimit: number; - public envFile: string; - public disablePromptForFeatures: string[]; - public venvPath: string; - public venvFolders: string[]; - public devOptions: string[]; - public linting: ILintingSettings; - public formatting: IFormattingSettings; - public autoComplete: IAutoCompeteSettings; - public unitTest: IUnitTestSettings; - public terminal: ITerminalSettings; - public sortImports: ISortImportSettings; - public workspaceSymbols: IWorkspaceSymbolSettings; - public disableInstallationChecks: boolean; - public globalModuleInstallation: boolean; - - private workspaceRoot: vscode.Uri; - private disposables: vscode.Disposable[] = []; + public jediEnabled = true; + public jediPath = ''; + public jediMemoryLimit = 1024; + public envFile = ''; + public disablePromptForFeatures: string[] = []; + public venvPath = ''; + public venvFolders: string[] = []; + public devOptions: string[] = []; + public linting?: ILintingSettings; + public formatting?: IFormattingSettings; + public autoComplete?: IAutoCompeteSettings; + public unitTest?: IUnitTestSettings; + public terminal?: ITerminalSettings; + public sortImports?: ISortImportSettings; + public workspaceSymbols?: IWorkspaceSymbolSettings; + public disableInstallationChecks = false; + public globalModuleInstallation = false; + + private workspaceRoot: Uri; + private disposables: Disposable[] = []; // tslint:disable-next-line:variable-name - private _pythonPath: string; + private _pythonPath = ''; + constructor(workspaceFolder?: Uri) { super(); - this.workspaceRoot = workspaceFolder ? workspaceFolder : vscode.Uri.file(__dirname); - this.disposables.push(vscode.workspace.onDidChangeConfiguration(() => { + this.workspaceRoot = workspaceFolder ? workspaceFolder : Uri.file(__dirname); + this.disposables.push(workspace.onDidChangeConfiguration(() => { this.initializeSettings(); // If workspace config changes, then we could have a cascading effect of on change events. @@ -74,12 +74,13 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { return PythonSettings.pythonSettings.get(workspaceFolderKey)!; } + // tslint:disable-next-line:type-literal-delimiter public static getSettingsUriAndTarget(resource?: Uri): { uri: Uri | undefined, target: ConfigurationTarget } { - const workspaceFolder = resource ? vscode.workspace.getWorkspaceFolder(resource) : undefined; + const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; let workspaceFolderUri: Uri | undefined = workspaceFolder ? workspaceFolder.uri : undefined; - if (!workspaceFolderUri && Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0) { - workspaceFolderUri = vscode.workspace.workspaceFolders[0].uri; + if (!workspaceFolderUri && Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + workspaceFolderUri = workspace.workspaceFolders[0].uri; } const target = workspaceFolderUri ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Global; @@ -105,21 +106,26 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { private initializeSettings() { const workspaceRoot = this.workspaceRoot.fsPath; const systemVariables: SystemVariables = new SystemVariables(this.workspaceRoot ? this.workspaceRoot.fsPath : undefined); - const pythonSettings = vscode.workspace.getConfiguration('python', this.workspaceRoot); + const pythonSettings = workspace.getConfiguration('python', this.workspaceRoot); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion this.pythonPath = systemVariables.resolveAny(pythonSettings.get('pythonPath'))!; this.pythonPath = getAbsolutePath(this.pythonPath, workspaceRoot); // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion this.venvPath = systemVariables.resolveAny(pythonSettings.get('venvPath'))!; this.venvFolders = systemVariables.resolveAny(pythonSettings.get('venvFolders'))!; - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - this.jediPath = systemVariables.resolveAny(pythonSettings.get('jediPath'))!; - if (typeof this.jediPath === 'string' && this.jediPath.length > 0) { - this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), workspaceRoot); - } else { - this.jediPath = ''; + + this.jediEnabled = systemVariables.resolveAny(pythonSettings.get('jediEnabled'))!; + if (this.jediEnabled) { + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + this.jediPath = systemVariables.resolveAny(pythonSettings.get('jediPath'))!; + if (typeof this.jediPath === 'string' && this.jediPath.length > 0) { + this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), workspaceRoot); + } else { + this.jediPath = ''; + } + this.jediMemoryLimit = pythonSettings.get('jediMemoryLimit')!; } - this.jediMemoryLimit = pythonSettings.get('jediMemoryLimit')!; // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion this.envFile = systemVariables.resolveAny(pythonSettings.get('envFile'))!; @@ -127,6 +133,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion no-any this.devOptions = systemVariables.resolveAny(pythonSettings.get('devOptions'))!; this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion const lintingSettings = systemVariables.resolveAny(pythonSettings.get('linting'))!; // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion @@ -163,27 +170,27 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { pydocstyleArgs: [], pydocstyleEnabled: false, pydocstylePath: 'pydocstyle', pylintArgs: [], pylintEnabled: false, pylintPath: 'pylint', pylintCategorySeverity: { - convention: vscode.DiagnosticSeverity.Hint, - error: vscode.DiagnosticSeverity.Error, - fatal: vscode.DiagnosticSeverity.Error, - refactor: vscode.DiagnosticSeverity.Hint, - warning: vscode.DiagnosticSeverity.Warning + convention: DiagnosticSeverity.Hint, + error: DiagnosticSeverity.Error, + fatal: DiagnosticSeverity.Error, + refactor: DiagnosticSeverity.Hint, + warning: DiagnosticSeverity.Warning }, pep8CategorySeverity: { - E: vscode.DiagnosticSeverity.Error, - W: vscode.DiagnosticSeverity.Warning + E: DiagnosticSeverity.Error, + W: DiagnosticSeverity.Warning }, flake8CategorySeverity: { - E: vscode.DiagnosticSeverity.Error, - W: vscode.DiagnosticSeverity.Warning, + E: DiagnosticSeverity.Error, + W: DiagnosticSeverity.Warning, // Per http://flake8.pycqa.org/en/latest/glossary.html#term-error-code // 'F' does not mean 'fatal as in PyLint but rather 'pyflakes' such as // unused imports, variables, etc. - F: vscode.DiagnosticSeverity.Warning + F: DiagnosticSeverity.Warning }, mypyCategorySeverity: { - error: vscode.DiagnosticSeverity.Error, - note: vscode.DiagnosticSeverity.Hint + error: DiagnosticSeverity.Error, + note: DiagnosticSeverity.Hint }, pylintUseMinimalCheckers: false }; @@ -251,6 +258,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { this.unitTest = unitTestSettings; if (isTestExecution() && !this.unitTest) { // tslint:disable-next-line:prefer-type-cast + // tslint:disable-next-line:no-object-literal-type-assertion this.unitTest = { nosetestArgs: [], pyTestArgs: [], unittestArgs: [], promptToConfigure: true, debugPort: 3000, @@ -287,6 +295,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { this.terminal = terminalSettings; if (isTestExecution() && !this.terminal) { // tslint:disable-next-line:prefer-type-cast + // tslint:disable-next-line:no-object-literal-type-assertion this.terminal = {} as ITerminalSettings; } } diff --git a/src/client/common/configuration/service.ts b/src/client/common/configuration/service.ts index effbba72efed..e5d862930ca3 100644 --- a/src/client/common/configuration/service.ts +++ b/src/client/common/configuration/service.ts @@ -1,16 +1,23 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { injectable } from 'inversify'; +import { inject, injectable } from 'inversify'; import { ConfigurationTarget, Uri, workspace, WorkspaceConfiguration } from 'vscode'; +import { IServiceContainer } from '../../ioc/types'; +import { IApplicationShell } from '../application/types'; import { PythonSettings } from '../configSettings'; +import { IProcessService } from '../process/types'; import { IConfigurationService, IPythonSettings } from '../types'; @injectable() export class ConfigurationService implements IConfigurationService { + constructor(@inject(IServiceContainer) private services: IServiceContainer) { + } + public getSettings(resource?: Uri): IPythonSettings { return PythonSettings.getInstance(resource); } + public async updateSettingAsync(setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise { const settingsInfo = PythonSettings.getSettingsUriAndTarget(resource); @@ -32,6 +39,10 @@ export class ConfigurationService implements IConfigurationService { return process.env.VSC_PYTHON_CI_TEST === '1'; } + public async checkDependencies(): Promise { + return this.checkDotNet(); + } + private async verifySetting(pythonConfig: WorkspaceConfiguration, target: ConfigurationTarget, settingName: string, value?: {}): Promise { if (this.isTestExecution()) { let retries = 0; @@ -55,4 +66,22 @@ export class ConfigurationService implements IConfigurationService { } while (retries < 20); } } + + private async checkDotNet(): Promise { + if (!await this.isDotNetInstalled()) { + const appShell = this.services.get(IApplicationShell); + if (await appShell.showErrorMessage('Python Tools require .NET Core Runtime. Would you like to install it now?', 'Yes', 'No') === 'Yes') { + appShell.openUrl('https://www.microsoft.com/net/download/core#/runtime'); + appShell.showWarningMessage('Please restart VS Code after .NET Runtime installation is complete.'); + } + return false; + } + return true; + } + + private async isDotNetInstalled(): Promise { + const ps = this.services.get(IProcessService); + const result = await ps.exec('dotnet', ['--version']); + return result.stdout.trim().startsWith('2.'); + } } diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 4083b0965fd9..266d1a812cb0 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -98,16 +98,17 @@ export interface IPythonSettings { readonly pythonPath: string; readonly venvPath: string; readonly venvFolders: string[]; + readonly jediEnabled: boolean; readonly jediPath: string; readonly jediMemoryLimit: number; readonly devOptions: string[]; - readonly linting: ILintingSettings; - readonly formatting: IFormattingSettings; - readonly unitTest: IUnitTestSettings; - readonly autoComplete: IAutoCompeteSettings; - readonly terminal: ITerminalSettings; - readonly sortImports: ISortImportSettings; - readonly workspaceSymbols: IWorkspaceSymbolSettings; + readonly linting?: ILintingSettings; + readonly formatting?: IFormattingSettings; + readonly unitTest?: IUnitTestSettings; + readonly autoComplete?: IAutoCompeteSettings; + readonly terminal?: ITerminalSettings; + readonly sortImports?: ISortImportSettings; + readonly workspaceSymbols?: IWorkspaceSymbolSettings; readonly envFile: string; readonly disablePromptForFeatures: string[]; readonly disableInstallationChecks: boolean; @@ -218,6 +219,7 @@ export interface IConfigurationService { getSettings(resource?: Uri): IPythonSettings; isTestExecution(): boolean; updateSettingAsync(setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise; + checkDependencies(): Promise; } export const ISocketServer = Symbol('ISocketServer'); diff --git a/src/client/extension.ts b/src/client/extension.ts index e680bfb21553..dc8ad86358e4 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -11,6 +11,10 @@ import { extensions, IndentAction, languages, Memento, OutputChannel, window } from 'vscode'; +import { IS_ANALYSIS_ENGINE_TEST } from '../test/constants'; +import { AnalysisExtensionActivator } from './activation/analysis'; +import { ClassicExtensionActivator } from './activation/classic'; +import { IExtensionActivator } from './activation/types'; import { PythonSettings } from './common/configSettings'; import { STANDARD_OUTPUT_CHANNEL } from './common/constants'; import { FeatureDeprecationManager } from './common/featureDeprecationManager'; @@ -21,34 +25,24 @@ import { registerTypes as platformRegisterTypes } from './common/platform/servic import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; import { StopWatch } from './common/stopWatch'; -import { GLOBAL_MEMENTO, IDisposableRegistry, ILogger, IMemento, IOutputChannel, IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types'; +import { GLOBAL_MEMENTO, IConfigurationService, IDisposableRegistry, ILogger, IMemento, IOutputChannel, IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types'; import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; import { BaseConfigurationProvider } from './debugger/configProviders/baseProvider'; import { registerTypes as debugConfigurationRegisterTypes } from './debugger/configProviders/serviceRegistry'; import { IDebugConfigurationProvider } from './debugger/types'; import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; import { IInterpreterSelector } from './interpreter/configuration/types'; -import { ICondaService, IInterpreterService, IShebangCodeLensProvider } from './interpreter/contracts'; +import { ICondaService, IInterpreterService } from './interpreter/contracts'; import { registerTypes as interpretersRegisterTypes } from './interpreter/serviceRegistry'; import { ServiceContainer } from './ioc/container'; import { ServiceManager } from './ioc/serviceManager'; import { IServiceContainer } from './ioc/types'; -import { JediFactory } from './languageServices/jediProxyFactory'; import { LinterCommands } from './linters/linterCommands'; import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; import { ILintingEngine } from './linters/types'; -import { PythonCompletionItemProvider } from './providers/completionProvider'; -import { PythonDefinitionProvider } from './providers/definitionProvider'; import { PythonFormattingEditProvider } from './providers/formatProvider'; -import { PythonHoverProvider } from './providers/hoverProvider'; import { LinterProvider } from './providers/linterProvider'; -import { activateGoToObjectDefinitionProvider } from './providers/objectDefinitionProvider'; -import { PythonReferenceProvider } from './providers/referenceProvider'; -import { PythonRenameProvider } from './providers/renameProvider'; import { ReplProvider } from './providers/replProvider'; -import { PythonSignatureProvider } from './providers/signatureProvider'; -import { activateSimplePythonRefactorProvider } from './providers/simpleRefactorProvider'; -import { PythonSymbolProvider } from './providers/symbolProvider'; import { TerminalProvider } from './providers/terminalProvider'; import { activateUpdateSparkLibraryProvider } from './providers/updateSparkLibraryProvider'; import * as sortImports from './sortImports'; @@ -59,54 +53,40 @@ import { ICodeExecutionManager } from './terminals/types'; import { BlockFormatProviders } from './typeFormatters/blockFormatProvider'; import { OnEnterFormatter } from './typeFormatters/onEnterFormatter'; import { TEST_OUTPUT_CHANNEL } from './unittests/common/constants'; -import * as tests from './unittests/main'; import { registerTypes as unitTestsRegisterTypes } from './unittests/serviceRegistry'; import { WorkspaceSymbols } from './workspaceSymbols/main'; -const PYTHON: DocumentFilter = { language: 'python' }; const activationDeferred = createDeferred(); export const activated = activationDeferred.promise; +const PYTHON: DocumentFilter = { language: 'python' }; // tslint:disable-next-line:max-func-body-length export async function activate(context: ExtensionContext) { const cont = new Container(); const serviceManager = new ServiceManager(cont); const serviceContainer = new ServiceContainer(cont); - serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); - serviceManager.addSingletonInstance(IDisposableRegistry, context.subscriptions); - serviceManager.addSingletonInstance(IMemento, context.globalState, GLOBAL_MEMENTO); - serviceManager.addSingletonInstance(IMemento, context.workspaceState, WORKSPACE_MEMENTO); + registerServices(context, serviceManager, serviceContainer); - const standardOutputChannel = window.createOutputChannel('Python'); - const unitTestOutChannel = window.createOutputChannel('Python Test Log'); - serviceManager.addSingletonInstance(IOutputChannel, standardOutputChannel, STANDARD_OUTPUT_CHANNEL); - serviceManager.addSingletonInstance(IOutputChannel, unitTestOutChannel, TEST_OUTPUT_CHANNEL); + const interpreterManager = serviceContainer.get(IInterpreterService); + // This must be completed before we can continue as language server needs the interpreter path. + interpreterManager.initialize(); + await interpreterManager.autoSetInterpreter(); - commonRegisterTypes(serviceManager); - processRegisterTypes(serviceManager); - variableRegisterTypes(serviceManager); - unitTestsRegisterTypes(serviceManager); - lintersRegisterTypes(serviceManager); - interpretersRegisterTypes(serviceManager); - formattersRegisterTypes(serviceManager); - platformRegisterTypes(serviceManager); - installerRegisterTypes(serviceManager); - commonRegisterTerminalTypes(serviceManager); - debugConfigurationRegisterTypes(serviceManager); + const configuration = serviceManager.get(IConfigurationService); + const pythonSettings = configuration.getSettings(); - serviceManager.get(ICodeExecutionManager).registerCommands(); + const activator: IExtensionActivator = IS_ANALYSIS_ENGINE_TEST || !pythonSettings.jediEnabled + ? new AnalysisExtensionActivator(serviceManager, pythonSettings) + : new ClassicExtensionActivator(serviceManager, pythonSettings); - const persistentStateFactory = serviceManager.get(IPersistentStateFactory); - const pythonSettings = PythonSettings.getInstance(); - // tslint:disable-next-line:no-floating-promises - sendStartupTelemetry(activated, serviceContainer); + await activator.activate(context); - sortImports.activate(context, standardOutputChannel, serviceContainer); - const interpreterManager = serviceContainer.get(IInterpreterService); + const standardOutputChannel = serviceManager.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + sortImports.activate(context, standardOutputChannel, serviceManager); - // This must be completed before we can continue. - interpreterManager.initialize(); - await interpreterManager.autoSetInterpreter(); + serviceManager.get(ICodeExecutionManager).registerCommands(); + // tslint:disable-next-line:no-floating-promises + sendStartupTelemetry(activated, serviceContainer); const pythonInstaller = new PythonInstaller(serviceContainer); pythonInstaller.checkPythonInstallation(PythonSettings.getInstance()) @@ -115,15 +95,13 @@ export async function activate(context: ExtensionContext) { interpreterManager.refresh() .catch(ex => console.error('Python Extension: interpreterManager.refresh', ex)); - context.subscriptions.push(serviceContainer.get(IInterpreterSelector)); - context.subscriptions.push(activateUpdateSparkLibraryProvider()); - activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceContainer); - const jediFactory = new JediFactory(context.asAbsolutePath('.'), serviceContainer); - context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); + const jupyterExtension = extensions.getExtension('donjayamanne.jupyter'); + const lintingEngine = serviceManager.get(ILintingEngine); + lintingEngine.linkJupiterExtension(jupyterExtension).ignoreErrors(); - context.subscriptions.push(new ReplProvider(serviceContainer)); - context.subscriptions.push(new TerminalProvider(serviceContainer)); - context.subscriptions.push(new LinterCommands(serviceContainer)); + context.subscriptions.push(new LinterCommands(serviceManager)); + const linterProvider = new LinterProvider(context, serviceManager); + context.subscriptions.push(linterProvider); // Enable indentAction // tslint:disable-next-line:no-non-null-assertion @@ -146,47 +124,55 @@ export async function activate(context: ExtensionContext) { ] }); - context.subscriptions.push(jediFactory); - context.subscriptions.push(languages.registerRenameProvider(PYTHON, new PythonRenameProvider(serviceContainer))); - const definitionProvider = new PythonDefinitionProvider(jediFactory); - context.subscriptions.push(languages.registerDefinitionProvider(PYTHON, definitionProvider)); - context.subscriptions.push(languages.registerHoverProvider(PYTHON, new PythonHoverProvider(jediFactory))); - context.subscriptions.push(languages.registerReferenceProvider(PYTHON, new PythonReferenceProvider(jediFactory))); - context.subscriptions.push(languages.registerCompletionItemProvider(PYTHON, new PythonCompletionItemProvider(jediFactory, serviceContainer), '.')); - context.subscriptions.push(languages.registerCodeLensProvider(PYTHON, serviceContainer.get(IShebangCodeLensProvider))); - - const symbolProvider = new PythonSymbolProvider(jediFactory); - context.subscriptions.push(languages.registerDocumentSymbolProvider(PYTHON, symbolProvider)); - if (pythonSettings.devOptions.indexOf('DISABLE_SIGNATURE') === -1) { - context.subscriptions.push(languages.registerSignatureHelpProvider(PYTHON, new PythonSignatureProvider(jediFactory), '(', ',')); - } - if (pythonSettings.formatting.provider !== 'none') { + if (pythonSettings && pythonSettings.formatting && pythonSettings.formatting.provider !== 'none') { const formatProvider = new PythonFormattingEditProvider(context, serviceContainer); context.subscriptions.push(languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); context.subscriptions.push(languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); } - const linterProvider = new LinterProvider(context, serviceContainer); - context.subscriptions.push(linterProvider); + context.subscriptions.push(languages.registerOnTypeFormattingEditProvider(PYTHON, new BlockFormatProviders(), ':')); + context.subscriptions.push(languages.registerOnTypeFormattingEditProvider(PYTHON, new OnEnterFormatter(), '\n')); - const jupyterExtension = extensions.getExtension('donjayamanne.jupyter'); - const lintingEngine = serviceContainer.get(ILintingEngine); - lintingEngine.linkJupiterExtension(jupyterExtension).ignoreErrors(); + const persistentStateFactory = serviceManager.get(IPersistentStateFactory); + const deprecationMgr = new FeatureDeprecationManager(persistentStateFactory, !!jupyterExtension); + deprecationMgr.initialize(); + context.subscriptions.push(new FeatureDeprecationManager(persistentStateFactory, !!jupyterExtension)); - tests.activate(context, unitTestOutChannel, symbolProvider, serviceContainer); + context.subscriptions.push(serviceContainer.get(IInterpreterSelector)); + context.subscriptions.push(activateUpdateSparkLibraryProvider()); + context.subscriptions.push(new ReplProvider(serviceContainer)); + context.subscriptions.push(new TerminalProvider(serviceContainer)); context.subscriptions.push(new WorkspaceSymbols(serviceContainer)); - context.subscriptions.push(languages.registerOnTypeFormattingEditProvider(PYTHON, new BlockFormatProviders(), ':')); - context.subscriptions.push(languages.registerOnTypeFormattingEditProvider(PYTHON, new OnEnterFormatter(), '\n')); serviceContainer.getAll(IDebugConfigurationProvider).forEach(debugConfig => { context.subscriptions.push(debug.registerDebugConfigurationProvider(debugConfig.debugType, debugConfig)); }); activationDeferred.resolve(); +} - const deprecationMgr = new FeatureDeprecationManager(persistentStateFactory, !!jupyterExtension); - deprecationMgr.initialize(); - context.subscriptions.push(new FeatureDeprecationManager(persistentStateFactory, !!jupyterExtension)); +function registerServices(context: ExtensionContext, serviceManager: ServiceManager, serviceContainer: ServiceContainer) { + serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); + serviceManager.addSingletonInstance(IDisposableRegistry, context.subscriptions); + serviceManager.addSingletonInstance(IMemento, context.globalState, GLOBAL_MEMENTO); + serviceManager.addSingletonInstance(IMemento, context.workspaceState, WORKSPACE_MEMENTO); + + const standardOutputChannel = window.createOutputChannel('Python'); + const unitTestOutChannel = window.createOutputChannel('Python Test Log'); + serviceManager.addSingletonInstance(IOutputChannel, standardOutputChannel, STANDARD_OUTPUT_CHANNEL); + serviceManager.addSingletonInstance(IOutputChannel, unitTestOutChannel, TEST_OUTPUT_CHANNEL); + + commonRegisterTypes(serviceManager); + processRegisterTypes(serviceManager); + variableRegisterTypes(serviceManager); + unitTestsRegisterTypes(serviceManager); + lintersRegisterTypes(serviceManager); + interpretersRegisterTypes(serviceManager); + formattersRegisterTypes(serviceManager); + platformRegisterTypes(serviceManager); + installerRegisterTypes(serviceManager); + commonRegisterTerminalTypes(serviceManager); + debugConfigurationRegisterTypes(serviceManager); } async function sendStartupTelemetry(activatedPromise: Promise, serviceContainer: IServiceContainer) { diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index cc64e708bea1..ba9c239e801d 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -5,11 +5,8 @@ "python.unitTest.nosetestArgs": [], "python.unitTest.pyTestArgs": [], "python.unitTest.unittestArgs": [ - "-v", - "-s", - ".", - "-p", - "*test*.py" + "-s=./tests", + "-p=test_*.py" ], "python.sortImports.args": [], "python.linting.lintOnSave": false, @@ -20,5 +17,6 @@ "python.linting.pylamaEnabled": false, "python.linting.mypyEnabled": false, "python.formatting.provider": "yapf", - "python.linting.pylintUseMinimalCheckers": false + "python.linting.pylintUseMinimalCheckers": false, + "python.pythonPath": "python" } diff --git a/src/test/analysisEngineTest.ts b/src/test/analysisEngineTest.ts new file mode 100644 index 000000000000..509cba41160c --- /dev/null +++ b/src/test/analysisEngineTest.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-console no-require-imports no-var-requires +import * as path from 'path'; + +process.env.CODE_TESTS_WORKSPACE = path.join(__dirname, '..', '..', 'src', 'test'); +process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; +process.env.VSC_PYTHON_ANALYSIS = '1'; + +function start() { + console.log('*'.repeat(100)); + console.log('Start Analysis Engine tests'); + require('../../node_modules/vscode/bin/test'); +} +start(); diff --git a/src/test/autocomplete/base.test.ts b/src/test/autocomplete/base.test.ts index 4c4b8fd65992..5fc13ce33f9b 100644 --- a/src/test/autocomplete/base.test.ts +++ b/src/test/autocomplete/base.test.ts @@ -6,8 +6,9 @@ import * as assert from 'assert'; import { EOL } from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; +import { IConfigurationService } from '../../client/common/types'; import { rootWorkspaceUri } from '../common'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { closeActiveWindows, initialize, initializeTest, IS_ANALYSIS_ENGINE_TEST } from '../initialize'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); @@ -24,6 +25,7 @@ const fileSuppress = path.join(autoCompPath, 'suppress.py'); suite('Autocomplete', () => { let isPython2: boolean; let ioc: UnitTestIocContainer; + suiteSetup(async () => { await initialize(); initializeDI(); @@ -95,6 +97,10 @@ suite('Autocomplete', () => { // https://github.com/DonJayamanne/pythonVSCode/issues/630 test('For "abc.decorators"', async () => { + // Disabled for MS Python Code Analysis, see https://github.com/Microsoft/PTVS/issues/3857 + if (IS_ANALYSIS_ENGINE_TEST) { + return; + } const textDocument = await vscode.workspace.openTextDocument(fileDecorator); await vscode.window.showTextDocument(textDocument); let position = new vscode.Position(3, 9); @@ -156,8 +162,11 @@ suite('Autocomplete', () => { const items = list!.items.filter(item => item.label === 'bar'); assert.equal(items.length, 1, 'bar not found'); - const expected = `说明 - keep this line, it works${EOL}delete following line, it works${EOL}如果存在需要等待审批或正在执行的任务,将不刷新页面`; - checkDocumentation(items[0], expected); + const expected1 = '说明 - keep this line, it works'; + checkDocumentation(items[0], expected1); + + const expected2 = '如果存在需要等待审批或正在执行的任务,将不刷新页面'; + checkDocumentation(items[0], expected2); }).then(done, done); }); @@ -178,13 +187,21 @@ suite('Autocomplete', () => { items = list!.items.filter(item => item.label === 'showMessage'); assert.equal(items.length, 1, 'showMessage not found'); - const expected = `Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи. ${EOL}Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку.`; - checkDocumentation(items[0], expected); + const expected1 = 'Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи.'; + checkDocumentation(items[0], expected1); + + const expected2 = 'Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку.'; + checkDocumentation(items[0], expected2); }).then(done, done); }); // https://github.com/Microsoft/vscode-python/issues/110 test('Suppress in strings/comments', async () => { + // Excluded from MS Python Code Analysis b/c skipping of strings and comments + // is not yet there. See https://github.com/Microsoft/PTVS/issues/3798 + if (IS_ANALYSIS_ENGINE_TEST) { + return; + } const positions = [ new vscode.Position(0, 1), // false new vscode.Position(0, 9), // true @@ -214,9 +231,18 @@ suite('Autocomplete', () => { // tslint:disable-next-line:no-any function checkDocumentation(item: vscode.CompletionItem, expectedContains: string): void { - const documentation = item.documentation as vscode.MarkdownString; - assert.notEqual(documentation, null, 'Documentation is not MarkdownString'); + let isValidType = false; + let documentation: string; + + if (typeof item.documentation === 'string') { + isValidType = true; + documentation = item.documentation; + } else { + documentation = (item.documentation as vscode.MarkdownString).value; + isValidType = documentation !== undefined && documentation !== null; + } + assert.equal(isValidType, true, 'Documentation is neither string nor vscode.MarkdownString'); - const inDoc = documentation.value.indexOf(expectedContains) >= 0; + const inDoc = documentation.indexOf(expectedContains) >= 0; assert.equal(inDoc, true, 'Documentation incorrect'); } diff --git a/src/test/autocomplete/pep484.test.ts b/src/test/autocomplete/pep484.test.ts index 1eb13a792f1c..bac83c7afefa 100644 --- a/src/test/autocomplete/pep484.test.ts +++ b/src/test/autocomplete/pep484.test.ts @@ -2,6 +2,7 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; import { rootWorkspaceUri } from '../common'; +import { IS_ANALYSIS_ENGINE_TEST } from '../constants'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; @@ -11,10 +12,20 @@ const filePep484 = path.join(autoCompPath, 'pep484.py'); suite('Autocomplete PEP 484', () => { let isPython2: boolean; let ioc: UnitTestIocContainer; - suiteSetup(async () => { + suiteSetup(async function () { + // https://github.com/Microsoft/PTVS/issues/3917 + if (IS_ANALYSIS_ENGINE_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } await initialize(); initializeDI(); isPython2 = await ioc.getPythonMajorVersion(rootWorkspaceUri) === 2; + if (isPython2) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + return; + } }); setup(initializeTest); suiteTeardown(closeActiveWindows); @@ -29,12 +40,7 @@ suite('Autocomplete PEP 484', () => { ioc.registerProcessTypes(); } - test('argument', async function () { - if (isPython2) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - return; - } + test('argument', async () => { const textDocument = await vscode.workspace.openTextDocument(filePep484); await vscode.window.showTextDocument(textDocument); assert(vscode.window.activeTextEditor, 'No active editor'); @@ -46,9 +52,6 @@ suite('Autocomplete PEP 484', () => { }); test('return value', async () => { - if (isPython2) { - return; - } const textDocument = await vscode.workspace.openTextDocument(filePep484); await vscode.window.showTextDocument(textDocument); assert(vscode.window.activeTextEditor, 'No active editor'); diff --git a/src/test/autocomplete/pep526.test.ts b/src/test/autocomplete/pep526.test.ts index 099df5af10ab..24aadb2eb04d 100644 --- a/src/test/autocomplete/pep526.test.ts +++ b/src/test/autocomplete/pep526.test.ts @@ -2,19 +2,30 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; import { rootWorkspaceUri } from '../common'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { closeActiveWindows, initialize, initializeTest, IS_ANALYSIS_ENGINE_TEST } from '../initialize'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); const filePep526 = path.join(autoCompPath, 'pep526.py'); +// tslint:disable-next-line:max-func-body-length suite('Autocomplete PEP 526', () => { let isPython2: boolean; let ioc: UnitTestIocContainer; - suiteSetup(async () => { + suiteSetup(async function () { + // https://github.com/Microsoft/PTVS/issues/3917 + if (IS_ANALYSIS_ENGINE_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } await initialize(); initializeDI(); isPython2 = await ioc.getPythonMajorVersion(rootWorkspaceUri) === 2; + if (isPython2) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + return; + } }); setup(initializeTest); suiteTeardown(closeActiveWindows); @@ -29,12 +40,7 @@ suite('Autocomplete PEP 526', () => { ioc.registerProcessTypes(); } - test('variable (abc:str)', async function () { - if (isPython2) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - return; - } + test('variable (abc:str)', async () => { const textDocument = await vscode.workspace.openTextDocument(filePep526); await vscode.window.showTextDocument(textDocument); assert(vscode.window.activeTextEditor, 'No active editor'); @@ -45,11 +51,7 @@ suite('Autocomplete PEP 526', () => { assert.notEqual(list!.items.filter(item => item.label === 'lower').length, 0, 'lower not found'); }); - test('variable (abc: str = "")', async function () { - if (isPython2) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - } + test('variable (abc: str = "")', async () => { const textDocument = await vscode.workspace.openTextDocument(filePep526); await vscode.window.showTextDocument(textDocument); assert(vscode.window.activeTextEditor, 'No active editor'); @@ -60,12 +62,7 @@ suite('Autocomplete PEP 526', () => { assert.notEqual(list!.items.filter(item => item.label === 'lower').length, 0, 'lower not found'); }); - test('variable (abc = UNKNOWN # type: str)', async function () { - if (isPython2) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - return; - } + test('variable (abc = UNKNOWN # type: str)', async () => { const textDocument = await vscode.workspace.openTextDocument(filePep526); await vscode.window.showTextDocument(textDocument); assert(vscode.window.activeTextEditor, 'No active editor'); @@ -76,12 +73,7 @@ suite('Autocomplete PEP 526', () => { assert.notEqual(list!.items.filter(item => item.label === 'lower').length, 0, 'lower not found'); }); - test('class methods', async function () { - if (isPython2) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - return; - } + test('class methods', async () => { const textDocument = await vscode.workspace.openTextDocument(filePep526); await vscode.window.showTextDocument(textDocument); assert(vscode.window.activeTextEditor, 'No active editor'); @@ -94,12 +86,7 @@ suite('Autocomplete PEP 526', () => { assert.notEqual(list!.items.filter(item => item.label === 'b').length, 0, 'method b not found'); }); - test('class method types', async function () { - if (isPython2) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - return; - } + test('class method types', async () => { const textDocument = await vscode.workspace.openTextDocument(filePep526); await vscode.window.showTextDocument(textDocument); assert(vscode.window.activeTextEditor, 'No active editor'); diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index 72201771d5fc..698d424111a8 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -19,7 +19,7 @@ import { CurrentProcess } from '../../client/common/process/currentProcess'; import { IProcessService, IPythonExecutionFactory } from '../../client/common/process/types'; import { ITerminalService, ITerminalServiceFactory } from '../../client/common/terminal/types'; import { IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IPythonSettings, IsWindows } from '../../client/common/types'; -import { ICondaService, IInterpreterLocatorService, IInterpreterService, INTERPRETER_LOCATOR_SERVICE, InterpreterType, PIPENV_SERVICE } from '../../client/interpreter/contracts'; +import { ICondaService, IInterpreterLocatorService, IInterpreterService, INTERPRETER_LOCATOR_SERVICE, InterpreterType, PIPENV_SERVICE, PythonInterpreter } from '../../client/interpreter/contracts'; import { IServiceContainer } from '../../client/ioc/types'; import { rootWorkspaceUri } from '../common'; import { MockModuleInstaller } from '../mocks/moduleInstaller'; @@ -71,8 +71,10 @@ suite('Module Installer', () => { ioc.serviceManager.addSingleton(IModuleInstaller, PipEnvInstaller); condaService = TypeMoq.Mock.ofType(); ioc.serviceManager.addSingletonInstance(ICondaService, condaService.object); + interpreterService = TypeMoq.Mock.ofType(); ioc.serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); + ioc.serviceManager.addSingleton(IPathUtils, PathUtils); ioc.serviceManager.addSingleton(ICurrentProcess, CurrentProcess); ioc.serviceManager.addSingleton(IFileSystem, FileSystem); @@ -191,6 +193,12 @@ suite('Module Installer', () => { ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, TypeMoq.Mock.ofType().object, PIPENV_SERVICE); + const interpreter: PythonInterpreter = { + type: InterpreterType.Unknown, + path: 'python' + }; + interpreterService.setup(x => x.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreter)); + const moduleName = 'xyz'; const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); diff --git a/src/test/common/process/pythonProc.simple.multiroot.test.ts b/src/test/common/process/pythonProc.simple.multiroot.test.ts index 2b9a3b513ad7..d8ff7c09cbb4 100644 --- a/src/test/common/process/pythonProc.simple.multiroot.test.ts +++ b/src/test/common/process/pythonProc.simple.multiroot.test.ts @@ -45,11 +45,9 @@ suite('PythonExecutableService', () => { this.skip(); } await clearPythonPathInWorkspaceFolder(workspace4Path); - - await (new ConfigurationService()).updateSettingAsync('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); await initialize(); }); - setup(() => { + setup(async () => { cont = new Container(); serviceContainer = new ServiceContainer(cont); const serviceManager = new ServiceManager(cont); @@ -69,6 +67,7 @@ suite('PythonExecutableService', () => { configService = serviceManager.get(IConfigurationService); pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); + await configService.updateSettingAsync('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); return initializeTest(); }); suiteTeardown(closeActiveWindows); diff --git a/src/test/constants.ts b/src/test/constants.ts index 1b468a7d9d57..eafa32191cda 100644 --- a/src/test/constants.ts +++ b/src/test/constants.ts @@ -3,9 +3,11 @@ // tslint:disable:no-string-literal import { workspace } from 'vscode'; +import { PythonSettings } from '../client/common/configSettings'; export const IS_APPVEYOR = process.env['APPVEYOR'] === 'true'; -export const IS_CI_SERVER = process.env['TRAVIS'] === 'true' || IS_APPVEYOR; +export const IS_TRAVIS = process.env['TRAVIS'] === 'true'; +export const IS_CI_SERVER = IS_TRAVIS || IS_APPVEYOR; export const TEST_TIMEOUT = 25000; export const IS_MULTI_ROOT_TEST = isMultitrootTest(); export const IS_CI_SERVER_TEST_DEBUGGER = process.env['IS_CI_SERVER_TEST_DEBUGGER'] === '1'; @@ -15,3 +17,6 @@ export const TEST_DEBUGGER = IS_CI_SERVER ? IS_CI_SERVER_TEST_DEBUGGER : true; function isMultitrootTest() { return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 1; } + +export const IS_ANALYSIS_ENGINE_TEST = + !IS_TRAVIS && (process.env['VSC_PYTHON_ANALYSIS'] === '1' || !PythonSettings.getInstance().jediEnabled); diff --git a/src/test/definitions/hover.test.ts b/src/test/definitions/hover.jedi.test.ts similarity index 98% rename from src/test/definitions/hover.test.ts rename to src/test/definitions/hover.jedi.test.ts index ba194a902446..6b72b3d56622 100644 --- a/src/test/definitions/hover.test.ts +++ b/src/test/definitions/hover.jedi.test.ts @@ -2,6 +2,7 @@ import * as assert from 'assert'; import { EOL } from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; +import { IS_ANALYSIS_ENGINE_TEST } from '../constants'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; import { normalizeMarkedString } from '../textUtils'; @@ -15,8 +16,14 @@ const fileHover = path.join(autoCompPath, 'hoverTest.py'); const fileStringFormat = path.join(hoverPath, 'stringFormat.py'); // tslint:disable-next-line:max-func-body-length -suite('Hover Definition', () => { - suiteSetup(initialize); +suite('Hover Definition (Jedi)', () => { + suiteSetup(async function () { + if (IS_ANALYSIS_ENGINE_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); setup(initializeTest); suiteTeardown(closeActiveWindows); teardown(closeActiveWindows); diff --git a/src/test/definitions/hover.ptvs.test.ts b/src/test/definitions/hover.ptvs.test.ts new file mode 100644 index 000000000000..0635b708bd70 --- /dev/null +++ b/src/test/definitions/hover.ptvs.test.ts @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import '../../client/common/extensions'; +import { IS_ANALYSIS_ENGINE_TEST } from '../constants'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { normalizeMarkedString } from '../textUtils'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); +const hoverPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'hover'); +const fileOne = path.join(autoCompPath, 'one.py'); +const fileThree = path.join(autoCompPath, 'three.py'); +const fileEncoding = path.join(autoCompPath, 'four.py'); +const fileEncodingUsed = path.join(autoCompPath, 'five.py'); +const fileHover = path.join(autoCompPath, 'hoverTest.py'); +const fileStringFormat = path.join(hoverPath, 'stringFormat.py'); + +let textDocument: vscode.TextDocument; + +// tslint:disable-next-line:max-func-body-length +suite('Hover Definition (Analysis Engine)', () => { + suiteSetup(async function () { + if (!IS_ANALYSIS_ENGINE_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + async function openAndHover(file: string, line: number, character: number): Promise { + textDocument = await vscode.workspace.openTextDocument(file); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(line, character); + const result = await vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + return result ? result : []; + } + + test('Method', async () => { + const def = await openAndHover(fileOne, 30, 5); + assert.equal(def.length, 1, 'Definition length is incorrect'); + + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '30,0', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '30,11', 'End position is incorrect'); + assert.equal(def[0].contents.length, 1, 'Invalid content items'); + + const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); + assert.equal(lines.length, 2, 'incorrect number of lines'); + assert.equal(lines[0].trim(), 'obj.method1: method method1 of one.Class1 objects', 'function signature line #1 is incorrect'); + assert.equal(lines[1].trim(), 'This is method1', 'function signature line #2 is incorrect'); + }); + + test('Across files', async () => { + const def = await openAndHover(fileThree, 1, 12); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '1,0', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '1,12', 'End position is incorrect'); + + const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); + assert.equal(lines.length, 2, 'incorrect number of lines'); + assert.equal(lines[0].trim(), 'two.ct().fun: method fun of two.ct objects', 'function signature line #1 is incorrect'); + assert.equal(lines[1].trim(), 'This is fun', 'function signature line #2 is incorrect'); + }); + + test('With Unicode Characters', async () => { + const def = await openAndHover(fileEncoding, 25, 6); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '25,0', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '25,7', 'End position is incorrect'); + + const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); + assert.equal(lines.length, 5, 'incorrect number of lines'); + assert.equal(lines[0].trim(), 'Foo.bar: def four.Foo.bar()', 'function signature line #1 is incorrect'); + assert.equal(lines[1].trim(), '说明 - keep this line, it works', 'function signature line #2 is incorrect'); + assert.equal(lines[2].trim(), 'delete following line, it works', 'function signature line #3 is incorrect'); + assert.equal(lines[3].trim(), '如果存在需要等待审批或正在执行的任务,将不刷新页面', 'function signature line #4 is incorrect'); + assert.equal(lines[4].trim(), 'declared in Foo', 'function signature line #5 is incorrect'); + }); + + test('Across files with Unicode Characters', async () => { + const def = await openAndHover(fileEncodingUsed, 1, 11); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '1,0', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '1,16', 'End position is incorrect'); + + const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); + assert.equal(lines.length, 3, 'incorrect number of lines'); + assert.equal(lines[0].trim(), 'four.showMessage: def four.showMessage()', 'function signature line #1 is incorrect'); + assert.equal(lines[1].trim(), 'Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи.', 'function signature line #2 is incorrect'); + assert.equal(lines[2].trim(), 'Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку.', 'function signature line #3 is incorrect'); + }); + + test('Nothing for keywords (class)', async () => { + const def = await openAndHover(fileOne, 5, 1); + assert.equal(def.length, 0, 'Definition length is incorrect'); + }); + + test('Nothing for keywords (for)', async () => { + const def = await openAndHover(fileHover, 3, 1); + assert.equal(def!.length, 0, 'Definition length is incorrect'); + }); + + test('Highlighting Class', async () => { + const def = await openAndHover(fileHover, 11, 15); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '11,7', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '11,18', 'End position is incorrect'); + + const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); + assert.equal(lines.length, 9, 'incorrect number of lines'); + assert.equal(lines[0].trim(), 'misc.Random: class misc.Random(_random.Random)', 'function signature line #1 is incorrect'); + assert.equal(lines[1].trim(), 'Random number generator base class used by bound module functions.', 'function signature line #2 is incorrect'); + }); + + test('Highlight Method', async () => { + const def = await openAndHover(fileHover, 12, 10); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '12,0', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '12,12', 'End position is incorrect'); + + const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); + assert.equal(lines.length, 2, 'incorrect number of lines'); + assert.equal(lines[0].trim(), 'rnd2.randint: method randint of misc.Random objects -> int', 'function signature line #1 is incorrect'); + assert.equal(lines[1].trim(), 'Return random integer in range [a, b], including both end points.', 'function signature line #2 is incorrect'); + }); + + test('Highlight Function', async () => { + const def = await openAndHover(fileHover, 8, 14); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '8,6', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '8,15', 'End position is incorrect'); + + const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); + assert.equal(lines.length, 3, 'incorrect number of lines'); + assert.equal(lines[0].trim(), 'math.acos: built-in function acos(x)', 'function signature line #1 is incorrect'); + assert.equal(lines[1].trim(), 'acos(x)', 'function signature line #2 is incorrect'); + assert.equal(lines[2].trim(), 'Return the arc cosine (measured in radians) of x.', 'function signature line #3 is incorrect'); + }); + + test('Highlight Multiline Method Signature', async () => { + const def = await openAndHover(fileHover, 14, 14); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '14,4', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '14,15', 'End position is incorrect'); + + const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); + assert.equal(lines.length, 3, 'incorrect number of lines'); + assert.equal(lines[0].trim(), 'misc.Thread: class misc.Thread(_Verbose)', 'function signature line #1 is incorrect'); + assert.equal(lines[1].trim(), 'A class that represents a thread of control.', 'function signature line #2 is incorrect'); + + }); + + test('Variable', async () => { + const def = await openAndHover(fileHover, 6, 2); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(def[0].contents.length, 1, 'Only expected one result'); + const contents = normalizeMarkedString(def[0].contents[0]); + if (contents.indexOf('Random') === -1) { + assert.fail(contents, '', 'Variable type is missing', 'compare'); + } + }); + + test('format().capitalize()', async function () { + // https://github.com/Microsoft/PTVS/issues/3868 + // tslint:disable-next-line:no-invalid-this + this.skip(); + const def = await openAndHover(fileStringFormat, 5, 41); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(def[0].contents.length, 1, 'Only expected one result'); + const contents = normalizeMarkedString(def[0].contents[0]); + if (contents.indexOf('capitalize') === -1) { + assert.fail(contents, '', '\'capitalize\' is missing', 'compare'); + } + if (contents.indexOf('Return a capitalized version of S') === -1 && + contents.indexOf('Return a copy of the string S with only its first character') === -1) { + assert.fail(contents, '', '\'Return a capitalized version of S/Return a copy of the string S with only its first character\' message missing', 'compare'); + } + }); +}); diff --git a/src/test/definitions/parallel.test.ts b/src/test/definitions/parallel.jedi.test.ts similarity index 89% rename from src/test/definitions/parallel.test.ts rename to src/test/definitions/parallel.jedi.test.ts index 535587ba5e03..9aaa47a60d37 100644 --- a/src/test/definitions/parallel.test.ts +++ b/src/test/definitions/parallel.jedi.test.ts @@ -2,16 +2,22 @@ import * as assert from 'assert'; import { EOL } from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; -import { Hover } from 'vscode'; import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { IS_ANALYSIS_ENGINE_TEST } from '../constants'; import { closeActiveWindows, initialize } from '../initialize'; import { normalizeMarkedString } from '../textUtils'; const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); const fileOne = path.join(autoCompPath, 'one.py'); -suite('Code, Hover Definition and Intellisense', () => { - suiteSetup(initialize); +suite('Code, Hover Definition and Intellisense (Jedi)', () => { + suiteSetup(async function () { + if (IS_ANALYSIS_ENGINE_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); suiteTeardown(closeActiveWindows); teardown(closeActiveWindows); diff --git a/src/test/definitions/parallel.ptvs.test.ts b/src/test/definitions/parallel.ptvs.test.ts new file mode 100644 index 000000000000..9acec294e666 --- /dev/null +++ b/src/test/definitions/parallel.ptvs.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { EOL } from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { IS_ANALYSIS_ENGINE_TEST } from '../constants'; +import { closeActiveWindows, initialize } from '../initialize'; +import { normalizeMarkedString } from '../textUtils'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); +const fileOne = path.join(autoCompPath, 'one.py'); + +suite('Code, Hover Definition and Intellisense (MS Python Code Analysis)', () => { + suiteSetup(async function () { + // https://github.com/Microsoft/vscode-python/issues/1061 + // tslint:disable-next-line:no-invalid-this + this.skip(); + + if (!IS_ANALYSIS_ENGINE_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + test('All three together', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileOne); + + let position = new vscode.Position(30, 5); + const hoverDef = await vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + const codeDef = await vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); + position = new vscode.Position(3, 10); + const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + + assert.equal(list!.items.filter(item => item.label === 'api_version').length, 1, 'api_version not found'); + + assert.equal(codeDef!.length, 1, 'Definition length is incorrect'); + const expectedPath = IS_WINDOWS ? fileOne.toUpperCase() : fileOne; + const actualPath = IS_WINDOWS ? codeDef![0].uri.fsPath.toUpperCase() : codeDef![0].uri.fsPath; + assert.equal(actualPath, expectedPath, 'Incorrect file'); + assert.equal(`${codeDef![0].range!.start.line},${codeDef![0].range!.start.character}`, '17,4', 'Start position is incorrect'); + assert.equal(`${codeDef![0].range!.end.line},${codeDef![0].range!.end.character}`, '21,11', 'End position is incorrect'); + + assert.equal(hoverDef!.length, 1, 'Definition length is incorrect'); + assert.equal(`${hoverDef![0].range!.start.line},${hoverDef![0].range!.start.character}`, '30,4', 'Start position is incorrect'); + assert.equal(`${hoverDef![0].range!.end.line},${hoverDef![0].range!.end.character}`, '30,11', 'End position is incorrect'); + assert.equal(hoverDef![0].contents.length, 1, 'Invalid content items'); + // tslint:disable-next-line:prefer-template + const expectedContent = '```python' + EOL + 'def method1()' + EOL + '```' + EOL + 'This is method1'; + assert.equal(normalizeMarkedString(hoverDef![0].contents[0]), expectedContent, 'function signature incorrect'); + }); +}); diff --git a/src/test/linters/lint.commands.test.ts b/src/test/linters/lint.commands.test.ts index d843c7128f4b..5cac8d6f997f 100644 --- a/src/test/linters/lint.commands.test.ts +++ b/src/test/linters/lint.commands.test.ts @@ -36,6 +36,7 @@ suite('Linting - Linter Selector', () => { const cont = new Container(); const serviceManager = new ServiceManager(cont); serviceContainer = new ServiceContainer(cont); + serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); appShell = TypeMoq.Mock.ofType(); serviceManager.addSingleton(IConfigurationService, ConfigurationService); @@ -74,7 +75,7 @@ suite('Linting - Linter Selector', () => { }); test('Run linter command', async () => { - commands.runLinting(); + await commands.runLinting(); engine.verify(p => p.lintOpenPythonFiles(), TypeMoq.Times.once()); }); diff --git a/src/test/linters/lint.manager.test.ts b/src/test/linters/lint.manager.test.ts index 68f1e20e39c3..06bbd289d4ba 100644 --- a/src/test/linters/lint.manager.test.ts +++ b/src/test/linters/lint.manager.test.ts @@ -8,6 +8,7 @@ import { EnumEx } from '../../client/common/enumUtils'; import { IConfigurationService, ILintingSettings, IPythonSettings, Product } from '../../client/common/types'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; +import { IServiceContainer } from '../../client/ioc/types'; import { LinterManager } from '../../client/linters/linterManager'; import { ILinterManager, LinterId } from '../../client/linters/types'; import { initialize } from '../initialize'; @@ -23,6 +24,7 @@ suite('Linting - Manager', () => { const cont = new Container(); const serviceManager = new ServiceManager(cont); const serviceContainer = new ServiceContainer(cont); + serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); serviceManager.addSingleton(IConfigurationService, ConfigurationService); configService = serviceManager.get(IConfigurationService); diff --git a/src/test/pythonFiles/autocomp/four.py b/src/test/pythonFiles/autocomp/four.py index f67f78af0856..470338f71157 100644 --- a/src/test/pythonFiles/autocomp/four.py +++ b/src/test/pythonFiles/autocomp/four.py @@ -1,4 +1,4 @@ -# -*- coding:utf-8 -*- +# -*- coding: utf-8 -*- # pylint: disable=E0401, W0512 import os diff --git a/src/test/pythonFiles/definition/four.py b/src/test/pythonFiles/definition/four.py index f67f78af0856..470338f71157 100644 --- a/src/test/pythonFiles/definition/four.py +++ b/src/test/pythonFiles/definition/four.py @@ -1,4 +1,4 @@ -# -*- coding:utf-8 -*- +# -*- coding: utf-8 -*- # pylint: disable=E0401, W0512 import os diff --git a/src/test/signature/signature.test.ts b/src/test/signature/signature.jedi.test.ts similarity index 95% rename from src/test/signature/signature.test.ts rename to src/test/signature/signature.jedi.test.ts index a4d002e4e750..1ab80cce964d 100644 --- a/src/test/signature/signature.test.ts +++ b/src/test/signature/signature.jedi.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; import { rootWorkspaceUri } from '../common'; +import { IS_ANALYSIS_ENGINE_TEST } from '../constants'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; @@ -20,10 +21,14 @@ class SignatureHelpResult { } // tslint:disable-next-line:max-func-body-length -suite('Signatures', () => { +suite('Signatures (Jedi)', () => { let isPython2: boolean; let ioc: UnitTestIocContainer; - suiteSetup(async () => { + suiteSetup(async function () { + if (IS_ANALYSIS_ENGINE_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } await initialize(); initializeDI(); isPython2 = await ioc.getPythonMajorVersion(rootWorkspaceUri) === 2; diff --git a/src/test/signature/signature.ptvs.test.ts b/src/test/signature/signature.ptvs.test.ts new file mode 100644 index 000000000000..68720e33cde1 --- /dev/null +++ b/src/test/signature/signature.ptvs.test.ts @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { rootWorkspaceUri } from '../common'; +import { IS_ANALYSIS_ENGINE_TEST } from '../constants'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { UnitTestIocContainer } from '../unittests/serviceRegistry'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'signature'); + +class SignatureHelpResult { + constructor( + public line: number, + public index: number, + public signaturesCount: number, + public activeParameter: number, + public parameterName: string | null) { } +} + +// tslint:disable-next-line:max-func-body-length +suite('Signatures (Analysis Engine)', () => { + let isPython2: boolean; + let ioc: UnitTestIocContainer; + suiteSetup(async function () { + if (!IS_ANALYSIS_ENGINE_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + initializeDI(); + isPython2 = await ioc.getPythonMajorVersion(rootWorkspaceUri) === 2; + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + ioc.dispose(); + }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + } + + test('For ctor', async () => { + const expected = [ + new SignatureHelpResult(5, 11, 1, -1, null), + new SignatureHelpResult(5, 12, 1, 0, 'name'), + new SignatureHelpResult(5, 13, 1, 0, 'name'), + new SignatureHelpResult(5, 14, 1, 0, 'name'), + new SignatureHelpResult(5, 15, 1, 0, 'name'), + new SignatureHelpResult(5, 16, 1, 0, 'name'), + new SignatureHelpResult(5, 17, 1, 0, 'name'), + new SignatureHelpResult(5, 18, 1, 1, 'age'), + new SignatureHelpResult(5, 19, 1, 1, 'age'), + new SignatureHelpResult(5, 20, 1, -1, null) + ]; + + const document = await openDocument(path.join(autoCompPath, 'classCtor.py')); + for (let i = 0; i < expected.length; i += 1) { + await checkSignature(expected[i], document!.uri, i); + } + }); + + test('For intrinsic', async () => { + const expected = [ + new SignatureHelpResult(0, 0, 1, -1, null), + new SignatureHelpResult(0, 1, 1, -1, null), + new SignatureHelpResult(0, 2, 1, -1, null), + new SignatureHelpResult(0, 3, 1, -1, null), + new SignatureHelpResult(0, 4, 1, -1, null), + new SignatureHelpResult(0, 5, 1, -1, null), + new SignatureHelpResult(0, 6, 1, 0, 'stop'), + new SignatureHelpResult(0, 7, 1, 0, 'stop') + // https://github.com/Microsoft/PTVS/issues/3869 + // new SignatureHelpResult(0, 8, 1, 1, 'stop'), + // new SignatureHelpResult(0, 9, 1, 1, 'stop'), + // new SignatureHelpResult(0, 10, 1, 1, 'stop'), + // new SignatureHelpResult(0, 11, 1, 2, 'step'), + // new SignatureHelpResult(1, 0, 1, 2, 'step') + ]; + + const document = await openDocument(path.join(autoCompPath, 'basicSig.py')); + for (let i = 0; i < expected.length; i += 1) { + await checkSignature(expected[i], document!.uri, i); + } + }); + + test('For ellipsis', async function () { + if (isPython2) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + return; + } + const expected = [ + new SignatureHelpResult(0, 5, 0, 0, null), + new SignatureHelpResult(0, 6, 1, 0, 'value'), + new SignatureHelpResult(0, 7, 1, 0, 'value'), + new SignatureHelpResult(0, 8, 1, 1, '...'), + new SignatureHelpResult(0, 9, 1, 1, '...'), + new SignatureHelpResult(0, 10, 1, 1, '...'), + new SignatureHelpResult(0, 11, 1, 2, 'sep'), + new SignatureHelpResult(0, 12, 1, 2, 'sep') + ]; + + const document = await openDocument(path.join(autoCompPath, 'ellipsis.py')); + for (let i = 0; i < expected.length; i += 1) { + await checkSignature(expected[i], document!.uri, i); + } + }); + + test('For pow', async () => { + let expected: SignatureHelpResult; + if (isPython2) { + expected = new SignatureHelpResult(0, 4, 1, 0, 'x'); + } else { + expected = new SignatureHelpResult(0, 4, 1, 0, null); + } + + const document = await openDocument(path.join(autoCompPath, 'noSigPy3.py')); + await checkSignature(expected, document!.uri, 0); + }); +}); + +async function openDocument(documentPath: string): Promise { + const document = await vscode.workspace.openTextDocument(documentPath); + await vscode.window.showTextDocument(document!); + return document; +} + +async function checkSignature(expected: SignatureHelpResult, uri: vscode.Uri, caseIndex: number) { + const position = new vscode.Position(expected.line, expected.index); + const actual = await vscode.commands.executeCommand('vscode.executeSignatureHelpProvider', uri, position); + assert.equal(actual!.signatures.length, expected.signaturesCount, `Signature count does not match, case ${caseIndex}`); + if (expected.signaturesCount > 0) { + assert.equal(actual!.activeParameter, expected.activeParameter, `Parameter index does not match, case ${caseIndex}`); + if (expected.parameterName) { + const parameter = actual!.signatures[0].parameters[expected.activeParameter]; + assert.equal(parameter.label, expected.parameterName, `Parameter name is incorrect, case ${caseIndex}`); + } + } +} diff --git a/vscode-python-signing.csproj b/vscode-python-signing.csproj new file mode 100644 index 000000000000..7fb333c4b277 --- /dev/null +++ b/vscode-python-signing.csproj @@ -0,0 +1,20 @@ + + + netcoreapp2.0 + + + + + + $(OutputPath)\python-$(Branch).vsix + $(UserProfile)\AppData\Roaming\npm\vsce + + + + + + + VsixSHA2 + + + From f3e1a0e097c428d1e418d5687981c374d0780490 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 28 Mar 2018 13:22:07 -0700 Subject: [PATCH 22/24] :sparkles: experimental debugger attach --- src/client/debugger/mainV2.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/client/debugger/mainV2.ts b/src/client/debugger/mainV2.ts index 295711ad6b5a..c7b23495862d 100644 --- a/src/client/debugger/mainV2.ts +++ b/src/client/debugger/mainV2.ts @@ -55,6 +55,11 @@ export class PythonDebugger extends DebugSession { } super.shutdown(); } + public async createAttachDebugServer(attachRequest: DebugProtocol.AttachRequest) { + const launcher = CreateAttachDebugClient(attachRequest.arguments as AttachRequestArguments, this); + this.debugServer = launcher.CreateDebugServer(undefined, this.serviceContainer); + await this.debugServer!.Start(); + } protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { const body = response.body!; From 804a059bced12721e913e5f137219bc6b81be996 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 2 Apr 2018 17:27:29 -0700 Subject: [PATCH 23/24] :white_check_mark: reattach tests --- src/test/debugger/attach.again.ptvsd.test.ts | 150 ++++++++++++++++++ ...moteDebugger-start-with-ptvsd.re-attach.py | 9 ++ 2 files changed, 159 insertions(+) create mode 100644 src/test/debugger/attach.again.ptvsd.test.ts create mode 100644 src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.re-attach.py diff --git a/src/test/debugger/attach.again.ptvsd.test.ts b/src/test/debugger/attach.again.ptvsd.test.ts new file mode 100644 index 000000000000..83b6c046faec --- /dev/null +++ b/src/test/debugger/attach.again.ptvsd.test.ts @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-this max-func-body-length no-empty no-increment-decrement + +import { expect } from 'chai'; +import { ChildProcess, spawn } from 'child_process'; +import * as getFreePort from 'get-port'; +import * as path from 'path'; +import { DebugClient } from 'vscode-debugadapter-testsupport'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import '../../client/common/extensions'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { sleep } from '../common'; +import { initialize, IS_APPVEYOR, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { DEBUGGER_TIMEOUT } from './common/constants'; +import { DebugClientEx } from './debugClient'; + +const fileToDebug = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'remoteDebugger-start-with-ptvsd.re-attach.py'); +const ptvsdPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd'); +const DEBUG_ADAPTER = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'mainV2.js'); + +suite('Attach Debugger - detach and again again - Experimental', () => { + let debugClient: DebugClient; + let procToKill: ChildProcess; + suiteSetup(initialize); + + setup(async function () { + if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + this.skip(); + } + await startDebugger(); + }); + teardown(async () => { + // Wait for a second before starting another test (sometimes, sockets take a while to get closed). + await sleep(1000); + try { + await debugClient.stop().catch(() => { }); + } catch (ex) { } + if (procToKill) { + try { + procToKill.kill(); + } catch { } + } + }); + async function startDebugger() { + await sleep(1000); + debugClient = createDebugAdapter(); + debugClient.defaultTimeout = DEBUGGER_TIMEOUT; + debugClient.defaultTimeout = 1000000; + await debugClient.start(); + } + /** + * Creates the debug aimport { AttachRequestArguments } from '../../client/debugger/Common/Contracts'; + * We do not need to support code coverage on AppVeyor, lets use the standard test adapter. + * @returns {DebugClient} + */ + function createDebugAdapter(): DebugClient { + if (IS_WINDOWS) { + return new DebugClient('node', DEBUG_ADAPTER, 'pythonExperimental'); + } else { + const coverageDirectory = path.join(EXTENSION_ROOT_DIR, 'debug_coverage_attach_ptvsd'); + return new DebugClientEx(DEBUG_ADAPTER, 'pythonExperimental', coverageDirectory, { cwd: EXTENSION_ROOT_DIR }); + } + } + async function startRemoteProcess() { + const port = await getFreePort({ host: 'localhost', port: 9091 }); + const customEnv = { ...process.env }; + + // Set the path for PTVSD to be picked up. + // tslint:disable-next-line:no-string-literal + customEnv['PYTHONPATH'] = ptvsdPath; + const pythonArgs = ['-m', 'ptvsd', '--server', '--port', `${port}`, '--file', fileToDebug.fileToCommandArgument()]; + procToKill = spawn('python', pythonArgs, { env: customEnv, cwd: path.dirname(fileToDebug) }); + return port; + } + + async function waitForDebuggerCondfigurationDone(port: number) { + // Send initialize, attach + const initializePromise = debugClient.initializeRequest({ + adapterID: 'pythonExperimental', + linesStartAt1: true, + columnsStartAt1: true, + supportsRunInTerminalRequest: true, + pathFormat: 'path', + supportsVariableType: true, + supportsVariablePaging: true + }); + const attachPromise = debugClient.attachRequest({ + localRoot: path.dirname(fileToDebug), + remoteRoot: path.dirname(fileToDebug), + type: 'pythonExperimental', + port: port, + host: 'localhost', + logToFile: false, + debugOptions: ['RedirectOutput'] + }); + + await Promise.all([ + initializePromise, + attachPromise, + debugClient.waitForEvent('initialized') + ]); + + await debugClient.configurationDoneRequest(); + } + async function testAttaching(port: number) { + await waitForDebuggerCondfigurationDone(port); + let threads = await debugClient.threadsRequest(); + expect(threads).to.be.not.equal(undefined, 'no threads response'); + expect(threads.body.threads).to.be.lengthOf(1); + + await debugClient.setExceptionBreakpointsRequest({ filters: [] }); + const breakpointLocation = { path: fileToDebug, column: 1, line: 7 }; + await debugClient.setBreakpointsRequest({ + lines: [breakpointLocation.line], + breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }], + source: { path: breakpointLocation.path } + }); + + await debugClient.assertStoppedLocation('breakpoint', breakpointLocation); + await debugClient.setBreakpointsRequest({ lines: [], breakpoints: [], source: { path: breakpointLocation.path } }); + + threads = await debugClient.threadsRequest(); + expect(threads).to.be.not.equal(undefined, 'no threads response'); + expect(threads.body.threads).to.be.lengthOf(1); + + await debugClient.continueRequest({ threadId: threads.body.threads[0].id }); + } + + test('Confirm we are able to attach, detach and attach to a running program', async function () { + this.timeout(20000); + // Lets skip this test on AppVeyor (very flaky on AppVeyor). + if (IS_APPVEYOR) { + return; + } + + const port = await startRemoteProcess(); + await testAttaching(port); + await debugClient.disconnectRequest({}); + await startDebugger(); + await testAttaching(port); + + const terminatedPromise = debugClient.waitForEvent('terminated'); + procToKill.kill(); + await terminatedPromise; + }); +}); diff --git a/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.re-attach.py b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.re-attach.py new file mode 100644 index 000000000000..13409d35f4dd --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.re-attach.py @@ -0,0 +1,9 @@ +import sys +import time +# Give the debugger some time to add a breakpoint. +time.sleep(5) +for i in range(10000): + time.sleep(0.5) + print(i) + +print('bye') From 534ea19e4e714c09491bee5c49674284b5d3fe4d Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 2 Apr 2018 19:27:19 -0700 Subject: [PATCH 24/24] remove unwanted code and add delay for socket server to start --- src/test/debugger/attach.again.ptvsd.test.ts | 3 ++- .../workspace5/remoteDebugger-start-with-ptvsd.re-attach.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/debugger/attach.again.ptvsd.test.ts b/src/test/debugger/attach.again.ptvsd.test.ts index 83b6c046faec..80ab6b9a0e2d 100644 --- a/src/test/debugger/attach.again.ptvsd.test.ts +++ b/src/test/debugger/attach.again.ptvsd.test.ts @@ -49,7 +49,6 @@ suite('Attach Debugger - detach and again again - Experimental', () => { await sleep(1000); debugClient = createDebugAdapter(); debugClient.defaultTimeout = DEBUGGER_TIMEOUT; - debugClient.defaultTimeout = 1000000; await debugClient.start(); } /** @@ -74,6 +73,8 @@ suite('Attach Debugger - detach and again again - Experimental', () => { customEnv['PYTHONPATH'] = ptvsdPath; const pythonArgs = ['-m', 'ptvsd', '--server', '--port', `${port}`, '--file', fileToDebug.fileToCommandArgument()]; procToKill = spawn('python', pythonArgs, { env: customEnv, cwd: path.dirname(fileToDebug) }); + // wait for socket server to start. + await sleep(1000); return port; } diff --git a/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.re-attach.py b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.re-attach.py index 13409d35f4dd..1f4808fb5fb0 100644 --- a/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.re-attach.py +++ b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.re-attach.py @@ -4,6 +4,6 @@ time.sleep(5) for i in range(10000): time.sleep(0.5) - print(i) + pass print('bye')