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) <noreply@anthropic.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Co-authored-by: TerryJReedy <tjreedy@udel.edu>
This commit is contained in:
authored and GitHub committed 2026-09-08 20:20:25 -04:00
1 parent 2ddc218b20
commit f8f8c30ed4
7 files changed
+290 -38

No files matched your search

+1
View File
@@ -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
+1 -1
View File
@@ -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.
+1
View File
@@ -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
+42 -16
View File
@@ -454,9 +454,6 @@ class IdleConfTest(unittest.TestCase):
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 @@ class IdleConfTest(unittest.TestCase):
def test_get_current_keyset(self):
current_platform = sys.platform
conf = self.mock_config()
try:
# Ensure that platform isn't darwin
sys.platform = 'some-linux'
self.assertEqual(conf.GetCurrentKeySet(),
conf.GetKeySet(conf.CurrentKeys()))
# 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
# 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 @@ class ChangesTest(unittest.TestCase):
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()
+132 -7
View File
@@ -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
@unittest.skip('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 @@ class ShedPageTest(unittest.TestCase):
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):
+54 -4
View File
@@ -211,8 +211,10 @@ class IndentSearcherTest(unittest.TestCase):
self.assertEqual(actual_pair, expected_pair)
@unittest.skip('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 @@ class RMenuTest(unittest.TestCase):
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 @@ class RMenuTest(unittest.TestCase):
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__':
+59 -10
View File
@@ -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 @@ class Grep_itTest(unittest.TestCase):
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__':