Skip to content

Commit

Permalink
testing: run what you see
Browse files Browse the repository at this point in the history
Fixes #130522
  • Loading branch information
connor4312 committed Aug 10, 2021
1 parent ceb9bcf commit 7a034ba
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 79 deletions.
2 changes: 1 addition & 1 deletion src/vs/workbench/api/common/extHostTypeConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}),
Expand Down
85 changes: 31 additions & 54 deletions src/vs/workbench/contrib/testing/browser/testExplorerActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -263,67 +262,58 @@ 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,
});
}

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

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

Expand All @@ -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),
}]
Expand Down
128 changes: 104 additions & 24 deletions src/vs/workbench/contrib/testing/browser/testingExplorerView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,25 +48,25 @@ 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';
import { ITestExplorerFilterState, TestExplorerFilterState, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
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;
Expand Down Expand Up @@ -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.

This comment has been minimized.

Copy link
@jogo-

jogo- Aug 11, 2021

Contributor

Typo: the -> then

&& (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
*/
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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),
}]
});
},
));
}
}
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 7a034ba

Please sign in to comment.