From 8d8be27f225ef1f59c275231d269d0d840568f66 Mon Sep 17 00:00:00 2001 From: Kevin Cunnane Date: Tue, 4 Dec 2018 10:01:10 -0800 Subject: [PATCH] Add basic notebook model tests (#3396) - Ported from the extension - Only adding tests that related to the internally implemented functionality, not to anything provider-specific. --- package.json | 3 + src/sql/services/notebook/sessionManager.ts | 4 +- src/sqltest/parts/notebook/common.ts | 95 +++++++ src/sqltest/parts/notebook/model/cell.test.ts | 262 ++++++++++++++++++ .../notebook/model/clientSession.test.ts | 202 ++++++++++++++ .../notebook/model/contentManagers.test.ts | 77 +++++ .../notebook/model/notebookModel.test.ts | 251 +++++++++++++++++ src/typings/should.d.ts | 238 ++++++++++++++++ src/typings/temp-write.d.ts | 3 + yarn.lock | 82 +++++- 10 files changed, 1214 insertions(+), 3 deletions(-) create mode 100644 src/sqltest/parts/notebook/common.ts create mode 100644 src/sqltest/parts/notebook/model/cell.test.ts create mode 100644 src/sqltest/parts/notebook/model/clientSession.test.ts create mode 100644 src/sqltest/parts/notebook/model/contentManagers.test.ts create mode 100644 src/sqltest/parts/notebook/model/notebookModel.test.ts create mode 100755 src/typings/should.d.ts create mode 100644 src/typings/temp-write.d.ts diff --git a/package.json b/package.json index 33458d4b6b..c56d05d1e8 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "@types/mocha": "2.2.39", "@types/sanitize-html": "^1.18.2", "@types/semver": "5.3.30", + "@types/should": "^13.0.0", "@types/sinon": "1.16.34", "@types/winreg": "^1.2.30", "asar": "^0.14.0", @@ -148,8 +149,10 @@ "queue": "3.0.6", "remap-istanbul": "^0.6.4", "rimraf": "^2.2.8", + "should": "^13.2.3", "sinon": "^1.17.2", "source-map": "^0.4.4", + "temp-write": "^3.4.0", "tslint": "^5.9.1", "typemoq": "^0.3.2", "typescript": "2.9.2", diff --git a/src/sql/services/notebook/sessionManager.ts b/src/sql/services/notebook/sessionManager.ts index 49fe3e9f37..7d24f015cc 100644 --- a/src/sql/services/notebook/sessionManager.ts +++ b/src/sql/services/notebook/sessionManager.ts @@ -40,7 +40,7 @@ export class SessionManager implements nb.SessionManager { } } -class EmptySession implements nb.ISession { +export class EmptySession implements nb.ISession { private _kernel: EmptyKernel; private _defaultKernelLoaded = false; @@ -146,7 +146,7 @@ class EmptyKernel implements nb.IKernel { } } -class EmptyFuture implements FutureInternal { +export class EmptyFuture implements FutureInternal { get inProgress(): boolean { diff --git a/src/sqltest/parts/notebook/common.ts b/src/sqltest/parts/notebook/common.ts new file mode 100644 index 0000000000..ae50b1fc55 --- /dev/null +++ b/src/sqltest/parts/notebook/common.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { nb, IConnectionProfile } from 'sqlops'; + +import { Event, Emitter } from 'vs/base/common/event'; +import { INotebookModel, ICellModel, IClientSession, IDefaultConnection } from 'sql/parts/notebook/models/modelInterfaces'; +import { NotebookChangeType, CellType } from 'sql/parts/notebook/models/contracts'; +import { INotebookManager } from 'sql/services/notebook/notebookService'; + +export class NotebookModelStub implements INotebookModel { + constructor(private _languageInfo?: nb.ILanguageInfo) { + } + public trustedMode: boolean; + + public get languageInfo(): nb.ILanguageInfo { + return this._languageInfo; + } + onCellChange(cell: ICellModel, change: NotebookChangeType): void { + // Default: do nothing + } + get cells(): ReadonlyArray { + throw new Error('method not implemented.'); + } + get clientSession(): IClientSession { + throw new Error('method not implemented.'); + } + get notebookManager(): INotebookManager { + throw new Error('method not implemented.'); + } + get kernelChanged(): Event { + throw new Error('method not implemented.'); + } + get kernelsChanged(): Event { + throw new Error('method not implemented.'); + } get defaultKernel(): nb.IKernelSpec { + throw new Error('method not implemented.'); + } + get contextsChanged(): Event { + throw new Error('method not implemented.'); + } + get specs(): nb.IAllKernels { + throw new Error('method not implemented.'); + } + get contexts(): IDefaultConnection { + throw new Error('method not implemented.'); + } + changeKernel(displayName: string): void { + throw new Error('Method not implemented.'); + } + changeContext(host: string, connection?: IConnectionProfile): void { + throw new Error('Method not implemented.'); + } + findCellIndex(cellModel: ICellModel): number { + throw new Error('Method not implemented.'); + } + addCell(cellType: CellType, index?: number): void { + throw new Error('Method not implemented.'); + } + deleteCell(cellModel: ICellModel): void { + throw new Error('Method not implemented.'); + } + saveModel(): Promise { + throw new Error('Method not implemented.'); + } +} + +export class NotebookManagerStub implements INotebookManager { + providerId: string; + contentManager: nb.ContentManager; + sessionManager: nb.SessionManager; + serverManager: nb.ServerManager; +} + +export class ServerManagerStub implements nb.ServerManager { + public onServerStartedEmitter = new Emitter(); + onServerStarted: Event = this.onServerStartedEmitter.event; + isStarted: boolean = false; + calledStart: boolean = false; + calledEnd: boolean = false; + public result: Promise = undefined; + + startServer(): Promise { + this.calledStart = true; + return this.result; + } + stopServer(): Promise { + this.calledEnd = true; + return this.result; + } +} \ No newline at end of file diff --git a/src/sqltest/parts/notebook/model/cell.test.ts b/src/sqltest/parts/notebook/model/cell.test.ts new file mode 100644 index 0000000000..5136e4f695 --- /dev/null +++ b/src/sqltest/parts/notebook/model/cell.test.ts @@ -0,0 +1,262 @@ +/*--------------------------------------------------------------------------------------------- + * 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 should from 'should'; +import * as TypeMoq from 'typemoq'; +import { nb } from 'sqlops'; + +import * as objects from 'vs/base/common/objects'; + +import { CellTypes } from 'sql/parts/notebook/models/contracts'; +import { ModelFactory } from 'sql/parts/notebook/models/modelFactory'; +import { NotebookModelStub } from '../common'; +import { EmptyFuture } from 'sql/services/notebook/sessionManager'; +import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; + +describe('Cell Model', function (): void { + let factory = new ModelFactory(); + it('Should set default values if none defined', async function (): Promise { + let cell = factory.createCell(undefined, undefined); + should(cell.cellType).equal(CellTypes.Code); + should(cell.source).equal(''); + }); + + it('Should update values', async function (): Promise { + let cell = factory.createCell(undefined, undefined); + cell.language = 'sql'; + should(cell.language).equal('sql'); + cell.source = 'abcd'; + should(cell.source).equal('abcd'); + }); + + it('Should match ICell values if defined', async function (): Promise { + let output: nb.IStreamResult = { + output_type: 'stream', + text: 'Some output', + name: 'stdout' + }; + let cellData: nb.ICell = { + cell_type: CellTypes.Markdown, + source: 'some *markdown*', + outputs: [output], + metadata: { language: 'python'}, + execution_count: 1 + }; + let cell = factory.createCell(cellData, undefined); + should(cell.cellType).equal(cellData.cell_type); + should(cell.source).equal(cellData.source); + should(cell.outputs).have.length(1); + should(cell.outputs[0].output_type).equal('stream'); + should((cell.outputs[0]).text).equal('Some output'); + }); + + + it('Should set cell language to python if defined as python in languageInfo', async function (): Promise { + let cellData: nb.ICell = { + cell_type: CellTypes.Code, + source: 'print(\'1\')', + metadata: { language: 'python'}, + execution_count: 1 + }; + + let notebookModel = new NotebookModelStub({ + name: 'python', + version: '', + mimetype: '' + }); + let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false }); + should(cell.language).equal('python'); + }); + + it('Should set cell language to python if defined as pyspark in languageInfo', async function (): Promise { + let cellData: nb.ICell = { + cell_type: CellTypes.Code, + source: 'print(\'1\')', + metadata: { language: 'python'}, + execution_count: 1 + }; + + let notebookModel = new NotebookModelStub({ + name: 'pyspark', + version: '', + mimetype: '' + }); + let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false }); + should(cell.language).equal('python'); + }); + + it('Should set cell language to scala if defined as scala in languageInfo', async function (): Promise { + let cellData: nb.ICell = { + cell_type: CellTypes.Code, + source: 'print(\'1\')', + metadata: { language: 'python'}, + execution_count: 1 + }; + + let notebookModel = new NotebookModelStub({ + name: 'scala', + version: '', + mimetype: '' + }); + let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false }); + should(cell.language).equal('scala'); + }); + + it('Should set cell language to python if no language defined', async function (): Promise { + let cellData: nb.ICell = { + cell_type: CellTypes.Code, + source: 'print(\'1\')', + metadata: { language: 'python'}, + execution_count: 1 + }; + + let notebookModel = new NotebookModelStub({ + name: '', + version: '', + mimetype: '' + }); + let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false }); + should(cell.language).equal('python'); + }); + + it('Should match cell language to language specified if unknown language defined in languageInfo', async function (): Promise { + let cellData: nb.ICell = { + cell_type: CellTypes.Code, + source: 'std::cout << "hello world";', + metadata: { language: 'python'}, + execution_count: 1 + }; + + let notebookModel = new NotebookModelStub({ + name: 'cplusplus', + version: '', + mimetype: '' + }); + let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false }); + should(cell.language).equal('cplusplus'); + }); + + it('Should match cell language to mimetype name is not supplied in languageInfo', async function (): Promise { + let cellData: nb.ICell = { + cell_type: CellTypes.Code, + source: 'print(\'1\')', + metadata: { language: 'python'}, + execution_count: 1 + }; + + let notebookModel = new NotebookModelStub({ + name: '', + version: '', + mimetype: 'x-scala' + }); + let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false }); + should(cell.language).equal('scala'); + }); + + describe('Model Future handling', function(): void { + let future: TypeMoq.Mock; + let cell: ICellModel; + beforeEach(() => { + future = TypeMoq.Mock.ofType(EmptyFuture); + cell = factory.createCell({ + cell_type: CellTypes.Code, + source: 'print "Hello"', + metadata: { language: 'python'}, + execution_count: 1 + }, { + notebook: new NotebookModelStub({ + name: '', + version: '', + mimetype: 'x-scala' + }), + isTrusted: false + }); + }); + + it('should send and handle incoming messages', async () => { + // Given a future + let onReply: nb.MessageHandler; + let onIopub: nb.MessageHandler; + future.setup(f => f.setReplyHandler(TypeMoq.It.isAny())).callback((handler) => onReply = handler); + future.setup(f => f.setIOPubHandler(TypeMoq.It.isAny())).callback((handler) => onIopub = handler); + let outputs: ReadonlyArray = undefined; + cell.onOutputsChanged((o => outputs = o)); + + // When I set it on the cell + cell.setFuture(future.object); + + // Then I expect outputs to have been cleared + should(outputs).have.length(0); + should(onReply).not.be.undefined(); + // ... And when I send an IoPub message + let message: nb.IIOPubMessage = { + channel: 'iopub', + type: 'iopub', + parent_header: undefined, + metadata: undefined, + header: { + msg_type: 'stream' + }, + content: { + text: 'Printed hello world' + } + }; + onIopub.handle(message); + // Then I expect an output to be added + should(outputs).have.length(1); + should(outputs[0].output_type).equal('stream'); + + message = objects.deepClone(message); + message.header.msg_type = 'display_data'; + onIopub.handle(message); + should(outputs[1].output_type).equal('display_data'); + + // ... TODO: And when I sent a reply I expect it to be processed. + }); + + it('should delete transient tag while handling incoming messages', async () => { + // Given a future + let onIopub: nb.MessageHandler; + future.setup(f => f.setIOPubHandler(TypeMoq.It.isAny())).callback((handler) => onIopub = handler); + let outputs: ReadonlyArray = undefined; + cell.onOutputsChanged((o => outputs = o)); + + //Set the future + cell.setFuture(future.object); + + // ... And when I send an IoPub message + let message: nb.IIOPubMessage = { + channel: 'iopub', + type: 'iopub', + parent_header: undefined, + metadata: undefined, + header: { + msg_type: 'display_data' + }, + content: { + text: 'Printed hello world', + transient: 'transient data' + } + }; + onIopub.handle(message); + //Output array's length should be 1 + //'transient' tag should no longer exist in the output + should(outputs).have.length(1); + should(outputs[0]['transient']).be.undefined(); + }); + + it('should dispose old future', async () => { + let oldFuture = TypeMoq.Mock.ofType(EmptyFuture); + cell.setFuture(oldFuture.object); + + cell.setFuture(future.object); + + oldFuture.verify(f => f.dispose(), TypeMoq.Times.once()); + }); + }); + +}); diff --git a/src/sqltest/parts/notebook/model/clientSession.test.ts b/src/sqltest/parts/notebook/model/clientSession.test.ts new file mode 100644 index 0000000000..54668f4c94 --- /dev/null +++ b/src/sqltest/parts/notebook/model/clientSession.test.ts @@ -0,0 +1,202 @@ + +/*--------------------------------------------------------------------------------------------- + * 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 should from 'should'; +import * as TypeMoq from 'typemoq'; +import { nb } from 'sqlops'; + +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import URI from 'vs/base/common/uri'; + +import { ClientSession } from 'sql/parts/notebook/models/clientSession'; +import { SessionManager, EmptySession } from 'sql/services/notebook/sessionManager'; +import { NotebookManagerStub, ServerManagerStub } from 'sqltest/parts/notebook/common'; + +describe('Client Session', function(): void { + let path = URI.file('my/notebook.ipynb'); + let notebookManager: NotebookManagerStub; + let serverManager: ServerManagerStub; + let mockSessionManager: TypeMoq.Mock; + let notificationService: TypeMoq.Mock; + let session: ClientSession; + let remoteSession: ClientSession; + + beforeEach(() => { + serverManager = new ServerManagerStub(); + mockSessionManager = TypeMoq.Mock.ofType(SessionManager); + notebookManager = new NotebookManagerStub(); + notebookManager.serverManager = serverManager; + notebookManager.sessionManager = mockSessionManager.object; + notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); + + session = new ClientSession({ + notebookManager: notebookManager, + notebookUri: path, + notificationService: notificationService.object + }); + + let serverlessNotebookManager = new NotebookManagerStub(); + serverlessNotebookManager.sessionManager = mockSessionManager.object; + remoteSession = new ClientSession({ + notebookManager: serverlessNotebookManager, + notebookUri: path, + notificationService: notificationService.object + }); + }); + + it('Should set path, isReady and ready on construction', function(): void { + should(session.notebookUri).equal(path); + should(session.ready).not.be.undefined(); + should(session.isReady).be.false(); + should(session.status).equal('starting'); + should(session.isInErrorState).be.false(); + should(session.errorMessage).be.undefined(); + }); + + it('Should call on serverManager startup if set', async function(): Promise { + // Given I have a serverManager that starts successfully + serverManager.result = Promise.resolve(); + should(session.isReady).be.false(); + + // When I kick off initialization + await session.initialize(); + + // Then I expect ready to be completed too + await session.ready; + should(serverManager.calledStart).be.true(); + should(session.isReady).be.true(); + }); + + it('Should go to error state if serverManager startup fails', async function(): Promise { + // Given I have a serverManager that fails to start + serverManager.result = Promise.reject('error'); + should(session.isInErrorState).be.false(); + + // When I initialize + await session.initialize(); + + // Then I expect ready to complete, but isInErrorState to be true + await session.ready; + should(session.isReady).be.true(); + should(serverManager.calledStart).be.true(); + should(session.isInErrorState).be.true(); + should(session.errorMessage).equal('error'); + }); + + it('Should be ready when session manager is ready', async function(): Promise { + serverManager.result = new Promise((resolve) => { + serverManager.isStarted = true; + resolve(); + }); + mockSessionManager.setup(s => s.ready).returns(() => Promise.resolve()); + + // When I call initialize + await session.initialize(); + + // Then + should(session.isReady).be.true(); + should(session.isInErrorState).be.false(); + await session.ready; + }); + + it('Should be in error state if server fails to start', async function(): Promise { + serverManager.result = new Promise((resolve) => { + serverManager.isStarted = false; + resolve(); + }); + mockSessionManager.setup(s => s.ready).returns(() => Promise.resolve()); + + // When I call initialize + await session.initialize(); + + // Then + await session.ready; + should(session.isReady).be.true(); + should(session.isInErrorState).be.true(); + }); + + it('Should go to error state if sessionManager fails', async function(): Promise { + serverManager.isStarted = true; + mockSessionManager.setup(s => s.isReady).returns(() => false); + mockSessionManager.setup(s => s.ready).returns(() => Promise.reject('error')); + + // When I call initialize + await session.initialize(); + + // Then + should(session.isReady).be.true(); + should(session.isInErrorState).be.true(); + should(session.errorMessage).equal('error'); + }); + + it('Should start session automatically if kernel preference requests it', async function(): Promise { + serverManager.isStarted = true; + mockSessionManager.setup(s => s.ready).returns(() => Promise.resolve()); + let sessionMock = TypeMoq.Mock.ofType(EmptySession); + let startOptions: nb.ISessionOptions = undefined; + mockSessionManager.setup(s => s.startNew(TypeMoq.It.isAny())).returns((options) => { + startOptions = options; + return Promise.resolve(sessionMock.object); + }); + + // When I call initialize after defining kernel preferences + session.kernelPreference = { + shouldStart: true, + name: 'python' + }; + await session.initialize(); + + // Then + should(session.isReady).be.true(); + should(session.isInErrorState).be.false(); + should(startOptions.kernelName).equal('python'); + should(startOptions.path).equal(path.fsPath); + }); + + it('Should shutdown session even if no serverManager is set', async function(): Promise { + // Given a session against a remote server + let expectedId = 'abc'; + mockSessionManager.setup(s => s.isReady).returns(() => true); + mockSessionManager.setup(s => s.shutdown(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + let sessionMock = TypeMoq.Mock.ofType(EmptySession); + sessionMock.setup(s => s.id).returns(() => expectedId); + mockSessionManager.setup(s => s.startNew(TypeMoq.It.isAny())).returns(() => Promise.resolve(sessionMock.object)); + + remoteSession.kernelPreference = { + shouldStart: true, + name: 'python' + }; + await remoteSession.initialize(); + + // When I call shutdown + await remoteSession.shutdown(); + + // Then + mockSessionManager.verify(s => s.shutdown(TypeMoq.It.isValue(expectedId)), TypeMoq.Times.once()); + }); + + + it('Should stop server if server is set', async function(): Promise { + // Given a kernel has been started + serverManager.isStarted = true; + serverManager.result = Promise.resolve(); + mockSessionManager.setup(s => s.isReady).returns(() => true); + mockSessionManager.setup(s => s.shutdown(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + + await session.initialize(); + + // When I call shutdown + await session.shutdown(); + + // Then + should(serverManager.calledEnd).be.true(); + }); + + +}); diff --git a/src/sqltest/parts/notebook/model/contentManagers.test.ts b/src/sqltest/parts/notebook/model/contentManagers.test.ts new file mode 100644 index 0000000000..d5cc5de0a0 --- /dev/null +++ b/src/sqltest/parts/notebook/model/contentManagers.test.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * 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 should from 'should'; +import * as TypeMoq from 'typemoq'; +import * as path from 'path'; +import { nb } from 'sqlops'; + +import URI from 'vs/base/common/uri'; +import * as tempWrite from 'temp-write'; +import { LocalContentManager } from 'sql/services/notebook/localContentManager'; +import * as testUtils from '../../../utils/testUtils'; +import { CellTypes } from 'sql/parts/notebook/models/contracts'; + +let expectedNotebookContent: nb.INotebook = { + cells: [{ + cell_type: CellTypes.Code, + source: 'insert into t1 values (c1, c2)', + metadata: { language: 'python' }, + execution_count: 1 + }], + metadata: { + kernelspec: { + name: 'mssql', + language: 'sql' + } + }, + nbformat: 5, + nbformat_minor: 0 +}; +let notebookContentString = JSON.stringify(expectedNotebookContent); + +function verifyMatchesExpectedNotebook(notebook: nb.INotebook): void { + should(notebook.cells).have.length(1, 'Expected 1 cell'); + should(notebook.cells[0].cell_type).equal(CellTypes.Code); + should(notebook.cells[0].source).equal(expectedNotebookContent.cells[0].source); + should(notebook.metadata.kernelspec.name).equal(expectedNotebookContent.metadata.kernelspec.name); + should(notebook.nbformat).equal(expectedNotebookContent.nbformat); + should(notebook.nbformat_minor).equal(expectedNotebookContent.nbformat_minor); +} + +describe('Local Content Manager', function(): void { + let contentManager = new LocalContentManager(); + + it('Should return undefined if path is undefined', async function(): Promise { + let content = await contentManager.getNotebookContents(undefined); + should(content).be.undefined(); + // tslint:disable-next-line:no-null-keyword + content = await contentManager.getNotebookContents(null); + should(content).be.undefined(); + }); + + it('Should throw if file does not exist', async function(): Promise { + await testUtils.assertThrowsAsync(async () => await contentManager.getNotebookContents(URI.file('/path/doesnot/exist.ipynb')), undefined); + }); + it('Should return notebook contents parsed as INotebook when valid notebook file parsed', async function(): Promise { + // Given a file containing a valid notebook + let localFile = tempWrite.sync(notebookContentString, 'notebook.ipynb'); + // when I read the content + let notebook = await contentManager.getNotebookContents(URI.file(localFile)); + // then I expect notebook format to match + verifyMatchesExpectedNotebook(notebook); + }); + it('Should ignore invalid content in the notebook file', async function(): Promise { + // Given a file containing a notebook with some garbage properties + let invalidContent = notebookContentString + '\\nasddfdsafasdf'; + let localFile = tempWrite.sync(invalidContent, 'notebook.ipynb'); + // when I read the content + let notebook = await contentManager.getNotebookContents(URI.file(localFile)); + // then I expect notebook format to still be valid + verifyMatchesExpectedNotebook(notebook); + }); +}); diff --git a/src/sqltest/parts/notebook/model/notebookModel.test.ts b/src/sqltest/parts/notebook/model/notebookModel.test.ts new file mode 100644 index 0000000000..076c98088e --- /dev/null +++ b/src/sqltest/parts/notebook/model/notebookModel.test.ts @@ -0,0 +1,251 @@ +/*--------------------------------------------------------------------------------------------- + * 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 should from 'should'; +import * as TypeMoq from 'typemoq'; +import { nb } from 'sqlops'; + +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import URI from 'vs/base/common/uri'; + +import { LocalContentManager } from 'sql/services/notebook/localContentManager'; +import * as testUtils from '../../../utils/testUtils'; +import { NotebookManagerStub } from '../common'; +import { NotebookModel } from 'sql/parts/notebook/models/notebookModel'; +import { ModelFactory } from 'sql/parts/notebook/models/modelFactory'; +import { IClientSession, ICellModel, INotebookModelOptions } from 'sql/parts/notebook/models/modelInterfaces'; +import { ClientSession } from 'sql/parts/notebook/models/clientSession'; +import { CellTypes } from 'sql/parts/notebook/models/contracts'; +import { Deferred } from 'sql/base/common/promise'; +import { ConnectionManagementService } from 'sql/parts/connection/common/connectionManagementService'; +import { Memento } from 'vs/workbench/common/memento'; +import { Emitter } from 'vs/base/common/event'; + +let expectedNotebookContent: nb.INotebook = { + cells: [{ + cell_type: CellTypes.Code, + source: 'insert into t1 values (c1, c2)', + metadata: { language: 'python' }, + execution_count: 1 + }, { + cell_type: CellTypes.Markdown, + source: 'I am *markdown*', + metadata: { language: 'python' }, + execution_count: 1 + }], + metadata: { + kernelspec: { + name: 'mssql', + language: 'sql' + } + }, + nbformat: 5, + nbformat_minor: 0 +}; + +let expectedNotebookContentOneCell: nb.INotebook = { + cells: [{ + cell_type: CellTypes.Code, + source: 'insert into t1 values (c1, c2)', + metadata: { language: 'python' }, + execution_count: 1 + }], + metadata: { + kernelspec: { + name: 'mssql', + language: 'sql' + } + }, + nbformat: 5, + nbformat_minor: 0 +}; + +let defaultUri = URI.file('/some/path.ipynb'); + +let mockClientSession: TypeMoq.Mock; +let sessionReady: Deferred; +let mockModelFactory: TypeMoq.Mock; +let notificationService: TypeMoq.Mock; + +describe('notebook model', function(): void { + let notebookManager = new NotebookManagerStub(); + let memento: TypeMoq.Mock; + let queryConnectionService: TypeMoq.Mock; + let defaultModelOptions: INotebookModelOptions; + beforeEach(() => { + sessionReady = new Deferred(); + notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); + memento = TypeMoq.Mock.ofType(Memento, TypeMoq.MockBehavior.Loose, ''); + memento.setup(x => x.getMemento(TypeMoq.It.isAny())).returns(() => void 0); + queryConnectionService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, memento.object, undefined); + queryConnectionService.callBase = true; + defaultModelOptions = { + notebookUri: defaultUri, + factory: new ModelFactory(), + notebookManager, + notificationService: notificationService.object, + connectionService: queryConnectionService.object }; + mockClientSession = TypeMoq.Mock.ofType(ClientSession, undefined, defaultModelOptions); + mockClientSession.setup(c => c.initialize(TypeMoq.It.isAny())).returns(() => { + return Promise.resolve(); + }); + mockClientSession.setup(c => c.ready).returns(() => sessionReady.promise); + mockModelFactory = TypeMoq.Mock.ofType(ModelFactory); + mockModelFactory.callBase = true; + mockModelFactory.setup(f => f.createClientSession(TypeMoq.It.isAny())).returns(() => { + return mockClientSession.object; + }); + }); + + it('Should create single cell if model has no contents', async function(): Promise { + // Given an empty notebook + let emptyNotebook: nb.INotebook = { + cells: [], + metadata: { + kernelspec: { + name: 'mssql', + language: 'sql' + } + }, + nbformat: 5, + nbformat_minor: 0 + }; + + let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); + mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(emptyNotebook)); + notebookManager.contentManager = mockContentManager.object; + + // When I initialize the model + let model = new NotebookModel(defaultModelOptions); + await model.requestModelLoad(); + + // Then I expect to have 1 code cell as the contents + should(model.cells).have.length(1); + should(model.cells[0].source).be.empty(); + }); + + it('Should throw if model load fails', async function(): Promise { + // Given a call to get Contents fails + let error = new Error('File not found'); + let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); + mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).throws(error); + notebookManager.contentManager = mockContentManager.object; + + // When I initalize the model + // Then it should throw + let model = new NotebookModel(defaultModelOptions); + should(model.inErrorState).be.false(); + await testUtils.assertThrowsAsync(() => model.requestModelLoad(), error.message); + should(model.inErrorState).be.true(); + }); + + it('Should convert cell info to CellModels', async function(): Promise { + // Given a notebook with 2 cells + let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); + mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContent)); + notebookManager.contentManager = mockContentManager.object; + + // When I initalize the model + let model = new NotebookModel(defaultModelOptions); + await model.requestModelLoad(); + + // Then I expect all cells to be in the model + should(model.cells).have.length(2); + should(model.cells[0].source).be.equal(expectedNotebookContent.cells[0].source); + should(model.cells[1].source).be.equal(expectedNotebookContent.cells[1].source); + }); + + it('Should load contents but then go to error state if client session startup fails', async function(): Promise { + let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); + mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContentOneCell)); + notebookManager.contentManager = mockContentManager.object; + + // Given I have a session that fails to start + mockClientSession.setup(c => c.isInErrorState).returns(() => true); + mockClientSession.setup(c => c.errorMessage).returns(() => 'Error'); + sessionReady.resolve(); + let sessionFired = false; + + let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, > { + factory: mockModelFactory.object + }); + let model = new NotebookModel(options); + model.onClientSessionReady((session) => sessionFired = true); + await model.requestModelLoad(); + model.backgroundStartSession(); + + // Then I expect load to succeed + shouldHaveOneCell(model); + should(model.clientSession).not.be.undefined(); + // but on server load completion I expect error state to be set + // Note: do not expect serverLoad event to throw even if failed + await model.sessionLoadFinished; + should(model.inErrorState).be.true(); + should(sessionFired).be.false(); + }); + + it('Should not be in error state if client session initialization succeeds', async function(): Promise { + let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); + mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContentOneCell)); + notebookManager.contentManager = mockContentManager.object; + let kernelChangedEmitter: Emitter = new Emitter(); + + mockClientSession.setup(c => c.isInErrorState).returns(() => false); + mockClientSession.setup(c => c.isReady).returns(() => true); + mockClientSession.setup(c => c.kernelChanged).returns(() => kernelChangedEmitter.event); + + queryConnectionService.setup(c => c.getActiveConnections(TypeMoq.It.isAny())).returns(() => null); + + sessionReady.resolve(); + let actualSession: IClientSession = undefined; + + let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, > { + factory: mockModelFactory.object + }); + let model = new NotebookModel(options, false); + model.onClientSessionReady((session) => actualSession = session); + await model.requestModelLoad(); + model.backgroundStartSession(); + + // Then I expect load to succeed + should(model.clientSession).not.be.undefined(); + // but on server load completion I expect error state to be set + // Note: do not expect serverLoad event to throw even if failed + let kernelChangedArg: nb.IKernelChangedArgs = undefined; + model.kernelChanged((kernel) => kernelChangedArg = kernel); + await model.sessionLoadFinished; + should(model.inErrorState).be.false(); + should(actualSession).equal(mockClientSession.object); + should(model.clientSession).equal(mockClientSession.object); + }); + + it('Should sanitize kernel display name when IP is included', async function(): Promise { + let model = new NotebookModel(defaultModelOptions); + let displayName = 'PySpark (1.1.1.1)'; + let sanitizedDisplayName = model.sanitizeDisplayName(displayName); + should(sanitizedDisplayName).equal('PySpark'); + }); + + it('Should sanitize kernel display name properly when IP is not included', async function(): Promise { + let model = new NotebookModel(defaultModelOptions); + let displayName = 'PySpark'; + let sanitizedDisplayName = model.sanitizeDisplayName(displayName); + should(sanitizedDisplayName).equal('PySpark'); + }); + + function shouldHaveOneCell(model: NotebookModel): void { + should(model.cells).have.length(1); + verifyCellModel(model.cells[0], { cell_type: CellTypes.Code, source: 'insert into t1 values (c1, c2)', metadata: { language: 'python' }, execution_count: 1 }); + } + + function verifyCellModel(cellModel: ICellModel, expected: nb.ICell): void { + should(cellModel.cellType).equal(expected.cell_type); + should(cellModel.source).equal(expected.source); + } + +}); diff --git a/src/typings/should.d.ts b/src/typings/should.d.ts new file mode 100755 index 0000000000..2fa87323fa --- /dev/null +++ b/src/typings/should.d.ts @@ -0,0 +1,238 @@ +// Type definitions for should.js + +declare function should(obj: any): should.Assertion; + +// node assert methods +/*interface NodeAssert { + fail(actual: any, expected: any, message?: string, operator?: string): void; + ok(value: any, message?: string): void; + equal(actual: any, expected: any, message?: string): void; + notEqual(actual: any, expected: any, message?: string): void; + deepEqual(actual: any, expected: any, message?: string): void; + notDeepEqual(actual: any, expected: any, message?: string): void; + strictEqual(actual: any, expected: any, message?: string): void; + notStrictEqual(actual: any, expected: any, message?: string): void; + + throws(block: Function, message?: string): void; + throws(block: Function, error: Function, message?: string): void; + throws(block: Function, error: RegExp, message?: string): void; + throws(block: Function, error: (err: any) => boolean, message?: string): void; + + doesNotThrow(block: Function, message?: string): void; + doesNotThrow(block: Function, error: Function, message?: string): void; + doesNotThrow(block: Function, error: RegExp, message?: string): void; + doesNotThrow(block: Function, error: (err: any) => boolean, message?: string): void; + + ifError(value: any): void; +} + + + +interface should extends NodeAssert, ShouldAssertExt { + not: ShouldAssertExt; +}*/ + +declare module should { + interface ShouldAssertExt { + exist(obj: any, msg?: string): void; + exists(obj: any, msg?: string): void; + } + + function fail(actual: any, expected: any, message?: string, operator?: string): void; + function ok(value: any, message?: string): void; + function equal(actual: any, expected: any, message?: string): void; + function notEqual(actual: any, expected: any, message?: string): void; + function deepEqual(actual: any, expected: any, message?: string): void; + function notDeepEqual(actual: any, expected: any, message?: string): void; + function strictEqual(actual: any, expected: any, message?: string): void; + function notStrictEqual(actual: any, expected: any, message?: string): void; + + function throws(block: Function, message?: string): void; + function throws(block: Function, error: Function, message?: string): void; + function throws(block: Function, error: RegExp, message?: string): void; + function throws(block: Function, error: (err: any) => boolean, message?: string): void; + + function doesNotThrow(block: Function, message?: string): void; + function doesNotThrow(block: Function, error: Function, message?: string): void; + function doesNotThrow(block: Function, error: RegExp, message?: string): void; + function doesNotThrow(block: Function, error: (err: any) => boolean, message?: string): void; + + function ifError(value: any): void; + + function exist(obj: any, msg?: string): void; + function exists(obj: any, msg?: string): void; + + const not: ShouldAssertExt; + + interface Assertion { + assert(expr: boolean): this; + fail(): this; + + not: this; + any: this; + only: this; + + // bool + true(message?: string): this; + True(message?: string): this; + + false(message?: string): this; + False(message?: string): this; + + ok(): this; + + //chain + an: this; + of: this; + a: this; + and: this; + be: this; + been: this; + has: this; + have: this; + with: this; + is: this; + which: this; + the: this; + it: this; + + //contain + containEql(obj: any): this; + containDeepOrdered(obj: any): this; + containDeep(obj: any): this; + + // eql + eql(obj: any, description?: string): this; + eqls(obj: any, description?: string): this; + deepEqual(obj: any, description?: string): this; + + equal(obj: any, description?: string): this; + equals(obj: any, description?: string): this; + exactly(obj: any, description?: string): this; + + equalOneOf(...objs: any[]): this; + equalOneOf(obj: any[]): this; + oneOf(...objs: any[]): this; + oneOf(obj: any[]): this; + + //error + throw(): this; + throw(msg: RegExp | string | Function, properties?: {}): this; + throw(properties: {}): this; + //TODO how to express generators??? + throwError(): this; + throwError(msg: RegExp | string | Function, properties?: {}): this; + throwError(properties: {}): this; + + // match + match( + obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {}, + description?: string + ): this; + matchEach( + obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {}, + description?: string + ): this; + matchEvery( + obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {}, + description?: string + ): this; + matchAny( + obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {}, + description?: string + ): this; + matchSome( + obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {}, + description?: string + ): this; + + //number + NaN(): this; + Infinity(): this; + within(start: number, finish: number, description?: string): this; + approximately(value: number, delta: number, description?: string): this; + above(value: number, description?: string): this; + greaterThan(value: number, description?: string): this; + below(value: number, description?: string): this; + lessThan(value: number, description?: string): this; + aboveOrEqual(value: number, description?: string): this; + greaterThanOrEqual(value: number, description?: string): this; + belowOrEqual(value: number, description?: string): this; + lessThanOrEqual(value: number, description?: string): this; + + //promise + Promise(): this; + + fulfilled(): Promise; + resolved(): Promise; + rejected(): Promise; + + fulfilledWith(obj: any): Promise; + resolvedWith(obj: any): Promise; + rejectedWith(msg: RegExp | string | Error, properties?: {}): Promise; + rejectedWith(properties: {}): Promise; + finally: PromisedAssertion; + eventually: PromisedAssertion; + + // property + propertyWithDescriptor(name: string, descriptor: {}): this; + + property(name: string, value?: any): this; + properties(...names: string[]): this; + properties(names: string[]): this; + properties(props: {}): this; + + length(value: number, description?: string): this; + lengthOf(value: number, description?: string): this; + + ownProperty(name: string, description?: string): this; + hasOwnProperty(name: string, description?: string): this; + + empty(): this; + + keys(...keys: any[]): this; + key(key: any): this; + + value(key: any, value: any): this; + + size(value: number): this; + + propertyByPath(...path: string[]): this; + propertyByPath(path: string[]): this; + + //string + startWith(prefix: string, description?: string): this; + endWith(postfix: string, description?: string): this; + + //type + Number(): this; + arguments(): this; + Arguments(): this; + type(typeName: string, description?: string): this; + instanceof(constructor: Function, description?: string): this; + instanceOf(constructor: Function, description?: string): this; + Function(): this; + Object(): this; + String(): this; + Array(): this; + Boolean(): this; + Error(): this; + Date(): this; + null(): this; + Null(): this; + class(className: string): this; + Class(className: string): this; + undefined(): this; + Undefined(): this; + iterable(): this; + iterator(): this; + generator(): this; + } + + interface PromisedAssertion extends Assertion, PromiseLike {} +} + + +declare module 'should' { + export = should; +} \ No newline at end of file diff --git a/src/typings/temp-write.d.ts b/src/typings/temp-write.d.ts new file mode 100644 index 0000000000..6356d00aae --- /dev/null +++ b/src/typings/temp-write.d.ts @@ -0,0 +1,3 @@ +declare module 'temp-write' { + function sync(input: string, filePath?: string): string; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index ce67901792..1b492c360e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -99,6 +99,13 @@ version "5.5.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" +"@types/should@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@types/should/-/should-13.0.0.tgz#96c00117f1896177848fdecfa336313c230c879e" + integrity sha512-Mi6YZ2ABnnGGFMuiBDP0a8s1ZDCDNHqP97UH8TyDmCWuGGavpsFMfJnAMYaaqmDlSCOCNbVLHBrSDEOpx/oLhw== + dependencies: + should "*" + "@types/sinon@1.16.34": version "1.16.34" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-1.16.34.tgz#a9761fff33d0f7b3fe61875b577778a2576a9a03" @@ -4373,6 +4380,13 @@ macaddress@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + make-error-cause@^1.1.1: version "1.2.2" resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" @@ -5265,6 +5279,11 @@ pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -6350,6 +6369,50 @@ shelljs@^0.7.5: interpret "^1.0.0" rechoir "^0.6.2" +should-equal@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" + integrity sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA== + dependencies: + should-type "^1.4.0" + +should-format@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1" + integrity sha1-m/yPdPo5IFxT04w01xcwPidxJPE= + dependencies: + should-type "^1.3.0" + should-type-adaptors "^1.0.1" + +should-type-adaptors@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a" + integrity sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA== + dependencies: + should-type "^1.3.0" + should-util "^1.0.0" + +should-type@^1.3.0, should-type@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3" + integrity sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM= + +should-util@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/should-util/-/should-util-1.0.0.tgz#c98cda374aa6b190df8ba87c9889c2b4db620063" + integrity sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM= + +should@*, should@^13.2.3: + version "13.2.3" + resolved "https://registry.yarnpkg.com/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10" + integrity sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ== + dependencies: + should-equal "^2.0.0" + should-format "^3.0.3" + should-type "^1.4.0" + should-type-adaptors "^1.0.1" + should-util "^1.0.0" + sigmund@^1.0.1, sigmund@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" @@ -6804,6 +6867,23 @@ tar-stream@^1.1.2: to-buffer "^1.1.0" xtend "^4.0.0" +temp-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" + integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= + +temp-write@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-3.4.0.tgz#8cff630fb7e9da05f047c74ce4ce4d685457d492" + integrity sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI= + dependencies: + graceful-fs "^4.1.2" + is-stream "^1.1.0" + make-dir "^1.0.0" + pify "^3.0.0" + temp-dir "^1.0.0" + uuid "^3.0.1" + temp@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59" @@ -7228,7 +7308,7 @@ uuid@^3.0.0, uuid@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" -uuid@^3.3.2: +uuid@^3.0.1, uuid@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"