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 .api .plugins import Plugins
25
+ from spyder .app .cli_options import get_options
26
+ from spyder .config .manager import CONF
27
+ from spyder .plugins .debugger .plugin import Debugger
28
+ from spyder .plugins .help .utils .sphinxify import CSS_PATH
29
+ from spyder .plugins .ipythonconsole .plugin import IPythonConsole
30
+ from spyder .plugins .ipythonconsole .utils .style import create_style_class
31
+
9
32
33
+ # =============================================================================
34
+ # ---- Constants
35
+ # =============================================================================
36
+ SHELL_TIMEOUT = 20000
37
+ TEMP_DIRECTORY = tempfile .gettempdir ()
38
+ NEW_DIR = 'new_workingdir'
10
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,284 @@ 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
+ # Use the automatic backend if requested
140
+ auto_backend = request .node .get_closest_marker ('auto_backend' )
141
+ if auto_backend :
142
+ configuration .set ('ipython_console' , 'pylab/backend' , 1 )
143
+
144
+ # Use the Tkinter backend if requested
145
+ tk_backend = request .node .get_closest_marker ('tk_backend' )
146
+ if tk_backend :
147
+ configuration .set ('ipython_console' , 'pylab/backend' , 3 )
148
+
149
+ # Start a Pylab client if requested
150
+ pylab_client = request .node .get_closest_marker ('pylab_client' )
151
+ is_pylab = True if pylab_client else False
152
+
153
+ # Start a Sympy client if requested
154
+ sympy_client = request .node .get_closest_marker ('sympy_client' )
155
+ is_sympy = True if sympy_client else False
156
+
157
+ # Start a Cython client if requested
158
+ cython_client = request .node .get_closest_marker ('cython_client' )
159
+ is_cython = True if cython_client else False
160
+
161
+ # Use an external interpreter if requested
162
+ external_interpreter = request .node .get_closest_marker (
163
+ 'external_interpreter' )
164
+ if external_interpreter :
165
+ configuration .set ('main_interpreter' , 'default' , False )
166
+ configuration .set ('main_interpreter' , 'executable' , sys .executable )
167
+ else :
168
+ configuration .set ('main_interpreter' , 'default' , True )
169
+ configuration .set ('main_interpreter' , 'executable' , '' )
170
+
171
+ # Use the test environment interpreter if requested
172
+ test_environment_interpreter = request .node .get_closest_marker (
173
+ 'test_environment_interpreter' )
174
+ if test_environment_interpreter :
175
+ configuration .set ('main_interpreter' , 'default' , False )
176
+ configuration .set (
177
+ 'main_interpreter' , 'executable' , get_conda_test_env ()[1 ])
178
+ else :
179
+ configuration .set ('main_interpreter' , 'default' , True )
180
+ configuration .set ('main_interpreter' , 'executable' , '' )
181
+
182
+ # Conf css_path in the Appeareance plugin
183
+ configuration .set ('appearance' , 'css_path' , CSS_PATH )
184
+
185
+ # Create the console and a new client and set environment
186
+ os .environ ['IPYCONSOLE_TESTING' ] = 'True'
187
+ window = MainWindowMock ()
188
+ console = IPythonConsole (parent = window , configuration = configuration )
189
+
190
+ # connect to a debugger plugin
191
+ debugger = Debugger (parent = window , configuration = configuration )
192
+
193
+ def get_plugin (name ):
194
+ if name == Plugins .IPythonConsole :
195
+ return console
196
+ return None
197
+
198
+ debugger .get_plugin = get_plugin
199
+ debugger .on_ipython_console_available ()
200
+ console .on_initialize ()
201
+ console ._register ()
202
+ console .create_new_client (is_pylab = is_pylab ,
203
+ is_sympy = is_sympy ,
204
+ is_cython = is_cython )
205
+ window .setCentralWidget (console .get_widget ())
206
+
207
+ # Set exclamation mark to True
208
+ configuration .set ('debugger' , 'pdb_use_exclamation_mark' , True )
209
+
210
+ if os .name == 'nt' :
211
+ qtbot .addWidget (window )
212
+
213
+ with qtbot .waitExposed (window ):
214
+ window .resize (640 , 480 )
215
+ window .show ()
216
+
217
+ if auto_backend or tk_backend :
218
+ qtbot .wait (SHELL_TIMEOUT )
219
+ console .create_new_client ()
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
+ if test_environment_interpreter :
226
+ # conda version is not always up to date, so a version warning
227
+ # might be displayed, so shell.spyder_kernel_ready will not be True
228
+ qtbot .waitUntil (
229
+ lambda : shell ._prompt_html is not None ,
230
+ timeout = SHELL_TIMEOUT )
231
+ else :
232
+ qtbot .waitUntil (
233
+ lambda : (
234
+ shell .spyder_kernel_ready
235
+ and shell ._prompt_html is not None
236
+ ),
237
+ timeout = SHELL_TIMEOUT )
238
+ except Exception :
239
+ # Print content of shellwidget and close window
240
+ print (console .get_current_shellwidget (
241
+ )._control .toPlainText ())
242
+ client = console .get_current_client ()
243
+ if client .info_page != client .blank_page :
244
+ print ('info_page' )
245
+ print (client .info_page )
246
+ raise
247
+
248
+ # Check for thread or open file leaks
249
+ known_leak = request .node .get_closest_marker ('known_leak' )
250
+
251
+ if os .name != 'nt' and not known_leak :
252
+ # _DummyThread are created if current_thread() is called from them.
253
+ # They will always leak (From python doc) so we ignore them.
254
+ init_threads = [
255
+ repr (thread ) for thread in threading .enumerate ()
256
+ if not isinstance (thread , threading ._DummyThread )]
257
+ proc = psutil .Process ()
258
+ init_files = [repr (f ) for f in proc .open_files ()]
259
+ init_subprocesses = [repr (f ) for f in proc .children ()]
260
+
261
+ yield console
262
+
263
+ # Print shell content if failed
264
+ if request .node .rep_setup .passed :
265
+ if request .node .rep_call .failed :
266
+ # Print content of shellwidget and close window
267
+ print (console .get_current_shellwidget (
268
+ )._control .toPlainText ())
269
+ client = console .get_current_client ()
270
+ if client .info_page != client .blank_page :
271
+ print ('info_page' )
272
+ print (client .info_page )
273
+
274
+ # Close
275
+ console .on_close ()
276
+ os .environ .pop ('IPYCONSOLE_TESTING' )
277
+
278
+ if os .name == 'nt' or known_leak :
279
+ # Do not test for leaks
280
+ return
281
+
282
+ def show_diff (init_list , now_list , name ):
283
+ sys .stderr .write (f"Extra { name } before test:\n " )
284
+ for item in init_list :
285
+ if item in now_list :
286
+ now_list .remove (item )
287
+ else :
288
+ sys .stderr .write (item + "\n " )
289
+ sys .stderr .write (f"Extra { name } after test:\n " )
290
+ for item in now_list :
291
+ sys .stderr .write (item + "\n " )
292
+
293
+ # The test is not allowed to open new files or threads.
294
+ try :
295
+ def threads_condition ():
296
+ threads = [
297
+ thread for thread in threading .enumerate ()
298
+ if not isinstance (thread , threading ._DummyThread )]
299
+ return (len (init_threads ) >= len (threads ))
300
+
301
+ qtbot .waitUntil (threads_condition , timeout = SHELL_TIMEOUT )
302
+ except Exception :
303
+ now_threads = [
304
+ thread for thread in threading .enumerate ()
305
+ if not isinstance (thread , threading ._DummyThread )]
306
+ threads = [repr (t ) for t in now_threads ]
307
+ show_diff (init_threads , threads , "thread" )
308
+ sys .stderr .write ("Running Threads stacks:\n " )
309
+ now_thread_ids = [t .ident for t in now_threads ]
310
+ for thread_id , frame in sys ._current_frames ().items ():
311
+ if thread_id in now_thread_ids :
312
+ sys .stderr .write ("\n Thread " + str (threads ) + ":\n " )
313
+ traceback .print_stack (frame )
314
+ raise
315
+
316
+ try :
317
+ # -1 from closed client
318
+ qtbot .waitUntil (lambda : (
319
+ len (init_subprocesses ) - 1 >= len (proc .children ())),
320
+ timeout = SHELL_TIMEOUT )
321
+ except Exception :
322
+ subprocesses = [repr (f ) for f in proc .children ()]
323
+ show_diff (init_subprocesses , subprocesses , "processes" )
324
+ raise
325
+
326
+ try :
327
+ qtbot .waitUntil (
328
+ lambda : (len (init_files ) >= len (proc .open_files ())),
329
+ timeout = SHELL_TIMEOUT )
330
+ except Exception :
331
+ files = [repr (f ) for f in proc .open_files ()]
332
+ show_diff (init_files , files , "files" )
333
+ raise
0 commit comments