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
2831def 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 )
5040def 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 ())
555627def test_enums_available (env ):
0 commit comments