Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 17a8626

Browse filesBrowse files
authored
Merge pull request #21244 from meeseeksmachine/auto-backport-of-pr-20907-on-v3.5.x
Backport PR #20907 on branch v3.5.x (Move sigint tests into subprocesses)
2 parents c702960 + 69824d8 commit 17a8626
Copy full SHA for 17a8626

File tree

Expand file treeCollapse file tree

3 files changed

+160
-63
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+160
-63
lines changed

‎.appveyor.yml

Copy file name to clipboardExpand all lines: .appveyor.yml
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ install:
6060
# pull pywin32 from conda because on py38 there is something wrong with finding
6161
# the dlls when insalled from pip
6262
- conda install -c conda-forge pywin32
63+
# install pyqt from conda-forge
64+
- conda install -c conda-forge pyqt
6365
- echo %PYTHON_VERSION% %TARGET_ARCH%
6466
# Install dependencies from PyPI.
6567
- python -m pip install --upgrade -r requirements/testing/all.txt %EXTRAREQS% %PINNEDVERS%

‎lib/matplotlib/backends/qt_compat.py

Copy file name to clipboardExpand all lines: lib/matplotlib/backends/qt_compat.py
+13-1Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,20 @@ def _maybe_allow_interrupt(qapp):
228228
rsock.fileno(), _enum('QtCore.QSocketNotifier.Type').Read
229229
)
230230

231+
# We do not actually care about this value other than running some
232+
# Python code to ensure that the interpreter has a chance to handle the
233+
# signal in Python land. We also need to drain the socket because it
234+
# will be written to as part of the wakeup! There are some cases where
235+
# this may fire too soon / more than once on Windows so we should be
236+
# forgiving about reading an empty socket.
237+
rsock.setblocking(False)
231238
# Clear the socket to re-arm the notifier.
232-
sn.activated.connect(lambda *args: rsock.recv(1))
239+
@sn.activated.connect
240+
def _may_clear_sock(*args):
241+
try:
242+
rsock.recv(1)
243+
except BlockingIOError:
244+
pass
233245

234246
def handle(*args):
235247
nonlocal handler_args

‎lib/matplotlib/tests/test_backend_qt.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_backend_qt.py
+145-62Lines changed: 145 additions & 62 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,143 @@ 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 WaitForStringPopen(subprocess.Popen):
58+
"""
59+
A Popen that passes flags that allow triggering KeyboardInterrupt.
60+
"""
7761

78-
qt_core.QTimer.singleShot(100, fire_signal)
79-
with pytest.raises(KeyboardInterrupt):
62+
def __init__(self, *args, **kwargs):
63+
if sys.platform == 'win32':
64+
kwargs['creationflags'] = subprocess.CREATE_NEW_CONSOLE
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+
84+
def _test_sigint_impl(backend, target_name, kwargs):
85+
import sys
86+
import matplotlib.pyplot as plt
87+
import os
88+
import threading
89+
90+
plt.switch_backend(backend)
91+
from matplotlib.backends.qt_compat import QtCore
92+
93+
def interupter():
94+
if sys.platform == 'win32':
95+
import win32api
96+
win32api.GenerateConsoleCtrlEvent(0, 0)
97+
else:
98+
import signal
99+
os.kill(os.getpid(), signal.SIGINT)
100+
101+
target = getattr(plt, target_name)
102+
timer = threading.Timer(1, interupter)
103+
fig = plt.figure()
104+
fig.canvas.mpl_connect(
105+
'draw_event',
106+
lambda *args: print('DRAW', flush=True)
107+
)
108+
fig.canvas.mpl_connect(
109+
'draw_event',
110+
lambda *args: timer.start()
111+
)
112+
try:
80113
target(**kwargs)
114+
except KeyboardInterrupt:
115+
print('SUCCESS', flush=True)
81116

82117

83118
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
84119
@pytest.mark.parametrize("target, kwargs", [
85-
(plt.show, {"block": True}),
86-
(plt.pause, {"interval": 10})
120+
('show', {'block': True}),
121+
('pause', {'interval': 10})
87122
])
88-
def test_other_signal_before_sigint(qt_core, platform_simulate_ctrl_c,
89-
target, kwargs):
90-
plt.figure()
123+
def test_sigint(target, kwargs):
124+
backend = plt.get_backend()
125+
proc = WaitForStringPopen(
126+
[sys.executable, "-c",
127+
inspect.getsource(_test_sigint_impl) +
128+
f"\n_test_sigint_impl({backend!r}, {target!r}, {kwargs!r})"])
129+
try:
130+
proc.wait_for('DRAW')
131+
stdout, _ = proc.communicate(timeout=_test_timeout)
132+
except:
133+
proc.kill()
134+
stdout, _ = proc.communicate()
135+
raise
136+
print(stdout)
137+
assert 'SUCCESS' in stdout
138+
139+
140+
def _test_other_signal_before_sigint_impl(backend, target_name, kwargs):
141+
import signal
142+
import sys
143+
import matplotlib.pyplot as plt
144+
plt.switch_backend(backend)
145+
from matplotlib.backends.qt_compat import QtCore
91146

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)
147+
target = getattr(plt, target_name)
97148

98-
def fire_other_signal():
99-
os.kill(os.getpid(), signal.SIGCHLD)
149+
fig = plt.figure()
150+
fig.canvas.mpl_connect('draw_event',
151+
lambda *args: print('DRAW', flush=True))
100152

101-
def fire_sigint():
102-
platform_simulate_ctrl_c()
153+
timer = fig.canvas.new_timer(interval=1)
154+
timer.single_shot = True
155+
timer.add_callback(print, 'SIGUSR1', flush=True)
103156

104-
qt_core.QTimer.singleShot(50, fire_other_signal)
105-
qt_core.QTimer.singleShot(100, fire_sigint)
157+
def custom_signal_handler(signum, frame):
158+
timer.start()
159+
signal.signal(signal.SIGUSR1, custom_signal_handler)
106160

107-
with pytest.raises(KeyboardInterrupt):
161+
try:
108162
target(**kwargs)
163+
except KeyboardInterrupt:
164+
print('SUCCESS', flush=True)
109165

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

112195

113196
@pytest.mark.backend('Qt5Agg')
@@ -140,29 +223,31 @@ def custom_handler(signum, frame):
140223

141224
signal.signal(signal.SIGINT, custom_handler)
142225

143-
# mainloop() sets SIGINT, starts Qt event loop (which triggers timer and
144-
# exits) and then mainloop() resets SIGINT
145-
matplotlib.backends.backend_qt._BackendQT.mainloop()
226+
try:
227+
# mainloop() sets SIGINT, starts Qt event loop (which triggers timer
228+
# and exits) and then mainloop() resets SIGINT
229+
matplotlib.backends.backend_qt._BackendQT.mainloop()
146230

147-
# Assert: signal handler during loop execution is changed
148-
# (can't test equality with func)
149-
assert event_loop_handler != custom_handler
231+
# Assert: signal handler during loop execution is changed
232+
# (can't test equality with func)
233+
assert event_loop_handler != custom_handler
150234

151-
# Assert: current signal handler is the same as the one we set before
152-
assert signal.getsignal(signal.SIGINT) == custom_handler
235+
# Assert: current signal handler is the same as the one we set before
236+
assert signal.getsignal(signal.SIGINT) == custom_handler
153237

154-
# Repeat again to test that SIG_DFL and SIG_IGN will not be overridden
155-
for custom_handler in (signal.SIG_DFL, signal.SIG_IGN):
156-
qt_core.QTimer.singleShot(0, fire_signal_and_quit)
157-
signal.signal(signal.SIGINT, custom_handler)
238+
# Repeat again to test that SIG_DFL and SIG_IGN will not be overridden
239+
for custom_handler in (signal.SIG_DFL, signal.SIG_IGN):
240+
qt_core.QTimer.singleShot(0, fire_signal_and_quit)
241+
signal.signal(signal.SIGINT, custom_handler)
158242

159-
_BackendQT5.mainloop()
243+
_BackendQT5.mainloop()
160244

161-
assert event_loop_handler == custom_handler
162-
assert signal.getsignal(signal.SIGINT) == custom_handler
245+
assert event_loop_handler == custom_handler
246+
assert signal.getsignal(signal.SIGINT) == custom_handler
163247

164-
# Reset SIGINT handler to what it was before the test
165-
signal.signal(signal.SIGINT, original_handler)
248+
finally:
249+
# Reset SIGINT handler to what it was before the test
250+
signal.signal(signal.SIGINT, original_handler)
166251

167252

168253
@pytest.mark.parametrize(
@@ -548,8 +633,6 @@ def _get_testable_qt_backends():
548633
envs.append(pytest.param(env, marks=marks, id=str(env)))
549634
return envs
550635

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

554637
@pytest.mark.parametrize("env", _get_testable_qt_backends())
555638
def test_enums_available(env):

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.