From a3085833ad655d846c9a6ef616a4f228965ce9b3 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 6 Jan 2023 15:46:27 -0800 Subject: [PATCH] testing: initial implementation of continuous run First pass of https://github.com/microsoft/vscode/issues/134941 The UI is pretty minimal so far, just a new icon (though I have a proposal for some changes in the UX channel.) If there's more that one profile that supports continuous test runs, then we open a quickpick and ask the user which ones they want to use. The data flow is then fairly simple. We call the `profile.runHandler` with the appropriate request, but just doesn't "track" it like we do for normal test runs. Then, any `createTestRun` calls are tracked as ordinary extension-triggered test runs. Currently we don't do anything to associate these new test runs with the fact that they were from a specific auto-run request, but that could be added if need to in the future. --- .../api/browser/mainThreadTesting.ts | 1 + .../workbench/api/common/extHost.api.impl.ts | 3 +- .../workbench/api/common/extHost.protocol.ts | 5 +- src/vs/workbench/api/common/extHostTesting.ts | 75 ++++++++--- src/vs/workbench/api/common/extHostTypes.ts | 12 ++ .../api/test/browser/extHostTesting.test.ts | 1 + .../contrib/testing/browser/icons.ts | 2 + .../testing/browser/testExplorerActions.ts | 124 ++++++++++++++++-- .../testing/browser/testing.contribution.ts | 2 + .../testing/browser/testingOutputPeek.ts | 2 +- .../contrib/testing/common/configuration.ts | 8 +- .../contrib/testing/common/constants.ts | 5 +- .../testing/common/testProfileService.ts | 2 + .../testing/common/testResultService.ts | 2 +- .../contrib/testing/common/testService.ts | 12 +- .../contrib/testing/common/testServiceImpl.ts | 37 +++++- .../contrib/testing/common/testTypes.ts | 29 +++- .../testing/common/testingContextKeys.ts | 3 + .../common/testingContinuousRunService.ts | 90 +++++++++++++ .../test/common/testProfileService.test.ts | 1 + .../common/extensionsApiProposals.ts | 1 + .../vscode.proposed.testContinuousRun.d.ts | 66 ++++++++++ 22 files changed, 438 insertions(+), 45 deletions(-) create mode 100644 src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts create mode 100644 src/vscode-dts/vscode.proposed.testContinuousRun.d.ts diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index db0440295671b..717ebbddabab9 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -175,6 +175,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh refreshTests: token => this.proxy.$refreshTests(controllerId, token), configureRunProfile: id => this.proxy.$configureRunProfile(controllerId, id), runTests: (reqs, token) => this.proxy.$runControllerTests(reqs, token), + startContinuousRun: (reqs, token) => this.proxy.$startContinuousRun(reqs, token), expandTest: (testId, levels) => this.proxy.$expandTest(testId, isFinite(levels) ? levels : -1), }; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 23078a3cffc41..473b42e47bfb0 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -384,7 +384,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: tests const tests: typeof vscode.tests = { createTestController(provider, label, refreshHandler?: (token: vscode.CancellationToken) => Thenable | void) { - return extHostTesting.createTestController(provider, label, refreshHandler); + return extHostTesting.createTestController(extension, provider, label, refreshHandler); }, createTestObserver() { checkProposedApiEnabled(extension, 'testObserver'); @@ -1370,6 +1370,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I LinkedEditingRanges: extHostTypes.LinkedEditingRanges, TestResultState: extHostTypes.TestResultState, TestRunRequest: extHostTypes.TestRunRequest, + TestRunRequest2: extHostTypes.TestRunRequest2, TestMessage: extHostTypes.TestMessage, TestTag: extHostTypes.TestTag, TestRunProfileKind: extHostTypes.TestRunProfileKind, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 5433e2149a62e..8c789248827af 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -56,7 +56,7 @@ import { OutputChannelUpdateMode } from 'vs/workbench/services/output/common/out import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; import { IWorkspaceSymbol } from 'vs/workbench/contrib/search/common/search'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ISerializedTestResults, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, RunTestForControllerRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; +import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, IStartControllerTests, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/workbench/services/authentication/common/authentication'; @@ -2209,7 +2209,8 @@ export const enum ExtHostTestingResource { } export interface ExtHostTestingShape { - $runControllerTests(req: RunTestForControllerRequest[], token: CancellationToken): Promise<{ error?: string }[]>; + $runControllerTests(req: IStartControllerTests[], token: CancellationToken): Promise<{ error?: string }[]>; + $startContinuousRun(req: ICallProfileRunHandler[], token: CancellationToken): Promise<{ error?: string }[]>; $cancelExtensionTestRun(runId: string | undefined): void; /** Handles a diff of tests, as a result of a subscribeToDiffs() call */ $acceptDiff(diff: TestsDiffOp.Serialized[]): void; diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 68393c258e217..cc4aa15b791da 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -15,16 +15,18 @@ import { MarshalledId } from 'vs/base/common/marshallingIds'; import { deepFreeze } from 'vs/base/common/objects'; import { isDefined } from 'vs/base/common/types'; import { generateUuid } from 'vs/base/common/uuid'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostTestingShape, ILocationDto, MainContext, MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostTestItemCollection, TestItemImpl, TestItemRootImpl, toItemFromContext } from 'vs/workbench/api/common/extHostTestItem'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { TestRunProfileKind, TestRunRequest } from 'vs/workbench/api/common/extHostTypes'; +import { TestRunProfileKind, TestRunRequest2 } from 'vs/workbench/api/common/extHostTypes'; import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; -import { AbstractIncrementalTestCollection, CoverageDetails, IFileCoverage, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestItem, ITestItemContext, RunTestForControllerRequest, RunTestForControllerResult, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; +import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestItem, ITestItemContext, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; interface ControllerInfo { @@ -69,7 +71,7 @@ export class ExtHostTesting implements ExtHostTestingShape { /** * Implements vscode.test.registerTestProvider */ - public createTestController(controllerId: string, label: string, refreshHandler?: (token: CancellationToken) => Thenable | void): vscode.TestController { + public createTestController(extension: IExtensionDescription, controllerId: string, label: string, refreshHandler?: (token: CancellationToken) => Thenable | void): vscode.TestController { if (this.controllers.has(controllerId)) { throw new Error(`Attempt to insert a duplicate controller with ID "${controllerId}"`); } @@ -101,7 +103,7 @@ export class ExtHostTesting implements ExtHostTestingShape { get id() { return controllerId; }, - createRunProfile: (label, group, runHandler, isDefault, tag?: vscode.TestTag | undefined) => { + createRunProfile: (label, group, runHandler, isDefault, tag?: vscode.TestTag | undefined, supportsContinuousRun?: boolean) => { // Derive the profile ID from a hash so that the same profile will tend // to have the same hashes, allowing re-run requests to work across reloads. let profileId = hash(label); @@ -109,7 +111,11 @@ export class ExtHostTesting implements ExtHostTestingShape { profileId++; } - return new TestRunProfileImpl(this.proxy, profiles, controllerId, profileId, label, group, runHandler, isDefault, tag); + if (supportsContinuousRun !== undefined) { + checkProposedApiEnabled(extension, 'testContinuousRun'); + } + + return new TestRunProfileImpl(this.proxy, profiles, controllerId, profileId, label, group, runHandler, isDefault, tag, supportsContinuousRun); }, createTestItem(id, label, uri) { return new TestItemImpl(controllerId, id, label, uri); @@ -250,13 +256,31 @@ export class ExtHostTesting implements ExtHostTestingShape { /** * Runs tests with the given set of IDs. Allows for test from multiple * providers to be run. - * @override + * @inheritdoc */ - public async $runControllerTests(reqs: RunTestForControllerRequest[], token: CancellationToken): Promise { - return Promise.all(reqs.map(req => this.runControllerTestRequest(req, token))); + public async $runControllerTests(reqs: IStartControllerTests[], token: CancellationToken): Promise { + return Promise.all(reqs.map(req => this.runControllerTestRequest(req, false, token))); } - public async runControllerTestRequest(req: RunTestForControllerRequest, token: CancellationToken): Promise { + /** + * Starts continuous test runs with the given set of IDs. Allows for test from + * multiple providers to be run. + * @inheritdoc + */ + public async $startContinuousRun(reqs: IStartControllerTests[], token: CancellationToken): Promise { + const cts = new CancellationTokenSource(token); + const res = await Promise.all(reqs.map(req => this.runControllerTestRequest(req, true, cts.token))); + + // avoid returning until cancellation is requested, otherwise ipc disposes of the token + if (!token.isCancellationRequested && !res.some(r => r.error)) { + await new Promise(r => token.onCancellationRequested(r)); + } + + cts.dispose(true); + return res; + } + + private async runControllerTestRequest(req: ICallProfileRunHandler | ICallProfileRunHandler, isContinuous: boolean, token: CancellationToken): Promise { const lookup = this.controllers.get(req.controllerId); if (!lookup) { return {}; @@ -283,13 +307,14 @@ export class ExtHostTesting implements ExtHostTestingShape { return {}; } - const publicReq = new TestRunRequest( + const publicReq = new TestRunRequest2( includeTests.some(i => i.actual instanceof TestItemRootImpl) ? undefined : includeTests.map(t => t.actual), excludeTests.map(t => t.actual), profile, + isContinuous, ); - const tracker = this.runTracker.prepareForMainThreadTestRun( + const tracker = isStartControllerTests(req) && this.runTracker.prepareForMainThreadTestRun( publicReq, TestRunDto.fromInternal(req, lookup.collection), token, @@ -301,11 +326,13 @@ export class ExtHostTesting implements ExtHostTestingShape { } catch (e) { return { error: String(e) }; } finally { - if (tracker.hasRunningTasks && !token.isCancellationRequested) { - await Event.toPromise(tracker.onEnd); - } + if (tracker) { + if (tracker.hasRunningTasks && !token.isCancellationRequested) { + await Event.toPromise(tracker.onEnd); + } - tracker.dispose(); + tracker.dispose(); + } } } @@ -584,7 +611,7 @@ export class TestRunCoordinator { /** * Implements the public `createTestRun` API. */ - public createTestRun(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { + public createTestRun(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest2, name: string | undefined, persist: boolean): vscode.TestRun { const existing = this.tracked.get(request); if (existing) { return existing.createRun(name); @@ -596,6 +623,7 @@ export class TestRunCoordinator { const profile = tryGetProfileFromTestRunReq(request); this.proxy.$startedExtensionTestRun({ controllerId, + continuous: !!request.continuous, profile: profile && { group: profileGroupToBitset[profile.kind], id: profile.profileId }, exclude: request.exclude?.map(t => TestId.fromExtHostTestItem(t, collection.root.id).toString()) ?? [], id: dto.id, @@ -647,7 +675,7 @@ export class TestRunDto { ); } - public static fromInternal(request: RunTestForControllerRequest, collection: ExtHostTestItemCollection) { + public static fromInternal(request: IStartControllerTests, collection: ExtHostTestItemCollection) { return new TestRunDto( request.controllerId, request.runId, @@ -948,6 +976,17 @@ export class TestRunProfileImpl implements vscode.TestRunProfile { } } + public get supportsContinuousRun() { + return this._supportsContinuousRun; + } + + public set supportsContinuousRun(supports: boolean) { + if (supports !== this._supportsContinuousRun) { + this._supportsContinuousRun = supports; + this.#proxy.$updateTestRunConfig(this.controllerId, this.profileId, { supportsContinuousRun: supports }); + } + } + public get isDefault() { return this._isDefault; } @@ -993,6 +1032,7 @@ export class TestRunProfileImpl implements vscode.TestRunProfile { public runHandler: (request: vscode.TestRunRequest, token: vscode.CancellationToken) => Thenable | void, private _isDefault = false, public _tag: vscode.TestTag | undefined = undefined, + private _supportsContinuousRun = false, ) { this.#proxy = proxy; this.#profiles = profiles; @@ -1011,6 +1051,7 @@ export class TestRunProfileImpl implements vscode.TestRunProfile { group: groupBitset, isDefault: _isDefault, hasConfigurationHandler: false, + supportsContinuousRun: _supportsContinuousRun, }); } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 1f0ad6b07496d..c77b040b492be 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3766,6 +3766,18 @@ export class TestRunRequest implements vscode.TestRunRequest { ) { } } +@es5ClassCompat +export class TestRunRequest2 extends TestRunRequest implements vscode.TestRunRequest2 { + constructor( + include: vscode.TestItem[] | undefined = undefined, + exclude: vscode.TestItem[] | undefined = undefined, + profile: vscode.TestRunProfile | undefined = undefined, + public readonly continuous = false, + ) { + super(include, exclude, profile); + } +} + @es5ClassCompat export class TestMessage implements vscode.TestMessage { public expectedOutput?: string; diff --git a/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/src/vs/workbench/api/test/browser/extHostTesting.test.ts index a5ec96b4f7a1a..f4e445ae444e0 100644 --- a/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -701,6 +701,7 @@ suite('ExtHost Testing', () => { include: [single.root.id], exclude: [new TestId(['ctrlId', 'id-b']).toString()], persist: false, + continuous: false, }] ]); diff --git a/src/vs/workbench/contrib/testing/browser/icons.ts b/src/vs/workbench/contrib/testing/browser/icons.ts index d4a2e945addb6..adcd2a3dbf3a4 100644 --- a/src/vs/workbench/contrib/testing/browser/icons.ts +++ b/src/vs/workbench/contrib/testing/browser/icons.ts @@ -25,6 +25,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 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/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 711e397c7077e..1271a41394553 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -28,7 +28,7 @@ import { IActionableTestTreeElement, TestItemTreeElement } from 'vs/workbench/co import * as icons from 'vs/workbench/contrib/testing/browser/icons'; import type { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService'; -import { TestCommandId, TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants'; +import { TestCommandId, testConfigurationGroupNames, TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { InternalTestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; @@ -43,6 +43,8 @@ import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ITestingContinuousRunService } from 'vs/workbench/contrib/testing/common/testingContinuousRunService'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; const category = Categories.Test; @@ -52,8 +54,8 @@ const enum ActionOrder { Run, Debug, Coverage, + RunContinuous, RunUsing, - AutoRun, // Submenu: Collapse, @@ -279,6 +281,115 @@ export class ConfigureTestProfilesAction extends Action2 { } } +const continuousMenus = (whenIsContinuousOn: boolean): IAction2Options['menu'] => [ + { + id: MenuId.ViewTitle, + group: 'navigation', + order: ActionOrder.RunUsing, + when: ContextKeyExpr.and( + ContextKeyExpr.equals('view', Testing.ExplorerViewId), + TestingContextKeys.supportsContinuousRun.isEqualTo(true), + TestingContextKeys.isContinuousModeOn.isEqualTo(whenIsContinuousOn), + ), + }, + { + id: MenuId.CommandPalette, + when: TestingContextKeys.supportsContinuousRun.isEqualTo(true), + }, +]; + +class StopContinuousRunAction extends Action2 { + constructor() { + super({ + id: TestCommandId.StopContinousRun, + title: { value: localize('testing.stopContinuous', "Stop Continuous Run"), original: 'Stop Continuous Run' }, + category, + icon: icons.testingTurnContinuousRunOff, + menu: continuousMenus(true), + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(ITestingContinuousRunService).stop(); + } +} + +class StartContinuousRunAction extends Action2 { + constructor() { + super({ + id: TestCommandId.StartContinousRun, + title: { value: localize('testing.startContinuous', "Start Continuous Run"), original: 'Enable Continuous Run' }, + category, + icon: icons.testingTurnContinuousRunOn, + menu: continuousMenus(false), + }); + } + run(accessor: ServicesAccessor, ...args: any[]): void { + const controllerProfiles = accessor.get(ITestProfileService).all(); + const notificationService = accessor.get(INotificationService); + 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, + }); + } + } + } + + 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(); + } + }); + } +} + abstract class ExecuteSelectedAction extends ViewAction { constructor(options: IAction2Options, private readonly group: TestRunProfileBitset) { super({ @@ -312,7 +423,6 @@ abstract class ExecuteSelectedAction extends ViewAction { } export class RunSelectedAction extends ExecuteSelectedAction { - constructor() { super({ id: TestCommandId.RunSelectedAction, @@ -324,7 +434,6 @@ export class RunSelectedAction extends ExecuteSelectedAction { export class DebugSelectedAction extends ExecuteSelectedAction { constructor() { - super({ id: TestCommandId.DebugSelectedAction, title: localize('debugSelectedTests', 'Debug Tests'), @@ -1202,9 +1311,6 @@ export class CancelTestRefreshAction extends Action2 { } export const allTestActions = [ - // todo: these are disabled until we figure out how we want autorun to work - // AutoRunOffAction, - // AutoRunOnAction, CancelTestRefreshAction, CancelTestRunAction, ClearTestResultsAction, @@ -1232,12 +1338,14 @@ export const allTestActions = [ SearchForTestExtension, SelectDefaultTestProfiles, ShowMostRecentOutputAction, + StartContinuousRunAction, + StopContinuousRunAction, TestingSortByDurationAction, TestingSortByLocationAction, TestingSortByStatusAction, TestingViewAsListAction, TestingViewAsTreeAction, ToggleInlineTestOutput, - UnhideTestAction, UnhideAllTestsAction, + UnhideTestAction, ]; diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index 2bccbdac914b5..cc76910d35eca 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -43,10 +43,12 @@ import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { allTestActions, discoverAndRunTests } from './testExplorerActions'; import './testingConfigurationUi'; +import { ITestingContinuousRunService, TestingContinuousRunService } from 'vs/workbench/contrib/testing/common/testingContinuousRunService'; registerSingleton(ITestService, TestService, InstantiationType.Delayed); registerSingleton(ITestResultStorage, TestResultStorage, InstantiationType.Delayed); registerSingleton(ITestProfileService, TestProfileService, InstantiationType.Delayed); +registerSingleton(ITestingContinuousRunService, TestingContinuousRunService, InstantiationType.Delayed); registerSingleton(ITestResultService, TestResultService, InstantiationType.Delayed); registerSingleton(ITestExplorerFilterState, TestExplorerFilterState, InstantiationType.Delayed); registerSingleton(ITestingOutputTerminalService, TestingOutputTerminalService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index d499d9d2b60fa..b5e15bc21d2bf 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -243,7 +243,7 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener return; } - if (evt.result.request.isAutoRun && !getTestingConfiguration(this.configuration, TestingConfigKeys.AutoOpenPeekViewDuringAutoRun)) { + if (evt.result.request.continuous && !getTestingConfiguration(this.configuration, TestingConfigKeys.AutoOpenPeekViewDuringContinuousRun)) { return; } diff --git a/src/vs/workbench/contrib/testing/common/configuration.ts b/src/vs/workbench/contrib/testing/common/configuration.ts index de02f6f908f29..4156d0244dc1c 100644 --- a/src/vs/workbench/contrib/testing/common/configuration.ts +++ b/src/vs/workbench/contrib/testing/common/configuration.ts @@ -10,7 +10,7 @@ import { IConfigurationNode } from 'vs/platform/configuration/common/configurati export const enum TestingConfigKeys { AutoRunDelay = 'testing.autoRun.delay', AutoOpenPeekView = 'testing.automaticallyOpenPeekView', - AutoOpenPeekViewDuringAutoRun = 'testing.automaticallyOpenPeekViewDuringAutoRun', + AutoOpenPeekViewDuringContinuousRun = 'testing.automaticallyOpenPeekViewDuringAutoRun', OpenTesting = 'testing.openTesting', FollowRunningTest = 'testing.followRunningTest', DefaultGutterClickAction = 'testing.defaultGutterClickAction', @@ -63,8 +63,8 @@ export const testingConfiguation: IConfigurationNode = { localize('testing.automaticallyOpenPeekView.never', "Never automatically open."), ], }, - [TestingConfigKeys.AutoOpenPeekViewDuringAutoRun]: { - description: localize('testing.automaticallyOpenPeekViewDuringAutoRun', "Controls whether to automatically open the Peek view during auto-run mode."), + [TestingConfigKeys.AutoOpenPeekViewDuringContinuousRun]: { + description: localize('testing.automaticallyOpenPeekViewDuringContinuousRun', "Controls whether to automatically open the Peek view during continuous run mode."), type: 'boolean', default: false, }, @@ -122,7 +122,7 @@ export const testingConfiguation: IConfigurationNode = { export interface ITestingConfiguration { [TestingConfigKeys.AutoRunDelay]: number; [TestingConfigKeys.AutoOpenPeekView]: AutoOpenPeekViewWhen; - [TestingConfigKeys.AutoOpenPeekViewDuringAutoRun]: boolean; + [TestingConfigKeys.AutoOpenPeekViewDuringContinuousRun]: boolean; [TestingConfigKeys.FollowRunningTest]: boolean; [TestingConfigKeys.DefaultGutterClickAction]: DefaultGutterClickAction; [TestingConfigKeys.GutterEnabled]: boolean; diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index b996aa36be109..3edb527930ff4 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -76,13 +76,14 @@ export const enum TestCommandId { SearchForTestExtension = 'testing.searchForTestExtension', SelectDefaultTestProfiles = 'testing.selectDefaultTestProfiles', ShowMostRecentOutputAction = 'testing.showMostRecentOutput', + StartContinousRun = 'testing.startContinuousRun', + StopContinousRun = 'testing.stopContinuousRun', TestingSortByDurationAction = 'testing.sortByDuration', TestingSortByLocationAction = 'testing.sortByLocation', TestingSortByStatusAction = 'testing.sortByStatus', TestingViewAsListAction = 'testing.viewAsList', TestingViewAsTreeAction = 'testing.viewAsTree', - ToggleAutoRun = 'testing.toggleautoRun', ToggleInlineTestOutput = 'testing.toggleInlineTestOutput', - UnhideTestAction = 'testing.unhideTest', UnhideAllTestsAction = 'testing.unhideAllTests', + UnhideTestAction = 'testing.unhideTest', } diff --git a/src/vs/workbench/contrib/testing/common/testProfileService.ts b/src/vs/workbench/contrib/testing/common/testProfileService.ts index 60d5972e130de..b324d46961dc1 100644 --- a/src/vs/workbench/contrib/testing/common/testProfileService.ts +++ b/src/vs/workbench/contrib/testing/common/testProfileService.ts @@ -129,6 +129,7 @@ export class TestProfileService implements ITestProfileService { [TestRunProfileBitset.Coverage]: TestingContextKeys.hasCoverableTests.bindTo(contextKeyService), [TestRunProfileBitset.HasNonDefaultProfile]: TestingContextKeys.hasNonDefaultProfile.bindTo(contextKeyService), [TestRunProfileBitset.HasConfigurable]: TestingContextKeys.hasConfigurableProfile.bindTo(contextKeyService), + [TestRunProfileBitset.SupportsContinuousRun]: TestingContextKeys.supportsContinuousRun.bindTo(contextKeyService), }; this.refreshContextKeys(); @@ -286,6 +287,7 @@ export class TestProfileService implements ITestProfileService { for (const { profiles } of this.controllerProfiles.values()) { for (const profile of profiles) { allCapabilities |= allCapabilities & profile.group ? TestRunProfileBitset.HasNonDefaultProfile : profile.group; + allCapabilities |= profile.supportsContinuousRun ? TestRunProfileBitset.SupportsContinuousRun : 0; } } diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index 3eb95541eefc0..0df9e450a3ff6 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -146,7 +146,7 @@ export class TestResultService implements ITestResultService { isUiTriggered: false, targets: [], exclude: req.exclude, - isAutoRun: false, + continuous: req.continuous, }; if (profile) { diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index ebe7e71da93f0..fb48979e23ac9 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -12,7 +12,7 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IObservableValue, MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; -import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, ITestItemContext, ResolvedTestRunRequest, RunTestForControllerRequest, RunTestForControllerResult, TestItemExpandState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; +import { AbstractIncrementalTestCollection, ICallProfileRunHandler, IncrementalTestCollectionItem, InternalTestItem, ITestItemContext, ResolvedTestRunRequest, IStartControllerTests, IStartControllerTestsResult, TestItemExpandState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestExclusions } from 'vs/workbench/contrib/testing/common/testExclusions'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; @@ -27,7 +27,8 @@ export interface IMainThreadTestController { refreshTests(token: CancellationToken): Promise; configureRunProfile(profileId: number): void; expandTest(id: string, levels: number): Promise; - runTests(request: RunTestForControllerRequest[], token: CancellationToken): Promise; + startContinuousRun(request: ICallProfileRunHandler[], token: CancellationToken): Promise; + runTests(request: IStartControllerTests[], token: CancellationToken): Promise; } export interface IMainThreadTestCollection extends AbstractIncrementalTestCollection { @@ -198,7 +199,7 @@ export interface AmbiguousRunTestsRequest { /** Tests to exclude. If not given, the current UI excluded tests are used */ exclude?: InternalTestItem[]; /** Whether this was triggered from an auto run. */ - isAutoRun?: boolean; + continuous?: boolean; } export interface ITestService { @@ -254,6 +255,11 @@ export interface ITestService { */ cancelRefreshTests(): void; + /** + * Requests that tests be executed continuously, until the token is cancelled. + */ + startContinuousRun(req: ResolvedTestRunRequest, token: CancellationToken): Promise; + /** * Requests that tests be executed. */ diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index d383918413f29..915899261e8f3 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -133,7 +133,7 @@ export class TestService extends Disposable implements ITestService { const resolved: ResolvedTestRunRequest = { targets: [], exclude: req.exclude?.map(t => t.item.extId), - isAutoRun: req.isAutoRun, + continuous: req.continuous, }; // First, try to run the tests using the default run profiles... @@ -178,6 +178,41 @@ export class TestService extends Disposable implements ITestService { return this.runResolvedTests(resolved, token); } + /** @inheritdoc */ + public async startContinuousRun(req: ResolvedTestRunRequest, token: CancellationToken) { + if (!req.exclude) { + req.exclude = [...this.excluded.all]; + } + + const trust = await this.workspaceTrustRequestService.requestWorkspaceTrust({ + message: localize('testTrust', "Running tests may execute code in your workspace."), + }); + + if (!trust) { + return; + } + + const byController = groupBy(req.targets, (a, b) => a.controllerId.localeCompare(b.controllerId)); + const requests = byController.map( + group => this.testControllers.get(group[0].controllerId)?.startContinuousRun( + group.map(controlReq => ({ + excludeExtIds: req.exclude!.filter(t => !controlReq.testIds.includes(t)), + profileId: controlReq.profileId, + controllerId: controlReq.controllerId, + testIds: controlReq.testIds, + })), + token, + ).then(result => { + const errs = result.map(r => r.error).filter(isDefined); + if (errs.length) { + this.notificationService.error(localize('testError', 'An error occurred attempting to run tests: {0}', errs.join(' '))); + } + }) + ); + + await Promise.all(requests); + } + /** * @inheritdoc */ diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index 0eae7d8c52ce9..6a9a61a761589 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -26,6 +26,7 @@ export const enum TestRunProfileBitset { Coverage = 1 << 3, HasNonDefaultProfile = 1 << 4, HasConfigurable = 1 << 5, + SupportsContinuousRun = 1 << 6, } /** @@ -36,6 +37,8 @@ export const testRunProfileBitsetList = [ TestRunProfileBitset.Debug, TestRunProfileBitset.Coverage, TestRunProfileBitset.HasNonDefaultProfile, + TestRunProfileBitset.HasConfigurable, + TestRunProfileBitset.SupportsContinuousRun, ]; /** @@ -49,6 +52,7 @@ export interface ITestRunProfile { isDefault: boolean; tag: string | null; hasConfigurationHandler: boolean; + supportsContinuousRun: boolean; } /** @@ -63,7 +67,8 @@ export interface ResolvedTestRunRequest { profileId: number; }[]; exclude?: string[]; - isAutoRun?: boolean; + /** Whether this is a continuous test run */ + continuous?: boolean; /** Whether this was trigged by a user action in UI. Default=true */ isUiTriggered?: boolean; } @@ -78,20 +83,34 @@ export interface ExtensionRunTestsRequest { controllerId: string; profile?: { group: TestRunProfileBitset; id: number }; persist: boolean; + /** Whether this is a result of a continuous test run request */ + continuous: boolean; } /** - * Request from the main thread to run tests for a single controller. + * Request parameters a controller run handler. This is different than + * {@link IStartControllerTests}. The latter is used to ask for one or more test + * runs tracked directly by the renderer. + * + * This alone can be used to start an autorun, without a specific associated runId. */ -export interface RunTestForControllerRequest { - runId: string; +export interface ICallProfileRunHandler { controllerId: string; profileId: number; excludeExtIds: string[]; testIds: string[]; } -export interface RunTestForControllerResult { +export const isStartControllerTests = (t: ICallProfileRunHandler | IStartControllerTests): t is IStartControllerTests => 'runIn' in t; + +/** + * Request from the main thread to run tests for a single controller. + */ +export interface IStartControllerTests extends ICallProfileRunHandler { + runId: string; +} + +export interface IStartControllerTestsResult { error?: string; } diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index 5bdcb36c2efa1..0dca278940526 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -12,11 +12,13 @@ export namespace TestingContextKeys { export const providerCount = new RawContextKey('testing.providerCount', 0); export const canRefreshTests = new RawContextKey('testing.canRefresh', false, { type: 'boolean', description: localize('testing.canRefresh', 'Indicates whether any test controller has an attached refresh handler.') }); export const isRefreshingTests = new RawContextKey('testing.isRefreshing', false, { type: 'boolean', description: localize('testing.isRefreshing', 'Indicates whether any test controller is currently refreshing tests.') }); + export const isContinuousModeOn = new RawContextKey('testing.isContinuousModeOn', false, { type: 'boolean', description: localize('testing.isContinuousModeOn', 'Indicates whether continuous test mode is on.') }); 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 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 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 } = { @@ -25,6 +27,7 @@ export namespace TestingContextKeys { [TestRunProfileBitset.Debug]: hasDebuggableTests, [TestRunProfileBitset.HasNonDefaultProfile]: hasNonDefaultProfile, [TestRunProfileBitset.HasConfigurable]: hasConfigurableProfile, + [TestRunProfileBitset.SupportsContinuousRun]: supportsContinuousRun, }; export const hasAnyResults = new RawContextKey('testing.hasAnyResults', false); diff --git a/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts b/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts new file mode 100644 index 0000000000000..94c0567ab3851 --- /dev/null +++ b/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Disposable, MutableDisposable } 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'; +import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; +import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; +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'; + +export const ITestingContinuousRunService = createDecorator('testingContinuousRunService'); + +export interface ITestingContinuousRunService { + readonly _serviceBrand: undefined; + + /** + * Gets a list of the last test profiles that were continuously run in the workspace. + */ + readonly lastRunProfileIds: ReadonlySet; + + /** + * Starts a continuous auto run with a specific profile or set of profiles. + */ + start(profile: ITestRunProfile[]): void; + + /** + * Stops any continuous run. + */ + stop(): void; +} + +export class TestingContinuousRunService extends Disposable implements ITestingContinuousRunService { + declare readonly _serviceBrand: undefined; + + private readonly lastRun: StoredValue>; + private readonly cancellation = this._register(new MutableDisposable()); + private readonly isOn: IContextKey; + + public get lastRunProfileIds() { + return this.lastRun.get(new Set()); + } + + constructor( + @ITestService private readonly testService: TestService, + @IStorageService storageService: IStorageService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + super(); + this.isOn = TestingContextKeys.isContinuousModeOn.bindTo(contextKeyService); + this.lastRun = new StoredValue>({ + key: 'lastContinuousRunProfileIds', + scope: StorageScope.WORKSPACE, + target: StorageTarget.USER, + serialization: { + deserialize: v => new Set(JSON.parse(v)), + serialize: v => JSON.stringify([...v]) + }, + }, storageService); + } + + /** @inheritdoc */ + public start(profile: ITestRunProfile[]): void { + this.cancellation.value?.cancel(); + const cts = this.cancellation.value = new CancellationTokenSource(); + + this.isOn.set(true); + this.lastRun.store(new Set(profile.map(p => p.profileId))); + this.testService.startContinuousRun({ + continuous: true, + targets: profile.map(p => ({ + testIds: [p.controllerId], // root id + controllerId: p.controllerId, + profileGroup: p.group, + profileId: p.profileId + })), + }, cts.token); + } + + stop(): void { + this.isOn.set(false); + this.cancellation.value?.cancel(); + this.cancellation.value = undefined; + } +} diff --git a/src/vs/workbench/contrib/testing/test/common/testProfileService.test.ts b/src/vs/workbench/contrib/testing/test/common/testProfileService.test.ts index f12f301beae55..129ce196195c7 100644 --- a/src/vs/workbench/contrib/testing/test/common/testProfileService.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testProfileService.test.ts @@ -31,6 +31,7 @@ suite('Workbench - TestProfileService', () => { profileId: idCounter++, hasConfigurationHandler: false, tag: null, + supportsContinuousRun: false, ...profile, }; diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index f0f0b916875b6..f7b1daf6f699a 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -63,6 +63,7 @@ export const allApiProposals = Object.freeze({ terminalDataWriteEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts', terminalDimensions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDimensions.d.ts', terminalQuickFixProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts', + testContinuousRun: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testContinuousRun.d.ts', testCoverage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testCoverage.d.ts', testObserver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', diff --git a/src/vscode-dts/vscode.proposed.testContinuousRun.d.ts b/src/vscode-dts/vscode.proposed.testContinuousRun.d.ts new file mode 100644 index 0000000000000..bb6240132a96d --- /dev/null +++ b/src/vscode-dts/vscode.proposed.testContinuousRun.d.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + export interface TestRunProfile { + /** + * Whether this profile supports continuous running of requests. If so, + * then {@link TestRunRequest.continuous} may be set to `true`. Defaults + * to false. + */ + supportsContinuousRun: boolean; + + /** + * Handler called to start a test run. When invoked, the function should call + * {@link TestController.createTestRun} at least once, and all test runs + * associated with the request should be created before the function returns + * or the returned promise is resolved. + * + * If {@link supportsContinuousRun} is set, then {@link TestRunRequest2.continuous} + * may be `true`. In this case, the profile should observe changes to + * source code and create new test runs by calling {@link TestController.createTestRun}, + * until the cancellation is requested on the `token`. + * + * @param request Request information for the test run. + * @param cancellationToken Token that signals the used asked to abort the + * test run. If cancellation is requested on this token, all {@link TestRun} + * instances associated with the request will be + * automatically cancelled as well. + */ + runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable | void; + } + + export interface TestController { + /** + * Creates a profile used for running tests. Extensions must create + * at least one profile in order for tests to be run. + * @param label A human-readable label for this profile. + * @param kind Configures what kind of execution this profile manages. + * @param runHandler Function called to start a test run. + * @param isDefault Whether this is the default action for its kind. + * @param tag Profile test tag. + * @param supportsContinuousRun Whether the profile supports continuous running. + * @returns An instance of a {@link TestRunProfile}, which is automatically + * associated with this controller. + */ + createRunProfile(label: string, kind: TestRunProfileKind, runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable | void, isDefault?: boolean, tag?: TestTag, supportsContinuousRun?: boolean): TestRunProfile; + } + + export class TestRunRequest2 extends TestRunRequest { + /** + * Whether the profile should run continuously as source code changes. Only + * relevant for profiles that set {@link TestRunProfile.supportsContinuousRun}. + */ + readonly continuous?: boolean; + + /** + * @param tests Array of specific tests to run, or undefined to run all tests + * @param exclude An array of tests to exclude from the run. + * @param profile The run profile used for this request. + * @param continuous Whether to run tests continuously as source changes. + */ + constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean); + } +}