5
5
# Licensed under the terms of the MIT License
6
6
# ----------------------------------------------------------------------------
7
7
8
+ # Standard library imports
9
+ import os
10
+ import os .path as osp
11
+ import sys
12
+ import tempfile
13
+ import threading
14
+ import traceback
15
+ from unittest .mock import Mock
16
+
17
+ # Third-party imports
18
+ import psutil
19
+ from pygments .token import Name
8
20
import pytest
21
+ from qtpy .QtWidgets import QMainWindow
22
+
23
+ # Local imports
24
+ from spyder .app .cli_options import get_options
25
+ from spyder .config .manager import CONF
26
+ from spyder .plugins .help .utils .sphinxify import CSS_PATH
27
+ from spyder .plugins .ipythonconsole .plugin import IPythonConsole
28
+ from spyder .plugins .ipythonconsole .utils .style import create_style_class
29
+
9
30
10
31
32
+ # =============================================================================
33
+ # ---- Constants
34
+ # =============================================================================
35
+ SHELL_TIMEOUT = 20000
36
+ TEMP_DIRECTORY = tempfile .gettempdir ()
37
+ NON_ASCII_DIR = osp .join (TEMP_DIRECTORY , u'測試' , u'اختبار' )
38
+ NEW_DIR = 'new_workingdir'
39
+
40
+
41
+ # =============================================================================
42
+ # ---- Pytest adjustments
43
+ # =============================================================================
11
44
@pytest .hookimpl (tryfirst = True , hookwrapper = True )
12
45
def pytest_runtest_makereport (item , call ):
13
46
# execute all other hooks to obtain the report object
@@ -17,3 +50,275 @@ def pytest_runtest_makereport(item, call):
17
50
# set a report attribute for each phase of a call, which can
18
51
# be "setup", "call", "teardown"
19
52
setattr (item , "rep_" + rep .when , rep )
53
+
54
+
55
+ # =============================================================================
56
+ # ---- Utillity Functions
57
+ # =============================================================================
58
+ def get_console_font_color (syntax_style ):
59
+ styles = create_style_class (syntax_style ).styles
60
+ font_color = styles [Name ]
61
+ return font_color
62
+
63
+
64
+ def get_console_background_color (style_sheet ):
65
+ background_color = style_sheet .split ('background-color:' )[1 ]
66
+ background_color = background_color .split (';' )[0 ]
67
+ return background_color
68
+
69
+
70
+ def get_conda_test_env (test_env_name = 'spytest-ž' ):
71
+ """
72
+ Return the full prefix path of the given `test_env_name` and its
73
+ executable.
74
+ """
75
+ if 'envs' in sys .prefix :
76
+ root_prefix = osp .dirname (osp .dirname (sys .prefix ))
77
+ else :
78
+ root_prefix = sys .prefix
79
+
80
+ test_env_prefix = osp .join (root_prefix , 'envs' , test_env_name )
81
+
82
+ if os .name == 'nt' :
83
+ test_env_executable = osp .join (test_env_prefix , 'python.exe' )
84
+ else :
85
+ test_env_executable = osp .join (test_env_prefix , 'bin' , 'python' )
86
+
87
+ return (test_env_prefix , test_env_executable )
88
+
89
+
90
+ # =============================================================================
91
+ # ---- Fixtures
92
+ # =============================================================================
93
+ @pytest .fixture
94
+ def ipyconsole (qtbot , request , tmpdir ):
95
+ """IPython console fixture."""
96
+ configuration = CONF
97
+ no_web_widgets = request .node .get_closest_marker ('no_web_widgets' )
98
+
99
+ class MainWindowMock (QMainWindow ):
100
+
101
+ def __init__ (self ):
102
+ # This avoids using the cli options passed to pytest
103
+ sys_argv = [sys .argv [0 ]]
104
+ self ._cli_options = get_options (sys_argv )[0 ]
105
+ if no_web_widgets :
106
+ self ._cli_options .no_web_widgets = True
107
+ super ().__init__ ()
108
+
109
+ def __getattr__ (self , attr ):
110
+ if attr == 'consoles_menu_actions' :
111
+ return []
112
+ elif attr == 'editor' :
113
+ return None
114
+ else :
115
+ return Mock ()
116
+
117
+ # Tests assume inline backend
118
+ configuration .set ('ipython_console' , 'pylab/backend' , 0 )
119
+
120
+ # Start the console in a fixed working directory
121
+ use_startup_wdir = request .node .get_closest_marker ('use_startup_wdir' )
122
+ if use_startup_wdir :
123
+ new_wdir = str (tmpdir .mkdir (NEW_DIR ))
124
+ configuration .set (
125
+ 'workingdir' ,
126
+ 'startup/use_project_or_home_directory' ,
127
+ False
128
+ )
129
+ configuration .set ('workingdir' , 'startup/use_fixed_directory' , True )
130
+ configuration .set ('workingdir' , 'startup/fixed_directory' , new_wdir )
131
+ else :
132
+ configuration .set (
133
+ 'workingdir' ,
134
+ 'startup/use_project_or_home_directory' ,
135
+ True
136
+ )
137
+ configuration .set ('workingdir' , 'startup/use_fixed_directory' , False )
138
+
139
+ # Test the console with a non-ascii temp dir
140
+ non_ascii_dir = request .node .get_closest_marker ('non_ascii_dir' )
141
+ if non_ascii_dir :
142
+ test_dir = NON_ASCII_DIR
143
+ else :
144
+ test_dir = ''
145
+
146
+ # Instruct the console to not use a stderr file
147
+ no_stderr_file = request .node .get_closest_marker ('no_stderr_file' )
148
+ if no_stderr_file :
149
+ test_no_stderr = 'True'
150
+ else :
151
+ test_no_stderr = ''
152
+
153
+ # Use the automatic backend if requested
154
+ auto_backend = request .node .get_closest_marker ('auto_backend' )
155
+ if auto_backend :
156
+ configuration .set ('ipython_console' , 'pylab/backend' , 1 )
157
+
158
+ # Use the Tkinter backend if requested
159
+ tk_backend = request .node .get_closest_marker ('tk_backend' )
160
+ if tk_backend :
161
+ configuration .set ('ipython_console' , 'pylab/backend' , 3 )
162
+
163
+ # Start a Pylab client if requested
164
+ pylab_client = request .node .get_closest_marker ('pylab_client' )
165
+ is_pylab = True if pylab_client else False
166
+
167
+ # Start a Sympy client if requested
168
+ sympy_client = request .node .get_closest_marker ('sympy_client' )
169
+ is_sympy = True if sympy_client else False
170
+
171
+ # Start a Cython client if requested
172
+ cython_client = request .node .get_closest_marker ('cython_client' )
173
+ is_cython = True if cython_client else False
174
+
175
+ # Use an external interpreter if requested
176
+ external_interpreter = request .node .get_closest_marker (
177
+ 'external_interpreter' )
178
+ if external_interpreter :
179
+ configuration .set ('main_interpreter' , 'default' , False )
180
+ configuration .set ('main_interpreter' , 'executable' , sys .executable )
181
+ else :
182
+ configuration .set ('main_interpreter' , 'default' , True )
183
+ configuration .set ('main_interpreter' , 'executable' , '' )
184
+
185
+ # Use the test environment interpreter if requested
186
+ test_environment_interpreter = request .node .get_closest_marker (
187
+ 'test_environment_interpreter' )
188
+ if test_environment_interpreter :
189
+ configuration .set ('main_interpreter' , 'default' , False )
190
+ configuration .set (
191
+ 'main_interpreter' , 'executable' , get_conda_test_env ()[1 ])
192
+ else :
193
+ configuration .set ('main_interpreter' , 'default' , True )
194
+ configuration .set ('main_interpreter' , 'executable' , '' )
195
+
196
+ # Conf css_path in the Appeareance plugin
197
+ configuration .set ('appearance' , 'css_path' , CSS_PATH )
198
+
199
+ # Create the console and a new client and set environment
200
+ os .environ ['IPYCONSOLE_TESTING' ] = 'True'
201
+ os .environ ['IPYCONSOLE_TEST_DIR' ] = test_dir
202
+ os .environ ['IPYCONSOLE_TEST_NO_STDERR' ] = test_no_stderr
203
+ window = MainWindowMock ()
204
+ console = IPythonConsole (parent = window , configuration = configuration )
205
+ console ._register ()
206
+ console .create_new_client (is_pylab = is_pylab ,
207
+ is_sympy = is_sympy ,
208
+ is_cython = is_cython )
209
+ window .setCentralWidget (console .get_widget ())
210
+
211
+ # Set exclamation mark to True
212
+ configuration .set ('ipython_console' , 'pdb_use_exclamation_mark' , True )
213
+
214
+ if os .name == 'nt' :
215
+ qtbot .addWidget (window )
216
+
217
+ with qtbot .waitExposed (window ):
218
+ window .resize (640 , 480 )
219
+ window .show ()
220
+
221
+ # Wait until the window is fully up
222
+ qtbot .waitUntil (lambda : console .get_current_shellwidget () is not None )
223
+ shell = console .get_current_shellwidget ()
224
+ try :
225
+ qtbot .waitUntil (lambda : shell ._prompt_html is not None ,
226
+ timeout = SHELL_TIMEOUT )
227
+ except Exception :
228
+ # Print content of shellwidget and close window
229
+ print (console .get_current_shellwidget (
230
+ )._control .toPlainText ())
231
+ client = console .get_current_client ()
232
+ if client .info_page != client .blank_page :
233
+ print ('info_page' )
234
+ print (client .info_page )
235
+ raise
236
+
237
+ # Check for thread or open file leaks
238
+ known_leak = request .node .get_closest_marker ('known_leak' )
239
+
240
+ if os .name != 'nt' and not known_leak :
241
+ # _DummyThread are created if current_thread() is called from them.
242
+ # They will always leak (From python doc) so we ignore them.
243
+ init_threads = [
244
+ repr (thread ) for thread in threading .enumerate ()
245
+ if not isinstance (thread , threading ._DummyThread )]
246
+ proc = psutil .Process ()
247
+ init_files = [repr (f ) for f in proc .open_files ()]
248
+ init_subprocesses = [repr (f ) for f in proc .children ()]
249
+
250
+ yield console
251
+
252
+ # Print shell content if failed
253
+ if request .node .rep_setup .passed :
254
+ if request .node .rep_call .failed :
255
+ # Print content of shellwidget and close window
256
+ print (console .get_current_shellwidget (
257
+ )._control .toPlainText ())
258
+ client = console .get_current_client ()
259
+ if client .info_page != client .blank_page :
260
+ print ('info_page' )
261
+ print (client .info_page )
262
+
263
+ # Close
264
+ console .on_close ()
265
+ os .environ .pop ('IPYCONSOLE_TESTING' )
266
+ os .environ .pop ('IPYCONSOLE_TEST_DIR' )
267
+ os .environ .pop ('IPYCONSOLE_TEST_NO_STDERR' )
268
+
269
+ if os .name == 'nt' or known_leak :
270
+ # Do not test for leaks
271
+ return
272
+
273
+ def show_diff (init_list , now_list , name ):
274
+ sys .stderr .write (f"Extra { name } before test:\n " )
275
+ for item in init_list :
276
+ if item in now_list :
277
+ now_list .remove (item )
278
+ else :
279
+ sys .stderr .write (item + "\n " )
280
+ sys .stderr .write (f"Extra { name } after test:\n " )
281
+ for item in now_list :
282
+ sys .stderr .write (item + "\n " )
283
+
284
+ # The test is not allowed to open new files or threads.
285
+ try :
286
+ def threads_condition ():
287
+ threads = [
288
+ thread for thread in threading .enumerate ()
289
+ if not isinstance (thread , threading ._DummyThread )]
290
+ return (len (init_threads ) >= len (threads ))
291
+
292
+ qtbot .waitUntil (threads_condition , timeout = SHELL_TIMEOUT )
293
+ except Exception :
294
+ now_threads = [
295
+ thread for thread in threading .enumerate ()
296
+ if not isinstance (thread , threading ._DummyThread )]
297
+ threads = [repr (t ) for t in now_threads ]
298
+ show_diff (init_threads , threads , "thread" )
299
+ sys .stderr .write ("Running Threads stacks:\n " )
300
+ now_thread_ids = [t .ident for t in now_threads ]
301
+ for threadId , frame in sys ._current_frames ().items ():
302
+ if threadId in now_thread_ids :
303
+ sys .stderr .write ("\n Thread " + str (threads ) + ":\n " )
304
+ traceback .print_stack (frame )
305
+ raise
306
+
307
+ try :
308
+ # -1 from closed client
309
+ qtbot .waitUntil (lambda : (
310
+ len (init_subprocesses ) - 1 >= len (proc .children ())),
311
+ timeout = SHELL_TIMEOUT )
312
+ except Exception :
313
+ subprocesses = [repr (f ) for f in proc .children ()]
314
+ show_diff (init_subprocesses , subprocesses , "processes" )
315
+ raise
316
+
317
+ try :
318
+ qtbot .waitUntil (
319
+ lambda : (len (init_files ) >= len (proc .open_files ())),
320
+ timeout = SHELL_TIMEOUT )
321
+ except Exception :
322
+ files = [repr (f ) for f in proc .open_files ()]
323
+ show_diff (init_files , files , "files" )
324
+ raise
0 commit comments