diff --git a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts index b7b84ddeb6..197846de7f 100644 --- a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts +++ b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts @@ -670,11 +670,14 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { let cmd = `"${pythonExePath ?? this.pythonExecutable}" -m pip list --format=json`; let packagesInfo = await this.executeBufferedCommand(cmd); - let packagesResult: PythonPkgDetails[] = []; + let packages: PythonPkgDetails[] = []; if (packagesInfo) { - packagesResult = JSON.parse(packagesInfo); + let parsedResult = JSON.parse(packagesInfo); + if (parsedResult) { + packages = parsedResult; + } } - return packagesResult; + return packages; } catch (err) { this.outputChannel.appendLine(msgPackageRetrievalFailed(utils.getErrorMessage(err))); @@ -716,15 +719,14 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { let cmd = `"${condaExe}" list --json`; let packagesInfo = await this.executeBufferedCommand(cmd); + let packages: PythonPkgDetails[] = []; if (packagesInfo) { - let packagesResult = JSON.parse(packagesInfo); - if (Array.isArray(packagesResult)) { - return packagesResult - .filter(pkg => pkg && pkg.channel && pkg.channel !== 'pypi') - .map(pkg => { name: pkg.name, version: pkg.version }); + let parsedResult = JSON.parse(packagesInfo); + if (parsedResult) { + packages = parsedResult.filter(pkg => pkg && pkg.channel && pkg.channel !== 'pypi'); } } - return []; + return packages; } catch (err) { this.outputChannel.appendLine(msgPackageRetrievalFailed(utils.getErrorMessage(err))); @@ -787,10 +789,6 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { } private isCondaInstalled(): boolean { - if (!this._usingExistingPython) { - return false; - } - let condaExePath = this.getCondaExePath(); // eslint-disable-next-line no-sync return fs.existsSync(condaExePath); @@ -886,6 +884,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { export interface PythonPkgDetails { name: string; version: string; + channel?: string; } export interface PipPackageOverview { diff --git a/extensions/notebook/src/test/configurePython.test.ts b/extensions/notebook/src/test/python/configurePython.test.ts similarity index 92% rename from extensions/notebook/src/test/configurePython.test.ts rename to extensions/notebook/src/test/python/configurePython.test.ts index 9c9463bda8..934b1537ae 100644 --- a/extensions/notebook/src/test/configurePython.test.ts +++ b/extensions/notebook/src/test/python/configurePython.test.ts @@ -5,13 +5,13 @@ import * as azdata from 'azdata'; import * as TypeMoq from 'typemoq'; -import { ConfigurePythonWizard, ConfigurePythonModel } from '../dialog/configurePython/configurePythonWizard'; -import { JupyterServerInstallation } from '../jupyter/jupyterServerInstallation'; -import { ConfigurePathPage } from '../dialog/configurePython/configurePathPage'; +import { ConfigurePythonWizard, ConfigurePythonModel } from '../../dialog/configurePython/configurePythonWizard'; +import { JupyterServerInstallation } from '../../jupyter/jupyterServerInstallation'; +import { ConfigurePathPage } from '../../dialog/configurePython/configurePathPage'; import * as should from 'should'; -import { PickPackagesPage } from '../dialog/configurePython/pickPackagesPage'; -import { python3DisplayName, allKernelsName } from '../common/constants'; -import { TestContext, createViewContext, TestButton } from './common'; +import { PickPackagesPage } from '../../dialog/configurePython/pickPackagesPage'; +import { python3DisplayName, allKernelsName } from '../../common/constants'; +import { TestContext, createViewContext, TestButton } from '../common'; import { EventEmitter } from 'vscode'; describe('Configure Python Wizard', function () { diff --git a/extensions/notebook/src/test/python/jupyterInstallation.test.ts b/extensions/notebook/src/test/python/jupyterInstallation.test.ts new file mode 100644 index 0000000000..11ea724e2a --- /dev/null +++ b/extensions/notebook/src/test/python/jupyterInstallation.test.ts @@ -0,0 +1,196 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as should from 'should'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import * as uuid from 'uuid'; +import * as fs from 'fs-extra'; +import { JupyterServerInstallation, PythonPkgDetails } from '../../jupyter/jupyterServerInstallation'; + +describe('Jupyter Server Installation', function () { + let outputChannelStub: TypeMoq.IMock; + let installation: JupyterServerInstallation; + + beforeEach(function (): void { + outputChannelStub = TypeMoq.Mock.ofType(); + outputChannelStub.setup(c => c.show(TypeMoq.It.isAny())); + outputChannelStub.setup(c => c.appendLine(TypeMoq.It.isAnyString())); + + installation = new JupyterServerInstallation('', outputChannelStub.object); + }); + + afterEach(function (): void { + sinon.restore(); + }); + + it('Get pip packages', async function() { + // Should return nothing if passed an invalid python path + let fakePath = uuid.v4(); + let pkgResult = await installation.getInstalledPipPackages(fakePath); + should(pkgResult).not.be.undefined(); + should(pkgResult.length).be.equal(0); + + // Should return nothing if python is not installed + sinon.stub(JupyterServerInstallation, 'isPythonInstalled').returns(false); + pkgResult = await installation.getInstalledPipPackages(); + should(pkgResult).not.be.undefined(); + should(pkgResult.length).be.equal(0); + + // Should return nothing on error + sinon.restore(); + sinon.stub(JupyterServerInstallation, 'isPythonInstalled').returns(true); + sinon.stub(installation, 'executeBufferedCommand').rejects(new Error('Expected test failure.')); + pkgResult = await installation.getInstalledPipPackages(); + should(pkgResult).not.be.undefined(); + should(pkgResult.length).be.equal(0); + outputChannelStub.verify(c => c.appendLine(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + + // Normal use case + sinon.restore(); + let testPackages: PythonPkgDetails[] = [{ + name: 'TestPkg1', + version: '1.2.3' + }, { + name: 'TestPkg2', + version: '4.5.6' + }]; + sinon.stub(JupyterServerInstallation, 'isPythonInstalled').returns(true); + sinon.stub(installation, 'executeBufferedCommand').resolves(JSON.stringify(testPackages)); + pkgResult = await installation.getInstalledPipPackages(); + should(pkgResult).be.deepEqual(testPackages); + }); + + it('Install pip package', async function() { + let commandStub = sinon.stub(installation, 'executeStreamedCommand').resolves(); + + // Should not execute any commands when passed an empty package list + await installation.installPipPackages(undefined, false); + should(commandStub.called).be.false(); + + await installation.installPipPackages([], false); + should(commandStub.called).be.false(); + + // Install package using exact version + let testPackages = [{ + name: 'TestPkg1', + version: '1.2.3' + }, { + name: 'TestPkg2', + version: '4.5.6' + }]; + await installation.installPipPackages(testPackages, false); + should(commandStub.calledOnce).be.true(); + let commandStr = commandStub.args[0][0] as string; + should(commandStr.includes('"TestPkg1==1.2.3" "TestPkg2==4.5.6"')).be.true(); + + // Install package using minimum version + await installation.installPipPackages(testPackages, true); + should(commandStub.calledTwice).be.true(); + commandStr = commandStub.args[1][0] as string; + should(commandStr.includes('"TestPkg1>=1.2.3" "TestPkg2>=4.5.6"')).be.true(); + }); + + it('Uninstall pip package', async function() { + let commandStub = sinon.stub(installation, 'executeStreamedCommand').resolves(); + + let testPackages = [{ + name: 'jupyter', + version: '1.0.0' + }, { + name: 'TestPkg2', + version: '4.5.6' + }]; + await installation.uninstallPipPackages(testPackages); + should(commandStub.calledOnce).be.true(); + let commandStr = commandStub.args[0][0] as string; + should(commandStr.includes('"jupyter==1.0.0" "TestPkg2==4.5.6"')).be.true(); + }); + + it('Get conda packages', async function() { + // Should return nothing if conda is not installed + sinon.stub(fs, 'existsSync').returns(false); + let pkgResult = await installation.getInstalledCondaPackages(); + should(pkgResult).not.be.undefined(); + should(pkgResult.length).be.equal(0); + + // Should return nothing on error + sinon.restore(); + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(installation, 'executeBufferedCommand').rejects(new Error('Expected test failure.')); + pkgResult = await installation.getInstalledCondaPackages(); + should(pkgResult).not.be.undefined(); + should(pkgResult.length).be.equal(0); + outputChannelStub.verify(c => c.appendLine(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + + // Normal use case + sinon.restore(); + let testPackages: PythonPkgDetails[] = [{ + name: 'TestPkg1', + version: '1.2.3', + channel: 'conda' + }, { + name: 'TestPkg2', + version: '4.5.6', + channel: 'pypi' + }, { + name: 'TestPkg3', + version: '7.8.9', + channel: 'conda' + }]; + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(installation, 'executeBufferedCommand').resolves(JSON.stringify(testPackages)); + pkgResult = await installation.getInstalledCondaPackages(); + let filteredPackages = testPackages.filter(pkg => pkg.channel !== 'pypi'); + should(pkgResult).be.deepEqual(filteredPackages); + }); + + it('Install conda package', async function() { + let commandStub = sinon.stub(installation, 'executeStreamedCommand').resolves(); + + // Should not execute any commands when passed an empty package list + await installation.installCondaPackages(undefined, false); + should(commandStub.called).be.false(); + + await installation.installCondaPackages([], false); + should(commandStub.called).be.false(); + + // Install package using exact version + let testPackages = [{ + name: 'TestPkg1', + version: '1.2.3' + }, { + name: 'TestPkg2', + version: '4.5.6' + }]; + await installation.installCondaPackages(testPackages, false); + should(commandStub.calledOnce).be.true(); + let commandStr = commandStub.args[0][0] as string; + should(commandStr.includes('"TestPkg1==1.2.3" "TestPkg2==4.5.6"')).be.true(); + + // Install package using minimum version + await installation.installCondaPackages(testPackages, true); + should(commandStub.calledTwice).be.true(); + commandStr = commandStub.args[1][0] as string; + should(commandStr.includes('"TestPkg1>=1.2.3" "TestPkg2>=4.5.6"')).be.true(); + }); + + it('Uninstall conda package', async function() { + let commandStub = sinon.stub(installation, 'executeStreamedCommand').resolves(); + + let testPackages = [{ + name: 'jupyter', + version: '1.0.0' + }, { + name: 'TestPkg2', + version: '4.5.6' + }]; + await installation.uninstallCondaPackages(testPackages); + should(commandStub.calledOnce).be.true(); + let commandStr = commandStub.args[0][0] as string; + should(commandStr.includes('"jupyter==1.0.0" "TestPkg2==4.5.6"')).be.true(); + }); +});