264 lines
8.1 KiB
Python
264 lines
8.1 KiB
Python
|
import functools
|
||
|
import importlib
|
||
|
import os
|
||
|
import platform
|
||
|
import subprocess
|
||
|
import sys
|
||
|
|
||
|
import pytest
|
||
|
|
||
|
from matplotlib import _c_internal_utils
|
||
|
from matplotlib.testing import subprocess_run_helper
|
||
|
|
||
|
|
||
|
_test_timeout = 60 # A reasonably safe value for slower architectures.
|
||
|
|
||
|
|
||
|
def _isolated_tk_test(success_count, func=None):
|
||
|
"""
|
||
|
A decorator to run *func* in a subprocess and assert that it prints
|
||
|
"success" *success_count* times and nothing on stderr.
|
||
|
|
||
|
TkAgg tests seem to have interactions between tests, so isolate each test
|
||
|
in a subprocess. See GH#18261
|
||
|
"""
|
||
|
|
||
|
if func is None:
|
||
|
return functools.partial(_isolated_tk_test, success_count)
|
||
|
|
||
|
if "MPL_TEST_ESCAPE_HATCH" in os.environ:
|
||
|
# set in subprocess_run_helper() below
|
||
|
return func
|
||
|
|
||
|
@pytest.mark.skipif(
|
||
|
not importlib.util.find_spec('tkinter'),
|
||
|
reason="missing tkinter"
|
||
|
)
|
||
|
@pytest.mark.skipif(
|
||
|
sys.platform == "linux" and not _c_internal_utils.display_is_valid(),
|
||
|
reason="$DISPLAY and $WAYLAND_DISPLAY are unset"
|
||
|
)
|
||
|
@pytest.mark.xfail( # https://github.com/actions/setup-python/issues/649
|
||
|
('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
|
||
|
sys.platform == 'darwin' and sys.version_info[:2] < (3, 11),
|
||
|
reason='Tk version mismatch on Azure macOS CI'
|
||
|
)
|
||
|
@functools.wraps(func)
|
||
|
def test_func():
|
||
|
# even if the package exists, may not actually be importable this can
|
||
|
# be the case on some CI systems.
|
||
|
pytest.importorskip('tkinter')
|
||
|
try:
|
||
|
proc = subprocess_run_helper(
|
||
|
func, timeout=_test_timeout, extra_env=dict(
|
||
|
MPLBACKEND="TkAgg", MPL_TEST_ESCAPE_HATCH="1"))
|
||
|
except subprocess.TimeoutExpired:
|
||
|
pytest.fail("Subprocess timed out")
|
||
|
except subprocess.CalledProcessError as e:
|
||
|
pytest.fail("Subprocess failed to test intended behavior\n"
|
||
|
+ str(e.stderr))
|
||
|
else:
|
||
|
# macOS may actually emit irrelevant errors about Accelerated
|
||
|
# OpenGL vs. software OpenGL, or some permission error on Azure, so
|
||
|
# suppress them.
|
||
|
# Asserting stderr first (and printing it on failure) should be
|
||
|
# more helpful for debugging that printing a failed success count.
|
||
|
ignored_lines = ["OpenGL", "CFMessagePort: bootstrap_register",
|
||
|
"/usr/include/servers/bootstrap_defs.h"]
|
||
|
assert not [line for line in proc.stderr.splitlines()
|
||
|
if all(msg not in line for msg in ignored_lines)]
|
||
|
assert proc.stdout.count("success") == success_count
|
||
|
|
||
|
return test_func
|
||
|
|
||
|
|
||
|
@_isolated_tk_test(success_count=6) # len(bad_boxes)
|
||
|
def test_blit():
|
||
|
import matplotlib.pyplot as plt
|
||
|
import numpy as np
|
||
|
import matplotlib.backends.backend_tkagg # noqa
|
||
|
from matplotlib.backends import _backend_tk, _tkagg
|
||
|
|
||
|
fig, ax = plt.subplots()
|
||
|
photoimage = fig.canvas._tkphoto
|
||
|
data = np.ones((4, 4, 4), dtype=np.uint8)
|
||
|
# Test out of bounds blitting.
|
||
|
bad_boxes = ((-1, 2, 0, 2),
|
||
|
(2, 0, 0, 2),
|
||
|
(1, 6, 0, 2),
|
||
|
(0, 2, -1, 2),
|
||
|
(0, 2, 2, 0),
|
||
|
(0, 2, 1, 6))
|
||
|
for bad_box in bad_boxes:
|
||
|
try:
|
||
|
_tkagg.blit(
|
||
|
photoimage.tk.interpaddr(), str(photoimage), data,
|
||
|
_tkagg.TK_PHOTO_COMPOSITE_OVERLAY, (0, 1, 2, 3), bad_box)
|
||
|
except ValueError:
|
||
|
print("success")
|
||
|
|
||
|
# Test blitting to a destroyed canvas.
|
||
|
plt.close(fig)
|
||
|
_backend_tk.blit(photoimage, data, (0, 1, 2, 3))
|
||
|
|
||
|
|
||
|
@_isolated_tk_test(success_count=1)
|
||
|
def test_figuremanager_preserves_host_mainloop():
|
||
|
import tkinter
|
||
|
import matplotlib.pyplot as plt
|
||
|
success = []
|
||
|
|
||
|
def do_plot():
|
||
|
plt.figure()
|
||
|
plt.plot([1, 2], [3, 5])
|
||
|
plt.close()
|
||
|
root.after(0, legitimate_quit)
|
||
|
|
||
|
def legitimate_quit():
|
||
|
root.quit()
|
||
|
success.append(True)
|
||
|
|
||
|
root = tkinter.Tk()
|
||
|
root.after(0, do_plot)
|
||
|
root.mainloop()
|
||
|
|
||
|
if success:
|
||
|
print("success")
|
||
|
|
||
|
|
||
|
@pytest.mark.skipif(platform.python_implementation() != 'CPython',
|
||
|
reason='PyPy does not support Tkinter threading: '
|
||
|
'https://foss.heptapod.net/pypy/pypy/-/issues/1929')
|
||
|
@pytest.mark.flaky(reruns=3)
|
||
|
@_isolated_tk_test(success_count=1)
|
||
|
def test_figuremanager_cleans_own_mainloop():
|
||
|
import tkinter
|
||
|
import time
|
||
|
import matplotlib.pyplot as plt
|
||
|
import threading
|
||
|
from matplotlib.cbook import _get_running_interactive_framework
|
||
|
|
||
|
root = tkinter.Tk()
|
||
|
plt.plot([1, 2, 3], [1, 2, 5])
|
||
|
|
||
|
def target():
|
||
|
while not 'tk' == _get_running_interactive_framework():
|
||
|
time.sleep(.01)
|
||
|
plt.close()
|
||
|
if show_finished_event.wait():
|
||
|
print('success')
|
||
|
|
||
|
show_finished_event = threading.Event()
|
||
|
thread = threading.Thread(target=target, daemon=True)
|
||
|
thread.start()
|
||
|
plt.show(block=True) # Testing if this function hangs.
|
||
|
show_finished_event.set()
|
||
|
thread.join()
|
||
|
|
||
|
|
||
|
@pytest.mark.flaky(reruns=3)
|
||
|
@_isolated_tk_test(success_count=0)
|
||
|
def test_never_update():
|
||
|
import tkinter
|
||
|
del tkinter.Misc.update
|
||
|
del tkinter.Misc.update_idletasks
|
||
|
|
||
|
import matplotlib.pyplot as plt
|
||
|
fig = plt.figure()
|
||
|
plt.show(block=False)
|
||
|
|
||
|
plt.draw() # Test FigureCanvasTkAgg.
|
||
|
fig.canvas.toolbar.configure_subplots() # Test NavigationToolbar2Tk.
|
||
|
# Test FigureCanvasTk filter_destroy callback
|
||
|
fig.canvas.get_tk_widget().after(100, plt.close, fig)
|
||
|
|
||
|
# Check for update() or update_idletasks() in the event queue, functionally
|
||
|
# equivalent to tkinter.Misc.update.
|
||
|
plt.show(block=True)
|
||
|
|
||
|
# Note that exceptions would be printed to stderr; _isolated_tk_test
|
||
|
# checks them.
|
||
|
|
||
|
|
||
|
@_isolated_tk_test(success_count=2)
|
||
|
def test_missing_back_button():
|
||
|
import matplotlib.pyplot as plt
|
||
|
from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk
|
||
|
|
||
|
class Toolbar(NavigationToolbar2Tk):
|
||
|
# Only display the buttons we need.
|
||
|
toolitems = [t for t in NavigationToolbar2Tk.toolitems if
|
||
|
t[0] in ('Home', 'Pan', 'Zoom')]
|
||
|
|
||
|
fig = plt.figure()
|
||
|
print("success")
|
||
|
Toolbar(fig.canvas, fig.canvas.manager.window) # This should not raise.
|
||
|
print("success")
|
||
|
|
||
|
|
||
|
@_isolated_tk_test(success_count=1)
|
||
|
def test_canvas_focus():
|
||
|
import tkinter as tk
|
||
|
import matplotlib.pyplot as plt
|
||
|
success = []
|
||
|
|
||
|
def check_focus():
|
||
|
tkcanvas = fig.canvas.get_tk_widget()
|
||
|
# Give the plot window time to appear
|
||
|
if not tkcanvas.winfo_viewable():
|
||
|
tkcanvas.wait_visibility()
|
||
|
# Make sure the canvas has the focus, so that it's able to receive
|
||
|
# keyboard events.
|
||
|
if tkcanvas.focus_lastfor() == tkcanvas:
|
||
|
success.append(True)
|
||
|
plt.close()
|
||
|
root.destroy()
|
||
|
|
||
|
root = tk.Tk()
|
||
|
fig = plt.figure()
|
||
|
plt.plot([1, 2, 3])
|
||
|
root.after(0, plt.show)
|
||
|
root.after(100, check_focus)
|
||
|
root.mainloop()
|
||
|
|
||
|
if success:
|
||
|
print("success")
|
||
|
|
||
|
|
||
|
@_isolated_tk_test(success_count=2)
|
||
|
def test_embedding():
|
||
|
import tkinter as tk
|
||
|
from matplotlib.backends.backend_tkagg import (
|
||
|
FigureCanvasTkAgg, NavigationToolbar2Tk)
|
||
|
from matplotlib.backend_bases import key_press_handler
|
||
|
from matplotlib.figure import Figure
|
||
|
|
||
|
root = tk.Tk()
|
||
|
|
||
|
def test_figure(master):
|
||
|
fig = Figure()
|
||
|
ax = fig.add_subplot()
|
||
|
ax.plot([1, 2, 3])
|
||
|
|
||
|
canvas = FigureCanvasTkAgg(fig, master=master)
|
||
|
canvas.draw()
|
||
|
canvas.mpl_connect("key_press_event", key_press_handler)
|
||
|
canvas.get_tk_widget().pack(expand=True, fill="both")
|
||
|
|
||
|
toolbar = NavigationToolbar2Tk(canvas, master, pack_toolbar=False)
|
||
|
toolbar.pack(expand=True, fill="x")
|
||
|
|
||
|
canvas.get_tk_widget().forget()
|
||
|
toolbar.forget()
|
||
|
|
||
|
test_figure(root)
|
||
|
print("success")
|
||
|
|
||
|
# Test with a dark button color. Doesn't actually check whether the icon
|
||
|
# color becomes lighter, just that the code doesn't break.
|
||
|
|
||
|
root.tk_setPalette(background="sky blue", selectColor="midnight blue",
|
||
|
foreground="white")
|
||
|
test_figure(root)
|
||
|
print("success")
|