8000 Factor out machinery for running subprocess tk tests. · matplotlib/matplotlib@47a5a61 · GitHub
[go: up one dir, main page]

Skip to content

Commit 47a5a61

Browse files
committed
Factor out machinery for running subprocess tk tests.
Probably it can also be shared with test_interactive_backends in the future.
1 parent 7b3ac93 commit 47a5a61

File tree

1 file changed

+147
-204
lines changed

1 file changed

+147
-204
lines changed
Lines changed: 147 additions & 204 deletions
Original file line numberDiff line numberDiff line change
@@ -1,236 +1,179 @@
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.
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
1155

1256

1357
@pytest.mark.backend('TkAgg', skip_on_importerror=True)
58+
@_isolated_tk_test(success_count=6) # len(bad_boxes)
1459
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)))
2167
height, width = data.shape[:2]
2268
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")
6283

6384

6485
@pytest.mark.backend('TkAgg', skip_on_importerror=True)
86+
@_isolated_tk_test(success_count=1)
6587
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")
106108

107109

108110
@pytest.mark.backend('TkAgg', skip_on_importerror=True)
109111
@pytest.mark.flaky(reruns=3)
112+
@_isolated_tk_test(success_count=1)
110113
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()
152136

153137

154138
@pytest.mark.backend('TkAgg', skip_on_importerror=True)
155139
@pytest.mark.flaky(reruns=3)
140+
@_isolated_tk_test(success_count=0)
156141
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.
201163

202164

203165
@pytest.mark.backend('TkAgg', skip_on_importerror=True)
166+
@_isolated_tk_test(success_count=2)
204167
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

Comments
 (0)
0