From f776eb0f0e046f2fa3a96540bb42d8cf970f6c55 Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Wed, 19 Jul 2017 05:17:56 +0800 Subject: bpo-30917: IDLE: Add config.IdleConf unittests (#2691) Patch by Louie Lu. --- Lib/idlelib/config.py | 22 +- Lib/idlelib/idle_test/test_config.py | 447 ++++++++++++++++++++- .../IDLE/2017-07-17-23-35-57.bpo-30917.hSiuuO.rst | 3 + 3 files changed, 454 insertions(+), 18 deletions(-) create mode 100644 Misc/NEWS.d/next/IDLE/2017-07-17-23-35-57.bpo-30917.hSiuuO.rst diff --git a/Lib/idlelib/config.py b/Lib/idlelib/config.py index 4cf4427..63d9a44 100644 --- a/Lib/idlelib/config.py +++ b/Lib/idlelib/config.py @@ -30,6 +30,7 @@ import os import sys from tkinter.font import Font +import idlelib class InvalidConfigType(Exception): pass class InvalidConfigSet(Exception): pass @@ -159,14 +160,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." @@ -215,7 +217,8 @@ class IdleConf: except OSError: warn = ('\n Warning: unable to create user config directory\n' + userDir + '\n Check path and permissions.\n Exiting!\n') - print(warn, file=sys.stderr) + if not idlelib.testing: + print(warn, file=sys.stderr) raise SystemExit # TODO continue without userDIr instead of exit return userDir @@ -463,16 +466,7 @@ class IdleConf: 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 [n for n in extnNameList if not n.endswith(('_bindings', '_cfgBindings'))] def GetExtnNameForEvent(self, virtualEvent): """Return the name of the extension binding virtualEvent, or None. diff --git a/Lib/idlelib/idle_test/test_config.py b/Lib/idlelib/idle_test/test_config.py index f5a609f..197452d 100644 --- a/Lib/idlelib/idle_test/test_config.py +++ b/Lib/idlelib/idle_test/test_config.py @@ -1,15 +1,19 @@ '''Test idlelib.config. -Coverage: 46% (100% for IdleConfParser, IdleUserConfParser*, ConfigChanges). -* Except is OSError clause in Save method. -Much of IdleConf is exercised by ConfigDialog and test_configdialog, -but it should be tested here. +Coverage: 96% (100% for IdleConfParser, IdleUserConfParser*, ConfigChanges). +* Exception is OSError clause in Save method. +Much of IdleConf is also exercised by ConfigDialog and test_configdialog. ''' +import copy +import sys import os import tempfile from test.support import captured_stderr, findfile import unittest +from unittest import mock +import idlelib from idlelib import config +from idlelib.idle_test.mock_idle import Func # Tests should not depend on fortuitous user configurations. # They must not affect actual user .cfg files. @@ -25,9 +29,11 @@ userkeys = testcfg['keys'] = config.IdleUserConfParser('') def setUpModule(): idleConf.userCfg = testcfg + idlelib.testing = True def tearDownModule(): idleConf.userCfg = usercfg + idlelib.testing = False class IdleConfParserTest(unittest.TestCase): @@ -185,6 +191,439 @@ class IdleUserConfParserTest(unittest.TestCase): self.assertFalse(os.path.exists(path)) +class IdleConfTest(unittest.TestCase): + """Test for idleConf""" + + @classmethod + def setUpClass(cls): + 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() + cls.conf = conf + cls.orig_warn = config._warn + config._warn = Func() + + @classmethod + def tearDownClass(cls): + config._warn = cls.orig_warn + + 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 + """ + conf = copy.deepcopy(self.conf) + + return conf + + @unittest.skipIf(sys.platform.startswith('win'), 'this is test for unix system') + def test_get_user_cfg_dir_unix(self): + "Test to get user config directory under unix" + conf = self.new_config(_utest=True) + + # 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() + + @unittest.skipIf(not sys.platform.startswith('win'), 'this is test for windows system') + def test_get_user_cfg_dir_windows(self): + "Test to get user config directory under windows" + conf = self.new_config(_utest=True) + + # Check normal way should success + with mock.patch('os.path.expanduser', return_value='C:\\foo'): + with mock.patch('os.path.exists', return_value=True): + self.assertEqual(conf.GetUserCfgDir(), 'C:\\foo\\.idlerc') + + # Check os.getcwd should success + with mock.patch('os.path.expanduser', return_value='~'): + with mock.patch('os.getcwd', return_value='C:\\foo\\cpython'): + with mock.patch('os.mkdir'): + self.assertEqual(conf.GetUserCfgDir(), + 'C:\\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_save_user_cfg_files(self): + conf = self.mock_config() + + with mock.patch('idlelib.config.IdleUserConfParser.Save') as m: + conf.SaveUserCfgFiles() + self.assertEqual(m.call_count, len(conf.userCfg)) + + def test_get_option(self): + conf = self.mock_config() + + eq = self.assertEqual + eq(conf.GetOption('main', 'EditorWindow', 'width'), '80') + eq(conf.GetOption('main', 'EditorWindow', 'width', type='int'), 80) + with mock.patch('idlelib.config._warn') as _warn: + eq(conf.GetOption('main', 'EditorWindow', 'font', type='int'), None) + eq(conf.GetOption('main', 'EditorWindow', 'NotExists'), None) + eq(conf.GetOption('main', 'EditorWindow', 'NotExists', default='NE'), 'NE') + eq(_warn.call_count, 4) + + def test_set_option(self): + conf = self.mock_config() + + conf.SetOption('main', 'Foo', 'bar', 'newbar') + self.assertEqual(conf.GetOption('main', 'Foo', 'bar'), 'newbar') + + 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']) + + with self.assertRaises(config.InvalidConfigSet): + conf.GetSectionList('foobar', 'main') + with self.assertRaises(config.InvalidConfigType): + conf.GetSectionList('default', 'notexists') + + 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'}) + + # Test get user themes + conf.SetOption('highlight', 'Foobar', 'normal-foreground', '#747474') + conf.SetOption('highlight', 'Foobar', 'normal-background', '#171717') + with mock.patch('idlelib.config._warn'): + eq(conf.GetHighlight('Foobar', 'normal'), {'foreground': '#747474', + 'background': '#171717'}) + + 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_get_current_theme_and_keys(self): + conf = self.mock_config() + + self.assertEqual(conf.CurrentTheme(), conf.current_colors_and_keys('Theme')) + self.assertEqual(conf.CurrentKeys(), conf.current_colors_and_keys('Keys')) + + def test_current_colors_and_keys(self): + conf = self.mock_config() + + self.assertEqual(conf.current_colors_and_keys('Theme'), 'IDLE Classic') + + def test_default_keys(self): + current_platform = sys.platform + conf = self.new_config(_utest=True) + + sys.platform = 'win32' + self.assertEqual(conf.default_keys(), 'IDLE Classic Windows') + + sys.platform = 'darwin' + self.assertEqual(conf.default_keys(), 'IDLE Classic OSX') + + sys.platform = 'some-linux' + self.assertEqual(conf.default_keys(), 'IDLE Modern Unix') + + # Restore platform + sys.platform = current_platform + + def test_get_extensions(self): + conf = self.mock_config() + + # Add disable extensions + conf.SetOption('extensions', 'DISABLE', 'enable', 'False') + + eq = self.assertEqual + eq(conf.GetExtensions(), + ['AutoComplete', 'AutoExpand', 'CallTips', 'CodeContext', + 'FormatParagraph', 'ParenMatch', 'RstripExtension', 'ScriptBinding', + 'ZoomHeight']) + eq(conf.GetExtensions(active_only=False), + ['AutoComplete', 'AutoExpand', 'CallTips', 'CodeContext', + 'FormatParagraph', 'ParenMatch', 'RstripExtension', 'ScriptBinding', + 'ZoomHeight', 'DISABLE']) + 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']) + eq(conf.GetExtensions(active_only=False, editor_only=True), + ['AutoComplete', 'AutoExpand', 'CallTips', 'CodeContext', + 'FormatParagraph', 'ParenMatch', 'RstripExtension', + 'ScriptBinding', 'ZoomHeight', 'DISABLE']) + eq(conf.GetExtensions(active_only=False, shell_only=True), + ['AutoComplete', 'AutoExpand', 'CallTips', 'CodeContext', + 'FormatParagraph', 'ParenMatch', 'RstripExtension', 'ScriptBinding', + 'ZoomHeight', 'DISABLE']) + + # Add user extensions + conf.SetOption('extensions', 'Foobar', 'enable', 'True') + eq(conf.GetExtensions(), + ['AutoComplete', 'AutoExpand', 'CallTips', 'CodeContext', + 'FormatParagraph', 'ParenMatch', 'RstripExtension', + 'ScriptBinding', 'ZoomHeight', 'Foobar']) # User extensions didn't sort + eq(conf.GetExtensions(active_only=False), + ['AutoComplete', 'AutoExpand', 'CallTips', 'CodeContext', + 'FormatParagraph', 'ParenMatch', 'RstripExtension', + 'ScriptBinding', 'ZoomHeight', 'DISABLE', 'Foobar']) + + def test_remove_key_bind_names(self): + conf = self.mock_config() + + self.assertCountEqual( + conf.RemoveKeyBindNames(conf.GetSectionList('default', 'extensions')), + ['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'), + {'<>': ['']}) + eq(conf.GetExtensionKeys('ParenMatch'), + {'<>': ['']}) + + key = [''] if sys.platform == 'darwin' else [''] + eq(conf.GetExtensionKeys('ZoomHeight'), {'<>': key}) + + def test_get_extension_bindings(self): + conf = self.mock_config() + + self.assertEqual(conf.GetExtensionBindings('NotExists'), {}) + + key = [''] if sys.platform == 'darwin' else [''] + self.assertEqual( + conf.GetExtensionBindings('ZoomHeight'), {'<>': key}) + + # Add non-configuarable bindings + conf.defaultCfg['extensions'].add_section('Foobar') + conf.defaultCfg['extensions'].add_section('Foobar_bindings') + conf.defaultCfg['extensions'].set('Foobar', 'enable', 'True') + conf.defaultCfg['extensions'].set('Foobar_bindings', 'foobar', '') + self.assertEqual(conf.GetExtensionBindings('Foobar'), {'<>': ['']}) + + def test_get_keybinding(self): + conf = self.mock_config() + + eq = self.assertEqual + eq(conf.GetKeyBinding('IDLE Modern Unix', '<>'), + ['', '']) + eq(conf.GetKeyBinding('IDLE Classic Unix', '<>'), + ['', '']) + eq(conf.GetKeyBinding('IDLE Classic Windows', '<>'), + ['', '']) + eq(conf.GetKeyBinding('IDLE Classic Mac', '<>'), ['']) + eq(conf.GetKeyBinding('IDLE Classic OSX', '<>'), ['']) + + # Test keybinding not exists + eq(conf.GetKeyBinding('NOT EXISTS', '<>'), []) + eq(conf.GetKeyBinding('IDLE Modern Unix', 'NOT EXISTS'), []) + + 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, sicne replace ') + self.assertEqual(conf.GetKeySet('IDLE Modern Unix')['<>'], '') + + 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_extra_help_source_list(self): + # Test GetExtraHelpSourceList and GetAllExtraHelpSourcesList in same + # place to prevent prepare input data twice. + conf = self.mock_config() + + # Test default with no extra help source + self.assertEqual(conf.GetExtraHelpSourceList('default'), []) + self.assertEqual(conf.GetExtraHelpSourceList('user'), []) + with self.assertRaises(config.InvalidConfigSet): + self.assertEqual(conf.GetExtraHelpSourceList('bad'), []) + self.assertCountEqual( + conf.GetAllExtraHelpSourcesList(), + conf.GetExtraHelpSourceList('default') + conf.GetExtraHelpSourceList('user')) + + # Add help source to user config + conf.userCfg['main'].SetOption('HelpFiles', '4', 'Python;https://python.org') # This is bad input + conf.userCfg['main'].SetOption('HelpFiles', '3', 'Python:https://python.org') # This is bad input + conf.userCfg['main'].SetOption('HelpFiles', '2', 'Pillow;https://pillow.readthedocs.io/en/latest/') + conf.userCfg['main'].SetOption('HelpFiles', '1', 'IDLE;C:/Programs/Python36/Lib/idlelib/help.html') + self.assertEqual(conf.GetExtraHelpSourceList('user'), + [('IDLE', 'C:/Programs/Python36/Lib/idlelib/help.html', '1'), + ('Pillow', 'https://pillow.readthedocs.io/en/latest/', '2'), + ('Python', 'https://python.org', '4')]) + self.assertCountEqual( + conf.GetAllExtraHelpSourcesList(), + conf.GetExtraHelpSourceList('default') + conf.GetExtraHelpSourceList('user')) + + def test_get_font(self): + from test.support import requires + from tkinter import Tk + from tkinter.font import Font + conf = self.mock_config() + + requires('gui') + 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'])) + + # Cleanup root + root.destroy() + del root + + def test_get_core_keys(self): + conf = self.mock_config() + + eq = self.assertEqual + eq(conf.GetCoreKeys()['<>'], ['']) + eq(conf.GetCoreKeys()['<>'], ['', '']) + eq(conf.GetCoreKeys()['<>'], ['']) + eq(conf.GetCoreKeys('IDLE Classic Windows')['<>'], + ['', '']) + eq(conf.GetCoreKeys('IDLE Classic OSX')['<>'], ['']) + eq(conf.GetCoreKeys('IDLE Classic Unix')['<>'], + ['', '']) + eq(conf.GetCoreKeys('IDLE Modern Unix')['<>'], + ['', '']) + + class CurrentColorKeysTest(unittest.TestCase): """ Test colorkeys function with user config [Theme] and [Keys] patterns. diff --git a/Misc/NEWS.d/next/IDLE/2017-07-17-23-35-57.bpo-30917.hSiuuO.rst b/Misc/NEWS.d/next/IDLE/2017-07-17-23-35-57.bpo-30917.hSiuuO.rst new file mode 100644 index 0000000..c2cc9cc --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2017-07-17-23-35-57.bpo-30917.hSiuuO.rst @@ -0,0 +1,3 @@ +Add tests for idlelib.config.IdleConf. +Increase coverage from 46% to 96%. +Patch by Louie Lu. -- cgit v0.12