diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 9d113148743..4b66374e5ea 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -39,6 +39,8 @@ export namespace Iterable { return false; } + export function filter(iterable: Iterable, predicate: (t: T) => t is R): Iterable; + export function filter(iterable: Iterable, predicate: (t: T) => boolean): Iterable; export function* filter(iterable: Iterable, predicate: (t: T) => boolean): Iterable { for (const element of iterable) { if (predicate(element)) { diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 4ee971c82f6..0fd593807bc 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -6,7 +6,6 @@ import { mapFind } from 'vs/base/common/arrays'; import { disposableTimeout, RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { throttle } from 'vs/base/common/decorators'; import { Emitter } from 'vs/base/common/event'; import { once } from 'vs/base/common/functional'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; @@ -19,7 +18,8 @@ import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { TestItem } from 'vs/workbench/api/common/extHostTypeConverters'; import { Disposable, RequiredTestItem } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; +import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsResult, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import type * as vscode from 'vscode'; const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`; @@ -224,210 +224,6 @@ export class ExtHostTesting implements ExtHostTestingShape { } } -const keyMap: { [K in keyof Omit]: null } = { - label: null, - location: null, - state: null, - debuggable: null, - description: null, - runnable: null -}; - -const simpleProps = Object.keys(keyMap) as ReadonlyArray; - -const itemEqualityComparator = (a: vscode.TestItem) => { - const values: unknown[] = []; - for (const prop of simpleProps) { - values.push(a[prop]); - } - - return (b: vscode.TestItem) => { - for (let i = 0; i < simpleProps.length; i++) { - if (values[i] !== b[simpleProps[i]]) { - return false; - } - } - - return true; - }; -}; - -/** - * @private - */ -export interface OwnedCollectionTestItem extends InternalTestItem { - actual: vscode.TestItem; - previousChildren: Set; - previousEquals: (v: vscode.TestItem) => boolean; -} - -/** - * @private - */ -export class OwnedTestCollection { - protected readonly testIdToInternal = new Map(); - - /** - * Gets test information by ID, if it was defined and still exists in this - * extension host. - */ - public getTestById(id: string) { - return this.testIdToInternal.get(id); - } - - /** - * Creates a new test collection for a specific hierarchy for a workspace - * or document observation. - */ - public createForHierarchy(publishDiff: (diff: TestsDiff) => void = () => undefined) { - return new SingleUseTestCollection(this.testIdToInternal, publishDiff); - } -} - -/** - * Maintains tests created and registered for a single set of hierarchies - * for a workspace or document. - * @private - */ -export class SingleUseTestCollection implements IDisposable { - protected readonly testItemToInternal = new Map(); - protected diff: TestsDiff = []; - private disposed = false; - - /** - * Debouncer for sending diffs. We use both a throttle and a debounce here, - * so that tests that all change state simultenously are effected together, - * but so we don't send hundreds of test updates per second to the main thread. - */ - private readonly debounceSendDiff = new RunOnceScheduler(() => this.throttleSendDiff(), 2); - - constructor(private readonly testIdToInternal: Map, private readonly publishDiff: (diff: TestsDiff) => void) { } - - /** - * Adds a new root node to the collection. - */ - public addRoot(item: vscode.TestItem, providerId: string) { - this.addItem(item, providerId, null); - this.debounceSendDiff.schedule(); - } - - /** - * Gets test information by its reference, if it was defined and still exists - * in this extension host. - */ - public getTestByReference(item: vscode.TestItem) { - return this.testItemToInternal.get(item); - } - - /** - * Should be called when an item change is fired on the test provider. - */ - public onItemChange(item: vscode.TestItem, providerId: string) { - const existing = this.testItemToInternal.get(item); - if (!existing) { - if (!this.disposed) { - console.warn(`Received a TestProvider.onDidChangeTest for a test that wasn't seen before as a child.`); - } - return; - } - - this.addItem(item, providerId, existing.parent); - this.debounceSendDiff.schedule(); - } - - /** - * Gets a diff of all changes that have been made, and clears the diff queue. - */ - public collectDiff() { - const diff = this.diff; - this.diff = []; - return diff; - } - - public dispose() { - for (const item of this.testItemToInternal.values()) { - this.testIdToInternal.delete(item.id); - } - - this.diff = []; - this.disposed = true; - } - - protected getId(): string { - return generateUuid(); - } - - private addItem(actual: vscode.TestItem, providerId: string, parent: string | null) { - let internal = this.testItemToInternal.get(actual); - if (!internal) { - internal = { - actual, - id: this.getId(), - parent, - item: TestItem.from(actual), - providerId, - previousChildren: new Set(), - previousEquals: itemEqualityComparator(actual), - }; - - this.testItemToInternal.set(actual, internal); - this.testIdToInternal.set(internal.id, internal); - this.diff.push([TestDiffOpType.Add, { id: internal.id, parent, providerId, item: internal.item }]); - } else if (!internal.previousEquals(actual)) { - internal.item = TestItem.from(actual); - internal.previousEquals = itemEqualityComparator(actual); - this.diff.push([TestDiffOpType.Update, { id: internal.id, parent, providerId, item: internal.item }]); - } - - // If there are children, track which ones are deleted - // and recursively and/update them. - if (actual.children) { - const deletedChildren = internal.previousChildren; - const currentChildren = new Set(); - for (const child of actual.children) { - const c = this.addItem(child, providerId, internal.id); - deletedChildren.delete(c.id); - currentChildren.add(c.id); - } - - for (const child of deletedChildren) { - this.removeItembyId(child); - } - - internal.previousChildren = currentChildren; - } - - - return internal; - } - - private removeItembyId(id: string) { - this.diff.push([TestDiffOpType.Remove, id]); - - const queue = [this.testIdToInternal.get(id)]; - while (queue.length) { - const item = queue.pop(); - if (!item) { - continue; - } - - this.testIdToInternal.delete(item.id); - this.testItemToInternal.delete(item.actual); - for (const child of item.previousChildren) { - queue.push(this.testIdToInternal.get(child)); - } - } - } - - @throttle(200) - protected throttleSendDiff() { - const diff = this.collectDiff(); - if (diff.length) { - this.publishDiff(diff); - } - } -} - /** * @private */ diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index a4cd5a326f6..80ffd426114 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2974,5 +2974,6 @@ export type RequiredTestItem = { [K in keyof Required]: K extends AllowedUndefined ? vscode.TestItem[K] : Required[K] }; +export type TestItem = vscode.TestItem; //#endregion diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts index f531b1f0577..38abc19ad74 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts @@ -3,11 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { Emitter } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; -import { Iterable } from 'vs/base/common/iterator'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; @@ -15,19 +13,16 @@ import { IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/work import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections'; import { HierarchicalElement, HierarchicalFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore'; -import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService'; +import { NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; import { InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; -import { testIdentityProvider as diffIdentityProvider } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; +import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService'; /** * Projection that lists tests in their traditional tree view. */ export class HierarchicalByLocationProjection extends Disposable implements ITestTreeProjection { private readonly updateEmitter = new Emitter(); - private lastHadMultipleFolders = true; - private newlyRenderedNodes = new Set(); - private updatedNodes = new Set(); - private removedNodes = new Set(); + private readonly changes = new NodeChangeList(); private readonly locations = new TestLocationStore(); /** @@ -63,7 +58,7 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes } for (const folder of this.folders.values()) { - this.newlyRenderedNodes.add(folder); + this.changes.added(folder); } } @@ -72,7 +67,7 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes const existing = this.folders.get(folder.uri.toString()); if (existing) { this.folders.delete(folder.uri.toString()); - this.removedNodes.add(existing); + this.changes.removed(existing); } this.updateEmitter.fire(); } @@ -94,7 +89,7 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes case TestDiffOpType.Add: { const item = this.createItem(op[1], folder); this.storeItem(item); - this.newlyRenderedNodes.add(item); + this.changes.added(item); break; } @@ -119,28 +114,18 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes break; } - this.deleteItem(toRemove); - toRemove.parentItem.children.delete(toRemove); - this.removedNodes.add(toRemove); + this.changes.removed(toRemove); const queue: Iterable[] = [[toRemove]]; while (queue.length) { for (const item of queue.pop()!) { - this.unstoreItem(item); - this.newlyRenderedNodes.delete(item); + queue.push(this.unstoreItem(item)); } } } } } - for (const [key, folder] of this.folders) { - if (folder.children.size === 0) { - this.removedNodes.add(folder); - this.folders.delete(key); - } - } - if (diff.length !== 0) { this.updateEmitter.fire(); } @@ -150,45 +135,7 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes * @inheritdoc */ public applyTo(tree: ObjectTree) { - const firstFolder = Iterable.first(this.folders.values()); - - if (!this.lastHadMultipleFolders && this.folders.size !== 1) { - tree.setChildren(null, Iterable.map(this.folders.values(), this.renderNode), { diffIdentityProvider }); - this.lastHadMultipleFolders = true; - } else if (this.lastHadMultipleFolders && this.folders.size === 1) { - tree.setChildren(null, Iterable.map(firstFolder!.children, this.renderNode), { diffIdentityProvider }); - this.lastHadMultipleFolders = false; - } else { - for (const node of this.updatedNodes) { - if (tree.hasElement(node)) { - tree.rerender(node); - } - } - - const alreadyUpdatedChildren = new Set(); - for (const nodeList of [this.newlyRenderedNodes, this.removedNodes]) { - for (let { parentItem, children } of nodeList) { - if (!alreadyUpdatedChildren.has(parentItem)) { - if (!this.lastHadMultipleFolders && parentItem === firstFolder) { - tree.setChildren(null, Iterable.map(firstFolder.children, this.renderNode), { diffIdentityProvider }); - } else { - const pchildren: Iterable = parentItem?.children ?? this.folders.values(); - tree.setChildren(parentItem, Iterable.map(pchildren, this.renderNode), { diffIdentityProvider }); - } - - alreadyUpdatedChildren.add(parentItem); - } - - for (const child of children) { - alreadyUpdatedChildren.add(child); - } - } - } - } - - this.newlyRenderedNodes.clear(); - this.removedNodes.clear(); - this.updatedNodes.clear(); + this.changes.applyTo(tree, this.renderNode, () => this.folders.values()); } protected createItem(item: InternalTestItem, folder: IWorkspaceFolder): HierarchicalElement { @@ -196,15 +143,11 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes return new HierarchicalElement(item, parent); } - protected deleteItem(item: HierarchicalElement) { - // no-op - } - protected getOrCreateFolderElement(folder: IWorkspaceFolder) { let f = this.folders.get(folder.uri.toString()); if (!f) { f = new HierarchicalFolder(folder); - this.newlyRenderedNodes.add(f); + this.changes.added(f); this.folders.set(folder.uri.toString(), f); } @@ -213,22 +156,22 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes protected readonly addUpdated = (item: ITestTreeElement) => { const cast = item as HierarchicalElement | HierarchicalFolder; - if (!this.newlyRenderedNodes.has(cast)) { - this.updatedNodes.add(cast); + this.changes.updated(cast); + }; + + protected renderNode: NodeRenderFn = (node, recurse) => { + if (node.depth < 2 && !peersHaveChildren(node, () => this.folders.values())) { + return NodeRenderDirective.Concat; } + + return { element: node, incompressible: true, children: recurse(node.children) }; }; - private readonly renderNode = (node: HierarchicalElement | HierarchicalFolder): ICompressedTreeElement => { - return { - element: node, - incompressible: true, - children: Iterable.map(node.children, this.renderNode), - }; - }; - - private unstoreItem(item: HierarchicalElement) { + protected unstoreItem(item: HierarchicalElement) { + item.parentItem.children.delete(item); this.items.delete(item.test.id); - this.locations.add(item); + this.locations.remove(item); + return item.children; } protected storeItem(item: HierarchicalElement) { diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts index 4ac51337298..60cba9a76a9 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts @@ -8,7 +8,9 @@ import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ITestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections'; import { HierarchicalByLocationProjection as HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; import { HierarchicalElement, HierarchicalFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; +import { NodeRenderDirective } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService'; /** * Type of test element in the list. @@ -30,7 +32,7 @@ export const enum ListElementType { export class HierarchicalByNameElement extends HierarchicalElement { public elementType: ListElementType = ListElementType.Unset; public readonly isTestRoot = !this.actualParent; - private readonly actualChildren = new Set(); + public readonly actualChildren = new Set(); public get description() { let description: string | undefined; @@ -41,6 +43,10 @@ export class HierarchicalByNameElement extends HierarchicalElement { return description; } + public get testId() { + return `hintest:${this.test.id}`; + } + /** * @param actualParent Parent of the item in the test heirarchy */ @@ -111,6 +117,19 @@ export class HierarchicalByNameElement extends HierarchicalElement { * test root rather than the heirarchal parent. */ export class HierarchicalByNameProjection extends HierarchicalByLocationProjection { + constructor(listener: TestSubscriptionListener) { + super(listener); + + const originalRenderNode = this.renderNode.bind(this); + this.renderNode = (node, recurse) => { + if (node instanceof HierarchicalByNameElement && node.elementType !== ListElementType.TestLeaf && !node.isTestRoot) { + return NodeRenderDirective.Concat; + } + + return originalRenderNode(node, recurse); + }; + } + /** * @override */ @@ -129,7 +148,13 @@ export class HierarchicalByNameProjection extends HierarchicalByLocationProjecti /** * @override */ - protected deleteItem(item: HierarchicalElement) { - (item as HierarchicalByNameElement).remove(); + protected unstoreItem(item: HierarchicalElement) { + const treeChildren = super.unstoreItem(item); + if (item instanceof HierarchicalByNameElement) { + item.remove(); + return item.actualChildren; + } + + return treeChildren; } } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts index 7df3f7cd1e9..12c0029c2a3 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts @@ -19,7 +19,7 @@ export class HierarchicalElement implements ITestTreeElement { public readonly depth: number = this.parentItem.depth + 1; public get treeId() { - return `test:${this.test.id}`; + return `hitest:${this.test.id}`; } public get label() { @@ -50,10 +50,6 @@ export class HierarchicalElement implements ITestTreeElement { this.test = { ...test, item: { ...test.item } }; // clone since we Object.assign updatese } - public getChildren() { - return this.children; - } - public update(actual: InternalTestItem, addUpdated: (n: ITestTreeElement) => void) { const stateChange = actual.item.state.runState !== this.state; Object.assign(this.test, actual); @@ -73,7 +69,7 @@ export class HierarchicalFolder implements ITestTreeElement { public computedState: TestRunState | undefined; public get treeId() { - return `folder:${this.folder.index}`; + return `hifolder:${this.folder.index}`; } public get runnable() { @@ -89,10 +85,6 @@ export class HierarchicalFolder implements ITestTreeElement { public get label() { return this.folder.name; } - - public getChildren() { - return this.children; - } } /** @@ -101,7 +93,7 @@ export class HierarchicalFolder implements ITestTreeElement { export const getComputedState = (node: ITestTreeElement) => { if (node.computedState === undefined) { node.computedState = node.state ?? TestRunState.Unset; - for (const child of node.getChildren()) { + for (const child of node.children) { node.computedState = maxPriority(node.computedState, getComputedState(child)); } } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts index 1af90023fa2..78459dfe316 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CompressibleObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -35,7 +35,7 @@ export interface ITestTreeProjection extends IDisposable { /** * Applies pending update to the tree. */ - applyTo(tree: CompressibleObjectTree): void; + applyTo(tree: ObjectTree): void; } @@ -47,6 +47,8 @@ export interface ITestTreeElement { */ computedState: TestRunState | undefined; + readonly children: Set; + /** * Unique ID of the element in the tree. */ @@ -88,5 +90,4 @@ export interface ITestTreeElement { readonly state?: TestRunState; readonly label: string; readonly parentItem: ITestTreeElement | null; - getChildren(): Iterable; } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts index 0f2b942b02c..cbb7eeb351e 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts @@ -5,8 +5,8 @@ import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; -import { CompressibleObjectTree, ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; -import { Iterable } from 'vs/base/common/iterator'; +import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { ITreeElement } from 'vs/base/browser/ui/tree/tree'; import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; import { ITestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections'; @@ -31,37 +31,84 @@ export const pruneNodesWithParentsNotInTree = (nodes } }; +/** + * Returns whether there are any children for other nodes besides this one + * in the tree. + * + * This is used for omitting test provider nodes if there's only a single + * test provider in the workspace (the common case) + */ +export const peersHaveChildren = (node: ITestTreeElement, roots: () => Iterable) => { + for (const child of node.parentItem ? node.parentItem.children : roots()) { + if (child !== node && child.children.size) { + return true; + } + } + + return false; +}; + +export const enum NodeRenderDirective { + /** Omit node and all its children */ + Omit, + /** Concat children with parent */ + Concat +} + +export type NodeRenderFn = (n: T, recurse: (items: Iterable) => Iterable>) => + ITreeElement | NodeRenderDirective; + +const pruneNodesNotInTree = (nodes: Set, tree: ObjectTree) => { + for (const node of nodes) { + if (node && !tree.hasElement(node)) { + nodes.delete(node); + } + } +}; + /** * Helper to gather and bulk-apply tree updates. */ export class NodeChangeList; parentItem: T | null; }> { private changedParents = new Set(); private updatedNodes = new Set(); + private omittedNodes = new WeakSet(); + private isFirstApply = true; public updated(node: T) { this.updatedNodes.add(node); } public removed(node: T) { - this.changedParents.add(node.parentItem); + this.added(node); } public added(node: T) { - this.changedParents.add(node.parentItem); + this.changedParents.add(this.getNearestNotOmittedParent(node)); } public applyTo( - tree: CompressibleObjectTree, - renderNode: (n: T) => ICompressedTreeElement, + tree: ObjectTree, + renderNode: NodeRenderFn, roots: () => Iterable, ) { - pruneNodesWithParentsNotInTree(this.changedParents, tree); - pruneNodesWithParentsNotInTree(this.updatedNodes, tree); + pruneNodesNotInTree(this.changedParents, tree); + pruneNodesNotInTree(this.updatedNodes, tree); + + const diffDeep = this.isFirstApply ? Infinity : 0; + this.isFirstApply = false; + + for (let parent of this.changedParents) { + while (parent && typeof renderNode(parent, () => []) !== 'object') { + parent = parent.parentItem; + } - for (const parent of this.changedParents) { if (parent === null || tree.hasElement(parent)) { - const pchildren: Iterable = parent ? parent.children : roots(); - tree.setChildren(parent, Iterable.map(pchildren, renderNode), { diffIdentityProvider: testIdentityProvider }); + tree.setChildren( + parent, + this.renderNodeList(renderNode, parent === null ? roots() : parent.children), + { diffIdentityProvider: testIdentityProvider, diffDeep }, + ); } } @@ -74,4 +121,30 @@ export class NodeChangeList this.changedParents.clear(); this.updatedNodes.clear(); } + + private getNearestNotOmittedParent(node: T | null) { + let parent = node && node.parentItem; + while (parent && this.omittedNodes.has(parent)) { + parent = parent.parentItem; + } + + return parent; + } + + private *renderNodeList(renderNode: NodeRenderFn, nodes: Iterable): Iterable> { + for (const node of nodes) { + const rendered = renderNode(node, this.renderNodeList.bind(this, renderNode)); + if (rendered === NodeRenderDirective.Omit) { + this.omittedNodes.add(node); + } else if (rendered === NodeRenderDirective.Concat) { + this.omittedNodes.add(node); + for (const nested of this.renderNodeList(renderNode, node.children)) { + yield nested; + } + } else { + this.omittedNodes.delete(node); + yield rendered; + } + } + } } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation.ts index c0923c97625..8f2a5f74e2d 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; -import { CompressibleObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { Emitter } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { Iterable } from 'vs/base/common/iterator'; @@ -15,11 +14,11 @@ import { Location as ModeLocation } from 'vs/editor/common/modes'; import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections'; import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore'; -import { isRunningState, NodeChangeList } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; +import { isRunningState, NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes'; import { statesInOrder } from 'vs/workbench/contrib/testing/browser/testExplorerTree'; -import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService'; import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService'; interface IStatusTestItem extends IncrementalTestCollectionItem { treeElements: Map; @@ -35,7 +34,7 @@ class TestStateElement implements ITestTreeElement { public computedState = this.state; public get treeId() { - return `test:${this.test.id}`; + return `sltest:${this.test.id}`; } public get label() { @@ -69,10 +68,6 @@ class TestStateElement implements ITestTreeElement { public readonly depth = this.test.depth; public readonly children = new Set(); - getChildren(): Iterable { - return this.children; - } - constructor( public readonly state: TestRunState, public readonly test: IStatusTestItem, @@ -156,15 +151,22 @@ export class StateByLocationProjection extends AbstractIncrementalTestCollection /** * @inheritdoc */ - public applyTo(tree: CompressibleObjectTree) { + public applyTo(tree: ObjectTree) { this.changes.applyTo(tree, this.renderNode, () => this.stateRoots.values()); } - private readonly renderNode = (node: TreeElement): ICompressedTreeElement => { + private readonly renderNode: NodeRenderFn = (node, recurse) => { + if (node.depth === 1 /* test provider */) { + if (node.children.size === 0) { + return NodeRenderDirective.Omit; + } else if (!peersHaveChildren(node, () => this.stateRoots.values())) { + return NodeRenderDirective.Concat; + } + } + return { element: node, - incompressible: node.depth > 0, - children: Iterable.map(node.children, this.renderNode), + children: recurse(node.children), }; }; @@ -315,7 +317,7 @@ export class StateByLocationProjection extends AbstractIncrementalTestCollection protected createItem(item: InternalTestItem, parentItem?: IStatusTestItem): IStatusTestItem { return { ...item, - depth: parentItem ? parentItem.depth + 1 : 0, + depth: parentItem ? parentItem.depth + 1 : 1, parentItem: parentItem, previousState: item.item.state.runState, location: item.item.location, diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByName.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByName.ts index 070c9a11b82..26e5d99e005 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByName.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByName.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; -import { CompressibleObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { Emitter } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { Iterable } from 'vs/base/common/iterator'; @@ -16,16 +15,16 @@ import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections'; import { ListElementType } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore'; -import { NodeChangeList } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; +import { isRunningState, NodeChangeList, NodeRenderFn } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes'; -import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService'; import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService'; class ListTestStateElement implements ITestTreeElement { public computedState = this.test.item.state.runState; public get treeId() { - return `test:${this.test.id}`; + return `sntest:${this.test.id}`; } public get label() { @@ -58,11 +57,7 @@ class ListTestStateElement implements ITestTreeElement { } public readonly depth = 1; - public readonly children = Iterable.empty(); - - getChildren(): Iterable { - return Iterable.empty(); - } + public readonly children = new Set(); constructor( public readonly test: IStatusListTestItem, @@ -143,15 +138,14 @@ export class StateByNameProjection extends AbstractIncrementalTestCollection) { + public applyTo(tree: ObjectTree) { this.changes.applyTo(tree, this.renderNode, () => this.stateRoots.values()); } - private readonly renderNode = (node: TreeElement): ICompressedTreeElement => { + private readonly renderNode: NodeRenderFn = (node, recurse) => { return { element: node, - incompressible: true, - children: node instanceof StateElement ? Iterable.map(node.children, this.renderNode) : undefined, + children: node instanceof StateElement ? recurse(node.children) : undefined, }; }; @@ -164,22 +158,22 @@ export class StateByNameProjection extends AbstractIncrementalTestCollection { + remove: (node, isNested) => { if (node.node) { this.locations.remove(node); } // for the top node being deleted, we need to update parents. For - // others we only need to remove them from the tree view. - if (isRoot) { - this.removeNode(node); - } else { + // others we only need to remove them from the locations cache. + if (isNested) { this.removeNodeSingle(node); + } else { + this.removeNode(node); } }, update: node => { if (node.item.state.runState !== node.previousState && node.node) { - if (node.item.state.runState === TestRunState.Running) { + if (isRunningState(node.item.state.runState)) { node.node.computedState = node.item.state.runState; } else { this.removeNode(node); @@ -210,7 +204,7 @@ export class StateByNameProjection extends AbstractIncrementalTestCollection this.items.get(c)!.type !== ListElementType.BranchWithoutLeaf) + const newType = Iterable.some(item.children, c => this.items.get(c)?.type !== ListElementType.BranchWithoutLeaf) ? ListElementType.BranchWithLeaf : item.item.runnable ? ListElementType.TestLeaf diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateNodes.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/stateNodes.ts index c6f9c83b507..5b3fed98aca 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateNodes.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/stateNodes.ts @@ -15,7 +15,7 @@ export class StateElement implements ITestTreeElemen public computedState = this.state; public get treeId() { - return `state:${this.state}`; + return `sestate:${this.state}`; } public readonly depth = 0; @@ -23,10 +23,6 @@ export class StateElement implements ITestTreeElemen public readonly parentItem = null; public readonly children = new Set(); - getChildren(): Iterable { - return this.children; - } - public get runnable() { return Iterable.concatNested(Iterable.map(this.children, c => c.runnable)); } diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index df7830264a0..e5927f74b1c 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -5,11 +5,11 @@ import * as dom from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; -import { CompressibleObjectTree, ICompressibleKeyboardNavigationLabelProvider, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; -import { ITreeEvent, ITreeFilter, ITreeNode, ITreeSorter, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; +import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { ITreeEvent, ITreeFilter, ITreeNode, ITreeRenderer, ITreeSorter, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { throttle } from 'vs/base/common/decorators'; import { Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; @@ -28,7 +28,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { FileKind } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; +import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -42,21 +42,21 @@ import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewl import { IViewDescriptorService } from 'vs/workbench/common/views'; import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections'; import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; -import { HierarchicalByNameElement, HierarchicalByNameProjection, ListElementType } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; +import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; import { getComputedState } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; import { StateByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation'; import { StateByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateByName'; import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes'; import { testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons'; import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/browser/testExplorerTree'; -import { ITestingCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService'; import { TestingExplorerFilter, TestingFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; +import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; import { TestExplorerViewGrouping, TestExplorerViewMode } from 'vs/workbench/contrib/testing/common/constants'; +import { ITestingCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { DebugAction, RunAction } from './testExplorerActions'; -import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; export class TestingExplorerView extends ViewPane { public viewModel!: TestingExplorerViewModel; @@ -161,7 +161,7 @@ export class TestingExplorerView extends ViewPane { } export class TestingExplorerViewModel extends Disposable { - public tree: CompressibleObjectTree; + public tree: ObjectTree; private filter: TestsFilter; public projection!: ITestTreeProjection; @@ -227,7 +227,7 @@ export class TestingExplorerViewModel extends Disposable { })); this.tree = instantiationService.createInstance( - WorkbenchCompressibleObjectTree, + WorkbenchObjectTree, 'Test Explorer List', listContainer, new ListDelegate(), @@ -241,7 +241,7 @@ export class TestingExplorerViewModel extends Disposable { keyboardNavigationLabelProvider: instantiationService.createInstance(TreeKeyboardNavigationLabelProvider), accessibilityProvider: instantiationService.createInstance(ListAccessibilityProvider), filter: this.filter, - }) as WorkbenchCompressibleObjectTree; + }) as WorkbenchObjectTree; this._register(this.tree); this.updatePreferredProjection(); @@ -482,15 +482,11 @@ class TestsFilter implements ITreeFilter { } public filter(element: ITestTreeElement): TreeFilterResult { - if (element instanceof HierarchicalByNameElement && element.elementType !== ListElementType.TestLeaf && !element.isTestRoot) { - return TreeVisibility.Hidden; - } - if (this.testFilterText(element.label)) { return TreeVisibility.Visible; } - return Iterable.isEmpty(element.getChildren()) ? TreeVisibility.Hidden : TreeVisibility.Recurse; + return Iterable.isEmpty(element.children) ? TreeVisibility.Hidden : TreeVisibility.Recurse; } private testFilterText(data: string) { @@ -532,7 +528,7 @@ class ListAccessibilityProvider implements IListAccessibilityProvider { +class TreeKeyboardNavigationLabelProvider implements IKeyboardNavigationLabelProvider { getCompressedNodeKeyboardNavigationLabel(elements: ITestTreeElement[]) { return this.getKeyboardNavigationLabel(elements[elements.length - 1]); } @@ -564,7 +560,7 @@ interface TestTemplateData { actionBar: ActionBar; } -class TestsRenderer implements ICompressibleTreeRenderer { +class TestsRenderer implements ITreeRenderer { public static readonly ID = 'testExplorer'; constructor( diff --git a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts new file mode 100644 index 00000000000..5d98a05714e --- /dev/null +++ b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts @@ -0,0 +1,216 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from 'vs/base/common/async'; +import { throttle } from 'vs/base/common/decorators'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { generateUuid } from 'vs/base/common/uuid'; +import { TestItem } from 'vs/workbench/api/common/extHostTypeConverters'; +import { RequiredTestItem, TestItem as ApiTestItem } from 'vs/workbench/api/common/extHostTypes'; +import { InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; + +/** + * @private + */ +export class OwnedTestCollection { + protected readonly testIdToInternal = new Map(); + + /** + * Gets test information by ID, if it was defined and still exists in this + * extension host. + */ + public getTestById(id: string) { + return this.testIdToInternal.get(id); + } + + /** + * Creates a new test collection for a specific hierarchy for a workspace + * or document observation. + */ + public createForHierarchy(publishDiff: (diff: TestsDiff) => void = () => undefined) { + return new SingleUseTestCollection(this.testIdToInternal, publishDiff); + } +} +/** + * @private + */ +export interface OwnedCollectionTestItem extends InternalTestItem { + actual: ApiTestItem; + previousChildren: Set; + previousEquals: (v: ApiTestItem) => boolean; +} + + +/** + * Maintains tests created and registered for a single set of hierarchies + * for a workspace or document. + * @private + */ +export class SingleUseTestCollection implements IDisposable { + protected readonly testItemToInternal = new Map(); + protected diff: TestsDiff = []; + private disposed = false; + + /** + * Debouncer for sending diffs. We use both a throttle and a debounce here, + * so that tests that all change state simultenously are effected together, + * but so we don't send hundreds of test updates per second to the main thread. + */ + private readonly debounceSendDiff = new RunOnceScheduler(() => this.throttleSendDiff(), 2); + + constructor(private readonly testIdToInternal: Map, private readonly publishDiff: (diff: TestsDiff) => void) { } + + /** + * Adds a new root node to the collection. + */ + public addRoot(item: ApiTestItem, providerId: string) { + this.addItem(item, providerId, null); + this.debounceSendDiff.schedule(); + } + + /** + * Gets test information by its reference, if it was defined and still exists + * in this extension host. + */ + public getTestByReference(item: ApiTestItem) { + return this.testItemToInternal.get(item); + } + + /** + * Should be called when an item change is fired on the test provider. + */ + public onItemChange(item: ApiTestItem, providerId: string) { + const existing = this.testItemToInternal.get(item); + if (!existing) { + if (!this.disposed) { + console.warn(`Received a TestProvider.onDidChangeTest for a test that wasn't seen before as a child.`); + } + return; + } + + this.addItem(item, providerId, existing.parent); + this.debounceSendDiff.schedule(); + } + + /** + * Gets a diff of all changes that have been made, and clears the diff queue. + */ + public collectDiff() { + const diff = this.diff; + this.diff = []; + return diff; + } + + public dispose() { + for (const item of this.testItemToInternal.values()) { + this.testIdToInternal.delete(item.id); + } + + this.diff = []; + this.disposed = true; + } + + protected getId(): string { + return generateUuid(); + } + + private addItem(actual: ApiTestItem, providerId: string, parent: string | null) { + let internal = this.testItemToInternal.get(actual); + if (!internal) { + internal = { + actual, + id: this.getId(), + parent, + item: TestItem.from(actual), + providerId, + previousChildren: new Set(), + previousEquals: itemEqualityComparator(actual), + }; + + this.testItemToInternal.set(actual, internal); + this.testIdToInternal.set(internal.id, internal); + this.diff.push([TestDiffOpType.Add, { id: internal.id, parent, providerId, item: internal.item }]); + } else if (!internal.previousEquals(actual)) { + internal.item = TestItem.from(actual); + internal.previousEquals = itemEqualityComparator(actual); + this.diff.push([TestDiffOpType.Update, { id: internal.id, parent, providerId, item: internal.item }]); + } + + // If there are children, track which ones are deleted + // and recursively and/update them. + if (actual.children) { + const deletedChildren = internal.previousChildren; + const currentChildren = new Set(); + for (const child of actual.children) { + const c = this.addItem(child, providerId, internal.id); + deletedChildren.delete(c.id); + currentChildren.add(c.id); + } + + for (const child of deletedChildren) { + this.removeItembyId(child); + } + + internal.previousChildren = currentChildren; + } + + + return internal; + } + + private removeItembyId(id: string) { + this.diff.push([TestDiffOpType.Remove, id]); + + const queue = [this.testIdToInternal.get(id)]; + while (queue.length) { + const item = queue.pop(); + if (!item) { + continue; + } + + this.testIdToInternal.delete(item.id); + this.testItemToInternal.delete(item.actual); + for (const child of item.previousChildren) { + queue.push(this.testIdToInternal.get(child)); + } + } + } + + @throttle(200) + protected throttleSendDiff() { + const diff = this.collectDiff(); + if (diff.length) { + this.publishDiff(diff); + } + } +} + +const keyMap: { [K in keyof Omit]: null } = { + label: null, + location: null, + state: null, + debuggable: null, + description: null, + runnable: null +}; + +const simpleProps = Object.keys(keyMap) as ReadonlyArray; + +const itemEqualityComparator = (a: ApiTestItem) => { + const values: unknown[] = []; + for (const prop of simpleProps) { + values.push(a[prop]); + } + + return (b: ApiTestItem) => { + for (let i = 0; i < simpleProps.length; i++) { + if (values[i] !== b[simpleProps[i]]) { + return false; + } + } + + return true; + }; +}; diff --git a/src/vs/workbench/contrib/testing/common/testStubs.ts b/src/vs/workbench/contrib/testing/common/testStubs.ts new file mode 100644 index 00000000000..33a9b5295ad --- /dev/null +++ b/src/vs/workbench/contrib/testing/common/testStubs.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TestItem, TestRunState, TestState } from 'vs/workbench/api/common/extHostTypes'; + +export const stubTest = (label: string): TestItem => ({ + label, + location: undefined, + state: new TestState(TestRunState.Unset), + debuggable: true, + runnable: true, + description: '' +}); + +export const testStubs = { + test: stubTest, + nested: () => ({ + ...stubTest('root'), + children: [ + { ...stubTest('a'), children: [stubTest('aa'), stubTest('ab')] }, + stubTest('b'), + ], + }), +}; + +export const ReExportedTestRunState = TestRunState; diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts new file mode 100644 index 00000000000..3975417f6c3 --- /dev/null +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; +import { testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; +import { makeTestWorkspaceFolder, TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; + +suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { + let harness: TestTreeTestHarness; + const folder1 = makeTestWorkspaceFolder('f1'); + const folder2 = makeTestWorkspaceFolder('f2'); + setup(() => { + harness = new TestTreeTestHarness(l => new HierarchicalByLocationProjection(l)); + }); + + teardown(() => { + harness.dispose(); + }); + + test('renders initial tree', () => { + harness.c.addRoot(testStubs.nested(), 'a'); + assert.deepStrictEqual(harness.flush(folder1), [ + { e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' } + ]); + }); + + test('updates render if a second folder is added', () => { + harness.c.addRoot(testStubs.nested(), 'a'); + harness.flush(folder1); + harness.c.addRoot(testStubs.nested(), 'a'); + harness.flush(folder2); + assert.deepStrictEqual(harness.flush(folder1), [ + { e: 'f1', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] }, + { e: 'f2', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] }, + ]); + }); + + test('updates render if second folder is removed', () => { + harness.c.addRoot(testStubs.nested(), 'a'); + harness.flush(folder1); + harness.c.addRoot(testStubs.nested(), 'a'); + harness.flush(folder2); + harness.onFolderChange.fire({ added: [], changed: [], removed: [folder1] }); + assert.deepStrictEqual(harness.flush(folder1), [ + { e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }, + ]); + }); + + test('updates render if second test provider appears', () => { + harness.c.addRoot(testStubs.nested(), 'a'); + harness.flush(folder1); + harness.c.addRoot({ + ...testStubs.test('root2'), + children: [testStubs.test('c')] + }, 'b'); + assert.deepStrictEqual(harness.flush(folder1), [ + { e: 'root', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] }, + { e: 'root2', children: [{ e: 'c' }] }, + ]); + }); + + test('updates nodes if they add children', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(folder1); + + tests.children[0].children?.push(testStubs.test('ac')); + harness.c.onItemChange(tests.children[0], 'a'); + + assert.deepStrictEqual(harness.flush(folder1), [ + { e: 'a', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'ac' }] }, + { e: 'b' } + ]); + }); + + test('updates nodes if they remove children', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(folder1); + + tests.children[0].children?.pop(); + harness.c.onItemChange(tests.children[0], 'a'); + + assert.deepStrictEqual(harness.flush(folder1), [ + { e: 'a', children: [{ e: 'aa' }] }, + { e: 'b' } + ]); + }); +}); + diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts new file mode 100644 index 00000000000..b699c910290 --- /dev/null +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; +import { testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; +import { makeTestWorkspaceFolder, TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; + +suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { + let harness: TestTreeTestHarness; + const folder1 = makeTestWorkspaceFolder('f1'); + const folder2 = makeTestWorkspaceFolder('f2'); + setup(() => { + harness = new TestTreeTestHarness(l => new HierarchicalByNameProjection(l)); + }); + + teardown(() => { + harness.dispose(); + }); + + test('renders initial tree', () => { + harness.c.addRoot(testStubs.nested(), 'a'); + assert.deepStrictEqual(harness.flush(folder1), [ + { e: 'aa' }, { e: 'ab' }, { e: 'b' } + ]); + }); + + test('updates render if a second folder is added', () => { + harness.c.addRoot(testStubs.nested(), 'a'); + harness.flush(folder1); + harness.c.addRoot(testStubs.nested(), 'a'); + harness.flush(folder2); + assert.deepStrictEqual(harness.flush(folder1), [ + { e: 'f1', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] }, + { e: 'f2', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] }, + ]); + }); + + test('updates render if second folder is removed', () => { + harness.c.addRoot(testStubs.nested(), 'a'); + harness.flush(folder1); + harness.c.addRoot(testStubs.nested(), 'a'); + harness.flush(folder2); + harness.onFolderChange.fire({ added: [], changed: [], removed: [folder1] }); + assert.deepStrictEqual(harness.flush(folder1), [ + { e: 'aa' }, { e: 'ab' }, { e: 'b' }, + ]); + }); + + test('updates render if second test provider appears', () => { + harness.c.addRoot(testStubs.nested(), 'a'); + harness.flush(folder1); + harness.c.addRoot({ + ...testStubs.test('root2'), + children: [testStubs.test('c')] + }, 'b'); + assert.deepStrictEqual(harness.flush(folder1), [ + { e: 'root', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] }, + { e: 'root2', children: [{ e: 'c' }] }, + ]); + }); + + test('updates nodes if they add children', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(folder1); + + tests.children[0].children?.push(testStubs.test('ac')); + harness.c.onItemChange(tests.children[0], 'a'); + + assert.deepStrictEqual(harness.flush(folder1), [ + { e: 'aa' }, + { e: 'ab' }, + { e: 'ac' }, + { e: 'b' } + ]); + }); + + test('updates nodes if they remove children', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(folder1); + + tests.children[0].children?.pop(); + harness.c.onItemChange(tests.children[0], 'a'); + + assert.deepStrictEqual(harness.flush(folder1), [ + { e: 'aa' }, + { e: 'b' } + ]); + }); + + test('swaps when node is no longer leaf', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(folder1); + + tests.children[1].children = [testStubs.test('ba')]; + harness.c.onItemChange(tests.children[1], 'a'); + + assert.deepStrictEqual(harness.flush(folder1), [ + { e: 'aa' }, + { e: 'ab' }, + { e: 'ba' }, + ]); + }); + + test('swaps when node is no longer runnable', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(folder1); + + tests.children[1].children = [testStubs.test('ba')]; + harness.c.onItemChange(tests.children[0], 'a'); + harness.flush(folder1); + + tests.children[1].children[0].runnable = false; + harness.c.onItemChange(tests.children[1].children[0], 'a'); + + assert.deepStrictEqual(harness.flush(folder1), [ + { e: 'aa' }, + { e: 'ab' }, + { e: 'b' }, + ]); + }); +}); + diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/stateByLocation.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/stateByLocation.test.ts new file mode 100644 index 00000000000..8472bd48c4d --- /dev/null +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/stateByLocation.test.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { StateByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation'; +import { ReExportedTestRunState as TestRunState, testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; +import { TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; + +suite('Workbench - Testing Explorer State by Location Projection', () => { + let harness: TestTreeTestHarness; + setup(() => { + harness = new TestTreeTestHarness(l => new StateByLocationProjection(l)); + }); + + teardown(() => { + harness.dispose(); + }); + + test('renders initial tree', () => { + harness.c.addRoot(testStubs.nested(), 'a'); + assert.deepStrictEqual(harness.flush(), [ + { e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] } + ]); + }); + + test('expands if second root is added', () => { + harness.c.addRoot(testStubs.nested(), 'a'); + harness.flush(); + harness.c.addRoot({ + ...testStubs.test('root2'), + children: [testStubs.test('c')] + }, 'b'); + assert.deepStrictEqual(harness.flush(), [ + { + e: 'Unset', children: [ + { e: 'root', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] }, + { e: 'root2', children: [{ e: 'c' }] }, + ] + } + ]); + }); + + test('recompacts if second root children are removed', () => { + harness.c.addRoot(testStubs.nested(), 'a'); + harness.flush(); + const root2 = { + ...testStubs.test('root2'), + children: [testStubs.test('c')] + }; + + harness.c.addRoot(root2, 'b'); + harness.flush(); + + root2.children.pop(); + harness.c.onItemChange(root2, 'b'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] } + ]); + }); + + test('updates nodes if they change', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(); + + tests.children[0].label = 'changed'; + harness.c.onItemChange(tests.children[0], 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Unset', children: [{ e: 'changed', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] } + ]); + }); + + test('updates nodes if they add children', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(); + + tests.children[0].children?.push(testStubs.test('ac')); + harness.c.onItemChange(tests.children[0], 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'ac' }] }, { e: 'b' }] } + ]); + }); + + test('updates nodes if they remove children', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(); + + tests.children[0].children?.pop(); + harness.c.onItemChange(tests.children[0], 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }] }, { e: 'b' }] } + ]); + }); + + test('moves nodes when states change', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(); + + const subchild = tests.children[0].children![0]; + subchild.state = { runState: TestRunState.Passed, messages: [] }; + harness.c.onItemChange(subchild, 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Passed', children: [{ e: 'a', children: [{ e: 'aa' }] }] }, + { e: 'Unset', children: [{ e: 'a', children: [{ e: 'ab' }] }, { e: 'b' }] }, + ]); + + subchild.state = { runState: TestRunState.Failed, messages: [] }; + harness.c.onItemChange(subchild, 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Failed', children: [{ e: 'a', children: [{ e: 'aa' }] }] }, + { e: 'Unset', children: [{ e: 'a', children: [{ e: 'ab' }] }, { e: 'b' }] }, + ]); + + subchild.state = { runState: TestRunState.Unset, messages: [] }; + harness.c.onItemChange(subchild, 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] }, + ]); + }); + + test('does not move when state is running', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(); + + const subchild = tests.children[0].children![0]; + subchild.state = { runState: TestRunState.Running, messages: [] }; + harness.c.onItemChange(subchild, 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] }, + ]); + + subchild.state = { runState: TestRunState.Failed, messages: [] }; + harness.c.onItemChange(subchild, 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Failed', children: [{ e: 'a', children: [{ e: 'aa' }] }] }, + { e: 'Unset', children: [{ e: 'a', children: [{ e: 'ab' }] }, { e: 'b' }] }, + ]); + }); +}); diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/stateByName.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/stateByName.test.ts new file mode 100644 index 00000000000..2670617d2a3 --- /dev/null +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/stateByName.test.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { StateByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateByName'; +import { ReExportedTestRunState as TestRunState, testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; +import { TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; + +suite('Workbench - Testing Explorer State by Name Projection', () => { + let harness: TestTreeTestHarness; + setup(() => { + harness = new TestTreeTestHarness(l => new StateByNameProjection(l)); + }); + + teardown(() => { + harness.dispose(); + }); + + test('renders initial tree', () => { + harness.c.addRoot(testStubs.nested(), 'a'); + assert.deepStrictEqual(harness.flush(), [ + { e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] } + ]); + }); + + test('swaps when node becomes leaf', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(); + + tests.children[0].children = []; + harness.c.onItemChange(tests.children[0], 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Unset', children: [{ e: 'a' }, { e: 'b' }] } + ]); + }); + + test('swaps when node is no longer leaf', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(); + + tests.children[1].children = [testStubs.test('ba')]; + harness.c.onItemChange(tests.children[1], 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'ba' }] } + ]); + }); + + test('swaps when node is no longer runnable', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(); + + tests.children[1].children = [testStubs.test('ba')]; + harness.c.onItemChange(tests.children[0], 'a'); + harness.flush(); + + tests.children[1].children[0].runnable = false; + harness.c.onItemChange(tests.children[1].children[0], 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] } + ]); + }); + + test('moves nodes when states change', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(); + + const subchild = tests.children[0].children![0]; + subchild.state = { runState: TestRunState.Passed, messages: [] }; + harness.c.onItemChange(subchild, 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Passed', children: [{ e: 'aa' }] }, + { e: 'Unset', children: [{ e: 'ab' }, { e: 'b' }] }, + ]); + + subchild.state = { runState: TestRunState.Failed, messages: [] }; + harness.c.onItemChange(subchild, 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Failed', children: [{ e: 'aa' }] }, + { e: 'Unset', children: [{ e: 'ab' }, { e: 'b' }] }, + ]); + + subchild.state = { runState: TestRunState.Unset, messages: [] }; + harness.c.onItemChange(subchild, 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] } + ]); + }); + + test('does not move when state is running', () => { + const tests = testStubs.nested(); + harness.c.addRoot(tests, 'a'); + harness.flush(); + + const subchild = tests.children[0].children![0]; + subchild.state = { runState: TestRunState.Running, messages: [] }; + harness.c.onItemChange(subchild, 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] } + ]); + + subchild.state = { runState: TestRunState.Failed, messages: [] }; + harness.c.onItemChange(subchild, 'a'); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'Failed', children: [{ e: 'aa' }] }, + { e: 'Unset', children: [{ e: 'ab' }, { e: 'b' }] }, + ]); + }); +}); diff --git a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts new file mode 100644 index 00000000000..c6a42068354 --- /dev/null +++ b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IWorkspaceFolder, IWorkspaceFolderData, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; +import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections'; +import { TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService'; +import { TestOwnedTestCollection, TestSingleUseCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; + +type SerializedTree = { e: string; children?: SerializedTree[] }; + +const element = document.createElement('div'); +element.style.height = '1000px'; +element.style.width = '200px'; + +export class TestObjectTree extends ObjectTree { + constructor(serializer: (node: T) => string) { + super( + 'test', + element, + { + getHeight: () => 20, + getTemplateId: () => 'default' + }, + [ + { + disposeTemplate: () => undefined, + renderElement: (node, _index, container: HTMLElement) => { + container.textContent = `${node.depth}:${serializer(node.element)}`; + }, + renderTemplate: c => c, + templateId: 'default' + } + ], + { + sorter: { + compare: (a, b) => serializer(a).localeCompare(serializer(b)) + } + } + ); + this.layout(1000, 200); + } + + public getModel() { + return this.model; + } + + public getRendered() { + const elements = element.querySelectorAll('.monaco-tl-contents'); + const sorted = [...elements].sort((a, b) => pos(a) - pos(b)); + let chain: SerializedTree[] = [{ e: '', children: [] }]; + for (const element of sorted) { + const [depthStr, label] = element.textContent!.split(':'); + const depth = Number(depthStr); + const parent = chain[depth - 1]; + const child = { e: label }; + parent.children = parent.children?.concat(child) ?? [child]; + chain[depth] = child; + } + + return chain[0].children; + } +} + +const pos = (element: Element) => Number(element.parentElement!.parentElement!.getAttribute('aria-posinset')); + +export const makeTestWorkspaceFolder = (name: string): IWorkspaceFolder => ({ + name, + uri: URI.file(`/${name}`), + index: 0, + toResource: path => URI.file(`/${name}/${path}`) +}); + +// names are hard +export class TestTreeTestHarness extends Disposable { + private readonly owned = new TestOwnedTestCollection(); + private readonly onDiff = this._register(new Emitter<[IWorkspaceFolderData, TestsDiff]>()); + public readonly onFolderChange = this._register(new Emitter()); + public readonly c: TestSingleUseCollection = this._register(this.owned.createForHierarchy(d => this.c.setDiff(d /* don't clear during testing */))); + public readonly projection: T; + public readonly tree: TestObjectTree; + + constructor(makeTree: (listener: TestSubscriptionListener) => T) { + super(); + this.projection = this._register(makeTree({ + workspaceFolderCollections: [], + onDiff: this.onDiff.event, + onFolderChange: this.onFolderChange.event, + } as any)); + this.tree = this._register(new TestObjectTree(t => t.label)); + } + + public flush(folder?: IWorkspaceFolderData) { + this.onDiff.fire([folder!, this.c.collectDiff()]); + this.projection.applyTo(this.tree); + return this.tree.getRendered(); + } +} diff --git a/src/vs/workbench/contrib/testing/test/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/test/common/ownedTestCollection.ts new file mode 100644 index 00000000000..a0f841e4b3e --- /dev/null +++ b/src/vs/workbench/contrib/testing/test/common/ownedTestCollection.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; +import { TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; + +export class TestSingleUseCollection extends SingleUseTestCollection { + private idCounter = 0; + + public get itemToInternal() { + return this.testItemToInternal; + } + + public get currentDiff() { + return this.diff; + } + + protected getId() { + return String(this.idCounter++); + } + + public setDiff(diff: TestsDiff) { + this.diff = diff; + } +} + +export class TestOwnedTestCollection extends OwnedTestCollection { + public get idToInternal() { + return this.testIdToInternal; + } + + public createForHierarchy(publishDiff: (diff: TestsDiff) => void = () => undefined) { + return new TestSingleUseCollection(this.testIdToInternal, publishDiff); + } +} diff --git a/src/vs/workbench/test/browser/api/extHostTesting.test.ts b/src/vs/workbench/test/browser/api/extHostTesting.test.ts index f143ecf17ce..71898b55689 100644 --- a/src/vs/workbench/test/browser/api/extHostTesting.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTesting.test.ts @@ -4,21 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { MirroredTestCollection, OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/api/common/extHostTesting'; +import { MirroredTestCollection } from 'vs/workbench/api/common/extHostTesting'; import * as convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { TestRunState, TestState } from 'vs/workbench/api/common/extHostTypes'; -import { TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +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'; -const stubTest = (label: string): TestItem => ({ - label, - location: undefined, - state: new TestState(TestRunState.Unset), - debuggable: true, - runnable: true, - description: '' -}); - const simplify = (item: TestItem) => { if ('toJSON' in item) { item = (item as any).toJSON(); @@ -43,44 +35,6 @@ const assertTreeListEqual = (a: ReadonlyArray>, b: ReadonlyAr a.forEach((_, i) => assertTreesEqual(a[i], b[i])); }; -const stubNestedTests = () => ({ - ...stubTest('root'), - children: [ - { ...stubTest('a'), children: [stubTest('aa'), stubTest('ab')] }, - stubTest('b'), - ] -}); - -class TestOwnedTestCollection extends OwnedTestCollection { - public get idToInternal() { - return this.testIdToInternal; - } - - public createForHierarchy(publishDiff: (diff: TestsDiff) => void = () => undefined) { - return new TestSingleUseCollection(this.testIdToInternal, publishDiff); - } -} - -class TestSingleUseCollection extends SingleUseTestCollection { - private idCounter = 0; - - public get itemToInternal() { - return this.testItemToInternal; - } - - public get currentDiff() { - return this.diff; - } - - protected getId() { - return String(this.idCounter++); - } - - public setDiff(diff: TestsDiff) { - this.diff = diff; - } -} - class TestMirroredCollection extends MirroredTestCollection { public changeEvent!: TestChangeEvent; @@ -104,12 +58,12 @@ suite('ExtHost Testing', () => { teardown(() => { single.dispose(); - assert.deepEqual(owned.idToInternal.size, 0, 'expected owned ids to be empty after dispose'); + assert.strictEqual(owned.idToInternal.size, 0, 'expected owned ids to be empty after dispose'); }); suite('OwnedTestCollection', () => { test('adds a root recursively', () => { - const tests = stubNestedTests(); + const tests = testStubs.nested(); single.addRoot(tests, 'pid'); assert.deepStrictEqual(single.collectDiff(), [ [TestDiffOpType.Add, { id: '0', providerId: 'pid', parent: null, item: convert.TestItem.from(stubTest('root')) }], @@ -121,14 +75,14 @@ suite('ExtHost Testing', () => { }); test('no-ops if items not changed', () => { - const tests = stubNestedTests(); + const tests = testStubs.nested(); single.addRoot(tests, 'pid'); single.collectDiff(); assert.deepStrictEqual(single.collectDiff(), []); }); test('watches property mutations', () => { - const tests = stubNestedTests(); + const tests = testStubs.nested(); single.addRoot(tests, 'pid'); single.collectDiff(); tests.children![0].description = 'Hello world'; /* item a */ @@ -142,7 +96,7 @@ suite('ExtHost Testing', () => { }); test('removes children', () => { - const tests = stubNestedTests(); + const tests = testStubs.nested(); single.addRoot(tests, 'pid'); single.collectDiff(); tests.children!.splice(0, 1); @@ -156,7 +110,7 @@ suite('ExtHost Testing', () => { }); test('adds new children', () => { - const tests = stubNestedTests(); + const tests = testStubs.nested(); single.addRoot(tests, 'pid'); single.collectDiff(); const child = stubTest('ac'); @@ -176,7 +130,7 @@ suite('ExtHost Testing', () => { setup(() => m = new TestMirroredCollection()); test('mirrors creation of the root', () => { - const tests = stubNestedTests(); + const tests = testStubs.nested(); single.addRoot(tests, 'pid'); m.apply(single.collectDiff()); assertTreesEqual(m.rootTestItems[0], owned.getTestById('0')!.actual); @@ -184,7 +138,7 @@ suite('ExtHost Testing', () => { }); test('mirrors node deletion', () => { - const tests = stubNestedTests(); + const tests = testStubs.nested(); single.addRoot(tests, 'pid'); m.apply(single.collectDiff()); tests.children!.splice(0, 1); @@ -196,7 +150,7 @@ suite('ExtHost Testing', () => { }); test('mirrors node addition', () => { - const tests = stubNestedTests(); + const tests = testStubs.nested(); single.addRoot(tests, 'pid'); m.apply(single.collectDiff()); tests.children![0].children!.push(stubTest('ac')); @@ -208,7 +162,7 @@ suite('ExtHost Testing', () => { }); test('mirrors node update', () => { - const tests = stubNestedTests(); + const tests = testStubs.nested(); single.addRoot(tests, 'pid'); m.apply(single.collectDiff()); tests.children![0].description = 'Hello world'; /* item a */ @@ -219,9 +173,9 @@ suite('ExtHost Testing', () => { }); suite('MirroredChangeCollector', () => { - let tests = stubNestedTests(); + let tests = testStubs.nested(); setup(() => { - tests = stubNestedTests(); + tests = testStubs.nested(); single.addRoot(tests, 'pid'); m.apply(single.collectDiff()); }); @@ -266,7 +220,7 @@ suite('ExtHost Testing', () => { }); test('is a no-op if a node is added and removed', () => { - const nested = stubNestedTests(); + const nested = testStubs.nested(); tests.children.push(nested); single.onItemChange(tests, 'pid'); tests.children.pop();