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