diff --git a/.vscode/launch.json b/.vscode/launch.json index 39f2c4888690..d993f2622cf5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -240,8 +240,9 @@ "env": { // Remove `X` prefix to test with real python (for DS functional tests). "XVSCODE_PYTHON_ROLLING": "1", + // Remove 'X' to turn on all logging in the debug output + "XVSC_PYTHON_FORCE_LOGGING": "1", // Remove `X` prefix and update path to test with real python interpreter (for DS functional tests). - // Do not use a conda environment (as it needs to be activated and the like). "XCI_PYTHON_PATH": "" }, "outFiles": [ diff --git a/build/ci/conda_base.yml b/build/ci/conda_base.yml new file mode 100644 index 000000000000..171868c9b879 --- /dev/null +++ b/build/ci/conda_base.yml @@ -0,0 +1,4 @@ +pandas +jupyter +numpy +matplotlib \ No newline at end of file diff --git a/build/ci/conda_env_1.yml b/build/ci/conda_env_1.yml new file mode 100644 index 000000000000..7949c0f767f8 --- /dev/null +++ b/build/ci/conda_env_1.yml @@ -0,0 +1,7 @@ +name: conda_env_1 +dependencies: + - python=3.7 + - pandas + - jupyter + - numpy + - matplotlib \ No newline at end of file diff --git a/build/ci/conda_env_2.yml b/build/ci/conda_env_2.yml new file mode 100644 index 000000000000..6c1fb6a0d335 --- /dev/null +++ b/build/ci/conda_env_2.yml @@ -0,0 +1,7 @@ +name: conda_env_2 +dependencies: + - python=3.8 + - pandas + - jupyter + - numpy + - matplotlib \ No newline at end of file diff --git a/build/ci/templates/test_phases.yml b/build/ci/templates/test_phases.yml index 5a407d9dee10..558aeafc0c95 100644 --- a/build/ci/templates/test_phases.yml +++ b/build/ci/templates/test_phases.yml @@ -97,16 +97,7 @@ steps: python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python/debugpy/no_wheels --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy displayName: 'pip install system test requirements' condition: and(succeeded(), eq(variables['NeedsPythonTestReqs'], 'true')) - - # Install the additional sqlite requirements - # - # This task will only run if variable `NeedsPythonFunctionalReqs` is true. - - bash: | - sudo apt-get install libsqlite3-dev - python -m pip install pysqlite - displayName: 'Setup python to run with sqlite on 2.7' - condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true'), eq(variables['Agent.Os'], 'Linux'), eq(variables['PythonVersion'], '2.7')) - + # Install the requirements for functional tests. # # This task will only run if variable `NeedsPythonFunctionalReqs` is true. @@ -118,9 +109,69 @@ steps: - bash: | python -m pip install numpy python -m pip install --upgrade -r ./build/functional-test-requirements.txt + python -c "import sys;print(sys.executable)" displayName: 'pip install functional requirements' + condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true')) + + # Add CONDA to the path so anaconda works + # + # This task will only run if variable `NeedsPythonFunctionalReqs` is true. + - bash: | + echo "##vso[task.prependpath]$CONDA/bin" + displayName: 'Add conda to the path' + condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true'), ne(variables['Agent.Os'], 'Windows_NT')) + + # Add CONDA to the path so anaconda works (windows) + # + # This task will only run if variable `NeedsPythonFunctionalReqs` is true. + - powershell: | + Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" + displayName: 'Add conda to the path' + condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true'), eq(variables['Agent.Os'], 'Windows_NT')) + + # On MAC let CONDA update install paths + # + # This task will only run if variable `NeedsPythonFunctionalReqs` is true. + - bash: | + sudo chown -R $USER $CONDA + displayName: 'Give CONDA permission to its own files' + condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true'), eq(variables['Agent.Os'], 'Darwin')) + + # Create the two anaconda environments + # + # This task will only run if variable `NeedsPythonFunctionalReqs` is true. + # + - script: | + conda env create --quiet --force --file build/ci/conda_env_1.yml + conda env create --quiet --force --file build/ci/conda_env_2.yml + displayName: 'Create CONDA Environments' condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true')) + # Run the pip installs in the 3 environments (darwin linux) + - bash: | + source activate base + conda install --quiet -y --file build/ci/conda_base.yml + python -m pip install --upgrade -r build/conda-functional-requirements.txt + source activate conda_env_1 + python -m pip install --upgrade -r build/conda-functional-requirements.txt + source activate conda_env_2 + python -m pip install --upgrade -r build/conda-functional-requirements.txt + conda deactivate + displayName: 'Install Pip requirements for CONDA envs' + condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true'), ne(variables['Agent.Os'], 'Windows_NT')) + + # Run the pip installs in the 3 environments (windows) + - script: | + call activate base + conda install --quiet -y --file build/ci/conda_base.yml + python -m pip install --upgrade -r build/conda-functional-requirements.txt + call activate conda_env_1 + python -m pip install --upgrade -r build/conda-functional-requirements.txt + call activate conda_env_2 + python -m pip install --upgrade -r build/conda-functional-requirements.txt + displayName: 'Install Pip requirements for CONDA envs' + condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true'), eq(variables['Agent.Os'], 'Windows_NT')) + # Downgrade pywin32 on Windows due to bug https://github.com/jupyter/notebook/issues/4909 # # This task will only run if variable `NeedsPythonFunctionalReqs` is true. diff --git a/build/ci/vscode-python-nightly-flake-ci.yaml b/build/ci/vscode-python-nightly-flake-ci.yaml index d9a749d90e59..99b952aa4191 100644 --- a/build/ci/vscode-python-nightly-flake-ci.yaml +++ b/build/ci/vscode-python-nightly-flake-ci.yaml @@ -50,22 +50,6 @@ stages: 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 @@ -85,22 +69,6 @@ stages: 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: '$(vmImageMacOS)' - steps: - - template: templates/test_phases.yml - - stage: Windows dependsOn: - Build @@ -119,19 +87,3 @@ stages: 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 diff --git a/build/conda-functional-requirements.txt b/build/conda-functional-requirements.txt new file mode 100644 index 000000000000..7f11f53981a7 --- /dev/null +++ b/build/conda-functional-requirements.txt @@ -0,0 +1,19 @@ +# List of requirements for conda environments that cannot be installed using conda +livelossplot +versioneer +flake8 +autopep8 +bandit +black ; python_version>='3.6' +yapf +pylint +pycodestyle +prospector +pydocstyle +nose +pytest==4.6.9 # Last version of pytest with Python 2.7 support +rope +flask +django +isort +pathlib2>=2.2.0 ; python_version<'3.6' # Python 2.7 compatibility (pytest) diff --git a/build/functional-test-requirements.txt b/build/functional-test-requirements.txt index dce60486ae64..d2f1977a7be4 100644 --- a/build/functional-test-requirements.txt +++ b/build/functional-test-requirements.txt @@ -1,7 +1,2 @@ -# List of requirements for functional tests -versioneer -jupyter -numpy -matplotlib -pandas -livelossplot +# List of requirements for functional tests +versioneer diff --git a/news/3 Code Health/10134.md b/news/3 Code Health/10134.md new file mode 100644 index 000000000000..4030f2477b91 --- /dev/null +++ b/news/3 Code Health/10134.md @@ -0,0 +1 @@ +Add conda environments to nightly test runs \ No newline at end of file diff --git a/src/client/common/asyncDisposableRegistry.ts b/src/client/common/asyncDisposableRegistry.ts index 0b4f18af81b7..590727a445f1 100644 --- a/src/client/common/asyncDisposableRegistry.ts +++ b/src/client/common/asyncDisposableRegistry.ts @@ -12,6 +12,7 @@ export class AsyncDisposableRegistry implements IAsyncDisposableRegistry { public async dispose(): Promise { const promises = this._list.map(l => l.dispose()); await Promise.all(promises); + this._list = []; } public push(disposable?: IDisposable | IAsyncDisposable) { diff --git a/src/client/common/process/proc.ts b/src/client/common/process/proc.ts index bfe2d496d7c4..89b5d4a8b05b 100644 --- a/src/client/common/process/proc.ts +++ b/src/client/common/process/proc.ts @@ -62,7 +62,8 @@ export class ProcessService extends EventEmitter implements IProcessService { const proc = spawn(file, args, spawnOptions); let procExited = false; const disposable: IDisposable = { - dispose: () => { + // tslint:disable-next-line: no-function-expression + dispose: function() { if (proc && !proc.killed && !procExited) { ProcessService.kill(proc.pid); } diff --git a/src/client/common/process/pythonDaemon.ts b/src/client/common/process/pythonDaemon.ts index 168a5ae9739e..11117f104fbe 100644 --- a/src/client/common/process/pythonDaemon.ts +++ b/src/client/common/process/pythonDaemon.ts @@ -66,6 +66,7 @@ export class PythonDaemonExecutionService implements IPythonDaemonExecutionServi try { // The daemon should die as a result of this. this.connection.sendNotification(new NotificationType('exit')); + this.proc.kill(); } catch { noop(); } diff --git a/src/client/common/process/pythonDaemonPool.ts b/src/client/common/process/pythonDaemonPool.ts index 1248a0d8756a..e8488900b930 100644 --- a/src/client/common/process/pythonDaemonPool.ts +++ b/src/client/common/process/pythonDaemonPool.ts @@ -38,6 +38,7 @@ export class PythonDaemonExecutionServicePool implements IPythonDaemonExecutionS private readonly observableDaemons: IPythonDaemonExecutionService[] = []; private readonly envVariables: NodeJS.ProcessEnv; private readonly pythonPath: string; + private _disposed = false; constructor( private readonly logger: IProcessLogger, private readonly disposables: IDisposableRegistry, @@ -67,6 +68,7 @@ export class PythonDaemonExecutionServicePool implements IPythonDaemonExecutionS // Always ignore warnings as the user should never see the output of the daemon running this.envVariables[PYTHON_WARNINGS] = 'ignore'; + this.disposables.push(this); } public async initialize() { const promises = Promise.all( @@ -85,7 +87,7 @@ export class PythonDaemonExecutionServicePool implements IPythonDaemonExecutionS await Promise.all([promises, promises2]); } public dispose() { - noop(); + this._disposed = true; } public async getInterpreterInformation(): Promise { const msg = { args: ['GetPythonVersion'] }; @@ -231,7 +233,7 @@ export class PythonDaemonExecutionServicePool implements IPythonDaemonExecutionS completed = true; if (!daemonProc || (!daemonProc.killed && ProcessService.isAlive(daemonProc.pid))) { this.pushDaemonIntoPool('ObservableDaemon', execService); - } else { + } else if (!this._disposed) { // Possible daemon is dead (explicitly killed or died due to some error). this.addDaemonService('ObservableDaemon').ignoreErrors(); } diff --git a/src/client/common/terminal/helper.ts b/src/client/common/terminal/helper.ts index 5d6ff1ad06f5..b56be21ed664 100644 --- a/src/client/common/terminal/helper.ts +++ b/src/client/common/terminal/helper.ts @@ -28,7 +28,7 @@ export class TerminalHelper implements ITerminalHelper { @inject(IPlatformService) private readonly platform: IPlatformService, @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, @inject(ICondaService) private readonly condaService: ICondaService, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IInterpreterService) readonly interpreterService: IInterpreterService, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, @inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.conda) @@ -71,9 +71,9 @@ export class TerminalHelper implements ITerminalHelper { const providers = [this.pipenv, this.pyenv, this.bashCShellFish, this.commandPromptAndPowerShell]; const promise = this.getActivationCommands(resource || undefined, interpreter, terminalShellType, providers); this.sendTelemetry( - resource, terminalShellType, EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL, + interpreter, promise ).ignoreErrors(); return promise; @@ -89,22 +89,21 @@ export class TerminalHelper implements ITerminalHelper { const providers = [this.bashCShellFish, this.commandPromptAndPowerShell]; const promise = this.getActivationCommands(resource, interpreter, shell, providers); this.sendTelemetry( - resource, shell, EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE, + interpreter, promise ).ignoreErrors(); return promise; } @traceDecorators.error('Failed to capture telemetry') protected async sendTelemetry( - resource: Resource, terminalShellType: TerminalShellType, eventName: EventName, + interpreter: PythonInterpreter | undefined, promise: Promise ): Promise { let hasCommands = false; - const interpreter = await this.interpreterService.getActiveInterpreter(resource); let failed = false; try { const cmds = await promise; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index c8ec93ed0633..b7d91a2cbc35 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -27,7 +27,7 @@ export interface IOutputChannel extends OutputChannel {} export const IDocumentSymbolProvider = Symbol('IDocumentSymbolProvider'); export interface IDocumentSymbolProvider extends DocumentSymbolProvider {} export const IsWindows = Symbol('IS_WINDOWS'); -export const IDisposableRegistry = Symbol('IDiposableRegistry'); +export const IDisposableRegistry = Symbol('IDisposableRegistry'); export type IDisposableRegistry = { push(disposable: Disposable): void }; export const IMemento = Symbol('IGlobalMemento'); export const GLOBAL_MEMENTO = Symbol('IGlobalMemento'); diff --git a/src/client/datascience/interactive-common/interactiveBase.ts b/src/client/datascience/interactive-common/interactiveBase.ts index f41064cfb1a6..cadd8538381f 100644 --- a/src/client/datascience/interactive-common/interactiveBase.ts +++ b/src/client/datascience/interactive-common/interactiveBase.ts @@ -326,6 +326,13 @@ export abstract class InteractiveBase extends WebViewHost l.dispose()); this.updateContexts(undefined); + + // When closing an editor, dispose of the notebook associated with it. + // This won't work when we have multiple views of the notebook though. Notebook ownership + // should probably move to whatever owns the backing model. + return this.notebook?.dispose().then(() => { + this._notebook = undefined; + }); } public startProgress() { @@ -1412,7 +1419,12 @@ export abstract class InteractiveBase extends WebViewHost { // Fire our event this.closedEvent.fire(this); - - // Restart our kernel so that execution counts are reset - let oldAsk: boolean | undefined = false; - const settings = this.configuration.getSettings(await this.getOwningResource()); - if (settings && settings.datascience) { - oldAsk = settings.datascience.askForKernelRestart; - settings.datascience.askForKernelRestart = false; - } - await this.restartKernel(true); - if (oldAsk && settings && settings.datascience) { - settings.datascience.askForKernelRestart = true; - } } protected saveAll() { diff --git a/src/client/datascience/interactive-window/interactiveWindow.ts b/src/client/datascience/interactive-window/interactiveWindow.ts index dd58caf104ae..b628cc6138a7 100644 --- a/src/client/datascience/interactive-window/interactiveWindow.ts +++ b/src/client/datascience/interactive-window/interactiveWindow.ts @@ -153,10 +153,11 @@ export class InteractiveWindow extends InteractiveBase implements IInteractiveWi } public dispose() { - super.dispose(); + const promise = super.dispose(); if (this.closedEvent) { this.closedEvent.fire(this); } + return promise; } public addMessage(message: string): Promise { @@ -272,7 +273,7 @@ export class InteractiveWindow extends InteractiveBase implements IInteractiveWi this.postMessage(InteractiveWindowMessages.ScrollToCell, { id }).ignoreErrors(); } - protected async getOwningResource(): Promise { + public async getOwningResource(): Promise { if (this.lastFile) { return Uri.file(this.lastFile); } diff --git a/src/client/datascience/jupyter/interpreter/jupyterInterpreterService.ts b/src/client/datascience/jupyter/interpreter/jupyterInterpreterService.ts index aec8156a216d..bff29827eb28 100644 --- a/src/client/datascience/jupyter/interpreter/jupyterInterpreterService.ts +++ b/src/client/datascience/jupyter/interpreter/jupyterInterpreterService.ts @@ -139,6 +139,15 @@ export class JupyterInterpreterService { } } + // Set the specified interpreter as our current selected interpreter. Public so can + // be set by the test code. + public async setAsSelectedInterpreter(interpreter: PythonInterpreter): Promise { + // Make sure that our initial set has happened before we allow a set so that + // calculation of the initial interpreter doesn't clobber the existing one + await this.setInitialInterpreter(); + this.changeSelectedInterpreterProperty(interpreter); + } + // Check the location that we stored jupyter launch path in the old version // if it's there, return it and clear the location private getInterpreterFromChangeOfOlderVersionOfExtension(): string | undefined { @@ -152,14 +161,6 @@ export class JupyterInterpreterService { return pythonPath; } - // Set the specified interpreter as our current selected interpreter - private async setAsSelectedInterpreter(interpreter: PythonInterpreter): Promise { - // Make sure that our initial set has happened before we allow a set so that - // calculation of the initial interpreter doesn't clobber the existing one - await this.setInitialInterpreter(); - this.changeSelectedInterpreterProperty(interpreter); - } - private changeSelectedInterpreterProperty(interpreter: PythonInterpreter) { this._selectedInterpreter = interpreter; this._onDidChangeInterpreter.fire(interpreter); diff --git a/src/client/datascience/jupyter/jupyterExecution.ts b/src/client/datascience/jupyter/jupyterExecution.ts index 2c884917387c..56bce4404885 100644 --- a/src/client/datascience/jupyter/jupyterExecution.ts +++ b/src/client/datascience/jupyter/jupyterExecution.ts @@ -181,6 +181,7 @@ export class JupyterExecutionBase implements IJupyterExecution { options?.metadata, cancelToken ); + await sessionManager.dispose(); } // If no kernel and not going to pick one, exit early diff --git a/src/client/datascience/jupyter/jupyterServer.ts b/src/client/datascience/jupyter/jupyterServer.ts index 49f4fad38caa..02df56fff004 100644 --- a/src/client/datascience/jupyter/jupyterServer.ts +++ b/src/client/datascience/jupyter/jupyterServer.ts @@ -68,9 +68,13 @@ export class JupyterServerBase implements INotebookServer { // Listen to the process going down if (this.launchInfo && this.launchInfo.connectionInfo) { this.connectionInfoDisconnectHandler = this.launchInfo.connectionInfo.disconnected(c => { - traceError(localize.DataScience.jupyterServerCrashed().format(c.toString())); - this.serverExitCode = c; - this.shutdown().ignoreErrors(); + try { + this.serverExitCode = c; + traceError(localize.DataScience.jupyterServerCrashed().format(c.toString())); + this.shutdown().ignoreErrors(); + } catch { + noop(); + } }); } diff --git a/src/client/datascience/jupyter/jupyterSession.ts b/src/client/datascience/jupyter/jupyterSession.ts index 1ab797a1e439..be26ef274e02 100644 --- a/src/client/datascience/jupyter/jupyterSession.ts +++ b/src/client/datascience/jupyter/jupyterSession.ts @@ -137,6 +137,12 @@ export class JupyterSession implements IJupyterSession { await this.session.kernel.restart(); return; } + + // Start the restart session now in case it wasn't started + if (!this.restartSessionPromise) { + this.startRestartSession(); + } + // Just kill the current session and switch to the other if (this.restartSessionPromise && this.session && this.sessionManager && this.contentsManager) { traceInfo(`Restarting ${this.session.kernel.id}`); @@ -196,7 +202,7 @@ export class JupyterSession implements IJupyterSession { : undefined; // It has been observed that starting the restart session slows down first time to execute a cell. // Solution is to start the restart session after the first execution of user code. - if (!content.silent && result) { + if (!content.silent && result && !isTestExecution()) { result.done.finally(() => this.startRestartSession()).ignoreErrors(); } return result; @@ -337,6 +343,8 @@ export class JupyterSession implements IJupyterSession { resolve(); } else if (e === 'dead') { traceError('Kernel died while waiting for idle'); + // If we throw an exception, make sure to shutdown the session as it's not usable anymore + this.shutdownSession(session, this.statusHandler).ignoreErrors(); reject( new JupyterInvalidKernelError({ ...session.kernel, @@ -377,6 +385,8 @@ export class JupyterSession implements IJupyterSession { return; } + // If we throw an exception, make sure to shutdown the session as it's not usable anymore + this.shutdownSession(session, this.statusHandler).ignoreErrors(); throw new JupyterWaitForIdleError(localize.DataScience.jupyterLaunchTimedOut()); } } @@ -432,7 +442,7 @@ export class JupyterSession implements IJupyterSession { this.sessionManager!.startNew(options) .then(s => { this.logRemoteOutput( - localize.DataScience.createdNewKernel().format(this.connInfo.baseUrl, s.kernel.id) + localize.DataScience.createdNewKernel().format(this.connInfo.baseUrl, s?.kernel?.id) ); return s; }) diff --git a/src/client/datascience/jupyter/jupyterSessionManager.ts b/src/client/datascience/jupyter/jupyterSessionManager.ts index a2faf3eb0779..c9ab3091006c 100644 --- a/src/client/datascience/jupyter/jupyterSessionManager.ts +++ b/src/client/datascience/jupyter/jupyterSessionManager.ts @@ -8,6 +8,7 @@ import { CancellationToken } from 'vscode-jsonrpc'; import { traceInfo } from '../../common/logger'; import { IConfigurationService, IOutputChannel } from '../../common/types'; import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; import { IConnection, IJupyterKernel, @@ -45,8 +46,32 @@ export class JupyterSessionManager implements IJupyterSessionManager { } if (this.sessionManager && !this.sessionManager.isDisposed) { traceInfo('ShutdownSessionAndConnection - dispose session manager'); - this.sessionManager.dispose(); - this.sessionManager = undefined; + // Make sure it finishes startup. + await this.sessionManager.ready; + + // tslint:disable-next-line: no-any + const sessionManager = this.sessionManager as any; + try { + await this.sessionManager.shutdownAll(); + } finally { + this.sessionManager.dispose(); + this.sessionManager = undefined; + } + + // The session manager can actually be stuck in the context of a timer. Clear out the specs inside of + // it so the memory for the session is minimized. Otherwise functional tests can run out of memory + if (sessionManager._specs) { + sessionManager._specs = {}; + } + if (sessionManager._sessions && sessionManager._sessions.clear) { + sessionManager._sessions.clear(); + } + if (sessionManager._pollModels) { + this.clearPoll(sessionManager._pollModels); + } + if (sessionManager._pollSpecs) { + this.clearPoll(sessionManager._pollSpecs); + } } } @@ -153,6 +178,15 @@ export class JupyterSessionManager implements IJupyterSessionManager { } } + // tslint:disable-next-line: no-any + private clearPoll(poll: { _timeout: any }) { + try { + clearTimeout(poll._timeout); + } catch { + noop(); + } + } + private getSessionCookieString(pwSettings: IJupyterPasswordConnectInfo): string { return `_xsrf=${pwSettings.xsrfCookie}; ${pwSettings.sessionCookieName}=${pwSettings.sessionCookieValue}`; } diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts b/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts index 79ec86f16be3..4a571ac1635c 100644 --- a/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts +++ b/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts @@ -74,7 +74,7 @@ export class GuestJupyterServer this.dataScience.activationStartTime ); this.notebooks.set(identity.toString(), result); - const oldDispose = result.dispose; + const oldDispose = result.dispose.bind(result); result.dispose = () => { this.notebooks.delete(identity.toString()); return oldDispose(); diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts index b510f2be3e13..3932895e36c0 100644 --- a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts +++ b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts @@ -10,6 +10,7 @@ import * as vsls from 'vsls/vscode'; import { nbformat } from '@jupyterlab/coreutils'; import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../../common/application/types'; +import { isTestExecution } from '../../../common/constants'; import { traceInfo } from '../../../common/logger'; import { IFileSystem } from '../../../common/platform/types'; import { @@ -266,7 +267,7 @@ export class HostJupyterServer extends LiveShareParticipantHost(JupyterServerBas resource, sessionManager, notebookMetadata, - false, + isTestExecution(), cancelToken ) : this.kernelSelector.getKernelForRemoteConnection( @@ -279,6 +280,7 @@ export class HostJupyterServer extends LiveShareParticipantHost(JupyterServerBas const kernelInfoToUse = kernelInfo?.kernelSpec || kernelInfo?.kernelModel; if (kernelInfoToUse) { launchInfo.kernelSpec = kernelInfoToUse; + launchInfo.interpreter = resourceInterpreter; changedKernel = true; } } diff --git a/src/client/datascience/jupyter/notebookStarter.ts b/src/client/datascience/jupyter/notebookStarter.ts index f5544d4378f8..77f7c4e757ea 100644 --- a/src/client/datascience/jupyter/notebookStarter.ts +++ b/src/client/datascience/jupyter/notebookStarter.ts @@ -147,7 +147,7 @@ export class NotebookStarter implements Disposable { // Something else went wrong. See if the local proc died or not. if (exitCode !== 0) { - throw new Error(localize.DataScience.jupyterServerCrashed().format(exitCode.toString())); + throw new Error(localize.DataScience.jupyterServerCrashed().format(exitCode?.toString())); } else { throw new Error(localize.DataScience.jupyterNotebookFailure().format(err)); } diff --git a/src/client/datascience/webViewHost.ts b/src/client/datascience/webViewHost.ts index 0d265ddec2bf..a3e265197201 100644 --- a/src/client/datascience/webViewHost.ts +++ b/src/client/datascience/webViewHost.ts @@ -7,6 +7,7 @@ import { injectable, unmanaged } from 'inversify'; import { ConfigurationChangeEvent, ViewColumn, WebviewPanel, WorkspaceConfiguration } from 'vscode'; import { IWebPanel, IWebPanelMessageListener, IWebPanelProvider, IWorkspaceService } from '../common/application/types'; +import { isTestExecution } from '../common/constants'; import { traceInfo, traceWarning } from '../common/logger'; import { IConfigurationService, IDisposable, Resource } from '../common/types'; import { createDeferred, Deferred } from '../common/utils/async'; @@ -66,8 +67,9 @@ export abstract class WebViewHost implements IDisposable { // Send the first settings message this.onDataScienceSettingsChanged().ignoreErrors(); - // Send the loc strings - this.postMessageInternal(SharedMessages.LocInit, localize.getCollectionJSON()).ignoreErrors(); + // Send the loc strings (skip during testing as it takes up a lot of memory) + const locStrings = isTestExecution() ? '{}' : localize.getCollectionJSON(); + this.postMessageInternal(SharedMessages.LocInit, locStrings).ignoreErrors(); } public async show(preserveFocus: boolean): Promise { diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index 1cc5f3541f95..247b87e9ebbb 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -8,11 +8,12 @@ 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 { LogOptions, traceDecorators, traceError, traceInfo, traceVerbose } from '../../common/logger'; import { IPlatformService } from '../../common/platform/types'; -import { IProcessServiceFactory } from '../../common/process/types'; +import { ExecutionResult, IProcessServiceFactory } from '../../common/process/types'; import { ITerminalHelper, TerminalShellType } from '../../common/terminal/types'; import { ICurrentProcess, IDisposable, Resource } from '../../common/types'; +import { sleep } from '../../common/utils/async'; import { InMemoryCache } from '../../common/utils/cacheUtils'; import { OSType } from '../../common/utils/platform'; import { IEnvironmentVariablesProvider } from '../../common/variables/types'; @@ -34,10 +35,63 @@ const defaultShells = { [OSType.Unknown]: undefined }; +const condaRetryMessages = [ + 'The process cannot access the file because it is being used by another process', + 'The directory is not empty' +]; + +/** + * This class exists so that the environment variable fetching can be cached in between tests. Normally + * this cache resides in memory for the duration of the EnvironmentActivationService's lifetime, but in the case + * of our functional tests, we want the cached data to exist outside of each test (where each test will destroy the EnvironmentActivationService) + * This gives each test a 3 or 4 second speedup. + */ +export class EnvironmentActivationServiceCache { + private static useStatic = false; + private static staticMap = new Map>(); + private normalMap = new Map>(); + + public static forceUseStatic() { + EnvironmentActivationServiceCache.useStatic = true; + } + public static forceUseNormal() { + EnvironmentActivationServiceCache.useStatic = false; + } + public get(key: string): InMemoryCache | undefined { + if (EnvironmentActivationServiceCache.useStatic) { + return EnvironmentActivationServiceCache.staticMap.get(key); + } + return this.normalMap.get(key); + } + + public set(key: string, value: InMemoryCache) { + if (EnvironmentActivationServiceCache.useStatic) { + EnvironmentActivationServiceCache.staticMap.set(key, value); + } else { + this.normalMap.set(key, value); + } + } + + public delete(key: string) { + if (EnvironmentActivationServiceCache.useStatic) { + EnvironmentActivationServiceCache.staticMap.delete(key); + } else { + this.normalMap.delete(key); + } + } + + public clear() { + // Don't clear during a test as the environment isn't going to change + if (!EnvironmentActivationServiceCache.useStatic) { + this.normalMap.clear(); + } + } +} + @injectable() export class EnvironmentActivationService implements IEnvironmentActivationService, IDisposable { private readonly disposables: IDisposable[] = []; - private readonly activatedEnvVariablesCache = new Map>(); + private readonly activatedEnvVariablesCache = new EnvironmentActivationServiceCache(); constructor( @inject(ITerminalHelper) private readonly helper: ITerminalHelper, @inject(IPlatformService) private readonly platform: IPlatformService, @@ -132,18 +186,40 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi const command = `${activationCommand} && echo '${getEnvironmentPrefix}' && python ${printEnvPyFile.fileToCommandArgument()}`; traceVerbose(`Activating Environment to capture Environment variables, ${command}`); - // Conda activate can hang on certain systems. Fail after 30 seconds. + // Do some wrapping of the call. For two reasons: + // 1) Conda activate can hang on certain systems. Fail after 30 seconds. // See the discussion from hidesoon in this issue: https://github.com/Microsoft/vscode-python/issues/4424 // His issue is conda never finishing during activate. This is a conda issue, but we // should at least tell the user. - const result = await processService.shellExec(command, { - env, - shell: shellInfo.shell, - timeout: getEnvironmentTimeout, - maxBuffer: 1000 * 1000 - }); - if (result.stderr && result.stderr.length > 0) { - throw new Error(`StdErr from ShellExec, ${result.stderr}`); + // 2) Retry because of this issue here: https://github.com/microsoft/vscode-python/issues/9244 + // This happens on AzDo machines a bunch when using Conda (and we can't dictate the conda version in order to get the fix) + let result: ExecutionResult | undefined; + let tryCount = 1; + while (!result) { + try { + result = await processService.shellExec(command, { + env, + shell: shellInfo.shell, + timeout: getEnvironmentTimeout, + maxBuffer: 1000 * 1000, + throwOnStdErr: false + }); + if (result.stderr && result.stderr.length > 0) { + throw new Error(`StdErr from ShellExec, ${result.stderr} for ${command}`); + } + } catch (exc) { + // Special case. Conda for some versions will state a file is in use. If + // that's the case, wait and try again. This happens especially on AzDo + const excString = exc.toString(); + if (condaRetryMessages.find(m => excString.includes(m)) && tryCount < 10) { + traceInfo(`Conda is busy, attempting to retry ...`); + result = undefined; + tryCount += 1; + await sleep(500); + } else { + throw exc; + } + } } const returnedEnv = this.parseEnvironmentOutput(result.stdout); diff --git a/src/client/interpreter/locators/services/cacheableLocatorService.ts b/src/client/interpreter/locators/services/cacheableLocatorService.ts index f2608fed6a34..d30625dc81f2 100644 --- a/src/client/interpreter/locators/services/cacheableLocatorService.ts +++ b/src/client/interpreter/locators/services/cacheableLocatorService.ts @@ -17,6 +17,12 @@ import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { IInterpreterLocatorService, IInterpreterWatcher, PythonInterpreter } from '../../contracts'; +/** + * This class exists so that the interpreter fetching can be cached in between tests. Normally + * this cache resides in memory for the duration of the CacheableLocatorService's lifetime, but in the case + * of our functional tests, we want the cached data to exist outside of each test (where each test will destroy the CacheableLocatorService) + * This gives each test a 20 second speedup. + */ export class CacheableLocatorPromiseCache { private static useStatic = false; private static staticMap = new Map>(); @@ -25,6 +31,9 @@ export class CacheableLocatorPromiseCache { public static forceUseStatic() { CacheableLocatorPromiseCache.useStatic = true; } + public static forceUseNormal() { + CacheableLocatorPromiseCache.useStatic = false; + } public get(key: string): Deferred | undefined { if (CacheableLocatorPromiseCache.useStatic) { return CacheableLocatorPromiseCache.staticMap.get(key); diff --git a/src/client/ioc/serviceManager.ts b/src/client/ioc/serviceManager.ts index b63559ab781d..0c1bb8eb9452 100644 --- a/src/client/ioc/serviceManager.ts +++ b/src/client/ioc/serviceManager.ts @@ -121,4 +121,9 @@ export class ServiceManager implements IServiceManager { this.container.rebind(serviceIdentifier).toConstantValue(instance); } } + + public dispose() { + this.container.unbindAll(); + this.container.unload(); + } } diff --git a/src/client/ioc/types.ts b/src/client/ioc/types.ts index 31312453da75..735b86de1da5 100644 --- a/src/client/ioc/types.ts +++ b/src/client/ioc/types.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { interfaces } from 'inversify'; +import { IDisposable } from '../common/types'; //tslint:disable:callable-types // tslint:disable-next-line:interface-name @@ -25,7 +26,7 @@ export type ClassType = { export const IServiceManager = Symbol('IServiceManager'); -export interface IServiceManager { +export interface IServiceManager extends IDisposable { add( serviceIdentifier: interfaces.ServiceIdentifier, constructor: ClassType, diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index d9f3bd8eeffc..89f951e3863b 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -86,7 +86,6 @@ suite('Module Installer', () => { }); suiteTeardown(async () => { await closeActiveWindows(); - await resetSettings(); }); teardown(async () => { await ioc.dispose(); diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 8ec338021b60..d8217add53f5 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -7,7 +7,7 @@ import { interfaces } from 'inversify'; import * as os from 'os'; import * as path from 'path'; import { SemVer } from 'semver'; -import { anything, instance, mock, when } from 'ts-mockito'; +import { anything, instance, mock, reset, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { CancellationTokenSource, @@ -18,7 +18,6 @@ import { FileSystemWatcher, Memento, Uri, - WorkspaceConfiguration, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; @@ -80,9 +79,10 @@ import { IWebPanelMessageListener, IWebPanelOptions, IWebPanelProvider, - IWorkspaceService, - WebPanelMessage + IWorkspaceService } from '../../client/common/application/types'; +import { WebPanel } from '../../client/common/application/webPanels/webPanel'; +import { WebPanelProvider } from '../../client/common/application/webPanels/webPanelProvider'; import { WorkspaceService } from '../../client/common/application/workspace'; import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; import { PythonSettings } from '../../client/common/configSettings'; @@ -150,6 +150,7 @@ import { IPythonExtensionBanner, IsWindows, ProductType, + Resource, WORKSPACE_MEMENTO } from '../../client/common/types'; import { Deferred, sleep } from '../../client/common/utils/async'; @@ -254,7 +255,10 @@ import { } from '../../client/datascience/types'; import { ProtocolParser } from '../../client/debugger/debugAdapter/Common/protocolParser'; import { IProtocolParser } from '../../client/debugger/debugAdapter/types'; -import { EnvironmentActivationService } from '../../client/interpreter/activation/service'; +import { + EnvironmentActivationService, + EnvironmentActivationServiceCache +} from '../../client/interpreter/activation/service'; import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; import { InterpreterComparer } from '../../client/interpreter/configuration/interpreterComparer'; import { InterpreterSelector } from '../../client/interpreter/configuration/interpreterSelector'; @@ -294,7 +298,6 @@ import { } from '../../client/interpreter/contracts'; import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider'; import { InterpreterHelper } from '../../client/interpreter/helpers'; -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'; @@ -327,6 +330,7 @@ import { } from '../../client/interpreter/locators/services/workspaceVirtualEnvService'; import { WorkspaceVirtualEnvWatcherService } from '../../client/interpreter/locators/services/workspaceVirtualEnvWatcherService'; import { IPipEnvServiceHelper, IPythonInPathCommandProvider } from '../../client/interpreter/locators/types'; +import { registerInterpreterTypes } from '../../client/interpreter/serviceRegistry'; import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; import { LanguageServerSurveyBanner } from '../../client/languageServices/languageServerSurveyBanner'; @@ -348,27 +352,38 @@ import { MockLanguageServerAnalysisOptions } from './mockLanguageServerAnalysisO import { MockLanguageServerProxy } from './mockLanguageServerProxy'; import { MockLiveShareApi } from './mockLiveShare'; import { MockWorkspaceConfiguration } from './mockWorkspaceConfig'; +import { MockWorkspaceFolder } from './mockWorkspaceFolder'; import { blurWindow, createMessageEvent } from './reactHelpers'; import { TestInteractiveWindowProvider } from './testInteractiveWindowProvider'; import { TestNativeEditorProvider } from './testNativeEditorProvider'; import { TestPersistentStateFactory } from './testPersistentStateFactory'; export class DataScienceIocContainer extends UnitTestIocContainer { + public get workingInterpreter() { + return this.workingPython; + } + + public get workingInterpreter2() { + return this.workingPython2; + } + + public get onContextSet(): Event<{ name: string; value: boolean }> { + return this.contextSetEvent.event; + } + + public get mockJupyter(): MockJupyterManager | undefined { + return this.jupyterMock ? this.jupyterMock.getManager() : undefined; + } + private static jupyterInterpreters: PythonInterpreter[] = []; public webPanelListener: IWebPanelMessageListener | undefined; public readonly useCommandFinderForJupyterServer = false; public wrapper: ReactWrapper, React.Component> | undefined; public wrapperCreatedPromise: Deferred | undefined; public postMessage: ((ev: MessageEvent) => void) | undefined; - public mockedWorkspaceConfig!: WorkspaceConfiguration; public applicationShell!: TypeMoq.IMock; // tslint:disable-next-line:no-any public datascience!: TypeMoq.IMock; private missedMessages: any[] = []; - private pythonSettings = new (class extends PythonSettings { - public fireChangeEvent() { - this.changed.fire(); - } - })(undefined, new MockAutoSelectionService()); private commandManager: MockCommandManager = new MockCommandManager(); private setContexts: Record = {}; private contextSetEvent: EventEmitter<{ name: string; value: boolean }> = new EventEmitter<{ @@ -401,8 +416,12 @@ export class DataScienceIocContainer extends UnitTestIocContainer { }; private extraListeners: ((m: string, p: any) => void)[] = []; - private webPanelProvider: TypeMoq.IMock | undefined; + private webPanelProvider = mock(WebPanelProvider); private settingsMap = new Map(); + private configMap = new Map(); + private emptyConfig = new MockWorkspaceConfiguration(); + private workspaceFolders: MockWorkspaceFolder[] = []; + private defaultPythonPath: string | undefined; private kernelServiceMock = mock(KernelService); constructor() { @@ -413,18 +432,6 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.asyncRegistry = new AsyncDisposableRegistry(); } - public get workingInterpreter() { - return this.workingPython; - } - - public get workingInterpreter2() { - return this.workingPython2; - } - - public get onContextSet(): Event<{ name: string; value: boolean }> { - return this.contextSetEvent.event; - } - public async dispose(): Promise { await this.asyncRegistry.dispose(); await super.dispose(); @@ -438,7 +445,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { } // Bounce this so that our editor has time to shutdown - await sleep(10); + await sleep(150); // Clear out the monaco global services. Some of these services are preventing shutdown. // tslint:disable: no-require-imports @@ -460,11 +467,39 @@ export class DataScienceIocContainer extends UnitTestIocContainer { if (config.getCSSBasedConfiguration) { config.getCSSBasedConfiguration().dispose(); } + + // Because there are outstanding promises holding onto this object, clear out everything we can + this.workspaceFolders = []; + this.settingsMap.clear(); + this.configMap.clear(); + this.setContexts = {}; + this.extraListeners = []; + this.webPanelListener = undefined; + reset(this.webPanelProvider); + + // Turn off the static maps for the environment and conda services. Otherwise this + // can mess up tests that don't depend upon them + CacheableLocatorPromiseCache.forceUseNormal(); + EnvironmentActivationServiceCache.forceUseNormal(); } //tslint:disable:max-func-body-length public registerDataScienceTypes(useCustomEditor: boolean = false) { - const testWorkspaceFolder = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); + // Inform the cacheable locator service to use a static map so that it stays in memory in between tests + CacheableLocatorPromiseCache.forceUseStatic(); + + // Do the same thing for the environment variable activation service. + EnvironmentActivationServiceCache.forceUseStatic(); + + // Make sure the default python path is set. + this.defaultPythonPath = this.findPythonPath(); + + // Create the workspace service first as it's used to set config values. + this.createWorkspaceService(); + + // Setup our webpanel provider to create our dummy web panel + when(this.webPanelProvider.create(anything())).thenCall(this.onCreateWebPanel.bind(this)); + this.serviceManager.addSingletonInstance(IWebPanelProvider, instance(this.webPanelProvider)); this.registerFileSystemTypes(); this.serviceManager.rebindInstance(IFileSystem, new MockFileSystem()); @@ -488,18 +523,10 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.addSingleton(IThemeFinder, ThemeFinder); this.serviceManager.addSingleton(ICodeCssGenerator, CodeCssGenerator); this.serviceManager.addSingleton(IStatusProvider, StatusProvider); - this.serviceManager.add( - IKnownSearchPathsForInterpreters, - KnownSearchPathsForInterpreters - ); this.serviceManager.addSingletonInstance( IAsyncDisposableRegistry, this.asyncRegistry ); - this.serviceManager.addSingleton( - IPythonInPathCommandProvider, - PythonInPathCommandProvider - ); this.serviceManager.addSingleton( IEnvironmentActivationService, EnvironmentActivationService @@ -581,7 +608,6 @@ export class DataScienceIocContainer extends UnitTestIocContainer { TerminalActivationProviders.pipenv ); this.serviceManager.addSingleton(ITerminalManager, TerminalManager); - this.serviceManager.addSingleton(IPipEnvServiceHelper, PipEnvServiceHelper); //const configuration = this.serviceManager.get(IConfigurationService); //const pythonSettings = configuration.getSettings(); @@ -634,13 +660,6 @@ export class DataScienceIocContainer extends UnitTestIocContainer { IInteractiveWindowListener ]); this.serviceManager.addSingleton(IShellDetector, TerminalNameShellDetector); - this.serviceManager.addSingleton( - InterpeterHashProviderFactory, - InterpeterHashProviderFactory - ); - this.serviceManager.addSingleton(WindowsStoreInterpreter, WindowsStoreInterpreter); - this.serviceManager.addSingleton(InterpreterHashProvider, InterpreterHashProvider); - this.serviceManager.addSingleton(InterpreterFilter, InterpreterFilter); this.serviceManager.addSingleton(JupyterCommandFinder, JupyterCommandFinder); this.serviceManager.addSingleton( IDiagnosticsService, @@ -661,8 +680,6 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.addSingleton(KernelSelector, KernelSelector); this.serviceManager.addSingleton(KernelSelectionProvider, KernelSelectionProvider); this.serviceManager.addSingleton(KernelSwitcher, KernelSwitcher); - this.serviceManager.addSingleton(IInterpreterSelector, InterpreterSelector); - this.serviceManager.addSingleton(IShebangCodeLensProvider, ShebangCodeLensProvider); this.serviceManager.addSingleton(IProductService, ProductService); this.serviceManager.addSingleton( IProductPathService, @@ -751,130 +768,24 @@ export class DataScienceIocContainer extends UnitTestIocContainer { }); this.serviceManager.addSingletonInstance(ICommandManager, this.commandManager); - // Also setup a mock execution service and interpreter service + // Mock the app shell const appShell = (this.applicationShell = TypeMoq.Mock.ofType()); - // const workspaceService = TypeMoq.Mock.ofType(); - const workspaceService = mock(WorkspaceService); const configurationService = TypeMoq.Mock.ofType(); - const interpreterDisplay = TypeMoq.Mock.ofType(); this.datascience = TypeMoq.Mock.ofType(); - // Setup default settings - this.pythonSettings.datascience = { - allowImportFromNotebook: true, - jupyterLaunchTimeout: 60000, - jupyterLaunchRetries: 3, - enabled: true, - jupyterServerURI: 'local', - // tslint:disable-next-line: no-invalid-template-strings - notebookFileRoot: '${fileDirname}', - changeDirOnImportExport: false, - useDefaultConfigForJupyter: true, - jupyterInterruptTimeout: 10000, - searchForJupyter: true, - showCellInputCode: true, - collapseCellInputCodeByDefault: true, - allowInput: true, - maxOutputSize: 400, - errorBackgroundColor: '#FFFFFF', - sendSelectionToInteractiveWindow: false, - codeRegularExpression: '^(#\\s*%%|#\\s*\\|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', - markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\)', - variableExplorerExclude: 'module;function;builtin_function_or_method', - liveShareConnectionTimeout: 100, - enablePlotViewer: true, - stopOnFirstLineWhileDebugging: true, - stopOnError: true, - addGotoCodeLenses: true, - enableCellCodeLens: true, - runStartupCommands: '', - debugJustMyCode: true, - variableQueries: [], - jupyterCommandLineArguments: [] - }; - this.pythonSettings.jediEnabled = false; - this.pythonSettings.downloadLanguageServer = false; - - const workspaceConfig = (this.mockedWorkspaceConfig = mock(MockWorkspaceConfiguration)); configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(this.getSettings.bind(this)); - when(workspaceConfig.get(anything(), anything())).thenCall((_, defaultValue) => defaultValue); - when(workspaceConfig.has(anything())).thenReturn(false); - when((workspaceConfig as any).then).thenReturn(undefined); - when(workspaceService.getConfiguration(anything())).thenReturn(instance(workspaceConfig)); - when(workspaceService.getConfiguration(anything(), anything())).thenReturn(instance(workspaceConfig)); - when(workspaceService.onDidChangeConfiguration).thenReturn(this.configChangeEvent.event); - when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(this.worksaceFoldersChangedEvent.event); - interpreterDisplay.setup(i => i.refresh(TypeMoq.It.isAny())).returns(() => Promise.resolve()); const startTime = Date.now(); this.datascience.setup(d => d.activationStartTime).returns(() => startTime); - class MockFileSystemWatcher implements FileSystemWatcher { - public ignoreCreateEvents: boolean = false; - public ignoreChangeEvents: boolean = false; - public ignoreDeleteEvents: boolean = false; - //tslint:disable-next-line:no-any - public onDidChange(_listener: (e: Uri) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { - return { dispose: noop }; - } - //tslint:disable-next-line:no-any - public onDidDelete(_listener: (e: Uri) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { - return { dispose: noop }; - } - //tslint:disable-next-line:no-any - public onDidCreate(_listener: (e: Uri) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { - return { dispose: noop }; - } - public dispose() { - noop(); - } - } - when(workspaceService.createFileSystemWatcher(anything(), anything(), anything(), anything())).thenReturn( - new MockFileSystemWatcher() - ); - when(workspaceService.createFileSystemWatcher(anything())).thenReturn(new MockFileSystemWatcher()); - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - const workspaceFolder = this.createMoqWorkspaceFolder(testWorkspaceFolder); - when(workspaceService.workspaceFolders).thenReturn([workspaceFolder]); - when(workspaceService.rootPath).thenReturn('~'); - - // Look on the path for python - const pythonPath = this.findPythonPath(); - - this.pythonSettings.pythonPath = pythonPath; - const folders = ['Envs', '.virtualenvs']; - this.pythonSettings.venvFolders = folders; - this.pythonSettings.venvPath = path.join('~', 'foo'); - this.pythonSettings.terminal = { - executeInFileDir: false, - launchArgs: [], - activateEnvironment: true, - activateEnvInCurrentTerminal: false - }; - this.serviceManager.addSingleton( IEnvironmentVariablesProvider, EnvironmentVariablesProvider ); - this.serviceManager.addSingleton( - IVirtualEnvironmentsSearchPathProvider, - GlobalVirtualEnvironmentsSearchPathProvider, - 'global' - ); - this.serviceManager.addSingleton( - IVirtualEnvironmentsSearchPathProvider, - WorkspaceVirtualEnvironmentsSearchPathProvider, - 'workspace' - ); - this.serviceManager.addSingleton( - IVirtualEnvironmentManager, - VirtualEnvironmentManager - ); this.serviceManager.addSingletonInstance(IApplicationShell, appShell.object); this.serviceManager.addSingleton(IClipboard, ClipboardService); this.serviceManager.addSingletonInstance(IDocumentManager, this.documentManager); - this.serviceManager.addSingletonInstance(IWorkspaceService, instance(workspaceService)); this.serviceManager.addSingletonInstance( IConfigurationService, configurationService.object @@ -888,75 +799,6 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.addSingleton(IPathUtils, PathUtils); this.serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); - this.serviceManager.add( - IInterpreterWatcher, - WorkspaceVirtualEnvWatcherService, - WORKSPACE_VIRTUAL_ENV_SERVICE - ); - this.serviceManager.addSingleton( - IInterpreterWatcherBuilder, - InterpreterWatcherBuilder - ); - - this.serviceManager.addSingleton( - IInterpreterLocatorService, - PythonInterpreterLocatorService, - INTERPRETER_LOCATOR_SERVICE - ); - this.serviceManager.addSingleton( - IInterpreterLocatorService, - CondaEnvFileService, - CONDA_ENV_FILE_SERVICE - ); - this.serviceManager.addSingleton( - IInterpreterLocatorService, - CondaEnvService, - CONDA_ENV_SERVICE - ); - this.serviceManager.addSingleton( - IInterpreterLocatorService, - CurrentPathService, - CURRENT_PATH_SERVICE - ); - this.serviceManager.addSingleton( - IInterpreterLocatorService, - GlobalVirtualEnvService, - GLOBAL_VIRTUAL_ENV_SERVICE - ); - this.serviceManager.addSingleton( - IInterpreterLocatorService, - WorkspaceVirtualEnvService, - WORKSPACE_VIRTUAL_ENV_SERVICE - ); - this.serviceManager.addSingleton( - IInterpreterLocatorService, - PipEnvService, - PIPENV_SERVICE - ); - this.serviceManager.addSingleton(IPipEnvService, PipEnvService); - this.serviceManager.addSingleton( - IInterpreterLocatorService, - WindowsRegistryService, - WINDOWS_REGISTRY_SERVICE - ); - - this.serviceManager.addSingleton( - IInterpreterLocatorService, - KnownPathsService, - KNOWN_PATH_SERVICE - ); - - this.serviceManager.addSingleton(IInterpreterHelper, InterpreterHelper); - this.serviceManager.addSingleton( - IInterpreterLocatorHelper, - InterpreterLocatorHelper - ); - this.serviceManager.addSingleton(IInterpreterComparer, InterpreterComparer); - this.serviceManager.addSingleton( - IInterpreterVersionService, - InterpreterVersionService - ); - const globalStorage = this.serviceManager.get(IMemento, GLOBAL_MEMENTO); const localStorage = this.serviceManager.get(IMemento, WORKSPACE_MEMENTO); @@ -966,20 +808,6 @@ export class DataScienceIocContainer extends UnitTestIocContainer { 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( - IPythonPathUpdaterServiceFactory, - PythonPathUpdaterServiceFactory - ); - this.serviceManager.addSingleton( - IPythonPathUpdaterServiceManager, - PythonPathUpdaterService - ); - const currentProcess = new CurrentProcess(); this.serviceManager.addSingletonInstance(ICurrentProcess, currentProcess); this.serviceManager.addSingleton(IRegistry, RegistryImplementation); @@ -1033,14 +861,8 @@ export class DataScienceIocContainer extends UnitTestIocContainer { ); } - // Don't use conda at all during functional tests. - const condaService = TypeMoq.Mock.ofType(); - this.serviceManager.addSingletonInstance(ICondaService, condaService.object); - condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(false)); - condaService - .setup(c => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve(false)); - condaService.setup(c => c.condaEnvironmentsFile).returns(() => undefined); + const interpreterDisplay = TypeMoq.Mock.ofType(); + interpreterDisplay.setup(i => i.refresh(TypeMoq.It.isAny())).returns(() => Promise.resolve()); // Create our jupyter mock if necessary if (this.shouldMockJupyter) { @@ -1048,12 +870,156 @@ export class DataScienceIocContainer extends UnitTestIocContainer { // When using mocked Jupyter, default to using default kernel. when(this.kernelServiceMock.searchAndRegisterKernel(anything(), anything())).thenResolve(undefined); this.serviceManager.addSingletonInstance(KernelService, instance(this.kernelServiceMock)); + + this.serviceManager.addSingleton( + InterpeterHashProviderFactory, + InterpeterHashProviderFactory + ); + this.serviceManager.addSingleton(WindowsStoreInterpreter, WindowsStoreInterpreter); + this.serviceManager.addSingleton(InterpreterHashProvider, InterpreterHashProvider); + this.serviceManager.addSingleton(InterpreterFilter, InterpreterFilter); + this.serviceManager.add( + IInterpreterWatcher, + WorkspaceVirtualEnvWatcherService, + WORKSPACE_VIRTUAL_ENV_SERVICE + ); + this.serviceManager.addSingleton( + IInterpreterWatcherBuilder, + InterpreterWatcherBuilder + ); + this.serviceManager.add( + IInterpreterWatcher, + WorkspaceVirtualEnvWatcherService, + WORKSPACE_VIRTUAL_ENV_SERVICE + ); + this.serviceManager.addSingleton( + IInterpreterWatcherBuilder, + InterpreterWatcherBuilder + ); + + this.serviceManager.addSingleton( + IInterpreterLocatorService, + PythonInterpreterLocatorService, + INTERPRETER_LOCATOR_SERVICE + ); + this.serviceManager.addSingleton( + IInterpreterLocatorService, + CondaEnvFileService, + CONDA_ENV_FILE_SERVICE + ); + this.serviceManager.addSingleton( + IInterpreterLocatorService, + CondaEnvService, + CONDA_ENV_SERVICE + ); + this.serviceManager.addSingleton( + IInterpreterLocatorService, + CurrentPathService, + CURRENT_PATH_SERVICE + ); + this.serviceManager.addSingleton( + IInterpreterLocatorService, + GlobalVirtualEnvService, + GLOBAL_VIRTUAL_ENV_SERVICE + ); + this.serviceManager.addSingleton( + IInterpreterLocatorService, + WorkspaceVirtualEnvService, + WORKSPACE_VIRTUAL_ENV_SERVICE + ); + this.serviceManager.addSingleton( + IInterpreterLocatorService, + PipEnvService, + PIPENV_SERVICE + ); + this.serviceManager.addSingleton(IPipEnvService, PipEnvService); + this.serviceManager.addSingleton( + IInterpreterLocatorService, + WindowsRegistryService, + WINDOWS_REGISTRY_SERVICE + ); + + this.serviceManager.addSingleton( + IInterpreterLocatorService, + KnownPathsService, + KNOWN_PATH_SERVICE + ); + + this.serviceManager.addSingleton(IInterpreterHelper, InterpreterHelper); + this.serviceManager.addSingleton( + IInterpreterLocatorHelper, + InterpreterLocatorHelper + ); + this.serviceManager.addSingleton(IInterpreterComparer, InterpreterComparer); + this.serviceManager.addSingleton( + IInterpreterVersionService, + InterpreterVersionService + ); + this.serviceManager.addSingleton( + IPythonInPathCommandProvider, + PythonInPathCommandProvider + ); + + this.serviceManager.addSingleton(IPipEnvServiceHelper, PipEnvServiceHelper); + this.serviceManager.addSingleton(IInterpreterSelector, InterpreterSelector); + this.serviceManager.addSingleton( + IShebangCodeLensProvider, + ShebangCodeLensProvider + ); + this.serviceManager.addSingleton( + IPythonPathUpdaterServiceFactory, + PythonPathUpdaterServiceFactory + ); + this.serviceManager.addSingleton( + IPythonPathUpdaterServiceManager, + PythonPathUpdaterService + ); + + // Don't use conda at all when mocking + const condaService = TypeMoq.Mock.ofType(); + this.serviceManager.addSingletonInstance(ICondaService, condaService.object); + condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(false)); + condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + condaService.setup(c => c.condaEnvironmentsFile).returns(() => undefined); + + this.serviceManager.addSingleton( + IVirtualEnvironmentsSearchPathProvider, + GlobalVirtualEnvironmentsSearchPathProvider, + 'global' + ); + this.serviceManager.addSingleton( + IVirtualEnvironmentsSearchPathProvider, + WorkspaceVirtualEnvironmentsSearchPathProvider, + 'workspace' + ); + this.serviceManager.addSingleton( + IVirtualEnvironmentManager, + VirtualEnvironmentManager + ); + this.serviceManager.add( + IKnownSearchPathsForInterpreters, + KnownSearchPathsForInterpreters + ); + this.serviceManager.addSingleton( + IPythonInPathCommandProvider, + PythonInPathCommandProvider + ); + this.serviceManager.addSingletonInstance( + IInterpreterDisplay, + interpreterDisplay.object + ); } else { this.serviceManager.addSingleton(IInstaller, ProductInstaller); this.serviceManager.addSingleton(KernelService, KernelService); this.serviceManager.addSingleton(IProcessServiceFactory, ProcessServiceFactory); this.serviceManager.addSingleton(IPythonExecutionFactory, PythonExecutionFactory); - this.serviceManager.addSingleton(IInterpreterService, InterpreterService); + + // Make sure full interpreter services are available. + registerInterpreterTypes(this.serviceManager); + + // Rebind the interpreter display as we don't want to use the real one + this.serviceManager.rebindInstance(IInterpreterDisplay, interpreterDisplay.object); + this.serviceManager.addSingleton( IJupyterSessionManagerFactory, JupyterSessionManagerFactory @@ -1068,11 +1034,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { } }; - appShell - .setup(a => a.showErrorMessage(TypeMoq.It.isAnyString())) - .returns(e => { - throw e; - }); + appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAnyString())).returns(() => Promise.resolve('')); appShell .setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve('')); @@ -1093,8 +1055,10 @@ export class DataScienceIocContainer extends UnitTestIocContainer { const interpreterManager = this.serviceContainer.get(IInterpreterService); interpreterManager.initialize(); - this.addInterpreter(this.workingPython2, SupportedCommands.all); - this.addInterpreter(this.workingPython, SupportedCommands.all); + if (this.mockJupyter) { + this.addInterpreter(this.workingPython2, SupportedCommands.all); + this.addInterpreter(this.workingPython, SupportedCommands.all); + } } public setFileContents(uri: Uri, contents: string) { const fileSystem = this.serviceManager.get(IFileSystem) as MockFileSystem; @@ -1106,33 +1070,29 @@ export class DataScienceIocContainer extends UnitTestIocContainer { const activationServices = this.serviceManager.getAll( IExtensionSingleActivationService ); + await Promise.all(activationServices.map(a => a.activate())); + + // Then force our interpreter to be one that supports jupyter (unless in a mock state when we don't have to) + if (!this.mockJupyter) { + const interpreterService = this.serviceManager.get(IInterpreterService); + const activeInterpreter = await interpreterService.getActiveInterpreter(); + if (!activeInterpreter || !(await this.hasJupyter(activeInterpreter))) { + const list = await this.getJupyterInterpreters(); + this.forceSettingsChanged(undefined, list[0].path); + + // Also set this as the interpreter to use for jupyter + await this.serviceManager + .get(JupyterInterpreterService) + .setAsSelectedInterpreter(list[0]); + } + } } public get kernelService() { return this.kernelServiceMock; } - public createWebPanel(): IWebPanel { - const webPanel = TypeMoq.Mock.ofType(); - webPanel - .setup(p => p.postMessage(TypeMoq.It.isAny())) - .callback((m: WebPanelMessage) => { - const message = createMessageEvent(m); - if (this.postMessage) { - this.postMessage(message); - } else { - throw new Error('postMessage callback not defined'); - } - }); - webPanel.setup(p => p.show(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - - // See https://github.com/florinn/typemoq/issues/67 for why this is necessary - webPanel.setup((p: any) => p.then).returns(() => undefined); - - return webPanel.object; - } - // tslint:disable:any public createWebView( mount: () => ReactWrapper, React.Component>, @@ -1144,54 +1104,10 @@ export class DataScienceIocContainer extends UnitTestIocContainer { liveShareTest.forceRole(role); } - if (!this.webPanelProvider) { - this.webPanelProvider = TypeMoq.Mock.ofType(); - this.serviceManager.addSingletonInstance( - IWebPanelProvider, - this.webPanelProvider.object - ); - } else { - this.webPanelProvider.reset(); - } - const webPanel = this.createWebPanel(); - - // Setup the webpanel provider so that it returns our dummy web panel. It will have to talk to our global JSDOM window so that the react components can link into it - this.webPanelProvider - .setup(p => p.create(TypeMoq.It.isAny())) - .returns((options: IWebPanelOptions) => { - // Keep track of the current listener. It listens to messages through the vscode api - this.webPanelListener = options.listener; - - // Send messages that were already posted but were missed. - // During normal operation, the react control will not be created before - // the webPanelListener - if (this.missedMessages.length && this.webPanelListener) { - // This needs to be async because we are being called in the ctor of the webpanel. It can't - // handle some messages during the ctor. - setTimeout(() => { - this.missedMessages.forEach(m => - this.webPanelListener ? this.webPanelListener.onMessage(m.type, m.payload) : noop() - ); - }, 0); - - // Note, you might think we should clean up the messages. However since the mount only occurs once, we might - // create multiple webpanels with the same mount. We need to resend these messages to - // other webpanels that get created with the same mount. - } - - // Return our dummy web panel - return Promise.resolve(webPanel); - }); // We need to mount the react control before we even create an interactive window object. Otherwise the mount will miss rendering some parts this.mountReactControl(mount); } - public createMoqWorkspaceFolder(folderPath: string) { - const folder = TypeMoq.Mock.ofType(); - folder.setup(f => f.uri).returns(() => Uri.file(folderPath)); - return folder.object; - } - public getContext(name: string): boolean { if (this.setContexts.hasOwnProperty(name)) { return this.setContexts[name]; @@ -1201,14 +1117,32 @@ export class DataScienceIocContainer extends UnitTestIocContainer { } public getSettings(resource?: Uri) { - const setting = resource ? this.settingsMap.get(resource.toString()) : this.pythonSettings; - return setting ? setting : this.pythonSettings; + const key = this.getResourceKey(resource); + let setting = this.settingsMap.get(key); + if (!setting) { + // Make sure we have the default config for this resource first. + this.getWorkspaceConfig('python', resource); + setting = new (class extends PythonSettings { + public fireChangeEvent() { + this.changed.fire(); + } + })(resource, new MockAutoSelectionService(), this.serviceManager.get(IWorkspaceService)); + this.settingsMap.set(key, setting); + } + return setting; } - public forceSettingsChanged(newPath: string, datascienceSettings?: IDataScienceSettings) { - this.pythonSettings.pythonPath = newPath; - this.pythonSettings.datascience = datascienceSettings ? datascienceSettings : this.pythonSettings.datascience; - this.pythonSettings.fireChangeEvent(); + public forceSettingsChanged(resource: Resource, newPath: string, datascienceSettings?: IDataScienceSettings) { + const settings = this.getSettings(resource); + settings.pythonPath = newPath; + settings.datascience = datascienceSettings ? datascienceSettings : settings.datascience; + + // The workspace config must be updated too as a config change event will cause the data to be reread from + // the config. + const config = this.getWorkspaceConfig('python', resource); + config.update('pythonPath', newPath).ignoreErrors(); + config.update('dataScience', settings.datascience).ignoreErrors(); + settings.fireChangeEvent(); this.configChangeEvent.fire({ affectsConfiguration(_s: string, _r?: Uri): boolean { return true; @@ -1216,24 +1150,35 @@ export class DataScienceIocContainer extends UnitTestIocContainer { }); } - public async addNewSetting(resource: Uri, pythonPath: string | undefined) { - // Force a new config setting to appear. - if (!pythonPath) { - const active = await this.get(IInterpreterService).getActiveInterpreter(undefined); - const list = await this.get(IInterpreterService).getInterpreters(undefined); + public async getJupyterCapableInterpreter(): Promise { + const list = await this.getJupyterInterpreters(); + return list ? list[0] : undefined; + } - // Should support jupyter? How to enforce this - const supportsJupyter = list.filter(l => l.path !== active?.path).filter(f => this.hasJupyter(f.path)); - pythonPath = supportsJupyter ? supportsJupyter[0].path : undefined; - } - if (pythonPath) { - const newSettings = { ...this.pythonSettings, pythonPath }; - this.settingsMap.set(resource.toString(), newSettings); + public async getJupyterInterpreters(): Promise { + // This should be cacheable as we don't install new interpreters during tests + if (DataScienceIocContainer.jupyterInterpreters.length > 0) { + return DataScienceIocContainer.jupyterInterpreters; } + const list = await this.get(IInterpreterService).getInterpreters(undefined); + const promises = list.map(f => this.hasJupyter(f).then(b => (b ? f : undefined))); + const resolved = await Promise.all(promises); + DataScienceIocContainer.jupyterInterpreters = resolved.filter(r => r) as PythonInterpreter[]; + return DataScienceIocContainer.jupyterInterpreters; } - public get mockJupyter(): MockJupyterManager | undefined { - return this.jupyterMock ? this.jupyterMock.getManager() : undefined; + public addWorkspaceFolder(folderPath: string) { + const workspaceFolder = new MockWorkspaceFolder(folderPath, this.workspaceFolders.length); + this.workspaceFolders.push(workspaceFolder); + return workspaceFolder; + } + + public addResourceToFolder(resource: Uri, folderPath: string) { + let folder = this.workspaceFolders.find(f => f.uri.fsPath === folderPath); + if (!folder) { + folder = this.addWorkspaceFolder(folderPath); + } + folder.ownedResources.add(resource.toString()); } public get(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T { @@ -1278,13 +1223,180 @@ export class DataScienceIocContainer extends UnitTestIocContainer { if (this.wrapperCreatedPromise && !this.wrapperCreatedPromise.resolved) { this.wrapperCreatedPromise.resolve(); } + + // Clear out msg payload + delete msg.payload; + } + + public getWorkspaceConfig(section: string | undefined, resource?: Resource): MockWorkspaceConfiguration { + if (!section || section !== 'python') { + return this.emptyConfig; + } + const key = this.getResourceKey(resource); + let result = this.configMap.get(key); + if (!result) { + result = this.generatePythonWorkspaceConfig(); + this.configMap.set(key, result); + } + return result; + } + + private createWebPanel(): IWebPanel { + const webPanel = mock(WebPanel); + when(webPanel.postMessage(anything())).thenCall(m => { + const message = createMessageEvent(m); + if (this.postMessage) { + this.postMessage(message); + } + if (m.payload) { + delete m.payload; + } + }); + when((webPanel as any).then).thenReturn(undefined); + return instance(webPanel); + } + + private async onCreateWebPanel(options: IWebPanelOptions) { + // Keep track of the current listener. It listens to messages through the vscode api + this.webPanelListener = options.listener; + + // Send messages that were already posted but were missed. + // During normal operation, the react control will not be created before + // the webPanelListener + if (this.missedMessages.length && this.webPanelListener) { + // This needs to be async because we are being called in the ctor of the webpanel. It can't + // handle some messages during the ctor. + setTimeout(() => { + this.missedMessages.forEach(m => + this.webPanelListener ? this.webPanelListener.onMessage(m.type, m.payload) : noop() + ); + }, 0); + + // Note, you might think we should clean up the messages. However since the mount only occurs once, we might + // create multiple webpanels with the same mount. We need to resend these messages to + // other webpanels that get created with the same mount. + } + + // Return our dummy web panel + return this.createWebPanel(); + } + + private generatePythonWorkspaceConfig(): MockWorkspaceConfiguration { + // Create a dummy settings just to setup the workspace config + const pythonSettings = new PythonSettings(undefined, new MockAutoSelectionService()); + pythonSettings.pythonPath = this.defaultPythonPath!; + pythonSettings.datascience = { + allowImportFromNotebook: true, + jupyterLaunchTimeout: 20000, + jupyterLaunchRetries: 3, + enabled: true, + jupyterServerURI: 'local', + // tslint:disable-next-line: no-invalid-template-strings + notebookFileRoot: '${fileDirname}', + changeDirOnImportExport: false, + useDefaultConfigForJupyter: true, + jupyterInterruptTimeout: 10000, + searchForJupyter: true, + showCellInputCode: true, + collapseCellInputCodeByDefault: true, + allowInput: true, + maxOutputSize: 400, + errorBackgroundColor: '#FFFFFF', + sendSelectionToInteractiveWindow: false, + codeRegularExpression: '^(#\\s*%%|#\\s*\\|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', + markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\)', + variableExplorerExclude: 'module;function;builtin_function_or_method', + liveShareConnectionTimeout: 100, + enablePlotViewer: true, + stopOnFirstLineWhileDebugging: true, + stopOnError: true, + addGotoCodeLenses: true, + enableCellCodeLens: true, + runStartupCommands: '', + debugJustMyCode: true, + variableQueries: [], + jupyterCommandLineArguments: [], + disableJupyterAutoStart: true + }; + pythonSettings.jediEnabled = false; + pythonSettings.downloadLanguageServer = false; + const folders = ['Envs', '.virtualenvs']; + pythonSettings.venvFolders = folders; + pythonSettings.venvPath = path.join('~', 'foo'); + pythonSettings.terminal = { + executeInFileDir: false, + launchArgs: [], + activateEnvironment: true, + activateEnvInCurrentTerminal: false + }; + + // Use these settings to default all of the settings in a python configuration + return new MockWorkspaceConfiguration(pythonSettings); + } + + private createWorkspaceService() { + class MockFileSystemWatcher implements FileSystemWatcher { + public ignoreCreateEvents: boolean = false; + public ignoreChangeEvents: boolean = false; + public ignoreDeleteEvents: boolean = false; + //tslint:disable-next-line:no-any + public onDidChange(_listener: (e: Uri) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { + return { dispose: noop }; + } + //tslint:disable-next-line:no-any + public onDidDelete(_listener: (e: Uri) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { + return { dispose: noop }; + } + //tslint:disable-next-line:no-any + public onDidCreate(_listener: (e: Uri) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { + return { dispose: noop }; + } + public dispose() { + noop(); + } + } + + const workspaceService = mock(WorkspaceService); + this.serviceManager.addSingletonInstance(IWorkspaceService, instance(workspaceService)); + when(workspaceService.onDidChangeConfiguration).thenReturn(this.configChangeEvent.event); + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(this.worksaceFoldersChangedEvent.event); + + // Create another config for other parts of the workspace config. + when(workspaceService.getConfiguration(anything())).thenCall(this.getWorkspaceConfig.bind(this)); + when(workspaceService.getConfiguration(anything(), anything())).thenCall(this.getWorkspaceConfig.bind(this)); + const testWorkspaceFolder = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); + + when(workspaceService.createFileSystemWatcher(anything(), anything(), anything(), anything())).thenReturn( + new MockFileSystemWatcher() + ); + when(workspaceService.createFileSystemWatcher(anything())).thenReturn(new MockFileSystemWatcher()); + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + when(workspaceService.workspaceFolders).thenReturn(this.workspaceFolders); + when(workspaceService.rootPath).thenReturn(testWorkspaceFolder); + when(workspaceService.getWorkspaceFolder(anything())).thenCall(this.getWorkspaceFolder.bind(this)); + this.addWorkspaceFolder(testWorkspaceFolder); + return workspaceService; } - private hasJupyter(pythonPath: string) { + private getWorkspaceFolder(uri: Resource): WorkspaceFolder | undefined { + if (uri) { + return this.workspaceFolders.find(w => w.ownedResources.has(uri.toString())); + } + return undefined; + } + + private getResourceKey(resource: Resource): string { + const workspace = this.serviceManager.get(IWorkspaceService); + const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource, workspace).uri; + return workspaceFolderUri ? workspaceFolderUri.fsPath : ''; + } + + private async hasJupyter(interpreter: PythonInterpreter): Promise { try { - // Try importing jupyter - const output = child_process.execFileSync(pythonPath, ['-c', 'import jupyter;'], { encoding: 'utf8' }); - return !output.includes('ModuleNotFoundError'); + const dependencyChecker = this.serviceManager.get( + JupyterInterpreterDependencyService + ); + return dependencyChecker.areDependenciesInstalled(interpreter); } catch (ex) { return false; } diff --git a/src/test/datascience/dataviewer.functional.test.tsx b/src/test/datascience/dataviewer.functional.test.tsx index 43879ef0088b..f30cb782cdf9 100644 --- a/src/test/datascience/dataviewer.functional.test.tsx +++ b/src/test/datascience/dataviewer.functional.test.tsx @@ -46,9 +46,10 @@ suite('DataScience DataViewer tests', () => { } }); - setup(() => { + setup(async () => { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(); + return ioc.activate(); }); function mountWebView(): ReactWrapper, React.Component> { diff --git a/src/test/datascience/debugger.functional.test.tsx b/src/test/datascience/debugger.functional.test.tsx index da1476094795..6d1ae82079d4 100644 --- a/src/test/datascience/debugger.functional.test.tsx +++ b/src/test/datascience/debugger.functional.test.tsx @@ -22,7 +22,7 @@ import { } from '../../client/datascience/types'; import { DataScienceIocContainer } from './dataScienceIocContainer'; import { getInteractiveCellResults, getOrCreateInteractiveWindow } from './interactiveWindowTestHelpers'; -import { getConnectionInfo, getNotebookCapableInterpreter } from './jupyterHelpers'; +import { getConnectionInfo } from './jupyterHelpers'; import { MockDebuggerService } from './mockDebugService'; import { MockDocument } from './mockDocument'; import { MockDocumentManager } from './mockDocumentManager'; @@ -54,6 +54,7 @@ suite('DataScience Debugger tests', () => { ioc = createContainer(); mockDebuggerService = ioc.serviceManager.get(IDebugService) as MockDebuggerService; processFactory = ioc.serviceManager.get(IProcessServiceFactory); + return ioc.activate(); }); teardown(async () => { @@ -223,7 +224,7 @@ suite('DataScience Debugger tests', () => { }); test('Debug remote', async () => { - const python = await getNotebookCapableInterpreter(ioc, processFactory); + const python = await ioc.getJupyterCapableInterpreter(); const procService = await processFactory.create(); if (procService && python) { diff --git a/src/test/datascience/editor-integration/gotocell.functional.test.ts b/src/test/datascience/editor-integration/gotocell.functional.test.ts index 017445514491..d1d3e01b2f03 100644 --- a/src/test/datascience/editor-integration/gotocell.functional.test.ts +++ b/src/test/datascience/editor-integration/gotocell.functional.test.ts @@ -34,13 +34,14 @@ suite('DataScience gotocell tests', () => { let documentManager: MockDocumentManager; let visibleCells: ICell[] = []; - setup(() => { + setup(async () => { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(); codeLensProvider = ioc.serviceManager.get(IDataScienceCodeLensProvider); jupyterExecution = ioc.serviceManager.get(IJupyterExecution); documentManager = ioc.serviceManager.get(IDocumentManager) as MockDocumentManager; codeLensFactory = ioc.serviceManager.get(ICodeLensFactory); + await ioc.activate(); }); teardown(async () => { diff --git a/src/test/datascience/errorHandler.functional.test.tsx b/src/test/datascience/errorHandler.functional.test.tsx index 9b1bc3d6abc1..760a5cf17dfb 100644 --- a/src/test/datascience/errorHandler.functional.test.tsx +++ b/src/test/datascience/errorHandler.functional.test.tsx @@ -18,7 +18,7 @@ suite('DataScience Error Handler Functional Tests', () => { let ioc: DataScienceIocContainer; let channels: TypeMoq.IMock; let stubbedInstallMissingDependencies: sinon.SinonStub<[(JupyterInstallError | undefined)?], Promise>; - setup(() => { + setup(async () => { stubbedInstallMissingDependencies = sinon.stub( JupyterInterpreterSubCommandExecutionService.prototype, 'installMissingDependencies' @@ -26,6 +26,7 @@ suite('DataScience Error Handler Functional Tests', () => { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(); ioc = modifyContainer(); + return ioc.activate(); }); teardown(async () => { diff --git a/src/test/datascience/intellisense.functional.test.tsx b/src/test/datascience/intellisense.functional.test.tsx index bf4a8843ff4f..4e5388e813a1 100644 --- a/src/test/datascience/intellisense.functional.test.tsx +++ b/src/test/datascience/intellisense.functional.test.tsx @@ -19,9 +19,10 @@ suite('DataScience Intellisense tests', () => { const disposables: Disposable[] = []; let ioc: DataScienceIocContainer; - setup(() => { + setup(async () => { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(); + return ioc.activate(); }); teardown(async () => { @@ -153,7 +154,7 @@ suite('DataScience Intellisense tests', () => { const interpreters = await interpreterService.getInterpreters(undefined); if (interpreters.length > 1 && oldActive) { const firstOther = interpreters.filter(i => i.path !== oldActive.path); - ioc.forceSettingsChanged(firstOther[0].path); + ioc.forceSettingsChanged(undefined, firstOther[0].path); const active = await interpreterService.getActiveInterpreter(undefined); assert.notDeepEqual(active, oldActive, 'Should have changed interpreter'); } diff --git a/src/test/datascience/interactiveWindow.functional.test.tsx b/src/test/datascience/interactiveWindow.functional.test.tsx index 611978139272..1c04ad52aad3 100644 --- a/src/test/datascience/interactiveWindow.functional.test.tsx +++ b/src/test/datascience/interactiveWindow.functional.test.tsx @@ -13,10 +13,13 @@ import { IApplicationShell, IDocumentManager } from '../../client/common/applica import { IDataScienceSettings } from '../../client/common/types'; import { createDeferred, waitForPromise } from '../../client/common/utils/async'; import { noop } from '../../client/common/utils/misc'; +import { EXTENSION_ROOT_DIR } from '../../client/constants'; import { generateCellsFromDocument } from '../../client/datascience/cellFactory'; import { EditorContexts } from '../../client/datascience/constants'; import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow'; +import { IInteractiveWindowProvider } from '../../client/datascience/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; import { concatMultilineStringInput } from '../../datascience-ui/common'; import { InteractivePanel } from '../../datascience-ui/history-react/interactivePanel'; import { ImageButton } from '../../datascience-ui/react-common/imageButton'; @@ -25,6 +28,7 @@ import { createDocument } from './editor-integration/helpers'; import { defaultDataScienceSettings } from './helpers'; import { addCode, + closeInteractiveWindow, getInteractiveCellResults, getOrCreateInteractiveWindow, runMountedTest @@ -43,6 +47,7 @@ import { findButton, getInteractiveEditor, getLastOutputCell, + mountWebView, srcDirectory, submitInput, toggleCellExpansion, @@ -53,16 +58,16 @@ import { waitForMessageResponse } from './testHelpers'; -//import { asyncDump } from '../common/asyncDump'; // tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string suite('DataScience Interactive Window output tests', () => { const disposables: Disposable[] = []; let ioc: DataScienceIocContainer; const defaultCellMarker = '# %%'; - setup(() => { + setup(async () => { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(); + return ioc.activate(); }); teardown(async () => { @@ -83,7 +88,7 @@ suite('DataScience Interactive Window output tests', () => { const window = await getOrCreateInteractiveWindow(ioc); await window.show(); const update = waitForMessage(ioc, InteractiveWindowMessages.SettingsUpdated); - ioc.forceSettingsChanged(ioc.getSettings().pythonPath, newSettings); + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath, newSettings); return update; } @@ -570,42 +575,54 @@ Type: builtin_function_or_method`, runMountedTest( 'Multiple Interpreters', - async _wrapper => { - // if (!ioc.mockJupyter) { - // const interactiveWindowProvider = ioc.get(IInteractiveWindowProvider); - // const interpreterService = ioc.get(IInterpreterService); - // const window = (await interactiveWindowProvider.getOrCreateActive()) as InteractiveWindow; - // await addCode(ioc, wrapper, 'a=1\na'); - // const activeInterpreter = await interpreterService.getActiveInterpreter( - // await window.getNotebookResource() - // ); - // verifyHtmlOnCell(wrapper, 'InteractiveCell', '1', CellPosition.Last); - // assert.equal( - // window.notebook!.getMatchingInterpreter()?.path, - // activeInterpreter?.path, - // 'Active intrepreter not used to launch notebook' - // ); - // await closeInteractiveWindow(window, wrapper); - - // // Add another python path (hopefully there's more than one on the machine?) - // const secondUri = Uri.file('bar.py'); - // await ioc.addNewSetting(secondUri, undefined); - // const newWrapper = mountWebView(ioc, 'interactive'); - // assert.ok(newWrapper, 'Could not mount a second time'); - // const newWindow = (await interactiveWindowProvider.getOrCreateActive()) as InteractiveWindow; - // await addCode(ioc, wrapper, 'a=1\na', false, secondUri); - // verifyHtmlOnCell(wrapper, 'InteractiveCell', '1', CellPosition.Last); - // assert.notEqual( - // newWindow.notebook!.getMatchingInterpreter()?.path, - // activeInterpreter?.path, - // 'Active intrepreter used to launch second notebook when it should not have' - // ); - // } else { - // tslint:disable-next-line: no-console - console.log( - 'Multiple interpreters test skipped for now. Reenable after fixing https://github.com/microsoft/vscode-python/issues/10134' - ); - // } + async (wrapper, context) => { + if (!ioc.mockJupyter) { + const interactiveWindowProvider = ioc.get(IInteractiveWindowProvider); + const interpreterService = ioc.get(IInterpreterService); + const interpreters = await ioc.getJupyterInterpreters(); + if (interpreters.length < 2) { + // tslint:disable-next-line: no-console + console.log( + 'Multiple interpreters skipped because local machine does not have more than one jupyter environment' + ); + context.skip(); + return; + } + const window = (await interactiveWindowProvider.getOrCreateActive()) as InteractiveWindow; + await addCode(ioc, wrapper, 'a=1\na'); + const activeInterpreter = await interpreterService.getActiveInterpreter( + await window.getOwningResource() + ); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '1', CellPosition.Last); + assert.equal( + window.notebook!.getMatchingInterpreter()?.path, + activeInterpreter?.path, + 'Active intrepreter not used to launch notebook' + ); + await closeInteractiveWindow(window, wrapper); + + // Add another python path + const secondUri = Uri.file('bar.py'); + ioc.addResourceToFolder(secondUri, path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience2')); + ioc.forceSettingsChanged( + secondUri, + interpreters.filter(i => i.path !== activeInterpreter?.path)[0].path + ); + + // Then open a second time and make sure it uses this new path + const newWrapper = mountWebView(ioc, 'interactive'); + assert.ok(newWrapper, 'Could not mount a second time'); + const newWindow = (await interactiveWindowProvider.getOrCreateActive()) as InteractiveWindow; + await addCode(ioc, newWrapper, 'a=1\na', false, secondUri); + assert.notEqual( + newWindow.notebook!.getMatchingInterpreter()?.path, + activeInterpreter?.path, + 'Active intrepreter used to launch second notebook when it should not have' + ); + verifyHtmlOnCell(newWrapper, 'InteractiveCell', '1', CellPosition.Last); + } else { + context.skip(); + } }, () => { return ioc; diff --git a/src/test/datascience/interactiveWindowTestHelpers.tsx b/src/test/datascience/interactiveWindowTestHelpers.tsx index 28d96909779a..46d890f1a273 100644 --- a/src/test/datascience/interactiveWindowTestHelpers.tsx +++ b/src/test/datascience/interactiveWindowTestHelpers.tsx @@ -39,19 +39,20 @@ export function closeInteractiveWindow( export function runMountedTest( name: string, // tslint:disable-next-line:no-any - testFunc: (wrapper: ReactWrapper, React.Component>) => Promise, + testFunc: (wrapper: ReactWrapper, React.Component>, context: Mocha.Context) => Promise, getIOC: () => DataScienceIocContainer ) { - test(name, async () => { + test(name, async function() { const ioc = getIOC(); const jupyterExecution = ioc.get(IJupyterExecution); if (await jupyterExecution.isNotebookSupported()) { addMockData(ioc, 'a=1\na', 1); const wrapper = mountWebView(ioc, 'interactive'); - await testFunc(wrapper); + // tslint:disable-next-line: no-invalid-this + await testFunc(wrapper, this); } else { - // tslint:disable-next-line:no-console - console.log(`${name} skipped, no Jupyter installed.`); + // tslint:disable-next-line:no-invalid-this + this.skip(); } }); } diff --git a/src/test/datascience/jupyter/jupyterSession.unit.test.ts b/src/test/datascience/jupyter/jupyterSession.unit.test.ts index a2ed45bc6e03..0789f5ad70c0 100644 --- a/src/test/datascience/jupyter/jupyterSession.unit.test.ts +++ b/src/test/datascience/jupyter/jupyterSession.unit.test.ts @@ -10,6 +10,7 @@ import * as sinon from 'sinon'; import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; +import { traceInfo } from '../../../client/common/logger'; import { createDeferred, Deferred } from '../../../client/common/utils/async'; import { DataScience } from '../../../client/common/utils/localize'; import { noop } from '../../../client/common/utils/misc'; @@ -259,22 +260,22 @@ suite('Data Science - JupyterSession', () => { }); }); suite('Local Sessions', async () => { - let restartSession: Session.ISession; - let restartKernel: Kernel.IKernelConnection; - let restartStatusChangedSignal: ISignal; - let restartKernelChangedSignal: ISignal; + let newSession: Session.ISession; + let newKernelConnection: Kernel.IKernelConnection; + let newStatusChangedSignal: ISignal; + let newKernelChangedSignal: ISignal; let kernelAddedToIgnoreList: Deferred; let kernelRemovedFromIgnoreList: Deferred; let newSessionCreated: Deferred; setup(async () => { - restartSession = mock(DefaultSession); - restartKernel = mock(DefaultKernel); - restartStatusChangedSignal = mock(Signal); - restartKernelChangedSignal = mock(Signal); + newSession = mock(DefaultSession); + newKernelConnection = mock(DefaultKernel); + newStatusChangedSignal = mock(Signal); + newKernelChangedSignal = mock(Signal); kernelAddedToIgnoreList = createDeferred(); kernelRemovedFromIgnoreList = createDeferred(); - when(restartSession.statusChanged).thenReturn(instance(restartStatusChangedSignal)); - when(restartSession.kernelChanged).thenReturn(instance(restartKernelChangedSignal)); + when(newSession.statusChanged).thenReturn(instance(newStatusChangedSignal)); + when(newSession.kernelChanged).thenReturn(instance(newKernelChangedSignal)); when(kernelSelector.addKernelToIgnoreList(anything())).thenCall(() => kernelAddedToIgnoreList.resolve() ); @@ -282,17 +283,17 @@ suite('Data Science - JupyterSession', () => { kernelRemovedFromIgnoreList.resolve() ); // tslint:disable-next-line: no-any - (instance(restartSession) as any).then = undefined; + (instance(newSession) as any).then = undefined; newSessionCreated = createDeferred(); when(session.isRemoteSession).thenReturn(false); when(session.isDisposed).thenReturn(false); - when(restartKernel.id).thenReturn('restartId'); - when(restartKernel.clientId).thenReturn('restartClientId'); - when(restartKernel.status).thenReturn('idle'); - when(restartSession.kernel).thenReturn(instance(restartKernel)); + when(newKernelConnection.id).thenReturn('restartId'); + when(newKernelConnection.clientId).thenReturn('restartClientId'); + when(newKernelConnection.status).thenReturn('idle'); + when(newSession.kernel).thenReturn(instance(newKernelConnection)); when(sessionManager.startNew(anything())).thenCall(() => { newSessionCreated.resolve(); - return Promise.resolve(instance(restartSession)); + return Promise.resolve(instance(newSession)); }); }); teardown(() => { @@ -313,7 +314,7 @@ suite('Data Science - JupyterSession', () => { // Wait untill a new session has been started. await newSessionCreated.promise; - // One original, one new session and one restart session. + // One original, one new session. verify(sessionManager.startNew(anything())).thrice(); }); suite('Executing user code', async () => { @@ -330,30 +331,8 @@ suite('Data Science - JupyterSession', () => { assert.isOk(result); await result!.done; - - // Wait untill a new session has been started. - await newSessionCreated.promise; } - test('Must start a restart session', async () => { - verify(sessionManager.startNew(anything())).twice(); - }); - test('Restart session must be excluded from kernel picker', async () => { - await kernelAddedToIgnoreList.promise; - verify(kernelSelector.addKernelToIgnoreList(anything())).once(); - }); - test('Shutdown kills restart Session', async () => { - connection.setup(c => c.localLaunch).returns(() => true); - when(session.isRemoteSession).thenReturn(false); - when(session.isDisposed).thenReturn(false); - when(session.shutdown()).thenResolve(); - when(session.dispose()).thenReturn(); - - await jupyterSession.shutdown(); - - verify(restartSession.shutdown()).once(); - verify(restartSession.dispose()).once(); - }); test('Restart should create a new session & kill old session', async () => { const oldSessionShutDown = createDeferred(); connection.setup(c => c.localLaunch).returns(() => true); @@ -363,7 +342,10 @@ suite('Data Science - JupyterSession', () => { oldSessionShutDown.resolve(); return Promise.resolve(); }); - when(session.dispose()).thenReturn(); + when(session.dispose()).thenCall(() => { + traceInfo('Shutting down'); + return Promise.resolve(); + }); await jupyterSession.restart(0); diff --git a/src/test/datascience/jupyterHelpers.ts b/src/test/datascience/jupyterHelpers.ts index 25d4bde79bc7..fd60a39737ce 100644 --- a/src/test/datascience/jupyterHelpers.ts +++ b/src/test/datascience/jupyterHelpers.ts @@ -1,30 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -import { IProcessServiceFactory } from '../../client/common/process/types'; -import { IInterpreterService, PythonInterpreter } from '../../client/interpreter/contracts'; -import { DataScienceIocContainer } from './dataScienceIocContainer'; - -export async function getNotebookCapableInterpreter( - ioc: DataScienceIocContainer, - processFactory: IProcessServiceFactory -): Promise { - const is = ioc.serviceContainer.get(IInterpreterService); - const list = await is.getInterpreters(); - const procService = await processFactory.create(); - if (procService) { - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < list.length; i += 1) { - const result = await procService.exec(list[i].path, ['-m', 'jupyter', 'notebook', '--version'], { - env: process.env - }); - if (!result.stderr) { - return list[i]; - } - } - } - return undefined; -} // IP = * format is a bit different from localhost format export function getIPConnectionInfo(output: string): string | undefined { diff --git a/src/test/datascience/liveshare.functional.test.tsx b/src/test/datascience/liveshare.functional.test.tsx index ba3139608b89..a03a1cbec255 100644 --- a/src/test/datascience/liveshare.functional.test.tsx +++ b/src/test/datascience/liveshare.functional.test.tsx @@ -39,9 +39,10 @@ suite('DataScience LiveShare tests', () => { let guestContainer: DataScienceIocContainer; let lastErrorMessage: string | undefined; - setup(() => { + setup(async () => { hostContainer = createContainer(vsls.Role.Host); guestContainer = createContainer(vsls.Role.Guest); + return Promise.all([hostContainer.activate(), guestContainer.activate()]); }); teardown(async () => { diff --git a/src/test/datascience/mockWorkspaceConfig.ts b/src/test/datascience/mockWorkspaceConfig.ts index f71ba64e064f..aa8c5706c09a 100644 --- a/src/test/datascience/mockWorkspaceConfig.ts +++ b/src/test/datascience/mockWorkspaceConfig.ts @@ -7,15 +7,35 @@ import { ConfigurationTarget, WorkspaceConfiguration } from 'vscode'; export class MockWorkspaceConfiguration implements WorkspaceConfiguration { // tslint:disable: no-any - public get(key: string): any; - public get(section: string): T | undefined; - public get(section: string, defaultValue: T): T; - public get(section: any, defaultValue?: any): any; - public get(_: string, defaultValue?: any): any { + private values = new Map(); + + constructor(defaultSettings?: any) { + if (defaultSettings) { + const keys = [...Object.keys(defaultSettings)]; + keys.forEach(k => this.values.set(k, defaultSettings[k])); + } + + // Special case python path (not in the object) + if (defaultSettings && defaultSettings.pythonPath) { + this.values.set('pythonPath', defaultSettings.pythonPath); + } + + // Special case datascience. Not the same case + if (defaultSettings && defaultSettings.datascience) { + this.values.set('dataScience', defaultSettings.datascience); + } + } + + public get(key: string, defaultValue?: T): T | undefined { + // tslint:disable-next-line: use-named-parameter + if (this.values.has(key)) { + return this.values.get(key); + } + return arguments.length > 1 ? defaultValue : (undefined as any); } - public has(_section: string): boolean { - return false; + public has(section: string): boolean { + return this.values.has(section); } public inspect( _section: string @@ -31,10 +51,11 @@ export class MockWorkspaceConfiguration implements WorkspaceConfiguration { return; } public update( - _section: string, - _value: any, + section: string, + value: any, _configurationTarget?: boolean | ConfigurationTarget | undefined ): Promise { + this.values.set(section, value); return Promise.resolve(); } } diff --git a/src/test/datascience/mockWorkspaceFolder.ts b/src/test/datascience/mockWorkspaceFolder.ts new file mode 100644 index 000000000000..3b56a9860bf5 --- /dev/null +++ b/src/test/datascience/mockWorkspaceFolder.ts @@ -0,0 +1,12 @@ +import { Uri, WorkspaceFolder } from 'vscode'; + +export class MockWorkspaceFolder implements WorkspaceFolder { + public uri: Uri; + public name: string; + public ownedResources = new Set(); + + constructor(folder: string, public index: number) { + this.uri = Uri.file(folder); + this.name = folder; + } +} diff --git a/src/test/datascience/nativeEditor.functional.test.tsx b/src/test/datascience/nativeEditor.functional.test.tsx index fbb31447864e..c8e76b560771 100644 --- a/src/test/datascience/nativeEditor.functional.test.tsx +++ b/src/test/datascience/nativeEditor.functional.test.tsx @@ -13,7 +13,12 @@ import * as sinon from 'sinon'; import { anything, objectContaining, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Disposable, TextDocument, TextEditor, Uri, WindowState } from 'vscode'; -import { IApplicationShell, ICustomEditorService, IDocumentManager } from '../../client/common/application/types'; +import { + IApplicationShell, + ICustomEditorService, + IDocumentManager, + IWorkspaceService +} from '../../client/common/application/types'; import { IFileSystem } from '../../client/common/platform/types'; import { createDeferred, sleep, waitForPromise } from '../../client/common/utils/async'; import { noop } from '../../client/common/utils/misc'; @@ -129,6 +134,7 @@ suite('DataScience Native Editor', () => { setup(async () => { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(useCustomEditorApi); + await ioc.activate(); const appShell = TypeMoq.Mock.ofType(); appShell @@ -476,6 +482,10 @@ df.head()`; async (_wrapper, context) => { if (ioc.mockJupyter) { await ioc.activate(); + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath, { + ...ioc.getSettings().datascience, + disableJupyterAutoStart: false + }); // Create an editor so something is listening to messages const editor = await createNewEditor(ioc); @@ -901,6 +911,7 @@ df.head()`; function initIoc() { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(useCustomEditorApi); + return ioc.activate(); } async function setupFunction(this: Mocha.Context, fileContents?: any) { const wrapperPossiblyUndefined = await setupWebview(ioc); @@ -1040,7 +1051,7 @@ df.head()`; suite('Selection/Focus', () => { setup(async function() { - initIoc(); + await initIoc(); // tslint:disable-next-line: no-invalid-this await setupFunction.call(this); }); @@ -1137,7 +1148,7 @@ df.head()`; suite('Model updates', () => { setup(async function() { - initIoc(); + await initIoc(); // tslint:disable-next-line: no-invalid-this await setupFunction.call(this); }); @@ -1399,7 +1410,7 @@ df.head()`; suite('Keyboard Shortcuts', () => { setup(async function() { (window.navigator as any).platform = originalPlatform; - initIoc(); + await initIoc(); // tslint:disable-next-line: no-invalid-this await setupFunction.call(this); }); @@ -2121,7 +2132,7 @@ df.head()`; // tslint:disable-next-line: no-invalid-this return this.skip(); } - initIoc(); + await initIoc(); windowStateChangeHandlers = []; // Keep track of all handlers for the onDidChangeWindowState event. @@ -2144,11 +2155,18 @@ df.head()`; await addCell(wrapper, ioc, 'a', false); } + async function updateFileConfig(key: string, value: any) { + return ioc + .get(IWorkspaceService) + .getConfiguration('file') + .update(key, value); + } + test('Auto save notebook every 1s', async () => { // Configure notebook to save automatically ever 1s. - when(ioc.mockedWorkspaceConfig.get('autoSave', 'off')).thenReturn('afterDelay'); - when(ioc.mockedWorkspaceConfig.get('autoSaveDelay', anything())).thenReturn(1_000); - ioc.forceSettingsChanged(ioc.getSettings().pythonPath); + await updateFileConfig('autoSave', 'afterDelay'); + await updateFileConfig('autoSaveDelay', 1_000); + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath); /** * Make some changes to a cell of a notebook, then verify the notebook is auto saved. @@ -2180,9 +2198,10 @@ df.head()`; test('File saved with same format', async () => { // Configure notebook to save automatically ever 1s. - when(ioc.mockedWorkspaceConfig.get('autoSave', 'off')).thenReturn('afterDelay'); - when(ioc.mockedWorkspaceConfig.get('autoSaveDelay', anything())).thenReturn(2_000); - ioc.forceSettingsChanged(ioc.getSettings().pythonPath); + await updateFileConfig('autoSave', 'afterDelay'); + await updateFileConfig('autoSaveDelay', 2_000); + + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath); const notebookFileContents = await fs.readFile(notebookFile.filePath, 'utf8'); const dirtyPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookDirty); const cleanPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean); @@ -2204,11 +2223,12 @@ df.head()`; const notebookFileContents = await fs.readFile(notebookFile.filePath, 'utf8'); // Configure notebook to to never save. - when(ioc.mockedWorkspaceConfig.get('autoSave', 'off')).thenReturn('off'); - when(ioc.mockedWorkspaceConfig.get('autoSaveDelay', anything())).thenReturn(1000); + await updateFileConfig('autoSave', 'off'); + await updateFileConfig('autoSaveDelay', 1_000); + // Update the settings and wait for the component to receive it and process it. const promise = waitForMessage(ioc, InteractiveWindowMessages.SettingsUpdated); - ioc.forceSettingsChanged(ioc.getSettings().pythonPath, { + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath, { ...defaultDataScienceSettings(), showCellInputCode: false }); @@ -2244,8 +2264,8 @@ df.head()`; await dirtyPromise; // Configure notebook to save when active editor changes. - when(ioc.mockedWorkspaceConfig.get('autoSave', 'off')).thenReturn('onFocusChange'); - ioc.forceSettingsChanged(ioc.getSettings().pythonPath); + await updateFileConfig('autoSave', 'onFocusChange'); + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath); // Now that the notebook is dirty, change the active editor. const docManager = ioc.get(IDocumentManager) as MockDocumentManager; @@ -2276,8 +2296,8 @@ df.head()`; await dirtyPromise; // Configure notebook to save when window state changes. - when(ioc.mockedWorkspaceConfig.get('autoSave', 'off')).thenReturn('onWindowChange'); - ioc.forceSettingsChanged(ioc.getSettings().pythonPath); + await updateFileConfig('autoSave', 'onWindowChange'); + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath); // Now that the notebook is dirty, change the active editor. // This should not trigger a save of notebook (as its configured to save only when window state changes). @@ -2299,8 +2319,8 @@ df.head()`; await dirtyPromise; // Configure notebook to save when active editor changes. - when(ioc.mockedWorkspaceConfig.get('autoSave', 'off')).thenReturn('onWindowChange'); - ioc.forceSettingsChanged(ioc.getSettings().pythonPath); + await updateFileConfig('autoSave', 'onWindowChange'); + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath); // Now that the notebook is dirty, send notification about changes to window state. windowStateChangeHandlers.forEach(item => item({ focused })); @@ -2329,8 +2349,8 @@ df.head()`; await dirtyPromise; // Configure notebook to save when active editor changes. - when(ioc.mockedWorkspaceConfig.get('autoSave', 'off')).thenReturn('onFocusChange'); - ioc.forceSettingsChanged(ioc.getSettings().pythonPath); + await updateFileConfig('autoSave', 'onFocusChange'); + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath); // Now that the notebook is dirty, change window state. // This should not trigger a save of notebook (as its configured to save only when focus is changed). @@ -2414,7 +2434,7 @@ df.head()`; suite('Update Metadata', () => { setup(async function() { - initIoc(); + await initIoc(); // tslint:disable-next-line: no-invalid-this await setupFunction.call(this, JSON.stringify(oldJson)); }); @@ -2460,7 +2480,7 @@ df.head()`; suite('Clear Outputs', () => { setup(async function() { - initIoc(); + await initIoc(); // tslint:disable-next-line: no-invalid-this await setupFunction.call(this, JSON.stringify(oldJson)); }); diff --git a/src/test/datascience/notebook.functional.test.ts b/src/test/datascience/notebook.functional.test.ts index 3228f686cb83..e81a387cb3d6 100644 --- a/src/test/datascience/notebook.functional.test.ts +++ b/src/test/datascience/notebook.functional.test.ts @@ -21,7 +21,7 @@ import { Cancellation, CancellationError } from '../../client/common/cancellatio import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; import { traceError, traceInfo } from '../../client/common/logger'; import { IFileSystem } from '../../client/common/platform/types'; -import { IProcessServiceFactory, IPythonExecutionFactory, Output } from '../../client/common/process/types'; +import { IPythonExecutionFactory, IPythonExecutionService, Output } from '../../client/common/process/types'; import { Product } from '../../client/common/types'; import { createDeferred, waitForPromise } from '../../client/common/utils/async'; import { noop } from '../../client/common/utils/misc'; @@ -52,10 +52,9 @@ import { } from '../../client/interpreter/contracts'; import { concatMultilineStringInput } from '../../datascience-ui/common'; import { generateTestState, ICellViewModel } from '../../datascience-ui/interactive-common/mainState'; -import { asyncDump } from '../common/asyncDump'; import { sleep } from '../core'; import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { getConnectionInfo, getIPConnectionInfo, getNotebookCapableInterpreter } from './jupyterHelpers'; +import { getConnectionInfo, getIPConnectionInfo } from './jupyterHelpers'; import { SupportedCommands } from './mockJupyterManager'; import { MockPythonService } from './mockPythonService'; @@ -63,26 +62,24 @@ import { MockPythonService } from './mockPythonService'; suite('DataScience notebook tests', () => { const disposables: Disposable[] = []; let jupyterExecution: IJupyterExecution; - let processFactory: IProcessServiceFactory; + let pythonFactory: IPythonExecutionFactory; let ioc: DataScienceIocContainer; let modifiedConfig = false; const baseUri = Uri.file('foo.py'); - setup(() => { + setup(async () => { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(); + await ioc.activate(); }); teardown(async () => { try { if (modifiedConfig) { traceInfo('Attempting to put jupyter default config back'); - const python = await getNotebookCapableInterpreter(ioc, processFactory); - const procService = await processFactory.create(); - if (procService && python) { - await procService.exec(python.path, ['-m', 'jupyter', 'notebook', '--generate-config', '-y'], { - env: process.env - }); + const procService = await createPythonService(); + if (procService) { + await procService.exec(['-m', 'jupyter', 'notebook', '--generate-config', '-y'], {}); } } traceInfo('Shutting down after test.'); @@ -106,10 +103,6 @@ suite('DataScience notebook tests', () => { } }); - suiteTeardown(() => { - asyncDump(); - }); - function escapePath(p: string) { return p.replace(/\\/g, '\\\\'); } @@ -274,7 +267,7 @@ suite('DataScience notebook tests', () => { rebindFunc(); } jupyterExecution = ioc.serviceManager.get(IJupyterExecution); - processFactory = ioc.serviceManager.get(IProcessServiceFactory); + pythonFactory = ioc.serviceManager.get(IPythonExecutionFactory); console.log(`Starting test ${name} ...`); if (await jupyterExecution.isNotebookSupported()) { // tslint:disable-next-line: no-invalid-this @@ -345,12 +338,29 @@ suite('DataScience notebook tests', () => { } } - runTest('Remote Self Certs', async (_this: Mocha.Context) => { - const python = await getNotebookCapableInterpreter(ioc, processFactory); + async function createPythonService(versionRequirement?: number): Promise { + if (!ioc.mockJupyter) { + const python = await ioc.getJupyterCapableInterpreter(); + + if ( + python && + python.version?.major && + (!versionRequirement || python.version?.major > versionRequirement) + ) { + return pythonFactory.createActivatedEnvironment({ + resource: undefined, + interpreter: python, + allowEnvironmentFetchExceptions: true, + bypassCondaExecution: true + }); + } + } + } - if (python && python.version?.major && python.version?.major > 2) { - const procService = await processFactory.create(); + runTest('Remote Self Certs', async (_this: Mocha.Context) => { + const pythonService = await createPythonService(2); + if (pythonService) { // We will only connect if we allow for self signed cert connections ioc.getSettings().datascience.allowUnauthorizedRemoteConnection = true; @@ -380,8 +390,7 @@ suite('DataScience notebook tests', () => { 'jkey.key' ); - const exeResult = procService.execObservable( - python.path, + const exeResult = pythonService.execObservable( [ '-m', 'jupyter', @@ -391,7 +400,6 @@ suite('DataScience notebook tests', () => { `--keyfile=${keyFile}` ], { - env: process.env, throwOnStdErr: false } ); @@ -429,10 +437,9 @@ suite('DataScience notebook tests', () => { runTest( 'Remote No Auth', async () => { - const python = await getNotebookCapableInterpreter(ioc, processFactory); - const procService = await processFactory.create(); + const pythonService = await createPythonService(); - if (procService && python) { + if (pythonService) { const connectionFound = createDeferred(); const configFile = path.join( EXTENSION_ROOT_DIR, @@ -442,10 +449,9 @@ suite('DataScience notebook tests', () => { 'serverConfigFiles', 'remoteNoAuth.py' ); - const exeResult = procService.execObservable( - python.path, + const exeResult = pythonService.execObservable( ['-m', 'jupyter', 'notebook', `--config=${configFile}`], - { env: process.env, throwOnStdErr: false } + { throwOnStdErr: false } ); disposables.push(exeResult); @@ -514,10 +520,9 @@ suite('DataScience notebook tests', () => { ); runTest('Remote Password', async () => { - const python = await getNotebookCapableInterpreter(ioc, processFactory); - const procService = await processFactory.create(); + const pythonService = await createPythonService(); - if (procService && python) { + if (pythonService) { const connectionFound = createDeferred(); const configFile = path.join( EXTENSION_ROOT_DIR, @@ -527,11 +532,9 @@ suite('DataScience notebook tests', () => { 'serverConfigFiles', 'remotePassword.py' ); - const exeResult = procService.execObservable( - python.path, - ['-m', 'jupyter', 'notebook', `--config=${configFile}`], - { env: process.env, throwOnStdErr: false } - ); + const exeResult = pythonService.execObservable(['-m', 'jupyter', 'notebook', `--config=${configFile}`], { + throwOnStdErr: false + }); disposables.push(exeResult); exeResult.out.subscribe((output: Output) => { @@ -561,10 +564,9 @@ suite('DataScience notebook tests', () => { }); runTest('Remote', async () => { - const python = await getNotebookCapableInterpreter(ioc, processFactory); - const procService = await processFactory.create(); + const pythonService = await createPythonService(); - if (procService && python) { + if (pythonService) { const connectionFound = createDeferred(); const configFile = path.join( EXTENSION_ROOT_DIR, @@ -574,11 +576,9 @@ suite('DataScience notebook tests', () => { 'serverConfigFiles', 'remoteToken.py' ); - const exeResult = procService.execObservable( - python.path, - ['-m', 'jupyter', 'notebook', `--config=${configFile}`], - { env: process.env, throwOnStdErr: false } - ); + const exeResult = pythonService.execObservable(['-m', 'jupyter', 'notebook', `--config=${configFile}`], { + throwOnStdErr: false + }); disposables.push(exeResult); exeResult.out.subscribe((output: Output) => { @@ -626,7 +626,6 @@ suite('DataScience notebook tests', () => { test('Not installed', async () => { jupyterExecution = ioc.serviceManager.get(IJupyterExecution); - processFactory = ioc.serviceManager.get(IProcessServiceFactory); // Rewire our data we use to search for processes @injectable() class EmptyInterpreterService implements IInterpreterService { @@ -697,7 +696,7 @@ suite('DataScience notebook tests', () => { // Make sure we have a change dir happening const settings = { ...ioc.getSettings().datascience }; settings.changeDirOnImportExport = true; - ioc.forceSettingsChanged(ioc.getSettings().pythonPath, settings); + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath, settings); const exporter = ioc.serviceManager.get(INotebookExporter); const newFolderPath = path.join( @@ -923,7 +922,7 @@ suite('DataScience notebook tests', () => { } // Force a settings changed so that all of the cached data is cleared - ioc.forceSettingsChanged('/usr/bin/test3/python'); + ioc.forceSettingsChanged(undefined, '/usr/bin/test3/python'); assert.ok( await testCancelableMethod( @@ -1173,17 +1172,13 @@ plt.show()`, ]); async function generateNonDefaultConfig() { - const usable = await getNotebookCapableInterpreter(ioc, processFactory); + const usable = await ioc.getJupyterCapableInterpreter(); assert.ok(usable, 'Cant find jupyter enabled python'); // Manually generate an invalid jupyter config - const procService = await processFactory.create(); + const procService = await createPythonService(); assert.ok(procService, 'Can not get a process service'); - const results = await procService!.exec( - usable!.path, - ['-m', 'jupyter', 'notebook', '--generate-config', '-y'], - { env: process.env } - ); + const results = await procService!.exec(['-m', 'jupyter', 'notebook', '--generate-config', '-y'], {}); // Results should have our path to the config. const match = /^.*\s+(.*jupyter_notebook_config.py)\s+.*$/m.exec(results.stdout); @@ -1224,7 +1219,7 @@ plt.show()`, const tempDir = os.tmpdir(); const settings = ioc.getSettings(); settings.datascience.jupyterCommandLineArguments = ['--NotebookApp.port=9975', `--notebook-dir=${tempDir}`]; - ioc.forceSettingsChanged(settings.pythonPath, settings.datascience); + ioc.forceSettingsChanged(undefined, settings.pythonPath, settings.datascience); const notebook = await createNotebook(true); assert.ok(notebook, 'Server should have started on port 9975'); const hs = notebook as HostJupyterNotebook; @@ -1441,7 +1436,6 @@ plt.show()`, ioc.serviceManager.rebindInstance(IApplicationShell, instance(application)); jupyterExecution = ioc.serviceManager.get(IJupyterExecution); - processFactory = ioc.serviceManager.get(IProcessServiceFactory); // Change notebook command to fail with some goofy output await disableJupyter(ioc.workingInterpreter.path); diff --git a/src/test/datascience/plotViewer.functional.test.tsx b/src/test/datascience/plotViewer.functional.test.tsx index 3a64652f82d2..4e666e68fb64 100644 --- a/src/test/datascience/plotViewer.functional.test.tsx +++ b/src/test/datascience/plotViewer.functional.test.tsx @@ -21,9 +21,10 @@ suite('DataScience PlotViewer tests', () => { let plotViewerProvider: IPlotViewerProvider; let ioc: DataScienceIocContainer; - setup(() => { + setup(async () => { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(); + await ioc.activate(); }); function mountWebView(): ReactWrapper, React.Component> { diff --git a/src/test/datascience/reactHelpers.ts b/src/test/datascience/reactHelpers.ts index a80ad2f07fa2..e83b03062509 100644 --- a/src/test/datascience/reactHelpers.ts +++ b/src/test/datascience/reactHelpers.ts @@ -399,34 +399,6 @@ export function setUpDomEnvironment() { (global as any)['Headers'] = fetchMod.Headers; // tslint:disable-next-line:no-string-literal no-eval no-any (global as any)['WebSocket'] = eval('require')('ws'); - - // For the loc test to work, we have to have a global getter for loc strings - // tslint:disable-next-line:no-string-literal no-eval no-any - (global as any)['getLocStrings'] = () => { - return { 'DataScience.unknownMimeType': 'Unknown mime type from helper' }; - }; - - // tslint:disable-next-line:no-string-literal no-eval no-any - (global as any)['getInitialSettings'] = () => { - return { - allowImportFromNotebook: true, - jupyterLaunchTimeout: 10, - jupyterLaunchRetries: 3, - enabled: true, - jupyterServerURI: 'local', - // tslint:disable-next-line: no-invalid-template-strings - notebookFileRoot: '${fileDirname}', - changeDirOnImportExport: false, - useDefaultConfigForJupyter: true, - jupyterInterruptTimeout: 10000, - searchForJupyter: true, - showCellInputCode: true, - collapseCellInputCodeByDefault: true, - allowInput: true, - variableExplorerExclude: 'module;function;builtin_function_or_method' - }; - }; - (global as any)['DOMParser'] = dom.window.DOMParser; (global as any)['Blob'] = dom.window.Blob; diff --git a/src/test/datascience/variableexplorer.functional.test.tsx b/src/test/datascience/variableexplorer.functional.test.tsx index f8b1e4e6cd5e..1d20a8cd47b4 100644 --- a/src/test/datascience/variableexplorer.functional.test.tsx +++ b/src/test/datascience/variableexplorer.functional.test.tsx @@ -37,10 +37,11 @@ suite('DataScience Interactive Window variable explorer tests', () => { } }); - setup(() => { + setup(async () => { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(); createdNotebook = false; + await ioc.activate(); }); teardown(async () => { diff --git a/src/test/interpreters/activation/service.unit.test.ts b/src/test/interpreters/activation/service.unit.test.ts index cea844a7c247..18c9d9014dc9 100644 --- a/src/test/interpreters/activation/service.unit.test.ts +++ b/src/test/interpreters/activation/service.unit.test.ts @@ -171,11 +171,13 @@ suite('Interpreters Activation - Python Environment Variables', () => { const options = capture(processService.shellExec).first()[1]; const expectedShell = defaultShells[osType.value]; + // tslint:disable-next-line: chai-vague-errors expect(options).to.deep.equal({ shell: expectedShell, env: envVars, timeout: 30000, - maxBuffer: 1000 * 1000 + maxBuffer: 1000 * 1000, + throwOnStdErr: false }); }); test('Use current process variables if there are no custom variables', async () => { @@ -204,11 +206,13 @@ suite('Interpreters Activation - Python Environment Variables', () => { const options = capture(processService.shellExec).first()[1]; const expectedShell = defaultShells[osType.value]; + // tslint:disable-next-line: chai-vague-errors expect(options).to.deep.equal({ env: envVars, shell: expectedShell, timeout: 30000, - maxBuffer: 1000 * 1000 + maxBuffer: 1000 * 1000, + throwOnStdErr: false }); }); test('Error must be swallowed when activation fails', async () => { diff --git a/src/test/linters/lint.functional.test.ts b/src/test/linters/lint.functional.test.ts index 6050e5caf373..9d5980d27b80 100644 --- a/src/test/linters/lint.functional.test.ts +++ b/src/test/linters/lint.functional.test.ts @@ -3,6 +3,7 @@ 'use strict'; import * as assert from 'assert'; +import * as child_process from 'child_process'; import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; @@ -662,7 +663,9 @@ class TestFixture extends BaseTestFixture { undefined, TypeMoq.MockBehavior.Strict ); - envVarsService.setup(e => e.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); + envVarsService + .setup(e => e.getEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(process.env)); serviceContainer .setup(c => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider), TypeMoq.It.isAny())) .returns(() => envVarsService.object); @@ -752,6 +755,10 @@ suite('Linting Functional Tests', () => { fixture.serviceContainer.object ); + const pythonPath = child_process.execSync(`python -c "import sys;print(sys.executable)"`); + // tslint:disable-next-line: no-console + console.log(`Testing linter with python ${pythonPath}`); + const messages = await linter.lint(doc, new CancellationTokenSource().token); if (messagesToBeReceived.length === 0) { diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index 3c1a4f8c45cb..280538257167 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -244,6 +244,8 @@ export class IocContainer { await promise; } } + this.disposables = []; + this.serviceManager.dispose(); } public registerCommonTypes(registerFileSystem: boolean = true) { diff --git a/src/test/unittests.ts b/src/test/unittests.ts index 4d77117a1cfd..b57e17d241a1 100644 --- a/src/test/unittests.ts +++ b/src/test/unittests.ts @@ -2,8 +2,20 @@ // Licensed under the MIT License. 'use strict'; -// tslint:disable:no-any no-require-imports no-var-requires +// Not sure why but on windows, if you execute a process from the System32 directory, it will just crash Node. +// Not throw an exception, just make node exit. +// However if a system32 process is run first, everything works. +import * as child_process from 'child_process'; +import * as os from 'os'; +if (os.platform() === 'win32') { + const proc = child_process.spawn('C:\\Windows\\System32\\Reg.exe', ['/?']); + proc.on('error', () => { + // tslint:disable-next-line: no-console + console.error('error during reg.exe'); + }); +} +// tslint:disable:no-any no-require-imports no-var-requires if ((Reflect as any).metadata === undefined) { require('reflect-metadata'); }