Skip to content

Commit c95126f

Browse files
committed
IPython console: Move ipyconsole fixture to conftest
1 parent da661cc commit c95126f

File tree

2 files changed

+309
-296
lines changed

2 files changed

+309
-296
lines changed

spyder/plugins/ipythonconsole/tests/conftest.py

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,42 @@
55
# Licensed under the terms of the MIT License
66
# ----------------------------------------------------------------------------
77

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
820
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+
930

1031

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+
# =============================================================================
1144
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
1245
def pytest_runtest_makereport(item, call):
1346
# execute all other hooks to obtain the report object
@@ -17,3 +50,275 @@ def pytest_runtest_makereport(item, call):
1750
# set a report attribute for each phase of a call, which can
1851
# be "setup", "call", "teardown"
1952
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("\nThread " + 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

Comments
 (0)