Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d60a7ad

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 d60a7ad

1 file changed

Lines changed: 156 additions & 209 deletions

File tree

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,
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)