Begin defining Extension-based Notebook Provider (#3172)

Implements provider contribution in the MainThreadNotebook, with matching function calls in the ExtHostNotebook class. This will allow us to proxy through notebook providers (specifically, creation of a notebook manager with required content, server managers) from an extension up through to the main process.

Implemented in this PR:
- Callthroughs for content and server manager APIs
- Very basic unit tests covering provider & manager registration

Not implemented:
- Fuller unit tests on the specific callthrough methods for content & server manager.
- Contribution point needed to test this (so we can actually pass through the extension's existing Notebook implementation)
This commit is contained in:
Kevin Cunnane
2018-11-08 13:06:40 -08:00
committed by GitHub
parent 71c14a0837
commit 9765269d27
14 changed files with 597 additions and 49 deletions

View File

@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
export async function assertThrowsAsync(fn, regExp?: string): Promise<void> {
let f = () => {
// Empty
};
try {
await fn();
} catch (e) {
f = () => { throw e; };
} finally {
assert.throws(f, regExp);
}
}

View File

@@ -63,7 +63,7 @@ suite('ExtHostBackgroundTaskManagement Tests', () => {
operation: (op: sqlops.BackgroundOperation) => {
op.onCanceled(() => {
op.updateStatus(TaskStatus.Canceled);
})
});
},
operationId: operationId
};

View File

@@ -0,0 +1,146 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import * as assert from 'assert';
import * as TypeMoq from 'typemoq';
import URI from 'vs/base/common/uri';
import { IMainContext } from 'vs/workbench/api/node/extHost.protocol';
import { ExtHostNotebook } from 'sql/workbench/api/node/extHostNotebook';
import { MainThreadNotebookShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import * as testUtils from '../../utils/testUtils';
import { INotebookManagerDetails } from 'sql/workbench/api/common/sqlExtHostTypes';
suite('ExtHostNotebook Tests', () => {
let extHostNotebook: ExtHostNotebook;
let mockProxy: TypeMoq.Mock<MainThreadNotebookShape>;
let notebookUri: URI;
let notebookProviderMock: TypeMoq.Mock<NotebookProviderStub>;
setup(() => {
mockProxy = TypeMoq.Mock.ofInstance(<MainThreadNotebookShape> {
$registerNotebookProvider: (providerId, handle) => undefined,
$unregisterNotebookProvider: (handle) => undefined,
dispose: () => undefined
});
let mainContext = <IMainContext>{
getProxy: proxyType => mockProxy.object
};
extHostNotebook = new ExtHostNotebook(mainContext);
notebookUri = URI.parse('file:/user/default/my.ipynb');
notebookProviderMock = TypeMoq.Mock.ofType(NotebookProviderStub, TypeMoq.MockBehavior.Loose);
notebookProviderMock.callBase = true;
});
suite('getNotebookManager', () => {
test('Should throw if no matching provider is defined', async () => {
await testUtils.assertThrowsAsync(() => extHostNotebook.$getNotebookManager(-1, notebookUri));
});
suite('with provider', () => {
let providerHandle: number = -1;
setup(() => {
mockProxy.setup(p =>
p.$registerNotebookProvider(TypeMoq.It.isValue(notebookProviderMock.object.providerId), TypeMoq.It.isAnyNumber()))
.returns((providerId, handle) => {
providerHandle = handle;
return undefined;
});
// Register the provider so we can test behavior with this present
extHostNotebook.registerNotebookProvider(notebookProviderMock.object);
});
test('Should return a notebook manager with correct info on content and server manager existence', async () => {
// Given the provider returns a manager with no
let expectedManager = new NotebookManagerStub();
notebookProviderMock.setup(p => p.getNotebookManager(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedManager));
// When I call through using the handle provided during registration
let managerDetails: INotebookManagerDetails = await extHostNotebook.$getNotebookManager(providerHandle, notebookUri);
// Then I expect the same manager to be returned
assert.ok(managerDetails.hasContentManager === false, 'Expect no content manager defined');
assert.ok(managerDetails.hasServerManager === false, 'Expect no server manager defined');
assert.ok(managerDetails.handle > 0, 'Expect a valid handle defined');
});
test('Should have a unique handle for each notebook URI', async () => {
// Given the we request 2 URIs
let expectedManager = new NotebookManagerStub();
notebookProviderMock.setup(p => p.getNotebookManager(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedManager));
// When I call through using the handle provided during registration
let originalManagerDetails = await extHostNotebook.$getNotebookManager(providerHandle, notebookUri);
let differentDetails = await extHostNotebook.$getNotebookManager(providerHandle, URI.parse('file://other/file.ipynb'));
let sameDetails = await extHostNotebook.$getNotebookManager(providerHandle, notebookUri);
// Then I expect the 2 different handles in the managers returned.
// This is because we can't easily track identity of the managers, so just track which one is assigned to
// a notebook by the handle ID
assert.notEqual(originalManagerDetails.handle, differentDetails.handle, 'Should have unique handle for each manager');
assert.equal(originalManagerDetails.handle, sameDetails.handle, 'Should have same handle when same URI is passed in');
});
});
});
suite('registerNotebookProvider', () => {
let savedHandle: number = -1;
setup(() => {
mockProxy.setup(p =>
p.$registerNotebookProvider(TypeMoq.It.isValue(notebookProviderMock.object.providerId), TypeMoq.It.isAnyNumber()))
.returns((providerId, handle) => {
savedHandle = handle;
return undefined;
});
});
test('Should register with a new handle to the proxy', () => {
extHostNotebook.registerNotebookProvider(notebookProviderMock.object);
mockProxy.verify(p =>
p.$registerNotebookProvider(TypeMoq.It.isValue(notebookProviderMock.object.providerId),
TypeMoq.It.isAnyNumber()), TypeMoq.Times.once());
// It shouldn't unregister until requested
mockProxy.verify(p => p.$unregisterNotebookProvider(TypeMoq.It.isValue(savedHandle)), TypeMoq.Times.never());
});
test('Should call unregister on disposing', () => {
let disposable = extHostNotebook.registerNotebookProvider(notebookProviderMock.object);
disposable.dispose();
mockProxy.verify(p => p.$unregisterNotebookProvider(TypeMoq.It.isValue(savedHandle)), TypeMoq.Times.once());
});
});
});
class NotebookProviderStub implements sqlops.nb.NotebookProvider {
providerId: string = 'TestProvider';
getNotebookManager(notebookUri: vscode.Uri): Thenable<sqlops.nb.NotebookManager> {
throw new Error('Method not implemented.');
}
handleNotebookClosed(notebookUri: vscode.Uri): void {
throw new Error('Method not implemented.');
}
}
class NotebookManagerStub implements sqlops.nb.NotebookManager {
get contentManager(): sqlops.nb.ContentManager {
return undefined;
}
get sessionManager(): sqlops.nb.SessionManager {
return undefined;
}
get serverManager(): sqlops.nb.ServerManager {
return undefined;
}
}

View File

@@ -0,0 +1,123 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import * as TypeMoq from 'typemoq';
import URI from 'vs/base/common/uri';
import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol';
import { ExtHostNotebookShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { MainThreadNotebook } from 'sql/workbench/api/node/mainThreadNotebook';
import { NotebookService } from 'sql/services/notebook/notebookServiceImpl';
import { INotebookProvider } from 'sql/services/notebook/notebookService';
import { INotebookManagerDetails } from 'sql/workbench/api/common/sqlExtHostTypes';
import { LocalContentManager } from 'sql/services/notebook/localContentManager';
suite('MainThreadNotebook Tests', () => {
let mainThreadNotebook: MainThreadNotebook;
let mockProxy: TypeMoq.Mock<ExtHostNotebookShape>;
let notebookUri: URI;
let mockNotebookService: TypeMoq.Mock<NotebookService>;
let providerId = 'TestProvider';
setup(() => {
mockProxy = TypeMoq.Mock.ofInstance(<ExtHostNotebookShape> {
$getNotebookManager: (handle, uri) => undefined,
$handleNotebookClosed: (uri) => undefined,
$getNotebookContents: (handle, uri) => undefined,
$save: (handle, uri, notebook) => undefined,
$doStartServer: (handle) => undefined,
$doStopServer: (handle) => undefined
});
let extContext = <IExtHostContext>{
getProxy: proxyType => mockProxy.object
};
mockNotebookService = TypeMoq.Mock.ofType(NotebookService);
notebookUri = URI.parse('file:/user/default/my.ipynb');
mainThreadNotebook = new MainThreadNotebook(extContext, mockNotebookService.object);
});
suite('On registering a provider', () => {
let provider: INotebookProvider;
let registeredProviderId: string;
setup(() => {
mockNotebookService.setup(s => s.registerProvider(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns((id, providerImpl) => {
registeredProviderId = id;
provider = providerImpl;
});
});
test('should call through to notebook service', () => {
// When I register a provider
mainThreadNotebook.$registerNotebookProvider(providerId, 1);
// Then I expect a provider implementation to be passed to the service
mockNotebookService.verify(s => s.registerProvider(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
assert.equal(provider.providerId, providerId);
});
test('should unregister in service', () => {
// Given we have a provider
mainThreadNotebook.$registerNotebookProvider(providerId, 1);
// When I unregister a provider twice
mainThreadNotebook.$unregisterNotebookProvider(1);
mainThreadNotebook.$unregisterNotebookProvider(1);
// Then I expect it to be unregistered in the service just 1 time
mockNotebookService.verify(s => s.unregisterProvider(TypeMoq.It.isValue(providerId)), TypeMoq.Times.once());
});
});
suite('getNotebookManager', () => {
let managerWithAllFeatures: INotebookManagerDetails;
let provider: INotebookProvider;
setup(() => {
managerWithAllFeatures = {
handle: 2,
hasContentManager: true,
hasServerManager: true
};
mockNotebookService.setup(s => s.registerProvider(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns((id, providerImpl) => {
provider = providerImpl;
});
mainThreadNotebook.$registerNotebookProvider(providerId, 1);
});
test('should return manager with default content manager & undefined server manager if extension host has none', async () => {
// Given the extension provider doesn't have acontent or server manager
let details: INotebookManagerDetails = {
handle: 2,
hasContentManager: false,
hasServerManager: false
};
mockProxy.setup(p => p.$getNotebookManager(TypeMoq.It.isAnyNumber(), TypeMoq.It.isValue(notebookUri)))
.returns(() => Promise.resolve(details));
// When I get the notebook manager
let manager = await provider.getNotebookManager(notebookUri);
// Then it should use the built-in content manager
assert.ok(manager.contentManager instanceof LocalContentManager);
// And it should not define a server manager
assert.equal(manager.serverManager, undefined);
});
test('should return manager with a content & server manager if extension host has these', async () => {
// Given the extension provider doesn't have acontent or server manager
mockProxy.setup(p => p.$getNotebookManager(TypeMoq.It.isAnyNumber(), TypeMoq.It.isValue(notebookUri)))
.returns(() => Promise.resolve(managerWithAllFeatures));
// When I get the notebook manager
let manager = await provider.getNotebookManager(notebookUri);
// Then it shouldn't have wrappers for the content or server manager
assert.ok(!(manager.contentManager instanceof LocalContentManager));
assert.notEqual(manager.serverManager, undefined);
});
});
});