From 48086a5778e8e9f1b14ca5c39c046a556c1c0e9b Mon Sep 17 00:00:00 2001 From: Noortheen Raja Date: Wed, 23 Mar 2022 21:18:09 +0530 Subject: [PATCH] feat: move fixtures to pytest-plugin --- setup.py | 4 +- tests/conftest.py | 347 ------------------------- xonsh/pytest/__init__.py | 0 xonsh/pytest/plugin.py | 418 +++++++++++++++++++++++++++++++ {tests => xonsh/pytest}/tools.py | 0 xonsh/pytest_plugin.py | 77 ------ 6 files changed, 420 insertions(+), 426 deletions(-) create mode 100644 xonsh/pytest/__init__.py create mode 100644 xonsh/pytest/plugin.py rename {tests => xonsh/pytest}/tools.py (100%) delete mode 100644 xonsh/pytest_plugin.py diff --git a/setup.py b/setup.py index 3c463c7e6..6acdd6e9a 100755 --- a/setup.py +++ b/setup.py @@ -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", diff --git a/tests/conftest.py b/tests/conftest.py index 99ae9ab77..e69de29bb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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] diff --git a/xonsh/pytest/__init__.py b/xonsh/pytest/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/xonsh/pytest/plugin.py b/xonsh/pytest/plugin.py new file mode 100644 index 000000000..927ea0f74 --- /dev/null +++ b/xonsh/pytest/plugin.py @@ -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] diff --git a/tests/tools.py b/xonsh/pytest/tools.py similarity index 100% rename from tests/tools.py rename to xonsh/pytest/tools.py diff --git a/xonsh/pytest_plugin.py b/xonsh/pytest_plugin.py deleted file mode 100644 index 545f3fc38..000000000 --- a/xonsh/pytest_plugin.py +++ /dev/null @@ -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}"