Skip to content

Commit 4ec9249

Browse files
authored
Merge pull request #20335 from ccordoba12/move-ipyconsole-fixture
PR: Move ipyconsole fixture to conftest (Testing)
2 parents 2bb3542 + 85dfbd8 commit 4ec9249

File tree

2 files changed

+316
-310
lines changed

2 files changed

+316
-310
lines changed

spyder/plugins/ipythonconsole/tests/conftest.py

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,41 @@
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

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'
1038

39+
40+
# =============================================================================
41+
# ---- Pytest adjustments
42+
# =============================================================================
1143
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
1244
def pytest_runtest_makereport(item, call):
1345
# execute all other hooks to obtain the report object
@@ -17,3 +49,279 @@ def pytest_runtest_makereport(item, call):
1749
# set a report attribute for each phase of a call, which can
1850
# be "setup", "call", "teardown"
1951
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("\nThread " + 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

Comments
 (0)