Update Configure Python dialog to allow packages to be installed for specific kernels. (#10286)

This commit is contained in:
Cory Rivera
2020-05-08 16:02:59 -07:00
committed by GitHub
parent 9bcd7cdd80
commit f94a9d0d58
23 changed files with 1045 additions and 95 deletions

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { IServerInstance } from '../jupyter/common';
@@ -255,3 +256,160 @@ export class FutureStub implements Kernel.IFuture {
}
}
//#endregion
//#region test modelView components
class TestComponentBase implements azdata.Component {
id: string = '';
updateProperties(properties: { [key: string]: any; }): Thenable<void> {
Object.assign(this, properties);
return Promise.resolve();
}
updateProperty(key: string, value: any): Thenable<void> {
throw new Error('Method not implemented');
}
updateCssStyles(cssStyles: { [key: string]: string; }): Thenable<void> {
throw new Error('Method not implemented');
}
onValidityChanged: vscode.Event<boolean> = undefined;
valid: boolean = true;
validate(): Thenable<boolean> {
return Promise.resolve(true);
}
focus(): Thenable<void> {
return Promise.resolve();
}
}
class TestDropdownComponent extends TestComponentBase implements azdata.DropDownComponent {
constructor(private onClick: vscode.EventEmitter<any>) {
super();
}
onValueChanged: vscode.Event<any> = this.onClick.event;
}
class TestDeclarativeTableComponent extends TestComponentBase implements azdata.DeclarativeTableComponent {
constructor(private onClick: vscode.EventEmitter<any>) {
super();
}
onDataChanged: vscode.Event<any> = this.onClick.event;
data: any[][];
columns: azdata.DeclarativeTableColumn[];
}
class TestButtonComponent extends TestComponentBase implements azdata.ButtonComponent {
constructor(private onClick: vscode.EventEmitter<any>) {
super();
}
onDidClick: vscode.Event<any> = this.onClick.event;
}
class TestRadioButtonComponent extends TestComponentBase implements azdata.RadioButtonComponent {
constructor(private onClick: vscode.EventEmitter<any>) {
super();
}
onDidClick: vscode.Event<any> = this.onClick.event;
}
class TestTextComponent extends TestComponentBase implements azdata.TextComponent {
}
class TestLoadingComponent extends TestComponentBase implements azdata.LoadingComponent {
loading: boolean;
component: azdata.Component;
}
class TestFormContainer extends TestComponentBase implements azdata.FormContainer {
items: azdata.Component[] = [];
clearItems(): void {
}
addItems(itemConfigs: azdata.Component[], itemLayout?: azdata.FormItemLayout): void {
}
addItem(component: azdata.Component, itemLayout?: azdata.FormItemLayout): void {
}
insertItem(component: azdata.Component, index: number, itemLayout?: azdata.FormItemLayout): void {
}
removeItem(component: azdata.Component): boolean {
return true;
}
setLayout(layout: azdata.FormLayout): void {
}
setItemLayout(component: azdata.Component, layout: azdata.FormItemLayout): void {
}
}
class TestComponentBuilder<T extends azdata.Component> implements azdata.ComponentBuilder<T> {
constructor(private _component: T) {
}
component(): T {
return this._component;
}
withProperties<U>(properties: U): azdata.ComponentBuilder<T> {
this._component.updateProperties(properties);
return this;
}
withValidation(validation: (component: T) => boolean): azdata.ComponentBuilder<T> {
return this;
}
}
class TestLoadingBuilder extends TestComponentBuilder<azdata.LoadingComponent> implements azdata.LoadingComponentBuilder {
withItem(component: azdata.Component): azdata.LoadingComponentBuilder {
this.component().component = component;
return this;
}
}
export function createViewContext(): TestContext {
let onClick: vscode.EventEmitter<any> = new vscode.EventEmitter<any>();
let form: azdata.FormContainer = new TestFormContainer();
let textBuilder: azdata.ComponentBuilder<azdata.TextComponent> = new TestComponentBuilder(new TestTextComponent());
let buttonBuilder: azdata.ComponentBuilder<azdata.ButtonComponent> = new TestComponentBuilder(new TestButtonComponent(onClick));
let radioButtonBuilder: azdata.ComponentBuilder<azdata.ButtonComponent> = new TestComponentBuilder(new TestRadioButtonComponent(onClick));
let declarativeTableBuilder: azdata.ComponentBuilder<azdata.DeclarativeTableComponent> = new TestComponentBuilder(new TestDeclarativeTableComponent(onClick));
let loadingBuilder: azdata.LoadingComponentBuilder = new TestLoadingBuilder(new TestLoadingComponent());
let dropdownBuilder: azdata.ComponentBuilder<azdata.DropDownComponent> = new TestComponentBuilder(new TestDropdownComponent(onClick));
let formBuilder: azdata.FormBuilder = Object.assign({}, {
component: () => form,
addFormItem: () => { },
insertFormItem: () => { },
removeFormItem: () => true,
addFormItems: () => { },
withFormItems: () => formBuilder,
withProperties: () => formBuilder,
withValidation: () => formBuilder,
withItems: () => formBuilder,
withLayout: () => formBuilder
});
let view: azdata.ModelView = {
onClosed: undefined!,
connection: undefined!,
serverInfo: undefined!,
valid: true,
onValidityChanged: undefined!,
validate: undefined!,
initializeModel: () => { return Promise.resolve(); },
modelBuilder: <azdata.ModelBuilder>{
radioButton: () => radioButtonBuilder,
text: () => textBuilder,
button: () => buttonBuilder,
dropDown: () => dropdownBuilder,
declarativeTable: () => declarativeTableBuilder,
formContainer: () => formBuilder,
loadingComponent: () => loadingBuilder
}
};
return {
view: view,
onClick: onClick,
};
}
export interface TestContext {
view: azdata.ModelView;
onClick: vscode.EventEmitter<any>;
}
//#endregion

View File

@@ -0,0 +1,133 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as TypeMoq from 'typemoq';
import { ApiWrapper } from '../common/apiWrapper';
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 } from './common';
describe('Configure Python Wizard', function () {
let apiWrapper: ApiWrapper = new ApiWrapper();
let testWizard: ConfigurePythonWizard;
let viewContext: TestContext;
let testInstallation: JupyterServerInstallation;
beforeEach(() => {
let mockInstall = TypeMoq.Mock.ofType(JupyterServerInstallation);
mockInstall.setup(i => i.getInstalledPipPackages(TypeMoq.It.isAnyString())).returns(() => Promise.resolve([]));
testInstallation = mockInstall.object;
let mockWizard = TypeMoq.Mock.ofType(ConfigurePythonWizard);
mockWizard.setup(w => w.showErrorMessage(TypeMoq.It.isAnyString()));
testWizard = mockWizard.object;
viewContext = createViewContext();
});
// These wizard tests are disabled due to errors with disposable objects
//
// it('Start wizard test', async () => {
// let wizard = new ConfigurePythonWizard(apiWrapper, testInstallation);
// await wizard.start();
// await wizard.close();
// await should(wizard.setupComplete).be.resolved();
// });
// it('Reject setup on cancel test', async () => {
// let wizard = new ConfigurePythonWizard(apiWrapper, testInstallation);
// await wizard.start(undefined, true);
// await wizard.close();
// await should(wizard.setupComplete).be.rejected();
// });
// it('Error message test', async () => {
// let wizard = new ConfigurePythonWizard(apiWrapper, testInstallation);
// await wizard.start();
// should(wizard.wizard.message).be.undefined();
// let testMsg = 'Test message';
// wizard.showErrorMessage(testMsg);
// should(wizard.wizard.message.text).be.equal(testMsg);
// should(wizard.wizard.message.level).be.equal(azdata.window.MessageLevel.Error);
// wizard.clearStatusMessage();
// should(wizard.wizard.message).be.undefined();
// await wizard.close();
// });
it('Configure Path Page test', async () => {
let testPythonLocation = '/not/a/real/path';
let model = <ConfigurePythonModel>{
useExistingPython: true,
pythonPathsPromise: Promise.resolve([{
installDir: testPythonLocation,
version: '4000'
}])
};
let page = azdata.window.createWizardPage('Page 1');
let configurePathPage = new ConfigurePathPage(apiWrapper, testWizard, page, model, viewContext.view);
should(await configurePathPage.initialize()).be.true();
// First page, so onPageEnter should do nothing
await should(configurePathPage.onPageEnter()).be.resolved();
should(await configurePathPage.onPageLeave()).be.true();
should(model.useExistingPython).be.true();
should(model.pythonLocation).be.equal(testPythonLocation);
});
it('Pick Packages Page test', async () => {
let model = <ConfigurePythonModel>{
kernelName: allKernelsName,
installation: testInstallation,
pythonLocation: '/not/a/real/path',
useExistingPython: true
};
let page = azdata.window.createWizardPage('Page 2');
let pickPackagesPage = new PickPackagesPage(apiWrapper, testWizard, page, model, viewContext.view);
should(await pickPackagesPage.initialize()).be.true();
should((<any>pickPackagesPage).kernelLabel).not.be.undefined();
should((<any>pickPackagesPage).kernelDropdown).be.undefined();
// Last page, so onPageLeave should do nothing
should(await pickPackagesPage.onPageLeave()).be.true();
await should(pickPackagesPage.onPageEnter()).be.resolved();
should(model.packagesToInstall).be.deepEqual(JupyterServerInstallation.getRequiredPackagesForKernel(allKernelsName));
});
it('Undefined kernel test', async () => {
let model = <ConfigurePythonModel>{
kernelName: undefined,
installation: testInstallation,
pythonLocation: '/not/a/real/path',
useExistingPython: true
};
let page = azdata.window.createWizardPage('Page 2');
let pickPackagesPage = new PickPackagesPage(apiWrapper, testWizard, page, model, viewContext.view);
should(await pickPackagesPage.initialize()).be.true();
should((<any>pickPackagesPage).kernelLabel).be.undefined();
should((<any>pickPackagesPage).kernelDropdown).not.be.undefined();
await should(pickPackagesPage.onPageEnter()).be.resolved();
should(model.packagesToInstall).be.deepEqual(JupyterServerInstallation.getRequiredPackagesForKernel(python3DisplayName));
});
});

View File

@@ -8,7 +8,7 @@ import * as azdata from 'azdata';
import * as should from 'should';
import 'mocha';
import * as TypeMoq from 'typemoq';
import { JupyterServerInstallation, PythonPkgDetails, IJupyterServerInstallation } from '../../jupyter/jupyterServerInstallation';
import { JupyterServerInstallation, PythonPkgDetails, IJupyterServerInstallation, PythonInstallSettings } from '../../jupyter/jupyterServerInstallation';
import { LocalCondaPackageManageProvider } from '../../jupyter/localCondaPackageManageProvider';
import * as constants from '../../common/constants';
import { LocalPipPackageManageProvider } from '../../jupyter/localPipPackageManageProvider';
@@ -188,7 +188,7 @@ describe('Manage Package Providers', () => {
serverInstallation: {
installCondaPackages: (packages: PythonPkgDetails[], useMinVersion: boolean) => { return Promise.resolve(); },
configurePackagePaths: () => { return Promise.resolve(); },
startInstallProcess: (forceInstall: boolean, installSettings?: { installPath: string, existingPython: boolean }) => { return Promise.resolve(); },
startInstallProcess: (forceInstall: boolean, installSettings?: PythonInstallSettings) => { return Promise.resolve(); },
getInstalledPipPackages: () => { return Promise.resolve([]); },
installPipPackages: (packages: PythonPkgDetails[], useMinVersion: boolean) => { return Promise.resolve(); },
uninstallPipPackages: (packages: PythonPkgDetails[]) => { return Promise.resolve(); },

View File

@@ -6,6 +6,7 @@
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import 'mocha';
import { JupyterServerInstanceStub } from '../common';
@@ -18,6 +19,10 @@ import { IServerInstance } from '../../jupyter/common';
import { MockExtensionContext } from '../common/stubs';
describe('Local Jupyter Server Manager', function (): void {
const pythonKernelSpec: azdata.nb.IKernelSpec = {
name: 'python3',
display_name: 'Python 3'
};
let expectedPath = 'my/notebook.ipynb';
let serverManager: LocalJupyterServerManager;
let deferredInstall: Deferred<void>;
@@ -33,7 +38,7 @@ describe('Local Jupyter Server Manager', function (): void {
deferredInstall = new Deferred<void>();
let mockInstall = TypeMoq.Mock.ofType(JupyterServerInstallation, undefined, undefined, '/root');
mockInstall.setup(j => j.promptForPythonInstall()).returns(() => deferredInstall.promise);
mockInstall.setup(j => j.promptForPythonInstall(TypeMoq.It.isAny())).returns(() => deferredInstall.promise);
mockInstall.object.execOptions = { env: Object.assign({}, process.env) };
serverManager = new LocalJupyterServerManager({
@@ -53,7 +58,7 @@ describe('Local Jupyter Server Manager', function (): void {
it('Should show error message on install failure', async function (): Promise<void> {
let error = 'Error!!';
deferredInstall.reject(error);
await testUtils.assertThrowsAsync(() => serverManager.startServer(), undefined);
await testUtils.assertThrowsAsync(() => serverManager.startServer(pythonKernelSpec), undefined);
});
it('Should configure and start install', async function (): Promise<void> {
@@ -65,7 +70,7 @@ describe('Local Jupyter Server Manager', function (): void {
// When I start the server
let notified = false;
serverManager.onServerStarted(() => notified = true);
await serverManager.startServer();
await serverManager.startServer(pythonKernelSpec);
// Then I expect the port to be included in settings
should(serverManager.serverSettings.baseUrl.indexOf('1234') > -1).be.true();
@@ -89,7 +94,7 @@ describe('Local Jupyter Server Manager', function (): void {
deferredInstall.resolve();
// When I start and then the server
await serverManager.startServer();
await serverManager.startServer(pythonKernelSpec);
await serverManager.stopServer();
// Then I expect stop to have been called on the server instance
@@ -104,7 +109,7 @@ describe('Local Jupyter Server Manager', function (): void {
deferredInstall.resolve();
// When I start and then dispose the extension
await serverManager.startServer();
await serverManager.startServer(pythonKernelSpec);
should(mockExtensionContext.subscriptions).have.length(1);
mockExtensionContext.subscriptions[0].dispose();

View File

@@ -108,7 +108,7 @@ describe('Jupyter Session', function (): void {
beforeEach(() => {
mockJupyterSession = TypeMoq.Mock.ofType(SessionStub);
session = new JupyterSession(mockJupyterSession.object);
session = new JupyterSession(mockJupyterSession.object, undefined);
});
it('should always be able to change kernels', function (): void {