217 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			217 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			Python
		
	
	
	
import dataclasses
 | 
						|
import re
 | 
						|
import sys
 | 
						|
from typing import List
 | 
						|
 | 
						|
import pytest
 | 
						|
from _pytest.monkeypatch import MonkeyPatch
 | 
						|
from _pytest.pytester import Pytester
 | 
						|
 | 
						|
if sys.gettrace():
 | 
						|
 | 
						|
    @pytest.fixture(autouse=True)
 | 
						|
    def restore_tracing():
 | 
						|
        """Restore tracing function (when run with Coverage.py).
 | 
						|
 | 
						|
        https://bugs.python.org/issue37011
 | 
						|
        """
 | 
						|
        orig_trace = sys.gettrace()
 | 
						|
        yield
 | 
						|
        if sys.gettrace() != orig_trace:
 | 
						|
            sys.settrace(orig_trace)
 | 
						|
 | 
						|
 | 
						|
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
 | 
						|
def pytest_collection_modifyitems(items):
 | 
						|
    """Prefer faster tests.
 | 
						|
 | 
						|
    Use a hookwrapper to do this in the beginning, so e.g. --ff still works
 | 
						|
    correctly.
 | 
						|
    """
 | 
						|
    fast_items = []
 | 
						|
    slow_items = []
 | 
						|
    slowest_items = []
 | 
						|
    neutral_items = []
 | 
						|
 | 
						|
    spawn_names = {"spawn_pytest", "spawn"}
 | 
						|
 | 
						|
    for item in items:
 | 
						|
        try:
 | 
						|
            fixtures = item.fixturenames
 | 
						|
        except AttributeError:
 | 
						|
            # doctest at least
 | 
						|
            # (https://github.com/pytest-dev/pytest/issues/5070)
 | 
						|
            neutral_items.append(item)
 | 
						|
        else:
 | 
						|
            if "pytester" in fixtures:
 | 
						|
                co_names = item.function.__code__.co_names
 | 
						|
                if spawn_names.intersection(co_names):
 | 
						|
                    item.add_marker(pytest.mark.uses_pexpect)
 | 
						|
                    slowest_items.append(item)
 | 
						|
                elif "runpytest_subprocess" in co_names:
 | 
						|
                    slowest_items.append(item)
 | 
						|
                else:
 | 
						|
                    slow_items.append(item)
 | 
						|
                item.add_marker(pytest.mark.slow)
 | 
						|
            else:
 | 
						|
                marker = item.get_closest_marker("slow")
 | 
						|
                if marker:
 | 
						|
                    slowest_items.append(item)
 | 
						|
                else:
 | 
						|
                    fast_items.append(item)
 | 
						|
 | 
						|
    items[:] = fast_items + neutral_items + slow_items + slowest_items
 | 
						|
 | 
						|
    yield
 | 
						|
 | 
						|
 | 
						|
@pytest.fixture
 | 
						|
def tw_mock():
 | 
						|
    """Returns a mock terminal writer"""
 | 
						|
 | 
						|
    class TWMock:
 | 
						|
        WRITE = object()
 | 
						|
 | 
						|
        def __init__(self):
 | 
						|
            self.lines = []
 | 
						|
            self.is_writing = False
 | 
						|
 | 
						|
        def sep(self, sep, line=None):
 | 
						|
            self.lines.append((sep, line))
 | 
						|
 | 
						|
        def write(self, msg, **kw):
 | 
						|
            self.lines.append((TWMock.WRITE, msg))
 | 
						|
 | 
						|
        def _write_source(self, lines, indents=()):
 | 
						|
            if not indents:
 | 
						|
                indents = [""] * len(lines)
 | 
						|
            for indent, line in zip(indents, lines):
 | 
						|
                self.line(indent + line)
 | 
						|
 | 
						|
        def line(self, line, **kw):
 | 
						|
            self.lines.append(line)
 | 
						|
 | 
						|
        def markup(self, text, **kw):
 | 
						|
            return text
 | 
						|
 | 
						|
        def get_write_msg(self, idx):
 | 
						|
            flag, msg = self.lines[idx]
 | 
						|
            assert flag == TWMock.WRITE
 | 
						|
            return msg
 | 
						|
 | 
						|
        fullwidth = 80
 | 
						|
 | 
						|
    return TWMock()
 | 
						|
 | 
						|
 | 
						|
@pytest.fixture
 | 
						|
def dummy_yaml_custom_test(pytester: Pytester):
 | 
						|
    """Writes a conftest file that collects and executes a dummy yaml test.
 | 
						|
 | 
						|
    Taken from the docs, but stripped down to the bare minimum, useful for
 | 
						|
    tests which needs custom items collected.
 | 
						|
    """
 | 
						|
    pytester.makeconftest(
 | 
						|
        """
 | 
						|
        import pytest
 | 
						|
 | 
						|
        def pytest_collect_file(parent, file_path):
 | 
						|
            if file_path.suffix == ".yaml" and file_path.name.startswith("test"):
 | 
						|
                return YamlFile.from_parent(path=file_path, parent=parent)
 | 
						|
 | 
						|
        class YamlFile(pytest.File):
 | 
						|
            def collect(self):
 | 
						|
                yield YamlItem.from_parent(name=self.path.name, parent=self)
 | 
						|
 | 
						|
        class YamlItem(pytest.Item):
 | 
						|
            def runtest(self):
 | 
						|
                pass
 | 
						|
    """
 | 
						|
    )
 | 
						|
    pytester.makefile(".yaml", test1="")
 | 
						|
 | 
						|
 | 
						|
@pytest.fixture
 | 
						|
def pytester(pytester: Pytester, monkeypatch: MonkeyPatch) -> Pytester:
 | 
						|
    monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
 | 
						|
    return pytester
 | 
						|
 | 
						|
 | 
						|
@pytest.fixture(scope="session")
 | 
						|
def color_mapping():
 | 
						|
    """Returns a utility class which can replace keys in strings in the form "{NAME}"
 | 
						|
    by their equivalent ASCII codes in the terminal.
 | 
						|
 | 
						|
    Used by tests which check the actual colors output by pytest.
 | 
						|
    """
 | 
						|
 | 
						|
    class ColorMapping:
 | 
						|
        COLORS = {
 | 
						|
            "red": "\x1b[31m",
 | 
						|
            "green": "\x1b[32m",
 | 
						|
            "yellow": "\x1b[33m",
 | 
						|
            "bold": "\x1b[1m",
 | 
						|
            "reset": "\x1b[0m",
 | 
						|
            "kw": "\x1b[94m",
 | 
						|
            "hl-reset": "\x1b[39;49;00m",
 | 
						|
            "function": "\x1b[92m",
 | 
						|
            "number": "\x1b[94m",
 | 
						|
            "str": "\x1b[33m",
 | 
						|
            "print": "\x1b[96m",
 | 
						|
            "endline": "\x1b[90m\x1b[39;49;00m",
 | 
						|
        }
 | 
						|
        RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()}
 | 
						|
 | 
						|
        @classmethod
 | 
						|
        def format(cls, lines: List[str]) -> List[str]:
 | 
						|
            """Straightforward replacement of color names to their ASCII codes."""
 | 
						|
            return [line.format(**cls.COLORS) for line in lines]
 | 
						|
 | 
						|
        @classmethod
 | 
						|
        def format_for_fnmatch(cls, lines: List[str]) -> List[str]:
 | 
						|
            """Replace color names for use with LineMatcher.fnmatch_lines"""
 | 
						|
            return [line.format(**cls.COLORS).replace("[", "[[]") for line in lines]
 | 
						|
 | 
						|
        @classmethod
 | 
						|
        def format_for_rematch(cls, lines: List[str]) -> List[str]:
 | 
						|
            """Replace color names for use with LineMatcher.re_match_lines"""
 | 
						|
            return [line.format(**cls.RE_COLORS) for line in lines]
 | 
						|
 | 
						|
    return ColorMapping
 | 
						|
 | 
						|
 | 
						|
@pytest.fixture
 | 
						|
def mock_timing(monkeypatch: MonkeyPatch):
 | 
						|
    """Mocks _pytest.timing with a known object that can be used to control timing in tests
 | 
						|
    deterministically.
 | 
						|
 | 
						|
    pytest itself should always use functions from `_pytest.timing` instead of `time` directly.
 | 
						|
 | 
						|
    This then allows us more control over time during testing, if testing code also
 | 
						|
    uses `_pytest.timing` functions.
 | 
						|
 | 
						|
    Time is static, and only advances through `sleep` calls, thus tests might sleep over large
 | 
						|
    numbers and obtain accurate time() calls at the end, making tests reliable and instant.
 | 
						|
    """
 | 
						|
 | 
						|
    @dataclasses.dataclass
 | 
						|
    class MockTiming:
 | 
						|
        _current_time: float = 1590150050.0
 | 
						|
 | 
						|
        def sleep(self, seconds: float) -> None:
 | 
						|
            self._current_time += seconds
 | 
						|
 | 
						|
        def time(self) -> float:
 | 
						|
            return self._current_time
 | 
						|
 | 
						|
        def patch(self) -> None:
 | 
						|
            from _pytest import timing
 | 
						|
 | 
						|
            monkeypatch.setattr(timing, "sleep", self.sleep)
 | 
						|
            monkeypatch.setattr(timing, "time", self.time)
 | 
						|
            monkeypatch.setattr(timing, "perf_counter", self.time)
 | 
						|
 | 
						|
    result = MockTiming()
 | 
						|
    result.patch()
 | 
						|
    return result
 |