Skip to content

bpo-30917: IDLE: Add config.IdleConf unittest #2691

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jul 18, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4d90afc
bpo-30917: IDLE: Add config.IdleConf unittest
mlouielu Jul 13, 2017
9fc7670
Add pragma to exclude coverage count
mlouielu Jul 13, 2017
d4dac80
Add unittest
mlouielu Jul 13, 2017
eb32d91
Add gui requires
mlouielu Jul 13, 2017
ddedbc7
Revert "Add pragma to exclude coverage count"
mlouielu Jul 15, 2017
06d8329
Add MacOS specific test correct
mlouielu Jul 15, 2017
f29e0da
Split get_user_config_dir to unix and windows
mlouielu Jul 15, 2017
3f2fdcc
Remove unused import
mlouielu Jul 15, 2017
e445a0e
Destroy tk.root after test cases end
mlouielu Jul 15, 2017
0f1607d
Add skipIf at get_user_cfg_dir test
mlouielu Jul 17, 2017
980a801
Update mock_config to deepcopy make it faster
mlouielu Jul 17, 2017
4ecfb1a
Add comment explain why put extra_help_source_list in same test
mlouielu Jul 18, 2017
d9b3702
Add disable extension to unittest
mlouielu Jul 18, 2017
511b726
Add get_keybinding unittest
mlouielu Jul 18, 2017
49e0534
Add save_user_cfg_files unittest
mlouielu Jul 18, 2017
7fba576
News blurb
terryjreedy Jul 18, 2017
206ae1e
supress warnintg, update coverage
terryjreedy Jul 18, 2017
c850b7f
Update 2017-07-17-23-35-57.bpo-30917.hSiuuO.rst
terryjreedy Jul 18, 2017
ef45060
Addressed terry's comments
mlouielu Jul 18, 2017
7627733
Put extra help source list input data reverse to check it is sorted
mlouielu Jul 18, 2017
2cbb960
Merge branch 'add_idleconf_unittest' of github.com:mlouielu/cpython i…
mlouielu Jul 18, 2017
c94d4b5
Remove sorted in RemoveKeyBindNames
mlouielu Jul 18, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 10 additions & 15 deletions Lib/idlelib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,15 @@ class IdleConf:
for config_type in self.config_types:
(user home dir)/.idlerc/config-{config-type}.cfg
"""
def __init__(self):
def __init__(self, _utest=False):
self.config_types = ('main', 'highlight', 'keys', 'extensions')
self.defaultCfg = {}
self.userCfg = {}
self.cfg = {} # TODO use to select userCfg vs defaultCfg
self.CreateConfigHandlers()
self.LoadCfgFiles()

if not _utest:
self.CreateConfigHandlers()
self.LoadCfgFiles()

def CreateConfigHandlers(self):
"Populate default and user config parser dictionaries."
Expand Down Expand Up @@ -463,16 +464,8 @@ def GetExtensions(self, active_only=True,

def RemoveKeyBindNames(self, extnNameList):
"Return extnNameList with keybinding section names removed."
# TODO Easier to return filtered copy with list comp
names = extnNameList
kbNameIndicies = []
for name in names:
if name.endswith(('_bindings', '_cfgBindings')):
kbNameIndicies.append(names.index(name))
kbNameIndicies.sort(reverse=True)
for index in kbNameIndicies: #delete each keybinding section name
del(names[index])
return names
return sorted(
[n for n in extnNameList if not n.endswith(('_bindings', '_cfgBindings'))])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original deleted in_place and did not sort. It was a roundabout version of

for i in reversed(range(names)):
    if names[i].endswith(('_bindings', '_cfgBindings')):
        del(names[i])

I checked that in-place and sorted are not needed. In fact, when doing cleanups, this should be a set comprehension. The using code can then use set union in place of 3 current lines.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

     def RemoveKeyBindNames(self, extnNameList):
         "Return extnNameList with keybinding section names removed."
-        return sorted(
-            [n for n in extnNameList if not n.endswith(('_bindings', '_cfgBindings'))])
+        return [n for n in extnNameList
+                if not n.endswith(('_bindings', '_cfgBindings'))]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was the sorted to be removed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will if I feel like it.


def GetExtnNameForEvent(self, virtualEvent):
"""Return the name of the extension binding virtualEvent, or None.
Expand Down Expand Up @@ -878,7 +871,8 @@ def clear(self):


# TODO Revise test output, write expanded unittest
def _dump(): # htest # (not really, but ignore in coverage)
def _dump(): # pragma: no cover
# htest # (not really, but ignore in coverage)
Copy link
Member

@terryjreedy terryjreedy Jul 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to annotate things that coverage skips (it is now documented ;-).
Moving the # htest # annotation, which I added about a week ago, disables its effect.
The only thing skipped is the comment itself instead of the whole function.
_dump is a human test, but not specifically an htest. I added this note as a reminder for when I grep idlelib for '* htest *', and so no one else would be confused.

from zlib import crc32
line, crc = 0, 0

Expand Down Expand Up @@ -907,7 +901,8 @@ def dumpCfg(cfg):
dumpCfg(idleConf.userCfg)
print('\nlines = ', line, ', crc = ', crc, sep='')

if __name__ == '__main__':

if __name__ == '__main__': # pragma: no cover
Copy link
Member

@terryjreedy terryjreedy Jul 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This skip comes with coverage. I am reviewing now and will probably have other edits. If so, will do this too.

import unittest
unittest.main('idlelib.idle_test.test_config',
verbosity=2, exit=False)
Expand Down
283 changes: 283 additions & 0 deletions Lib/idlelib/idle_test/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import tempfile
from test.support import captured_stderr, findfile
import unittest
from unittest import mock
from idlelib import config

# Tests should not depend on fortuitous user configurations.
Expand Down Expand Up @@ -185,6 +186,288 @@ def test_save(self):
self.assertFalse(os.path.exists(path))


class IdleConfTest(unittest.TestCase):
"""Test for idleConf"""

def new_config(self, _utest=False):
return config.IdleConf(_utest=_utest)

def mock_config(self):
"""Return a mocked idleConf

Both default and user config used the same config-*.def
"""
import sys
conf = config.IdleConf(_utest=True)
if __name__ != '__main__':
idle_dir = os.path.dirname(__file__)
else:
idle_dir = os.path.abspath(sys.path[0])
for ctype in conf.config_types:
config_path = os.path.join(idle_dir, '../config-%s.def' % ctype)
conf.defaultCfg[ctype] = config.IdleConfParser(config_path)
conf.userCfg[ctype] = config.IdleUserConfParser(config_path)
conf.LoadCfgFiles()

return conf
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point of this is unclear just from reading. Having duplicate info in defaultCfg and userCfg is contrary to intended reality (though users can over-ride by hand editing). In general, I think userCfg should be loaded with fake data with read_string the way I did in the CurrentColorKeysTest.

Re-reading and parsing the .def files twice for each of multiple functions is needlessly slow. The files can be read once and then loaded into empty parsers. Besides being faster for reloads, not passing the actual file name prevents accidentally writing the files. I believe that idleConf never changes defaultCfg. (If true, we could in a followup isssue define a readonly IdleDefConfParser that raised on any attempt to change defaults.) If we want to change a particular default parser for a test, we should read into a particular parser in an expanded testCfg.

I did not yet make any changes based on the above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I make this to be done at setUpClass, after that, mock_config will return a deepcopy copy for cls.conf, it should prevent the issue you said here.


def test_get_user_cfg_dir(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed some while ago that IdleConf.GetUserCfgDir is not really a method, in that it does not use the self parameter (idleConf). I have been planning to make it a module function, and with tests written, did it, and moved the test function into a new class, along with that for _warn.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be reference from ipython source code, it turyly seperate this function from a class like what we do here. https://github.com/ipython/ipython/blob/master/IPython/utils/path.py#L172

I think this will need to be discuss on mailing list or bpo. But I prefer to first finish the unittest, then do other stuff to change the behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In config, rename function and move to module level.

+def get_user_directory():
+    """Return a filesystem directory for storing user config files.
+# etc

-        self.userdir = userDir = self.GetUserCfgDir()
+        self.userdir = userDir = get_user_directory()

-    def GetUserCfgDir(self):
-        """Return a filesystem directory for storing user config files.
- # etc

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In test_config

+class FunctionTest(unittest.TestCase):
+    "Test module functions."
+
+    def test_get_user_directory(self):
+        getdir = config.get_user_directory
+        # etc, with getdir replacing call in code as it was.

Move test_warn into same class.

"Test to get user config directory"
conf = self.new_config(_utest=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was only needed to access the function.


# Check normal way should success
with mock.patch('os.path.expanduser', return_value='/home/foo'):
with mock.patch('os.path.exists', return_value=True):
self.assertEqual(conf.GetUserCfgDir(), '/home/foo/.idlerc')

# Check os.getcwd should success
with mock.patch('os.path.expanduser', return_value='~'):
with mock.patch('os.getcwd', return_value='/home/foo/cpython'):
with mock.patch('os.mkdir'):
self.assertEqual(conf.GetUserCfgDir(),
'/home/foo/cpython/.idlerc')

# Check user dir not exists and created failed should raise SystemExit
with mock.patch('os.path.join', return_value='/path/not/exists'):
with self.assertRaises(SystemExit):
with self.assertRaises(FileNotFoundError):
conf.GetUserCfgDir()

def test_create_config_handlers(self):
conf = self.new_config(_utest=True)

# Mock out idle_dir
idle_dir = '/home/foo'
with mock.patch.dict({'__name__': '__foo__'}):
with mock.patch('os.path.dirname', return_value=idle_dir):
conf.CreateConfigHandlers()

# Check keys are equal
self.assertCountEqual(conf.defaultCfg.keys(), conf.config_types)
self.assertCountEqual(conf.userCfg.keys(), conf.config_types)

# Check conf parser are correct type
for default_parser in conf.defaultCfg.values():
self.assertIsInstance(default_parser, config.IdleConfParser)
for user_parser in conf.userCfg.values():
self.assertIsInstance(user_parser, config.IdleUserConfParser)

# Check config path are correct
for config_type, parser in conf.defaultCfg.items():
self.assertEqual(parser.file,
os.path.join(idle_dir, 'config-%s.def' % config_type))
for config_type, parser in conf.userCfg.items():
self.assertEqual(parser.file,
os.path.join(conf.userdir, 'config-%s.cfg' % config_type))

def test_load_cfg_files(self):
conf = self.new_config(_utest=True)

# Borrow test/cfgparser.1 from test_configparser.
config_path = findfile('cfgparser.1')
conf.defaultCfg['foo'] = config.IdleConfParser(config_path)
conf.userCfg['foo'] = config.IdleUserConfParser(config_path)

# Load all config from path
conf.LoadCfgFiles()

eq = self.assertEqual

# Check defaultCfg is loaded
eq(conf.defaultCfg['foo'].Get('Foo Bar', 'foo'), 'newbar')
eq(conf.defaultCfg['foo'].GetOptionList('Foo Bar'), ['foo'])

# Check userCfg is loaded
eq(conf.userCfg['foo'].Get('Foo Bar', 'foo'), 'newbar')
eq(conf.userCfg['foo'].GetOptionList('Foo Bar'), ['foo'])

def test_get_section_list(self):
conf = self.mock_config()

self.assertCountEqual(
conf.GetSectionList('default', 'main'),
['General', 'EditorWindow', 'Indent', 'Theme',
'Keys', 'History', 'HelpFiles'])
self.assertCountEqual(
conf.GetSectionList('user', 'main'),
['General', 'EditorWindow', 'Indent', 'Theme',
'Keys', 'History', 'HelpFiles'])

def test_get_highlight(self):
conf = self.mock_config()

eq = self.assertEqual
eq(conf.GetHighlight('IDLE Classic', 'normal'), {'foreground': '#000000',
'background': '#ffffff'})
eq(conf.GetHighlight('IDLE Classic', 'normal', 'fg'), '#000000')
eq(conf.GetHighlight('IDLE Classic', 'normal', 'bg'), '#ffffff')
with self.assertRaises(config.InvalidFgBg):
conf.GetHighlight('IDLE Classic', 'normal', 'fb')

# Test cursor (this background should be normal-background)
eq(conf.GetHighlight('IDLE Classic', 'cursor'), {'foreground': 'black',
'background': '#ffffff'})

def test_get_theme_dict(self):
"XXX: NOT YET DONE"
conf = self.mock_config()

# These two should be the same
self.assertEqual(
conf.GetThemeDict('default', 'IDLE Classic'),
conf.GetThemeDict('user', 'IDLE Classic'))

with self.assertRaises(config.InvalidTheme):
conf.GetThemeDict('bad', 'IDLE Classic')

def test_current_colors_and_keys(self):
conf = self.mock_config()

self.assertEqual(conf.current_colors_and_keys('Theme'), 'IDLE Classic')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if there needs to be more tested here by using SetOption to change the currently active theme or keys. There's a lot in the docstring and code for just one test.

If it helps, on config dialog, there is a separate boolean for both themes and keys and defines what kind is currently selected. Meaning, if the user has selected a custom (user) theme, then the boolean is false for is_builtin_theme. If the user has selected a default keyset, then the boolean is true for are_builtin_keys. The first check for default is the test of this boolean value to see what kind of theme or keys the user has selected. Then, the rest actually gets the name of the theme (or keys) based on whether the user selected the default or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, just got to the other test class for this. You can probably skip my original comment. Sorry about that.


def test_default_keys(self):
import sys
current_platform = sys.platform
conf = self.new_config(_utest=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a similar test in help_about and Terry said it wasn't necessary to test each branch based on the OS. Maybe his comments don't apply here, but here's a link to the issue:
#2380

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is because #2380 is about bitness, that affect isn't quite huge, but this test do will affect user in the different platform (darwin using Option-Key and other using Alt-Key). So I think it will need to test this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cheryl, #2380 was a peculiar situation in the the correct answer depends on the machine, not the OS, and there is no automated way to get the correct answer. But we do the the correct range -- a set of two items.

While the default_keys tests mimic the current implementation, I can think of two other strategies, and ways to introduce bugs that would be detected by this test.


sys.platform = 'win'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this 'win32'. This is the current platform for consumer Windows regardless of bitness. If someone were to change the current implementation, 'win32' must still work.

self.assertEqual(conf.default_keys(), 'IDLE Classic Windows')

sys.platform = 'darwin'
self.assertEqual(conf.default_keys(), 'IDLE Classic OSX')

sys.platform = 'linux'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this 'somelinux', for similar reason to above.

self.assertEqual(conf.default_keys(), 'IDLE Modern Unix')

# Restore platform
sys.platform = current_platform

def test_get_extensions(self):
conf = self.mock_config()

eq = self.assertEqual
eq(conf.GetExtensions(),
['AutoComplete', 'AutoExpand', 'CallTips', 'CodeContext',
'FormatParagraph', 'ParenMatch', 'RstripExtension', 'ScriptBinding',
'ZoomHeight'])
eq(conf.GetExtensions(active_only=False),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worthwhile to set some to enable=False before running some of the tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks for pointing out. I'll put some extra test data into it.

['AutoComplete', 'AutoExpand', 'CallTips', 'CodeContext',
'FormatParagraph', 'ParenMatch', 'RstripExtension', 'ScriptBinding',
'ZoomHeight'])
eq(conf.GetExtensions(editor_only=True),
['AutoComplete', 'AutoExpand', 'CallTips', 'CodeContext',
'FormatParagraph', 'ParenMatch', 'RstripExtension', 'ScriptBinding',
'ZoomHeight'])
eq(conf.GetExtensions(shell_only=True),
['AutoComplete', 'AutoExpand', 'CallTips', 'FormatParagraph',
'ParenMatch', 'ZoomHeight'])

def test_remove_key_bind_names(self):
conf = self.mock_config()

self.assertEqual(
conf.RemoveKeyBindNames(conf.GetSectionList('default', 'extensions')),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the sorted call removed from the function, add .sort() here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better to add .sort() or to use assertCountEqual()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to assertCountEqual, if we want to changed this to return sorted list, I think it should be after this.

['AutoComplete', 'AutoExpand', 'CallTips', 'CodeContext',
'FormatParagraph', 'ParenMatch', 'RstripExtension', 'ScriptBinding',
'ZoomHeight'])

def test_get_extn_name_for_event(self):
conf = self.mock_config()

eq = self.assertEqual
eq(conf.GetExtnNameForEvent('force-open-completions'), 'AutoComplete')
eq(conf.GetExtnNameForEvent('expand-word'), 'AutoExpand')
eq(conf.GetExtnNameForEvent('force-open-calltip'), 'CallTips')
eq(conf.GetExtnNameForEvent('zoom-height'), 'ZoomHeight')

def test_get_extension_keys(self):
conf = self.mock_config()

eq = self.assertEqual
eq(conf.GetExtensionKeys('AutoComplete'),
{'<<force-open-completions>>': ['<Control-Key-space>']})
eq(conf.GetExtensionKeys('ParenMatch'),
{'<<flash-paren>>': ['<Control-Key-0>']})
eq(conf.GetExtensionKeys('ZoomHeight'),
{'<<zoom-height>>': ['<Alt-Key-2>']})

def test_get_extension_bindings(self):
conf = self.mock_config()

self.assertEqual(conf.GetExtensionBindings('NotExists'), {})
self.assertEqual(
conf.GetExtensionBindings('ZoomHeight'),
{'<<zoom-height>>': ['<Alt-Key-2>']})

def test_get_current_keyset(self):
import sys
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be removed since it is imported at the top of the test, I'll leave here until @terryjreedy push his commit to GitHub (there is a conflict here)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I not pushing anything until tomorrow my afternoon, to avoid conflict with you. (Its past midnight here.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this has been removed.

current_platform = sys.platform
conf = self.mock_config()

# Ensure that platform isn't darwin
sys.platform = 'linux'
self.assertEqual(conf.GetCurrentKeySet(), conf.GetKeySet(conf.CurrentKeys()))

# This should not be the same, sicne replace <Alt- to <Option-
sys.platform = 'darwin'
self.assertNotEqual(conf.GetCurrentKeySet(), conf.GetKeySet(conf.CurrentKeys()))

# Restore platform
sys.platform = current_platform

def test_is_core_binding(self):
# XXX: Should move out the core keys to config file or other place
conf = self.mock_config()

self.assertTrue(conf.IsCoreBinding('copy'))
self.assertTrue(conf.IsCoreBinding('cut'))
self.assertTrue(conf.IsCoreBinding('del-word-right'))
self.assertFalse(conf.IsCoreBinding('not-exists'))

def test_get_extra_help_source_list(self):
conf = self.mock_config()

self.assertEqual(conf.GetExtraHelpSourceList('default'), [])
self.assertEqual(conf.GetExtraHelpSourceList('user'), [])
with self.assertRaises(config.InvalidConfigSet):
self.assertEqual(conf.GetExtraHelpSourceList('bad'), [])

def test_get_all_extra_help_source_list(self):
conf = self.mock_config()

self.assertCountEqual(
conf.GetAllExtraHelpSourcesList(),
conf.GetExtraHelpSourceList('default') + conf.GetExtraHelpSourceList('user'))

def test_get_font(self):
from tkinter import Tk
from tkinter.font import Font
conf = self.mock_config()

root = Tk()
root.withdraw()
f = Font.actual(Font(name='TkFixedFont', exists=True, root=root))

self.assertEqual(
conf.GetFont(root, 'main', 'EditorWindow'),
(f['family'], 10 if f['size'] < 10 else f['size'], f['weight']))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End function with root.destroy() so that children are gone before root disappears.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated.


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if you saw this for TkFixedFont.
https://bugs.python.org/issue24745

Basically, the code picks the best Font for the platform instead of always using the default. I think it might be OK to mock TkFixedFont and f, but maybe it's not necessary.

def test_get_core_keys(self):
conf = self.mock_config()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you want to mock out _warn as you did in other tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, do you point the wrong one? I didn't get any stdout in test_get_core_keys case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I meant that the method for GetCoreKeys has calls to _warn, so I thought you might need to cover them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mocked _warn last night to prevent printed output. Adding more code to provoke calls to warn, and assert that it is called, is a good idea, and needed for complete coverage. But that can be a follow-on PR by any one of us. Reviewing such new code will be easier with this merged.

Requesting a fix for a faulty test that does not test what it claims to test is a different matter. You did well to pick up the number order = alpha order thing, which I missed.


eq = self.assertEqual
eq(conf.GetCoreKeys()['<<center-insert>>'], ['<Control-l>'])
eq(conf.GetCoreKeys()['<<copy>>'], ['<Control-c>', '<Control-C>'])
eq(conf.GetCoreKeys()['<<history-next>>'], ['<Alt-n>'])
eq(conf.GetCoreKeys('IDLE Classic Windows')['<<center-insert>>'],
['<Control-Key-l>', '<Control-Key-L>'])
eq(conf.GetCoreKeys('IDLE Classic OSX')['<<copy>>'], ['<Command-Key-c>'])
eq(conf.GetCoreKeys('IDLE Classic Unix')['<<history-next>>'],
['<Alt-Key-n>', '<Meta-Key-n>'])
eq(conf.GetCoreKeys('IDLE Modern Unix')['<<history-next>>'],
['<Alt-Key-n>', '<Meta-Key-n>'])


class CurrentColorKeysTest(unittest.TestCase):
""" Test colorkeys function with user config [Theme] and [Keys] patterns.

Expand Down