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
This commit is contained in:
Connor Peet
2023-04-04 20:03:50 -07:00
committed by GitHub
parent 7a1e910d68
commit 8f74fbfd1f
7 changed files with 324 additions and 115 deletions
@@ -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<TestResultState, ThemeIcon>([
@@ -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;
}
@@ -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<any> {
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<any> {
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<Readonly<{
controller?: IMainThreadTestController;
profiles: ITestRunProfile[];
}>>,
): Promise<ITestRunProfile[]> {
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<IQuickPickItem & { profile: ITestRunProfile }>();
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<void> {
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<IQuickPickItem & { profile: ITestRunProfile }>();
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,
@@ -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<TestExplorerTreeElement> {
}
class TreeSorter implements ITreeSorter<TestExplorerTreeElement> {
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<TestTreeErrorMessage, FuzzyScore, I
}
interface IActionableElementTemplateData {
current?: TestItemTreeElement;
label: HTMLElement;
icon: HTMLElement;
wrapper: HTMLElement;
@@ -1186,8 +1205,10 @@ interface IActionableElementTemplateData {
templateDisposable: IDisposable[];
}
abstract class ActionableItemTemplateData<T extends TestItemTreeElement> extends Disposable
implements ITreeRenderer<T, FuzzyScore, IActionableElementTemplateData> {
class TestItemRenderer extends Disposable
implements ITreeRenderer<TestItemTreeElement, FuzzyScore, IActionableElementTemplateData> {
public static readonly ID = 'testItem';
constructor(
private readonly actionRunner: TestExplorerActionRunner,
@IMenuService private readonly menuService: IMenuService,
@@ -1195,6 +1216,7 @@ abstract class ActionableItemTemplateData<T extends TestItemTreeElement> 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<T extends TestItemTreeElement> extends
/**
* @inheritdoc
*/
abstract get templateId(): string;
public readonly templateId = TestItemRenderer.ID;
/**
* @inheritdoc
@@ -1222,14 +1244,14 @@ abstract class ActionableItemTemplateData<T extends TestItemTreeElement> 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<T, FuzzyScore>, _: 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<T extends TestItemTreeElement> extends
/**
* @inheritdoc
*/
disposeElement(_element: ITreeNode<T, FuzzyScore>, _: number, templateData: IActionableElementTemplateData): void {
disposeElement(_element: ITreeNode<TestItemTreeElement, FuzzyScore>, _: 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<TestItemTreeElement> {
public static readonly ID = 'testItem';
/**
* @inheritdoc
*/
get templateId(): string {
return TestItemRenderer.ID;
}
/**
* @inheritdoc
*/
public override renderElement(node: ITreeNode<TestItemTreeElement, FuzzyScore>, depth: number, data: IActionableElementTemplateData): void {
super.renderElement(node, depth, data);
public renderElement(node: ITreeNode<TestItemTreeElement, FuzzyScore>, _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();
}
@@ -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',
@@ -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<boolean> } = {
@@ -36,7 +37,6 @@ export namespace TestingContextKeys {
export const isRunning = new RawContextKey<boolean>('testing.isRunning', false);
export const isInPeek = new RawContextKey<boolean>('testing.isInPeek', true);
export const isPeekVisible = new RawContextKey<boolean>('testing.isPeekVisible', false);
export const autoRun = new RawContextKey<boolean>('testing.autoRun', false);
export const peekItemType = new RawContextKey<string | undefined>('peekItemType', undefined, {
type: 'string',
@@ -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<ITestingContinuousRunService>('testingContinuousRunService');
@@ -25,22 +27,44 @@ export interface ITestingContinuousRunService {
readonly lastRunProfileIds: ReadonlySet<number>;
/**
* 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<string | undefined>;
/**
* 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<string | undefined>();
private readonly running = new Map<string | undefined, CancellationTokenSource>();
private readonly lastRun: StoredValue<Set<number>>;
private readonly cancellation = this._register(new MutableDisposable<CancellationTokenSource>());
private readonly isOn: IContextKey<boolean>;
private readonly isGloballyOn: IContextKey<boolean>;
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<Set<number>>({
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);
}
}