2023-01-20 17:13:36 +08:00
|
|
|
import dataclasses
|
2020-02-12 19:32:37 +08:00
|
|
|
import re
|
2019-06-05 17:21:44 +08:00
|
|
|
import sys
|
2023-06-13 03:30:06 +08:00
|
|
|
from typing import Generator
|
2020-02-12 19:32:37 +08:00
|
|
|
from typing import List
|
2019-06-05 17:21:44 +08:00
|
|
|
|
2019-04-19 07:23:08 +08:00
|
|
|
import pytest
|
2020-10-26 21:01:38 +08:00
|
|
|
from _pytest.monkeypatch import MonkeyPatch
|
|
|
|
from _pytest.pytester import Pytester
|
2019-04-19 07:23:08 +08:00
|
|
|
|
2019-06-05 17:21:44 +08:00
|
|
|
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)
|
|
|
|
|
2019-04-19 07:23:08 +08:00
|
|
|
|
2023-09-11 20:22:56 +08:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def set_column_width(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
|
"""
|
|
|
|
Force terminal width to 80: some tests check the formatting of --help, which is sensible
|
|
|
|
to terminal width.
|
|
|
|
"""
|
|
|
|
monkeypatch.setenv("COLUMNS", "80")
|
|
|
|
|
|
|
|
|
2023-11-26 22:09:18 +08:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def reset_colors(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
|
"""
|
|
|
|
Reset all color-related variables to prevent them from affecting internal pytest output
|
|
|
|
in tests that depend on it.
|
|
|
|
"""
|
|
|
|
monkeypatch.delenv("PY_COLORS", raising=False)
|
|
|
|
monkeypatch.delenv("NO_COLOR", raising=False)
|
|
|
|
monkeypatch.delenv("FORCE_COLOR", raising=False)
|
|
|
|
|
|
|
|
|
2023-06-13 03:30:06 +08:00
|
|
|
@pytest.hookimpl(wrapper=True, tryfirst=True)
|
|
|
|
def pytest_collection_modifyitems(items) -> Generator[None, None, None]:
|
2019-04-19 07:23:08 +08:00
|
|
|
"""Prefer faster tests.
|
|
|
|
|
2023-06-13 03:30:06 +08:00
|
|
|
Use a hook wrapper to do this in the beginning, so e.g. --ff still works
|
2019-04-19 07:23:08 +08:00
|
|
|
correctly.
|
|
|
|
"""
|
2019-04-08 01:08:59 +08:00
|
|
|
fast_items = []
|
|
|
|
slow_items = []
|
2019-05-27 09:20:58 +08:00
|
|
|
slowest_items = []
|
2019-04-08 01:08:59 +08:00
|
|
|
neutral_items = []
|
|
|
|
|
2019-05-27 09:20:58 +08:00
|
|
|
spawn_names = {"spawn_pytest", "spawn"}
|
2019-04-08 01:08:59 +08:00
|
|
|
|
|
|
|
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:
|
2020-10-26 21:01:38 +08:00
|
|
|
if "pytester" in fixtures:
|
2019-10-27 09:46:40 +08:00
|
|
|
co_names = item.function.__code__.co_names
|
|
|
|
if spawn_names.intersection(co_names):
|
2019-05-27 09:20:58 +08:00
|
|
|
item.add_marker(pytest.mark.uses_pexpect)
|
2019-05-30 03:30:45 +08:00
|
|
|
slowest_items.append(item)
|
2019-10-27 09:46:40 +08:00
|
|
|
elif "runpytest_subprocess" in co_names:
|
|
|
|
slowest_items.append(item)
|
2019-05-27 09:20:58 +08:00
|
|
|
else:
|
2019-05-30 03:30:45 +08:00
|
|
|
slow_items.append(item)
|
2019-05-30 12:55:38 +08:00
|
|
|
item.add_marker(pytest.mark.slow)
|
2019-04-08 01:08:59 +08:00
|
|
|
else:
|
|
|
|
marker = item.get_closest_marker("slow")
|
|
|
|
if marker:
|
2019-05-27 09:20:58 +08:00
|
|
|
slowest_items.append(item)
|
2019-04-08 01:08:59 +08:00
|
|
|
else:
|
|
|
|
fast_items.append(item)
|
|
|
|
|
2019-05-30 03:30:45 +08:00
|
|
|
items[:] = fast_items + neutral_items + slow_items + slowest_items
|
2019-04-19 07:23:08 +08:00
|
|
|
|
2023-06-13 03:30:06 +08:00
|
|
|
return (yield)
|
2019-08-26 22:32:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
@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))
|
|
|
|
|
2020-02-12 19:32:37 +08:00
|
|
|
def _write_source(self, lines, indents=()):
|
|
|
|
if not indents:
|
|
|
|
indents = [""] * len(lines)
|
|
|
|
for indent, line in zip(indents, lines):
|
|
|
|
self.line(indent + line)
|
|
|
|
|
2019-08-26 22:32:57 +08:00
|
|
|
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()
|
2019-09-28 22:52:09 +08:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2023-06-02 16:41:36 +08:00
|
|
|
def dummy_yaml_custom_test(pytester: Pytester) -> None:
|
2019-09-28 22:52:09 +08:00
|
|
|
"""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.
|
|
|
|
"""
|
2020-10-26 21:01:38 +08:00
|
|
|
pytester.makeconftest(
|
2019-09-28 22:52:09 +08:00
|
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
2021-12-03 20:14:09 +08:00
|
|
|
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)
|
2019-09-28 22:52:09 +08:00
|
|
|
|
|
|
|
class YamlFile(pytest.File):
|
|
|
|
def collect(self):
|
2021-03-15 04:20:53 +08:00
|
|
|
yield YamlItem.from_parent(name=self.path.name, parent=self)
|
2019-09-28 22:52:09 +08:00
|
|
|
|
|
|
|
class YamlItem(pytest.Item):
|
|
|
|
def runtest(self):
|
|
|
|
pass
|
|
|
|
"""
|
|
|
|
)
|
2020-10-26 21:01:38 +08:00
|
|
|
pytester.makefile(".yaml", test1="")
|
2020-02-04 09:59:20 +08:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2020-10-26 21:01:38 +08:00
|
|
|
def pytester(pytester: Pytester, monkeypatch: MonkeyPatch) -> Pytester:
|
|
|
|
monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
|
|
|
|
return pytester
|
2020-02-12 19:32:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
@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",
|
2023-10-24 19:42:21 +08:00
|
|
|
"light-gray": "\x1b[90m",
|
|
|
|
"light-red": "\x1b[91m",
|
|
|
|
"light-green": "\x1b[92m",
|
2020-02-12 19:32:37 +08:00
|
|
|
"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",
|
2023-01-04 16:29:53 +08:00
|
|
|
"endline": "\x1b[90m\x1b[39;49;00m",
|
2020-02-12 19:32:37 +08:00
|
|
|
}
|
|
|
|
RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()}
|
2023-10-24 19:42:21 +08:00
|
|
|
NO_COLORS = {k: "" for k in COLORS.keys()}
|
2020-02-12 19:32:37 +08:00
|
|
|
|
|
|
|
@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]
|
|
|
|
|
2023-10-24 19:42:21 +08:00
|
|
|
@classmethod
|
|
|
|
def strip_colors(cls, lines: List[str]) -> List[str]:
|
|
|
|
"""Entirely remove every color code"""
|
|
|
|
return [line.format(**cls.NO_COLORS) for line in lines]
|
|
|
|
|
2020-02-12 19:32:37 +08:00
|
|
|
return ColorMapping
|
2020-05-23 03:10:51 +08:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2020-10-26 21:01:38 +08:00
|
|
|
def mock_timing(monkeypatch: MonkeyPatch):
|
2020-05-23 03:10:51 +08:00
|
|
|
"""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.
|
|
|
|
"""
|
|
|
|
|
2023-01-20 17:13:36 +08:00
|
|
|
@dataclasses.dataclass
|
2020-05-23 03:10:51 +08:00
|
|
|
class MockTiming:
|
2023-01-20 17:13:36 +08:00
|
|
|
_current_time: float = 1590150050.0
|
2020-05-23 03:10:51 +08:00
|
|
|
|
2023-01-20 17:13:36 +08:00
|
|
|
def sleep(self, seconds: float) -> None:
|
2020-05-23 03:10:51 +08:00
|
|
|
self._current_time += seconds
|
|
|
|
|
2023-01-20 17:13:36 +08:00
|
|
|
def time(self) -> float:
|
2020-05-23 03:10:51 +08:00
|
|
|
return self._current_time
|
|
|
|
|
2023-01-20 17:13:36 +08:00
|
|
|
def patch(self) -> None:
|
2020-05-23 03:10:51 +08:00
|
|
|
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
|