testing: run what you see

Fixes #130522
This commit is contained in:
Connor Peet 2021-08-10 15:45:37 -07:00
parent ceb9bcfb57
commit 7a034baa92
No known key found for this signature in database
GPG key ID: CF8FD2EA0DBC61BD
3 changed files with 136 additions and 79 deletions

View file

@ -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);
}),

View file

@ -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<TestingExplorerView> {
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<TestingExplorerView> {
* @override
*/
public runInView(accessor: ServicesAccessor, view: TestingExplorerView): Promise<ITestResult | undefined> {
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),
}]

View file

@ -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) {