未验证 提交 7a7d11fc 编写于 作者: C Connor Peet

Merge branch 'testing-group-by-result'

......@@ -22,8 +22,8 @@ export namespace Iterable {
return iterable || _empty;
}
export function isEmpty<T>(iterable: Iterable<T>): boolean {
return iterable[Symbol.iterator]().next().done === true;
export function isEmpty<T>(iterable: Iterable<T> | undefined | null): boolean {
return !iterable || iterable[Symbol.iterator]().next().done === true;
}
export function first<T>(iterable: Iterable<T>): T | undefined {
......@@ -61,6 +61,14 @@ export namespace Iterable {
}
}
export function* concatNested<T>(iterables: Iterable<Iterable<T>>): Iterable<T> {
for (const iterable of iterables) {
for (const element of iterable) {
yield element;
}
}
}
/**
* Consumes `atMost` elements from iterable and returns the consumed elements,
* and an iterable for the rest of the elements.
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* 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';
import { IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
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/browser/testingCollectionService';
import { InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
/**
* Projection that lists tests in their traditional tree view.
*/
export class HierarchicalByLocationProjection extends Disposable implements ITestTreeProjection {
private readonly updateEmitter = new Emitter<void>();
private lastHadMultipleFolders = true;
private newlyRenderedNodes = new Set<HierarchicalElement | HierarchicalFolder>();
private updatedNodes = new Set<HierarchicalElement | HierarchicalFolder>();
private removedNodes = new Set<HierarchicalElement | HierarchicalFolder>();
private readonly locations = new TestLocationStore<HierarchicalElement>();
/**
* Map of item IDs to test item objects.
*/
protected readonly items = new Map<string, HierarchicalElement>();
/**
* Root folders
*/
protected readonly folders = new Map<string, HierarchicalFolder>();
/**
* @inheritdoc
*/
public readonly onUpdate = this.updateEmitter.event;
constructor(listener: TestSubscriptionListener) {
super();
this._register(listener.onDiff(([folder, diff]) => this.applyDiff(folder, diff)));
this._register(listener.onFolderChange(this.applyFolderChange, this));
for (const [folder, collection] of listener.workspaceFolderCollections) {
const queue = [collection.rootNodes];
while (queue.length) {
for (const id of queue.pop()!) {
const node = collection.getNodeById(id)!;
const item = this.createItem(node, folder.folder);
this.storeItem(item);
queue.push(node.children);
}
}
}
for (const folder of this.folders.values()) {
this.newlyRenderedNodes.add(folder);
}
}
private applyFolderChange(evt: IWorkspaceFoldersChangeEvent) {
for (const folder of evt.removed) {
const existing = this.folders.get(folder.uri.toString());
if (existing) {
this.folders.delete(folder.uri.toString());
this.removedNodes.add(existing);
}
this.updateEmitter.fire();
}
}
/**
* @inheritdoc
*/
public getTestAtPosition(uri: URI, position: Position) {
return this.locations.getTestAtPosition(uri, position);
}
/**
* @inheritdoc
*/
private applyDiff(folder: IWorkspaceFolder, diff: TestsDiff) {
for (const op of diff) {
switch (op[0]) {
case TestDiffOpType.Add: {
const item = this.createItem(op[1], folder);
this.storeItem(item);
this.newlyRenderedNodes.add(item);
break;
}
case TestDiffOpType.Update: {
const item = op[1];
const existing = this.items.get(item.id);
if (!existing) {
break;
}
const locationChanged = !locationsEqual(existing.location, item.item.location);
if (locationChanged) { this.locations.remove(existing); }
existing.update(item, this.addUpdated);
if (locationChanged) { this.locations.add(existing); }
this.addUpdated(existing);
break;
}
case TestDiffOpType.Remove: {
const toRemove = this.items.get(op[1]);
if (!toRemove) {
break;
}
this.deleteItem(toRemove);
toRemove.parentItem.children.delete(toRemove);
this.removedNodes.add(toRemove);
const queue: Iterable<HierarchicalElement>[] = [[toRemove]];
while (queue.length) {
for (const item of queue.pop()!) {
this.unstoreItem(item);
this.newlyRenderedNodes.delete(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();
}
}
/**
* @inheritdoc
*/
public applyTo(tree: ObjectTree<ITestTreeElement, FuzzyScore>) {
const firstFolder = Iterable.first(this.folders.values());
if (!this.lastHadMultipleFolders && this.folders.size !== 1) {
tree.setChildren(null, Iterable.map(this.folders.values(), this.renderNode));
this.lastHadMultipleFolders = true;
} else if (this.lastHadMultipleFolders && this.folders.size === 1) {
tree.setChildren(null, Iterable.map(firstFolder!.children, this.renderNode));
this.lastHadMultipleFolders = false;
} else {
for (const node of this.updatedNodes) {
if (tree.hasElement(node)) {
tree.rerender(node);
}
}
const alreadyUpdatedChildren = new Set<HierarchicalElement | HierarchicalFolder | null>();
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));
} else {
const pchildren: Iterable<HierarchicalElement | HierarchicalFolder> = parentItem?.children ?? this.folders.values();
tree.setChildren(parentItem, Iterable.map(pchildren, this.renderNode));
}
alreadyUpdatedChildren.add(parentItem);
}
for (const child of children) {
alreadyUpdatedChildren.add(child);
}
}
}
}
this.newlyRenderedNodes.clear();
this.removedNodes.clear();
this.updatedNodes.clear();
}
protected createItem(item: InternalTestItem, folder: IWorkspaceFolder): HierarchicalElement {
const parent = item.parent ? this.items.get(item.parent)! : this.getOrCreateFolderElement(folder);
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.folders.set(folder.uri.toString(), f);
}
return f;
}
protected readonly addUpdated = (item: ITestTreeElement) => {
const cast = item as HierarchicalElement | HierarchicalFolder;
if (!this.newlyRenderedNodes.has(cast)) {
this.updatedNodes.add(cast);
}
};
private readonly renderNode = (node: HierarchicalElement | HierarchicalFolder): ICompressedTreeElement<ITestTreeElement> => {
return {
element: node,
incompressible: true,
children: Iterable.map(node.children, this.renderNode),
};
};
private unstoreItem(item: HierarchicalElement) {
this.items.delete(item.test.id);
this.locations.add(item);
}
protected storeItem(item: HierarchicalElement) {
item.parentItem.children.add(item);
this.items.set(item.test.id, item);
this.locations.add(item);
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Iterable } from 'vs/base/common/iterator';
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 { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection';
/**
* Type of test element in the list.
*/
export const enum ListElementType {
/** The element is a leaf test that should be shown in the list */
TestLeaf,
/** The element is not runnable, but doesn't have any nested leaf tests */
BranchWithLeaf,
/** The element has nested leaf tests */
BranchWithoutLeaf,
/** State not yet computed */
Unset,
}
/**
* Version of the HierarchicalElement that is displayed as a list.
*/
export class HierarchicalByNameElement extends HierarchicalElement {
public elementType: ListElementType = ListElementType.Unset;
public readonly isTestRoot = !this.actualParent;
private readonly actualChildren = new Set<HierarchicalByNameElement>();
public get description() {
let description: string | undefined;
for (let parent = this.actualParent; parent && !parent.isTestRoot; parent = parent.actualParent) {
description = description ? `${parent.label}${description}` : parent.label;
}
return description;
}
/**
* @param actualParent Parent of the item in the test heirarchy
*/
constructor(
internal: InternalTestItem,
parentItem: HierarchicalFolder | HierarchicalElement,
private readonly addUpdated: (n: ITestTreeElement) => void,
private readonly actualParent?: HierarchicalByNameElement,
) {
super(internal, parentItem);
actualParent?.addChild(this);
this.updateLeafTestState();
}
/**
* @override
*/
public update(actual: InternalTestItem, addUpdated: (n: ITestTreeElement) => void) {
const wasRunnable = this.test.item.runnable;
super.update(actual, addUpdated);
if (this.test.item.runnable !== wasRunnable) {
this.updateLeafTestState();
}
}
/**
* Should be called when the list element is removed.
*/
public remove() {
this.actualParent?.removeChild(this);
}
private removeChild(element: HierarchicalByNameElement) {
this.actualChildren.delete(element);
this.updateLeafTestState();
}
private addChild(element: HierarchicalByNameElement) {
this.actualChildren.add(element);
this.updateLeafTestState();
}
/**
* Updates the test leaf state for this node. Should be called when a child
* or this node is modified. Note that we never need to look at the children
* here, the children will already be leaves, or not.
*/
private updateLeafTestState() {
const newType = Iterable.some(this.actualChildren, c => c.elementType !== ListElementType.BranchWithoutLeaf)
? ListElementType.BranchWithLeaf
: this.test.item.runnable
? ListElementType.TestLeaf
: ListElementType.BranchWithoutLeaf;
if (newType !== this.elementType) {
this.elementType = newType;
this.addUpdated(this);
}
this.actualParent?.updateLeafTestState();
}
}
/**
* Projection that shows tests in a flat list (grouped by provider). The only
* change is that, while creating the item, the item parent is set to the
* test root rather than the heirarchal parent.
*/
export class HierarchicalByNameProjection extends HierarchicalByLocationProjection {
/**
* @override
*/
protected createItem(item: InternalTestItem, folder: IWorkspaceFolder): HierarchicalElement {
const parent = this.getOrCreateFolderElement(folder);
const actualParent = item.parent ? this.items.get(item.parent) as HierarchicalByNameElement : undefined;
for (const testRoot of parent.children) {
if (testRoot.test.providerId === item.providerId) {
return new HierarchicalByNameElement(item, testRoot, this.addUpdated, actualParent);
}
}
return new HierarchicalByNameElement(item, parent, this.addUpdated);
}
/**
* @override
*/
protected deleteItem(item: HierarchicalElement) {
(item as HierarchicalByNameElement).remove();
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Iterable } from 'vs/base/common/iterator';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
import { ITestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections';
import { maxPriority, statePriority } from 'vs/workbench/contrib/testing/browser/testExplorerTree';
import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
/**
* Test tree element element that groups be hierarchy.
*/
export class HierarchicalElement implements ITestTreeElement {
public readonly children = new Set<HierarchicalElement>();
public computedState: TestRunState | undefined;
public readonly depth: number = this.parentItem.depth + 1;
public get treeId() {
return `test:${this.test.id}`;
}
public get label() {
return this.test.item.label;
}
public get state() {
return this.test.item.state.runState;
}
public get location() {
return this.test.item.location;
}
public get runnable(): Iterable<TestIdWithProvider> {
return this.test.item.runnable
? [{ providerId: this.test.providerId, testId: this.test.id }]
: Iterable.empty();
}
public get debuggable() {
return this.test.item.debuggable
? [{ providerId: this.test.providerId, testId: this.test.id }]
: Iterable.empty();
}
constructor(public readonly test: InternalTestItem, public readonly parentItem: HierarchicalFolder | HierarchicalElement) {
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);
if (stateChange) {
refreshComputedState(this, addUpdated);
}
}
}
/**
* Workspace folder in the hierarcha view.
*/
export class HierarchicalFolder implements ITestTreeElement {
public readonly children = new Set<HierarchicalElement>();
public readonly parentItem = null;
public readonly depth = 0;
public computedState: TestRunState | undefined;
public get treeId() {
return `folder:${this.folder.index}`;
}
public get runnable() {
return Iterable.concatNested(Iterable.map(this.children, c => c.runnable));
}
public get debuggable() {
return Iterable.concatNested(Iterable.map(this.children, c => c.debuggable));
}
constructor(private readonly folder: IWorkspaceFolder) { }
public get label() {
return this.folder.name;
}
public getChildren() {
return this.children;
}
}
/**
* Gets the computed state for the node.
*/
export const getComputedState = (node: ITestTreeElement) => {
if (node.computedState === undefined) {
node.computedState = node.state ?? TestRunState.Unset;
for (const child of node.getChildren()) {
node.computedState = maxPriority(node.computedState, getComputedState(child));
}
}
return node.computedState;
};
/**
* Refreshes the computed state for the node and its parents. Any changes
* elements cause `addUpdated` to be called.
*/
export const refreshComputedState = (node: ITestTreeElement, addUpdated: (n: ITestTreeElement) => void) => {
if (node.computedState === undefined) {
return;
}
const oldPriority = statePriority[node.computedState];
node.computedState = undefined;
const newState = getComputedState(node);
const newPriority = statePriority[getComputedState(node)];
if (newPriority === oldPriority) {
return;
}
addUpdated(node);
if (newPriority > oldPriority) {
// Update all parents to ensure they're at least this priority.
for (let parent = node.parentItem; parent; parent = parent.parentItem) {
const prev = parent.computedState;
if (prev !== undefined && statePriority[prev] >= newPriority) {
break;
}
parent.computedState = newState;
addUpdated(parent);
}
} else if (newPriority < oldPriority) {
// Re-render all parents of this node whose computed priority might have come from this node
for (let parent = node.parentItem; parent; parent = parent.parentItem) {
const prev = parent.computedState;
if (prev === undefined || statePriority[prev] > oldPriority) {
break;
}
parent.computedState = undefined;
parent.computedState = getComputedState(parent);
addUpdated(parent);
}
}
};
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* 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 { Event } from 'vs/base/common/event';
import { FuzzyScore } from 'vs/base/common/filters';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { Position } from 'vs/editor/common/core/position';
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
/**
* Describes a rendering of tests in the explorer view. Different
* implementations of this are used for trees and lists, and groupings.
* Originally this was implemented as inline logic within the ViewModel and
* using a single IncrementalTestChangeCollector, but this became hairy
* with status projections.
*/
export interface ITestTreeProjection extends IDisposable {
/**
* Event that fires when the projection changes.
*/
onUpdate: Event<void>;
/**
* Gets the test at the given position in th editor. Should be fast,
* since it is called on each cursor move.
*/
getTestAtPosition(uri: URI, position: Position): ITestTreeElement | undefined;
/**
* Applies pending update to the tree.
*/
applyTo(tree: CompressibleObjectTree<ITestTreeElement, FuzzyScore>): void;
}
export interface ITestTreeElement {
/**
* Computed element state. Will be set automatically if not initially provided.
* The projection is responsible for clearing (or updating) this if it
* becomes invalid.
*/
computedState: TestRunState | undefined;
/**
* Unique ID of the element in the tree.
*/
readonly treeId: string;
/**
* Location of the test, if any.
*/
readonly location?: { uri: URI; range: ITextEditorSelection };
/**
* Test item, if any.
*/
readonly test?: Readonly<InternalTestItem>;
/**
* Tree description.
*/
readonly description?: string;
/**
* Depth of the item in the tree.
*/
readonly depth: number;
/**
* Tests that can be run using this tree item.
*/
readonly runnable: Iterable<TestIdWithProvider>;
/**
* Tests that can be run using this tree item.
*/
readonly debuggable: Iterable<TestIdWithProvider>;
/**
* State of of the tree item. Mostly used for deriving the computed state.
*/
readonly state?: TestRunState;
readonly label: string;
readonly parentItem: ITestTreeElement | null;
getChildren(): Iterable<ITestTreeElement>;
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { findFirstInSorted } from 'vs/base/common/arrays';
import { URI } from 'vs/base/common/uri';
import { Position } from 'vs/editor/common/core/position';
import { Location as ModeLocation } from 'vs/editor/common/modes';
export const locationsEqual = (a: ModeLocation | undefined, b: ModeLocation | undefined) => {
if (a === undefined || b === undefined) {
return b === a;
}
return a.uri.toString() === b.uri.toString()
&& a.range.startLineNumber === b.range.startLineNumber
&& a.range.startColumn === b.range.startColumn
&& a.range.endLineNumber === b.range.endLineNumber
&& a.range.endColumn === b.range.endColumn;
};
/**
* Stores and looks up test-item-like-objects by their uri/range. Used to
* implement the 'reveal' action efficiently.
*/
export class TestLocationStore<T extends { location?: ModeLocation, depth: number }> {
private readonly itemsByUri = new Map<string, T[]>();
public getTestAtPosition(uri: URI, position: Position) {
const tests = this.itemsByUri.get(uri.toString());
if (!tests) {
return;
}
return tests.find(test => {
const range = test.location?.range;
return range
&& new Position(range.startLineNumber, range.startColumn).isBeforeOrEqual(position)
&& position.isBefore(new Position(
range.endLineNumber ?? range.startLineNumber,
range.endColumn ?? range.startColumn,
));
});
}
public remove(item: T, fromLocation = item.location) {
if (!fromLocation) {
return;
}
const key = fromLocation.uri.toString();
const arr = this.itemsByUri.get(key);
if (!arr) {
return;
}
for (let i = 0; i < arr.length; i++) {
if (arr[i] === item) {
arr.splice(i, 1);
return;
}
}
}
public add(item: T) {
if (!item.location) {
return;
}
const key = item.location.uri.toString();
const arr = this.itemsByUri.get(key);
if (!arr) {
this.itemsByUri.set(key, [item]);
return;
}
arr.splice(findFirstInSorted(arr, x => x.depth < item.depth), 0, item);
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* 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, ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
import { Iterable } from 'vs/base/common/iterator';
import { ITestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections';
/**
* Removes nodes from the set whose parents don't exist in the tree. This is
* useful to remove nodes that are queued to be updated or rendered, who will
* be rendered by a call to setChildren.
*/
export const pruneNodesWithParentsNotInTree = <T extends ITestTreeElement>(nodes: Set<T | null>, tree: ObjectTree<ITestTreeElement, any>) => {
for (const node of nodes) {
if (node && node.parentItem && !tree.hasElement(node.parentItem)) {
nodes.delete(node);
}
}
};
/**
* Helper to gather and bulk-apply tree updates.
*/
export class NodeChangeList<T extends ITestTreeElement & { children: Iterable<T>; parentItem: T | null }> {
private changedParents = new Set<T | null>();
private updatedNodes = new Set<T>();
public updated(node: T) {
this.updatedNodes.add(node);
}
public removed(node: T) {
this.changedParents.add(node.parentItem);
}
public added(node: T) {
this.changedParents.add(node.parentItem);
}
public applyTo(
tree: CompressibleObjectTree<ITestTreeElement, any>,
renderNode: (n: T) => ICompressedTreeElement<ITestTreeElement>,
roots: () => Iterable<T>,
) {
pruneNodesWithParentsNotInTree(this.changedParents, tree);
pruneNodesWithParentsNotInTree(this.updatedNodes, tree);
for (const parent of this.changedParents) {
if (parent === null || tree.hasElement(parent)) {
const pchildren: Iterable<T> = parent ? parent.children : roots();
tree.setChildren(parent, Iterable.map(pchildren, renderNode));
}
}
for (const node of this.updatedNodes) {
if (tree.hasElement(node)) {
tree.rerender(node);
}
}
this.changedParents.clear();
this.updatedNodes.clear();
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* 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 { Emitter } from 'vs/base/common/event';
import { FuzzyScore } from 'vs/base/common/filters';
import { Iterable } from 'vs/base/common/iterator';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { Position } from 'vs/editor/common/core/position';
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 { NodeChangeList } 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/browser/testingCollectionService';
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
interface IStatusTestItem extends IncrementalTestCollectionItem {
treeElements: Map<TestRunState, TestStateElement>;
previousState: TestRunState;
depth: number;
parentItem?: IStatusTestItem;
location?: ModeLocation;
}
type TreeElement = StateElement<TestStateElement> | TestStateElement;
class TestStateElement implements ITestTreeElement {
public computedState = this.state;
public get treeId() {
return `test:${this.test.id}`;
}
public get label() {
return this.test.item.label;
}
public get location() {
return this.test.item.location;
}
public get runnable(): Iterable<TestIdWithProvider> {
// if this item is runnable and all its children are in the same state,
// we can run all of them in one go. This will eventually be true
// for leaf nodes, whose treeElements contain only their own state.
if (this.test.item.runnable && this.test.treeElements.size === 1) {
return [{ testId: this.test.id, providerId: this.test.providerId }];
}
return Iterable.concatNested(Iterable.map(this.children, c => c.runnable));
}
public get debuggable(): Iterable<TestIdWithProvider> {
// same logic as runnable above
if (this.test.item.debuggable && this.test.treeElements.size === 1) {
return [{ testId: this.test.id, providerId: this.test.providerId }];
}
return Iterable.concatNested(Iterable.map(this.children, c => c.debuggable));
}
public readonly depth = this.test.depth;
public readonly children = new Set<TestStateElement>();
getChildren(): Iterable<ITestTreeElement> {
return this.children;
}
constructor(
public readonly state: TestRunState,
public readonly test: IStatusTestItem,
public readonly parentItem: TestStateElement | StateElement<TestStateElement>,
) {
parentItem.children.add(this);
}
public remove() {
this.parentItem.children.delete(this);
}
}
/**
* Shows tests in a hierarchical way, but grouped by status. This is more
* complex than it may look at first glance, because nodes can appear in
* multiple places if they have children with different statuses.
*/
export class StateByLocationProjection extends AbstractIncrementalTestCollection<IStatusTestItem> implements ITestTreeProjection {
private readonly updateEmitter = new Emitter<void>();
private readonly changes = new NodeChangeList<TreeElement>();
private readonly locations = new TestLocationStore<IStatusTestItem>();
private readonly disposable = new DisposableStore();
/**
* @inheritdoc
*/
public readonly onUpdate = this.updateEmitter.event;
/**
* Root elements for states in the tree.
*/
protected readonly stateRoots = new Map<TestRunState, StateElement<TestStateElement>>();
constructor(listener: TestSubscriptionListener) {
super();
this.disposable.add(listener.onDiff(([, diff]) => this.apply(diff)));
const firstDiff: TestsDiff = [];
for (const [, collection] of listener.workspaceFolderCollections) {
const queue = [collection.rootNodes];
while (queue.length) {
for (const id of queue.pop()!) {
const node = collection.getNodeById(id)!;
firstDiff.push([TestDiffOpType.Add, node]);
queue.push(node.children);
}
}
}
this.apply(firstDiff);
}
/**
* Frees listeners associated with the projection.
*/
public dispose() {
this.disposable.dispose();
}
/**
* @inheritdoc
*/
public getTestAtPosition(uri: URI, position: Position) {
const item = this.locations.getTestAtPosition(uri, position);
if (!item) {
return undefined;
}
for (const state of statesInOrder) {
const element = item.treeElements.get(state);
if (element) {
return element;
}
}
return undefined;
}
/**
* @inheritdoc
*/
public applyTo(tree: CompressibleObjectTree<ITestTreeElement, FuzzyScore>) {
this.changes.applyTo(tree, this.renderNode, () => this.stateRoots.values());
}
private readonly renderNode = (node: TreeElement): ICompressedTreeElement<ITestTreeElement> => {
return {
element: node,
incompressible: node.depth > 0,
children: Iterable.map(node.children, this.renderNode),
};
};
/**
* @override
*/
protected createChangeCollector(): IncrementalChangeCollector<IStatusTestItem> {
return {
add: node => {
this.resolveNodesRecursive(node);
this.locations.add(node);
},
remove: (node, isNested) => {
this.locations.remove(node);
if (!isNested) {
for (const state of node.treeElements.keys()) {
this.pruneStateElements(node, state, true);
}
}
},
update: node => {
if (node.item.state.runState !== node.previousState) {
this.pruneStateElements(node, node.previousState);
this.resolveNodesRecursive(node);
}
const locationChanged = !locationsEqual(node.location, node.item.location);
if (locationChanged) {
this.locations.remove(node);
node.location = node.item.location;
this.locations.add(node);
}
const treeNode = node.treeElements.get(node.item.state.runState)!;
this.changes.updated(treeNode);
},
complete: () => {
this.updateEmitter.fire();
}
};
}
/**
* Ensures tree nodes for the item state are present in the tree.
*/
protected resolveNodesRecursive(item: IStatusTestItem) {
const state = item.item.state.runState;
item.previousState = item.item.state.runState;
// Create a list of items until the current item who don't have a tree node for the status yet
let chain: IStatusTestItem[] = [];
for (let i: IStatusTestItem | undefined = item; i && !i.treeElements.has(state); i = i.parentItem) {
chain.push(i);
}
for (let i = chain.length - 1; i >= 0; i--) {
const item2 = chain[i];
// the loop would have stopped pushing parents when either it reaches
// the root, or it reaches a parent who already has a node for this state.
const parent = item2.parentItem?.treeElements.get(state) ?? this.getOrCreateStateElement(state);
const node = this.createElement(state, item2, parent);
item2.treeElements.set(state, node);
parent.children.add(node);
if (i === chain.length - 1) {
this.changes.added(node);
}
}
}
protected createElement(state: TestRunState, item: IStatusTestItem, parent: TreeElement) {
return new TestStateElement(state, item, parent);
}
/**
* Recursively (from the leaf to the root) removes tree elements if there's
* no children who have the given state left.
*
* Returns true if it resulted in a node being removed.
*/
protected pruneStateElements(item: IStatusTestItem | undefined, state: TestRunState, force = false) {
if (!item) {
const stateRoot = this.stateRoots.get(state);
if (stateRoot?.children.size === 0) {
this.changes.removed(stateRoot);
this.stateRoots.delete(state);
return true;
}
return false;
}
const node = item.treeElements.get(state);
if (!node) {
return false;
}
// Check to make sure we aren't in the state, and there's no child with the
// state. For the unset state, only show the node if it's a leaf or it
// has children in the unset state.
if (!force) {
if (item.item.state.runState === state && !(state === TestRunState.Unset && item.children.size > 0)) {
return false;
}
for (const childId of item.children) {
if (this.items.get(childId)?.treeElements.has(state)) {
return false;
}
}
}
// If so, proceed to deletion and recurse upwards.
item.treeElements.delete(state);
node.remove();
if (!this.pruneStateElements(item.parentItem, state)) {
this.changes.removed(node);
}
return true;
}
protected getOrCreateStateElement(state: TestRunState) {
let s = this.stateRoots.get(state);
if (!s) {
s = new StateElement(state);
this.changes.added(s);
this.stateRoots.set(state, s);
}
return s;
}
protected createItem(item: InternalTestItem, parentItem?: IStatusTestItem): IStatusTestItem {
return {
...item,
depth: parentItem ? parentItem.depth + 1 : 0,
parentItem: parentItem,
previousState: item.item.state.runState,
location: item.item.location,
children: new Set(),
treeElements: new Map(),
};
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* 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 { Emitter } from 'vs/base/common/event';
import { FuzzyScore } from 'vs/base/common/filters';
import { Iterable } from 'vs/base/common/iterator';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { Position } from 'vs/editor/common/core/position';
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 { 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 { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes';
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/browser/testingCollectionService';
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
class ListTestStateElement implements ITestTreeElement {
public computedState = this.test.item.state.runState;
public get treeId() {
return `test:${this.test.id}`;
}
public get label() {
return this.test.item.label;
}
public get location() {
return this.test.item.location;
}
public get runnable(): Iterable<TestIdWithProvider> {
return this.test.item.runnable
? [{ testId: this.test.id, providerId: this.test.providerId }]
: Iterable.empty();
}
public get debuggable(): Iterable<TestIdWithProvider> {
return this.test.item.debuggable
? [{ testId: this.test.id, providerId: this.test.providerId }]
: Iterable.empty();
}
public get description() {
let description: string | undefined;
for (let parent = this.test.parentItem; parent && parent.depth > 0; parent = parent.parentItem) {
description = description ? `${parent.item.label}${description}` : parent.item.label;
}
return description;
}
public readonly depth = 1;
public readonly children = Iterable.empty();
getChildren(): Iterable<never> {
return Iterable.empty();
}
constructor(
public readonly test: IStatusListTestItem,
public readonly parentItem: StateElement<ListTestStateElement>,
) {
parentItem.children.add(this);
}
public remove() {
this.parentItem.children.delete(this);
}
}
interface IStatusListTestItem extends IncrementalTestCollectionItem {
node?: ListTestStateElement;
type: ListElementType;
previousState: TestRunState;
depth: number;
parentItem?: IStatusListTestItem;
location?: ModeLocation;
}
type TreeElement = StateElement<ListTestStateElement> | ListTestStateElement;
/**
* Projection that shows tests in a flat list (grouped by status).
*/
export class StateByNameProjection extends AbstractIncrementalTestCollection<IStatusListTestItem> implements ITestTreeProjection {
private readonly updateEmitter = new Emitter<void>();
private readonly changes = new NodeChangeList<TreeElement>();
private readonly locations = new TestLocationStore<IStatusListTestItem>();
private readonly disposable = new DisposableStore();
/**
* @inheritdoc
*/
public readonly onUpdate = this.updateEmitter.event;
/**
* Root elements for states in the tree.
*/
protected readonly stateRoots = new Map<TestRunState, StateElement<ListTestStateElement>>();
constructor(listener: TestSubscriptionListener) {
super();
this.disposable.add(listener.onDiff(([, diff]) => this.apply(diff)));
const firstDiff: TestsDiff = [];
for (const [, collection] of listener.workspaceFolderCollections) {
const queue = [collection.rootNodes];
while (queue.length) {
for (const id of queue.pop()!) {
const node = collection.getNodeById(id)!;
firstDiff.push([TestDiffOpType.Add, node]);
queue.push(node.children);
}
}
}
this.apply(firstDiff);
}
/**
* Frees listeners associated with the projection.
*/
public dispose() {
this.disposable.dispose();
}
/**
* @inheritdoc
*/
public getTestAtPosition(uri: URI, position: Position) {
return this.locations.getTestAtPosition(uri, position)?.node;
}
/**
* @inheritdoc
*/
public applyTo(tree: CompressibleObjectTree<ITestTreeElement, FuzzyScore>) {
this.changes.applyTo(tree, this.renderNode, () => this.stateRoots.values());
}
private readonly renderNode = (node: TreeElement): ICompressedTreeElement<ITestTreeElement> => {
return {
element: node,
incompressible: true,
children: node instanceof StateElement ? Iterable.map(node.children, this.renderNode) : undefined,
};
};
/**
* @override
*/
protected createChangeCollector(): IncrementalChangeCollector<IStatusListTestItem> {
return {
add: node => {
this.resolveNodesRecursive(node);
this.locations.add(node);
},
remove: (node, isRoot) => {
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 {
this.removeNodeSingle(node);
}
},
update: node => {
if (node.item.state.runState !== node.previousState) {
this.removeNode(node);
}
this.resolveNodesRecursive(node);
const locationChanged = !locationsEqual(node.location, node.item.location);
if (locationChanged) {
this.locations.remove(node);
node.location = node.item.location;
this.locations.add(node);
}
if (node.node) {
this.changes.updated(node.node);
}
},
complete: () => {
this.updateEmitter.fire();
}
};
}
/**
* Ensures tree nodes for the item state are present in the tree.
*/
protected resolveNodesRecursive(item: IStatusListTestItem) {
const newType = Iterable.some(item.children, c => this.items.get(c)!.type !== ListElementType.BranchWithoutLeaf)
? ListElementType.BranchWithLeaf
: item.item.runnable
? ListElementType.TestLeaf
: ListElementType.BranchWithoutLeaf;
if (newType === item.type) {
return;
}
const isVisible = newType === ListElementType.TestLeaf;
const wasVisible = item.type === ListElementType.TestLeaf;
item.type = newType;
if (!isVisible && wasVisible && item.node) {
this.removeNodeSingle(item);
} else if (isVisible && !wasVisible) {
const state = item.item.state.runState;
item.node = item.node || new ListTestStateElement(item, this.getOrCreateStateElement(state));
this.changes.added(item.node);
}
if (item.parentItem) {
this.resolveNodesRecursive(item.parentItem);
}
}
/**
* Recursively (from the leaf to the root) removes tree elements if there's
* no children who have the given state left.
*
* Returns true if it resulted in a node being removed.
*/
private removeNode(item: IStatusListTestItem) {
if (!item.node) {
return;
}
this.removeNodeSingle(item);
if (item.parentItem) {
this.resolveNodesRecursive(item.parentItem);
}
}
private removeNodeSingle(item: IStatusListTestItem) {
if (!item.node) {
return;
}
item.node.remove();
this.changes.removed(item.node);
const parent = item.node.parentItem;
item.node = undefined;
item.type = ListElementType.Unset;
if (parent.children.size === 0) {
this.changes.removed(parent);
this.stateRoots.delete(parent.state);
}
}
private getOrCreateStateElement(state: TestRunState) {
let s = this.stateRoots.get(state);
if (!s) {
s = new StateElement(state);
this.changes.added(s);
this.stateRoots.set(state, s);
}
return s;
}
/**
* @override
*/
protected createItem(item: InternalTestItem, parentItem?: IStatusListTestItem): IStatusListTestItem {
return {
...item,
type: ListElementType.Unset,
depth: parentItem ? parentItem.depth + 1 : 0,
parentItem: parentItem,
previousState: item.item.state.runState,
location: item.item.location,
children: new Set(),
};
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Iterable } from 'vs/base/common/iterator';
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
import { ITestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections';
import { testStateNames } from 'vs/workbench/contrib/testing/common/constants';
/**
* Base state node element, used in both name and location grouping.
*/
export class StateElement<T extends ITestTreeElement> implements ITestTreeElement {
public computedState = this.state;
public get treeId() {
return `state:${this.state}`;
}
public readonly depth = 0;
public readonly label = testStateNames[this.state];
public readonly parentItem = null;
public readonly children = new Set<T>();
getChildren(): Iterable<T> {
return this.children;
}
public get runnable() {
return Iterable.concatNested(Iterable.map(this.children, c => c.runnable));
}
public get debuggable() {
return Iterable.concatNested(Iterable.map(this.children, c => c.debuggable));
}
constructor(public readonly state: TestRunState) { }
}
......@@ -11,12 +11,11 @@ import { Action2, MenuId } from 'vs/platform/actions/common/actions';
import { ContextKeyAndExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
import * as icons from 'vs/workbench/contrib/testing/browser/icons';
import { ITestingCollectionService } from 'vs/workbench/contrib/testing/browser/testingCollectionService';
import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import { TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
import { TestExplorerViewGrouping, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
import { EMPTY_TEST_RESULT, InternalTestItem, RunTestsResult, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
......@@ -40,14 +39,15 @@ export const filterVisibleActions = (actions: ReadonlyArray<Action>) =>
export class DebugAction extends Action {
constructor(
private readonly test: InternalTestItem,
private readonly tests: Iterable<TestIdWithProvider>,
isRunning: boolean,
@ITestService private readonly testService: ITestService
) {
super(
'action.run',
localize('debug test', 'Debug Test'),
'test-action ' + ThemeIcon.asClassName(icons.testingDebugIcon),
/* enabled= */ test.item.state.runState !== TestRunState.Running
/* enabled= */ !isRunning
);
}
......@@ -56,7 +56,7 @@ export class DebugAction extends Action {
*/
public run(): Promise<any> {
return this.testService.runTests({
tests: [{ testId: this.test.id, providerId: this.test.providerId }],
tests: [...this.tests],
debug: true,
});
}
......@@ -64,14 +64,15 @@ export class DebugAction extends Action {
export class RunAction extends Action {
constructor(
private readonly test: InternalTestItem,
private readonly tests: Iterable<TestIdWithProvider>,
isRunning: boolean,
@ITestService private readonly testService: ITestService
) {
super(
'action.run',
localize('run test', 'Run Test'),
'test-action ' + ThemeIcon.asClassName(icons.testingRunIcon),
/* enabled= */ test.item.state.runState !== TestRunState.Running,
/* enabled= */ !isRunning,
);
}
......@@ -80,7 +81,7 @@ export class RunAction extends Action {
*/
public run(): Promise<any> {
return this.testService.runTests({
tests: [{ testId: this.test.id, providerId: this.test.providerId }],
tests: [...this.tests],
debug: false,
});
}
......@@ -270,3 +271,75 @@ export class TestingViewAsTreeAction extends ViewAction<TestingExplorerView> {
view.viewModel.viewMode = TestExplorerViewMode.Tree;
}
}
export class TestingGroupByLocationAction extends ViewAction<TestingExplorerView> {
constructor() {
super({
id: 'testing.groupByLocation',
viewId: Testing.ExplorerViewId,
title: localize('testing.groupByLocation', "Sort by Name"),
f1: false,
toggled: TestingContextKeys.viewGrouping.isEqualTo(TestExplorerViewGrouping.ByLocation),
menu: {
id: MenuId.ViewTitle,
order: 10,
group: 'groupBy',
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
}
});
}
/**
* @override
*/
public runInView(_accessor: ServicesAccessor, view: TestingExplorerView) {
view.viewModel.viewGrouping = TestExplorerViewGrouping.ByLocation;
}
}
export class TestingGroupByStatusAction extends ViewAction<TestingExplorerView> {
constructor() {
super({
id: 'testing.groupByStatus',
viewId: Testing.ExplorerViewId,
title: localize('testing.groupByStatus', "Sort by Status"),
f1: false,
toggled: TestingContextKeys.viewGrouping.isEqualTo(TestExplorerViewGrouping.ByStatus),
menu: {
id: MenuId.ViewTitle,
order: 10,
group: 'groupBy',
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
}
});
}
/**
* @override
*/
public runInView(_accessor: ServicesAccessor, view: TestingExplorerView) {
view.viewModel.viewGrouping = TestExplorerViewGrouping.ByStatus;
}
}
export class RefreshTestsAction extends Action2 {
constructor() {
super({
id: 'testing.refreshTests',
title: localize('testing.refresh', "Refresh Tests"),
menu: {
id: MenuId.ViewTitle,
order: 0,
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
}
});
}
/**
* @override
*/
public run(accessor: ServicesAccessor) {
accessor.get(ITestService).resubscribeToAllTests();
}
}
......@@ -31,4 +31,8 @@ export const stateNodes = Object.entries(statePriority).reduce(
}, {} as { [K in TestRunState]: TreeStateNode }
);
export const cmpPriority = (a: TestRunState, b: TestRunState) => statePriority[b] - statePriority[a];
export const maxPriority = (a: TestRunState, b: TestRunState) => statePriority[a] > statePriority[b] ? a : b;
export const statesInOrder = Object.keys(statePriority).map(s => Number(s) as TestRunState).sort(cmpPriority);
......@@ -73,6 +73,9 @@ registerAction2(Action.TestingViewAsTreeAction);
registerAction2(Action.CancelTestRunAction);
registerAction2(Action.RunSelectedAction);
registerAction2(Action.DebugSelectedAction);
registerAction2(Action.TestingGroupByLocationAction);
registerAction2(Action.TestingGroupByStatusAction);
registerAction2(Action.RefreshTestsAction);
CommandsRegistry.registerCommand({
id: 'vscode.runTests',
......
......@@ -3,6 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
export const enum Testing {
ViewletId = 'workbench.view.testing',
ExplorerViewId = 'workbench.view.testing',
......@@ -17,3 +20,13 @@ export const enum TestExplorerViewGrouping {
ByLocation = 'location',
ByStatus = 'status',
}
export const testStateNames: { [K in TestRunState]: string } = {
[TestRunState.Errored]: localize('testState.errored', 'Errored'),
[TestRunState.Failed]: localize('testState.failed', 'Failed'),
[TestRunState.Passed]: localize('testState.passed', 'Passed'),
[TestRunState.Queued]: localize('testState.queued', 'Queued'),
[TestRunState.Running]: localize('testState.running', 'Running'),
[TestRunState.Skipped]: localize('testState.skipped', 'Skipped'),
[TestRunState.Unset]: localize('testState.unset', 'Unset'),
};
......@@ -52,4 +52,9 @@ export interface ITestService {
* Updates the number of test providers still discovering tests for the given resource.
*/
updateDiscoveringCount(resource: ExtHostTestingResource, uri: URI, delta: number): void;
/**
* Requests to resubscribe to all active subscriptions, discarding old tests.
*/
resubscribeToAllTests(): void;
}
......@@ -156,6 +156,18 @@ export class TestService extends Disposable implements ITestService {
}
}
/**
* @inheritdoc
*/
public resubscribeToAllTests() {
for (const subscription of this.testSubscriptions.values()) {
this.unsubscribeEmitter.fire(subscription.ident);
const diff = subscription.collection.clear();
subscription.onDiff.fire(diff);
this.subscribeEmitter.fire(subscription.ident);
}
}
/**
* @inheritdoc
*/
......@@ -241,6 +253,22 @@ class MainThreadTestCollection extends AbstractIncrementalTestCollection<Increme
return ops;
}
/**
* Clears everything from the collection, and returns a diff that applies
* that action.
*/
public clear() {
const ops: TestsDiff = [];
for (const root of this.roots) {
ops.push([TestDiffOpType.Remove, root]);
}
this.roots.clear();
this.items.clear();
return ops;
}
protected createItem(internal: InternalTestItem): IncrementalTestCollectionItem {
return { ...internal, children: new Set() };
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册