test_backend_tk.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import functools
  2. import importlib
  3. import os
  4. import platform
  5. import subprocess
  6. import sys
  7. import pytest
  8. from matplotlib import _c_internal_utils
  9. from matplotlib.testing import subprocess_run_helper
  10. _test_timeout = 60 # A reasonably safe value for slower architectures.
  11. def _isolated_tk_test(success_count, func=None):
  12. """
  13. A decorator to run *func* in a subprocess and assert that it prints
  14. "success" *success_count* times and nothing on stderr.
  15. TkAgg tests seem to have interactions between tests, so isolate each test
  16. in a subprocess. See GH#18261
  17. """
  18. if func is None:
  19. return functools.partial(_isolated_tk_test, success_count)
  20. if "MPL_TEST_ESCAPE_HATCH" in os.environ:
  21. # set in subprocess_run_helper() below
  22. return func
  23. @pytest.mark.skipif(
  24. not importlib.util.find_spec('tkinter'),
  25. reason="missing tkinter"
  26. )
  27. @pytest.mark.skipif(
  28. sys.platform == "linux" and not _c_internal_utils.xdisplay_is_valid(),
  29. reason="$DISPLAY is unset"
  30. )
  31. @pytest.mark.xfail( # https://github.com/actions/setup-python/issues/649
  32. ('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
  33. sys.platform == 'darwin' and sys.version_info[:2] < (3, 11),
  34. reason='Tk version mismatch on Azure macOS CI'
  35. )
  36. @functools.wraps(func)
  37. def test_func():
  38. # even if the package exists, may not actually be importable this can
  39. # be the case on some CI systems.
  40. pytest.importorskip('tkinter')
  41. try:
  42. proc = subprocess_run_helper(
  43. func, timeout=_test_timeout, extra_env=dict(
  44. MPLBACKEND="TkAgg", MPL_TEST_ESCAPE_HATCH="1"))
  45. except subprocess.TimeoutExpired:
  46. pytest.fail("Subprocess timed out")
  47. except subprocess.CalledProcessError as e:
  48. pytest.fail("Subprocess failed to test intended behavior\n"
  49. + str(e.stderr))
  50. else:
  51. # macOS may actually emit irrelevant errors about Accelerated
  52. # OpenGL vs. software OpenGL, or some permission error on Azure, so
  53. # suppress them.
  54. # Asserting stderr first (and printing it on failure) should be
  55. # more helpful for debugging that printing a failed success count.
  56. ignored_lines = ["OpenGL", "CFMessagePort: bootstrap_register",
  57. "/usr/include/servers/bootstrap_defs.h"]
  58. assert not [line for line in proc.stderr.splitlines()
  59. if all(msg not in line for msg in ignored_lines)]
  60. assert proc.stdout.count("success") == success_count
  61. return test_func
  62. @_isolated_tk_test(success_count=6) # len(bad_boxes)
  63. def test_blit():
  64. import matplotlib.pyplot as plt
  65. import numpy as np
  66. import matplotlib.backends.backend_tkagg # noqa
  67. from matplotlib.backends import _backend_tk, _tkagg
  68. fig, ax = plt.subplots()
  69. photoimage = fig.canvas._tkphoto
  70. data = np.ones((4, 4, 4), dtype=np.uint8)
  71. # Test out of bounds blitting.
  72. bad_boxes = ((-1, 2, 0, 2),
  73. (2, 0, 0, 2),
  74. (1, 6, 0, 2),
  75. (0, 2, -1, 2),
  76. (0, 2, 2, 0),
  77. (0, 2, 1, 6))
  78. for bad_box in bad_boxes:
  79. try:
  80. _tkagg.blit(
  81. photoimage.tk.interpaddr(), str(photoimage), data,
  82. _tkagg.TK_PHOTO_COMPOSITE_OVERLAY, (0, 1, 2, 3), bad_box)
  83. except ValueError:
  84. print("success")
  85. # Test blitting to a destroyed canvas.
  86. plt.close(fig)
  87. _backend_tk.blit(photoimage, data, (0, 1, 2, 3))
  88. @_isolated_tk_test(success_count=1)
  89. def test_figuremanager_preserves_host_mainloop():
  90. import tkinter
  91. import matplotlib.pyplot as plt
  92. success = []
  93. def do_plot():
  94. plt.figure()
  95. plt.plot([1, 2], [3, 5])
  96. plt.close()
  97. root.after(0, legitimate_quit)
  98. def legitimate_quit():
  99. root.quit()
  100. success.append(True)
  101. root = tkinter.Tk()
  102. root.after(0, do_plot)
  103. root.mainloop()
  104. if success:
  105. print("success")
  106. @pytest.mark.skipif(platform.python_implementation() != 'CPython',
  107. reason='PyPy does not support Tkinter threading: '
  108. 'https://foss.heptapod.net/pypy/pypy/-/issues/1929')
  109. @pytest.mark.flaky(reruns=3)
  110. @_isolated_tk_test(success_count=1)
  111. def test_figuremanager_cleans_own_mainloop():
  112. import tkinter
  113. import time
  114. import matplotlib.pyplot as plt
  115. import threading
  116. from matplotlib.cbook import _get_running_interactive_framework
  117. root = tkinter.Tk()
  118. plt.plot([1, 2, 3], [1, 2, 5])
  119. def target():
  120. while not 'tk' == _get_running_interactive_framework():
  121. time.sleep(.01)
  122. plt.close()
  123. if show_finished_event.wait():
  124. print('success')
  125. show_finished_event = threading.Event()
  126. thread = threading.Thread(target=target, daemon=True)
  127. thread.start()
  128. plt.show(block=True) # Testing if this function hangs.
  129. show_finished_event.set()
  130. thread.join()
  131. @pytest.mark.flaky(reruns=3)
  132. @_isolated_tk_test(success_count=0)
  133. def test_never_update():
  134. import tkinter
  135. del tkinter.Misc.update
  136. del tkinter.Misc.update_idletasks
  137. import matplotlib.pyplot as plt
  138. fig = plt.figure()
  139. plt.show(block=False)
  140. plt.draw() # Test FigureCanvasTkAgg.
  141. fig.canvas.toolbar.configure_subplots() # Test NavigationToolbar2Tk.
  142. # Test FigureCanvasTk filter_destroy callback
  143. fig.canvas.get_tk_widget().after(100, plt.close, fig)
  144. # Check for update() or update_idletasks() in the event queue, functionally
  145. # equivalent to tkinter.Misc.update.
  146. plt.show(block=True)
  147. # Note that exceptions would be printed to stderr; _isolated_tk_test
  148. # checks them.
  149. @_isolated_tk_test(success_count=2)
  150. def test_missing_back_button():
  151. import matplotlib.pyplot as plt
  152. from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk
  153. class Toolbar(NavigationToolbar2Tk):
  154. # Only display the buttons we need.
  155. toolitems = [t for t in NavigationToolbar2Tk.toolitems if
  156. t[0] in ('Home', 'Pan', 'Zoom')]
  157. fig = plt.figure()
  158. print("success")
  159. Toolbar(fig.canvas, fig.canvas.manager.window) # This should not raise.
  160. print("success")
  161. @_isolated_tk_test(success_count=2)
  162. def test_save_figure_return():
  163. import matplotlib.pyplot as plt
  164. from unittest import mock
  165. fig = plt.figure()
  166. prop = "tkinter.filedialog.asksaveasfilename"
  167. with mock.patch(prop, return_value="foobar.png"):
  168. fname = fig.canvas.manager.toolbar.save_figure()
  169. os.remove("foobar.png")
  170. assert fname == "foobar.png"
  171. print("success")
  172. with mock.patch(prop, return_value=""):
  173. fname = fig.canvas.manager.toolbar.save_figure()
  174. assert fname is None
  175. print("success")
  176. @_isolated_tk_test(success_count=1)
  177. def test_canvas_focus():
  178. import tkinter as tk
  179. import matplotlib.pyplot as plt
  180. success = []
  181. def check_focus():
  182. tkcanvas = fig.canvas.get_tk_widget()
  183. # Give the plot window time to appear
  184. if not tkcanvas.winfo_viewable():
  185. tkcanvas.wait_visibility()
  186. # Make sure the canvas has the focus, so that it's able to receive
  187. # keyboard events.
  188. if tkcanvas.focus_lastfor() == tkcanvas:
  189. success.append(True)
  190. plt.close()
  191. root.destroy()
  192. root = tk.Tk()
  193. fig = plt.figure()
  194. plt.plot([1, 2, 3])
  195. root.after(0, plt.show)
  196. root.after(100, check_focus)
  197. root.mainloop()
  198. if success:
  199. print("success")
  200. @_isolated_tk_test(success_count=2)
  201. def test_embedding():
  202. import tkinter as tk
  203. from matplotlib.backends.backend_tkagg import (
  204. FigureCanvasTkAgg, NavigationToolbar2Tk)
  205. from matplotlib.backend_bases import key_press_handler
  206. from matplotlib.figure import Figure
  207. root = tk.Tk()
  208. def test_figure(master):
  209. fig = Figure()
  210. ax = fig.add_subplot()
  211. ax.plot([1, 2, 3])
  212. canvas = FigureCanvasTkAgg(fig, master=master)
  213. canvas.draw()
  214. canvas.mpl_connect("key_press_event", key_press_handler)
  215. canvas.get_tk_widget().pack(expand=True, fill="both")
  216. toolbar = NavigationToolbar2Tk(canvas, master, pack_toolbar=False)
  217. toolbar.pack(expand=True, fill="x")
  218. canvas.get_tk_widget().forget()
  219. toolbar.forget()
  220. test_figure(root)
  221. print("success")
  222. # Test with a dark button color. Doesn't actually check whether the icon
  223. # color becomes lighter, just that the code doesn't break.
  224. root.tk_setPalette(background="sky blue", selectColor="midnight blue",
  225. foreground="white")
  226. test_figure(root)
  227. print("success")