testing: complete test run experience update

Fixes https://github.com/microsoft/vscode/issues/127096
This commit is contained in:
Connor Peet
2021-07-13 18:20:48 -07:00
parent fa9255c0de
commit e4ac0cd64c
12 changed files with 362 additions and 126 deletions
@@ -149,9 +149,8 @@ export class ExtHostTesting implements ExtHostTestingShape {
await this.proxy.$runTests({
targets: [{
testIds: req.tests.map(t => t.id),
configGroup: configGroupToBitset[config.group],
configLabel: config.label,
configId: config.configId,
profileGroup: configGroupToBitset[config.group],
profileId: config.configId,
controllerId: config.controllerId,
}],
exclude: req.exclude
@@ -256,7 +255,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
const tracker = this.runTracker.prepareForMainThreadTestRun(publicReq, TestRunDto.fromInternal(req), token);
try {
configuration.runHandler(publicReq, token);
await configuration.runHandler(publicReq, token);
} finally {
if (tracker.isRunning && !token.isCancellationRequested) {
await Event.toPromise(tracker.onEnd);
@@ -866,7 +865,7 @@ export class TestRunConfigurationImpl implements vscode.TestRunConfiguration {
}
this.#proxy.$publishTestRunConfig({
configId,
profileId: configId,
controllerId,
label: _label,
group: groupBitset,
@@ -48,12 +48,15 @@ const enum ActionOrder {
Run = 10,
Debug,
Coverage,
RunUsing,
AutoRun,
// Submenu:
Collapse,
DisplayMode,
Sort,
GoToTest,
HideTest,
}
const hasAnyTestProvider = ContextKeyGreaterExpr.create(TestingContextKeys.providerCount.key, 0);
@@ -90,6 +93,7 @@ export class UnhideTestAction extends Action2 {
title: localize('unhideTest', 'Unhide Test'),
menu: {
id: MenuId.TestItem,
order: ActionOrder.HideTest,
when: TestingContextKeys.testItemIsHidden.isEqualTo(true)
},
});
@@ -130,6 +134,46 @@ export class DebugAction extends Action2 {
}
}
export class RunUsingProfileAction extends Action2 {
public static readonly ID = 'testing.runUsing';
constructor() {
super({
id: RunUsingProfileAction.ID,
title: localize('testing.runUsing', 'Execute Using Profile...'),
icon: icons.testingDebugIcon,
menu: {
id: MenuId.TestItem,
order: ActionOrder.RunUsing,
when: TestingContextKeys.hasNonDefaultProfile.isEqualTo(true),
},
});
}
public override async run(acessor: ServicesAccessor, ...elements: IActionableTestTreeElement[]): Promise<any> {
const testElements = elements.filter((e): e is TestItemTreeElement => e instanceof TestItemTreeElement);
if (testElements.length === 0) {
return;
}
const commandService = acessor.get(ICommandService);
const testService = acessor.get(ITestService);
const controllerId = testElements[0].test.controllerId;
const profile: ITestRunConfiguration | undefined = await commandService.executeCommand('vscode.pickTestProfile', { onlyControllerId: controllerId });
if (!profile) {
return;
}
testService.runResolvedTests({
targets: [{
profileGroup: profile.group,
profileId: profile.profileId,
controllerId: profile.controllerId,
testIds: testElements.filter(t => controllerId === t.test.controllerId).map(t => t.test.item.extId)
}]
});
}
}
export class RunAction extends Action2 {
public static readonly ID = 'testing.run';
constructor() {
@@ -157,12 +201,38 @@ export class RunAction extends Action2 {
}
}
export class ConfigureTestsAction extends Action2 {
public static readonly ID = 'testing.configureTests';
export class SelectDefaultTestProfiles extends Action2 {
public static readonly ID = 'testing.selectDefaultTestProfiles';
constructor() {
super({
id: ConfigureTestsAction.ID,
title: localize('testing.configureTests', 'Configure Tests'),
id: SelectDefaultTestProfiles.ID,
title: localize('testing.selectDefaultTestProfiles', 'Select Default Profile'),
icon: icons.testingUpdateConfiguration,
category,
});
}
public override async run(acessor: ServicesAccessor, onlyGroup: TestRunConfigurationBitset) {
const commands = acessor.get(ICommandService);
const testConfigurationService = acessor.get(ITestConfigurationService);
const configurations = await commands.executeCommand<ITestRunConfiguration[]>('vscode.pickMultipleTestProfiles', {
showConfigureButtons: false,
selected: testConfigurationService.getGroupDefaultConfigurations(onlyGroup),
onlyGroup,
});
if (configurations?.length) {
testConfigurationService.setGroupDefaultConfigurations(onlyGroup, configurations);
}
}
}
export class ConfigureTestProfilesAction extends Action2 {
public static readonly ID = 'testing.configureProfile';
constructor() {
super({
id: ConfigureTestProfilesAction.ID,
title: localize('testing.configureProfile', 'Configure Test Profiles'),
icon: icons.testingUpdateConfiguration,
f1: true,
category,
@@ -173,17 +243,18 @@ export class ConfigureTestsAction extends Action2 {
});
}
public override async run(acessor: ServicesAccessor) {
public override async run(acessor: ServicesAccessor, onlyGroup?: TestRunConfigurationBitset) {
const commands = acessor.get(ICommandService);
const testConfigurationService = acessor.get(ITestConfigurationService);
const configuration = await commands.executeCommand<ITestRunConfiguration>('vscode.pickTestConfiguration', {
placeholder: localize('configureTests', 'Select a configuration to update'),
const configuration = await commands.executeCommand<ITestRunConfiguration>('vscode.pickTestProfile', {
placeholder: localize('configureProfile', 'Select a profile to update'),
showConfigureButtons: false,
onlyConfigurable: true,
onlyGroup,
});
if (configuration) {
testConfigurationService.configure(configuration.controllerId, configuration.configId);
testConfigurationService.configure(configuration.controllerId, configuration.profileId);
}
}
}
@@ -569,6 +640,7 @@ export class GoToTest extends Action2 {
menu: {
id: MenuId.TestItem,
when: TestingContextKeys.testItemHasUri.isEqualTo(true),
order: ActionOrder.GoToTest,
},
keybinding: {
weight: KeybindingWeight.EditorContrib - 10,
@@ -1088,7 +1160,7 @@ export const allTestActions = [
CancelTestRunAction,
ClearTestResultsAction,
CollapseAllAction,
ConfigureTestsAction,
ConfigureTestProfilesAction,
DebugAction,
DebugAllAction,
DebugAtCursor,
@@ -1106,7 +1178,9 @@ export const allTestActions = [
RunAtCursor,
RunCurrentFile,
RunSelectedAction,
RunUsingProfileAction,
SearchForTestExtension,
SelectDefaultTestProfiles,
ShowMostRecentOutputAction,
TestingSortByLocationAction,
TestingSortByStatusAction,
@@ -4,89 +4,150 @@
*--------------------------------------------------------------------------------------------*/
import { groupBy } from 'vs/base/common/arrays';
import { isDefined } from 'vs/base/common/types';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { localize } from 'vs/nls';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { QuickPickInput, IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { QuickPickInput, IQuickPickItem, IQuickInputService, IQuickPickItemButtonEvent } from 'vs/platform/quickinput/common/quickInput';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { testingUpdateConfiguration } from 'vs/workbench/contrib/testing/browser/icons';
import { testConfigurationGroupNames } from 'vs/workbench/contrib/testing/common/constants';
import { ITestRunConfiguration } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestRunConfiguration, TestRunConfigurationBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestConfigurationService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
CommandsRegistry.registerCommand({
id: 'vscode.pickTestConfiguration',
handler: async (accessor: ServicesAccessor, {
controllerId,
placeholder = localize('testConfigurationUi.pick', 'Pick a test configuration to use'),
showConfigureButtons = true,
onlyConfigurable = false,
}: {
controllerId?: string;
showConfigureButtons?: boolean;
placeholder?: string;
onlyConfigurable?: boolean;
}) => {
const configService = accessor.get(ITestConfigurationService);
const items: QuickPickInput<IQuickPickItem & { config: ITestRunConfiguration }>[] = [];
const pushItems = (allConfigs: ITestRunConfiguration[], description?: string) => {
for (const configs of groupBy(allConfigs, (a, b) => a.group - b.group)) {
let added = false;
interface IConfigurationPickerOptions {
/** Placeholder text */
placeholder?: string;
/** Show buttons to trigger configuration */
showConfigureButtons?: boolean;
/** Only show configurations from this controller */
onlyControllerId?: string;
/** Only show this group */
onlyGroup?: TestRunConfigurationBitset;
/** Only show items which are configurable */
onlyConfigurable?: boolean;
}
for (const config of configs) {
if (onlyConfigurable && !config.hasConfigurationHandler) {
continue;
}
if (!added) {
items.push({ type: 'separator', label: testConfigurationGroupNames[configs[0].group] });
added = true;
}
items.push(({
type: 'item',
config,
label: config.label,
description,
alwaysShow: true,
buttons: config.hasConfigurationHandler && showConfigureButtons
? [{
iconClass: ThemeIcon.asClassName(testingUpdateConfiguration),
tooltip: localize('updateTestConfiguration', 'Update Test Configuration')
}] : []
}));
function buildPicker(accessor: ServicesAccessor, {
onlyGroup,
showConfigureButtons,
onlyControllerId,
onlyConfigurable,
placeholder = localize('testConfigurationUi.pick', 'Pick a test configuration to use'),
}: IConfigurationPickerOptions) {
const configService = accessor.get(ITestConfigurationService);
const items: QuickPickInput<IQuickPickItem & { config: ITestRunConfiguration }>[] = [];
const pushItems = (allConfigs: ITestRunConfiguration[], description?: string) => {
for (const configs of groupBy(allConfigs, (a, b) => a.group - b.group)) {
let addedHeader = false;
if (onlyGroup) {
if (configs[0].group !== onlyGroup) {
continue;
}
}
};
if (controllerId !== undefined) {
const lookup = configService.getControllerConfigurations(controllerId);
if (!lookup) {
return;
addedHeader = true; // showing one group, no need for label
}
pushItems(lookup.configs);
} else {
for (const { configs, controller } of configService.all()) {
pushItems(configs, controller.label.value);
for (const config of configs) {
if (onlyConfigurable && !config.hasConfigurationHandler) {
continue;
}
if (!addedHeader) {
items.push({ type: 'separator', label: testConfigurationGroupNames[configs[0].group] });
addedHeader = true;
}
items.push(({
type: 'item',
config,
label: config.label,
description,
alwaysShow: true,
buttons: config.hasConfigurationHandler && showConfigureButtons
? [{
iconClass: ThemeIcon.asClassName(testingUpdateConfiguration),
tooltip: localize('updateTestConfiguration', 'Update Test Configuration')
}] : []
}));
}
}
};
const quickpick = accessor.get(IQuickInputService).createQuickPick();
quickpick.items = items;
quickpick.placeholder = placeholder;
if (onlyControllerId !== undefined) {
const lookup = configService.getControllerConfigurations(onlyControllerId);
if (!lookup) {
return;
}
const pick = await new Promise<ITestRunConfiguration | undefined>(resolve => {
quickpick.onDidAccept(() => resolve((quickpick.selectedItems[0] as { config?: ITestRunConfiguration })?.config));
quickpick.onDidHide(() => resolve(undefined));
quickpick.onDidTriggerItemButton(evt => {
const config = (evt.item as { config?: ITestRunConfiguration }).config;
if (config) {
configService.configure(config.controllerId, config.configId);
resolve(undefined);
}
pushItems(lookup.configs);
} else {
for (const { configs, controller } of configService.all()) {
pushItems(configs, controller.label.value);
}
}
const quickpick = accessor.get(IQuickInputService).createQuickPick<IQuickPickItem & { config: ITestRunConfiguration }>();
quickpick.items = items;
quickpick.placeholder = placeholder;
return quickpick;
}
const triggerButtonHandler = (service: ITestConfigurationService, resolve: (arg: undefined) => void) =>
(evt: IQuickPickItemButtonEvent<IQuickPickItem>) => {
const config = (evt.item as { config?: ITestRunConfiguration }).config;
if (config) {
service.configure(config.controllerId, config.profileId);
resolve(undefined);
}
};
CommandsRegistry.registerCommand({
id: 'vscode.pickMultipleTestProfiles',
handler: async (accessor: ServicesAccessor, options: IConfigurationPickerOptions & {
selected?: ITestRunConfiguration[],
}) => {
const configService = accessor.get(ITestConfigurationService);
const quickpick = buildPicker(accessor, options);
if (!quickpick) {
return;
}
quickpick.canSelectMany = true;
if (options.selected) {
quickpick.selectedItems = quickpick.items
.filter((i): i is IQuickPickItem & { config: ITestRunConfiguration } => i.type === 'item')
.filter(i => options.selected!.some(s => s.controllerId === i.config.controllerId && s.profileId === i.config.profileId));
}
const pick = await new Promise<ITestRunConfiguration[] | undefined>(resolve => {
quickpick.onDidAccept(() => {
const selected = quickpick.selectedItems as readonly { config?: ITestRunConfiguration }[];
resolve(selected.map(s => s.config).filter(isDefined));
});
quickpick.onDidHide(() => resolve(undefined));
quickpick.onDidTriggerItemButton(triggerButtonHandler(configService, resolve));
quickpick.show();
});
quickpick.dispose();
return pick;
}
});
CommandsRegistry.registerCommand({
id: 'vscode.pickTestProfile',
handler: async (accessor: ServicesAccessor, options: IConfigurationPickerOptions) => {
const configService = accessor.get(ITestConfigurationService);
const quickpick = buildPicker(accessor, options);
if (!quickpick) {
return;
}
const pick = await new Promise<ITestRunConfiguration | undefined>(resolve => {
quickpick.onDidAccept(() => resolve((quickpick.selectedItems[0] as { config?: ITestRunConfiguration })?.config));
quickpick.onDidHide(() => resolve(undefined));
quickpick.onDidTriggerItemButton(triggerButtonHandler(configService, resolve));
quickpick.show();
});
@@ -420,17 +420,16 @@ abstract class RunTestDecoration extends Disposable {
}
if (capabilities & TestRunConfigurationBitset.HasNonDefaultConfig) {
testActions.push(new Action('testing.gutter.runUsing', localize('run using', 'Run Using...'), undefined, undefined, async () => {
const config: ITestRunConfiguration | undefined = await this.commandService.executeCommand('vscode.pickTestConfiguration', test.controllerId);
testActions.push(new Action('testing.runUsing', localize('testing.runUsing', 'Execute Using Profile...'), undefined, undefined, async () => {
const config: ITestRunConfiguration | undefined = await this.commandService.executeCommand('vscode.pickTestProfile', { onlyControllerId: test.controllerId });
if (!config) {
return;
}
this.testService.runResolvedTests({
targets: [{
configGroup: config.group,
configId: config.configId,
configLabel: config.label,
profileGroup: config.group,
profileId: config.profileId,
controllerId: config.controllerId,
testIds: [test.item.extId]
}]
@@ -27,6 +27,7 @@ import { localize } from 'vs/nls';
import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem';
import { createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
@@ -54,7 +55,7 @@ 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, TestIdPath, TestItemExpandState, TestRunConfigurationBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { identifyTest, ITestRunConfiguration, TestIdPath, TestItemExpandState, TestRunConfigurationBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { capabilityContextKeys, ITestConfigurationService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener';
@@ -62,7 +63,7 @@ import { cmpPriority, isFailedState, isStateWithResult } from 'vs/workbench/cont
import { getPathForTestInResult, 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 { DebugAllAction, GoToTest, RunAllAction } from './testExplorerActions';
import { ConfigureTestProfilesAction, DebugAllAction, GoToTest, RunAllAction, SelectDefaultTestProfiles } from './testExplorerActions';
export class TestingExplorerView extends ViewPane {
public viewModel!: TestingExplorerViewModel;
@@ -87,6 +88,7 @@ export class TestingExplorerView extends ViewPane {
@ITelemetryService telemetryService: ITelemetryService,
@ITestingProgressUiService private readonly testProgressService: ITestingProgressUiService,
@ITestConfigurationService private readonly testConfigurationService: ITestConfigurationService,
@ICommandService private readonly commandService: ICommandService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
this.location.set(viewDescriptorService.getViewLocationById(Testing.ExplorerViewId) ?? ViewContainerLocation.Sidebar);
@@ -101,6 +103,8 @@ export class TestingExplorerView extends ViewPane {
this._register(testService.collection.onBusyProvidersChange(busy => {
this.updateDiscoveryProgress(busy);
}));
this._register(testConfigurationService.onDidChange(() => this.updateActions()));
}
/**
@@ -171,9 +175,11 @@ export class TestingExplorerView extends ViewPane {
/** @inheritdoc */
private getTestConfigGroupActions(group: TestRunConfigurationBitset) {
const actions: IAction[] = [];
const profileActions: IAction[] = [];
let participatingGroups = 0;
let hasConfigurable = false;
const defaults = this.testConfigurationService.getGroupDefaultConfigurations(group);
for (const { configs, controller } of this.testConfigurationService.all()) {
let hasAdded = false;
@@ -185,19 +191,19 @@ export class TestingExplorerView extends ViewPane {
if (!hasAdded) {
hasAdded = true;
participatingGroups++;
actions.push(new Action(`${controller.id}.$root`, controller.label.value, undefined, false));
profileActions.push(new Action(`${controller.id}.$root`, controller.label.value, undefined, false));
}
actions.push(new Action(
`${controller.id}.${config.configId}`,
config.label,
hasConfigurable = hasConfigurable || config.hasConfigurationHandler;
profileActions.push(new Action(
`${controller.id}.${config.profileId}`,
defaults.includes(config) ? localize('defaultTestProfile', '{0} (Default)', config.label) : config.label,
undefined,
undefined,
() => this.testService.runResolvedTests({
targets: [{
configGroup: config.group,
configId: config.configId,
configLabel: config.label,
profileGroup: config.group,
profileId: config.profileId,
controllerId: config.controllerId,
testIds: this.getSelectedOrVisibleItems()
.filter(i => i.controllerId === config.controllerId)
@@ -210,10 +216,31 @@ export class TestingExplorerView extends ViewPane {
// If there's only one group, don't add a heading for it in the dropdown.
if (participatingGroups === 1) {
actions.shift();
profileActions.shift();
}
return actions;
let postActions: IAction[] = [];
if (profileActions.length > 1) {
postActions.push(new Action(
'selectDefaultTestConfigurations',
localize('selectDefaultConfigs', 'Select Default Profile'),
undefined,
undefined,
() => this.commandService.executeCommand<ITestRunConfiguration>(SelectDefaultTestProfiles.ID, group),
));
}
if (hasConfigurable) {
postActions.push(new Action(
'configureTestProfiles',
localize('configureTestProfiles', 'Configure Test Profiles'),
undefined,
undefined,
() => this.commandService.executeCommand<ITestRunConfiguration>(ConfigureTestProfilesAction.ID, group),
));
}
return Separator.join(profileActions, postActions);
}
/**
@@ -41,7 +41,7 @@ export const testRunConfigurationBitsetList = [
*/
export interface ITestRunConfiguration {
controllerId: string;
configId: number;
profileId: number;
label: string;
group: TestRunConfigurationBitset;
isDefault: boolean;
@@ -62,9 +62,8 @@ export interface ResolvedTestRunRequest {
targets: {
testIds: string[];
controllerId: string;
configLabel: string;
configGroup: TestRunConfigurationBitset;
configId: number;
profileGroup: TestRunConfigurationBitset;
profileId: number;
}[]
exclude?: ITestIdWithSrc[];
isAutoRun?: boolean;
@@ -4,8 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { isDefined } from 'vs/base/common/types';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue';
import { ITestRunConfiguration, TestRunConfigurationBitset, testRunConfigurationBitsetList } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { IMainThreadTestController } from 'vs/workbench/contrib/testing/common/testService';
@@ -51,7 +54,21 @@ export interface ITestConfigurationService {
/**
* Gets all registered controllers, grouping by controller.
*/
all(): Iterable<Readonly<{ controller: IMainThreadTestController, configs: ITestRunConfiguration[] }>>;
all(): Iterable<Readonly<{
capabilities: number,
controller: IMainThreadTestController,
configs: ITestRunConfiguration[],
}>>;
/**
* Gets the default configurations to be run for a given run group.
*/
getGroupDefaultConfigurations(group: TestRunConfigurationBitset): ITestRunConfiguration[];
/**
* Sets the default configurations to be run for a given run group.
*/
setGroupDefaultConfigurations(group: TestRunConfigurationBitset, configs: ITestRunConfiguration[]): void;
/**
* Gets the configurations for a controller, in priority order.
@@ -88,6 +105,7 @@ export const capabilityContextKeys = (capabilities: number): [key: string, value
export class TestConfigurationService implements ITestConfigurationService {
declare readonly _serviceBrand: undefined;
private readonly preferredDefaults: StoredValue<{ [K in TestRunConfigurationBitset]?: { controllerId: string; configId: number }[] }>;
private readonly capabilitiesContexts: { [K in TestRunConfigurationBitset]: IContextKey<boolean> };
private readonly changeEmitter = new Emitter<void>();
private readonly controllerConfigs = new Map</* controller ID */string, {
@@ -101,12 +119,19 @@ export class TestConfigurationService implements ITestConfigurationService {
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@IStorageService storageService: IStorageService,
) {
this.preferredDefaults = new StoredValue({
key: 'testingPreferredConfigs',
scope: StorageScope.WORKSPACE,
target: StorageTarget.USER,
}, storageService);
this.capabilitiesContexts = {
[TestRunConfigurationBitset.Run]: TestingContextKeys.hasRunnableTests.bindTo(contextKeyService),
[TestRunConfigurationBitset.Debug]: TestingContextKeys.hasDebuggableTests.bindTo(contextKeyService),
[TestRunConfigurationBitset.Coverage]: TestingContextKeys.hasCoverableTests.bindTo(contextKeyService),
[TestRunConfigurationBitset.HasNonDefaultConfig]: TestingContextKeys.hasNonDefaultConfig.bindTo(contextKeyService),
[TestRunConfigurationBitset.HasNonDefaultConfig]: TestingContextKeys.hasNonDefaultProfile.bindTo(contextKeyService),
[TestRunConfigurationBitset.HasConfigurable]: TestingContextKeys.hasConfigurableConfig.bindTo(contextKeyService),
};
@@ -144,7 +169,7 @@ export class TestConfigurationService implements ITestConfigurationService {
return;
}
const config = ctrl.configs.find(c => c.controllerId === controllerId && c.configId === configId);
const config = ctrl.configs.find(c => c.controllerId === controllerId && c.profileId === configId);
if (!config) {
return;
}
@@ -172,7 +197,7 @@ export class TestConfigurationService implements ITestConfigurationService {
return;
}
const index = ctrl.configs.findIndex(c => c.configId === configId);
const index = ctrl.configs.findIndex(c => c.profileId === configId);
if (index === -1) {
return;
}
@@ -207,6 +232,43 @@ export class TestConfigurationService implements ITestConfigurationService {
return this.controllerConfigs.get(controllerId)?.configs.filter(c => c.group === group) ?? [];
}
/** @inheritdoc */
public getGroupDefaultConfigurations(group: TestRunConfigurationBitset) {
const preferred = this.preferredDefaults.get();
if (!preferred) {
return this.getBaseDefaults(group);
}
const configs = preferred[group]
?.map(p => this.controllerConfigs.get(p.controllerId)?.configs.find(
c => c.profileId === p.configId && c.group === group))
.filter(isDefined);
return configs?.length ? configs : this.getBaseDefaults(group);
}
/** @inheritdoc */
public setGroupDefaultConfigurations(group: TestRunConfigurationBitset, configs: ITestRunConfiguration[]) {
this.preferredDefaults.store({
...this.preferredDefaults.get(),
[group]: configs.map(c => ({ configId: c.profileId, controllerId: c.controllerId })),
});
this.changeEmitter.fire();
}
private getBaseDefaults(group: TestRunConfigurationBitset) {
const defaults: ITestRunConfiguration[] = [];
for (const { configs } of this.controllerConfigs.values()) {
const config = configs.find(c => c.group === group);
if (config) {
defaults.push(config);
}
}
return defaults;
}
private refreshContextKeys() {
let allCapabilities = 0;
for (const { capabilities } of this.controllerConfigs.values()) {
@@ -142,7 +142,7 @@ export class TestResultService implements ITestResultService {
config = this.testConfiguration.getControllerGroupConfigurations(req.controllerId, TestRunConfigurationBitset.Run)[0];
} else {
const configs = this.testConfiguration.getControllerGroupConfigurations(req.controllerId, req.config.group);
config = configs.find(c => c.configId === req.config!.id) || configs[0];
config = configs.find(c => c.profileId === req.config!.id) || configs[0];
}
const resolved: ResolvedTestRunRequest = {
@@ -153,9 +153,8 @@ export class TestResultService implements ITestResultService {
if (config) {
resolved.targets.push({
configGroup: config.group,
configId: config.configId,
configLabel: config.label,
profileGroup: config.group,
profileId: config.profileId,
controllerId: req.controllerId,
testIds: req.tests,
});
@@ -172,8 +172,6 @@ export interface ITestRootProvider {
export interface AmbiguousRunTestsRequest {
/** Group to run */
group: TestRunConfigurationBitset;
/** If there is a configuration in the group with this label, use it */
preferGroupLabel?: string;
/** Tests to run. Allowed to be from different controllers */
tests: ITestIdWithSrc[];
/** Tests to exclude. If not given, the current UI excluded tests are used */
@@ -94,20 +94,39 @@ export class TestService extends Disposable implements ITestService {
*/
public async runTests(req: AmbiguousRunTestsRequest, token = CancellationToken.None): Promise<ITestResult> {
const resolved: ResolvedTestRunRequest = { targets: [], exclude: req.exclude, isAutoRun: req.isAutoRun };
for (const byController of groupBy(req.tests, (a, b) => a.controllerId === b.controllerId ? 0 : 1)) {
const groups = this.testConfigurationService.getControllerGroupConfigurations(byController[0].controllerId, req.group);
const group = req.preferGroupLabel ? (groups.find(g => g.label === req.preferGroupLabel) || groups[0]) : groups[0];
if (group) {
// First, try to run the tests using the default run configurations...
const defaultConfigs = this.testConfigurationService.getGroupDefaultConfigurations(req.group);
for (const config of defaultConfigs) {
const testIds = req.tests.filter(t => t.controllerId === config.controllerId).map(t => t.testId);
if (testIds.length) {
resolved.targets.push({
testIds: byController.map(t => t.testId),
configLabel: group.label,
configGroup: group.group,
configId: group.configId,
controllerId: group.controllerId,
testIds: testIds,
profileGroup: config.group,
profileId: config.profileId,
controllerId: config.controllerId,
});
}
}
// If no tests are covered by the defaults, just use whatever the defaults
// for their controller are. This can happen if the user chose specific
// configs for the run button, but then asked to run a single test from the
// explorer or decoration. We shouldn't no-op.
if (resolved.targets.length === 0) {
for (const byController of groupBy(req.tests, (a, b) => a.controllerId === b.controllerId ? 0 : 1)) {
const configs = this.testConfigurationService.getControllerGroupConfigurations(byController[0].controllerId, req.group);
if (configs.length) {
resolved.targets.push({
testIds: byController.map(t => t.testId),
profileGroup: req.group,
profileId: configs[0].profileId,
controllerId: configs[0].controllerId,
});
}
}
}
return this.runResolvedTests(resolved, token);
}
@@ -138,7 +157,7 @@ export class TestService extends Disposable implements ITestService {
{
runId: result.id,
excludeExtIds: req.exclude!.filter(t => t.controllerId === group.controllerId).map(t => t.testId),
configId: group.configId,
configId: group.profileId,
controllerId: group.controllerId,
testIds: group.testIds,
},
@@ -14,14 +14,14 @@ export namespace TestingContextKeys {
export const hasDebuggableTests = new RawContextKey('testing.hasDebuggableTests', false, { type: 'boolean', description: localize('testing.hasDebuggableTests', 'Indicates whether any test controller has registered a debug configuration') });
export const hasRunnableTests = new RawContextKey('testing.hasRunnableTests', false, { type: 'boolean', description: localize('testing.hasRunnableTests', 'Indicates whether any test controller has registered a run configuration') });
export const hasCoverableTests = new RawContextKey('testing.hasCoverableTests', false, { type: 'boolean', description: localize('testing.hasCoverableTests', 'Indicates whether any test controller has registered a coverage configuration') });
export const hasNonDefaultConfig = new RawContextKey('testing.hasNonDefaultConfig', false, { type: 'boolean', description: localize('testing.hasNonDefaultConfig', 'Indicates whether any test controller has registered a non-default configuration') });
export const hasNonDefaultProfile = new RawContextKey('testing.hasNonDefaultConfig', false, { type: 'boolean', description: localize('testing.hasNonDefaultConfig', 'Indicates whether any test controller has registered a non-default configuration') });
export const hasConfigurableConfig = new RawContextKey('testing.hasConfigurableConfig', false, { type: 'boolean', description: localize('testing.hasConfigurableConfig', 'Indicates whether any test configuration can be configured') });
export const capabilityToContextKey: { [K in TestRunConfigurationBitset]: RawContextKey<boolean> } = {
[TestRunConfigurationBitset.Run]: hasRunnableTests,
[TestRunConfigurationBitset.Coverage]: hasCoverableTests,
[TestRunConfigurationBitset.Debug]: hasDebuggableTests,
[TestRunConfigurationBitset.HasNonDefaultConfig]: hasNonDefaultConfig,
[TestRunConfigurationBitset.HasNonDefaultConfig]: hasNonDefaultProfile,
[TestRunConfigurationBitset.HasConfigurable]: hasConfigurableConfig,
};
@@ -35,9 +35,8 @@ suite('Workbench - Test Results Service', () => {
const defaultOpts = (testIds: string[]): ResolvedTestRunRequest => ({
targets: [{
configGroup: TestRunConfigurationBitset.Run,
configId: 0,
configLabel: 'Run Tests',
profileGroup: TestRunConfigurationBitset.Run,
profileId: 0,
controllerId: 'ctrlId',
testIds,
}]
@@ -204,7 +203,7 @@ suite('Workbench - Test Results Service', () => {
setup(() => {
storage = new InMemoryResultStorage(new TestStorageService(), new NullLogService());
results = new TestTestResultService(new MockContextKeyService(), storage, new TestConfigurationService(new MockContextKeyService()));
results = new TestTestResultService(new MockContextKeyService(), storage, new TestConfigurationService(new MockContextKeyService(), new TestStorageService()));
});
test('pushes new result', () => {
@@ -221,7 +220,7 @@ suite('Workbench - Test Results Service', () => {
results = new TestResultService(
new MockContextKeyService(),
storage,
new TestConfigurationService(new MockContextKeyService()),
new TestConfigurationService(new MockContextKeyService(), new TestStorageService()),
);
assert.strictEqual(0, results.results.length);