Skip to content

Commit

Permalink
Filtering breakpoints on exception in debugger (jupyterlab#13601)
Browse files Browse the repository at this point in the history
* Adds a filter selection to the ExceptionBreakpoints of the debugger

* Add test on this feature

* Disable the button if the debugger is stopped

* Fix tests

* fix eslint

* Fix the command for the commandPalette

* Fix debugger tests

* Update packages/debugger/src/panels/breakpoints/index.ts

Co-authored-by: Frédéric Collonval <[email protected]>

* Update packages/debugger-extension/src/index.ts

Co-authored-by: Frédéric Collonval <[email protected]>

* Add docstring and clean the code

* Fix wrong type

* Replace some screenshots tests by CSS matching

* Allows selecting several filters, as available from DAP

* Await for gutters before select them in UI test

Co-authored-by: Frédéric Collonval <[email protected]>
  • Loading branch information
brichet and fcollonval authored Jan 10, 2023
1 parent 88fa178 commit c725a22
Show file tree
Hide file tree
Showing 17 changed files with 438 additions and 119 deletions.
4 changes: 4 additions & 0 deletions galata/src/helpers/notebook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,10 @@ export class NotebookHelper {
}

const panel = await this.activity.getPanel();
await panel!.waitForSelector(
'.cm-gutters > .cm-gutter.cm-breakpoint-gutter > .cm-gutterElement',
{ state: 'attached' }
);
const gutters = await panel!.$$(
'.cm-gutters > .cm-gutter.cm-breakpoint-gutter > .cm-gutterElement'
);
Expand Down
68 changes: 68 additions & 0 deletions galata/test/documentation/debugger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,74 @@ test.describe('Debugger', () => {
await page.click('button[title^=Continue]');
});

test('Breakpoints on exception', async ({ page, tmpPath }) => {
await page.goto(`tree/${tmpPath}`);

await createNotebook(page);

await page.debugger.switchOn();
await page.waitForCondition(() => page.debugger.isOpen());
await setSidebarWidth(page, 251, 'right');

await expect(page.locator('button.jp-PauseOnExceptions')).not.toHaveClass(
/lm-mod-toggled/
);
await page.locator('button.jp-PauseOnExceptions').click();
const menu = page.locator('.jp-PauseOnExceptions-menu');
await expect(menu).toBeVisible();
await expect(menu.locator('li.lm-Menu-item')).toHaveCount(3);
await expect(menu.locator('li.lm-Menu-item.lm-mod-toggled')).toHaveCount(0);

await menu
.locator('li div.lm-Menu-itemLabel:text("userUnhandled")')
.click();

await expect(page.locator('button.jp-PauseOnExceptions')).toHaveClass(
/lm-mod-toggled/
);

await page.notebook.enterCellEditingMode(0);
const keyboard = page.keyboard;
await keyboard.press('Control+A');
await keyboard.type('try:\n1/0\n', { delay: 100 });
await keyboard.press('Backspace');
await keyboard.type('except:\n2/0\n', { delay: 100 });

void page.notebook.runCell(0);

// Wait to be stopped on the breakpoint
await page.debugger.waitForCallStack();
expect(
await page.screenshot({
clip: { y: 110, x: 300, width: 300, height: 80 }
})
).toMatchSnapshot('debugger_stop_on_unhandled_exception.png');

await page.click('button[title^=Continue]');
await page.notebook.waitForRun(0);

await page.locator('button.jp-PauseOnExceptions').click();

await expect(menu.locator('li.lm-Menu-item.lm-mod-toggled')).toHaveCount(1);
await expect(
menu.locator('li:has(div.lm-Menu-itemLabel:text("userUnhandled"))')
).toHaveClass(/lm-mod-toggled/);

await menu.locator('li div.lm-Menu-itemLabel:text("raised")').click();

void page.notebook.runCell(0);

// Wait to be stopped on the breakpoint
await page.debugger.waitForCallStack();
expect(
await page.screenshot({
clip: { y: 110, x: 300, width: 300, height: 80 }
})
).toMatchSnapshot('debugger_stop_on_raised_exception.png');
await page.click('button[title^=Continue]');
await page.click('button[title^=Continue]');
});

test('Debugger sidebar', async ({ page, tmpPath }) => {
await page.goto(`tree/${tmpPath}`);

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified galata/test/documentation/screenshots/debugger-breakpoints.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 28 additions & 18 deletions packages/debugger-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import {
Clipboard,
ICommandPalette,
InputDialog,
IThemeManager,
MainAreaWidget,
sessionContextDialogs,
Expand Down Expand Up @@ -532,7 +533,7 @@ const sidebar: JupyterFrontEndPlugin<IDebugger.ISidebar> = {

const breakpointsCommands = {
registry: commands,
pause: CommandIDs.pauseOnExceptions
pauseOnExceptions: CommandIDs.pauseOnExceptions
};

const sidebar = new Debugger.Sidebar({
Expand Down Expand Up @@ -738,24 +739,33 @@ const main: JupyterFrontEndPlugin<void> = {
});

commands.addCommand(CommandIDs.pauseOnExceptions, {
label: trans.__('Enable / Disable pausing on exceptions'),
caption: () =>
service.isStarted
? service.pauseOnExceptionsIsValid()
? service.isPausingOnExceptions
? trans.__('Disable pausing on exceptions')
: trans.__('Enable pausing on exceptions')
: trans.__('Kernel does not support pausing on exceptions.')
: trans.__('Enable / Disable pausing on exceptions'),
className: 'jp-PauseOnExceptions',
icon: Debugger.Icons.pauseOnExceptionsIcon,
isToggled: () => {
return service.isPausingOnExceptions;
},
label: args => (args.filter as string) || 'Breakpoints on exception',
caption: args => args.description as string,
isToggled: args =>
service.session?.isPausingOnException(args.filter as string) || false,
isEnabled: () => service.pauseOnExceptionsIsValid(),
execute: async () => {
await service.pauseOnExceptions(!service.isPausingOnExceptions);
commands.notifyCommandChanged();
execute: async args => {
if (args?.filter) {
let filter = args.filter as string;
await service.pauseOnExceptionsFilter(filter as string);
} else {
let items: string[] = [];
service.session?.exceptionBreakpointFilters?.forEach(
availableFilter => {
items.push(availableFilter.filter);
}
);
const result = await InputDialog.getMultipleItems({
title: trans.__('Select a filter for breakpoints on exception'),
items: items,
defaults: service.session?.currentExceptionFilters || []
});

let filters = result.button.accept ? result.value : null;
if (filters !== null) {
await service.pauseOnExceptions(filters);
}
}
}
});

Expand Down
5 changes: 4 additions & 1 deletion packages/debugger/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,10 @@ export class DebuggerHandler implements DebuggerHandler.IHandler {

// update the active debug session
if (!this._service.session) {
this._service.session = new Debugger.Session({ connection });
this._service.session = new Debugger.Session({
connection,
config: this._service.config
});
} else {
this._previousConnection = this._service.session!.connection?.kernel
? this._service.session.connection
Expand Down
6 changes: 6 additions & 0 deletions packages/debugger/src/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import variableSvgStr from '../style/icons/variable.svg';
import pauseSvgStr from '../style/icons/pause.svg';
import viewBreakpointSvgStr from '../style/icons/view-breakpoint.svg';
import openKernelSourceSvgStr from '../style/icons/open-kernel-source.svg';
import exceptionSvgStr from '../style/icons/exceptions.svg';

export {
runIcon as continueIcon,
Expand All @@ -22,6 +23,11 @@ export const closeAllIcon = new LabIcon({
svgstr: closeAllSvgStr
});

export const exceptionIcon = new LabIcon({
name: 'debugger:pause-on-exception',
svgstr: exceptionSvgStr
});

export const pauseIcon = new LabIcon({
name: 'debugger:pause',
svgstr: pauseSvgStr
Expand Down
25 changes: 12 additions & 13 deletions packages/debugger/src/panels/breakpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@

import { Dialog, showDialog } from '@jupyterlab/apputils';
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
import {
CommandToolbarButton,
PanelWithToolbar,
ToolbarButton
} from '@jupyterlab/ui-components';
import { PanelWithToolbar, ToolbarButton } from '@jupyterlab/ui-components';
import { CommandRegistry } from '@lumino/commands';
import { Signal } from '@lumino/signaling';
import { Panel } from '@lumino/widgets';
import { closeAllIcon } from '../../icons';
import { closeAllIcon, exceptionIcon } from '../../icons';
import { IDebugger } from '../../tokens';
import { BreakpointsBody } from './body';
import { PauseOnExceptionsWidget } from './pauseonexceptions';

/**
* A Panel to show a list of breakpoints.
*/
Expand All @@ -32,11 +30,12 @@ export class Breakpoints extends PanelWithToolbar {
const body = new BreakpointsBody(model);

this.toolbar.addItem(
'pause',
new CommandToolbarButton({
commands: commands.registry,
id: commands.pause,
label: ''
'pauseOnException',
new PauseOnExceptionsWidget({
service: service,
commands: commands,
icon: exceptionIcon,
tooltip: trans.__('Pause on exception filter')
})
);

Expand Down Expand Up @@ -86,9 +85,9 @@ export namespace Breakpoints {
registry: CommandRegistry;

/**
* The pause command ID.
* The pause on exceptions command ID.
*/
pause: string;
pauseOnExceptions: string;
}
/**
* Instantiation options for `Breakpoints`.
Expand Down
141 changes: 141 additions & 0 deletions packages/debugger/src/panels/breakpoints/pauseonexceptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import * as React from 'react';
import {
MenuSvg,
ToolbarButton,
ToolbarButtonComponent
} from '@jupyterlab/ui-components';
import { IDebugger } from '../../tokens';
import { Breakpoints } from './index';

const PAUSE_ON_EXCEPTION_CLASS = 'jp-debugger-pauseOnExceptions';
const PAUSE_ON_EXCEPTION_BUTTON_CLASS = 'jp-PauseOnExceptions';
const PAUSE_ON_EXCEPTION_MENU_CLASS = 'jp-PauseOnExceptions-menu';

/**
* A button which display a menu on click, to select the filter.
*/
export class PauseOnExceptionsWidget extends ToolbarButton {
/**
* Constructor of the button.
*/
constructor(props: PauseOnExceptions.IProps) {
super();
this._menu = new PauseOnExceptionsMenu({
service: props.service,
commands: {
registry: props.commands.registry,
pauseOnExceptions: props.commands.pauseOnExceptions
}
});

this.node.className = PAUSE_ON_EXCEPTION_CLASS;

this._props = props;
this._props.className = PAUSE_ON_EXCEPTION_BUTTON_CLASS;
this._props.service.eventMessage.connect((_, event): void => {
if (event.event === 'initialized' || event.event === 'terminated') {
this.onChange();
}
}, this);
this._props.enabled = this._props.service.pauseOnExceptionsIsValid();
this._props.service.pauseOnExceptionChanged.connect(this.onChange, this);
}

/**
* Called when the debugger is initialized or the filter changed.
*/
onChange() {
const session = this._props.service.session;
const exceptionBreakpointFilters = session?.exceptionBreakpointFilters;
this._props.className = PAUSE_ON_EXCEPTION_BUTTON_CLASS;
if (this._props.service.session?.isStarted && exceptionBreakpointFilters) {
if (session.isPausingOnException()) {
this._props.className += ' lm-mod-toggled';
}
this._props.enabled = true;
} else {
this._props.enabled = false;
}
this.update();
}

/**
* open menu on click.
*/
onclick = () => {
this._menu.open(
this.node.getBoundingClientRect().left,
this.node.getBoundingClientRect().bottom
);
};

render(): JSX.Element {
return <ToolbarButtonComponent {...this._props} onClick={this.onclick} />;
}

private _menu: PauseOnExceptionsMenu;
private _props: PauseOnExceptions.IProps;
}

/**
* A menu with all the available filter from the debugger as entries.
*/
export class PauseOnExceptionsMenu extends MenuSvg {
/**
* The constructor of the menu.
*/
constructor(props: PauseOnExceptions.IProps) {
super({ commands: props.commands.registry });
this._service = props.service;
this._command = props.commands.pauseOnExceptions;

props.service.eventMessage.connect((_, event): void => {
if (event.event === 'initialized') {
this._build();
}
}, this);

this._build();
this.addClass(PAUSE_ON_EXCEPTION_MENU_CLASS);
}

private _build(): void {
this.clearItems();
const exceptionsBreakpointFilters =
this._service.session?.exceptionBreakpointFilters ?? [];
exceptionsBreakpointFilters.map((filter, _) => {
this.addItem({
command: this._command,
args: {
filter: filter.filter,
description: filter.description as string
}
});
});
}

private _service: IDebugger;
private _command: string;
}

/**
* A namespace for the widget.
*/
export namespace PauseOnExceptions {
/**
* The properties of the widget and menu.
*/
export interface IProps extends ToolbarButtonComponent.IProps {
/**
* The debugger service linked to the widget.
*/
service: IDebugger;
/**
* The commands registry and the command ID associated to the menu.
*/
commands: Breakpoints.ICommands;
}
}
Loading

0 comments on commit c725a22

Please sign in to comment.