8000 Merge pull request #19825 from anntzer/tkt · matplotlib/matplotlib@10a0e82 · GitHub
[go: up one dir, main page]

Skip to content

Commit 10a0e82

Browse files
authored
Merge pull request #19825 from anntzer/tkt
Factor out machinery for running subprocess tk tests.
2 parents abe4117 + d60a7ad commit 10a0e82

File tree

1 file changed

+156
-209
lines changed

1 file changed

+156
-209
lines changed
Lines changed: 156 additions & 209 deletions
Original file line numberDiff line numberDiff line change
@@ -1,236 +1,183 @@
1+
import functools
2+
import inspect
13
import os
4+
import re
25
import subprocess
36
import sys
47

58
import pytest
69

710
_test_timeout = 10 # Empirically, 1s is not enough on CI.
811

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. Because its source is extracted and run by
23+
itself, coverage will consider it as not being run, so it should be marked
24+
with ``# pragma: no cover``
25+
"""
26+
27+
if func is None:
28+
return functools.partial(_isolated_tk_test, success_count)
29+
30+
# Remove decorators.
31+
source = re.search(r"(?ms)^def .*", inspect.getsource(func)).group(0)
32+
33+
@functools.wraps(func)
34+
def test_func():
35+
try:
36+
proc = subprocess.run(
37+
[sys.executable, "-c", f"{source}\n{func.__name__}()"],
38+
env={**os.environ, "MPLBACKEND": "TkAgg"},
39+
timeout=_test_timeout,
40+
stdout=subprocess.PIPE,
41+
stderr=subprocess.PIPE,
42+
check=True,
43+
universal_newlines=True,
44+
)
45+
except subprocess.TimeoutExpired:
46+
pytest.fail("Subprocess timed out")
47+
except subprocess.CalledProcessError:
48+
pytest.fail("Subprocess failed to test intended behavior")
49+
else:
50+
# macOS may actually emit irrelevant errors about Accelerated
51+
# OpenGL vs. software OpenGL, so suppress them.
52+
# Asserting stderr first (and printing it on failure) should be
53+
# more helpful for debugging that printing a failed success count.
54+
assert not [line for line in proc.stderr.splitlines()
55+
if "OpenGL" not in line]
56+
assert proc.stdout.count("success") == success_count
57+
58+
return test_func
1159

1260

1361
@pytest.mark.backend('TkAgg', skip_on_importerror=True)
14-
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)
62+
@_isolated_tk_test(success_count=6) # len(bad_boxes)
63+
def test_blit(): # pragma: no cover
64+
import matplotlib.pyplot as plt
65+
import numpy as np
66+
from matplotlib.backends import _tkagg
67+
68+
fig, ax = plt.subplots()
69+
photoimage = fig.canvas._tkphoto
70+
data = np.ones((4, 4, 4))
2171
height, width = data.shape[:2]
2272
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)
73+
# Test out of bounds blitting.
74+
bad_boxes = ((-1, 2, 0, 2),
75+
(2, 0, 0, 2),
76+
(1, 6, 0, 2),
77+
(0, 2, -1, 2),
78+
(0, 2, 2, 0),
79+
(0, 2, 1, 6))
80+
for bad_box in bad_boxes:
81+
try:
82+
_tkagg.blit(
83+
photoimage.tk.interpaddr(), str(photoimage), dataptr,
84+
(0, 1, 2, 3), bad_box)
85+
except ValueError:
86+
print("success")
6287

6388

6489
@pytest.mark.backend('TkAgg', skip_on_importerror=True)
65-
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,
10000 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
90+
@_isolated_tk_test(success_count=1)
91+
def test_figuremanager_preserves_host_mainloop(): # pragma: no cover
92+
import tkinter
93+
import matplotlib.pyplot as plt
94+
success = []
95+
96+
def do_plot():
97+
plt.figure()
98+
plt.plot([1, 2], [3, 5])
99+
plt.close()
100+
root.after(0, legitimate_quit)
101+
102+
def legitimate_quit():
103+
root.quit()
104+
success.append(True)
105+
106+
root = tkinter.Tk()
107+
root.after(0, do_plot)
108+
root.mainloop()
109+
110+
if success:
111+
print("success")
106112

107113

108114
@pytest.mark.backend('TkAgg', skip_on_importerror=True)
109115
@pytest.mark.flaky(reruns=3)
110-
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
116+
@_isolated_tk_test(success_count=1)
117+
def test_figuremanager_cleans_own_mainloop(): # pragma: no cover
118+
import tkinter
119+
import time
120+
import matplotlib.pyplot as plt
121+
import threading
122+
from matplotlib.cbook import _get_running_interactive_framework
123+
124+
root = tkinter.Tk()
125+
plt.plot([1, 2, 3], [1, 2, 5])
126+
127+
def target():
128+
while not 'tk' == _get_running_interactive_framework():
129+
time.sleep(.01)
130+
plt.close()
131+
if show_finished_event.wait():
132+
print('success')
133+
134+
show_finished_event = threading.Event()
135+
thread = threading.Thread(target=target, daemon=True)
136+
thread.start()
137+
plt.show(block=True) # Testing if this function hangs.
138+
show_finished_event.set()
139+
thread.join()
152140

153141

154142
@pytest.mark.backend('TkAgg', skip_on_importerror=True)
155143
@pytest.mark.flaky(reruns=3)
156-
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")
144+
@_isolated_tk_test(success_count=0)
145+
def test_never_update(): # pragma: no cover
146+
import tkinter
147+
del tkinter.Misc.update
148+
del tkinter.Misc.update_idletasks
149+
150+
import matplotlib.pyplot as plt
151+
fig = plt.figure()
152+
plt.show(block=False)
153+
154+
plt.draw() # Test FigureCanvasTkAgg.
155+
fig.canvas.toolbar.configure_subplots() # Test NavigationToolbar2Tk.
156+
157+
# Check for update() or update_idletasks() in the event queue, functionally
158+
# equivalent to tkinter.Misc.update.
159+
# Must pause >= 1 ms to process tcl idle events plus extra time to avoid
160+
# flaky tests on slow systems.
161+
plt.pause(0.1)
162+
163+
plt.close(fig) # Test FigureCanvasTk filter_destroy callback
164+
165+
# Note that exceptions would be printed to stderr; _isolated_tk_test
166+
# checks them.
201167

202168

203169
@pytest.mark.backend('TkAgg', skip_on_importerror=True)
204-
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")
170+
@_isolated_tk_test(success_count=2)
171+
def test_missing_back_button(): # pragma: no cover
172+
import matplotlib.pyplot as plt
173+
from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk
174+
175+
class Toolbar(NavigationToolbar2Tk):
176+
# Only display the buttons we need.
177+
toolitems = [t for t in NavigationToolbar2Tk.toolitems if
178+
t[0] in ('Home', 'Pan', 'Zoom')]
179+
180+
fig = plt.figure()
181+
print("success")
182+
Toolbar(fig.canvas, fig.canvas.manager.window) # This should not raise.
183+
print("success")

0 commit comments

Comments
 (0)
0