From 9691fab91700620f8db8b01b7372ccd5f41ecbcf Mon Sep 17 00:00:00 2001 From: Cory Rivera Date: Wed, 4 Dec 2019 14:45:48 -0800 Subject: [PATCH] Add code coverage tests for cell.ts (#8564) --- .../contrib/notebook/browser/models/cell.ts | 18 +- .../browser/models/modelInterfaces.ts | 3 + .../test/electron-browser/cell.test.ts | 248 +++++++++++++++++- .../notebook/test/electron-browser/common.ts | 181 ++++++++++++- 4 files changed, 429 insertions(+), 21 deletions(-) diff --git a/src/sql/workbench/contrib/notebook/browser/models/cell.ts b/src/sql/workbench/contrib/notebook/browser/models/cell.ts index abc61231f4..46ea0841a5 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/cell.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/cell.ts @@ -80,7 +80,7 @@ export class CellModel implements ICellModel { } public equals(other: ICellModel) { - return other && other.id === this.id; + return other !== undefined && other.id === this.id; } public get onCollapseStateChanged(): Event { @@ -91,10 +91,6 @@ export class CellModel implements ICellModel { return this._onOutputsChanged.event; } - public get onCellModeChanged(): Event { - return this._onCellModeChanged.event; - } - public get isEditMode(): boolean { return this._isEditMode; } @@ -191,17 +187,13 @@ export class CellModel implements ICellModel { } public get notebookModel(): NotebookModel { - return this.options.notebook; + return this._options && this._options.notebook; } public set cellUri(value: URI) { this._cellUri = value; } - public get options(): ICellModelOptions { - return this._options; - } - public get cellType(): CellType { return this._cellType; } @@ -234,7 +226,7 @@ export class CellModel implements ICellModel { if (this._language) { return this._language; } - return this.options.notebook.language; + return this._options.notebook.language; } public get cellGuid(): string { @@ -370,7 +362,7 @@ export class CellModel implements ICellModel { } private async getOrStartKernel(notificationService: INotificationService): Promise { - let model = this.options.notebook; + let model = this._options.notebook; let clientSession = model && model.clientSession; if (!clientSession) { this.sendNotification(notificationService, Severity.Error, localize('notebookNotReady', "The session for this notebook is not yet ready")); @@ -516,7 +508,7 @@ export class CellModel implements ICellModel { try { let result = output as nb.IDisplayResult; if (result && result.data && result.data['text/html']) { - let model = (this as CellModel).options.notebook as NotebookModel; + let model = this._options.notebook as NotebookModel; if (model.activeConnection) { let gatewayEndpointInfo = this.getGatewayEndpoint(model.activeConnection); if (gatewayEndpointInfo) { diff --git a/src/sql/workbench/contrib/notebook/browser/models/modelInterfaces.ts b/src/sql/workbench/contrib/notebook/browser/models/modelInterfaces.ts index eb8f6fb551..3dae44471a 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/modelInterfaces.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/modelInterfaces.ts @@ -418,6 +418,8 @@ export interface INotebookModel { * @param cell New active cell */ updateActiveCell(cell: ICellModel); + + requestConnection(): Promise; } export interface NotebookContentChange { @@ -491,6 +493,7 @@ export interface ICellModel { isCollapsed: boolean; readonly onCollapseStateChanged: Event; modelContentChangedEvent: IModelContentChangedEvent; + isEditMode: boolean; } export interface FutureInternal extends nb.IFuture { diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts index f4197b1805..1caea8e046 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts @@ -11,15 +11,21 @@ import * as objects from 'vs/base/common/objects'; import { CellTypes } from 'sql/workbench/contrib/notebook/common/models/contracts'; import { ModelFactory } from 'sql/workbench/contrib/notebook/browser/models/modelFactory'; -import { NotebookModelStub } from './common'; +import { NotebookModelStub, ClientSessionStub, KernelStub, FutureStub } from './common'; import { EmptyFuture } from 'sql/workbench/services/notebook/browser/sessionManager'; -import { ICellModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces'; +import { ICellModel, ICellModelOptions, IClientSession, INotebookModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces'; import { Deferred } from 'sql/base/common/promise'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { startsWith } from 'vs/base/common/strings'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; +import { Promise } from 'es6-promise'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; let instantiationService: IInstantiationService; @@ -590,4 +596,242 @@ suite('Cell Model', function (): void { }); }); + test('Getters and setters test', async function (): Promise { + // Code Cell + let cellData: nb.ICellContents = { + cell_type: CellTypes.Code, + source: '1+1', + outputs: [], + metadata: { language: 'python' }, + execution_count: 1 + }; + let cell = factory.createCell(cellData, undefined); + + assert.strictEqual(cell.trustedMode, false, 'Cell should not be trusted by default'); + cell.trustedMode = true; + assert.strictEqual(cell.trustedMode, true, 'Cell should be trusted after manually setting trustedMode'); + + assert.strictEqual(cell.isEditMode, true, 'Code cells should be editable by default'); + cell.isEditMode = false; + assert.strictEqual(cell.isEditMode, false, 'Cell should not be editable after manually setting isEditMode'); + + cell.hover = true; + assert.strictEqual(cell.hover, true, 'Cell should be hovered after manually setting hover=true'); + cell.hover = false; + assert.strictEqual(cell.hover, false, 'Cell should be hovered after manually setting hover=false'); + + let cellUri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${cell.id}` }); + assert.deepStrictEqual(cell.cellUri, cellUri); + cellUri = URI.from({ scheme: Schemas.untitled, path: `test-uri-12345` }); + cell.cellUri = cellUri; + assert.deepStrictEqual(cell.cellUri, cellUri); + + assert.strictEqual(cell.language, 'python'); + + assert.strictEqual(cell.notebookModel, undefined); + + assert.strictEqual(cell.modelContentChangedEvent, undefined); + let contentChangedEvent = {}; + cell.modelContentChangedEvent = contentChangedEvent; + assert.strictEqual(cell.modelContentChangedEvent, contentChangedEvent); + + assert.strictEqual(cell.stdInVisible, false, 'Cell stdin should not be visible by default'); + cell.stdInVisible = true; + assert.strictEqual(cell.stdInVisible, true, 'Cell stdin should not be visible by default'); + + cell.loaded = true; + assert.strictEqual(cell.loaded, true, 'Cell should be loaded after manually setting loaded=true'); + cell.loaded = false; + assert.strictEqual(cell.loaded, false, 'Cell should be loaded after manually setting loaded=false'); + + assert.ok(cell.onExecutionStateChange !== undefined, 'onExecutionStateChange event should not be undefined'); + + assert.ok(cell.onLoaded !== undefined, 'onLoaded event should not be undefined'); + + // Markdown cell + cellData = { + cell_type: CellTypes.Markdown, + source: 'some *markdown*', + outputs: [], + metadata: { language: 'python' } + }; + let notebookModel = new NotebookModelStub({ + name: 'python', + version: '', + mimetype: '' + }); + + let cellOptions: ICellModelOptions = { notebook: notebookModel, isTrusted: true }; + cell = factory.createCell(cellData, cellOptions); + + assert.strictEqual(cell.isEditMode, false, 'Markdown cells should not be editable by default'); + assert.strictEqual(cell.trustedMode, true, 'Cell should be trusted when providing isTrusted=true in the cell options'); + assert.strictEqual(cell.language, 'markdown'); + assert.strictEqual(cell.notebookModel, notebookModel); + }); + + test('Equals test', async function (): Promise { + let cell = factory.createCell(undefined, undefined); + + let result = cell.equals(undefined); + assert.strictEqual(result, false, 'Cell should not be equal to undefined'); + + result = cell.equals(cell); + assert.strictEqual(result, true, 'Cell should be equal to itself'); + + let otherCell = factory.createCell(undefined, undefined); + result = cell.equals(otherCell); + assert.strictEqual(result, false, 'Cell should not be equal to a different cell'); + }); + + suite('Run Cell tests', function (): void { + let cellOptions: ICellModelOptions; + let mockClientSession: TypeMoq.Mock; + let mockNotebookModel: TypeMoq.Mock; + let mockKernel: TypeMoq.Mock; + + const codeCellContents: nb.ICellContents = { + cell_type: CellTypes.Code, + source: '1+1', + outputs: [], + metadata: { language: 'python' }, + execution_count: 1 + }; + const markdownCellContents: nb.ICellContents = { + cell_type: CellTypes.Markdown, + source: 'some *markdown*', + outputs: [], + metadata: { language: 'python' } + }; + + setup(() => { + mockKernel = TypeMoq.Mock.ofType(KernelStub); + + mockClientSession = TypeMoq.Mock.ofType(ClientSessionStub); + mockClientSession.setup(s => s.kernel).returns(() => mockKernel.object); + mockClientSession.setup(s => s.isReady).returns(() => true); + + mockNotebookModel = TypeMoq.Mock.ofType(NotebookModelStub); + mockNotebookModel.setup(m => m.clientSession).returns(() => mockClientSession.object); + mockNotebookModel.setup(m => m.updateActiveCell(TypeMoq.It.isAny())); + + cellOptions = { notebook: mockNotebookModel.object, isTrusted: true }; + }); + + test('Run markdown cell', async function (): Promise { + let cell = factory.createCell(markdownCellContents, cellOptions); + let result = await cell.runCell(); + assert.strictEqual(result, false, 'Markdown cells should not be runnable'); + }); + + test('No client session provided', async function (): Promise { + mockNotebookModel.reset(); + mockNotebookModel.setup(m => m.clientSession).returns(() => undefined); + mockNotebookModel.setup(m => m.updateActiveCell(TypeMoq.It.isAny())); + cellOptions.notebook = mockNotebookModel.object; + + let cell = factory.createCell(codeCellContents, cellOptions); + let result = await cell.runCell(); + assert.strictEqual(result, false, 'Running code cell without a client session should fail'); + }); + + test('No Kernel provided', async function (): Promise { + mockClientSession.reset(); + mockClientSession.setup(s => s.kernel).returns(() => null); + mockClientSession.setup(s => s.isReady).returns(() => true); + mockNotebookModel.reset(); + mockNotebookModel.setup(m => m.defaultKernel).returns(() => null); + mockNotebookModel.setup(m => m.clientSession).returns(() => mockClientSession.object); + mockNotebookModel.setup(m => m.updateActiveCell(TypeMoq.It.isAny())); + cellOptions.notebook = mockNotebookModel.object; + + let cell = factory.createCell(codeCellContents, cellOptions); + let result = await cell.runCell(); + assert.strictEqual(result, false, 'Running code cell without a kernel should fail'); + }); + + test('Kernel fails to connect', async function (): Promise { + mockKernel.setup(k => k.requiresConnection).returns(() => true); + mockNotebookModel.setup(m => m.requestConnection()).returns(() => Promise.resolve(false)); + + let cell = factory.createCell(codeCellContents, cellOptions); + let result = await cell.runCell(); + assert.strictEqual(result, false, 'Running code cell should fail after connection fails'); + }); + + test('Normal execute', async function (): Promise { + mockKernel.setup(k => k.requiresConnection).returns(() => false); + mockKernel.setup(k => k.requestExecute(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { + let replyMsg: nb.IExecuteReplyMsg = { + content: { + execution_count: 1, + status: 'ok' + } + }; + + return new FutureStub(undefined, Promise.resolve(replyMsg)); + }); + + let cell = factory.createCell(codeCellContents, cellOptions); + let result = await cell.runCell(); + assert.strictEqual(result, true, 'Running normal code cell should succeed'); + }); + + test('Execute returns error status', async function (): Promise { + mockKernel.setup(k => k.requiresConnection).returns(() => false); + mockKernel.setup(k => k.requestExecute(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { + let replyMsg: nb.IExecuteReplyMsg = { + content: { + execution_count: 1, + status: 'error' + } + }; + + return new FutureStub(undefined, Promise.resolve(replyMsg)); + }); + + let cell = factory.createCell(codeCellContents, cellOptions); + let result = await cell.runCell(); + assert.strictEqual(result, false, 'Run cell should fail if execute returns error status'); + }); + + test('Execute returns abort status', async function (): Promise { + mockKernel.setup(k => k.requiresConnection).returns(() => false); + mockKernel.setup(k => k.requestExecute(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { + let replyMsg: nb.IExecuteReplyMsg = { + content: { + execution_count: 1, + status: 'abort' + } + }; + + return new FutureStub(undefined, Promise.resolve(replyMsg)); + }); + + let cell = factory.createCell(codeCellContents, cellOptions); + let result = await cell.runCell(); + assert.strictEqual(result, false, 'Run cell should fail if execute returns abort status'); + }); + + test('Execute throws exception', async function (): Promise { + let testMsg = 'Test message'; + mockKernel.setup(k => k.requiresConnection).returns(() => false); + mockKernel.setup(k => k.requestExecute(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { + throw new Error(testMsg); + }); + + let actualMsg: string; + let mockNotification = TypeMoq.Mock.ofType(TestNotificationService); + mockNotification.setup(n => n.notify(TypeMoq.It.isAny())).returns(notification => { + actualMsg = notification.message; + return undefined; + }); + + let cell = factory.createCell(codeCellContents, cellOptions); + let result = await cell.runCell(mockNotification.object); + assert.strictEqual(result, true, 'Run cell should report errors via notification service'); + assert.ok(actualMsg !== undefined, 'Should have received an error notification'); + assert.strictEqual(actualMsg, testMsg); + }); + }); }); diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/common.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/common.ts index 74afa99ed1..31244a2aac 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/common.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/common.ts @@ -6,7 +6,7 @@ import { nb, IConnectionProfile } from 'azdata'; import { Event, Emitter } from 'vs/base/common/event'; -import { INotebookModel, ICellModel, IClientSession, IDefaultConnection, NotebookContentChange } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces'; +import { INotebookModel, ICellModel, IClientSession, IDefaultConnection, NotebookContentChange, IKernelPreference } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces'; import { NotebookChangeType, CellType } from 'sql/workbench/contrib/notebook/common/models/contracts'; import { INotebookManager, INotebookService, INotebookEditor, ILanguageMagic, INotebookProvider, INavigationProvider } from 'sql/workbench/services/notebook/browser/notebookService'; import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes'; @@ -17,11 +17,11 @@ import { RenderMimeRegistry } from 'sql/workbench/contrib/notebook/browser/outpu export class NotebookModelStub implements INotebookModel { constructor(private _languageInfo?: nb.ILanguageInfo) { } - public trustedMode: boolean; + trustedMode: boolean; language: string; standardKernels: IStandardKernelWithProvider[]; - public get languageInfo(): nb.ILanguageInfo { + get languageInfo(): nb.ILanguageInfo { return this._languageInfo; } onCellChange(cell: ICellModel, change: NotebookChangeType): void { @@ -114,7 +114,9 @@ export class NotebookModelStub implements INotebookModel { updateActiveCell(cell: ICellModel) { throw new Error('Method not implemented.'); } - + requestConnection(): Promise { + throw new Error('Method not implemented.'); + } } export class NotebookManagerStub implements INotebookManager { @@ -125,12 +127,12 @@ export class NotebookManagerStub implements INotebookManager { } export class ServerManagerStub implements nb.ServerManager { - public onServerStartedEmitter = new Emitter(); + onServerStartedEmitter = new Emitter(); onServerStarted: Event = this.onServerStartedEmitter.event; isStarted: boolean = false; calledStart: boolean = false; calledEnd: boolean = false; - public result: Promise = undefined; + result: Promise = undefined; startServer(): Promise { this.calledStart = true; @@ -202,3 +204,170 @@ export class NotebookServiceStub implements INotebookService { throw new Error('Method not implemented.'); } } + +export class ClientSessionStub implements IClientSession { + initialize(): Promise { + throw new Error('Method not implemented.'); + } + changeKernel(options: nb.IKernelSpec, oldKernel?: nb.IKernel): Promise { + throw new Error('Method not implemented.'); + } + configureKernel(options: nb.IKernelSpec): Promise { + throw new Error('Method not implemented.'); + } + shutdown(): Promise { + throw new Error('Method not implemented.'); + } + selectKernel(): Promise { + throw new Error('Method not implemented.'); + } + restart(): Promise { + throw new Error('Method not implemented.'); + } + setPath(path: string): Promise { + throw new Error('Method not implemented.'); + } + setName(name: string): Promise { + throw new Error('Method not implemented.'); + } + setType(type: string): Promise { + throw new Error('Method not implemented.'); + } + updateConnection(connection: IConnectionProfile): Promise { + throw new Error('Method not implemented.'); + } + onKernelChanging(changeHandler: (kernel: nb.IKernelChangedArgs) => Promise): void { + throw new Error('Method not implemented.'); + } + dispose(): void { + throw new Error('Method not implemented.'); + } + get terminated(): Event { + throw new Error('Method not implemented.'); + } + get kernelChanged(): Event { + throw new Error('Method not implemented.'); + } + get statusChanged(): Event { + throw new Error('Method not implemented.'); + } + get iopubMessage(): Event { + throw new Error('Method not implemented.'); + } + get unhandledMessage(): Event { + throw new Error('Method not implemented.'); + } + get propertyChanged(): Event<'path' | 'name' | 'type'> { + throw new Error('Method not implemented.'); + } + get kernel(): nb.IKernel | null { + throw new Error('Method not implemented.'); + } + get notebookUri(): URI { + throw new Error('Method not implemented.'); + } + get name(): string { + throw new Error('Method not implemented.'); + } + get type(): string { + throw new Error('Method not implemented.'); + } + get status(): nb.KernelStatus { + throw new Error('Method not implemented.'); + } + get isReady(): boolean { + throw new Error('Method not implemented.'); + } + get ready(): Promise { + throw new Error('Method not implemented.'); + } + get kernelChangeCompleted(): Promise { + throw new Error('Method not implemented.'); + } + get kernelPreference(): IKernelPreference { + throw new Error('Method not implemented.'); + } + set kernelPreference(value: IKernelPreference) { + throw new Error('Method not implemented.'); + } + get kernelDisplayName(): string { + throw new Error('Method not implemented.'); + } + get errorMessage(): string { + throw new Error('Method not implemented.'); + } + get isInErrorState(): boolean { + throw new Error('Method not implemented.'); + } + get cachedKernelSpec(): nb.IKernelSpec { + throw new Error('Method not implemented.'); + } +} + +export class KernelStub implements nb.IKernel { + get id(): string { + throw new Error('Method not implemented.'); + } + get name(): string { + throw new Error('Method not implemented.'); + } + get supportsIntellisense(): boolean { + throw new Error('Method not implemented.'); + } + get requiresConnection(): boolean { + throw new Error('Method not implemented.'); + } + get isReady(): boolean { + throw new Error('Method not implemented.'); + } + get ready(): Thenable { + throw new Error('Method not implemented.'); + } + get info(): nb.IInfoReply { + throw new Error('Method not implemented.'); + } + getSpec(): Thenable { + throw new Error('Method not implemented.'); + } + requestExecute(content: nb.IExecuteRequest, disposeOnDone?: boolean): nb.IFuture { + throw new Error('Method not implemented.'); + } + requestComplete(content: nb.ICompleteRequest): Thenable { + throw new Error('Method not implemented.'); + } + interrupt(): Thenable { + throw new Error('Method not implemented.'); + } +} + +export class FutureStub implements nb.IFuture { + constructor(private _msg: nb.IMessage, private _done: Thenable) { + } + get msg(): nb.IMessage { + return this._msg; + } + get done(): Thenable { + return this._done; + } + setReplyHandler(handler: nb.MessageHandler): void { + return; + } + setStdInHandler(handler: nb.MessageHandler): void { + return; + } + setIOPubHandler(handler: nb.MessageHandler): void { + return; + } + registerMessageHook(hook: (msg: nb.IIOPubMessage) => boolean | Thenable): void { + return; + } + removeMessageHook(hook: (msg: nb.IIOPubMessage) => boolean | Thenable): void { + return; + } + sendInputReply(content: nb.IInputReply): void { + return; + } + dispose() { + return; + } +}