"""Test sidebar, coverage 93%""" import idlelib.sidebar from itertools import chain import unittest import unittest.mock from test.support import requires import tkinter as tk from idlelib.delegator import Delegator from idlelib.percolator import Percolator class Dummy_editwin: def __init__(self, text): self.text = text self.text_frame = self.text.master self.per = Percolator(text) self.undo = Delegator() self.per.insertfilter(self.undo) def setvar(self, name, value): pass def getlineno(self, index): return int(float(self.text.index(index))) class LineNumbersTest(unittest.TestCase): @classmethod def setUpClass(cls): requires('gui') cls.root = tk.Tk() cls.text_frame = tk.Frame(cls.root) cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) cls.text_frame.rowconfigure(1, weight=1) cls.text_frame.columnconfigure(1, weight=1) cls.text = tk.Text(cls.text_frame, width=80, height=24, wrap=tk.NONE) cls.text.grid(row=1, column=1, sticky=tk.NSEW) cls.editwin = Dummy_editwin(cls.text) cls.editwin.vbar = tk.Scrollbar(cls.text_frame) @classmethod def tearDownClass(cls): cls.editwin.per.close() cls.root.update() cls.root.destroy() del cls.text, cls.text_frame, cls.editwin, cls.root def setUp(self): self.linenumber = idlelib.sidebar.LineNumbers(self.editwin) self.highlight_cfg = {"background": '#abcdef', "foreground": '#123456'} orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight def mock_idleconf_GetHighlight(theme, element): if element == 'linenumber': return self.highlight_cfg return orig_idleConf_GetHighlight(theme, element) GetHighlight_patcher = unittest.mock.patch.object( idlelib.sidebar.idleConf, 'GetHighlight', mock_idleconf_GetHighlight) GetHighlight_patcher.start() self.addCleanup(GetHighlight_patcher.stop) self.font_override = 'TkFixedFont' def mock_idleconf_GetFont(root, configType, section): return self.font_override GetFont_patcher = unittest.mock.patch.object( idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont) GetFont_patcher.start() self.addCleanup(GetFont_patcher.stop) def tearDown(self): self.text.delete('1.0', 'end') def get_selection(self): return tuple(map(str, self.text.tag_ranges('sel'))) def get_line_screen_position(self, line): bbox = self.linenumber.sidebar_text.bbox(f'{line}.end -1c') x = bbox[0] + 2 y = bbox[1] + 2 return x, y def assert_state_disabled(self): state = self.linenumber.sidebar_text.config()['state'] self.assertEqual(state[-1], tk.DISABLED) def get_sidebar_text_contents(self): return self.linenumber.sidebar_text.get('1.0', tk.END) def assert_sidebar_n_lines(self, n_lines): expected = '\n'.join(chain(map(str, range(1, n_lines + 1)), [''])) self.assertEqual(self.get_sidebar_text_contents(), expected) def assert_text_equals(self, expected): return self.assertEqual(self.text.get('1.0', 'end'), expected) def test_init_empty(self): self.assert_sidebar_n_lines(1) def test_init_not_empty(self): self.text.insert('insert', 'foo bar\n'*3) self.assert_text_equals('foo bar\n'*3 + '\n') self.assert_sidebar_n_lines(4) def test_toggle_linenumbering(self): self.assertEqual(self.linenumber.is_shown, False) self.linenumber.show_sidebar() self.assertEqual(self.linenumber.is_shown, True) self.linenumber.hide_sidebar() self.assertEqual(self.linenumber.is_shown, False) self.linenumber.hide_sidebar() self.assertEqual(self.linenumber.is_shown, False) self.linenumber.show_sidebar() self.assertEqual(self.linenumber.is_shown, True) self.linenumber.show_sidebar() self.assertEqual(self.linenumber.is_shown, True) def test_insert(self): self.text.insert('insert', 'foobar') self.assert_text_equals('foobar\n') self.assert_sidebar_n_lines(1) self.assert_state_disabled() self.text.insert('insert', '\nfoo') self.assert_text_equals('foobar\nfoo\n') self.assert_sidebar_n_lines(2) self.assert_state_disabled() self.text.insert('insert', 'hello\n'*2) self.assert_text_equals('foobar\nfoohello\nhello\n\n') self.assert_sidebar_n_lines(4) self.assert_state_disabled() self.text.insert('insert', '\nworld') self.assert_text_equals('foobar\nfoohello\nhello\n\nworld\n') self.assert_sidebar_n_lines(5) self.assert_state_disabled() def test_delete(self): self.text.insert('insert', 'foobar') self.assert_text_equals('foobar\n') self.text.delete('1.1', '1.3') self.assert_text_equals('fbar\n') self.assert_sidebar_n_lines(1) self.assert_state_disabled() self.text.insert('insert', 'foo\n'*2) self.assert_text_equals('fbarfoo\nfoo\n\n') self.assert_sidebar_n_lines(3) self.assert_state_disabled() # Note: 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) self.assert_state_disabled() self.text.delete('1.3', 'end') self.assert_text_equals('fba\n') self.assert_sidebar_n_lines(1) self.assert_state_disabled() # Note: 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) self.assert_state_disabled() def test_sidebar_text_width(self): """ Test that linenumber text widget is always at the minimum width """ def get_width(): return self.linenumber.sidebar_text.config()['width'][-1] self.assert_sidebar_n_lines(1) self.assertEqual(get_width(), 1) self.text.insert('insert', 'foo') self.assert_sidebar_n_lines(1) self.assertEqual(get_width(), 1) self.text.insert('insert', 'foo\n'*8) self.assert_sidebar_n_lines(9) self.assertEqual(get_width(), 1) self.text.insert('insert', 'foo\n') self.assert_sidebar_n_lines(10) self.assertEqual(get_width(), 2) self.text.insert('insert', 'foo\n') self.assert_sidebar_n_lines(11) self.assertEqual(get_width(), 2) self.text.delete('insert -1l linestart', 'insert linestart') self.assert_sidebar_n_lines(10) self.assertEqual(get_width(), 2) self.text.delete('insert -1l linestart', 'insert linestart') self.assert_sidebar_n_lines(9) self.assertEqual(get_width(), 1) self.text.insert('insert', 'foo\n'*90) self.assert_sidebar_n_lines(99) self.assertEqual(get_width(), 2) self.text.insert('insert', 'foo\n') self.assert_sidebar_n_lines(100) self.assertEqual(get_width(), 3) self.text.insert('insert', 'foo\n') self.assert_sidebar_n_lines(101) self.assertEqual(get_width(), 3) self.text.delete('insert -1l linestart', 'insert linestart') self.assert_sidebar_n_lines(100) self.assertEqual(get_width(), 3) self.text.delete('insert -1l linestart', 'insert linestart') self.assert_sidebar_n_lines(99) self.assertEqual(get_width(), 2) self.text.delete('50.0 -1c', 'end -1c') self.assert_sidebar_n_lines(49) self.assertEqual(get_width(), 2) self.text.delete('5.0 -1c', 'end -1c') self.assert_sidebar_n_lines(4) self.assertEqual(get_width(), 1) # Note: 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) def test_click_selection(self): self.linenumber.show_sidebar() self.text.insert('1.0', 'one\ntwo\nthree\nfour\n') self.root.update() # Click on the second line. x, y = self.get_line_screen_position(2) self.linenumber.sidebar_text.event_generate('', x=x, y=y) self.linenumber.sidebar_text.update() self.root.update() self.assertEqual(self.get_selection(), ('2.0', '3.0')) 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) self.linenumber.sidebar_text.event_generate('', x=start_x, y=start_y) self.root.update() def lerp(a, b, steps): """linearly interpolate from a to b (inclusive) in equal steps""" last_step = steps - 1 for i in range(steps): yield ((last_step - i) / last_step) * a + (i / last_step) * b for x, y in zip( map(int, lerp(start_x, end_x, steps=11)), map(int, lerp(start_y, end_y, steps=11)), ): self.linenumber.sidebar_text.event_generate('', x=x, y=y) self.root.update() self.linenumber.sidebar_text.event_generate('', x=end_x, y=end_y) self.root.update() def test_drag_selection_down(self): self.linenumber.show_sidebar() self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') self.root.update() # Drag from the second line to the fourth line. self.simulate_drag(2, 4) self.assertEqual(self.get_selection(), ('2.0', '5.0')) def test_drag_selection_up(self): self.linenumber.show_sidebar() self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') self.root.update() # Drag from the fourth line to the second line. self.simulate_drag(4, 2) self.assertEqual(self.get_selection(), ('2.0', '5.0')) def test_scroll(self): self.linenumber.show_sidebar() self.text.insert('1.0', 'line\n' * 100) self.root.update() # Scroll down 10 lines. self.text.yview_scroll(10, 'unit') self.root.update() self.assertEqual(self.text.index('@0,0'), '11.0') self.assertEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') # Generate a mouse-wheel event and make sure it scrolled up or down. # The meaning of the "delta" is OS-dependant, so this just checks for # any change. self.linenumber.sidebar_text.event_generate('', x=0, y=0, delta=10) self.root.update() self.assertNotEqual(self.text.index('@0,0'), '11.0') self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') def test_font(self): ln = self.linenumber orig_font = ln.sidebar_text['font'] test_font = 'TkTextFont' self.assertNotEqual(orig_font, test_font) # Ensure line numbers aren't shown. ln.hide_sidebar() self.font_override = test_font # Nothing breaks when line numbers aren't shown. ln.update_font() # Activate line numbers, previous font change is immediately effective. ln.show_sidebar() self.assertEqual(ln.sidebar_text['font'], test_font) # Call the font update with line numbers shown, change is picked up. self.font_override = orig_font ln.update_font() self.assertEqual(ln.sidebar_text['font'], orig_font) def test_highlight_colors(self): ln = self.linenumber orig_colors = dict(self.highlight_cfg) test_colors = {'background': '#222222', 'foreground': '#ffff00'} def assert_colors_are_equal(colors): self.assertEqual(ln.sidebar_text['background'], colors['background']) self.assertEqual(ln.sidebar_text['foreground'], colors['foreground']) # Ensure line numbers aren't shown. ln.hide_sidebar() self.highlight_cfg = test_colors # Nothing breaks with inactive code context. ln.update_colors() # Show line numbers, previous colors change is immediately effective. ln.show_sidebar() assert_colors_are_equal(test_colors) # Call colors update with no change to the configured colors. ln.update_colors() assert_colors_are_equal(test_colors) # Call the colors update with line numbers shown, change is picked up. self.highlight_cfg = orig_colors ln.update_colors() assert_colors_are_equal(orig_colors) if __name__ == '__main__': unittest.main(verbosity=2)