diff options
Diffstat (limited to 'Lib/idlelib/idle_test')
21 files changed, 1002 insertions, 117 deletions
diff --git a/Lib/idlelib/idle_test/README.txt b/Lib/idlelib/idle_test/README.txt index 2339926..dc7a286 100644 --- a/Lib/idlelib/idle_test/README.txt +++ b/Lib/idlelib/idle_test/README.txt @@ -2,12 +2,12 @@ README FOR IDLE TESTS IN IDLELIB.IDLE_TEST 0. Quick Start -Automated unit tests were added in 2.7 for Python 2.x and 3.3 for Python 3.x. +Automated unit tests were added in 3.3 for Python 3.x. To run the tests from a command line: python -m test.test_idle -Human-mediated tests were added later in 2.7 and in 3.4. +Human-mediated tests were added later in 3.4. python -m idlelib.idle_test.htest @@ -15,9 +15,9 @@ python -m idlelib.idle_test.htest 1. Test Files The idle directory, idlelib, has over 60 xyz.py files. The idle_test -subdirectory should contain a test_xyz.py for each, where 'xyz' is lowercased -even if xyz.py is not. Here is a possible template, with the blanks after after -'.' and 'as', and before and after '_' to be filled in. +subdirectory should contain a test_xyz.py for each, where 'xyz' is +lowercased even if xyz.py is not. Here is a possible template, with the +blanks after '.' and 'as', and before and after '_' to be filled in. import unittest from test.support import requires @@ -30,9 +30,9 @@ class _Test(unittest.TestCase): if __name__ == '__main__': unittest.main(verbosity=2) -Add the following at the end of xyy.py, with the appropriate name added after -'test_'. Some files already have something like this for htest. If so, insert -the import and unittest.main lines before the htest lines. +Add the following at the end of xyy.py, with the appropriate name added +after 'test_'. Some files already have something like this for htest. +If so, insert the import and unittest.main lines before the htest lines. if __name__ == "__main__": import unittest @@ -42,64 +42,82 @@ if __name__ == "__main__": 2. GUI Tests -When run as part of the Python test suite, Idle gui tests need to run -test.support.requires('gui') (test.test_support in 2.7). A test is a gui test -if it creates a Tk root or master object either directly or indirectly by -instantiating a tkinter or idle class. For the benefit of test processes that -either have no graphical environment available or are not allowed to use it, gui -tests must be 'guarded' by "requires('gui')" in a setUp function or method. -This will typically be setUpClass. +When run as part of the Python test suite, Idle GUI tests need to run +test.support.requires('gui'). A test is a GUI test if it creates a +tkinter.Tk root or master object either directly or indirectly by +instantiating a tkinter or idle class. GUI tests cannot run in test +processes that either have no graphical environment available or are not +allowed to use it. -To avoid interfering with other gui tests, all gui objects must be destroyed and -deleted by the end of the test. Widgets, such as a Tk root, created in a setUpX -function, should be destroyed in the corresponding tearDownX. Module and class -widget attributes should also be deleted.. +To guard a module consisting entirely of GUI tests, start with + +from test.support import requires +requires('gui') + +To guard a test class, put "requires('gui')" in its setUpClass function. + +To avoid interfering with other GUI tests, all GUI objects must be destroyed and +deleted by the end of the test. The Tk root created in a setUpX function should +be destroyed in the corresponding tearDownX and the module or class attribute +deleted. Others widgets should descend from the single root and the attributes +deleted BEFORE root is destroyed. See https://bugs.python.org/issue20567. @classmethod def setUpClass(cls): requires('gui') cls.root = tk.Tk() + cls.text = tk.Text(root) @classmethod def tearDownClass(cls): + del cls.text + cls.root.update_idletasks() cls.root.destroy() del cls.root +The update_idletasks call is sometimes needed to prevent the following warning +either when running a test alone or as part of the test suite (#27196). + can't invoke "event" command: application has been destroyed + ... + "ttk::ThemeChanged" Requires('gui') causes the test(s) it guards to be skipped if any of -a few conditions are met: - - - The tests are being run by regrtest.py, and it was started without enabling - the "gui" resource with the "-u" command line option. - - - The tests are being run on Windows by a service that is not allowed to - interact with the graphical environment. - - - The tests are being run on Mac OSX in a process that cannot make a window - manager connection. - +these conditions are met: + + - The tests are being run by regrtest.py, and it was started without + enabling the "gui" resource with the "-u" command line option. + + - The tests are being run on Windows by a service that is not allowed + to interact with the graphical environment. + + - The tests are being run on Linux and X Windows is not available. + + - The tests are being run on Mac OSX in a process that cannot make a + window manager connection. + - tkinter.Tk cannot be successfully instantiated for some reason. - + - test.support.use_resources has been set by something other than regrtest.py and does not contain "gui". - -Tests of non-gui operations should avoid creating tk widgets. Incidental uses of -tk variables and messageboxes can be replaced by the mock classes in -idle_test/mock_tk.py. The mock text handles some uses of the tk Text widget. + +Tests of non-GUI operations should avoid creating tk widgets. Incidental +uses of tk variables and messageboxes can be replaced by the mock +classes in idle_test/mock_tk.py. The mock text handles some uses of the +tk Text widget. 3. Running Unit Tests Assume that xyz.py and test_xyz.py both end with a unittest.main() call. -Running either from an Idle editor runs all tests in the test_xyz file with the -version of Python running Idle. Test output appears in the Shell window. The -'verbosity=2' option lists all test methods in the file, which is appropriate -when developing tests. The 'exit=False' option is needed in xyx.py files when an -htest follows. +Running either from an Idle editor runs all tests in the test_xyz file +with the version of Python running Idle. Test output appears in the +Shell window. The 'verbosity=2' option lists all test methods in the +file, which is appropriate when developing tests. The 'exit=False' +option is needed in xyx.py files when an htest follows. The following command lines also run all test methods, including -gui tests, in test_xyz.py. (Both '-m idlelib' and '-m idlelib.idle' start -Idle and so cannot run tests.) +GUI tests, in test_xyz.py. (Both '-m idlelib' and '-m idlelib.idle' +start Idle and so cannot run tests.) python -m idlelib.xyz python -m idlelib.idle_test.test_xyz @@ -109,35 +127,35 @@ The following runs all idle_test/test_*.py tests interactively. >>> import unittest >>> unittest.main('idlelib.idle_test', verbosity=2) -The following run all Idle tests at a command line. Option '-v' is the same as -'verbosity=2'. (For 2.7, replace 'test' in the second line with -'test.regrtest'.) +The following run all Idle tests at a command line. Option '-v' is the +same as 'verbosity=2'. python -m unittest -v idlelib.idle_test python -m test -v -ugui test_idle python -m test.test_idle -The idle tests are 'discovered' by idlelib.idle_test.__init__.load_tests, -which is also imported into test.test_idle. Normally, neither file should be -changed when working on individual test modules. The third command runs -unittest indirectly through regrtest. The same happens when the entire test -suite is run with 'python -m test'. So that command must work for buildbots -to stay green. Idle tests must not disturb the environment in a way that -makes other tests fail (issue 18081). +The idle tests are 'discovered' by +idlelib.idle_test.__init__.load_tests, which is also imported into +test.test_idle. Normally, neither file should be changed when working on +individual test modules. The third command runs unittest indirectly +through regrtest. The same happens when the entire test suite is run +with 'python -m test'. So that command must work for buildbots to stay +green. Idle tests must not disturb the environment in a way that makes +other tests fail (issue 18081). -To run an individual Testcase or test method, extend the dotted name given to -unittest on the command line. +To run an individual Testcase or test method, extend the dotted name +given to unittest on the command line. python -m unittest -v idlelib.idle_test.test_xyz.Test_case.test_meth 4. Human-mediated Tests -Human-mediated tests are widget tests that cannot be automated but need human -verification. They are contained in idlelib/idle_test/htest.py, which has -instructions. (Some modules need an auxiliary function, identified with # htest -# on the header line.) The set is about complete, though some tests need -improvement. To run all htests, run the htest file from an editor or from the -command line with: +Human-mediated tests are widget tests that cannot be automated but need +human verification. They are contained in idlelib/idle_test/htest.py, +which has instructions. (Some modules need an auxiliary function, +identified with "# htest # on the header line.) The set is about +complete, though some tests need improvement. To run all htests, run the +htest file from an editor or from the command line with: python -m idlelib.idle_test.htest diff --git a/Lib/idlelib/idle_test/__init__.py b/Lib/idlelib/idle_test/__init__.py index 1bc9536..845c92d 100644 --- a/Lib/idlelib/idle_test/__init__.py +++ b/Lib/idlelib/idle_test/__init__.py @@ -1,3 +1,9 @@ +'''idlelib.idle_test is a private implementation of test.test_idle, +which tests the IDLE application as part of the stdlib test suite. +Run IDLE tests alone with "python -m test.test_idle". +This package and its contained modules are subject to change and +any direct use is at your own risk. +''' from os.path import dirname def load_tests(loader, standard_tests, pattern): diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py index 3e24518..58e62cb 100644 --- a/Lib/idlelib/idle_test/htest.py +++ b/Lib/idlelib/idle_test/htest.py @@ -192,7 +192,10 @@ _io_binding_spec = { 'msg': "Test the following bindings.\n" "<Control-o> to open file from dialog.\n" "Edit the file.\n" + "<Control-p> to print the file.\n" "<Control-s> to save the file.\n" + "<Alt-s> to save-as another file.\n" + "<Control-c> to save-copy-as another file.\n" "Check that changes were saved by opening the file elsewhere." } diff --git a/Lib/idlelib/idle_test/mock_tk.py b/Lib/idlelib/idle_test/mock_tk.py index 86fe848..6e35129 100644 --- a/Lib/idlelib/idle_test/mock_tk.py +++ b/Lib/idlelib/idle_test/mock_tk.py @@ -296,3 +296,8 @@ class Text: def bind(sequence=None, func=None, add=None): "Bind to this widget at event sequence a call to function func." pass + +class Entry: + "Mock for tkinter.Entry." + def focus_set(self): + pass diff --git a/Lib/idlelib/idle_test/test_autocomplete.py b/Lib/idlelib/idle_test/test_autocomplete.py index 3a2192e..e4493d1 100644 --- a/Lib/idlelib/idle_test/test_autocomplete.py +++ b/Lib/idlelib/idle_test/test_autocomplete.py @@ -33,9 +33,8 @@ class AutoCompleteTest(unittest.TestCase): @classmethod def tearDownClass(cls): + del cls.editor, cls.text cls.root.destroy() - del cls.text - del cls.editor del cls.root def setUp(self): diff --git a/Lib/idlelib/idle_test/test_autoexpand.py b/Lib/idlelib/idle_test/test_autoexpand.py index 7ca941e..d2a3156 100644 --- a/Lib/idlelib/idle_test/test_autoexpand.py +++ b/Lib/idlelib/idle_test/test_autoexpand.py @@ -25,10 +25,10 @@ class AutoExpandTest(unittest.TestCase): @classmethod def tearDownClass(cls): + del cls.text, cls.auto_expand if hasattr(cls, 'tk'): cls.tk.destroy() del cls.tk - del cls.text, cls.auto_expand def tearDown(self): self.text.delete('1.0', 'end') diff --git a/Lib/idlelib/idle_test/test_config_help.py b/Lib/idlelib/idle_test/test_config_help.py new file mode 100644 index 0000000..664f8ed --- /dev/null +++ b/Lib/idlelib/idle_test/test_config_help.py @@ -0,0 +1,106 @@ +"""Unittests for idlelib.configHelpSourceEdit""" +import unittest +from idlelib.idle_test.mock_tk import Var, Mbox, Entry +from idlelib import configHelpSourceEdit as help_dialog_module + +help_dialog = help_dialog_module.GetHelpSourceDialog + + +class Dummy_help_dialog: + # Mock for testing the following methods of help_dialog + menu_ok = help_dialog.menu_ok + path_ok = help_dialog.path_ok + ok = help_dialog.ok + cancel = help_dialog.cancel + # Attributes, constant or variable, needed for tests + menu = Var() + entryMenu = Entry() + path = Var() + entryPath = Entry() + result = None + destroyed = False + + def destroy(self): + self.destroyed = True + + +# menu_ok and path_ok call Mbox.showerror if menu and path are not ok. +orig_mbox = help_dialog_module.tkMessageBox +showerror = Mbox.showerror + + +class ConfigHelpTest(unittest.TestCase): + dialog = Dummy_help_dialog() + + @classmethod + def setUpClass(cls): + help_dialog_module.tkMessageBox = Mbox + + @classmethod + def tearDownClass(cls): + help_dialog_module.tkMessageBox = orig_mbox + + def test_blank_menu(self): + self.dialog.menu.set('') + self.assertFalse(self.dialog.menu_ok()) + self.assertEqual(showerror.title, 'Menu Item Error') + self.assertIn('No', showerror.message) + + def test_long_menu(self): + self.dialog.menu.set('hello' * 10) + self.assertFalse(self.dialog.menu_ok()) + self.assertEqual(showerror.title, 'Menu Item Error') + self.assertIn('long', showerror.message) + + def test_good_menu(self): + self.dialog.menu.set('help') + showerror.title = 'No Error' # should not be called + self.assertTrue(self.dialog.menu_ok()) + self.assertEqual(showerror.title, 'No Error') + + def test_blank_path(self): + self.dialog.path.set('') + self.assertFalse(self.dialog.path_ok()) + self.assertEqual(showerror.title, 'File Path Error') + self.assertIn('No', showerror.message) + + def test_invalid_file_path(self): + self.dialog.path.set('foobar' * 100) + self.assertFalse(self.dialog.path_ok()) + self.assertEqual(showerror.title, 'File Path Error') + self.assertIn('not exist', showerror.message) + + def test_invalid_url_path(self): + self.dialog.path.set('ww.foobar.com') + self.assertFalse(self.dialog.path_ok()) + self.assertEqual(showerror.title, 'File Path Error') + self.assertIn('not exist', showerror.message) + + self.dialog.path.set('htt.foobar.com') + self.assertFalse(self.dialog.path_ok()) + self.assertEqual(showerror.title, 'File Path Error') + self.assertIn('not exist', showerror.message) + + def test_good_path(self): + self.dialog.path.set('https://docs.python.org') + showerror.title = 'No Error' # should not be called + self.assertTrue(self.dialog.path_ok()) + self.assertEqual(showerror.title, 'No Error') + + def test_ok(self): + self.dialog.destroyed = False + self.dialog.menu.set('help') + self.dialog.path.set('https://docs.python.org') + self.dialog.ok() + self.assertEqual(self.dialog.result, ('help', + 'https://docs.python.org')) + self.assertTrue(self.dialog.destroyed) + + def test_cancel(self): + self.dialog.destroyed = False + self.dialog.cancel() + self.assertEqual(self.dialog.result, None) + self.assertTrue(self.dialog.destroyed) + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_configdialog.py b/Lib/idlelib/idle_test/test_configdialog.py index 6883123..b063601 100644 --- a/Lib/idlelib/idle_test/test_configdialog.py +++ b/Lib/idlelib/idle_test/test_configdialog.py @@ -1,31 +1,31 @@ -'''Unittests for idlelib/configHandler.py - -Coverage: 46% just by creating dialog. The other half is change code. +'''Test idlelib.configDialog. +Coverage: 46% just by creating dialog. +The other half is code for working with user customizations. ''' -import unittest +from idlelib.configDialog import ConfigDialog # always test import from test.support import requires +requires('gui') from tkinter import Tk -from idlelib.configDialog import ConfigDialog -from idlelib.macosxSupport import _initializeTkVariantTests - +import unittest +from idlelib import macosxSupport as macosx class ConfigDialogTest(unittest.TestCase): @classmethod def setUpClass(cls): - requires('gui') cls.root = Tk() - _initializeTkVariantTests(cls.root) + macosx._initializeTkVariantTests(cls.root) @classmethod def tearDownClass(cls): + cls.root.update_idletasks() cls.root.destroy() del cls.root def test_dialog(self): - d=ConfigDialog(self.root, 'Test', _utest=True) - d.destroy() + d = ConfigDialog(self.root, 'Test', _utest=True) + d.remove_var_callbacks() if __name__ == '__main__': diff --git a/Lib/idlelib/idle_test/test_delegator.py b/Lib/idlelib/idle_test/test_delegator.py index b8ae5ee..1f0baa9 100644 --- a/Lib/idlelib/idle_test/test_delegator.py +++ b/Lib/idlelib/idle_test/test_delegator.py @@ -4,34 +4,37 @@ from idlelib.Delegator import Delegator class DelegatorTest(unittest.TestCase): def test_mydel(self): - # test a simple use scenario + # Test a simple use scenario. - # initialize + # Initialize an int delegator. mydel = Delegator(int) self.assertIs(mydel.delegate, int) self.assertEqual(mydel._Delegator__cache, set()) - - # add an attribute: + # Trying to access a non-attribute of int fails. self.assertRaises(AttributeError, mydel.__getattr__, 'xyz') + + # Add real int attribute 'bit_length' by accessing it. bl = mydel.bit_length self.assertIs(bl, int.bit_length) self.assertIs(mydel.__dict__['bit_length'], int.bit_length) self.assertEqual(mydel._Delegator__cache, {'bit_length'}) - # add a second attribute + # Add attribute 'numerator'. mydel.numerator self.assertEqual(mydel._Delegator__cache, {'bit_length', 'numerator'}) - # delete the second (which, however, leaves it in the name cache) + # Delete 'numerator'. del mydel.numerator self.assertNotIn('numerator', mydel.__dict__) - self.assertIn('numerator', mydel._Delegator__cache) + # The current implementation leaves it in the name cache. + # self.assertIn('numerator', mydel._Delegator__cache) + # However, this is not required and not part of the specification - # reset by calling .setdelegate, which calls .resetcache - mydel.setdelegate(float) - self.assertIs(mydel.delegate, float) + # Change delegate to float, first resetting the attributes. + mydel.setdelegate(float) # calls resetcache self.assertNotIn('bit_length', mydel.__dict__) self.assertEqual(mydel._Delegator__cache, set()) + self.assertIs(mydel.delegate, float) if __name__ == '__main__': unittest.main(verbosity=2, exit=2) diff --git a/Lib/idlelib/idle_test/test_editmenu.py b/Lib/idlelib/idle_test/test_editmenu.py new file mode 100644 index 0000000..50317a9 --- /dev/null +++ b/Lib/idlelib/idle_test/test_editmenu.py @@ -0,0 +1,71 @@ +'''Test (selected) IDLE Edit menu items. + +Edit modules have their own test files files +''' +from test.support import requires +requires('gui') +import tkinter as tk +import unittest +from idlelib import PyShell + +class PasteTest(unittest.TestCase): + '''Test pasting into widgets that allow pasting. + + On X11, replacing selections requires tk fix. + ''' + @classmethod + def setUpClass(cls): + cls.root = root = tk.Tk() + PyShell.fix_x11_paste(root) + cls.text = tk.Text(root) + cls.entry = tk.Entry(root) + cls.spin = tk.Spinbox(root) + root.clipboard_clear() + root.clipboard_append('two') + + @classmethod + def tearDownClass(cls): + del cls.text, cls.entry, cls.spin + cls.root.clipboard_clear() + cls.root.update_idletasks() + cls.root.destroy() + del cls.root + + def test_paste_text(self): + "Test pasting into text with and without a selection." + text = self.text + for tag, ans in ('', 'onetwo\n'), ('sel', 'two\n'): + with self.subTest(tag=tag, ans=ans): + text.delete('1.0', 'end') + text.insert('1.0', 'one', tag) + text.event_generate('<<Paste>>') + self.assertEqual(text.get('1.0', 'end'), ans) + + def test_paste_entry(self): + "Test pasting into an entry with and without a selection." + # On 3.6, generated <<Paste>> fails without empty select range + # for 'no selection'. Live widget works fine. + entry = self.entry + for end, ans in (0, 'onetwo'), ('end', 'two'): + with self.subTest(entry=entry, end=end, ans=ans): + entry.delete(0, 'end') + entry.insert(0, 'one') + entry.select_range(0, end) # see note + entry.event_generate('<<Paste>>') + self.assertEqual(entry.get(), ans) + + def test_paste_spin(self): + "Test pasting into a spinbox with and without a selection." + # See note above for entry. + spin = self.spin + for end, ans in (0, 'onetwo'), ('end', 'two'): + with self.subTest(end=end, ans=ans): + spin.delete(0, 'end') + spin.insert(0, 'one') + spin.selection('range', 0, end) # see note + spin.event_generate('<<Paste>>') + self.assertEqual(spin.get(), ans) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_formatparagraph.py b/Lib/idlelib/idle_test/test_formatparagraph.py index f6039e6..e5561d8 100644 --- a/Lib/idlelib/idle_test/test_formatparagraph.py +++ b/Lib/idlelib/idle_test/test_formatparagraph.py @@ -276,10 +276,9 @@ class FormatEventTest(unittest.TestCase): @classmethod def tearDownClass(cls): + del cls.text, cls.formatter cls.root.destroy() del cls.root - del cls.text - del cls.formatter def test_short_line(self): self.text.insert('1.0', "Short line\n") diff --git a/Lib/idlelib/idle_test/test_help_about.py b/Lib/idlelib/idle_test/test_help_about.py new file mode 100644 index 0000000..d0a0127 --- /dev/null +++ b/Lib/idlelib/idle_test/test_help_about.py @@ -0,0 +1,52 @@ +'''Test idlelib.help_about. + +Coverage: +''' +from idlelib import aboutDialog as help_about +from idlelib import textView as textview +from idlelib.idle_test.mock_idle import Func +from idlelib.idle_test.mock_tk import Mbox +import unittest + +About = help_about.AboutDialog +class Dummy_about_dialog(): + # Dummy class for testing file display functions. + idle_credits = About.ShowIDLECredits + idle_readme = About.ShowIDLEAbout + idle_news = About.ShowIDLENEWS + # Called by the above + display_file_text = About.display_file_text + + +class DisplayFileTest(unittest.TestCase): + "Test that .txt files are found and properly decoded." + dialog = Dummy_about_dialog() + + @classmethod + def setUpClass(cls): + cls.orig_mbox = textview.tkMessageBox + cls.orig_view = textview.view_text + cls.mbox = Mbox() + cls.view = Func() + textview.tkMessageBox = cls.mbox + textview.view_text = cls.view + cls.About = Dummy_about_dialog() + + @classmethod + def tearDownClass(cls): + textview.tkMessageBox = cls.orig_mbox + textview.view_text = cls.orig_view + + def test_file_isplay(self): + for handler in (self.dialog.idle_credits, + self.dialog.idle_readme, + self.dialog.idle_news): + self.mbox.showerror.message = '' + self.view.called = False + handler() + self.assertEqual(self.mbox.showerror.message, '') + self.assertEqual(self.view.called, True) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_parenmatch.py b/Lib/idlelib/idle_test/test_parenmatch.py index 9aba4be..95cc22c 100644 --- a/Lib/idlelib/idle_test/test_parenmatch.py +++ b/Lib/idlelib/idle_test/test_parenmatch.py @@ -1,10 +1,13 @@ -"""Test idlelib.ParenMatch.""" -# This must currently be a gui test because ParenMatch methods use -# several text methods not defined on idlelib.idle_test.mock_tk.Text. +'''Test idlelib.ParenMatch. + +This must currently be a gui test because ParenMatch methods use +several text methods not defined on idlelib.idle_test.mock_tk.Text. +''' +from test.support import requires +requires('gui') import unittest from unittest.mock import Mock -from test.support import requires from tkinter import Tk, Text from idlelib.ParenMatch import ParenMatch @@ -20,7 +23,6 @@ class ParenMatchTest(unittest.TestCase): @classmethod def setUpClass(cls): - requires('gui') cls.root = Tk() cls.text = Text(cls.root) cls.editwin = DummyEditwin(cls.text) @@ -29,6 +31,7 @@ class ParenMatchTest(unittest.TestCase): @classmethod def tearDownClass(cls): del cls.text, cls.editwin + cls.root.update_idletasks() cls.root.destroy() del cls.root diff --git a/Lib/idlelib/idle_test/test_percolator.py b/Lib/idlelib/idle_test/test_percolator.py new file mode 100644 index 0000000..4c0a7ad --- /dev/null +++ b/Lib/idlelib/idle_test/test_percolator.py @@ -0,0 +1,118 @@ +'''Test Percolator''' +from test.support import requires +requires('gui') + +import unittest +from tkinter import Text, Tk, END +from idlelib.Percolator import Percolator, Delegator + + +class MyFilter(Delegator): + def __init__(self): + Delegator.__init__(self, None) + + def insert(self, *args): + self.insert_called_with = args + self.delegate.insert(*args) + + def delete(self, *args): + self.delete_called_with = args + self.delegate.delete(*args) + + def uppercase_insert(self, index, chars, tags=None): + chars = chars.upper() + self.delegate.insert(index, chars) + + def lowercase_insert(self, index, chars, tags=None): + chars = chars.lower() + self.delegate.insert(index, chars) + + def dont_insert(self, index, chars, tags=None): + pass + + +class PercolatorTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.root = Tk() + cls.text = Text(cls.root) + + @classmethod + def tearDownClass(cls): + del cls.text + cls.root.destroy() + del cls.root + + def setUp(self): + self.percolator = Percolator(self.text) + self.filter_one = MyFilter() + self.filter_two = MyFilter() + self.percolator.insertfilter(self.filter_one) + self.percolator.insertfilter(self.filter_two) + + def tearDown(self): + self.percolator.close() + self.text.delete('1.0', END) + + def test_insertfilter(self): + self.assertIsNotNone(self.filter_one.delegate) + self.assertEqual(self.percolator.top, self.filter_two) + self.assertEqual(self.filter_two.delegate, self.filter_one) + self.assertEqual(self.filter_one.delegate, self.percolator.bottom) + + def test_removefilter(self): + filter_three = MyFilter() + self.percolator.removefilter(self.filter_two) + self.assertEqual(self.percolator.top, self.filter_one) + self.assertIsNone(self.filter_two.delegate) + + filter_three = MyFilter() + self.percolator.insertfilter(self.filter_two) + self.percolator.insertfilter(filter_three) + self.percolator.removefilter(self.filter_one) + self.assertEqual(self.percolator.top, filter_three) + self.assertEqual(filter_three.delegate, self.filter_two) + self.assertEqual(self.filter_two.delegate, self.percolator.bottom) + self.assertIsNone(self.filter_one.delegate) + + def test_insert(self): + self.text.insert('insert', 'foo') + self.assertEqual(self.text.get('1.0', END), 'foo\n') + self.assertTupleEqual(self.filter_one.insert_called_with, + ('insert', 'foo', None)) + + def test_modify_insert(self): + self.filter_one.insert = self.filter_one.uppercase_insert + self.text.insert('insert', 'bAr') + self.assertEqual(self.text.get('1.0', END), 'BAR\n') + + def test_modify_chain_insert(self): + filter_three = MyFilter() + self.percolator.insertfilter(filter_three) + self.filter_two.insert = self.filter_two.uppercase_insert + self.filter_one.insert = self.filter_one.lowercase_insert + self.text.insert('insert', 'BaR') + self.assertEqual(self.text.get('1.0', END), 'bar\n') + + def test_dont_insert(self): + self.filter_one.insert = self.filter_one.dont_insert + self.text.insert('insert', 'foo bar') + self.assertEqual(self.text.get('1.0', END), '\n') + self.filter_one.insert = self.filter_one.dont_insert + self.text.insert('insert', 'foo bar') + self.assertEqual(self.text.get('1.0', END), '\n') + + def test_without_filter(self): + self.text.insert('insert', 'hello') + self.assertEqual(self.text.get('1.0', 'end'), 'hello\n') + + def test_delete(self): + self.text.insert('insert', 'foo') + self.text.delete('1.0', '1.2') + self.assertEqual(self.text.get('1.0', END), 'o\n') + self.assertTupleEqual(self.filter_one.delete_called_with, + ('1.0', '1.2')) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_replacedialog.py b/Lib/idlelib/idle_test/test_replacedialog.py new file mode 100644 index 0000000..ff44820 --- /dev/null +++ b/Lib/idlelib/idle_test/test_replacedialog.py @@ -0,0 +1,293 @@ +"""Unittest for idlelib.ReplaceDialog""" +from test.support import requires +requires('gui') + +import unittest +from unittest.mock import Mock +from tkinter import Tk, Text +from idlelib.idle_test.mock_tk import Mbox +import idlelib.SearchEngine as se +import idlelib.ReplaceDialog as rd + +orig_mbox = se.tkMessageBox +showerror = Mbox.showerror + + +class ReplaceDialogTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.root = Tk() + cls.root.withdraw() + se.tkMessageBox = Mbox + cls.engine = se.SearchEngine(cls.root) + cls.dialog = rd.ReplaceDialog(cls.root, cls.engine) + cls.dialog.ok = Mock() + cls.text = Text(cls.root) + cls.text.undo_block_start = Mock() + cls.text.undo_block_stop = Mock() + cls.dialog.text = cls.text + + @classmethod + def tearDownClass(cls): + se.tkMessageBox = orig_mbox + del cls.text, cls.dialog, cls.engine + cls.root.destroy() + del cls.root + + def setUp(self): + self.text.insert('insert', 'This is a sample sTring') + + def tearDown(self): + self.engine.patvar.set('') + self.dialog.replvar.set('') + self.engine.wordvar.set(False) + self.engine.casevar.set(False) + self.engine.revar.set(False) + self.engine.wrapvar.set(True) + self.engine.backvar.set(False) + showerror.title = '' + showerror.message = '' + self.text.delete('1.0', 'end') + + def test_replace_simple(self): + # Test replace function with all options at default setting. + # Wrap around - True + # Regular Expression - False + # Match case - False + # Match word - False + # Direction - Forwards + text = self.text + equal = self.assertEqual + pv = self.engine.patvar + rv = self.dialog.replvar + replace = self.dialog.replace_it + + # test accessor method + self.engine.setpat('asdf') + equal(self.engine.getpat(), pv.get()) + + # text found and replaced + pv.set('a') + rv.set('asdf') + self.dialog.open(self.text) + replace() + equal(text.get('1.8', '1.12'), 'asdf') + + # dont "match word" case + text.mark_set('insert', '1.0') + pv.set('is') + rv.set('hello') + replace() + equal(text.get('1.2', '1.7'), 'hello') + + # dont "match case" case + pv.set('string') + rv.set('world') + replace() + equal(text.get('1.23', '1.28'), 'world') + + # without "regular expression" case + text.mark_set('insert', 'end') + text.insert('insert', '\nline42:') + before_text = text.get('1.0', 'end') + pv.set('[a-z][\d]+') + replace() + after_text = text.get('1.0', 'end') + equal(before_text, after_text) + + # test with wrap around selected and complete a cycle + text.mark_set('insert', '1.9') + pv.set('i') + rv.set('j') + replace() + equal(text.get('1.8'), 'i') + equal(text.get('2.1'), 'j') + replace() + equal(text.get('2.1'), 'j') + equal(text.get('1.8'), 'j') + before_text = text.get('1.0', 'end') + replace() + after_text = text.get('1.0', 'end') + equal(before_text, after_text) + + # text not found + before_text = text.get('1.0', 'end') + pv.set('foobar') + replace() + after_text = text.get('1.0', 'end') + equal(before_text, after_text) + + # test access method + self.dialog.find_it(0) + + def test_replace_wrap_around(self): + text = self.text + equal = self.assertEqual + pv = self.engine.patvar + rv = self.dialog.replvar + replace = self.dialog.replace_it + self.engine.wrapvar.set(False) + + # replace candidate found both after and before 'insert' + text.mark_set('insert', '1.4') + pv.set('i') + rv.set('j') + replace() + equal(text.get('1.2'), 'i') + equal(text.get('1.5'), 'j') + replace() + equal(text.get('1.2'), 'i') + equal(text.get('1.20'), 'j') + replace() + equal(text.get('1.2'), 'i') + + # replace candidate found only before 'insert' + text.mark_set('insert', '1.8') + pv.set('is') + before_text = text.get('1.0', 'end') + replace() + after_text = text.get('1.0', 'end') + equal(before_text, after_text) + + def test_replace_whole_word(self): + text = self.text + equal = self.assertEqual + pv = self.engine.patvar + rv = self.dialog.replvar + replace = self.dialog.replace_it + self.engine.wordvar.set(True) + + pv.set('is') + rv.set('hello') + replace() + equal(text.get('1.0', '1.4'), 'This') + equal(text.get('1.5', '1.10'), 'hello') + + def test_replace_match_case(self): + equal = self.assertEqual + text = self.text + pv = self.engine.patvar + rv = self.dialog.replvar + replace = self.dialog.replace_it + self.engine.casevar.set(True) + + before_text = self.text.get('1.0', 'end') + pv.set('this') + rv.set('that') + replace() + after_text = self.text.get('1.0', 'end') + equal(before_text, after_text) + + pv.set('This') + replace() + equal(text.get('1.0', '1.4'), 'that') + + def test_replace_regex(self): + equal = self.assertEqual + text = self.text + pv = self.engine.patvar + rv = self.dialog.replvar + replace = self.dialog.replace_it + self.engine.revar.set(True) + + before_text = text.get('1.0', 'end') + pv.set('[a-z][\d]+') + rv.set('hello') + replace() + after_text = text.get('1.0', 'end') + equal(before_text, after_text) + + text.insert('insert', '\nline42') + replace() + equal(text.get('2.0', '2.8'), 'linhello') + + pv.set('') + replace() + self.assertIn('error', showerror.title) + self.assertIn('Empty', showerror.message) + + pv.set('[\d') + replace() + self.assertIn('error', showerror.title) + self.assertIn('Pattern', showerror.message) + + showerror.title = '' + showerror.message = '' + pv.set('[a]') + rv.set('test\\') + replace() + self.assertIn('error', showerror.title) + self.assertIn('Invalid Replace Expression', showerror.message) + + # test access method + self.engine.setcookedpat("\'") + equal(pv.get(), "\\'") + + def test_replace_backwards(self): + equal = self.assertEqual + text = self.text + pv = self.engine.patvar + rv = self.dialog.replvar + replace = self.dialog.replace_it + self.engine.backvar.set(True) + + text.insert('insert', '\nis as ') + + pv.set('is') + rv.set('was') + replace() + equal(text.get('1.2', '1.4'), 'is') + equal(text.get('2.0', '2.3'), 'was') + replace() + equal(text.get('1.5', '1.8'), 'was') + replace() + equal(text.get('1.2', '1.5'), 'was') + + def test_replace_all(self): + text = self.text + pv = self.engine.patvar + rv = self.dialog.replvar + replace_all = self.dialog.replace_all + + text.insert('insert', '\n') + text.insert('insert', text.get('1.0', 'end')*100) + pv.set('is') + rv.set('was') + replace_all() + self.assertNotIn('is', text.get('1.0', 'end')) + + self.engine.revar.set(True) + pv.set('') + replace_all() + self.assertIn('error', showerror.title) + self.assertIn('Empty', showerror.message) + + pv.set('[s][T]') + rv.set('\\') + replace_all() + + self.engine.revar.set(False) + pv.set('text which is not present') + rv.set('foobar') + replace_all() + + def test_default_command(self): + text = self.text + pv = self.engine.patvar + rv = self.dialog.replvar + replace_find = self.dialog.default_command + equal = self.assertEqual + + pv.set('This') + rv.set('was') + replace_find() + equal(text.get('sel.first', 'sel.last'), 'was') + + self.engine.revar.set(True) + pv.set('') + replace_find() + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_searchdialog.py b/Lib/idlelib/idle_test/test_searchdialog.py new file mode 100644 index 0000000..190c866 --- /dev/null +++ b/Lib/idlelib/idle_test/test_searchdialog.py @@ -0,0 +1,80 @@ +"""Test SearchDialog class in SearchDialogue.py""" + +# Does not currently test the event handler wrappers. +# A usage test should simulate clicks and check hilighting. +# Tests need to be coordinated with SearchDialogBase tests +# to avoid duplication. + +from test.support import requires +requires('gui') + +import unittest +import tkinter as tk +from tkinter import BooleanVar +import idlelib.SearchEngine as se +import idlelib.SearchDialog as sd + + +class SearchDialogTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.root = tk.Tk() + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + del cls.root + + def setUp(self): + self.engine = se.SearchEngine(self.root) + self.dialog = sd.SearchDialog(self.root, self.engine) + self.text = tk.Text(self.root) + self.text.insert('1.0', 'Hello World!') + + def test_find_again(self): + # Search for various expressions + text = self.text + + self.engine.setpat('') + self.assertFalse(self.dialog.find_again(text)) + + self.engine.setpat('Hello') + self.assertTrue(self.dialog.find_again(text)) + + self.engine.setpat('Goodbye') + self.assertFalse(self.dialog.find_again(text)) + + self.engine.setpat('World!') + self.assertTrue(self.dialog.find_again(text)) + + self.engine.setpat('Hello World!') + self.assertTrue(self.dialog.find_again(text)) + + # Regular expression + self.engine.revar = BooleanVar(self.root, True) + self.engine.setpat('W[aeiouy]r') + self.assertTrue(self.dialog.find_again(text)) + + def test_find_selection(self): + # Select some text and make sure it's found + text = self.text + # Add additional line to find + self.text.insert('2.0', 'Hello World!') + + text.tag_add('sel', '1.0', '1.4') # Select 'Hello' + self.assertTrue(self.dialog.find_selection(text)) + + text.tag_remove('sel', '1.0', 'end') + text.tag_add('sel', '1.6', '1.11') # Select 'World!' + self.assertTrue(self.dialog.find_selection(text)) + + text.tag_remove('sel', '1.0', 'end') + text.tag_add('sel', '1.0', '1.11') # Select 'Hello World!' + self.assertTrue(self.dialog.find_selection(text)) + + # Remove additional line + text.delete('2.0', 'end') + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=2) diff --git a/Lib/idlelib/idle_test/test_searchengine.py b/Lib/idlelib/idle_test/test_searchengine.py index c7792fb..edbd558 100644 --- a/Lib/idlelib/idle_test/test_searchengine.py +++ b/Lib/idlelib/idle_test/test_searchengine.py @@ -178,7 +178,7 @@ class SearchEngineTest(unittest.TestCase): engine.revar.set(1) Equal(engine.getprog(), None) self.assertEqual(Mbox.showerror.message, - 'Error: nothing to repeat\nPattern: +') + 'Error: nothing to repeat at position 0\nPattern: +') def test_report_error(self): showerror = Mbox.showerror diff --git a/Lib/idlelib/idle_test/test_textview.py b/Lib/idlelib/idle_test/test_textview.py index 68e5b82..02d1472 100644 --- a/Lib/idlelib/idle_test/test_textview.py +++ b/Lib/idlelib/idle_test/test_textview.py @@ -1,4 +1,4 @@ -'''Test the functions and main class method of textView.py. +'''Test idlelib.textView. Since all methods and functions create (or destroy) a TextViewer, which is a widget containing multiple widgets, all tests must be gui tests. @@ -22,7 +22,9 @@ def setUpModule(): root = Tk() def tearDownModule(): - global root + global root, TV + del TV + root.update_idletasks() root.destroy() # pyflakes falsely sees root as undefined del root diff --git a/Lib/idlelib/idle_test/test_undodelegator.py b/Lib/idlelib/idle_test/test_undodelegator.py new file mode 100644 index 0000000..2b83c99 --- /dev/null +++ b/Lib/idlelib/idle_test/test_undodelegator.py @@ -0,0 +1,135 @@ +"""Unittest for UndoDelegator in idlelib.UndoDelegator. + +Coverage about 80% (retest). +""" +from test.support import requires +requires('gui') + +import unittest +from unittest.mock import Mock +from tkinter import Text, Tk +from idlelib.UndoDelegator import UndoDelegator +from idlelib.Percolator import Percolator + + +class UndoDelegatorTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.root = Tk() + cls.text = Text(cls.root) + cls.percolator = Percolator(cls.text) + + @classmethod + def tearDownClass(cls): + cls.percolator.redir.close() + del cls.percolator, cls.text + cls.root.destroy() + del cls.root + + def setUp(self): + self.delegator = UndoDelegator() + self.percolator.insertfilter(self.delegator) + self.delegator.bell = Mock(wraps=self.delegator.bell) + + def tearDown(self): + self.percolator.removefilter(self.delegator) + self.text.delete('1.0', 'end') + self.delegator.resetcache() + + def test_undo_event(self): + text = self.text + + text.insert('insert', 'foobar') + text.insert('insert', 'h') + text.event_generate('<<undo>>') + self.assertEqual(text.get('1.0', 'end'), '\n') + + text.insert('insert', 'foo') + text.insert('insert', 'bar') + text.delete('1.2', '1.4') + text.insert('insert', 'hello') + text.event_generate('<<undo>>') + self.assertEqual(text.get('1.0', '1.4'), 'foar') + text.event_generate('<<undo>>') + self.assertEqual(text.get('1.0', '1.6'), 'foobar') + text.event_generate('<<undo>>') + self.assertEqual(text.get('1.0', '1.3'), 'foo') + text.event_generate('<<undo>>') + self.delegator.undo_event('event') + self.assertTrue(self.delegator.bell.called) + + def test_redo_event(self): + text = self.text + + text.insert('insert', 'foo') + text.insert('insert', 'bar') + text.delete('1.0', '1.3') + text.event_generate('<<undo>>') + text.event_generate('<<redo>>') + self.assertEqual(text.get('1.0', '1.3'), 'bar') + text.event_generate('<<redo>>') + self.assertTrue(self.delegator.bell.called) + + def test_dump_event(self): + """ + Dump_event cannot be tested directly without changing + environment variables. So, test statements in dump_event + indirectly + """ + text = self.text + d = self.delegator + + text.insert('insert', 'foo') + text.insert('insert', 'bar') + text.delete('1.2', '1.4') + self.assertTupleEqual((d.pointer, d.can_merge), (3, True)) + text.event_generate('<<undo>>') + self.assertTupleEqual((d.pointer, d.can_merge), (2, False)) + + def test_get_set_saved(self): + # test the getter method get_saved + # test the setter method set_saved + # indirectly test check_saved + d = self.delegator + + self.assertTrue(d.get_saved()) + self.text.insert('insert', 'a') + self.assertFalse(d.get_saved()) + d.saved_change_hook = Mock() + + d.set_saved(True) + self.assertEqual(d.pointer, d.saved) + self.assertTrue(d.saved_change_hook.called) + + d.set_saved(False) + self.assertEqual(d.saved, -1) + self.assertTrue(d.saved_change_hook.called) + + def test_undo_start_stop(self): + # test the undo_block_start and undo_block_stop methods + text = self.text + + text.insert('insert', 'foo') + self.delegator.undo_block_start() + text.insert('insert', 'bar') + text.insert('insert', 'bar') + self.delegator.undo_block_stop() + self.assertEqual(text.get('1.0', '1.3'), 'foo') + + # test another code path + self.delegator.undo_block_start() + text.insert('insert', 'bar') + self.delegator.undo_block_stop() + self.assertEqual(text.get('1.0', '1.3'), 'foo') + + def test_addcmd(self): + text = self.text + # when number of undo operations exceeds max_undo + self.delegator.max_undo = max_undo = 10 + for i in range(max_undo + 10): + text.insert('insert', 'foo') + self.assertLessEqual(len(self.delegator.undolist), max_undo) + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_warning.py b/Lib/idlelib/idle_test/test_warning.py index 54ac993..18627dd 100644 --- a/Lib/idlelib/idle_test/test_warning.py +++ b/Lib/idlelib/idle_test/test_warning.py @@ -68,15 +68,6 @@ class ShellWarnTest(unittest.TestCase): 'Test', UserWarning, 'test_warning.py', 99, f, 'Line of code') self.assertEqual(shellmsg.splitlines(), f.getvalue().splitlines()) -class ImportWarnTest(unittest.TestCase): - def test_idlever(self): - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - import idlelib.idlever - self.assertEqual(len(w), 1) - self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) - self.assertIn("version", str(w[-1].message)) - if __name__ == '__main__': unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_widgetredir.py b/Lib/idlelib/idle_test/test_widgetredir.py index 6440561..c68dfcc 100644 --- a/Lib/idlelib/idle_test/test_widgetredir.py +++ b/Lib/idlelib/idle_test/test_widgetredir.py @@ -1,7 +1,7 @@ -"""Unittest for idlelib.WidgetRedirector +'''Test idlelib.WidgetRedirector. 100% coverage -""" +''' from test.support import requires import unittest from idlelib.idle_test.mock_idle import Func @@ -14,14 +14,14 @@ class InitCloseTest(unittest.TestCase): @classmethod def setUpClass(cls): requires('gui') - cls.tk = Tk() - cls.text = Text(cls.tk) + cls.root = Tk() + cls.text = Text(cls.root) @classmethod def tearDownClass(cls): - cls.text.destroy() - cls.tk.destroy() - del cls.text, cls.tk + del cls.text + cls.root.destroy() + del cls.root def test_init(self): redir = WidgetRedirector(self.text) @@ -43,14 +43,15 @@ class WidgetRedirectorTest(unittest.TestCase): @classmethod def setUpClass(cls): requires('gui') - cls.tk = Tk() - cls.text = Text(cls.tk) + cls.root = Tk() + cls.text = Text(cls.root) @classmethod def tearDownClass(cls): - cls.text.destroy() - cls.tk.destroy() - del cls.text, cls.tk + del cls.text + cls.root.update_idletasks() + cls.root.destroy() + del cls.root def setUp(self): self.redir = WidgetRedirector(self.text) @@ -108,13 +109,13 @@ class WidgetRedirectorTest(unittest.TestCase): def test_command_dispatch(self): # Test that .__init__ causes redirection of tk calls # through redir.dispatch - self.tk.call(self.text._w, 'insert', 'hello') + self.root.call(self.text._w, 'insert', 'hello') self.assertEqual(self.func.args, ('hello',)) self.assertEqual(self.text.get('1.0', 'end'), '\n') # Ensure that called through redir .dispatch and not through # self.text.insert by having mock raise TclError. self.func.__init__(TclError()) - self.assertEqual(self.tk.call(self.text._w, 'insert', 'boo'), '') + self.assertEqual(self.root.call(self.text._w, 'insert', 'boo'), '') |
