diff --git a/news/1 Enhancements/3908.md b/news/1 Enhancements/3908.md new file mode 100644 index 000000000000..4f28990779db --- /dev/null +++ b/news/1 Enhancements/3908.md @@ -0,0 +1 @@ +Validate Mac Interpreters in the background. diff --git a/src/client/application/diagnostics/applicationDiagnostics.ts b/src/client/application/diagnostics/applicationDiagnostics.ts index e270305f424e..d316248f9087 100644 --- a/src/client/application/diagnostics/applicationDiagnostics.ts +++ b/src/client/application/diagnostics/applicationDiagnostics.ts @@ -9,6 +9,7 @@ import { STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; import { ILogger, IOutputChannel } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { IApplicationDiagnostics } from '../types'; +import { InvalidMacPythonInterpreterServiceId } from './checks/macPythonInterpreter'; import { IDiagnostic, IDiagnosticsService, ISourceMapSupportService } from './types'; @injectable() @@ -27,6 +28,12 @@ export class ApplicationDiagnostics implements IApplicationDiagnostics { await diagnosticsService.handle(diagnostics); } })); + + // Validate the Mac interperter in the background. + const maccInterpreterService = this.serviceContainer.get(IDiagnosticsService, InvalidMacPythonInterpreterServiceId); + maccInterpreterService.diagnose() + .then(diagnostics => maccInterpreterService.handle(diagnostics)) + .ignoreErrors(); } private log(diagnostics: IDiagnostic[]): void { const logger = this.serviceContainer.get(ILogger); diff --git a/src/client/application/diagnostics/checks/macPythonInterpreter.ts b/src/client/application/diagnostics/checks/macPythonInterpreter.ts new file mode 100644 index 000000000000..9c33207bb129 --- /dev/null +++ b/src/client/application/diagnostics/checks/macPythonInterpreter.ts @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent, DiagnosticSeverity, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import '../../../common/extensions'; +import { IPlatformService } from '../../../common/platform/types'; +import { IConfigurationService, IDisposableRegistry } from '../../../common/types'; +import { IInterpreterHelper, IInterpreterService, InterpreterType } from '../../../interpreter/contracts'; +import { IServiceContainer } from '../../../ioc/types'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; +import { IDiagnosticsCommandFactory } from '../commands/types'; +import { DiagnosticCodes } from '../constants'; +import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; +import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService } from '../types'; + +const messages = { + [DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic]: 'You have selected the macOS system install of Python, which is not recommended for use with the Python extension. Some functionality will be limited, please select a different interpreter.', + [DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic]: 'The macOS system install of Python is not recommended, some functionality in the extension will be limited. Install another version of Python for the best experience.' +}; + +export class InvalidMacPythonInterpreterDiagnostic extends BaseDiagnostic { + constructor(code: DiagnosticCodes) { + super(code, messages[code], DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder); + } +} + +export const InvalidMacPythonInterpreterServiceId = 'InvalidMacPythonInterpreterServiceId'; + +@injectable() +export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { + protected changeThrottleTimeout = 1000; + private timeOut?: NodeJS.Timer; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper) { + super( + [ + DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, + DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic + ], serviceContainer); + this.addPythonPathChangedHandler(); + } + public async diagnose(): Promise { + if (!this.platform.isMac) { + return []; + } + const configurationService = this.serviceContainer.get(IConfigurationService); + const settings = configurationService.getSettings(); + if (settings.disableInstallationChecks === true) { + return []; + } + + const hasInterpreters = await this.interpreterService.hasInterpreters; + if (!hasInterpreters) { + return []; + } + + const currentInterpreter = await this.interpreterService.getActiveInterpreter(); + if (!currentInterpreter) { + return []; + } + + if (!this.helper.isMacDefaultPythonPath(settings.pythonPath)) { + return []; + } + if (!currentInterpreter || currentInterpreter.type !== InterpreterType.Unknown) { + return []; + } + + const interpreters = await this.interpreterService.getInterpreters(); + if (interpreters.filter(i => !this.helper.isMacDefaultPythonPath(i.path)).length === 0) { + return [new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic)]; + } + + return [new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic)]; + } + public async handle(diagnostics: IDiagnostic[]): Promise { + if (diagnostics.length === 0) { + return; + } + const messageService = this.serviceContainer.get>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerServiceId); + await Promise.all(diagnostics.map(async diagnostic => { + if (!this.canHandle(diagnostic)) { + return; + } + const commandPrompts = this.getCommandPrompts(diagnostic); + return messageService.handle(diagnostic, { commandPrompts, message: diagnostic.message }); + })); + } + protected addPythonPathChangedHandler() { + const workspaceService = this.serviceContainer.get(IWorkspaceService); + const disposables = this.serviceContainer.get(IDisposableRegistry); + disposables.push(workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + } + protected async onDidChangeConfiguration(event: ConfigurationChangeEvent) { + const workspaceService = this.serviceContainer.get(IWorkspaceService); + const workspacesUris: (Uri | undefined)[] = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders!.map(workspace => workspace.uri) : [undefined]; + if (workspacesUris.findIndex(uri => event.affectsConfiguration('python.pythonPath', uri)) === -1) { + return; + } + // Lets wait, for more changes, dirty simple throttling. + if (this.timeOut) { + clearTimeout(this.timeOut); + this.timeOut = undefined; + } + this.timeOut = setTimeout(() => { + this.timeOut = undefined; + this.diagnose().then(dianostics => this.handle(dianostics)).ignoreErrors(); + }, this.changeThrottleTimeout); + } + private getCommandPrompts(diagnostic: IDiagnostic): { prompt: string; command?: IDiagnosticCommand }[] { + const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); + switch (diagnostic.code) { + case DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic: { + return [{ + prompt: 'Select Python Interpreter', + command: commandFactory.createCommand(diagnostic, { type: 'executeVSCCommand', options: 'python.setInterpreter' }) + }]; + } + case DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic: { + return [{ + prompt: 'Learn more', + command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites' }) + }, + { + prompt: 'Download', + command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://www.python.org/downloads' }) + }]; + } + default: { + throw new Error('Invalid diagnostic for \'InvalidMacPythonInterpreterService\''); + } + } + } +} diff --git a/src/client/application/diagnostics/checks/pythonInterpreter.ts b/src/client/application/diagnostics/checks/pythonInterpreter.ts index dcff919abf7e..a90c853bc78c 100644 --- a/src/client/application/diagnostics/checks/pythonInterpreter.ts +++ b/src/client/application/diagnostics/checks/pythonInterpreter.ts @@ -4,12 +4,10 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { ConfigurationChangeEvent, DiagnosticSeverity, Uri } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; +import { DiagnosticSeverity } from 'vscode'; import '../../../common/extensions'; -import { IPlatformService } from '../../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry } from '../../../common/types'; -import { IInterpreterHelper, IInterpreterService, InterpreterType } from '../../../interpreter/contracts'; +import { IConfigurationService } from '../../../common/types'; +import { IInterpreterService } from '../../../interpreter/contracts'; import { IServiceContainer } from '../../../ioc/types'; import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; import { IDiagnosticsCommandFactory } from '../commands/types'; @@ -19,8 +17,6 @@ import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerSer const messages = { [DiagnosticCodes.NoPythonInterpretersDiagnostic]: 'Python is not installed. Please download and install Python before using the extension.', - [DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic]: 'You have selected the macOS system install of Python, which is not recommended for use with the Python extension. Some functionality will be limited, please select a different interpreter.', - [DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic]: 'The macOS system install of Python is not recommended, some functionality in the extension will be limited. Install another version of Python for the best experience.', [DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic]: 'No Python interpreter is selected. You need to select a Python interpreter to enable features such as IntelliSense, linting, and debugging.' }; @@ -34,17 +30,12 @@ export const InvalidPythonInterpreterServiceId = 'InvalidPythonInterpreterServic @injectable() export class InvalidPythonInterpreterService extends BaseDiagnosticsService { - protected changeThrottleTimeout = 1000; - private timeOut?: NodeJS.Timer; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super( [ DiagnosticCodes.NoPythonInterpretersDiagnostic, - DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, - DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic, DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic ], serviceContainer); - this.addPythonPathChangedHandler(); } public async diagnose(): Promise { const configurationService = this.serviceContainer.get(IConfigurationService); @@ -65,24 +56,7 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService { return [new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic)]; } - const platform = this.serviceContainer.get(IPlatformService); - if (!platform.isMac) { - return []; - } - - const helper = this.serviceContainer.get(IInterpreterHelper); - if (!helper.isMacDefaultPythonPath(settings.pythonPath)) { - return []; - } - if (!currentInterpreter || currentInterpreter.type !== InterpreterType.Unknown) { - return []; - } - const interpreters = await interpreterService.getInterpreters(); - if (interpreters.filter(i => !helper.isMacDefaultPythonPath(i.path)).length === 0) { - return [new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic)]; - } - - return [new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic)]; + return []; } public async handle(diagnostics: IDiagnostic[]): Promise { if (diagnostics.length === 0) { @@ -97,27 +71,6 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService { return messageService.handle(diagnostic, { commandPrompts, message: diagnostic.message }); })); } - protected addPythonPathChangedHandler() { - const workspaceService = this.serviceContainer.get(IWorkspaceService); - const disposables = this.serviceContainer.get(IDisposableRegistry); - disposables.push(workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); - } - protected async onDidChangeConfiguration(event: ConfigurationChangeEvent) { - const workspaceService = this.serviceContainer.get(IWorkspaceService); - const workspacesUris: (Uri | undefined)[] = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders!.map(workspace => workspace.uri) : [undefined]; - if (workspacesUris.findIndex(uri => event.affectsConfiguration('python.pythonPath', uri)) === -1) { - return; - } - // Lets wait, for more changes, dirty simple throttling. - if (this.timeOut) { - clearTimeout(this.timeOut); - this.timeOut = undefined; - } - this.timeOut = setTimeout(() => { - this.timeOut = undefined; - this.diagnose().then(dianostics => this.handle(dianostics)).ignoreErrors(); - }, this.changeThrottleTimeout); - } private getCommandPrompts(diagnostic: IDiagnostic): { prompt: string; command?: IDiagnosticCommand }[] { const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); switch (diagnostic.code) { @@ -127,23 +80,12 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService { command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://www.python.org/downloads' }) }]; } - case DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic: case DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic: { return [{ prompt: 'Select Python Interpreter', command: commandFactory.createCommand(diagnostic, { type: 'executeVSCCommand', options: 'python.setInterpreter' }) }]; } - case DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic: { - return [{ - prompt: 'Learn more', - command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites' }) - }, - { - prompt: 'Download', - command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://www.python.org/downloads' }) - }]; - } default: { throw new Error('Invalid diagnostic for \'InvalidPythonInterpreterService\''); } diff --git a/src/client/application/diagnostics/serviceRegistry.ts b/src/client/application/diagnostics/serviceRegistry.ts index 9cca48130258..b936b1cb81d5 100644 --- a/src/client/application/diagnostics/serviceRegistry.ts +++ b/src/client/application/diagnostics/serviceRegistry.ts @@ -10,6 +10,7 @@ import { EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagn import { InvalidDebuggerTypeDiagnosticsService, InvalidDebuggerTypeDiagnosticsServiceId } from './checks/invalidDebuggerType'; import { InvalidPythonPathInDebuggerService, InvalidPythonPathInDebuggerServiceId } from './checks/invalidPythonPathInDebugger'; import { LSNotSupportedDiagnosticService, LSNotSupportedDiagnosticServiceId } from './checks/lsNotSupported'; +import { InvalidMacPythonInterpreterService, InvalidMacPythonInterpreterServiceId } from './checks/macPythonInterpreter'; import { PowerShellActivationHackDiagnosticsService, PowerShellActivationHackDiagnosticsServiceId } from './checks/powerShellActivation'; import { InvalidPythonInterpreterService, InvalidPythonInterpreterServiceId } from './checks/pythonInterpreter'; import { DiagnosticsCommandFactory } from './commands/factory'; @@ -27,6 +28,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IDiagnosticsService, InvalidPythonPathInDebuggerService, InvalidPythonPathInDebuggerServiceId); serviceManager.addSingleton(IDiagnosticsService, LSNotSupportedDiagnosticService, LSNotSupportedDiagnosticServiceId); serviceManager.addSingleton(IDiagnosticsService, PowerShellActivationHackDiagnosticsService, PowerShellActivationHackDiagnosticsServiceId); + serviceManager.addSingleton(IDiagnosticsService, InvalidMacPythonInterpreterService, InvalidMacPythonInterpreterServiceId); serviceManager.addSingleton(IDiagnosticsCommandFactory, DiagnosticsCommandFactory); serviceManager.addSingleton(IApplicationDiagnostics, ApplicationDiagnostics); } diff --git a/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts b/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts index 415a34a7caf8..c4c3a36f694a 100644 --- a/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts +++ b/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts @@ -8,6 +8,7 @@ import * as typemoq from 'typemoq'; import { DiagnosticSeverity } from 'vscode'; import { ApplicationDiagnostics } from '../../../client/application/diagnostics/applicationDiagnostics'; +import { InvalidMacPythonInterpreterServiceId } from '../../../client/application/diagnostics/checks/macPythonInterpreter'; import { DiagnosticScope, IDiagnostic, IDiagnosticsService, ISourceMapSupportService } from '../../../client/application/diagnostics/types'; import { IApplicationDiagnostics } from '../../../client/application/types'; import { STANDARD_OUTPUT_CHANNEL } from '../../../client/common/constants'; @@ -19,6 +20,7 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { let serviceContainer: typemoq.IMock; let envHealthCheck: typemoq.IMock; let debuggerTypeCheck: typemoq.IMock; + let macInterperterCheck: typemoq.IMock; let outputChannel: typemoq.IMock; let logger: typemoq.IMock; let appDiagnostics: IApplicationDiagnostics; @@ -27,6 +29,7 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { serviceContainer = typemoq.Mock.ofType(); envHealthCheck = typemoq.Mock.ofType(); debuggerTypeCheck = typemoq.Mock.ofType(); + macInterperterCheck = typemoq.Mock.ofType(); outputChannel = typemoq.Mock.ofType(); logger = typemoq.Mock.ofType(); @@ -36,6 +39,9 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { .returns(() => outputChannel.object); serviceContainer.setup(d => d.get(typemoq.It.isValue(ILogger))) .returns(() => logger.object); + serviceContainer.setup(d => d.get(typemoq.It.isValue(IDiagnosticsService), + typemoq.It.isValue(InvalidMacPythonInterpreterServiceId))) + .returns(() => macInterperterCheck.object); appDiagnostics = new ApplicationDiagnostics(serviceContainer.object, outputChannel.object); }); @@ -52,21 +58,32 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { sourceMapService.verifyAll(); }); - test('Performing Pre Startup Health Check must check Path environment variable and Debugger Type', async () => { + test('Performing Pre Startup Health Check must check Path environment variable and Debugger Type along with Mac Interpreter', async () => { envHealthCheck.setup(e => e.diagnose()) .returns(() => Promise.resolve([])) .verifiable(typemoq.Times.once()); debuggerTypeCheck.setup(e => e.diagnose()) .returns(() => Promise.resolve([])) .verifiable(typemoq.Times.once()); + macInterperterCheck.setup(p => p.diagnose()) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); + macInterperterCheck.setup(p => p.handle(typemoq.It.isValue([]))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); await appDiagnostics.performPreStartupHealthCheck(); envHealthCheck.verifyAll(); debuggerTypeCheck.verifyAll(); + macInterperterCheck.verifyAll(); }); test('Diagnostics Returned by Per Startup Health Checks must be logged', async () => { + macInterperterCheck.setup(p => p.diagnose()) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); + const diagnostics: IDiagnostic[] = []; for (let i = 0; i <= (Math.random() * 10); i += 1) { const diagnostic: IDiagnostic = { diff --git a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts new file mode 100644 index 000000000000..9f20ebeee276 --- /dev/null +++ b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts @@ -0,0 +1,418 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any max-classes-per-file + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { ConfigurationChangeEvent } from 'vscode'; +import { InvalidMacPythonInterpreterDiagnostic, InvalidMacPythonInterpreterService } from '../../../../client/application/diagnostics/checks/macPythonInterpreter'; +import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; +import { IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService, IDiagnosticsService } from '../../../../client/application/diagnostics/types'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { IConfigurationService, IDisposableRegistry, IPythonSettings } from '../../../../client/common/types'; +import { sleep } from '../../../../client/common/utils/async'; +import { noop } from '../../../../client/common/utils/misc'; +import { IInterpreterHelper, IInterpreterService, InterpreterType } from '../../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Checks Python Interpreter', () => { + let diagnosticService: IDiagnosticsService; + let messageHandler: typemoq.IMock>; + let commandFactory: typemoq.IMock; + let settings: typemoq.IMock; + let interpreterService: typemoq.IMock; + let platformService: typemoq.IMock; + let helper: typemoq.IMock; + const pythonPath = 'My Python Path in Settings'; + let serviceContainer: typemoq.IMock; + function createContainer() { + serviceContainer = typemoq.Mock.ofType(); + messageHandler = typemoq.Mock.ofType>(); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IDiagnosticHandlerService), typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId))) + .returns(() => messageHandler.object); + commandFactory = typemoq.Mock.ofType(); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + .returns(() => commandFactory.object); + settings = typemoq.Mock.ofType(); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + const configService = typemoq.Mock.ofType(); + configService.setup(c => c.getSettings(typemoq.It.isAny())).returns(() => settings.object); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + interpreterService = typemoq.Mock.ofType(); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + platformService = typemoq.Mock.ofType(); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + helper = typemoq.Mock.ofType(); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IInterpreterHelper))) + .returns(() => helper.object); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IDisposableRegistry))) + .returns(() => []); + + platformService + .setup(p => p.isMac) + .returns(() => true) + .verifiable(typemoq.Times.once()); + return serviceContainer.object; + } + suite('Diagnostics', () => { + setup(() => { + diagnosticService = new class extends InvalidMacPythonInterpreterService { + protected addPythonPathChangedHandler() { noop(); } + }(createContainer(), interpreterService.object, platformService.object, helper.object); + }); + + test('Can handle InvalidPythonPathInterpreter diagnostics', async () => { + for (const code of [ + DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, + DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic + ]) { + const diagnostic = typemoq.Mock.ofType(); + diagnostic.setup(d => d.code) + .returns(() => code) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(true, `Should be able to handle ${code}`); + diagnostic.verifyAll(); + } + }); + test('Can not handle non-InvalidPythonPathInterpreter diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType(); + diagnostic.setup(d => d.code) + .returns(() => 'Something Else') + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); + test('Should return empty diagnostics if not a Macc', async () => { + platformService.reset(); + platformService + .setup(p => p.isMac) + .returns(() => true) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(); + expect(diagnostics).to.be.deep.equal([]); + platformService.verifyAll(); + }); + test('Should return empty diagnostics if installer check is disabled', async () => { + settings + .setup(s => s.disableInstallationChecks) + .returns(() => true) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(); + expect(diagnostics).to.be.deep.equal([]); + settings.verifyAll(); + platformService.verifyAll(); + }); + test('Should return empty diagnostics if there are interpreters, one is selected, and platform is not mac', async () => { + settings + .setup(s => s.disableInstallationChecks) + .returns(() => false) + .verifiable(typemoq.Times.once()); + interpreterService + .setup(i => i.hasInterpreters) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + interpreterService + .setup(i => i.getInterpreters(typemoq.It.isAny())) + .returns(() => Promise.resolve([{} as any])) + .verifiable(typemoq.Times.never()); + interpreterService + .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) + .verifiable(typemoq.Times.once()); + platformService + .setup(i => i.isMac) + .returns(() => false) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(); + expect(diagnostics).to.be.deep.equal([]); + settings.verifyAll(); + interpreterService.verifyAll(); + platformService.verifyAll(); + }); + test('Should return empty diagnostics if there are interpreters, platform is mac and selected interpreter is not default mac interpreter', async () => { + settings + .setup(s => s.disableInstallationChecks) + .returns(() => false) + .verifiable(typemoq.Times.once()); + interpreterService + .setup(i => i.hasInterpreters) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + interpreterService + .setup(i => i.getInterpreters(typemoq.It.isAny())) + .returns(() => Promise.resolve([{} as any])) + .verifiable(typemoq.Times.never()); + interpreterService + .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) + .verifiable(typemoq.Times.once()); + platformService + .setup(i => i.isMac) + .returns(() => true) + .verifiable(typemoq.Times.once()); + helper + .setup(i => i.isMacDefaultPythonPath(typemoq.It.isAny())) + .returns(() => false) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(); + expect(diagnostics).to.be.deep.equal([]); + settings.verifyAll(); + interpreterService.verifyAll(); + platformService.verifyAll(); + helper.verifyAll(); + }); + test('Should return diagnostic if there are no other interpreters, platform is mac and selected interpreter is default mac interpreter', async () => { + settings + .setup(s => s.disableInstallationChecks) + .returns(() => false) + .verifiable(typemoq.Times.once()); + interpreterService + .setup(i => i.getInterpreters(typemoq.It.isAny())) + .returns(() => Promise.resolve([ + { path: pythonPath } as any, + { path: pythonPath } as any + ])) + .verifiable(typemoq.Times.once()); + interpreterService + .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) + .verifiable(typemoq.Times.once()); + platformService + .setup(i => i.isMac) + .returns(() => true) + .verifiable(typemoq.Times.once()); + helper + .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) + .returns(() => true) + .verifiable(typemoq.Times.atLeastOnce()); + + const diagnostics = await diagnosticService.diagnose(); + expect(diagnostics).to.be.deep.equal([new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic)]); + settings.verifyAll(); + interpreterService.verifyAll(); + platformService.verifyAll(); + helper.verifyAll(); + }); + test('Should return diagnostic if there are other interpreters, platform is mac and selected interpreter is default mac interpreter', async () => { + const nonMacStandardInterpreter = 'Non Mac Std Interpreter'; + settings + .setup(s => s.disableInstallationChecks) + .returns(() => false) + .verifiable(typemoq.Times.once()); + interpreterService + .setup(i => i.getInterpreters(typemoq.It.isAny())) + .returns(() => Promise.resolve([ + { path: pythonPath } as any, + { path: pythonPath } as any, + { path: nonMacStandardInterpreter } as any + ])) + .verifiable(typemoq.Times.once()); + platformService + .setup(i => i.isMac) + .returns(() => true) + .verifiable(typemoq.Times.once()); + helper + .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) + .returns(() => true) + .verifiable(typemoq.Times.atLeastOnce()); + helper + .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(nonMacStandardInterpreter))) + .returns(() => false) + .verifiable(typemoq.Times.atLeastOnce()); + interpreterService + .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(); + expect(diagnostics).to.be.deep.equal([new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic)]); + settings.verifyAll(); + interpreterService.verifyAll(); + platformService.verifyAll(); + helper.verifyAll(); + }); + test('Handling no interpreters diagnostic should return select interpreter cmd', async () => { + const diagnostic = new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic); + const cmd = {} as any as IDiagnosticCommand; + let messagePrompt: MessageCommandPrompt | undefined; + messageHandler + .setup(i => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((d, p: MessageCommandPrompt) => messagePrompt = p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), + typemoq.It.isObjectWith>({ type: 'executeVSCCommand' }))) + .returns(() => cmd) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); + expect(messagePrompt!.commandPrompts).to.be.deep.equal([{ prompt: 'Select Python Interpreter', command: cmd }]); + }); + test('Handling no interpreters diagnostisc should return download and learn links', async () => { + const diagnostic = new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic); + const cmdDownload = {} as any as IDiagnosticCommand; + const cmdLearn = {} as any as IDiagnosticCommand; + let messagePrompt: MessageCommandPrompt | undefined; + messageHandler + .setup(i => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((d, p: MessageCommandPrompt) => messagePrompt = p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), + typemoq.It.isObjectWith>({ type: 'launch', options: 'https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites' }))) + .returns(() => cmdLearn) + .verifiable(typemoq.Times.once()); + commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), + typemoq.It.isObjectWith>({ type: 'launch', options: 'https://www.python.org/downloads' }))) + .returns(() => cmdDownload) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); + expect(messagePrompt!.commandPrompts).to.be.deep.equal([{ prompt: 'Learn more', command: cmdLearn }, { prompt: 'Download', command: cmdDownload }]); + }); + }); + + suite('Change Handlers.', () => { + test('Add PythonPath handler is invoked', async () => { + let invoked = false; + diagnosticService = new class extends InvalidMacPythonInterpreterService { + protected addPythonPathChangedHandler() { invoked = true; } + }(createContainer(), interpreterService.object, platformService.object, helper.object); + + expect(invoked).to.be.equal(true, 'Not invoked'); + }); + test('Event Handler is registered and invoked', async () => { + let invoked = false; + let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; + const workspaceService = { onDidChangeConfiguration: cb => callbackHandler = cb } as any; + const serviceContainerObject = createContainer(); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService); + diagnosticService = new class extends InvalidMacPythonInterpreterService { + protected async onDidChangeConfiguration(_event: ConfigurationChangeEvent) { invoked = true; } + }(serviceContainerObject, undefined as any, undefined as any, undefined as any); + + await callbackHandler({} as any); + expect(invoked).to.be.equal(true, 'Not invoked'); + }); + test('Event Handler is registered and not invoked', async () => { + let invoked = false; + const workspaceService = { onDidChangeConfiguration: noop } as any; + const serviceContainerObject = createContainer(); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService); + diagnosticService = new class extends InvalidMacPythonInterpreterService { + protected async onDidChangeConfiguration(_event: ConfigurationChangeEvent) { invoked = true; } + }(serviceContainerObject, undefined as any, undefined as any, undefined as any); + + expect(invoked).to.be.equal(false, 'Not invoked'); + }); + test('Diagnostics are checked when path changes', async () => { + const event = typemoq.Mock.ofType(); + const workspaceService = typemoq.Mock.ofType(); + const serviceContainerObject = createContainer(); + let diagnoseInvocationCount = 0; + workspaceService + .setup(w => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(typemoq.Times.once()); + workspaceService + .setup(w => w.workspaceFolders) + .returns(() => [{ uri: '' }] as any) + .verifiable(typemoq.Times.once()); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + const diagnosticSvc = new class extends InvalidMacPythonInterpreterService { + constructor(arg1, arg2, arg3, arg4) { + super(arg1, arg2, arg3, arg4); + this.changeThrottleTimeout = 1; + } + public onDidChangeConfigurationEx = e => super.onDidChangeConfiguration(e); + public diagnose(): Promise { + diagnoseInvocationCount += 1; + return Promise.resolve(); + } + }(serviceContainerObject, undefined, undefined, undefined); + + event + .setup(e => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) + .returns(() => true) + .verifiable(typemoq.Times.atLeastOnce()); + + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + event.verifyAll(); + await sleep(100); + expect(diagnoseInvocationCount).to.be.equal(1, 'Not invoked'); + + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + await sleep(100); + expect(diagnoseInvocationCount).to.be.equal(2, 'Not invoked'); + }); + test('Diagnostics are checked and throttled when path changes', async () => { + const event = typemoq.Mock.ofType(); + const workspaceService = typemoq.Mock.ofType(); + const serviceContainerObject = createContainer(); + let diagnoseInvocationCount = 0; + workspaceService + .setup(w => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(typemoq.Times.once()); + workspaceService + .setup(w => w.workspaceFolders) + .returns(() => [{ uri: '' }] as any) + .verifiable(typemoq.Times.once()); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + const diagnosticSvc = new class extends InvalidMacPythonInterpreterService { + constructor(arg1, arg2, arg3, arg4) { + super(arg1, arg2, arg3, arg4); + this.changeThrottleTimeout = 100; + } + public onDidChangeConfigurationEx = e => super.onDidChangeConfiguration(e); + public diagnose(): Promise { + diagnoseInvocationCount += 1; + return Promise.resolve(); + } + }(serviceContainerObject, undefined, undefined, undefined); + + event + .setup(e => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) + .returns(() => true) + .verifiable(typemoq.Times.atLeastOnce()); + + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + await sleep(500); + event.verifyAll(); + expect(diagnoseInvocationCount).to.be.equal(1, 'Not invoked'); + }); + }); +}); diff --git a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts index d45e66cbb98e..590a5f6a98a1 100644 --- a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts @@ -7,21 +7,18 @@ import { expect } from 'chai'; import * as typemoq from 'typemoq'; -import { ConfigurationChangeEvent } from 'vscode'; import { InvalidPythonInterpreterDiagnostic, InvalidPythonInterpreterService } from '../../../../client/application/diagnostics/checks/pythonInterpreter'; import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; import { IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService, IDiagnosticsService } from '../../../../client/application/diagnostics/types'; -import { IWorkspaceService } from '../../../../client/common/application/types'; import { IPlatformService } from '../../../../client/common/platform/types'; import { IConfigurationService, IDisposableRegistry, IPythonSettings } from '../../../../client/common/types'; import { noop } from '../../../../client/common/utils/misc'; -import { IInterpreterHelper, IInterpreterService, InterpreterType } from '../../../../client/interpreter/contracts'; +import { IInterpreterHelper, IInterpreterService } from '../../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../../client/ioc/types'; -import { sleep } from '../../../core'; -suite('Application Diagnostics - Checks Python Interpreter', () => { +suite('xApplication Diagnostics - Checks Python Interpreter', () => { let diagnosticService: IDiagnosticsService; let messageHandler: typemoq.IMock>; let commandFactory: typemoq.IMock; @@ -68,8 +65,6 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { test('Can handle InvalidPythonPathInterpreter diagnostics', async () => { for (const code of [ DiagnosticCodes.NoPythonInterpretersDiagnostic, - DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, - DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic, DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic ]) { const diagnostic = typemoq.Mock.ofType(); @@ -117,137 +112,6 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { settings.verifyAll(); interpreterService.verifyAll(); }); - test('Should return empty diagnostics if there are interpreters, one is selected, and platform is not mac', async () => { - settings - .setup(s => s.disableInstallationChecks) - .returns(() => false) - .verifiable(typemoq.Times.once()); - interpreterService - .setup(i => i.hasInterpreters) - .returns(() => Promise.resolve(true)) - .verifiable(typemoq.Times.once()); - interpreterService - .setup(i => i.getInterpreters(typemoq.It.isAny())) - .returns(() => Promise.resolve([{} as any])) - .verifiable(typemoq.Times.never()); - interpreterService - .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) - .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) - .verifiable(typemoq.Times.once()); - platformService - .setup(i => i.isMac) - .returns(() => false) - .verifiable(typemoq.Times.once()); - - const diagnostics = await diagnosticService.diagnose(); - expect(diagnostics).to.be.deep.equal([]); - settings.verifyAll(); - interpreterService.verifyAll(); - platformService.verifyAll(); - }); - test('Should return empty diagnostics if there are interpreters, platform is mac and selected interpreter is not default mac interpreter', async () => { - settings - .setup(s => s.disableInstallationChecks) - .returns(() => false) - .verifiable(typemoq.Times.once()); - interpreterService - .setup(i => i.hasInterpreters) - .returns(() => Promise.resolve(true)) - .verifiable(typemoq.Times.once()); - interpreterService - .setup(i => i.getInterpreters(typemoq.It.isAny())) - .returns(() => Promise.resolve([{} as any])) - .verifiable(typemoq.Times.never()); - interpreterService - .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) - .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) - .verifiable(typemoq.Times.once()); - platformService - .setup(i => i.isMac) - .returns(() => true) - .verifiable(typemoq.Times.once()); - helper - .setup(i => i.isMacDefaultPythonPath(typemoq.It.isAny())) - .returns(() => false) - .verifiable(typemoq.Times.once()); - - const diagnostics = await diagnosticService.diagnose(); - expect(diagnostics).to.be.deep.equal([]); - settings.verifyAll(); - interpreterService.verifyAll(); - platformService.verifyAll(); - helper.verifyAll(); - }); - test('Should return diagnostic if there are no other interpreters, platform is mac and selected interpreter is default mac interpreter', async () => { - settings - .setup(s => s.disableInstallationChecks) - .returns(() => false) - .verifiable(typemoq.Times.once()); - interpreterService - .setup(i => i.getInterpreters(typemoq.It.isAny())) - .returns(() => Promise.resolve([ - { path: pythonPath } as any, - { path: pythonPath } as any - ])) - .verifiable(typemoq.Times.once()); - interpreterService - .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) - .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) - .verifiable(typemoq.Times.once()); - platformService - .setup(i => i.isMac) - .returns(() => true) - .verifiable(typemoq.Times.once()); - helper - .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) - .returns(() => true) - .verifiable(typemoq.Times.atLeastOnce()); - - const diagnostics = await diagnosticService.diagnose(); - expect(diagnostics).to.be.deep.equal([new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic)]); - settings.verifyAll(); - interpreterService.verifyAll(); - platformService.verifyAll(); - helper.verifyAll(); - }); - test('Should return diagnostic if there are other interpreters, platform is mac and selected interpreter is default mac interpreter', async () => { - const nonMacStandardInterpreter = 'Non Mac Std Interpreter'; - settings - .setup(s => s.disableInstallationChecks) - .returns(() => false) - .verifiable(typemoq.Times.once()); - interpreterService - .setup(i => i.getInterpreters(typemoq.It.isAny())) - .returns(() => Promise.resolve([ - { path: pythonPath } as any, - { path: pythonPath } as any, - { path: nonMacStandardInterpreter } as any - ])) - .verifiable(typemoq.Times.once()); - platformService - .setup(i => i.isMac) - .returns(() => true) - .verifiable(typemoq.Times.once()); - helper - .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) - .returns(() => true) - .verifiable(typemoq.Times.atLeastOnce()); - helper - .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(nonMacStandardInterpreter))) - .returns(() => false) - .verifiable(typemoq.Times.atLeastOnce()); - interpreterService - .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) - .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) - .verifiable(typemoq.Times.once()); - - const diagnostics = await diagnosticService.diagnose(); - expect(diagnostics).to.be.deep.equal([new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic)]); - settings.verifyAll(); - interpreterService.verifyAll(); - platformService.verifyAll(); - helper.verifyAll(); - }); test('Handling no interpreters diagnostic should return download link', async () => { const diagnostic = new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.NoPythonInterpretersDiagnostic); const cmd = {} as any as IDiagnosticCommand; @@ -293,7 +157,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { expect(messagePrompt!.commandPrompts).to.be.deep.equal([{ prompt: 'Select Python Interpreter', command: cmd }]); }); test('Handling no interpreters diagnostic should return select interpreter cmd', async () => { - const diagnostic = new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic); + const diagnostic = new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic); const cmd = {} as any as IDiagnosticCommand; let messagePrompt: MessageCommandPrompt | undefined; messageHandler @@ -313,150 +177,5 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); expect(messagePrompt!.commandPrompts).to.be.deep.equal([{ prompt: 'Select Python Interpreter', command: cmd }]); }); - test('Handling no interpreters diagnostisc should return download and learn links', async () => { - const diagnostic = new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic); - const cmdDownload = {} as any as IDiagnosticCommand; - const cmdLearn = {} as any as IDiagnosticCommand; - let messagePrompt: MessageCommandPrompt | undefined; - messageHandler - .setup(i => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) - .callback((d, p: MessageCommandPrompt) => messagePrompt = p) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), - typemoq.It.isObjectWith>({ type: 'launch', options: 'https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites' }))) - .returns(() => cmdLearn) - .verifiable(typemoq.Times.once()); - commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), - typemoq.It.isObjectWith>({ type: 'launch', options: 'https://www.python.org/downloads' }))) - .returns(() => cmdDownload) - .verifiable(typemoq.Times.once()); - - await diagnosticService.handle([diagnostic]); - - messageHandler.verifyAll(); - commandFactory.verifyAll(); - expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); - expect(messagePrompt!.commandPrompts).to.be.deep.equal([{ prompt: 'Learn more', command: cmdLearn }, { prompt: 'Download', command: cmdDownload }]); - }); - }); - - suite('Change Handlers.', () => { - test('Add PythonPath handler is invoked', async () => { - let invoked = false; - diagnosticService = new class extends InvalidPythonInterpreterService { - protected addPythonPathChangedHandler() { invoked = true; } - }(createContainer()); - - expect(invoked).to.be.equal(true, 'Not invoked'); - }); - test('Event Handler is registered and invoked', async () => { - let invoked = false; - let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; - const workspaceService = { onDidChangeConfiguration: cb => callbackHandler = cb } as any; - const serviceContainerObject = createContainer(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService); - diagnosticService = new class extends InvalidPythonInterpreterService { - protected async onDidChangeConfiguration(_event: ConfigurationChangeEvent) { invoked = true; } - }(serviceContainerObject); - - await callbackHandler({} as any); - expect(invoked).to.be.equal(true, 'Not invoked'); - }); - test('Event Handler is registered and not invoked', async () => { - let invoked = false; - const workspaceService = { onDidChangeConfiguration: noop } as any; - const serviceContainerObject = createContainer(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService); - diagnosticService = new class extends InvalidPythonInterpreterService { - protected async onDidChangeConfiguration(_event: ConfigurationChangeEvent) { invoked = true; } - }(serviceContainerObject); - - expect(invoked).to.be.equal(false, 'Not invoked'); - }); - test('Diagnostics are checked when path changes', async () => { - const event = typemoq.Mock.ofType(); - const workspaceService = typemoq.Mock.ofType(); - const serviceContainerObject = createContainer(); - let diagnoseInvocationCount = 0; - workspaceService - .setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(typemoq.Times.once()); - workspaceService - .setup(w => w.workspaceFolders) - .returns(() => [{ uri: '' }] as any) - .verifiable(typemoq.Times.once()); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - const diagnosticSvc = new class extends InvalidPythonInterpreterService { - constructor(item) { - super(item); - this.changeThrottleTimeout = 1; - } - public onDidChangeConfigurationEx = e => super.onDidChangeConfiguration(e); - public diagnose(): Promise { - diagnoseInvocationCount += 1; - return Promise.resolve(); - } - }(serviceContainerObject); - - event - .setup(e => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) - .returns(() => true) - .verifiable(typemoq.Times.atLeastOnce()); - - await diagnosticSvc.onDidChangeConfigurationEx(event.object); - event.verifyAll(); - await sleep(100); - expect(diagnoseInvocationCount).to.be.equal(1, 'Not invoked'); - - await diagnosticSvc.onDidChangeConfigurationEx(event.object); - await sleep(100); - expect(diagnoseInvocationCount).to.be.equal(2, 'Not invoked'); - }); - test('Diagnostics are checked and throttled when path changes', async () => { - const event = typemoq.Mock.ofType(); - const workspaceService = typemoq.Mock.ofType(); - const serviceContainerObject = createContainer(); - let diagnoseInvocationCount = 0; - workspaceService - .setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(typemoq.Times.once()); - workspaceService - .setup(w => w.workspaceFolders) - .returns(() => [{ uri: '' }] as any) - .verifiable(typemoq.Times.once()); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - const diagnosticSvc = new class extends InvalidPythonInterpreterService { - constructor(item) { - super(item); - this.changeThrottleTimeout = 100; - } - public onDidChangeConfigurationEx = e => super.onDidChangeConfiguration(e); - public diagnose(): Promise { - diagnoseInvocationCount += 1; - return Promise.resolve(); - } - }(serviceContainerObject); - - event - .setup(e => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) - .returns(() => true) - .verifiable(typemoq.Times.atLeastOnce()); - - await diagnosticSvc.onDidChangeConfigurationEx(event.object); - await diagnosticSvc.onDidChangeConfigurationEx(event.object); - await diagnosticSvc.onDidChangeConfigurationEx(event.object); - await diagnosticSvc.onDidChangeConfigurationEx(event.object); - await diagnosticSvc.onDidChangeConfigurationEx(event.object); - await sleep(500); - event.verifyAll(); - expect(diagnoseInvocationCount).to.be.equal(1, 'Not invoked'); - }); }); });