-
-
Notifications
You must be signed in to change notification settings - Fork 32k
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
Conversation
@mlouielu, thanks for your PR! By analyzing the history of the files in this pull request, we identified @terryjreedy, @kbkaiser and @Yhg1s to be potential reviewers. |
@terryjreedy Could you provide the result of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lib/idlelib/config.py
Outdated
@@ -900,7 +901,8 @@ def dumpCfg(cfg): | |||
dumpCfg(idleConf.userCfg) | |||
print('\nlines = ', line, ', crc = ', crc, sep='') | |||
|
|||
if __name__ == '__main__': | |||
|
|||
if __name__ == '__main__': # pragma: no cover |
There was a problem hiding this comment.
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.
Lib/idlelib/config.py
Outdated
@@ -871,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) |
There was a problem hiding this comment.
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.
You were right. My machine and appveyor: FAIL: test_get_user_cfg_dir (main.IdleConfTest)
|
This reverts commit 9fc7670.
To be mention, I also got some failed on MacOS:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will push the exact changes requested so far.
Lib/idlelib/config.py
Outdated
del(names[index]) | ||
return names | ||
return sorted( | ||
[n for n in extnNameList if not n.endswith(('_bindings', '_cfgBindings'))]) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'))]
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Lib/idlelib/idle_test/test_config.py
Outdated
conf.userCfg[ctype] = config.IdleUserConfParser(config_path) | ||
conf.LoadCfgFiles() | ||
|
||
return conf |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Lib/idlelib/idle_test/test_config.py
Outdated
|
||
return conf | ||
|
||
def test_get_user_cfg_dir(self): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
Lib/idlelib/idle_test/test_config.py
Outdated
|
||
def test_get_user_cfg_dir(self): | ||
"Test to get user config directory" | ||
conf = self.new_config(_utest=True) |
There was a problem hiding this comment.
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.
Since you pushed conflicting comments, I will edit the comments. to show what I did. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The part from where I left off looks pretty good.
Lib/idlelib/idle_test/test_config.py
Outdated
conf = self.mock_config() | ||
|
||
self.assertEqual( | ||
conf.RemoveKeyBindNames(conf.GetSectionList('default', 'extensions')), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()?
There was a problem hiding this comment.
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.
eq(conf.GetExtensions(), | ||
['AutoComplete', 'AutoExpand', 'CallTips', 'CodeContext', | ||
'FormatParagraph', 'ParenMatch', 'RstripExtension', | ||
'ScriptBinding', 'ZoomHeight', 'Foobar']) # User extensions didn't sort |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you delete .sorted() from Remove... and add .sort() here, Foobar will be sorted in the list.
I see the point that loading user extensions with def extensions, even if unrealistic, makes it easy to test functions like this on both. This is good enough for now. I am mainly concerned about repeated disk reads.
Lib/idlelib/idle_test/test_config.py
Outdated
|
||
self.assertEqual( | ||
conf.GetFont(root, 'main', 'EditorWindow'), | ||
(f['family'], 10 if f['size'] < 10 else f['size'], f['weight'])) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated.
Lib/idlelib/idle_test/test_config.py
Outdated
self.assertEqual(conf.GetExtensionBindings('Foobar'), {'<<foobar>>': ['<Key-F>']}) | ||
|
||
def test_get_current_keyset(self): | ||
import sys |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this has been removed.
@csabella would you like to help for reviewing this? |
def test_current_colors_and_keys(self): | ||
conf = self.mock_config() | ||
|
||
self.assertEqual(conf.current_colors_and_keys('Theme'), 'IDLE Classic') |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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): | ||
current_platform = sys.platform | ||
conf = self.new_config(_utest=True) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
['AutoComplete', 'AutoExpand', 'CallTips', 'CodeContext', | ||
'FormatParagraph', 'ParenMatch', 'RstripExtension', 'ScriptBinding', | ||
'ZoomHeight']) | ||
eq(conf.GetExtensions(active_only=False), |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
self.assertEqual(conf.GetExtraHelpSourceList('bad'), []) | ||
self.assertCountEqual( | ||
conf.GetAllExtraHelpSourcesList(), | ||
conf.GetExtraHelpSourceList('default') + conf.GetExtraHelpSourceList('user')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the test for GetAllExtraHelpSourcesList() be in its own method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I merge them into one test because if we split out this test, we will need to prepare the same data twice in the different test (test_get_extra_help_source_list
and test_get_all_extrap_help_source_list
). I'll add the comment on this test. Thanks for pointing out this mistake.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Testing these two functions together, one of which is so trivial (summing 2 calls), is not a mistake. The test should put something into the user config with read_string and test that GetAll returns the same thing as the single call.
Lib/idlelib/idle_test/test_config.py
Outdated
conf.userCfg['main'].SetOption('HelpFiles', '3', 'Python:https://python.org') | ||
self.assertEqual(conf.GetExtraHelpSourceList('user'), | ||
[('IDLE', 'C:/Programs/Python36/Lib/idlelib/help.html', '1'), | ||
('Pillow', 'https://pillow.readthedocs.io/en/latest/', '2')]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code sorts the help sources in option order before returning them. Are they stored that way? Should you add them here out of order (1=Pillow and 2-IDLE) to show that they get sorted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, docstring says that it will sort by option
.
...therefore the returned list must be sorted by 'option'.
So I didn't perform another sort here and directly compare via assertEqual
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe Cheryl had a different, more subtle, and correct point. When number and alphabetical (codepoint) order agree, it does not matter which is used for the sort key. So a function that sorted by value, the first member of the tuple and therefore the default with no key given, instead of option, the third member, would still pass the test. Switch some pair of values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I finally know what is talking about here, I'll take a reverse input for this cases.
self.assertEqual( | ||
conf.GetFont(root, 'main', 'EditorWindow'), | ||
(f['family'], 10 if f['size'] < 10 else f['size'], f['weight'])) | ||
|
There was a problem hiding this comment.
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.
del root | ||
|
||
def test_get_core_keys(self): | ||
conf = self.mock_config() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You did a massive amount of work for this and it's overwhelming to look at. Very impressive. Overall I thought it looks great, but I'm new to unittests, so I think Terry's critical eye will be much more helpful.
I didn't see calls for GetKeyBinding or SaveUserCfgFiles. Since you already have a test for userCfg,Save(), then maybe that could be mocked and just tested that it gets called?
Thanks @csabella, your review helps a lot. I've addressed your point and update the commit. test of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As Cheryl said, this patch is more than enough to comfortably review. As it is, it is a big improvement, adding 50% to coverage.
I just pushed the two changes I consider essential: suppressing printed warnings (I had the one I made conditional on 'not idlelib.testing'); and updating coverage. All the other suggestions I consider optional for this patch. I plan to push it, with or without additional commits when I can get back to it tomorrow.
Lib/idlelib/config.py
Outdated
del(names[index]) | ||
return names | ||
return sorted( | ||
[n for n in extnNameList if not n.endswith(('_bindings', '_cfgBindings'))]) |
There was a problem hiding this comment.
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.
Lib/idlelib/idle_test/test_config.py
Outdated
current_platform = sys.platform | ||
conf = self.new_config(_utest=True) | ||
|
||
sys.platform = 'win' |
There was a problem hiding this comment.
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.
Lib/idlelib/idle_test/test_config.py
Outdated
sys.platform = 'darwin' | ||
self.assertEqual(conf.default_keys(), 'IDLE Classic OSX') | ||
|
||
sys.platform = 'linux' |
There was a problem hiding this comment.
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.
|
||
def test_default_keys(self): | ||
current_platform = sys.platform | ||
conf = self.new_config(_utest=True) |
There was a problem hiding this comment.
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.
Lib/idlelib/idle_test/test_config.py
Outdated
conf.userCfg['main'].SetOption('HelpFiles', '3', 'Python:https://python.org') | ||
self.assertEqual(conf.GetExtraHelpSourceList('user'), | ||
[('IDLE', 'C:/Programs/Python36/Lib/idlelib/help.html', '1'), | ||
('Pillow', 'https://pillow.readthedocs.io/en/latest/', '2')]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe Cheryl had a different, more subtle, and correct point. When number and alphabetical (codepoint) order agree, it does not matter which is used for the sort key. So a function that sorted by value, the first member of the tuple and therefore the default with no key given, instead of option, the third member, would still pass the test. Switch some pair of values.
Patch by Louie Lu. (cherry picked from commit f776eb0)
After this merged, I would like to cleanup config source code.