1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
|
# mypy: allow-untyped-defs
from __future__ import annotations
from collections.abc import Generator
import importlib.metadata
import re
import sys
from packaging.version import Version
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import Pytester
import pytest
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.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")
@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)
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_collection_modifyitems(items) -> Generator[None]:
"""Prefer faster tests.
Use a hook wrapper 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
return (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, strict=True):
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):
assert self.lines[idx][0] == TWMock.WRITE
msg = self.lines[idx][1]
return msg
fullwidth = 80
return TWMock()
@pytest.fixture
def dummy_yaml_custom_test(pytester: Pytester) -> None:
"""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.
"""
# https://github.com/pygments/pygments/commit/d24e272894a56a98b1b718d9ac5fabc20124882a
pygments_version = Version(importlib.metadata.version("pygments"))
pygments_has_kwspace_hl = pygments_version >= Version("2.19")
class ColorMapping:
COLORS = {
"red": "\x1b[31m",
"green": "\x1b[32m",
"yellow": "\x1b[33m",
"light-gray": "\x1b[90m",
"light-red": "\x1b[91m",
"light-green": "\x1b[92m",
"bold": "\x1b[1m",
"reset": "\x1b[0m",
"kw": "\x1b[94m",
"kwspace": "\x1b[90m \x1b[39;49;00m" if pygments_has_kwspace_hl else " ",
"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()}
NO_COLORS = {k: "" for k in COLORS.keys()}
@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]
@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]
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.
"""
from _pytest.timing import MockTiming
result = MockTiming()
result.patch(monkeypatch)
return result
@pytest.fixture(autouse=True)
def remove_ci_env_var(monkeypatch: MonkeyPatch, request: pytest.FixtureRequest) -> None:
"""Make the test insensitive if it is running in CI or not.
Use `@pytest.mark.keep_ci_var` in a test to avoid applying this fixture, letting the test
see the real `CI` variable (if present).
"""
has_keep_ci_mark = request.node.get_closest_marker("keep_ci_var") is not None
if not has_keep_ci_mark:
monkeypatch.delenv("CI", raising=False)
|