From 7a034baa9237a322bbf49f6f7f869e1c140f583a Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 10 Aug 2021 15:45:37 -0700 Subject: [PATCH] testing: run what you see Fixes #130522 --- .../api/common/extHostTypeConverters.ts | 2 +- .../testing/browser/testExplorerActions.ts | 85 +++++------- .../testing/browser/testingExplorerView.ts | 128 ++++++++++++++---- 3 files changed, 136 insertions(+), 79 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index d977e9e2417..6e5a72cab22 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1700,7 +1700,7 @@ export namespace TestItem { id: TestId.fromString(item.extId).localId, label: item.label, uri: URI.revive(item.uri), - tags: item.tags.map(t => { + tags: (item.tags || []).map(t => { const { tagId } = TestTag.denamespace(t); return new types.TestTag(tagId, tagId); }), diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index fe524505d9d..168cb9640e4 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -18,7 +18,6 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { FocusedViewContext } from 'vs/workbench/common/views'; @@ -27,7 +26,7 @@ import { REVEAL_IN_EXPLORER_COMMAND_ID } from 'vs/workbench/contrib/files/browse import { IActionableTestTreeElement, TestItemTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import * as icons from 'vs/workbench/contrib/testing/browser/icons'; import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; -import type { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; +import type { TestingExplorerView } 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 { InternalTestItem, ITestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; @@ -263,15 +262,25 @@ export class ConfigureTestProfilesAction extends Action2 { } abstract class ExecuteSelectedAction extends ViewAction { - constructor(id: string, title: string, icon: ThemeIcon, private readonly group: TestRunProfileBitset) { + constructor(options: IAction2Options, private readonly group: TestRunProfileBitset) { super({ - id, - title, - icon, - viewId: Testing.ExplorerViewId, - f1: true, + ...options, + menu: [{ + id: MenuId.ViewTitle, + order: group === TestRunProfileBitset.Run + ? ActionOrder.Run + : group === TestRunProfileBitset.Debug + ? ActionOrder.Debug + : ActionOrder.Coverage, + group: 'navigation', + when: ContextKeyAndExpr.create([ + ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId), + TestingContextKeys.isRunning.isEqualTo(false), + TestingContextKeys.capabilityToContextKey[group].isEqualTo(true), + ]) + }], category, - precondition: FocusedViewContext.isEqualTo(Testing.ExplorerViewId), + viewId: Testing.ExplorerViewId, }); } @@ -279,26 +288,8 @@ abstract class ExecuteSelectedAction extends ViewAction { * @override */ public runInView(accessor: ServicesAccessor, view: TestingExplorerView): Promise { - const tests = this.getActionableTests(accessor.get(ITestService), view.viewModel); - if (!tests.length) { - return Promise.resolve(undefined); - } - - return accessor.get(ITestService).runTests({ tests, group: this.group }); - } - - private getActionableTests(testService: ITestService, viewModel: TestingExplorerViewModel) { - const selected = viewModel.getSelectedTests(); - let tests: InternalTestItem[]; - if (!selected.length) { - tests = [...testService.collection.rootItems]; - } else { - tests = selected - .map(treeElement => treeElement instanceof TestItemTreeElement ? treeElement.test : undefined) - .filter(isDefined); - } - - return tests; + const { include, exclude } = view.getSelectedOrVisibleItems(); + return accessor.get(ITestService).runTests({ tests: include, exclude, group: this.group }); } } @@ -306,24 +297,23 @@ export class RunSelectedAction extends ExecuteSelectedAction { public static readonly ID = 'testing.runSelected'; constructor() { - super( - RunSelectedAction.ID, - localize('runSelectedTests', 'Run Selected Tests'), - icons.testingRunIcon, - TestRunProfileBitset.Run, - ); + super({ + id: RunSelectedAction.ID, + title: localize('runSelectedTests', 'Run Tests'), + icon: icons.testingRunAllIcon, + }, TestRunProfileBitset.Run); } } export class DebugSelectedAction extends ExecuteSelectedAction { public static readonly ID = 'testing.debugSelected'; constructor() { - super( - DebugSelectedAction.ID, - localize('debugSelectedTests', 'Debug Selected Tests'), - icons.testingDebugIcon, - TestRunProfileBitset.Debug, - ); + + super({ + id: DebugSelectedAction.ID, + title: localize('debugSelectedTests', 'Debug Tests'), + icon: icons.testingDebugAllIcon, + }, TestRunProfileBitset.Debug); } } @@ -343,19 +333,6 @@ abstract class RunOrDebugAllTestsAction extends Action2 { ...options, category, menu: [{ - id: MenuId.ViewTitle, - order: group === TestRunProfileBitset.Run - ? ActionOrder.Run - : group === TestRunProfileBitset.Debug - ? ActionOrder.Debug - : ActionOrder.Coverage, - group: 'navigation', - when: ContextKeyAndExpr.create([ - ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId), - TestingContextKeys.isRunning.isEqualTo(false), - TestingContextKeys.capabilityToContextKey[group].isEqualTo(true), - ]) - }, { id: MenuId.CommandPalette, when: TestingContextKeys.capabilityToContextKey[group].isEqualTo(true), }] diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 76d24653a02..ae5e53b9a14 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -48,7 +48,7 @@ import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; -import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; +import { ByNameTestItemElement, HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; import { ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay'; import * as icons from 'vs/workbench/contrib/testing/browser/icons'; @@ -56,17 +56,17 @@ 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 { ITestRunProfile, TestItemExpandState, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, ITestRunProfile, TestItemExpandState, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { cmpPriority, isFailedState, isStateWithResult } from 'vs/workbench/contrib/testing/common/testingStates'; -import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; +import { canUseProfileWithTest, ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { IMainThreadTestCollection, ITestService, testCollectionIsEmpty } from 'vs/workbench/contrib/testing/common/testService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ConfigureTestProfilesAction, DebugAllAction, RunAllAction, SelectDefaultTestProfiles } from './testExplorerActions'; +import { ConfigureTestProfilesAction, DebugSelectedAction, RunSelectedAction, SelectDefaultTestProfiles } from './testExplorerActions'; export class TestingExplorerView extends ViewPane { public viewModel!: TestingExplorerViewModel; @@ -117,6 +117,92 @@ export class TestingExplorerView extends ViewPane { return this.viewModel?.welcomeExperience === WelcomeExperience.ForWorkspace ?? true; } + public getSelectedOrVisibleItems(profile?: ITestRunProfile) { + const projection = this.viewModel.projection.value; + if (!projection) { + return { include: [], exclude: [] }; + } + + if (projection instanceof ByNameTestItemElement) { + return { + include: [...this.testService.collection.rootItems], + exclude: [], + }; + } + + // 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 exclude: InternalTestItem[] = []; + + const attempt = (element: TestExplorerTreeElement, alreadyIncluded: boolean) => { + // sanity check hasElement since updates are debounced and they may exist + // but not be rendered yet + if (!(element instanceof TestItemTreeElement) || !this.viewModel.tree.hasElement(element)) { + return; + } + + // If the current node is not visible or runnable in the current profile, it's excluded + const inTree = this.viewModel.tree.getNode(element); + if (!inTree.visible) { + if (alreadyIncluded) { exclude.push(element.test); } + return; + } + + // If it's not already included but most of its children are, then add it + // if it can be run under the current profile (when specified) + if ( + // If it's not already included... + !alreadyIncluded + // And it can be run using the current profile (if any) + && (!profile || canUseProfileWithTest(profile, element.test)) + // And either it's a leaf node or most children are included, the include it. + && (inTree.children.length === 0 || inTree.visibleChildrenCount * 2 >= inTree.children.length) + // And not if we're only showing a single of its children, since it + // probably fans out later. (Worse case we'll directly include its single child) + && inTree.visibleChildrenCount !== 1 + ) { + include.push(element.test); + alreadyIncluded = true; + } + + // Recurse ✨ + for (const child of element.children) { + attempt(child, alreadyIncluded); + } + }; + + for (const root of this.testService.collection.rootItems) { + const element = projection.getElementByTestId(root.item.extId); + if (!element) { + continue; + } + + if (profile && !canUseProfileWithTest(profile, root)) { + continue; + } + + // single controllers won't have visible root ID nodes, handle that case specially + if (!this.viewModel.tree.hasElement(element)) { + const visibleChildren = [...element.children].reduce((acc, c) => + this.viewModel.tree.hasElement(c) && this.viewModel.tree.getNode(c).visible ? acc + 1 : acc, 0); + + // 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); + element.children.forEach(c => attempt(c, true)); + } else { + element.children.forEach(c => attempt(c, false)); + } + } else { + attempt(element, false); + } + } + + return { include, exclude }; + } + /** * @override */ @@ -168,9 +254,9 @@ export class TestingExplorerView extends ViewPane { switch (action.id) { case Testing.FilterActionId: return this.instantiationService.createInstance(TestingExplorerFilter, action); - case RunAllAction.ID: + case RunSelectedAction.ID: return this.getRunGroupDropdown(TestRunProfileBitset.Run, action); - case DebugAllAction.ID: + case DebugSelectedAction.ID: return this.getRunGroupDropdown(TestRunProfileBitset.Debug, action); default: return super.getActionViewItem(action); @@ -204,16 +290,18 @@ export class TestingExplorerView extends ViewPane { defaults.includes(profile) ? localize('defaultTestProfile', '{0} (Default)', profile.label) : profile.label, undefined, undefined, - () => this.testService.runResolvedTests({ - targets: [{ - profileGroup: profile.group, - profileId: profile.profileId, - controllerId: profile.controllerId, - testIds: this.getSelectedOrVisibleItems() - .filter(i => i.controllerId === profile.controllerId) - .map(i => i.item.extId), - }] - }), + () => { + const { include, exclude } = this.getSelectedOrVisibleItems(profile); + this.testService.runResolvedTests({ + exclude: exclude.map(e => e.item.extId), + targets: [{ + profileGroup: profile.group, + profileId: profile.profileId, + controllerId: profile.controllerId, + testIds: include.map(i => i.item.extId), + }] + }); + }, )); } } @@ -254,14 +342,6 @@ export class TestingExplorerView extends ViewPane { super.saveState(); } - /** - * If items in the tree are selected, returns them. Otherwise, returns - * visible tests. - */ - private getSelectedOrVisibleItems() { - return [...this.testService.collection.rootItems]; // todo - } - private getRunGroupDropdown(group: TestRunProfileBitset, defaultAction: IAction) { const dropdownActions = this.getTestConfigGroupActions(group); if (dropdownActions.length < 2) {