Skip to content

Commit 28b9245

Browse files
committed
Merge from 5.x: PR #20335
2 parents 2e9c380 + 4ec9249 commit 28b9245

File tree

2 files changed

+321
-316
lines changed

2 files changed

+321
-316
lines changed

spyder/plugins/ipythonconsole/tests/conftest.py

Lines changed: 314 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.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+
932

33+
# =============================================================================
34+
# ---- Constants
35+
# =============================================================================
36+
SHELL_TIMEOUT = 20000
37+
TEMP_DIRECTORY = tempfile.gettempdir()
38+
NEW_DIR = 'new_workingdir'
1039

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,284 @@ 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+
# 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("\nThread " + 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

Comments
 (0)