feat: allow excluding tests from runs

Fixes https://github.com/microsoft/vscode/issues/115102
This commit is contained in:
Connor Peet 2021-02-22 13:21:11 -08:00
parent 967497247a
commit a776fe9af7
No known key found for this signature in database
GPG key ID: CF8FD2EA0DBC61BD
14 changed files with 338 additions and 43 deletions

View file

@ -2406,6 +2406,12 @@ declare module 'vscode' {
*/
tests: T[];
/**
* Array of tests the user wishes has marked as excluded in VS Code.
* May be omitted if no exclusions are present.
*/
exclude?: T[];
/**
* Whether or not tests in this run should be debugged.
*/

View file

@ -20,7 +20,7 @@ import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { TestItem, TestResults, TestState } from 'vs/workbench/api/common/extHostTypeConverters';
import { Disposable } from 'vs/workbench/api/common/extHostTypes';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
import { OwnedTestCollection, SingleUseTestCollection, TestPosition } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, RunTestForProviderRequest, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import type * as vscode from 'vscode';
@ -89,14 +89,15 @@ export class ExtHostTesting implements ExtHostTestingShape {
* Implements vscode.test.runTests
*/
public async runTests(req: vscode.TestRunOptions<vscode.TestItem>, token = CancellationToken.None) {
await this.proxy.$runTests({
tests: req.tests
// Find workspace items first, then owned tests, then document tests.
// If a test instance exists in both the workspace and document, prefer
// the workspace because it's less ephemeral.
const testListToProviders = (tests: ReadonlyArray<vscode.TestItem>) =>
tests
.map(this.getInternalTestForReference, this)
.filter(isDefined)
.map(t => ({ providerId: t.providerId, testId: t.item.extId })),
.map(t => ({ providerId: t.providerId, testId: t.item.extId }));
await this.proxy.$runTests({
exclude: req.exclude ? testListToProviders(req.exclude).map(t => t.testId) : undefined,
tests: testListToProviders(req.tests),
debug: req.debug
}, token);
}
@ -242,23 +243,39 @@ export class ExtHostTesting implements ExtHostTestingShape {
return;
}
const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual)
const includeTests = req.ids.map(id => this.ownedTests.getTestById(id)?.[1]).filter(isDefined);
const excludeTests = req.excludeExtIds
.map(id => this.ownedTests.getTestById(id))
.filter(isDefined)
// Only send the actual TestItem's to the user to run.
.map(t => t instanceof TestItemFilteredWrapper ? t.actual : t);
if (!tests.length) {
.filter(([tree, exclude]) =>
includeTests.some(include => tree.comparePositions(include, exclude) === TestPosition.IsChild),
);
if (!includeTests.length) {
return;
}
try {
await provider.runTests({
setState: (test, state) => {
// for test providers that don't support excluding natively,
// make sure not to report excluded result otherwise summaries will be off.
for (const [tree, exclude] of excludeTests) {
const e = tree.comparePositions(exclude, test.id);
if (e === TestPosition.IsChild || e === TestPosition.IsSame) {
return;
}
}
const internal = this.getInternalTestForReference(test);
if (internal) {
this.flushCollectionDiffs();
this.proxy.$updateTestStateInRun(req.runId, internal.item.extId, TestState.from(state));
}
}, tests, debug: req.debug
},
tests: includeTests.map(t => TestItemFilteredWrapper.unwrap(t.actual)),
exclude: excludeTests.map(([, t]) => TestItemFilteredWrapper.unwrap(t.actual)),
debug: req.debug,
}, cancellation);
for (const { collection } of this.testSubscriptions.values()) {
@ -278,7 +295,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
return Promise.resolve(undefined);
}
const { actual, previousChildren, previousEquals, ...item } = owned;
const { actual, previousChildren, previousEquals, ...item } = owned[1];
return Promise.resolve(item);
}
@ -389,6 +406,10 @@ export class TestItemFilteredWrapper implements vscode.TestItem {
return w;
}
public static unwrap(item: vscode.TestItem) {
return item instanceof TestItemFilteredWrapper ? item.actual : item;
}
public get id() {
return this.actual.id;
}

View file

@ -46,6 +46,10 @@
opacity: 0.7;
}
.test-explorer .test-is-hidden {
opacity: 0.8;
}
.test-explorer .test-explorer-messages {
padding: 0 12px 8px;
}

View file

@ -49,6 +49,26 @@ const enum ActionOrder {
Refresh,
}
export class HideOrShowTestAction extends Action {
constructor(
private readonly testId: string,
@ITestService private readonly testService: ITestService,
) {
super(
'testing.hideOrShowTest',
testService.excludeTests.value.has(testId) ? localize('unhideTest', 'Unhide Test') : localize('hideTest', 'Hide Test'),
);
}
/**
* @override
*/
public run() {
this.testService.setTestExcluded(this.testId);
return Promise.resolve();
}
}
export class DebugAction extends Action {
constructor(
private readonly tests: Iterable<TestIdWithProvider>,

View file

@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Action, IAction, Separator } from 'vs/base/common/actions';
import { Event } from 'vs/base/common/event';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { Disposable, dispose, IDisposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
@ -104,7 +105,7 @@ export class TestingDecorations extends Disposable implements IEditorContributio
this.setDecorations(this.currentUri);
}
}));
this._register(this.results.onResultsChanged(() => {
this._register(Event.any(this.results.onResultsChanged, this.testService.excludeTests.onDidChange)(() => {
if (this.currentUri) {
this.setDecorations(this.currentUri);
}

View file

@ -28,6 +28,7 @@ import { TestExplorerStateFilter, Testing } from 'vs/workbench/contrib/testing/c
import { ObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
export interface ITestExplorerFilterState {
_serviceBrand: undefined;
@ -36,6 +37,8 @@ export interface ITestExplorerFilterState {
readonly reveal: ObservableValue<string | undefined>;
readonly stateFilter: ObservableValue<TestExplorerStateFilter>;
readonly currentDocumentOnly: ObservableValue<boolean>;
/** Whether excluded test should be shown in the view */
readonly showExcludedTests: ObservableValue<boolean>;
readonly onDidRequestInputFocus: Event<void>;
focusInput(): void;
@ -58,6 +61,7 @@ export class TestExplorerFilterState implements ITestExplorerFilterState {
target: StorageTarget.USER
}, this.storage), false);
public readonly showExcludedTests = new ObservableValue(false);
public readonly reveal = new ObservableValue<string | undefined>(undefined);
public readonly onDidRequestInputFocus = this.focusEmitter.event;
@ -185,7 +189,8 @@ class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem {
action: IAction,
private readonly filters: ITestExplorerFilterState,
actionRunner: IActionRunner,
@IContextMenuService contextMenuService: IContextMenuService
@IContextMenuService contextMenuService: IContextMenuService,
@ITestService private readonly testService: ITestService,
) {
super(action,
{ getActions: () => this.getActions() },
@ -221,6 +226,27 @@ class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem {
dispose: () => null
})),
new Separator(),
{
checked: this.filters.showExcludedTests.value,
class: undefined,
enabled: true,
id: 'showExcluded',
label: localize('testing.filters.showExcludedTests', "Show Hidden Tests"),
run: async () => this.filters.showExcludedTests.value = !this.filters.showExcludedTests.value,
tooltip: '',
dispose: () => null
},
{
checked: false,
class: undefined,
enabled: this.testService.excludeTests.value.size > 0,
id: 'removeExcluded',
label: localize('testing.filters.removeTestExclusions', "Unhide All Tests"),
run: async () => this.testService.clearExcludedTests(),
tooltip: '',
dispose: () => null
},
new Separator(),
{
checked: this.filters.currentDocumentOnly.value,
class: undefined,

View file

@ -64,7 +64,7 @@ import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { IWorkspaceTestCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
import { IActivityService, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { DebugAction, RunAction } from './testExplorerActions';
import { DebugAction, HideOrShowTestAction, RunAction } from './testExplorerActions';
export class TestingExplorerView extends ViewPane {
public viewModel!: TestingExplorerViewModel;
@ -326,6 +326,8 @@ export class TestingExplorerViewModel extends Disposable {
this._register(Event.any(
filterState.text.onDidChange,
filterState.stateFilter.onDidChange,
filterState.showExcludedTests.onDidChange,
testService.excludeTests.onDidChange,
)(this.tree.refilter, this.tree));
this._register(this.tree);
@ -634,7 +636,10 @@ class TestsFilter implements ITreeFilter<ITestTreeElement> {
private filters: [include: boolean, value: string][] | undefined;
private _filterToUri: string | undefined;
constructor(@ITestExplorerFilterState private readonly state: ITestExplorerFilterState) { }
constructor(
@ITestExplorerFilterState private readonly state: ITestExplorerFilterState,
@ITestService private readonly testService: ITestService,
) { }
/**
* Parses and updates the tree filter. Supports lists of patterns that can be !negated.
@ -670,6 +675,14 @@ class TestsFilter implements ITreeFilter<ITestTreeElement> {
this.setFilter(this.state.text.value);
}
if (
element.test
&& !this.state.showExcludedTests.value
&& this.testService.excludeTests.value.has(element.test.item.extId)
) {
return TreeVisibility.Hidden;
}
switch (Math.min(this.testFilterText(element), this.testLocation(element), this.testState(element))) {
case FilterResult.Exclude:
return TreeVisibility.Hidden;
@ -801,6 +814,7 @@ class IdentityProvider implements IIdentityProvider<ITestTreeElement> {
interface TestTemplateData {
label: IResourceLabel;
icon: HTMLElement;
wrapper: HTMLElement;
actionBar: ActionBar;
elementDisposable: IDisposable[];
templateDisposable: IDisposable[];
@ -813,7 +827,8 @@ class TestsRenderer extends Disposable implements ITreeRenderer<ITestTreeElement
private labels: ResourceLabels,
@IMenuService private readonly menuService: IMenuService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IInstantiationService private readonly instantiationService: IInstantiationService
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ITestService private readonly testService: ITestService
) {
super();
}
@ -842,7 +857,7 @@ class TestsRenderer extends Disposable implements ITreeRenderer<ITestTreeElement
: undefined
});
return { label, actionBar, icon, elementDisposable: [], templateDisposable: [label, actionBar] };
return { wrapper, label, actionBar, icon, elementDisposable: [], templateDisposable: [label, actionBar] };
}
/**
@ -853,6 +868,9 @@ class TestsRenderer extends Disposable implements ITreeRenderer<ITestTreeElement
const options: IResourceLabelOptions = {};
data.actionBar.clear();
const testHidden = !!element.test && this.testService.excludeTests.value.has(element.test.item.extId);
data.wrapper.classList.toggle('test-is-hidden', testHidden);
const icon = testingStatesToIcons.get(element.state);
data.icon.className = 'computed-state ' + (icon ? ThemeIcon.asClassName(icon) : '');
if (element.retired) {
@ -873,6 +891,10 @@ class TestsRenderer extends Disposable implements ITreeRenderer<ITestTreeElement
options.title = title;
options.fileKind = FileKind.FILE;
label.description = element.description;
if (testHidden) {
const hidden = localize('testHidden', 'hidden');
label.description = label.description ? `${label.description} (${hidden})` : hidden;
}
} else {
options.fileKind = FileKind.ROOT_FOLDER;
}
@ -905,7 +927,12 @@ class TestsRenderer extends Disposable implements ITreeRenderer<ITestTreeElement
}
}
const getTestItemActions = (instantionService: IInstantiationService, contextKeyService: IContextKeyService, menuService: IMenuService, element: ITestTreeElement) => {
const getTestItemActions = (
instantionService: IInstantiationService,
contextKeyService: IContextKeyService,
menuService: IMenuService,
element: ITestTreeElement,
) => {
const contextOverlay = contextKeyService.createOverlay([
['view', Testing.ExplorerViewId],
[TestingContextKeys.testItemExtId.key, element.test?.item.extId]
@ -924,6 +951,10 @@ const getTestItemActions = (instantionService: IInstantiationService, contextKey
}
const secondary: IAction[] = [];
if (element.test) {
secondary.push(instantionService.createInstance(HideOrShowTestAction, element.test.item.extId));
}
const result = { primary, secondary };
const actionsDisposable = createAndFillInActionBarActions(menu, {
arg: element.test?.item.extId,

View file

@ -15,14 +15,17 @@ import { InternalTestItem, TestDiffOpType, TestsDiff, TestsDiffOp } from 'vs/wor
* @private
*/
export class OwnedTestCollection {
protected readonly testIdsToInternal = new Set<Map<string, OwnedCollectionTestItem>>();
protected readonly testIdsToInternal = new Set<TestTree<OwnedCollectionTestItem>>();
/**
* Gets test information by ID, if it was defined and still exists in this
* extension host.
*/
public getTestById(id: string) {
return mapFind(this.testIdsToInternal, m => m.get(id));
return mapFind(this.testIdsToInternal, t => {
const owned = t.get(id);
return owned && [t, owned] as const;
});
}
/**
@ -33,10 +36,10 @@ export class OwnedTestCollection {
return new SingleUseTestCollection(this.createIdMap(), publishDiff);
}
protected createIdMap(): IReference<Map<string, OwnedCollectionTestItem>> {
const map = new Map<string, OwnedCollectionTestItem>();
this.testIdsToInternal.add(map);
return { object: map, dispose: () => this.testIdsToInternal.delete(map) };
protected createIdMap(): IReference<TestTree<OwnedCollectionTestItem>> {
const tree = new TestTree<OwnedCollectionTestItem>();
this.testIdsToInternal.add(tree);
return { object: tree, dispose: () => this.testIdsToInternal.delete(tree) };
}
}
/**
@ -48,6 +51,117 @@ export interface OwnedCollectionTestItem extends InternalTestItem {
previousEquals: (v: ApiTestItem) => boolean;
}
/**
* Enum for describing relative positions of tests. Similar to
* `node.compareDocumentPosition` in the DOM.
*/
export const enum TestPosition {
/** Neither a nor b are a child of one another. They may share a common parent, though. */
Disconnected,
/** b is a child of a */
IsChild,
/** b is a parent of a */
IsParent,
/** a === b */
IsSame,
}
/**
* Test tree is (or will be after debt week 2020-03) the standard collection
* for test trees. Internally it indexes tests by their extension ID in
* a map.
*/
export class TestTree<T extends InternalTestItem> {
private readonly map = new Map<string, T>();
private readonly _roots = new Set<T>();
public readonly roots: ReadonlySet<T> = this._roots;
/**
* Gets the size of the tree.
*/
public get size() {
return this.map.size;
}
/**
* Adds a new test to the tree if it doesn't exist.
* @throws if a duplicate item is inserted
*/
public add(test: T) {
if (this.map.has(test.item.extId)) {
throw new Error(`Attempted to insert a duplicate test item ID ${test.item.extId}`);
}
this.map.set(test.item.extId, test);
if (!test.parent) {
this._roots.add(test);
}
}
/**
* Gets whether the test exists in the tree.
*/
public has(testId: string) {
return this.map.has(testId);
}
/**
* Removes a test ID from the tree. This is NOT recursive.
*/
public delete(testId: string) {
const existing = this.map.get(testId);
if (!existing) {
return false;
}
this.map.delete(testId);
this._roots.delete(existing);
return true;
}
/**
* Gets a test item by ID from the tree.
*/
public get(testId: string) {
return this.map.get(testId);
}
/**
* Compares the positions of the two items in the test tree.
*/
public comparePositions(aOrId: T | string, bOrId: T | string) {
const a = typeof aOrId === 'string' ? this.map.get(aOrId) : aOrId;
const b = typeof bOrId === 'string' ? this.map.get(bOrId) : bOrId;
if (!a || !b) {
return TestPosition.Disconnected;
}
if (a === b) {
return TestPosition.IsSame;
}
for (let p = this.map.get(b.parent!); p; p = this.map.get(p.parent!)) {
if (p === a) {
return TestPosition.IsChild;
}
}
for (let p = this.map.get(a.parent!); p; p = this.map.get(p.parent!)) {
if (p === b) {
return TestPosition.IsParent;
}
}
return TestPosition.Disconnected;
}
/**
* Iterates over all test in the tree.
*/
[Symbol.iterator]() {
return this.map.values();
}
}
/**
* Maintains tests created and registered for a single set of hierarchies
@ -67,7 +181,7 @@ export class SingleUseTestCollection implements IDisposable {
private readonly debounceSendDiff = new RunOnceScheduler(() => this.throttleSendDiff(), 2);
constructor(
private readonly testIdToInternal: IReference<Map<string, OwnedCollectionTestItem>>,
private readonly testIdToInternal: IReference<TestTree<OwnedCollectionTestItem>>,
private readonly publishDiff: (diff: TestsDiff) => void,
) { }
@ -142,8 +256,8 @@ export class SingleUseTestCollection implements IDisposable {
previousEquals: itemEqualityComparator(actual),
};
this.testIdToInternal.object.add(internal);
this.testItemToInternal.set(actual, internal);
this.testIdToInternal.object.set(actual.id, internal);
this.diff.push([TestDiffOpType.Add, { parent, providerId, item: internal.item }]);
} else if (!internal.previousEquals(actual)) {
internal.item = TestItem.from(actual);

View file

@ -19,6 +19,7 @@ export interface TestIdWithProvider {
*/
export interface RunTestsRequest {
tests: TestIdWithProvider[];
exclude?: string[];
debug: boolean;
isAutoRun?: boolean;
}
@ -28,6 +29,7 @@ export interface RunTestsRequest {
*/
export interface RunTestForProviderRequest {
runId: string;
excludeExtIds: string[];
providerId: string;
ids: string[];
debug: boolean;

View file

@ -153,6 +153,7 @@ const makeParents = (
const makeNodeAndChildren = (
collection: IMainThreadTestCollection,
test: IncrementalTestCollectionItem,
excluded: ReadonlySet<string>,
byExtId: Map<string, TestResultItem>,
isExecutedDirectly = true,
): TestResultItem => {
@ -168,8 +169,8 @@ const makeNodeAndChildren = (
for (const childId of test.children) {
const child = collection.getNodeById(childId);
if (child) {
makeNodeAndChildren(collection, child, byExtId, false);
if (child && !excluded.has(childId)) {
makeNodeAndChildren(collection, child, excluded, byExtId, false);
}
}
@ -190,6 +191,7 @@ export class LiveTestResult implements ITestResult {
req: RunTestsRequest,
) {
const testByExtId = new Map<string, TestResultItem>();
const excludeSet = new Set<string>(req.exclude);
for (const test of req.tests) {
for (const collection of collections) {
const node = collection.getNodeById(test.testId);
@ -197,12 +199,12 @@ export class LiveTestResult implements ITestResult {
continue;
}
makeNodeAndChildren(collection, node, testByExtId);
makeNodeAndChildren(collection, node, excludeSet, testByExtId);
makeParents(collection, node, testByExtId);
}
}
return new LiveTestResult(collections, testByExtId, !!req.isAutoRun);
return new LiveTestResult(collections, testByExtId, excludeSet, !!req.isAutoRun);
}
private readonly completeEmitter = new Emitter<void>();
@ -270,6 +272,7 @@ export class LiveTestResult implements ITestResult {
constructor(
private readonly collections: ReadonlyArray<IMainThreadTestCollection>,
private readonly testById: Map<string, TestResultItem>,
private readonly excluded: ReadonlySet<string>,
public readonly isAutoRun: boolean,
) {
this.counts[TestRunState.Unset] = testById.size;
@ -362,7 +365,7 @@ export class LiveTestResult implements ITestResult {
if (test) {
const originalSize = this.testById.size;
makeParents(collection, test, this.testById);
const node = makeNodeAndChildren(collection, test, this.testById);
const node = makeNodeAndChildren(collection, test, this.excluded, this.testById);
this.counts[TestRunState.Unset] += this.testById.size - originalSize;
return node;
}

View file

@ -9,6 +9,7 @@ import { DisposableStore, IReference } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
import { ObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsRequest, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResultService';
@ -102,6 +103,21 @@ export interface ITestService {
readonly subscriptions: ReadonlyArray<{ resource: ExtHostTestingResource, uri: URI; }>;
readonly testRuns: Iterable<RunTestsRequest>;
/**
* Set of test IDs the user asked to exclude.
*/
readonly excludeTests: ObservableValue<ReadonlySet<string>>;
/**
* Sets whether a test is excluded.
*/
setTestExcluded(testId: string, exclude?: boolean): void;
/**
* Removes all test exclusions.
*/
clearExcludedTests(): void;
registerTestController(id: string, controller: MainTestController): void;
unregisterTestController(id: string): void;
runTests(req: RunTestsRequest, token?: CancellationToken): Promise<ITestResult>;

View file

@ -12,7 +12,10 @@ import { URI, UriComponents } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
import { ObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue';
import { AbstractIncrementalTestCollection, getTestSubscriptionKey, IncrementalTestCollectionItem, InternalTestItem, RunTestsRequest, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestResult, ITestResultService, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResultService';
@ -44,13 +47,51 @@ export class TestService extends Disposable implements ITestService {
private readonly runningTests = new Map<RunTestsRequest, CancellationTokenSource>();
private rootProviderCount = 0;
constructor(@IContextKeyService contextKeyService: IContextKeyService, @INotificationService private readonly notificationService: INotificationService, @ITestResultService private readonly testResults: ITestResultService) {
public readonly excludeTests = ObservableValue.stored(new StoredValue<ReadonlySet<string>>({
key: 'excludedTestItes',
scope: StorageScope.WORKSPACE,
target: StorageTarget.USER,
serialization: {
deserialize: v => new Set(JSON.parse(v)),
serialize: v => JSON.stringify([...v])
},
}, this.storageService), new Set());
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@IStorageService private readonly storageService: IStorageService,
@INotificationService private readonly notificationService: INotificationService,
@ITestResultService private readonly testResults: ITestResultService,
) {
super();
this.providerCount = TestingContextKeys.providerCount.bindTo(contextKeyService);
this.hasDebuggable = TestingContextKeys.hasDebuggableTests.bindTo(contextKeyService);
this.hasRunnable = TestingContextKeys.hasRunnableTests.bindTo(contextKeyService);
}
/**
* @inheritdoc
*/
public clearExcludedTests() {
this.excludeTests.value = new Set();
}
/**
* @inheritdoc
*/
public setTestExcluded(testId: string, exclude = !this.excludeTests.value.has(testId)) {
const newSet = new Set(this.excludeTests.value);
if (exclude) {
newSet.add(testId);
} else {
newSet.delete(testId);
}
if (newSet.size !== this.excludeTests.value.size) {
this.excludeTests.value = newSet;
}
}
/**
* Gets currently running tests.
*/
@ -125,6 +166,10 @@ export class TestService extends Disposable implements ITestService {
* @inheritdoc
*/
public async runTests(req: RunTestsRequest, token = CancellationToken.None): Promise<ITestResult> {
if (!req.exclude) {
req.exclude = [...this.excludeTests.value];
}
const subscriptions = [...this.testSubscriptions.values()]
.filter(v => req.tests.some(t => v.collection.getNodeById(t.testId)))
.map(s => this.subscribeToDiffs(s.ident.resource, s.ident.uri));
@ -139,7 +184,13 @@ export class TestService extends Disposable implements ITestService {
const providerId = group[0].providerId;
const controller = this.testControllers.get(providerId);
return controller?.runTests(
{ runId: result.id, providerId, debug: req.debug, ids: group.map(t => t.testId) },
{
runId: result.id,
providerId,
debug: req.debug,
excludeExtIds: req.exclude ?? [],
ids: group.map(t => t.testId),
},
cancelSource.token,
).catch(err => {
this.notificationService.error(localize('testError', 'An error occurred attempting to run tests: {0}', err.message));

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Iterable } from 'vs/base/common/iterator';
import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
import { OwnedTestCollection, SingleUseTestCollection, TestTree } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
import { TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testServiceImpl';
import { testStubs } from 'vs/workbench/contrib/testing/common/testStubs';
@ -39,7 +39,7 @@ export class TestOwnedTestCollection extends OwnedTestCollection {
*/
export const getInitializedMainTestCollection = (root = testStubs.nested()) => {
const c = new MainThreadTestCollection(0);
const singleUse = new TestSingleUseCollection({ object: new Map(), dispose: () => undefined }, () => undefined);
const singleUse = new TestSingleUseCollection({ object: new TestTree(), dispose: () => undefined }, () => undefined);
singleUse.addRoot(root, 'provider');
c.apply(singleUse.collectDiff());
return c;

View file

@ -108,7 +108,7 @@ suite('ExtHost Testing', () => {
assert.deepStrictEqual(single.collectDiff(), [
[TestDiffOpType.Remove, 'id-a'],
]);
assert.deepStrictEqual([...owned.idToInternal.keys()].sort(), ['id-b', 'id-root']);
assert.deepStrictEqual([...owned.idToInternal].map(n => n.item.extId).sort(), ['id-b', 'id-root']);
assert.strictEqual(single.itemToInternal.size, 2);
});
@ -124,7 +124,7 @@ suite('ExtHost Testing', () => {
[TestDiffOpType.Add, { providerId: 'pid', parent: 'id-a', item: convert.TestItem.from(child) }],
]);
assert.deepStrictEqual(
[...owned.idToInternal.keys()].sort(),
[...owned.idToInternal].map(n => n.item.extId).sort(),
['id-a', 'id-aa', 'id-ab', 'id-ac', 'id-b', 'id-root'],
);
assert.strictEqual(single.itemToInternal.size, 6);
@ -139,7 +139,7 @@ suite('ExtHost Testing', () => {
const tests = testStubs.nested();
single.addRoot(tests, 'pid');
m.apply(single.collectDiff());
assertTreesEqual(m.rootTestItems[0], owned.getTestById('id-root')!.actual);
assertTreesEqual(m.rootTestItems[0], owned.getTestById('id-root')![1].actual);
assert.strictEqual(m.length, single.itemToInternal.size);
});
@ -151,7 +151,7 @@ suite('ExtHost Testing', () => {
single.onItemChange(tests, 'pid');
m.apply(single.collectDiff());
assertTreesEqual(m.rootTestItems[0], owned.getTestById('id-root')!.actual);
assertTreesEqual(m.rootTestItems[0], owned.getTestById('id-root')![1].actual);
assert.strictEqual(m.length, single.itemToInternal.size);
});
@ -163,7 +163,7 @@ suite('ExtHost Testing', () => {
single.onItemChange(tests, 'pid');
m.apply(single.collectDiff());
assertTreesEqual(m.rootTestItems[0], owned.getTestById('id-root')!.actual);
assertTreesEqual(m.rootTestItems[0], owned.getTestById('id-root')![1].actual);
assert.strictEqual(m.length, single.itemToInternal.size);
});
@ -175,7 +175,7 @@ suite('ExtHost Testing', () => {
single.onItemChange(tests, 'pid');
m.apply(single.collectDiff());
assertTreesEqual(m.rootTestItems[0], owned.getTestById('id-root')!.actual);
assertTreesEqual(m.rootTestItems[0], owned.getTestById('id-root')![1].actual);
});
suite('MirroredChangeCollector', () => {