24
24
pytestmark = pytest .mark .skip ('No usable Qt bindings' )
25
25
26
26
27
+ _test_timeout = 60 # A reasonably safe value for slower architectures.
28
+
29
+
27
30
@pytest .fixture
28
31
def qt_core (request ):
29
32
backend , = request .node .get_closest_marker ('backend' ).args
@@ -33,19 +36,6 @@ def qt_core(request):
33
36
return QtCore
34
37
35
38
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
-
49
39
@pytest .mark .backend ('QtAgg' , skip_on_importerror = True )
50
40
def test_fig_close ():
51
41
@@ -64,50 +54,143 @@ def test_fig_close():
64
54
assert init_figs == Gcf .figs
65
55
66
56
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
+ """
77
61
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 :
80
113
target (** kwargs )
114
+ except KeyboardInterrupt :
115
+ print ('SUCCESS' , flush = True )
81
116
82
117
83
118
@pytest .mark .backend ('QtAgg' , skip_on_importerror = True )
84
119
@pytest .mark .parametrize ("target, kwargs" , [
85
- (plt . show , {" block" : True }),
86
- (plt . pause , {" interval" : 10 })
120
+ (' show' , {' block' : True }),
121
+ (' pause' , {' interval' : 10 })
87
122
])
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
91
146
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 )
97
148
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 ))
100
152
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 )
103
156
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 )
106
160
107
- with pytest . raises ( KeyboardInterrupt ) :
161
+ try :
108
162
target (** kwargs )
163
+ except KeyboardInterrupt :
164
+ print ('SUCCESS' , flush = True )
109
165
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 ()
111
194
112
195
113
196
@pytest .mark .backend ('Qt5Agg' )
@@ -140,29 +223,31 @@ def custom_handler(signum, frame):
140
223
141
224
signal .signal (signal .SIGINT , custom_handler )
142
225
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 ()
146
230
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
150
234
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
153
237
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 )
158
242
159
- _BackendQT5 .mainloop ()
243
+ _BackendQT5 .mainloop ()
160
244
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
163
247
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 )
166
251
167
252
168
253
@pytest .mark .parametrize (
@@ -548,8 +633,6 @@ def _get_testable_qt_backends():
548
633
envs .append (pytest .param (env , marks = marks , id = str (env )))
549
634
return envs
550
635
551
- _test_timeout = 60 # A reasonably safe value for slower architectures.
552
-
553
636
554
637
@pytest .mark .parametrize ("env" , _get_testable_qt_backends ())
555
638
def test_enums_available (env ):
0 commit comments