From 63c419d9aec1a3762dd6045a43fd6fbce0e35d50 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Wed, 25 Oct 2017 20:41:25 -0400 Subject: [PATCH 1/5] Fix syncing of breakpoints when adding conditions and add unit tests. --- spyder/widgets/sourcecode/codeeditor.py | 29 +- .../sourcecode/tests/test_breakpoints.py | 259 ++++++++++++++++++ 2 files changed, 270 insertions(+), 18 deletions(-) create mode 100644 spyder/widgets/sourcecode/tests/test_breakpoints.py diff --git a/spyder/widgets/sourcecode/codeeditor.py b/spyder/widgets/sourcecode/codeeditor.py index 36d11fdd464..a2fb9182f9a 100644 --- a/spyder/widgets/sourcecode/codeeditor.py +++ b/spyder/widgets/sourcecode/codeeditor.py @@ -1313,37 +1313,27 @@ def add_remove_breakpoint(self, line_number=None, condition=None, else: block = self.document().findBlockByNumber(line_number-1) data = block.userData() - if data: - data.breakpoint = not data.breakpoint - old_breakpoint_condition = data.breakpoint_condition - data.breakpoint_condition = None - else: + if not data: data = BlockUserData(self) data.breakpoint = True - old_breakpoint_condition = None + elif not edit_condition: + data.breakpoint = not data.breakpoint + data.breakpoint_condition = None if condition is not None: data.breakpoint_condition = condition if edit_condition: - data.breakpoint = True condition = data.breakpoint_condition - if old_breakpoint_condition is not None: - condition = old_breakpoint_condition condition, valid = QInputDialog.getText(self, _('Breakpoint'), _("Condition:"), QLineEdit.Normal, condition) - if valid: - condition = str(condition) - if not condition: - condition = None - data.breakpoint_condition = condition - else: - data.breakpoint_condition = old_breakpoint_condition + if not valid: return + data.breakpoint = True + data.breakpoint_condition = str(condition) if condition else None if data.breakpoint: text = to_text_string(block.text()).strip() - if len(text) == 0 or text.startswith('#') or text.startswith('"') \ - or text.startswith("'"): + if len(text) == 0 or text.startswith(('#', '"', "'")): data.breakpoint = False block.setUserData(data) self.linenumberarea.update() @@ -1368,6 +1358,9 @@ def clear_breakpoints(self): data.breakpoint = False # data.breakpoint_condition = None # not necessary, but logical if data.is_empty(): + # This is not calling the __del__ in BlockUserData. Not + # sure if it's supposed to or not, but that seems to be the + # intent. del data def set_breakpoints(self, breakpoints): diff --git a/spyder/widgets/sourcecode/tests/test_breakpoints.py b/spyder/widgets/sourcecode/tests/test_breakpoints.py new file mode 100644 index 00000000000..80db9e03b7a --- /dev/null +++ b/spyder/widgets/sourcecode/tests/test_breakpoints.py @@ -0,0 +1,259 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# + +""" +Tests for breakpoints. +""" + +import unittest.mock as mock + +# Third party imports +import pytest +from qtpy.QtGui import QTextCursor + +# Local imports +from spyder.py3compat import to_text_string +import spyder.widgets.sourcecode.codeeditor as codeeditor + + +# --- Helper methods +# ----------------------------------------------------------------------------- +def reset_emits(editor): + "Reset signal mocks." + editor.linenumberarea.reset_mock() + editor.sig_flags_changed.reset_mock() + editor.breakpoints_changed.reset_mock() + + +def editor_assert_helper(editor, block=None, bp=False, bpc=None, emits=True): + """Run the tests for call to add_remove_breakpoint. + + Args: + editor: CodeEditor instance. + block: Block of text. + bp: Is breakpoint active? + bpc: Condition set for breakpoint. + emits: Boolean to test if signals were emitted? + """ + data = block.userData() + assert data.breakpoint == bp + assert data.breakpoint_condition == bpc + if emits: + editor.linenumberarea.update.assert_called() + editor.sig_flags_changed.emit.assert_called() + editor.breakpoints_changed.emit.assert_called() + else: + editor.linenumberarea.update.assert_not_called() + editor.sig_flags_changed.emit.assert_not_called() + editor.breakpoints_changed.emit.assert_not_called() + + +# --- Fixtures +# ----------------------------------------------------------------------------- +@pytest.fixture +def code_editor_bot(qtbot): + """Create code editor with default Python code.""" + editor = codeeditor.CodeEditor(parent=None) + indent_chars = ' ' * 4 + tab_stop_width_spaces = 4 + editor.setup_editor(language='Python', indent_chars=indent_chars, + tab_stop_width_spaces=tab_stop_width_spaces) + # Mock the screen updates and signal emits to test when they've been + # called. + editor.linenumberarea = mock.Mock() + editor.sig_flags_changed = mock.Mock() + editor.breakpoints_changed = mock.Mock() + text = ('def f1(a, b):\n' + '"Double quote string."\n' + '\n' # Blank line. + ' c = a * b\n' + ' return c\n' + ) + editor.set_text(text) + return editor, qtbot + + +# --- Tests +# ----------------------------------------------------------------------------- +def test_add_remove_breakpoint(code_editor_bot, mocker): + """Test CodeEditor.add_remove_breakpoint().""" + editor, qtbot = code_editor_bot + arb = editor.add_remove_breakpoint + + mocker.patch.object(codeeditor.QInputDialog, 'getText') + + editor.go_to_line(1) + block = editor.textCursor().block() + + # Breakpoints are only for Python-like files. + editor.set_language(None) + reset_emits(editor) + arb() + assert block # Block exists. + assert not block.userData() # But user data not added to it. + editor.linenumberarea.update.assert_not_called() + editor.sig_flags_changed.emit.assert_not_called() + editor.breakpoints_changed.emit.assert_not_called() + + # Reset language. + editor.set_language('Python') + + # Test with default call on text line containing code. + reset_emits(editor) + arb() + editor_assert_helper(editor, block, bp=True, bpc=None, emits=True) + + # Calling again removes breakpoint. + reset_emits(editor) + arb() + editor_assert_helper(editor, block, bp=False, bpc=None, emits=True) + + # Test on blank line. + reset_emits(editor) + editor.go_to_line(3) + block = editor.textCursor().block() + arb() + editor_assert_helper(editor, block, bp=False, bpc=None, emits=True) + + # Test adding condition on line containing code. + reset_emits(editor) + block = editor.document().findBlockByLineNumber(3) # Block is one less. + arb(line_number=4, condition='a > 50') + editor_assert_helper(editor, block, bp=True, bpc='a > 50', emits=True) + + # Call already set breakpoint with edit condition. + reset_emits(editor) + codeeditor.QInputDialog.getText.return_value = ('a == 42', False) + arb(line_number=4, edit_condition=True) + # Condition not changed because edit was cancelled. + editor_assert_helper(editor, block, bp=True, bpc='a > 50', emits=False) + + # Condition changed. + codeeditor.QInputDialog.getText.return_value = ('a == 42', True) # OK. + reset_emits(editor) + arb(line_number=4, edit_condition=True) + editor_assert_helper(editor, block, bp=True, bpc='a == 42', emits=True) + + +def test_add_remove_breakpoint_with_edit_condition(code_editor_bot, mocker): + """Test add/remove breakpoint with edit_condition.""" + # For issue 2179. + + editor, qtbot = code_editor_bot + arb = editor.add_remove_breakpoint + mocker.patch.object(codeeditor.QInputDialog, 'getText') + + linenumber = 5 + block = editor.document().findBlockByLineNumber(linenumber - 1) + + # Call with edit_breakpoint on line that has never had a breakpoint set. + # Once a line has a breakpoint set, it remains in userData(), which results + # in a different behavior when calling the dialog box (tested below). + reset_emits(editor) + codeeditor.QInputDialog.getText.return_value = ('b == 1', False) + arb(line_number=linenumber, edit_condition=True) + data = block.userData() + assert not data # Data isn't saved in this case. + # Confirm line number area, scrollflag, and breakpoints not called. + editor.linenumberarea.update.assert_not_called() + editor.sig_flags_changed.emit.assert_not_called() + editor.breakpoints_changed.emit.assert_not_called() + + # Call as if 'OK' button pressed. + reset_emits(editor) + codeeditor.QInputDialog.getText.return_value = ('b == 1', True) + arb(line_number=linenumber, edit_condition=True) + editor_assert_helper(editor, block, bp=True, bpc='b == 1', emits=True) + + # Call again with dialog cancelled - breakpoint is already active. + reset_emits(editor) + codeeditor.QInputDialog.getText.return_value = ('b == 9', False) + arb(line_number=linenumber, edit_condition=True) + # Breakpoint stays active, but signals aren't emitted. + editor_assert_helper(editor, block, bp=True, bpc='b == 1', emits=False) + + # Remove breakpoint and condition. + reset_emits(editor) + arb(line_number=linenumber) + editor_assert_helper(editor, block, bp=False, bpc=None, emits=True) + + # Call again with dialog cancelled. + reset_emits(editor) + codeeditor.QInputDialog.getText.return_value = ('b == 9', False) + arb(line_number=linenumber, edit_condition=True) + editor_assert_helper(editor, block, bp=False, bpc=None, emits=False) + + +def test_get_breakpoints(code_editor_bot): + """Test CodeEditor.get_breakpoints.""" + editor, qtbot = code_editor_bot + arb = editor.add_remove_breakpoint + gb = editor.get_breakpoints + + assert(gb() == []) + + # Add breakpoints. + bp = [(1, None), (3, None), (4, 'a > 1'), (5, 'c == 10')] + editor.set_breakpoints(bp) + assert(gb() == [(1, None), (4, 'a > 1'), (5, 'c == 10')]) + + # Only includes active breakpoints. Calling add_remove turns the + # status to inactive, even with a change to condition. + arb(line_number=1, condition='a < b') + arb(line_number=4) + assert(gb() == [(5, 'c == 10')]) + + +def test_clear_breakpoints(code_editor_bot): + """Test CodeEditor.clear_breakpoints.""" + editor, qtbot = code_editor_bot + + assert len(editor.blockuserdata_list) == 0 + + bp = [(1, None), (4, None)] + editor.set_breakpoints(bp) + assert editor.get_breakpoints() == bp + assert len(editor.blockuserdata_list) == 2 + + editor.clear_breakpoints() + assert editor.get_breakpoints() == [] + # Even though there is a 'del data' that would pop the item from the + # list, the __del__ funcion isn't called. + assert len(editor.blockuserdata_list) == 2 + for data in editor.blockuserdata_list: + assert not data.breakpoint + + +def test_set_breakpoints(code_editor_bot): + """Test CodeEditor.set_breakpoints.""" + editor, qtbot = code_editor_bot + + editor.set_breakpoints([]) + assert editor.get_breakpoints() == [] + + bp = [(1, 'a > b'), (4, None)] + editor.set_breakpoints(bp) + assert editor.get_breakpoints() == bp + assert editor.blockuserdata_list[0].breakpoint + + bp = [(1, None), (5, 'c == 50')] + editor.set_breakpoints(bp) + assert editor.get_breakpoints() == bp + assert editor.blockuserdata_list[0].breakpoint + + +def test_update_breakpoints(code_editor_bot): + """Test CodeEditor.update_breakpoints.""" + editor, qtbot = code_editor_bot + reset_emits(editor) + editor.breakpoints_changed.emit.assert_not_called() + # update_breakpoints is the slot for the blockCountChanged signal. + editor.textCursor().insertBlock() + editor.breakpoints_changed.emit.assert_called() + + +if __name__ == "__main__": + pytest.main() From 301eae8b365c8ced35ba03ed4ac12c80b8448aca Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Thu, 26 Oct 2017 03:50:12 -0400 Subject: [PATCH 2/5] Travis issues. --- .../sourcecode/tests/test_breakpoints.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/spyder/widgets/sourcecode/tests/test_breakpoints.py b/spyder/widgets/sourcecode/tests/test_breakpoints.py index 80db9e03b7a..f675f31c856 100644 --- a/spyder/widgets/sourcecode/tests/test_breakpoints.py +++ b/spyder/widgets/sourcecode/tests/test_breakpoints.py @@ -8,7 +8,10 @@ Tests for breakpoints. """ -import unittest.mock as mock +try: + from unittest.mock import Mock +except ImportError: + from mock import Mock # Python 2 # Third party imports import pytest @@ -42,9 +45,9 @@ def editor_assert_helper(editor, block=None, bp=False, bpc=None, emits=True): assert data.breakpoint == bp assert data.breakpoint_condition == bpc if emits: - editor.linenumberarea.update.assert_called() - editor.sig_flags_changed.emit.assert_called() - editor.breakpoints_changed.emit.assert_called() + editor.linenumberarea.update.assert_called_with() + editor.sig_flags_changed.emit.assert_called_with() + editor.breakpoints_changed.emit.assert_called_with() else: editor.linenumberarea.update.assert_not_called() editor.sig_flags_changed.emit.assert_not_called() @@ -63,9 +66,9 @@ def code_editor_bot(qtbot): tab_stop_width_spaces=tab_stop_width_spaces) # Mock the screen updates and signal emits to test when they've been # called. - editor.linenumberarea = mock.Mock() - editor.sig_flags_changed = mock.Mock() - editor.breakpoints_changed = mock.Mock() + editor.linenumberarea = Mock() + editor.sig_flags_changed = Mock() + editor.breakpoints_changed = Mock() text = ('def f1(a, b):\n' '"Double quote string."\n' '\n' # Blank line. From fd59dc757940ef734f24c30f29d1343396798118 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Thu, 26 Oct 2017 04:24:41 -0400 Subject: [PATCH 3/5] Travis issues. --- spyder/widgets/sourcecode/tests/test_breakpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder/widgets/sourcecode/tests/test_breakpoints.py b/spyder/widgets/sourcecode/tests/test_breakpoints.py index f675f31c856..2c86ae05e12 100644 --- a/spyder/widgets/sourcecode/tests/test_breakpoints.py +++ b/spyder/widgets/sourcecode/tests/test_breakpoints.py @@ -255,7 +255,7 @@ def test_update_breakpoints(code_editor_bot): editor.breakpoints_changed.emit.assert_not_called() # update_breakpoints is the slot for the blockCountChanged signal. editor.textCursor().insertBlock() - editor.breakpoints_changed.emit.assert_called() + editor.breakpoints_changed.emit.assert_called_with() if __name__ == "__main__": From d628488e0bdbf7310092f8c6c39c5649738c3981 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Thu, 26 Oct 2017 14:34:21 -0400 Subject: [PATCH 4/5] Changes to tests for 3.x --- spyder/widgets/sourcecode/tests/test_breakpoints.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/spyder/widgets/sourcecode/tests/test_breakpoints.py b/spyder/widgets/sourcecode/tests/test_breakpoints.py index 2c86ae05e12..8168ca35e7c 100644 --- a/spyder/widgets/sourcecode/tests/test_breakpoints.py +++ b/spyder/widgets/sourcecode/tests/test_breakpoints.py @@ -27,7 +27,7 @@ def reset_emits(editor): "Reset signal mocks." editor.linenumberarea.reset_mock() - editor.sig_flags_changed.reset_mock() + # editor.sig_flags_changed.reset_mock() # 4.0 editor.breakpoints_changed.reset_mock() @@ -46,11 +46,11 @@ def editor_assert_helper(editor, block=None, bp=False, bpc=None, emits=True): assert data.breakpoint_condition == bpc if emits: editor.linenumberarea.update.assert_called_with() - editor.sig_flags_changed.emit.assert_called_with() + # editor.sig_flags_changed.emit.assert_called_with() # 4.0 editor.breakpoints_changed.emit.assert_called_with() else: editor.linenumberarea.update.assert_not_called() - editor.sig_flags_changed.emit.assert_not_called() + # editor.sig_flags_changed.emit.assert_not_called() # 4.0 editor.breakpoints_changed.emit.assert_not_called() @@ -67,7 +67,8 @@ def code_editor_bot(qtbot): # Mock the screen updates and signal emits to test when they've been # called. editor.linenumberarea = Mock() - editor.sig_flags_changed = Mock() + editor.get_linenumberarea_width = Mock(return_value=1) # 3.x + # editor.sig_flags_changed = Mock() # 4.0 editor.breakpoints_changed = Mock() text = ('def f1(a, b):\n' '"Double quote string."\n' @@ -98,7 +99,7 @@ def test_add_remove_breakpoint(code_editor_bot, mocker): assert block # Block exists. assert not block.userData() # But user data not added to it. editor.linenumberarea.update.assert_not_called() - editor.sig_flags_changed.emit.assert_not_called() + # editor.sig_flags_changed.emit.assert_not_called() # 4.0 editor.breakpoints_changed.emit.assert_not_called() # Reset language. @@ -162,7 +163,7 @@ def test_add_remove_breakpoint_with_edit_condition(code_editor_bot, mocker): assert not data # Data isn't saved in this case. # Confirm line number area, scrollflag, and breakpoints not called. editor.linenumberarea.update.assert_not_called() - editor.sig_flags_changed.emit.assert_not_called() + # editor.sig_flags_changed.emit.assert_not_called() # 4.0 editor.breakpoints_changed.emit.assert_not_called() # Call as if 'OK' button pressed. From 839206dd14f0051611ff74f9e84459875cd2c381 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Thu, 26 Oct 2017 18:07:33 -0400 Subject: [PATCH 5/5] Changes to tests for Spyder versions --- .../sourcecode/tests/test_breakpoints.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/spyder/widgets/sourcecode/tests/test_breakpoints.py b/spyder/widgets/sourcecode/tests/test_breakpoints.py index 8168ca35e7c..69d3ac422eb 100644 --- a/spyder/widgets/sourcecode/tests/test_breakpoints.py +++ b/spyder/widgets/sourcecode/tests/test_breakpoints.py @@ -18,6 +18,7 @@ from qtpy.QtGui import QTextCursor # Local imports +from spyder import version_info from spyder.py3compat import to_text_string import spyder.widgets.sourcecode.codeeditor as codeeditor @@ -27,7 +28,8 @@ def reset_emits(editor): "Reset signal mocks." editor.linenumberarea.reset_mock() - # editor.sig_flags_changed.reset_mock() # 4.0 + if version_info > (4, ): + editor.sig_flags_changed.reset_mock() editor.breakpoints_changed.reset_mock() @@ -46,11 +48,13 @@ def editor_assert_helper(editor, block=None, bp=False, bpc=None, emits=True): assert data.breakpoint_condition == bpc if emits: editor.linenumberarea.update.assert_called_with() - # editor.sig_flags_changed.emit.assert_called_with() # 4.0 + if version_info > (4, ): + editor.sig_flags_changed.emit.assert_called_with() editor.breakpoints_changed.emit.assert_called_with() else: editor.linenumberarea.update.assert_not_called() - # editor.sig_flags_changed.emit.assert_not_called() # 4.0 + if version_info > (4, ): + editor.sig_flags_changed.emit.assert_not_called() editor.breakpoints_changed.emit.assert_not_called() @@ -67,8 +71,10 @@ def code_editor_bot(qtbot): # Mock the screen updates and signal emits to test when they've been # called. editor.linenumberarea = Mock() - editor.get_linenumberarea_width = Mock(return_value=1) # 3.x - # editor.sig_flags_changed = Mock() # 4.0 + if version_info > (4, ): + editor.sig_flags_changed = Mock() + else: + editor.get_linenumberarea_width = Mock(return_value=1) editor.breakpoints_changed = Mock() text = ('def f1(a, b):\n' '"Double quote string."\n' @@ -99,7 +105,8 @@ def test_add_remove_breakpoint(code_editor_bot, mocker): assert block # Block exists. assert not block.userData() # But user data not added to it. editor.linenumberarea.update.assert_not_called() - # editor.sig_flags_changed.emit.assert_not_called() # 4.0 + if version_info > (4, ): + editor.sig_flags_changed.emit.assert_not_called() editor.breakpoints_changed.emit.assert_not_called() # Reset language. @@ -163,7 +170,8 @@ def test_add_remove_breakpoint_with_edit_condition(code_editor_bot, mocker): assert not data # Data isn't saved in this case. # Confirm line number area, scrollflag, and breakpoints not called. editor.linenumberarea.update.assert_not_called() - # editor.sig_flags_changed.emit.assert_not_called() # 4.0 + if version_info > (4, ): + editor.sig_flags_changed.emit.assert_not_called() editor.breakpoints_changed.emit.assert_not_called() # Call as if 'OK' button pressed.