mirror of
https://github.com/Microsoft/vscode
synced 2024-09-18 01:58:27 +00:00
feat: allow excluding tests from runs
Fixes https://github.com/microsoft/vscode/issues/115102
This commit is contained in:
parent
967497247a
commit
a776fe9af7
6
src/vs/vscode.proposed.d.ts
vendored
6
src/vs/vscode.proposed.d.ts
vendored
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -46,6 +46,10 @@
|
|||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.test-explorer .test-is-hidden {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.test-explorer .test-explorer-messages {
|
||||
padding: 0 12px 8px;
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
Loading…
Reference in a new issue