diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index a850ef3edb9..7a1c43ab201 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -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, diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 87085d0ee50..a4a273e3997 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -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 { + 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('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('vscode.pickTestConfiguration', { - placeholder: localize('configureTests', 'Select a configuration to update'), + const configuration = await commands.executeCommand('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, diff --git a/src/vs/workbench/contrib/testing/browser/testingConfigurationUi.ts b/src/vs/workbench/contrib/testing/browser/testingConfigurationUi.ts index dccf0080a4c..f39adbdf5e4 100644 --- a/src/vs/workbench/contrib/testing/browser/testingConfigurationUi.ts +++ b/src/vs/workbench/contrib/testing/browser/testingConfigurationUi.ts @@ -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[] = []; - 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[] = []; + 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(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(); + quickpick.items = items; + quickpick.placeholder = placeholder; + return quickpick; +} + +const triggerButtonHandler = (service: ITestConfigurationService, resolve: (arg: undefined) => void) => + (evt: IQuickPickItemButtonEvent) => { + 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(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(resolve => { + quickpick.onDidAccept(() => resolve((quickpick.selectedItems[0] as { config?: ITestRunConfiguration })?.config)); + quickpick.onDidHide(() => resolve(undefined)); + quickpick.onDidTriggerItemButton(triggerButtonHandler(configService, resolve)); quickpick.show(); }); diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 902f9df4922..1b3c9bada71 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -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] }] diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 6ad5ceb0c54..76847cffa21 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -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(SelectDefaultTestProfiles.ID, group), + )); + } + + if (hasConfigurable) { + postActions.push(new Action( + 'configureTestProfiles', + localize('configureTestProfiles', 'Configure Test Profiles'), + undefined, + undefined, + () => this.commandService.executeCommand(ConfigureTestProfilesAction.ID, group), + )); + } + + return Separator.join(profileActions, postActions); } /** diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testCollection.ts index 04c14158ec1..3c40b896182 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testCollection.ts @@ -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; diff --git a/src/vs/workbench/contrib/testing/common/testConfigurationService.ts b/src/vs/workbench/contrib/testing/common/testConfigurationService.ts index 62ceb903451..f6553f228e1 100644 --- a/src/vs/workbench/contrib/testing/common/testConfigurationService.ts +++ b/src/vs/workbench/contrib/testing/common/testConfigurationService.ts @@ -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>; + all(): Iterable>; + + /** + * 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 }; private readonly changeEmitter = new Emitter(); private readonly controllerConfigs = new Map 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()) { diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index 71f3bb1a049..1452b465adf 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -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, }); diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index 24b2826908e..fd81b7bd1f7 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -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 */ diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 12604481f8a..5710997da05 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -94,20 +94,39 @@ export class TestService extends Disposable implements ITestService { */ public async runTests(req: AmbiguousRunTestsRequest, token = CancellationToken.None): Promise { 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, }, diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index 80c583eaa47..f43e40840fb 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -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 } = { [TestRunConfigurationBitset.Run]: hasRunnableTests, [TestRunConfigurationBitset.Coverage]: hasCoverableTests, [TestRunConfigurationBitset.Debug]: hasDebuggableTests, - [TestRunConfigurationBitset.HasNonDefaultConfig]: hasNonDefaultConfig, + [TestRunConfigurationBitset.HasNonDefaultConfig]: hasNonDefaultProfile, [TestRunConfigurationBitset.HasConfigurable]: hasConfigurableConfig, }; diff --git a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts index 5adb18ff7fe..567b79d52fb 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts @@ -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);