|
| 1 | +import functools |
| 2 | +import inspect |
1 | 3 | import os
|
| 4 | +import re |
2 | 5 | import subprocess
|
3 | 6 | import sys
|
4 | 7 |
|
5 | 8 | import pytest
|
6 | 9 |
|
7 | 10 | _test_timeout = 10 # Empirically, 1s is not enough on CI.
|
8 | 11 |
|
9 |
| -# NOTE: TkAgg tests seem to have interactions between tests, |
10 |
| -# So isolate each test in a subprocess. See GH#18261 |
| 12 | + |
| 13 | +def _isolated_tk_test(success_count, func=None): |
| 14 | + """ |
| 15 | + A decorator to run *func* in a subprocess and assert that it prints |
| 16 | + "success" *success_count* times and nothing on stderr. |
| 17 | +
|
| 18 | + TkAgg tests seem to have interactions between tests, so isolate each test |
| 19 | + in a subprocess. See GH#18261 |
| 20 | +
|
| 21 | + The decorated function must be fully self-contained, and thus perform |
| 22 | + all the imports it needs. |
| 23 | + """ |
| 24 | + |
| 25 | + if func is None: |
| 26 | + return functools.partial(_isolated_tk_test, success_count) |
| 27 | + |
| 28 | + # Remove decorators. |
| 29 | + source = re.search(r"(?ms)^def .*", inspect.getsource(func)).group(0) |
| 30 | + |
| 31 | + def test_func(): |
| 32 | + try: |
| 33 | + proc = subprocess.run( |
| 34 | + [sys.executable, "-c", f"{source}\n{func.__name__}()"], |
| 35 | + env={**os.environ, "MPLBACKEND": "TkAgg"}, |
| 36 | + timeout=_test_timeout, |
| 37 | + stdout=subprocess.PIPE, |
| 38 | + stderr=subprocess.PIPE, |
| 39 | + check=True, |
| 40 | + universal_newlines=True, |
| 41 | + ) |
| 42 | + except subprocess.TimeoutExpired: |
| 43 | + pytest.fail("Subprocess timed out") |
| 44 | + except subprocess.CalledProcessError: |
| 45 | + pytest.fail("Subprocess failed to test intended behavior") |
| 46 | + else: |
| 47 | + assert proc.stdout.count("success") == success_count |
| 48 | + # macOS may actually emit irrelevant errors about Accelerated |
| 49 | + # OpenGL vs. software OpenGL, so suppress them. |
| 50 | + assert not [line for line in proc.stderr.splitlines() |
| 51 | + if "OpenGL" not in line] |
| 52 | + |
| 53 | + test_func.__name__ = func.__name__ |
| 54 | + return test_func |
11 | 55 |
|
12 | 56 |
|
13 | 57 | @pytest.mark.backend('TkAgg', skip_on_importerror=True)
|
| 58 | +@_isolated_tk_test(success_count=6) # len(bad_boxes) |
14 | 59 | def test_blit():
|
15 |
| - script = """ |
16 |
| -import matplotlib.pyplot as plt |
17 |
| -import numpy as np |
18 |
| -from matplotlib.backends import _tkagg |
19 |
| -def evil_blit(photoimage, aggimage, offsets, bboxptr): |
20 |
| - data = np.asarray(aggimage) |
| 60 | + import matplotlib.pyplot as plt |
| 61 | + import numpy as np |
| 62 | + from matplotlib.backends import _tkagg |
| 63 | + |
| 64 | + fig, ax = plt.subplots() |
| 65 | + photoimage = fig.canvas._tkphoto |
| 66 | + data = np.asarray(np.ones((4, 4, 4))) |
21 | 67 | height, width = data.shape[:2]
|
22 | 68 | dataptr = (height, width, data.ctypes.data)
|
23 |
| - _tkagg.blit( |
24 |
| - photoimage.tk.interpaddr(), str(photoimage), dataptr, offsets, |
25 |
| - bboxptr) |
26 |
| -
|
27 |
| -fig, ax = plt.subplots() |
28 |
| -bad_boxes = ((-1, 2, 0, 2), |
29 |
| - (2, 0, 0, 2), |
30 |
| - (1, 6, 0, 2), |
31 |
| - (0, 2, -1, 2), |
32 |
| - (0, 2, 2, 0), |
33 |
| - (0, 2, 1, 6)) |
34 |
| -for bad_box in bad_boxes: |
35 |
| - try: |
36 |
| - evil_blit(fig.canvas._tkphoto, |
37 |
| - np.ones((4, 4, 4)), |
38 |
| - (0, 1, 2, 3), |
39 |
| - bad_box) |
40 |
| - except ValueError: |
41 |
| - print("success") |
42 |
| -""" |
43 |
| - try: |
44 |
| - proc = subprocess.run( |
45 |
| - [sys.executable, "-c", script], |
46 |
| - env={**os.environ, |
47 |
| - "MPLBACKEND": "TkAgg", |
48 |
| - "SOURCE_DATE_EPOCH": "0"}, |
49 |
| - timeout=_test_timeout, |
50 |
| - stdout=subprocess.PIPE, |
51 |
| - check=True, |
52 |
| - universal_newlines=True, |
53 |
| - ) |
54 |
| - except subprocess.TimeoutExpired: |
55 |
| - pytest.fail("Subprocess timed out") |
56 |
| - except subprocess.CalledProcessError: |
57 |
| - pytest.fail("Likely regression on out-of-bounds data access" |
58 |
| - " in _tkagg.cpp") |
59 |
| - else: |
60 |
| - print(proc.stdout) |
61 |
| - assert proc.stdout.count("success") == 6 # len(bad_boxes) |
| 69 | + # Test out of bounds blitting. |
| 70 | + bad_boxes = ((-1, 2, 0, 2), |
| 71 | + (2, 0, 0, 2), |
| 72 | + (1, 6, 0, 2), |
| 73 | + (0, 2, -1, 2), |
| 74 | + (0, 2, 2, 0), |
| 75 | + (0, 2, 1, 6)) |
| 76 | + for bad_box in bad_boxes: |
| 77 | + try: |
| 78 | + _tkagg.blit( |
| 79 | + photoimage.tk.interpaddr(), str(photoimage), dataptr, |
| 80 | + (0, 1, 2, 3), bad_box) |
| 81 | + except ValueError: |
| 82 | + print("success") |
62 | 83 |
|
63 | 84 |
|
64 | 85 | @pytest.mark.backend('TkAgg', skip_on_importerror=True)
|
| 86 | +@_isolated_tk_test(success_count=1) |
65 | 87 | def test_figuremanager_preserves_host_mainloop():
|
66 |
| - script = """ |
67 |
| -import tkinter |
68 |
| -import matplotlib.pyplot as plt |
69 |
| -success = False |
70 |
| -
|
71 |
| -def do_plot(): |
72 |
| - plt.figure() |
73 |
| - plt.plot([1, 2], [3, 5]) |
74 |
| - plt.close() |
75 |
| - root.after(0, legitimate_quit) |
76 |
| -
|
77 |
| -def legitimate_quit(): |
78 |
| - root.quit() |
79 |
| - global success |
80 |
| - success = True |
81 |
| -
|
82 |
| -root = tkinter.Tk() |
83 |
| -root.after(0, do_plot) |
84 |
| -root.mainloop() |
85 |
| -
|
86 |
| -if success: |
87 |
| - print("success") |
88 |
| -""" |
89 |
| - try: |
90 |
| - proc = subprocess.run( |
91 |
| - [sys.executable, "-c", script], |
92 |
| - env={**os.environ, |
93 |
| - "MPLBACKEND": "TkAgg", |
94 |
| - "SOURCE_DATE_EPOCH": "0"}, |
95 |
| - timeout=_test_timeout, |
96 |
| - stdout=subprocess.PIPE, |
97 |
| - check=True, |
98 |
| - universal_newlines=True, |
99 |
| - ) |
100 |
| - except subprocess.TimeoutExpired: |
101 |
| - pytest.fail("Subprocess timed out") |
102 |
| - except subprocess.CalledProcessError: |
103 |
| - pytest.fail("Subprocess failed to test intended behavior") |
104 |
| - else: |
105 |
| - assert proc.stdout.count("success") == 1 |
| 88 | + import tkinter |
| 89 | + import matplotlib.pyplot as plt |
| 90 | + success = [] |
| 91 | + |
| 92 | + def do_plot(): |
| 93 | + plt.figure() |
| 94 | + plt.plot([1, 2], [3, 5]) |
| 95 | + plt.close() |
| 96 | + root.after(0, legitimate_quit) |
| 97 | + |
| 98 | + def legitimate_quit(): |
| 99 | + root.quit() |
| 100 | + success.append(True) |
| 101 | + |
| 102 | + root = tkinter.Tk() |
| 103 | + root.after(0, do_plot) |
| 104 | + root.mainloop() |
| 105 | + |
| 106 | + if success: |
| 107 | + print("success") |
106 | 108 |
|
107 | 109 |
|
108 | 110 | @pytest.mark.backend('TkAgg', skip_on_importerror=True)
|
109 | 111 | @pytest.mark.flaky(reruns=3)
|
| 112 | +@_isolated_tk_test(success_count=1) |
110 | 113 | def test_figuremanager_cleans_own_mainloop():
|
111 |
| - script = ''' |
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 |
| -
|
118 |
| -root = tkinter.Tk() |
119 |
| -plt.plot([1, 2, 3], [1, 2, 5]) |
120 |
| -
|
121 |
| -def target(): |
122 |
| - while not 'tk' == _get_running_interactive_framework(): |
123 |
| - time.sleep(.01) |
124 |
| - plt.close() |
125 |
| - if show_finished_event.wait(): |
126 |
| - print('success') |
127 |
| -
|
128 |
| -show_finished_event = threading.Event() |
129 |
| -thread = threading.Thread(target=target, daemon=True) |
130 |
| -thread.start() |
131 |
| -plt.show(block=True) # testing if this function hangs |
132 |
| -show_finished_event.set() |
133 |
| -thread.join() |
134 |
| -
|
135 |
| -''' |
136 |
| - try: |
137 |
| - proc = subprocess.run( |
138 |
| - [sys.executable, "-c", script], |
139 |
| - env={**os.environ, |
140 |
| - "MPLBACKEND": "TkAgg", |
141 |
| - "SOURCE_DATE_EPOCH": "0"}, |
142 |
| - timeout=_test_timeout, |
143 |
| - stdout=subprocess.PIPE, |
144 |
| - universal_newlines=True, |
145 |
| - check=True |
146 |
| - ) |
147 |
| - except subprocess.TimeoutExpired: |
148 |
| - pytest.fail("Most likely plot.show(block=True) hung") |
149 |
| - except subprocess.CalledProcessError: |
150 |
| - pytest.fail("Subprocess failed to test intended behavior") |
151 |
| - assert proc.stdout.count("success") == 1 |
| 114 | + import tkinter |
| 115 | + import time |
| 116 | + import matplotlib.pyplot as plt |
| 117 | + import threading |
| 118 | + from matplotlib.cbook import _get_running_interactive_framework |
| 119 | + |
| 120 | + root = tkinter.Tk() |
| 121 | + plt.plot([1, 2, 3], [1, 2, 5]) |
| 122 | + |
| 123 | + def target(): |
| 124 | + while not 'tk' == _get_running_interactive_framework(): |
| 125 | + time.sleep(.01) |
| 126 | + plt.close() |
| 127 | + if show_finished_event.wait(): |
| 128 | + print('success') |
| 129 | + |
| 130 | + show_finished_event = threading.Event() |
| 131 | + thread = threading.Thread(target=target, daemon=True) |
| 132 | + thread.start() |
| 133 | + plt.show(block=True) # Testing if this function hangs. |
| 134 | + show_finished_event.set() |
| 135 | + thread.join() |
152 | 136 |
|
153 | 137 |
|
154 | 138 | @pytest.mark.backend('TkAgg', skip_on_importerror=True)
|
155 | 139 | @pytest.mark.flaky(reruns=3)
|
| 140 | +@_isolated_tk_test(success_count=0) |
156 | 141 | def test_never_update():
|
157 |
| - script = """ |
158 |
| -import tkinter |
159 |
| -del tkinter.Misc.update |
160 |
| -del tkinter.Misc.update_idletasks |
161 |
| -
|
162 |
| -import matplotlib.pyplot as plt |
163 |
| -fig = plt.figure() |
164 |
| -plt.show(block=False) |
165 |
| -
|
166 |
| -# regression test on FigureCanvasTkAgg |
167 |
| -plt.draw() |
168 |
| -# regression test on NavigationToolbar2Tk |
169 |
| -fig.canvas.toolbar.configure_subplots() |
170 |
| -
|
171 |
| -# check for update() or update_idletasks() in the event queue |
172 |
| -# functionally equivalent to tkinter.Misc.update |
173 |
| -# must pause >= 1 ms to process tcl idle events plus |
174 |
| -# extra time to avoid flaky tests on slow systems |
175 |
| -plt.pause(0.1) |
176 |
| -
|
177 |
| -# regression test on FigureCanvasTk filter_destroy callback |
178 |
| -plt.close(fig) |
179 |
| -""" |
180 |
| - try: |
181 |
| - proc = subprocess.run( |
182 |
| - [sys.executable, "-c", script], |
183 |
| - env={**os.environ, |
184 |
| - "MPLBACKEND": "TkAgg", |
185 |
| - "SOURCE_DATE_EPOCH": "0"}, |
186 |
| - timeout=_test_timeout, |
187 |
| - capture_output=True, |
188 |
| - universal_newlines=True, |
189 |
| - ) |
190 |
| - except subprocess.TimeoutExpired: |
191 |
| - pytest.fail("Subprocess timed out") |
192 |
| - else: |
193 |
| - # test framework doesn't see tkinter callback exceptions normally |
194 |
| - # see tkinter.Misc.report_callback_exception |
195 |
| - assert "Exception in Tkinter callback" not in proc.stderr |
196 |
| - # make sure we can see other issues |
197 |
| - print(proc.stderr, file=sys.stderr) |
198 |
| - # Checking return code late so the Tkinter assertion happens first |
199 |
| - if proc.returncode: |
200 |
| - pytest.fail("Subprocess failed to test intended behavior") |
| 142 | + import tkinter |
| 143 | + del tkinter.Misc.update |
| 144 | + del tkinter.Misc.update_idletasks |
| 145 | + |
| 146 | + import matplotlib.pyplot as plt |
| 147 | + fig = plt.figure() |
| 148 | + plt.show(block=False) |
| 149 | + |
| 150 | + plt.draw() # Test FigureCanvasTkAgg. |
| 151 | + fig.canvas.toolbar.configure_subplots() # Test NavigationToolbar2Tk. |
| 152 | + |
| 153 | + # Check for update() or update_idletasks() in the event queue, functionally |
| 154 | + # equivalent to tkinter.Misc.update. |
| 155 | + # Must pause >= 1 ms to process tcl idle events plus extra time to avoid |
| 156 | + # flaky tests on slow systems. |
| 157 | + plt.pause(0.1) |
| 158 | + |
| 159 | + plt.close(fig) # Test FigureCanvasTk filter_destroy callback |
| 160 | + |
| 161 | + # Note that exceptions would be printed to stderr; _isolated_tk_test |
| 162 | + # checks them. |
201 | 163 |
|
202 | 164 |
|
203 | 165 | @pytest.mark.backend('TkAgg', skip_on_importerror=True)
|
| 166 | +@_isolated_tk_test(success_count=2) |
204 | 167 | def test_missing_back_button():
|
205 |
| - script = """ |
206 |
| -import matplotlib.pyplot as plt |
207 |
| -from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk |
208 |
| -class Toolbar(NavigationToolbar2Tk): |
209 |
| - # only display the buttons we need |
210 |
| - toolitems = [t for t in NavigationToolbar2Tk.toolitems if |
211 |
| - t[0] in ('Home', 'Pan', 'Zoom')] |
212 |
| -
|
213 |
| -fig = plt.figure() |
214 |
| -print("setup complete") |
215 |
| -# this should not raise |
216 |
| -Toolbar(fig.canvas, fig.canvas.manager.window) |
217 |
| -print("success") |
218 |
| -""" |
219 |
| - try: |
220 |
| - proc = subprocess.run( |
221 |
| - [sys.executable, "-c", script], |
222 |
| - env={**os.environ, |
223 |
| - "MPLBACKEND": "TkAgg", |
224 |
| - "SOURCE_DATE_EPOCH": "0"}, |
225 |
| - timeout=_test_timeout, |
226 |
| - stdout=subprocess.PIPE, |
227 |
| - universal_newlines=True, |
228 |
| - ) |
229 |
| - except subprocess.TimeoutExpired: |
230 |
| - pytest.fail("Subprocess timed out") |
231 |
| - else: |
232 |
| - assert proc.stdout.count("setup complete") == 1 |
233 |
| - assert proc.stdout.count("success") == 1 |
234 |
| - # Checking return code late so the stdout assertions happen first |
235 |
| - if proc.returncode: |
236 |
| - pytest.fail("Subprocess failed to test intended behavior") |
| 168 | + import matplotlib.pyplot as plt |
| 169 | + from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk |
| 170 | + |
| 171 | + class Toolbar(NavigationToolbar2Tk): |
| 172 | + # Only display the buttons we need. |
| 173 | + toolitems = [t for t in NavigationToolbar2Tk.toolitems if |
| 174 | + t[0] in ('Home', 'Pan', 'Zoom')] |
| 175 | + |
| 176 | + fig = plt.figure() |
| 177 | + print("success") |
| 178 | + Toolbar(fig.canvas, fig.canvas.manager.window) # This should not raise. |
| 179 | + print("success") |
0 commit comments