From b1526603cc26ecc0c0e563b6606ceb0b4a55b3f2 Mon Sep 17 00:00:00 2001 From: Maddy <12754347+MaddyDev@users.noreply.github.com> Date: Tue, 7 Jan 2020 16:35:28 -0800 Subject: [PATCH] Tests/notebook find tests (#8827) * updates to existing book tests * notebookFindModal tests * remove commented code * undo book test changes * undo book test changes * resolve the find array * additional tests --- .../notebook/browser/models/notebookInput.ts | 2 +- .../notebook/find/notebookFindModel.ts | 81 ++++--- .../notebookFindModel.test.ts | 203 ++++++++++++++++++ 3 files changed, 242 insertions(+), 44 deletions(-) create mode 100644 src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts diff --git a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts index 9189c8c20b..dd47cc63bd 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts @@ -462,7 +462,7 @@ export abstract class NotebookInput extends EditorInput { } } -class NotebookEditorContentManager implements IContentManager { +export class NotebookEditorContentManager implements IContentManager { constructor( private notebookInput: NotebookInput, @IInstantiationService private readonly instantiationService: IInstantiationService) { diff --git a/src/sql/workbench/contrib/notebook/find/notebookFindModel.ts b/src/sql/workbench/contrib/notebook/find/notebookFindModel.ts index 385ee7cfa2..ec17c31606 100644 --- a/src/sql/workbench/contrib/notebook/find/notebookFindModel.ts +++ b/src/sql/workbench/contrib/notebook/find/notebookFindModel.ts @@ -494,13 +494,18 @@ export class NotebookFindModel extends Disposable implements INotebookFindModel this._findArray = new Array(); this._onFindCountChange.fire(this._findArray.length); if (exp) { - return new Promise((resolve) => { - const disp = this.onFindCountChange(e => { - resolve(this._findArray[this._findIndex]); - disp.dispose(); - }); - this._startSearch(exp, maxMatches); - }); + for (let i = 0; i < this.notebookModel.cells.length; i++) { + const item = this.notebookModel.cells[i]; + const result = this.searchFn(item, exp, maxMatches); + if (result) { + this._findArray.push(...result); + this._onFindCountChange.fire(this._findArray.length); + if (maxMatches > 0 && this._findArray.length === maxMatches) { + break; + } + } + } + return Promise.resolve(this._findArray[this._findIndex]); } else { return Promise.reject(new Error('no expression')); } @@ -518,51 +523,41 @@ export class NotebookFindModel extends Disposable implements INotebookFindModel return this.findArray; } - private _startSearch(exp: string, maxMatches: number = 0): void { - let searchFn = (cell: ICellModel, exp: string): NotebookRange[] => { - let findResults: NotebookRange[] = []; - let cellVal = cell.cellType === 'markdown' ? this.cleanUpCellSource(cell.source) : cell.source; - let index: number; - let start: number; - let end: number; - if (cellVal) { - if (typeof cellVal === 'string') { + private searchFn(cell: ICellModel, exp: string, maxMatches?: number): NotebookRange[] { + let findResults: NotebookRange[] = []; + let cellVal = cell.cellType === 'markdown' ? this.cleanUpCellSource(cell.source) : cell.source; + let index: number; + let start: number; + let end: number; + if (cellVal) { + if (typeof cellVal === 'string') { + index = 0; + while (cellVal.substr(index).toLocaleLowerCase().indexOf(exp.toLocaleLowerCase()) > -1) { + start = cellVal.substr(index).toLocaleLowerCase().indexOf(exp.toLocaleLowerCase()) + index; + end = start + exp.length; + let range = new NotebookRange(cell, 0, start, 0, end); + findResults = findResults.concat(range); + index = end; + } + } else { + for (let j = 0; j < cellVal.length; j++) { index = 0; - while (cellVal.substr(index).toLocaleLowerCase().indexOf(exp.toLocaleLowerCase()) > -1) { - start = cellVal.substr(index).toLocaleLowerCase().indexOf(exp.toLocaleLowerCase()) + index; + let cellValFormatted = cell.cellType === 'markdown' ? this.cleanMarkdownLinks(cellVal[j]) : cellVal[j]; + while (cellValFormatted.substr(index).toLocaleLowerCase().indexOf(exp.toLocaleLowerCase()) > -1) { + start = cellValFormatted.substr(index).toLocaleLowerCase().indexOf(exp.toLocaleLowerCase()) + index + 1; end = start + exp.length; - let range = new NotebookRange(cell, 0, start, 0, end); + // lineNumber: j+1 since notebook editors aren't zero indexed. + let range = new NotebookRange(cell, j + 1, start, j + 1, end); findResults = findResults.concat(range); index = end; } - } else { - for (let j = 0; j < cellVal.length; j++) { - index = 0; - let cellValFormatted = cell.cellType === 'markdown' ? this.cleanMarkdownLinks(cellVal[j]) : cellVal[j]; - while (cellValFormatted.substr(index).toLocaleLowerCase().indexOf(exp.toLocaleLowerCase()) > -1) { - start = cellValFormatted.substr(index).toLocaleLowerCase().indexOf(exp.toLocaleLowerCase()) + index + 1; - end = start + exp.length; - // lineNumber: j+1 since notebook editors aren't zero indexed. - let range = new NotebookRange(cell, j + 1, start, j + 1, end); - findResults = findResults.concat(range); - index = end; - } + if (findResults.length >= maxMatches) { + break; } } } - return findResults; - }; - for (let i = 0; i < this.notebookModel.cells.length; i++) { - const item = this.notebookModel.cells[i]; - const result = searchFn!(item, exp); - if (result) { - this._findArray = this._findArray.concat(result); - this._onFindCountChange.fire(this._findArray.length); - if (maxMatches > 0 && this._findArray.length === maxMatches) { - break; - } - } } + return findResults; } // In markdown links are defined as [Link Text](https://url/of/the/text). when searching for text we shouldn't diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts new file mode 100644 index 0000000000..671ecf8b10 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts @@ -0,0 +1,203 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as TypeMoq from 'typemoq'; +import { nb } from 'azdata'; +import * as assert from 'assert'; + +import { URI } from 'vs/base/common/uri'; +import { NotebookManagerStub } from 'sql/workbench/contrib/notebook/test/stubs'; +import { CellTypes } from 'sql/workbench/contrib/notebook/common/models/contracts'; +import { IClientSession, INotebookModelOptions } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces'; +import { NotebookModel } from 'sql/workbench/contrib/notebook/browser/models/notebookModel'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { NotebookFindModel } from 'sql/workbench/contrib/notebook/find/notebookFindModel'; +import { TestConnectionManagementService } from 'sql/platform/connection/test/common/testConnectionManagementService'; +import { Deferred } from 'sql/base/common/promise'; +import { ModelFactory } from 'sql/workbench/contrib/notebook/browser/models/modelFactory'; +import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { Memento } from 'vs/workbench/common/memento'; +import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; +import { ClientSession } from 'sql/workbench/contrib/notebook/browser/models/clientSession'; +import { TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { NotebookRange } from 'sql/workbench/contrib/notebook/find/notebookFindDecorations'; +import { NotebookEditorContentManager } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; + +let expectedNotebookContent: nb.INotebookContents = { + cells: [{ + cell_type: CellTypes.Code, + source: 'insert into t1 values (c1, c2) \ninsert into markdown values (*hello worls*)', + 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: 4, + nbformat_minor: 5 +}; + +let defaultUri = URI.file('/some/path.ipynb'); +let max_find_count = 3; +let mockClientSession: TypeMoq.Mock; +let sessionReady: Deferred; +let mockModelFactory: TypeMoq.Mock; +let notificationService: TypeMoq.Mock; +let capabilitiesService: TypeMoq.Mock; +let instantiationService: IInstantiationService; +let serviceCollection = new ServiceCollection(); + +suite('Notebook Find Model', function (): void { + + let notebookManagers = [new NotebookManagerStub()]; + let memento: TypeMoq.Mock; + let queryConnectionService: TypeMoq.Mock; + let defaultModelOptions: INotebookModelOptions; + const logService = new NullLogService(); + let model: NotebookModel; + + setup(async () => { + sessionReady = new Deferred(); + notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); + capabilitiesService = TypeMoq.Mock.ofType(TestCapabilitiesService); + memento = TypeMoq.Mock.ofType(Memento, TypeMoq.MockBehavior.Loose, ''); + memento.setup(x => x.getMemento(TypeMoq.It.isAny())).returns(() => void 0); + queryConnectionService = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Loose, memento.object, undefined, new TestStorageService()); + queryConnectionService.callBase = true; + + instantiationService = new InstantiationService(serviceCollection, true); + defaultModelOptions = { + notebookUri: defaultUri, + factory: new ModelFactory(instantiationService), + notebookManagers, + contentManager: undefined, + notificationService: notificationService.object, + connectionService: queryConnectionService.object, + providerId: 'SQL', + cellMagicMapper: undefined, + defaultKernel: undefined, + layoutChanged: undefined, + capabilitiesService: capabilitiesService.object + }; + mockClientSession = TypeMoq.Mock.ofType(ClientSession, undefined, defaultModelOptions); + mockClientSession.setup(c => c.initialize()).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; + }); + + await initNotebookModel(expectedNotebookContent); + }); + + test('Should find results in the notebook', async function (): Promise { + //initialize find + let notebookFindModel = new NotebookFindModel(model); + await notebookFindModel.find('markdown', max_find_count); + + assert(notebookFindModel.findMatches, new Error('Find in notebook failed.')); + assert.equal(notebookFindModel.findMatches.length, 2, 'Find couldnt find all occurances'); + }); + + test('Should not find results in the notebook', async function (): Promise { + //initialize find + let notebookFindModel = new NotebookFindModel(model); + await notebookFindModel.find('notFound', max_find_count); + + assert.equal(notebookFindModel.findMatches.length, 0, 'Find failed'); + }); + + test('Should match find result ranges', async function (): Promise { + let notebookFindModel = new NotebookFindModel(model); + await notebookFindModel.find('markdown', max_find_count); + + let expectedFindRange1 = new NotebookRange(model.cells[0], 2, 13, 2, 21); + assert.deepEqual(notebookFindModel.findMatches[0].range, expectedFindRange1, 'Find in markdown range is wrong :\n' + JSON.stringify(expectedFindRange1) + '\n ' + JSON.stringify(notebookFindModel.findMatches[0].range)); + + let expectedFindRange2 = new NotebookRange(model.cells[1], 1, 6, 1, 14); + assert.deepEqual(notebookFindModel.findMatches[1].range, expectedFindRange2, 'Find in markdown range is wrong :\n' + JSON.stringify(expectedFindRange2) + '\n ' + JSON.stringify(notebookFindModel.findMatches[1].range)); + }); + + test('Should ignore hyperlink markdown data and find correctly', async function (): Promise { + let markdownContent: nb.INotebookContents = { + cells: [{ + cell_type: CellTypes.Markdown, + source: 'I am markdown link: [best link ever](https://url/of/the/best-link-ever)', + metadata: { language: 'python' }, + execution_count: 1 + }], + metadata: { + kernelspec: { + name: 'mssql', + language: 'sql' + } + }, + nbformat: 4, + nbformat_minor: 5 + }; + await initNotebookModel(markdownContent); + + let notebookFindModel = new NotebookFindModel(model); + await notebookFindModel.find('best', max_find_count); + + assert.equal(notebookFindModel.findMatches.length, 1, 'Find failed on markdown link'); + + let expectedFindRange1 = new NotebookRange(model.cells[0], 1, 21, 1, 25); + assert.deepEqual(notebookFindModel.findMatches[0].range, expectedFindRange1, 'Find in markdown range is wrong :\n' + JSON.stringify(expectedFindRange1) + '\n ' + JSON.stringify(notebookFindModel.findMatches[0].range)); + + }); + + test('Should not find more than max results in the notebook', async function (): Promise { + let codeContent: nb.INotebookContents = { + cells: [{ + cell_type: CellTypes.Code, + source: ['import x', 'x.init()', 'x.show()', 'x.analyze()'], + metadata: { language: 'python' }, + execution_count: 1 + }], + metadata: { + kernelspec: { + name: 'python', + language: 'python' + } + }, + nbformat: 4, + nbformat_minor: 5 + }; + await initNotebookModel(codeContent); + //initialize find + let notebookFindModel = new NotebookFindModel(model); + await notebookFindModel.find('x', max_find_count); + + assert.equal(notebookFindModel.findMatches.length, 3, 'Find failed'); + }); + + async function initNotebookModel(contents: nb.INotebookContents): Promise { + let mockContentManager = TypeMoq.Mock.ofType(NotebookEditorContentManager); + mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(contents)); + defaultModelOptions.contentManager = mockContentManager.object; + // Initialize the model + model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, undefined); + await model.loadContents(); + await model.requestModelLoad(); + } + +}); +