https://github.com/python/cpython/commit/f8f8c30ed4e20208e8badbc9e2fc3822e8db8e49
commit: f8f8c30ed4e20208e8badbc9e2fc3822e8db8e49
branch: main
author: Serhiy Storchaka <[email protected]>
committer: terryjreedy <[email protected]>
date: 2026-09-08T20:20:25-04:00
summary:

gh-155648: Write the empty and placeholder IDLE tests (#156260)

Restore the placeholder import in idlelib/idle_test/template.py
It was removed as an unused import in GH-151478, but template.py is a
skeleton for creating new IDLE test files, and idle_test/README.txt
instructs the user to replace 'zzdummy' with the name of the module
under test.  Add a Ruff per-file ignore to keep it.

test_editor.RMenuTest was added in GH-18951, which fixed right-clicking
inside a selection, with the note that an automated test should follow.
Use the DummyRMenu class left there to test right_menu_event(), and test
the rmenu_check_*() methods that supply the menu entry states.

test_configdialog.ConfigDialogTest was left with two empty stubs in
GH-3592, named after the two ConfigDialog methods which the button tests
only check to be called.  Test them with a fake parent whose instance
dictionary contains an autospecced EditorWindow.

 test_configdialog.ExtPageTest was added empty, with a commented-out
"Nothing here yet TODO" skip, when ExtPage was factored out of
ConfigDialog in GH-26618.  Test load_extensions(), extension_selected(),
set_extension_value() and save_all_changed_extensions().

test_grep.Default_commandTest was left empty in 2013 because
GrepDialog.default_command() imports OutputWindow when called, and the
import cannot be moved to the top of the module due to an import loop.
Replace the imported class with a mock instead of moving the import.

test_config.ChangesTest.test_save_default never called save_all(), so
it tested nothing.  Add the missing assertions, and add the test for
the Save() calls that the following TODO comment asked for.

test_config.IdleConfTest.test_get_current_keyset only tested the
non-darwin branch, because the default key sets no longer contain Alt
keys.  Add an extension binding with an Alt key, so that its
replacement with Option can be tested.  Remove the stale commented-out
test in test_get_extension_keys, which used the ZoomHeight extension.

---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: Hugo van Kemenade <[email protected]>
Co-authored-by: TerryJReedy <[email protected]>

files:
M Lib/.ruff.toml
M Lib/idlelib/idle_test/README.txt
M Lib/idlelib/idle_test/template.py
M Lib/idlelib/idle_test/test_config.py
M Lib/idlelib/idle_test/test_configdialog.py
M Lib/idlelib/idle_test/test_editor.py
M Lib/idlelib/idle_test/test_grep.py

diff --git a/Lib/.ruff.toml b/Lib/.ruff.toml
index c9ae9b95641b31..3c8fb5dd7bfe40 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 `zzdummy`, 
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/README.txt b/Lib/idlelib/idle_test/README.txt
index 242de222524817..bb9aa1fb495fd4 100644
--- a/Lib/idlelib/idle_test/README.txt
+++ b/Lib/idlelib/idle_test/README.txt
@@ -18,7 +18,7 @@ The idle directory, idlelib, has over 60 xyz.py files. The 
idle_test
 subdirectory contains test_xyz.py for each implementation file xyz.py.
 To add a test for abc.py, open idle_test/template.py and immediately
 Save As test_abc.py.  Insert 'abc' on the first line, and replace
-'zzdummy' with 'abc.
+'zzdummy' with 'abc'.
 
 Remove the imports of requires and tkinter if not needed.  Otherwise,
 add to the tkinter imports as needed.
diff --git a/Lib/idlelib/idle_test/template.py 
b/Lib/idlelib/idle_test/template.py
index 7c3df6ba8fbce3..6c3e830769324e 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  # Replace with the module to test.
 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..028d9f9dbd613c 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'),
            {'<<z-in>>': ['<Control-Shift-KeyRelease-Insert>']})
         userextn.remove_section('ZzDummy')
-# need option key test
-##        key = ['<Option-Key-2>'] if sys.platform == 'darwin' else 
['<Alt-Key-2>']
-##        eq(conf.GetExtensionKeys('ZoomHeight'), {'<<zoom-height>>': 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 <Alt- to <Option-.
-        # Above depended on config-extensions.def having Alt keys,
-        # which is no longer true.
-        # sys.platform = 'darwin'
-        # self.assertNotEqual(conf.GetCurrentKeySet(), 
conf.GetKeySet(conf.CurrentKeys()))
-
-        # Restore platform
-        sys.platform = current_platform
+        try:
+            # Ensure that platform isn't darwin
+            sys.platform = 'some-linux'
+            self.assertEqual(conf.GetCurrentKeySet(),
+                             conf.GetKeySet(conf.CurrentKeys()))
+
+            # On darwin, '<Alt-' is replaced with '<Option-'.  Add an
+            # extension binding, as the default key sets have no Alt keys.
+            conf.defaultCfg['extensions'].add_section('Foobar')
+            conf.defaultCfg['extensions'].add_section('Foobar_cfgBindings')
+            conf.defaultCfg['extensions'].set('Foobar', 'enable', 'True')
+            conf.defaultCfg['extensions'].set('Foobar_cfgBindings', 'newfoo',
+                                              '<Alt-Shift-Key-F12>')
+            self.assertEqual(conf.GetKeySet(conf.CurrentKeys())['<<newfoo>>'],
+                             ['<Alt-Shift-Key-F12>'])
+            sys.platform = 'darwin'
+            self.assertEqual(conf.GetCurrentKeySet()['<<newfoo>>'],
+                             ['<Option-Shift-Key-F12>'])
+        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 config in testcfg.values():
+            config.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 config in testcfg.values():
+                del config.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 24d47854dddd9e..3c5f99f98f0bc2 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
@@ -50,14 +51,28 @@ def tearDownModule():
     root = dialog = None
 
 
[email protected]('Empty tests')
 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):
@@ -1296,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 1fcea4f1eb0d6a..873637f67defa3 100644
--- a/Lib/idlelib/idle_test/test_editor.py
+++ b/Lib/idlelib/idle_test/test_editor.py
@@ -211,8 +211,10 @@ def test_searcher(self):
                 self.assertEqual(actual_pair, expected_pair)
 
 
[email protected]('Empty test')
 class RMenuTest(unittest.TestCase):
+    # Test selection-rclick interaction in right_click_event and
+    # rmenu_check_copy(cut) status settings.  These are part of the rmenu
+    # functions common to all text windows with context windows.
 
     @classmethod
     def setUpClass(cls):
@@ -220,11 +222,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)
@@ -234,8 +238,54 @@ def tearDownClass(cls):
     class DummyRMenu:
         def tk_popup(x, y): pass
 
-    def test_rclick(self):
-        pass
+    def click(self):
+        """Simulate a right click at(text pixel 0,0).
+        """
+        Event = namedtuple('Event', ['x', 'y', 'x_root', 'y_root'])
+        event = Event(0, 0, 0, 0)
+        self.assertEqual(self.window.right_menu_event(event), 'break')
+        # This assertion should be moved to a new method that also
+        # text that dummy rmenu.tk_popup is called.
+
+    def test_rclick_not_in_a_selection(self):
+        # Like left click, 'insert' moves to click and any selection is 
deleted.
+        eq = self.assertEqual
+        text = self.text
+        insert(text, 'one two three')
+        # Selection exists but not clicked.
+        text.tag_add('sel', '1.4', '1.7')  # 'two' selected.
+        text.mark_set('insert', '1.7')  # Outside of selection.
+        self.click()
+        eq(text.tag_ranges('sel'), ())
+        eq(text.index('insert'), '1.0')
+        # No selection to click, same result.
+        text.mark_set('insert', '1.8')
+        index = self.click()
+        eq(text.tag_ranges('sel'), ())
+        eq(text.index('insert'), '1.0')
+
+    def test_rclick_inside_selection(self):
+        # Unlike left click, selection is not deleted.
+        eq = self.assertEqual
+        text = self.text
+        insert(text, 'one two three')  # 'insert' at 1.13.
+        # The selection contains the clicked character.
+        text.tag_add('sel', '1.0', '1.3')  # Select 'one'.
+        text.mark_set('insert', '1.3')  # If select rightward, 'insert' at 1.3.
+        self.click()
+        eq((text.index('sel.first'), text.index('sel.last')), ('1.0', '1.3'))
+        eq(text.index('insert'), '1.3')
+
+    def test_rmenu_check_copy(self):
+        # copy and cut only valid for click inside selection.
+        eq = self.assertEqual
+        text = self.text
+        insert(text, 'one two three')
+        eq(self.window.rmenu_check_copy(), 'disabled')
+        eq(self.window.rmenu_check_cut(), 'disabled')
+        text.tag_add('sel', '1.0', '1.3')  # Includes '1.0' click.
+        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..a8c265b9d91ee3 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,16 +24,23 @@ 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
+    # Simplifications: 1. Don't initialize superclass SearchEngineBase
+    # with searchengine.  2. Use directly set class vars instead of
+    # instance vars set from the GUI.
+    globvar = Var('')  # File name.
+    recvar = Var(False)  # Recurse down directories?
+    engine = searchengine  # Pattern and flags.
+    # Methods tested.
     grep_it = grep.GrepDialog.grep_it
-    # Other stuff needed
-    recvar = Var(False)
-    engine = searchengine
+    default_command = grep.GrepDialog.default_command  # Uses grep_it.
+
     def close(self):  # gui method
         pass
 
@@ -166,9 +176,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__':

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to