feat: move fixtures to pytest-plugin

This commit is contained in:
Noortheen Raja 2022-03-23 21:18:09 +05:30 committed by Noorhteen Raja NJ
parent 4983e67224
commit 48086a5778
6 changed files with 420 additions and 426 deletions

View file

@ -6,7 +6,7 @@ import os
import sys
import subprocess
from setuptools import setup, find_packages
from setuptools import setup
from setuptools.command.sdist import sdist
from setuptools.command.install import install
from setuptools.command.develop import develop
@ -354,7 +354,7 @@ def main():
"xonsh = xonsh.pyghooks:XonshLexer",
"xonshcon = xonsh.pyghooks:XonshConsoleLexer",
],
"pytest11": ["xonsh = xonsh.pytest_plugin"],
"pytest11": ["xonsh = xonsh.pytest.plugin"],
"console_scripts": [
"xonsh = xonsh.main:main",
"xonsh-cat = xonsh.xoreutils.cat:main",

View file

@ -1,347 +0,0 @@
import os
import sys
import types
import typing as tp
from unittest.mock import MagicMock
import pytest
from tools import DummyHistory, DummyShell, copy_env, sp
from xonsh import commands_cache
from xonsh.aliases import Aliases
from xonsh.built_ins import XSH, XonshSession
from xonsh.completer import Completer
from xonsh.events import events
from xonsh.execer import Execer
from xonsh.jobs import tasks
from xonsh.parsers.completion_context import CompletionContextParser
@pytest.fixture
def source_path():
"""Get the xonsh source path."""
pwd = os.path.dirname(__file__)
return os.path.dirname(pwd)
@pytest.fixture
def xonsh_execer(monkeypatch, xonsh_session):
"""Initiate the Execer with a mocked nop `load_builtins`"""
yield xonsh_session.execer
@pytest.fixture
def xonsh_execer_exec(xonsh_execer):
def factory(input, **kwargs):
xonsh_execer.exec(input, **kwargs)
return True
return factory
@pytest.fixture
def xonsh_execer_parse(xonsh_execer):
def factory(input):
tree = XSH.execer.parse(input, ctx=None)
return tree
return factory
@pytest.fixture
def patch_commands_cache_bins(xession, tmp_path, monkeypatch):
def _factory(binaries: tp.List[str]):
xession.env["PATH"] = [tmp_path]
exec_mock = MagicMock(return_value=binaries)
monkeypatch.setattr(commands_cache, "executables_in", exec_mock)
return xession.commands_cache
return _factory
@pytest.fixture
def patch_locate_binary(monkeypatch):
def locate_binary(self, name):
return os.path.join(os.path.dirname(__file__), "bin", name)
def factory(cc: commands_cache.CommandsCache):
monkeypatch.setattr(cc, "locate_binary", types.MethodType(locate_binary, cc))
return cc
return factory
@pytest.fixture
def monkeypatch_stderr(monkeypatch):
"""Monkeypath sys.stderr with no ResourceWarning."""
with open(os.devnull, "w") as fd:
monkeypatch.setattr(sys, "stderr", fd)
yield
@pytest.fixture
def xonsh_events():
yield events
for name, oldevent in vars(events).items():
# Heavily based on transmogrification
species = oldevent.species
newevent = events._mkevent(name, species, species.__doc__)
setattr(events, name, newevent)
@pytest.fixture(scope="session")
def session_os_env():
"""Env with values from os.environ like real session"""
from xonsh.environ import Env, default_env
return Env(default_env())
@pytest.fixture(scope="session")
def session_env():
"""Env with some initial values that doesn't load from os.environ"""
from xonsh.environ import Env
initial_vars = {
"UPDATE_OS_ENVIRON": False,
"XONSH_DEBUG": 1,
"XONSH_COLOR_STYLE": "default",
"VC_BRANCH_TIMEOUT": 1,
"XONSH_ENCODING": "utf-8",
"XONSH_ENCODING_ERRORS": "strict",
"COMMANDS_CACHE_SAVE_INTERMEDIATE": False,
}
env = Env(initial_vars)
return env
@pytest.fixture(scope="session")
def session_execer():
return Execer()
@pytest.fixture
def os_env(session_os_env):
"""A mutable copy of Original session_os_env"""
return copy_env(session_os_env)
@pytest.fixture
def env(tmpdir, session_env):
"""a mutable copy of session_env"""
env_copy = copy_env(session_env)
initial_vars = {"XONSH_DATA_DIR": str(tmpdir)}
env_copy.update(initial_vars)
return env_copy
@pytest.fixture
def xonsh_session(xonsh_events, session_execer, os_env, monkeypatch) -> XonshSession:
"""a fixture to use where XonshSession is fully loaded without any mocks"""
XSH.load(
ctx={},
execer=session_execer,
commands_cache=commands_cache.CommandsCache(),
env=os_env,
)
yield XSH
XSH.unload()
tasks.clear() # must to this to enable resetting all_jobs
@pytest.fixture
def mock_xonsh_session(monkeypatch, xonsh_events, xonsh_session, env):
"""Mock out most of the builtins xonsh attributes."""
# make sure that all other fixtures call this mock only one time
session = []
def factory(*attrs: str):
"""
Parameters
----------
attrs
do not mock the given attributes
Returns
-------
XonshSession
with most of the attributes mocked out
"""
if session:
raise RuntimeError("The factory should be called only once per test")
for attr, val in [
("env", env),
("shell", DummyShell()),
("help", lambda x: x),
("aliases", Aliases()),
("exit", False),
("history", DummyHistory()),
(
"commands_cache",
commands_cache.CommandsCache(),
), # since env,aliases change , patch cmds-cache
# ("subproc_captured", sp),
("subproc_uncaptured", sp),
("subproc_captured_stdout", sp),
("subproc_captured_inject", sp),
("subproc_captured_object", sp),
("subproc_captured_hiddenobject", sp),
]:
if attr in attrs:
continue
monkeypatch.setattr(xonsh_session, attr, val)
for attr, val in [
("evalx", eval),
("execx", None),
("compilex", None),
# Unlike all the other stuff, this has to refer to the "real" one because all modules that would
# be firing events on the global instance.
("events", xonsh_events),
]:
# attributes to builtins are dynamicProxy and should pickup the following
monkeypatch.setattr(xonsh_session.builtins, attr, val)
session.append(xonsh_session)
return xonsh_session
yield factory
session.clear()
@pytest.fixture
def xession(mock_xonsh_session) -> XonshSession:
"""Mock out most of the builtins xonsh attributes."""
return mock_xonsh_session()
@pytest.fixture
def xsh_with_aliases(mock_xonsh_session) -> XonshSession:
"""Xonsh mock-session with default set of aliases"""
return mock_xonsh_session("aliases")
@pytest.fixture
def xsh_with_env(mock_xonsh_session) -> XonshSession:
"""Xonsh mock-session with os.environ"""
return mock_xonsh_session("env")
@pytest.fixture(scope="session")
def completion_context_parse():
return CompletionContextParser().parse
@pytest.fixture(scope="session")
def completer_obj():
return Completer()
@pytest.fixture
def check_completer(completer_obj):
"""Helper function to run completer and parse the results as set of strings"""
completer = completer_obj
def _factory(
line: str, prefix: "None|str" = "", send_original=False, complete_fn=None
):
"""
Parameters
----------
line
prefix
send_original
if True, return the original result from the completer (e.g. RichCompletion instances ...)
complete_fn
if given, use that to get the completions
Returns
-------
completions as set of string if not send
"""
if prefix is not None:
line += " " + prefix
if complete_fn is None:
completions, _ = completer.complete_line(line)
else:
ctx = completer_obj.parse(line)
out = complete_fn(ctx)
if isinstance(out, tuple):
completions = out[0]
else:
completions = out
values = {getattr(i, "value", i).strip() for i in completions}
if send_original:
# just return the bare completions without appended-space for easier assertions
return values, completions
return values
return _factory
@pytest.fixture
def ptk_shell(xonsh_execer):
from prompt_toolkit.input import create_pipe_input
from prompt_toolkit.output import DummyOutput
from xonsh.ptk_shell.shell import PromptToolkitShell
inp = create_pipe_input()
out = DummyOutput()
shell = PromptToolkitShell(
execer=xonsh_execer, ctx={}, ptk_args={"input": inp, "output": out}
)
yield inp, out, shell
inp.close()
@pytest.fixture
def readline_shell(xonsh_execer, tmpdir, mocker):
from xonsh.readline_shell import ReadlineShell
inp_path = tmpdir / "in"
inp = inp_path.open("w+")
out_path = tmpdir / "out"
out = out_path.open("w+")
shell = ReadlineShell(execer=xonsh_execer, ctx={}, stdin=inp, stdout=out)
mocker.patch.object(shell, "_load_remaining_input_into_queue")
yield shell
inp.close()
out.close()
def pytest_configure(config):
"""Abort test run if --flake8 requested, since it would hang on parser_test.py"""
if config.getoption("--flake8", ""):
pytest.exit("pytest-flake8 no longer supported, use flake8 instead.")
@pytest.fixture
def load_xontrib():
to_unload = []
def wrapper(*names: str):
from xonsh.xontribs import xontribs_load
for name in names:
module = f"xontrib.{name}"
if module not in sys.modules:
to_unload.append(module)
xontribs_load([name])
return
yield wrapper
for mod in to_unload:
del sys.modules[mod]

0
xonsh/pytest/__init__.py Normal file
View file

418
xonsh/pytest/plugin.py Normal file
View file

@ -0,0 +1,418 @@
"""Pytest plugin for testing Xonsh.
These fixture names are Public API and need to be handled carefully as there are Xontribs dependent on them for testing
"""
import importlib
import os
import sys
import types
import typing as tp
from traceback import extract_tb, format_list
from unittest.mock import MagicMock
import pytest
from xonsh import commands_cache
from xonsh.aliases import Aliases
from xonsh.built_ins import XSH, XonshSession
from xonsh.completer import Completer
from xonsh.events import events
from xonsh.execer import Execer
from xonsh.jobs import tasks
from xonsh.main import setup
from xonsh.parsers.completion_context import CompletionContextParser
from .tools import DummyHistory, DummyShell, copy_env, sp
def pytest_configure(config):
setup()
def pytest_collection_modifyitems(items):
"""Move xsh test first to work around a bug in normal
pytest cleanup. The order of tests are otherwise preserved.
"""
xsh_items = []
other_items = []
for item in items:
if isinstance(item, XshFunction):
xsh_items.append(item)
else:
other_items.append(item)
items[:] = xsh_items + other_items
def _limited_traceback(excinfo):
"""Return a formatted traceback with all the stack
from this frame (i.e __file__) up removed
"""
tb = extract_tb(excinfo.tb)
try:
idx = [__file__ in e for e in tb].index(True)
return format_list(tb[idx + 1 :])
except ValueError:
return format_list(tb)
def pytest_collect_file(parent, path):
if path.ext.lower() == ".xsh" and path.basename.startswith("test_"):
return XshFile.from_parent(parent, fspath=path)
class XshFile(pytest.File):
def collect(self):
sys.path.append(self.fspath.dirname)
mod = importlib.import_module(self.fspath.purebasename)
sys.path.pop(0)
tests = [t for t in dir(mod) if t.startswith("test_")]
for test_name in tests:
obj = getattr(mod, test_name)
if hasattr(obj, "__call__"): # noqa
yield XshFunction.from_parent(
self, name=test_name, test_func=obj, test_module=mod
)
class XshFunction(pytest.Item):
def __init__(self, name, parent, test_func, test_module):
super().__init__(name, parent)
self._test_func = test_func
self._test_module = test_module
def runtest(self, *args, **kwargs):
self._test_func(*args, **kwargs)
def repr_failure(self, excinfo, **_):
"""called when self.runtest() raises an exception."""
formatted_tb = _limited_traceback(excinfo)
formatted_tb.insert(0, "xonsh execution failed\n")
formatted_tb.append(f"{excinfo.type.__name__}: {excinfo.value}")
return "".join(formatted_tb)
def reportinfo(self):
return self.fspath, 0, f"xonsh test: {self.name}"
@pytest.fixture
def source_path():
"""Get the xonsh source path."""
pwd = os.path.dirname(__file__)
return os.path.dirname(pwd)
@pytest.fixture
def xonsh_execer(monkeypatch, xonsh_session):
"""Initiate the Execer with a mocked nop `load_builtins`"""
yield xonsh_session.execer
@pytest.fixture
def xonsh_execer_exec(xonsh_execer):
def factory(input, **kwargs):
xonsh_execer.exec(input, **kwargs)
return True
return factory
@pytest.fixture
def xonsh_execer_parse(xonsh_execer):
def factory(input):
tree = XSH.execer.parse(input, ctx=None)
return tree
return factory
@pytest.fixture
def patch_commands_cache_bins(xession, tmp_path, monkeypatch):
def _factory(binaries: tp.List[str]):
xession.env["PATH"] = [tmp_path]
exec_mock = MagicMock(return_value=binaries)
monkeypatch.setattr(commands_cache, "executables_in", exec_mock)
return xession.commands_cache
return _factory
@pytest.fixture
def patch_locate_binary(monkeypatch):
def locate_binary(self, name):
return os.path.join(os.path.dirname(__file__), "bin", name)
def factory(cc: commands_cache.CommandsCache):
monkeypatch.setattr(cc, "locate_binary", types.MethodType(locate_binary, cc))
return cc
return factory
@pytest.fixture
def monkeypatch_stderr(monkeypatch):
"""Monkeypath sys.stderr with no ResourceWarning."""
with open(os.devnull, "w") as fd:
monkeypatch.setattr(sys, "stderr", fd)
yield
@pytest.fixture
def xonsh_events():
yield events
for name, oldevent in vars(events).items():
# Heavily based on transmogrification
species = oldevent.species
newevent = events._mkevent(name, species, species.__doc__)
setattr(events, name, newevent)
@pytest.fixture(scope="session")
def session_os_env():
"""Env with values from os.environ like real session"""
from xonsh.environ import Env, default_env
return Env(default_env())
@pytest.fixture(scope="session")
def session_env():
"""Env with some initial values that doesn't load from os.environ"""
from xonsh.environ import Env
initial_vars = {
"UPDATE_OS_ENVIRON": False,
"XONSH_DEBUG": 1,
"XONSH_COLOR_STYLE": "default",
"VC_BRANCH_TIMEOUT": 1,
"XONSH_ENCODING": "utf-8",
"XONSH_ENCODING_ERRORS": "strict",
"COMMANDS_CACHE_SAVE_INTERMEDIATE": False,
}
env = Env(initial_vars)
return env
@pytest.fixture(scope="session")
def session_execer():
return Execer()
@pytest.fixture
def os_env(session_os_env):
"""A mutable copy of Original session_os_env"""
return copy_env(session_os_env)
@pytest.fixture
def env(tmpdir, session_env):
"""a mutable copy of session_env"""
env_copy = copy_env(session_env)
initial_vars = {"XONSH_DATA_DIR": str(tmpdir)}
env_copy.update(initial_vars)
return env_copy
@pytest.fixture
def xonsh_session(xonsh_events, session_execer, os_env, monkeypatch) -> XonshSession:
"""a fixture to use where XonshSession is fully loaded without any mocks"""
XSH.load(
ctx={},
execer=session_execer,
commands_cache=commands_cache.CommandsCache(),
env=os_env,
)
yield XSH
XSH.unload()
tasks.clear() # must to this to enable resetting all_jobs
@pytest.fixture
def mock_xonsh_session(monkeypatch, xonsh_events, xonsh_session, env):
"""Mock out most of the builtins xonsh attributes."""
# make sure that all other fixtures call this mock only one time
session = []
def factory(*attrs: str):
"""
Parameters
----------
attrs
do not mock the given attributes
Returns
-------
XonshSession
with most of the attributes mocked out
"""
if session:
raise RuntimeError("The factory should be called only once per test")
for attr, val in [
("env", env),
("shell", DummyShell()),
("help", lambda x: x),
("aliases", Aliases()),
("exit", False),
("history", DummyHistory()),
(
"commands_cache",
commands_cache.CommandsCache(),
), # since env,aliases change , patch cmds-cache
# ("subproc_captured", sp),
("subproc_uncaptured", sp),
("subproc_captured_stdout", sp),
("subproc_captured_inject", sp),
("subproc_captured_object", sp),
("subproc_captured_hiddenobject", sp),
]:
if attr in attrs:
continue
monkeypatch.setattr(xonsh_session, attr, val)
for attr, val in [
("evalx", eval),
("execx", None),
("compilex", None),
# Unlike all the other stuff, this has to refer to the "real" one because all modules that would
# be firing events on the global instance.
("events", xonsh_events),
]:
# attributes to builtins are dynamicProxy and should pickup the following
monkeypatch.setattr(xonsh_session.builtins, attr, val)
session.append(xonsh_session)
return xonsh_session
yield factory
session.clear()
@pytest.fixture
def xession(mock_xonsh_session) -> XonshSession:
"""Mock out most of the builtins xonsh attributes."""
return mock_xonsh_session()
@pytest.fixture
def xsh_with_aliases(mock_xonsh_session) -> XonshSession:
"""Xonsh mock-session with default set of aliases"""
return mock_xonsh_session("aliases")
@pytest.fixture
def xsh_with_env(mock_xonsh_session) -> XonshSession:
"""Xonsh mock-session with os.environ"""
return mock_xonsh_session("env")
@pytest.fixture(scope="session")
def completion_context_parse():
return CompletionContextParser().parse
@pytest.fixture(scope="session")
def completer_obj():
return Completer()
@pytest.fixture
def check_completer(completer_obj):
"""Helper function to run completer and parse the results as set of strings"""
completer = completer_obj
def _factory(
line: str, prefix: "None|str" = "", send_original=False, complete_fn=None
):
"""
Parameters
----------
line
prefix
send_original
if True, return the original result from the completer (e.g. RichCompletion instances ...)
complete_fn
if given, use that to get the completions
Returns
-------
completions as set of string if not send
"""
if prefix is not None:
line += " " + prefix
if complete_fn is None:
completions, _ = completer.complete_line(line)
else:
ctx = completer_obj.parse(line)
out = complete_fn(ctx)
if isinstance(out, tuple):
completions = out[0]
else:
completions = out
values = {getattr(i, "value", i).strip() for i in completions}
if send_original:
# just return the bare completions without appended-space for easier assertions
return values, completions
return values
return _factory
@pytest.fixture
def ptk_shell(xonsh_execer):
from prompt_toolkit.input import create_pipe_input
from prompt_toolkit.output import DummyOutput
from xonsh.ptk_shell.shell import PromptToolkitShell
inp = create_pipe_input()
out = DummyOutput()
shell = PromptToolkitShell(
execer=xonsh_execer, ctx={}, ptk_args={"input": inp, "output": out}
)
yield inp, out, shell
inp.close()
@pytest.fixture
def readline_shell(xonsh_execer, tmpdir, mocker):
from xonsh.readline_shell import ReadlineShell
inp_path = tmpdir / "in"
inp = inp_path.open("w+")
out_path = tmpdir / "out"
out = out_path.open("w+")
shell = ReadlineShell(execer=xonsh_execer, ctx={}, stdin=inp, stdout=out)
mocker.patch.object(shell, "_load_remaining_input_into_queue")
yield shell
inp.close()
out.close()
@pytest.fixture
def load_xontrib():
to_unload = []
def wrapper(*names: str):
from xonsh.xontribs import xontribs_load
for name in names:
module = f"xontrib.{name}"
if module not in sys.modules:
to_unload.append(module)
xontribs_load([name])
return
yield wrapper
for mod in to_unload:
del sys.modules[mod]

View file

@ -1,77 +0,0 @@
"""Pytest plugin for testing xsh files."""
import importlib
import sys
from traceback import extract_tb, format_list
import pytest
from xonsh.main import setup
def pytest_configure(config):
setup()
def pytest_collection_modifyitems(items):
"""Move xsh test first to work around a bug in normal
pytest cleanup. The order of tests are otherwise preserved.
"""
xsh_items = []
other_items = []
for item in items:
if isinstance(item, XshFunction):
xsh_items.append(item)
else:
other_items.append(item)
items[:] = xsh_items + other_items
def _limited_traceback(excinfo):
"""Return a formatted traceback with all the stack
from this frame (i.e __file__) up removed
"""
tb = extract_tb(excinfo.tb)
try:
idx = [__file__ in e for e in tb].index(True)
return format_list(tb[idx + 1 :])
except ValueError:
return format_list(tb)
def pytest_collect_file(parent, path):
if path.ext.lower() == ".xsh" and path.basename.startswith("test_"):
return XshFile.from_parent(parent, fspath=path)
class XshFile(pytest.File):
def collect(self):
sys.path.append(self.fspath.dirname)
mod = importlib.import_module(self.fspath.purebasename)
sys.path.pop(0)
tests = [t for t in dir(mod) if t.startswith("test_")]
for test_name in tests:
obj = getattr(mod, test_name)
if hasattr(obj, "__call__"): # noqa
yield XshFunction.from_parent(
self, name=test_name, test_func=obj, test_module=mod
)
class XshFunction(pytest.Item):
def __init__(self, name, parent, test_func, test_module):
super().__init__(name, parent)
self._test_func = test_func
self._test_module = test_module
def runtest(self, *args, **kwargs):
self._test_func(*args, **kwargs)
def repr_failure(self, excinfo, **_):
"""called when self.runtest() raises an exception."""
formatted_tb = _limited_traceback(excinfo)
formatted_tb.insert(0, "xonsh execution failed\n")
formatted_tb.append(f"{excinfo.type.__name__}: {excinfo.value}")
return "".join(formatted_tb)
def reportinfo(self):
return self.fspath, 0, f"xonsh test: {self.name}"