From 352d3a6ee5d5dfd3dff95dc0a4880ef11dc1690a Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 3 Apr 2023 17:48:56 -0700 Subject: [PATCH] testing: add commands to get current selected tests/profiles Fixes #179065 --- src/vs/workbench/api/common/extHostTesting.ts | 19 ++++++++ .../testing/browser/testExplorerActions.ts | 47 ++++++++++++++++++- .../testing/browser/testingExplorerView.ts | 39 ++++++++++++--- .../contrib/testing/common/constants.ts | 2 + .../contrib/testing/common/testTypes.ts | 7 +++ 5 files changed, 106 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index a9029bcb3ec99..ab1ffe90cea81 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -23,6 +23,7 @@ import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostTestItemCollection, TestItemImpl, TestItemRootImpl, toItemFromContext } 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 { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestItem, ITestItemContext, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; @@ -65,6 +66,24 @@ export class ExtHostTesting implements ExtHostTestingShape { return controller?.collection.tree.get(targetTest)?.actual ?? toItemFromContext(arg); } }); + + commands.registerCommand(false, 'testing.getExplorerSelection', async (): Promise => { + const inner = await commands.executeCommand<{ + include: string[]; + exclude: string[]; + }>(TestCommandId.GetExplorerSelection); + + const lookup = (i: string) => { + const controller = this.controllers.get(TestId.root(i)); + if (!controller) { return undefined; } + return TestId.isRoot(i) ? controller.controller : controller.collection.tree.get(i)?.actual; + }; + + return { + include: inner?.include.map(lookup).filter(isDefined) || [], + exclude: inner?.exclude.map(lookup).filter(isDefined) || [], + }; + }); } /** diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index f75eb59d0cf4c..37c160ba36ed0 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -38,7 +38,7 @@ import { ITestProfileService, canUseProfileWithTest } from 'vs/workbench/contrib import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { IMainThreadTestCollection, ITestService, expandAndGetTestById, testsInFile } from 'vs/workbench/contrib/testing/common/testService'; -import { ITestRunProfile, InternalTestItem, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; +import { ExtTestRunProfileKind, ITestRunProfile, InternalTestItem, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingContinuousRunService } from 'vs/workbench/contrib/testing/common/testingContinuousRunService'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; @@ -417,11 +417,52 @@ abstract class ExecuteSelectedAction extends ViewAction { * @override */ public runInView(accessor: ServicesAccessor, view: TestingExplorerView): Promise { - const { include, exclude } = view.getSelectedOrVisibleItems(); + const { include, exclude } = view.getTreeIncludeExclude(); return accessor.get(ITestService).runTests({ tests: include, exclude, group: this.group }); } } +export class GetSelectedProfiles extends Action2 { + constructor() { + super({ id: TestCommandId.GetSelectedProfiles, title: localize('getSelectedProfiles', 'Get Selected Profiles') }); + } + + /** + * @override + */ + public override run(accessor: ServicesAccessor) { + const profiles = accessor.get(ITestProfileService); + return [ + ...profiles.getGroupDefaultProfiles(TestRunProfileBitset.Run), + ...profiles.getGroupDefaultProfiles(TestRunProfileBitset.Debug), + ...profiles.getGroupDefaultProfiles(TestRunProfileBitset.Coverage), + ].map(p => ({ + controllerId: p.controllerId, + label: p.label, + kind: p.group & TestRunProfileBitset.Coverage + ? ExtTestRunProfileKind.Coverage + : p.group & TestRunProfileBitset.Debug + ? ExtTestRunProfileKind.Debug + : ExtTestRunProfileKind.Run, + })); + } +} + +export class GetExplorerSelection extends ViewAction { + constructor() { + super({ id: TestCommandId.GetExplorerSelection, title: localize('getExplorerSelection', 'Get Explorer Selection'), viewId: Testing.ExplorerViewId }); + } + + /** + * @override + */ + public override runInView(_accessor: ServicesAccessor, view: TestingExplorerView) { + const { include, exclude } = view.getTreeIncludeExclude(undefined, 'selected'); + const mapper = (i: InternalTestItem) => i.item.extId; + return { include: include.map(mapper), exclude: exclude.map(mapper) }; + } +} + export class RunSelectedAction extends ExecuteSelectedAction { constructor() { super({ @@ -1334,6 +1375,8 @@ export const allTestActions = [ DebugLastRun, DebugSelectedAction, GoToTest, + GetExplorerSelection, + GetSelectedProfiles, HideTestAction, OpenOutputPeek, RefreshTestsAction, diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 18ebbef4bca07..d16820415b2d1 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -135,7 +135,11 @@ export class TestingExplorerView extends ViewPane { } } - public getSelectedOrVisibleItems(profile?: ITestRunProfile) { + /** + * Gets include/exclude items in the tree, based either on visible tests + * or a use selection. + */ + public getTreeIncludeExclude(profile?: ITestRunProfile, filterToType: 'visible' | 'selected' = 'visible') { const projection = this.viewModel.projection.value; if (!projection) { return { include: [], exclude: [] }; @@ -150,7 +154,7 @@ export class TestingExplorerView extends ViewPane { // To calculate includes and excludes, we include the first children that // have a majority of their items included too, and then apply exclusions. - const include: InternalTestItem[] = []; + const include = new Set(); const exclude: InternalTestItem[] = []; const attempt = (element: TestExplorerTreeElement, alreadyIncluded: boolean) => { @@ -180,7 +184,7 @@ export class TestingExplorerView extends ViewPane { // probably fans out later. (Worse case we'll directly include its single child) && inTree.visibleChildrenCount !== 1 ) { - include.push(element.test); + include.add(element.test); alreadyIncluded = true; } @@ -190,6 +194,29 @@ export class TestingExplorerView extends ViewPane { } }; + if (filterToType === 'selected') { + const sel = this.viewModel.tree.getSelection().filter(isDefined); + if (sel.length) { + + L: + for (const node of sel) { + if (node instanceof TestItemTreeElement) { + // avoid adding an item if its parent is already included + for (let i: TestItemTreeElement | null = node; i; i = i.parent) { + if (include.has(i.test)) { + continue L; + } + } + + include.add(node.test); + node.children.forEach(c => attempt(c, true)); + } + } + + return { include: [...include], exclude }; + } + } + for (const root of this.testService.collection.rootItems) { const element = projection.getElementByTestId(root.item.extId); if (!element) { @@ -208,7 +235,7 @@ export class TestingExplorerView extends ViewPane { // note we intentionally check children > 0 here, unlike above, since // we don't want to bother dispatching to controllers who have no discovered tests if (element.children.size > 0 && visibleChildren * 2 >= element.children.size) { - include.push(element.test); + include.add(element.test); element.children.forEach(c => attempt(c, true)); } else { element.children.forEach(c => attempt(c, false)); @@ -218,7 +245,7 @@ export class TestingExplorerView extends ViewPane { } } - return { include, exclude }; + return { include: [...include], exclude }; } /** @@ -294,7 +321,7 @@ export class TestingExplorerView extends ViewPane { undefined, undefined, () => { - const { include, exclude } = this.getSelectedOrVisibleItems(profile); + const { include, exclude } = this.getTreeIncludeExclude(profile); this.testService.runResolvedTests({ exclude: exclude.map(e => e.item.extId), targets: [{ diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index 542dbae955145..66c65ab4816c6 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -64,6 +64,8 @@ export const enum TestCommandId { DebugLastRun = 'testing.debugLastRun', DebugSelectedAction = 'testing.debugSelected', FilterAction = 'workbench.actions.treeView.testExplorer.filter', + GetExplorerSelection = '_testing.getExplorerSelection', + GetSelectedProfiles = 'testing.getSelectedProfiles', GoToTest = 'testing.editFocusedTest', HideTestAction = 'testing.hideTest', OpenOutputPeek = 'testing.openOutputPeek', diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index 9d9db6f50b479..a740cd40d8a63 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -20,6 +20,13 @@ export const enum TestResultState { Errored = 6 } +/** note: keep in sync with TestRunProfileKind in vscode.d.ts */ +export const enum ExtTestRunProfileKind { + Run = 1, + Debug = 2, + Coverage = 3, +} + export const enum TestRunProfileBitset { Run = 1 << 1, Debug = 1 << 2,