diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 4ccbd0e4a72..78c0314db77 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -112,7 +112,10 @@ export class ExtHostTesting implements ExtHostTestingShape { if (resource === ExtHostTestingResource.TextDocument) { const document = this.documents.getDocument(uri); if (document) { - method = p => p.createDocumentTestHierarchy?.(document.document); + const folder = await this.workspace.getWorkspaceFolder2(uri, false); + method = p => p.createDocumentTestHierarchy + ? p.createDocumentTestHierarchy(document.document) + : this.createDefaultDocumentTestHierarchy(p, document.document, folder); } } else { const folder = await this.workspace.getWorkspaceFolder2(uri, false); @@ -190,7 +193,10 @@ export class ExtHostTesting implements ExtHostTestingShape { return EMPTY_TEST_RESULT; } - const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual).filter(isDefined); + const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual) + .filter(isDefined) + // Only send the actual TestItem's to the user to run. + .map(t => t instanceof TestItemFilteredWrapper ? t.actual : t); if (!tests.length) { return EMPTY_TEST_RESULT; } @@ -217,6 +223,158 @@ export class ExtHostTesting implements ExtHostTestingShape { const { actual, previousChildren, previousEquals, ...item } = owned; return Promise.resolve(item); } + + private createDefaultDocumentTestHierarchy(provider: vscode.TestProvider, document: vscode.TextDocument, folder: vscode.WorkspaceFolder | undefined): vscode.TestHierarchy | undefined { + if (!folder) { + return; + } + + const workspaceHierarchy = provider.createWorkspaceTestHierarchy?.(folder); + if (!workspaceHierarchy) { + return; + } + + const onDidChangeTest = new Emitter(); + workspaceHierarchy.onDidChangeTest(node => { + const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(node, document); + const previouslySeen = wrapper.hasNodeMatchingFilter; + + if (previouslySeen) { + // reset cache and get whether you can currently see the TestItem. + wrapper.reset(); + const currentlySeen = wrapper.hasNodeMatchingFilter; + + if (currentlySeen) { + onDidChangeTest.fire(wrapper); + return; + } + + // Fire the event to say that the current visible parent has changed. + onDidChangeTest.fire(wrapper.visibleParent); + return; + } + + const previousParent = wrapper.visibleParent; + wrapper.reset(); + const currentlySeen = wrapper.hasNodeMatchingFilter; + + // It wasn't previously seen and isn't currently seen so + // nothing has actually changed. + if (!currentlySeen) { + return; + } + + // The test is now visible so we need to refresh the cache + // of the previous visible parent and fire that it has changed. + previousParent.reset(); + onDidChangeTest.fire(previousParent); + }); + + return { + root: TestItemFilteredWrapper.getWrapperForTestItem(workspaceHierarchy.root, document), + dispose: () => { + onDidChangeTest.dispose(); + TestItemFilteredWrapper.removeFilter(document); + }, + onDidChangeTest: onDidChangeTest.event + }; + } +} + +/* + * A class which wraps a vscode.TestItem that provides the ability to filter a TestItem's children + * to only the children that are located in a certain vscode.Uri. + */ +export class TestItemFilteredWrapper implements vscode.TestItem { + private static wrapperMap = new WeakMap>(); + public static removeFilter(document: vscode.TextDocument): void { + this.wrapperMap.delete(document); + } + + // Wraps the TestItem specified in a TestItemFilteredWrapper and pulls from a cache if it already exists. + public static getWrapperForTestItem(item: vscode.TestItem, filterDocument: vscode.TextDocument, parent?: TestItemFilteredWrapper): TestItemFilteredWrapper { + let innerMap = this.wrapperMap.get(filterDocument); + if (innerMap?.has(item)) { + return innerMap.get(item)!; + } + + if (!innerMap) { + innerMap = new WeakMap(); + this.wrapperMap.set(filterDocument, innerMap); + + } + + const w = new TestItemFilteredWrapper(item, filterDocument, parent); + innerMap.set(item, w); + return w; + } + + public get label() { + return this.actual.label; + } + + public get debuggable() { + return this.actual.debuggable; + } + + public get description() { + return this.actual.description; + } + + public get location() { + return this.actual.location; + } + + public get runnable() { + return this.actual.runnable; + } + + public get state() { + return this.actual.state; + } + + public get children() { + // We only want children that match the filter. + return this.getWrappedChildren().filter(child => child.hasNodeMatchingFilter); + } + + public get visibleParent(): TestItemFilteredWrapper { + return this.hasNodeMatchingFilter ? this : this.parent!.visibleParent; + } + + private matchesFilter: boolean | undefined; + + // Determines if the TestItem matches the filter. This would be true if: + // 1. We don't have a parent (because the root is the workspace root node) + // 2. The URI of the current node matches the filter URI + // 3. Some child of the current node matches the filter URI + public get hasNodeMatchingFilter(): boolean { + if (this.matchesFilter === undefined) { + this.matchesFilter = !this.parent + || this.actual.location?.uri.toString() === this.filterDocument.uri.toString() + || this.getWrappedChildren().some(child => child.hasNodeMatchingFilter); + } + + return this.matchesFilter; + } + + // Reset the cache of whether or not you can see a node from a particular node + // up to it's visible parent. + public reset(): void { + if (this !== this.visibleParent) { + this.parent?.reset(); + } + this.matchesFilter = undefined; + } + + + private constructor(public readonly actual: vscode.TestItem, private filterDocument: vscode.TextDocument, private readonly parent?: TestItemFilteredWrapper) { + this.getWrappedChildren(); + } + + private getWrappedChildren() { + return this.actual.children?.map(t => TestItemFilteredWrapper.getWrapperForTestItem(t, this.filterDocument, this)) || []; + } } /** diff --git a/src/vs/workbench/test/browser/api/extHostTesting.test.ts b/src/vs/workbench/test/browser/api/extHostTesting.test.ts index 71898b55689..11838dab2a2 100644 --- a/src/vs/workbench/test/browser/api/extHostTesting.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTesting.test.ts @@ -4,12 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { MirroredTestCollection } from 'vs/workbench/api/common/extHostTesting'; +import { MirroredTestCollection, TestItemFilteredWrapper } from 'vs/workbench/api/common/extHostTesting'; import * as convert from 'vs/workbench/api/common/extHostTypeConverters'; import { TestDiffOpType } from 'vs/workbench/contrib/testing/common/testCollection'; import { stubTest, testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; import { TestOwnedTestCollection, TestSingleUseCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; -import { TestChangeEvent, TestItem } from 'vscode'; +import { TestChangeEvent, TestItem, TextDocument } from 'vscode'; +import { URI } from 'vs/base/common/uri'; +import { Location } from 'vs/editor/common/modes'; +import { Range } from 'vs/editor/common/core/range'; const simplify = (item: TestItem) => { if ('toJSON' in item) { @@ -262,5 +265,141 @@ suite('ExtHost Testing', () => { assert.strictEqual(m.changeEvent.commonChangeAncestor?.label, 'root'); }); }); + + suite('TestItemFilteredWrapper', () => { + const stubTestWithLocation = (label: string, location: Location): TestItem => { + const t = stubTest(label); + t.location = location as any; + return t; + }; + + const location1: Location = { + range: new Range(0, 0, 0, 0), + uri: URI.parse('file:///foo.ts') + }; + + const location2: Location = { + range: new Range(0, 0, 0, 0), + uri: URI.parse('file:///bar.ts') + }; + + const location3: Location = { + range: new Range(0, 0, 0, 0), + uri: URI.parse('file:///baz.ts') + }; + + const textDocumentFilter = { + uri: location1.uri + } as TextDocument; + + let testsWithLocation: TestItem; + setup(() => { + testsWithLocation = { + ...stubTest('root'), + children: [ + { + ...stubTestWithLocation('a', location1), + children: [stubTestWithLocation('aa', location1), stubTestWithLocation('ab', location1)] + }, + { + ...stubTestWithLocation('b', location2), + children: [stubTestWithLocation('ba', location2), stubTestWithLocation('bb', location2)] + }, + { + ...stubTestWithLocation('b', location3), + } + ], + }; + }); + + teardown(() => { + TestItemFilteredWrapper.removeFilter(textDocumentFilter); + }); + + test('gets all actual properties', () => { + const testItem: TestItem = stubTest('test1'); + const wrapper: TestItemFilteredWrapper = TestItemFilteredWrapper.getWrapperForTestItem(testItem, textDocumentFilter); + + assert.strictEqual(testItem.debuggable, wrapper.debuggable); + assert.strictEqual(testItem.description, wrapper.description); + assert.strictEqual(testItem.label, wrapper.label); + assert.strictEqual(testItem.location, wrapper.location); + assert.strictEqual(testItem.runnable, wrapper.runnable); + assert.strictEqual(testItem.state, wrapper.state); + }); + + test('gets no children if nothing matches Uri filter', () => { + let tests: TestItem = testStubs.nested(); + const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(tests, textDocumentFilter); + assert.strictEqual(wrapper.children.length, 0); + }); + + test('filter is applied to children', () => { + const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(testsWithLocation, textDocumentFilter); + assert.strictEqual(wrapper.label, 'root'); + assert.strictEqual(wrapper.children.length, 1); + assert.strictEqual(wrapper.children[0] instanceof TestItemFilteredWrapper, true); + assert.strictEqual(wrapper.children[0].label, 'a'); + }); + + test('can get if node has matching filter', () => { + const rootWrapper = TestItemFilteredWrapper.getWrapperForTestItem(testsWithLocation, textDocumentFilter); + + const invisible = testsWithLocation.children![1]; + const invisibleWrapper = TestItemFilteredWrapper.getWrapperForTestItem(invisible, textDocumentFilter); + const visible = testsWithLocation.children![0]; + const visibleWrapper = TestItemFilteredWrapper.getWrapperForTestItem(visible, textDocumentFilter); + + // The root is always visible + assert.strictEqual(rootWrapper.hasNodeMatchingFilter, true); + assert.strictEqual(invisibleWrapper.hasNodeMatchingFilter, false); + assert.strictEqual(visibleWrapper.hasNodeMatchingFilter, true); + }); + + test('can get visible parent', () => { + const rootWrapper = TestItemFilteredWrapper.getWrapperForTestItem(testsWithLocation, textDocumentFilter); + + const invisible = testsWithLocation.children![1]; + const invisibleWrapper = TestItemFilteredWrapper.getWrapperForTestItem(invisible, textDocumentFilter); + const visible = testsWithLocation.children![0]; + const visibleWrapper = TestItemFilteredWrapper.getWrapperForTestItem(visible, textDocumentFilter); + + // The root is always visible + assert.strictEqual(rootWrapper.visibleParent, rootWrapper); + assert.strictEqual(invisibleWrapper.visibleParent, rootWrapper); + assert.strictEqual(visibleWrapper.visibleParent, visibleWrapper); + }); + + test('can reset cached value of hasNodeMatchingFilter', () => { + TestItemFilteredWrapper.getWrapperForTestItem(testsWithLocation, textDocumentFilter); + + const invisible = testsWithLocation.children![1]; + const invisibleWrapper = TestItemFilteredWrapper.getWrapperForTestItem(invisible, textDocumentFilter); + + assert.strictEqual(invisibleWrapper.hasNodeMatchingFilter, false); + invisible.location = location1 as any; + assert.strictEqual(invisibleWrapper.hasNodeMatchingFilter, false); + invisibleWrapper.reset(); + assert.strictEqual(invisibleWrapper.hasNodeMatchingFilter, true); + }); + + test('can reset cached value of hasNodeMatchingFilter of parents up to visible parent', () => { + const rootWrapper = TestItemFilteredWrapper.getWrapperForTestItem(testsWithLocation, textDocumentFilter); + + const invisibleParent = testsWithLocation.children![1]; + const invisibleParentWrapper = TestItemFilteredWrapper.getWrapperForTestItem(invisibleParent, textDocumentFilter); + const invisible = invisibleParent.children![1]; + const invisibleWrapper = TestItemFilteredWrapper.getWrapperForTestItem(invisible, textDocumentFilter); + + assert.strictEqual(invisibleParentWrapper.hasNodeMatchingFilter, false); + invisible.location = location1 as any; + assert.strictEqual(invisibleParentWrapper.hasNodeMatchingFilter, false); + invisibleWrapper.reset(); + assert.strictEqual(invisibleParentWrapper.hasNodeMatchingFilter, true); + + // the root should be undefined due to the reset. + assert.strictEqual((rootWrapper as any).matchesFilter, undefined); + }); + }); }); });