Removed the Requirement to Install Python and NodeJS (Now Bundled with Borealis)

This commit is contained in:
2025-04-24 00:42:19 -06:00
parent 785265d3e7
commit 9c68cdea84
7786 changed files with 2386458 additions and 217 deletions

View File

@ -0,0 +1,241 @@
README FOR IDLE TESTS IN IDLELIB.IDLE_TEST
0. Quick Start
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 3.4.
python -m idlelib.idle_test.htest
1. Test Files
The idle directory, idlelib, has over 60 xyz.py files. The idle_test
subdirectory contains test_xyz.py for each implementation file xyz.py.
To add a test for abc.py, open idle_test/template.py and immediately
Save As test_abc.py. Insert 'abc' on the first line, and replace
'zzdummy' with 'abc.
Remove the imports of requires and tkinter if not needed. Otherwise,
add to the tkinter imports as needed.
Add a prefix to 'Test' for the initial test class. The template class
contains code needed or possibly needed for gui tests. See the next
section if doing gui tests. If not, and not needed for further classes,
this code can be removed.
Add the following at the end of abc.py. If an htest was added first,
insert the import and main lines before the htest lines.
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_abc', verbosity=2, exit=False)
The ', exit=False' is only needed if an htest follows.
2. GUI Tests
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 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.
The template.py file does this.
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). It should not hurt if not needed.
can't invoke "event" command: application has been destroyed
...
"ttk::ThemeChanged"
If a test creates instance 'e' of EditorWindow, call 'e._close()' before
or as the first part of teardown. The effect of omitting this depends
on the later shutdown. Then enable the after_cancel loop in the
template. This prevents messages like the following.
bgerror failed to handle background error.
Original error: invalid command name "106096696timer_event"
Error in bgerror: can't invoke "tk" command: application has been destroyed
Requires('gui') causes the test(s) it guards to be skipped if any of
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.
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.
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.)
python -m idlelib.xyz
python -m idlelib.idle_test.test_xyz
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'.
python -m unittest -v idlelib.idle_test
python -m test -v -ugui test_idle
python -m test.test_idle
IDLE tests are 'discovered' by idlelib.idle_test.__init__.load_tests
when this is 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 (GH-62281).
To test subsets of modules, see idlelib.idle_test.__init__. This
can be used to find refleaks or possible sources of "Theme changed"
tcl messages (GH-71383).
To run an individual Testcase or test method, extend the dotted name
given to unittest on the command line or use the test -m option. The
latter allows use of other regrtest options. When using the latter,
all components of the pattern must be present, but any can be replaced
by '*'.
python -m unittest -v idlelib.idle_test.test_xyz.Test_case.test_meth
python -m test -m idlelib.idle_test.text_xyz.Test_case.test_meth test_idle
The test suite can be run in an IDLE user process from Shell.
>>> import test.autotest # Issue 25588, 2017/10/13, 3.6.4, 3.7.0a2.
There are currently failures not usually present, and this does not
work when run from the editor.
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:
python -m idlelib.idle_test.htest
5. Test Coverage
Install the coverage package into your Python 3.6 site-packages
directory. (Its exact location depends on the OS).
> python3 -m pip install coverage
(On Windows, replace 'python3 with 'py -3.6' or perhaps just 'python'.)
The problem with running coverage with repository python is that
coverage uses absolute imports for its submodules, hence it needs to be
in a directory in sys.path. One solution: copy the package to the
directory containing the cpython repository. Call it 'dev'. Then run
coverage either directly or from a script in that directory so that
'dev' is prepended to sys.path.
Either edit or add dev/.coveragerc so it looks something like this.
---
# .coveragerc sets coverage options.
[run]
branch = True
[report]
# Regexes for lines to exclude from consideration
exclude_lines =
# Don't complain if non-runnable code isn't run:
if 0:
if __name__ == .__main__.:
.*# htest #
if not _utest:
if _htest:
---
The additions for IDLE are 'branch = True', to test coverage both ways,
and the last three exclude lines, to exclude things peculiar to IDLE
that are not executed during tests.
A script like the following cover.bat (for Windows) is very handy.
---
@echo off
rem Usage: cover filename [test_ suffix] # proper case required by coverage
rem filename without .py, 2nd parameter if test is not test_filename
setlocal
set py=f:\dev\3x\pcbuild\win32\python_d.exe
set src=idlelib.%1
if "%2" EQU "" set tst=f:/dev/3x/Lib/idlelib/idle_test/test_%1.py
if "%2" NEQ "" set tst=f:/dev/ex/Lib/idlelib/idle_test/test_%2.py
%py% -m coverage run --pylib --source=%src% %tst%
%py% -m coverage report --show-missing
%py% -m coverage html
start htmlcov\3x_Lib_idlelib_%1_py.html
rem Above opens new report; htmlcov\index.html displays report index
---
The second parameter was added for tests of module x not named test_x.
(There were several before modules were renamed, now only one is left.)

View File

@ -0,0 +1,27 @@
"""idlelib.idle_test implements 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 (-v)".
This package and its contained modules are subject to change and
any direct use is at your own risk.
"""
from os.path import dirname
# test_idle imports load_tests for test discovery (default all).
# To run subsets of idlelib module tests, insert '[<chars>]' after '_'.
# Example: insert '[ac]' for modules beginning with 'a' or 'c'.
# Additional .discover/.addTest pairs with separate inserts work.
# Example: pairs with 'c' and 'g' test c* files and grep.
def load_tests(loader, standard_tests, pattern):
this_dir = dirname(__file__)
top_dir = dirname(dirname(this_dir))
module_tests = loader.discover(start_dir=this_dir,
pattern='test_*.py', # Insert here.
top_level_dir=top_dir)
standard_tests.addTests(module_tests)
## module_tests = loader.discover(start_dir=this_dir,
## pattern='test_*.py', # Insert here.
## top_level_dir=top_dir)
## standard_tests.addTests(module_tests)
return standard_tests

View File

@ -0,0 +1,4 @@
#!usr/bin/env python
def example_function(some_argument):
pass

View File

@ -0,0 +1,4 @@
# An example file to test recognition of a .pyi file as Python source code.
class Example:
def method(self, argument1: str, argument2: list[int]) -> None: ...

View File

@ -0,0 +1,442 @@
"""Run human tests of Idle's window, dialog, and popup widgets.
run(*tests) Create a master Tk() htest window. Within that, run each
callable in tests after finding the matching test spec in this file. If
tests is empty, run an htest for each spec dict in this file after
finding the matching callable in the module named in the spec. Close
the master window to end testing.
In a tested module, let X be a global name bound to a callable (class or
function) whose .__name__ attribute is also X (the usual situation). The
first parameter of X must be 'parent' or 'master'. When called, the
first argument will be the root window. X must create a child
Toplevel(parent/master) (or subclass thereof). The Toplevel may be a
test widget or dialog, in which case the callable is the corresponding
class. Or the Toplevel may contain the widget to be tested or set up a
context in which a test widget is invoked. In this latter case, the
callable is a wrapper function that sets up the Toplevel and other
objects. Wrapper function names, such as _editor_window', should start
with '_' and be lowercase.
End the module with
if __name__ == '__main__':
<run unittest.main with 'exit=False'>
from idlelib.idle_test.htest import run
run(callable) # There could be multiple comma-separated callables.
To have wrapper functions ignored by coverage reports, tag the def
header like so: "def _wrapper(parent): # htest #". Use the same tag
for htest lines in widget code. Make sure that the 'if __name__' line
matches the above. Then have make sure that .coveragerc includes the
following:
[report]
exclude_lines =
.*# htest #
if __name__ == .__main__.:
(The "." instead of "'" is intentional and necessary.)
To run any X, this file must contain a matching instance of the
following template, with X.__name__ prepended to '_spec'.
When all tests are run, the prefix is use to get X.
callable_spec = {
'file': '',
'kwds': {'title': ''},
'msg': ""
}
file (no .py): run() imports file.py.
kwds: augmented with {'parent':root} and passed to X as **kwds.
title: an example kwd; some widgets need this, delete line if not.
msg: master window hints about testing the widget.
TODO test these modules and classes:
autocomplete_w.AutoCompleteWindow
debugger.Debugger
outwin.OutputWindow (indirectly being tested with grep test)
pyshell.PyShellEditorWindow
"""
import idlelib.pyshell # Set Windows DPI awareness before Tk().
from importlib import import_module
import textwrap
import tkinter as tk
from tkinter.ttk import Scrollbar
tk.NoDefaultRoot()
AboutDialog_spec = {
'file': 'help_about',
'kwds': {'title': 'help_about test',
'_htest': True,
},
'msg': "Click on URL to open in default browser.\n"
"Verify x.y.z versions and test each button, including Close.\n "
}
# TODO implement ^\; adding '<Control-Key-\\>' to function does not work.
_calltip_window_spec = {
'file': 'calltip_w',
'kwds': {},
'msg': "Typing '(' should display a calltip.\n"
"Typing ') should hide the calltip.\n"
"So should moving cursor out of argument area.\n"
"Force-open-calltip does not work here.\n"
}
_color_delegator_spec = {
'file': 'colorizer',
'kwds': {},
'msg': "The text is sample Python code.\n"
"Ensure components like comments, keywords, builtins,\n"
"string, definitions, and break are correctly colored.\n"
"The default color scheme is in idlelib/config-highlight.def"
}
ConfigDialog_spec = {
'file': 'configdialog',
'kwds': {'title': 'ConfigDialogTest',
'_htest': True,},
'msg': "IDLE preferences dialog.\n"
"In the 'Fonts/Tabs' tab, changing font face, should update the "
"font face of the text in the area below it.\nIn the "
"'Highlighting' tab, try different color schemes. Clicking "
"items in the sample program should update the choices above it."
"\nIn the 'Keys', 'General' and 'Extensions' tabs, test settings "
"of interest."
"\n[Ok] to close the dialog.[Apply] to apply the settings and "
"and [Cancel] to revert all changes.\nRe-run the test to ensure "
"changes made have persisted."
}
CustomRun_spec = {
'file': 'query',
'kwds': {'title': 'Customize query.py Run',
'_htest': True},
'msg': "Enter with <Return> or [OK]. Print valid entry to Shell\n"
"Arguments are parsed into a list\n"
"Mode is currently restart True or False\n"
"Close dialog with valid entry, <Escape>, [Cancel], [X]"
}
_debug_object_browser_spec = {
'file': 'debugobj',
'kwds': {},
'msg': "Double click on items up to the lowest level.\n"
"Attributes of the objects and related information "
"will be displayed side-by-side at each level."
}
# TODO Improve message
_dyn_option_menu_spec = {
'file': 'dynoption',
'kwds': {},
'msg': "Select one of the many options in the 'old option set'.\n"
"Click the button to change the option set.\n"
"Select one of the many options in the 'new option set'."
}
# TODO edit wrapper
_editor_window_spec = {
'file': 'editor',
'kwds': {},
'msg': "Test editor functions of interest.\n"
"Best to close editor first."
}
GetKeysWindow_spec = {
'file': 'config_key',
'kwds': {'title': 'Test keybindings',
'action': 'find-again',
'current_key_sequences': [['<Control-Key-g>', '<Key-F3>', '<Control-Key-G>']],
'_htest': True,
},
'msg': "Test for different key modifier sequences.\n"
"<nothing> is invalid.\n"
"No modifier key is invalid.\n"
"Shift key with [a-z],[0-9], function key, move key, tab, space "
"is invalid.\nNo validity checking if advanced key binding "
"entry is used."
}
_grep_dialog_spec = {
'file': 'grep',
'kwds': {},
'msg': "Click the 'Show GrepDialog' button.\n"
"Test the various 'Find-in-files' functions.\n"
"The results should be displayed in a new '*Output*' window.\n"
"'Right-click'->'Go to file/line' in the search results\n "
"should open that file in a new EditorWindow."
}
HelpSource_spec = {
'file': 'query',
'kwds': {'title': 'Help name and source',
'menuitem': 'test',
'filepath': __file__,
'used_names': {'abc'},
'_htest': True},
'msg': "Enter menu item name and help file path\n"
"'', > than 30 chars, and 'abc' are invalid menu item names.\n"
"'' and file does not exist are invalid path items.\n"
"Any url ('www...', 'http...') is accepted.\n"
"Test Browse with and without path, as cannot unittest.\n"
"[Ok] or <Return> prints valid entry to shell\n"
"<Escape>, [Cancel], or [X] prints None to shell"
}
_io_binding_spec = {
'file': 'iomenu',
'kwds': {},
'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."
}
_multi_call_spec = {
'file': 'multicall',
'kwds': {},
'msg': "The following should trigger a print to console or IDLE Shell.\n"
"Entering and leaving the text area, key entry, <Control-Key>,\n"
"<Alt-Key-a>, <Control-Key-a>, <Alt-Control-Key-a>, \n"
"<Control-Button-1>, <Alt-Button-1> and focusing elsewhere."
}
_module_browser_spec = {
'file': 'browser',
'kwds': {},
'msg': textwrap.dedent("""
"Inspect names of module, class(with superclass if applicable),
"methods and functions. Toggle nested items. Double clicking
"on items prints a traceback for an exception that is ignored.""")
}
_multistatus_bar_spec = {
'file': 'statusbar',
'kwds': {},
'msg': "Ensure presence of multi-status bar below text area.\n"
"Click 'Update Status' to change the status text"
}
PathBrowser_spec = {
'file': 'pathbrowser',
'kwds': {'_htest': True},
'msg': "Test for correct display of all paths in sys.path.\n"
"Toggle nested items out to the lowest level.\n"
"Double clicking on an item prints a traceback\n"
"for an exception that is ignored."
}
_percolator_spec = {
'file': 'percolator',
'kwds': {},
'msg': "There are two tracers which can be toggled using a checkbox.\n"
"Toggling a tracer 'on' by checking it should print tracer "
"output to the console or to the IDLE shell.\n"
"If both the tracers are 'on', the output from the tracer which "
"was switched 'on' later, should be printed first\n"
"Test for actions like text entry, and removal."
}
Query_spec = {
'file': 'query',
'kwds': {'title': 'Query',
'message': 'Enter something',
'text0': 'Go',
'_htest': True},
'msg': "Enter with <Return> or [Ok]. Print valid entry to Shell\n"
"Blank line, after stripping, is ignored\n"
"Close dialog with valid entry, <Escape>, [Cancel], [X]"
}
_replace_dialog_spec = {
'file': 'replace',
'kwds': {},
'msg': "Click the 'Replace' button.\n"
"Test various replace options in the 'Replace dialog'.\n"
"Click [Close] or [X] to close the 'Replace Dialog'."
}
_scrolled_list_spec = {
'file': 'scrolledlist',
'kwds': {},
'msg': "You should see a scrollable list of items\n"
"Selecting (clicking) or double clicking an item "
"prints the name to the console or Idle shell.\n"
"Right clicking an item will display a popup."
}
_search_dialog_spec = {
'file': 'search',
'kwds': {},
'msg': "Click the 'Search' button.\n"
"Test various search options in the 'Search dialog'.\n"
"Click [Close] or [X] to close the 'Search Dialog'."
}
_searchbase_spec = {
'file': 'searchbase',
'kwds': {},
'msg': "Check the appearance of the base search dialog\n"
"Its only action is to close."
}
show_idlehelp_spec = {
'file': 'help',
'kwds': {},
'msg': "If the help text displays, this works.\n"
"Text is selectable. Window is scrollable."
}
_sidebar_number_scrolling_spec = {
'file': 'sidebar',
'kwds': {},
'msg': textwrap.dedent("""\
1. Click on the line numbers and drag down below the edge of the
window, moving the mouse a bit and then leaving it there for a
while. The text and line numbers should gradually scroll down,
with the selection updated continuously.
2. With the lines still selected, click on a line number above
or below the selected lines. Only the line whose number was
clicked should be selected.
3. Repeat step #1, dragging to above the window. The text and
line numbers should gradually scroll up, with the selection
updated continuously.
4. Repeat step #2, clicking a line number below the selection."""),
}
_stackbrowser_spec = {
'file': 'stackviewer',
'kwds': {},
'msg': "A stacktrace for a NameError exception.\n"
"Should have NameError and 1 traceback line."
}
_tooltip_spec = {
'file': 'tooltip',
'kwds': {},
'msg': "Place mouse cursor over both the buttons\n"
"A tooltip should appear with some text."
}
_tree_widget_spec = {
'file': 'tree',
'kwds': {},
'msg': "The canvas is scrollable.\n"
"Click on folders up to to the lowest level."
}
_undo_delegator_spec = {
'file': 'undo',
'kwds': {},
'msg': "Click [Undo] to undo any action.\n"
"Click [Redo] to redo any action.\n"
"Click [Dump] to dump the current state "
"by printing to the console or the IDLE shell.\n"
}
ViewWindow_spec = {
'file': 'textview',
'kwds': {'title': 'Test textview',
'contents': 'The quick brown fox jumps over the lazy dog.\n'*35,
'_htest': True},
'msg': "Test for read-only property of text.\n"
"Select text, scroll window, close"
}
_widget_redirector_spec = {
'file': 'redirector',
'kwds': {},
'msg': "Every text insert should be printed to the console "
"or the IDLE shell."
}
def run(*tests):
"Run callables in tests."
root = tk.Tk()
root.title('IDLE htest')
root.resizable(0, 0)
# A scrollable Label-like constant width text widget.
frameLabel = tk.Frame(root, padx=10)
frameLabel.pack()
text = tk.Text(frameLabel, wrap='word')
text.configure(bg=root.cget('bg'), relief='flat', height=4, width=70)
scrollbar = Scrollbar(frameLabel, command=text.yview)
text.config(yscrollcommand=scrollbar.set)
scrollbar.pack(side='right', fill='y', expand=False)
text.pack(side='left', fill='both', expand=True)
test_list = [] # Make list of (spec, callable) tuples.
if tests:
for test in tests:
test_spec = globals()[test.__name__ + '_spec']
test_spec['name'] = test.__name__
test_list.append((test_spec, test))
else:
for key, dic in globals().items():
if key.endswith('_spec'):
test_name = key[:-5]
test_spec = dic
test_spec['name'] = test_name
mod = import_module('idlelib.' + test_spec['file'])
test = getattr(mod, test_name)
test_list.append((test_spec, test))
test_list.reverse() # So can pop in proper order in next_test.
test_name = tk.StringVar(root)
callable_object = None
test_kwds = None
def next_test():
nonlocal test_name, callable_object, test_kwds
if len(test_list) == 1:
next_button.pack_forget()
test_spec, callable_object = test_list.pop()
test_kwds = test_spec['kwds']
test_name.set('Test ' + test_spec['name'])
text['state'] = 'normal' # Enable text replacement.
text.delete('1.0', 'end')
text.insert("1.0", test_spec['msg'])
text['state'] = 'disabled' # Restore read-only property.
def run_test(_=None):
widget = callable_object(root, **test_kwds)
try:
print(widget.result) # Only true for query classes(?).
except AttributeError:
pass
def close(_=None):
root.destroy()
button = tk.Button(root, textvariable=test_name,
default='active', command=run_test)
next_button = tk.Button(root, text="Next", command=next_test)
button.pack()
next_button.pack()
next_button.focus_set()
root.bind('<Key-Return>', run_test)
root.bind('<Key-Escape>', close)
next_test()
root.mainloop()
if __name__ == '__main__':
run()

View File

@ -0,0 +1,61 @@
'''Mock classes that imitate idlelib modules or classes.
Attributes and methods will be added as needed for tests.
'''
from idlelib.idle_test.mock_tk import Text
class Func:
'''Record call, capture args, return/raise result set by test.
When mock function is called, set or use attributes:
self.called - increment call number even if no args, kwds passed.
self.args - capture positional arguments.
self.kwds - capture keyword arguments.
self.result - return or raise value set in __init__.
self.return_self - return self instead, to mock query class return.
Most common use will probably be to mock instance methods.
Given class instance, can set and delete as instance attribute.
Mock_tk.Var and Mbox_func are special variants of this.
'''
def __init__(self, result=None, return_self=False):
self.called = 0
self.result = result
self.return_self = return_self
self.args = None
self.kwds = None
def __call__(self, *args, **kwds):
self.called += 1
self.args = args
self.kwds = kwds
if isinstance(self.result, BaseException):
raise self.result
elif self.return_self:
return self
else:
return self.result
class Editor:
'''Minimally imitate editor.EditorWindow class.
'''
def __init__(self, flist=None, filename=None, key=None, root=None,
text=None): # Allow real Text with mock Editor.
self.text = text or Text()
self.undo = UndoDelegator()
def get_selection_indices(self):
first = self.text.index('1.0')
last = self.text.index('end')
return first, last
class UndoDelegator:
'''Minimally imitate undo.UndoDelegator class.
'''
# A real undo block is only needed for user interaction.
def undo_block_start(*args):
pass
def undo_block_stop(*args):
pass

View File

@ -0,0 +1,307 @@
"""Classes that replace tkinter gui objects used by an object being tested.
A gui object is anything with a master or parent parameter, which is
typically required in spite of what the doc strings say.
"""
import re
from _tkinter import TclError
class Event:
'''Minimal mock with attributes for testing event handlers.
This is not a gui object, but is used as an argument for callbacks
that access attributes of the event passed. If a callback ignores
the event, other than the fact that is happened, pass 'event'.
Keyboard, mouse, window, and other sources generate Event instances.
Event instances have the following attributes: serial (number of
event), time (of event), type (of event as number), widget (in which
event occurred), and x,y (position of mouse). There are other
attributes for specific events, such as keycode for key events.
tkinter.Event.__doc__ has more but is still not complete.
'''
def __init__(self, **kwds):
"Create event with attributes needed for test"
self.__dict__.update(kwds)
class Var:
"Use for String/Int/BooleanVar: incomplete"
def __init__(self, master=None, value=None, name=None):
self.master = master
self.value = value
self.name = name
def set(self, value):
self.value = value
def get(self):
return self.value
class Mbox_func:
"""Generic mock for messagebox functions, which all have the same signature.
Instead of displaying a message box, the mock's call method saves the
arguments as instance attributes, which test functions can then examine.
The test can set the result returned to ask function
"""
def __init__(self, result=None):
self.result = result # Return None for all show funcs
def __call__(self, title, message, *args, **kwds):
# Save all args for possible examination by tester
self.title = title
self.message = message
self.args = args
self.kwds = kwds
return self.result # Set by tester for ask functions
class Mbox:
"""Mock for tkinter.messagebox with an Mbox_func for each function.
Example usage in test_module.py for testing functions in module.py:
---
from idlelib.idle_test.mock_tk import Mbox
import module
orig_mbox = module.messagebox
showerror = Mbox.showerror # example, for attribute access in test methods
class Test(unittest.TestCase):
@classmethod
def setUpClass(cls):
module.messagebox = Mbox
@classmethod
def tearDownClass(cls):
module.messagebox = orig_mbox
---
For 'ask' functions, set func.result return value before calling the method
that uses the message function. When messagebox functions are the
only GUI calls in a method, this replacement makes the method GUI-free,
"""
askokcancel = Mbox_func() # True or False
askquestion = Mbox_func() # 'yes' or 'no'
askretrycancel = Mbox_func() # True or False
askyesno = Mbox_func() # True or False
askyesnocancel = Mbox_func() # True, False, or None
showerror = Mbox_func() # None
showinfo = Mbox_func() # None
showwarning = Mbox_func() # None
class Text:
"""A semi-functional non-gui replacement for tkinter.Text text editors.
The mock's data model is that a text is a list of \n-terminated lines.
The mock adds an empty string at the beginning of the list so that the
index of actual lines start at 1, as with Tk. The methods never see this.
Tk initializes files with a terminal \n that cannot be deleted. It is
invisible in the sense that one cannot move the cursor beyond it.
This class is only tested (and valid) with strings of ascii chars.
For testing, we are not concerned with Tk Text's treatment of,
for instance, 0-width characters or character + accent.
"""
def __init__(self, master=None, cnf={}, **kw):
'''Initialize mock, non-gui, text-only Text widget.
At present, all args are ignored. Almost all affect visual behavior.
There are just a few Text-only options that affect text behavior.
'''
self.data = ['', '\n']
def index(self, index):
"Return string version of index decoded according to current text."
return "%s.%s" % self._decode(index, endflag=1)
def _decode(self, index, endflag=0):
"""Return a (line, char) tuple of int indexes into self.data.
This implements .index without converting the result back to a string.
The result is constrained by the number of lines and linelengths of
self.data. For many indexes, the result is initially (1, 0).
The input index may have any of several possible forms:
* line.char float: converted to 'line.char' string;
* 'line.char' string, where line and char are decimal integers;
* 'line.char lineend', where lineend='lineend' (and char is ignored);
* 'line.end', where end='end' (same as above);
* 'insert', the positions before terminal \n;
* 'end', whose meaning depends on the endflag passed to ._endex.
* 'sel.first' or 'sel.last', where sel is a tag -- not implemented.
"""
if isinstance(index, (float, bytes)):
index = str(index)
try:
index=index.lower()
except AttributeError:
raise TclError('bad text index "%s"' % index) from None
lastline = len(self.data) - 1 # same as number of text lines
if index == 'insert':
return lastline, len(self.data[lastline]) - 1
elif index == 'end':
return self._endex(endflag)
line, char = index.split('.')
line = int(line)
# Out of bounds line becomes first or last ('end') index
if line < 1:
return 1, 0
elif line > lastline:
return self._endex(endflag)
linelength = len(self.data[line]) -1 # position before/at \n
if char.endswith(' lineend') or char == 'end':
return line, linelength
# Tk requires that ignored chars before ' lineend' be valid int
if m := re.fullmatch(r'end-(\d*)c', char, re.A): # Used by hyperparser.
return line, linelength - int(m.group(1))
# Out of bounds char becomes first or last index of line
char = int(char)
if char < 0:
char = 0
elif char > linelength:
char = linelength
return line, char
def _endex(self, endflag):
'''Return position for 'end' or line overflow corresponding to endflag.
-1: position before terminal \n; for .insert(), .delete
0: position after terminal \n; for .get, .delete index 1
1: same viewed as beginning of non-existent next line (for .index)
'''
n = len(self.data)
if endflag == 1:
return n, 0
else:
n -= 1
return n, len(self.data[n]) + endflag
def insert(self, index, chars):
"Insert chars before the character at index."
if not chars: # ''.splitlines() is [], not ['']
return
chars = chars.splitlines(True)
if chars[-1][-1] == '\n':
chars.append('')
line, char = self._decode(index, -1)
before = self.data[line][:char]
after = self.data[line][char:]
self.data[line] = before + chars[0]
self.data[line+1:line+1] = chars[1:]
self.data[line+len(chars)-1] += after
def get(self, index1, index2=None):
"Return slice from index1 to index2 (default is 'index1+1')."
startline, startchar = self._decode(index1)
if index2 is None:
endline, endchar = startline, startchar+1
else:
endline, endchar = self._decode(index2)
if startline == endline:
return self.data[startline][startchar:endchar]
else:
lines = [self.data[startline][startchar:]]
for i in range(startline+1, endline):
lines.append(self.data[i])
lines.append(self.data[endline][:endchar])
return ''.join(lines)
def delete(self, index1, index2=None):
'''Delete slice from index1 to index2 (default is 'index1+1').
Adjust default index2 ('index+1) for line ends.
Do not delete the terminal \n at the very end of self.data ([-1][-1]).
'''
startline, startchar = self._decode(index1, -1)
if index2 is None:
if startchar < len(self.data[startline])-1:
# not deleting \n
endline, endchar = startline, startchar+1
elif startline < len(self.data) - 1:
# deleting non-terminal \n, convert 'index1+1 to start of next line
endline, endchar = startline+1, 0
else:
# do not delete terminal \n if index1 == 'insert'
return
else:
endline, endchar = self._decode(index2, -1)
# restricting end position to insert position excludes terminal \n
if startline == endline and startchar < endchar:
self.data[startline] = self.data[startline][:startchar] + \
self.data[startline][endchar:]
elif startline < endline:
self.data[startline] = self.data[startline][:startchar] + \
self.data[endline][endchar:]
startline += 1
for i in range(startline, endline+1):
del self.data[startline]
def compare(self, index1, op, index2):
line1, char1 = self._decode(index1)
line2, char2 = self._decode(index2)
if op == '<':
return line1 < line2 or line1 == line2 and char1 < char2
elif op == '<=':
return line1 < line2 or line1 == line2 and char1 <= char2
elif op == '>':
return line1 > line2 or line1 == line2 and char1 > char2
elif op == '>=':
return line1 > line2 or line1 == line2 and char1 >= char2
elif op == '==':
return line1 == line2 and char1 == char2
elif op == '!=':
return line1 != line2 or char1 != char2
else:
raise TclError('''bad comparison operator "%s": '''
'''must be <, <=, ==, >=, >, or !=''' % op)
# The following Text methods normally do something and return None.
# Whether doing nothing is sufficient for a test will depend on the test.
def mark_set(self, name, index):
"Set mark *name* before the character at index."
pass
def mark_unset(self, *markNames):
"Delete all marks in markNames."
def tag_remove(self, tagName, index1, index2=None):
"Remove tag tagName from all characters between index1 and index2."
pass
# The following Text methods affect the graphics screen and return None.
# Doing nothing should always be sufficient for tests.
def scan_dragto(self, x, y):
"Adjust the view of the text according to scan_mark"
def scan_mark(self, x, y):
"Remember the current X, Y coordinates."
def see(self, index):
"Scroll screen to make the character at INDEX is visible."
pass
# The following is a Misc method inherited by Text.
# It should properly go in a Misc mock, but is included here for now.
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

View File

@ -0,0 +1,30 @@
"Test , coverage %."
from idlelib import zzdummy
import unittest
from test.support import requires
from tkinter import Tk
class Test(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
## for id in cls.root.tk.call('after', 'info'):
## cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
def test_init(self):
self.assertTrue(True)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,305 @@
"Test autocomplete, coverage 93%."
import unittest
from unittest.mock import Mock, patch
from test.support import requires
from tkinter import Tk, Text
import os
import __main__
import idlelib.autocomplete as ac
import idlelib.autocomplete_w as acw
from idlelib.idle_test.mock_idle import Func
from idlelib.idle_test.mock_tk import Event
class DummyEditwin:
def __init__(self, root, text):
self.root = root
self.text = text
self.indentwidth = 8
self.tabwidth = 8
self.prompt_last_line = '>>>' # Currently not used by autocomplete.
class AutoCompleteTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.text = Text(cls.root)
cls.editor = DummyEditwin(cls.root, cls.text)
@classmethod
def tearDownClass(cls):
del cls.editor, cls.text
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def setUp(self):
self.text.delete('1.0', 'end')
self.autocomplete = ac.AutoComplete(self.editor)
def test_init(self):
self.assertEqual(self.autocomplete.editwin, self.editor)
self.assertEqual(self.autocomplete.text, self.text)
def test_make_autocomplete_window(self):
testwin = self.autocomplete._make_autocomplete_window()
self.assertIsInstance(testwin, acw.AutoCompleteWindow)
def test_remove_autocomplete_window(self):
acp = self.autocomplete
acp.autocompletewindow = m = Mock()
acp._remove_autocomplete_window()
m.hide_window.assert_called_once()
self.assertIsNone(acp.autocompletewindow)
def test_force_open_completions_event(self):
# Call _open_completions and break.
acp = self.autocomplete
open_c = Func()
acp.open_completions = open_c
self.assertEqual(acp.force_open_completions_event('event'), 'break')
self.assertEqual(open_c.args[0], ac.FORCE)
def test_autocomplete_event(self):
Equal = self.assertEqual
acp = self.autocomplete
# Result of autocomplete event: If modified tab, None.
ev = Event(mc_state=True)
self.assertIsNone(acp.autocomplete_event(ev))
del ev.mc_state
# If tab after whitespace, None.
self.text.insert('1.0', ' """Docstring.\n ')
self.assertIsNone(acp.autocomplete_event(ev))
self.text.delete('1.0', 'end')
# If active autocomplete window, complete() and 'break'.
self.text.insert('1.0', 're.')
acp.autocompletewindow = mock = Mock()
mock.is_active = Mock(return_value=True)
Equal(acp.autocomplete_event(ev), 'break')
mock.complete.assert_called_once()
acp.autocompletewindow = None
# If no active autocomplete window, open_completions(), None/break.
open_c = Func(result=False)
acp.open_completions = open_c
Equal(acp.autocomplete_event(ev), None)
Equal(open_c.args[0], ac.TAB)
open_c.result = True
Equal(acp.autocomplete_event(ev), 'break')
Equal(open_c.args[0], ac.TAB)
def test_try_open_completions_event(self):
Equal = self.assertEqual
text = self.text
acp = self.autocomplete
trycompletions = acp.try_open_completions_event
after = Func(result='after1')
acp.text.after = after
# If no text or trigger, after not called.
trycompletions()
Equal(after.called, 0)
text.insert('1.0', 're')
trycompletions()
Equal(after.called, 0)
# Attribute needed, no existing callback.
text.insert('insert', ' re.')
acp._delayed_completion_id = None
trycompletions()
Equal(acp._delayed_completion_index, text.index('insert'))
Equal(after.args,
(acp.popupwait, acp._delayed_open_completions, ac.TRY_A))
cb1 = acp._delayed_completion_id
Equal(cb1, 'after1')
# File needed, existing callback cancelled.
text.insert('insert', ' "./Lib/')
after.result = 'after2'
cancel = Func()
acp.text.after_cancel = cancel
trycompletions()
Equal(acp._delayed_completion_index, text.index('insert'))
Equal(cancel.args, (cb1,))
Equal(after.args,
(acp.popupwait, acp._delayed_open_completions, ac.TRY_F))
Equal(acp._delayed_completion_id, 'after2')
def test_delayed_open_completions(self):
Equal = self.assertEqual
acp = self.autocomplete
open_c = Func()
acp.open_completions = open_c
self.text.insert('1.0', '"dict.')
# Set autocomplete._delayed_completion_id to None.
# Text index changed, don't call open_completions.
acp._delayed_completion_id = 'after'
acp._delayed_completion_index = self.text.index('insert+1c')
acp._delayed_open_completions('dummy')
self.assertIsNone(acp._delayed_completion_id)
Equal(open_c.called, 0)
# Text index unchanged, call open_completions.
acp._delayed_completion_index = self.text.index('insert')
acp._delayed_open_completions((1, 2, 3, ac.FILES))
self.assertEqual(open_c.args[0], (1, 2, 3, ac.FILES))
def test_oc_cancel_comment(self):
none = self.assertIsNone
acp = self.autocomplete
# Comment is in neither code or string.
acp._delayed_completion_id = 'after'
after = Func(result='after')
acp.text.after_cancel = after
self.text.insert(1.0, '# comment')
none(acp.open_completions(ac.TAB)) # From 'else' after 'elif'.
none(acp._delayed_completion_id)
def test_oc_no_list(self):
acp = self.autocomplete
fetch = Func(result=([],[]))
acp.fetch_completions = fetch
self.text.insert('1.0', 'object')
self.assertIsNone(acp.open_completions(ac.TAB))
self.text.insert('insert', '.')
self.assertIsNone(acp.open_completions(ac.TAB))
self.assertEqual(fetch.called, 2)
def test_open_completions_none(self):
# Test other two None returns.
none = self.assertIsNone
acp = self.autocomplete
# No object for attributes or need call not allowed.
self.text.insert(1.0, '.')
none(acp.open_completions(ac.TAB))
self.text.insert('insert', ' int().')
none(acp.open_completions(ac.TAB))
# Blank or quote trigger 'if complete ...'.
self.text.delete(1.0, 'end')
self.assertFalse(acp.open_completions(ac.TAB))
self.text.insert('1.0', '"')
self.assertFalse(acp.open_completions(ac.TAB))
self.text.delete('1.0', 'end')
class dummy_acw:
__init__ = Func()
show_window = Func(result=False)
hide_window = Func()
def test_open_completions(self):
# Test completions of files and attributes.
acp = self.autocomplete
fetch = Func(result=(['tem'],['tem', '_tem']))
acp.fetch_completions = fetch
def make_acw(): return self.dummy_acw()
acp._make_autocomplete_window = make_acw
self.text.insert('1.0', 'int.')
acp.open_completions(ac.TAB)
self.assertIsInstance(acp.autocompletewindow, self.dummy_acw)
self.text.delete('1.0', 'end')
# Test files.
self.text.insert('1.0', '"t')
self.assertTrue(acp.open_completions(ac.TAB))
self.text.delete('1.0', 'end')
def test_completion_kwds(self):
self.assertIn('and', ac.completion_kwds)
self.assertIn('case', ac.completion_kwds)
self.assertNotIn('None', ac.completion_kwds)
def test_fetch_completions(self):
# Test that fetch_completions returns 2 lists:
# For attribute completion, a large list containing all variables, and
# a small list containing non-private variables.
# For file completion, a large list containing all files in the path,
# and a small list containing files that do not start with '.'.
acp = self.autocomplete
small, large = acp.fetch_completions(
'', ac.ATTRS)
if hasattr(__main__, '__file__') and __main__.__file__ != ac.__file__:
self.assertNotIn('AutoComplete', small) # See issue 36405.
# Test attributes
s, b = acp.fetch_completions('', ac.ATTRS)
self.assertLess(len(small), len(large))
self.assertTrue(all(filter(lambda x: x.startswith('_'), s)))
self.assertTrue(any(filter(lambda x: x.startswith('_'), b)))
# Test smalll should respect to __all__.
with patch.dict('__main__.__dict__', {'__all__': ['a', 'b']}):
s, b = acp.fetch_completions('', ac.ATTRS)
self.assertEqual(s, ['a', 'b'])
self.assertIn('__name__', b) # From __main__.__dict__.
self.assertIn('sum', b) # From __main__.__builtins__.__dict__.
self.assertIn('nonlocal', b) # From keyword.kwlist.
pos = b.index('False') # Test False not included twice.
self.assertNotEqual(b[pos+1], 'False')
# Test attributes with name entity.
mock = Mock()
mock._private = Mock()
with patch.dict('__main__.__dict__', {'foo': mock}):
s, b = acp.fetch_completions('foo', ac.ATTRS)
self.assertNotIn('_private', s)
self.assertIn('_private', b)
self.assertEqual(s, [i for i in sorted(dir(mock)) if i[:1] != '_'])
self.assertEqual(b, sorted(dir(mock)))
# Test files
def _listdir(path):
# This will be patch and used in fetch_completions.
if path == '.':
return ['foo', 'bar', '.hidden']
return ['monty', 'python', '.hidden']
with patch.object(os, 'listdir', _listdir):
s, b = acp.fetch_completions('', ac.FILES)
self.assertEqual(s, ['bar', 'foo'])
self.assertEqual(b, ['.hidden', 'bar', 'foo'])
s, b = acp.fetch_completions('~', ac.FILES)
self.assertEqual(s, ['monty', 'python'])
self.assertEqual(b, ['.hidden', 'monty', 'python'])
def test_get_entity(self):
# Test that a name is in the namespace of sys.modules and
# __main__.__dict__.
acp = self.autocomplete
Equal = self.assertEqual
Equal(acp.get_entity('int'), int)
# Test name from sys.modules.
mock = Mock()
with patch.dict('sys.modules', {'tempfile': mock}):
Equal(acp.get_entity('tempfile'), mock)
# Test name from __main__.__dict__.
di = {'foo': 10, 'bar': 20}
with patch.dict('__main__.__dict__', {'d': di}):
Equal(acp.get_entity('d'), di)
# Test name not in namespace.
with patch.dict('__main__.__dict__', {}):
with self.assertRaises(NameError):
acp.get_entity('not_exist')
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,32 @@
"Test autocomplete_w, coverage 11%."
import unittest
from test.support import requires
from tkinter import Tk, Text
import idlelib.autocomplete_w as acw
class AutoCompleteWindowTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.text = Text(cls.root)
cls.acw = acw.AutoCompleteWindow(cls.text, tags=None)
@classmethod
def tearDownClass(cls):
del cls.text, cls.acw
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def test_init(self):
self.assertEqual(self.acw.widget, self.text)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,155 @@
"Test autoexpand, coverage 100%."
from idlelib.autoexpand import AutoExpand
import unittest
from test.support import requires
from tkinter import Text, Tk
class DummyEditwin:
# AutoExpand.__init__ only needs .text
def __init__(self, text):
self.text = text
class AutoExpandTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.tk = Tk()
cls.text = Text(cls.tk)
cls.auto_expand = AutoExpand(DummyEditwin(cls.text))
cls.auto_expand.bell = lambda: None
# If mock_tk.Text._decode understood indexes 'insert' with suffixed 'linestart',
# 'wordstart', and 'lineend', used by autoexpand, we could use the following
# to run these test on non-gui machines (but check bell).
## try:
## requires('gui')
## #raise ResourceDenied() # Uncomment to test mock.
## except ResourceDenied:
## from idlelib.idle_test.mock_tk import Text
## cls.text = Text()
## cls.text.bell = lambda: None
## else:
## from tkinter import Tk, Text
## cls.tk = Tk()
## cls.text = Text(cls.tk)
@classmethod
def tearDownClass(cls):
del cls.text, cls.auto_expand
if hasattr(cls, 'tk'):
cls.tk.destroy()
del cls.tk
def tearDown(self):
self.text.delete('1.0', 'end')
def test_get_prevword(self):
text = self.text
previous = self.auto_expand.getprevword
equal = self.assertEqual
equal(previous(), '')
text.insert('insert', 't')
equal(previous(), 't')
text.insert('insert', 'his')
equal(previous(), 'this')
text.insert('insert', ' ')
equal(previous(), '')
text.insert('insert', 'is')
equal(previous(), 'is')
text.insert('insert', '\nsample\nstring')
equal(previous(), 'string')
text.delete('3.0', 'insert')
equal(previous(), '')
text.delete('1.0', 'end')
equal(previous(), '')
def test_before_only(self):
previous = self.auto_expand.getprevword
expand = self.auto_expand.expand_word_event
equal = self.assertEqual
self.text.insert('insert', 'ab ac bx ad ab a')
equal(self.auto_expand.getwords(), ['ab', 'ad', 'ac', 'a'])
expand('event')
equal(previous(), 'ab')
expand('event')
equal(previous(), 'ad')
expand('event')
equal(previous(), 'ac')
expand('event')
equal(previous(), 'a')
def test_after_only(self):
# Also add punctuation 'noise' that should be ignored.
text = self.text
previous = self.auto_expand.getprevword
expand = self.auto_expand.expand_word_event
equal = self.assertEqual
text.insert('insert', 'a, [ab] ac: () bx"" cd ac= ad ya')
text.mark_set('insert', '1.1')
equal(self.auto_expand.getwords(), ['ab', 'ac', 'ad', 'a'])
expand('event')
equal(previous(), 'ab')
expand('event')
equal(previous(), 'ac')
expand('event')
equal(previous(), 'ad')
expand('event')
equal(previous(), 'a')
def test_both_before_after(self):
text = self.text
previous = self.auto_expand.getprevword
expand = self.auto_expand.expand_word_event
equal = self.assertEqual
text.insert('insert', 'ab xy yz\n')
text.insert('insert', 'a ac by ac')
text.mark_set('insert', '2.1')
equal(self.auto_expand.getwords(), ['ab', 'ac', 'a'])
expand('event')
equal(previous(), 'ab')
expand('event')
equal(previous(), 'ac')
expand('event')
equal(previous(), 'a')
def test_other_expand_cases(self):
text = self.text
expand = self.auto_expand.expand_word_event
equal = self.assertEqual
# no expansion candidate found
equal(self.auto_expand.getwords(), [])
equal(expand('event'), 'break')
text.insert('insert', 'bx cy dz a')
equal(self.auto_expand.getwords(), [])
# reset state by successfully expanding once
# move cursor to another position and expand again
text.insert('insert', 'ac xy a ac ad a')
text.mark_set('insert', '1.7')
expand('event')
initial_state = self.auto_expand.state
text.mark_set('insert', '1.end')
expand('event')
new_state = self.auto_expand.state
self.assertNotEqual(initial_state, new_state)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,257 @@
"Test browser, coverage 90%."
from idlelib import browser
from test.support import requires
import unittest
from unittest import mock
from idlelib.idle_test.mock_idle import Func
from idlelib.util import py_extensions
from collections import deque
import os.path
import pyclbr
from tkinter import Tk
from idlelib.tree import TreeNode
class ModuleBrowserTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.mb = browser.ModuleBrowser(cls.root, __file__, _utest=True)
@classmethod
def tearDownClass(cls):
cls.mb.close()
cls.root.update_idletasks()
cls.root.destroy()
del cls.root, cls.mb
def test_init(self):
mb = self.mb
eq = self.assertEqual
eq(mb.path, __file__)
eq(pyclbr._modules, {})
self.assertIsInstance(mb.node, TreeNode)
self.assertIsNotNone(browser.file_open)
def test_settitle(self):
mb = self.mb
self.assertIn(os.path.basename(__file__), mb.top.title())
self.assertEqual(mb.top.iconname(), 'Module Browser')
def test_rootnode(self):
mb = self.mb
rn = mb.rootnode()
self.assertIsInstance(rn, browser.ModuleBrowserTreeItem)
def test_close(self):
mb = self.mb
mb.top.destroy = Func()
mb.node.destroy = Func()
mb.close()
self.assertTrue(mb.top.destroy.called)
self.assertTrue(mb.node.destroy.called)
del mb.top.destroy, mb.node.destroy
def test_is_browseable_extension(self):
path = "/path/to/file"
for ext in py_extensions:
with self.subTest(ext=ext):
filename = f'{path}{ext}'
actual = browser.is_browseable_extension(filename)
expected = ext not in browser.browseable_extension_blocklist
self.assertEqual(actual, expected)
# Nested tree same as in test_pyclbr.py except for supers on C0. C1.
mb = pyclbr
module, fname = 'test', 'test.py'
C0 = mb.Class(module, 'C0', ['base'], fname, 1, end_lineno=9)
F1 = mb._nest_function(C0, 'F1', 3, 5)
C1 = mb._nest_class(C0, 'C1', 6, 9, [''])
C2 = mb._nest_class(C1, 'C2', 7, 9)
F3 = mb._nest_function(C2, 'F3', 9, 9)
f0 = mb.Function(module, 'f0', fname, 11, end_lineno=15)
f1 = mb._nest_function(f0, 'f1', 12, 14)
f2 = mb._nest_function(f1, 'f2', 13, 13)
c1 = mb._nest_class(f0, 'c1', 15, 15)
mock_pyclbr_tree = {'C0': C0, 'f0': f0}
# Adjust C0.name, C1.name so tests do not depend on order.
browser.transform_children(mock_pyclbr_tree, 'test') # C0(base)
browser.transform_children(C0.children) # C1()
# The class below checks that the calls above are correct
# and that duplicate calls have no effect.
class TransformChildrenTest(unittest.TestCase):
def test_transform_module_children(self):
eq = self.assertEqual
transform = browser.transform_children
# Parameter matches tree module.
tcl = list(transform(mock_pyclbr_tree, 'test'))
eq(tcl, [C0, f0])
eq(tcl[0].name, 'C0(base)')
eq(tcl[1].name, 'f0')
# Check that second call does not change suffix.
tcl = list(transform(mock_pyclbr_tree, 'test'))
eq(tcl[0].name, 'C0(base)')
# Nothing to traverse if parameter name isn't same as tree module.
tcl = list(transform(mock_pyclbr_tree, 'different name'))
eq(tcl, [])
def test_transform_node_children(self):
eq = self.assertEqual
transform = browser.transform_children
# Class with two children, one name altered.
tcl = list(transform(C0.children))
eq(tcl, [F1, C1])
eq(tcl[0].name, 'F1')
eq(tcl[1].name, 'C1()')
tcl = list(transform(C0.children))
eq(tcl[1].name, 'C1()')
# Function with two children.
eq(list(transform(f0.children)), [f1, c1])
class ModuleBrowserTreeItemTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.mbt = browser.ModuleBrowserTreeItem(fname)
def test_init(self):
self.assertEqual(self.mbt.file, fname)
def test_gettext(self):
self.assertEqual(self.mbt.GetText(), fname)
def test_geticonname(self):
self.assertEqual(self.mbt.GetIconName(), 'python')
def test_isexpandable(self):
self.assertTrue(self.mbt.IsExpandable())
def test_listchildren(self):
save_rex = browser.pyclbr.readmodule_ex
save_tc = browser.transform_children
browser.pyclbr.readmodule_ex = Func(result=mock_pyclbr_tree)
browser.transform_children = Func(result=[f0, C0])
try:
self.assertEqual(self.mbt.listchildren(), [f0, C0])
finally:
browser.pyclbr.readmodule_ex = save_rex
browser.transform_children = save_tc
def test_getsublist(self):
mbt = self.mbt
mbt.listchildren = Func(result=[f0, C0])
sub0, sub1 = mbt.GetSubList()
del mbt.listchildren
self.assertIsInstance(sub0, browser.ChildBrowserTreeItem)
self.assertIsInstance(sub1, browser.ChildBrowserTreeItem)
self.assertEqual(sub0.name, 'f0')
self.assertEqual(sub1.name, 'C0(base)')
@mock.patch('idlelib.browser.file_open')
def test_ondoubleclick(self, fopen):
mbt = self.mbt
with mock.patch('os.path.exists', return_value=False):
mbt.OnDoubleClick()
fopen.assert_not_called()
with mock.patch('os.path.exists', return_value=True):
mbt.OnDoubleClick()
fopen.assert_called_once_with(fname)
class ChildBrowserTreeItemTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
CBT = browser.ChildBrowserTreeItem
cls.cbt_f1 = CBT(f1)
cls.cbt_C1 = CBT(C1)
cls.cbt_F1 = CBT(F1)
@classmethod
def tearDownClass(cls):
del cls.cbt_C1, cls.cbt_f1, cls.cbt_F1
def test_init(self):
eq = self.assertEqual
eq(self.cbt_C1.name, 'C1()')
self.assertFalse(self.cbt_C1.isfunction)
eq(self.cbt_f1.name, 'f1')
self.assertTrue(self.cbt_f1.isfunction)
def test_gettext(self):
self.assertEqual(self.cbt_C1.GetText(), 'class C1()')
self.assertEqual(self.cbt_f1.GetText(), 'def f1(...)')
def test_geticonname(self):
self.assertEqual(self.cbt_C1.GetIconName(), 'folder')
self.assertEqual(self.cbt_f1.GetIconName(), 'python')
def test_isexpandable(self):
self.assertTrue(self.cbt_C1.IsExpandable())
self.assertTrue(self.cbt_f1.IsExpandable())
self.assertFalse(self.cbt_F1.IsExpandable())
def test_getsublist(self):
eq = self.assertEqual
CBT = browser.ChildBrowserTreeItem
f1sublist = self.cbt_f1.GetSubList()
self.assertIsInstance(f1sublist[0], CBT)
eq(len(f1sublist), 1)
eq(f1sublist[0].name, 'f2')
eq(self.cbt_F1.GetSubList(), [])
@mock.patch('idlelib.browser.file_open')
def test_ondoubleclick(self, fopen):
goto = fopen.return_value.gotoline = mock.Mock()
self.cbt_F1.OnDoubleClick()
fopen.assert_called()
goto.assert_called()
goto.assert_called_with(self.cbt_F1.obj.lineno)
# Failure test would have to raise OSError or AttributeError.
class NestedChildrenTest(unittest.TestCase):
"Test that all the nodes in a nested tree are added to the BrowserTree."
def test_nested(self):
queue = deque()
actual_names = []
# The tree items are processed in breadth first order.
# Verify that processing each sublist hits every node and
# in the right order.
expected_names = ['f0', 'C0(base)',
'f1', 'c1', 'F1', 'C1()',
'f2', 'C2',
'F3']
CBT = browser.ChildBrowserTreeItem
queue.extend((CBT(f0), CBT(C0)))
while queue:
cb = queue.popleft()
sublist = cb.GetSubList()
queue.extend(sublist)
self.assertIn(cb.name, cb.GetText())
self.assertIn(cb.GetIconName(), ('python', 'folder'))
self.assertIs(cb.IsExpandable(), sublist != [])
actual_names.append(cb.name)
self.assertEqual(actual_names, expected_names)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,372 @@
"Test calltip, coverage 76%"
from idlelib import calltip
import unittest
from unittest.mock import Mock
import textwrap
import types
import re
from idlelib.idle_test.mock_tk import Text
from test.support import MISSING_C_DOCSTRINGS
# Test Class TC is used in multiple get_argspec test methods
class TC:
'doc'
tip = "(ai=None, *b)"
def __init__(self, ai=None, *b): 'doc'
__init__.tip = "(self, ai=None, *b)"
def t1(self): 'doc'
t1.tip = "(self)"
def t2(self, ai, b=None): 'doc'
t2.tip = "(self, ai, b=None)"
def t3(self, ai, *args): 'doc'
t3.tip = "(self, ai, *args)"
def t4(self, *args): 'doc'
t4.tip = "(self, *args)"
def t5(self, ai, b=None, *args, **kw): 'doc'
t5.tip = "(self, ai, b=None, *args, **kw)"
def t6(no, self): 'doc'
t6.tip = "(no, self)"
def __call__(self, ci): 'doc'
__call__.tip = "(self, ci)"
def nd(self): pass # No doc.
# attaching .tip to wrapped methods does not work
@classmethod
def cm(cls, a): 'doc'
@staticmethod
def sm(b): 'doc'
tc = TC()
default_tip = calltip._default_callable_argspec
get_spec = calltip.get_argspec
class Get_argspecTest(unittest.TestCase):
# The get_spec function must return a string, even if blank.
# Test a variety of objects to be sure that none cause it to raise
# (quite aside from getting as correct an answer as possible).
# The tests of builtins may break if inspect or the docstrings change,
# but a red buildbot is better than a user crash (as has happened).
# For a simple mismatch, change the expected output to the actual.
@unittest.skipIf(MISSING_C_DOCSTRINGS,
"Signature information for builtins requires docstrings")
def test_builtins(self):
def tiptest(obj, out):
self.assertEqual(get_spec(obj), out)
# Python class that inherits builtin methods
class List(list): "List() doc"
# Simulate builtin with no docstring for default tip test
class SB: __call__ = None
if List.__doc__ is not None:
tiptest(List,
f'(iterable=(), /)'
f'\n{List.__doc__}')
tiptest(list.__new__,
'(*args, **kwargs)\n'
'Create and return a new object. '
'See help(type) for accurate signature.')
tiptest(list.__init__,
'(self, /, *args, **kwargs)\n'
'Initialize self. See help(type(self)) for accurate signature.')
append_doc = "\nAppend object to the end of the list."
tiptest(list.append, '(self, object, /)' + append_doc)
tiptest(List.append, '(self, object, /)' + append_doc)
tiptest([].append, '(object, /)' + append_doc)
# The use of 'object' above matches the signature text.
tiptest(types.MethodType,
'(function, instance, /)\n'
'Create a bound instance method object.')
tiptest(SB(), default_tip)
p = re.compile('')
tiptest(re.sub, '''\
(pattern, repl, string, count=0, flags=0)
Return the string obtained by replacing the leftmost
non-overlapping occurrences of the pattern in string by the
replacement repl. repl can be either a string or a callable;
if a string, backslash escapes in it are processed. If it is
a callable, it's passed the Match object and must return''')
tiptest(p.sub, '''\
(repl, string, count=0)
Return the string obtained by replacing the leftmost \
non-overlapping occurrences o...''')
def test_signature_wrap(self):
if textwrap.TextWrapper.__doc__ is not None:
self.assertEqual(get_spec(textwrap.TextWrapper), '''\
(width=70, initial_indent='', subsequent_indent='', expand_tabs=True,
replace_whitespace=True, fix_sentence_endings=False, break_long_words=True,
drop_whitespace=True, break_on_hyphens=True, tabsize=8, *, max_lines=None,
placeholder=' [...]')
Object for wrapping/filling text. The public interface consists of
the wrap() and fill() methods; the other methods are just there for
subclasses to override in order to tweak the default behaviour.
If you want to completely replace the main wrapping algorithm,
you\'ll probably have to override _wrap_chunks().''')
def test_properly_formatted(self):
def foo(s='a'*100):
pass
def bar(s='a'*100):
"""Hello Guido"""
pass
def baz(s='a'*100, z='b'*100):
pass
indent = calltip._INDENT
sfoo = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
"aaaaaaaaaa')"
sbar = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
"aaaaaaaaaa')\nHello Guido"
sbaz = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
"aaaaaaaaaa', z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\
"bbbbbbbbbbbbbbbbb\n" + indent + "bbbbbbbbbbbbbbbbbbbbbb"\
"bbbbbbbbbbbbbbbbbbbbbb')"
for func,doc in [(foo, sfoo), (bar, sbar), (baz, sbaz)]:
with self.subTest(func=func, doc=doc):
self.assertEqual(get_spec(func), doc)
def test_docline_truncation(self):
def f(): pass
f.__doc__ = 'a'*300
self.assertEqual(get_spec(f), f"()\n{'a'*(calltip._MAX_COLS-3) + '...'}")
@unittest.skipIf(MISSING_C_DOCSTRINGS,
"Signature information for builtins requires docstrings")
def test_multiline_docstring(self):
# Test fewer lines than max.
self.assertEqual(get_spec(range),
"range(stop) -> range object\n"
"range(start, stop[, step]) -> range object")
# Test max lines
self.assertEqual(get_spec(bytes), '''\
bytes(iterable_of_ints) -> bytes
bytes(string, encoding[, errors]) -> bytes
bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
bytes(int) -> bytes object of size given by the parameter initialized with null bytes
bytes() -> empty bytes object''')
def test_multiline_docstring_2(self):
# Test more than max lines
def f(): pass
f.__doc__ = 'a\n' * 15
self.assertEqual(get_spec(f), '()' + '\na' * calltip._MAX_LINES)
def test_functions(self):
def t1(): 'doc'
t1.tip = "()"
def t2(a, b=None): 'doc'
t2.tip = "(a, b=None)"
def t3(a, *args): 'doc'
t3.tip = "(a, *args)"
def t4(*args): 'doc'
t4.tip = "(*args)"
def t5(a, b=None, *args, **kw): 'doc'
t5.tip = "(a, b=None, *args, **kw)"
doc = '\ndoc' if t1.__doc__ is not None else ''
for func in (t1, t2, t3, t4, t5, TC):
with self.subTest(func=func):
self.assertEqual(get_spec(func), func.tip + doc)
def test_methods(self):
doc = '\ndoc' if TC.__doc__ is not None else ''
for meth in (TC.t1, TC.t2, TC.t3, TC.t4, TC.t5, TC.t6, TC.__call__):
with self.subTest(meth=meth):
self.assertEqual(get_spec(meth), meth.tip + doc)
self.assertEqual(get_spec(TC.cm), "(a)" + doc)
self.assertEqual(get_spec(TC.sm), "(b)" + doc)
def test_bound_methods(self):
# test that first parameter is correctly removed from argspec
doc = '\ndoc' if TC.__doc__ is not None else ''
for meth, mtip in ((tc.t1, "()"), (tc.t4, "(*args)"),
(tc.t6, "(self)"), (tc.__call__, '(ci)'),
(tc, '(ci)'), (TC.cm, "(a)"),):
with self.subTest(meth=meth, mtip=mtip):
self.assertEqual(get_spec(meth), mtip + doc)
def test_starred_parameter(self):
# test that starred first parameter is *not* removed from argspec
class C:
def m1(*args): pass
c = C()
for meth, mtip in ((C.m1, '(*args)'), (c.m1, "(*args)"),):
with self.subTest(meth=meth, mtip=mtip):
self.assertEqual(get_spec(meth), mtip)
def test_invalid_method_get_spec(self):
class C:
def m2(**kwargs): pass
class Test:
def __call__(*, a): pass
mtip = calltip._invalid_method
self.assertEqual(get_spec(C().m2), mtip)
self.assertEqual(get_spec(Test()), mtip)
def test_non_ascii_name(self):
# test that re works to delete a first parameter name that
# includes non-ascii chars, such as various forms of A.
uni = "(A\u0391\u0410\u05d0\u0627\u0905\u1e00\u3042, a)"
assert calltip._first_param.sub('', uni) == '(a)'
def test_no_docstring(self):
for meth, mtip in ((TC.nd, "(self)"), (tc.nd, "()")):
with self.subTest(meth=meth, mtip=mtip):
self.assertEqual(get_spec(meth), mtip)
def test_buggy_getattr_class(self):
class NoCall:
def __getattr__(self, name): # Not invoked for class attribute.
raise IndexError # Bug.
class CallA(NoCall):
def __call__(self, ci): # Bug does not matter.
pass
class CallB(NoCall):
def __call__(oui, a, b, c): # Non-standard 'self'.
pass
for meth, mtip in ((NoCall, default_tip), (CallA, default_tip),
(NoCall(), ''), (CallA(), '(ci)'),
(CallB(), '(a, b, c)')):
with self.subTest(meth=meth, mtip=mtip):
self.assertEqual(get_spec(meth), mtip)
def test_metaclass_class(self): # Failure case for issue 38689.
class Type(type): # Type() requires 3 type args, returns class.
__class__ = property({}.__getitem__, {}.__setitem__)
class Object(metaclass=Type):
__slots__ = '__class__'
for meth, mtip in ((Type, get_spec(type)), (Object, default_tip),
(Object(), '')):
with self.subTest(meth=meth, mtip=mtip):
self.assertEqual(get_spec(meth), mtip)
def test_non_callables(self):
for obj in (0, 0.0, '0', b'0', [], {}):
with self.subTest(obj=obj):
self.assertEqual(get_spec(obj), '')
class Get_entityTest(unittest.TestCase):
def test_bad_entity(self):
self.assertIsNone(calltip.get_entity('1/0'))
def test_good_entity(self):
self.assertIs(calltip.get_entity('int'), int)
# Test the 9 Calltip methods.
# open_calltip is about half the code; the others are fairly trivial.
# The default mocks are what are needed for open_calltip.
class mock_Shell:
"Return mock sufficient to pass to hyperparser."
def __init__(self, text):
text.tag_prevrange = Mock(return_value=None)
self.text = text
self.prompt_last_line = ">>> "
self.indentwidth = 4
self.tabwidth = 8
class mock_TipWindow:
def __init__(self):
pass
def showtip(self, text, parenleft, parenright):
self.args = parenleft, parenright
self.parenline, self.parencol = map(int, parenleft.split('.'))
class WrappedCalltip(calltip.Calltip):
def _make_tk_calltip_window(self):
return mock_TipWindow()
def remove_calltip_window(self, event=None):
if self.active_calltip: # Setup to None.
self.active_calltip = None
self.tips_removed += 1 # Setup to 0.
def fetch_tip(self, expression):
return 'tip'
class CalltipTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.text = Text()
cls.ct = WrappedCalltip(mock_Shell(cls.text))
def setUp(self):
self.text.delete('1.0', 'end') # Insert and call
self.ct.active_calltip = None
# Test .active_calltip, +args
self.ct.tips_removed = 0
def open_close(self, testfunc):
# Open-close template with testfunc called in between.
opentip = self.ct.open_calltip
self.text.insert(1.0, 'f(')
opentip(False)
self.tip = self.ct.active_calltip
testfunc(self) ###
self.text.insert('insert', ')')
opentip(False)
self.assertIsNone(self.ct.active_calltip, None)
def test_open_close(self):
def args(self):
self.assertEqual(self.tip.args, ('1.1', '1.end'))
self.open_close(args)
def test_repeated_force(self):
def force(self):
for char in 'abc':
self.text.insert('insert', 'a')
self.ct.open_calltip(True)
self.ct.open_calltip(True)
self.assertIs(self.ct.active_calltip, self.tip)
self.open_close(force)
def test_repeated_parens(self):
def parens(self):
for context in "a", "'":
with self.subTest(context=context):
self.text.insert('insert', context)
for char in '(()())':
self.text.insert('insert', char)
self.assertIs(self.ct.active_calltip, self.tip)
self.text.insert('insert', "'")
self.open_close(parens)
def test_comment_parens(self):
def comment(self):
self.text.insert('insert', "# ")
for char in '(()())':
self.text.insert('insert', char)
self.assertIs(self.ct.active_calltip, self.tip)
self.text.insert('insert', "\n")
self.open_close(comment)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,29 @@
"Test calltip_w, coverage 18%."
from idlelib import calltip_w
import unittest
from test.support import requires
from tkinter import Tk, Text
class CallTipWindowTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.text = Text(cls.root)
cls.calltip = calltip_w.CalltipWindow(cls.text)
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
cls.root.destroy()
del cls.text, cls.root
def test_init(self):
self.assertEqual(self.calltip.anchor_widget, self.text)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,455 @@
"Test codecontext, coverage 100%"
from idlelib import codecontext
import unittest
import unittest.mock
from test.support import requires
from tkinter import NSEW, Tk, Frame, Text, TclError
from unittest import mock
import re
from idlelib import config
usercfg = codecontext.idleConf.userCfg
testcfg = {
'main': config.IdleUserConfParser(''),
'highlight': config.IdleUserConfParser(''),
'keys': config.IdleUserConfParser(''),
'extensions': config.IdleUserConfParser(''),
}
code_sample = """\
class C1:
# Class comment.
def __init__(self, a, b):
self.a = a
self.b = b
def compare(self):
if a > b:
return a
elif a < b:
return b
else:
return None
"""
class DummyEditwin:
def __init__(self, root, frame, text):
self.root = root
self.top = root
self.text_frame = frame
self.text = text
self.label = ''
def getlineno(self, index):
return int(float(self.text.index(index)))
def update_menu_label(self, **kwargs):
self.label = kwargs['label']
class CodeContextTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
root = cls.root = Tk()
root.withdraw()
frame = cls.frame = Frame(root)
text = cls.text = Text(frame)
text.insert('1.0', code_sample)
# Need to pack for creation of code context text widget.
frame.pack(side='left', fill='both', expand=1)
text.grid(row=1, column=1, sticky=NSEW)
cls.editor = DummyEditwin(root, frame, text)
codecontext.idleConf.userCfg = testcfg
@classmethod
def tearDownClass(cls):
codecontext.idleConf.userCfg = usercfg
cls.editor.text.delete('1.0', 'end')
del cls.editor, cls.frame, cls.text
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def setUp(self):
self.text.yview(0)
self.text['font'] = 'TkFixedFont'
self.cc = codecontext.CodeContext(self.editor)
self.highlight_cfg = {"background": '#abcdef',
"foreground": '#123456'}
orig_idleConf_GetHighlight = codecontext.idleConf.GetHighlight
def mock_idleconf_GetHighlight(theme, element):
if element == 'context':
return self.highlight_cfg
return orig_idleConf_GetHighlight(theme, element)
GetHighlight_patcher = unittest.mock.patch.object(
codecontext.idleConf, 'GetHighlight', mock_idleconf_GetHighlight)
GetHighlight_patcher.start()
self.addCleanup(GetHighlight_patcher.stop)
self.font_override = 'TkFixedFont'
def mock_idleconf_GetFont(root, configType, section):
return self.font_override
GetFont_patcher = unittest.mock.patch.object(
codecontext.idleConf, 'GetFont', mock_idleconf_GetFont)
GetFont_patcher.start()
self.addCleanup(GetFont_patcher.stop)
def tearDown(self):
if self.cc.context:
self.cc.context.destroy()
# Explicitly call __del__ to remove scheduled scripts.
self.cc.__del__()
del self.cc.context, self.cc
def test_init(self):
eq = self.assertEqual
ed = self.editor
cc = self.cc
eq(cc.editwin, ed)
eq(cc.text, ed.text)
eq(cc.text['font'], ed.text['font'])
self.assertIsNone(cc.context)
eq(cc.info, [(0, -1, '', False)])
eq(cc.topvisible, 1)
self.assertIsNone(self.cc.t1)
def test_del(self):
self.cc.__del__()
def test_del_with_timer(self):
timer = self.cc.t1 = self.text.after(10000, lambda: None)
self.cc.__del__()
with self.assertRaises(TclError) as cm:
self.root.tk.call('after', 'info', timer)
self.assertIn("doesn't exist", str(cm.exception))
def test_reload(self):
codecontext.CodeContext.reload()
self.assertEqual(self.cc.context_depth, 15)
def test_toggle_code_context_event(self):
eq = self.assertEqual
cc = self.cc
toggle = cc.toggle_code_context_event
# Make sure code context is off.
if cc.context:
toggle()
# Toggle on.
toggle()
self.assertIsNotNone(cc.context)
eq(cc.context['font'], self.text['font'])
eq(cc.context['fg'], self.highlight_cfg['foreground'])
eq(cc.context['bg'], self.highlight_cfg['background'])
eq(cc.context.get('1.0', 'end-1c'), '')
eq(cc.editwin.label, 'Hide Code Context')
eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer')
# Toggle off.
toggle()
self.assertIsNone(cc.context)
eq(cc.editwin.label, 'Show Code Context')
self.assertIsNone(self.cc.t1)
# Scroll down and toggle back on.
line11_context = '\n'.join(x[2] for x in cc.get_context(11)[0])
cc.text.yview(11)
toggle()
eq(cc.context.get('1.0', 'end-1c'), line11_context)
# Toggle off and on again.
toggle()
toggle()
eq(cc.context.get('1.0', 'end-1c'), line11_context)
def test_get_context(self):
eq = self.assertEqual
gc = self.cc.get_context
# stopline must be greater than 0.
with self.assertRaises(AssertionError):
gc(1, stopline=0)
eq(gc(3), ([(2, 0, 'class C1:', 'class')], 0))
# Don't return comment.
eq(gc(4), ([(2, 0, 'class C1:', 'class')], 0))
# Two indentation levels and no comment.
eq(gc(5), ([(2, 0, 'class C1:', 'class'),
(4, 4, ' def __init__(self, a, b):', 'def')], 0))
# Only one 'def' is returned, not both at the same indent level.
eq(gc(10), ([(2, 0, 'class C1:', 'class'),
(7, 4, ' def compare(self):', 'def'),
(8, 8, ' if a > b:', 'if')], 0))
# With 'elif', also show the 'if' even though it's at the same level.
eq(gc(11), ([(2, 0, 'class C1:', 'class'),
(7, 4, ' def compare(self):', 'def'),
(8, 8, ' if a > b:', 'if'),
(10, 8, ' elif a < b:', 'elif')], 0))
# Set stop_line to not go back to first line in source code.
# Return includes stop_line.
eq(gc(11, stopline=2), ([(2, 0, 'class C1:', 'class'),
(7, 4, ' def compare(self):', 'def'),
(8, 8, ' if a > b:', 'if'),
(10, 8, ' elif a < b:', 'elif')], 0))
eq(gc(11, stopline=3), ([(7, 4, ' def compare(self):', 'def'),
(8, 8, ' if a > b:', 'if'),
(10, 8, ' elif a < b:', 'elif')], 4))
eq(gc(11, stopline=8), ([(8, 8, ' if a > b:', 'if'),
(10, 8, ' elif a < b:', 'elif')], 8))
# Set stop_indent to test indent level to stop at.
eq(gc(11, stopindent=4), ([(7, 4, ' def compare(self):', 'def'),
(8, 8, ' if a > b:', 'if'),
(10, 8, ' elif a < b:', 'elif')], 4))
# Check that the 'if' is included.
eq(gc(11, stopindent=8), ([(8, 8, ' if a > b:', 'if'),
(10, 8, ' elif a < b:', 'elif')], 8))
def test_update_code_context(self):
eq = self.assertEqual
cc = self.cc
# Ensure code context is active.
if not cc.context:
cc.toggle_code_context_event()
# Invoke update_code_context without scrolling - nothing happens.
self.assertIsNone(cc.update_code_context())
eq(cc.info, [(0, -1, '', False)])
eq(cc.topvisible, 1)
# Scroll down to line 1.
cc.text.yview(1)
cc.update_code_context()
eq(cc.info, [(0, -1, '', False)])
eq(cc.topvisible, 2)
eq(cc.context.get('1.0', 'end-1c'), '')
# Scroll down to line 2.
cc.text.yview(2)
cc.update_code_context()
eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1:', 'class')])
eq(cc.topvisible, 3)
eq(cc.context.get('1.0', 'end-1c'), 'class C1:')
# Scroll down to line 3. Since it's a comment, nothing changes.
cc.text.yview(3)
cc.update_code_context()
eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1:', 'class')])
eq(cc.topvisible, 4)
eq(cc.context.get('1.0', 'end-1c'), 'class C1:')
# Scroll down to line 4.
cc.text.yview(4)
cc.update_code_context()
eq(cc.info, [(0, -1, '', False),
(2, 0, 'class C1:', 'class'),
(4, 4, ' def __init__(self, a, b):', 'def')])
eq(cc.topvisible, 5)
eq(cc.context.get('1.0', 'end-1c'), 'class C1:\n'
' def __init__(self, a, b):')
# Scroll down to line 11. Last 'def' is removed.
cc.text.yview(11)
cc.update_code_context()
eq(cc.info, [(0, -1, '', False),
(2, 0, 'class C1:', 'class'),
(7, 4, ' def compare(self):', 'def'),
(8, 8, ' if a > b:', 'if'),
(10, 8, ' elif a < b:', 'elif')])
eq(cc.topvisible, 12)
eq(cc.context.get('1.0', 'end-1c'), 'class C1:\n'
' def compare(self):\n'
' if a > b:\n'
' elif a < b:')
# No scroll. No update, even though context_depth changed.
cc.update_code_context()
cc.context_depth = 1
eq(cc.info, [(0, -1, '', False),
(2, 0, 'class C1:', 'class'),
(7, 4, ' def compare(self):', 'def'),
(8, 8, ' if a > b:', 'if'),
(10, 8, ' elif a < b:', 'elif')])
eq(cc.topvisible, 12)
eq(cc.context.get('1.0', 'end-1c'), 'class C1:\n'
' def compare(self):\n'
' if a > b:\n'
' elif a < b:')
# Scroll up.
cc.text.yview(5)
cc.update_code_context()
eq(cc.info, [(0, -1, '', False),
(2, 0, 'class C1:', 'class'),
(4, 4, ' def __init__(self, a, b):', 'def')])
eq(cc.topvisible, 6)
# context_depth is 1.
eq(cc.context.get('1.0', 'end-1c'), ' def __init__(self, a, b):')
def test_jumptoline(self):
eq = self.assertEqual
cc = self.cc
jump = cc.jumptoline
if not cc.context:
cc.toggle_code_context_event()
# Empty context.
cc.text.yview('2.0')
cc.update_code_context()
eq(cc.topvisible, 2)
cc.context.mark_set('insert', '1.5')
jump()
eq(cc.topvisible, 1)
# 4 lines of context showing.
cc.text.yview('12.0')
cc.update_code_context()
eq(cc.topvisible, 12)
cc.context.mark_set('insert', '3.0')
jump()
eq(cc.topvisible, 8)
# More context lines than limit.
cc.context_depth = 2
cc.text.yview('12.0')
cc.update_code_context()
eq(cc.topvisible, 12)
cc.context.mark_set('insert', '1.0')
jump()
eq(cc.topvisible, 8)
# Context selection stops jump.
cc.text.yview('5.0')
cc.update_code_context()
cc.context.tag_add('sel', '1.0', '2.0')
cc.context.mark_set('insert', '1.0')
jump() # Without selection, to line 2.
eq(cc.topvisible, 5)
@mock.patch.object(codecontext.CodeContext, 'update_code_context')
def test_timer_event(self, mock_update):
# Ensure code context is not active.
if self.cc.context:
self.cc.toggle_code_context_event()
self.cc.timer_event()
mock_update.assert_not_called()
# Activate code context.
self.cc.toggle_code_context_event()
self.cc.timer_event()
mock_update.assert_called()
def test_font(self):
eq = self.assertEqual
cc = self.cc
orig_font = cc.text['font']
test_font = 'TkTextFont'
self.assertNotEqual(orig_font, test_font)
# Ensure code context is not active.
if cc.context is not None:
cc.toggle_code_context_event()
self.font_override = test_font
# Nothing breaks or changes with inactive code context.
cc.update_font()
# Activate code context, previous font change is immediately effective.
cc.toggle_code_context_event()
eq(cc.context['font'], test_font)
# Call the font update, change is picked up.
self.font_override = orig_font
cc.update_font()
eq(cc.context['font'], orig_font)
def test_highlight_colors(self):
eq = self.assertEqual
cc = self.cc
orig_colors = dict(self.highlight_cfg)
test_colors = {'background': '#222222', 'foreground': '#ffff00'}
def assert_colors_are_equal(colors):
eq(cc.context['background'], colors['background'])
eq(cc.context['foreground'], colors['foreground'])
# Ensure code context is not active.
if cc.context:
cc.toggle_code_context_event()
self.highlight_cfg = test_colors
# Nothing breaks with inactive code context.
cc.update_highlight_colors()
# Activate code context, previous colors change is immediately effective.
cc.toggle_code_context_event()
assert_colors_are_equal(test_colors)
# Call colors update with no change to the configured colors.
cc.update_highlight_colors()
assert_colors_are_equal(test_colors)
# Call the colors update with code context active, change is picked up.
self.highlight_cfg = orig_colors
cc.update_highlight_colors()
assert_colors_are_equal(orig_colors)
class HelperFunctionText(unittest.TestCase):
def test_get_spaces_firstword(self):
get = codecontext.get_spaces_firstword
test_lines = (
(' first word', (' ', 'first')),
('\tfirst word', ('\t', 'first')),
(' \u19D4\u19D2: ', (' ', '\u19D4\u19D2')),
('no spaces', ('', 'no')),
('', ('', '')),
('# TEST COMMENT', ('', '')),
(' (continuation)', (' ', ''))
)
for line, expected_output in test_lines:
self.assertEqual(get(line), expected_output)
# Send the pattern in the call.
self.assertEqual(get(' (continuation)',
c=re.compile(r'^(\s*)([^\s]*)')),
(' ', '(continuation)'))
def test_get_line_info(self):
eq = self.assertEqual
gli = codecontext.get_line_info
lines = code_sample.splitlines()
# Line 1 is not a BLOCKOPENER.
eq(gli(lines[0]), (codecontext.INFINITY, '', False))
# Line 2 is a BLOCKOPENER without an indent.
eq(gli(lines[1]), (0, 'class C1:', 'class'))
# Line 3 is not a BLOCKOPENER and does not return the indent level.
eq(gli(lines[2]), (codecontext.INFINITY, ' # Class comment.', False))
# Line 4 is a BLOCKOPENER and is indented.
eq(gli(lines[3]), (4, ' def __init__(self, a, b):', 'def'))
# Line 8 is a different BLOCKOPENER and is indented.
eq(gli(lines[7]), (8, ' if a > b:', 'if'))
# Test tab.
eq(gli('\tif a == b:'), (1, '\tif a == b:', 'if'))
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,622 @@
"Test colorizer, coverage 99%."
from idlelib import colorizer
from test.support import requires
import unittest
from unittest import mock
from idlelib.idle_test.tkinter_testing_utils import run_in_tk_mainloop
from functools import partial
import textwrap
from tkinter import Tk, Text
from idlelib import config
from idlelib.percolator import Percolator
usercfg = colorizer.idleConf.userCfg
testcfg = {
'main': config.IdleUserConfParser(''),
'highlight': config.IdleUserConfParser(''),
'keys': config.IdleUserConfParser(''),
'extensions': config.IdleUserConfParser(''),
}
source = textwrap.dedent("""\
if True: int ('1') # keyword, builtin, string, comment
elif False: print(0) # 'string' in comment
else: float(None) # if in comment
if iF + If + IF: 'keyword matching must respect case'
if'': x or'' # valid keyword-string no-space combinations
async def f(): await g()
# Strings should be entirely colored, including quotes.
'x', '''x''', "x", \"""x\"""
'abc\\
def'
'''abc\\
def'''
# All valid prefixes for unicode and byte strings should be colored.
r'x', u'x', R'x', U'x', f'x', F'x'
fr'x', Fr'x', fR'x', FR'x', rf'x', rF'x', Rf'x', RF'x'
b'x',B'x', br'x',Br'x',bR'x',BR'x', rb'x', rB'x',Rb'x',RB'x'
# Invalid combinations of legal characters should be half colored.
ur'x', ru'x', uf'x', fu'x', UR'x', ufr'x', rfu'x', xf'x', fx'x'
match point:
case (x, 0) as _:
print(f"X={x}")
case [_, [_], "_",
_]:
pass
case _ if ("a" if _ else set()): pass
case _:
raise ValueError("Not a point _")
'''
case _:'''
"match x:"
""")
def setUpModule():
colorizer.idleConf.userCfg = testcfg
def tearDownModule():
colorizer.idleConf.userCfg = usercfg
class FunctionTest(unittest.TestCase):
def test_any(self):
self.assertEqual(colorizer.any('test', ('a', 'b', 'cd')),
'(?P<test>a|b|cd)')
def test_make_pat(self):
# Tested in more detail by testing prog.
self.assertTrue(colorizer.make_pat())
def test_prog(self):
prog = colorizer.prog
eq = self.assertEqual
line = 'def f():\n print("hello")\n'
m = prog.search(line)
eq(m.groupdict()['KEYWORD'], 'def')
m = prog.search(line, m.end())
eq(m.groupdict()['SYNC'], '\n')
m = prog.search(line, m.end())
eq(m.groupdict()['BUILTIN'], 'print')
m = prog.search(line, m.end())
eq(m.groupdict()['STRING'], '"hello"')
m = prog.search(line, m.end())
eq(m.groupdict()['SYNC'], '\n')
def test_idprog(self):
idprog = colorizer.idprog
m = idprog.match('nospace')
self.assertIsNone(m)
m = idprog.match(' space')
self.assertEqual(m.group(0), ' space')
class ColorConfigTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
root = cls.root = Tk()
root.withdraw()
cls.text = Text(root)
@classmethod
def tearDownClass(cls):
del cls.text
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def test_color_config(self):
text = self.text
eq = self.assertEqual
colorizer.color_config(text)
# Uses IDLE Classic theme as default.
eq(text['background'], '#ffffff')
eq(text['foreground'], '#000000')
eq(text['selectbackground'], 'gray')
eq(text['selectforeground'], '#000000')
eq(text['insertbackground'], 'black')
eq(text['inactiveselectbackground'], 'gray')
class ColorDelegatorInstantiationTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
root = cls.root = Tk()
root.withdraw()
cls.text = Text(root)
@classmethod
def tearDownClass(cls):
del cls.text
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def setUp(self):
self.color = colorizer.ColorDelegator()
def tearDown(self):
self.color.close()
self.text.delete('1.0', 'end')
self.color.resetcache()
del self.color
def test_init(self):
color = self.color
self.assertIsInstance(color, colorizer.ColorDelegator)
def test_init_state(self):
# init_state() is called during the instantiation of
# ColorDelegator in setUp().
color = self.color
self.assertIsNone(color.after_id)
self.assertTrue(color.allow_colorizing)
self.assertFalse(color.colorizing)
self.assertFalse(color.stop_colorizing)
class ColorDelegatorTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
root = cls.root = Tk()
root.withdraw()
text = cls.text = Text(root)
cls.percolator = Percolator(text)
# Delegator stack = [Delegator(text)]
@classmethod
def tearDownClass(cls):
cls.percolator.close()
del cls.percolator, cls.text
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def setUp(self):
self.color = colorizer.ColorDelegator()
self.percolator.insertfilter(self.color)
# Calls color.setdelegate(Delegator(text)).
def tearDown(self):
self.color.close()
self.percolator.removefilter(self.color)
self.text.delete('1.0', 'end')
self.color.resetcache()
del self.color
def test_setdelegate(self):
# Called in setUp when filter is attached to percolator.
color = self.color
self.assertIsInstance(color.delegate, colorizer.Delegator)
# It is too late to mock notify_range, so test side effect.
self.assertEqual(self.root.tk.call(
'after', 'info', color.after_id)[1], 'timer')
def test_LoadTagDefs(self):
highlight = partial(config.idleConf.GetHighlight, theme='IDLE Classic')
for tag, colors in self.color.tagdefs.items():
with self.subTest(tag=tag):
self.assertIn('background', colors)
self.assertIn('foreground', colors)
if tag not in ('SYNC', 'TODO'):
self.assertEqual(colors, highlight(element=tag.lower()))
def test_config_colors(self):
text = self.text
highlight = partial(config.idleConf.GetHighlight, theme='IDLE Classic')
for tag in self.color.tagdefs:
for plane in ('background', 'foreground'):
with self.subTest(tag=tag, plane=plane):
if tag in ('SYNC', 'TODO'):
self.assertEqual(text.tag_cget(tag, plane), '')
else:
self.assertEqual(text.tag_cget(tag, plane),
highlight(element=tag.lower())[plane])
# 'sel' is marked as the highest priority.
self.assertEqual(text.tag_names()[-1], 'sel')
@mock.patch.object(colorizer.ColorDelegator, 'notify_range')
def test_insert(self, mock_notify):
text = self.text
# Initial text.
text.insert('insert', 'foo')
self.assertEqual(text.get('1.0', 'end'), 'foo\n')
mock_notify.assert_called_with('1.0', '1.0+3c')
# Additional text.
text.insert('insert', 'barbaz')
self.assertEqual(text.get('1.0', 'end'), 'foobarbaz\n')
mock_notify.assert_called_with('1.3', '1.3+6c')
@mock.patch.object(colorizer.ColorDelegator, 'notify_range')
def test_delete(self, mock_notify):
text = self.text
# Initialize text.
text.insert('insert', 'abcdefghi')
self.assertEqual(text.get('1.0', 'end'), 'abcdefghi\n')
# Delete single character.
text.delete('1.7')
self.assertEqual(text.get('1.0', 'end'), 'abcdefgi\n')
mock_notify.assert_called_with('1.7')
# Delete multiple characters.
text.delete('1.3', '1.6')
self.assertEqual(text.get('1.0', 'end'), 'abcgi\n')
mock_notify.assert_called_with('1.3')
def test_notify_range(self):
text = self.text
color = self.color
eq = self.assertEqual
# Colorizing already scheduled.
save_id = color.after_id
eq(self.root.tk.call('after', 'info', save_id)[1], 'timer')
self.assertFalse(color.colorizing)
self.assertFalse(color.stop_colorizing)
self.assertTrue(color.allow_colorizing)
# Coloring scheduled and colorizing in progress.
color.colorizing = True
color.notify_range('1.0', 'end')
self.assertFalse(color.stop_colorizing)
eq(color.after_id, save_id)
# No colorizing scheduled and colorizing in progress.
text.after_cancel(save_id)
color.after_id = None
color.notify_range('1.0', '1.0+3c')
self.assertTrue(color.stop_colorizing)
self.assertIsNotNone(color.after_id)
eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
# New event scheduled.
self.assertNotEqual(color.after_id, save_id)
# No colorizing scheduled and colorizing off.
text.after_cancel(color.after_id)
color.after_id = None
color.allow_colorizing = False
color.notify_range('1.4', '1.4+10c')
# Nothing scheduled when colorizing is off.
self.assertIsNone(color.after_id)
def test_toggle_colorize_event(self):
color = self.color
eq = self.assertEqual
# Starts with colorizing allowed and scheduled.
self.assertFalse(color.colorizing)
self.assertFalse(color.stop_colorizing)
self.assertTrue(color.allow_colorizing)
eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
# Toggle colorizing off.
color.toggle_colorize_event()
self.assertIsNone(color.after_id)
self.assertFalse(color.colorizing)
self.assertFalse(color.stop_colorizing)
self.assertFalse(color.allow_colorizing)
# Toggle on while colorizing in progress (doesn't add timer).
color.colorizing = True
color.toggle_colorize_event()
self.assertIsNone(color.after_id)
self.assertTrue(color.colorizing)
self.assertFalse(color.stop_colorizing)
self.assertTrue(color.allow_colorizing)
# Toggle off while colorizing in progress.
color.toggle_colorize_event()
self.assertIsNone(color.after_id)
self.assertTrue(color.colorizing)
self.assertTrue(color.stop_colorizing)
self.assertFalse(color.allow_colorizing)
# Toggle on while colorizing not in progress.
color.colorizing = False
color.toggle_colorize_event()
eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
self.assertFalse(color.colorizing)
self.assertTrue(color.stop_colorizing)
self.assertTrue(color.allow_colorizing)
@mock.patch.object(colorizer.ColorDelegator, 'recolorize_main')
def test_recolorize(self, mock_recmain):
text = self.text
color = self.color
eq = self.assertEqual
# Call recolorize manually and not scheduled.
text.after_cancel(color.after_id)
# No delegate.
save_delegate = color.delegate
color.delegate = None
color.recolorize()
mock_recmain.assert_not_called()
color.delegate = save_delegate
# Toggle off colorizing.
color.allow_colorizing = False
color.recolorize()
mock_recmain.assert_not_called()
color.allow_colorizing = True
# Colorizing in progress.
color.colorizing = True
color.recolorize()
mock_recmain.assert_not_called()
color.colorizing = False
# Colorizing is done, but not completed, so rescheduled.
color.recolorize()
self.assertFalse(color.stop_colorizing)
self.assertFalse(color.colorizing)
mock_recmain.assert_called()
eq(mock_recmain.call_count, 1)
# Rescheduled when TODO tag still exists.
eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
# No changes to text, so no scheduling added.
text.tag_remove('TODO', '1.0', 'end')
color.recolorize()
self.assertFalse(color.stop_colorizing)
self.assertFalse(color.colorizing)
mock_recmain.assert_called()
eq(mock_recmain.call_count, 2)
self.assertIsNone(color.after_id)
@mock.patch.object(colorizer.ColorDelegator, 'notify_range')
def test_recolorize_main(self, mock_notify):
text = self.text
color = self.color
eq = self.assertEqual
text.insert('insert', source)
expected = (('1.0', ('KEYWORD',)), ('1.2', ()), ('1.3', ('KEYWORD',)),
('1.7', ()), ('1.9', ('BUILTIN',)), ('1.14', ('STRING',)),
('1.19', ('COMMENT',)),
('2.1', ('KEYWORD',)), ('2.18', ()), ('2.25', ('COMMENT',)),
('3.6', ('BUILTIN',)), ('3.12', ('KEYWORD',)), ('3.21', ('COMMENT',)),
('4.0', ('KEYWORD',)), ('4.3', ()), ('4.6', ()),
('5.2', ('STRING',)), ('5.8', ('KEYWORD',)), ('5.10', ('STRING',)),
('6.0', ('KEYWORD',)), ('6.10', ('DEFINITION',)), ('6.11', ()),
('8.0', ('STRING',)), ('8.4', ()), ('8.5', ('STRING',)),
('8.12', ()), ('8.14', ('STRING',)),
('19.0', ('KEYWORD',)),
('20.4', ('KEYWORD',)), ('20.16', ('KEYWORD',)),# ('20.19', ('KEYWORD',)),
#('22.4', ('KEYWORD',)), ('22.10', ('KEYWORD',)), ('22.14', ('KEYWORD',)), ('22.19', ('STRING',)),
#('23.12', ('KEYWORD',)),
('24.8', ('KEYWORD',)),
('25.4', ('KEYWORD',)), ('25.9', ('KEYWORD',)),
('25.11', ('KEYWORD',)), ('25.15', ('STRING',)),
('25.19', ('KEYWORD',)), ('25.22', ()),
('25.24', ('KEYWORD',)), ('25.29', ('BUILTIN',)), ('25.37', ('KEYWORD',)),
('26.4', ('KEYWORD',)), ('26.9', ('KEYWORD',)),# ('26.11', ('KEYWORD',)), ('26.14', (),),
('27.25', ('STRING',)), ('27.38', ('STRING',)),
('29.0', ('STRING',)),
('30.1', ('STRING',)),
# SYNC at the end of every line.
('1.55', ('SYNC',)), ('2.50', ('SYNC',)), ('3.34', ('SYNC',)),
)
# Nothing marked to do therefore no tags in text.
text.tag_remove('TODO', '1.0', 'end')
color.recolorize_main()
for tag in text.tag_names():
with self.subTest(tag=tag):
eq(text.tag_ranges(tag), ())
# Source marked for processing.
text.tag_add('TODO', '1.0', 'end')
# Check some indexes.
color.recolorize_main()
for index, expected_tags in expected:
with self.subTest(index=index):
eq(text.tag_names(index), expected_tags)
# Check for some tags for ranges.
eq(text.tag_nextrange('TODO', '1.0'), ())
eq(text.tag_nextrange('KEYWORD', '1.0'), ('1.0', '1.2'))
eq(text.tag_nextrange('COMMENT', '2.0'), ('2.22', '2.43'))
eq(text.tag_nextrange('SYNC', '2.0'), ('2.43', '3.0'))
eq(text.tag_nextrange('STRING', '2.0'), ('4.17', '4.53'))
eq(text.tag_nextrange('STRING', '8.0'), ('8.0', '8.3'))
eq(text.tag_nextrange('STRING', '8.3'), ('8.5', '8.12'))
eq(text.tag_nextrange('STRING', '8.12'), ('8.14', '8.17'))
eq(text.tag_nextrange('STRING', '8.17'), ('8.19', '8.26'))
eq(text.tag_nextrange('SYNC', '8.0'), ('8.26', '9.0'))
eq(text.tag_nextrange('SYNC', '30.0'), ('30.10', '32.0'))
def _assert_highlighting(self, source, tag_ranges):
"""Check highlighting of a given piece of code.
This inserts just this code into the Text widget. It will then
check that the resulting highlighting tag ranges exactly match
those described in the given `tag_ranges` dict.
Note that the irrelevant tags 'sel', 'TODO' and 'SYNC' are
ignored.
"""
text = self.text
with mock.patch.object(colorizer.ColorDelegator, 'notify_range'):
text.delete('1.0', 'end-1c')
text.insert('insert', source)
text.tag_add('TODO', '1.0', 'end-1c')
self.color.recolorize_main()
# Make a dict with highlighting tag ranges in the Text widget.
text_tag_ranges = {}
for tag in set(text.tag_names()) - {'sel', 'TODO', 'SYNC'}:
indexes = [rng.string for rng in text.tag_ranges(tag)]
for index_pair in zip(indexes[::2], indexes[1::2]):
text_tag_ranges.setdefault(tag, []).append(index_pair)
self.assertEqual(text_tag_ranges, tag_ranges)
with mock.patch.object(colorizer.ColorDelegator, 'notify_range'):
text.delete('1.0', 'end-1c')
def test_def_statement(self):
# empty def
self._assert_highlighting('def', {'KEYWORD': [('1.0', '1.3')]})
# def followed by identifier
self._assert_highlighting('def foo:', {'KEYWORD': [('1.0', '1.3')],
'DEFINITION': [('1.4', '1.7')]})
# def followed by partial identifier
self._assert_highlighting('def fo', {'KEYWORD': [('1.0', '1.3')],
'DEFINITION': [('1.4', '1.6')]})
# def followed by non-keyword
self._assert_highlighting('def ++', {'KEYWORD': [('1.0', '1.3')]})
def test_match_soft_keyword(self):
# empty match
self._assert_highlighting('match', {'KEYWORD': [('1.0', '1.5')]})
# match followed by partial identifier
self._assert_highlighting('match fo', {'KEYWORD': [('1.0', '1.5')]})
# match followed by identifier and colon
self._assert_highlighting('match foo:', {'KEYWORD': [('1.0', '1.5')]})
# match followed by keyword
self._assert_highlighting('match and', {'KEYWORD': [('1.6', '1.9')]})
# match followed by builtin with keyword prefix
self._assert_highlighting('match int:', {'KEYWORD': [('1.0', '1.5')],
'BUILTIN': [('1.6', '1.9')]})
# match followed by non-text operator
self._assert_highlighting('match^', {})
self._assert_highlighting('match @', {})
# match followed by colon
self._assert_highlighting('match :', {})
# match followed by comma
self._assert_highlighting('match\t,', {})
# match followed by a lone underscore
self._assert_highlighting('match _:', {'KEYWORD': [('1.0', '1.5')]})
def test_case_soft_keyword(self):
# empty case
self._assert_highlighting('case', {'KEYWORD': [('1.0', '1.4')]})
# case followed by partial identifier
self._assert_highlighting('case fo', {'KEYWORD': [('1.0', '1.4')]})
# case followed by identifier and colon
self._assert_highlighting('case foo:', {'KEYWORD': [('1.0', '1.4')]})
# case followed by keyword
self._assert_highlighting('case and', {'KEYWORD': [('1.5', '1.8')]})
# case followed by builtin with keyword prefix
self._assert_highlighting('case int:', {'KEYWORD': [('1.0', '1.4')],
'BUILTIN': [('1.5', '1.8')]})
# case followed by non-text operator
self._assert_highlighting('case^', {})
self._assert_highlighting('case @', {})
# case followed by colon
self._assert_highlighting('case :', {})
# case followed by comma
self._assert_highlighting('case\t,', {})
# case followed by a lone underscore
self._assert_highlighting('case _:', {'KEYWORD': [('1.0', '1.4'),
('1.5', '1.6')]})
def test_long_multiline_string(self):
source = textwrap.dedent('''\
"""a
b
c
d
e"""
''')
self._assert_highlighting(source, {'STRING': [('1.0', '5.4')]})
@run_in_tk_mainloop(delay=50)
def test_incremental_editing(self):
text = self.text
eq = self.assertEqual
# Simulate typing 'inte'. During this, the highlighting should
# change from normal to keyword to builtin to normal.
text.insert('insert', 'i')
yield
eq(text.tag_nextrange('BUILTIN', '1.0'), ())
eq(text.tag_nextrange('KEYWORD', '1.0'), ())
text.insert('insert', 'n')
yield
eq(text.tag_nextrange('BUILTIN', '1.0'), ())
eq(text.tag_nextrange('KEYWORD', '1.0'), ('1.0', '1.2'))
text.insert('insert', 't')
yield
eq(text.tag_nextrange('BUILTIN', '1.0'), ('1.0', '1.3'))
eq(text.tag_nextrange('KEYWORD', '1.0'), ())
text.insert('insert', 'e')
yield
eq(text.tag_nextrange('BUILTIN', '1.0'), ())
eq(text.tag_nextrange('KEYWORD', '1.0'), ())
# Simulate deleting three characters from the end of 'inte'.
# During this, the highlighting should change from normal to
# builtin to keyword to normal.
text.delete('insert-1c', 'insert')
yield
eq(text.tag_nextrange('BUILTIN', '1.0'), ('1.0', '1.3'))
eq(text.tag_nextrange('KEYWORD', '1.0'), ())
text.delete('insert-1c', 'insert')
yield
eq(text.tag_nextrange('BUILTIN', '1.0'), ())
eq(text.tag_nextrange('KEYWORD', '1.0'), ('1.0', '1.2'))
text.delete('insert-1c', 'insert')
yield
eq(text.tag_nextrange('BUILTIN', '1.0'), ())
eq(text.tag_nextrange('KEYWORD', '1.0'), ())
@mock.patch.object(colorizer.ColorDelegator, 'recolorize')
@mock.patch.object(colorizer.ColorDelegator, 'notify_range')
def test_removecolors(self, mock_notify, mock_recolorize):
text = self.text
color = self.color
text.insert('insert', source)
color.recolorize_main()
# recolorize_main doesn't add these tags.
text.tag_add("ERROR", "1.0")
text.tag_add("TODO", "1.0")
text.tag_add("hit", "1.0")
for tag in color.tagdefs:
with self.subTest(tag=tag):
self.assertNotEqual(text.tag_ranges(tag), ())
color.removecolors()
for tag in color.tagdefs:
with self.subTest(tag=tag):
self.assertEqual(text.tag_ranges(tag), ())
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,805 @@
"""Test config, coverage 93%.
(100% for IdleConfParser, IdleUserConfParser*, ConfigChanges).
* Exception is OSError clause in Save method.
Much of IdleConf is also exercised by ConfigDialog and test_configdialog.
"""
from idlelib import config
import sys
import os
import tempfile
from test.support import captured_stderr, findfile
import unittest
from unittest import mock
import idlelib
from idlelib.idle_test.mock_idle import Func
# Tests should not depend on fortuitous user configurations.
# They must not affect actual user .cfg files.
# Replace user parsers with empty parsers that cannot be saved
# due to getting '' as the filename when created.
idleConf = config.idleConf
usercfg = idleConf.userCfg
testcfg = {}
usermain = testcfg['main'] = config.IdleUserConfParser('')
userhigh = testcfg['highlight'] = config.IdleUserConfParser('')
userkeys = testcfg['keys'] = config.IdleUserConfParser('')
userextn = testcfg['extensions'] = config.IdleUserConfParser('')
def setUpModule():
idleConf.userCfg = testcfg
idlelib.testing = True
def tearDownModule():
idleConf.userCfg = usercfg
idlelib.testing = False
class IdleConfParserTest(unittest.TestCase):
"""Test that IdleConfParser works"""
config = """
[one]
one = false
two = true
three = 10
[two]
one = a string
two = true
three = false
"""
def test_get(self):
parser = config.IdleConfParser('')
parser.read_string(self.config)
eq = self.assertEqual
# Test with type argument.
self.assertIs(parser.Get('one', 'one', type='bool'), False)
self.assertIs(parser.Get('one', 'two', type='bool'), True)
eq(parser.Get('one', 'three', type='int'), 10)
eq(parser.Get('two', 'one'), 'a string')
self.assertIs(parser.Get('two', 'two', type='bool'), True)
self.assertIs(parser.Get('two', 'three', type='bool'), False)
# Test without type should fallback to string.
eq(parser.Get('two', 'two'), 'true')
eq(parser.Get('two', 'three'), 'false')
# If option not exist, should return None, or default.
self.assertIsNone(parser.Get('not', 'exist'))
eq(parser.Get('not', 'exist', default='DEFAULT'), 'DEFAULT')
def test_get_option_list(self):
parser = config.IdleConfParser('')
parser.read_string(self.config)
get_list = parser.GetOptionList
self.assertCountEqual(get_list('one'), ['one', 'two', 'three'])
self.assertCountEqual(get_list('two'), ['one', 'two', 'three'])
self.assertEqual(get_list('not exist'), [])
def test_load_nothing(self):
parser = config.IdleConfParser('')
parser.Load()
self.assertEqual(parser.sections(), [])
def test_load_file(self):
# Borrow test/configdata/cfgparser.1 from test_configparser.
config_path = findfile('cfgparser.1', subdir='configdata')
parser = config.IdleConfParser(config_path)
parser.Load()
self.assertEqual(parser.Get('Foo Bar', 'foo'), 'newbar')
self.assertEqual(parser.GetOptionList('Foo Bar'), ['foo'])
class IdleUserConfParserTest(unittest.TestCase):
"""Test that IdleUserConfParser works"""
def new_parser(self, path=''):
return config.IdleUserConfParser(path)
def test_set_option(self):
parser = self.new_parser()
parser.add_section('Foo')
# Setting new option in existing section should return True.
self.assertTrue(parser.SetOption('Foo', 'bar', 'true'))
# Setting existing option with same value should return False.
self.assertFalse(parser.SetOption('Foo', 'bar', 'true'))
# Setting exiting option with new value should return True.
self.assertTrue(parser.SetOption('Foo', 'bar', 'false'))
self.assertEqual(parser.Get('Foo', 'bar'), 'false')
# Setting option in new section should create section and return True.
self.assertTrue(parser.SetOption('Bar', 'bar', 'true'))
self.assertCountEqual(parser.sections(), ['Bar', 'Foo'])
self.assertEqual(parser.Get('Bar', 'bar'), 'true')
def test_remove_option(self):
parser = self.new_parser()
parser.AddSection('Foo')
parser.SetOption('Foo', 'bar', 'true')
self.assertTrue(parser.RemoveOption('Foo', 'bar'))
self.assertFalse(parser.RemoveOption('Foo', 'bar'))
self.assertFalse(parser.RemoveOption('Not', 'Exist'))
def test_add_section(self):
parser = self.new_parser()
self.assertEqual(parser.sections(), [])
# Should not add duplicate section.
# Configparser raises DuplicateError, IdleParser not.
parser.AddSection('Foo')
parser.AddSection('Foo')
parser.AddSection('Bar')
self.assertCountEqual(parser.sections(), ['Bar', 'Foo'])
def test_remove_empty_sections(self):
parser = self.new_parser()
parser.AddSection('Foo')
parser.AddSection('Bar')
parser.SetOption('Idle', 'name', 'val')
self.assertCountEqual(parser.sections(), ['Bar', 'Foo', 'Idle'])
parser.RemoveEmptySections()
self.assertEqual(parser.sections(), ['Idle'])
def test_is_empty(self):
parser = self.new_parser()
parser.AddSection('Foo')
parser.AddSection('Bar')
self.assertTrue(parser.IsEmpty())
self.assertEqual(parser.sections(), [])
parser.SetOption('Foo', 'bar', 'false')
parser.AddSection('Bar')
self.assertFalse(parser.IsEmpty())
self.assertCountEqual(parser.sections(), ['Foo'])
def test_save(self):
with tempfile.TemporaryDirectory() as tdir:
path = os.path.join(tdir, 'test.cfg')
parser = self.new_parser(path)
parser.AddSection('Foo')
parser.SetOption('Foo', 'bar', 'true')
# Should save to path when config is not empty.
self.assertFalse(os.path.exists(path))
parser.Save()
self.assertTrue(os.path.exists(path))
# Should remove the file from disk when config is empty.
parser.remove_section('Foo')
parser.Save()
self.assertFalse(os.path.exists(path))
class IdleConfTest(unittest.TestCase):
"""Test for idleConf"""
@classmethod
def setUpClass(cls):
cls.config_string = {}
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)
with open(config_path) as f:
cls.config_string[ctype] = f.read()
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 = config.IdleConf(_utest=True)
for ctype in conf.config_types:
conf.defaultCfg[ctype] = config.IdleConfParser('')
conf.defaultCfg[ctype].read_string(self.config_string[ctype])
conf.userCfg[ctype] = config.IdleUserConfParser('')
conf.userCfg[ctype].read_string(self.config_string[ctype])
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, conf.config_types)
self.assertCountEqual(conf.userCfg, 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 cfg_type, parser in conf.defaultCfg.items():
self.assertEqual(parser.file,
os.path.join(idle_dir, f'config-{cfg_type}.def'))
for cfg_type, parser in conf.userCfg.items():
self.assertEqual(parser.file,
os.path.join(conf.userdir or '#', f'config-{cfg_type}.cfg'))
def test_load_cfg_files(self):
conf = self.new_config(_utest=True)
# Borrow test/configdata/cfgparser.1 from test_configparser.
config_path = findfile('cfgparser.1', subdir='configdata')
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', 'PyShell', 'Indent', 'Theme',
'Keys', 'History', 'HelpFiles'])
self.assertCountEqual(
conf.GetSectionList('user', 'main'),
['General', 'EditorWindow', 'PyShell', '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'})
# 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):
# TODO: finish.
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):
userextn.read_string('''
[ZzDummy]
enable = True
[DISABLE]
enable = False
''')
eq = self.assertEqual
iGE = idleConf.GetExtensions
eq(iGE(shell_only=True), [])
eq(iGE(), ['ZzDummy'])
eq(iGE(editor_only=True), ['ZzDummy'])
eq(iGE(active_only=False), ['ZzDummy', 'DISABLE'])
eq(iGE(active_only=False, editor_only=True), ['ZzDummy', 'DISABLE'])
userextn.remove_section('ZzDummy')
userextn.remove_section('DISABLE')
def test_remove_key_bind_names(self):
conf = self.mock_config()
self.assertCountEqual(
conf.RemoveKeyBindNames(conf.GetSectionList('default', 'extensions')),
['AutoComplete', 'CodeContext', 'FormatParagraph', 'ParenMatch', 'ZzDummy'])
def test_get_extn_name_for_event(self):
userextn.read_string('''
[ZzDummy]
enable = True
''')
eq = self.assertEqual
eq(idleConf.GetExtnNameForEvent('z-in'), 'ZzDummy')
eq(idleConf.GetExtnNameForEvent('z-out'), None)
userextn.remove_section('ZzDummy')
def test_get_extension_keys(self):
userextn.read_string('''
[ZzDummy]
enable = True
''')
self.assertEqual(idleConf.GetExtensionKeys('ZzDummy'),
{'<<z-in>>': ['<Control-Shift-KeyRelease-Insert>']})
userextn.remove_section('ZzDummy')
# need option key test
## key = ['<Option-Key-2>'] if sys.platform == 'darwin' else ['<Alt-Key-2>']
## eq(conf.GetExtensionKeys('ZoomHeight'), {'<<zoom-height>>': key})
def test_get_extension_bindings(self):
userextn.read_string('''
[ZzDummy]
enable = True
''')
eq = self.assertEqual
iGEB = idleConf.GetExtensionBindings
eq(iGEB('NotExists'), {})
expect = {'<<z-in>>': ['<Control-Shift-KeyRelease-Insert>'],
'<<z-out>>': ['<Control-Shift-KeyRelease-Delete>']}
eq(iGEB('ZzDummy'), expect)
userextn.remove_section('ZzDummy')
def test_get_keybinding(self):
conf = self.mock_config()
eq = self.assertEqual
eq(conf.GetKeyBinding('IDLE Modern Unix', '<<copy>>'),
['<Control-Shift-Key-C>', '<Control-Key-Insert>'])
eq(conf.GetKeyBinding('IDLE Classic Unix', '<<copy>>'),
['<Alt-Key-w>', '<Meta-Key-w>'])
eq(conf.GetKeyBinding('IDLE Classic Windows', '<<copy>>'),
['<Control-Key-c>', '<Control-Key-C>'])
eq(conf.GetKeyBinding('IDLE Classic Mac', '<<copy>>'), ['<Command-Key-c>'])
eq(conf.GetKeyBinding('IDLE Classic OSX', '<<copy>>'), ['<Command-Key-c>'])
# Test keybinding not exists
eq(conf.GetKeyBinding('NOT EXISTS', '<<copy>>'), [])
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, since replace <Alt- to <Option-.
# Above depended on config-extensions.def having Alt keys,
# which is no longer true.
# sys.platform = 'darwin'
# self.assertNotEqual(conf.GetCurrentKeySet(), conf.GetKeySet(conf.CurrentKeys()))
# Restore platform
sys.platform = current_platform
def test_get_keyset(self):
conf = self.mock_config()
# Conflict with key set, should be disable to ''
conf.defaultCfg['extensions'].add_section('Foobar')
conf.defaultCfg['extensions'].add_section('Foobar_cfgBindings')
conf.defaultCfg['extensions'].set('Foobar', 'enable', 'True')
conf.defaultCfg['extensions'].set('Foobar_cfgBindings', 'newfoo', '<Key-F3>')
self.assertEqual(conf.GetKeySet('IDLE Modern Unix')['<<newfoo>>'], '')
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'] <= 0 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()['<<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.
colorkeys = config.IdleConf.current_colors_and_keys
Test all patterns written by IDLE and some errors
Item 'default' should really be 'builtin' (versus 'custom).
"""
colorkeys = idleConf.current_colors_and_keys
default_theme = 'IDLE Classic'
default_keys = idleConf.default_keys()
def test_old_builtin_theme(self):
# On initial installation, user main is blank.
self.assertEqual(self.colorkeys('Theme'), self.default_theme)
# For old default, name2 must be blank.
usermain.read_string('''
[Theme]
default = True
''')
# IDLE omits 'name' for default old builtin theme.
self.assertEqual(self.colorkeys('Theme'), self.default_theme)
# IDLE adds 'name' for non-default old builtin theme.
usermain['Theme']['name'] = 'IDLE New'
self.assertEqual(self.colorkeys('Theme'), 'IDLE New')
# Erroneous non-default old builtin reverts to default.
usermain['Theme']['name'] = 'non-existent'
self.assertEqual(self.colorkeys('Theme'), self.default_theme)
usermain.remove_section('Theme')
def test_new_builtin_theme(self):
# IDLE writes name2 for new builtins.
usermain.read_string('''
[Theme]
default = True
name2 = IDLE Dark
''')
self.assertEqual(self.colorkeys('Theme'), 'IDLE Dark')
# Leftover 'name', not removed, is ignored.
usermain['Theme']['name'] = 'IDLE New'
self.assertEqual(self.colorkeys('Theme'), 'IDLE Dark')
# Erroneous non-default new builtin reverts to default.
usermain['Theme']['name2'] = 'non-existent'
self.assertEqual(self.colorkeys('Theme'), self.default_theme)
usermain.remove_section('Theme')
def test_user_override_theme(self):
# Erroneous custom name (no definition) reverts to default.
usermain.read_string('''
[Theme]
default = False
name = Custom Dark
''')
self.assertEqual(self.colorkeys('Theme'), self.default_theme)
# Custom name is valid with matching Section name.
userhigh.read_string('[Custom Dark]\na=b')
self.assertEqual(self.colorkeys('Theme'), 'Custom Dark')
# Name2 is ignored.
usermain['Theme']['name2'] = 'non-existent'
self.assertEqual(self.colorkeys('Theme'), 'Custom Dark')
usermain.remove_section('Theme')
userhigh.remove_section('Custom Dark')
def test_old_builtin_keys(self):
# On initial installation, user main is blank.
self.assertEqual(self.colorkeys('Keys'), self.default_keys)
# For old default, name2 must be blank, name is always used.
usermain.read_string('''
[Keys]
default = True
name = IDLE Classic Unix
''')
self.assertEqual(self.colorkeys('Keys'), 'IDLE Classic Unix')
# Erroneous non-default old builtin reverts to default.
usermain['Keys']['name'] = 'non-existent'
self.assertEqual(self.colorkeys('Keys'), self.default_keys)
usermain.remove_section('Keys')
def test_new_builtin_keys(self):
# IDLE writes name2 for new builtins.
usermain.read_string('''
[Keys]
default = True
name2 = IDLE Modern Unix
''')
self.assertEqual(self.colorkeys('Keys'), 'IDLE Modern Unix')
# Leftover 'name', not removed, is ignored.
usermain['Keys']['name'] = 'IDLE Classic Unix'
self.assertEqual(self.colorkeys('Keys'), 'IDLE Modern Unix')
# Erroneous non-default new builtin reverts to default.
usermain['Keys']['name2'] = 'non-existent'
self.assertEqual(self.colorkeys('Keys'), self.default_keys)
usermain.remove_section('Keys')
def test_user_override_keys(self):
# Erroneous custom name (no definition) reverts to default.
usermain.read_string('''
[Keys]
default = False
name = Custom Keys
''')
self.assertEqual(self.colorkeys('Keys'), self.default_keys)
# Custom name is valid with matching Section name.
userkeys.read_string('[Custom Keys]\na=b')
self.assertEqual(self.colorkeys('Keys'), 'Custom Keys')
# Name2 is ignored.
usermain['Keys']['name2'] = 'non-existent'
self.assertEqual(self.colorkeys('Keys'), 'Custom Keys')
usermain.remove_section('Keys')
userkeys.remove_section('Custom Keys')
class ChangesTest(unittest.TestCase):
empty = {'main':{}, 'highlight':{}, 'keys':{}, 'extensions':{}}
def load(self): # Test_add_option verifies that this works.
changes = self.changes
changes.add_option('main', 'Msec', 'mitem', 'mval')
changes.add_option('highlight', 'Hsec', 'hitem', 'hval')
changes.add_option('keys', 'Ksec', 'kitem', 'kval')
return changes
loaded = {'main': {'Msec': {'mitem': 'mval'}},
'highlight': {'Hsec': {'hitem': 'hval'}},
'keys': {'Ksec': {'kitem':'kval'}},
'extensions': {}}
def setUp(self):
self.changes = config.ConfigChanges()
def test_init(self):
self.assertEqual(self.changes, self.empty)
def test_add_option(self):
changes = self.load()
self.assertEqual(changes, self.loaded)
changes.add_option('main', 'Msec', 'mitem', 'mval')
self.assertEqual(changes, self.loaded)
def test_save_option(self): # Static function does not touch changes.
save_option = self.changes.save_option
self.assertTrue(save_option('main', 'Indent', 'what', '0'))
self.assertFalse(save_option('main', 'Indent', 'what', '0'))
self.assertEqual(usermain['Indent']['what'], '0')
self.assertTrue(save_option('main', 'Indent', 'use-spaces', '0'))
self.assertEqual(usermain['Indent']['use-spaces'], '0')
self.assertTrue(save_option('main', 'Indent', 'use-spaces', '1'))
self.assertFalse(usermain.has_option('Indent', 'use-spaces'))
usermain.remove_section('Indent')
def test_save_added(self):
changes = self.load()
self.assertTrue(changes.save_all())
self.assertEqual(usermain['Msec']['mitem'], 'mval')
self.assertEqual(userhigh['Hsec']['hitem'], 'hval')
self.assertEqual(userkeys['Ksec']['kitem'], 'kval')
changes.add_option('main', 'Msec', 'mitem', 'mval')
self.assertFalse(changes.save_all())
usermain.remove_section('Msec')
userhigh.remove_section('Hsec')
userkeys.remove_section('Ksec')
def test_save_help(self):
# Any change to HelpFiles overwrites entire section.
changes = self.changes
changes.save_option('main', 'HelpFiles', 'IDLE', 'idledoc')
changes.add_option('main', 'HelpFiles', 'ELDI', 'codeldi')
changes.save_all()
self.assertFalse(usermain.has_option('HelpFiles', 'IDLE'))
self.assertTrue(usermain.has_option('HelpFiles', 'ELDI'))
def test_save_default(self): # Cover 2nd and 3rd false branches.
changes = self.changes
changes.add_option('main', 'Indent', 'use-spaces', '1')
# save_option returns False; cfg_type_changed remains False.
# TODO: test that save_all calls usercfg Saves.
def test_delete_section(self):
changes = self.load()
changes.delete_section('main', 'fake') # Test no exception.
self.assertEqual(changes, self.loaded) # Test nothing deleted.
for cfgtype, section in (('main', 'Msec'), ('keys', 'Ksec')):
testcfg[cfgtype].SetOption(section, 'name', 'value')
changes.delete_section(cfgtype, section)
with self.assertRaises(KeyError):
changes[cfgtype][section] # Test section gone from changes
testcfg[cfgtype][section] # and from mock userCfg.
# TODO test for save call.
def test_clear(self):
changes = self.load()
changes.clear()
self.assertEqual(changes, self.empty)
class WarningTest(unittest.TestCase):
def test_warn(self):
Equal = self.assertEqual
config._warned = set()
with captured_stderr() as stderr:
config._warn('warning', 'key')
Equal(config._warned, {('warning','key')})
Equal(stderr.getvalue(), 'warning'+'\n')
with captured_stderr() as stderr:
config._warn('warning', 'key')
Equal(stderr.getvalue(), '')
with captured_stderr() as stderr:
config._warn('warn2', 'yek')
Equal(config._warned, {('warning','key'), ('warn2','yek')})
Equal(stderr.getvalue(), 'warn2'+'\n')
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,356 @@
"""Test config_key, coverage 98%.
Coverage is effectively 100%. Tkinter dialog is mocked, Mac-only line
may be skipped, and dummy function in bind test should not be called.
Not tested: exit with 'self.advanced or self.keys_ok(keys) ...' False.
"""
from idlelib import config_key
from test.support import requires
import unittest
from unittest import mock
from tkinter import Tk, TclError
from idlelib.idle_test.mock_idle import Func
from idlelib.idle_test.mock_tk import Mbox_func
class ValidationTest(unittest.TestCase):
"Test validation methods: ok, keys_ok, bind_ok."
class Validator(config_key.GetKeysFrame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class list_keys_final:
get = Func()
self.list_keys_final = list_keys_final
get_modifiers = Func()
showerror = Mbox_func()
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
keylist = [['<Key-F12>'], ['<Control-Key-x>', '<Control-Key-X>']]
cls.dialog = cls.Validator(cls.root, '<<Test>>', keylist)
@classmethod
def tearDownClass(cls):
del cls.dialog
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def setUp(self):
self.dialog.showerror.message = ''
# A test that needs a particular final key value should set it.
# A test that sets a non-blank modifier list should reset it to [].
def test_ok_empty(self):
self.dialog.key_string.set(' ')
self.dialog.ok()
self.assertEqual(self.dialog.result, '')
self.assertEqual(self.dialog.showerror.message, 'No key specified.')
def test_ok_good(self):
self.dialog.key_string.set('<Key-F11>')
self.dialog.list_keys_final.get.result = 'F11'
self.dialog.ok()
self.assertEqual(self.dialog.result, '<Key-F11>')
self.assertEqual(self.dialog.showerror.message, '')
def test_keys_no_ending(self):
self.assertFalse(self.dialog.keys_ok('<Control-Shift'))
self.assertIn('Missing the final', self.dialog.showerror.message)
def test_keys_no_modifier_bad(self):
self.dialog.list_keys_final.get.result = 'A'
self.assertFalse(self.dialog.keys_ok('<Key-A>'))
self.assertIn('No modifier', self.dialog.showerror.message)
def test_keys_no_modifier_ok(self):
self.dialog.list_keys_final.get.result = 'F11'
self.assertTrue(self.dialog.keys_ok('<Key-F11>'))
self.assertEqual(self.dialog.showerror.message, '')
def test_keys_shift_bad(self):
self.dialog.list_keys_final.get.result = 'a'
self.dialog.get_modifiers.result = ['Shift']
self.assertFalse(self.dialog.keys_ok('<a>'))
self.assertIn('shift modifier', self.dialog.showerror.message)
self.dialog.get_modifiers.result = []
def test_keys_dup(self):
for mods, final, seq in (([], 'F12', '<Key-F12>'),
(['Control'], 'x', '<Control-Key-x>'),
(['Control'], 'X', '<Control-Key-X>')):
with self.subTest(m=mods, f=final, s=seq):
self.dialog.list_keys_final.get.result = final
self.dialog.get_modifiers.result = mods
self.assertFalse(self.dialog.keys_ok(seq))
self.assertIn('already in use', self.dialog.showerror.message)
self.dialog.get_modifiers.result = []
def test_bind_ok(self):
self.assertTrue(self.dialog.bind_ok('<Control-Shift-Key-a>'))
self.assertEqual(self.dialog.showerror.message, '')
def test_bind_not_ok(self):
self.assertFalse(self.dialog.bind_ok('<Control-Shift>'))
self.assertIn('not accepted', self.dialog.showerror.message)
class ToggleLevelTest(unittest.TestCase):
"Test toggle between Basic and Advanced frames."
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.dialog = config_key.GetKeysFrame(cls.root, '<<Test>>', [])
@classmethod
def tearDownClass(cls):
del cls.dialog
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def test_toggle_level(self):
dialog = self.dialog
def stackorder():
"""Get the stack order of the children of the frame.
winfo_children() stores the children in stack order, so
this can be used to check whether a frame is above or
below another one.
"""
for index, child in enumerate(dialog.winfo_children()):
if child._name == 'keyseq_basic':
basic = index
if child._name == 'keyseq_advanced':
advanced = index
return basic, advanced
# New window starts at basic level.
self.assertFalse(dialog.advanced)
self.assertIn('Advanced', dialog.button_level['text'])
basic, advanced = stackorder()
self.assertGreater(basic, advanced)
# Toggle to advanced.
dialog.toggle_level()
self.assertTrue(dialog.advanced)
self.assertIn('Basic', dialog.button_level['text'])
basic, advanced = stackorder()
self.assertGreater(advanced, basic)
# Toggle to basic.
dialog.button_level.invoke()
self.assertFalse(dialog.advanced)
self.assertIn('Advanced', dialog.button_level['text'])
basic, advanced = stackorder()
self.assertGreater(basic, advanced)
class KeySelectionTest(unittest.TestCase):
"Test selecting key on Basic frames."
class Basic(config_key.GetKeysFrame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class list_keys_final:
get = Func()
select_clear = Func()
yview = Func()
self.list_keys_final = list_keys_final
def set_modifiers_for_platform(self):
self.modifiers = ['foo', 'bar', 'BAZ']
self.modifier_label = {'BAZ': 'ZZZ'}
showerror = Mbox_func()
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.dialog = cls.Basic(cls.root, '<<Test>>', [])
@classmethod
def tearDownClass(cls):
del cls.dialog
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def setUp(self):
self.dialog.clear_key_seq()
def test_get_modifiers(self):
dialog = self.dialog
gm = dialog.get_modifiers
eq = self.assertEqual
# Modifiers are set on/off by invoking the checkbutton.
dialog.modifier_checkbuttons['foo'].invoke()
eq(gm(), ['foo'])
dialog.modifier_checkbuttons['BAZ'].invoke()
eq(gm(), ['foo', 'BAZ'])
dialog.modifier_checkbuttons['foo'].invoke()
eq(gm(), ['BAZ'])
@mock.patch.object(config_key.GetKeysFrame, 'get_modifiers')
def test_build_key_string(self, mock_modifiers):
dialog = self.dialog
key = dialog.list_keys_final
string = dialog.key_string.get
eq = self.assertEqual
key.get.result = 'a'
mock_modifiers.return_value = []
dialog.build_key_string()
eq(string(), '<Key-a>')
mock_modifiers.return_value = ['mymod']
dialog.build_key_string()
eq(string(), '<mymod-Key-a>')
key.get.result = ''
mock_modifiers.return_value = ['mymod', 'test']
dialog.build_key_string()
eq(string(), '<mymod-test>')
@mock.patch.object(config_key.GetKeysFrame, 'get_modifiers')
def test_final_key_selected(self, mock_modifiers):
dialog = self.dialog
key = dialog.list_keys_final
string = dialog.key_string.get
eq = self.assertEqual
mock_modifiers.return_value = ['Shift']
key.get.result = '{'
dialog.final_key_selected()
eq(string(), '<Shift-Key-braceleft>')
class CancelWindowTest(unittest.TestCase):
"Simulate user clicking [Cancel] button."
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.dialog = config_key.GetKeysWindow(
cls.root, 'Title', '<<Test>>', [], _utest=True)
@classmethod
def tearDownClass(cls):
cls.dialog.cancel()
del cls.dialog
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
@mock.patch.object(config_key.GetKeysFrame, 'ok')
def test_cancel(self, mock_frame_ok):
self.assertEqual(self.dialog.winfo_class(), 'Toplevel')
self.dialog.button_cancel.invoke()
with self.assertRaises(TclError):
self.dialog.winfo_class()
self.assertEqual(self.dialog.result, '')
mock_frame_ok.assert_not_called()
class OKWindowTest(unittest.TestCase):
"Simulate user clicking [OK] button."
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.dialog = config_key.GetKeysWindow(
cls.root, 'Title', '<<Test>>', [], _utest=True)
@classmethod
def tearDownClass(cls):
cls.dialog.cancel()
del cls.dialog
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
@mock.patch.object(config_key.GetKeysFrame, 'ok')
def test_ok(self, mock_frame_ok):
self.assertEqual(self.dialog.winfo_class(), 'Toplevel')
self.dialog.button_ok.invoke()
with self.assertRaises(TclError):
self.dialog.winfo_class()
mock_frame_ok.assert_called()
class WindowResultTest(unittest.TestCase):
"Test window result get and set."
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.dialog = config_key.GetKeysWindow(
cls.root, 'Title', '<<Test>>', [], _utest=True)
@classmethod
def tearDownClass(cls):
cls.dialog.cancel()
del cls.dialog
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def test_result(self):
dialog = self.dialog
eq = self.assertEqual
dialog.result = ''
eq(dialog.result, '')
eq(dialog.frame.result,'')
dialog.result = 'bar'
eq(dialog.result,'bar')
eq(dialog.frame.result,'bar')
dialog.frame.result = 'foo'
eq(dialog.result, 'foo')
eq(dialog.frame.result,'foo')
class HelperTest(unittest.TestCase):
"Test module level helper functions."
def test_translate_key(self):
tr = config_key.translate_key
eq = self.assertEqual
# Letters return unchanged with no 'Shift'.
eq(tr('q', []), 'Key-q')
eq(tr('q', ['Control', 'Alt']), 'Key-q')
# 'Shift' uppercases single lowercase letters.
eq(tr('q', ['Shift']), 'Key-Q')
eq(tr('q', ['Control', 'Shift']), 'Key-Q')
eq(tr('q', ['Control', 'Alt', 'Shift']), 'Key-Q')
# Convert key name to keysym.
eq(tr('Page Up', []), 'Key-Prior')
# 'Shift' doesn't change case when it's not a single char.
eq(tr('*', ['Shift']), 'Key-asterisk')
if __name__ == '__main__':
unittest.main(verbosity=2)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,298 @@
"""Test debugger, coverage 66%
Try to make tests pass with draft bdbx, which may replace bdb in 3.13+.
"""
from idlelib import debugger
from collections import namedtuple
from textwrap import dedent
from tkinter import Tk
from test.support import requires
from test.support.testcase import ExtraAssertions
import unittest
from unittest import mock
from unittest.mock import Mock, patch
"""A test python script for the debug tests."""
TEST_CODE = dedent("""
i = 1
i += 2
if i == 3:
print(i)
""")
class MockFrame:
"Minimal mock frame."
def __init__(self, code, lineno):
self.f_code = code
self.f_lineno = lineno
class IdbTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.gui = Mock()
cls.idb = debugger.Idb(cls.gui)
# Create test and code objects to simulate a debug session.
code_obj = compile(TEST_CODE, 'idlelib/file.py', mode='exec')
frame1 = MockFrame(code_obj, 1)
frame1.f_back = None
frame2 = MockFrame(code_obj, 2)
frame2.f_back = frame1
cls.frame = frame2
cls.msg = 'file.py:2: <module>()'
def test_init(self):
self.assertIs(self.idb.gui, self.gui)
# Won't test super call since two Bdbs are very different.
def test_user_line(self):
# Test that .user_line() creates a string message for a frame.
self.gui.interaction = Mock()
self.idb.user_line(self.frame)
self.gui.interaction.assert_called_once_with(self.msg, self.frame)
def test_user_exception(self):
# Test that .user_exception() creates a string message for a frame.
exc_info = (type(ValueError), ValueError(), None)
self.gui.interaction = Mock()
self.idb.user_exception(self.frame, exc_info)
self.gui.interaction.assert_called_once_with(
self.msg, self.frame, exc_info)
class FunctionTest(unittest.TestCase):
# Test module functions together.
def test_functions(self):
rpc_obj = compile(TEST_CODE,'rpc.py', mode='exec')
rpc_frame = MockFrame(rpc_obj, 2)
rpc_frame.f_back = rpc_frame
self.assertTrue(debugger._in_rpc_code(rpc_frame))
self.assertEqual(debugger._frame2message(rpc_frame),
'rpc.py:2: <module>()')
code_obj = compile(TEST_CODE, 'idlelib/debugger.py', mode='exec')
code_frame = MockFrame(code_obj, 1)
code_frame.f_back = None
self.assertFalse(debugger._in_rpc_code(code_frame))
self.assertEqual(debugger._frame2message(code_frame),
'debugger.py:1: <module>()')
code_frame.f_back = code_frame
self.assertFalse(debugger._in_rpc_code(code_frame))
code_frame.f_back = rpc_frame
self.assertTrue(debugger._in_rpc_code(code_frame))
class DebuggerTest(unittest.TestCase):
"Tests for Debugger that do not need a real root."
@classmethod
def setUpClass(cls):
cls.pyshell = Mock()
cls.pyshell.root = Mock()
cls.idb = Mock()
with patch.object(debugger.Debugger, 'make_gui'):
cls.debugger = debugger.Debugger(cls.pyshell, cls.idb)
cls.debugger.root = Mock()
def test_cont(self):
self.debugger.cont()
self.idb.set_continue.assert_called_once()
def test_step(self):
self.debugger.step()
self.idb.set_step.assert_called_once()
def test_quit(self):
self.debugger.quit()
self.idb.set_quit.assert_called_once()
def test_next(self):
with patch.object(self.debugger, 'frame') as frame:
self.debugger.next()
self.idb.set_next.assert_called_once_with(frame)
def test_ret(self):
with patch.object(self.debugger, 'frame') as frame:
self.debugger.ret()
self.idb.set_return.assert_called_once_with(frame)
def test_clear_breakpoint(self):
self.debugger.clear_breakpoint('test.py', 4)
self.idb.clear_break.assert_called_once_with('test.py', 4)
def test_clear_file_breaks(self):
self.debugger.clear_file_breaks('test.py')
self.idb.clear_all_file_breaks.assert_called_once_with('test.py')
def test_set_load_breakpoints(self):
# Test the .load_breakpoints() method calls idb.
FileIO = namedtuple('FileIO', 'filename')
class MockEditWindow(object):
def __init__(self, fn, breakpoints):
self.io = FileIO(fn)
self.breakpoints = breakpoints
self.pyshell.flist = Mock()
self.pyshell.flist.inversedict = (
MockEditWindow('test1.py', [4, 4]),
MockEditWindow('test2.py', [13, 44, 45]),
)
self.debugger.set_breakpoint('test0.py', 1)
self.idb.set_break.assert_called_once_with('test0.py', 1)
self.debugger.load_breakpoints() # Call set_breakpoint 5 times.
self.idb.set_break.assert_has_calls(
[mock.call('test0.py', 1),
mock.call('test1.py', 4),
mock.call('test1.py', 4),
mock.call('test2.py', 13),
mock.call('test2.py', 44),
mock.call('test2.py', 45)])
def test_sync_source_line(self):
# Test that .sync_source_line() will set the flist.gotofileline with fixed frame.
test_code = compile(TEST_CODE, 'test_sync.py', 'exec')
test_frame = MockFrame(test_code, 1)
self.debugger.frame = test_frame
self.debugger.flist = Mock()
with patch('idlelib.debugger.os.path.exists', return_value=True):
self.debugger.sync_source_line()
self.debugger.flist.gotofileline.assert_called_once_with('test_sync.py', 1)
class DebuggerGuiTest(unittest.TestCase):
"""Tests for debugger.Debugger that need tk root.
close needs debugger.top set in make_gui.
"""
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = root = Tk()
root.withdraw()
cls.pyshell = Mock()
cls.pyshell.root = root
cls.idb = Mock()
# stack tests fail with debugger here.
## cls.debugger = debugger.Debugger(cls.pyshell, cls.idb)
## cls.debugger.root = root
## # real root needed for real make_gui
## # run, interacting, abort_loop
@classmethod
def tearDownClass(cls):
cls.root.destroy()
del cls.root
def setUp(self):
self.debugger = debugger.Debugger(self.pyshell, self.idb)
self.debugger.root = self.root
# real root needed for real make_gui
# run, interacting, abort_loop
def test_run_debugger(self):
self.debugger.run(1, 'two')
self.idb.run.assert_called_once_with(1, 'two')
self.assertEqual(self.debugger.interacting, 0)
def test_close(self):
# Test closing the window in an idle state.
self.debugger.close()
self.pyshell.close_debugger.assert_called_once()
def test_show_stack(self):
self.debugger.show_stack()
self.assertEqual(self.debugger.stackviewer.gui, self.debugger)
def test_show_stack_with_frame(self):
test_frame = MockFrame(None, None)
self.debugger.frame = test_frame
# Reset the stackviewer to force it to be recreated.
self.debugger.stackviewer = None
self.idb.get_stack.return_value = ([], 0)
self.debugger.show_stack()
# Check that the newly created stackviewer has the test gui as a field.
self.assertEqual(self.debugger.stackviewer.gui, self.debugger)
self.idb.get_stack.assert_called_once_with(test_frame, None)
class StackViewerTest(unittest.TestCase, ExtraAssertions):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
@classmethod
def tearDownClass(cls):
cls.root.destroy()
del cls.root
def setUp(self):
self.code = compile(TEST_CODE, 'test_stackviewer.py', 'exec')
self.stack = [
(MockFrame(self.code, 1), 1),
(MockFrame(self.code, 2), 2)
]
# Create a stackviewer and load the test stack.
self.sv = debugger.StackViewer(self.root, None, None)
self.sv.load_stack(self.stack)
def test_init(self):
# Test creation of StackViewer.
gui = None
flist = None
master_window = self.root
sv = debugger.StackViewer(master_window, flist, gui)
self.assertHasAttr(sv, 'stack')
def test_load_stack(self):
# Test the .load_stack() method against a fixed test stack.
# Check the test stack is assigned and the list contains the repr of them.
self.assertEqual(self.sv.stack, self.stack)
self.assertTrue('?.<module>(), line 1:' in self.sv.get(0))
self.assertEqual(self.sv.get(1), '?.<module>(), line 2: ')
def test_show_source(self):
# Test the .show_source() method against a fixed test stack.
# Patch out the file list to monitor it
self.sv.flist = Mock()
# Patch out isfile to pretend file exists.
with patch('idlelib.debugger.os.path.isfile', return_value=True) as isfile:
self.sv.show_source(1)
isfile.assert_called_once_with('test_stackviewer.py')
self.sv.flist.open.assert_called_once_with('test_stackviewer.py')
class NameSpaceTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
@classmethod
def tearDownClass(cls):
cls.root.destroy()
del cls.root
def test_init(self):
debugger.NamespaceViewer(self.root, 'Test')
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,36 @@
"Test debugger_r, coverage 30%."
from idlelib import debugger_r
import unittest
# Boilerplate likely to be needed for future test classes.
##from test.support import requires
##from tkinter import Tk
##class Test(unittest.TestCase):
## @classmethod
## def setUpClass(cls):
## requires('gui')
## cls.root = Tk()
## @classmethod
## def tearDownClass(cls):
## cls.root.destroy()
# GUIProxy, IdbAdapter, FrameProxy, CodeProxy, DictProxy,
# GUIAdapter, IdbProxy, and 7 functions still need tests.
class IdbAdapterTest(unittest.TestCase):
def test_dict_item_noattr(self): # Issue 33065.
class BinData:
def __repr__(self):
return self.length
debugger_r.dicttable[0] = {'BinData': BinData()}
idb = debugger_r.IdbAdapter(None)
self.assertTrue(idb.dict_item(0, 'BinData'))
debugger_r.dicttable.clear()
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,57 @@
"Test debugobj, coverage 40%."
from idlelib import debugobj
import unittest
class ObjectTreeItemTest(unittest.TestCase):
def test_init(self):
ti = debugobj.ObjectTreeItem('label', 22)
self.assertEqual(ti.labeltext, 'label')
self.assertEqual(ti.object, 22)
self.assertEqual(ti.setfunction, None)
class ClassTreeItemTest(unittest.TestCase):
def test_isexpandable(self):
ti = debugobj.ClassTreeItem('label', 0)
self.assertTrue(ti.IsExpandable())
class AtomicObjectTreeItemTest(unittest.TestCase):
def test_isexpandable(self):
ti = debugobj.AtomicObjectTreeItem('label', 0)
self.assertFalse(ti.IsExpandable())
class SequenceTreeItemTest(unittest.TestCase):
def test_isexpandable(self):
ti = debugobj.SequenceTreeItem('label', ())
self.assertFalse(ti.IsExpandable())
ti = debugobj.SequenceTreeItem('label', (1,))
self.assertTrue(ti.IsExpandable())
def test_keys(self):
ti = debugobj.SequenceTreeItem('label', 'abc')
self.assertEqual(list(ti.keys()), [0, 1, 2]) # keys() is a range.
class DictTreeItemTest(unittest.TestCase):
def test_isexpandable(self):
ti = debugobj.DictTreeItem('label', {})
self.assertFalse(ti.IsExpandable())
ti = debugobj.DictTreeItem('label', {1:1})
self.assertTrue(ti.IsExpandable())
def test_keys(self):
ti = debugobj.DictTreeItem('label', {1:1, 0:0, 2:2})
self.assertEqual(ti.keys(), [0, 1, 2]) # keys() is a sorted list.
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,22 @@
"Test debugobj_r, coverage 56%."
from idlelib import debugobj_r
import unittest
class WrappedObjectTreeItemTest(unittest.TestCase):
def test_getattr(self):
ti = debugobj_r.WrappedObjectTreeItem(list)
self.assertEqual(ti.append, list.append)
class StubObjectTreeItemTest(unittest.TestCase):
def test_init(self):
ti = debugobj_r.StubObjectTreeItem('socket', 1111)
self.assertEqual(ti.sockio, 'socket')
self.assertEqual(ti.oid, 1111)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,44 @@
"Test delegator, coverage 100%."
from idlelib.delegator import Delegator
import unittest
class DelegatorTest(unittest.TestCase):
def test_mydel(self):
# Test a simple use scenario.
# Initialize an int delegator.
mydel = Delegator(int)
self.assertIs(mydel.delegate, int)
self.assertEqual(mydel._Delegator__cache, set())
# 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 attribute 'numerator'.
mydel.numerator
self.assertEqual(mydel._Delegator__cache, {'bit_length', 'numerator'})
# Delete 'numerator'.
del mydel.numerator
self.assertNotIn('numerator', mydel.__dict__)
# 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
# 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)

View File

@ -0,0 +1,74 @@
'''Test (selected) IDLE Edit menu items.
Edit modules have their own test files
'''
from test.support import requires
requires('gui')
import tkinter as tk
from tkinter import ttk
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()
cls.root.withdraw()
pyshell.fix_x11_paste(root)
cls.text = tk.Text(root)
cls.entry = tk.Entry(root)
cls.tentry = ttk.Entry(root)
cls.spin = tk.Spinbox(root)
root.clipboard_clear()
root.clipboard_append('two')
@classmethod
def tearDownClass(cls):
del cls.text, cls.entry, cls.tentry
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."
# Generated <<Paste>> fails for tk entry without empty select
# range for 'no selection'. Live widget works fine.
for entry in self.entry, self.tentry:
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)
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)

View File

@ -0,0 +1,241 @@
"Test editor, coverage 53%."
from idlelib import editor
import unittest
from collections import namedtuple
from test.support import requires
from tkinter import Tk, Text
Editor = editor.EditorWindow
class EditorWindowTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
cls.root.after_cancel(id)
cls.root.destroy()
del cls.root
def test_init(self):
e = Editor(root=self.root)
self.assertEqual(e.root, self.root)
e._close()
class GetLineIndentTest(unittest.TestCase):
def test_empty_lines(self):
for tabwidth in [1, 2, 4, 6, 8]:
for line in ['', '\n']:
with self.subTest(line=line, tabwidth=tabwidth):
self.assertEqual(
editor.get_line_indent(line, tabwidth=tabwidth),
(0, 0),
)
def test_tabwidth_4(self):
# (line, (raw, effective))
tests = (('no spaces', (0, 0)),
# Internal space isn't counted.
(' space test', (4, 4)),
('\ttab test', (1, 4)),
('\t\tdouble tabs test', (2, 8)),
# Different results when mixing tabs and spaces.
(' \tmixed test', (5, 8)),
(' \t mixed test', (5, 6)),
('\t mixed test', (5, 8)),
# Spaces not divisible by tabwidth.
(' \tmixed test', (3, 4)),
(' \t mixed test', (3, 5)),
('\t mixed test', (3, 6)),
# Only checks spaces and tabs.
('\nnewline test', (0, 0)))
for line, expected in tests:
with self.subTest(line=line):
self.assertEqual(
editor.get_line_indent(line, tabwidth=4),
expected,
)
def test_tabwidth_8(self):
# (line, (raw, effective))
tests = (('no spaces', (0, 0)),
# Internal space isn't counted.
(' space test', (8, 8)),
('\ttab test', (1, 8)),
('\t\tdouble tabs test', (2, 16)),
# Different results when mixing tabs and spaces.
(' \tmixed test', (9, 16)),
(' \t mixed test', (9, 10)),
('\t mixed test', (9, 16)),
# Spaces not divisible by tabwidth.
(' \tmixed test', (3, 8)),
(' \t mixed test', (3, 9)),
('\t mixed test', (3, 10)),
# Only checks spaces and tabs.
('\nnewline test', (0, 0)))
for line, expected in tests:
with self.subTest(line=line):
self.assertEqual(
editor.get_line_indent(line, tabwidth=8),
expected,
)
def insert(text, string):
text.delete('1.0', 'end')
text.insert('end', string)
text.update_idletasks() # Force update for colorizer to finish.
class IndentAndNewlineTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.window = Editor(root=cls.root)
cls.window.indentwidth = 2
cls.window.tabwidth = 2
@classmethod
def tearDownClass(cls):
cls.window._close()
del cls.window
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
cls.root.after_cancel(id)
cls.root.destroy()
del cls.root
def test_indent_and_newline_event(self):
eq = self.assertEqual
w = self.window
text = w.text
get = text.get
nl = w.newline_and_indent_event
TestInfo = namedtuple('Tests', ['label', 'text', 'expected', 'mark'])
tests = (TestInfo('Empty line inserts with no indent.',
' \n def __init__(self):',
'\n \n def __init__(self):\n',
'1.end'),
TestInfo('Inside bracket before space, deletes space.',
' def f1(self, a, b):',
' def f1(self,\n a, b):\n',
'1.14'),
TestInfo('Inside bracket after space, deletes space.',
' def f1(self, a, b):',
' def f1(self,\n a, b):\n',
'1.15'),
TestInfo('Inside string with one line - no indent.',
' """Docstring."""',
' """Docstring.\n"""\n',
'1.15'),
TestInfo('Inside string with more than one line.',
' """Docstring.\n Docstring Line 2"""',
' """Docstring.\n Docstring Line 2\n """\n',
'2.18'),
TestInfo('Backslash with one line.',
'a =\\',
'a =\\\n \n',
'1.end'),
TestInfo('Backslash with more than one line.',
'a =\\\n multiline\\',
'a =\\\n multiline\\\n \n',
'2.end'),
TestInfo('Block opener - indents +1 level.',
' def f1(self):\n pass',
' def f1(self):\n \n pass\n',
'1.end'),
TestInfo('Block closer - dedents -1 level.',
' def f1(self):\n pass',
' def f1(self):\n pass\n \n',
'2.end'),
)
for test in tests:
with self.subTest(label=test.label):
insert(text, test.text)
text.mark_set('insert', test.mark)
nl(event=None)
eq(get('1.0', 'end'), test.expected)
# Selected text.
insert(text, ' def f1(self, a, b):\n return a + b')
text.tag_add('sel', '1.17', '1.end')
nl(None)
# Deletes selected text before adding new line.
eq(get('1.0', 'end'), ' def f1(self, a,\n \n return a + b\n')
class IndentSearcherTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.text = Text(cls.root)
@classmethod
def tearDownClass(cls):
cls.root.destroy()
del cls.root
def test_searcher(self):
text = self.text
searcher = (self.text)
test_info = (# text, (block, indent))
("", (None, None)),
("[1,", (None, None)), # TokenError
("if 1:\n", ('if 1:\n', None)),
("if 1:\n 2\n 3\n", ('if 1:\n', ' 2\n')),
)
for code, expected_pair in test_info:
with self.subTest(code=code):
insert(text, code)
actual_pair = editor.IndentSearcher(text).run()
self.assertEqual(actual_pair, expected_pair)
class RMenuTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.window = Editor(root=cls.root)
@classmethod
def tearDownClass(cls):
cls.window._close()
del cls.window
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
cls.root.after_cancel(id)
cls.root.destroy()
del cls.root
class DummyRMenu:
def tk_popup(x, y): pass
def test_rclick(self):
pass
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,33 @@
"Test filelist, coverage 19%."
from idlelib import filelist
import unittest
from test.support import requires
from tkinter import Tk
class FileListTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
cls.root.after_cancel(id)
cls.root.destroy()
del cls.root
def test_new_empty(self):
flist = filelist.FileList(self.root)
self.assertEqual(flist.root, self.root)
e = flist.new()
self.assertEqual(type(e), flist.EditorWindow)
e._close()
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,668 @@
"Test format, coverage 99%."
from idlelib import format as ft
import unittest
from unittest import mock
from test.support import requires
from tkinter import Tk, Text
from idlelib.editor import EditorWindow
from idlelib.idle_test.mock_idle import Editor as MockEditor
class Is_Get_Test(unittest.TestCase):
"""Test the is_ and get_ functions"""
test_comment = '# This is a comment'
test_nocomment = 'This is not a comment'
trailingws_comment = '# This is a comment '
leadingws_comment = ' # This is a comment'
leadingws_nocomment = ' This is not a comment'
def test_is_all_white(self):
self.assertTrue(ft.is_all_white(''))
self.assertTrue(ft.is_all_white('\t\n\r\f\v'))
self.assertFalse(ft.is_all_white(self.test_comment))
def test_get_indent(self):
Equal = self.assertEqual
Equal(ft.get_indent(self.test_comment), '')
Equal(ft.get_indent(self.trailingws_comment), '')
Equal(ft.get_indent(self.leadingws_comment), ' ')
Equal(ft.get_indent(self.leadingws_nocomment), ' ')
def test_get_comment_header(self):
Equal = self.assertEqual
# Test comment strings
Equal(ft.get_comment_header(self.test_comment), '#')
Equal(ft.get_comment_header(self.trailingws_comment), '#')
Equal(ft.get_comment_header(self.leadingws_comment), ' #')
# Test non-comment strings
Equal(ft.get_comment_header(self.leadingws_nocomment), ' ')
Equal(ft.get_comment_header(self.test_nocomment), '')
class FindTest(unittest.TestCase):
"""Test the find_paragraph function in paragraph module.
Using the runcase() function, find_paragraph() is called with 'mark' set at
multiple indexes before and inside the test paragraph.
It appears that code with the same indentation as a quoted string is grouped
as part of the same paragraph, which is probably incorrect behavior.
"""
@classmethod
def setUpClass(cls):
from idlelib.idle_test.mock_tk import Text
cls.text = Text()
def runcase(self, inserttext, stopline, expected):
# Check that find_paragraph returns the expected paragraph when
# the mark index is set to beginning, middle, end of each line
# up to but not including the stop line
text = self.text
text.insert('1.0', inserttext)
for line in range(1, stopline):
linelength = int(text.index("%d.end" % line).split('.')[1])
for col in (0, linelength//2, linelength):
tempindex = "%d.%d" % (line, col)
self.assertEqual(ft.find_paragraph(text, tempindex), expected)
text.delete('1.0', 'end')
def test_find_comment(self):
comment = (
"# Comment block with no blank lines before\n"
"# Comment line\n"
"\n")
self.runcase(comment, 3, ('1.0', '3.0', '#', comment[0:58]))
comment = (
"\n"
"# Comment block with whitespace line before and after\n"
"# Comment line\n"
"\n")
self.runcase(comment, 4, ('2.0', '4.0', '#', comment[1:70]))
comment = (
"\n"
" # Indented comment block with whitespace before and after\n"
" # Comment line\n"
"\n")
self.runcase(comment, 4, ('2.0', '4.0', ' #', comment[1:82]))
comment = (
"\n"
"# Single line comment\n"
"\n")
self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:23]))
comment = (
"\n"
" # Single line comment with leading whitespace\n"
"\n")
self.runcase(comment, 3, ('2.0', '3.0', ' #', comment[1:51]))
comment = (
"\n"
"# Comment immediately followed by code\n"
"x = 42\n"
"\n")
self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:40]))
comment = (
"\n"
" # Indented comment immediately followed by code\n"
"x = 42\n"
"\n")
self.runcase(comment, 3, ('2.0', '3.0', ' #', comment[1:53]))
comment = (
"\n"
"# Comment immediately followed by indented code\n"
" x = 42\n"
"\n")
self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:49]))
def test_find_paragraph(self):
teststring = (
'"""String with no blank lines before\n'
'String line\n'
'"""\n'
'\n')
self.runcase(teststring, 4, ('1.0', '4.0', '', teststring[0:53]))
teststring = (
"\n"
'"""String with whitespace line before and after\n'
'String line.\n'
'"""\n'
'\n')
self.runcase(teststring, 5, ('2.0', '5.0', '', teststring[1:66]))
teststring = (
'\n'
' """Indented string with whitespace before and after\n'
' Comment string.\n'
' """\n'
'\n')
self.runcase(teststring, 5, ('2.0', '5.0', ' ', teststring[1:85]))
teststring = (
'\n'
'"""Single line string."""\n'
'\n')
self.runcase(teststring, 3, ('2.0', '3.0', '', teststring[1:27]))
teststring = (
'\n'
' """Single line string with leading whitespace."""\n'
'\n')
self.runcase(teststring, 3, ('2.0', '3.0', ' ', teststring[1:55]))
class ReformatFunctionTest(unittest.TestCase):
"""Test the reformat_paragraph function without the editor window."""
def test_reformat_paragraph(self):
Equal = self.assertEqual
reform = ft.reformat_paragraph
hw = "O hello world"
Equal(reform(' ', 1), ' ')
Equal(reform("Hello world", 20), "Hello world")
# Test without leading newline
Equal(reform(hw, 1), "O\nhello\nworld")
Equal(reform(hw, 6), "O\nhello\nworld")
Equal(reform(hw, 7), "O hello\nworld")
Equal(reform(hw, 12), "O hello\nworld")
Equal(reform(hw, 13), "O hello world")
# Test with leading newline
hw = "\nO hello world"
Equal(reform(hw, 1), "\nO\nhello\nworld")
Equal(reform(hw, 6), "\nO\nhello\nworld")
Equal(reform(hw, 7), "\nO hello\nworld")
Equal(reform(hw, 12), "\nO hello\nworld")
Equal(reform(hw, 13), "\nO hello world")
class ReformatCommentTest(unittest.TestCase):
"""Test the reformat_comment function without the editor window."""
def test_reformat_comment(self):
Equal = self.assertEqual
# reformat_comment formats to a minimum of 20 characters
test_string = (
" \"\"\"this is a test of a reformat for a triple quoted string"
" will it reformat to less than 70 characters for me?\"\"\"")
result = ft.reformat_comment(test_string, 70, " ")
expected = (
" \"\"\"this is a test of a reformat for a triple quoted string will it\n"
" reformat to less than 70 characters for me?\"\"\"")
Equal(result, expected)
test_comment = (
"# this is a test of a reformat for a triple quoted string will "
"it reformat to less than 70 characters for me?")
result = ft.reformat_comment(test_comment, 70, "#")
expected = (
"# this is a test of a reformat for a triple quoted string will it\n"
"# reformat to less than 70 characters for me?")
Equal(result, expected)
class FormatClassTest(unittest.TestCase):
def test_init_close(self):
instance = ft.FormatParagraph('editor')
self.assertEqual(instance.editwin, 'editor')
instance.close()
self.assertEqual(instance.editwin, None)
# For testing format_paragraph_event, Initialize FormatParagraph with
# a mock Editor with .text and .get_selection_indices. The text must
# be a Text wrapper that adds two methods
# A real EditorWindow creates unneeded, time-consuming baggage and
# sometimes emits shutdown warnings like this:
# "warning: callback failed in WindowList <class '_tkinter.TclError'>
# : invalid command name ".55131368.windows".
# Calling EditorWindow._close in tearDownClass prevents this but causes
# other problems (windows left open).
class TextWrapper:
def __init__(self, master):
self.text = Text(master=master)
def __getattr__(self, name):
return getattr(self.text, name)
def undo_block_start(self): pass
def undo_block_stop(self): pass
class Editor:
def __init__(self, root):
self.text = TextWrapper(root)
get_selection_indices = EditorWindow. get_selection_indices
class FormatEventTest(unittest.TestCase):
"""Test the formatting of text inside a Text widget.
This is done with FormatParagraph.format.paragraph_event,
which calls functions in the module as appropriate.
"""
test_string = (
" '''this is a test of a reformat for a triple "
"quoted string will it reformat to less than 70 "
"characters for me?'''\n")
multiline_test_string = (
" '''The first line is under the max width.\n"
" The second line's length is way over the max width. It goes "
"on and on until it is over 100 characters long.\n"
" Same thing with the third line. It is also way over the max "
"width, but FormatParagraph will fix it.\n"
" '''\n")
multiline_test_comment = (
"# The first line is under the max width.\n"
"# The second line's length is way over the max width. It goes on "
"and on until it is over 100 characters long.\n"
"# Same thing with the third line. It is also way over the max "
"width, but FormatParagraph will fix it.\n"
"# The fourth line is short like the first line.")
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
editor = Editor(root=cls.root)
cls.text = editor.text.text # Test code does not need the wrapper.
cls.formatter = ft.FormatParagraph(editor).format_paragraph_event
# Sets the insert mark just after the re-wrapped and inserted text.
@classmethod
def tearDownClass(cls):
del cls.text, cls.formatter
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def test_short_line(self):
self.text.insert('1.0', "Short line\n")
self.formatter("Dummy")
self.assertEqual(self.text.get('1.0', 'insert'), "Short line\n" )
self.text.delete('1.0', 'end')
def test_long_line(self):
text = self.text
# Set cursor ('insert' mark) to '1.0', within text.
text.insert('1.0', self.test_string)
text.mark_set('insert', '1.0')
self.formatter('ParameterDoesNothing', limit=70)
result = text.get('1.0', 'insert')
# find function includes \n
expected = (
" '''this is a test of a reformat for a triple quoted string will it\n"
" reformat to less than 70 characters for me?'''\n") # yes
self.assertEqual(result, expected)
text.delete('1.0', 'end')
# Select from 1.11 to line end.
text.insert('1.0', self.test_string)
text.tag_add('sel', '1.11', '1.end')
self.formatter('ParameterDoesNothing', limit=70)
result = text.get('1.0', 'insert')
# selection excludes \n
expected = (
" '''this is a test of a reformat for a triple quoted string will it reformat\n"
" to less than 70 characters for me?'''") # no
self.assertEqual(result, expected)
text.delete('1.0', 'end')
def test_multiple_lines(self):
text = self.text
# Select 2 long lines.
text.insert('1.0', self.multiline_test_string)
text.tag_add('sel', '2.0', '4.0')
self.formatter('ParameterDoesNothing', limit=70)
result = text.get('2.0', 'insert')
expected = (
" The second line's length is way over the max width. It goes on and\n"
" on until it is over 100 characters long. Same thing with the third\n"
" line. It is also way over the max width, but FormatParagraph will\n"
" fix it.\n")
self.assertEqual(result, expected)
text.delete('1.0', 'end')
def test_comment_block(self):
text = self.text
# Set cursor ('insert') to '1.0', within block.
text.insert('1.0', self.multiline_test_comment)
self.formatter('ParameterDoesNothing', limit=70)
result = text.get('1.0', 'insert')
expected = (
"# The first line is under the max width. The second line's length is\n"
"# way over the max width. It goes on and on until it is over 100\n"
"# characters long. Same thing with the third line. It is also way over\n"
"# the max width, but FormatParagraph will fix it. The fourth line is\n"
"# short like the first line.\n")
self.assertEqual(result, expected)
text.delete('1.0', 'end')
# Select line 2, verify line 1 unaffected.
text.insert('1.0', self.multiline_test_comment)
text.tag_add('sel', '2.0', '3.0')
self.formatter('ParameterDoesNothing', limit=70)
result = text.get('1.0', 'insert')
expected = (
"# The first line is under the max width.\n"
"# The second line's length is way over the max width. It goes on and\n"
"# on until it is over 100 characters long.\n")
self.assertEqual(result, expected)
text.delete('1.0', 'end')
# The following block worked with EditorWindow but fails with the mock.
# Lines 2 and 3 get pasted together even though the previous block left
# the previous line alone. More investigation is needed.
## # Select lines 3 and 4
## text.insert('1.0', self.multiline_test_comment)
## text.tag_add('sel', '3.0', '5.0')
## self.formatter('ParameterDoesNothing')
## result = text.get('3.0', 'insert')
## expected = (
##"# Same thing with the third line. It is also way over the max width,\n"
##"# but FormatParagraph will fix it. The fourth line is short like the\n"
##"# first line.\n")
## self.assertEqual(result, expected)
## text.delete('1.0', 'end')
class DummyEditwin:
def __init__(self, root, text):
self.root = root
self.text = text
self.indentwidth = 4
self.tabwidth = 4
self.usetabs = False
self.context_use_ps1 = True
_make_blanks = EditorWindow._make_blanks
get_selection_indices = EditorWindow.get_selection_indices
class FormatRegionTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.text = Text(cls.root)
cls.text.undo_block_start = mock.Mock()
cls.text.undo_block_stop = mock.Mock()
cls.editor = DummyEditwin(cls.root, cls.text)
cls.formatter = ft.FormatRegion(cls.editor)
@classmethod
def tearDownClass(cls):
del cls.text, cls.formatter, cls.editor
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def setUp(self):
self.text.insert('1.0', self.code_sample)
def tearDown(self):
self.text.delete('1.0', 'end')
code_sample = """\
# WS line needed for test.
class C1:
# Class comment.
def __init__(self, a, b):
self.a = a
self.b = b
def compare(self):
if a > b:
return a
elif a < b:
return b
else:
return None
"""
def test_get_region(self):
get = self.formatter.get_region
text = self.text
eq = self.assertEqual
# Add selection.
text.tag_add('sel', '7.0', '10.0')
expected_lines = ['',
' def compare(self):',
' if a > b:',
'']
eq(get(), ('7.0', '10.0', '\n'.join(expected_lines), expected_lines))
# Remove selection.
text.tag_remove('sel', '1.0', 'end')
eq(get(), ('15.0', '16.0', '\n', ['', '']))
def test_set_region(self):
set_ = self.formatter.set_region
text = self.text
eq = self.assertEqual
save_bell = text.bell
text.bell = mock.Mock()
line6 = self.code_sample.splitlines()[5]
line10 = self.code_sample.splitlines()[9]
text.tag_add('sel', '6.0', '11.0')
head, tail, chars, lines = self.formatter.get_region()
# No changes.
set_(head, tail, chars, lines)
text.bell.assert_called_once()
eq(text.get('6.0', '11.0'), chars)
eq(text.get('sel.first', 'sel.last'), chars)
text.tag_remove('sel', '1.0', 'end')
# Alter selected lines by changing lines and adding a newline.
newstring = 'added line 1\n\n\n\n'
newlines = newstring.split('\n')
set_('7.0', '10.0', chars, newlines)
# Selection changed.
eq(text.get('sel.first', 'sel.last'), newstring)
# Additional line added, so last index is changed.
eq(text.get('7.0', '11.0'), newstring)
# Before and after lines unchanged.
eq(text.get('6.0', '7.0-1c'), line6)
eq(text.get('11.0', '12.0-1c'), line10)
text.tag_remove('sel', '1.0', 'end')
text.bell = save_bell
def test_indent_region_event(self):
indent = self.formatter.indent_region_event
text = self.text
eq = self.assertEqual
text.tag_add('sel', '7.0', '10.0')
indent()
# Blank lines aren't affected by indent.
eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n'))
def test_dedent_region_event(self):
dedent = self.formatter.dedent_region_event
text = self.text
eq = self.assertEqual
text.tag_add('sel', '7.0', '10.0')
dedent()
# Blank lines aren't affected by dedent.
eq(text.get('7.0', '10.0'), ('\ndef compare(self):\n if a > b:\n'))
def test_comment_region_event(self):
comment = self.formatter.comment_region_event
text = self.text
eq = self.assertEqual
text.tag_add('sel', '7.0', '10.0')
comment()
eq(text.get('7.0', '10.0'), ('##\n## def compare(self):\n## if a > b:\n'))
def test_uncomment_region_event(self):
comment = self.formatter.comment_region_event
uncomment = self.formatter.uncomment_region_event
text = self.text
eq = self.assertEqual
text.tag_add('sel', '7.0', '10.0')
comment()
uncomment()
eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n'))
# Only remove comments at the beginning of a line.
text.tag_remove('sel', '1.0', 'end')
text.tag_add('sel', '3.0', '4.0')
uncomment()
eq(text.get('3.0', '3.end'), (' # Class comment.'))
self.formatter.set_region('3.0', '4.0', '', ['# Class comment.', ''])
uncomment()
eq(text.get('3.0', '3.end'), (' Class comment.'))
@mock.patch.object(ft.FormatRegion, "_asktabwidth")
def test_tabify_region_event(self, _asktabwidth):
tabify = self.formatter.tabify_region_event
text = self.text
eq = self.assertEqual
text.tag_add('sel', '7.0', '10.0')
# No tabwidth selected.
_asktabwidth.return_value = None
self.assertIsNone(tabify())
_asktabwidth.return_value = 3
self.assertIsNotNone(tabify())
eq(text.get('7.0', '10.0'), ('\n\t def compare(self):\n\t\t if a > b:\n'))
@mock.patch.object(ft.FormatRegion, "_asktabwidth")
def test_untabify_region_event(self, _asktabwidth):
untabify = self.formatter.untabify_region_event
text = self.text
eq = self.assertEqual
text.tag_add('sel', '7.0', '10.0')
# No tabwidth selected.
_asktabwidth.return_value = None
self.assertIsNone(untabify())
_asktabwidth.return_value = 2
self.formatter.tabify_region_event()
_asktabwidth.return_value = 3
self.assertIsNotNone(untabify())
eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n'))
@mock.patch.object(ft, "askinteger")
def test_ask_tabwidth(self, askinteger):
ask = self.formatter._asktabwidth
askinteger.return_value = 10
self.assertEqual(ask(), 10)
class IndentsTest(unittest.TestCase):
@mock.patch.object(ft, "askyesno")
def test_toggle_tabs(self, askyesno):
editor = DummyEditwin(None, None) # usetabs == False.
indents = ft.Indents(editor)
askyesno.return_value = True
indents.toggle_tabs_event(None)
self.assertEqual(editor.usetabs, True)
self.assertEqual(editor.indentwidth, 8)
indents.toggle_tabs_event(None)
self.assertEqual(editor.usetabs, False)
self.assertEqual(editor.indentwidth, 8)
@mock.patch.object(ft, "askinteger")
def test_change_indentwidth(self, askinteger):
editor = DummyEditwin(None, None) # indentwidth == 4.
indents = ft.Indents(editor)
askinteger.return_value = None
indents.change_indentwidth_event(None)
self.assertEqual(editor.indentwidth, 4)
askinteger.return_value = 3
indents.change_indentwidth_event(None)
self.assertEqual(editor.indentwidth, 3)
askinteger.return_value = 5
editor.usetabs = True
indents.change_indentwidth_event(None)
self.assertEqual(editor.indentwidth, 3)
class RstripTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.text = Text(cls.root)
cls.editor = MockEditor(text=cls.text)
cls.do_rstrip = ft.Rstrip(cls.editor).do_rstrip
@classmethod
def tearDownClass(cls):
del cls.text, cls.do_rstrip, cls.editor
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def tearDown(self):
self.text.delete('1.0', 'end-1c')
def test_rstrip_lines(self):
original = (
"Line with an ending tab \n"
"Line ending in 5 spaces \n"
"Linewithnospaces\n"
" indented line\n"
" indented line with trailing space \n"
" \n")
stripped = (
"Line with an ending tab\n"
"Line ending in 5 spaces\n"
"Linewithnospaces\n"
" indented line\n"
" indented line with trailing space\n")
self.text.insert('1.0', original)
self.do_rstrip()
self.assertEqual(self.text.get('1.0', 'insert'), stripped)
def test_rstrip_end(self):
text = self.text
for code in ('', '\n', '\n\n\n'):
with self.subTest(code=code):
text.insert('1.0', code)
self.do_rstrip()
self.assertEqual(text.get('1.0','end-1c'), '')
for code in ('a\n', 'a\n\n', 'a\n\n\n'):
with self.subTest(code=code):
text.delete('1.0', 'end-1c')
text.insert('1.0', code)
self.do_rstrip()
self.assertEqual(text.get('1.0','end-1c'), 'a\n')
if __name__ == '__main__':
unittest.main(verbosity=2, exit=2)

View File

@ -0,0 +1,157 @@
""" !Changing this line will break Test_findfile.test_found!
Non-gui unit tests for grep.GrepDialog methods.
dummy_command calls grep_it calls findfiles.
An exception raised in one method will fail callers.
Otherwise, tests are mostly independent.
Currently only test grep_it, coverage 51%.
"""
from idlelib import grep
import unittest
from test.support import captured_stdout
from test.support.testcase import ExtraAssertions
from idlelib.idle_test.mock_tk import Var
import os
import re
class Dummy_searchengine:
'''GrepDialog.__init__ calls parent SearchDiabolBase which attaches the
passed in SearchEngine instance as attribute 'engine'. Only a few of the
many possible self.engine.x attributes are needed here.
'''
def getpat(self):
return self._pat
searchengine = Dummy_searchengine()
class Dummy_grep:
# Methods tested
#default_command = GrepDialog.default_command
grep_it = grep.GrepDialog.grep_it
# Other stuff needed
recvar = Var(False)
engine = searchengine
def close(self): # gui method
pass
_grep = Dummy_grep()
class FindfilesTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.realpath = os.path.realpath(__file__)
cls.path = os.path.dirname(cls.realpath)
@classmethod
def tearDownClass(cls):
del cls.realpath, cls.path
def test_invaliddir(self):
with captured_stdout() as s:
filelist = list(grep.findfiles('invaliddir', '*.*', False))
self.assertEqual(filelist, [])
self.assertIn('invalid', s.getvalue())
def test_curdir(self):
# Test os.curdir.
ff = grep.findfiles
save_cwd = os.getcwd()
os.chdir(self.path)
filename = 'test_grep.py'
filelist = list(ff(os.curdir, filename, False))
self.assertIn(os.path.join(os.curdir, filename), filelist)
os.chdir(save_cwd)
def test_base(self):
ff = grep.findfiles
readme = os.path.join(self.path, 'README.txt')
# Check for Python files in path where this file lives.
filelist = list(ff(self.path, '*.py', False))
# This directory has many Python files.
self.assertGreater(len(filelist), 10)
self.assertIn(self.realpath, filelist)
self.assertNotIn(readme, filelist)
# Look for .txt files in path where this file lives.
filelist = list(ff(self.path, '*.txt', False))
self.assertNotEqual(len(filelist), 0)
self.assertNotIn(self.realpath, filelist)
self.assertIn(readme, filelist)
# Look for non-matching pattern.
filelist = list(ff(self.path, 'grep.*', False))
self.assertEqual(len(filelist), 0)
self.assertNotIn(self.realpath, filelist)
def test_recurse(self):
ff = grep.findfiles
parent = os.path.dirname(self.path)
grepfile = os.path.join(parent, 'grep.py')
pat = '*.py'
# Get Python files only in parent directory.
filelist = list(ff(parent, pat, False))
parent_size = len(filelist)
# Lots of Python files in idlelib.
self.assertGreater(parent_size, 20)
self.assertIn(grepfile, filelist)
# Without subdirectories, this file isn't returned.
self.assertNotIn(self.realpath, filelist)
# Include subdirectories.
filelist = list(ff(parent, pat, True))
# More files found now.
self.assertGreater(len(filelist), parent_size)
self.assertIn(grepfile, filelist)
# This file exists in list now.
self.assertIn(self.realpath, filelist)
# Check another level up the tree.
parent = os.path.dirname(parent)
filelist = list(ff(parent, '*.py', True))
self.assertIn(self.realpath, filelist)
class Grep_itTest(unittest.TestCase, ExtraAssertions):
# Test captured reports with 0 and some hits.
# Should test file names, but Windows reports have mixed / and \ separators
# from incomplete replacement, so 'later'.
def report(self, pat):
_grep.engine._pat = pat
with captured_stdout() as s:
_grep.grep_it(re.compile(pat), __file__)
lines = s.getvalue().split('\n')
lines.pop() # remove bogus '' after last \n
return lines
def test_unfound(self):
pat = 'xyz*'*7
lines = self.report(pat)
self.assertEqual(len(lines), 2)
self.assertIn(pat, lines[0])
self.assertEqual(lines[1], 'No hits.')
def test_found(self):
pat = '""" !Changing this line will break Test_findfile.test_found!'
lines = self.report(pat)
self.assertEqual(len(lines), 5)
self.assertIn(pat, lines[0])
self.assertIn('py: 1:', lines[1]) # line number 1
self.assertIn('2', lines[3]) # hits found 2
self.assertStartsWith(lines[4], '(Hint:')
class Default_commandTest(unittest.TestCase):
# To write this, move outwin import to top of GrepDialog
# so it can be replaced by captured_stdout in class setup/teardown.
pass
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,36 @@
"Test help, coverage 94%."
from idlelib import help
import unittest
from test.support import requires
requires('gui')
from os.path import abspath, dirname, join
from tkinter import Tk
class IdleDocTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
"By itself, this tests that file parsed without exception."
cls.root = root = Tk()
root.withdraw()
cls.window = help.show_idlehelp(root)
@classmethod
def tearDownClass(cls):
del cls.window
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def test_1window(self):
self.assertIn('IDLE Doc', self.window.wm_title())
def test_4text(self):
text = self.window.frame.text
self.assertEqual(text.get('1.0', '1.end'), ' IDLE — Python editor and shell ')
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,182 @@
"""Test help_about, coverage 100%.
help_about.build_bits branches on sys.platform='darwin'.
'100% combines coverage on Mac and others.
"""
from idlelib import help_about
import unittest
from test.support import requires, findfile
from tkinter import Tk, TclError
from idlelib.idle_test.mock_idle import Func
from idlelib.idle_test.mock_tk import Mbox_func
from idlelib import textview
import os.path
from platform import python_version
About = help_about.AboutDialog
class LiveDialogTest(unittest.TestCase):
"""Simulate user clicking buttons other than [Close].
Test that invoked textview has text from source.
"""
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.dialog = About(cls.root, 'About IDLE', _utest=True)
@classmethod
def tearDownClass(cls):
del cls.dialog
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def test_build_bits(self):
self.assertIn(help_about.bits, ('32', '64'))
def test_dialog_title(self):
"""Test about dialog title"""
self.assertEqual(self.dialog.title(), 'About IDLE')
def test_dialog_logo(self):
"""Test about dialog logo."""
path, file = os.path.split(self.dialog.icon_image['file'])
fn, ext = os.path.splitext(file)
self.assertEqual(fn, 'idle_48')
def test_printer_buttons(self):
"""Test buttons whose commands use printer function."""
dialog = self.dialog
button_sources = [(dialog.py_license, license, 'license'),
(dialog.py_copyright, copyright, 'copyright'),
(dialog.py_credits, credits, 'credits')]
for button, printer, name in button_sources:
with self.subTest(name=name):
printer._Printer__setup()
button.invoke()
get = dialog._current_textview.viewframe.textframe.text.get
lines = printer._Printer__lines
if len(lines) < 2:
self.fail(name + ' full text was not found')
self.assertEqual(lines[0], get('1.0', '1.end'))
self.assertEqual(lines[1], get('2.0', '2.end'))
dialog._current_textview.destroy()
def test_file_buttons(self):
"""Test buttons that display files."""
dialog = self.dialog
button_sources = [(self.dialog.readme, 'README.txt', 'readme'),
(self.dialog.idle_news, 'News3.txt', 'news'),
(self.dialog.idle_credits, 'CREDITS.txt', 'credits')]
for button, filename, name in button_sources:
with self.subTest(name=name):
button.invoke()
fn = findfile(filename, subdir='idlelib')
get = dialog._current_textview.viewframe.textframe.text.get
with open(fn, encoding='utf-8') as f:
self.assertEqual(f.readline().strip(), get('1.0', '1.end'))
f.readline()
self.assertEqual(f.readline().strip(), get('3.0', '3.end'))
dialog._current_textview.destroy()
class DefaultTitleTest(unittest.TestCase):
"Test default title."
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.dialog = About(cls.root, _utest=True)
@classmethod
def tearDownClass(cls):
del cls.dialog
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def test_dialog_title(self):
"""Test about dialog title"""
self.assertEqual(self.dialog.title(),
f'About IDLE {python_version()}'
f' ({help_about.bits} bit)')
class CloseTest(unittest.TestCase):
"""Simulate user clicking [Close] button"""
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.dialog = About(cls.root, 'About IDLE', _utest=True)
@classmethod
def tearDownClass(cls):
del cls.dialog
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def test_close(self):
self.assertEqual(self.dialog.winfo_class(), 'Toplevel')
self.dialog.button_ok.invoke()
with self.assertRaises(TclError):
self.dialog.winfo_class()
class Dummy_about_dialog:
# Dummy class for testing file display functions.
idle_credits = About.show_idle_credits
idle_readme = About.show_readme
idle_news = About.show_idle_news
# Called by the above
display_file_text = About.display_file_text
_utest = True
class DisplayFileTest(unittest.TestCase):
"""Test functions that display files.
While somewhat redundant with gui-based test_file_dialog,
these unit tests run on all buildbots, not just a few.
"""
dialog = Dummy_about_dialog()
@classmethod
def setUpClass(cls):
cls.orig_error = textview.showerror
cls.orig_view = textview.view_text
cls.error = Mbox_func()
cls.view = Func()
textview.showerror = cls.error
textview.view_text = cls.view
@classmethod
def tearDownClass(cls):
textview.showerror = cls.orig_error
textview.view_text = cls.orig_view
def test_file_display(self):
for handler in (self.dialog.idle_credits,
self.dialog.idle_readme,
self.dialog.idle_news):
self.error.message = ''
self.view.called = False
with self.subTest(handler=handler):
handler()
self.assertEqual(self.error.message, '')
self.assertEqual(self.view.called, True)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,172 @@
" Test history, coverage 100%."
from idlelib.history import History
import unittest
from test.support import requires
import tkinter as tk
from tkinter import Text as tkText
from idlelib.idle_test.mock_tk import Text as mkText
from idlelib.config import idleConf
line1 = 'a = 7'
line2 = 'b = a'
class StoreTest(unittest.TestCase):
'''Tests History.__init__ and History.store with mock Text'''
@classmethod
def setUpClass(cls):
cls.text = mkText()
cls.history = History(cls.text)
def tearDown(self):
self.text.delete('1.0', 'end')
self.history.history = []
def test_init(self):
self.assertIs(self.history.text, self.text)
self.assertEqual(self.history.history, [])
self.assertIsNone(self.history.prefix)
self.assertIsNone(self.history.pointer)
self.assertEqual(self.history.cyclic,
idleConf.GetOption("main", "History", "cyclic", 1, "bool"))
def test_store_short(self):
self.history.store('a')
self.assertEqual(self.history.history, [])
self.history.store(' a ')
self.assertEqual(self.history.history, [])
def test_store_dup(self):
self.history.store(line1)
self.assertEqual(self.history.history, [line1])
self.history.store(line2)
self.assertEqual(self.history.history, [line1, line2])
self.history.store(line1)
self.assertEqual(self.history.history, [line2, line1])
def test_store_reset(self):
self.history.prefix = line1
self.history.pointer = 0
self.history.store(line2)
self.assertIsNone(self.history.prefix)
self.assertIsNone(self.history.pointer)
class TextWrapper:
def __init__(self, master):
self.text = tkText(master=master)
self._bell = False
def __getattr__(self, name):
return getattr(self.text, name)
def bell(self):
self._bell = True
class FetchTest(unittest.TestCase):
'''Test History.fetch with wrapped tk.Text.
'''
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = tk.Tk()
cls.root.withdraw()
def setUp(self):
self.text = text = TextWrapper(self.root)
text.insert('1.0', ">>> ")
text.mark_set('iomark', '1.4')
text.mark_gravity('iomark', 'left')
self.history = History(text)
self.history.history = [line1, line2]
@classmethod
def tearDownClass(cls):
cls.root.destroy()
del cls.root
def fetch_test(self, reverse, line, prefix, index, *, bell=False):
# Perform one fetch as invoked by Alt-N or Alt-P
# Test the result. The line test is the most important.
# The last two are diagnostic of fetch internals.
History = self.history
History.fetch(reverse)
Equal = self.assertEqual
Equal(self.text.get('iomark', 'end-1c'), line)
Equal(self.text._bell, bell)
if bell:
self.text._bell = False
Equal(History.prefix, prefix)
Equal(History.pointer, index)
Equal(self.text.compare("insert", '==', "end-1c"), 1)
def test_fetch_prev_cyclic(self):
prefix = ''
test = self.fetch_test
test(True, line2, prefix, 1)
test(True, line1, prefix, 0)
test(True, prefix, None, None, bell=True)
def test_fetch_next_cyclic(self):
prefix = ''
test = self.fetch_test
test(False, line1, prefix, 0)
test(False, line2, prefix, 1)
test(False, prefix, None, None, bell=True)
# Prefix 'a' tests skip line2, which starts with 'b'
def test_fetch_prev_prefix(self):
prefix = 'a'
self.text.insert('iomark', prefix)
self.fetch_test(True, line1, prefix, 0)
self.fetch_test(True, prefix, None, None, bell=True)
def test_fetch_next_prefix(self):
prefix = 'a'
self.text.insert('iomark', prefix)
self.fetch_test(False, line1, prefix, 0)
self.fetch_test(False, prefix, None, None, bell=True)
def test_fetch_prev_noncyclic(self):
prefix = ''
self.history.cyclic = False
test = self.fetch_test
test(True, line2, prefix, 1)
test(True, line1, prefix, 0)
test(True, line1, prefix, 0, bell=True)
def test_fetch_next_noncyclic(self):
prefix = ''
self.history.cyclic = False
test = self.fetch_test
test(False, prefix, None, None, bell=True)
test(True, line2, prefix, 1)
test(False, prefix, None, None, bell=True)
test(False, prefix, None, None, bell=True)
def test_fetch_cursor_move(self):
# Move cursor after fetch
self.history.fetch(reverse=True) # initialization
self.text.mark_set('insert', 'iomark')
self.fetch_test(True, line2, None, None, bell=True)
def test_fetch_edit(self):
# Edit after fetch
self.history.fetch(reverse=True) # initialization
self.text.delete('iomark', 'insert', )
self.text.insert('iomark', 'a =')
self.fetch_test(True, line1, 'a =', 0) # prefix is reset
def test_history_prev_next(self):
# Minimally test functions bound to events
self.history.history_prev('dummy event')
self.assertEqual(self.history.pointer, 1)
self.history.history_next('dummy event')
self.assertEqual(self.history.pointer, None)
if __name__ == '__main__':
unittest.main(verbosity=2, exit=2)

View File

@ -0,0 +1,276 @@
"Test hyperparser, coverage 98%."
from idlelib.hyperparser import HyperParser
import unittest
from test.support import requires
from tkinter import Tk, Text
from idlelib.editor import EditorWindow
class DummyEditwin:
def __init__(self, text):
self.text = text
self.indentwidth = 8
self.tabwidth = 8
self.prompt_last_line = '>>>'
self.num_context_lines = 50, 500, 1000
_build_char_in_string_func = EditorWindow._build_char_in_string_func
is_char_in_string = EditorWindow.is_char_in_string
class HyperParserTest(unittest.TestCase):
code = (
'"""This is a module docstring"""\n'
'# this line is a comment\n'
'x = "this is a string"\n'
"y = 'this is also a string'\n"
'l = [i for i in range(10)]\n'
'm = [py*py for # comment\n'
' py in l]\n'
'x.__len__\n'
"z = ((r'asdf')+('a')))\n"
'[x for x in\n'
'for = False\n'
'cliché = "this is a string with unicode, what a cliché"'
)
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.text = Text(cls.root)
cls.editwin = DummyEditwin(cls.text)
@classmethod
def tearDownClass(cls):
del cls.text, cls.editwin
cls.root.destroy()
del cls.root
def setUp(self):
self.text.insert('insert', self.code)
def tearDown(self):
self.text.delete('1.0', 'end')
self.editwin.prompt_last_line = '>>>'
def get_parser(self, index):
"""
Return a parser object with index at 'index'
"""
return HyperParser(self.editwin, index)
def test_init(self):
"""
test corner cases in the init method
"""
with self.assertRaises(ValueError) as ve:
self.text.tag_add('console', '1.0', '1.end')
p = self.get_parser('1.5')
self.assertIn('precedes', str(ve.exception))
# test without ps1
self.editwin.prompt_last_line = ''
# number of lines lesser than 50
p = self.get_parser('end')
self.assertEqual(p.rawtext, self.text.get('1.0', 'end'))
# number of lines greater than 50
self.text.insert('end', self.text.get('1.0', 'end')*4)
p = self.get_parser('54.5')
def test_is_in_string(self):
get = self.get_parser
p = get('1.0')
self.assertFalse(p.is_in_string())
p = get('1.4')
self.assertTrue(p.is_in_string())
p = get('2.3')
self.assertFalse(p.is_in_string())
p = get('3.3')
self.assertFalse(p.is_in_string())
p = get('3.7')
self.assertTrue(p.is_in_string())
p = get('4.6')
self.assertTrue(p.is_in_string())
p = get('12.54')
self.assertTrue(p.is_in_string())
def test_is_in_code(self):
get = self.get_parser
p = get('1.0')
self.assertTrue(p.is_in_code())
p = get('1.1')
self.assertFalse(p.is_in_code())
p = get('2.5')
self.assertFalse(p.is_in_code())
p = get('3.4')
self.assertTrue(p.is_in_code())
p = get('3.6')
self.assertFalse(p.is_in_code())
p = get('4.14')
self.assertFalse(p.is_in_code())
def test_get_surrounding_bracket(self):
get = self.get_parser
def without_mustclose(parser):
# a utility function to get surrounding bracket
# with mustclose=False
return parser.get_surrounding_brackets(mustclose=False)
def with_mustclose(parser):
# a utility function to get surrounding bracket
# with mustclose=True
return parser.get_surrounding_brackets(mustclose=True)
p = get('3.2')
self.assertIsNone(with_mustclose(p))
self.assertIsNone(without_mustclose(p))
p = get('5.6')
self.assertTupleEqual(without_mustclose(p), ('5.4', '5.25'))
self.assertTupleEqual(without_mustclose(p), with_mustclose(p))
p = get('5.23')
self.assertTupleEqual(without_mustclose(p), ('5.21', '5.24'))
self.assertTupleEqual(without_mustclose(p), with_mustclose(p))
p = get('6.15')
self.assertTupleEqual(without_mustclose(p), ('6.4', '6.end'))
self.assertIsNone(with_mustclose(p))
p = get('9.end')
self.assertIsNone(with_mustclose(p))
self.assertIsNone(without_mustclose(p))
def test_get_expression(self):
get = self.get_parser
p = get('4.2')
self.assertEqual(p.get_expression(), 'y ')
p = get('4.7')
with self.assertRaises(ValueError) as ve:
p.get_expression()
self.assertIn('is inside a code', str(ve.exception))
p = get('5.25')
self.assertEqual(p.get_expression(), 'range(10)')
p = get('6.7')
self.assertEqual(p.get_expression(), 'py')
p = get('6.8')
self.assertEqual(p.get_expression(), '')
p = get('7.9')
self.assertEqual(p.get_expression(), 'py')
p = get('8.end')
self.assertEqual(p.get_expression(), 'x.__len__')
p = get('9.13')
self.assertEqual(p.get_expression(), "r'asdf'")
p = get('9.17')
with self.assertRaises(ValueError) as ve:
p.get_expression()
self.assertIn('is inside a code', str(ve.exception))
p = get('10.0')
self.assertEqual(p.get_expression(), '')
p = get('10.6')
self.assertEqual(p.get_expression(), '')
p = get('10.11')
self.assertEqual(p.get_expression(), '')
p = get('11.3')
self.assertEqual(p.get_expression(), '')
p = get('11.11')
self.assertEqual(p.get_expression(), 'False')
p = get('12.6')
self.assertEqual(p.get_expression(), 'cliché')
def test_eat_identifier(self):
def is_valid_id(candidate):
result = HyperParser._eat_identifier(candidate, 0, len(candidate))
if result == len(candidate):
return True
elif result == 0:
return False
else:
err_msg = "Unexpected result: {} (expected 0 or {}".format(
result, len(candidate)
)
raise Exception(err_msg)
# invalid first character which is valid elsewhere in an identifier
self.assertFalse(is_valid_id('2notid'))
# ASCII-only valid identifiers
self.assertTrue(is_valid_id('valid_id'))
self.assertTrue(is_valid_id('_valid_id'))
self.assertTrue(is_valid_id('valid_id_'))
self.assertTrue(is_valid_id('_2valid_id'))
# keywords which should be "eaten"
self.assertTrue(is_valid_id('True'))
self.assertTrue(is_valid_id('False'))
self.assertTrue(is_valid_id('None'))
# keywords which should not be "eaten"
self.assertFalse(is_valid_id('for'))
self.assertFalse(is_valid_id('import'))
self.assertFalse(is_valid_id('return'))
# valid unicode identifiers
self.assertTrue(is_valid_id('cliche'))
self.assertTrue(is_valid_id('cliché'))
self.assertTrue(is_valid_id(''))
# invalid unicode identifiers
self.assertFalse(is_valid_id('2a'))
self.assertFalse(is_valid_id('٢a'))
self.assertFalse(is_valid_id(''))
# valid identifier after "punctuation"
self.assertEqual(HyperParser._eat_identifier('+ var', 0, 5), len('var'))
self.assertEqual(HyperParser._eat_identifier('+var', 0, 4), len('var'))
self.assertEqual(HyperParser._eat_identifier('.var', 0, 4), len('var'))
# invalid identifiers
self.assertFalse(is_valid_id('+'))
self.assertFalse(is_valid_id(' '))
self.assertFalse(is_valid_id(':'))
self.assertFalse(is_valid_id('?'))
self.assertFalse(is_valid_id('^'))
self.assertFalse(is_valid_id('\\'))
self.assertFalse(is_valid_id('"'))
self.assertFalse(is_valid_id('"a string"'))
def test_eat_identifier_various_lengths(self):
eat_id = HyperParser._eat_identifier
for length in range(1, 21):
self.assertEqual(eat_id('a' * length, 0, length), length)
self.assertEqual(eat_id('é' * length, 0, length), length)
self.assertEqual(eat_id('a' + '2' * (length - 1), 0, length), length)
self.assertEqual(eat_id('é' + '2' * (length - 1), 0, length), length)
self.assertEqual(eat_id('é' + 'a' * (length - 1), 0, length), length)
self.assertEqual(eat_id('é' * (length - 1) + 'a', 0, length), length)
self.assertEqual(eat_id('+' * length, 0, length), 0)
self.assertEqual(eat_id('2' + 'a' * (length - 1), 0, length), 0)
self.assertEqual(eat_id('2' + 'é' * (length - 1), 0, length), 0)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,84 @@
"Test , coverage 17%."
from idlelib import iomenu
import unittest
from test.support import requires
from tkinter import Tk
from idlelib.editor import EditorWindow
from idlelib import util
from idlelib.idle_test.mock_idle import Func
# Fail if either tokenize.open and t.detect_encoding does not exist.
# These are used in loadfile and encode.
# Also used in pyshell.MI.execfile and runscript.tabnanny.
from tokenize import open, detect_encoding
# Remove when we have proper tests that use both.
class IOBindingTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.editwin = EditorWindow(root=cls.root)
cls.io = iomenu.IOBinding(cls.editwin)
@classmethod
def tearDownClass(cls):
cls.io.close()
cls.editwin._close()
del cls.editwin
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
def test_init(self):
self.assertIs(self.io.editwin, self.editwin)
def test_fixnewlines_end(self):
eq = self.assertEqual
io = self.io
fix = io.fixnewlines
text = io.editwin.text
# Make the editor temporarily look like Shell.
self.editwin.interp = None
shelltext = '>>> if 1'
self.editwin.get_prompt_text = Func(result=shelltext)
eq(fix(), shelltext) # Get... call and '\n' not added.
del self.editwin.interp, self.editwin.get_prompt_text
text.insert(1.0, 'a')
eq(fix(), 'a'+io.eol_convention)
eq(text.get('1.0', 'end-1c'), 'a\n')
eq(fix(), 'a'+io.eol_convention)
def _extension_in_filetypes(extension):
return any(
f'*{extension}' in filetype_tuple[1]
for filetype_tuple in iomenu.IOBinding.filetypes
)
class FiletypesTest(unittest.TestCase):
def test_python_source_files(self):
for extension in util.py_extensions:
with self.subTest(extension=extension):
self.assertTrue(
_extension_in_filetypes(extension)
)
def test_text_files(self):
self.assertTrue(_extension_in_filetypes('.txt'))
def test_all_files(self):
self.assertTrue(_extension_in_filetypes(''))
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,113 @@
"Test macosx, coverage 45% on Windows."
from idlelib import macosx
import unittest
from test.support import requires
import tkinter as tk
import unittest.mock as mock
from idlelib.filelist import FileList
mactypes = {'carbon', 'cocoa', 'xquartz'}
nontypes = {'other'}
alltypes = mactypes | nontypes
def setUpModule():
global orig_tktype
orig_tktype = macosx._tk_type
def tearDownModule():
macosx._tk_type = orig_tktype
class InitTktypeTest(unittest.TestCase):
"Test _init_tk_type."
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = tk.Tk()
cls.root.withdraw()
cls.orig_platform = macosx.platform
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
macosx.platform = cls.orig_platform
def test_init_sets_tktype(self):
"Test that _init_tk_type sets _tk_type according to platform."
for platform, types in ('darwin', alltypes), ('other', nontypes):
with self.subTest(platform=platform):
macosx.platform = platform
macosx._tk_type = None
macosx._init_tk_type()
self.assertIn(macosx._tk_type, types)
class IsTypeTkTest(unittest.TestCase):
"Test each of the four isTypeTk predecates."
isfuncs = ((macosx.isAquaTk, ('carbon', 'cocoa')),
(macosx.isCarbonTk, ('carbon')),
(macosx.isCocoaTk, ('cocoa')),
(macosx.isXQuartz, ('xquartz')),
)
@mock.patch('idlelib.macosx._init_tk_type')
def test_is_calls_init(self, mockinit):
"Test that each isTypeTk calls _init_tk_type when _tk_type is None."
macosx._tk_type = None
for func, whentrue in self.isfuncs:
with self.subTest(func=func):
func()
self.assertTrue(mockinit.called)
mockinit.reset_mock()
def test_isfuncs(self):
"Test that each isTypeTk return correct bool."
for func, whentrue in self.isfuncs:
for tktype in alltypes:
with self.subTest(func=func, whentrue=whentrue, tktype=tktype):
macosx._tk_type = tktype
(self.assertTrue if tktype in whentrue else self.assertFalse)\
(func())
class SetupTest(unittest.TestCase):
"Test setupApp."
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = tk.Tk()
cls.root.withdraw()
def cmd(tkpath, func):
assert isinstance(tkpath, str)
assert isinstance(func, type(cmd))
cls.root.createcommand = cmd
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
@mock.patch('idlelib.macosx.overrideRootMenu') #27312
def test_setupapp(self, overrideRootMenu):
"Call setupApp with each possible graphics type."
root = self.root
flist = FileList(root)
for tktype in alltypes:
with self.subTest(tktype=tktype):
macosx._tk_type = tktype
macosx.setupApp(root, flist)
if tktype in ('carbon', 'cocoa'):
self.assertTrue(overrideRootMenu.called)
overrideRootMenu.reset_mock()
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,42 @@
"Test mainmenu, coverage 100%."
# Reported as 88%; mocking turtledemo absence would have no point.
from idlelib import mainmenu
import re
import unittest
class MainMenuTest(unittest.TestCase):
def test_menudefs(self):
actual = [item[0] for item in mainmenu.menudefs]
expect = ['file', 'edit', 'format', 'run', 'shell',
'debug', 'options', 'window', 'help']
self.assertEqual(actual, expect)
def test_default_keydefs(self):
self.assertGreaterEqual(len(mainmenu.default_keydefs), 50)
def test_tcl_indexes(self):
# Test tcl patterns used to find menuitem to alter.
# On failure, change pattern here and in function(s).
# Patterns here have '.*' for re instead of '*' for tcl.
for menu, pattern in (
('debug', '.*tack.*iewer'), # PyShell.debug_menu_postcommand
('options', '.*ode.*ontext'), # EW.__init__, CodeContext.toggle...
('options', '.*ine.*umbers'), # EW.__init__, EW.toggle...event.
):
with self.subTest(menu=menu, pattern=pattern):
for menutup in mainmenu.menudefs:
if menutup[0] == menu:
break
else:
self.assertTrue(0, f"{menu} not in menudefs")
self.assertTrue(any(re.search(pattern, menuitem[0])
for menuitem in menutup[1]
if menuitem is not None), # Separator.
f"{pattern} not in {menu}")
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,49 @@
"Test multicall, coverage 33%."
from idlelib import multicall
import unittest
from test.support import requires
from test.support.testcase import ExtraAssertions
from tkinter import Tk, Text
class MultiCallTest(unittest.TestCase, ExtraAssertions):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.mc = multicall.MultiCallCreator(Text)
@classmethod
def tearDownClass(cls):
del cls.mc
cls.root.update_idletasks()
## for id in cls.root.tk.call('after', 'info'):
## cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
def test_creator(self):
mc = self.mc
self.assertIs(multicall._multicall_dict[Text], mc)
self.assertIsSubclass(mc, Text)
mc2 = multicall.MultiCallCreator(Text)
self.assertIs(mc, mc2)
def test_init(self):
mctext = self.mc(self.root)
self.assertIsInstance(mctext._MultiCall__binders, list)
def test_yview(self):
# Added for tree.wheel_event
# (it depends on yview to not be overridden)
mc = self.mc
self.assertIs(mc.yview, Text.yview)
mctext = self.mc(self.root)
self.assertIs(mctext.yview.__func__, Text.yview)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,171 @@
"Test outwin, coverage 76%."
from idlelib import outwin
import sys
import unittest
from test.support import requires
from tkinter import Tk, Text
from idlelib.idle_test.mock_tk import Mbox_func
from idlelib.idle_test.mock_idle import Func
from unittest import mock
class OutputWindowTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
root = cls.root = Tk()
root.withdraw()
w = cls.window = outwin.OutputWindow(None, None, None, root)
cls.text = w.text = Text(root)
if sys.platform == 'darwin': # Issue 112938
cls.text.update = cls.text.update_idletasks
# Without this, test write, writelines, and goto... fail.
# The reasons and why macOS-specific are unclear.
@classmethod
def tearDownClass(cls):
cls.window.close()
del cls.text, cls.window
cls.root.destroy()
del cls.root
def setUp(self):
self.text.delete('1.0', 'end')
def test_ispythonsource(self):
# OutputWindow overrides ispythonsource to always return False.
w = self.window
self.assertFalse(w.ispythonsource('test.txt'))
self.assertFalse(w.ispythonsource(__file__))
def test_window_title(self):
self.assertEqual(self.window.top.title(), 'Output')
def test_maybesave(self):
w = self.window
eq = self.assertEqual
w.get_saved = Func()
w.get_saved.result = False
eq(w.maybesave(), 'no')
eq(w.get_saved.called, 1)
w.get_saved.result = True
eq(w.maybesave(), 'yes')
eq(w.get_saved.called, 2)
del w.get_saved
def test_write(self):
eq = self.assertEqual
delete = self.text.delete
get = self.text.get
write = self.window.write
# No new line - insert stays on same line.
delete('1.0', 'end')
test_text = 'test text'
eq(write(test_text), len(test_text))
eq(get('1.0', '1.end'), 'test text')
eq(get('insert linestart', 'insert lineend'), 'test text')
# New line - insert moves to next line.
delete('1.0', 'end')
test_text = 'test text\n'
eq(write(test_text), len(test_text))
eq(get('1.0', '1.end'), 'test text')
eq(get('insert linestart', 'insert lineend'), '')
# Text after new line is tagged for second line of Text widget.
delete('1.0', 'end')
test_text = 'test text\nLine 2'
eq(write(test_text), len(test_text))
eq(get('1.0', '1.end'), 'test text')
eq(get('2.0', '2.end'), 'Line 2')
eq(get('insert linestart', 'insert lineend'), 'Line 2')
# Test tags.
delete('1.0', 'end')
test_text = 'test text\n'
test_text2 = 'Line 2\n'
eq(write(test_text, tags='mytag'), len(test_text))
eq(write(test_text2, tags='secondtag'), len(test_text2))
eq(get('mytag.first', 'mytag.last'), test_text)
eq(get('secondtag.first', 'secondtag.last'), test_text2)
eq(get('1.0', '1.end'), test_text.rstrip('\n'))
eq(get('2.0', '2.end'), test_text2.rstrip('\n'))
def test_writelines(self):
eq = self.assertEqual
get = self.text.get
writelines = self.window.writelines
writelines(('Line 1\n', 'Line 2\n', 'Line 3\n'))
eq(get('1.0', '1.end'), 'Line 1')
eq(get('2.0', '2.end'), 'Line 2')
eq(get('3.0', '3.end'), 'Line 3')
eq(get('insert linestart', 'insert lineend'), '')
def test_goto_file_line(self):
eq = self.assertEqual
w = self.window
text = self.text
w.flist = mock.Mock()
gfl = w.flist.gotofileline = Func()
showerror = w.showerror = Mbox_func()
# No file/line number.
w.write('Not a file line')
self.assertIsNone(w.goto_file_line())
eq(gfl.called, 0)
eq(showerror.title, 'No special line')
# Current file/line number.
w.write(f'{str(__file__)}: 42: spam\n')
w.write(f'{str(__file__)}: 21: spam')
self.assertIsNone(w.goto_file_line())
eq(gfl.args, (str(__file__), 21))
# Previous line has file/line number.
text.delete('1.0', 'end')
w.write(f'{str(__file__)}: 42: spam\n')
w.write('Not a file line')
self.assertIsNone(w.goto_file_line())
eq(gfl.args, (str(__file__), 42))
del w.flist.gotofileline, w.showerror
class ModuleFunctionTest(unittest.TestCase):
@classmethod
def setUp(cls):
outwin.file_line_progs = None
def test_compile_progs(self):
outwin.compile_progs()
for pat, regex in zip(outwin.file_line_pats, outwin.file_line_progs):
self.assertEqual(regex.pattern, pat)
@mock.patch('builtins.open')
def test_file_line_helper(self, mock_open):
flh = outwin.file_line_helper
test_lines = (
(r'foo file "testfile1", line 42, bar', ('testfile1', 42)),
(r'foo testfile2(21) bar', ('testfile2', 21)),
(r' testfile3 : 42: foo bar\n', (' testfile3 ', 42)),
(r'foo testfile4.py :1: ', ('foo testfile4.py ', 1)),
('testfile5: \u19D4\u19D2: ', ('testfile5', 42)),
(r'testfile6: 42', None), # only one `:`
(r'testfile7 42 text', None) # no separators
)
for line, expected_output in test_lines:
self.assertEqual(flh(line), expected_output)
if expected_output:
mock_open.assert_called_with(expected_output[0])
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,112 @@
"""Test parenmatch, coverage 91%.
This must currently be a gui test because ParenMatch methods use
several text methods not defined on idlelib.idle_test.mock_tk.Text.
"""
from idlelib.parenmatch import ParenMatch
from test.support import requires
requires('gui')
import unittest
from unittest.mock import Mock
from tkinter import Tk, Text
class DummyEditwin:
def __init__(self, text):
self.text = text
self.indentwidth = 8
self.tabwidth = 8
self.prompt_last_line = '>>>' # Currently not used by parenmatch.
class ParenMatchTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.root = Tk()
cls.root.withdraw()
cls.text = Text(cls.root)
cls.editwin = DummyEditwin(cls.text)
cls.editwin.text_frame = Mock()
@classmethod
def tearDownClass(cls):
del cls.text, cls.editwin
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def tearDown(self):
self.text.delete('1.0', 'end')
def get_parenmatch(self):
pm = ParenMatch(self.editwin)
pm.bell = lambda: None
return pm
def test_paren_styles(self):
"""
Test ParenMatch with each style.
"""
text = self.text
pm = self.get_parenmatch()
for style, range1, range2 in (
('opener', ('1.10', '1.11'), ('1.10', '1.11')),
('default',('1.10', '1.11'),('1.10', '1.11')),
('parens', ('1.14', '1.15'), ('1.15', '1.16')),
('expression', ('1.10', '1.15'), ('1.10', '1.16'))):
with self.subTest(style=style):
text.delete('1.0', 'end')
pm.STYLE = style
text.insert('insert', 'def foobar(a, b')
pm.flash_paren_event('event')
self.assertIn('<<parenmatch-check-restore>>', text.event_info())
if style == 'parens':
self.assertTupleEqual(text.tag_nextrange('paren', '1.0'),
('1.10', '1.11'))
self.assertTupleEqual(
text.tag_prevrange('paren', 'end'), range1)
text.insert('insert', ')')
pm.restore_event()
self.assertNotIn('<<parenmatch-check-restore>>',
text.event_info())
self.assertEqual(text.tag_prevrange('paren', 'end'), ())
pm.paren_closed_event('event')
self.assertTupleEqual(
text.tag_prevrange('paren', 'end'), range2)
def test_paren_corner(self):
"""
Test corner cases in flash_paren_event and paren_closed_event.
Force execution of conditional expressions and alternate paths.
"""
text = self.text
pm = self.get_parenmatch()
text.insert('insert', '# Comment.)')
pm.paren_closed_event('event')
text.insert('insert', '\ndef')
pm.flash_paren_event('event')
pm.paren_closed_event('event')
text.insert('insert', ' a, *arg)')
pm.paren_closed_event('event')
def test_handle_restore_timer(self):
pm = self.get_parenmatch()
pm.restore_event = Mock()
pm.handle_restore_timer(0)
self.assertTrue(pm.restore_event.called)
pm.restore_event.reset_mock()
pm.handle_restore_timer(1)
self.assertFalse(pm.restore_event.called)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,86 @@
"Test pathbrowser, coverage 95%."
from idlelib import pathbrowser
import unittest
from test.support import requires
from tkinter import Tk
import os.path
import pyclbr # for _modules
import sys # for sys.path
from idlelib.idle_test.mock_idle import Func
import idlelib # for __file__
from idlelib import browser
from idlelib.tree import TreeNode
class PathBrowserTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.pb = pathbrowser.PathBrowser(cls.root, _utest=True)
@classmethod
def tearDownClass(cls):
cls.pb.close()
cls.root.update_idletasks()
cls.root.destroy()
del cls.root, cls.pb
def test_init(self):
pb = self.pb
eq = self.assertEqual
eq(pb.master, self.root)
eq(pyclbr._modules, {})
self.assertIsInstance(pb.node, TreeNode)
self.assertIsNotNone(browser.file_open)
def test_settitle(self):
pb = self.pb
self.assertEqual(pb.top.title(), 'Path Browser')
self.assertEqual(pb.top.iconname(), 'Path Browser')
def test_rootnode(self):
pb = self.pb
rn = pb.rootnode()
self.assertIsInstance(rn, pathbrowser.PathBrowserTreeItem)
def test_close(self):
pb = self.pb
pb.top.destroy = Func()
pb.node.destroy = Func()
pb.close()
self.assertTrue(pb.top.destroy.called)
self.assertTrue(pb.node.destroy.called)
del pb.top.destroy, pb.node.destroy
class DirBrowserTreeItemTest(unittest.TestCase):
def test_DirBrowserTreeItem(self):
# Issue16226 - make sure that getting a sublist works
d = pathbrowser.DirBrowserTreeItem('')
d.GetSubList()
self.assertEqual('', d.GetText())
dir = os.path.split(os.path.abspath(idlelib.__file__))[0]
self.assertEqual(d.ispackagedir(dir), True)
self.assertEqual(d.ispackagedir(dir + '/Icons'), False)
class PathBrowserTreeItemTest(unittest.TestCase):
def test_PathBrowserTreeItem(self):
p = pathbrowser.PathBrowserTreeItem()
self.assertEqual(p.GetText(), 'sys.path')
sub = p.GetSubList()
self.assertEqual(len(sub), len(sys.path))
self.assertEqual(type(sub[0]), pathbrowser.DirBrowserTreeItem)
if __name__ == '__main__':
unittest.main(verbosity=2, exit=False)

View File

@ -0,0 +1,118 @@
"Test percolator, coverage 100%."
from idlelib.percolator import Percolator, Delegator
import unittest
from test.support import requires
requires('gui')
from tkinter import Text, Tk, END
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)

View File

@ -0,0 +1,483 @@
"Test pyparse, coverage 96%."
from idlelib import pyparse
import unittest
from collections import namedtuple
class ParseMapTest(unittest.TestCase):
def test_parsemap(self):
keepwhite = {ord(c): ord(c) for c in ' \t\n\r'}
mapping = pyparse.ParseMap(keepwhite)
self.assertEqual(mapping[ord('\t')], ord('\t'))
self.assertEqual(mapping[ord('a')], ord('x'))
self.assertEqual(mapping[1000], ord('x'))
def test_trans(self):
# trans is the production instance of ParseMap, used in _study1
parser = pyparse.Parser(4, 4)
self.assertEqual('\t a([{b}])b"c\'d\n'.translate(pyparse.trans),
'xxx(((x)))x"x\'x\n')
class PyParseTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.parser = pyparse.Parser(indentwidth=4, tabwidth=4)
@classmethod
def tearDownClass(cls):
del cls.parser
def test_init(self):
self.assertEqual(self.parser.indentwidth, 4)
self.assertEqual(self.parser.tabwidth, 4)
def test_set_code(self):
eq = self.assertEqual
p = self.parser
setcode = p.set_code
# Not empty and doesn't end with newline.
with self.assertRaises(AssertionError):
setcode('a')
tests = ('',
'a\n')
for string in tests:
with self.subTest(string=string):
setcode(string)
eq(p.code, string)
eq(p.study_level, 0)
def test_find_good_parse_start(self):
eq = self.assertEqual
p = self.parser
setcode = p.set_code
start = p.find_good_parse_start
def char_in_string_false(index): return False
# First line starts with 'def' and ends with ':', then 0 is the pos.
setcode('def spam():\n')
eq(start(char_in_string_false), 0)
# First line begins with a keyword in the list and ends
# with an open brace, then 0 is the pos. This is how
# hyperparser calls this function as the newline is not added
# in the editor, but rather on the call to setcode.
setcode('class spam( ' + ' \n')
eq(start(char_in_string_false), 0)
# Split def across lines.
setcode('"""This is a module docstring"""\n'
'class C:\n'
' def __init__(self, a,\n'
' b=True):\n'
' pass\n'
)
pos0, pos = 33, 42 # Start of 'class...', ' def' lines.
# Passing no value or non-callable should fail (issue 32989).
with self.assertRaises(TypeError):
start()
with self.assertRaises(TypeError):
start(False)
# Make text look like a string. This returns pos as the start
# position, but it's set to None.
self.assertIsNone(start(is_char_in_string=lambda index: True))
# Make all text look like it's not in a string. This means that it
# found a good start position.
eq(start(char_in_string_false), pos)
# If the beginning of the def line is not in a string, then it
# returns that as the index.
eq(start(is_char_in_string=lambda index: index > pos), pos)
# If the beginning of the def line is in a string, then it
# looks for a previous index.
eq(start(is_char_in_string=lambda index: index >= pos), pos0)
# If everything before the 'def' is in a string, then returns None.
# The non-continuation def line returns 44 (see below).
eq(start(is_char_in_string=lambda index: index < pos), None)
# Code without extra line break in def line - mostly returns the same
# values.
setcode('"""This is a module docstring"""\n'
'class C:\n'
' def __init__(self, a, b=True):\n'
' pass\n'
) # Does not affect class, def positions.
eq(start(char_in_string_false), pos)
eq(start(is_char_in_string=lambda index: index > pos), pos)
eq(start(is_char_in_string=lambda index: index >= pos), pos0)
# When the def line isn't split, this returns which doesn't match the
# split line test.
eq(start(is_char_in_string=lambda index: index < pos), pos)
def test_set_lo(self):
code = (
'"""This is a module docstring"""\n'
'class C:\n'
' def __init__(self, a,\n'
' b=True):\n'
' pass\n'
)
pos = 42
p = self.parser
p.set_code(code)
# Previous character is not a newline.
with self.assertRaises(AssertionError):
p.set_lo(5)
# A value of 0 doesn't change self.code.
p.set_lo(0)
self.assertEqual(p.code, code)
# An index that is preceded by a newline.
p.set_lo(pos)
self.assertEqual(p.code, code[pos:])
def test_study1(self):
eq = self.assertEqual
p = self.parser
setcode = p.set_code
study = p._study1
(NONE, BACKSLASH, FIRST, NEXT, BRACKET) = range(5)
TestInfo = namedtuple('TestInfo', ['string', 'goodlines',
'continuation'])
tests = (
TestInfo('', [0], NONE),
# Docstrings.
TestInfo('"""This is a complete docstring."""\n', [0, 1], NONE),
TestInfo("'''This is a complete docstring.'''\n", [0, 1], NONE),
TestInfo('"""This is a continued docstring.\n', [0, 1], FIRST),
TestInfo("'''This is a continued docstring.\n", [0, 1], FIRST),
TestInfo('"""Closing quote does not match."\n', [0, 1], FIRST),
TestInfo('"""Bracket in docstring [\n', [0, 1], FIRST),
TestInfo("'''Incomplete two line docstring.\n\n", [0, 2], NEXT),
# Single-quoted strings.
TestInfo('"This is a complete string."\n', [0, 1], NONE),
TestInfo('"This is an incomplete string.\n', [0, 1], NONE),
TestInfo("'This is more incomplete.\n\n", [0, 1, 2], NONE),
# Comment (backslash does not continue comments).
TestInfo('# Comment\\\n', [0, 1], NONE),
# Brackets.
TestInfo('("""Complete string in bracket"""\n', [0, 1], BRACKET),
TestInfo('("""Open string in bracket\n', [0, 1], FIRST),
TestInfo('a = (1 + 2) - 5 *\\\n', [0, 1], BACKSLASH), # No bracket.
TestInfo('\n def function1(self, a,\n b):\n',
[0, 1, 3], NONE),
TestInfo('\n def function1(self, a,\\\n', [0, 1, 2], BRACKET),
TestInfo('\n def function1(self, a,\n', [0, 1, 2], BRACKET),
TestInfo('())\n', [0, 1], NONE), # Extra closer.
TestInfo(')(\n', [0, 1], BRACKET), # Extra closer.
# For the mismatched example, it doesn't look like continuation.
TestInfo('{)(]\n', [0, 1], NONE), # Mismatched.
)
for test in tests:
with self.subTest(string=test.string):
setcode(test.string) # resets study_level
study()
eq(p.study_level, 1)
eq(p.goodlines, test.goodlines)
eq(p.continuation, test.continuation)
# Called again, just returns without reprocessing.
self.assertIsNone(study())
def test_get_continuation_type(self):
eq = self.assertEqual
p = self.parser
setcode = p.set_code
gettype = p.get_continuation_type
(NONE, BACKSLASH, FIRST, NEXT, BRACKET) = range(5)
TestInfo = namedtuple('TestInfo', ['string', 'continuation'])
tests = (
TestInfo('', NONE),
TestInfo('"""This is a continuation docstring.\n', FIRST),
TestInfo("'''This is a multiline-continued docstring.\n\n", NEXT),
TestInfo('a = (1 + 2) - 5 *\\\n', BACKSLASH),
TestInfo('\n def function1(self, a,\\\n', BRACKET)
)
for test in tests:
with self.subTest(string=test.string):
setcode(test.string)
eq(gettype(), test.continuation)
def test_study2(self):
eq = self.assertEqual
p = self.parser
setcode = p.set_code
study = p._study2
TestInfo = namedtuple('TestInfo', ['string', 'start', 'end', 'lastch',
'openbracket', 'bracketing'])
tests = (
TestInfo('', 0, 0, '', None, ((0, 0),)),
TestInfo("'''This is a multiline continuation docstring.\n\n",
0, 48, "'", None, ((0, 0), (0, 1), (48, 0))),
TestInfo(' # Comment\\\n',
0, 12, '', None, ((0, 0), (1, 1), (12, 0))),
# A comment without a space is a special case
TestInfo(' #Comment\\\n',
0, 0, '', None, ((0, 0),)),
# Backslash continuation.
TestInfo('a = (1 + 2) - 5 *\\\n',
0, 19, '*', None, ((0, 0), (4, 1), (11, 0))),
# Bracket continuation with close.
TestInfo('\n def function1(self, a,\n b):\n',
1, 48, ':', None, ((1, 0), (17, 1), (46, 0))),
# Bracket continuation with unneeded backslash.
TestInfo('\n def function1(self, a,\\\n',
1, 28, ',', 17, ((1, 0), (17, 1))),
# Bracket continuation.
TestInfo('\n def function1(self, a,\n',
1, 27, ',', 17, ((1, 0), (17, 1))),
# Bracket continuation with comment at end of line with text.
TestInfo('\n def function1(self, a, # End of line comment.\n',
1, 51, ',', 17, ((1, 0), (17, 1), (28, 2), (51, 1))),
# Multi-line statement with comment line in between code lines.
TestInfo(' a = ["first item",\n # Comment line\n "next item",\n',
0, 55, ',', 6, ((0, 0), (6, 1), (7, 2), (19, 1),
(23, 2), (38, 1), (42, 2), (53, 1))),
TestInfo('())\n',
0, 4, ')', None, ((0, 0), (0, 1), (2, 0), (3, 0))),
TestInfo(')(\n', 0, 3, '(', 1, ((0, 0), (1, 0), (1, 1))),
# Wrong closers still decrement stack level.
TestInfo('{)(]\n',
0, 5, ']', None, ((0, 0), (0, 1), (2, 0), (2, 1), (4, 0))),
# Character after backslash.
TestInfo(':\\a\n', 0, 4, '\\a', None, ((0, 0),)),
TestInfo('\n', 0, 0, '', None, ((0, 0),)),
)
for test in tests:
with self.subTest(string=test.string):
setcode(test.string)
study()
eq(p.study_level, 2)
eq(p.stmt_start, test.start)
eq(p.stmt_end, test.end)
eq(p.lastch, test.lastch)
eq(p.lastopenbracketpos, test.openbracket)
eq(p.stmt_bracketing, test.bracketing)
# Called again, just returns without reprocessing.
self.assertIsNone(study())
def test_get_num_lines_in_stmt(self):
eq = self.assertEqual
p = self.parser
setcode = p.set_code
getlines = p.get_num_lines_in_stmt
TestInfo = namedtuple('TestInfo', ['string', 'lines'])
tests = (
TestInfo('[x for x in a]\n', 1), # Closed on one line.
TestInfo('[x\nfor x in a\n', 2), # Not closed.
TestInfo('[x\\\nfor x in a\\\n', 2), # "", unneeded backslashes.
TestInfo('[x\nfor x in a\n]\n', 3), # Closed on multi-line.
TestInfo('\n"""Docstring comment L1"""\nL2\nL3\nL4\n', 1),
TestInfo('\n"""Docstring comment L1\nL2"""\nL3\nL4\n', 1),
TestInfo('\n"""Docstring comment L1\\\nL2\\\nL3\\\nL4\\\n', 4),
TestInfo('\n\n"""Docstring comment L1\\\nL2\\\nL3\\\nL4\\\n"""\n', 5)
)
# Blank string doesn't have enough elements in goodlines.
setcode('')
with self.assertRaises(IndexError):
getlines()
for test in tests:
with self.subTest(string=test.string):
setcode(test.string)
eq(getlines(), test.lines)
def test_compute_bracket_indent(self):
eq = self.assertEqual
p = self.parser
setcode = p.set_code
indent = p.compute_bracket_indent
TestInfo = namedtuple('TestInfo', ['string', 'spaces'])
tests = (
TestInfo('def function1(self, a,\n', 14),
# Characters after bracket.
TestInfo('\n def function1(self, a,\n', 18),
TestInfo('\n\tdef function1(self, a,\n', 18),
# No characters after bracket.
TestInfo('\n def function1(\n', 8),
TestInfo('\n\tdef function1(\n', 8),
TestInfo('\n def function1( \n', 8), # Ignore extra spaces.
TestInfo('[\n"first item",\n # Comment line\n "next item",\n', 0),
TestInfo('[\n "first item",\n # Comment line\n "next item",\n', 2),
TestInfo('["first item",\n # Comment line\n "next item",\n', 1),
TestInfo('(\n', 4),
TestInfo('(a\n', 1),
)
# Must be C_BRACKET continuation type.
setcode('def function1(self, a, b):\n')
with self.assertRaises(AssertionError):
indent()
for test in tests:
setcode(test.string)
eq(indent(), test.spaces)
def test_compute_backslash_indent(self):
eq = self.assertEqual
p = self.parser
setcode = p.set_code
indent = p.compute_backslash_indent
# Must be C_BACKSLASH continuation type.
errors = (('def function1(self, a, b\\\n'), # Bracket.
(' """ (\\\n'), # Docstring.
('a = #\\\n'), # Inline comment.
)
for string in errors:
with self.subTest(string=string):
setcode(string)
with self.assertRaises(AssertionError):
indent()
TestInfo = namedtuple('TestInfo', ('string', 'spaces'))
tests = (TestInfo('a = (1 + 2) - 5 *\\\n', 4),
TestInfo('a = 1 + 2 - 5 *\\\n', 4),
TestInfo(' a = 1 + 2 - 5 *\\\n', 8),
TestInfo(' a = "spam"\\\n', 6),
TestInfo(' a = \\\n"a"\\\n', 4),
TestInfo(' a = #\\\n"a"\\\n', 5),
TestInfo('a == \\\n', 2),
TestInfo('a != \\\n', 2),
# Difference between containing = and those not.
TestInfo('\\\n', 2),
TestInfo(' \\\n', 6),
TestInfo('\t\\\n', 6),
TestInfo('a\\\n', 3),
TestInfo('{}\\\n', 4),
TestInfo('(1 + 2) - 5 *\\\n', 3),
)
for test in tests:
with self.subTest(string=test.string):
setcode(test.string)
eq(indent(), test.spaces)
def test_get_base_indent_string(self):
eq = self.assertEqual
p = self.parser
setcode = p.set_code
baseindent = p.get_base_indent_string
TestInfo = namedtuple('TestInfo', ['string', 'indent'])
tests = (TestInfo('', ''),
TestInfo('def a():\n', ''),
TestInfo('\tdef a():\n', '\t'),
TestInfo(' def a():\n', ' '),
TestInfo(' def a(\n', ' '),
TestInfo('\t\n def a(\n', ' '),
TestInfo('\t\n # Comment.\n', ' '),
)
for test in tests:
with self.subTest(string=test.string):
setcode(test.string)
eq(baseindent(), test.indent)
def test_is_block_opener(self):
yes = self.assertTrue
no = self.assertFalse
p = self.parser
setcode = p.set_code
opener = p.is_block_opener
TestInfo = namedtuple('TestInfo', ['string', 'assert_'])
tests = (
TestInfo('def a():\n', yes),
TestInfo('\n def function1(self, a,\n b):\n', yes),
TestInfo(':\n', yes),
TestInfo('a:\n', yes),
TestInfo('):\n', yes),
TestInfo('(:\n', yes),
TestInfo('":\n', no),
TestInfo('\n def function1(self, a,\n', no),
TestInfo('def function1(self, a):\n pass\n', no),
TestInfo('# A comment:\n', no),
TestInfo('"""A docstring:\n', no),
TestInfo('"""A docstring:\n', no),
)
for test in tests:
with self.subTest(string=test.string):
setcode(test.string)
test.assert_(opener())
def test_is_block_closer(self):
yes = self.assertTrue
no = self.assertFalse
p = self.parser
setcode = p.set_code
closer = p.is_block_closer
TestInfo = namedtuple('TestInfo', ['string', 'assert_'])
tests = (
TestInfo('return\n', yes),
TestInfo('\tbreak\n', yes),
TestInfo(' continue\n', yes),
TestInfo(' raise\n', yes),
TestInfo('pass \n', yes),
TestInfo('pass\t\n', yes),
TestInfo('return #\n', yes),
TestInfo('raised\n', no),
TestInfo('returning\n', no),
TestInfo('# return\n', no),
TestInfo('"""break\n', no),
TestInfo('"continue\n', no),
TestInfo('def function1(self, a):\n pass\n', yes),
)
for test in tests:
with self.subTest(string=test.string):
setcode(test.string)
test.assert_(closer())
def test_get_last_stmt_bracketing(self):
eq = self.assertEqual
p = self.parser
setcode = p.set_code
bracketing = p.get_last_stmt_bracketing
TestInfo = namedtuple('TestInfo', ['string', 'bracket'])
tests = (
TestInfo('', ((0, 0),)),
TestInfo('a\n', ((0, 0),)),
TestInfo('()()\n', ((0, 0), (0, 1), (2, 0), (2, 1), (4, 0))),
TestInfo('(\n)()\n', ((0, 0), (0, 1), (3, 0), (3, 1), (5, 0))),
TestInfo('()\n()\n', ((3, 0), (3, 1), (5, 0))),
TestInfo('()(\n)\n', ((0, 0), (0, 1), (2, 0), (2, 1), (5, 0))),
TestInfo('(())\n', ((0, 0), (0, 1), (1, 2), (3, 1), (4, 0))),
TestInfo('(\n())\n', ((0, 0), (0, 1), (2, 2), (4, 1), (5, 0))),
# Same as matched test.
TestInfo('{)(]\n', ((0, 0), (0, 1), (2, 0), (2, 1), (4, 0))),
TestInfo('(((())\n',
((0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (5, 3), (6, 2))),
)
for test in tests:
with self.subTest(string=test.string):
setcode(test.string)
eq(bracketing(), test.bracket)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,148 @@
"Test pyshell, coverage 12%."
# Plus coverage of test_warning. Was 20% with test_openshell.
from idlelib import pyshell
import unittest
from test.support import requires
from tkinter import Tk
class FunctionTest(unittest.TestCase):
# Test stand-alone module level non-gui functions.
def test_restart_line_wide(self):
eq = self.assertEqual
for file, mul, extra in (('', 22, ''), ('finame', 21, '=')):
width = 60
bar = mul * '='
with self.subTest(file=file, bar=bar):
file = file or 'Shell'
line = pyshell.restart_line(width, file)
eq(len(line), width)
eq(line, f"{bar+extra} RESTART: {file} {bar}")
def test_restart_line_narrow(self):
expect, taglen = "= RESTART: Shell", 16
for width in (taglen-1, taglen, taglen+1):
with self.subTest(width=width):
self.assertEqual(pyshell.restart_line(width, ''), expect)
self.assertEqual(pyshell.restart_line(taglen+2, ''), expect+' =')
class PyShellFileListTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
@classmethod
def tearDownClass(cls):
#cls.root.update_idletasks()
## for id in cls.root.tk.call('after', 'info'):
## cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
def test_init(self):
psfl = pyshell.PyShellFileList(self.root)
self.assertEqual(psfl.EditorWindow, pyshell.PyShellEditorWindow)
self.assertIsNone(psfl.pyshell)
# The following sometimes causes 'invalid command name "109734456recolorize"'.
# Uncommenting after_cancel above prevents this, but results in
# TclError: bad window path name ".!listedtoplevel.!frame.text"
# which is normally prevented by after_cancel.
## def test_openshell(self):
## pyshell.use_subprocess = False
## ps = pyshell.PyShellFileList(self.root).open_shell()
## self.assertIsInstance(ps, pyshell.PyShell)
class PyShellRemoveLastNewlineAndSurroundingWhitespaceTest(unittest.TestCase):
regexp = pyshell.PyShell._last_newline_re
def all_removed(self, text):
self.assertEqual('', self.regexp.sub('', text))
def none_removed(self, text):
self.assertEqual(text, self.regexp.sub('', text))
def check_result(self, text, expected):
self.assertEqual(expected, self.regexp.sub('', text))
def test_empty(self):
self.all_removed('')
def test_newline(self):
self.all_removed('\n')
def test_whitespace_no_newline(self):
self.all_removed(' ')
self.all_removed(' ')
self.all_removed(' ')
self.all_removed(' ' * 20)
self.all_removed('\t')
self.all_removed('\t\t')
self.all_removed('\t\t\t')
self.all_removed('\t' * 20)
self.all_removed('\t ')
self.all_removed(' \t')
self.all_removed(' \t \t ')
self.all_removed('\t \t \t')
def test_newline_with_whitespace(self):
self.all_removed(' \n')
self.all_removed('\t\n')
self.all_removed(' \t\n')
self.all_removed('\t \n')
self.all_removed('\n ')
self.all_removed('\n\t')
self.all_removed('\n \t')
self.all_removed('\n\t ')
self.all_removed(' \n ')
self.all_removed('\t\n ')
self.all_removed(' \n\t')
self.all_removed('\t\n\t')
self.all_removed('\t \t \t\n')
self.all_removed(' \t \t \n')
self.all_removed('\n\t \t \t')
self.all_removed('\n \t \t ')
def test_multiple_newlines(self):
self.check_result('\n\n', '\n')
self.check_result('\n' * 5, '\n' * 4)
self.check_result('\n' * 5 + '\t', '\n' * 4)
self.check_result('\n' * 20, '\n' * 19)
self.check_result('\n' * 20 + ' ', '\n' * 19)
self.check_result(' \n \n ', ' \n')
self.check_result(' \n\n ', ' \n')
self.check_result(' \n\n', ' \n')
self.check_result('\t\n\n', '\t\n')
self.check_result('\n\n ', '\n')
self.check_result('\n\n\t', '\n')
self.check_result(' \n \n ', ' \n')
self.check_result('\t\n\t\n\t', '\t\n')
def test_non_whitespace(self):
self.none_removed('a')
self.check_result('a\n', 'a')
self.check_result('a\n ', 'a')
self.check_result('a \n ', 'a')
self.check_result('a \n\t', 'a')
self.none_removed('-')
self.check_result('-\n', '-')
self.none_removed('.')
self.check_result('.\n', '.')
def test_unsupported_whitespace(self):
self.none_removed('\v')
self.none_removed('\n\v')
self.check_result('\v\n', '\v')
self.none_removed(' \n\v')
self.check_result('\v\n ', '\v')
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,452 @@
"""Test query, coverage 93%.
Non-gui tests for Query, SectionName, ModuleName, and HelpSource use
dummy versions that extract the non-gui methods and add other needed
attributes. GUI tests create an instance of each class and simulate
entries and button clicks. Subclass tests only target the new code in
the subclass definition.
The appearance of the widgets is checked by the Query and
HelpSource htests. These are run by running query.py.
"""
from idlelib import query
import unittest
from test.support import requires
from test.support.testcase import ExtraAssertions
from tkinter import Tk, END
import sys
from unittest import mock
from idlelib.idle_test.mock_tk import Var
# NON-GUI TESTS
class QueryTest(unittest.TestCase):
"Test Query base class."
class Dummy_Query:
# Test the following Query methods.
entry_ok = query.Query.entry_ok
ok = query.Query.ok
cancel = query.Query.cancel
# Add attributes and initialization needed for tests.
def __init__(self, dummy_entry):
self.entry = Var(value=dummy_entry)
self.entry_error = {'text': ''}
self.result = None
self.destroyed = False
def showerror(self, message):
self.entry_error['text'] = message
def destroy(self):
self.destroyed = True
def test_entry_ok_blank(self):
dialog = self.Dummy_Query(' ')
self.assertEqual(dialog.entry_ok(), None)
self.assertEqual((dialog.result, dialog.destroyed), (None, False))
self.assertIn('blank line', dialog.entry_error['text'])
def test_entry_ok_good(self):
dialog = self.Dummy_Query(' good ')
Equal = self.assertEqual
Equal(dialog.entry_ok(), 'good')
Equal((dialog.result, dialog.destroyed), (None, False))
Equal(dialog.entry_error['text'], '')
def test_ok_blank(self):
dialog = self.Dummy_Query('')
dialog.entry.focus_set = mock.Mock()
self.assertEqual(dialog.ok(), None)
self.assertTrue(dialog.entry.focus_set.called)
del dialog.entry.focus_set
self.assertEqual((dialog.result, dialog.destroyed), (None, False))
def test_ok_good(self):
dialog = self.Dummy_Query('good')
self.assertEqual(dialog.ok(), None)
self.assertEqual((dialog.result, dialog.destroyed), ('good', True))
def test_cancel(self):
dialog = self.Dummy_Query('does not matter')
self.assertEqual(dialog.cancel(), None)
self.assertEqual((dialog.result, dialog.destroyed), (None, True))
class SectionNameTest(unittest.TestCase):
"Test SectionName subclass of Query."
class Dummy_SectionName:
entry_ok = query.SectionName.entry_ok # Function being tested.
used_names = ['used']
def __init__(self, dummy_entry):
self.entry = Var(value=dummy_entry)
self.entry_error = {'text': ''}
def showerror(self, message):
self.entry_error['text'] = message
def test_blank_section_name(self):
dialog = self.Dummy_SectionName(' ')
self.assertEqual(dialog.entry_ok(), None)
self.assertIn('no name', dialog.entry_error['text'])
def test_used_section_name(self):
dialog = self.Dummy_SectionName('used')
self.assertEqual(dialog.entry_ok(), None)
self.assertIn('use', dialog.entry_error['text'])
def test_long_section_name(self):
dialog = self.Dummy_SectionName('good'*8)
self.assertEqual(dialog.entry_ok(), None)
self.assertIn('longer than 30', dialog.entry_error['text'])
def test_good_section_name(self):
dialog = self.Dummy_SectionName(' good ')
self.assertEqual(dialog.entry_ok(), 'good')
self.assertEqual(dialog.entry_error['text'], '')
class ModuleNameTest(unittest.TestCase, ExtraAssertions):
"Test ModuleName subclass of Query."
class Dummy_ModuleName:
entry_ok = query.ModuleName.entry_ok # Function being tested.
text0 = ''
def __init__(self, dummy_entry):
self.entry = Var(value=dummy_entry)
self.entry_error = {'text': ''}
def showerror(self, message):
self.entry_error['text'] = message
def test_blank_module_name(self):
dialog = self.Dummy_ModuleName(' ')
self.assertEqual(dialog.entry_ok(), None)
self.assertIn('no name', dialog.entry_error['text'])
def test_bogus_module_name(self):
dialog = self.Dummy_ModuleName('__name_xyz123_should_not_exist__')
self.assertEqual(dialog.entry_ok(), None)
self.assertIn('not found', dialog.entry_error['text'])
def test_c_source_name(self):
dialog = self.Dummy_ModuleName('itertools')
self.assertEqual(dialog.entry_ok(), None)
self.assertIn('source-based', dialog.entry_error['text'])
def test_good_module_name(self):
dialog = self.Dummy_ModuleName('idlelib')
self.assertEndsWith(dialog.entry_ok(), '__init__.py')
self.assertEqual(dialog.entry_error['text'], '')
dialog = self.Dummy_ModuleName('idlelib.idle')
self.assertEndsWith(dialog.entry_ok(), 'idle.py')
self.assertEqual(dialog.entry_error['text'], '')
class GotoTest(unittest.TestCase):
"Test Goto subclass of Query."
class Dummy_ModuleName:
entry_ok = query.Goto.entry_ok # Function being tested.
def __init__(self, dummy_entry):
self.entry = Var(value=dummy_entry)
self.entry_error = {'text': ''}
def showerror(self, message):
self.entry_error['text'] = message
def test_bogus_goto(self):
dialog = self.Dummy_ModuleName('a')
self.assertEqual(dialog.entry_ok(), None)
self.assertIn('not a base 10 integer', dialog.entry_error['text'])
def test_bad_goto(self):
dialog = self.Dummy_ModuleName('0')
self.assertEqual(dialog.entry_ok(), None)
self.assertIn('not a positive integer', dialog.entry_error['text'])
def test_good_goto(self):
dialog = self.Dummy_ModuleName('1')
self.assertEqual(dialog.entry_ok(), 1)
self.assertEqual(dialog.entry_error['text'], '')
# 3 HelpSource test classes each test one method.
class HelpsourceBrowsefileTest(unittest.TestCase):
"Test browse_file method of ModuleName subclass of Query."
class Dummy_HelpSource:
browse_file = query.HelpSource.browse_file
pathvar = Var()
def test_file_replaces_path(self):
dialog = self.Dummy_HelpSource()
# Path is widget entry, either '' or something.
# Func return is file dialog return, either '' or something.
# Func return should override widget entry.
# We need all 4 combinations to test all (most) code paths.
for path, func, result in (
('', lambda a,b,c:'', ''),
('', lambda a,b,c: __file__, __file__),
('htest', lambda a,b,c:'', 'htest'),
('htest', lambda a,b,c: __file__, __file__)):
with self.subTest():
dialog.pathvar.set(path)
dialog.askfilename = func
dialog.browse_file()
self.assertEqual(dialog.pathvar.get(), result)
class HelpsourcePathokTest(unittest.TestCase):
"Test path_ok method of HelpSource subclass of Query."
class Dummy_HelpSource:
path_ok = query.HelpSource.path_ok
def __init__(self, dummy_path):
self.path = Var(value=dummy_path)
self.path_error = {'text': ''}
def showerror(self, message, widget=None):
self.path_error['text'] = message
orig_platform = query.platform # Set in test_path_ok_file.
@classmethod
def tearDownClass(cls):
query.platform = cls.orig_platform
def test_path_ok_blank(self):
dialog = self.Dummy_HelpSource(' ')
self.assertEqual(dialog.path_ok(), None)
self.assertIn('no help file', dialog.path_error['text'])
def test_path_ok_bad(self):
dialog = self.Dummy_HelpSource(__file__ + 'bad-bad-bad')
self.assertEqual(dialog.path_ok(), None)
self.assertIn('not exist', dialog.path_error['text'])
def test_path_ok_web(self):
dialog = self.Dummy_HelpSource('')
Equal = self.assertEqual
for url in 'www.py.org', 'http://py.org':
with self.subTest():
dialog.path.set(url)
self.assertEqual(dialog.path_ok(), url)
self.assertEqual(dialog.path_error['text'], '')
def test_path_ok_file(self):
dialog = self.Dummy_HelpSource('')
for platform, prefix in ('darwin', 'file://'), ('other', ''):
with self.subTest():
query.platform = platform
dialog.path.set(__file__)
self.assertEqual(dialog.path_ok(), prefix + __file__)
self.assertEqual(dialog.path_error['text'], '')
class HelpsourceEntryokTest(unittest.TestCase):
"Test entry_ok method of HelpSource subclass of Query."
class Dummy_HelpSource:
entry_ok = query.HelpSource.entry_ok
entry_error = {}
path_error = {}
def item_ok(self):
return self.name
def path_ok(self):
return self.path
def test_entry_ok_helpsource(self):
dialog = self.Dummy_HelpSource()
for name, path, result in ((None, None, None),
(None, 'doc.txt', None),
('doc', None, None),
('doc', 'doc.txt', ('doc', 'doc.txt'))):
with self.subTest():
dialog.name, dialog.path = name, path
self.assertEqual(dialog.entry_ok(), result)
# 2 CustomRun test classes each test one method.
class CustomRunCLIargsokTest(unittest.TestCase):
"Test cli_ok method of the CustomRun subclass of Query."
class Dummy_CustomRun:
cli_args_ok = query.CustomRun.cli_args_ok
def __init__(self, dummy_entry):
self.entry = Var(value=dummy_entry)
self.entry_error = {'text': ''}
def showerror(self, message):
self.entry_error['text'] = message
def test_blank_args(self):
dialog = self.Dummy_CustomRun(' ')
self.assertEqual(dialog.cli_args_ok(), [])
def test_invalid_args(self):
dialog = self.Dummy_CustomRun("'no-closing-quote")
self.assertEqual(dialog.cli_args_ok(), None)
self.assertIn('No closing', dialog.entry_error['text'])
def test_good_args(self):
args = ['-n', '10', '--verbose', '-p', '/path', '--name']
dialog = self.Dummy_CustomRun(' '.join(args) + ' "my name"')
self.assertEqual(dialog.cli_args_ok(), args + ["my name"])
self.assertEqual(dialog.entry_error['text'], '')
class CustomRunEntryokTest(unittest.TestCase):
"Test entry_ok method of the CustomRun subclass of Query."
class Dummy_CustomRun:
entry_ok = query.CustomRun.entry_ok
entry_error = {}
restartvar = Var()
def cli_args_ok(self):
return self.cli_args
def test_entry_ok_customrun(self):
dialog = self.Dummy_CustomRun()
for restart in {True, False}:
dialog.restartvar.set(restart)
for cli_args, result in ((None, None),
(['my arg'], (['my arg'], restart))):
with self.subTest(restart=restart, cli_args=cli_args):
dialog.cli_args = cli_args
self.assertEqual(dialog.entry_ok(), result)
# GUI TESTS
class QueryGuiTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = root = Tk()
cls.root.withdraw()
cls.dialog = query.Query(root, 'TEST', 'test', _utest=True)
cls.dialog.destroy = mock.Mock()
@classmethod
def tearDownClass(cls):
del cls.dialog.destroy
del cls.dialog
cls.root.destroy()
del cls.root
def setUp(self):
self.dialog.entry.delete(0, 'end')
self.dialog.result = None
self.dialog.destroy.reset_mock()
def test_click_ok(self):
dialog = self.dialog
dialog.entry.insert(0, 'abc')
dialog.button_ok.invoke()
self.assertEqual(dialog.result, 'abc')
self.assertTrue(dialog.destroy.called)
def test_click_blank(self):
dialog = self.dialog
dialog.button_ok.invoke()
self.assertEqual(dialog.result, None)
self.assertFalse(dialog.destroy.called)
def test_click_cancel(self):
dialog = self.dialog
dialog.entry.insert(0, 'abc')
dialog.button_cancel.invoke()
self.assertEqual(dialog.result, None)
self.assertTrue(dialog.destroy.called)
class SectionnameGuiTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
def test_click_section_name(self):
root = Tk()
root.withdraw()
dialog = query.SectionName(root, 'T', 't', {'abc'}, _utest=True)
Equal = self.assertEqual
self.assertEqual(dialog.used_names, {'abc'})
dialog.entry.insert(0, 'okay')
dialog.button_ok.invoke()
self.assertEqual(dialog.result, 'okay')
root.destroy()
class ModulenameGuiTest(unittest.TestCase, ExtraAssertions):
@classmethod
def setUpClass(cls):
requires('gui')
def test_click_module_name(self):
root = Tk()
root.withdraw()
dialog = query.ModuleName(root, 'T', 't', 'idlelib', _utest=True)
self.assertEqual(dialog.text0, 'idlelib')
self.assertEqual(dialog.entry.get(), 'idlelib')
dialog.button_ok.invoke()
self.assertEndsWith(dialog.result, '__init__.py')
root.destroy()
class GotoGuiTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
def test_click_module_name(self):
root = Tk()
root.withdraw()
dialog = query.Goto(root, 'T', 't', _utest=True)
dialog.entry.insert(0, '22')
dialog.button_ok.invoke()
self.assertEqual(dialog.result, 22)
root.destroy()
class HelpsourceGuiTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
def test_click_help_source(self):
root = Tk()
root.withdraw()
dialog = query.HelpSource(root, 'T', menuitem='__test__',
filepath=__file__, _utest=True)
Equal = self.assertEqual
Equal(dialog.entry.get(), '__test__')
Equal(dialog.path.get(), __file__)
dialog.button_ok.invoke()
prefix = "file://" if sys.platform == 'darwin' else ''
Equal(dialog.result, ('__test__', prefix + __file__))
root.destroy()
class CustomRunGuiTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
def test_click_args(self):
root = Tk()
root.withdraw()
dialog = query.CustomRun(root, 'Title',
cli_args=['a', 'b=1'], _utest=True)
self.assertEqual(dialog.entry.get(), 'a b=1')
dialog.entry.insert(END, ' c')
dialog.button_ok.invoke()
self.assertEqual(dialog.result, (['a', 'b=1', 'c'], True))
root.destroy()
if __name__ == '__main__':
unittest.main(verbosity=2, exit=False)

View File

@ -0,0 +1,123 @@
"Test redirector, coverage 100%."
from idlelib.redirector import WidgetRedirector
import unittest
from test.support import requires
from test.support.testcase import ExtraAssertions
from tkinter import Tk, Text, TclError
from idlelib.idle_test.mock_idle import Func
class InitCloseTest(unittest.TestCase, ExtraAssertions):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.text = Text(cls.root)
@classmethod
def tearDownClass(cls):
del cls.text
cls.root.destroy()
del cls.root
def test_init(self):
redir = WidgetRedirector(self.text)
self.assertEqual(redir.widget, self.text)
self.assertEqual(redir.tk, self.text.tk)
self.assertRaises(TclError, WidgetRedirector, self.text)
redir.close() # restore self.tk, self.text
def test_close(self):
redir = WidgetRedirector(self.text)
redir.register('insert', Func)
redir.close()
self.assertEqual(redir._operations, {})
self.assertNotHasAttr(self.text, 'widget')
class WidgetRedirectorTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.text = Text(cls.root)
@classmethod
def tearDownClass(cls):
del cls.text
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def setUp(self):
self.redir = WidgetRedirector(self.text)
self.func = Func()
self.orig_insert = self.redir.register('insert', self.func)
self.text.insert('insert', 'asdf') # leaves self.text empty
def tearDown(self):
self.text.delete('1.0', 'end')
self.redir.close()
def test_repr(self): # partly for 100% coverage
self.assertIn('Redirector', repr(self.redir))
self.assertIn('Original', repr(self.orig_insert))
def test_register(self):
self.assertEqual(self.text.get('1.0', 'end'), '\n')
self.assertEqual(self.func.args, ('insert', 'asdf'))
self.assertIn('insert', self.redir._operations)
self.assertIn('insert', self.text.__dict__)
self.assertEqual(self.text.insert, self.func)
def test_original_command(self):
self.assertEqual(self.orig_insert.operation, 'insert')
self.assertEqual(self.orig_insert.tk_call, self.text.tk.call)
self.orig_insert('insert', 'asdf')
self.assertEqual(self.text.get('1.0', 'end'), 'asdf\n')
def test_unregister(self):
self.assertIsNone(self.redir.unregister('invalid operation name'))
self.assertEqual(self.redir.unregister('insert'), self.func)
self.assertNotIn('insert', self.redir._operations)
self.assertNotIn('insert', self.text.__dict__)
def test_unregister_no_attribute(self):
del self.text.insert
self.assertEqual(self.redir.unregister('insert'), self.func)
def test_dispatch_intercept(self):
self.func.__init__(True)
self.assertTrue(self.redir.dispatch('insert', False))
self.assertFalse(self.func.args[0])
def test_dispatch_bypass(self):
self.orig_insert('insert', 'asdf')
# tk.call returns '' where Python would return None
self.assertEqual(self.redir.dispatch('delete', '1.0', 'end'), '')
self.assertEqual(self.text.get('1.0', 'end'), '\n')
def test_dispatch_error(self):
self.func.__init__(TclError())
self.assertEqual(self.redir.dispatch('insert', False), '')
self.assertEqual(self.redir.dispatch('invalid'), '')
def test_command_dispatch(self):
# Test that .__init__ causes redirection of tk calls
# through redir.dispatch
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.root.call(self.text._w, 'insert', 'boo'), '')
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,294 @@
"Test replace, coverage 78%."
from idlelib.replace import ReplaceDialog
import unittest
from test.support import requires
requires('gui')
from tkinter import Tk, Text
from unittest.mock import Mock
from idlelib.idle_test.mock_tk import Mbox
import idlelib.searchengine as se
orig_mbox = se.messagebox
showerror = Mbox.showerror
class ReplaceDialogTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.root = Tk()
cls.root.withdraw()
se.messagebox = Mbox
cls.engine = se.SearchEngine(cls.root)
cls.dialog = ReplaceDialog(cls.root, cls.engine)
cls.dialog.bell = lambda: None
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.messagebox = 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')
replace()
equal(text.get('1.8', '1.12'), 'asdf')
# don't "match word" case
text.mark_set('insert', '1.0')
pv.set('is')
rv.set('hello')
replace()
equal(text.get('1.2', '1.7'), 'hello')
# don't "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(r'[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(r'[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(r'[\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)

View File

@ -0,0 +1,29 @@
"Test rpc, coverage 20%."
from idlelib import rpc
import unittest
class CodePicklerTest(unittest.TestCase):
def test_pickle_unpickle(self):
def f(): return a + b + c
func, (cbytes,) = rpc.pickle_code(f.__code__)
self.assertIs(func, rpc.unpickle_code)
self.assertIn(b'test_rpc.py', cbytes)
code = rpc.unpickle_code(cbytes)
self.assertEqual(code.co_names, ('a', 'b', 'c'))
def test_code_pickler(self):
self.assertIn(type((lambda:None).__code__),
rpc.CodePickler.dispatch_table)
def test_dumps(self):
def f(): pass
# The main test here is that pickling code does not raise.
self.assertIn(b'test_rpc.py', rpc.dumps(f.__code__))
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,433 @@
"Test run, coverage 54%."
from idlelib import run
import io
import sys
from test.support import captured_output, captured_stderr
import unittest
from unittest import mock
import idlelib
from idlelib.idle_test.mock_idle import Func
from test.support import force_not_colorized
idlelib.testing = True # Use {} for executing test user code.
class ExceptionTest(unittest.TestCase):
def test_print_exception_unhashable(self):
class UnhashableException(Exception):
def __eq__(self, other):
return True
ex1 = UnhashableException('ex1')
ex2 = UnhashableException('ex2')
try:
raise ex2 from ex1
except UnhashableException:
try:
raise ex1
except UnhashableException:
with captured_stderr() as output:
with mock.patch.object(run, 'cleanup_traceback') as ct:
ct.side_effect = lambda t, e: t
run.print_exception()
tb = output.getvalue().strip().splitlines()
self.assertEqual(11, len(tb))
self.assertIn('UnhashableException: ex2', tb[3])
self.assertIn('UnhashableException: ex1', tb[10])
data = (('1/0', ZeroDivisionError, "division by zero\n"),
('abc', NameError, "name 'abc' is not defined. "
"Did you mean: 'abs'? "
"Or did you forget to import 'abc'?\n"),
('int.reel', AttributeError,
"type object 'int' has no attribute 'reel'. "
"Did you mean: 'real'?\n"),
)
@force_not_colorized
def test_get_message(self):
for code, exc, msg in self.data:
with self.subTest(code=code):
try:
eval(compile(code, '', 'eval'))
except exc:
typ, val, tb = sys.exc_info()
actual = run.get_message_lines(typ, val, tb)[0]
expect = f'{exc.__name__}: {msg}'
self.assertEqual(actual, expect)
@force_not_colorized
@mock.patch.object(run, 'cleanup_traceback',
new_callable=lambda: (lambda t, e: None))
def test_get_multiple_message(self, mock):
d = self.data
data2 = ((d[0], d[1]), (d[1], d[2]), (d[2], d[0]))
subtests = 0
for (code1, exc1, msg1), (code2, exc2, msg2) in data2:
with self.subTest(codes=(code1,code2)):
try:
eval(compile(code1, '', 'eval'))
except exc1:
try:
eval(compile(code2, '', 'eval'))
except exc2:
with captured_stderr() as output:
run.print_exception()
actual = output.getvalue()
self.assertIn(msg1, actual)
self.assertIn(msg2, actual)
subtests += 1
self.assertEqual(subtests, len(data2)) # All subtests ran?
# StdioFile tests.
class S(str):
def __str__(self):
return '%s:str' % type(self).__name__
def __unicode__(self):
return '%s:unicode' % type(self).__name__
def __len__(self):
return 3
def __iter__(self):
return iter('abc')
def __getitem__(self, *args):
return '%s:item' % type(self).__name__
def __getslice__(self, *args):
return '%s:slice' % type(self).__name__
class MockShell:
def __init__(self):
self.reset()
def write(self, *args):
self.written.append(args)
def readline(self):
return self.lines.pop()
def close(self):
pass
def reset(self):
self.written = []
def push(self, lines):
self.lines = list(lines)[::-1]
class StdInputFilesTest(unittest.TestCase):
def test_misc(self):
shell = MockShell()
f = run.StdInputFile(shell, 'stdin')
self.assertIsInstance(f, io.TextIOBase)
self.assertEqual(f.encoding, 'utf-8')
self.assertEqual(f.errors, 'strict')
self.assertIsNone(f.newlines)
self.assertEqual(f.name, '<stdin>')
self.assertFalse(f.closed)
self.assertTrue(f.isatty())
self.assertTrue(f.readable())
self.assertFalse(f.writable())
self.assertFalse(f.seekable())
def test_unsupported(self):
shell = MockShell()
f = run.StdInputFile(shell, 'stdin')
self.assertRaises(OSError, f.fileno)
self.assertRaises(OSError, f.tell)
self.assertRaises(OSError, f.seek, 0)
self.assertRaises(OSError, f.write, 'x')
self.assertRaises(OSError, f.writelines, ['x'])
def test_read(self):
shell = MockShell()
f = run.StdInputFile(shell, 'stdin')
shell.push(['one\n', 'two\n', ''])
self.assertEqual(f.read(), 'one\ntwo\n')
shell.push(['one\n', 'two\n', ''])
self.assertEqual(f.read(-1), 'one\ntwo\n')
shell.push(['one\n', 'two\n', ''])
self.assertEqual(f.read(None), 'one\ntwo\n')
shell.push(['one\n', 'two\n', 'three\n', ''])
self.assertEqual(f.read(2), 'on')
self.assertEqual(f.read(3), 'e\nt')
self.assertEqual(f.read(10), 'wo\nthree\n')
shell.push(['one\n', 'two\n'])
self.assertEqual(f.read(0), '')
self.assertRaises(TypeError, f.read, 1.5)
self.assertRaises(TypeError, f.read, '1')
self.assertRaises(TypeError, f.read, 1, 1)
def test_readline(self):
shell = MockShell()
f = run.StdInputFile(shell, 'stdin')
shell.push(['one\n', 'two\n', 'three\n', 'four\n'])
self.assertEqual(f.readline(), 'one\n')
self.assertEqual(f.readline(-1), 'two\n')
self.assertEqual(f.readline(None), 'three\n')
shell.push(['one\ntwo\n'])
self.assertEqual(f.readline(), 'one\n')
self.assertEqual(f.readline(), 'two\n')
shell.push(['one', 'two', 'three'])
self.assertEqual(f.readline(), 'one')
self.assertEqual(f.readline(), 'two')
shell.push(['one\n', 'two\n', 'three\n'])
self.assertEqual(f.readline(2), 'on')
self.assertEqual(f.readline(1), 'e')
self.assertEqual(f.readline(1), '\n')
self.assertEqual(f.readline(10), 'two\n')
shell.push(['one\n', 'two\n'])
self.assertEqual(f.readline(0), '')
self.assertRaises(TypeError, f.readlines, 1.5)
self.assertRaises(TypeError, f.readlines, '1')
self.assertRaises(TypeError, f.readlines, 1, 1)
def test_readlines(self):
shell = MockShell()
f = run.StdInputFile(shell, 'stdin')
shell.push(['one\n', 'two\n', ''])
self.assertEqual(f.readlines(), ['one\n', 'two\n'])
shell.push(['one\n', 'two\n', ''])
self.assertEqual(f.readlines(-1), ['one\n', 'two\n'])
shell.push(['one\n', 'two\n', ''])
self.assertEqual(f.readlines(None), ['one\n', 'two\n'])
shell.push(['one\n', 'two\n', ''])
self.assertEqual(f.readlines(0), ['one\n', 'two\n'])
shell.push(['one\n', 'two\n', ''])
self.assertEqual(f.readlines(3), ['one\n'])
shell.push(['one\n', 'two\n', ''])
self.assertEqual(f.readlines(4), ['one\n', 'two\n'])
shell.push(['one\n', 'two\n', ''])
self.assertRaises(TypeError, f.readlines, 1.5)
self.assertRaises(TypeError, f.readlines, '1')
self.assertRaises(TypeError, f.readlines, 1, 1)
def test_close(self):
shell = MockShell()
f = run.StdInputFile(shell, 'stdin')
shell.push(['one\n', 'two\n', ''])
self.assertFalse(f.closed)
self.assertEqual(f.readline(), 'one\n')
f.close()
self.assertFalse(f.closed)
self.assertEqual(f.readline(), 'two\n')
self.assertRaises(TypeError, f.close, 1)
class StdOutputFilesTest(unittest.TestCase):
def test_misc(self):
shell = MockShell()
f = run.StdOutputFile(shell, 'stdout')
self.assertIsInstance(f, io.TextIOBase)
self.assertEqual(f.encoding, 'utf-8')
self.assertEqual(f.errors, 'strict')
self.assertIsNone(f.newlines)
self.assertEqual(f.name, '<stdout>')
self.assertFalse(f.closed)
self.assertTrue(f.isatty())
self.assertFalse(f.readable())
self.assertTrue(f.writable())
self.assertFalse(f.seekable())
def test_unsupported(self):
shell = MockShell()
f = run.StdOutputFile(shell, 'stdout')
self.assertRaises(OSError, f.fileno)
self.assertRaises(OSError, f.tell)
self.assertRaises(OSError, f.seek, 0)
self.assertRaises(OSError, f.read, 0)
self.assertRaises(OSError, f.readline, 0)
def test_write(self):
shell = MockShell()
f = run.StdOutputFile(shell, 'stdout')
f.write('test')
self.assertEqual(shell.written, [('test', 'stdout')])
shell.reset()
f.write('t\xe8\u015b\U0001d599')
self.assertEqual(shell.written, [('t\xe8\u015b\U0001d599', 'stdout')])
shell.reset()
f.write(S('t\xe8\u015b\U0001d599'))
self.assertEqual(shell.written, [('t\xe8\u015b\U0001d599', 'stdout')])
self.assertEqual(type(shell.written[0][0]), str)
shell.reset()
self.assertRaises(TypeError, f.write)
self.assertEqual(shell.written, [])
self.assertRaises(TypeError, f.write, b'test')
self.assertRaises(TypeError, f.write, 123)
self.assertEqual(shell.written, [])
self.assertRaises(TypeError, f.write, 'test', 'spam')
self.assertEqual(shell.written, [])
def test_write_stderr_nonencodable(self):
shell = MockShell()
f = run.StdOutputFile(shell, 'stderr', 'iso-8859-15', 'backslashreplace')
f.write('t\xe8\u015b\U0001d599\xa4')
self.assertEqual(shell.written, [('t\xe8\\u015b\\U0001d599\\xa4', 'stderr')])
shell.reset()
f.write(S('t\xe8\u015b\U0001d599\xa4'))
self.assertEqual(shell.written, [('t\xe8\\u015b\\U0001d599\\xa4', 'stderr')])
self.assertEqual(type(shell.written[0][0]), str)
shell.reset()
self.assertRaises(TypeError, f.write)
self.assertEqual(shell.written, [])
self.assertRaises(TypeError, f.write, b'test')
self.assertRaises(TypeError, f.write, 123)
self.assertEqual(shell.written, [])
self.assertRaises(TypeError, f.write, 'test', 'spam')
self.assertEqual(shell.written, [])
def test_writelines(self):
shell = MockShell()
f = run.StdOutputFile(shell, 'stdout')
f.writelines([])
self.assertEqual(shell.written, [])
shell.reset()
f.writelines(['one\n', 'two'])
self.assertEqual(shell.written,
[('one\n', 'stdout'), ('two', 'stdout')])
shell.reset()
f.writelines(['on\xe8\n', 'tw\xf2'])
self.assertEqual(shell.written,
[('on\xe8\n', 'stdout'), ('tw\xf2', 'stdout')])
shell.reset()
f.writelines([S('t\xe8st')])
self.assertEqual(shell.written, [('t\xe8st', 'stdout')])
self.assertEqual(type(shell.written[0][0]), str)
shell.reset()
self.assertRaises(TypeError, f.writelines)
self.assertEqual(shell.written, [])
self.assertRaises(TypeError, f.writelines, 123)
self.assertEqual(shell.written, [])
self.assertRaises(TypeError, f.writelines, [b'test'])
self.assertRaises(TypeError, f.writelines, [123])
self.assertEqual(shell.written, [])
self.assertRaises(TypeError, f.writelines, [], [])
self.assertEqual(shell.written, [])
def test_close(self):
shell = MockShell()
f = run.StdOutputFile(shell, 'stdout')
self.assertFalse(f.closed)
f.write('test')
f.close()
self.assertTrue(f.closed)
self.assertRaises(ValueError, f.write, 'x')
self.assertEqual(shell.written, [('test', 'stdout')])
f.close()
self.assertRaises(TypeError, f.close, 1)
class RecursionLimitTest(unittest.TestCase):
# Test (un)install_recursionlimit_wrappers and fixdoc.
def test_bad_setrecursionlimit_calls(self):
run.install_recursionlimit_wrappers()
self.addCleanup(run.uninstall_recursionlimit_wrappers)
f = sys.setrecursionlimit
self.assertRaises(TypeError, f, limit=100)
self.assertRaises(TypeError, f, 100, 1000)
self.assertRaises(ValueError, f, 0)
def test_roundtrip(self):
run.install_recursionlimit_wrappers()
self.addCleanup(run.uninstall_recursionlimit_wrappers)
# Check that setting the recursion limit works.
orig_reclimit = sys.getrecursionlimit()
self.addCleanup(sys.setrecursionlimit, orig_reclimit)
sys.setrecursionlimit(orig_reclimit + 3)
# Check that the new limit is returned by sys.getrecursionlimit().
new_reclimit = sys.getrecursionlimit()
self.assertEqual(new_reclimit, orig_reclimit + 3)
def test_default_recursion_limit_preserved(self):
orig_reclimit = sys.getrecursionlimit()
run.install_recursionlimit_wrappers()
self.addCleanup(run.uninstall_recursionlimit_wrappers)
new_reclimit = sys.getrecursionlimit()
self.assertEqual(new_reclimit, orig_reclimit)
def test_fixdoc(self):
# Put here until better place for miscellaneous test.
def func(): "docstring"
run.fixdoc(func, "more")
self.assertEqual(func.__doc__, "docstring\n\nmore")
func.__doc__ = None
run.fixdoc(func, "more")
self.assertEqual(func.__doc__, "more")
class HandleErrorTest(unittest.TestCase):
# Method of MyRPCServer
def test_fatal_error(self):
eq = self.assertEqual
with captured_output('__stderr__') as err,\
mock.patch('idlelib.run.thread.interrupt_main',
new_callable=Func) as func:
try:
raise EOFError
except EOFError:
run.MyRPCServer.handle_error(None, 'abc', '123')
eq(run.exit_now, True)
run.exit_now = False
eq(err.getvalue(), '')
try:
raise IndexError
except IndexError:
run.MyRPCServer.handle_error(None, 'abc', '123')
eq(run.quitting, True)
run.quitting = False
msg = err.getvalue()
self.assertIn('abc', msg)
self.assertIn('123', msg)
self.assertIn('IndexError', msg)
eq(func.called, 2)
class ExecRuncodeTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.addClassCleanup(setattr,run,'print_exception',run.print_exception)
cls.prt = Func() # Need reference.
run.print_exception = cls.prt
mockrpc = mock.Mock()
mockrpc.console.getvar = Func(result=False)
cls.ex = run.Executive(mockrpc)
@classmethod
def tearDownClass(cls):
assert sys.excepthook == sys.__excepthook__
def test_exceptions(self):
ex = self.ex
ex.runcode('1/0')
self.assertIs(ex.user_exc_info[0], ZeroDivisionError)
self.addCleanup(setattr, sys, 'excepthook', sys.__excepthook__)
sys.excepthook = lambda t, e, tb: run.print_exception(t)
ex.runcode('1/0')
self.assertIs(self.prt.args[0], ZeroDivisionError)
sys.excepthook = lambda: None
ex.runcode('1/0')
t, e, tb = ex.user_exc_info
self.assertIs(t, TypeError)
self.assertTrue(isinstance(e.__context__, ZeroDivisionError))
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,33 @@
"Test runscript, coverage 16%."
from idlelib import runscript
import unittest
from test.support import requires
from tkinter import Tk
from idlelib.editor import EditorWindow
class ScriptBindingTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
def test_init(self):
ew = EditorWindow(root=self.root)
sb = runscript.ScriptBinding(ew)
ew._close()
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,27 @@
"Test scrolledlist, coverage 38%."
from idlelib.scrolledlist import ScrolledList
import unittest
from test.support import requires
requires('gui')
from tkinter import Tk
class ScrolledListTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.root = Tk()
@classmethod
def tearDownClass(cls):
cls.root.destroy()
del cls.root
def test_init(self):
ScrolledList(self.root)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,80 @@
"Test search, coverage 69%."
from idlelib import search
import unittest
from test.support import requires
requires('gui')
from tkinter import Tk, Text, BooleanVar
from idlelib import searchengine
# Does not currently test the event handler wrappers.
# A usage test should simulate clicks and check highlighting.
# Tests need to be coordinated with SearchDialogBase tests
# to avoid duplication.
class SearchDialogTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.root = Tk()
@classmethod
def tearDownClass(cls):
cls.root.destroy()
del cls.root
def setUp(self):
self.engine = searchengine.SearchEngine(self.root)
self.dialog = search.SearchDialog(self.root, self.engine)
self.dialog.bell = lambda: None
self.text = 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.dialog.bell = lambda: None
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)

View File

@ -0,0 +1,160 @@
"Test searchbase, coverage 98%."
# The only thing not covered is inconsequential --
# testing skipping of suite when self.needwrapbutton is false.
import unittest
from test.support import requires
from tkinter import Text, Tk, Toplevel
from tkinter.ttk import Frame
from idlelib import searchengine as se
from idlelib import searchbase as sdb
from idlelib.idle_test.mock_idle import Func
## from idlelib.idle_test.mock_tk import Var
# The ## imports above & following could help make some tests gui-free.
# However, they currently make radiobutton tests fail.
##def setUpModule():
## # Replace tk objects used to initialize se.SearchEngine.
## se.BooleanVar = Var
## se.StringVar = Var
##
##def tearDownModule():
## se.BooleanVar = BooleanVar
## se.StringVar = StringVar
class SearchDialogBaseTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def setUp(self):
self.engine = se.SearchEngine(self.root) # None also seems to work
self.dialog = sdb.SearchDialogBase(root=self.root, engine=self.engine)
def tearDown(self):
self.dialog.close()
def test_open_and_close(self):
# open calls create_widgets, which needs default_command
self.dialog.default_command = None
toplevel = Toplevel(self.root)
text = Text(toplevel)
self.dialog.open(text)
self.assertEqual(self.dialog.top.state(), 'normal')
self.dialog.close()
self.assertEqual(self.dialog.top.state(), 'withdrawn')
self.dialog.open(text, searchphrase="hello")
self.assertEqual(self.dialog.ent.get(), 'hello')
toplevel.update_idletasks()
toplevel.destroy()
def test_create_widgets(self):
self.dialog.create_entries = Func()
self.dialog.create_option_buttons = Func()
self.dialog.create_other_buttons = Func()
self.dialog.create_command_buttons = Func()
self.dialog.default_command = None
self.dialog.create_widgets()
self.assertTrue(self.dialog.create_entries.called)
self.assertTrue(self.dialog.create_option_buttons.called)
self.assertTrue(self.dialog.create_other_buttons.called)
self.assertTrue(self.dialog.create_command_buttons.called)
def test_make_entry(self):
equal = self.assertEqual
self.dialog.row = 0
self.dialog.frame = Frame(self.root)
entry, label = self.dialog.make_entry("Test:", 'hello')
equal(label['text'], 'Test:')
self.assertIn(entry.get(), 'hello')
egi = entry.grid_info()
equal(int(egi['row']), 0)
equal(int(egi['column']), 1)
equal(int(egi['rowspan']), 1)
equal(int(egi['columnspan']), 1)
equal(self.dialog.row, 1)
def test_create_entries(self):
self.dialog.frame = Frame(self.root)
self.dialog.row = 0
self.engine.setpat('hello')
self.dialog.create_entries()
self.assertIn(self.dialog.ent.get(), 'hello')
def test_make_frame(self):
self.dialog.row = 0
self.dialog.frame = Frame(self.root)
frame, label = self.dialog.make_frame()
self.assertEqual(label, '')
self.assertEqual(str(type(frame)), "<class 'tkinter.ttk.Frame'>")
# self.assertIsInstance(frame, Frame) fails when test is run by
# test_idle not run from IDLE editor. See issue 33987 PR.
frame, label = self.dialog.make_frame('testlabel')
self.assertEqual(label['text'], 'testlabel')
def btn_test_setup(self, meth):
self.dialog.frame = Frame(self.root)
self.dialog.row = 0
return meth()
def test_create_option_buttons(self):
e = self.engine
for state in (0, 1):
for var in (e.revar, e.casevar, e.wordvar, e.wrapvar):
var.set(state)
frame, options = self.btn_test_setup(
self.dialog.create_option_buttons)
for spec, button in zip (options, frame.pack_slaves()):
var, label = spec
self.assertEqual(button['text'], label)
self.assertEqual(var.get(), state)
def test_create_other_buttons(self):
for state in (False, True):
var = self.engine.backvar
var.set(state)
frame, others = self.btn_test_setup(
self.dialog.create_other_buttons)
buttons = frame.pack_slaves()
for spec, button in zip(others, buttons):
val, label = spec
self.assertEqual(button['text'], label)
if val == state:
# hit other button, then this one
# indexes depend on button order
self.assertEqual(var.get(), state)
def test_make_button(self):
self.dialog.frame = Frame(self.root)
self.dialog.buttonframe = Frame(self.dialog.frame)
btn = self.dialog.make_button('Test', self.dialog.close)
self.assertEqual(btn['text'], 'Test')
def test_create_command_buttons(self):
self.dialog.frame = Frame(self.root)
self.dialog.create_command_buttons()
# Look for close button command in buttonframe
closebuttoncommand = ''
for child in self.dialog.buttonframe.winfo_children():
if child['text'] == 'Close':
closebuttoncommand = child['command']
self.assertIn('close', closebuttoncommand)
if __name__ == '__main__':
unittest.main(verbosity=2, exit=2)

View File

@ -0,0 +1,332 @@
"Test searchengine, coverage 99%."
from idlelib import searchengine as se
import unittest
# from test.support import requires
from tkinter import BooleanVar, StringVar, TclError # ,Tk, Text
from tkinter import messagebox
from idlelib.idle_test.mock_tk import Var, Mbox
from idlelib.idle_test.mock_tk import Text as mockText
import re
# With mock replacements, the module does not use any gui widgets.
# The use of tk.Text is avoided (for now, until mock Text is improved)
# by patching instances with an index function returning what is needed.
# This works because mock Text.get does not use .index.
# The tkinter imports are used to restore searchengine.
def setUpModule():
# Replace s-e module tkinter imports other than non-gui TclError.
se.BooleanVar = Var
se.StringVar = Var
se.messagebox = Mbox
def tearDownModule():
# Restore 'just in case', though other tests should also replace.
se.BooleanVar = BooleanVar
se.StringVar = StringVar
se.messagebox = messagebox
class Mock:
def __init__(self, *args, **kwargs): pass
class GetTest(unittest.TestCase):
# SearchEngine.get returns singleton created & saved on first call.
def test_get(self):
saved_Engine = se.SearchEngine
se.SearchEngine = Mock # monkey-patch class
try:
root = Mock()
engine = se.get(root)
self.assertIsInstance(engine, se.SearchEngine)
self.assertIs(root._searchengine, engine)
self.assertIs(se.get(root), engine)
finally:
se.SearchEngine = saved_Engine # restore class to module
class GetLineColTest(unittest.TestCase):
# Test simple text-independent helper function
def test_get_line_col(self):
self.assertEqual(se.get_line_col('1.0'), (1, 0))
self.assertEqual(se.get_line_col('1.11'), (1, 11))
self.assertRaises(ValueError, se.get_line_col, ('1.0 lineend'))
self.assertRaises(ValueError, se.get_line_col, ('end'))
class GetSelectionTest(unittest.TestCase):
# Test text-dependent helper function.
## # Need gui for text.index('sel.first/sel.last/insert').
## @classmethod
## def setUpClass(cls):
## requires('gui')
## cls.root = Tk()
##
## @classmethod
## def tearDownClass(cls):
## cls.root.destroy()
## del cls.root
def test_get_selection(self):
# text = Text(master=self.root)
text = mockText()
text.insert('1.0', 'Hello World!')
# fix text.index result when called in get_selection
def sel(s):
# select entire text, cursor irrelevant
if s == 'sel.first': return '1.0'
if s == 'sel.last': return '1.12'
raise TclError
text.index = sel # replaces .tag_add('sel', '1.0, '1.12')
self.assertEqual(se.get_selection(text), ('1.0', '1.12'))
def mark(s):
# no selection, cursor after 'Hello'
if s == 'insert': return '1.5'
raise TclError
text.index = mark # replaces .mark_set('insert', '1.5')
self.assertEqual(se.get_selection(text), ('1.5', '1.5'))
class ReverseSearchTest(unittest.TestCase):
# Test helper function that searches backwards within a line.
def test_search_reverse(self):
Equal = self.assertEqual
line = "Here is an 'is' test text."
prog = re.compile('is')
Equal(se.search_reverse(prog, line, len(line)).span(), (12, 14))
Equal(se.search_reverse(prog, line, 14).span(), (12, 14))
Equal(se.search_reverse(prog, line, 13).span(), (5, 7))
Equal(se.search_reverse(prog, line, 7).span(), (5, 7))
Equal(se.search_reverse(prog, line, 6), None)
class SearchEngineTest(unittest.TestCase):
# Test class methods that do not use Text widget.
def setUp(self):
self.engine = se.SearchEngine(root=None)
# Engine.root is only used to create error message boxes.
# The mock replacement ignores the root argument.
def test_is_get(self):
engine = self.engine
Equal = self.assertEqual
Equal(engine.getpat(), '')
engine.setpat('hello')
Equal(engine.getpat(), 'hello')
Equal(engine.isre(), False)
engine.revar.set(1)
Equal(engine.isre(), True)
Equal(engine.iscase(), False)
engine.casevar.set(1)
Equal(engine.iscase(), True)
Equal(engine.isword(), False)
engine.wordvar.set(1)
Equal(engine.isword(), True)
Equal(engine.iswrap(), True)
engine.wrapvar.set(0)
Equal(engine.iswrap(), False)
Equal(engine.isback(), False)
engine.backvar.set(1)
Equal(engine.isback(), True)
def test_setcookedpat(self):
engine = self.engine
engine.setcookedpat(r'\s')
self.assertEqual(engine.getpat(), r'\s')
engine.revar.set(1)
engine.setcookedpat(r'\s')
self.assertEqual(engine.getpat(), r'\\s')
def test_getcookedpat(self):
engine = self.engine
Equal = self.assertEqual
Equal(engine.getcookedpat(), '')
engine.setpat('hello')
Equal(engine.getcookedpat(), 'hello')
engine.wordvar.set(True)
Equal(engine.getcookedpat(), r'\bhello\b')
engine.wordvar.set(False)
engine.setpat(r'\s')
Equal(engine.getcookedpat(), r'\\s')
engine.revar.set(True)
Equal(engine.getcookedpat(), r'\s')
def test_getprog(self):
engine = self.engine
Equal = self.assertEqual
engine.setpat('Hello')
temppat = engine.getprog()
Equal(temppat.pattern, re.compile('Hello', re.IGNORECASE).pattern)
engine.casevar.set(1)
temppat = engine.getprog()
Equal(temppat.pattern, re.compile('Hello').pattern, 0)
engine.setpat('')
Equal(engine.getprog(), None)
Equal(Mbox.showerror.message,
'Error: Empty regular expression')
engine.setpat('+')
engine.revar.set(1)
Equal(engine.getprog(), None)
Equal(Mbox.showerror.message,
'Error: nothing to repeat\nPattern: +\nOffset: 0')
def test_report_error(self):
showerror = Mbox.showerror
Equal = self.assertEqual
pat = '[a-z'
msg = 'unexpected end of regular expression'
Equal(self.engine.report_error(pat, msg), None)
Equal(showerror.title, 'Regular expression error')
expected_message = ("Error: " + msg + "\nPattern: [a-z")
Equal(showerror.message, expected_message)
Equal(self.engine.report_error(pat, msg, 5), None)
Equal(showerror.title, 'Regular expression error')
expected_message += "\nOffset: 5"
Equal(showerror.message, expected_message)
class SearchTest(unittest.TestCase):
# Test that search_text makes right call to right method.
@classmethod
def setUpClass(cls):
## requires('gui')
## cls.root = Tk()
## cls.text = Text(master=cls.root)
cls.text = mockText()
test_text = (
'First line\n'
'Line with target\n'
'Last line\n')
cls.text.insert('1.0', test_text)
cls.pat = re.compile('target')
cls.engine = se.SearchEngine(None)
cls.engine.search_forward = lambda *args: ('f', args)
cls.engine.search_backward = lambda *args: ('b', args)
## @classmethod
## def tearDownClass(cls):
## cls.root.destroy()
## del cls.root
def test_search(self):
Equal = self.assertEqual
engine = self.engine
search = engine.search_text
text = self.text
pat = self.pat
engine.patvar.set(None)
#engine.revar.set(pat)
Equal(search(text), None)
def mark(s):
# no selection, cursor after 'Hello'
if s == 'insert': return '1.5'
raise TclError
text.index = mark
Equal(search(text, pat), ('f', (text, pat, 1, 5, True, False)))
engine.wrapvar.set(False)
Equal(search(text, pat), ('f', (text, pat, 1, 5, False, False)))
engine.wrapvar.set(True)
engine.backvar.set(True)
Equal(search(text, pat), ('b', (text, pat, 1, 5, True, False)))
engine.backvar.set(False)
def sel(s):
if s == 'sel.first': return '2.10'
if s == 'sel.last': return '2.16'
raise TclError
text.index = sel
Equal(search(text, pat), ('f', (text, pat, 2, 16, True, False)))
Equal(search(text, pat, True), ('f', (text, pat, 2, 10, True, True)))
engine.backvar.set(True)
Equal(search(text, pat), ('b', (text, pat, 2, 10, True, False)))
Equal(search(text, pat, True), ('b', (text, pat, 2, 16, True, True)))
class ForwardBackwardTest(unittest.TestCase):
# Test that search_forward method finds the target.
## @classmethod
## def tearDownClass(cls):
## cls.root.destroy()
## del cls.root
@classmethod
def setUpClass(cls):
cls.engine = se.SearchEngine(None)
## requires('gui')
## cls.root = Tk()
## cls.text = Text(master=cls.root)
cls.text = mockText()
# search_backward calls index('end-1c')
cls.text.index = lambda index: '4.0'
test_text = (
'First line\n'
'Line with target\n'
'Last line\n')
cls.text.insert('1.0', test_text)
cls.pat = re.compile('target')
cls.res = (2, (10, 16)) # line, slice indexes of 'target'
cls.failpat = re.compile('xyz') # not in text
cls.emptypat = re.compile(r'\w*') # empty match possible
def make_search(self, func):
def search(pat, line, col, wrap, ok=0):
res = func(self.text, pat, line, col, wrap, ok)
# res is (line, matchobject) or None
return (res[0], res[1].span()) if res else res
return search
def test_search_forward(self):
# search for non-empty match
Equal = self.assertEqual
forward = self.make_search(self.engine.search_forward)
pat = self.pat
Equal(forward(pat, 1, 0, True), self.res)
Equal(forward(pat, 3, 0, True), self.res) # wrap
Equal(forward(pat, 3, 0, False), None) # no wrap
Equal(forward(pat, 2, 10, False), self.res)
Equal(forward(self.failpat, 1, 0, True), None)
Equal(forward(self.emptypat, 2, 9, True, ok=True), (2, (9, 9)))
#Equal(forward(self.emptypat, 2, 9, True), self.res)
# While the initial empty match is correctly ignored, skipping
# the rest of the line and returning (3, (0,4)) seems buggy - tjr.
Equal(forward(self.emptypat, 2, 10, True), self.res)
def test_search_backward(self):
# search for non-empty match
Equal = self.assertEqual
backward = self.make_search(self.engine.search_backward)
pat = self.pat
Equal(backward(pat, 3, 5, True), self.res)
Equal(backward(pat, 2, 0, True), self.res) # wrap
Equal(backward(pat, 2, 0, False), None) # no wrap
Equal(backward(pat, 2, 16, False), self.res)
Equal(backward(self.failpat, 3, 9, True), None)
Equal(backward(self.emptypat, 2, 10, True, ok=True), (2, (9,9)))
# Accepted because 9 < 10, not because ok=True.
# It is not clear that ok=True is useful going back - tjr
Equal(backward(self.emptypat, 2, 9, True), (2, (5, 9)))
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,775 @@
"""Test sidebar, coverage 85%"""
from textwrap import dedent
import sys
from itertools import chain
import unittest
import unittest.mock
from test.support import adjust_int_max_str_digits, requires, swap_attr
from test.support.testcase import ExtraAssertions
import tkinter as tk
from idlelib.idle_test.tkinter_testing_utils import run_in_tk_mainloop
from idlelib.delegator import Delegator
from idlelib.editor import fixwordbreaks
from idlelib.percolator import Percolator
import idlelib.pyshell
from idlelib.pyshell import fix_x11_paste, PyShell, PyShellFileList
from idlelib.run import fix_scaling
import idlelib.sidebar
from idlelib.sidebar import get_end_linenumber, get_lineno
class Dummy_editwin:
def __init__(self, text):
self.text = text
self.text_frame = self.text.master
self.per = Percolator(text)
self.undo = Delegator()
self.per.insertfilter(self.undo)
def setvar(self, name, value):
pass
def getlineno(self, index):
return int(float(self.text.index(index)))
class LineNumbersTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = tk.Tk()
cls.root.withdraw()
cls.text_frame = tk.Frame(cls.root)
cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
cls.text_frame.rowconfigure(1, weight=1)
cls.text_frame.columnconfigure(1, weight=1)
cls.text = tk.Text(cls.text_frame, width=80, height=24, wrap=tk.NONE)
cls.text.grid(row=1, column=1, sticky=tk.NSEW)
cls.editwin = Dummy_editwin(cls.text)
cls.editwin.vbar = tk.Scrollbar(cls.text_frame)
@classmethod
def tearDownClass(cls):
cls.editwin.per.close()
cls.root.update_idletasks()
cls.root.destroy()
del cls.text, cls.text_frame, cls.editwin, cls.root
def setUp(self):
self.linenumber = idlelib.sidebar.LineNumbers(self.editwin)
self.highlight_cfg = {"background": '#abcdef',
"foreground": '#123456'}
orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight
def mock_idleconf_GetHighlight(theme, element):
if element == 'linenumber':
return self.highlight_cfg
return orig_idleConf_GetHighlight(theme, element)
GetHighlight_patcher = unittest.mock.patch.object(
idlelib.sidebar.idleConf, 'GetHighlight', mock_idleconf_GetHighlight)
GetHighlight_patcher.start()
self.addCleanup(GetHighlight_patcher.stop)
self.font_override = 'TkFixedFont'
def mock_idleconf_GetFont(root, configType, section):
return self.font_override
GetFont_patcher = unittest.mock.patch.object(
idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont)
GetFont_patcher.start()
self.addCleanup(GetFont_patcher.stop)
def tearDown(self):
self.text.delete('1.0', 'end')
def get_selection(self):
return tuple(map(str, self.text.tag_ranges('sel')))
def get_line_screen_position(self, line):
bbox = self.linenumber.sidebar_text.bbox(f'{line}.end -1c')
x = bbox[0] + 2
y = bbox[1] + 2
return x, y
def assert_state_disabled(self):
state = self.linenumber.sidebar_text.config()['state']
self.assertEqual(state[-1], tk.DISABLED)
def get_sidebar_text_contents(self):
return self.linenumber.sidebar_text.get('1.0', tk.END)
def assert_sidebar_n_lines(self, n_lines):
expected = '\n'.join(chain(map(str, range(1, n_lines + 1)), ['']))
self.assertEqual(self.get_sidebar_text_contents(), expected)
def assert_text_equals(self, expected):
return self.assertEqual(self.text.get('1.0', 'end'), expected)
def test_init_empty(self):
self.assert_sidebar_n_lines(1)
def test_init_not_empty(self):
self.text.insert('insert', 'foo bar\n'*3)
self.assert_text_equals('foo bar\n'*3 + '\n')
self.assert_sidebar_n_lines(4)
def test_toggle_linenumbering(self):
self.assertEqual(self.linenumber.is_shown, False)
self.linenumber.show_sidebar()
self.assertEqual(self.linenumber.is_shown, True)
self.linenumber.hide_sidebar()
self.assertEqual(self.linenumber.is_shown, False)
self.linenumber.hide_sidebar()
self.assertEqual(self.linenumber.is_shown, False)
self.linenumber.show_sidebar()
self.assertEqual(self.linenumber.is_shown, True)
self.linenumber.show_sidebar()
self.assertEqual(self.linenumber.is_shown, True)
def test_insert(self):
self.text.insert('insert', 'foobar')
self.assert_text_equals('foobar\n')
self.assert_sidebar_n_lines(1)
self.assert_state_disabled()
self.text.insert('insert', '\nfoo')
self.assert_text_equals('foobar\nfoo\n')
self.assert_sidebar_n_lines(2)
self.assert_state_disabled()
self.text.insert('insert', 'hello\n'*2)
self.assert_text_equals('foobar\nfoohello\nhello\n\n')
self.assert_sidebar_n_lines(4)
self.assert_state_disabled()
self.text.insert('insert', '\nworld')
self.assert_text_equals('foobar\nfoohello\nhello\n\nworld\n')
self.assert_sidebar_n_lines(5)
self.assert_state_disabled()
def test_delete(self):
self.text.insert('insert', 'foobar')
self.assert_text_equals('foobar\n')
self.text.delete('1.1', '1.3')
self.assert_text_equals('fbar\n')
self.assert_sidebar_n_lines(1)
self.assert_state_disabled()
self.text.insert('insert', 'foo\n'*2)
self.assert_text_equals('fbarfoo\nfoo\n\n')
self.assert_sidebar_n_lines(3)
self.assert_state_disabled()
# Deleting up to "2.end" doesn't delete the final newline.
self.text.delete('2.0', '2.end')
self.assert_text_equals('fbarfoo\n\n\n')
self.assert_sidebar_n_lines(3)
self.assert_state_disabled()
self.text.delete('1.3', 'end')
self.assert_text_equals('fba\n')
self.assert_sidebar_n_lines(1)
self.assert_state_disabled()
# Text widgets always keep a single '\n' character at the end.
self.text.delete('1.0', 'end')
self.assert_text_equals('\n')
self.assert_sidebar_n_lines(1)
self.assert_state_disabled()
def test_sidebar_text_width(self):
"""
Test that linenumber text widget is always at the minimum
width
"""
def get_width():
return self.linenumber.sidebar_text.config()['width'][-1]
self.assert_sidebar_n_lines(1)
self.assertEqual(get_width(), 1)
self.text.insert('insert', 'foo')
self.assert_sidebar_n_lines(1)
self.assertEqual(get_width(), 1)
self.text.insert('insert', 'foo\n'*8)
self.assert_sidebar_n_lines(9)
self.assertEqual(get_width(), 1)
self.text.insert('insert', 'foo\n')
self.assert_sidebar_n_lines(10)
self.assertEqual(get_width(), 2)
self.text.insert('insert', 'foo\n')
self.assert_sidebar_n_lines(11)
self.assertEqual(get_width(), 2)
self.text.delete('insert -1l linestart', 'insert linestart')
self.assert_sidebar_n_lines(10)
self.assertEqual(get_width(), 2)
self.text.delete('insert -1l linestart', 'insert linestart')
self.assert_sidebar_n_lines(9)
self.assertEqual(get_width(), 1)
self.text.insert('insert', 'foo\n'*90)
self.assert_sidebar_n_lines(99)
self.assertEqual(get_width(), 2)
self.text.insert('insert', 'foo\n')
self.assert_sidebar_n_lines(100)
self.assertEqual(get_width(), 3)
self.text.insert('insert', 'foo\n')
self.assert_sidebar_n_lines(101)
self.assertEqual(get_width(), 3)
self.text.delete('insert -1l linestart', 'insert linestart')
self.assert_sidebar_n_lines(100)
self.assertEqual(get_width(), 3)
self.text.delete('insert -1l linestart', 'insert linestart')
self.assert_sidebar_n_lines(99)
self.assertEqual(get_width(), 2)
self.text.delete('50.0 -1c', 'end -1c')
self.assert_sidebar_n_lines(49)
self.assertEqual(get_width(), 2)
self.text.delete('5.0 -1c', 'end -1c')
self.assert_sidebar_n_lines(4)
self.assertEqual(get_width(), 1)
# Text widgets always keep a single '\n' character at the end.
self.text.delete('1.0', 'end -1c')
self.assert_sidebar_n_lines(1)
self.assertEqual(get_width(), 1)
# The following tests are temporarily disabled due to relying on
# simulated user input and inspecting which text is selected, which
# are fragile and can fail when several GUI tests are run in parallel
# or when the windows created by the test lose focus.
#
# TODO: Re-work these tests or remove them from the test suite.
@unittest.skip('test disabled')
def test_click_selection(self):
self.linenumber.show_sidebar()
self.text.insert('1.0', 'one\ntwo\nthree\nfour\n')
self.root.update()
# Click on the second line.
x, y = self.get_line_screen_position(2)
self.linenumber.sidebar_text.event_generate('<Button-1>', x=x, y=y)
self.linenumber.sidebar_text.update()
self.root.update()
self.assertEqual(self.get_selection(), ('2.0', '3.0'))
def simulate_drag(self, start_line, end_line):
start_x, start_y = self.get_line_screen_position(start_line)
end_x, end_y = self.get_line_screen_position(end_line)
self.linenumber.sidebar_text.event_generate('<Button-1>',
x=start_x, y=start_y)
self.root.update()
def lerp(a, b, steps):
"""linearly interpolate from a to b (inclusive) in equal steps"""
last_step = steps - 1
for i in range(steps):
yield ((last_step - i) / last_step) * a + (i / last_step) * b
for x, y in zip(
map(int, lerp(start_x, end_x, steps=11)),
map(int, lerp(start_y, end_y, steps=11)),
):
self.linenumber.sidebar_text.event_generate('<B1-Motion>', x=x, y=y)
self.root.update()
self.linenumber.sidebar_text.event_generate('<ButtonRelease-1>',
x=end_x, y=end_y)
self.root.update()
@unittest.skip('test disabled')
def test_drag_selection_down(self):
self.linenumber.show_sidebar()
self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n')
self.root.update()
# Drag from the second line to the fourth line.
self.simulate_drag(2, 4)
self.assertEqual(self.get_selection(), ('2.0', '5.0'))
@unittest.skip('test disabled')
def test_drag_selection_up(self):
self.linenumber.show_sidebar()
self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n')
self.root.update()
# Drag from the fourth line to the second line.
self.simulate_drag(4, 2)
self.assertEqual(self.get_selection(), ('2.0', '5.0'))
def test_scroll(self):
self.linenumber.show_sidebar()
self.text.insert('1.0', 'line\n' * 100)
self.root.update()
# Scroll down 10 lines.
self.text.yview_scroll(10, 'unit')
self.root.update()
self.assertEqual(self.text.index('@0,0'), '11.0')
self.assertEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0')
# Generate a mouse-wheel event and make sure it scrolled up or down.
# The meaning of the "delta" is OS-dependent, so this just checks for
# any change.
self.linenumber.sidebar_text.event_generate('<MouseWheel>',
x=0, y=0,
delta=10)
self.root.update()
self.assertNotEqual(self.text.index('@0,0'), '11.0')
self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0')
def test_font(self):
ln = self.linenumber
orig_font = ln.sidebar_text['font']
test_font = 'TkTextFont'
self.assertNotEqual(orig_font, test_font)
# Ensure line numbers aren't shown.
ln.hide_sidebar()
self.font_override = test_font
# Nothing breaks when line numbers aren't shown.
ln.update_font()
# Activate line numbers, previous font change is immediately effective.
ln.show_sidebar()
self.assertEqual(ln.sidebar_text['font'], test_font)
# Call the font update with line numbers shown, change is picked up.
self.font_override = orig_font
ln.update_font()
self.assertEqual(ln.sidebar_text['font'], orig_font)
def test_highlight_colors(self):
ln = self.linenumber
orig_colors = dict(self.highlight_cfg)
test_colors = {'background': '#222222', 'foreground': '#ffff00'}
def assert_colors_are_equal(colors):
self.assertEqual(ln.sidebar_text['background'], colors['background'])
self.assertEqual(ln.sidebar_text['foreground'], colors['foreground'])
# Ensure line numbers aren't shown.
ln.hide_sidebar()
self.highlight_cfg = test_colors
# Nothing breaks with inactive line numbers.
ln.update_colors()
# Show line numbers, previous colors change is immediately effective.
ln.show_sidebar()
assert_colors_are_equal(test_colors)
# Call colors update with no change to the configured colors.
ln.update_colors()
assert_colors_are_equal(test_colors)
# Call the colors update with line numbers shown, change is picked up.
self.highlight_cfg = orig_colors
ln.update_colors()
assert_colors_are_equal(orig_colors)
class ShellSidebarTest(unittest.TestCase, ExtraAssertions):
root: tk.Tk = None
shell: PyShell = None
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = root = tk.Tk()
root.withdraw()
fix_scaling(root)
fixwordbreaks(root)
fix_x11_paste(root)
cls.flist = flist = PyShellFileList(root)
# See #43981 about macosx.setupApp(root, flist) causing failure.
root.update_idletasks()
cls.init_shell()
@classmethod
def tearDownClass(cls):
if cls.shell is not None:
cls.shell.executing = False
cls.shell.close()
cls.shell = None
cls.flist = None
cls.root.update_idletasks()
cls.root.destroy()
cls.root = None
@classmethod
def init_shell(cls):
cls.shell = cls.flist.open_shell()
cls.shell.pollinterval = 10
cls.root.update()
cls.n_preface_lines = get_lineno(cls.shell.text, 'end-1c') - 1
@classmethod
def reset_shell(cls):
cls.shell.per.bottom.delete(f'{cls.n_preface_lines+1}.0', 'end-1c')
cls.shell.shell_sidebar.update_sidebar()
cls.root.update()
def setUp(self):
# In some test environments, e.g. Azure Pipelines (as of
# Apr. 2021), sys.stdout is changed between tests. However,
# PyShell relies on overriding sys.stdout when run without a
# sub-process (as done here; see setUpClass).
self._saved_stdout = None
if sys.stdout != self.shell.stdout:
self._saved_stdout = sys.stdout
sys.stdout = self.shell.stdout
self.reset_shell()
def tearDown(self):
if self._saved_stdout is not None:
sys.stdout = self._saved_stdout
def get_sidebar_lines(self):
canvas = self.shell.shell_sidebar.canvas
texts = list(canvas.find(tk.ALL))
texts_by_y_coords = {
canvas.bbox(text)[1]: canvas.itemcget(text, 'text')
for text in texts
}
line_y_coords = self.get_shell_line_y_coords()
return [texts_by_y_coords.get(y, None) for y in line_y_coords]
def assert_sidebar_lines_end_with(self, expected_lines):
self.shell.shell_sidebar.update_sidebar()
self.assertEqual(
self.get_sidebar_lines()[-len(expected_lines):],
expected_lines,
)
def get_shell_line_y_coords(self):
text = self.shell.text
y_coords = []
index = text.index("@0,0")
if index.split('.', 1)[1] != '0':
index = text.index(f"{index} +1line linestart")
while (lineinfo := text.dlineinfo(index)) is not None:
y_coords.append(lineinfo[1])
index = text.index(f"{index} +1line")
return y_coords
def get_sidebar_line_y_coords(self):
canvas = self.shell.shell_sidebar.canvas
texts = list(canvas.find(tk.ALL))
texts.sort(key=lambda text: canvas.bbox(text)[1])
return [canvas.bbox(text)[1] for text in texts]
def assert_sidebar_lines_synced(self):
self.assertLessEqual(
set(self.get_sidebar_line_y_coords()),
set(self.get_shell_line_y_coords()),
)
def do_input(self, input):
shell = self.shell
text = shell.text
for line_index, line in enumerate(input.split('\n')):
if line_index > 0:
text.event_generate('<<newline-and-indent>>')
text.insert('insert', line, 'stdin')
def test_initial_state(self):
sidebar_lines = self.get_sidebar_lines()
self.assertEqual(
sidebar_lines,
[None] * (len(sidebar_lines) - 1) + ['>>>'],
)
self.assert_sidebar_lines_synced()
@run_in_tk_mainloop()
def test_single_empty_input(self):
self.do_input('\n')
yield
self.assert_sidebar_lines_end_with(['>>>', '>>>'])
@run_in_tk_mainloop()
def test_single_line_statement(self):
self.do_input('1\n')
yield
self.assert_sidebar_lines_end_with(['>>>', None, '>>>'])
@run_in_tk_mainloop()
def test_multi_line_statement(self):
# Block statements are not indented because IDLE auto-indents.
self.do_input(dedent('''\
if True:
print(1)
'''))
yield
self.assert_sidebar_lines_end_with([
'>>>',
'...',
'...',
'...',
None,
'>>>',
])
@run_in_tk_mainloop()
def test_single_long_line_wraps(self):
self.do_input('1' * 200 + '\n')
yield
self.assert_sidebar_lines_end_with(['>>>', None, '>>>'])
self.assert_sidebar_lines_synced()
@run_in_tk_mainloop()
def test_squeeze_multi_line_output(self):
shell = self.shell
text = shell.text
self.do_input('print("a\\nb\\nc")\n')
yield
self.assert_sidebar_lines_end_with(['>>>', None, None, None, '>>>'])
text.mark_set('insert', f'insert -1line linestart')
text.event_generate('<<squeeze-current-text>>')
yield
self.assert_sidebar_lines_end_with(['>>>', None, '>>>'])
self.assert_sidebar_lines_synced()
shell.squeezer.expandingbuttons[0].expand()
yield
self.assert_sidebar_lines_end_with(['>>>', None, None, None, '>>>'])
self.assert_sidebar_lines_synced()
@run_in_tk_mainloop()
def test_interrupt_recall_undo_redo(self):
text = self.shell.text
# Block statements are not indented because IDLE auto-indents.
initial_sidebar_lines = self.get_sidebar_lines()
self.do_input(dedent('''\
if True:
print(1)
'''))
yield
self.assert_sidebar_lines_end_with(['>>>', '...', '...'])
with_block_sidebar_lines = self.get_sidebar_lines()
self.assertNotEqual(with_block_sidebar_lines, initial_sidebar_lines)
# Control-C
text.event_generate('<<interrupt-execution>>')
yield
self.assert_sidebar_lines_end_with(['>>>', '...', '...', None, '>>>'])
# Recall previous via history
text.event_generate('<<history-previous>>')
text.event_generate('<<interrupt-execution>>')
yield
self.assert_sidebar_lines_end_with(['>>>', '...', None, '>>>'])
# Recall previous via recall
text.mark_set('insert', text.index('insert -2l'))
text.event_generate('<<newline-and-indent>>')
yield
text.event_generate('<<undo>>')
yield
self.assert_sidebar_lines_end_with(['>>>'])
text.event_generate('<<redo>>')
yield
self.assert_sidebar_lines_end_with(['>>>', '...'])
text.event_generate('<<newline-and-indent>>')
text.event_generate('<<newline-and-indent>>')
yield
self.assert_sidebar_lines_end_with(
['>>>', '...', '...', '...', None, '>>>']
)
@run_in_tk_mainloop()
def test_very_long_wrapped_line(self):
with adjust_int_max_str_digits(11_111), \
swap_attr(self.shell, 'squeezer', None):
self.do_input('x = ' + '1'*10_000 + '\n')
yield
self.assertEqual(self.get_sidebar_lines(), ['>>>'])
def test_font(self):
sidebar = self.shell.shell_sidebar
test_font = 'TkTextFont'
def mock_idleconf_GetFont(root, configType, section):
return test_font
GetFont_patcher = unittest.mock.patch.object(
idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont)
GetFont_patcher.start()
def cleanup():
GetFont_patcher.stop()
sidebar.update_font()
self.addCleanup(cleanup)
def get_sidebar_font():
canvas = sidebar.canvas
texts = list(canvas.find(tk.ALL))
fonts = {canvas.itemcget(text, 'font') for text in texts}
self.assertEqual(len(fonts), 1)
return next(iter(fonts))
self.assertNotEqual(get_sidebar_font(), test_font)
sidebar.update_font()
self.assertEqual(get_sidebar_font(), test_font)
def test_highlight_colors(self):
sidebar = self.shell.shell_sidebar
test_colors = {"background": '#abcdef', "foreground": '#123456'}
orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight
def mock_idleconf_GetHighlight(theme, element):
if element in ['linenumber', 'console']:
return test_colors
return orig_idleConf_GetHighlight(theme, element)
GetHighlight_patcher = unittest.mock.patch.object(
idlelib.sidebar.idleConf, 'GetHighlight',
mock_idleconf_GetHighlight)
GetHighlight_patcher.start()
def cleanup():
GetHighlight_patcher.stop()
sidebar.update_colors()
self.addCleanup(cleanup)
def get_sidebar_colors():
canvas = sidebar.canvas
texts = list(canvas.find(tk.ALL))
fgs = {canvas.itemcget(text, 'fill') for text in texts}
self.assertEqual(len(fgs), 1)
fg = next(iter(fgs))
bg = canvas.cget('background')
return {"background": bg, "foreground": fg}
self.assertNotEqual(get_sidebar_colors(), test_colors)
sidebar.update_colors()
self.assertEqual(get_sidebar_colors(), test_colors)
@run_in_tk_mainloop()
def test_mousewheel(self):
sidebar = self.shell.shell_sidebar
text = self.shell.text
# Enter a 100-line string to scroll the shell screen down.
self.do_input('x = """' + '\n'*100 + '"""\n')
yield
self.assertGreater(get_lineno(text, '@0,0'), 1)
last_lineno = get_end_linenumber(text)
self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0')))
# Delta for <MouseWheel>, whose meaning is platform-dependent.
delta = 1 if sidebar.canvas._windowingsystem == 'aqua' else 120
# Scroll up.
if sidebar.canvas._windowingsystem == 'x11':
sidebar.canvas.event_generate('<Button-4>', x=0, y=0)
else:
sidebar.canvas.event_generate('<MouseWheel>', x=0, y=0, delta=delta)
yield
self.assertIsNone(text.dlineinfo(text.index(f'{last_lineno}.0')))
# Scroll back down.
if sidebar.canvas._windowingsystem == 'x11':
sidebar.canvas.event_generate('<Button-5>', x=0, y=0)
else:
sidebar.canvas.event_generate('<MouseWheel>', x=0, y=0, delta=-delta)
yield
self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0')))
@run_in_tk_mainloop()
def test_copy(self):
sidebar = self.shell.shell_sidebar
text = self.shell.text
first_line = get_end_linenumber(text)
self.do_input(dedent('''\
if True:
print(1)
'''))
yield
text.tag_add('sel', f'{first_line}.0', 'end-1c')
selected_text = text.get('sel.first', 'sel.last')
self.assertStartsWith(selected_text, 'if True:\n')
self.assertIn('\n1\n', selected_text)
text.event_generate('<<copy>>')
self.addCleanup(text.clipboard_clear)
copied_text = text.clipboard_get()
self.assertEqual(copied_text, selected_text)
@run_in_tk_mainloop()
def test_copy_with_prompts(self):
sidebar = self.shell.shell_sidebar
text = self.shell.text
first_line = get_end_linenumber(text)
self.do_input(dedent('''\
if True:
print(1)
'''))
yield
text.tag_add('sel', f'{first_line}.3', 'end-1c')
selected_text = text.get('sel.first', 'sel.last')
self.assertStartsWith(selected_text, 'True:\n')
selected_lines_text = text.get('sel.first linestart', 'sel.last')
selected_lines = selected_lines_text.split('\n')
selected_lines.pop() # Final '' is a split artifact, not a line.
# Expect a block of input and a single output line.
expected_prompts = \
['>>>'] + ['...'] * (len(selected_lines) - 2) + [None]
selected_text_with_prompts = '\n'.join(
line if prompt is None else prompt + ' ' + line
for prompt, line in zip(expected_prompts,
selected_lines,
strict=True)
) + '\n'
text.event_generate('<<copy-with-prompts>>')
self.addCleanup(text.clipboard_clear)
copied_text = text.clipboard_get()
self.assertEqual(copied_text, selected_text_with_prompts)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,466 @@
"Test squeezer, coverage 95%"
from textwrap import dedent
from tkinter import Text, Tk
import unittest
from unittest.mock import Mock, NonCallableMagicMock, patch, sentinel, ANY
from test.support import requires
from idlelib.config import idleConf
from idlelib.percolator import Percolator
from idlelib.squeezer import count_lines_with_wrapping, ExpandingButton, \
Squeezer
from idlelib import macosx
from idlelib.textview import view_text
from idlelib.tooltip import Hovertip
SENTINEL_VALUE = sentinel.SENTINEL_VALUE
def get_test_tk_root(test_instance):
"""Helper for tests: Create a root Tk object."""
requires('gui')
root = Tk()
root.withdraw()
def cleanup_root():
root.update_idletasks()
root.destroy()
test_instance.addCleanup(cleanup_root)
return root
class CountLinesTest(unittest.TestCase):
"""Tests for the count_lines_with_wrapping function."""
def check(self, expected, text, linewidth):
return self.assertEqual(
expected,
count_lines_with_wrapping(text, linewidth),
)
def test_count_empty(self):
"""Test with an empty string."""
self.assertEqual(count_lines_with_wrapping(""), 0)
def test_count_begins_with_empty_line(self):
"""Test with a string which begins with a newline."""
self.assertEqual(count_lines_with_wrapping("\ntext"), 2)
def test_count_ends_with_empty_line(self):
"""Test with a string which ends with a newline."""
self.assertEqual(count_lines_with_wrapping("text\n"), 1)
def test_count_several_lines(self):
"""Test with several lines of text."""
self.assertEqual(count_lines_with_wrapping("1\n2\n3\n"), 3)
def test_empty_lines(self):
self.check(expected=1, text='\n', linewidth=80)
self.check(expected=2, text='\n\n', linewidth=80)
self.check(expected=10, text='\n' * 10, linewidth=80)
def test_long_line(self):
self.check(expected=3, text='a' * 200, linewidth=80)
self.check(expected=3, text='a' * 200 + '\n', linewidth=80)
def test_several_lines_different_lengths(self):
text = dedent("""\
13 characters
43 is the number of characters on this line
7 chars
13 characters""")
self.check(expected=5, text=text, linewidth=80)
self.check(expected=5, text=text + '\n', linewidth=80)
self.check(expected=6, text=text, linewidth=40)
self.check(expected=7, text=text, linewidth=20)
self.check(expected=11, text=text, linewidth=10)
class SqueezerTest(unittest.TestCase):
"""Tests for the Squeezer class."""
def make_mock_editor_window(self, with_text_widget=False):
"""Create a mock EditorWindow instance."""
editwin = NonCallableMagicMock()
editwin.width = 80
if with_text_widget:
editwin.root = get_test_tk_root(self)
text_widget = self.make_text_widget(root=editwin.root)
editwin.text = editwin.per.bottom = text_widget
return editwin
def make_squeezer_instance(self, editor_window=None):
"""Create an actual Squeezer instance with a mock EditorWindow."""
if editor_window is None:
editor_window = self.make_mock_editor_window()
squeezer = Squeezer(editor_window)
return squeezer
def make_text_widget(self, root=None):
if root is None:
root = get_test_tk_root(self)
text_widget = Text(root)
text_widget["font"] = ('Courier', 10)
text_widget.mark_set("iomark", "1.0")
return text_widget
def set_idleconf_option_with_cleanup(self, configType, section, option, value):
prev_val = idleConf.GetOption(configType, section, option)
idleConf.SetOption(configType, section, option, value)
self.addCleanup(idleConf.SetOption,
configType, section, option, prev_val)
def test_count_lines(self):
"""Test Squeezer.count_lines() with various inputs."""
editwin = self.make_mock_editor_window()
squeezer = self.make_squeezer_instance(editwin)
for text_code, line_width, expected in [
(r"'\n'", 80, 1),
(r"'\n' * 3", 80, 3),
(r"'a' * 40 + '\n'", 80, 1),
(r"'a' * 80 + '\n'", 80, 1),
(r"'a' * 200 + '\n'", 80, 3),
(r"'aa\t' * 20", 80, 2),
(r"'aa\t' * 21", 80, 3),
(r"'aa\t' * 20", 40, 4),
]:
with self.subTest(text_code=text_code,
line_width=line_width,
expected=expected):
text = eval(text_code)
with patch.object(editwin, 'width', line_width):
self.assertEqual(squeezer.count_lines(text), expected)
def test_init(self):
"""Test the creation of Squeezer instances."""
editwin = self.make_mock_editor_window()
squeezer = self.make_squeezer_instance(editwin)
self.assertIs(squeezer.editwin, editwin)
self.assertEqual(squeezer.expandingbuttons, [])
def test_write_no_tags(self):
"""Test Squeezer's overriding of the EditorWindow's write() method."""
editwin = self.make_mock_editor_window()
for text in ['', 'TEXT', 'LONG TEXT' * 1000, 'MANY_LINES\n' * 100]:
editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE)
squeezer = self.make_squeezer_instance(editwin)
self.assertEqual(squeezer.editwin.write(text, ()), SENTINEL_VALUE)
self.assertEqual(orig_write.call_count, 1)
orig_write.assert_called_with(text, ())
self.assertEqual(len(squeezer.expandingbuttons), 0)
def test_write_not_stdout(self):
"""Test Squeezer's overriding of the EditorWindow's write() method."""
for text in ['', 'TEXT', 'LONG TEXT' * 1000, 'MANY_LINES\n' * 100]:
editwin = self.make_mock_editor_window()
editwin.write.return_value = SENTINEL_VALUE
orig_write = editwin.write
squeezer = self.make_squeezer_instance(editwin)
self.assertEqual(squeezer.editwin.write(text, "stderr"),
SENTINEL_VALUE)
self.assertEqual(orig_write.call_count, 1)
orig_write.assert_called_with(text, "stderr")
self.assertEqual(len(squeezer.expandingbuttons), 0)
def test_write_stdout(self):
"""Test Squeezer's overriding of the EditorWindow's write() method."""
editwin = self.make_mock_editor_window()
for text in ['', 'TEXT']:
editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE)
squeezer = self.make_squeezer_instance(editwin)
squeezer.auto_squeeze_min_lines = 50
self.assertEqual(squeezer.editwin.write(text, "stdout"),
SENTINEL_VALUE)
self.assertEqual(orig_write.call_count, 1)
orig_write.assert_called_with(text, "stdout")
self.assertEqual(len(squeezer.expandingbuttons), 0)
for text in ['LONG TEXT' * 1000, 'MANY_LINES\n' * 100]:
editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE)
squeezer = self.make_squeezer_instance(editwin)
squeezer.auto_squeeze_min_lines = 50
self.assertEqual(squeezer.editwin.write(text, "stdout"), None)
self.assertEqual(orig_write.call_count, 0)
self.assertEqual(len(squeezer.expandingbuttons), 1)
def test_auto_squeeze(self):
"""Test that the auto-squeezing creates an ExpandingButton properly."""
editwin = self.make_mock_editor_window(with_text_widget=True)
text_widget = editwin.text
squeezer = self.make_squeezer_instance(editwin)
squeezer.auto_squeeze_min_lines = 5
squeezer.count_lines = Mock(return_value=6)
editwin.write('TEXT\n'*6, "stdout")
self.assertEqual(text_widget.get('1.0', 'end'), '\n')
self.assertEqual(len(squeezer.expandingbuttons), 1)
def test_squeeze_current_text(self):
"""Test the squeeze_current_text method."""
# Squeezing text should work for both stdout and stderr.
for tag_name in ["stdout", "stderr"]:
editwin = self.make_mock_editor_window(with_text_widget=True)
text_widget = editwin.text
squeezer = self.make_squeezer_instance(editwin)
squeezer.count_lines = Mock(return_value=6)
# Prepare some text in the Text widget.
text_widget.insert("1.0", "SOME\nTEXT\n", tag_name)
text_widget.mark_set("insert", "1.0")
self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n')
self.assertEqual(len(squeezer.expandingbuttons), 0)
# Test squeezing the current text.
retval = squeezer.squeeze_current_text()
self.assertEqual(retval, "break")
self.assertEqual(text_widget.get('1.0', 'end'), '\n\n')
self.assertEqual(len(squeezer.expandingbuttons), 1)
self.assertEqual(squeezer.expandingbuttons[0].s, 'SOME\nTEXT')
# Test that expanding the squeezed text works and afterwards
# the Text widget contains the original text.
squeezer.expandingbuttons[0].expand()
self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n')
self.assertEqual(len(squeezer.expandingbuttons), 0)
def test_squeeze_current_text_no_allowed_tags(self):
"""Test that the event doesn't squeeze text without a relevant tag."""
editwin = self.make_mock_editor_window(with_text_widget=True)
text_widget = editwin.text
squeezer = self.make_squeezer_instance(editwin)
squeezer.count_lines = Mock(return_value=6)
# Prepare some text in the Text widget.
text_widget.insert("1.0", "SOME\nTEXT\n", "TAG")
text_widget.mark_set("insert", "1.0")
self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n')
self.assertEqual(len(squeezer.expandingbuttons), 0)
# Test squeezing the current text.
retval = squeezer.squeeze_current_text()
self.assertEqual(retval, "break")
self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n')
self.assertEqual(len(squeezer.expandingbuttons), 0)
def test_squeeze_text_before_existing_squeezed_text(self):
"""Test squeezing text before existing squeezed text."""
editwin = self.make_mock_editor_window(with_text_widget=True)
text_widget = editwin.text
squeezer = self.make_squeezer_instance(editwin)
squeezer.count_lines = Mock(return_value=6)
# Prepare some text in the Text widget and squeeze it.
text_widget.insert("1.0", "SOME\nTEXT\n", "stdout")
text_widget.mark_set("insert", "1.0")
squeezer.squeeze_current_text()
self.assertEqual(len(squeezer.expandingbuttons), 1)
# Test squeezing the current text.
text_widget.insert("1.0", "MORE\nSTUFF\n", "stdout")
text_widget.mark_set("insert", "1.0")
retval = squeezer.squeeze_current_text()
self.assertEqual(retval, "break")
self.assertEqual(text_widget.get('1.0', 'end'), '\n\n\n')
self.assertEqual(len(squeezer.expandingbuttons), 2)
self.assertTrue(text_widget.compare(
squeezer.expandingbuttons[0],
'<',
squeezer.expandingbuttons[1],
))
def test_reload(self):
"""Test the reload() class-method."""
editwin = self.make_mock_editor_window(with_text_widget=True)
squeezer = self.make_squeezer_instance(editwin)
orig_auto_squeeze_min_lines = squeezer.auto_squeeze_min_lines
# Increase auto-squeeze-min-lines.
new_auto_squeeze_min_lines = orig_auto_squeeze_min_lines + 10
self.set_idleconf_option_with_cleanup(
'main', 'PyShell', 'auto-squeeze-min-lines',
str(new_auto_squeeze_min_lines))
Squeezer.reload()
self.assertEqual(squeezer.auto_squeeze_min_lines,
new_auto_squeeze_min_lines)
def test_reload_no_squeezer_instances(self):
"""Test that Squeezer.reload() runs without any instances existing."""
Squeezer.reload()
class ExpandingButtonTest(unittest.TestCase):
"""Tests for the ExpandingButton class."""
# In these tests the squeezer instance is a mock, but actual tkinter
# Text and Button instances are created.
def make_mock_squeezer(self):
"""Helper for tests: Create a mock Squeezer object."""
root = get_test_tk_root(self)
squeezer = Mock()
squeezer.editwin.text = Text(root)
squeezer.editwin.per = Percolator(squeezer.editwin.text)
self.addCleanup(squeezer.editwin.per.close)
# Set default values for the configuration settings.
squeezer.auto_squeeze_min_lines = 50
return squeezer
@patch('idlelib.squeezer.Hovertip', autospec=Hovertip)
def test_init(self, MockHovertip):
"""Test the simplest creation of an ExpandingButton."""
squeezer = self.make_mock_squeezer()
text_widget = squeezer.editwin.text
expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
self.assertEqual(expandingbutton.s, 'TEXT')
# Check that the underlying tkinter.Button is properly configured.
self.assertEqual(expandingbutton.master, text_widget)
self.assertTrue('50 lines' in expandingbutton.cget('text'))
# Check that the text widget still contains no text.
self.assertEqual(text_widget.get('1.0', 'end'), '\n')
# Check that the mouse events are bound.
self.assertIn('<Double-Button-1>', expandingbutton.bind())
right_button_code = '<Button-%s>' % ('2' if macosx.isAquaTk() else '3')
self.assertIn(right_button_code, expandingbutton.bind())
# Check that ToolTip was called once, with appropriate values.
self.assertEqual(MockHovertip.call_count, 1)
MockHovertip.assert_called_with(expandingbutton, ANY, hover_delay=ANY)
# Check that 'right-click' appears in the tooltip text.
tooltip_text = MockHovertip.call_args[0][1]
self.assertIn('right-click', tooltip_text.lower())
def test_expand(self):
"""Test the expand event."""
squeezer = self.make_mock_squeezer()
expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
# Insert the button into the text widget
# (this is normally done by the Squeezer class).
text_widget = squeezer.editwin.text
text_widget.window_create("1.0", window=expandingbutton)
# trigger the expand event
retval = expandingbutton.expand(event=Mock())
self.assertEqual(retval, None)
# Check that the text was inserted into the text widget.
self.assertEqual(text_widget.get('1.0', 'end'), 'TEXT\n')
# Check that the 'TAGS' tag was set on the inserted text.
text_end_index = text_widget.index('end-1c')
self.assertEqual(text_widget.get('1.0', text_end_index), 'TEXT')
self.assertEqual(text_widget.tag_nextrange('TAGS', '1.0'),
('1.0', text_end_index))
# Check that the button removed itself from squeezer.expandingbuttons.
self.assertEqual(squeezer.expandingbuttons.remove.call_count, 1)
squeezer.expandingbuttons.remove.assert_called_with(expandingbutton)
def test_expand_dangerous_oupput(self):
"""Test that expanding very long output asks user for confirmation."""
squeezer = self.make_mock_squeezer()
text = 'a' * 10**5
expandingbutton = ExpandingButton(text, 'TAGS', 50, squeezer)
expandingbutton.set_is_dangerous()
self.assertTrue(expandingbutton.is_dangerous)
# Insert the button into the text widget
# (this is normally done by the Squeezer class).
text_widget = expandingbutton.text
text_widget.window_create("1.0", window=expandingbutton)
# Patch the message box module to always return False.
with patch('idlelib.squeezer.messagebox') as mock_msgbox:
mock_msgbox.askokcancel.return_value = False
mock_msgbox.askyesno.return_value = False
# Trigger the expand event.
retval = expandingbutton.expand(event=Mock())
# Check that the event chain was broken and no text was inserted.
self.assertEqual(retval, 'break')
self.assertEqual(expandingbutton.text.get('1.0', 'end-1c'), '')
# Patch the message box module to always return True.
with patch('idlelib.squeezer.messagebox') as mock_msgbox:
mock_msgbox.askokcancel.return_value = True
mock_msgbox.askyesno.return_value = True
# Trigger the expand event.
retval = expandingbutton.expand(event=Mock())
# Check that the event chain wasn't broken and the text was inserted.
self.assertEqual(retval, None)
self.assertEqual(expandingbutton.text.get('1.0', 'end-1c'), text)
def test_copy(self):
"""Test the copy event."""
# Testing with the actual clipboard proved problematic, so this
# test replaces the clipboard manipulation functions with mocks
# and checks that they are called appropriately.
squeezer = self.make_mock_squeezer()
expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
expandingbutton.clipboard_clear = Mock()
expandingbutton.clipboard_append = Mock()
# Trigger the copy event.
retval = expandingbutton.copy(event=Mock())
self.assertEqual(retval, None)
# Vheck that the expanding button called clipboard_clear() and
# clipboard_append('TEXT') once each.
self.assertEqual(expandingbutton.clipboard_clear.call_count, 1)
self.assertEqual(expandingbutton.clipboard_append.call_count, 1)
expandingbutton.clipboard_append.assert_called_with('TEXT')
def test_view(self):
"""Test the view event."""
squeezer = self.make_mock_squeezer()
expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
expandingbutton.selection_own = Mock()
with patch('idlelib.squeezer.view_text', autospec=view_text)\
as mock_view_text:
# Trigger the view event.
expandingbutton.view(event=Mock())
# Check that the expanding button called view_text.
self.assertEqual(mock_view_text.call_count, 1)
# Check that the proper text was passed.
self.assertEqual(mock_view_text.call_args[0][2], 'TEXT')
def test_rmenu(self):
"""Test the context menu."""
squeezer = self.make_mock_squeezer()
expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
with patch('tkinter.Menu') as mock_Menu:
mock_menu = Mock()
mock_Menu.return_value = mock_menu
mock_event = Mock()
mock_event.x = 10
mock_event.y = 10
expandingbutton.context_menu_event(event=mock_event)
self.assertEqual(mock_menu.add_command.call_count,
len(expandingbutton.rmenu_specs))
for label, *data in expandingbutton.rmenu_specs:
mock_menu.add_command.assert_any_call(label=label, command=ANY)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,41 @@
"Test stackviewer, coverage 63%."
from idlelib import stackviewer
import unittest
from test.support import requires
from tkinter import Tk
from idlelib.tree import TreeNode, ScrolledCanvas
class StackBrowserTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
## for id in cls.root.tk.call('after', 'info'):
## cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
def test_init(self):
try:
abc
except NameError as exc:
sb = stackviewer.StackBrowser(self.root, exc)
isi = self.assertIsInstance
isi(stackviewer.sc, ScrolledCanvas)
isi(stackviewer.item, stackviewer.StackTreeItem)
isi(stackviewer.node, TreeNode)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,41 @@
"Test statusbar, coverage 100%."
from idlelib import statusbar
import unittest
from test.support import requires
from tkinter import Tk
class Test(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def test_init(self):
bar = statusbar.MultiStatusBar(self.root)
self.assertEqual(bar.labels, {})
def test_set_label(self):
bar = statusbar.MultiStatusBar(self.root)
bar.set_label('left', text='sometext', width=10)
self.assertIn('left', bar.labels)
left = bar.labels['left']
self.assertEqual(left['text'], 'sometext')
self.assertEqual(left['width'], 10)
bar.set_label('left', text='revised text')
self.assertEqual(left['text'], 'revised text')
bar.set_label('right', text='correct text')
self.assertEqual(bar.labels['right']['text'], 'correct text')
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,236 @@
''' Test mock_tk.Text class against tkinter.Text class
Run same tests with both by creating a mixin class.
'''
import unittest
from test.support import requires
from _tkinter import TclError
class TextTest:
"Define items common to both sets of tests."
hw = 'hello\nworld' # Several tests insert this after initialization.
hwn = hw+'\n' # \n present at initialization, before insert
# setUpClass defines cls.Text and maybe cls.root.
# setUp defines self.text from Text and maybe root.
def test_init(self):
self.assertEqual(self.text.get('1.0'), '\n')
self.assertEqual(self.text.get('end'), '')
def test_index_empty(self):
index = self.text.index
for dex in (-1.0, 0.3, '1.-1', '1.0', '1.0 lineend', '1.end', '1.33',
'insert'):
self.assertEqual(index(dex), '1.0')
for dex in 'end', 2.0, '2.1', '33.44':
self.assertEqual(index(dex), '2.0')
def test_index_data(self):
index = self.text.index
self.text.insert('1.0', self.hw)
for dex in -1.0, 0.3, '1.-1', '1.0':
self.assertEqual(index(dex), '1.0')
for dex in '1.0 lineend', '1.end', '1.33':
self.assertEqual(index(dex), '1.5')
for dex in 'end', '33.44':
self.assertEqual(index(dex), '3.0')
def test_get(self):
get = self.text.get
Equal = self.assertEqual
self.text.insert('1.0', self.hw)
Equal(get('end'), '')
Equal(get('end', 'end'), '')
Equal(get('1.0'), 'h')
Equal(get('1.0', '1.1'), 'h')
Equal(get('1.0', '1.3'), 'hel')
Equal(get('1.1', '1.3'), 'el')
Equal(get('1.0', '1.0 lineend'), 'hello')
Equal(get('1.0', '1.10'), 'hello')
Equal(get('1.0 lineend'), '\n')
Equal(get('1.1', '2.3'), 'ello\nwor')
Equal(get('1.0', '2.5'), self.hw)
Equal(get('1.0', 'end'), self.hwn)
Equal(get('0.0', '5.0'), self.hwn)
def test_insert(self):
insert = self.text.insert
get = self.text.get
Equal = self.assertEqual
insert('1.0', self.hw)
Equal(get('1.0', 'end'), self.hwn)
insert('1.0', '') # nothing
Equal(get('1.0', 'end'), self.hwn)
insert('1.0', '*')
Equal(get('1.0', 'end'), '*hello\nworld\n')
insert('1.0 lineend', '*')
Equal(get('1.0', 'end'), '*hello*\nworld\n')
insert('2.3', '*')
Equal(get('1.0', 'end'), '*hello*\nwor*ld\n')
insert('end', 'x')
Equal(get('1.0', 'end'), '*hello*\nwor*ldx\n')
insert('1.4', 'x\n')
Equal(get('1.0', 'end'), '*helx\nlo*\nwor*ldx\n')
def test_no_delete(self):
# if index1 == 'insert' or 'end' or >= end, there is no deletion
delete = self.text.delete
get = self.text.get
Equal = self.assertEqual
self.text.insert('1.0', self.hw)
delete('insert')
Equal(get('1.0', 'end'), self.hwn)
delete('end')
Equal(get('1.0', 'end'), self.hwn)
delete('insert', 'end')
Equal(get('1.0', 'end'), self.hwn)
delete('insert', '5.5')
Equal(get('1.0', 'end'), self.hwn)
delete('1.4', '1.0')
Equal(get('1.0', 'end'), self.hwn)
delete('1.4', '1.4')
Equal(get('1.0', 'end'), self.hwn)
def test_delete_char(self):
delete = self.text.delete
get = self.text.get
Equal = self.assertEqual
self.text.insert('1.0', self.hw)
delete('1.0')
Equal(get('1.0', '1.end'), 'ello')
delete('1.0', '1.1')
Equal(get('1.0', '1.end'), 'llo')
# delete \n and combine 2 lines into 1
delete('1.end')
Equal(get('1.0', '1.end'), 'lloworld')
self.text.insert('1.3', '\n')
delete('1.10')
Equal(get('1.0', '1.end'), 'lloworld')
self.text.insert('1.3', '\n')
delete('1.3', '2.0')
Equal(get('1.0', '1.end'), 'lloworld')
def test_delete_slice(self):
delete = self.text.delete
get = self.text.get
Equal = self.assertEqual
self.text.insert('1.0', self.hw)
delete('1.0', '1.0 lineend')
Equal(get('1.0', 'end'), '\nworld\n')
delete('1.0', 'end')
Equal(get('1.0', 'end'), '\n')
self.text.insert('1.0', self.hw)
delete('1.0', '2.0')
Equal(get('1.0', 'end'), 'world\n')
delete('1.0', 'end')
Equal(get('1.0', 'end'), '\n')
self.text.insert('1.0', self.hw)
delete('1.2', '2.3')
Equal(get('1.0', 'end'), 'held\n')
def test_multiple_lines(self): # insert and delete
self.text.insert('1.0', 'hello')
self.text.insert('1.3', '1\n2\n3\n4\n5')
self.assertEqual(self.text.get('1.0', 'end'), 'hel1\n2\n3\n4\n5lo\n')
self.text.delete('1.3', '5.1')
self.assertEqual(self.text.get('1.0', 'end'), 'hello\n')
def test_compare(self):
compare = self.text.compare
Equal = self.assertEqual
# need data so indexes not squished to 1,0
self.text.insert('1.0', 'First\nSecond\nThird\n')
self.assertRaises(TclError, compare, '2.2', 'op', '2.2')
for op, less1, less0, equal, greater0, greater1 in (
('<', True, True, False, False, False),
('<=', True, True, True, False, False),
('>', False, False, False, True, True),
('>=', False, False, True, True, True),
('==', False, False, True, False, False),
('!=', True, True, False, True, True),
):
Equal(compare('1.1', op, '2.2'), less1, op)
Equal(compare('2.1', op, '2.2'), less0, op)
Equal(compare('2.2', op, '2.2'), equal, op)
Equal(compare('2.3', op, '2.2'), greater0, op)
Equal(compare('3.3', op, '2.2'), greater1, op)
class MockTextTest(TextTest, unittest.TestCase):
@classmethod
def setUpClass(cls):
from idlelib.idle_test.mock_tk import Text
cls.Text = Text
def setUp(self):
self.text = self.Text()
def test_decode(self):
# test endflags (-1, 0) not tested by test_index (which uses +1)
decode = self.text._decode
Equal = self.assertEqual
self.text.insert('1.0', self.hw)
Equal(decode('end', -1), (2, 5))
Equal(decode('3.1', -1), (2, 5))
Equal(decode('end', 0), (2, 6))
Equal(decode('3.1', 0), (2, 6))
class TkTextTest(TextTest, unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
from tkinter import Tk, Text
cls.Text = Text
cls.root = Tk()
@classmethod
def tearDownClass(cls):
cls.root.destroy()
del cls.root
def setUp(self):
self.text = self.Text(self.root)
if __name__ == '__main__':
unittest.main(verbosity=2, exit=False)

View File

@ -0,0 +1,233 @@
"""Test textview, coverage 100%.
Since all methods and functions create (or destroy) a ViewWindow, which
is a widget containing a widget, etcetera, all tests must be gui tests.
Using mock Text would not change this. Other mocks are used to retrieve
information about calls.
"""
from idlelib import textview as tv
from test.support import requires
requires('gui')
import os
import unittest
from tkinter import Tk, TclError, CHAR, NONE, WORD
from tkinter.ttk import Button
from idlelib.idle_test.mock_idle import Func
from idlelib.idle_test.mock_tk import Mbox_func
def setUpModule():
global root
root = Tk()
root.withdraw()
def tearDownModule():
global root
root.update_idletasks()
root.destroy()
del root
# If we call ViewWindow or wrapper functions with defaults
# modal=True, _utest=False, test hangs on call to wait_window.
# Have also gotten tk error 'can't invoke "event" command'.
class VW(tv.ViewWindow): # Used in ViewWindowTest.
transient = Func()
grab_set = Func()
wait_window = Func()
# Call wrapper class VW with mock wait_window.
class ViewWindowTest(unittest.TestCase):
def setUp(self):
VW.transient.__init__()
VW.grab_set.__init__()
VW.wait_window.__init__()
def test_init_modal(self):
view = VW(root, 'Title', 'test text')
self.assertTrue(VW.transient.called)
self.assertTrue(VW.grab_set.called)
self.assertTrue(VW.wait_window.called)
view.ok()
def test_init_nonmodal(self):
view = VW(root, 'Title', 'test text', modal=False)
self.assertFalse(VW.transient.called)
self.assertFalse(VW.grab_set.called)
self.assertFalse(VW.wait_window.called)
view.ok()
def test_ok(self):
view = VW(root, 'Title', 'test text', modal=False)
view.destroy = Func()
view.ok()
self.assertTrue(view.destroy.called)
del view.destroy # Unmask real function.
view.destroy()
class AutoHideScrollbarTest(unittest.TestCase):
# Method set is tested in ScrollableTextFrameTest
def test_forbidden_geometry(self):
scroll = tv.AutoHideScrollbar(root)
self.assertRaises(TclError, scroll.pack)
self.assertRaises(TclError, scroll.place)
class ScrollableTextFrameTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.root = root = Tk()
root.withdraw()
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def make_frame(self, wrap=NONE, **kwargs):
frame = tv.ScrollableTextFrame(self.root, wrap=wrap, **kwargs)
def cleanup_frame():
frame.update_idletasks()
frame.destroy()
self.addCleanup(cleanup_frame)
return frame
def test_line1(self):
frame = self.make_frame()
frame.text.insert('1.0', 'test text')
self.assertEqual(frame.text.get('1.0', '1.end'), 'test text')
def test_horiz_scrollbar(self):
# The horizontal scrollbar should be shown/hidden according to
# the 'wrap' setting: It should only be shown when 'wrap' is
# set to NONE.
# wrap = NONE -> with horizontal scrolling
frame = self.make_frame(wrap=NONE)
self.assertEqual(frame.text.cget('wrap'), NONE)
self.assertIsNotNone(frame.xscroll)
# wrap != NONE -> no horizontal scrolling
for wrap in [CHAR, WORD]:
with self.subTest(wrap=wrap):
frame = self.make_frame(wrap=wrap)
self.assertEqual(frame.text.cget('wrap'), wrap)
self.assertIsNone(frame.xscroll)
class ViewFrameTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.root = root = Tk()
root.withdraw()
cls.frame = tv.ViewFrame(root, 'test text')
@classmethod
def tearDownClass(cls):
del cls.frame
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def test_line1(self):
get = self.frame.text.get
self.assertEqual(get('1.0', '1.end'), 'test text')
# Call ViewWindow with modal=False.
class ViewFunctionTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.orig_error = tv.showerror
tv.showerror = Mbox_func()
@classmethod
def tearDownClass(cls):
tv.showerror = cls.orig_error
del cls.orig_error
def test_view_text(self):
view = tv.view_text(root, 'Title', 'test text', modal=False)
self.assertIsInstance(view, tv.ViewWindow)
self.assertIsInstance(view.viewframe, tv.ViewFrame)
view.viewframe.ok()
def test_view_file(self):
view = tv.view_file(root, 'Title', __file__, 'ascii', modal=False)
self.assertIsInstance(view, tv.ViewWindow)
self.assertIsInstance(view.viewframe, tv.ViewFrame)
get = view.viewframe.textframe.text.get
self.assertIn('Test', get('1.0', '1.end'))
view.ok()
def test_bad_file(self):
# Mock showerror will be used; view_file will return None.
view = tv.view_file(root, 'Title', 'abc.xyz', 'ascii', modal=False)
self.assertIsNone(view)
self.assertEqual(tv.showerror.title, 'File Load Error')
def test_bad_encoding(self):
p = os.path
fn = p.abspath(p.join(p.dirname(__file__), '..', 'CREDITS.txt'))
view = tv.view_file(root, 'Title', fn, 'ascii', modal=False)
self.assertIsNone(view)
self.assertEqual(tv.showerror.title, 'Unicode Decode Error')
def test_nowrap(self):
view = tv.view_text(root, 'Title', 'test', modal=False, wrap='none')
text_widget = view.viewframe.textframe.text
self.assertEqual(text_widget.cget('wrap'), 'none')
# Call ViewWindow with _utest=True.
class ButtonClickTest(unittest.TestCase):
def setUp(self):
self.view = None
self.called = False
def tearDown(self):
if self.view:
self.view.destroy()
def test_view_text_bind_with_button(self):
def _command():
self.called = True
self.view = tv.view_text(root, 'TITLE_TEXT', 'COMMAND', _utest=True)
button = Button(root, text='BUTTON', command=_command)
button.invoke()
self.addCleanup(button.destroy)
self.assertEqual(self.called, True)
self.assertEqual(self.view.title(), 'TITLE_TEXT')
self.assertEqual(self.view.viewframe.textframe.text.get('1.0', '1.end'),
'COMMAND')
def test_view_file_bind_with_button(self):
def _command():
self.called = True
self.view = tv.view_file(root, 'TITLE_FILE', __file__,
encoding='ascii', _utest=True)
button = Button(root, text='BUTTON', command=_command)
button.invoke()
self.addCleanup(button.destroy)
self.assertEqual(self.called, True)
self.assertEqual(self.view.title(), 'TITLE_FILE')
get = self.view.viewframe.textframe.text.get
with open(__file__) as f:
self.assertEqual(get('1.0', '1.end'), f.readline().strip())
f.readline()
self.assertEqual(get('3.0', '3.end'), f.readline().strip())
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,161 @@
"""Test tooltip, coverage 100%.
Coverage is 100% after excluding 6 lines with "# pragma: no cover".
They involve TclErrors that either should or should not happen in a
particular situation, and which are 'pass'ed if they do.
"""
from idlelib.tooltip import TooltipBase, Hovertip
from test.support import requires
requires('gui')
from functools import wraps
import time
from tkinter import Button, Tk, Toplevel
import unittest
def setUpModule():
global root
root = Tk()
def tearDownModule():
global root
root.update_idletasks()
root.destroy()
del root
def add_call_counting(func):
@wraps(func)
def wrapped_func(*args, **kwargs):
wrapped_func.call_args_list.append((args, kwargs))
return func(*args, **kwargs)
wrapped_func.call_args_list = []
return wrapped_func
def _make_top_and_button(testobj):
global root
top = Toplevel(root)
testobj.addCleanup(top.destroy)
top.title("Test tooltip")
button = Button(top, text='ToolTip test button')
button.pack()
testobj.addCleanup(button.destroy)
top.lift()
return top, button
class ToolTipBaseTest(unittest.TestCase):
def setUp(self):
self.top, self.button = _make_top_and_button(self)
def test_base_class_is_unusable(self):
global root
top = Toplevel(root)
self.addCleanup(top.destroy)
button = Button(top, text='ToolTip test button')
button.pack()
self.addCleanup(button.destroy)
with self.assertRaises(NotImplementedError):
tooltip = TooltipBase(button)
tooltip.showtip()
class HovertipTest(unittest.TestCase):
def setUp(self):
self.top, self.button = _make_top_and_button(self)
def is_tipwindow_shown(self, tooltip):
return tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()
def test_showtip(self):
tooltip = Hovertip(self.button, 'ToolTip text')
self.addCleanup(tooltip.hidetip)
self.assertFalse(self.is_tipwindow_shown(tooltip))
tooltip.showtip()
self.assertTrue(self.is_tipwindow_shown(tooltip))
def test_showtip_twice(self):
tooltip = Hovertip(self.button, 'ToolTip text')
self.addCleanup(tooltip.hidetip)
self.assertFalse(self.is_tipwindow_shown(tooltip))
tooltip.showtip()
self.assertTrue(self.is_tipwindow_shown(tooltip))
orig_tipwindow = tooltip.tipwindow
tooltip.showtip()
self.assertTrue(self.is_tipwindow_shown(tooltip))
self.assertIs(tooltip.tipwindow, orig_tipwindow)
def test_hidetip(self):
tooltip = Hovertip(self.button, 'ToolTip text')
self.addCleanup(tooltip.hidetip)
tooltip.showtip()
tooltip.hidetip()
self.assertFalse(self.is_tipwindow_shown(tooltip))
def test_showtip_on_mouse_enter_no_delay(self):
tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=None)
self.addCleanup(tooltip.hidetip)
tooltip.showtip = add_call_counting(tooltip.showtip)
root.update()
self.assertFalse(self.is_tipwindow_shown(tooltip))
self.button.event_generate('<Enter>', x=0, y=0)
root.update()
self.assertTrue(self.is_tipwindow_shown(tooltip))
self.assertGreater(len(tooltip.showtip.call_args_list), 0)
def test_hover_with_delay(self):
# Run multiple tests requiring an actual delay simultaneously.
# Test #1: A hover tip with a non-zero delay appears after the delay.
tooltip1 = Hovertip(self.button, 'ToolTip text', hover_delay=100)
self.addCleanup(tooltip1.hidetip)
tooltip1.showtip = add_call_counting(tooltip1.showtip)
root.update()
self.assertFalse(self.is_tipwindow_shown(tooltip1))
self.button.event_generate('<Enter>', x=0, y=0)
root.update()
self.assertFalse(self.is_tipwindow_shown(tooltip1))
# Test #2: A hover tip with a non-zero delay doesn't appear when
# the mouse stops hovering over the base widget before the delay
# expires.
tooltip2 = Hovertip(self.button, 'ToolTip text', hover_delay=100)
self.addCleanup(tooltip2.hidetip)
tooltip2.showtip = add_call_counting(tooltip2.showtip)
root.update()
self.button.event_generate('<Enter>', x=0, y=0)
root.update()
self.button.event_generate('<Leave>', x=0, y=0)
root.update()
time.sleep(0.15)
root.update()
# Test #1 assertions.
self.assertTrue(self.is_tipwindow_shown(tooltip1))
self.assertGreater(len(tooltip1.showtip.call_args_list), 0)
# Test #2 assertions.
self.assertFalse(self.is_tipwindow_shown(tooltip2))
self.assertEqual(tooltip2.showtip.call_args_list, [])
def test_hidetip_on_mouse_leave(self):
tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=None)
self.addCleanup(tooltip.hidetip)
tooltip.showtip = add_call_counting(tooltip.showtip)
root.update()
self.button.event_generate('<Enter>', x=0, y=0)
root.update()
self.button.event_generate('<Leave>', x=0, y=0)
root.update()
self.assertFalse(self.is_tipwindow_shown(tooltip))
self.assertGreater(len(tooltip.showtip.call_args_list), 0)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,60 @@
"Test tree. coverage 56%."
from idlelib import tree
import unittest
from test.support import requires
requires('gui')
from tkinter import Tk, EventType, SCROLL
class TreeTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.root = Tk()
cls.root.withdraw()
@classmethod
def tearDownClass(cls):
cls.root.destroy()
del cls.root
def test_init(self):
# Start with code slightly adapted from htest.
sc = tree.ScrolledCanvas(
self.root, bg="white", highlightthickness=0, takefocus=1)
sc.frame.pack(expand=1, fill="both", side='left')
item = tree.FileTreeItem(tree.ICONDIR)
node = tree.TreeNode(sc.canvas, None, item)
node.expand()
class TestScrollEvent(unittest.TestCase):
def test_wheel_event(self):
# Fake widget class containing `yview` only.
class _Widget:
def __init__(widget, *expected):
widget.expected = expected
def yview(widget, *args):
self.assertTupleEqual(widget.expected, args)
# Fake event class
class _Event:
pass
# (type, delta, num, amount)
tests = ((EventType.MouseWheel, 120, -1, -5),
(EventType.MouseWheel, -120, -1, 5),
(EventType.ButtonPress, -1, 4, -5),
(EventType.ButtonPress, -1, 5, 5))
event = _Event()
for ty, delta, num, amount in tests:
event.type = ty
event.delta = delta
event.num = num
res = tree.wheel_event(event, _Widget(SCROLL, amount, "units"))
self.assertEqual(res, "break")
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,135 @@
"Test undo, coverage 77%."
# Only test UndoDelegator so far.
from idlelib.undo import UndoDelegator
import unittest
from test.support import requires
requires('gui')
from unittest.mock import Mock
from tkinter import Text, Tk
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.delegator.bell = Mock()
self.percolator.insertfilter(self.delegator)
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)

View File

@ -0,0 +1,14 @@
"""Test util, coverage 100%"""
import unittest
from idlelib import util
class UtilTest(unittest.TestCase):
def test_extensions(self):
for extension in {'.pyi', '.py', '.pyw'}:
self.assertIn(extension, util.py_extensions)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,73 @@
'''Test warnings replacement in pyshell.py and run.py.
This file could be expanded to include traceback overrides
(in same two modules). If so, change name.
Revise if output destination changes (http://bugs.python.org/issue18318).
Make sure warnings module is left unaltered (http://bugs.python.org/issue18081).
'''
from idlelib import run
from idlelib import pyshell as shell
import unittest
from test.support import captured_stderr
import warnings
# Try to capture default showwarning before Idle modules are imported.
showwarning = warnings.showwarning
# But if we run this file within idle, we are in the middle of the run.main loop
# and default showwarnings has already been replaced.
running_in_idle = 'idle' in showwarning.__name__
# The following was generated from pyshell.idle_formatwarning
# and checked as matching expectation.
idlemsg = '''
Warning (from warnings module):
File "test_warning.py", line 99
Line of code
UserWarning: Test
'''
shellmsg = idlemsg + ">>> "
class RunWarnTest(unittest.TestCase):
@unittest.skipIf(running_in_idle, "Does not work when run within Idle.")
def test_showwarnings(self):
self.assertIs(warnings.showwarning, showwarning)
run.capture_warnings(True)
self.assertIs(warnings.showwarning, run.idle_showwarning_subproc)
run.capture_warnings(False)
self.assertIs(warnings.showwarning, showwarning)
def test_run_show(self):
with captured_stderr() as f:
run.idle_showwarning_subproc(
'Test', UserWarning, 'test_warning.py', 99, f, 'Line of code')
# The following uses .splitlines to erase line-ending differences
self.assertEqual(idlemsg.splitlines(), f.getvalue().splitlines())
class ShellWarnTest(unittest.TestCase):
@unittest.skipIf(running_in_idle, "Does not work when run within Idle.")
def test_showwarnings(self):
self.assertIs(warnings.showwarning, showwarning)
shell.capture_warnings(True)
self.assertIs(warnings.showwarning, shell.idle_showwarning)
shell.capture_warnings(False)
self.assertIs(warnings.showwarning, showwarning)
def test_idle_formatter(self):
# Will fail if format changed without regenerating idlemsg
s = shell.idle_formatwarning(
'Test', UserWarning, 'test_warning.py', 99, 'Line of code')
self.assertEqual(idlemsg, s)
def test_shell_show(self):
with captured_stderr() as f:
shell.idle_showwarning(
'Test', UserWarning, 'test_warning.py', 99, f, 'Line of code')
self.assertEqual(shellmsg.splitlines(), f.getvalue().splitlines())
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,45 @@
"Test window, coverage 47%."
from idlelib import window
import unittest
from test.support import requires
from tkinter import Tk
class WindowListTest(unittest.TestCase):
def test_init(self):
wl = window.WindowList()
self.assertEqual(wl.dict, {})
self.assertEqual(wl.callbacks, [])
# Further tests need mock Window.
class ListedToplevelTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
window.registry = set()
requires('gui')
cls.root = Tk()
cls.root.withdraw()
@classmethod
def tearDownClass(cls):
window.registry = window.WindowList()
cls.root.update_idletasks()
## for id in cls.root.tk.call('after', 'info'):
## cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
def test_init(self):
win = window.ListedToplevel(self.root)
self.assertIn(win, window.registry)
self.assertEqual(win.focused_widget, win)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,39 @@
"Test zoomheight, coverage 66%."
# Some code is system dependent.
from idlelib import zoomheight
import unittest
from test.support import requires
from tkinter import Tk
from idlelib.editor import EditorWindow
class Test(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.editwin = EditorWindow(root=cls.root)
@classmethod
def tearDownClass(cls):
cls.editwin._close()
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
def test_init(self):
zoom = zoomheight.ZoomHeight(self.editwin)
self.assertIs(zoom.editwin, self.editwin)
def test_zoom_height_event(self):
zoom = zoomheight.ZoomHeight(self.editwin)
zoom.zoom_height_event()
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,152 @@
"Test zzdummy, coverage 100%."
from idlelib import zzdummy
import unittest
from test.support import requires
from tkinter import Tk, Text
from unittest import mock
from idlelib import config
from idlelib import editor
from idlelib import format
usercfg = zzdummy.idleConf.userCfg
testcfg = {
'main': config.IdleUserConfParser(''),
'highlight': config.IdleUserConfParser(''),
'keys': config.IdleUserConfParser(''),
'extensions': config.IdleUserConfParser(''),
}
code_sample = """\
class C1:
# Class comment.
def __init__(self, a, b):
self.a = a
self.b = b
"""
class DummyEditwin:
get_selection_indices = editor.EditorWindow.get_selection_indices
def __init__(self, root, text):
self.root = root
self.top = root
self.text = text
self.fregion = format.FormatRegion(self)
self.text.undo_block_start = mock.Mock()
self.text.undo_block_stop = mock.Mock()
class ZZDummyTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
root = cls.root = Tk()
root.withdraw()
text = cls.text = Text(cls.root)
cls.editor = DummyEditwin(root, text)
zzdummy.idleConf.userCfg = testcfg
@classmethod
def tearDownClass(cls):
zzdummy.idleConf.userCfg = usercfg
del cls.editor, cls.text
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
def setUp(self):
text = self.text
text.insert('1.0', code_sample)
text.undo_block_start.reset_mock()
text.undo_block_stop.reset_mock()
zz = self.zz = zzdummy.ZzDummy(self.editor)
zzdummy.ZzDummy.ztext = '# ignore #'
def tearDown(self):
self.text.delete('1.0', 'end')
del self.zz
def checklines(self, text, value):
# Verify that there are lines being checked.
end_line = int(float(text.index('end')))
# Check each line for the starting text.
actual = []
for line in range(1, end_line):
txt = text.get(f'{line}.0', f'{line}.end')
actual.append(txt.startswith(value))
return actual
def test_init(self):
zz = self.zz
self.assertEqual(zz.editwin, self.editor)
self.assertEqual(zz.text, self.editor.text)
def test_reload(self):
self.assertEqual(self.zz.ztext, '# ignore #')
testcfg['extensions'].SetOption('ZzDummy', 'z-text', 'spam')
zzdummy.ZzDummy.reload()
self.assertEqual(self.zz.ztext, 'spam')
def test_z_in_event(self):
eq = self.assertEqual
zz = self.zz
text = zz.text
eq(self.zz.ztext, '# ignore #')
# No lines have the leading text.
expected = [False, False, False, False, False, False, False]
actual = self.checklines(text, zz.ztext)
eq(expected, actual)
text.tag_add('sel', '2.0', '4.end')
eq(zz.z_in_event(), 'break')
expected = [False, True, True, True, False, False, False]
actual = self.checklines(text, zz.ztext)
eq(expected, actual)
text.undo_block_start.assert_called_once()
text.undo_block_stop.assert_called_once()
def test_z_out_event(self):
eq = self.assertEqual
zz = self.zz
text = zz.text
eq(self.zz.ztext, '# ignore #')
# Prepend text.
text.tag_add('sel', '2.0', '5.end')
zz.z_in_event()
text.undo_block_start.reset_mock()
text.undo_block_stop.reset_mock()
# Select a few lines to remove text.
text.tag_remove('sel', '1.0', 'end')
text.tag_add('sel', '3.0', '4.end')
eq(zz.z_out_event(), 'break')
expected = [False, True, False, False, True, False, False]
actual = self.checklines(text, zz.ztext)
eq(expected, actual)
text.undo_block_start.assert_called_once()
text.undo_block_stop.assert_called_once()
def test_roundtrip(self):
# Insert and remove to all code should give back original text.
zz = self.zz
text = zz.text
text.tag_add('sel', '1.0', 'end-1c')
zz.z_in_event()
zz.z_out_event()
self.assertEqual(text.get('1.0', 'end-1c'), code_sample)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,62 @@
"""Utilities for testing with Tkinter"""
import functools
def run_in_tk_mainloop(delay=1):
"""Decorator for running a test method with a real Tk mainloop.
This starts a Tk mainloop before running the test, and stops it
at the end. This is faster and more robust than the common
alternative method of calling .update() and/or .update_idletasks().
Test methods using this must be written as generator functions,
using "yield" to allow the mainloop to process events and "after"
callbacks, and then continue the test from that point.
The delay argument is passed into root.after(...) calls as the number
of ms to wait before passing execution back to the generator function.
This also assumes that the test class has a .root attribute,
which is a tkinter.Tk object.
For example (from test_sidebar.py):
@run_test_with_tk_mainloop()
def test_single_empty_input(self):
self.do_input('\n')
yield
self.assert_sidebar_lines_end_with(['>>>', '>>>'])
"""
def decorator(test_method):
@functools.wraps(test_method)
def new_test_method(self):
test_generator = test_method(self)
root = self.root
# Exceptions raised by self.assert...() need to be raised
# outside of the after() callback in order for the test
# harness to capture them.
exception = None
def after_callback():
nonlocal exception
try:
next(test_generator)
except StopIteration:
root.quit()
except Exception as exc:
exception = exc
root.quit()
else:
# Schedule the Tk mainloop to call this function again,
# using a robust method of ensuring that it gets a
# chance to process queued events before doing so.
# See: https://stackoverflow.com/q/18499082#comment65004099_38817470
root.after(delay, root.after_idle, after_callback)
root.after(0, root.after_idle, after_callback)
root.mainloop()
if exception:
raise exception
return new_test_method
return decorator