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

File tree

1 file changed

+117
-45
lines changed

1 file changed

+117
-45
lines changed

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)