testing: add support for test tags

For #129456
This commit is contained in:
Connor Peet 2021-08-09 16:30:49 -07:00
parent 40ab771cf9
commit 9125758184
No known key found for this signature in database
GPG key ID: CF8FD2EA0DBC61BD
29 changed files with 357 additions and 204 deletions

View file

@ -1806,6 +1806,55 @@ declare module 'vscode' {
}
//#endregion
//#region test tags https://github.com/microsoft/vscode/issues/129456
/**
* Tags can be associated with {@link TestItem | TestItems} and
* {@link TestRunProfile | TestRunProfiles}. A profile with a tag can only
* execute tests that include that tag in their {@link TestItem.tags} array.
*/
export class TestTag {
/**
* Unique ID of the test tag.
*/
readonly id: string;
/**
* Human-readable name of the tag. If present, the tag will be visible as
* a filter option in the UI.
*/
readonly label?: string;
/**
* Creates a new TestTag instance.
* @param id Unique ID of the test tag.
* @param label Human-readable name of the tag. If present, the tag will
* be visible as a filter option in the UI.
*/
constructor(id: string, label?: string);
}
export interface TestRunProfile {
/**
* Associated tag for the profile. If this is set, only {@link TestItem}
* instances with the same tag will be eligible to execute in this profile.
*/
tag?: TestTag;
}
export interface TestItem {
/**
* Tags associated with this test item. May be used in combination with
* {@link TestRunProfile.tags}, or simply as an organizational feature.
*/
tags: readonly TestTag[];
}
export interface TestController {
createRunProfile(label: string, kind: TestRunProfileKind, runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable<void> | void, isDefault?: boolean, tag?: TestTag): TestRunProfile;
}
//#endregion
//#region proposed test APIs https://github.com/microsoft/vscode/issues/107467
export namespace tests {
/**

View file

@ -12,7 +12,7 @@ import { Range } from 'vs/editor/common/core/range';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
import { ExtensionRunTestsRequest, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestDiffOpType, TestResultState, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage';
import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
@ -195,8 +195,9 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
id: controllerId,
label,
configureRunProfile: id => this.proxy.$configureRunProfile(controllerId, id),
getTags: () => this.proxy.$getTestTags(controllerId),
runTests: (req, token) => this.proxy.$runControllerTests(req, token),
expandTest: (src, levels) => this.proxy.$expandTest(src, isFinite(levels) ? levels : -1),
expandTest: (testId, levels) => this.proxy.$expandTest(testId, isFinite(levels) ? levels : -1),
};

View file

@ -1277,6 +1277,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
TestResultState: extHostTypes.TestResultState,
TestRunRequest: extHostTypes.TestRunRequest,
TestMessage: extHostTypes.TestMessage,
TestTag: extHostTypes.TestTag,
TestRunProfileKind: extHostTypes.TestRunProfileKind,
TextSearchCompleteMessageType: TextSearchCompleteMessageType,
TestMessageSeverity: extHostTypes.TestMessageSeverity,

View file

@ -24,6 +24,7 @@ import { EndOfLineSequence, ISingleEditOperation } from 'vs/editor/common/model'
import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel';
import * as modes from 'vs/editor/common/modes';
import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/modes/languageConfiguration';
import { ILanguageStatus } from 'vs/editor/common/services/languageStatusService';
import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility';
import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands';
import { ConfigurationTarget, IConfigurationChange, IConfigurationData, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration';
@ -47,8 +48,8 @@ import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtens
import { ExtHostInteractive } from 'vs/workbench/api/common/extHostInteractive';
import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService';
import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes';
import { TreeDataTransferDTO } from 'vs/workbench/api/common/shared/treeDataTransfer';
import * as tasks from 'vs/workbench/api/common/shared/tasks';
import { TreeDataTransferDTO } from 'vs/workbench/api/common/shared/treeDataTransfer';
import { SaveReason } from 'vs/workbench/common/editor';
import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views';
import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
@ -58,7 +59,7 @@ import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange';
import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm';
import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder';
import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { ExtensionRunTestsRequest, ISerializedTestResults, ITestItem, ITestMessage, ITestRunTask, RunTestForControllerRequest, ResolvedTestRunRequest, ITestIdWithSrc, TestsDiff, IFileCoverage, CoverageDetails, ITestRunProfile, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection';
import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ISerializedTestResults, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ITestTagDisplayInfo, ResolvedTestRunRequest, RunTestForControllerRequest, TestResultState, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { InternalTimelineOptions, Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline';
import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy';
import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn';
@ -67,7 +68,6 @@ import { createExtHostContextProxyIdentifier as createExtId, createMainContextPr
import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService';
import * as search from 'vs/workbench/services/search/common/search';
import * as statusbar from 'vs/workbench/services/statusbar/common/statusbar';
import { ILanguageStatus } from 'vs/editor/common/services/languageStatusService';
export interface IEnvironment {
isExtensionDevelopmentDebug: boolean;
@ -2098,7 +2098,7 @@ export interface ExtHostTestingShape {
/** Publishes that a test run finished. */
$publishTestResults(results: ISerializedTestResults[]): void;
/** Expands a test item's children, by the given number of levels. */
$expandTest(src: ITestIdWithSrc, levels: number): Promise<void>;
$expandTest(testId: string, levels: number): Promise<void>;
/** Requests file coverage for a test run. Errors if not available. */
$provideFileCoverage(runId: string, taskId: string, token: CancellationToken): Promise<IFileCoverage[]>;
/**
@ -2108,6 +2108,8 @@ export interface ExtHostTestingShape {
$resolveFileCoverage(runId: string, taskId: string, fileIndex: number, token: CancellationToken): Promise<CoverageDetails[]>;
/** Configures a test run config. */
$configureRunProfile(controllerId: string, configId: number): void;
/** Gets all in-use tags from the controller. */
$getTestTags(controllerId: string): Promise<ITestTagDisplayInfo[]>;
}
export interface MainThreadTestingShape {

View file

@ -21,7 +21,7 @@ import { InvalidTestItemError, TestItemImpl, TestItemRootImpl } from 'vs/workben
import * as Convert from 'vs/workbench/api/common/extHostTypeConverters';
import { TestRunProfileKind, TestRunRequest } from 'vs/workbench/api/common/extHostTypes';
import { SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
import { AbstractIncrementalTestCollection, CoverageDetails, IFileCoverage, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestIdWithSrc, ITestItem, RunTestForControllerRequest, TestResultState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { AbstractIncrementalTestCollection, CoverageDetails, IFileCoverage, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestItem, ITestTagDisplayInfo, RunTestForControllerRequest, TestResultState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId';
import type * as vscode from 'vscode';
@ -80,7 +80,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
get id() {
return controllerId;
},
createRunProfile: (label, group, runHandler, isDefault) => {
createRunProfile: (label, group, runHandler, isDefault, tag?: vscode.TestTag | undefined) => {
// Derive the profile ID from a hash so that the same profile will tend
// to have the same hashes, allowing re-run requests to work across reloads.
let profileId = hash(label);
@ -88,7 +88,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
profileId++;
}
const profile = new TestRunProfileImpl(this.proxy, controllerId, profileId, label, group, runHandler, isDefault);
const profile = new TestRunProfileImpl(this.proxy, controllerId, profileId, label, group, runHandler, isDefault, tag);
profiles.set(profileId, profile);
return profile;
},
@ -153,7 +153,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
profileId: profile.profileId,
controllerId: profile.controllerId,
}],
exclude: req.exclude?.map(t => ({ testId: t.id, controllerId: profile.controllerId })),
exclude: req.exclude?.map(t => t.id),
}, token);
}
@ -176,6 +176,29 @@ export class ExtHostTesting implements ExtHostTestingShape {
this.controllers.get(controllerId)?.profiles.get(profileId)?.configureHandler?.();
}
/** @inheritdoc */
$getTestTags(controllerId: string) {
const record = this.controllers.get(controllerId);
if (!record) {
return Promise.resolve([]);
}
const tags = new Map<string, ITestTagDisplayInfo>();
for (const profile of record.profiles.values()) {
if (profile.tag) {
const display = Convert.TestTag.display(controllerId, profile.tag);
tags.set(display.id, display);
}
}
for (const tag of record.collection.tags()) {
const display = Convert.TestTag.display(controllerId, tag);
tags.set(display.id, display);
}
return Promise.resolve([...tags.values()]);
}
/**
* Updates test results shown to extensions.
* @override
@ -196,8 +219,8 @@ export class ExtHostTesting implements ExtHostTestingShape {
* Expands the nodes in the test tree. If levels is less than zero, it will
* be treated as infinite.
*/
public async $expandTest({ controllerId, testId }: ITestIdWithSrc, levels: number) {
const collection = this.controllers.get(controllerId)?.collection;
public async $expandTest(testId: string, levels: number) {
const collection = this.controllers.get(TestId.fromString(testId).controllerId)?.collection;
if (collection) {
await collection.expand(testId, levels < 0 ? Infinity : levels);
collection.flushDiff();
@ -862,6 +885,19 @@ export class TestRunProfileImpl implements vscode.TestRunProfile {
}
}
public get tag() {
return this._tag;
}
public set tag(tag: vscode.TestTag | undefined) {
if (tag?.id !== this._tag?.id) {
this._tag = tag;
this.#proxy.$updateTestRunConfig(this.controllerId, this.profileId, {
tag: tag ? Convert.TestTag.namespace(this.controllerId, tag.id) : null,
});
}
}
public get configureHandler() {
return this._configureHandler;
}
@ -881,6 +917,7 @@ export class TestRunProfileImpl implements vscode.TestRunProfile {
public readonly kind: vscode.TestRunProfileKind,
public runHandler: (request: vscode.TestRunRequest, token: vscode.CancellationToken) => Thenable<void> | void,
private _isDefault = false,
public _tag: vscode.TestTag | undefined = undefined,
) {
this.#proxy = proxy;
@ -892,6 +929,7 @@ export class TestRunProfileImpl implements vscode.TestRunProfile {
this.#proxy.$publishTestRunProfile({
profileId: profileId,
controllerId,
tag: _tag ? Convert.TestTag.namespace(this.controllerId, _tag.id) : null,
label: _label,
group: groupBitset,
isDefault: _isDefault,

View file

@ -88,7 +88,7 @@ const testItemPropAccessor = <K extends keyof vscode.TestItem>(
};
};
type WritableProps = Pick<vscode.TestItem, 'range' | 'label' | 'description' | 'canResolveChildren' | 'busy' | 'error'>;
type WritableProps = Pick<vscode.TestItem, 'range' | 'label' | 'description' | 'canResolveChildren' | 'busy' | 'error' | 'tags'>;
const strictEqualComparator = <T>(a: T, b: T) => a === b;
@ -102,7 +102,18 @@ const propComparators: { [K in keyof Required<WritableProps>]: (a: vscode.TestIt
description: strictEqualComparator,
busy: strictEqualComparator,
error: strictEqualComparator,
canResolveChildren: strictEqualComparator
canResolveChildren: strictEqualComparator,
tags: (a, b) => {
if (a.length !== b.length) {
return false;
}
if (a.some(t1 => !b.find(t2 => t1.id === t2.id))) {
return false;
}
return true;
},
};
const writablePropKeys = Object.keys(propComparators) as (keyof Required<WritableProps>)[];
@ -114,6 +125,7 @@ const makePropDescriptors = (api: IExtHostTestItemApi, label: string): { [K in k
canResolveChildren: testItemPropAccessor(api, 'canResolveChildren', false, propComparators.canResolveChildren),
busy: testItemPropAccessor(api, 'busy', false, propComparators.busy),
error: testItemPropAccessor(api, 'error', undefined, propComparators.error),
tags: testItemPropAccessor(api, 'tags', [], propComparators.tags),
});
/**
@ -253,6 +265,7 @@ export class TestItemImpl implements vscode.TestItem {
public error!: string | vscode.MarkdownString;
public busy!: boolean;
public canResolveChildren!: boolean;
public tags!: readonly vscode.TestTag[];
/**
* Note that data is deprecated and here for back-compat only

View file

@ -31,7 +31,7 @@ import { SaveReason } from 'vs/workbench/common/editor';
import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange';
import * as search from 'vs/workbench/contrib/search/common/search';
import { CoverageDetails, DetailType, ICoveredCount, IFileCoverage, ISerializedTestResults, ITestItem, ITestItemContext, ITestMessage, SerializedTestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { CoverageDetails, DetailType, ICoveredCount, IFileCoverage, ISerializedTestResults, ITestItem, ITestItemContext, ITestMessage, ITestTag, ITestTagDisplayInfo, SerializedTestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn';
import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
@ -1657,14 +1657,38 @@ export namespace TestMessage {
}
}
export namespace TestTag {
const enum Constants {
Delimiter = '\0',
}
export const namespace = (ctrlId: string, tagId: string) =>
ctrlId + Constants.Delimiter + tagId;
export function display(controllerId: string, tag: vscode.TestTag): ITestTagDisplayInfo {
return {
displayId: tag.id,
id: namespace(controllerId, tag.id),
label: tag.label,
};
}
export const denamespace = (namespaced: string) => {
const index = namespaced.indexOf(Constants.Delimiter);
return { ctrlId: namespaced.slice(0, index), tagId: namespaced.slice(index + 1) };
};
}
export namespace TestItem {
export type Raw = vscode.TestItem;
export function from(item: TestItemImpl): ITestItem {
const ctrlId = getPrivateApiFor(item).controllerId;
return {
extId: TestId.fromExtHostTestItem(item, getPrivateApiFor(item).controllerId).toString(),
extId: TestId.fromExtHostTestItem(item, ctrlId).toString(),
label: item.label,
uri: item.uri,
tags: item.tags.map(t => TestTag.namespace(ctrlId, t.id)),
range: Range.from(item.range) || null,
description: item.description || null,
error: item.error ? (MarkdownString.fromStrict(item.error) || null) : null,
@ -1676,6 +1700,10 @@ export namespace TestItem {
id: TestId.fromString(item.extId).localId,
label: item.label,
uri: URI.revive(item.uri),
tags: item.tags.map(t => {
const { tagId } = TestTag.denamespace(t);
return new types.TestTag(tagId, tagId);
}),
range: Range.to(item.range || undefined),
invalidateResults: () => undefined,
canResolveChildren: false,
@ -1704,6 +1732,16 @@ export namespace TestItem {
}
}
export namespace TestTag {
export function from(tag: vscode.TestTag): ITestTag {
return { id: tag.id, label: tag.label };
}
export function to(tag: ITestTag): vscode.TestTag {
return new types.TestTag(tag.id, tag.label);
}
}
export namespace TestResults {
const convertTestResultItem = (item: SerializedTestResultItem, byInternalId: Map<string, SerializedTestResultItem>): vscode.TestResultSnapshot => {
const snapshot: vscode.TestResultSnapshot = ({

View file

@ -3339,6 +3339,14 @@ export class TestMessage implements vscode.TestMessage {
constructor(public message: string | vscode.MarkdownString) { }
}
@es5ClassCompat
export class TestTag implements vscode.TestTag {
constructor(
public readonly id: string,
public readonly label?: string,
) { }
}
//#endregion
//#region Test Coverage

View file

@ -10,7 +10,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent';
import { Iterable } from 'vs/base/common/iterator';
import { IDisposable } from 'vs/base/common/lifecycle';
import { MarshalledId } from 'vs/base/common/marshalling';
import { TestResultState, identifyTest, InternalTestItem, ITestIdWithSrc, ITestItemContext } from 'vs/workbench/contrib/testing/common/testCollection';
import { InternalTestItem, ITestItemContext, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection';
/**
* Describes a rendering of tests in the explorer view. Different
@ -68,7 +68,7 @@ export interface IActionableTestTreeElement {
/**
* Iterable of the tests this element contains.
*/
tests: Iterable<ITestIdWithSrc>;
tests: Iterable<InternalTestItem>;
/**
* State to show on the item. This is generally the item's computed state
@ -108,7 +108,7 @@ export class TestItemTreeElement implements IActionableTestTreeElement {
public depth: number = this.parent ? this.parent.depth + 1 : 0;
public get tests() {
return Iterable.single(identifyTest(this.test));
return Iterable.single(this.test);
}
public get description() {

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { capabilityContextKeys } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { capabilityContextKeys } from 'vs/workbench/contrib/testing/common/testProfileService';
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';

View file

@ -30,12 +30,12 @@ import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/t
import type { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService';
import { TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants';
import { identifyTest, InternalTestItem, ITestIdWithSrc, ITestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { InternalTestItem, ITestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener';
import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates';
import { canUseProfileWithTest, ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { expandAndGetTestById, IMainThreadTestCollection, ITestService, testsInFile } from 'vs/workbench/contrib/testing/common/testService';
@ -80,7 +80,7 @@ export class HideTestAction extends Action2 {
const service = accessor.get(ITestService);
for (const element of elements) {
if (element instanceof TestItemTreeElement) {
service.excluded.toggle(identifyTest(element.test), true);
service.excluded.toggle(element.test, true);
}
}
return Promise.resolve();
@ -105,7 +105,7 @@ export class UnhideTestAction extends Action2 {
const service = accessor.get(ITestService);
for (const element of elements) {
if (element instanceof TestItemTreeElement) {
service.excluded.toggle(identifyTest(element.test), false);
service.excluded.toggle(element.test, false);
}
}
return Promise.resolve();
@ -159,8 +159,9 @@ export class RunUsingProfileAction extends Action2 {
const commandService = acessor.get(ICommandService);
const testService = acessor.get(ITestService);
const controllerId = testElements[0].test.controllerId;
const profile: ITestRunProfile | undefined = await commandService.executeCommand('vscode.pickTestProfile', { onlyControllerId: controllerId });
const profile: ITestRunProfile | undefined = await commandService.executeCommand('vscode.pickTestProfile', {
onlyForTest: testElements[0].test,
});
if (!profile) {
return;
}
@ -170,7 +171,7 @@ export class RunUsingProfileAction extends Action2 {
profileGroup: profile.group,
profileId: profile.profileId,
controllerId: profile.controllerId,
testIds: testElements.filter(t => controllerId === t.test.controllerId).map(t => t.test.item.extId)
testIds: testElements.filter(t => canUseProfileWithTest(profile, t.test)).map(t => t.test.item.extId)
}]
});
}
@ -288,14 +289,13 @@ abstract class ExecuteSelectedAction extends ViewAction<TestingExplorerView> {
private getActionableTests(testService: ITestService, viewModel: TestingExplorerViewModel) {
const selected = viewModel.getSelectedTests();
let tests: ITestIdWithSrc[];
let tests: InternalTestItem[];
if (!selected.length) {
tests = ([...testService.collection.rootItems].map(identifyTest));
tests = [...testService.collection.rootItems];
} else {
tests = selected
.map(treeElement => treeElement instanceof TestItemTreeElement ? treeElement.test : undefined)
.filter(isDefined)
.map(identifyTest);
.filter(isDefined);
}
return tests;
@ -372,7 +372,7 @@ abstract class RunOrDebugAllTestsAction extends Action2 {
return;
}
await testService.runTests({ tests: roots.map(identifyTest), group: this.group });
await testService.runTests({ tests: roots, group: this.group });
}
}
@ -795,7 +795,7 @@ abstract class ExecuteTestAtCursor extends Action2 {
if (bestNode) {
await testService.runTests({
group: this.group,
tests: [identifyTest(bestNode)],
tests: [bestNode],
});
}
}
@ -861,7 +861,7 @@ abstract class ExecuteTestsInCurrentFile extends Action2 {
for (const test of testService.collection.all) {
if (test.item.uri?.toString() === demandedUri) {
return testService.runTests({
tests: [identifyTest(test)],
tests: [test],
group: this.group,
});
}
@ -1015,7 +1015,7 @@ export class ReRunFailedTests extends RunOrDebugFailedTests {
protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise<ITestResult> {
return service.runTests({
group: TestRunProfileBitset.Run,
tests: internalTests.map(identifyTest),
tests: internalTests,
});
}
}
@ -1037,7 +1037,7 @@ export class DebugFailedTests extends RunOrDebugFailedTests {
protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise<ITestResult> {
return service.runTests({
group: TestRunProfileBitset.Debug,
tests: internalTests.map(identifyTest),
tests: internalTests,
});
}
}
@ -1059,7 +1059,7 @@ export class ReRunLastRun extends RunOrDebugLastRun {
protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise<ITestResult> {
return service.runTests({
group: TestRunProfileBitset.Run,
tests: internalTests.map(identifyTest),
tests: internalTests,
});
}
}
@ -1081,7 +1081,7 @@ export class DebugLastRun extends RunOrDebugLastRun {
protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise<ITestResult> {
return service.runTests({
group: TestRunProfileBitset.Debug,
tests: internalTests.map(identifyTest),
tests: internalTests,
});
}
}

View file

@ -28,13 +28,13 @@ import { ITestingProgressUiService, TestingProgressUiService } from 'vs/workbenc
import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer';
import { testingConfiguation } from 'vs/workbench/contrib/testing/common/configuration';
import { Testing } from 'vs/workbench/contrib/testing/common/constants';
import { identifyTest, ITestIdWithSrc, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestProfileService, TestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestId, TestPosition } from 'vs/workbench/contrib/testing/common/testId';
import { ITestingAutoRun, TestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun';
import { TestingContentProvider } from 'vs/workbench/contrib/testing/common/testingContentProvider';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener';
import { ITestProfileService, TestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
import { ITestResultService, TestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { ITestResultStorage, TestResultStorage } from 'vs/workbench/contrib/testing/common/testResultStorage';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
@ -115,22 +115,6 @@ Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).regi
registerEditorContribution(Testing.OutputPeekContributionId, TestingOutputPeekController);
registerEditorContribution(Testing.DecorationsContributionId, TestingDecorations);
CommandsRegistry.registerCommand({
id: 'vscode.runTests',
handler: async (accessor: ServicesAccessor, tests: ITestIdWithSrc[]) => {
const testService = accessor.get(ITestService);
testService.runTests({ group: TestRunProfileBitset.Run, tests });
}
});
CommandsRegistry.registerCommand({
id: 'vscode.debugTests',
handler: async (accessor: ServicesAccessor, tests: ITestIdWithSrc[]) => {
const testService = accessor.get(ITestService);
testService.runTests({ group: TestRunProfileBitset.Debug, tests });
}
});
CommandsRegistry.registerCommand({
id: 'vscode.revealTestInExplorer',
handler: async (accessor: ServicesAccessor, testId: string, focus?: boolean) => {
@ -216,7 +200,7 @@ CommandsRegistry.registerCommand({
accessor.get(ITestService).collection,
accessor.get(IProgressService),
testIds,
tests => testService.runTests({ group, tests: tests.map(identifyTest) }),
tests => testService.runTests({ group, tests }),
);
}
});

View file

@ -12,8 +12,8 @@ import { QuickPickInput, IQuickPickItem, IQuickInputService, IQuickPickItemButto
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { testingUpdateProfiles } from 'vs/workbench/contrib/testing/browser/icons';
import { testConfigurationGroupNames } from 'vs/workbench/contrib/testing/common/constants';
import { ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { InternalTestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { canUseProfileWithTest, ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
interface IConfigurationPickerOptions {
/** Placeholder text */
@ -21,7 +21,7 @@ interface IConfigurationPickerOptions {
/** Show buttons to trigger configuration */
showConfigureButtons?: boolean;
/** Only show configurations from this controller */
onlyControllerId?: string;
onlyForTest?: InternalTestItem;
/** Only show this group */
onlyGroup?: TestRunProfileBitset;
/** Only show items which are configurable */
@ -31,7 +31,7 @@ interface IConfigurationPickerOptions {
function buildPicker(accessor: ServicesAccessor, {
onlyGroup,
showConfigureButtons = true,
onlyControllerId,
onlyForTest,
onlyConfigurable,
placeholder = localize('testConfigurationUi.pick', 'Pick a test profile to use'),
}: IConfigurationPickerOptions) {
@ -74,13 +74,8 @@ function buildPicker(accessor: ServicesAccessor, {
}
};
if (onlyControllerId !== undefined) {
const lookup = profileService.getControllerProfiles(onlyControllerId);
if (!lookup) {
return;
}
pushItems(lookup.profiles);
if (onlyForTest !== undefined) {
pushItems(profileService.getControllerProfiles(onlyForTest.controllerId).filter(p => canUseProfileWithTest(p, onlyForTest)));
} else {
for (const { profiles, controller } of profileService.all()) {
pushItems(profiles, controller.label.value);

View file

@ -34,10 +34,10 @@ import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browse
import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme';
import { DefaultGutterClickAction, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
import { labelForTestInState } from 'vs/workbench/contrib/testing/common/constants';
import { identifyTest, IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, ITestRunProfile, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, ITestRunProfile, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { isFailedState, maxPriority } from 'vs/workbench/contrib/testing/common/testingStates';
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { getContextForTestItem, ITestService, testsInFile } from 'vs/workbench/contrib/testing/common/testService';
@ -413,24 +413,24 @@ abstract class RunTestDecoration extends Disposable {
*/
protected getTestContextMenuActions(test: InternalTestItem, resultItem?: TestResultItem): IReference<IAction[]> {
const testActions: IAction[] = [];
const capabilities = this.testProfileService.controllerCapabilities(test.controllerId);
const capabilities = this.testProfileService.capabilitiesForTest(test);
if (capabilities & TestRunProfileBitset.Run) {
testActions.push(new Action('testing.gutter.run', localize('run test', 'Run Test'), undefined, undefined, () => this.testService.runTests({
group: TestRunProfileBitset.Run,
tests: [identifyTest(test)],
tests: [test],
})));
}
if (capabilities & TestRunProfileBitset.Debug) {
testActions.push(new Action('testing.gutter.debug', localize('debug test', 'Debug Test'), undefined, undefined, () => this.testService.runTests({
group: TestRunProfileBitset.Debug,
tests: [identifyTest(test)],
tests: [test],
})));
}
if (capabilities & TestRunProfileBitset.HasNonDefaultProfile) {
testActions.push(new Action('testing.runUsing', localize('testing.runUsing', 'Execute Using Profile...'), undefined, undefined, async () => {
const profile: ITestRunProfile | undefined = await this.commandService.executeCommand('vscode.pickTestProfile', { onlyControllerId: test.controllerId });
const profile: ITestRunProfile | undefined = await this.commandService.executeCommand('vscode.pickTestProfile', { onlyForTest: test });
if (!profile) {
return;
}
@ -499,11 +499,11 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio
protected override getContextMenuActions() {
const allActions: IAction[] = [];
if (this.tests.some(({ test }) => this.testProfileService.controllerCapabilities(test.controllerId) & TestRunProfileBitset.Run)) {
if (this.tests.some(({ test }) => this.testProfileService.capabilitiesForTest(test) & TestRunProfileBitset.Run)) {
allActions.push(new Action('testing.gutter.runAll', localize('run all test', 'Run All Tests'), undefined, undefined, () => this.defaultRun()));
}
if (this.tests.some(({ test }) => this.testProfileService.controllerCapabilities(test.controllerId) & TestRunProfileBitset.Debug)) {
if (this.tests.some(({ test }) => this.testProfileService.capabilitiesForTest(test) & TestRunProfileBitset.Debug)) {
allActions.push(new Action('testing.gutter.debugAll', localize('debug all test', 'Debug All Tests'), undefined, undefined, () => this.defaultDebug()));
}
@ -519,14 +519,14 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio
protected override defaultRun() {
return this.testService.runTests({
tests: this.tests.map(({ test }) => identifyTest(test)),
tests: this.tests.map(({ test }) => test),
group: TestRunProfileBitset.Run,
});
}
protected override defaultDebug() {
return this.testService.runTests({
tests: this.tests.map(({ test }) => identifyTest(test)),
tests: this.tests.map(({ test }) => test),
group: TestRunProfileBitset.Run,
});
}
@ -561,14 +561,14 @@ class RunSingleTestDecoration extends RunTestDecoration implements ITestDecorati
protected override defaultRun() {
return this.testService.runTests({
tests: [identifyTest(this.test)],
tests: [this.test],
group: TestRunProfileBitset.Run,
});
}
protected override defaultDebug() {
return this.testService.runTests({
tests: [identifyTest(this.test)],
tests: [this.test],
group: TestRunProfileBitset.Debug,
});
}

View file

@ -56,12 +56,12 @@ import { ITestExplorerFilterState, TestExplorerFilterState, TestingExplorerFilte
import { ITestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService';
import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
import { labelForTestInState, TestExplorerStateFilter, TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants';
import { identifyTest, ITestRunProfile, TestItemExpandState, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { ITestRunProfile, TestItemExpandState, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener';
import { cmpPriority, isFailedState, isStateWithResult } from 'vs/workbench/contrib/testing/common/testingStates';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
import { TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { IMainThreadTestCollection, ITestService, testCollectionIsEmpty } from 'vs/workbench/contrib/testing/common/testService';
@ -590,7 +590,7 @@ export class TestingExplorerViewModel extends Disposable {
// If the node or any of its children are excluded, flip on the 'show
// excluded tests' checkbox automatically.
for (let n: TestItemTreeElement | null = element; n instanceof TestItemTreeElement; n = n.parent) {
if (n.test && this.testService.excluded.contains(identifyTest(n.test))) {
if (n.test && this.testService.excluded.contains(n.test)) {
this.filterState.showExcludedTests.value = true;
break;
}
@ -674,7 +674,7 @@ export class TestingExplorerViewModel extends Disposable {
if (toRun.length) {
this.testService.runTests({
group: TestRunProfileBitset.Run,
tests: toRun.map(t => identifyTest(t.test)),
tests: toRun.map(t => t.test),
});
}
}
@ -814,7 +814,7 @@ class TestsFilter implements ITreeFilter<TestExplorerTreeElement> {
if (
element.test
&& !this.state.showExcludedTests.value
&& this.testService.excluded.contains(identifyTest(element.test))
&& this.testService.excluded.contains(element.test)
) {
return TreeVisibility.Hidden;
}
@ -1147,7 +1147,7 @@ class TestItemRenderer extends ActionableItemTemplateData<TestItemTreeElement> {
const options: IResourceLabelOptions = {};
data.label.setResource(label, options);
const testHidden = this.testService.excluded.contains(identifyTest(node.element.test));
const testHidden = this.testService.excluded.contains(node.element.test);
data.wrapper.classList.toggle('test-is-hidden', testHidden);
const icon = icons.testingStatesToIcons.get(
@ -1197,8 +1197,8 @@ const getActionableElementActions = (
const test = element instanceof TestItemTreeElement ? element.test : undefined;
const contextOverlay = contextKeyService.createOverlay([
['view', Testing.ExplorerViewId],
[TestingContextKeys.testItemIsHidden.key, !!test && testService.excluded.contains(identifyTest(test))],
...getTestItemContextOverlay(test, test ? profiles.controllerCapabilities(test.controllerId) : 0),
[TestingContextKeys.testItemIsHidden.key, !!test && testService.excluded.contains(test)],
...getTestItemContextOverlay(test, test ? profiles.capabilitiesForTest(test) : 0),
]);
const menu = menuService.createMenu(MenuId.TestItem, contextOverlay);

View file

@ -63,7 +63,7 @@ import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme';
import { AutoOpenPeekViewWhen, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
import { Testing } from 'vs/workbench/contrib/testing/common/constants';
import { IRichLocation, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener';
import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates';
@ -1368,7 +1368,7 @@ class TreeActionsProvider {
public provideActionBar(element: ITreeElement) {
const test = element instanceof TestCaseElement ? element.test : undefined;
const capabilities = test ? this.testProfileService.controllerCapabilities(test.controllerId) : 0;
const capabilities = test ? this.testProfileService.capabilitiesForTest(test) : 0;
const contextOverlay = this.contextKeyService.createOverlay([
['peek', Testing.OutputPeekContributionId],
[TestingContextKeys.peekItemType.key, element.type],

View file

@ -5,7 +5,7 @@
import { Emitter } from 'vs/base/common/event';
import { Iterable } from 'vs/base/common/iterator';
import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, ITestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService';
export class MainThreadTestCollection extends AbstractIncrementalTestCollection<IncrementalTestCollectionItem> implements IMainThreadTestCollection {
@ -45,7 +45,7 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection<
public readonly onBusyProvidersChange = this.busyProvidersChangeEmitter.event;
public readonly onDidRetireTest = this.retireTestEmitter.event;
constructor(private readonly expandActual: (src: ITestIdWithSrc, levels: number) => Promise<void>) {
constructor(private readonly expandActual: (id: string, levels: number) => Promise<void>) {
super();
}
@ -64,7 +64,7 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection<
return existing.prom;
}
const prom = this.expandActual({ controllerId: test.controllerId, testId: test.item.extId }, levels);
const prom = this.expandActual(test.item.extId, levels);
const record = { doneLvl: existing ? existing.doneLvl : -1, pendingLvl: levels, prom };
this.expandPromises.set(test, record);

View file

@ -10,7 +10,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { assertNever } from 'vs/base/common/types';
import { diffTestItems, ExtHostTestItemEvent, ExtHostTestItemEventOp, getPrivateApiFor, TestItemImpl, TestItemRootImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi';
import * as Convert from 'vs/workbench/api/common/extHostTypeConverters';
import { applyTestItemUpdate, TestDiffOpType, TestItemExpandState, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testCollection';
import { applyTestItemUpdate, ITestTag, TestDiffOpType, TestItemExpandState, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
type TestItemRaw = Convert.TestItem.Raw;
@ -78,6 +78,21 @@ export class SingleUseTestCollection extends Disposable {
return diff;
}
/**
* Gets all tags associated with items in the collection.
*/
public *tags() {
const seen = new Set<string>();
for (const item of this.tree.values()) {
for (const tag of item.actual.tags) {
if (!seen.has(tag.id)) {
seen.add(tag.id);
yield tag;
}
}
}
}
/**
* Pushes a new diff entry onto the collected diff list.
*/
@ -169,6 +184,9 @@ export class SingleUseTestCollection extends Disposable {
case 'canResolveChildren':
this.updateExpandability(internal);
break;
case 'tags':
this.pushDiff([TestDiffOpType.Update, { extId, item: { tags: (value as ITestTag[]).map(v => v.id) }, }]);
break;
case 'range':
this.pushDiff([TestDiffOpType.Update, { extId, item: { range: Convert.Range.from(value) }, }]);
break;

View file

@ -10,11 +10,6 @@ import { IPosition } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { TestMessageSeverity } from 'vs/workbench/api/common/extHostTypes';
export interface ITestIdWithSrc {
testId: string;
controllerId: string;
}
export const enum TestResultState {
Unset = 0,
Queued = 1,
@ -25,9 +20,6 @@ export const enum TestResultState {
Errored = 6
}
export const identifyTest = (test: { controllerId: string, item: { extId: string } }): ITestIdWithSrc =>
({ testId: test.item.extId, controllerId: test.controllerId });
export const enum TestRunProfileBitset {
Run = 1 << 1,
Debug = 1 << 2,
@ -55,6 +47,7 @@ export interface ITestRunProfile {
label: string;
group: TestRunProfileBitset;
isDefault: boolean;
tag: string | null;
hasConfigurationHandler: boolean;
}
@ -69,7 +62,7 @@ export interface ResolvedTestRunRequest {
profileGroup: TestRunProfileBitset;
profileId: number;
}[]
exclude?: ITestIdWithSrc[];
exclude?: string[];
isAutoRun?: boolean;
}
@ -125,6 +118,17 @@ export interface ITestRunTask {
running: boolean;
}
export interface ITestTag {
id: string;
label?: string;
}
export interface ITestTagDisplayInfo {
id: string;
displayId: string;
label?: string;
}
/**
* The TestItem from .d.ts, as a plain object without children.
*/
@ -132,6 +136,7 @@ export interface ITestItem {
/** ID of the test given by the test controller */
extId: string;
label: string;
tags: string[];
busy?: boolean;
children?: never;
uri?: URI;

View file

@ -9,7 +9,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue';
import { ITestIdWithSrc } from 'vs/workbench/contrib/testing/common/testCollection';
import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection';
export class TestExclusions extends Disposable {
private readonly excluded = this._register(
@ -43,30 +43,26 @@ export class TestExclusions extends Disposable {
/**
* Gets all excluded tests.
*/
public get all() {
return Iterable.map(this.excluded.value, v => {
const [controllerId, testId] = JSON.parse(v);
return { controllerId, testId };
});
public get all(): Iterable<string> {
return this.excluded.value;
}
/**
* Sets whether a test is excluded.
*/
public toggle(test: ITestIdWithSrc, exclude?: boolean): void {
const slug = this.identify(test);
if (exclude !== true && this.excluded.value.has(slug)) {
this.excluded.value = new Set(Iterable.filter(this.excluded.value, e => e !== slug));
} else if (exclude !== false && !this.excluded.value.has(slug)) {
this.excluded.value = new Set([...this.excluded.value, slug]);
public toggle(test: InternalTestItem, exclude?: boolean): void {
if (exclude !== true && this.excluded.value.has(test.item.extId)) {
this.excluded.value = new Set(Iterable.filter(this.excluded.value, e => e !== test.item.extId));
} else if (exclude !== false && !this.excluded.value.has(test.item.extId)) {
this.excluded.value = new Set([...this.excluded.value, test.item.extId]);
}
}
/**
* Gets whether a test is excluded.
*/
public contains(test: ITestIdWithSrc): boolean {
return this.excluded.value.has(this.identify(test));
public contains(test: InternalTestItem): boolean {
return this.excluded.value.has(test.item.extId);
}
/**
@ -75,8 +71,4 @@ export class TestExclusions extends Disposable {
public clear(): void {
this.excluded.value = new Set();
}
private identify(test: ITestIdWithSrc) {
return JSON.stringify([test.controllerId, test.testId]);
}
}

View file

@ -48,6 +48,13 @@ export class TestId {
return new TestId(path.reverse());
}
/**
* Cheaply ets whether the ID refers to the root .
*/
public static isRoot(idString: string) {
return !idString.includes(TestIdPathParts.Delimiter);
}
/**
* Creates a test ID from a serialized TestId instance.
*/

View file

@ -9,7 +9,8 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue';
import { ITestRunProfile, TestRunProfileBitset, testRunProfileBitsetList } from 'vs/workbench/contrib/testing/common/testCollection';
import { InternalTestItem, ITestRunProfile, TestRunProfileBitset, testRunProfileBitsetList } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { IMainThreadTestController } from 'vs/workbench/contrib/testing/common/testService';
@ -40,11 +41,11 @@ export interface ITestProfileService {
removeProfile(controllerId: string, profileId?: number): void;
/**
* Gets capabilities for the given controller by ID, indicating whether
* there's any profiles available for those groups.
* Gets capabilities for the given test, indicating whether
* there's any usable profiles available for those groups.
* @returns a bitset to use with {@link TestRunProfileBitset}
*/
controllerCapabilities(controllerId: string): number;
capabilitiesForTest(test: InternalTestItem): number;
/**
* Configures a test profile.
@ -55,7 +56,6 @@ export interface ITestProfileService {
* Gets all registered controllers, grouping by controller.
*/
all(): Iterable<Readonly<{
capabilities: number,
controller: IMainThreadTestController,
profiles: ITestRunProfile[],
}>>;
@ -73,17 +73,15 @@ export interface ITestProfileService {
/**
* Gets the profiles for a controller, in priority order.
*/
getControllerProfiles(controllerId: string): undefined | {
controller: IMainThreadTestController;
profiles: ITestRunProfile[];
};
/**
* Gets the profiles for the group in a controller, in priorty order.
*/
getControllerGroupProfiles(controllerId: string, group: TestRunProfileBitset): readonly ITestRunProfile[];
getControllerProfiles(controllerId: string): ITestRunProfile[];
}
/**
* Gets whether the given profile can be used to run the test.
*/
export const canUseProfileWithTest = (profile: ITestRunProfile, test: InternalTestItem) =>
profile.controllerId === test.controllerId && (TestId.isRoot(test.item.extId) || !profile.tag || test.item.tags.includes(profile.tag));
const sorter = (a: ITestRunProfile, b: ITestRunProfile) => {
if (a.isDefault !== b.isDefault) {
return a.isDefault ? -1 : 1;
@ -102,7 +100,6 @@ export const capabilityContextKeys = (capabilities: number): [key: string, value
[TestingContextKeys.hasCoverableTests.key, (capabilities & TestRunProfileBitset.Coverage) !== 0],
];
export class TestProfileService implements ITestProfileService {
declare readonly _serviceBrand: undefined;
private readonly preferredDefaults: StoredValue<{ [K in TestRunProfileBitset]?: { controllerId: string; profileId: number }[] }>;
@ -111,7 +108,6 @@ export class TestProfileService implements ITestProfileService {
private readonly controllerProfiles = new Map</* controller ID */string, {
profiles: ITestRunProfile[],
controller: IMainThreadTestController,
capabilities: number,
}>();
/** @inheritdoc */
@ -144,20 +140,14 @@ export class TestProfileService implements ITestProfileService {
if (record) {
record.profiles.push(profile);
record.profiles.sort(sorter);
record.capabilities |= profile.group;
} else {
record = {
profiles: [profile],
controller,
capabilities: profile.group
};
this.controllerProfiles.set(profile.controllerId, record);
}
if (!profile.isDefault) {
record.capabilities |= TestRunProfileBitset.HasNonDefaultProfile;
}
this.refreshContextKeys();
this.changeEmitter.fire();
}
@ -203,18 +193,25 @@ export class TestProfileService implements ITestProfileService {
}
ctrl.profiles.splice(index, 1);
ctrl.capabilities = 0;
for (const { group } of ctrl.profiles) {
ctrl.capabilities |= group;
}
this.refreshContextKeys();
this.changeEmitter.fire();
}
/** @inheritdoc */
public controllerCapabilities(controllerId: string) {
return this.controllerProfiles.get(controllerId)?.capabilities || 0;
public capabilitiesForTest(test: InternalTestItem) {
const ctrl = this.controllerProfiles.get(test.controllerId);
if (!ctrl) {
return 0;
}
let capabilities = 0;
for (const profile of ctrl.profiles) {
if (!profile.tag || test.item.tags.includes(profile.tag)) {
capabilities |= capabilities & profile.group ? TestRunProfileBitset.HasNonDefaultProfile : profile.group;
}
}
return capabilities;
}
/** @inheritdoc */
@ -224,12 +221,7 @@ export class TestProfileService implements ITestProfileService {
/** @inheritdoc */
public getControllerProfiles(profileId: string) {
return this.controllerProfiles.get(profileId);
}
/** @inheritdoc */
public getControllerGroupProfiles(controllerId: string, group: TestRunProfileBitset) {
return this.controllerProfiles.get(controllerId)?.profiles.filter(c => c.group === group) ?? [];
return this.controllerProfiles.get(profileId)?.profiles ?? [];
}
/** @inheritdoc */
@ -271,8 +263,10 @@ export class TestProfileService implements ITestProfileService {
private refreshContextKeys() {
let allCapabilities = 0;
for (const { capabilities } of this.controllerProfiles.values()) {
allCapabilities |= capabilities;
for (const { profiles } of this.controllerProfiles.values()) {
for (const profile of profiles) {
allCapabilities |= allCapabilities & profile.group ? TestRunProfileBitset.HasNonDefaultProfile : profile.group;
}
}
for (const group of testRunProfileBitsetList) {

View file

@ -10,9 +10,9 @@ import { once } from 'vs/base/common/functional';
import { generateUuid } from 'vs/base/common/uuid';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ExtensionRunTestsRequest, ITestRunProfile, ResolvedTestRunRequest, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { ExtensionRunTestsRequest, ITestRunProfile, ResolvedTestRunRequest, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
import { ITestResult, LiveTestResult, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage';
@ -137,16 +137,14 @@ export class TestResultService implements ITestResultService {
}
let profile: ITestRunProfile | undefined;
if (!req.profile) {
profile = this.testProfiles.getControllerGroupProfiles(req.controllerId, TestRunProfileBitset.Run)[0];
} else {
const profiles = this.testProfiles.getControllerGroupProfiles(req.controllerId, req.profile.group);
profile = profiles.find(c => c.profileId === req.profile!.id) || profiles[0];
if (req.profile) {
const profiles = this.testProfiles.getControllerProfiles(req.controllerId);
profile = profiles.find(c => c.profileId === req.profile!.id);
}
const resolved: ResolvedTestRunRequest = {
targets: [],
exclude: req.exclude.map(testId => ({ testId, controllerId: req.controllerId })),
exclude: req.exclude,
isAutoRun: false,
};

View file

@ -12,7 +12,7 @@ import { MarshalledId } from 'vs/base/common/marshalling';
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, ITestIdWithSrc, ITestItemContext, ResolvedTestRunRequest, RunTestForControllerRequest, TestItemExpandState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, ITestItemContext, ITestTagDisplayInfo, ResolvedTestRunRequest, RunTestForControllerRequest, TestItemExpandState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestExclusions } from 'vs/workbench/contrib/testing/common/testExclusions';
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
@ -22,8 +22,9 @@ export const ITestService = createDecorator<ITestService>('testService');
export interface IMainThreadTestController {
readonly id: string;
readonly label: IObservableValue<string>;
getTags(): Promise<ITestTagDisplayInfo[]>;
configureRunProfile(profileId: number): void;
expandTest(src: ITestIdWithSrc, levels: number): Promise<void>;
expandTest(id: string, levels: number): Promise<void>;
runTests(request: RunTestForControllerRequest, token: CancellationToken): Promise<void>;
}
@ -194,9 +195,9 @@ export interface AmbiguousRunTestsRequest {
/** Group to run */
group: TestRunProfileBitset;
/** Tests to run. Allowed to be from different controllers */
tests: ITestIdWithSrc[];
tests: readonly InternalTestItem[];
/** Tests to exclude. If not given, the current UI excluded tests are used */
exclude?: ITestIdWithSrc[];
exclude?: InternalTestItem[];
/** Whether this was triggered from an auto run. */
isAutoRun?: boolean;
}

View file

@ -13,10 +13,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust';
import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/mainThreadTestCollection';
import { ITestIdWithSrc, ResolvedTestRunRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { ResolvedTestRunRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestExclusions } from 'vs/workbench/contrib/testing/common/testExclusions';
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { canUseProfileWithTest, ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { AmbiguousRunTestsRequest, IMainThreadTestController, ITestService } from 'vs/workbench/contrib/testing/common/testService';
@ -70,8 +71,8 @@ export class TestService extends Disposable implements ITestService {
/**
* @inheritdoc
*/
public async expandTest(test: ITestIdWithSrc, levels: number) {
await this.testControllers.get(test.controllerId)?.expandTest(test, levels);
public async expandTest(id: string, levels: number) {
await this.testControllers.get(TestId.fromString(id).controllerId)?.expandTest(id, levels);
}
/**
@ -93,11 +94,15 @@ export class TestService extends Disposable implements ITestService {
* @inheritdoc
*/
public async runTests(req: AmbiguousRunTestsRequest, token = CancellationToken.None): Promise<ITestResult> {
const resolved: ResolvedTestRunRequest = { targets: [], exclude: req.exclude, isAutoRun: req.isAutoRun };
const resolved: ResolvedTestRunRequest = {
targets: [],
exclude: req.exclude?.map(t => t.item.extId),
isAutoRun: req.isAutoRun,
};
// First, try to run the tests using the default run profiles...
for (const profile of this.testProfiles.getGroupDefaultProfiles(req.group)) {
const testIds = req.tests.filter(t => t.controllerId === profile.controllerId).map(t => t.testId);
const testIds = req.tests.filter(t => canUseProfileWithTest(profile, t)).map(t => t.item.extId);
if (testIds.length) {
resolved.targets.push({
testIds: testIds,
@ -114,14 +119,22 @@ export class TestService extends Disposable implements ITestService {
// explorer or decoration. We shouldn't no-op.
if (resolved.targets.length === 0) {
for (const byController of groupBy(req.tests, (a, b) => a.controllerId === b.controllerId ? 0 : 1)) {
const profiles = this.testProfiles.getControllerGroupProfiles(byController[0].controllerId, req.group);
if (profiles.length) {
resolved.targets.push({
testIds: byController.map(t => t.testId),
profileGroup: req.group,
profileId: profiles[0].profileId,
controllerId: profiles[0].controllerId,
});
const profiles = this.testProfiles.getControllerProfiles(byController[0].controllerId);
const withControllers = byController.map(test => ({
profile: profiles.find(p => p.group === req.group && canUseProfileWithTest(p, test)),
test,
}));
for (const byProfile of groupBy(withControllers, (a, b) => a.profile === b.profile ? 0 : 1)) {
const profile = byProfile[0].profile;
if (profile) {
resolved.targets.push({
testIds: byProfile.map(t => t.test.item.extId),
profileGroup: req.group,
profileId: profile.profileId,
controllerId: profile.controllerId,
});
}
}
}
}
@ -155,9 +168,7 @@ export class TestService extends Disposable implements ITestService {
group => this.testControllers.get(group.controllerId)?.runTests(
{
runId: result.id,
excludeExtIds: req.exclude!
.filter(t => t.controllerId === group.controllerId && !group.testIds.includes(t.testId))
.map(t => t.testId),
excludeExtIds: req.exclude!.filter(t => !group.testIds.includes(t)),
profileId: group.profileId,
controllerId: group.controllerId,
testIds: group.testIds,

View file

@ -16,7 +16,7 @@ export { TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi';
* roots/stubs.
*/
export const getInitializedMainTestCollection = async (singleUse = testStubs.nested()) => {
const c = new MainThreadTestCollection(async (t, l) => singleUse.expand(t.testId, l));
const c = new MainThreadTestCollection(async (t, l) => singleUse.expand(t, l));
await singleUse.expand(singleUse.root.id, Infinity);
c.apply(singleUse.collectDiff());
return c;

View file

@ -11,7 +11,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { AutoRunMode, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
import { identifyTest, ITestIdWithSrc, TestDiffOpType, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { InternalTestItem, TestDiffOpType, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
import { isRunningTests, ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
@ -65,7 +65,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun {
* Runs them on a debounce.
*/
private makeRunner() {
const rerunIds = new Map<string, ITestIdWithSrc>();
const rerunIds = new Map<string, InternalTestItem>();
const store = new DisposableStore();
const cts = new CancellationTokenSource();
store.add(toDisposable(() => cts.dispose(true)));
@ -90,15 +90,15 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun {
}
}, delay));
const addToRerun = (test: ITestIdWithSrc) => {
rerunIds.set(getTestKey(test), test);
const addToRerun = (test: InternalTestItem) => {
rerunIds.set(test.item.extId, test);
if (!isRunningTests(this.results)) {
scheduler.schedule(delay);
}
};
const removeFromRerun = (test: ITestIdWithSrc) => {
rerunIds.delete(getTestKey(test));
const removeFromRerun = (test: InternalTestItem) => {
rerunIds.delete(test.item.extId);
if (rerunIds.size === 0) {
scheduler.cancel();
}
@ -106,9 +106,9 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun {
store.add(this.results.onTestChanged(evt => {
if (evt.reason === TestResultItemChangeReason.Retired) {
addToRerun(identifyTest(evt.item));
addToRerun(evt.item);
} else if ((evt.reason === TestResultItemChangeReason.OwnStateChange || evt.reason === TestResultItemChangeReason.ComputedStateChange)) {
removeFromRerun(identifyTest(evt.item));
removeFromRerun(evt.item);
}
}));
@ -126,12 +126,12 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun {
const test = entry[1];
const isQueued = Iterable.some(
getCollectionItemParents(this.testService.collection, test),
t => rerunIds.has(getTestKey(identifyTest(test))),
t => rerunIds.has(test.item.extId),
);
const state = this.results.getStateById(test.item.extId);
if (!isQueued && (!state || state[1].retired)) {
addToRerun(identifyTest(test));
addToRerun(test);
}
}
}
@ -139,12 +139,10 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun {
for (const root of this.testService.collection.rootItems) {
addToRerun(identifyTest(root));
addToRerun(root);
}
}
return store;
}
}
const getTestKey = (test: ITestIdWithSrc) => `${test.controllerId}\0${test.testId}`;

View file

@ -87,8 +87,8 @@ export class TestTreeTestHarness<T extends ITestTreeProjection = ITestTreeProjec
this._register(c);
this.c.onDidGenerateDiff(d => this.c.setDiff(d /* don't clear during testing */));
const collection = new MainThreadTestCollection((src, levels) => {
this.c.expand(src.testId, levels);
const collection = new MainThreadTestCollection((testId, levels) => {
this.c.expand(testId, levels);
if (!this.isProcessingDiff) {
this.onDiff.fire(this.c.collectDiff());
}

View file

@ -11,7 +11,7 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe
import { NullLogService } from 'vs/platform/log/common/log';
import { SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
import { ITestTaskState, ResolvedTestRunRequest, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { TestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
import { HydratedTestResult, LiveOutputController, LiveTestResult, makeEmptyCounts, resultItemParents, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
import { TestResultService } from 'vs/workbench/contrib/testing/common/testResultService';