From 8f74fbfd1f2d8f6268a42df131726b218aafe511 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 4 Apr 2023 20:03:50 -0700 Subject: [PATCH] testing: enable continuous run for selected tests (#179215) feat: enable auto-run for selected tests For #178973. Demo in https://github.com/microsoft/vscode/issues/178973#issuecomment-1496713352 --- .../contrib/testing/browser/icons.ts | 3 +- .../contrib/testing/browser/media/testing.css | 21 +- .../testing/browser/testExplorerActions.ts | 228 +++++++++++++----- .../testing/browser/testingExplorerView.ts | 88 ++++--- .../contrib/testing/common/constants.ts | 2 + .../testing/common/testingContextKeys.ts | 2 +- .../common/testingContinuousRunService.ts | 95 ++++++-- 7 files changed, 324 insertions(+), 115 deletions(-) diff --git a/src/vs/workbench/contrib/testing/browser/icons.ts b/src/vs/workbench/contrib/testing/browser/icons.ts index b41cb573333..e06e72919f3 100644 --- a/src/vs/workbench/contrib/testing/browser/icons.ts +++ b/src/vs/workbench/contrib/testing/browser/icons.ts @@ -28,7 +28,8 @@ export const testingShowAsTree = registerIcon('testing-show-as-list-icon', Codic export const testingUpdateProfiles = registerIcon('testing-update-profiles', Codicon.gear, localize('testingUpdateProfiles', 'Icon shown to update test profiles.')); export const testingRefreshTests = registerIcon('testing-refresh-tests', Codicon.refresh, localize('testingRefreshTests', 'Icon on the button to refresh tests.')); export const testingTurnContinuousRunOn = registerIcon('testing-turn-continuous-run-on', Codicon.eye, localize('testingTurnContinuousRunOn', 'Icon to turn continuous test runs on.')); -export const testingTurnContinuousRunOff = registerIcon('testing-turn-continuous-run-pff', Codicon.eyeClosed, localize('testingTurnContinuousRunOff', 'Icon to turn continuous test runs off.')); +export const testingTurnContinuousRunOff = registerIcon('testing-turn-continuous-run-off', Codicon.eyeClosed, localize('testingTurnContinuousRunOff', 'Icon to turn continuous test runs off.')); +export const testingContinuousIsOn = registerIcon('testing-continuous-is-on', Codicon.eye, localize('testingTurnContinuousRunIsOn', 'Icon when continuous run is on for a test ite,.')); export const testingCancelRefreshTests = registerIcon('testing-cancel-refresh-tests', Codicon.stop, localize('testingCancelRefreshTests', 'Icon on the button to cancel refreshing tests.')); export const testingStatesToIcons = new Map([ diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 9f7dcf17d36..40266ba5ac3 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -70,9 +70,26 @@ margin-right: 0.8em; } +.test-explorer .monaco-list-row .monaco-action-bar .codicon-testing-continuous-is-on { + color: var(--vscode-inputOption-activeForeground); + border-color: var(--vscode-inputOption-activeBorder); + background: var(--vscode-inputOption-activeBackground); + border: 1px solid var(--vscode-inputOption-activeBorder); + border-radius: 3px; +} + +.test-explorer .monaco-list-row:not(.focused, :hover) .monaco-action-bar.testing-is-continuous-run .action-item { + display: none; +} + +.test-explorer .monaco-list-row .monaco-action-bar.testing-is-continuous-run .action-item:last-child { + display: block !important; +} + .test-explorer .monaco-list-row:hover .monaco-action-bar, -.test-output-peek-tree .monaco-list-row:hover .monaco-action-bar, .test-explorer .monaco-list-row.focused .monaco-action-bar, +.test-explorer .monaco-list-row .monaco-action-bar.testing-is-continuous-run, +.test-output-peek-tree .monaco-list-row:hover .monaco-action-bar, .test-output-peek-tree .monaco-list-row.focused .monaco-action-bar { display: initial; } @@ -124,7 +141,7 @@ .monaco-action-bar .action-item > .action-label { - padding: 2px; + padding: 1px 2px; margin-right: 2px; } diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 37c160ba36e..e940b30270c 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -37,7 +37,7 @@ import { TestCommandId, TestExplorerViewMode, TestExplorerViewSorting, Testing, import { ITestProfileService, canUseProfileWithTest } from 'vs/workbench/contrib/testing/common/testProfileService'; import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { IMainThreadTestCollection, ITestService, expandAndGetTestById, testsInFile } from 'vs/workbench/contrib/testing/common/testService'; +import { IMainThreadTestCollection, IMainThreadTestController, ITestService, expandAndGetTestById, testsInFile } from 'vs/workbench/contrib/testing/common/testService'; import { ExtTestRunProfileKind, ITestRunProfile, InternalTestItem, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingContinuousRunService } from 'vs/workbench/contrib/testing/common/testingContinuousRunService'; @@ -64,6 +64,7 @@ const enum ActionOrder { Sort, GoToTest, HideTest, + ContinuousRunTest = -1 >>> 1, // max int, always at the end to avoid shifting on hover } const hasAnyTestProvider = ContextKeyGreaterExpr.create(TestingContextKeys.providerCount.key, 0); @@ -250,6 +251,91 @@ export class SelectDefaultTestProfiles extends Action2 { } } +export class ContinuousRunTestAction extends Action2 { + constructor() { + super({ + id: TestCommandId.ToggleContinousRunForTest, + title: localize('testing.toggleContinuousRunOn', 'Turn on Continuous Run'), + icon: icons.testingTurnContinuousRunOn, + precondition: ContextKeyExpr.or( + TestingContextKeys.isContinuousModeOn.isEqualTo(true), + TestingContextKeys.isParentRunningContinuously.isEqualTo(false) + ), + toggled: { + condition: TestingContextKeys.isContinuousModeOn.isEqualTo(true), + icon: icons.testingContinuousIsOn, + title: localize('testing.toggleContinuousRunOff', 'Turn off Continuous Run'), + }, + menu: testItemInlineAndInContext(ActionOrder.ContinuousRunTest, TestingContextKeys.supportsContinuousRun.isEqualTo(true)), + }); + } + + public override async run(accessor: ServicesAccessor, ...elements: IActionableTestTreeElement[]): Promise { + const crService = accessor.get(ITestingContinuousRunService); + const profileService = accessor.get(ITestProfileService); + for (const element of elements) { + if (!(element instanceof TestItemTreeElement)) { + continue; + } + + const id = element.test.item.extId; + if (crService.isSpecificallyEnabledFor(id)) { + crService.stop(id); + continue; + } + + const profiles = profileService.getGroupDefaultProfiles(TestRunProfileBitset.Run) + .filter(p => p.supportsContinuousRun && p.controllerId === element.test.controllerId); + if (!profiles.length) { + continue; + } + + crService.start(profiles, id); + } + } +} + +export class ContinuousRunUsingProfileTestAction extends Action2 { + constructor() { + super({ + id: TestCommandId.ContinousRunUsingForTest, + title: localize('testing.startContinuousRunUsing', 'Start Continous Run Using...'), + icon: icons.testingDebugIcon, + menu: [ + { + id: MenuId.TestItem, + order: ActionOrder.RunContinuous, + group: 'builtin@2', + when: ContextKeyExpr.and( + TestingContextKeys.supportsContinuousRun.isEqualTo(true), + TestingContextKeys.isContinuousModeOn.isEqualTo(false), + ) + } + ], + }); + } + + public override async run(accessor: ServicesAccessor, ...elements: IActionableTestTreeElement[]): Promise { + const crService = accessor.get(ITestingContinuousRunService); + const profileService = accessor.get(ITestProfileService); + const notificationService = accessor.get(INotificationService); + const quickInputService = accessor.get(IQuickInputService); + + for (const element of elements) { + if (!(element instanceof TestItemTreeElement)) { + continue; + } + + const selected = await selectContinuousRunProfiles(crService, notificationService, quickInputService, + [{ profiles: profileService.getControllerProfiles(element.test.controllerId) }]); + + if (selected.length) { + crService.start(selected, element.test.item.extId); + } + } + } +} + export class ConfigureTestProfilesAction extends Action2 { constructor() { super({ @@ -314,6 +400,79 @@ class StopContinuousRunAction extends Action2 { } } +function selectContinuousRunProfiles( + crs: ITestingContinuousRunService, + notificationService: INotificationService, + quickInputService: IQuickInputService, + profilesToPickFrom: Iterable>, +): Promise { + type ItemType = IQuickPickItem & { profile: ITestRunProfile }; + + const items: ItemType[] = []; + for (const { controller, profiles } of profilesToPickFrom) { + for (const profile of profiles) { + if (profile.supportsContinuousRun) { + items.push({ + label: profile.label || controller?.label.value || '', + description: controller?.label.value, + profile, + }); + } + } + } + + if (items.length === 0) { + notificationService.info(localize('testing.noProfiles', 'No test continuous run-enabled profiles were found')); + return Promise.resolve([]); + } + + // special case: don't bother to quick a pickpick if there's only a single profile + if (items.length === 1) { + return Promise.resolve([items[0].profile]); + } + + const qpItems: (ItemType | IQuickPickSeparator)[] = []; + const selectedItems: ItemType[] = []; + const lastRun = crs.lastRunProfileIds; + + items.sort((a, b) => a.profile.group - b.profile.group + || a.profile.controllerId.localeCompare(b.profile.controllerId) + || a.label.localeCompare(b.label)); + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (i === 0 || items[i - 1].profile.group !== item.profile.group) { + qpItems.push({ type: 'separator', label: testConfigurationGroupNames[item.profile.group] }); + } + + qpItems.push(item); + if (lastRun.has(item.profile.profileId)) { + selectedItems.push(item); + } + } + + const quickpick = quickInputService.createQuickPick(); + quickpick.title = localize('testing.selectContinuousProfiles', 'Select profiles to run when files change:'); + quickpick.canSelectMany = true; + quickpick.items = qpItems; + quickpick.selectedItems = selectedItems; + quickpick.show(); + return new Promise((resolve, reject) => { + quickpick.onDidAccept(() => { + resolve(quickpick.selectedItems.map(i => i.profile)); + quickpick.dispose(); + }); + + quickpick.onDidHide(() => { + resolve([]); + quickpick.dispose(); + }); + }); +} + class StartContinuousRunAction extends Action2 { constructor() { super({ @@ -324,69 +483,12 @@ class StartContinuousRunAction extends Action2 { menu: continuousMenus(false), }); } - run(accessor: ServicesAccessor, ...args: any[]): void { - const controllerProfiles = accessor.get(ITestProfileService).all(); - const notificationService = accessor.get(INotificationService); + async run(accessor: ServicesAccessor, ...args: any[]): Promise { const crs = accessor.get(ITestingContinuousRunService); - - type ItemType = IQuickPickItem & { profile: ITestRunProfile }; - - const items: ItemType[] = []; - for (const { controller, profiles } of controllerProfiles) { - for (const profile of profiles) { - if (profile.supportsContinuousRun) { - items.push({ - label: profile.label || controller.label.value, - description: controller.label.value, - profile, - }); - } - } + const selected = await selectContinuousRunProfiles(crs, accessor.get(INotificationService), accessor.get(IQuickInputService), accessor.get(ITestProfileService).all()); + if (selected.length) { + crs.start(selected); } - - if (items.length === 0) { - notificationService.info(localize('testing.noProfiles', 'No test continuous run-enabled profiles were found')); - return; - } - - // special case: don't bother to quick a pickpick if there's only a single profile - if (items.length === 1) { - return crs.start([items[0].profile]); - } - - const qpItems: (ItemType | IQuickPickSeparator)[] = []; - const selectedItems: ItemType[] = []; - const lastRun = crs.lastRunProfileIds; - - items.sort((a, b) => a.profile.group - b.profile.group - || a.profile.controllerId.localeCompare(b.profile.controllerId) - || a.label.localeCompare(b.label)); - - for (let i = 0; i < items.length; i++) { - const item = items[i]; - if (i === 0 || items[i - 1].profile.group !== item.profile.group) { - qpItems.push({ type: 'separator', label: testConfigurationGroupNames[item.profile.group] }); - } - - qpItems.push(item); - if (lastRun.has(item.profile.profileId)) { - selectedItems.push(item); - } - } - - const quickpick = accessor.get(IQuickInputService).createQuickPick(); - quickpick.title = localize('testing.selectContinuousProfiles', 'Select profiles to run when files change:'); - quickpick.canSelectMany = true; - quickpick.items = qpItems; - quickpick.selectedItems = selectedItems; - quickpick.show(); - - quickpick.onDidAccept(() => { - if (quickpick.selectedItems.length) { - crs.start(quickpick.selectedItems.map(i => i.profile)); - quickpick.dispose(); - } - }); } } @@ -1367,6 +1469,8 @@ export const allTestActions = [ ClearTestResultsAction, CollapseAllAction, ConfigureTestProfilesAction, + ContinuousRunTestAction, + ContinuousRunUsingProfileTestAction, DebugAction, DebugAllAction, DebugAtCursor, diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index d16820415b2..145945fab7f 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -69,6 +69,7 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { cmpPriority, isFailedState, isStateWithResult } from 'vs/workbench/contrib/testing/common/testingStates'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITestingContinuousRunService } from 'vs/workbench/contrib/testing/common/testingContinuousRunService'; const enum LastFocusState { Input, @@ -513,6 +514,7 @@ class TestingExplorerViewModel extends Disposable { @ITestResultService private readonly testResults: ITestResultService, @ITestingPeekOpener private readonly peekOpener: ITestingPeekOpener, @ITestProfileService private readonly testProfileService: ITestProfileService, + @ITestingContinuousRunService private readonly crService: ITestingContinuousRunService, ) { super(); @@ -563,6 +565,14 @@ class TestingExplorerViewModel extends Disposable { } })); + this._register(this.crService.onDidChange(testId => { + if (testId) { + // a continuous run test will sort to the top: + const elem = this.projection.value?.getElementByTestId(testId); + this.tree.resort(elem?.parent && this.tree.hasElement(elem.parent) ? elem.parent : null, false); + } + })); + this._register(onDidChangeVisibility(visible => { if (visible) { this.ensureProjection(); @@ -781,7 +791,7 @@ class TestingExplorerViewModel extends Disposable { return; } - const actions = getActionableElementActions(this.contextKeyService, this.menuService, this.testService, this.testProfileService, element); + const { actions } = getActionableElementActions(this.contextKeyService, this.menuService, this.testService, this.crService, this.testProfileService, element); this.contextMenuService.showContextMenu({ getAnchor: () => evt.anchor, getActions: () => actions.secondary, @@ -1007,13 +1017,21 @@ class TestsFilter implements ITreeFilter { } class TreeSorter implements ITreeSorter { - constructor(private readonly viewModel: TestingExplorerViewModel) { } + constructor( + private readonly viewModel: TestingExplorerViewModel, + @ITestingContinuousRunService private readonly crService: ITestingContinuousRunService, + ) { } public compare(a: TestExplorerTreeElement, b: TestExplorerTreeElement): number { if (a instanceof TestTreeErrorMessage || b instanceof TestTreeErrorMessage) { return (a instanceof TestTreeErrorMessage ? -1 : 0) + (b instanceof TestTreeErrorMessage ? 1 : 0); } + const crDelta = +this.crService.isSpecificallyEnabledFor(b.test.item.extId) - +this.crService.isSpecificallyEnabledFor(a.test.item.extId); + if (crDelta !== 0) { + return crDelta; + } + const durationDelta = (b.duration || 0) - (a.duration || 0); if (this.viewModel.viewSorting === TestExplorerViewSorting.ByDuration && durationDelta !== 0) { return durationDelta; @@ -1178,6 +1196,7 @@ class ErrorRenderer implements ITreeRenderer extends Disposable - implements ITreeRenderer { +class TestItemRenderer extends Disposable + implements ITreeRenderer { + public static readonly ID = 'testItem'; + constructor( private readonly actionRunner: TestExplorerActionRunner, @IMenuService private readonly menuService: IMenuService, @@ -1195,6 +1216,7 @@ abstract class ActionableItemTemplateData extends @ITestProfileService protected readonly profiles: ITestProfileService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @ITestingContinuousRunService private readonly crService: ITestingContinuousRunService, ) { super(); } @@ -1202,7 +1224,7 @@ abstract class ActionableItemTemplateData extends /** * @inheritdoc */ - abstract get templateId(): string; + public readonly templateId = TestItemRenderer.ID; /** * @inheritdoc @@ -1222,14 +1244,14 @@ abstract class ActionableItemTemplateData extends : undefined }); - return { wrapper, label, actionBar, icon, elementDisposable: [], templateDisposable: [actionBar] }; - } + const crListener = this.crService.onDidChange(changed => { + if (templateData.current && (!changed || changed === templateData.current.test.item.extId)) { + this.fillActionBar(templateData.current, templateData); + } + }); - /** - * @inheritdoc - */ - public renderElement({ element }: ITreeNode, _: number, data: IActionableElementTemplateData): void { - this.fillActionBar(element, data); + const templateData: IActionableElementTemplateData = { wrapper, label, actionBar, icon, elementDisposable: [], templateDisposable: [actionBar, crListener] }; + return templateData; } /** @@ -1243,34 +1265,25 @@ abstract class ActionableItemTemplateData extends /** * @inheritdoc */ - disposeElement(_element: ITreeNode, _: number, templateData: IActionableElementTemplateData): void { + disposeElement(_element: ITreeNode, _: number, templateData: IActionableElementTemplateData): void { dispose(templateData.elementDisposable); templateData.elementDisposable = []; } - private fillActionBar(element: T, data: IActionableElementTemplateData) { - const actions = getActionableElementActions(this.contextKeyService, this.menuService, this.testService, this.profiles, element); + private fillActionBar(element: TestItemTreeElement, data: IActionableElementTemplateData) { + const { actions, contextOverlay } = getActionableElementActions(this.contextKeyService, this.menuService, this.testService, this.crService, this.profiles, element); + data.actionBar.domNode.classList.toggle('testing-is-continuous-run', !!contextOverlay.getContextKeyValue(TestingContextKeys.isContinuousModeOn.key)); data.actionBar.clear(); data.actionBar.context = element; data.actionBar.push(actions.primary, { icon: true, label: false }); } -} - -class TestItemRenderer extends ActionableItemTemplateData { - public static readonly ID = 'testItem'; /** * @inheritdoc */ - get templateId(): string { - return TestItemRenderer.ID; - } - - /** - * @inheritdoc - */ - public override renderElement(node: ITreeNode, depth: number, data: IActionableElementTemplateData): void { - super.renderElement(node, depth, data); + public renderElement(node: ITreeNode, _depth: number, data: IActionableElementTemplateData): void { + data.current = node.element; + this.fillActionBar(node.element, data); const testHidden = this.testService.excluded.contains(node.element.test); data.wrapper.classList.toggle('test-is-hidden', testHidden); @@ -1321,6 +1334,7 @@ const getActionableElementActions = ( contextKeyService: IContextKeyService, menuService: IMenuService, testService: ITestService, + crService: ITestingContinuousRunService, profiles: ITestProfileService, element: TestItemTreeElement, ) => { @@ -1328,13 +1342,23 @@ const getActionableElementActions = ( const contextKeys: [string, unknown][] = getTestItemContextOverlay(test, test ? profiles.capabilitiesForTest(test) : 0); contextKeys.push(['view', Testing.ExplorerViewId]); if (test) { + const ctrl = testService.getTestController(test.controllerId); + const supportsCr = !!ctrl && profiles.getControllerProfiles(ctrl.id).some(p => p.supportsContinuousRun); contextKeys.push([ TestingContextKeys.canRefreshTests.key, - TestId.isRoot(test.item.extId) && testService.getTestController(test.item.extId)?.canRefresh.value - ]); - contextKeys.push([ + !!ctrl?.canRefresh.value && TestId.isRoot(test.item.extId), + ], [ TestingContextKeys.testItemIsHidden.key, testService.excluded.contains(test) + ], [ + TestingContextKeys.isContinuousModeOn.key, + supportsCr && crService.isSpecificallyEnabledFor(test.item.extId) + ], [ + TestingContextKeys.isParentRunningContinuously.key, + supportsCr && crService.isEnabledForAParentOf(test.item.extId) + ], [ + TestingContextKeys.supportsContinuousRun.key, + supportsCr, ]); } @@ -1349,7 +1373,7 @@ const getActionableElementActions = ( shouldForwardArgs: true, }, result, 'inline'); - return result; + return { actions: result, contextOverlay }; } finally { menu.dispose(); } diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index 66c65ab4816..fa3849e5949 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -56,6 +56,7 @@ export const enum TestCommandId { ClearTestResultsAction = 'testing.clearTestResults', CollapseAllAction = 'testing.collapseAll', ConfigureTestProfilesAction = 'testing.configureProfile', + ContinousRunUsingForTest = 'testing.continuousRunUsingForTest', DebugAction = 'testing.debug', DebugAllAction = 'testing.debugAll', DebugAtCursor = 'testing.debugAtCursor', @@ -88,6 +89,7 @@ export const enum TestCommandId { TestingSortByStatusAction = 'testing.sortByStatus', TestingViewAsListAction = 'testing.viewAsList', TestingViewAsTreeAction = 'testing.viewAsTree', + ToggleContinousRunForTest = 'testing.toggleContinuousRunForTest', ToggleInlineTestOutput = 'testing.toggleInlineTestOutput', UnhideAllTestsAction = 'testing.unhideAllTests', UnhideTestAction = 'testing.unhideTest', diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index 0dca2789405..879483949e2 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -19,6 +19,7 @@ export namespace TestingContextKeys { export const hasNonDefaultProfile = new RawContextKey('testing.hasNonDefaultProfile', false, { type: 'boolean', description: localize('testing.hasNonDefaultConfig', 'Indicates whether any test controller has registered a non-default configuration') }); export const hasConfigurableProfile = new RawContextKey('testing.hasConfigurableProfile', false, { type: 'boolean', description: localize('testing.hasConfigurableConfig', 'Indicates whether any test configuration can be configured') }); export const supportsContinuousRun = new RawContextKey('testing.supportsContinuousRun', false, { type: 'boolean', description: localize('testing.supportsContinuousRun', 'Indicates whether continous test running is supported') }); + export const isParentRunningContinuously = new RawContextKey('testing.isParentRunningContinuously', false, { type: 'boolean', description: localize('testing.isParentRunningContinuously', 'Indicates whether the parent of a test is continuously running, set in the menu context of test items') }); export const activeEditorHasTests = new RawContextKey('testing.activeEditorHasTests', false, { type: 'boolean', description: localize('testing.activeEditorHasTests', 'Indicates whether any tests are present in the current editor') }); export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey } = { @@ -36,7 +37,6 @@ export namespace TestingContextKeys { export const isRunning = new RawContextKey('testing.isRunning', false); export const isInPeek = new RawContextKey('testing.isInPeek', true); export const isPeekVisible = new RawContextKey('testing.isPeekVisible', false); - export const autoRun = new RawContextKey('testing.autoRun', false); export const peekItemType = new RawContextKey('peekItemType', undefined, { type: 'string', diff --git a/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts b/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts index 94c0567ab38..f803f57bc89 100644 --- a/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts +++ b/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; 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'; @@ -13,6 +13,8 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl'; import { ITestRunProfile } from 'vs/workbench/contrib/testing/common/testTypes'; +import { Emitter, Event } from 'vs/base/common/event'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; export const ITestingContinuousRunService = createDecorator('testingContinuousRunService'); @@ -25,22 +27,44 @@ export interface ITestingContinuousRunService { readonly lastRunProfileIds: ReadonlySet; /** - * Starts a continuous auto run with a specific profile or set of profiles. + * Fired when a test is added or removed from continous run, or when + * enablement is changed globally. */ - start(profile: ITestRunProfile[]): void; + onDidChange: Event; /** - * Stops any continuous run. + * Gets whether continous run is specifically enabled for the given test ID. */ - stop(): void; + isSpecificallyEnabledFor(testId: string): boolean; + + /** + * Gets whether continous run is specifically enabled for + * the given test ID, or any of its parents. + */ + isEnabledForAParentOf(testId: string): boolean; + + /** + * Starts a continuous auto run with a specific profile or set of profiles. + * Globally if no test is given, for a specific test otherwise. + */ + start(profile: ITestRunProfile[], testId?: string): void; + + /** + * Stops any continuous run + * Globally if no test is given, for a specific test otherwise. + */ + stop(testId?: string): void; } export class TestingContinuousRunService extends Disposable implements ITestingContinuousRunService { declare readonly _serviceBrand: undefined; + private readonly changeEmitter = new Emitter(); + private readonly running = new Map(); private readonly lastRun: StoredValue>; - private readonly cancellation = this._register(new MutableDisposable()); - private readonly isOn: IContextKey; + private readonly isGloballyOn: IContextKey; + + public readonly onDidChange = this.changeEmitter.event; public get lastRunProfileIds() { return this.lastRun.get(new Set()); @@ -52,7 +76,7 @@ export class TestingContinuousRunService extends Disposable implements ITestingC @IContextKeyService contextKeyService: IContextKeyService, ) { super(); - this.isOn = TestingContextKeys.isContinuousModeOn.bindTo(contextKeyService); + this.isGloballyOn = TestingContextKeys.isContinuousModeOn.bindTo(contextKeyService); this.lastRun = new StoredValue>({ key: 'lastContinuousRunProfileIds', scope: StorageScope.WORKSPACE, @@ -65,26 +89,63 @@ export class TestingContinuousRunService extends Disposable implements ITestingC } /** @inheritdoc */ - public start(profile: ITestRunProfile[]): void { - this.cancellation.value?.cancel(); - const cts = this.cancellation.value = new CancellationTokenSource(); + public isSpecificallyEnabledFor(testId: string): boolean { + return this.running.has(testId); + } - this.isOn.set(true); + /** @inheritdoc */ + public isEnabledForAParentOf(testId: string): boolean { + if (!this.running.size) { + return false; + } + + if (this.running.has(undefined)) { + return true; + } + + for (const part of TestId.fromString(testId).idsFromRoot()) { + if (this.running.has(part.toString())) { + return true; + } + } + + return false; + } + + /** @inheritdoc */ + public start(profile: ITestRunProfile[], testId?: string): void { + const cts = new CancellationTokenSource(); + + if (testId === undefined) { + this.isGloballyOn.set(true); + } + + this.running.get(testId)?.dispose(true); + this.running.set(testId, cts); this.lastRun.store(new Set(profile.map(p => p.profileId))); + this.testService.startContinuousRun({ continuous: true, targets: profile.map(p => ({ - testIds: [p.controllerId], // root id + testIds: [testId ?? p.controllerId], controllerId: p.controllerId, profileGroup: p.group, profileId: p.profileId })), }, cts.token); + + this.changeEmitter.fire(testId); } - stop(): void { - this.isOn.set(false); - this.cancellation.value?.cancel(); - this.cancellation.value = undefined; + /** @inheritdoc */ + public stop(testId?: string): void { + this.running.get(testId)?.dispose(true); + this.running.delete(testId); + + if (testId === undefined) { + this.isGloballyOn.set(false); + } + + this.changeEmitter.fire(testId); } }