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

Skip to content

Commit 67ce245

Browse files
committed
Move sigint tests into subprocesses.
This prevents them accidentally breaking the test runner itself, depending on platform.
1 parent ba820b7 commit 67ce245

1 file changed

Lines changed: 117 additions & 45 deletions

File tree

lib/matplotlib/tests/test_backend_qt.py

Lines changed: 117 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
pytestmark = pytest.mark.skip('No usable Qt bindings')
2525

2626

27+
_test_timeout = 60 # A reasonably safe value for slower architectures.
28+
29+
2730
@pytest.fixture
2831
def qt_core(request):
2932
backend, = request.node.get_closest_marker('backend').args
@@ -33,19 +36,6 @@ def qt_core(request):
3336
return QtCore
3437

3538

36-
@pytest.fixture
37-
def platform_simulate_ctrl_c(request):
38-
import signal
39-
from functools import partial
40-
41-
if hasattr(signal, "CTRL_C_EVENT"):
42-
win32api = pytest.importorskip('win32api')
43-
return partial(win32api.GenerateConsoleCtrlEvent, 0, 0)
44-
else:
45-
# we're not on windows
46-
return partial(os.kill, os.getpid(), signal.SIGINT)
47-
48-
4939
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
5040
def test_fig_close():
5141

@@ -64,50 +54,134 @@ def test_fig_close():
6454
assert init_figs == Gcf.figs
6555

6656

67-
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
68-
@pytest.mark.parametrize("target, kwargs", [
69-
(plt.show, {"block": True}),
70-
(plt.pause, {"interval": 10})
71-
])
72-
def test_sigint(qt_core, platform_simulate_ctrl_c, target,
73-
kwargs):
74-
plt.figure()
75-
def fire_signal():
76-
platform_simulate_ctrl_c()
57+
class InterruptiblePopen(subprocess.Popen):
58+
"""
59+
A Popen that passes flags that allow triggering KeyboardInterrupt.
60+
"""
61+
62+
def __init__(self, *args, **kwargs):
63+
if sys.platform == 'win32':
64+
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
65+
super().__init__(
66+
*args, **kwargs,
67+
# Force Agg so that each test can switch to its desired Qt backend.
68+
env={**os.environ, "MPLBACKEND": "Agg", "SOURCE_DATE_EPOCH": "0"},
69+
stdout=subprocess.PIPE, universal_newlines=True)
70+
71+
def wait_for(self, terminator):
72+
"""Read until the terminator is reached."""
73+
buf = ''
74+
while True:
75+
c = self.stdout.read(1)
76+
if not c:
77+
raise RuntimeError(
78+
f'Subprocess died before emitting expected {terminator!r}')
79+
buf += c
80+
if buf.endswith(terminator):
81+
return
82+
83+
def interrupt(self):
84+
"""Interrupt process in a platform-specific way."""
85+
if sys.platform == 'win32':
86+
self.send_signal(signal.CTRL_C_EVENT)
87+
else:
88+
self.send_signal(signal.SIGINT)
89+
90+
91+
def _test_sigint_impl(backend, target_name, kwargs):
92+
import sys
93+
import matplotlib.pyplot as plt
94+
plt.switch_backend(backend)
95+
from matplotlib.backends.qt_compat import QtCore
7796

78-
qt_core.QTimer.singleShot(100, fire_signal)
79-
with pytest.raises(KeyboardInterrupt):
97+
target = getattr(plt, target_name)
98+
99+
fig = plt.figure()
100+
fig.canvas.mpl_connect('draw_event',
101+
lambda *args: print('DRAW', flush=True))
102+
try:
80103
target(**kwargs)
104+
except KeyboardInterrupt:
105+
print('SUCCESS', flush=True)
81106

82107

83108
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
84109
@pytest.mark.parametrize("target, kwargs", [
85-
(plt.show, {"block": True}),
86-
(plt.pause, {"interval": 10})
110+
('show', {'block': True}),
111+
('pause', {'interval': 10})
87112
])
88-
def test_other_signal_before_sigint(qt_core, platform_simulate_ctrl_c,
89-
target, kwargs):
90-
plt.figure()
113+
def test_sigint(target, kwargs):
114+
backend = plt.get_backend()
115+
proc = InterruptiblePopen(
116+
[sys.executable, "-c",
117+
inspect.getsource(_test_sigint_impl) +
118+
f"\n_test_sigint_impl({backend!r}, {target!r}, {kwargs!r})"])
119+
try:
120+
proc.wait_for('DRAW')
121+
proc.interrupt()
122+
stdout, _ = proc.communicate(timeout=_test_timeout)
123+
except:
124+
proc.kill()
125+
stdout, _ = proc.communicate()
126+
raise
127+
print(stdout)
128+
assert 'SUCCESS' in stdout
129+
130+
131+
def _test_other_signal_before_sigint_impl(backend, target_name, kwargs):
132+
import signal
133+
import sys
134+
import matplotlib.pyplot as plt
135+
plt.switch_backend(backend)
136+
from matplotlib.backends.qt_compat import QtCore
91137

92-
sigcld_caught = False
93-
def custom_sigpipe_handler(signum, frame):
94-
nonlocal sigcld_caught
95-
sigcld_caught = True
96-
signal.signal(signal.SIGCHLD, custom_sigpipe_handler)
138+
target = getattr(plt, target_name)
97139

98-
def fire_other_signal():
99-
os.kill(os.getpid(), signal.SIGCHLD)
140+
fig = plt.figure()
141+
fig.canvas.mpl_connect('draw_event',
142+
lambda *args: print('DRAW', flush=True))
100143

101-
def fire_sigint():
102-
platform_simulate_ctrl_c()
144+
timer = fig.canvas.new_timer(interval=1)
145+
timer.single_shot = True
146+
timer.add_callback(print, 'SIGUSR1', flush=True)
103147

104-
qt_core.QTimer.singleShot(50, fire_other_signal)
105-
qt_core.QTimer.singleShot(100, fire_sigint)
148+
def custom_signal_handler(signum, frame):
149+
timer.start()
150+
signal.signal(signal.SIGUSR1, custom_signal_handler)
106151

107-
with pytest.raises(KeyboardInterrupt):
152+
try:
108153
target(**kwargs)
154+
except KeyboardInterrupt:
155+
print('SUCCESS', flush=True)
109156

110-
assert sigcld_caught
157+
158+
@pytest.mark.skipif(sys.platform == 'win32',
159+
reason='No other signal available to send on Windows')
160+
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
161+
@pytest.mark.parametrize("target, kwargs", [
162+
('show', {'block': True}),
163+
('pause', {'interval': 10})
164+
])
165+
def test_other_signal_before_sigint(target, kwargs):
166+
backend = plt.get_backend()
167+
proc = InterruptiblePopen(
168+
[sys.executable, "-c",
169+
inspect.getsource(_test_other_signal_before_sigint_impl) +
170+
"\n_test_other_signal_before_sigint_impl("
171+
f"{backend!r}, {target!r}, {kwargs!r})"])
172+
try:
173+
proc.wait_for('DRAW')
174+
os.kill(proc.pid, signal.SIGUSR1)
175+
proc.wait_for('SIGUSR1')
176+
proc.interrupt()
177+
stdout, _ = proc.communicate(timeout=_test_timeout)
178+
except:
179+
proc.kill()
180+
stdout, _ = proc.communicate()
181+
raise
182+
print(stdout)
183+
assert 'SUCCESS' in stdout
184+
plt.figure()
111185

112186

113187
@pytest.mark.backend('Qt5Agg')
@@ -548,8 +622,6 @@ def _get_testable_qt_backends():
548622
envs.append(pytest.param(env, marks=marks, id=str(env)))
549623
return envs
550624

551-
_test_timeout = 60 # A reasonably safe value for slower architectures.
552-
553625

554626
@pytest.mark.parametrize("env", _get_testable_qt_backends())
555627
def test_enums_available(env):

0 commit comments

Comments
 (0)