2424pytestmark = 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
2831def qt_core (request ):
2932backend ,= request .node .get_closest_marker ('backend' ).args
@@ -33,19 +36,6 @@ def qt_core(request):
3336return 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 )
5040def test_fig_close ():
5141
@@ -64,50 +54,143 @@ def test_fig_close():
6454assert 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 :
80113target (** 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 :
108162target (** 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
141224signal .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():
548633envs .append (pytest .param (env ,marks = marks ,id = str (env )))
549634return envs
550635
551- _test_timeout = 60 # A reasonably safe value for slower architectures.
552-
553636
554637@pytest .mark .parametrize ("env" ,_get_testable_qt_backends ())
555638def test_enums_available (env ):