diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 7adbb836a48..4ce965496fa 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2190,11 +2190,11 @@ declare module 'vscode' { */ export interface TestItem { /** - * Unique identifier for the TestItem. This is used to correlate + * Identifier for the TestItem. This is used to correlate * test results and tests in the document with those in the workspace - * (test explorer). This must not change for the lifetime of the TestItem. + * (test explorer). This cannot change for the lifetime of the TestItem, + * and must be unique among its parent's direct children. */ - // todo@API globally vs extension vs controller unique. I would strongly recommend non-global readonly id: string; /** @@ -2380,6 +2380,11 @@ declare module 'vscode' { */ readonly id: string; + /** + * Parent of this item. + */ + readonly parent?: TestResultSnapshot; + /** * URI this TestItem is associated with. May be a file or file. */ diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index aa26313dd73..4d876ab14d7 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RunOnceScheduler } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -18,11 +17,12 @@ import { generateUuid } from 'vs/base/common/uuid'; import { ExtHostTestingShape, 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 { TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; +import { TestItemImpl, TestItemRootImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; import { TestRunProfileGroup, TestRunRequest } from 'vs/workbench/api/common/extHostTypes'; -import { SingleUseTestCollection, TestPosition } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; +import { SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; import { AbstractIncrementalTestCollection, CoverageDetails, IFileCoverage, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestIdWithSrc, ITestItem, RunTestForControllerRequest, 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'; interface ControllerInfo { @@ -58,7 +58,6 @@ export class ExtHostTesting implements ExtHostTestingShape { public createTestController(controllerId: string, label: string): vscode.TestController { const disposable = new DisposableStore(); const collection = disposable.add(new SingleUseTestCollection(controllerId)); - const initialExpand = disposable.add(new RunOnceScheduler(() => collection.expand(collection.root.id, 0), 0)); const profiles = new Map(); const proxy = this.proxy; @@ -92,9 +91,6 @@ export class ExtHostTesting implements ExtHostTestingShape { }, set resolveChildrenHandler(fn) { collection.resolveHandler = fn; - if (fn) { - initialExpand.schedule(); - } }, get resolveChildrenHandler() { return collection.resolveHandler; @@ -239,7 +235,7 @@ export class ExtHostTesting implements ExtHostTestingShape { .map(id => lookup.collection.tree.get(id)) .filter(isDefined) .filter(exclude => includeTests.some( - include => collection.tree.comparePositions(include, exclude) === TestPosition.IsChild, + include => include.fullId.compare(exclude.fullId) === TestPosition.IsChild, )); if (!includeTests.length) { @@ -247,7 +243,7 @@ export class ExtHostTesting implements ExtHostTestingShape { } const publicReq = new TestRunRequest( - includeTests.map(t => t.actual), + includeTests.some(i => i.actual instanceof TestItemRootImpl) ? undefined : includeTests.map(t => t.actual), excludeTests.map(t => t.actual), profile, ); @@ -437,12 +433,15 @@ const tryGetProfileFromTestRunReq = (request: vscode.TestRunRequest) => { }; export class TestRunDto { + private readonly includePrefix: string[]; + private readonly excludePrefix: string[]; + public static fromPublic(controllerId: string, collection: SingleUseTestCollection, request: vscode.TestRunRequest) { return new TestRunDto( controllerId, generateUuid(), - request.include && new Set(request.include.map(t => t.id)), - new Set(request.exclude?.map(t => t.id) ?? Iterable.empty()), + request.include?.map(t => TestId.fromExtHostTestItem(t, controllerId).toString()) ?? [controllerId], + request.exclude?.map(t => TestId.fromExtHostTestItem(t, controllerId).toString()) ?? [], collection, ); } @@ -451,8 +450,8 @@ export class TestRunDto { return new TestRunDto( request.controllerId, request.runId, - request.testIds.includes(collection.root.id) ? undefined : new Set(request.testIds), - new Set(request.excludeExtIds), + request.testIds, + request.excludeExtIds, collection, ); } @@ -460,21 +459,29 @@ export class TestRunDto { constructor( public readonly controllerId: string, public readonly id: string, - private readonly include: ReadonlySet | undefined, - private readonly exclude: ReadonlySet, + include: string[], + exclude: string[], public readonly colllection: SingleUseTestCollection, - ) { } + ) { + this.includePrefix = include.map(id => id + TestIdPathParts.Delimiter); + this.excludePrefix = exclude.map(id => id + TestIdPathParts.Delimiter); + } public isIncluded(test: vscode.TestItem) { - for (let t: vscode.TestItem | undefined = test; t; t = t.parent) { - if (this.include?.has(t.id)) { - return true; - } else if (this.exclude.has(t.id)) { + const id = TestId.fromExtHostTestItem(test, this.controllerId).toString() + TestIdPathParts.Delimiter; + for (const prefix of this.excludePrefix) { + if (id === prefix || id.startsWith(prefix)) { return false; } } - return this.include === undefined; // default to true if running all tests with include=undefined + for (const prefix of this.includePrefix) { + if (id === prefix || id.startsWith(prefix)) { + return true; + } + } + + return false; } } @@ -574,16 +581,18 @@ class TestRunImpl implements vscode.TestRun { } setState(test: vscode.TestItem, state: vscode.TestResultState, duration?: number): void { - if (!this.#ended && this.#req.isIncluded(test)) { + const req = this.#req; + if (!this.#ended && req.isIncluded(test)) { this.ensureTestIsKnown(test); - this.#proxy.$updateTestStateInRun(this.#req.id, this.taskId, test.id, state, duration); + this.#proxy.$updateTestStateInRun(req.id, this.taskId, TestId.fromExtHostTestItem(test, req.controllerId).toString(), state, duration); } } appendMessage(test: vscode.TestItem, message: vscode.TestMessage): void { - if (!this.#ended && this.#req.isIncluded(test)) { + const req = this.#req; + if (!this.#ended && req.isIncluded(test)) { this.ensureTestIsKnown(test); - this.#proxy.$appendTestMessageInRun(this.#req.id, this.taskId, test.id, Convert.TestMessage.from(message)); + this.#proxy.$appendTestMessageInRun(req.id, this.taskId, TestId.fromExtHostTestItem(test, req.controllerId).toString(), Convert.TestMessage.from(message)); } } @@ -608,8 +617,9 @@ class TestRunImpl implements vscode.TestRun { } const chain: ITestItem[] = []; + const root = this.#req.colllection.root; while (true) { - chain.unshift(Convert.TestItem.from(test)); + chain.unshift(Convert.TestItem.from(test, root.id)); if (sent.has(test.id)) { break; @@ -623,10 +633,9 @@ class TestRunImpl implements vscode.TestRun { test = test.parent; } - const root = this.#req.colllection.root; if (!sent.has(root.id)) { sent.add(root.id); - chain.unshift(Convert.TestItem.from(root)); + chain.unshift(Convert.TestItem.from(root, root.id)); } this.#proxy.$addTestsToRun(this.#req.controllerId, this.#req.id, chain); diff --git a/src/vs/workbench/api/common/extHostTestingPrivateApi.ts b/src/vs/workbench/api/common/extHostTestingPrivateApi.ts index 523913b68a8..5e53538b97b 100644 --- a/src/vs/workbench/api/common/extHostTestingPrivateApi.ts +++ b/src/vs/workbench/api/common/extHostTestingPrivateApi.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { TestIdPathParts } from 'vs/workbench/contrib/testing/common/testId'; import * as vscode from 'vscode'; export const enum ExtHostTestItemEventOp { @@ -242,6 +243,9 @@ export class TestItemImpl implements vscode.TestItem { */ constructor(id: string, label: string, uri: vscode.Uri | undefined) { const api = getPrivateApiFor(this); + if (id.includes(TestIdPathParts.Delimiter)) { + throw new Error(`Test IDs may not include the ${JSON.stringify(id)} symbol`); + } Object.defineProperties(this, { id: { @@ -256,7 +260,9 @@ export class TestItemImpl implements vscode.TestItem { }, parent: { enumerable: false, - get() { return api.parent; }, + get() { + return api.parent instanceof TestItemRootImpl ? undefined : api.parent; + }, }, children: { value: createTestItemCollection(this), @@ -272,3 +278,9 @@ export class TestItemImpl implements vscode.TestItem { getPrivateApiFor(this).listener?.({ op: ExtHostTestItemEventOp.Invalidated }); } } + +export class TestItemRootImpl extends TestItemImpl { + constructor(controllerId: string, label: string) { + super(controllerId, label, undefined); + } +} diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 5fb6b5ac777..769a05510ad 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -32,6 +32,7 @@ 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 { 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'; import type * as vscode from 'vscode'; @@ -1658,11 +1659,11 @@ export namespace TestMessage { } export namespace TestItem { - export type Raw = vscode.TestItem; + export type Raw = vscode.TestItem; - export function from(item: vscode.TestItem): ITestItem { + export function from(item: vscode.TestItem, controllerId: string): ITestItem { return { - extId: item.id, + extId: TestId.fromExtHostTestItem(item, controllerId).toString(), label: item.label, uri: item.uri, range: Range.from(item.range) || null, @@ -1671,20 +1672,9 @@ export namespace TestItem { }; } - export function fromResultSnapshot(item: vscode.TestResultSnapshot): ITestItem { - return { - extId: item.id, - label: item.label, - uri: item.uri, - range: Range.from(item.range) || null, - description: item.description || null, - error: null, - }; - } - export function toPlain(item: ITestItem): Omit { return { - id: item.extId, + id: TestId.fromString(item.extId).localId, label: item.label, uri: URI.revive(item.uri), range: Range.to(item.range || undefined), @@ -1695,8 +1685,8 @@ export namespace TestItem { }; } - export function to(item: ITestItem): TestItemImpl { - const testItem = new TestItemImpl(item.extId, item.label, URI.revive(item.uri)); + function to(item: ITestItem): TestItemImpl { + const testItem = new TestItemImpl(TestId.fromString(item.extId).localId, item.label, URI.revive(item.uri)); testItem.range = Range.to(item.range || undefined); testItem.description = item.description || undefined; return testItem; @@ -1715,18 +1705,27 @@ export namespace TestItem { } export namespace TestResults { - const convertTestResultItem = (item: SerializedTestResultItem, byInternalId: Map): vscode.TestResultSnapshot => ({ - ...TestItem.toPlain(item.item), - taskStates: item.tasks.map(t => ({ - state: t.state, - duration: t.duration, - messages: t.messages.map(TestMessage.to), - })), - children: item.children - .map(c => byInternalId.get(c)) - .filter(isDefined) - .map(c => convertTestResultItem(c, byInternalId)), - }); + const convertTestResultItem = (item: SerializedTestResultItem, byInternalId: Map): vscode.TestResultSnapshot => { + const snapshot: vscode.TestResultSnapshot = ({ + ...TestItem.toPlain(item.item), + parent: undefined, + taskStates: item.tasks.map(t => ({ + state: t.state, + duration: t.duration, + messages: t.messages.map(TestMessage.to), + })), + children: item.children + .map(c => byInternalId.get(c)) + .filter(isDefined) + .map(c => convertTestResultItem(c, byInternalId)) + }); + + for (const child of snapshot.children) { + (child as any).parent = snapshot; + } + + return snapshot; + }; export function to(serialized: ISerializedTestResults): vscode.TestRunResult { const roots: SerializedTestResultItem[] = []; diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index f6529951ac0..b9642dc2f03 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -29,15 +29,15 @@ 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, TestIdPath, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { identifyTest, InternalTestItem, ITestIdWithSrc, ITestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService'; 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 { getPathForTestInResult, ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; +import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { getTestByPath, IMainThreadTestCollection, ITestService, testsInFile } from 'vs/workbench/contrib/testing/common/testService'; +import { expandAndGetTestById, IMainThreadTestCollection, ITestService, testsInFile } from 'vs/workbench/contrib/testing/common/testService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -660,7 +660,7 @@ export class GoToTest extends Action2 { const editorService = accessor.get(IEditorService); const { range, uri, extId } = element.test.item; - accessor.get(ITestExplorerFilterState).reveal.value = [extId]; + accessor.get(ITestExplorerFilterState).reveal.value = extId; accessor.get(ITestingPeekOpener).closeAllPeeks(); let isFile = true; @@ -710,7 +710,7 @@ export class GoToTest extends Action2 { const fileService = accessor.get(IFileService); const editorService = accessor.get(IEditorService); - accessor.get(ITestExplorerFilterState).reveal.value = [test.extId]; + accessor.get(ITestExplorerFilterState).reveal.value = test.extId; accessor.get(ITestingPeekOpener).closeAllPeeks(); let isFile = true; @@ -928,13 +928,13 @@ export class DebugCurrentFile extends ExecuteTestsInCurrentFile { } } -export const runTestsByPath = async ( +export const discoverAndRunTests = async ( collection: IMainThreadTestCollection, progress: IProgressService, - paths: ReadonlyArray, + ids: ReadonlyArray, runTests: (tests: ReadonlyArray) => Promise, ): Promise => { - const todo = Promise.all(paths.map(p => getTestByPath(collection, p))); + const todo = Promise.all(ids.map(p => expandAndGetTestById(collection, p))); const tests = (await showDiscoveringWhile(progress, todo)).filter(isDefined); return tests.length ? await runTests(tests) : undefined; }; @@ -945,7 +945,7 @@ abstract class RunOrDebugExtsByPath extends Action2 { */ public async run(accessor: ServicesAccessor, ...args: unknown[]) { const testService = accessor.get(ITestService); - await runTestsByPath( + await discoverAndRunTests( accessor.get(ITestService).collection, accessor.get(IProgressService), [...this.getTestExtIdsToRun(accessor, ...args)], @@ -953,7 +953,7 @@ abstract class RunOrDebugExtsByPath extends Action2 { ); } - protected abstract getTestExtIdsToRun(accessor: ServicesAccessor, ...args: unknown[]): Iterable; + protected abstract getTestExtIdsToRun(accessor: ServicesAccessor, ...args: unknown[]): Iterable; protected abstract runTest(service: ITestService, node: readonly InternalTestItem[]): Promise; } @@ -971,23 +971,21 @@ abstract class RunOrDebugFailedTests extends RunOrDebugExtsByPath { /** * @inheritdoc */ - protected getTestExtIdsToRun(accessor: ServicesAccessor): Iterable { + protected getTestExtIdsToRun(accessor: ServicesAccessor) { const { results } = accessor.get(ITestResultService); - const paths = new Map(); - const sep = '$$TEST SEP$$'; + const ids = new Set(); for (let i = results.length - 1; i >= 0; i--) { const resultSet = results[i]; for (const test of resultSet.tests) { - const path = getPathForTestInResult(test, resultSet).join(sep); if (isFailedState(test.ownComputedState)) { - paths.set(test.item.extId, path); + ids.add(test.item.extId); } else { - paths.delete(test.item.extId); + ids.delete(test.item.extId); } } } - return Iterable.map(paths.values(), p => p.split(sep)); + return ids; } } @@ -1008,7 +1006,7 @@ abstract class RunOrDebugLastRun extends RunOrDebugExtsByPath { /** * @inheritdoc */ - protected *getTestExtIdsToRun(accessor: ServicesAccessor, runId?: string): Iterable { + protected *getTestExtIdsToRun(accessor: ServicesAccessor, runId?: string): Iterable { const resultService = accessor.get(ITestResultService); const lastResult = runId ? resultService.results.find(r => r.id === runId) : resultService.results[0]; if (!lastResult) { @@ -1017,10 +1015,7 @@ abstract class RunOrDebugLastRun extends RunOrDebugExtsByPath { for (const test of lastResult.request.targets) { for (const testId of test.testIds) { - const test = lastResult.getStateById(testId); - if (test) { - yield getPathForTestInResult(test, lastResult); - } + yield testId; } } } @@ -1086,7 +1081,7 @@ export class ReRunLastRun extends RunOrDebugLastRun { protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise { return service.runTests({ - group: TestRunProfileBitset.Debug, + group: TestRunProfileBitset.Run, tests: internalTests.map(identifyTest), }); } diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index b4e86bcce99..441d522da6e 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -26,7 +26,7 @@ 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, TestIdPath, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { identifyTest, ITestIdWithSrc, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestProfileService, TestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService'; import { ITestingAutoRun, TestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun'; import { TestingContentProvider } from 'vs/workbench/contrib/testing/common/testingContentProvider'; @@ -37,7 +37,7 @@ import { ITestResultStorage, TestResultStorage } from 'vs/workbench/contrib/test import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { allTestActions, runTestsByPath } from './testExplorerActions'; +import { allTestActions, discoverAndRunTests } from './testExplorerActions'; import './testingConfigurationUi'; registerSingleton(ITestService, TestService, true); @@ -127,8 +127,8 @@ CommandsRegistry.registerCommand({ CommandsRegistry.registerCommand({ id: 'vscode.revealTestInExplorer', - handler: async (accessor: ServicesAccessor, pathToTest: TestIdPath) => { - accessor.get(ITestExplorerFilterState).reveal.value = pathToTest; + handler: async (accessor: ServicesAccessor, testId: string) => { + accessor.get(ITestExplorerFilterState).reveal.value = testId; accessor.get(IViewsService).openView(Testing.ExplorerViewId); } }); @@ -144,13 +144,13 @@ CommandsRegistry.registerCommand({ }); CommandsRegistry.registerCommand({ - id: 'vscode.runTestsByPath', - handler: async (accessor: ServicesAccessor, group: TestRunProfileBitset, ...pathToTests: TestIdPath[]) => { + id: 'vscode.runTestsById', + handler: async (accessor: ServicesAccessor, group: TestRunProfileBitset, ...testIds: string[]) => { const testService = accessor.get(ITestService); - await runTestsByPath( + await discoverAndRunTests( accessor.get(ITestService).collection, accessor.get(IProgressService), - pathToTests, + testIds, tests => testService.runTests({ group, tests: tests.map(identifyTest) }), ); } diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index dabf4c5f025..4e844b4539c 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -437,20 +437,8 @@ abstract class RunTestDecoration extends Disposable { })); } - testActions.push(new Action('testing.gutter.reveal', localize('reveal test', 'Reveal in Test Explorer'), undefined, undefined, async () => { - const path = [test]; - while (true) { - const parentId = path[0].parent; - const parent = parentId && collection.getNodeById(parentId); - if (!parent) { - break; - } - - path.unshift(parent); - } - - await this.commandService.executeCommand('vscode.revealTestInExplorer', path.map(t => t.item.extId)); - })); + testActions.push(new Action('testing.gutter.reveal', localize('reveal test', 'Reveal in Test Explorer'), undefined, undefined, + () => this.commandService.executeCommand('vscode.revealTestInExplorer', test.item.extId))); return testActions; } diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index 41ff97d5148..85d94133da6 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -27,19 +27,14 @@ import { testingFilterIcon } from 'vs/workbench/contrib/testing/browser/icons'; import { TestExplorerStateFilter, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { TestIdPath } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; export interface ITestExplorerFilterState { _serviceBrand: undefined; readonly text: MutableObservableValue; - /** - * Reveal request: the path to the test to reveal. The last element of the - * array is the test the user wanted to reveal, and the previous - * items are its parents. - */ - readonly reveal: MutableObservableValue; + /** Test ID the user wants to reveal in the explorer */ + readonly reveal: MutableObservableValue; readonly stateFilter: MutableObservableValue; readonly currentDocumentOnly: MutableObservableValue; /** Whether excluded test should be shown in the view */ @@ -67,7 +62,7 @@ export class TestExplorerFilterState implements ITestExplorerFilterState { }, this.storage), false); public readonly showExcludedTests = new MutableObservableValue(false); - public readonly reveal = new MutableObservableValue(undefined); + public readonly reveal = new MutableObservableValue(undefined); public readonly onDidRequestInputFocus = this.focusEmitter.event; diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 3bc95d75718..38788e48739 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -55,12 +55,13 @@ 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, TestIdPath, TestItemExpandState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { identifyTest, ITestRunProfile, TestItemExpandState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; import { capabilityContextKeys, ITestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService'; +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 { getPathForTestInResult, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; +import { TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService, testCollectionIsEmpty } from 'vs/workbench/contrib/testing/common/testService'; import { ConfigureTestProfilesAction, DebugAllAction, GoToTest, RunAllAction, SelectDefaultTestProfiles } from './testExplorerActions'; @@ -451,7 +452,7 @@ export class TestingExplorerViewModel extends Disposable { } })); - this._register(filterState.reveal.onDidChange(this.revealByIdPath, this)); + this._register(filterState.reveal.onDidChange(this.revealById, this)); this._register(onDidChangeVisibility(visible => { if (visible) { @@ -495,7 +496,7 @@ export class TestingExplorerViewModel extends Disposable { return; } - this.revealByIdPath(getPathForTestInResult(evt.item, evt.result), false, false); + this.revealById(evt.item.item.extId, false, false); })); this._register(testResults.onResultsChanged(evt => { @@ -525,8 +526,8 @@ export class TestingExplorerViewModel extends Disposable { * Tries to reveal by extension ID. Queues the request if the extension * ID is not currently available. */ - private revealByIdPath(idPath: TestIdPath | undefined, expand = true, focus = true) { - if (!idPath) { + private revealById(id: string | undefined, expand = true, focus = true) { + if (!id) { this.hasPendingReveal = false; return; } @@ -538,6 +539,7 @@ export class TestingExplorerViewModel extends Disposable { // If the item itself is visible in the tree, show it. Otherwise, expand // its closest parent. let expandToLevel = 0; + const idPath = [...TestId.fromString(id).idsFromRoot()]; for (let i = idPath.length - 1; i >= expandToLevel; i--) { const element = this.projection.value.getElementByTestId(idPath[i]); // Skip all elements that aren't in the tree. @@ -687,7 +689,7 @@ export class TestingExplorerViewModel extends Disposable { this.projection.value?.applyTo(this.tree); if (this.hasPendingReveal) { - this.revealByIdPath(this.filterState.reveal.value); + this.revealById(this.filterState.reveal.value); } } diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 94451ad8390..cc966df4966 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -64,7 +64,7 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; -import { getPathForTestInResult, ITestResult, maxCountPriority, resultItemParents, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; +import { ITestResult, maxCountPriority, resultItemParents, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService, ResultChangeEvent } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; @@ -900,10 +900,6 @@ export class TestCaseElement implements ITreeElement { return icons.testingStatesToIcons.get(this.test.computedState); } - public get path() { - return getPathForTestInResult(this.test, this.results); - } - constructor( private readonly results: ITestResult, public readonly test: TestResultItem, @@ -926,11 +922,7 @@ class TestTaskElement implements ITreeElement { public readonly label: string; public readonly icon = undefined; - public get path() { - return getPathForTestInResult(this.test, this.results); - } - - constructor(private readonly results: ITestResult, public readonly test: TestResultItem, index: number) { + constructor(results: ITestResult, public readonly test: TestResultItem, index: number) { this.id = `${results.id}/${test.item.extId}/${index}`; this.task = results.tasks[index]; this.context = String(index); @@ -1318,12 +1310,13 @@ class TreeActionsProvider { } if (element instanceof TestCaseElement || element instanceof TestTaskElement) { + const extId = element.test.item.extId; primary.push(new Action( 'testing.outputPeek.revealInExplorer', localize('testing.revealInExplorer', "Reveal in Test Explorer"), Codicon.listTree.classNames, undefined, - () => this.commandService.executeCommand('vscode.revealTestInExplorer', element.path), + () => this.commandService.executeCommand('vscode.revealTestInExplorer', extId), )); if (capabilities & TestRunProfileBitset.Run) { @@ -1332,17 +1325,17 @@ class TreeActionsProvider { localize('run test', 'Run Test'), ThemeIcon.asClassName(icons.testingRunIcon), undefined, - () => this.commandService.executeCommand('vscode.runTestsByPath', false, element.path), + () => this.commandService.executeCommand('vscode.runTestsById', TestRunProfileBitset.Run, extId), )); } - if (capabilities & TestRunProfileBitset.Coverage) { + if (capabilities & TestRunProfileBitset.Debug) { primary.push(new Action( 'testing.outputPeek.debugTest', localize('debug test', 'Debug Test'), ThemeIcon.asClassName(icons.testingDebugIcon), undefined, - () => this.commandService.executeCommand('vscode.runTestsByPath', true, element.path), + () => this.commandService.executeCommand('vscode.runTestsById', TestRunProfileBitset.Debug, extId), )); } } diff --git a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts index dc16f399a76..5590b0aa936 100644 --- a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts @@ -8,9 +8,10 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { assertNever } from 'vs/base/common/types'; -import { diffTestItems, ExtHostTestItemEvent, ExtHostTestItemEventOp, getPrivateApiFor, TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; +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 { TestId } from 'vs/workbench/contrib/testing/common/testId'; type TestItemRaw = Convert.TestItem.Raw; @@ -22,9 +23,10 @@ export interface IHierarchyProvider { * @private */ export interface OwnedCollectionTestItem { - expand: TestItemExpandState; - parent: string | null; + readonly fullId: TestId; + readonly parent: TestId | null; actual: TestItemImpl; + expand: TestItemExpandState; /** * Number of levels of items below this one that are expanded. May be infinite. */ @@ -32,118 +34,6 @@ export interface OwnedCollectionTestItem { resolveBarrier?: Barrier; } -/** - * 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 { - private readonly map = new Map(); - private readonly _roots = new Set(); - public readonly roots: ReadonlySet = 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.actual.id)) { - throw new Error(`Attempted to insert a duplicate test item ID ${test.actual.id}`); - } - - this.map.set(test.actual.id, 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 * for a workspace or document. @@ -154,15 +44,13 @@ export class SingleUseTestCollection extends Disposable { private readonly diffOpEmitter = this._register(new Emitter()); private _resolveHandler?: (item: TestItemRaw) => Promise | void; - public readonly root = new TestItemImpl(`${this.controllerId}Root`, this.controllerId, undefined); - public readonly tree = new TestTree(); + public readonly root = new TestItemRootImpl(this.controllerId, this.controllerId); + public readonly tree = new Map(); protected diff: TestsDiff = []; - constructor( - private readonly controllerId: string, - ) { + constructor(private readonly controllerId: string) { super(); - this.upsertItem(this.root, null); + this.upsertItem(this.root, undefined); } /** @@ -170,7 +58,7 @@ export class SingleUseTestCollection extends Disposable { */ public set resolveHandler(handler: undefined | ((item: TestItemRaw) => void)) { this._resolveHandler = handler; - for (const test of this.tree) { + for (const test of this.tree.values()) { this.updateExpandability(test); } } @@ -244,24 +132,23 @@ export class SingleUseTestCollection extends Disposable { } public override dispose() { - for (const item of this.tree) { + for (const item of this.tree.values()) { getPrivateApiFor(item.actual).listener = undefined; } + this.tree.clear(); this.diff = []; super.dispose(); } private onTestItemEvent(internal: OwnedCollectionTestItem, evt: ExtHostTestItemEvent) { - const extId = internal?.actual.id; - switch (evt.op) { case ExtHostTestItemEventOp.Invalidated: - this.pushDiff([TestDiffOpType.Retire, extId]); + this.pushDiff([TestDiffOpType.Retire, internal.fullId.toString()]); break; case ExtHostTestItemEventOp.RemoveChild: - this.removeItem(evt.id); + this.removeItem(TestId.joinToString(internal.fullId, evt.id)); break; case ExtHostTestItemEventOp.Upsert: @@ -276,6 +163,7 @@ export class SingleUseTestCollection extends Disposable { case ExtHostTestItemEventOp.SetProp: const { key, value } = evt; + const extId = internal.fullId.toString(); switch (key) { case 'canResolveChildren': this.updateExpandability(internal); @@ -296,35 +184,43 @@ export class SingleUseTestCollection extends Disposable { } } - private upsertItem(actual: TestItemRaw, parent: OwnedCollectionTestItem | null) { + private upsertItem(actual: TestItemRaw, parent: OwnedCollectionTestItem | undefined) { if (!(actual instanceof TestItemImpl)) { throw new Error(`TestItems provided to the VS Code API must extend \`vscode.TestItem\`, but ${actual.id} did not`); } + const fullId = TestId.fromExtHostTestItem(actual, this.root.id, parent?.actual); - // If the item already exists under a different parent, remove it. - let internal = this.tree.get(actual.id); - if (internal && internal.parent !== parent?.actual.id) { - (internal.actual.parent ?? this.root).children.delete(actual.id); - internal = undefined; + // If this test item exists elsewhere in the tree already (exists at an + // old ID with an existing parent), remove that old item. + const privateApi = getPrivateApiFor(actual); + if (privateApi.parent && privateApi.parent !== parent?.actual) { + privateApi.parent.children.delete(actual.id); } + let internal = this.tree.get(fullId.toString()); // Case 1: a brand new item if (!internal) { - const parentId = parent ? parent.actual.id : null; // always expand root node to know if there are tests (and whether to show the welcome view) const pExpandLvls = parent ? parent.expandLevels : 1; internal = { + fullId, actual, - parent: parentId, + parent: parent ? fullId.parentId : null, expandLevels: pExpandLvls /* intentionally undefined or 0 */ ? pExpandLvls - 1 : undefined, expand: TestItemExpandState.NotExpandable, // updated by `connectItemAndChildren` }; - this.tree.add(internal); + this.tree.set(internal.fullId.toString(), internal); + this.setItemParent(actual, parent); this.pushDiff([ TestDiffOpType.Add, - { parent: parentId, controllerId: this.controllerId, expand: internal.expand, item: Convert.TestItem.from(actual) }, + { + parent: internal.parent && internal.parent.toString(), + controllerId: this.controllerId, + expand: internal.expand, + item: Convert.TestItem.from(actual, this.root.id), + }, ]); this.connectItemAndChildren(actual, internal, parent); @@ -351,24 +247,27 @@ export class SingleUseTestCollection extends Disposable { this.connectItemAndChildren(actual, internal, parent); - // Remove any children still referencing the old parent that aren't - // included in the new one. Note that children might have moved to a new - // parent, so the parent ID check is done. + // Remove any orphaned children. for (const child of oldChildren) { - if (!actual.children.get(child.id) && this.tree.get(child.id)?.parent === actual.id) { - this.removeItem(child.id); + if (!actual.children.get(child.id)) { + this.removeItem(TestId.joinToString(fullId, child.id)); } } } - private connectItem(actual: TestItemImpl, internal: OwnedCollectionTestItem, parent: OwnedCollectionTestItem | null) { + private setItemParent(actual: TestItemImpl, parent: OwnedCollectionTestItem | undefined) { + getPrivateApiFor(actual).parent = parent && parent.actual !== this.root ? parent.actual : undefined; + } + + private connectItem(actual: TestItemImpl, internal: OwnedCollectionTestItem, parent: OwnedCollectionTestItem | undefined) { + this.setItemParent(actual, parent); const api = getPrivateApiFor(actual); - api.parent = parent && parent.actual !== this.root ? parent.actual : undefined; + api.parent = parent?.actual; api.listener = evt => this.onTestItemEvent(internal, evt); this.updateExpandability(internal); } - private connectItemAndChildren(actual: TestItemImpl, internal: OwnedCollectionTestItem, parent: OwnedCollectionTestItem | null) { + private connectItemAndChildren(actual: TestItemImpl, internal: OwnedCollectionTestItem, parent: OwnedCollectionTestItem | undefined) { this.connectItem(actual, internal, parent); // Discover any existing children that might have already been added @@ -401,7 +300,7 @@ export class SingleUseTestCollection extends Disposable { } internal.expand = newState; - this.pushDiff([TestDiffOpType.Update, { extId: internal.actual.id, expand: newState }]); + this.pushDiff([TestDiffOpType.Update, { extId: internal.fullId.toString(), expand: newState }]); if (newState === TestItemExpandState.Expandable && internal.expandLevels !== undefined) { this.resolveChildren(internal); @@ -419,7 +318,7 @@ export class SingleUseTestCollection extends Disposable { } const asyncChildren = internal.actual.children.all - .map(c => this.expand(c.id, levels)) + .map(c => this.expand(TestId.joinToString(internal.fullId, c.id), levels)) .filter(isThenable); if (asyncChildren.length) { @@ -467,7 +366,7 @@ export class SingleUseTestCollection extends Disposable { } private pushExpandStateUpdate(internal: OwnedCollectionTestItem) { - this.pushDiff([TestDiffOpType.Update, { extId: internal.actual.id, expand: internal.expand }]); + this.pushDiff([TestDiffOpType.Update, { extId: internal.fullId.toString(), expand: internal.expand }]); } private removeItem(childId: string) { @@ -486,9 +385,9 @@ export class SingleUseTestCollection extends Disposable { } getPrivateApiFor(item.actual).listener = undefined; - this.tree.delete(item.actual.id); + this.tree.delete(item.fullId.toString()); for (const child of item.actual.children.all) { - queue.push(this.tree.get(child.id)); + queue.push(this.tree.get(TestId.joinToString(item.fullId, child.id))); } } } diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testCollection.ts index 5bb153d8e51..e1c495b3664 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testCollection.ts @@ -50,12 +50,6 @@ export interface ITestRunProfile { hasConfigurationHandler: boolean; } -/** - * Defines the path to a test, as a list of test IDs. The last element of the - * array is the test ID, and the predecessors are its parents, in order. - */ -export type TestIdPath = string[]; - /** * A fully-resolved request to run tests, passsed between the main thread * and extension host. diff --git a/src/vs/workbench/contrib/testing/common/testId.ts b/src/vs/workbench/contrib/testing/common/testId.ts new file mode 100644 index 00000000000..1fe4d2f7b35 --- /dev/null +++ b/src/vs/workbench/contrib/testing/common/testId.ts @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const enum TestIdPathParts { + /** Delimiter for path parts in test IDs */ + Delimiter = '\0', +} + +/** + * Enum for describing relative positions of tests. Similar to + * `node.compareDocumentPosition` in the DOM. + */ +export const enum TestPosition { + /** a === b */ + IsSame, + /** 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, +} + +type TestItemLike = { id: string; parent?: TestItemLike }; + +/** + * The test ID is a stringifiable client that + */ +export class TestId { + private stringifed?: string; + + /** + * Creates a test ID from an ext host test item. + */ + public static fromExtHostTestItem(item: TestItemLike, rootId: string, parent = item.parent) { + if (item.id === rootId) { + return new TestId([rootId]); + } + + let path = [item.id]; + for (let i = parent; i && i.id !== rootId; i = i.parent) { + path.push(i.id); + } + path.push(rootId); + + return new TestId(path.reverse()); + } + + /** + * Creates a test ID from a serialized TestId instance. + */ + public static fromString(idString: string) { + return new TestId(idString.split(TestIdPathParts.Delimiter)); + } + + /** + * Gets the ID resulting from adding b to the base ID. + */ + public static join(base: TestId, b: string) { + return new TestId([...base.path, b]); + } + + /** + * Gets the string ID resulting from adding b to the base ID. + */ + public static joinToString(base: string | TestId, b: string) { + return base.toString() + TestIdPathParts.Delimiter + b; + } + + constructor( + public readonly path: readonly string[], + private readonly viewEnd = path.length, + ) { + if (path.length === 0 || viewEnd < 1) { + throw new Error('cannot create test with empty path'); + } + } + + /** + * Gets the ID of the parent test. + */ + public get parentId(): TestId { + return this.viewEnd > 1 ? new TestId(this.path, this.viewEnd - 1) : this; + } + + /** + * Gets the local ID of the current full test ID. + */ + public get localId() { + return this.path[this.viewEnd - 1]; + } + + /** + * Gets whether this ID refers to the root. + */ + public get isRoot() { + return this.viewEnd === 1; + } + + /** + * Returns an iterable that yields IDs of all parent items down to and + * including the current item. + */ + public *idsFromRoot() { + let built = this.path[0]; + yield built; + + for (let i = 1; i < this.viewEnd; i++) { + built += TestIdPathParts.Delimiter; + built += this.path[i]; + yield built; + } + } + + /** + * Compares the other test ID with this one. + */ + public compare(other: TestId) { + for (let i = 0; i < other.viewEnd && i < this.viewEnd; i++) { + if (other.path[i] !== this.path[i]) { + return TestPosition.Disconnected; + } + } + + if (other.viewEnd > this.viewEnd) { + return TestPosition.IsChild; + } + + if (other.viewEnd < this.viewEnd) { + return TestPosition.IsParent; + } + + return TestPosition.IsSame; + } + + /** + * Serializes the ID. + */ + public toJSON() { + return this.toString(); + } + + /** + * Serializes the ID to a string. + */ + public toString() { + if (!this.stringifed) { + this.stringifed = this.path[0]; + for (let i = 1; i < this.viewEnd; i++) { + this.stringifed += TestIdPathParts.Delimiter; + this.stringifed += this.path[i]; + } + } + + return this.stringifed; + } +} diff --git a/src/vs/workbench/contrib/testing/common/testResult.ts b/src/vs/workbench/contrib/testing/common/testResult.ts index ae6cc612bbd..394f5e85bbe 100644 --- a/src/vs/workbench/contrib/testing/common/testResult.ts +++ b/src/vs/workbench/contrib/testing/common/testResult.ts @@ -13,7 +13,7 @@ import { localize } from 'vs/nls'; import { TestResultState } from 'vs/workbench/api/common/extHostTypes'; import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState'; import { IObservableValue, MutableObservableValue, staticObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; -import { ISerializedTestResults, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, ResolvedTestRunRequest, TestIdPath, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ISerializedTestResults, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, ResolvedTestRunRequest, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { maxPriority, statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates'; @@ -86,15 +86,6 @@ export const resultItemParents = function* (results: ITestResult, item: TestResu } }; -export const getPathForTestInResult = (test: TestResultItem, results: ITestResult): TestIdPath => { - const path: TestIdPath = []; - for (const node of resultItemParents(results, test)) { - path.unshift(node.item.extId); - } - - return path; -}; - /** * Count of the number of tests in each run state. */ diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index 8a5dd2ac480..27058cc34d4 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -11,8 +11,9 @@ import { IDisposable } from 'vs/base/common/lifecycle'; 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, ResolvedTestRunRequest, RunTestForControllerRequest, TestIdPath, TestItemExpandState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, ITestIdWithSrc, 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'; export const ITestService = createDecorator('testService'); @@ -82,18 +83,12 @@ export const testCollectionIsEmpty = (collection: IMainThreadTestCollection) => !Iterable.some(collection.rootItems, r => r.children.size > 0); /** - * Ensures the test with the given path exists in the collection, if possible. + * Ensures the test with the given ID exists in the collection, if possible. * If cancellation is requested, or the test cannot be found, it will return * undefined. */ -export const getTestByPath = async (collection: IMainThreadTestCollection, idPath: TestIdPath, ct = CancellationToken.None) => { - // Expand all direct children since roots might well have different IDs, but - // children should start matching. - await expandFirstLevel(collection); - - if (ct.isCancellationRequested) { - return undefined; - } +export const expandAndGetTestById = async (collection: IMainThreadTestCollection, id: string, ct = CancellationToken.None) => { + const idPath = [...TestId.fromString(id).idsFromRoot()]; let expandToLevel = 0; for (let i = idPath.length - 1; !ct.isCancellationRequested && i >= expandToLevel;) { diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 7438a00c14a..5a744341853 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -155,7 +155,9 @@ 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).map(t => t.testId), + excludeExtIds: req.exclude! + .filter(t => t.controllerId === group.controllerId && !group.testIds.includes(t.testId)) + .map(t => t.testId), profileId: group.profileId, controllerId: group.controllerId, testIds: group.testIds, diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts index 9099ecbeef9..570fbe3e92d 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; import { TestDiffOpType, TestItemExpandState, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestResultState } from 'vs/workbench/contrib/testing/common/testingStates'; import { TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { Convert, TestItemImpl } from 'vs/workbench/contrib/testing/common/testStubs'; @@ -44,7 +45,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { test('expands children', async () => { harness.flush(); - harness.tree.expand(harness.projection.getElementByTestId('id-a')!); + harness.tree.expand(harness.projection.getElementByTestId(new TestId(['ctrlId', 'id-a']).toString())!); assert.deepStrictEqual(harness.flush(), [ { e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' } ]); @@ -54,10 +55,10 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { harness.flush(); harness.pushDiff([ TestDiffOpType.Add, - { controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: Convert.TestItem.from(new TestItemImpl('c', 'c', undefined)) }, + { controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: Convert.TestItem.from(new TestItemImpl('c', 'c', undefined), 'ctrl2') }, ], [ TestDiffOpType.Add, - { controllerId: 'ctrl2', parent: 'c', expand: TestItemExpandState.NotExpandable, item: Convert.TestItem.from(new TestItemImpl('c-a', 'ca', undefined)) }, + { controllerId: 'ctrl2', parent: new TestId(['ctrl2', 'c']).toString(), expand: TestItemExpandState.NotExpandable, item: Convert.TestItem.from(new TestItemImpl('c-a', 'ca', undefined), 'ctrl2') }, ]); assert.deepStrictEqual(harness.flush(), [ @@ -68,7 +69,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { test('updates nodes if they add children', async () => { harness.flush(); - harness.tree.expand(harness.projection.getElementByTestId('id-a')!); + harness.tree.expand(harness.projection.getElementByTestId(new TestId(['ctrlId', 'id-a']).toString())!); assert.deepStrictEqual(harness.flush(), [ { e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, @@ -85,7 +86,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { test('updates nodes if they remove children', async () => { harness.flush(); - harness.tree.expand(harness.projection.getElementByTestId('id-a')!); + harness.tree.expand(harness.projection.getElementByTestId(new TestId(['ctrlId', 'id-a']).toString())!); assert.deepStrictEqual(harness.flush(), [ { e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, @@ -105,7 +106,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { resultsService.getStateById = () => [undefined, resultInState(TestResultState.Failed)]; const resultInState = (state: TestResultState): TestResultItem => ({ - item: Convert.TestItem.from(harness.c.tree.get('id-a')!.actual), + item: Convert.TestItem.from(harness.c.tree.get(new TestId(['ctrlId', 'id-a']).toString())!.actual, 'ctrlId'), parent: 'id-root', tasks: [], retired: false, diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts index 5f4607c3734..91578aeddc2 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; import { TestDiffOpType, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestResultItemChange } from 'vs/workbench/contrib/testing/common/testResult'; import { Convert, TestItemImpl } from 'vs/workbench/contrib/testing/common/testStubs'; import { TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; @@ -42,10 +43,10 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { harness.flush(); harness.pushDiff([ TestDiffOpType.Add, - { controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: Convert.TestItem.from(new TestItemImpl('c', 'root2', undefined)) }, + { controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: Convert.TestItem.from(new TestItemImpl('c', 'root2', undefined), 'ctrl2') }, ], [ TestDiffOpType.Add, - { controllerId: 'ctrl2', parent: 'c', expand: TestItemExpandState.NotExpandable, item: Convert.TestItem.from(new TestItemImpl('c-a', 'c', undefined)) }, + { controllerId: 'ctrl2', parent: new TestId(['ctrl2', 'c']).toString(), expand: TestItemExpandState.NotExpandable, item: Convert.TestItem.from(new TestItemImpl('c-a', 'c', undefined), 'ctrl2') }, ]); assert.deepStrictEqual(harness.flush(), [ diff --git a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts index 86b878394b9..209ea14cbd3 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts @@ -12,8 +12,9 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; import { ITestTaskState, ResolvedTestRunRequest, TestResultItem, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestResultState } from 'vs/workbench/contrib/testing/common/testingStates'; -import { getPathForTestInResult, HydratedTestResult, LiveOutputController, LiveTestResult, makeEmptyCounts, resultItemParents, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; +import { HydratedTestResult, LiveOutputController, LiveTestResult, makeEmptyCounts, resultItemParents, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { TestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { InMemoryResultStorage, ITestResultStorage } from 'vs/workbench/contrib/testing/common/testResultStorage'; import { Convert, getInitializedMainTestCollection, testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; @@ -63,15 +64,15 @@ suite('Workbench - Test Results Service', () => { tests = testStubs.nested(); await tests.expand(tests.root.id, Infinity); - r.addTestChainToRun('ctrl', [ - Convert.TestItem.from(tests.root), - Convert.TestItem.from(tests.root.children.get('id-a')!), - Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-aa')!), + r.addTestChainToRun('ctrlId', [ + Convert.TestItem.from(tests.root, 'ctrlId'), + Convert.TestItem.from(tests.root.children.get('id-a')!, 'ctrlId'), + Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-aa')!, 'ctrlId'), ]); - r.addTestChainToRun('ctrl', [ - Convert.TestItem.from(tests.root.children.get('id-a')!), - Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-ab')!), + r.addTestChainToRun('ctrlId', [ + Convert.TestItem.from(tests.root.children.get('id-a')!, 'ctrlId'), + Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-ab')!, 'ctrlId'), ]); }); @@ -122,8 +123,8 @@ suite('Workbench - Test Results Service', () => { [TestResultState.Failed]: 3, }); - assert.deepStrictEqual(r.getStateById('id-a')?.ownComputedState, TestResultState.Failed); - assert.deepStrictEqual(r.getStateById('id-a')?.tasks[0].state, TestResultState.Failed); + assert.deepStrictEqual(r.getStateById(new TestId(['ctrlId', 'id-a']).toString())?.ownComputedState, TestResultState.Failed); + assert.deepStrictEqual(r.getStateById(new TestId(['ctrlId', 'id-a']).toString())?.tasks[0].state, TestResultState.Failed); assert.deepStrictEqual(getChangeSummary(), [ { label: 'a', reason: TestResultItemChangeReason.OwnStateChange }, { label: 'aa', reason: TestResultItemChangeReason.OwnStateChange }, @@ -134,14 +135,14 @@ suite('Workbench - Test Results Service', () => { test('updateState', () => { changed.clear(); - r.updateState('id-aa', 't', TestResultState.Running); + r.updateState(new TestId(['ctrlId', 'id-a', 'id-aa']).toString(), 't', TestResultState.Running); assert.deepStrictEqual(r.counts, { ...makeEmptyCounts(), [TestResultState.Unset]: 2, [TestResultState.Running]: 1, [TestResultState.Queued]: 1, }); - assert.deepStrictEqual(r.getStateById('id-aa')?.ownComputedState, TestResultState.Running); + assert.deepStrictEqual(r.getStateById(new TestId(['ctrlId', 'id-a', 'id-aa']).toString())?.ownComputedState, TestResultState.Running); // update computed state: assert.deepStrictEqual(r.getStateById(tests.root.id)?.computedState, TestResultState.Running); assert.deepStrictEqual(getChangeSummary(), [ @@ -153,7 +154,7 @@ suite('Workbench - Test Results Service', () => { test('retire', () => { changed.clear(); - r.retire('id-a'); + r.retire(new TestId(['ctrlId', 'id-a']).toString()); assert.deepStrictEqual(getChangeSummary(), [ { label: 'a', reason: TestResultItemChangeReason.Retired }, { label: 'aa', reason: TestResultItemChangeReason.ParentRetired }, @@ -161,24 +162,24 @@ suite('Workbench - Test Results Service', () => { ]); changed.clear(); - r.retire('id-a'); + r.retire(new TestId(['ctrlId', 'id-a']).toString()); assert.strictEqual(changed.size, 0); }); test('ignores outside run', () => { changed.clear(); - r.updateState('id-b', 't', TestResultState.Running); + r.updateState(new TestId(['ctrlId', 'id-b']).toString(), 't', TestResultState.Running); assert.deepStrictEqual(r.counts, { ...makeEmptyCounts(), [TestResultState.Queued]: 2, [TestResultState.Unset]: 2, }); - assert.deepStrictEqual(r.getStateById('id-b'), undefined); + assert.deepStrictEqual(r.getStateById(new TestId(['ctrlId', 'id-b']).toString()), undefined); }); test('markComplete', () => { r.setAllToState(TestResultState.Queued, 't', () => true); - r.updateState('id-aa', 't', TestResultState.Passed); + r.updateState(new TestId(['ctrlId', 'id-a', 'id-aa']).toString(), 't', TestResultState.Passed); changed.clear(); r.markComplete(); @@ -190,7 +191,7 @@ suite('Workbench - Test Results Service', () => { }); assert.deepStrictEqual(r.getStateById(tests.root.id)?.ownComputedState, TestResultState.Unset); - assert.deepStrictEqual(r.getStateById('id-aa')?.ownComputedState, TestResultState.Passed); + assert.deepStrictEqual(r.getStateById(new TestId(['ctrlId', 'id-a', 'id-aa']).toString())?.ownComputedState, TestResultState.Passed); }); }); @@ -214,7 +215,7 @@ suite('Workbench - Test Results Service', () => { test('serializes and re-hydrates', async () => { results.push(r); - r.updateState('id-aa', 't', TestResultState.Passed); + r.updateState(new TestId(['ctrlId', 'id-a', 'id-aa']).toString(), 't', TestResultState.Passed); r.markComplete(); await timeout(10); // allow persistImmediately async to happen @@ -235,7 +236,7 @@ suite('Workbench - Test Results Service', () => { delete expected.item.description; expected.item.uri = actual.item.uri; - assert.deepStrictEqual(actual, { ...expected, src: undefined, retired: true, children: ['id-a'] }); + assert.deepStrictEqual(actual, { ...expected, src: undefined, retired: true, children: [new TestId(['ctrlId', 'id-a']).toString()] }); assert.deepStrictEqual(rehydrated.counts, r.counts); assert.strictEqual(typeof rehydrated.completedAt, 'number'); }); @@ -278,7 +279,7 @@ suite('Workbench - Test Results Service', () => { name: 'hello world', request: defaultOpts([]), items: [{ - ...(await getInitializedMainTestCollection()).getNodeById('id-a')!, + ...(await getInitializedMainTestCollection()).getNodeById(new TestId(['ctrlId', 'id-a']).toString())!, tasks: [{ state, duration: 0, messages: [] }], computedState: state, ownComputedState: state, @@ -312,22 +313,14 @@ suite('Workbench - Test Results Service', () => { }); test('resultItemParents', () => { - assert.deepStrictEqual([...resultItemParents(r, r.getStateById('id-aa')!)], [ - r.getStateById('id-aa'), - r.getStateById('id-a'), - r.getStateById(tests.root.id), + assert.deepStrictEqual([...resultItemParents(r, r.getStateById(new TestId(['ctrlId', 'id-a', 'id-aa']).toString())!)], [ + r.getStateById(new TestId(['ctrlId', 'id-a', 'id-aa']).toString()), + r.getStateById(new TestId(['ctrlId', 'id-a']).toString()), + r.getStateById(new TestId(['ctrlId']).toString()), ]); assert.deepStrictEqual([...resultItemParents(r, r.getStateById(tests.root.id)!)], [ r.getStateById(tests.root.id), ]); }); - - test('getPathForTestInResult', () => { - assert.deepStrictEqual([...getPathForTestInResult(r.getStateById('id-aa')!, r)], [ - tests.root.id, - 'id-a', - 'id-aa', - ]); - }); }); diff --git a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts index 56a49f73aa8..67d41de0bb6 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { range } from 'vs/base/common/arrays'; import { NullLogService } from 'vs/platform/log/common/log'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { InMemoryResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage'; import { Convert, testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; @@ -26,14 +27,14 @@ suite('Workbench - Test Result Storage', () => { t.addTask({ id: 't', name: undefined, running: true }); const tests = testStubs.nested(); tests.expand(tests.root.id, Infinity); - t.addTestChainToRun('ctrl', [ - Convert.TestItem.from(tests.root), - Convert.TestItem.from(tests.root.children.get('id-a')!), - Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-aa')!), + t.addTestChainToRun('ctrlId', [ + Convert.TestItem.from(tests.root, 'ctrlId'), + Convert.TestItem.from(tests.root.children.get('id-a')!, 'ctrlId'), + Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-aa')!, 'ctrlId'), ]); if (addMessage) { - t.appendMessage('id-a', 't', { + t.appendMessage(new TestId(['ctrlId', 'id-a']).toString(), 't', { message: addMessage, actualOutput: undefined, expectedOutput: undefined, @@ -75,7 +76,8 @@ suite('Workbench - Test Result Storage', () => { test('limits stored result by budget', async () => { const r = range(100).map(() => makeResult('a'.repeat(2048))); await storage.persist(r); - assert.strictEqual(true, (await storage.read()).length < 50); + const length = (await storage.read()).length; + assert.strictEqual(true, length < 50); }); test('always stores the min number of results', async () => { diff --git a/src/vs/workbench/test/browser/api/extHostTesting.test.ts b/src/vs/workbench/test/browser/api/extHostTesting.test.ts index 5b86cac39ba..213420a76a1 100644 --- a/src/vs/workbench/test/browser/api/extHostTesting.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTesting.test.ts @@ -13,6 +13,7 @@ import { TestRunProfileImpl, TestRunCoordinator, TestRunDto } from 'vs/workbench import * as convert from 'vs/workbench/api/common/extHostTypeConverters'; import { TestMessage, TestResultState, TestRunProfileGroup } from 'vs/workbench/api/common/extHostTypes'; import { TestDiffOpType, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestItemImpl, testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; import { TestSingleUseCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; import type { TestItem, TestRunRequest } from 'vscode'; @@ -73,18 +74,20 @@ suite('ExtHost Testing', () => { suite('OwnedTestCollection', () => { test('adds a root recursively', async () => { await single.expand(single.root.id, Infinity); + const a = single.root.children.get('id-a')!; + const b = single.root.children.get('id-b')!; assert.deepStrictEqual(single.collectDiff(), [ [ TestDiffOpType.Add, - { controllerId: 'ctrlId', parent: null, expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(single.root) } } + { controllerId: 'ctrlId', parent: null, expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(single.root, 'ctrlId') } } ], [ TestDiffOpType.Add, - { controllerId: 'ctrlId', parent: single.root.id, expand: TestItemExpandState.Expandable, item: { ...convert.TestItem.from(single.tree.get('id-a')!.actual) } } + { controllerId: 'ctrlId', parent: single.root.id, expand: TestItemExpandState.Expandable, item: { ...convert.TestItem.from(a, 'ctrlId') } } ], [ TestDiffOpType.Add, - { controllerId: 'ctrlId', parent: single.root.id, expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(single.tree.get('id-b')!.actual) } + { controllerId: 'ctrlId', parent: single.root.id, expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(b, 'ctrlId') } ], [ TestDiffOpType.Update, @@ -92,19 +95,19 @@ suite('ExtHost Testing', () => { ], [ TestDiffOpType.Update, - { extId: 'id-a', expand: TestItemExpandState.BusyExpanding } + { extId: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.BusyExpanding } ], [ TestDiffOpType.Add, - { controllerId: 'ctrlId', parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(single.tree.get('id-aa')!.actual) } + { controllerId: 'ctrlId', parent: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(a.children.get('id-aa')!, 'ctrlId') } ], [ TestDiffOpType.Add, - { controllerId: 'ctrlId', parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(single.tree.get('id-ab')!.actual) } + { controllerId: 'ctrlId', parent: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(a.children.get('id-ab')!, 'ctrlId') } ], [ TestDiffOpType.Update, - { extId: 'id-a', expand: TestItemExpandState.Expanded } + { extId: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.Expanded } ], ]); }); @@ -132,7 +135,7 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ [ TestDiffOpType.Update, - { extId: 'id-a', item: { description: 'Hello world' } }], + { extId: new TestId(['ctrlId', 'id-a']).toString(), item: { description: 'Hello world' } }], ]); }); @@ -142,9 +145,12 @@ suite('ExtHost Testing', () => { single.root.children.delete('id-a'); assert.deepStrictEqual(single.collectDiff(), [ - [TestDiffOpType.Remove, 'id-a'], + [TestDiffOpType.Remove, new TestId(['ctrlId', 'id-a']).toString()], ]); - assert.deepStrictEqual([...single.tree].map(n => n.actual.id).sort(), [single.root.id, 'id-b']); + assert.deepStrictEqual( + [...single.tree.keys()].sort(), + [single.root.id, new TestId(['ctrlId', 'id-b']).toString()], + ); assert.strictEqual(single.tree.size, 2); }); @@ -157,13 +163,13 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ [TestDiffOpType.Add, { controllerId: 'ctrlId', - parent: 'id-a', + parent: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.NotExpandable, - item: convert.TestItem.from(child), + item: convert.TestItem.from(child, 'ctrlId'), }], ]); assert.deepStrictEqual( - [...single.tree].map(n => n.actual.id).sort(), + [...single.tree.values()].map(n => n.actual.id).sort(), [single.root.id, 'id-a', 'id-aa', 'id-ab', 'id-ac', 'id-b'], ); assert.strictEqual(single.tree.size, 6); @@ -184,7 +190,7 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ [ TestDiffOpType.Update, - { extId: 'id-a', expand: TestItemExpandState.Expanded, item: { label: 'Hello world' } }, + { extId: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.Expanded, item: { label: 'Hello world' } }, ], ]); @@ -192,7 +198,7 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ [ TestDiffOpType.Update, - { extId: 'id-a', item: { label: 'still connected' } } + { extId: new TestId(['ctrlId', 'id-a']).toString(), item: { label: 'still connected' } } ], ]); @@ -215,11 +221,11 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ [ TestDiffOpType.Update, - { extId: 'id-a', expand: TestItemExpandState.Expanded }, + { extId: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.Expanded }, ], [ TestDiffOpType.Update, - { extId: 'id-ab', item: { label: 'Hello world' } }, + { extId: TestId.fromExtHostTestItem(oldAB, 'ctrlId').toString(), item: { label: 'Hello world' } }, ], ]); @@ -229,11 +235,11 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ [ TestDiffOpType.Update, - { extId: 'id-aa', item: { label: 'still connected1' } } + { extId: new TestId(['ctrlId', 'id-a', 'id-aa']).toString(), item: { label: 'still connected1' } } ], [ TestDiffOpType.Update, - { extId: 'id-ab', item: { label: 'still connected2' } } + { extId: new TestId(['ctrlId', 'id-a', 'id-ab']).toString(), item: { label: 'still connected2' } } ], ]); @@ -250,11 +256,11 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ [ TestDiffOpType.Remove, - 'id-b', + new TestId(['ctrlId', 'id-b']).toString(), ], [ TestDiffOpType.Add, - { controllerId: 'ctrlId', parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(single.tree.get('id-b')!.actual) } + { controllerId: 'ctrlId', parent: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(b, 'ctrlId') } ], ]); @@ -262,7 +268,7 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ [ TestDiffOpType.Update, - { extId: 'id-b', item: { label: 'still connected' } } + { extId: new TestId(['ctrlId', 'id-a', 'id-b']).toString(), item: { label: 'still connected' } } ], ]); @@ -469,7 +475,7 @@ suite('ExtHost Testing', () => { assert.strictEqual(tracker.isRunning, true); assert.deepStrictEqual(proxy.$startedExtensionTestRun.args, [ [{ - config: { group: 2, id: 42 }, + profile: { group: 2, id: 42 }, controllerId: 'ctrl', id: tracker.id, include: [single.root.id], @@ -504,9 +510,9 @@ suite('ExtHost Testing', () => { 'ctrl', tracker.id, [ - convert.TestItem.from(single.root), - convert.TestItem.from(single.root.children.get('id-a')!), - convert.TestItem.from(single.root.children.get('id-a')!.children.get('id-aa')!), + convert.TestItem.from(single.root, 'ctrlId'), + convert.TestItem.from(single.root.children.get('id-a')!, 'ctrlId'), + convert.TestItem.from(single.root.children.get('id-a')!.children.get('id-aa')!, 'ctrlId'), ] ]); assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs); @@ -517,8 +523,8 @@ suite('ExtHost Testing', () => { 'ctrl', tracker.id, [ - convert.TestItem.from(single.root.children.get('id-a')!), - convert.TestItem.from(single.root.children.get('id-a')!.children.get('id-ab')!), + convert.TestItem.from(single.root.children.get('id-a')!, 'ctrlId'), + convert.TestItem.from(single.root.children.get('id-a')!.children.get('id-ab')!, 'ctrlId'), ], ]); assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs); @@ -543,13 +549,12 @@ suite('ExtHost Testing', () => { test('excludes tests outside tree or explicitly excluded', () => { single.expand(single.root.id, Infinity); - const task = c.createTestRun('ctrl', single, { + const task = c.createTestRun('ctrlId', single, { profile: configuration, include: [single.root.children.get('id-a')!], exclude: [single.root.children.get('id-a')!.children.get('id-aa')!], }, 'hello world', false); - task.setState(single.root.children.get('b')!, TestResultState.Passed); task.setState(single.root.children.get('id-a')!.children.get('id-aa')!, TestResultState.Passed); task.setState(single.root.children.get('id-a')!.children.get('id-ab')!, TestResultState.Passed); @@ -558,7 +563,7 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(proxy.$updateTestStateInRun.args, [[ args[0], args[1], - 'id-ab', + new TestId(['ctrlId', 'id-a', 'id-ab']).toString(), TestResultState.Passed, undefined, ]]);