summaryrefslogtreecommitdiffstats
path: root/Lib/idlelib/idle_test
diff options
context:
space:
mode:
authorTal Einat <532281+taleinat@users.noreply.github.com>2021-04-28 22:27:55 (GMT)
committerGitHub <noreply@github.com>2021-04-28 22:27:55 (GMT)
commit15d386185659683fc044ccaa300aa8cd7d49cc1a (patch)
tree6fac7df4ac125b39648d8f0d7fbb008212dc6ba8 /Lib/idlelib/idle_test
parent103d5e420dd90489933ad9da8bb1d6008773384d (diff)
downloadcpython-15d386185659683fc044ccaa300aa8cd7d49cc1a.zip
cpython-15d386185659683fc044ccaa300aa8cd7d49cc1a.tar.gz
cpython-15d386185659683fc044ccaa300aa8cd7d49cc1a.tar.bz2
bpo-37903: IDLE: Shell sidebar with prompts (GH-22682)
The first followup will change shell indents to spaces. More are expected. Co-authored-by: Terry Jan Reedy <tjreedy@udel.edu>
Diffstat (limited to 'Lib/idlelib/idle_test')
-rw-r--r--Lib/idlelib/idle_test/test_editor.py8
-rw-r--r--Lib/idlelib/idle_test/test_pyshell.py84
-rw-r--r--Lib/idlelib/idle_test/test_sidebar.py350
-rw-r--r--Lib/idlelib/idle_test/test_squeezer.py32
-rw-r--r--Lib/idlelib/idle_test/tkinter_testing_utils.py56
5 files changed, 494 insertions, 36 deletions
diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py
index 443dcf0..8665d68 100644
--- a/Lib/idlelib/idle_test/test_editor.py
+++ b/Lib/idlelib/idle_test/test_editor.py
@@ -167,7 +167,6 @@ class IndentAndNewlineTest(unittest.TestCase):
'2.end'),
)
- w.prompt_last_line = ''
for test in tests:
with self.subTest(label=test.label):
insert(text, test.text)
@@ -182,13 +181,6 @@ class IndentAndNewlineTest(unittest.TestCase):
# Deletes selected text before adding new line.
eq(get('1.0', 'end'), ' def f1(self, a,\n \n return a + b\n')
- # Preserves the whitespace in shell prompt.
- w.prompt_last_line = '>>> '
- insert(text, '>>> \t\ta =')
- text.mark_set('insert', '1.5')
- nl(None)
- eq(get('1.0', 'end'), '>>> \na =\n')
-
class RMenuTest(unittest.TestCase):
diff --git a/Lib/idlelib/idle_test/test_pyshell.py b/Lib/idlelib/idle_test/test_pyshell.py
index 4a09667..7067039 100644
--- a/Lib/idlelib/idle_test/test_pyshell.py
+++ b/Lib/idlelib/idle_test/test_pyshell.py
@@ -60,5 +60,89 @@ class PyShellFileListTest(unittest.TestCase):
## self.assertIsInstance(ps, pyshell.PyShell)
+class PyShellRemoveLastNewlineAndSurroundingWhitespaceTest(unittest.TestCase):
+ regexp = pyshell.PyShell._last_newline_re
+
+ def all_removed(self, text):
+ self.assertEqual('', self.regexp.sub('', text))
+
+ def none_removed(self, text):
+ self.assertEqual(text, self.regexp.sub('', text))
+
+ def check_result(self, text, expected):
+ self.assertEqual(expected, self.regexp.sub('', text))
+
+ def test_empty(self):
+ self.all_removed('')
+
+ def test_newline(self):
+ self.all_removed('\n')
+
+ def test_whitespace_no_newline(self):
+ self.all_removed(' ')
+ self.all_removed(' ')
+ self.all_removed(' ')
+ self.all_removed(' ' * 20)
+ self.all_removed('\t')
+ self.all_removed('\t\t')
+ self.all_removed('\t\t\t')
+ self.all_removed('\t' * 20)
+ self.all_removed('\t ')
+ self.all_removed(' \t')
+ self.all_removed(' \t \t ')
+ self.all_removed('\t \t \t')
+
+ def test_newline_with_whitespace(self):
+ self.all_removed(' \n')
+ self.all_removed('\t\n')
+ self.all_removed(' \t\n')
+ self.all_removed('\t \n')
+ self.all_removed('\n ')
+ self.all_removed('\n\t')
+ self.all_removed('\n \t')
+ self.all_removed('\n\t ')
+ self.all_removed(' \n ')
+ self.all_removed('\t\n ')
+ self.all_removed(' \n\t')
+ self.all_removed('\t\n\t')
+ self.all_removed('\t \t \t\n')
+ self.all_removed(' \t \t \n')
+ self.all_removed('\n\t \t \t')
+ self.all_removed('\n \t \t ')
+
+ def test_multiple_newlines(self):
+ self.check_result('\n\n', '\n')
+ self.check_result('\n' * 5, '\n' * 4)
+ self.check_result('\n' * 5 + '\t', '\n' * 4)
+ self.check_result('\n' * 20, '\n' * 19)
+ self.check_result('\n' * 20 + ' ', '\n' * 19)
+ self.check_result(' \n \n ', ' \n')
+ self.check_result(' \n\n ', ' \n')
+ self.check_result(' \n\n', ' \n')
+ self.check_result('\t\n\n', '\t\n')
+ self.check_result('\n\n ', '\n')
+ self.check_result('\n\n\t', '\n')
+ self.check_result(' \n \n ', ' \n')
+ self.check_result('\t\n\t\n\t', '\t\n')
+
+ def test_non_whitespace(self):
+ self.none_removed('a')
+ self.check_result('a\n', 'a')
+ self.check_result('a\n ', 'a')
+ self.check_result('a \n ', 'a')
+ self.check_result('a \n\t', 'a')
+ self.none_removed('-')
+ self.check_result('-\n', '-')
+ self.none_removed('.')
+ self.check_result('.\n', '.')
+
+ def test_unsupported_whitespace(self):
+ self.none_removed('\v')
+ self.none_removed('\n\v')
+ self.check_result('\v\n', '\v')
+ self.none_removed(' \n\v')
+ self.check_result('\v\n ', '\v')
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py
index 2974a9a..7228d0e 100644
--- a/Lib/idlelib/idle_test/test_sidebar.py
+++ b/Lib/idlelib/idle_test/test_sidebar.py
@@ -1,13 +1,23 @@
-"""Test sidebar, coverage 93%"""
-import idlelib.sidebar
+"""Test sidebar, coverage 85%"""
+from textwrap import dedent
+import sys
+
from itertools import chain
import unittest
import unittest.mock
-from test.support import requires
+from test.support import requires, swap_attr
import tkinter as tk
+from .tkinter_testing_utils import run_in_tk_mainloop
from idlelib.delegator import Delegator
+from idlelib.editor import fixwordbreaks
+from idlelib import macosx
from idlelib.percolator import Percolator
+import idlelib.pyshell
+from idlelib.pyshell import fix_x11_paste, PyShell, PyShellFileList
+from idlelib.run import fix_scaling
+import idlelib.sidebar
+from idlelib.sidebar import get_end_linenumber, get_lineno
class Dummy_editwin:
@@ -31,6 +41,7 @@ class LineNumbersTest(unittest.TestCase):
def setUpClass(cls):
requires('gui')
cls.root = tk.Tk()
+ cls.root.withdraw()
cls.text_frame = tk.Frame(cls.root)
cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
@@ -154,7 +165,7 @@ class LineNumbersTest(unittest.TestCase):
self.assert_sidebar_n_lines(3)
self.assert_state_disabled()
- # Note: deleting up to "2.end" doesn't delete the final newline.
+ # Deleting up to "2.end" doesn't delete the final newline.
self.text.delete('2.0', '2.end')
self.assert_text_equals('fbarfoo\n\n\n')
self.assert_sidebar_n_lines(3)
@@ -165,7 +176,7 @@ class LineNumbersTest(unittest.TestCase):
self.assert_sidebar_n_lines(1)
self.assert_state_disabled()
- # Note: Text widgets always keep a single '\n' character at the end.
+ # Text widgets always keep a single '\n' character at the end.
self.text.delete('1.0', 'end')
self.assert_text_equals('\n')
self.assert_sidebar_n_lines(1)
@@ -234,11 +245,19 @@ class LineNumbersTest(unittest.TestCase):
self.assert_sidebar_n_lines(4)
self.assertEqual(get_width(), 1)
- # Note: Text widgets always keep a single '\n' character at the end.
+ # Text widgets always keep a single '\n' character at the end.
self.text.delete('1.0', 'end -1c')
self.assert_sidebar_n_lines(1)
self.assertEqual(get_width(), 1)
+ # The following tests are temporarily disabled due to relying on
+ # simulated user input and inspecting which text is selected, which
+ # are fragile and can fail when several GUI tests are run in parallel
+ # or when the windows created by the test lose focus.
+ #
+ # TODO: Re-work these tests or remove them from the test suite.
+
+ @unittest.skip('test disabled')
def test_click_selection(self):
self.linenumber.show_sidebar()
self.text.insert('1.0', 'one\ntwo\nthree\nfour\n')
@@ -252,6 +271,7 @@ class LineNumbersTest(unittest.TestCase):
self.assertEqual(self.get_selection(), ('2.0', '3.0'))
+ @unittest.skip('test disabled')
def simulate_drag(self, start_line, end_line):
start_x, start_y = self.get_line_screen_position(start_line)
end_x, end_y = self.get_line_screen_position(end_line)
@@ -277,6 +297,7 @@ class LineNumbersTest(unittest.TestCase):
x=end_x, y=end_y)
self.root.update()
+ @unittest.skip('test disabled')
def test_drag_selection_down(self):
self.linenumber.show_sidebar()
self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n')
@@ -286,6 +307,7 @@ class LineNumbersTest(unittest.TestCase):
self.simulate_drag(2, 4)
self.assertEqual(self.get_selection(), ('2.0', '5.0'))
+ @unittest.skip('test disabled')
def test_drag_selection_up(self):
self.linenumber.show_sidebar()
self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n')
@@ -353,7 +375,7 @@ class LineNumbersTest(unittest.TestCase):
ln.hide_sidebar()
self.highlight_cfg = test_colors
- # Nothing breaks with inactive code context.
+ # Nothing breaks with inactive line numbers.
ln.update_colors()
# Show line numbers, previous colors change is immediately effective.
@@ -370,5 +392,319 @@ class LineNumbersTest(unittest.TestCase):
assert_colors_are_equal(orig_colors)
+class ShellSidebarTest(unittest.TestCase):
+ root: tk.Tk = None
+ shell: PyShell = None
+
+ @classmethod
+ def setUpClass(cls):
+ requires('gui')
+
+ cls.root = root = tk.Tk()
+ root.withdraw()
+
+ fix_scaling(root)
+ fixwordbreaks(root)
+ fix_x11_paste(root)
+
+ cls.flist = flist = PyShellFileList(root)
+ macosx.setupApp(root, flist)
+ root.update_idletasks()
+
+ cls.init_shell()
+
+ @classmethod
+ def tearDownClass(cls):
+ if cls.shell is not None:
+ cls.shell.executing = False
+ cls.shell.close()
+ cls.shell = None
+ cls.flist = None
+ cls.root.update_idletasks()
+ cls.root.destroy()
+ cls.root = None
+
+ @classmethod
+ def init_shell(cls):
+ cls.shell = cls.flist.open_shell()
+ cls.shell.pollinterval = 10
+ cls.root.update()
+ cls.n_preface_lines = get_lineno(cls.shell.text, 'end-1c') - 1
+
+ @classmethod
+ def reset_shell(cls):
+ cls.shell.per.bottom.delete(f'{cls.n_preface_lines+1}.0', 'end-1c')
+ cls.shell.shell_sidebar.update_sidebar()
+ cls.root.update()
+
+ def setUp(self):
+ # In some test environments, e.g. Azure Pipelines (as of
+ # Apr. 2021), sys.stdout is changed between tests. However,
+ # PyShell relies on overriding sys.stdout when run without a
+ # sub-process (as done here; see setUpClass).
+ self._saved_stdout = None
+ if sys.stdout != self.shell.stdout:
+ self._saved_stdout = sys.stdout
+ sys.stdout = self.shell.stdout
+
+ self.reset_shell()
+
+ def tearDown(self):
+ if self._saved_stdout is not None:
+ sys.stdout = self._saved_stdout
+
+ def get_sidebar_lines(self):
+ canvas = self.shell.shell_sidebar.canvas
+ texts = list(canvas.find(tk.ALL))
+ texts_by_y_coords = {
+ canvas.bbox(text)[1]: canvas.itemcget(text, 'text')
+ for text in texts
+ }
+ line_y_coords = self.get_shell_line_y_coords()
+ return [texts_by_y_coords.get(y, None) for y in line_y_coords]
+
+ def assert_sidebar_lines_end_with(self, expected_lines):
+ self.shell.shell_sidebar.update_sidebar()
+ self.assertEqual(
+ self.get_sidebar_lines()[-len(expected_lines):],
+ expected_lines,
+ )
+
+ def get_shell_line_y_coords(self):
+ text = self.shell.text
+ y_coords = []
+ index = text.index("@0,0")
+ if index.split('.', 1)[1] != '0':
+ index = text.index(f"{index} +1line linestart")
+ while True:
+ lineinfo = text.dlineinfo(index)
+ if lineinfo is None:
+ break
+ y_coords.append(lineinfo[1])
+ index = text.index(f"{index} +1line")
+ return y_coords
+
+ def get_sidebar_line_y_coords(self):
+ canvas = self.shell.shell_sidebar.canvas
+ texts = list(canvas.find(tk.ALL))
+ texts.sort(key=lambda text: canvas.bbox(text)[1])
+ return [canvas.bbox(text)[1] for text in texts]
+
+ def assert_sidebar_lines_synced(self):
+ self.assertLessEqual(
+ set(self.get_sidebar_line_y_coords()),
+ set(self.get_shell_line_y_coords()),
+ )
+
+ def do_input(self, input):
+ shell = self.shell
+ text = shell.text
+ for line_index, line in enumerate(input.split('\n')):
+ if line_index > 0:
+ text.event_generate('<<newline-and-indent>>')
+ text.insert('insert', line, 'stdin')
+
+ def test_initial_state(self):
+ sidebar_lines = self.get_sidebar_lines()
+ self.assertEqual(
+ sidebar_lines,
+ [None] * (len(sidebar_lines) - 1) + ['>>>'],
+ )
+ self.assert_sidebar_lines_synced()
+
+ @run_in_tk_mainloop
+ def test_single_empty_input(self):
+ self.do_input('\n')
+ yield
+ self.assert_sidebar_lines_end_with(['>>>', '>>>'])
+
+ @run_in_tk_mainloop
+ def test_single_line_statement(self):
+ self.do_input('1\n')
+ yield
+ self.assert_sidebar_lines_end_with(['>>>', None, '>>>'])
+
+ @run_in_tk_mainloop
+ def test_multi_line_statement(self):
+ # Block statements are not indented because IDLE auto-indents.
+ self.do_input(dedent('''\
+ if True:
+ print(1)
+
+ '''))
+ yield
+ self.assert_sidebar_lines_end_with([
+ '>>>',
+ '...',
+ '...',
+ '...',
+ None,
+ '>>>',
+ ])
+
+ @run_in_tk_mainloop
+ def test_single_long_line_wraps(self):
+ self.do_input('1' * 200 + '\n')
+ yield
+ self.assert_sidebar_lines_end_with(['>>>', None, '>>>'])
+ self.assert_sidebar_lines_synced()
+
+ @run_in_tk_mainloop
+ def test_squeeze_multi_line_output(self):
+ shell = self.shell
+ text = shell.text
+
+ self.do_input('print("a\\nb\\nc")\n')
+ yield
+ self.assert_sidebar_lines_end_with(['>>>', None, None, None, '>>>'])
+
+ text.mark_set('insert', f'insert -1line linestart')
+ text.event_generate('<<squeeze-current-text>>')
+ yield
+ self.assert_sidebar_lines_end_with(['>>>', None, '>>>'])
+ self.assert_sidebar_lines_synced()
+
+ shell.squeezer.expandingbuttons[0].expand()
+ yield
+ self.assert_sidebar_lines_end_with(['>>>', None, None, None, '>>>'])
+ self.assert_sidebar_lines_synced()
+
+ @run_in_tk_mainloop
+ def test_interrupt_recall_undo_redo(self):
+ text = self.shell.text
+ # Block statements are not indented because IDLE auto-indents.
+ initial_sidebar_lines = self.get_sidebar_lines()
+
+ self.do_input(dedent('''\
+ if True:
+ print(1)
+ '''))
+ yield
+ self.assert_sidebar_lines_end_with(['>>>', '...', '...'])
+ with_block_sidebar_lines = self.get_sidebar_lines()
+ self.assertNotEqual(with_block_sidebar_lines, initial_sidebar_lines)
+
+ # Control-C
+ text.event_generate('<<interrupt-execution>>')
+ yield
+ self.assert_sidebar_lines_end_with(['>>>', '...', '...', None, '>>>'])
+
+ # Recall previous via history
+ text.event_generate('<<history-previous>>')
+ text.event_generate('<<interrupt-execution>>')
+ yield
+ self.assert_sidebar_lines_end_with(['>>>', '...', None, '>>>'])
+
+ # Recall previous via recall
+ text.mark_set('insert', text.index('insert -2l'))
+ text.event_generate('<<newline-and-indent>>')
+ yield
+
+ text.event_generate('<<undo>>')
+ yield
+ self.assert_sidebar_lines_end_with(['>>>'])
+
+ text.event_generate('<<redo>>')
+ yield
+ self.assert_sidebar_lines_end_with(['>>>', '...'])
+
+ text.event_generate('<<newline-and-indent>>')
+ text.event_generate('<<newline-and-indent>>')
+ yield
+ self.assert_sidebar_lines_end_with(
+ ['>>>', '...', '...', '...', None, '>>>']
+ )
+
+ @run_in_tk_mainloop
+ def test_very_long_wrapped_line(self):
+ with swap_attr(self.shell, 'squeezer', None):
+ self.do_input('x = ' + '1'*10_000 + '\n')
+ yield
+ self.assertEqual(self.get_sidebar_lines(), ['>>>'])
+
+ def test_font(self):
+ sidebar = self.shell.shell_sidebar
+
+ test_font = 'TkTextFont'
+
+ def mock_idleconf_GetFont(root, configType, section):
+ return test_font
+ GetFont_patcher = unittest.mock.patch.object(
+ idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont)
+ GetFont_patcher.start()
+ def cleanup():
+ GetFont_patcher.stop()
+ sidebar.update_font()
+ self.addCleanup(cleanup)
+
+ def get_sidebar_font():
+ canvas = sidebar.canvas
+ texts = list(canvas.find(tk.ALL))
+ fonts = {canvas.itemcget(text, 'font') for text in texts}
+ self.assertEqual(len(fonts), 1)
+ return next(iter(fonts))
+
+ self.assertNotEqual(get_sidebar_font(), test_font)
+ sidebar.update_font()
+ self.assertEqual(get_sidebar_font(), test_font)
+
+ def test_highlight_colors(self):
+ sidebar = self.shell.shell_sidebar
+
+ test_colors = {"background": '#abcdef', "foreground": '#123456'}
+
+ orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight
+ def mock_idleconf_GetHighlight(theme, element):
+ if element in ['linenumber', 'console']:
+ return test_colors
+ return orig_idleConf_GetHighlight(theme, element)
+ GetHighlight_patcher = unittest.mock.patch.object(
+ idlelib.sidebar.idleConf, 'GetHighlight',
+ mock_idleconf_GetHighlight)
+ GetHighlight_patcher.start()
+ def cleanup():
+ GetHighlight_patcher.stop()
+ sidebar.update_colors()
+ self.addCleanup(cleanup)
+
+ def get_sidebar_colors():
+ canvas = sidebar.canvas
+ texts = list(canvas.find(tk.ALL))
+ fgs = {canvas.itemcget(text, 'fill') for text in texts}
+ self.assertEqual(len(fgs), 1)
+ fg = next(iter(fgs))
+ bg = canvas.cget('background')
+ return {"background": bg, "foreground": fg}
+
+ self.assertNotEqual(get_sidebar_colors(), test_colors)
+ sidebar.update_colors()
+ self.assertEqual(get_sidebar_colors(), test_colors)
+
+ @run_in_tk_mainloop
+ def test_mousewheel(self):
+ sidebar = self.shell.shell_sidebar
+ text = self.shell.text
+
+ # Enter a 100-line string to scroll the shell screen down.
+ self.do_input('x = """' + '\n'*100 + '"""\n')
+ yield
+ self.assertGreater(get_lineno(text, '@0,0'), 1)
+
+ last_lineno = get_end_linenumber(text)
+ self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0')))
+
+ # Scroll up using the <MouseWheel> event.
+ # The meaning delta is platform-dependant.
+ delta = -1 if sys.platform == 'darwin' else 120
+ sidebar.canvas.event_generate('<MouseWheel>', x=0, y=0, delta=delta)
+ yield
+ self.assertIsNone(text.dlineinfo(text.index(f'{last_lineno}.0')))
+
+ # Scroll back down using the <Button-5> event.
+ sidebar.canvas.event_generate('<Button-5>', x=0, y=0)
+ yield
+ self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0')))
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/Lib/idlelib/idle_test/test_squeezer.py b/Lib/idlelib/idle_test/test_squeezer.py
index ee1bbd7..eaf81a5 100644
--- a/Lib/idlelib/idle_test/test_squeezer.py
+++ b/Lib/idlelib/idle_test/test_squeezer.py
@@ -7,13 +7,12 @@ from unittest.mock import Mock, NonCallableMagicMock, patch, sentinel, ANY
from test.support import requires
from idlelib.config import idleConf
+from idlelib.percolator import Percolator
from idlelib.squeezer import count_lines_with_wrapping, ExpandingButton, \
Squeezer
from idlelib import macosx
from idlelib.textview import view_text
from idlelib.tooltip import Hovertip
-from idlelib.pyshell import PyShell
-
SENTINEL_VALUE = sentinel.SENTINEL_VALUE
@@ -205,8 +204,8 @@ class SqueezerTest(unittest.TestCase):
self.assertEqual(text_widget.get('1.0', 'end'), '\n')
self.assertEqual(len(squeezer.expandingbuttons), 1)
- def test_squeeze_current_text_event(self):
- """Test the squeeze_current_text event."""
+ def test_squeeze_current_text(self):
+ """Test the squeeze_current_text method."""
# Squeezing text should work for both stdout and stderr.
for tag_name in ["stdout", "stderr"]:
editwin = self.make_mock_editor_window(with_text_widget=True)
@@ -222,7 +221,7 @@ class SqueezerTest(unittest.TestCase):
self.assertEqual(len(squeezer.expandingbuttons), 0)
# Test squeezing the current text.
- retval = squeezer.squeeze_current_text_event(event=Mock())
+ retval = squeezer.squeeze_current_text()
self.assertEqual(retval, "break")
self.assertEqual(text_widget.get('1.0', 'end'), '\n\n')
self.assertEqual(len(squeezer.expandingbuttons), 1)
@@ -230,11 +229,11 @@ class SqueezerTest(unittest.TestCase):
# Test that expanding the squeezed text works and afterwards
# the Text widget contains the original text.
- squeezer.expandingbuttons[0].expand(event=Mock())
+ squeezer.expandingbuttons[0].expand()
self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n')
self.assertEqual(len(squeezer.expandingbuttons), 0)
- def test_squeeze_current_text_event_no_allowed_tags(self):
+ def test_squeeze_current_text_no_allowed_tags(self):
"""Test that the event doesn't squeeze text without a relevant tag."""
editwin = self.make_mock_editor_window(with_text_widget=True)
text_widget = editwin.text
@@ -249,7 +248,7 @@ class SqueezerTest(unittest.TestCase):
self.assertEqual(len(squeezer.expandingbuttons), 0)
# Test squeezing the current text.
- retval = squeezer.squeeze_current_text_event(event=Mock())
+ retval = squeezer.squeeze_current_text()
self.assertEqual(retval, "break")
self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n')
self.assertEqual(len(squeezer.expandingbuttons), 0)
@@ -264,13 +263,13 @@ class SqueezerTest(unittest.TestCase):
# Prepare some text in the Text widget and squeeze it.
text_widget.insert("1.0", "SOME\nTEXT\n", "stdout")
text_widget.mark_set("insert", "1.0")
- squeezer.squeeze_current_text_event(event=Mock())
+ squeezer.squeeze_current_text()
self.assertEqual(len(squeezer.expandingbuttons), 1)
# Test squeezing the current text.
text_widget.insert("1.0", "MORE\nSTUFF\n", "stdout")
text_widget.mark_set("insert", "1.0")
- retval = squeezer.squeeze_current_text_event(event=Mock())
+ retval = squeezer.squeeze_current_text()
self.assertEqual(retval, "break")
self.assertEqual(text_widget.get('1.0', 'end'), '\n\n\n')
self.assertEqual(len(squeezer.expandingbuttons), 2)
@@ -311,6 +310,7 @@ class ExpandingButtonTest(unittest.TestCase):
root = get_test_tk_root(self)
squeezer = Mock()
squeezer.editwin.text = Text(root)
+ squeezer.editwin.per = Percolator(squeezer.editwin.text)
# Set default values for the configuration settings.
squeezer.auto_squeeze_min_lines = 50
@@ -352,14 +352,9 @@ class ExpandingButtonTest(unittest.TestCase):
# Insert the button into the text widget
# (this is normally done by the Squeezer class).
- text_widget = expandingbutton.text
+ text_widget = squeezer.editwin.text
text_widget.window_create("1.0", window=expandingbutton)
- # Set base_text to the text widget, so that changes are actually
- # made to it (by ExpandingButton) and we can inspect these
- # changes afterwards.
- expandingbutton.base_text = expandingbutton.text
-
# trigger the expand event
retval = expandingbutton.expand(event=Mock())
self.assertEqual(retval, None)
@@ -390,11 +385,6 @@ class ExpandingButtonTest(unittest.TestCase):
text_widget = expandingbutton.text
text_widget.window_create("1.0", window=expandingbutton)
- # Set base_text to the text widget, so that changes are actually
- # made to it (by ExpandingButton) and we can inspect these
- # changes afterwards.
- expandingbutton.base_text = expandingbutton.text
-
# Patch the message box module to always return False.
with patch('idlelib.squeezer.messagebox') as mock_msgbox:
mock_msgbox.askokcancel.return_value = False
diff --git a/Lib/idlelib/idle_test/tkinter_testing_utils.py b/Lib/idlelib/idle_test/tkinter_testing_utils.py
new file mode 100644
index 0000000..a9f8386
--- /dev/null
+++ b/Lib/idlelib/idle_test/tkinter_testing_utils.py
@@ -0,0 +1,56 @@
+"""Utilities for testing with Tkinter"""
+import functools
+
+
+def run_in_tk_mainloop(test_method):
+ """Decorator for running a test method with a real Tk mainloop.
+
+ This starts a Tk mainloop before running the test, and stops it
+ at the end. This is faster and more robust than the common
+ alternative method of calling .update() and/or .update_idletasks().
+
+ Test methods using this must be written as generator functions,
+ using "yield" to allow the mainloop to process events and "after"
+ callbacks, and then continue the test from that point.
+
+ This also assumes that the test class has a .root attribute,
+ which is a tkinter.Tk object.
+
+ For example (from test_sidebar.py):
+
+ @run_test_with_tk_mainloop
+ def test_single_empty_input(self):
+ self.do_input('\n')
+ yield
+ self.assert_sidebar_lines_end_with(['>>>', '>>>'])
+ """
+ @functools.wraps(test_method)
+ def new_test_method(self):
+ test_generator = test_method(self)
+ root = self.root
+ # Exceptions raised by self.assert...() need to be raised
+ # outside of the after() callback in order for the test
+ # harness to capture them.
+ exception = None
+ def after_callback():
+ nonlocal exception
+ try:
+ next(test_generator)
+ except StopIteration:
+ root.quit()
+ except Exception as exc:
+ exception = exc
+ root.quit()
+ else:
+ # Schedule the Tk mainloop to call this function again,
+ # using a robust method of ensuring that it gets a
+ # chance to process queued events before doing so.
+ # See: https://stackoverflow.com/q/18499082#comment65004099_38817470
+ root.after(1, root.after_idle, after_callback)
+ root.after(0, root.after_idle, after_callback)
+ root.mainloop()
+
+ if exception:
+ raise exception
+
+ return new_test_method