File indexing completed on 2025-01-18 10:17:51
0001 """pytest configuration
0002
0003 Extends output capture as needed by pybind11: ignore constructors, optional unordered lines.
0004 Adds docstring and exceptions message sanitizers.
0005 """
0006
0007 import contextlib
0008 import difflib
0009 import gc
0010 import multiprocessing
0011 import os
0012 import re
0013 import textwrap
0014
0015 import pytest
0016
0017
0018 import pybind11_tests
0019
0020
0021 @pytest.fixture(scope="session", autouse=True)
0022 def always_forkserver_on_unix():
0023 if os.name == "nt":
0024 return
0025
0026
0027
0028
0029
0030
0031
0032
0033 multiprocessing.set_start_method("forkserver")
0034
0035
0036 _long_marker = re.compile(r"([0-9])L")
0037 _hexadecimal = re.compile(r"0x[0-9a-fA-F]+")
0038
0039
0040 collect_ignore = []
0041
0042
0043 def _strip_and_dedent(s):
0044 """For triple-quote strings"""
0045 return textwrap.dedent(s.lstrip("\n").rstrip())
0046
0047
0048 def _split_and_sort(s):
0049 """For output which does not require specific line order"""
0050 return sorted(_strip_and_dedent(s).splitlines())
0051
0052
0053 def _make_explanation(a, b):
0054 """Explanation for a failed assert -- the a and b arguments are List[str]"""
0055 return ["--- actual / +++ expected"] + [
0056 line.strip("\n") for line in difflib.ndiff(a, b)
0057 ]
0058
0059
0060 class Output:
0061 """Basic output post-processing and comparison"""
0062
0063 def __init__(self, string):
0064 self.string = string
0065 self.explanation = []
0066
0067 def __str__(self):
0068 return self.string
0069
0070 def __eq__(self, other):
0071
0072 a = [
0073 line
0074 for line in self.string.strip().splitlines()
0075 if not line.startswith("###")
0076 ]
0077 b = _strip_and_dedent(other).splitlines()
0078 if a == b:
0079 return True
0080 else:
0081 self.explanation = _make_explanation(a, b)
0082 return False
0083
0084
0085 class Unordered(Output):
0086 """Custom comparison for output without strict line ordering"""
0087
0088 def __eq__(self, other):
0089 a = _split_and_sort(self.string)
0090 b = _split_and_sort(other)
0091 if a == b:
0092 return True
0093 else:
0094 self.explanation = _make_explanation(a, b)
0095 return False
0096
0097
0098 class Capture:
0099 def __init__(self, capfd):
0100 self.capfd = capfd
0101 self.out = ""
0102 self.err = ""
0103
0104 def __enter__(self):
0105 self.capfd.readouterr()
0106 return self
0107
0108 def __exit__(self, *args):
0109 self.out, self.err = self.capfd.readouterr()
0110
0111 def __eq__(self, other):
0112 a = Output(self.out)
0113 b = other
0114 if a == b:
0115 return True
0116 else:
0117 self.explanation = a.explanation
0118 return False
0119
0120 def __str__(self):
0121 return self.out
0122
0123 def __contains__(self, item):
0124 return item in self.out
0125
0126 @property
0127 def unordered(self):
0128 return Unordered(self.out)
0129
0130 @property
0131 def stderr(self):
0132 return Output(self.err)
0133
0134
0135 @pytest.fixture
0136 def capture(capsys):
0137 """Extended `capsys` with context manager and custom equality operators"""
0138 return Capture(capsys)
0139
0140
0141 class SanitizedString:
0142 def __init__(self, sanitizer):
0143 self.sanitizer = sanitizer
0144 self.string = ""
0145 self.explanation = []
0146
0147 def __call__(self, thing):
0148 self.string = self.sanitizer(thing)
0149 return self
0150
0151 def __eq__(self, other):
0152 a = self.string
0153 b = _strip_and_dedent(other)
0154 if a == b:
0155 return True
0156 else:
0157 self.explanation = _make_explanation(a.splitlines(), b.splitlines())
0158 return False
0159
0160
0161 def _sanitize_general(s):
0162 s = s.strip()
0163 s = s.replace("pybind11_tests.", "m.")
0164 s = _long_marker.sub(r"\1", s)
0165 return s
0166
0167
0168 def _sanitize_docstring(thing):
0169 s = thing.__doc__
0170 s = _sanitize_general(s)
0171 return s
0172
0173
0174 @pytest.fixture
0175 def doc():
0176 """Sanitize docstrings and add custom failure explanation"""
0177 return SanitizedString(_sanitize_docstring)
0178
0179
0180 def _sanitize_message(thing):
0181 s = str(thing)
0182 s = _sanitize_general(s)
0183 s = _hexadecimal.sub("0", s)
0184 return s
0185
0186
0187 @pytest.fixture
0188 def msg():
0189 """Sanitize messages and add custom failure explanation"""
0190 return SanitizedString(_sanitize_message)
0191
0192
0193
0194 def pytest_assertrepr_compare(op, left, right):
0195 """Hook to insert custom failure explanation"""
0196 if hasattr(left, "explanation"):
0197 return left.explanation
0198
0199
0200 @contextlib.contextmanager
0201 def suppress(exception):
0202 """Suppress the desired exception"""
0203 try:
0204 yield
0205 except exception:
0206 pass
0207
0208
0209 def gc_collect():
0210 """Run the garbage collector twice (needed when running
0211 reference counting tests with PyPy)"""
0212 gc.collect()
0213 gc.collect()
0214
0215
0216 def pytest_configure():
0217 pytest.suppress = suppress
0218 pytest.gc_collect = gc_collect
0219
0220
0221 def pytest_report_header(config):
0222 del config
0223 assert (
0224 pybind11_tests.compiler_info is not None
0225 ), "Please update pybind11_tests.cpp if this assert fails."
0226 return (
0227 "C++ Info:"
0228 f" {pybind11_tests.compiler_info}"
0229 f" {pybind11_tests.cpp_std}"
0230 f" {pybind11_tests.PYBIND11_INTERNALS_ID}"
0231 f" PYBIND11_SIMPLE_GIL_MANAGEMENT={pybind11_tests.PYBIND11_SIMPLE_GIL_MANAGEMENT}"
0232 )