From 763e8cf55baf6c4fafa17fae99d7753303ce9e37 Mon Sep 17 00:00:00 2001 From: Panpan Lin Date: Thu, 20 Feb 2020 21:17:34 +0000 Subject: [PATCH 01/10] fixing a typo in CONTRIBUTING.md (#10044) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f061841c70e3..2068c7ce6414 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -188,7 +188,7 @@ From there use the ```Extension + Debugger``` launch option. Information on our coding standards can be found [here](https://github.com/Microsoft/vscode-python/blob/master/CODING_STANDARDS.md). We have CI tests to ensure the code committed will adhere to the above coding standards. *You can run this locally by executing the command `npx gulp precommit` or use the `precommit` Task. -Messages displayed to the user must ve localized using/created constants from/in the [localize.ts](https://github.com/Microsoft/vscode-python/blob/master/src/client/common/utils/localize.ts) file. +Messages displayed to the user must be localized using/created constants from/in the [localize.ts](https://github.com/Microsoft/vscode-python/blob/master/src/client/common/utils/localize.ts) file. ## Development process From 0d917aa5c93d715547471d99b81af1d51e6e3ff7 Mon Sep 17 00:00:00 2001 From: Ian Huff Date: Thu, 20 Feb 2020 14:00:07 -0800 Subject: [PATCH 02/10] fix for button backgrounds (#10234) --- news/2 Fixes/10154.md | 1 + src/datascience-ui/native-editor/nativeEditor.less | 2 +- src/datascience-ui/react-common/imageButton.tsx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 news/2 Fixes/10154.md diff --git a/news/2 Fixes/10154.md b/news/2 Fixes/10154.md new file mode 100644 index 000000000000..b5ab48cc2c8b --- /dev/null +++ b/news/2 Fixes/10154.md @@ -0,0 +1 @@ +Correct image backgrounds for notebook editor. \ No newline at end of file diff --git a/src/datascience-ui/native-editor/nativeEditor.less b/src/datascience-ui/native-editor/nativeEditor.less index 4e2fdcf656f9..51ccba4b05ec 100644 --- a/src/datascience-ui/native-editor/nativeEditor.less +++ b/src/datascience-ui/native-editor/nativeEditor.less @@ -293,7 +293,7 @@ .native-button { margin-top: 3px; - background: var(--override-background, var(--vscode-editor-background)); + background: transparent; z-index: 10; } diff --git a/src/datascience-ui/react-common/imageButton.tsx b/src/datascience-ui/react-common/imageButton.tsx index 8e393ef1609e..6105b49ec693 100644 --- a/src/datascience-ui/react-common/imageButton.tsx +++ b/src/datascience-ui/react-common/imageButton.tsx @@ -22,7 +22,7 @@ export class ImageButton extends React.Component { public render() { const classNames = `image-button image-button-${this.props.baseTheme} ${this.props.hidden ? 'hide' : ''} ${ - this.props.className + this.props.className ? this.props.className : '' }`; const innerFilter = this.props.disabled ? 'image-button-inner-disabled-filter' : ''; const ariaDisabled = this.props.disabled ? 'true' : 'false'; From 430c0892ed0a6035deec9918e97bfb3491833bdf Mon Sep 17 00:00:00 2001 From: Ian Huff Date: Thu, 20 Feb 2020 16:39:05 -0800 Subject: [PATCH 03/10] add kernel connection sys info string (#10236) --- news/2 Fixes/9132.md | 1 + package.nls.json | 1 + src/client/common/utils/localize.ts | 1 + .../interactive-common/interactiveBase.ts | 12 ++++++++++-- .../interactive-common/interactiveWindowTypes.ts | 3 ++- 5 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 news/2 Fixes/9132.md diff --git a/news/2 Fixes/9132.md b/news/2 Fixes/9132.md new file mode 100644 index 000000000000..e7b32a58069f --- /dev/null +++ b/news/2 Fixes/9132.md @@ -0,0 +1 @@ +Have sys info show that we have connected to an existing server. \ No newline at end of file diff --git a/package.nls.json b/package.nls.json index a0f3388551bf..d3dde7184731 100644 --- a/package.nls.json +++ b/package.nls.json @@ -178,6 +178,7 @@ "DataScience.pythonVersionHeader": "Python version:", "DataScience.pythonRestartHeader": "Restarted kernel:", "DataScience.pythonNewHeader": "Started new kernel:", + "DataScience.pythonConnectHeader": "Connected to kernel:", "DataScience.executingCodeFailure": "Executing code failed : {0}", "DataScience.inputWatermark": "Type code here and press shift-enter to run", "DataScience.deleteButtonTooltip": "Remove cell", diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index f1f1951f9641..167f970d0116 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -409,6 +409,7 @@ export namespace DataScience { export const pythonVersionHeader = localize('DataScience.pythonVersionHeader', 'Python Version:'); export const pythonRestartHeader = localize('DataScience.pythonRestartHeader', 'Restarted Kernel:'); export const pythonNewHeader = localize('DataScience.pythonNewHeader', 'Started new kernel:'); + export const pythonConnectHeader = localize('DataScience.pythonConnectHeader', 'Connected to kernel:'); export const jupyterSelectURIPrompt = localize( 'DataScience.jupyterSelectURIPrompt', diff --git a/src/client/datascience/interactive-common/interactiveBase.ts b/src/client/datascience/interactive-common/interactiveBase.ts index 6aaf62522820..5206e923a21f 100644 --- a/src/client/datascience/interactive-common/interactiveBase.ts +++ b/src/client/datascience/interactive-common/interactiveBase.ts @@ -1299,6 +1299,9 @@ export abstract class InteractiveBase extends WebViewHost Date: Fri, 21 Feb 2020 08:55:42 -0800 Subject: [PATCH 04/10] Fix to return env variables of interpreter that is not current interpreter (#10251) For #10250 * Bug fix --- news/2 Fixes/10250.md | 1 + src/client/common/logger.ts | 6 +- src/client/interpreter/activation/service.ts | 47 ++++++-- .../activation/service.unit.test.ts | 105 ++++++++++++++---- 4 files changed, 126 insertions(+), 33 deletions(-) create mode 100644 news/2 Fixes/10250.md diff --git a/news/2 Fixes/10250.md b/news/2 Fixes/10250.md new file mode 100644 index 000000000000..e3ed4fd36c48 --- /dev/null +++ b/news/2 Fixes/10250.md @@ -0,0 +1 @@ +Ensure to correctly return env variables of the activated interpreter, when dealing with non-workspace interpreters. diff --git a/src/client/common/logger.ts b/src/client/common/logger.ts index 624d1221a6b6..556fa5d71e21 100644 --- a/src/client/common/logger.ts +++ b/src/client/common/logger.ts @@ -290,7 +290,11 @@ function trace(message: string, options: LogOptions = LogOptions.None, logLevel? // tslint:disable-next-line:no-any function writeToLog(elapsedTime: number, returnValue?: any, ex?: Error) { const messagesToLog = [message]; - messagesToLog.push(`Class name = ${className}, completed in ${elapsedTime}ms`); + messagesToLog.push( + `Class name = ${className}, completed in ${elapsedTime}ms, has a ${ + returnValue ? 'truthy' : 'falsy' + } return value` + ); if ((options && LogOptions.Arguments) === LogOptions.Arguments) { messagesToLog.push(argsToLogString(args)); } diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index 284d3ccfc75a..1cc5f3541f95 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -6,22 +6,20 @@ import '../../common/extensions'; import { inject, injectable } from 'inversify'; import * as path from 'path'; +import { IWorkspaceService } from '../../common/application/types'; import { PYTHON_WARNINGS } from '../../common/constants'; import { LogOptions, traceDecorators, traceError, traceVerbose } from '../../common/logger'; import { IPlatformService } from '../../common/platform/types'; import { IProcessServiceFactory } from '../../common/process/types'; import { ITerminalHelper, TerminalShellType } from '../../common/terminal/types'; import { ICurrentProcess, IDisposable, Resource } from '../../common/types'; -import { - cacheResourceSpecificInterpreterData, - clearCachedResourceSpecificIngterpreterData -} from '../../common/utils/decorators'; +import { InMemoryCache } from '../../common/utils/cacheUtils'; import { OSType } from '../../common/utils/platform'; import { IEnvironmentVariablesProvider } from '../../common/variables/types'; import { EXTENSION_ROOT_DIR } from '../../constants'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { PythonInterpreter } from '../contracts'; +import { IInterpreterService, PythonInterpreter } from '../contracts'; import { IEnvironmentActivationService } from './types'; const getEnvironmentPrefix = 'e8b39361-0157-4923-80e1-22d70d46dee6'; @@ -39,15 +37,24 @@ const defaultShells = { @injectable() export class EnvironmentActivationService implements IEnvironmentActivationService, IDisposable { private readonly disposables: IDisposable[] = []; + private readonly activatedEnvVariablesCache = new Map>(); constructor( @inject(ITerminalHelper) private readonly helper: ITerminalHelper, @inject(IPlatformService) private readonly platform: IPlatformService, @inject(IProcessServiceFactory) private processServiceFactory: IProcessServiceFactory, @inject(ICurrentProcess) private currentProcess: ICurrentProcess, + @inject(IWorkspaceService) private workspace: IWorkspaceService, + @inject(IInterpreterService) private interpreterService: IInterpreterService, @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider ) { this.envVarsService.onDidEnvironmentVariablesChange( - this.onDidEnvironmentVariablesChange, + () => this.activatedEnvVariablesCache.clear(), + this, + this.disposables + ); + + this.interpreterService.onDidChangeInterpreter( + () => this.activatedEnvVariablesCache.clear(), this, this.disposables ); @@ -58,11 +65,33 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } @traceDecorators.verbose('getActivatedEnvironmentVariables', LogOptions.Arguments) @captureTelemetry(EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, { failed: false }, true) - @cacheResourceSpecificInterpreterData('ActivatedEnvironmentVariables', cacheDuration) public async getActivatedEnvironmentVariables( resource: Resource, interpreter?: PythonInterpreter, allowExceptions?: boolean + ): Promise { + // Cache key = resource + interpreter. + const workspaceKey = this.workspace.getWorkspaceFolderIdentifier(resource); + const interpreterPath = this.platform.isWindows ? interpreter?.path.toLowerCase() : interpreter?.path; + const cacheKey = `${workspaceKey}_${interpreterPath}`; + + if (this.activatedEnvVariablesCache.get(cacheKey)?.hasData) { + return this.activatedEnvVariablesCache.get(cacheKey)!.data; + } + + // Cache only if successful, else keep trying & failing if necessary. + const cache = new InMemoryCache(cacheDuration, ''); + return this.getActivatedEnvironmentVariablesImpl(resource, interpreter, allowExceptions).then(vars => { + cache.data = vars; + this.activatedEnvVariablesCache.set(cacheKey, cache); + return vars; + }); + } + + public async getActivatedEnvironmentVariablesImpl( + resource: Resource, + interpreter?: PythonInterpreter, + allowExceptions?: boolean ): Promise { const shellInfo = defaultShells[this.platform.osType]; if (!shellInfo) { @@ -138,9 +167,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } } } - protected onDidEnvironmentVariablesChange(affectedResource: Resource) { - clearCachedResourceSpecificIngterpreterData('ActivatedEnvironmentVariables', affectedResource); - } + protected fixActivationCommands(commands: string[]): string[] { // Replace 'source ' with '. ' as that works in shell exec return commands.map(cmd => cmd.replace(/^source\s+/, '. ')); diff --git a/src/test/interpreters/activation/service.unit.test.ts b/src/test/interpreters/activation/service.unit.test.ts index eec2c36d85ee..cea844a7c247 100644 --- a/src/test/interpreters/activation/service.unit.test.ts +++ b/src/test/interpreters/activation/service.unit.test.ts @@ -7,8 +7,9 @@ import { EOL } from 'os'; import * as path from 'path'; import { SemVer } from 'semver'; import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { EventEmitter, Uri, workspace as workspaceType, WorkspaceConfiguration } from 'vscode'; +import { EventEmitter, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; import { PlatformService } from '../../../client/common/platform/platformService'; import { IPlatformService } from '../../../client/common/platform/types'; import { CurrentProcess } from '../../../client/common/process/currentProcess'; @@ -18,15 +19,14 @@ import { IProcessService, IProcessServiceFactory } from '../../../client/common/ import { TerminalHelper } from '../../../client/common/terminal/helper'; import { ITerminalHelper } from '../../../client/common/terminal/types'; import { ICurrentProcess } from '../../../client/common/types'; -import { clearCache } from '../../../client/common/utils/cacheUtils'; import { getNamesAndValues } from '../../../client/common/utils/enum'; import { Architecture, OSType } from '../../../client/common/utils/platform'; import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; import { EXTENSION_ROOT_DIR } from '../../../client/constants'; import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; -import { InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts'; -import { mockedVSCodeNamespaces } from '../../vscode-mock'; +import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; const getEnvironmentPrefix = 'e8b39361-0157-4923-80e1-22d70d46dee6'; const defaultShells = { @@ -45,8 +45,10 @@ suite('Interpreters Activation - Python Environment Variables', () => { let processService: IProcessService; let currentProcess: ICurrentProcess; let envVarsService: IEnvironmentVariablesProvider; - let workspace: typemoq.IMock; - + let workspace: IWorkspaceService; + let interpreterService: IInterpreterService; + let onDidChangeEnvVariables: EventEmitter; + let onDidChangeInterpreter: EventEmitter; const pythonInterpreter: PythonInterpreter = { path: '/foo/bar/python.exe', version: new SemVer('3.6.6-final'), @@ -63,29 +65,22 @@ suite('Interpreters Activation - Python Environment Variables', () => { processService = mock(ProcessService); currentProcess = mock(CurrentProcess); envVarsService = mock(EnvironmentVariablesProvider); - workspace = mockedVSCodeNamespaces.workspace!; - when(envVarsService.onDidEnvironmentVariablesChange).thenReturn(new EventEmitter().event); + interpreterService = mock(InterpreterService); + workspace = mock(WorkspaceService); + onDidChangeEnvVariables = new EventEmitter(); + onDidChangeInterpreter = new EventEmitter(); + when(envVarsService.onDidEnvironmentVariablesChange).thenReturn(onDidChangeEnvVariables.event); + when(interpreterService.onDidChangeInterpreter).thenReturn(onDidChangeInterpreter.event); service = new EnvironmentActivationService( instance(helper), instance(platform), instance(processServiceFactory), instance(currentProcess), + instance(workspace), + instance(interpreterService), instance(envVarsService) ); - - const cfg = typemoq.Mock.ofType(); - workspace - .setup(w => w.getConfiguration(typemoq.It.isValue('python'), typemoq.It.isAny())) - .returns(() => cfg.object); - workspace.setup(w => w.workspaceFolders).returns(() => []); - cfg.setup(c => c.inspect(typemoq.It.isValue('pythonPath'))).returns(() => { - return { globalValue: 'GlobalValuepython' } as any; - }); - clearCache(); } - teardown(() => { - mockedVSCodeNamespaces.workspace!.reset(); - }); function title(resource?: Uri, interpreter?: PythonInterpreter) { return `${resource ? 'With a resource' : 'Without a resource'}${interpreter ? ' and an interpreter' : ''}`; @@ -262,6 +257,72 @@ suite('Interpreters Activation - Python Environment Variables', () => { verify(envVarsService.getEnvironmentVariables(resource)).once(); verify(processService.shellExec(anything(), anything())).once(); }); + test('Cache Variables', async () => { + const cmd = ['1', '2']; + const varsFromEnv = { one: '11', two: '22', HELLO: 'xxx' }; + const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve({}); + when(processService.shellExec(anything(), anything())).thenResolve({ stdout: stdout }); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + const env2 = await service.getActivatedEnvironmentVariables(resource, interpreter); + const env3 = await service.getActivatedEnvironmentVariables(resource, interpreter); + + expect(env).to.deep.equal(varsFromEnv); + // All same objects. + expect(env) + .to.equal(env2) + .to.equal(env3); + + // All methods invoked only once. + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).once(); + verify(processServiceFactory.create(resource)).once(); + verify(envVarsService.getEnvironmentVariables(resource)).once(); + verify(processService.shellExec(anything(), anything())).once(); + }); + async function testClearingCache(bustCache: Function) { + const cmd = ['1', '2']; + const varsFromEnv = { one: '11', two: '22', HELLO: 'xxx' }; + const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve({}); + when(processService.shellExec(anything(), anything())).thenResolve({ stdout: stdout }); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + bustCache(); + const env2 = await service.getActivatedEnvironmentVariables(resource, interpreter); + + expect(env).to.deep.equal(varsFromEnv); + // Objects are different (not same reference). + expect(env).to.not.equal(env2); + // However variables are the same. + expect(env).to.deep.equal(env2); + + // All methods invoked twice as cache was blown. + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).twice(); + verify(processServiceFactory.create(resource)).twice(); + verify(envVarsService.getEnvironmentVariables(resource)).twice(); + verify(processService.shellExec(anything(), anything())).twice(); + } + test('Cache Variables get cleared when changing interpreter', async () => { + await testClearingCache(onDidChangeInterpreter.fire.bind(onDidChangeInterpreter)); + }); + test('Cache Variables get cleared when changing env variables file', async () => { + await testClearingCache(onDidChangeEnvVariables.fire.bind(onDidChangeEnvVariables)); + }); }); }); }); From 2265043eaf767cd8f086021a4bd3a9ec27c0e846 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Fri, 21 Feb 2020 09:27:42 -0800 Subject: [PATCH 05/10] Support jupyter output in the remote scenario too (#10241) * Add some output for remote situations * Add news entry --- news/2 Fixes/9177.md | 1 + package.nls.json | 5 +++- src/client/common/utils/localize.ts | 8 ++++++ .../datascience/jupyter/jupyterServer.ts | 26 ++++++++++++++++--- .../jupyter/jupyterServerWrapper.ts | 20 ++++++++++---- .../datascience/jupyter/jupyterSession.ts | 20 ++++++++++++-- .../jupyter/jupyterSessionManager.ts | 8 +++--- .../jupyter/jupyterSessionManagerFactory.ts | 11 +++++--- .../jupyter/liveshare/hostJupyterServer.ts | 13 +++++++--- .../jupyter/jupyterSession.unit.test.ts | 5 +++- 10 files changed, 95 insertions(+), 22 deletions(-) create mode 100644 news/2 Fixes/9177.md diff --git a/news/2 Fixes/9177.md b/news/2 Fixes/9177.md new file mode 100644 index 000000000000..cd241c5851b8 --- /dev/null +++ b/news/2 Fixes/9177.md @@ -0,0 +1 @@ +Jupyter output tab was not showing anything when connecting to a remote server. \ No newline at end of file diff --git a/package.nls.json b/package.nls.json index d3dde7184731..c846ee5cb7e3 100644 --- a/package.nls.json +++ b/package.nls.json @@ -443,5 +443,8 @@ "DataScience.gettingListOfKernelSpecs": "Fetching Kernel specs", "DataScience.startingJupyterNotebook": "Starting Jupyter Notebook", "DataScience.registeringKernel": "Registering Kernel", - "DataScience.trimmedOutput": "Output was trimmed for performance reasons.\nTo see the full output set the setting \"python.dataScience.textOutputLimit\" to 0." + "DataScience.trimmedOutput": "Output was trimmed for performance reasons.\nTo see the full output set the setting \"python.dataScience.textOutputLimit\" to 0.", + "DataScience.connectingToJupyterUri" : "Connecting to Jupyter server at {0}", + "DataScience.createdNewNotebook" : "{0}: Creating new notebook ", + "DataScience.createdNewKernel" : "{0}: Kernel started: {1}" } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 167f970d0116..1fab27bdf6e1 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -791,6 +791,14 @@ export namespace DataScience { 'DataScience.jupyterCommandLinePrompt', 'Enter your custom command line for Jupyter' ); + + export const connectingToJupyterUri = localize( + 'DataScience.connectingToJupyterUri', + 'Connecting to Jupyter server at {0}' + ); + export const createdNewNotebook = localize('DataScience.createdNewNotebook', '{0}: Creating new notebook '); + + export const createdNewKernel = localize('DataScience.createdNewKernel', '{0}: Kernel started: {1}'); } export namespace DebugConfigStrings { diff --git a/src/client/datascience/jupyter/jupyterServer.ts b/src/client/datascience/jupyter/jupyterServer.ts index 60b2a383ab97..49f4fad38caa 100644 --- a/src/client/datascience/jupyter/jupyterServer.ts +++ b/src/client/datascience/jupyter/jupyterServer.ts @@ -8,7 +8,13 @@ import { CancellationToken } from 'vscode-jsonrpc'; import { ILiveShareApi } from '../../common/application/types'; import '../../common/extensions'; import { traceError, traceInfo } from '../../common/logger'; -import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + Resource +} from '../../common/types'; import { createDeferred, Deferred } from '../../common/utils/async'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; @@ -42,7 +48,8 @@ export class JupyterServerBase implements INotebookServer { private disposableRegistry: IDisposableRegistry, protected readonly configService: IConfigurationService, private sessionManagerFactory: IJupyterSessionManagerFactory, - private loggers: INotebookExecutionLogger[] + private loggers: INotebookExecutionLogger[], + private jupyterOutputChannel: IOutputChannel ) { this.asyncRegistry.push(this); } @@ -67,6 +74,9 @@ export class JupyterServerBase implements INotebookServer { }); } + // Indicate we have a new session on the output channel + this.logRemoteOutput(localize.DataScience.connectingToJupyterUri().format(launchInfo.connectionInfo.baseUrl)); + // Create our session manager this.sessionManager = await this.sessionManagerFactory.create(launchInfo.connectionInfo); // Try creating a session just to ensure we're connected. Callers of this function check to make sure jupyter @@ -104,7 +114,11 @@ export class JupyterServerBase implements INotebookServer { this.loggers, notebookMetadata, cancelToken - ); + ).then(r => { + const baseUrl = this.launchInfo?.connectionInfo.baseUrl || ''; + this.logRemoteOutput(localize.DataScience.createdNewNotebook().format(baseUrl)); + return r; + }); } public async shutdown(): Promise { @@ -217,4 +231,10 @@ export class JupyterServerBase implements INotebookServer { this.launchInfo.kernelSpec = undefined; } } + + private logRemoteOutput(output: string) { + if (this.launchInfo && !this.launchInfo.connectionInfo.localLaunch) { + this.jupyterOutputChannel.appendLine(output); + } + } } diff --git a/src/client/datascience/jupyter/jupyterServerWrapper.ts b/src/client/datascience/jupyter/jupyterServerWrapper.ts index 3d121f9aaf7c..b9c9a0643902 100644 --- a/src/client/datascience/jupyter/jupyterServerWrapper.ts +++ b/src/client/datascience/jupyter/jupyterServerWrapper.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. 'use strict'; import { nbformat } from '@jupyterlab/coreutils'; -import { inject, injectable, multiInject, optional } from 'inversify'; +import { inject, injectable, multiInject, named, optional } from 'inversify'; import * as uuid from 'uuid/v4'; import { Uri } from 'vscode'; import { CancellationToken } from 'vscode-jsonrpc'; @@ -10,8 +10,15 @@ import * as vsls from 'vsls/vscode'; import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../common/application/types'; import '../../common/extensions'; import { IFileSystem } from '../../common/platform/types'; -import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + Resource +} from '../../common/types'; import { IInterpreterService } from '../../interpreter/contracts'; +import { JUPYTER_OUTPUT_CHANNEL } from '../constants'; import { IConnection, IDataScience, @@ -43,7 +50,8 @@ type JupyterServerClassType = { appShell: IApplicationShell, fs: IFileSystem, kernelSelector: KernelSelector, - interpreterService: IInterpreterService + interpreterService: IInterpreterService, + outputChannel: IOutputChannel ): IJupyterServerInterface; }; // tslint:enable:callable-types @@ -69,7 +77,8 @@ export class JupyterServerWrapper implements INotebookServer, ILiveShareHasRole @inject(IApplicationShell) appShell: IApplicationShell, @inject(IFileSystem) fs: IFileSystem, @inject(IInterpreterService) interpreterService: IInterpreterService, - @inject(KernelSelector) kernelSelector: KernelSelector + @inject(KernelSelector) kernelSelector: KernelSelector, + @inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) jupyterOutput: IOutputChannel ) { // The server factory will create the appropriate HostJupyterServer or GuestJupyterServer based on // the liveshare state. @@ -88,7 +97,8 @@ export class JupyterServerWrapper implements INotebookServer, ILiveShareHasRole appShell, fs, kernelSelector, - interpreterService + interpreterService, + jupyterOutput ); } diff --git a/src/client/datascience/jupyter/jupyterSession.ts b/src/client/datascience/jupyter/jupyterSession.ts index 5b14719506bc..b206719b99bf 100644 --- a/src/client/datascience/jupyter/jupyterSession.ts +++ b/src/client/datascience/jupyter/jupyterSession.ts @@ -19,6 +19,7 @@ import { ServerStatus } from '../../../datascience-ui/interactive-common/mainSta import { Cancellation } from '../../common/cancellation'; import { isTestExecution } from '../../common/constants'; import { traceInfo, traceWarning } from '../../common/logger'; +import { IOutputChannel } from '../../common/types'; import { sleep, waitForPromise } from '../../common/utils/async'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; @@ -69,7 +70,8 @@ export class JupyterSession implements IJupyterSession { private kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined, private sessionManager: SessionManager, private contentsManager: ContentsManager, - private readonly kernelSelector: KernelSelector + private readonly kernelSelector: KernelSelector, + private readonly outputChannel: IOutputChannel ) { this.statusHandler = this.onStatusChanged.bind(this); } @@ -386,11 +388,25 @@ export class JupyterSession implements IJupyterSession { }; return Cancellation.race( - () => this.sessionManager!.startNew(options).catch(ex => Promise.reject(new JupyterSessionStartError(ex))), + () => + this.sessionManager!.startNew(options) + .then(s => { + this.logRemoteOutput( + localize.DataScience.createdNewKernel().format(this.connInfo.baseUrl, s.kernel.id) + ); + return s; + }) + .catch(ex => Promise.reject(new JupyterSessionStartError(ex))), cancelToken ); } + private logRemoteOutput(output: string) { + if (this.connInfo && !this.connInfo.localLaunch) { + this.outputChannel.appendLine(output); + } + } + private async waitForKernelPromise( kernelPromise: Promise, timeout: number, diff --git a/src/client/datascience/jupyter/jupyterSessionManager.ts b/src/client/datascience/jupyter/jupyterSessionManager.ts index bdd1b1dc29be..a2faf3eb0779 100644 --- a/src/client/datascience/jupyter/jupyterSessionManager.ts +++ b/src/client/datascience/jupyter/jupyterSessionManager.ts @@ -6,7 +6,7 @@ import { Agent as HttpsAgent } from 'https'; import { CancellationToken } from 'vscode-jsonrpc'; import { traceInfo } from '../../common/logger'; -import { IConfigurationService } from '../../common/types'; +import { IConfigurationService, IOutputChannel } from '../../common/types'; import * as localize from '../../common/utils/localize'; import { IConnection, @@ -33,7 +33,8 @@ export class JupyterSessionManager implements IJupyterSessionManager { private jupyterPasswordConnect: IJupyterPasswordConnect, private config: IConfigurationService, private failOnPassword: boolean | undefined, - private kernelSelector: KernelSelector + private kernelSelector: KernelSelector, + private outputChannel: IOutputChannel ) {} public async dispose() { @@ -115,7 +116,8 @@ export class JupyterSessionManager implements IJupyterSessionManager { kernelSpec, this.sessionManager, this.contentsManager, - this.kernelSelector + this.kernelSelector, + this.outputChannel ); try { await session.connect(cancelToken); diff --git a/src/client/datascience/jupyter/jupyterSessionManagerFactory.ts b/src/client/datascience/jupyter/jupyterSessionManagerFactory.ts index 096d6a9c93b1..d9ac95af806e 100644 --- a/src/client/datascience/jupyter/jupyterSessionManagerFactory.ts +++ b/src/client/datascience/jupyter/jupyterSessionManagerFactory.ts @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -import { inject, injectable } from 'inversify'; +import { inject, injectable, named } from 'inversify'; -import { IConfigurationService } from '../../common/types'; +import { IConfigurationService, IOutputChannel } from '../../common/types'; +import { JUPYTER_OUTPUT_CHANNEL } from '../constants'; import { IConnection, IJupyterPasswordConnect, IJupyterSessionManager, IJupyterSessionManagerFactory } from '../types'; import { JupyterSessionManager } from './jupyterSessionManager'; import { KernelSelector } from './kernels/kernelSelector'; @@ -13,7 +14,8 @@ export class JupyterSessionManagerFactory implements IJupyterSessionManagerFacto constructor( @inject(IJupyterPasswordConnect) private jupyterPasswordConnect: IJupyterPasswordConnect, @inject(IConfigurationService) private config: IConfigurationService, - @inject(KernelSelector) private kernelSelector: KernelSelector + @inject(KernelSelector) private kernelSelector: KernelSelector, + @inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) private jupyterOutput: IOutputChannel ) {} /** @@ -26,7 +28,8 @@ export class JupyterSessionManagerFactory implements IJupyterSessionManagerFacto this.jupyterPasswordConnect, this.config, failOnPassword, - this.kernelSelector + this.kernelSelector, + this.jupyterOutput ); await result.initialize(connInfo); return result; diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts index 8cd714a7bf19..b510f2be3e13 100644 --- a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts +++ b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts @@ -12,7 +12,13 @@ import { nbformat } from '@jupyterlab/coreutils'; import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../../common/application/types'; import { traceInfo } from '../../../common/logger'; import { IFileSystem } from '../../../common/platform/types'; -import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + Resource +} from '../../../common/types'; import * as localize from '../../../common/utils/localize'; import { IInterpreterService } from '../../../interpreter/contracts'; import { Identifiers, LiveShare, LiveShareCommands, RegExpValues } from '../../constants'; @@ -52,9 +58,10 @@ export class HostJupyterServer extends LiveShareParticipantHost(JupyterServerBas private appService: IApplicationShell, private fs: IFileSystem, private readonly kernelSelector: KernelSelector, - private readonly interpreterService: IInterpreterService + private readonly interpreterService: IInterpreterService, + outputChannel: IOutputChannel ) { - super(liveShare, asyncRegistry, disposableRegistry, configService, sessionManager, loggers); + super(liveShare, asyncRegistry, disposableRegistry, configService, sessionManager, loggers, outputChannel); } public async dispose(): Promise { diff --git a/src/test/datascience/jupyter/jupyterSession.unit.test.ts b/src/test/datascience/jupyter/jupyterSession.unit.test.ts index 61a147131f91..73933ca22de1 100644 --- a/src/test/datascience/jupyter/jupyterSession.unit.test.ts +++ b/src/test/datascience/jupyter/jupyterSession.unit.test.ts @@ -17,6 +17,7 @@ import { JupyterSession } from '../../../client/datascience/jupyter/jupyterSessi import { KernelSelector } from '../../../client/datascience/jupyter/kernels/kernelSelector'; import { LiveKernelModel } from '../../../client/datascience/jupyter/kernels/types'; import { IConnection, IJupyterKernelSpec } from '../../../client/datascience/types'; +import { MockOutputChannel } from '../../mockClasses'; // tslint:disable: max-func-body-length suite('Data Science - JupyterSession', () => { @@ -63,6 +64,7 @@ suite('Data Science - JupyterSession', () => { kernelChangedSignal = mock(Signal); when(session.statusChanged).thenReturn(instance(statusChangedSignal)); when(session.kernelChanged).thenReturn(instance(kernelChangedSignal)); + const channel = new MockOutputChannel('JUPYTER'); // tslint:disable-next-line: no-any (instance(session) as any).then = undefined; sessionManager = mock(SessionManager); @@ -73,7 +75,8 @@ suite('Data Science - JupyterSession', () => { kernelSpec.object, instance(sessionManager), instance(contentsManager), - instance(kernelSelector) + instance(kernelSelector), + channel ); }); From d1c904788123c3d7ac85f785a0eb77f26c2e5d55 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 21 Feb 2020 10:32:27 -0800 Subject: [PATCH 06/10] Skip flaky FS test (#10244) Merely disables test for #10240 --- src/test/common/platform/filesystem.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/common/platform/filesystem.test.ts b/src/test/common/platform/filesystem.test.ts index 24a4bf8f9dcd..99cbc5f9a1fe 100644 --- a/src/test/common/platform/filesystem.test.ts +++ b/src/test/common/platform/filesystem.test.ts @@ -469,7 +469,10 @@ suite('FileSystem - raw', () => { }); suite('listdir', () => { - test('mixed', async () => { + test('mixed', async function() { + // https://github.com/microsoft/vscode-python/issues/10240 + // tslint:disable-next-line: no-invalid-this + return this.skip(); // Create the target directory and its contents. const dirname = await fix.createDirectory('x/y/z'); const file1 = await fix.createFile('x/y/z/__init__.py', ''); From 9fb5f4ec6c93f20b63a6385aef07beb866c99408 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 21 Feb 2020 10:42:27 -0800 Subject: [PATCH 07/10] Switch experiments settings scope to machine (#10237) * Switch experiments settings scope to machine * Add news item --- news/2 Fixes/10232.md | 1 + package.json | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 news/2 Fixes/10232.md diff --git a/news/2 Fixes/10232.md b/news/2 Fixes/10232.md new file mode 100644 index 000000000000..54d69f4262ba --- /dev/null +++ b/news/2 Fixes/10232.md @@ -0,0 +1 @@ +Users can opt into or opt out of experiments in remote scenarios. diff --git a/package.json b/package.json index b9cb9398b7b3..c8f906e2742a 100644 --- a/package.json +++ b/package.json @@ -1506,7 +1506,7 @@ "type": "boolean", "default": true, "description": "Enables/disables A/B tests.", - "scope": "application" + "scope": "machine" }, "python.experiments.optInto": { "type": "array", @@ -1526,7 +1526,7 @@ ] }, "description": "List of experiment to opt into. If empty, user is assigned the default experiment groups. See https://github.com/microsoft/vscode-python/wiki/Experiments for more details.", - "scope": "application" + "scope": "machine" }, "python.experiments.optOutFrom": { "type": "array", @@ -1546,7 +1546,7 @@ ] }, "description": "List of experiment to opt out of. If empty, user is assigned the default experiment groups. See https://github.com/microsoft/vscode-python/wiki/Experiments for more details.", - "scope": "application" + "scope": "machine" }, "python.dataScience.allowImportFromNotebook": { "type": "boolean", From 82bf8fd8e36979ab877d63e2cf52e21bb562adcc Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 21 Feb 2020 12:04:03 -0800 Subject: [PATCH 08/10] Show quickfixes for launch.json (#10245) * Added implementation * Added tests * Moved into providers * Add code actions vscode mock and convert .test.ts into unit.test.ts * Rename folders * Dispose registered services * News entry * Added unit tests * Rename core service --- news/1 Enhancements/10245.md | 1 + src/client/extension.ts | 2 +- .../launchJsonCodeActionProvider.ts | 32 ++++++++++ .../providers/codeActionProvider/main.ts | 27 +++++++++ .../pythonCodeActionProvider.ts} | 0 src/client/providers/serviceRegistry.ts | 6 ++ src/test/mocks/vsc/index.ts | 13 ++++ .../launchJsonCodeActionProvider.unit.test.ts | 59 +++++++++++++++++++ .../codeActionProvider/main.unit.test.ts | 57 ++++++++++++++++++ .../pythonCodeActionsProvider.unit.test.ts} | 4 +- .../providers/serviceRegistry.unit.test.ts | 8 +++ src/test/vscode-mock.ts | 1 + 12 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 news/1 Enhancements/10245.md create mode 100644 src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts create mode 100644 src/client/providers/codeActionProvider/main.ts rename src/client/providers/{codeActionsProvider.ts => codeActionProvider/pythonCodeActionProvider.ts} (100%) create mode 100644 src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts create mode 100644 src/test/providers/codeActionProvider/main.unit.test.ts rename src/test/providers/{codeActionsProvider.test.ts => codeActionProvider/pythonCodeActionsProvider.unit.test.ts} (90%) diff --git a/news/1 Enhancements/10245.md b/news/1 Enhancements/10245.md new file mode 100644 index 000000000000..e59965b96e63 --- /dev/null +++ b/news/1 Enhancements/10245.md @@ -0,0 +1 @@ +Show quickfixes for launch.json diff --git a/src/client/extension.ts b/src/client/extension.ts index 03e6987da962..38f491259d21 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -95,7 +95,7 @@ import { IServiceContainer, IServiceManager } from './ioc/types'; import { getLanguageConfiguration } from './language/languageConfiguration'; import { LinterCommands } from './linters/linterCommands'; import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; -import { PythonCodeActionProvider } from './providers/codeActionsProvider'; +import { PythonCodeActionProvider } from './providers/codeActionProvider/pythonCodeActionProvider'; import { PythonFormattingEditProvider } from './providers/formatProvider'; import { ReplProvider } from './providers/replProvider'; import { registerTypes as providersRegisterTypes } from './providers/serviceRegistry'; diff --git a/src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts b/src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts new file mode 100644 index 000000000000..e3cb8f90ec60 --- /dev/null +++ b/src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CodeAction, + CodeActionContext, + CodeActionKind, + CodeActionProvider, + Diagnostic, + Range, + TextDocument, + WorkspaceEdit +} from 'vscode'; + +/** + * Provides code actions for launch.json + */ +export class LaunchJsonCodeActionProvider implements CodeActionProvider { + public provideCodeActions(document: TextDocument, _: Range, context: CodeActionContext): CodeAction[] { + return context.diagnostics + .filter(diagnostic => diagnostic.message === 'Incorrect type. Expected "string".') + .map(diagnostic => this.createFix(document, diagnostic)); + } + + private createFix(document: TextDocument, diagnostic: Diagnostic): CodeAction { + const finalText = `"${document.getText(diagnostic.range)}"`; + const fix = new CodeAction(`Convert to ${finalText}`, CodeActionKind.QuickFix); + fix.edit = new WorkspaceEdit(); + fix.edit.replace(document.uri, diagnostic.range, finalText); + return fix; + } +} diff --git a/src/client/providers/codeActionProvider/main.ts b/src/client/providers/codeActionProvider/main.ts new file mode 100644 index 000000000000..375c9986d949 --- /dev/null +++ b/src/client/providers/codeActionProvider/main.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as vscodeTypes from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IDisposableRegistry } from '../../common/types'; +import { LaunchJsonCodeActionProvider } from './launchJsonCodeActionProvider'; + +@injectable() +export class CodeActionProviderService implements IExtensionSingleActivationService { + constructor(@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry) {} + public async activate(): Promise { + // tslint:disable-next-line:no-require-imports + const vscode = require('vscode') as typeof vscodeTypes; + const documentSelector: vscodeTypes.DocumentFilter = { + scheme: 'file', + language: 'jsonc', + pattern: '**/launch.json' + }; + this.disposableRegistry.push( + vscode.languages.registerCodeActionsProvider(documentSelector, new LaunchJsonCodeActionProvider(), { + providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] + }) + ); + } +} diff --git a/src/client/providers/codeActionsProvider.ts b/src/client/providers/codeActionProvider/pythonCodeActionProvider.ts similarity index 100% rename from src/client/providers/codeActionsProvider.ts rename to src/client/providers/codeActionProvider/pythonCodeActionProvider.ts diff --git a/src/client/providers/serviceRegistry.ts b/src/client/providers/serviceRegistry.ts index 7418e0175e51..66640455f08a 100644 --- a/src/client/providers/serviceRegistry.ts +++ b/src/client/providers/serviceRegistry.ts @@ -3,10 +3,16 @@ 'use strict'; +import { IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; +import { CodeActionProviderService } from './codeActionProvider/main'; import { SortImportsEditingProvider } from './importSortProvider'; import { ISortImportsEditingProvider } from './types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(ISortImportsEditingProvider, SortImportsEditingProvider); + serviceManager.addSingleton( + IExtensionSingleActivationService, + CodeActionProviderService + ); } diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts index 0f86994533fe..b309c0a55e58 100644 --- a/src/test/mocks/vsc/index.ts +++ b/src/test/mocks/vsc/index.ts @@ -76,6 +76,19 @@ export namespace vscMock { } } + export class CodeAction { + public title: string; + public edit?: vscode.WorkspaceEdit; + public diagnostics?: vscode.Diagnostic[]; + public command?: vscode.Command; + public kind?: CodeActionKind; + public isPreferred?: boolean; + constructor(_title: string, _kind?: CodeActionKind) { + this.title = _title; + this.kind = _kind; + } + } + export enum CompletionItemKind { Text = 0, Method = 1, diff --git a/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts b/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts new file mode 100644 index 000000000000..ceae05d4bbd0 --- /dev/null +++ b/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { CodeActionContext, CodeActionKind, Diagnostic, Range, TextDocument, Uri } from 'vscode'; +import { LaunchJsonCodeActionProvider } from '../../../client/providers/codeActionProvider/launchJsonCodeActionProvider'; + +suite('LaunchJson CodeAction Provider', () => { + const documentUri = Uri.parse('a'); + let document: TypeMoq.IMock; + let range: TypeMoq.IMock; + let context: TypeMoq.IMock; + let diagnostic: TypeMoq.IMock; + let codeActionsProvider: LaunchJsonCodeActionProvider; + + setup(() => { + codeActionsProvider = new LaunchJsonCodeActionProvider(); + document = TypeMoq.Mock.ofType(); + range = TypeMoq.Mock.ofType(); + context = TypeMoq.Mock.ofType(); + diagnostic = TypeMoq.Mock.ofType(); + document.setup(d => d.getText(TypeMoq.It.isAny())).returns(() => 'Diagnostic text'); + document.setup(d => d.uri).returns(() => documentUri); + context.setup(c => c.diagnostics).returns(() => [diagnostic.object]); + }); + + test('Ensure correct code action is returned if diagnostic message equals `Incorrect type. Expected "string".`', async () => { + diagnostic.setup(d => d.message).returns(() => 'Incorrect type. Expected "string".'); + diagnostic.setup(d => d.range).returns(() => new Range(2, 0, 7, 8)); + + const codeActions = codeActionsProvider.provideCodeActions(document.object, range.object, context.object); + + // Now ensure that the code action object is as expected + expect(codeActions).to.have.length(1); + expect(codeActions[0].kind).to.eq(CodeActionKind.QuickFix); + expect(codeActions[0].title).to.equal('Convert to "Diagnostic text"'); + + // Ensure the correct TextEdit is provided + const entries = codeActions[0].edit!.entries(); + // Edits the correct document is edited + assert.deepEqual(entries[0][0], documentUri); + const edit = entries[0][1][0]; + // Final text is as expected + expect(edit.newText).to.equal('"Diagnostic text"'); + // Text edit range is as expected + expect(edit.range.isEqual(new Range(2, 0, 7, 8))).to.equal(true, 'Text edit range not as expected'); + }); + + test('Ensure no code action is returned if diagnostic message does not equal `Incorrect type. Expected "string".`', async () => { + diagnostic.setup(d => d.message).returns(() => 'Random diagnostic message'); + + const codeActions = codeActionsProvider.provideCodeActions(document.object, range.object, context.object); + + expect(codeActions).to.have.length(0); + }); +}); diff --git a/src/test/providers/codeActionProvider/main.unit.test.ts b/src/test/providers/codeActionProvider/main.unit.test.ts new file mode 100644 index 000000000000..a29b78fcd4fb --- /dev/null +++ b/src/test/providers/codeActionProvider/main.unit.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable: match-default-export-name +import { assert, expect } from 'chai'; +import rewiremock from 'rewiremock'; +import * as typemoq from 'typemoq'; +import { CodeActionProvider, CodeActionProviderMetadata, DocumentSelector } from 'vscode'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { LaunchJsonCodeActionProvider } from '../../../client/providers/codeActionProvider/launchJsonCodeActionProvider'; +import { CodeActionProviderService } from '../../../client/providers/codeActionProvider/main'; + +suite('Code Action Provider service', async () => { + setup(() => { + rewiremock.disable(); + }); + test('Code actions are registered correctly', async () => { + let selector: DocumentSelector; + let provider: CodeActionProvider; + let metadata: CodeActionProviderMetadata; + const vscodeMock = { + languages: { + registerCodeActionsProvider: ( + _selector: DocumentSelector, + _provider: CodeActionProvider, + _metadata: CodeActionProviderMetadata + ) => { + selector = _selector; + provider = _provider; + metadata = _metadata; + } + }, + CodeActionKind: { + QuickFix: 'CodeAction' + } + }; + rewiremock.enable(); + rewiremock('vscode').with(vscodeMock); + const quickFixService = new CodeActionProviderService(typemoq.Mock.ofType().object); + + await quickFixService.activate(); + + // Ensure QuickFixLaunchJson is registered with correct arguments + assert.deepEqual(selector!, { + scheme: 'file', + language: 'jsonc', + pattern: '**/launch.json' + }); + assert.deepEqual(metadata!, { + // tslint:disable-next-line:no-any + providedCodeActionKinds: ['CodeAction' as any] + }); + expect(provider!).instanceOf(LaunchJsonCodeActionProvider); + }); +}); diff --git a/src/test/providers/codeActionsProvider.test.ts b/src/test/providers/codeActionProvider/pythonCodeActionsProvider.unit.test.ts similarity index 90% rename from src/test/providers/codeActionsProvider.test.ts rename to src/test/providers/codeActionProvider/pythonCodeActionsProvider.unit.test.ts index 8147063d649e..454dd2bc27ad 100644 --- a/src/test/providers/codeActionsProvider.test.ts +++ b/src/test/providers/codeActionProvider/pythonCodeActionsProvider.unit.test.ts @@ -6,9 +6,9 @@ import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; import { CancellationToken, CodeActionContext, CodeActionKind, Range, TextDocument } from 'vscode'; -import { PythonCodeActionProvider } from '../../client/providers/codeActionsProvider'; +import { PythonCodeActionProvider } from '../../../client/providers/codeActionProvider/pythonCodeActionProvider'; -suite('CodeAction Provider', () => { +suite('Python CodeAction Provider', () => { let codeActionsProvider: PythonCodeActionProvider; let document: TypeMoq.IMock; let range: TypeMoq.IMock; diff --git a/src/test/providers/serviceRegistry.unit.test.ts b/src/test/providers/serviceRegistry.unit.test.ts index d7e29720e3ff..fe4da7a03b12 100644 --- a/src/test/providers/serviceRegistry.unit.test.ts +++ b/src/test/providers/serviceRegistry.unit.test.ts @@ -4,8 +4,10 @@ 'use strict'; import { instance, mock, verify } from 'ts-mockito'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceManager } from '../../client/ioc/types'; +import { CodeActionProviderService } from '../../client/providers/codeActionProvider/main'; import { SortImportsEditingProvider } from '../../client/providers/importSortProvider'; import { registerTypes } from '../../client/providers/serviceRegistry'; import { ISortImportsEditingProvider } from '../../client/providers/types'; @@ -25,5 +27,11 @@ suite('Common Providers Service Registry', () => { SortImportsEditingProvider ) ).once(); + verify( + serviceManager.addSingleton( + IExtensionSingleActivationService, + CodeActionProviderService + ) + ).once(); }); }); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index f3105a9bd1fd..0712ce3810c6 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -50,6 +50,7 @@ export function initialize() { } mockedVSCode.Disposable = vscodeMocks.vscMock.Disposable as any; +mockedVSCode.CodeAction = vscodeMocks.vscMock.CodeAction; mockedVSCode.EventEmitter = vscodeMocks.vscMock.EventEmitter; mockedVSCode.CancellationTokenSource = vscodeMocks.vscMock.CancellationTokenSource; mockedVSCode.CompletionItemKind = vscodeMocks.vscMock.CompletionItemKind; From 5e304878dd689f25beb693ecf8a25cfde775aa19 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Fri, 21 Feb 2020 14:07:18 -0700 Subject: [PATCH 09/10] Reduce the number of build agents used by CI. (#10221) * Drop extra jobs from the PR-validation pipeline. This reduces the number of build agents we are using most frequently, without sacrificing much coverage. Note that this relies on 2 things: * sufficient unit test coverage * manual "full CI" runs when there may be OS-specific concerns With this change: * all tests get run on 3.x on linux (including smoke) * for 2.7 only the unit and functional tests are run (and only on linux) * on Windows and OSX, only functional and "single workspace" tests are run The total number of test jobs (and hence agents) drops from 13 to 8. * Drop extra jobs from the PR-merge pipeline. This change relies on the nightly CI run covering the full matrix. The total number of test jobs (and hence agents) drops from 39 to 24. * Limit # of parallel jobs in each job matrix in the PR-merge and nightly pipelines. This reduces the max number of agents used by the PR-merge pipeline to 8 (from 24). For the nightly pipeline it goes down from 84 to 12. Both will take longer to complete, but that shouldn't be a major problem. * Add a "manual build" CI pipeline for faster turnaround. * Do not test internal tools on lower than Python 3.7. * Drop the temporary workaround jobs. * (again) Do not test internal tools on lower than Python 3.7. --- build/ci/vscode-python-ci-manual.yaml | 296 ++++++++++++++++++++++ build/ci/vscode-python-ci.yaml | 119 ++------- build/ci/vscode-python-nightly-ci.yaml | 57 +++-- build/ci/vscode-python-pr-validation.yaml | 86 ++----- 4 files changed, 372 insertions(+), 186 deletions(-) create mode 100644 build/ci/vscode-python-ci-manual.yaml diff --git a/build/ci/vscode-python-ci-manual.yaml b/build/ci/vscode-python-ci-manual.yaml new file mode 100644 index 000000000000..dab4e97f07a4 --- /dev/null +++ b/build/ci/vscode-python-ci-manual.yaml @@ -0,0 +1,296 @@ +# manual CI build + +name: '$(Year:yyyy).$(Month).0.$(BuildID)-manual' + +trigger: none +pr: none + +# Variables that are available for the entire pipeline. +variables: +- template: templates/globals.yml + +stages: +- stage: Build + jobs: + - template: templates/jobs/build_compile.yml + +# Each item in each matrix has a number of possible values it may +# define. They are detailed in templates/test_phases.yml. The only +# required value is "TestsToRun". + +- stage: Linux + dependsOn: [] + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + # with mocks + # focused on small units (i.e. functions) + # and tightly controlled dependencies + TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + # no mocks, no vscode + # focused on integration + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + # no mocks, with vscode + # focused on integration + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + # no mocks, with vscode + # focused on integration + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Venv': + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + # This is for the venvTests to use, not needed if you don't run venv tests... + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Smoke': + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + #maxParallel: 3 + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + # This is the oldest Python 3 version we support. + - job: 'Py36' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.6' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '3.6' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '3.6' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + #maxParallel: 3 + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + # This is the oldest Python 3 version we support. + - job: 'Py35' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.5' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '3.5' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '3.5' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Venv': + PythonVersion: '3.5' + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + PythonVersion: '3.5' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + #maxParallel: 3 + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + +- stage: Mac + dependsOn: [] + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Venv': + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Smoke': + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + #maxParallel: 3 + pool: + vmImage: 'macos-10.13' + steps: + - template: templates/test_phases.yml + + # This is the oldest Python 3 version we support. + - job: 'Py35' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.5' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '3.5' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '3.5' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Venv': + PythonVersion: '3.5' + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + PythonVersion: '3.5' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + #maxParallel: 3 + pool: + vmImage: 'macos-10.13' + steps: + - template: templates/test_phases.yml + +- stage: Windows + dependsOn: [] + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Venv': + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Smoke': + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + #maxParallel: 3 + pool: + vmImage: 'vs2017-win2016' + steps: + - template: templates/test_phases.yml + + # This is the oldest Python 3 version we support. + - job: 'Py35' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.5' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '3.5' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '3.5' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Venv': + PythonVersion: '3.5' + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + PythonVersion: '3.5' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + #maxParallel: 3 + pool: + vmImage: 'vs2017-win2016' + steps: + - template: templates/test_phases.yml + +- stage: Reports + dependsOn: + - Linux + - Mac + - Windows + condition: always() + jobs: + - template: templates/jobs/coverage.yml diff --git a/build/ci/vscode-python-ci.yaml b/build/ci/vscode-python-ci.yaml index 62bcd5c5f321..fc1cae3c0cb9 100644 --- a/build/ci/vscode-python-ci.yaml +++ b/build/ci/vscode-python-ci.yaml @@ -1,5 +1,7 @@ +# CI build (PR merge) + name: '$(Year:yyyy).$(Month).0.$(BuildID)-ci' -# CI build. + # Notes: Only trigger a commit for master and release, and skip build/rebuild # on changes in the news and .vscode folders. trigger: @@ -65,47 +67,38 @@ stages: TestsToRun: 'testDebugger' NeedsPythonTestReqs: true 'Smoke': - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + maxParallel: 2 pool: vmImage: 'ubuntu-16.04' steps: - template: templates/test_phases.yml - - job: 'Py36' + # This is the oldest Python 3 version we support. + - job: 'Py35' dependsOn: [] timeoutInMinutes: 120 strategy: matrix: 'Unit': - PythonVersion: '3.6' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + PythonVersion: '3.5' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' NeedsPythonTestReqs: true NeedsIPythonReqs: true - 'Functional': - PythonVersion: '3.6' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '3.6' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Multi Workspace': - PythonVersion: '3.6' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true 'Venv': - PythonVersion: '3.6' + PythonVersion: '3.5' TestsToRun: 'venvTests' NeedsPythonTestReqs: true NeedsIPythonReqs: true PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' 'Debugger': - PythonVersion: '3.6' + PythonVersion: '3.5' TestsToRun: 'testDebugger' NeedsPythonTestReqs: true + maxParallel: 2 pool: vmImage: 'ubuntu-16.04' steps: @@ -142,47 +135,10 @@ stages: TestsToRun: 'testDebugger' NeedsPythonTestReqs: true 'Smoke': - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - pool: - vmImage: 'macos-10.13' - steps: - - template: templates/test_phases.yml - - - job: 'Py36' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '3.6' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + TestsToRun: 'testSmoke' NeedsPythonTestReqs: true NeedsIPythonReqs: true - 'Functional': - PythonVersion: '3.6' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '3.6' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Multi Workspace': - PythonVersion: '3.6' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Venv': - PythonVersion: '3.6' - TestsToRun: 'venvTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Debugger': - PythonVersion: '3.6' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true + maxParallel: 2 pool: vmImage: 'macos-10.13' steps: @@ -219,47 +175,10 @@ stages: TestsToRun: 'testDebugger' NeedsPythonTestReqs: true 'Smoke': - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - pool: - vmImage: 'vs2017-win2016' - steps: - - template: templates/test_phases.yml - - - job: 'Py36' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '3.6' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + TestsToRun: 'testSmoke' NeedsPythonTestReqs: true NeedsIPythonReqs: true - 'Functional': - PythonVersion: '3.6' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '3.6' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Multi Workspace': - PythonVersion: '3.6' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Venv': - PythonVersion: '3.6' - TestsToRun: 'venvTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Debugger': - PythonVersion: '3.6' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true + maxParallel: 2 pool: vmImage: 'vs2017-win2016' steps: diff --git a/build/ci/vscode-python-nightly-ci.yaml b/build/ci/vscode-python-nightly-ci.yaml index 447fba09f0c8..50395a6fc494 100644 --- a/build/ci/vscode-python-nightly-ci.yaml +++ b/build/ci/vscode-python-nightly-ci.yaml @@ -63,9 +63,10 @@ stages: # This is for the venvTests to use, not needed if you don't run venv tests... PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' 'Smoke': - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + maxParallel: 1 pool: vmImage: 'ubuntu-16.04' steps: @@ -78,7 +79,8 @@ stages: matrix: 'Unit': PythonVersion: '3.6' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' NeedsPythonTestReqs: true NeedsIPythonReqs: true 'Functional': @@ -104,6 +106,7 @@ stages: NeedsPythonTestReqs: true PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 pool: vmImage: 'ubuntu-16.04' steps: @@ -116,7 +119,8 @@ stages: matrix: 'Unit': PythonVersion: '3.5' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' NeedsPythonTestReqs: true NeedsIPythonReqs: true 'Functional': @@ -142,6 +146,7 @@ stages: NeedsPythonTestReqs: true PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 pool: vmImage: 'ubuntu-16.04' steps: @@ -154,7 +159,8 @@ stages: matrix: 'Unit': PythonVersion: '2.7' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' NeedsPythonTestReqs: true NeedsIPythonReqs: true 'Functional': @@ -176,6 +182,7 @@ stages: NeedsPythonTestReqs: true # Note: Virtual env tests use `venv` and won't currently work with Python 2.7 # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 pool: vmImage: 'ubuntu-16.04' steps: @@ -212,9 +219,10 @@ stages: NeedsPythonTestReqs: true PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' 'Smoke': - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + maxParallel: 1 pool: vmImage: 'macos-10.13' steps: @@ -227,7 +235,8 @@ stages: matrix: 'Unit': PythonVersion: '3.6' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' NeedsPythonTestReqs: true NeedsIPythonReqs: true 'Functional': @@ -253,6 +262,7 @@ stages: NeedsPythonTestReqs: true PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 pool: vmImage: 'macos-10.13' steps: @@ -265,7 +275,8 @@ stages: matrix: 'Unit': PythonVersion: '3.5' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' NeedsPythonTestReqs: true NeedsIPythonReqs: true 'Functional': @@ -291,6 +302,7 @@ stages: NeedsPythonTestReqs: true PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 pool: vmImage: 'macos-10.13' steps: @@ -303,7 +315,8 @@ stages: matrix: 'Unit': PythonVersion: '2.7' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' NeedsPythonTestReqs: true NeedsIPythonReqs: true 'Functional': @@ -325,6 +338,7 @@ stages: NeedsPythonTestReqs: true # Note: Virtual env tests use `venv` and won't currently work with Python 2.7 # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 pool: vmImage: 'macos-10.13' steps: @@ -361,9 +375,10 @@ stages: NeedsPythonTestReqs: true PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' 'Smoke': - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + maxParallel: 1 pool: vmImage: 'vs2017-win2016' steps: @@ -376,7 +391,8 @@ stages: matrix: 'Unit': PythonVersion: '3.6' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' NeedsPythonTestReqs: true NeedsIPythonReqs: true 'Functional': @@ -402,6 +418,7 @@ stages: NeedsPythonTestReqs: true PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 pool: vmImage: 'vs2017-win2016' steps: @@ -414,7 +431,8 @@ stages: matrix: 'Unit': PythonVersion: '3.5' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' NeedsPythonTestReqs: true NeedsIPythonReqs: true 'Functional': @@ -440,6 +458,7 @@ stages: NeedsPythonTestReqs: true PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 pool: vmImage: 'vs2017-win2016' steps: @@ -452,7 +471,8 @@ stages: matrix: 'Unit': PythonVersion: '2.7' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' NeedsPythonTestReqs: true NeedsIPythonReqs: true 'Functional': @@ -474,6 +494,7 @@ stages: NeedsPythonTestReqs: true # Note: Virtual env tests use `venv` and won't currently work with Python 2.7 # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 pool: vmImage: 'vs2017-win2016' steps: diff --git a/build/ci/vscode-python-pr-validation.yaml b/build/ci/vscode-python-pr-validation.yaml index c198c2140eeb..fd143602a23b 100644 --- a/build/ci/vscode-python-pr-validation.yaml +++ b/build/ci/vscode-python-pr-validation.yaml @@ -1,6 +1,7 @@ +# PR Validation build. + name: '$(Year:yyyy).$(Month).0.$(BuildID)-pr' -# PR Validation build. # Notes: Only trigger a PR build for master and release, and skip build/rebuild # on changes in the news and .vscode folders. pr: @@ -57,10 +58,16 @@ stages: dependsOn: [] strategy: matrix: - 'Unit+Single': + 'Unit': PythonVersion: '2.7' - TestsToRun: 'pythonUnitTests, testSingleWorkspace' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests' + NeedsPythonTestReqs: true + 'Functional': + PythonVersion: '2.7' + TestsToRun: 'testfunctional' NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true pool: vmImage: 'ubuntu-16.04' steps: @@ -73,30 +80,11 @@ stages: dependsOn: [] strategy: matrix: - 'Unit': - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Single Workspace': - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Smoke': - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - pool: - vmImage: 'macos-10.13' - steps: - - template: templates/test_phases.yml - - - job: 'Py27' - dependsOn: [] - strategy: - matrix: - 'Unit+Single': - PythonVersion: '2.7' - TestsToRun: 'pythonUnitTests, testSingleWorkspace' + # This gives us our best functional coverage for the OS. + 'Functional+Single': + TestsToRun: 'testfunctional, testSingleWorkspace' NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true pool: vmImage: 'macos-10.13' steps: @@ -109,27 +97,11 @@ stages: dependsOn: [] strategy: matrix: - 'Unit': - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Smoke': - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - pool: - vmImage: 'vs2017-win2016' - steps: - - template: templates/test_phases.yml - - - job: 'Py27' - dependsOn: [] - strategy: - matrix: - 'Unit': - PythonVersion: '2.7' - TestsToRun: 'pythonUnitTests' + # This gives us our best functional coverage for the OS. + 'Functional+Single': + TestsToRun: 'testfunctional, testSingleWorkspace' NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true pool: vmImage: 'vs2017-win2016' steps: @@ -143,25 +115,3 @@ stages: condition: always() jobs: - template: templates/jobs/coverage.yml - -# The following is a temporary workaround for transitioning the -# job names for 2 jobs set as required checks in GitHub. It will -# be removed once we adjust the GitHub configuration. - -- stage: Tests - dependsOn: [] - jobs: - - job: 'Test' - dependsOn: [] - strategy: - matrix: - 'Linux-Py3.7 Functional': - noop: true - 'Linux-Py3.7 Unit': - noop: true - # Essentially, this is a noop. - steps: - - task: Delay@1 - inputs: - delayForMinutes: '0' - pool: server From ccd0848e535455f54867ad4ffcd21c803d5bb3d2 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Fri, 21 Feb 2020 14:46:50 -0800 Subject: [PATCH 10/10] Improve the perf of functional tests running with real jupyter (#10242) * Improve the perf of subsequent tests by caching interpreters * Add back the nightly flake * Remove coverage * Add news entry * Use a static map to allow promise to be cleared on new interpreters (as it was before) --- build/.mocha.functional.perf.opts | 9 ++ build/ci/vscode-python-nightly-flake-ci.yaml | 137 ++++++++++++++++++ news/3 Code Health/7997.md | 1 + package.json | 1 + .../services/cacheableLocatorService.ts | 36 ++++- .../datascience/dataScienceIocContainer.ts | 23 ++- .../datascience/testPersistentStateFactory.ts | 53 +++++++ 7 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 build/.mocha.functional.perf.opts create mode 100644 build/ci/vscode-python-nightly-flake-ci.yaml create mode 100644 news/3 Code Health/7997.md create mode 100644 src/test/datascience/testPersistentStateFactory.ts diff --git a/build/.mocha.functional.perf.opts b/build/.mocha.functional.perf.opts new file mode 100644 index 000000000000..decfc3ece6e6 --- /dev/null +++ b/build/.mocha.functional.perf.opts @@ -0,0 +1,9 @@ +./out/test/**/*.functional.test.js +--require=out/test/unittests.js +--exclude=out/**/*.jsx +--ui=tdd +--recursive +--colors +--exit +--timeout=180000 +--reporter spec diff --git a/build/ci/vscode-python-nightly-flake-ci.yaml b/build/ci/vscode-python-nightly-flake-ci.yaml new file mode 100644 index 000000000000..191e3503a3ea --- /dev/null +++ b/build/ci/vscode-python-nightly-flake-ci.yaml @@ -0,0 +1,137 @@ +# Nightly build + +name: '$(Year:yyyy).$(Month).0.$(BuildID)-nightly-flake' + +# Not the CI build, see `vscode-python-nightly-flake-ci.yaml`. +trigger: none + +# Not the PR build for merges to master and release. +pr: none + +schedules: +- cron: "0 8 * * 1-5" + # Daily midnight PST build, runs Monday - Friday always + displayName: Nightly Flake build + branches: + include: + - master + - release* + always: true + +# Variables that are available for the entire pipeline. +variables: +- template: templates/globals.yml + +stages: +- stage: Build + jobs: + - template: templates/jobs/build_compile.yml + +# Each item in each matrix has a number of possible values it may +# define. They are detailed in templates/test_phases.yml. The only +# required value is "TestsToRun". + +- stage: Linux + dependsOn: + - Build + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + VSCODE_PYTHON_ROLLING: true + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + - job: 'Py36' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Functional': + PythonVersion: '3.6' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + VSCODE_PYTHON_ROLLING: true + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + +- stage: Mac + dependsOn: + - Build + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + VSCODE_PYTHON_ROLLING: true + pool: + vmImage: 'macos-10.13' + steps: + - template: templates/test_phases.yml + + - job: 'Py36' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Functional': + PythonVersion: '3.6' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + VSCODE_PYTHON_ROLLING: true + pool: + vmImage: 'macos-10.13' + steps: + - template: templates/test_phases.yml + +- stage: Windows + dependsOn: + - Build + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + VSCODE_PYTHON_ROLLING: true + pool: + vmImage: 'vs2017-win2016' + steps: + - template: templates/test_phases.yml + + - job: 'Py36' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Functional': + PythonVersion: '3.6' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + VSCODE_PYTHON_ROLLING: true + pool: + vmImage: 'vs2017-win2016' + steps: + - template: templates/test_phases.yml \ No newline at end of file diff --git a/news/3 Code Health/7997.md b/news/3 Code Health/7997.md new file mode 100644 index 000000000000..cd92de44dd9d --- /dev/null +++ b/news/3 Code Health/7997.md @@ -0,0 +1 @@ +Functional tests using real jupyter can take 30-90 seconds each. Most of this time is searching for interpreters. Cache the interpreter search. \ No newline at end of file diff --git a/package.json b/package.json index c8f906e2742a..0e956b67adb1 100644 --- a/package.json +++ b/package.json @@ -2857,6 +2857,7 @@ "test:unittests": "mocha --opts ./build/.mocha.unittests.js.opts", "test:unittests:cover": "nyc --no-clean --nycrc-path build/.nycrc mocha --opts ./build/.mocha.unittests.ts.opts", "test:functional": "mocha --require source-map-support/register --opts ./build/.mocha.functional.opts", + "test:functional:perf": "node --inspect-brk ./node_modules/mocha/bin/_mocha --require source-map-support/register --opts ./build/.mocha.functional.perf.opts", "test:functional:cover": "npm run test:functional", "test:cover:report": "nyc --nycrc-path build/.nycrc report --reporter=text --reporter=html --reporter=text-summary --reporter=cobertura", "testDebugger": "node ./out/test/testBootstrap.js ./out/test/debuggerTest.js", diff --git a/src/client/interpreter/locators/services/cacheableLocatorService.ts b/src/client/interpreter/locators/services/cacheableLocatorService.ts index 8a5dd240324c..f2608fed6a34 100644 --- a/src/client/interpreter/locators/services/cacheableLocatorService.ts +++ b/src/client/interpreter/locators/services/cacheableLocatorService.ts @@ -17,10 +17,42 @@ import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { IInterpreterLocatorService, IInterpreterWatcher, PythonInterpreter } from '../../contracts'; +export class CacheableLocatorPromiseCache { + private static useStatic = false; + private static staticMap = new Map>(); + private normalMap = new Map>(); + + public static forceUseStatic() { + CacheableLocatorPromiseCache.useStatic = true; + } + public get(key: string): Deferred | undefined { + if (CacheableLocatorPromiseCache.useStatic) { + return CacheableLocatorPromiseCache.staticMap.get(key); + } + return this.normalMap.get(key); + } + + public set(key: string, value: Deferred) { + if (CacheableLocatorPromiseCache.useStatic) { + CacheableLocatorPromiseCache.staticMap.set(key, value); + } else { + this.normalMap.set(key, value); + } + } + + public delete(key: string) { + if (CacheableLocatorPromiseCache.useStatic) { + CacheableLocatorPromiseCache.staticMap.delete(key); + } else { + this.normalMap.delete(key); + } + } +} + @injectable() export abstract class CacheableLocatorService implements IInterpreterLocatorService { protected readonly _hasInterpreters: Deferred; - private readonly promisesPerResource = new Map>(); + private readonly promisesPerResource = new CacheableLocatorPromiseCache(); private readonly handlersAddedToResource = new Set(); private readonly cacheKeyPrefix: string; private readonly locating = new EventEmitter>(); @@ -32,6 +64,7 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ this._hasInterpreters = createDeferred(); this.cacheKeyPrefix = `INTERPRETERS_CACHE_v3_${name}`; } + public get onLocating(): Event> { return this.locating.event; } @@ -43,7 +76,6 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ public async getInterpreters(resource?: Uri, ignoreCache?: boolean): Promise { const cacheKey = this.getCacheKey(resource); let deferred = this.promisesPerResource.get(cacheKey); - if (!deferred || ignoreCache) { deferred = createDeferred(); this.promisesPerResource.set(cacheKey, deferred); diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index a4a8e5a1619b..63c0af10be7e 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -16,6 +16,7 @@ import { Event, EventEmitter, FileSystemWatcher, + Memento, Uri, WorkspaceConfiguration, WorkspaceFolder, @@ -100,7 +101,6 @@ import { } from '../../client/common/installer/productPath'; import { ProductService } from '../../client/common/installer/productService'; import { IInstallationChannelManager, IProductPathService, IProductService } from '../../client/common/installer/types'; -import { PersistentStateFactory } from '../../client/common/persistentState'; import { IS_WINDOWS } from '../../client/common/platform/constants'; import { PathUtils } from '../../client/common/platform/pathUtils'; import { RegistryImplementation } from '../../client/common/platform/registry'; @@ -131,6 +131,7 @@ import { } from '../../client/common/terminal/types'; import { BANNER_NAME_LS_SURVEY, + GLOBAL_MEMENTO, IAsyncDisposableRegistry, IConfigurationService, ICryptoUtils, @@ -140,12 +141,14 @@ import { IExtensionContext, IExtensions, IInstaller, + IMemento, IOutputChannel, IPathUtils, IPersistentStateFactory, IPythonExtensionBanner, IsWindows, - ProductType + ProductType, + WORKSPACE_MEMENTO } from '../../client/common/types'; import { Deferred, sleep } from '../../client/common/utils/async'; import { noop } from '../../client/common/utils/misc'; @@ -285,6 +288,7 @@ import { InterpreterService } from '../../client/interpreter/interpreterService' import { InterpreterVersionService } from '../../client/interpreter/interpreterVersion'; import { PythonInterpreterLocatorService } from '../../client/interpreter/locators'; import { InterpreterLocatorHelper } from '../../client/interpreter/locators/helpers'; +import { CacheableLocatorPromiseCache } from '../../client/interpreter/locators/services/cacheableLocatorService'; import { CondaEnvFileService } from '../../client/interpreter/locators/services/condaEnvFileService'; import { CondaEnvService } from '../../client/interpreter/locators/services/condaEnvService'; import { @@ -337,6 +341,7 @@ import { MockWorkspaceConfiguration } from './mockWorkspaceConfig'; import { blurWindow, createMessageEvent } from './reactHelpers'; import { TestInteractiveWindowProvider } from './testInteractiveWindowProvider'; import { TestNativeEditorProvider } from './testNativeEditorProvider'; +import { TestPersistentStateFactory } from './testPersistentStateFactory'; export class DataScienceIocContainer extends UnitTestIocContainer { public webPanelListener: IWebPanelMessageListener | undefined; @@ -923,7 +928,19 @@ export class DataScienceIocContainer extends UnitTestIocContainer { IInterpreterVersionService, InterpreterVersionService ); - this.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); + + const globalStorage = this.serviceManager.get(IMemento, GLOBAL_MEMENTO); + const localStorage = this.serviceManager.get(IMemento, WORKSPACE_MEMENTO); + + // Create a custom persistent state factory that remembers specific things between tests + this.serviceManager.addSingletonInstance( + IPersistentStateFactory, + new TestPersistentStateFactory(globalStorage, localStorage) + ); + + // Inform the cacheable locator service to use a static map so that it stays in memory in between tests + CacheableLocatorPromiseCache.forceUseStatic(); + this.serviceManager.addSingletonInstance(IInterpreterDisplay, interpreterDisplay.object); this.serviceManager.addSingleton( diff --git a/src/test/datascience/testPersistentStateFactory.ts b/src/test/datascience/testPersistentStateFactory.ts new file mode 100644 index 000000000000..53743a202de1 --- /dev/null +++ b/src/test/datascience/testPersistentStateFactory.ts @@ -0,0 +1,53 @@ +import { Memento } from 'vscode'; +import { PersistentStateFactory } from '../../client/common/persistentState'; +import { IPersistentState, IPersistentStateFactory } from '../../client/common/types'; + +const PrefixesToStore = ['INTERPRETERS_CACHE']; + +// tslint:disable-next-line: no-any +const persistedState = new Map(); + +class TestPersistentState implements IPersistentState { + constructor(private key: string, defaultValue?: T | undefined) { + if (defaultValue) { + persistedState.set(key, defaultValue); + } + } + public get value(): T { + return persistedState.get(this.key); + } + public async updateValue(value: T): Promise { + persistedState.set(this.key, value); + } +} + +// This class is used to make certain values persist across tests. +export class TestPersistentStateFactory implements IPersistentStateFactory { + private realStateFactory: PersistentStateFactory; + constructor(globalState: Memento, localState: Memento) { + this.realStateFactory = new PersistentStateFactory(globalState, localState); + } + + public createGlobalPersistentState( + key: string, + defaultValue?: T | undefined, + expiryDurationMs?: number | undefined + ): IPersistentState { + if (PrefixesToStore.find(p => key.startsWith(p))) { + return new TestPersistentState(key, defaultValue); + } + + return this.realStateFactory.createGlobalPersistentState(key, defaultValue, expiryDurationMs); + } + public createWorkspacePersistentState( + key: string, + defaultValue?: T | undefined, + expiryDurationMs?: number | undefined + ): IPersistentState { + if (PrefixesToStore.find(p => key.startsWith(p))) { + return new TestPersistentState(key, defaultValue); + } + + return this.realStateFactory.createWorkspacePersistentState(key, defaultValue, expiryDurationMs); + } +}