diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 2ea50c6e796..3eb3045b9a1 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -21,7 +21,7 @@ import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ISelection } from 'vs/editor/common/core/selection'; -import { TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; +import { TestItemImpl } from 'vs/workbench/api/common/extHostTestItem'; import { VSBuffer } from 'vs/base/common/buffer'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { toErrorMessage } from 'vs/base/common/errorMessage'; diff --git a/src/vs/workbench/api/common/extHostTestItem.ts b/src/vs/workbench/api/common/extHostTestItem.ts new file mode 100644 index 00000000000..7807782afd7 --- /dev/null +++ b/src/vs/workbench/api/common/extHostTestItem.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as editorRange from 'vs/editor/common/core/range'; +import { createPrivateApiFor, getPrivateApiFor, IExtHostTestItemApi } from 'vs/workbench/api/common/extHostTestingPrivateApi'; +import { TestIdPathParts } from 'vs/workbench/contrib/testing/common/testId'; +import { createTestItemChildren, ExtHostTestItemEvent, ITestChildrenLike, ITestItemApi, ITestItemChildren, TestItemCollection, TestItemEventOp } from 'vs/workbench/contrib/testing/common/testItemCollection'; +import { ITestItem } from 'vs/workbench/contrib/testing/common/testTypes'; +import type * as vscode from 'vscode'; +import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; + +const testItemPropAccessor = ( + api: IExtHostTestItemApi, + defaultValue: vscode.TestItem[K], + equals: (a: vscode.TestItem[K], b: vscode.TestItem[K]) => boolean, + toUpdate: (newValue: vscode.TestItem[K], oldValue: vscode.TestItem[K]) => ExtHostTestItemEvent, +) => { + let value = defaultValue; + return { + enumerable: true, + configurable: false, + get() { + return value; + }, + set(newValue: vscode.TestItem[K]) { + if (!equals(value, newValue)) { + const oldValue = value; + value = newValue; + api.listener?.(toUpdate(newValue, oldValue)); + } + }, + }; +}; + +type WritableProps = Pick; + +const strictEqualComparator = (a: T, b: T) => a === b; + +const propComparators: { [K in keyof Required]: (a: vscode.TestItem[K], b: vscode.TestItem[K]) => boolean } = { + range: (a, b) => { + if (a === b) { return true; } + if (!a || !b) { return false; } + return a.isEqual(b); + }, + label: strictEqualComparator, + description: strictEqualComparator, + sortText: strictEqualComparator, + busy: strictEqualComparator, + error: 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 evSetProps = (fn: (newValue: T) => Partial): (newValue: T) => ExtHostTestItemEvent => + v => ({ op: TestItemEventOp.SetProp, update: fn(v) }); + +const makePropDescriptors = (api: IExtHostTestItemApi, label: string): { [K in keyof Required]: PropertyDescriptor } => ({ + range: testItemPropAccessor<'range'>(api, undefined, propComparators.range, evSetProps(r => ({ range: editorRange.Range.lift(Convert.Range.from(r)) }))), + label: testItemPropAccessor<'label'>(api, label, propComparators.label, evSetProps(label => ({ label }))), + description: testItemPropAccessor<'description'>(api, undefined, propComparators.description, evSetProps(description => ({ description }))), + sortText: testItemPropAccessor<'sortText'>(api, undefined, propComparators.sortText, evSetProps(sortText => ({ sortText }))), + canResolveChildren: testItemPropAccessor<'canResolveChildren'>(api, false, propComparators.canResolveChildren, state => ({ + op: TestItemEventOp.UpdateCanResolveChildren, + state, + })), + busy: testItemPropAccessor<'busy'>(api, false, propComparators.busy, evSetProps(busy => ({ busy }))), + error: testItemPropAccessor<'error'>(api, undefined, propComparators.error, evSetProps(error => ({ error: Convert.MarkdownString.fromStrict(error) || null }))), + tags: testItemPropAccessor<'tags'>(api, [], propComparators.tags, (current, previous) => ({ + op: TestItemEventOp.SetTags, + new: current.map(Convert.TestTag.from), + old: previous.map(Convert.TestTag.from), + })), +}); + +export class TestItemImpl implements vscode.TestItem { + public readonly id!: string; + public readonly uri!: vscode.Uri | undefined; + public readonly children!: ITestItemChildren; + public readonly parent!: TestItemImpl | undefined; + + public range!: vscode.Range | undefined; + public description!: string | undefined; + public sortText!: string | undefined; + public label!: string; + 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 + */ + constructor(controllerId: string, id: string, label: string, uri: vscode.Uri | undefined) { + if (id.includes(TestIdPathParts.Delimiter)) { + throw new Error(`Test IDs may not include the ${JSON.stringify(id)} symbol`); + } + + const api = createPrivateApiFor(this, controllerId); + Object.defineProperties(this, { + id: { + value: id, + enumerable: true, + writable: false, + }, + uri: { + value: uri, + enumerable: true, + writable: false, + }, + parent: { + enumerable: false, + get() { + return api.parent instanceof TestItemRootImpl ? undefined : api.parent; + }, + }, + children: { + value: createTestItemChildren(api, getPrivateApiFor, TestItemImpl), + enumerable: true, + writable: false, + }, + ...makePropDescriptors(api, label), + }); + } +} + +export class TestItemRootImpl extends TestItemImpl { + constructor(controllerId: string, label: string) { + super(controllerId, controllerId, label, undefined); + } +} + +export class ExtHostTestItemCollection extends TestItemCollection { + constructor(controllerId: string, controllerLabel: string) { + super({ + controllerId, + getApiFor: getPrivateApiFor as (impl: TestItemImpl) => ITestItemApi, + getChildren: (item) => item.children as ITestChildrenLike, + root: new TestItemRootImpl(controllerId, controllerLabel), + toITestItem: Convert.TestItem.from, + }); + } +} diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 4cd81db3bbe..0fb81ed4046 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -17,12 +17,12 @@ import { generateUuid } from 'vs/base/common/uuid'; import { ExtHostTestingShape, ILocationDto, MainContext, MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { ExtHostTestItemCollection, TestItemImpl, TestItemRootImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; +import { ExtHostTestItemCollection, TestItemImpl, TestItemRootImpl } from 'vs/workbench/api/common/extHostTestItem'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; import { TestRunProfileKind, TestRunRequest } from 'vs/workbench/api/common/extHostTypes'; -import { AbstractIncrementalTestCollection, CoverageDetails, IFileCoverage, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestItem, RunTestForControllerRequest, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; +import { AbstractIncrementalTestCollection, CoverageDetails, IFileCoverage, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestItem, RunTestForControllerRequest, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import type * as vscode from 'vscode'; interface ControllerInfo { diff --git a/src/vs/workbench/api/common/extHostTestingPrivateApi.ts b/src/vs/workbench/api/common/extHostTestingPrivateApi.ts index cd3e746442b..f0870fd5a00 100644 --- a/src/vs/workbench/api/common/extHostTestingPrivateApi.ts +++ b/src/vs/workbench/api/common/extHostTestingPrivateApi.ts @@ -3,22 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as editorRange from 'vs/editor/common/core/range'; -import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { ITestItem } from 'vs/workbench/contrib/testing/common/testTypes'; -import { TestIdPathParts } from 'vs/workbench/contrib/testing/common/testId'; -import { createTestItemChildren, ExtHostTestItemEvent, InvalidTestItemError, ITestChildrenLike, ITestItemChildren, TestItemCollection, TestItemEventOp } from 'vs/workbench/contrib/testing/common/testItemCollection'; +import { ExtHostTestItemEvent, InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; import * as vscode from 'vscode'; export interface IExtHostTestItemApi { controllerId: string; - parent?: TestItemImpl; + parent?: vscode.TestItem; listener?: (evt: ExtHostTestItemEvent) => void; } -const eventPrivateApis = new WeakMap(); +const eventPrivateApis = new WeakMap(); -export const createPrivateApiFor = (impl: TestItemImpl, controllerId: string) => { +export const createPrivateApiFor = (impl: vscode.TestItem, controllerId: string) => { const api: IExtHostTestItemApi = { controllerId }; eventPrivateApis.set(impl, api); return api; @@ -29,7 +25,7 @@ export const createPrivateApiFor = (impl: TestItemImpl, controllerId: string) => * is a managed object, but we keep a weakmap to avoid exposing any of the * internals to extensions. */ -export const getPrivateApiFor = (impl: TestItemImpl) => { +export const getPrivateApiFor = (impl: vscode.TestItem) => { const api = eventPrivateApis.get(impl); if (!api) { throw new InvalidTestItemError(impl?.id || ''); @@ -37,145 +33,3 @@ export const getPrivateApiFor = (impl: TestItemImpl) => { return api; }; - -const testItemPropAccessor = ( - api: IExtHostTestItemApi, - defaultValue: vscode.TestItem[K], - equals: (a: vscode.TestItem[K], b: vscode.TestItem[K]) => boolean, - toUpdate: (newValue: vscode.TestItem[K], oldValue: vscode.TestItem[K]) => ExtHostTestItemEvent, -) => { - let value = defaultValue; - return { - enumerable: true, - configurable: false, - get() { - return value; - }, - set(newValue: vscode.TestItem[K]) { - if (!equals(value, newValue)) { - const oldValue = value; - value = newValue; - api.listener?.(toUpdate(newValue, oldValue)); - } - }, - }; -}; - -type WritableProps = Pick; - -const strictEqualComparator = (a: T, b: T) => a === b; - -const propComparators: { [K in keyof Required]: (a: vscode.TestItem[K], b: vscode.TestItem[K]) => boolean } = { - range: (a, b) => { - if (a === b) { return true; } - if (!a || !b) { return false; } - return a.isEqual(b); - }, - label: strictEqualComparator, - description: strictEqualComparator, - sortText: strictEqualComparator, - busy: strictEqualComparator, - error: 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 evSetProps = (fn: (newValue: T) => Partial): (newValue: T) => ExtHostTestItemEvent => - v => ({ op: TestItemEventOp.SetProp, update: fn(v) }); - -const makePropDescriptors = (api: IExtHostTestItemApi, label: string): { [K in keyof Required]: PropertyDescriptor } => ({ - range: testItemPropAccessor<'range'>(api, undefined, propComparators.range, evSetProps(r => ({ range: editorRange.Range.lift(Convert.Range.from(r)) }))), - label: testItemPropAccessor<'label'>(api, label, propComparators.label, evSetProps(label => ({ label }))), - description: testItemPropAccessor<'description'>(api, undefined, propComparators.description, evSetProps(description => ({ description }))), - sortText: testItemPropAccessor<'sortText'>(api, undefined, propComparators.sortText, evSetProps(sortText => ({ sortText }))), - canResolveChildren: testItemPropAccessor<'canResolveChildren'>(api, false, propComparators.canResolveChildren, state => ({ - op: TestItemEventOp.UpdateCanResolveChildren, - state, - })), - busy: testItemPropAccessor<'busy'>(api, false, propComparators.busy, evSetProps(busy => ({ busy }))), - error: testItemPropAccessor<'error'>(api, undefined, propComparators.error, evSetProps(error => ({ error: Convert.MarkdownString.fromStrict(error) || null }))), - tags: testItemPropAccessor<'tags'>(api, [], propComparators.tags, (current, previous) => ({ - op: TestItemEventOp.SetTags, - new: current.map(Convert.TestTag.from), - old: previous.map(Convert.TestTag.from), - })), -}); - -export class TestItemImpl implements vscode.TestItem { - public readonly id!: string; - public readonly uri!: vscode.Uri | undefined; - public readonly children!: ITestItemChildren; - public readonly parent!: TestItemImpl | undefined; - - public range!: vscode.Range | undefined; - public description!: string | undefined; - public sortText!: string | undefined; - public label!: string; - 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 - */ - constructor(controllerId: string, id: string, label: string, uri: vscode.Uri | undefined) { - if (id.includes(TestIdPathParts.Delimiter)) { - throw new Error(`Test IDs may not include the ${JSON.stringify(id)} symbol`); - } - - const api = createPrivateApiFor(this, controllerId); - Object.defineProperties(this, { - id: { - value: id, - enumerable: true, - writable: false, - }, - uri: { - value: uri, - enumerable: true, - writable: false, - }, - parent: { - enumerable: false, - get() { - return api.parent instanceof TestItemRootImpl ? undefined : api.parent; - }, - }, - children: { - value: createTestItemChildren(api, getPrivateApiFor, TestItemImpl), - enumerable: true, - writable: false, - }, - ...makePropDescriptors(api, label), - }); - } -} - -export class TestItemRootImpl extends TestItemImpl { - constructor(controllerId: string, label: string) { - super(controllerId, controllerId, label, undefined); - } -} - -export class ExtHostTestItemCollection extends TestItemCollection { - constructor(controllerId: string, controllerLabel: string) { - super({ - controllerId, - getApiFor: getPrivateApiFor, - getChildren: (item) => item.children as ITestChildrenLike, - root: new TestItemRootImpl(controllerId, controllerLabel), - toITestItem: Convert.TestItem.from, - }); - } -} diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 0214f02b65b..1c333145344 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -25,14 +25,14 @@ import { EditorResolution, ITextEditorOptions } from 'vs/platform/editor/common/ import { IMarkerData, IRelatedInformation, MarkerSeverity, MarkerTag } from 'vs/platform/markers/common/markers'; import { ProgressLocation as MainProgressLocation } from 'vs/platform/progress/common/progress'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; -import { getPrivateApiFor, TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; +import { getPrivateApiFor } from 'vs/workbench/api/common/extHostTestingPrivateApi'; import { SaveReason } from 'vs/workbench/common/editor'; import { IViewBadge } from 'vs/workbench/common/views'; 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, denamespaceTestTag, DetailType, ICoveredCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestItemContext, ITestTag, namespaceTestTag, TestMessageType, TestResultItem } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { CoverageDetails, denamespaceTestTag, DetailType, ICoveredCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestItemContext, ITestTag, namespaceTestTag, TestMessageType, TestResultItem } from 'vs/workbench/contrib/testing/common/testTypes'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import type * as vscode from 'vscode'; @@ -1736,7 +1736,7 @@ export namespace TestTag { export namespace TestItem { export type Raw = vscode.TestItem; - export function from(item: TestItemImpl): ITestItem { + export function from(item: vscode.TestItem): ITestItem { const ctrlId = getPrivateApiFor(item).controllerId; return { extId: TestId.fromExtHostTestItem(item, ctrlId).toString(), @@ -1751,7 +1751,7 @@ export namespace TestItem { }; } - export function toPlain(item: ITestItem.Serialized): Omit { + export function toPlain(item: ITestItem.Serialized): vscode.TestItem { return { parent: undefined, error: undefined, @@ -1762,6 +1762,14 @@ export namespace TestItem { const { tagId } = TestTag.denamespace(t); return new types.TestTag(tagId); }), + children: { + add: () => { }, + delete: () => { }, + forEach: () => { }, + get: () => undefined, + replace: () => { }, + size: 0, + }, range: Range.to(item.range || undefined), canResolveChildren: false, busy: false, @@ -1770,21 +1778,19 @@ export namespace TestItem { }; } - function to(item: ITestItem): TestItemImpl { - const testId = TestId.fromString(item.extId); - const testItem = new TestItemImpl(testId.controllerId, testId.localId, item.label, URI.revive(item.uri) || undefined); - testItem.range = Range.to(item.range || undefined); - testItem.description = item.description || undefined; - testItem.sortText = item.sortText || undefined; - testItem.tags = item.tags.map(t => TestTag.to({ id: TestTag.denamespace(t).tagId })); - return testItem; - } - - export function toItemFromContext(context: ITestItemContext): TestItemImpl { - let node: TestItemImpl | undefined; + export function toItemFromContext(context: ITestItemContext): vscode.TestItem { + let node: vscode.TestItem | undefined; for (const test of context.tests) { - const next = to(test.item); - getPrivateApiFor(next).parent = node; + const next = toPlain(test.item); + (node as any).children = { + add: () => { }, + delete: () => { }, + forEach(fn) { fn(next, this); }, + get: id => id === test.item.extId ? test.item : undefined, + replace: () => { }, + size: 1, + } as vscode.TestItemCollection; + (next as any).parent = node; node = next; } diff --git a/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/src/vs/workbench/api/test/browser/extHostTesting.test.ts index 50083abc4a7..c4ce107acf1 100644 --- a/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -11,7 +11,7 @@ import { URI } from 'vs/base/common/uri'; import { mockObject, MockObject } from 'vs/base/test/common/mock'; import { MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; import { TestRunCoordinator, TestRunDto, TestRunProfileImpl } from 'vs/workbench/api/common/extHostTesting'; -import { ExtHostTestItemCollection, TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; +import { ExtHostTestItemCollection, TestItemImpl } from 'vs/workbench/api/common/extHostTestItem'; import * as convert from 'vs/workbench/api/common/extHostTypeConverters'; import { Location, Position, Range, TestMessage, TestResultState, TestRunProfileKind, TestRunRequest as TestRunRequestImpl, TestTag } from 'vs/workbench/api/common/extHostTypes'; import { TestDiffOpType, TestItemExpandState, TestMessageType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts index ed0f1e103cd..b4fa7d0aad1 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts @@ -158,11 +158,11 @@ export class TestItemTreeElement implements IActionableTestTreeElement { const context: ITestItemContext = { $mid: MarshalledId.TestItemContext, - tests: [this.test], + tests: [InternalTestItem.serialize(this.test)], }; for (let p = this.parent; p && p.depth > 0; p = p.parent) { - context.tests.unshift(p.test); + context.tests.unshift(InternalTestItem.serialize(p.test)); } return context; diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index 9fe7e7ca431..6f3e639ef90 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -561,7 +561,7 @@ export interface ITestItemContext { /** Marshalling marker */ $mid: MarshalledId.TestItemContext; /** Tests and parents from the root to the current items */ - tests: InternalTestItem[]; + tests: InternalTestItem.Serialized[]; } /**