diff --git a/Lib/.ruff.toml b/Lib/.ruff.toml index c9ae9b95641b31..b651f0b6e6b770 100644 --- a/Lib/.ruff.toml +++ b/Lib/.ruff.toml @@ -12,6 +12,7 @@ select = [ "ctypes/__init__.py" = ["F401"] # Re-exports from _ctypes "ensurepip/__init__.py" = ["F401"] # `import zlib` availability check "idlelib/idle_test/htest.py" = ["F401"] # Import for Windows DPI side effect +"idlelib/idle_test/template.py" = ["F401"] # Placeholder import, see README.txt "idlelib/idle_test/test_iomenu.py" = ["F401"] # Imports checked for existence "importlib/_abc.py" = ["F401"] # Bootstrap-sensitive _bootstrap import "importlib/machinery.py" = ["F401"] # NamespacePath re-export diff --git a/Lib/idlelib/idle_test/template.py b/Lib/idlelib/idle_test/template.py index 0a4bd8b8e981fc..69a2af22efa149 100644 --- a/Lib/idlelib/idle_test/template.py +++ b/Lib/idlelib/idle_test/template.py @@ -1,5 +1,6 @@ "Test , coverage %." +from idlelib import zzdummy import unittest from test.support import requires from tkinter import Tk diff --git a/Lib/idlelib/idle_test/test_config.py b/Lib/idlelib/idle_test/test_config.py index 6d75cf7aa67dcc..56779702082871 100644 --- a/Lib/idlelib/idle_test/test_config.py +++ b/Lib/idlelib/idle_test/test_config.py @@ -454,9 +454,6 @@ def test_get_extension_keys(self): self.assertEqual(idleConf.GetExtensionKeys('ZzDummy'), {'<>': ['']}) userextn.remove_section('ZzDummy') -# need option key test -## key = [''] if sys.platform == 'darwin' else [''] -## eq(conf.GetExtensionKeys('ZoomHeight'), {'<>': key}) def test_get_extension_bindings(self): userextn.read_string(''' @@ -491,19 +488,27 @@ def test_get_keybinding(self): def test_get_current_keyset(self): current_platform = sys.platform conf = self.mock_config() - - # Ensure that platform isn't darwin - sys.platform = 'some-linux' - self.assertEqual(conf.GetCurrentKeySet(), conf.GetKeySet(conf.CurrentKeys())) - - # This should not be the same, since replace ') + self.assertEqual(conf.GetKeySet(conf.CurrentKeys())['<>'], + ['']) + sys.platform = 'darwin' + self.assertEqual(conf.GetCurrentKeySet()['<>'], + ['']) + finally: + # Restore platform + sys.platform = current_platform def test_get_keyset(self): conf = self.mock_config() @@ -762,8 +767,29 @@ def test_save_default(self): # Cover 2nd and 3rd false branches. changes = self.changes changes.add_option('main', 'Indent', 'use-spaces', '1') # save_option returns False; cfg_type_changed remains False. + self.assertFalse(changes.save_all()) + self.assertFalse(usermain.has_option('Indent', 'use-spaces')) + self.assertEqual(changes, self.empty) - # TODO: test that save_all calls usercfg Saves. + def test_save_all_saves_files(self): + eq = self.assertEqual + changes = self.changes + for parser in testcfg.values(): + parser.Save = Func() + try: + # 'main', 'highlight' and 'keys' are saved even if unchanged. + self.assertFalse(changes.save_all()) + eq([testcfg[cfgtype].Save.called + for cfgtype in ('main', 'highlight', 'keys', 'extensions')], + [1, 1, 1, 0]) + # A changed configuration type is saved too. + changes.add_option('extensions', 'Esec', 'eitem', 'eval') + self.assertTrue(changes.save_all()) + eq(testcfg['extensions'].Save.called, 1) + finally: + for parser in testcfg.values(): + del parser.Save + userextn.remove_section('Esec') def test_delete_section(self): changes = self.load() diff --git a/Lib/idlelib/idle_test/test_configdialog.py b/Lib/idlelib/idle_test/test_configdialog.py index 696a3b2f8f1bc2..a63fdfeec2f723 100644 --- a/Lib/idlelib/idle_test/test_configdialog.py +++ b/Lib/idlelib/idle_test/test_configdialog.py @@ -3,6 +3,7 @@ Half the class creates dialog, half works with user customizations. """ from idlelib import configdialog +from idlelib.editor import EditorWindow from test.support import requires requires('gui') import unittest @@ -51,12 +52,27 @@ def tearDownModule(): class ConfigDialogTest(unittest.TestCase): + # The methods tested here are mocked out in the tests below. + + def setUp(self): + self.parent = dialog.parent + self.instance = mock.create_autospec(EditorWindow, instance=True) + dialog.parent = mock.Mock(instance_dict={self.instance: []}) + + def tearDown(self): + dialog.parent = self.parent def test_deactivate_current_config(self): - pass + dialog.deactivate_current_config() + self.instance.RemoveKeybindings.assert_called_once_with() - def activate_config_changes(self): - pass + def test_activate_config_changes(self): + dialog.activate_config_changes() + for name in ('ResetColorizer', 'ResetFont', 'set_notabs_indentwidth', + 'ApplyKeybindings', 'reset_help_menu_entries', + 'update_cursor_blink'): + with self.subTest(name=name): + getattr(self.instance, name).assert_called_once_with() class ButtonTest(unittest.TestCase): @@ -1295,13 +1311,123 @@ def test_context(self): self.assertEqual(extpage, {'CodeContext': {'maxlines': '1'}}) -#unittest.skip("Nothing here yet TODO") class ExtPageTest(unittest.TestCase): - """Test that the help source list works correctly.""" + """Test that the extension page works correctly. + + The page loads the options of each extension from the default and + user config files, displays those of the selected extension, and + saves the changed ones to the user config file. ZzDummy is the + only extension shipped with IDLE. + """ @classmethod def setUpClass(cls): - page = dialog.extpage + page = cls.page = dialog.extpage dialog.note.select(page) + page.update() + + def setUp(self): + # Restore the option vars changed by the previous test. + self.page.load_extensions() + + def tearDown(self): + self.page.ext_userCfg.remove_section('ZzDummy') + + def test_load_extensions(self): + eq = self.assertEqual + extensions = self.page.extensions + eq(list(extensions), sorted(idleConf.GetExtensions(active_only=False))) + opts = extensions['ZzDummy'] + # The 'enable' options come first, the others follow, both sorted. + eq([opt['name'] for opt in opts], + ['enable', 'enable_editor', 'enable_shell', 'z-text']) + eq([opt['type'] for opt in opts], ['bool', 'bool', 'bool', None]) + eq([opt['default'] for opt in opts], ['False', 'True', 'False', 'Z']) + eq([opt['value'] for opt in opts], [False, True, False, 'Z']) + for opt in opts: + with self.subTest(name=opt['name']): + eq(opt['var'].get(), str(opt['value'])) + + def test_load_extensions_enable_first(self): + # The 'enable' options come first even if they sort last. + page = self.page + page.ext_userCfg.SetOption('ZzDummy', 'a-text', 'A') + page.load_extensions() + self.assertEqual([opt['name'] for opt in page.extensions['ZzDummy']], + ['enable', 'enable_editor', 'enable_shell', + 'a-text', 'z-text']) + + def test_load_extensions_user_value(self): + # A user option overrides the default value, but not the default. + page = self.page + page.ext_userCfg.SetOption('ZzDummy', 'z-text', 'user') + page.load_extensions() + opt = page.extensions['ZzDummy'][-1] + self.assertEqual(opt['name'], 'z-text') + self.assertEqual(opt['default'], 'Z') + self.assertEqual(opt['value'], 'user') + self.assertEqual(opt['var'].get(), 'user') + + def test_extension_selected(self): + eq = self.assertEqual + page = self.page + frame = page.config_frame['ZzDummy'] + # Deselecting hides the options of the current extension. + page.extension_list.selection_clear(0, 'end') + page.extension_selected(None) + eq(page.current_extension, None) + eq(page.details_frame.cget('text'), '') + eq(frame.winfo_manager(), '') + # Selecting shows the options of the selected extension. + page.extension_list.selection_set(0) + page.extension_selected(None) + eq(page.current_extension, 'ZzDummy') + eq(page.details_frame.cget('text'), 'ZzDummy') + eq(frame.winfo_manager(), 'grid') + + def test_set_extension_value_changed(self): + page = self.page + opt = page.extensions['ZzDummy'][-1] # z-text, default 'Z'. + opt['var'].set('user') + self.assertTrue(page.set_extension_value('ZzDummy', opt)) + self.assertEqual(page.ext_userCfg.Get('ZzDummy', 'z-text'), 'user') + # Saving the same value again is not a change. + self.assertFalse(page.set_extension_value('ZzDummy', opt)) + + def test_set_extension_value_default(self): + page = self.page + opt = page.extensions['ZzDummy'][-1] + # The default value is not saved in the user config file. + opt['var'].set('Z') + self.assertFalse(page.set_extension_value('ZzDummy', opt)) + self.assertFalse(page.ext_userCfg.has_option('ZzDummy', 'z-text')) + # Setting it back to the default removes the user option. + page.ext_userCfg.SetOption('ZzDummy', 'z-text', 'user') + self.assertTrue(page.set_extension_value('ZzDummy', opt)) + self.assertFalse(page.ext_userCfg.has_option('ZzDummy', 'z-text')) + + def test_set_extension_value_empty(self): + # An empty value is replaced with the default. + page = self.page + opt = page.extensions['ZzDummy'][-1] + opt['var'].set(' ') + self.assertFalse(page.set_extension_value('ZzDummy', opt)) + self.assertEqual(opt['var'].get(), 'Z') + + def test_save_all_changed_extensions(self): + page = self.page + page.ext_userCfg.Save = Func() + try: + # Nothing is saved if nothing is changed. + page.save_all_changed_extensions() + self.assertEqual(page.ext_userCfg.Save.called, 0) + page.extensions['ZzDummy'][0]['var'].set('True') + page.extensions['ZzDummy'][-1]['var'].set('user') + page.save_all_changed_extensions() + self.assertEqual(page.ext_userCfg.Save.called, 1) + self.assertEqual(page.ext_userCfg.Get('ZzDummy', 'enable'), 'True') + self.assertEqual(page.ext_userCfg.Get('ZzDummy', 'z-text'), 'user') + finally: + del page.ext_userCfg.Save class HelpSourceTest(unittest.TestCase): diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index e28ee549f180aa..b8eddb0e8cf954 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -219,11 +219,13 @@ def setUpClass(cls): cls.root = Tk() cls.root.withdraw() cls.window = Editor(root=cls.root) + cls.text = cls.window.text + cls.window.rmenu = cls.DummyRMenu @classmethod def tearDownClass(cls): cls.window._close() - del cls.window + del cls.window, cls.text cls.root.update_idletasks() for id in cls.root.after_info(): cls.root.after_cancel(id) @@ -233,8 +235,57 @@ def tearDownClass(cls): class DummyRMenu: def tk_popup(x, y): pass - def test_rclick(self): - pass + def click(self, x=0, y=0): + """Simulate a right click at the (x, y) pixel of the text. + + Return the index of the clicked character, as computed by the + widget itself. It cannot be computed here, because the geometry + of the text is unknown while its window is not mapped. + """ + index = self.text.index(f'@{x},{y}') + Event = namedtuple('Event', ['x', 'y', 'x_root', 'y_root']) + event = Event(x, y, x_root=0, y_root=0) + self.assertEqual(self.window.right_menu_event(event), 'break') + return index + + def test_rclick_no_selection(self): + text = self.text + insert(text, 'one two three') + index = self.click() + self.assertEqual(text.tag_ranges('sel'), ()) + self.assertEqual(text.index('insert'), index) + + def test_rclick_outside_selection(self): + text = self.text + insert(text, 'one two three') + # The selection does not contain the clicked character. + text.tag_add('sel', '1.5', '1.8') + text.mark_set('insert', '1.8') + index = self.click() + self.assertEqual(text.tag_ranges('sel'), ()) + self.assertEqual(text.index('insert'), index) + + def test_rclick_inside_selection(self): + text = self.text + insert(text, 'one two three') + # The selection contains the clicked character. + index = text.index('@0,0') + text.tag_add('sel', index, f'{index}+3c') + text.mark_set('insert', 'end-1c') + self.click() + self.assertEqual(text.index('sel.first'), index) + self.assertEqual(text.index('sel.last'), text.index(f'{index}+3c')) + self.assertEqual(text.index('insert'), text.index('end-1c')) + + def test_rmenu_check_copy(self): + text = self.text + insert(text, 'one two three') + eq = self.assertEqual + eq(self.window.rmenu_check_copy(), 'disabled') + eq(self.window.rmenu_check_cut(), 'disabled') + text.tag_add('sel', '1.0', '1.3') + eq(self.window.rmenu_check_copy(), 'normal') + eq(self.window.rmenu_check_cut(), 'normal') if __name__ == '__main__': diff --git a/Lib/idlelib/idle_test/test_grep.py b/Lib/idlelib/idle_test/test_grep.py index 94f217effb656e..08c07b1f0b01c4 100644 --- a/Lib/idlelib/idle_test/test_grep.py +++ b/Lib/idlelib/idle_test/test_grep.py @@ -1,16 +1,19 @@ """ !Changing this line will break Test_findfile.test_found! Non-gui unit tests for grep.GrepDialog methods. -dummy_command calls grep_it calls findfiles. +default_command calls grep_it calls findfiles. An exception raised in one method will fail callers. Otherwise, tests are mostly independent. -Currently only test grep_it, coverage 51%. +Currently test default_command and grep_it, coverage 51%. """ from idlelib import grep import unittest +from unittest import mock from test.support import captured_stdout from idlelib.idle_test.mock_tk import Var +import io import os import re +import sys class Dummy_searchengine: @@ -21,12 +24,15 @@ class Dummy_searchengine: def getpat(self): return self._pat + def getprog(self): + return self._prog + searchengine = Dummy_searchengine() class Dummy_grep: # Methods tested - #default_command = GrepDialog.default_command + default_command = grep.GrepDialog.default_command grep_it = grep.GrepDialog.grep_it # Other stuff needed recvar = Var(False) @@ -166,9 +172,48 @@ def test_found(self): class Default_commandTest(unittest.TestCase): - # To write this, move outwin import to top of GrepDialog - # so it can be replaced by captured_stdout in class setup/teardown. - pass + # default_command searches with grep_it, tested above, writing to an + # OutputWindow, which is replaced here by a text buffer. + + def setUp(self): + self.dialog = Dummy_grep() + self.dialog.top = mock.Mock() # For top.bell(). + self.dialog.flist = 'flist' + self.dialog.globvar = Var(value=__file__) + searchengine._pat = pat = 'xyz*' * 7 # Not in this file. + searchengine._prog = re.compile(pat) + + def default_command(self): + "Return the mock OutputWindow class and what was written to it." + save = sys.stdout + with mock.patch('idlelib.outwin.OutputWindow') as OutputWindow: + OutputWindow.return_value = out = io.StringIO() + self.dialog.default_command() + self.assertIs(sys.stdout, save) # Restored even when not replaced. + return OutputWindow, out.getvalue() + + def test_no_pattern(self): + # An invalid pattern is reported by getprog, not here. + searchengine._prog = None + OutputWindow, output = self.default_command() + OutputWindow.assert_not_called() + self.assertEqual(output, '') + self.dialog.top.bell.assert_not_called() + + def test_no_path(self): + self.dialog.globvar = Var(value='') + OutputWindow, output = self.default_command() + self.dialog.top.bell.assert_called_once() + OutputWindow.assert_not_called() + self.assertEqual(output, '') + + def test_search(self): + OutputWindow, output = self.default_command() + OutputWindow.assert_called_once_with('flist') # The flist argument. + lines = output.split('\n') + self.assertIn(searchengine._pat, lines[0]) + self.assertEqual(lines[1], 'No hits.') + self.dialog.top.bell.assert_not_called() if __name__ == '__main__':