From 03b76b2e8e1310a97815fdb3fae1f08b8d4af701 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 11 May 2018 18:09:36 -0700 Subject: [PATCH] EH Search - add extHostSearch.test.ts --- src/vs/workbench/api/node/extHostSearch.ts | 23 ++- .../api/extHostSearch.test.ts | 170 ++++++++++++++++++ 2 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index 807613d7585..e16c1c1f2b8 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -295,6 +295,8 @@ class FileSearchEngine { private resultCount: number; private isCanceled: boolean; + private activeCancellationTokens: Set; + // private filesWalked: number; // private directoriesWalked: number; @@ -309,6 +311,7 @@ class FileSearchEngine { // this.maxFilesize = config.maxFileSize || null; this.resultCount = 0; this.isLimitHit = false; + this.activeCancellationTokens = new Set(); // this.filesWalked = 0; // this.directoriesWalked = 0; @@ -345,6 +348,8 @@ class FileSearchEngine { public cancel(): void { this.isCanceled = true; + this.activeCancellationTokens.forEach(t => t.cancel()); + this.activeCancellationTokens = new Set(); } public search(): PPromise<{ isLimitHit: boolean }, IInternalFileMatch> { @@ -405,7 +410,6 @@ class FileSearchEngine { }); }); }); - } private searchInFolder(fq: IFolderQuery): PPromise { @@ -417,6 +421,10 @@ class FileSearchEngine { const tree = this.initDirectoryTree(); const onProviderResult = (result: URI) => { + if (this.isCanceled) { + return; + } + // TODO@roblou - What if it is not relative to the folder query. // This is slow... const relativePath = path.relative(folderStr, result.toString()); @@ -443,8 +451,17 @@ class FileSearchEngine { // TODO@roblou const noSiblingsClauses = true; - this.provider.provideFileSearchResults(options, { report: onProviderResult }, cancellation.token) + new TPromise(resolve => process.nextTick(resolve)) .then(() => { + this.activeCancellationTokens.add(cancellation); + return this.provider.provideFileSearchResults(options, { report: onProviderResult }, cancellation.token); + }) + .then(() => { + this.activeCancellationTokens.delete(cancellation); + if (this.isCanceled) { + return null; + } + if (noSiblingsClauses && this.isLimitHit) { if (!filePatternSeen) { // If the limit was hit, check whether filePattern is an exact relative match because it must be included @@ -472,8 +489,6 @@ class FileSearchEngine { cancellation.dispose(); reject(err); }); - }, () => { - cancellation.cancel(); }); } diff --git a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts new file mode 100644 index 00000000000..302d8b8e694 --- /dev/null +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -0,0 +1,170 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; +import * as path from 'path'; +import URI, { UriComponents } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IRawFileMatch2, IRawSearchQuery, QueryType, ISearchQuery } from 'vs/platform/search/common/search'; +import { MainContext, MainThreadSearchShape } from 'vs/workbench/api/node/extHost.protocol'; +import { ExtHostSearch } from 'vs/workbench/api/node/extHostSearch'; +import { TestRPCProtocol } from 'vs/workbench/test/electron-browser/api/testRPCProtocol'; +import * as vscode from 'vscode'; +import { dispose } from '../../../../base/common/lifecycle'; +import { isPromiseCanceledError } from '../../../../base/common/errors'; + +let rpcProtocol: TestRPCProtocol; +let extHostSearch: ExtHostSearch; +let disposables: vscode.Disposable[] = []; + +let mockMainThreadSearch: MockMainThreadSearch; +class MockMainThreadSearch implements MainThreadSearchShape { + lastHandle: number; + + results: (UriComponents | IRawFileMatch2)[] = []; + + $registerSearchProvider(handle: number, scheme: string): void { + this.lastHandle = handle; + } + + $unregisterProvider(handle: number): void { + } + + $handleFindMatch(handle: number, session: number, data: UriComponents | IRawFileMatch2[]): void { + if (Array.isArray(data)) { + this.results.push(...data); + } else { + this.results.push(data); + } + } + + dispose() { + } +} + +suite.only('ExtHostSearch', () => { + async function registerTestSearchProvider(provider: vscode.SearchProvider): TPromise { + disposables.push(extHostSearch.registerSearchProvider('file', provider)); + await rpcProtocol.sync(); + } + + async function runFileSearch(query: IRawSearchQuery, cancel = false): TPromise { + try { + const p = extHostSearch.$provideFileSearchResults(mockMainThreadSearch.lastHandle, 0, query); + if (cancel) { + await new TPromise(resolve => process.nextTick(resolve)); + p.cancel(); + } + + await p; + } catch (err) { + if (!isPromiseCanceledError(err)) { + await rpcProtocol.sync(); + throw err; + } + } + + await rpcProtocol.sync(); + return (mockMainThreadSearch.results).map(r => URI.revive(r)); + } + + setup(() => { + rpcProtocol = new TestRPCProtocol(); + + mockMainThreadSearch = new MockMainThreadSearch(); + + rpcProtocol.set(MainContext.MainThreadSearch, mockMainThreadSearch); + extHostSearch = new ExtHostSearch(rpcProtocol); + }); + + teardown(() => { + dispose(disposables); + return rpcProtocol.sync(); + }); + + suite('File', () => { + const rootFolderA = URI.parse('/foo/bar'); + const simpleQuery: ISearchQuery = { + type: QueryType.File, + + filePattern: '', + folderQueries: [ + { folder: rootFolderA } + ] + }; + + function makeFileResult(root: URI, relativePath: string): URI { + return URI.parse( + path.join(root.toString(), relativePath)); + } + + function compareURIs(a: URI[], b: URI[]) { + assert.deepEqual( + a.map(u => u.toString()), + b.map(u => u.toString())); + } + + test('no results', async () => { + await registerTestSearchProvider({ + provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + return TPromise.wrap(null); + } + }); + + return runFileSearch(simpleQuery).then(results => { + assert(!results.length); + }); + }); + + test('simple results', async () => { + const reportedResults = [ + makeFileResult(rootFolderA, 'file1.ts'), + makeFileResult(rootFolderA, 'file2.ts'), + makeFileResult(rootFolderA, 'file3.ts'), + ]; + + await registerTestSearchProvider({ + provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + reportedResults.forEach(r => progress.report(r)); + return TPromise.wrap(null); + } + }); + + return runFileSearch(simpleQuery).then(results => { + assert.equal(results.length, 3); + compareURIs(results, reportedResults); + }); + }); + + // Sibling clauses + // Extra files + // Max result count + // Absolute/relative logic + // Includes/excludes passed to provider correctly + // Provider misbehaves + + test('Search canceled', async () => { + let cancelRequested = false; + await registerTestSearchProvider({ + provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + return new TPromise((resolve, reject) => { + token.onCancellationRequested(() => { + cancelRequested = true; + progress.report(makeFileResult(rootFolderA, 'file1.ts')); + + resolve(null); // or reject or nothing? + }); + }); + } + }); + + return runFileSearch(simpleQuery, true).then(results => { + assert(cancelRequested); + assert(!results.length); + }); + }); + }); +});