|
"""pytest configuration |
|
|
|
Extends output capture as needed by pybind11: ignore constructors, optional unordered lines. |
|
Adds docstring and exceptions message sanitizers. |
|
""" |
|
|
|
import contextlib |
|
import difflib |
|
import gc |
|
import multiprocessing |
|
import re |
|
import sys |
|
import textwrap |
|
import traceback |
|
|
|
import pytest |
|
|
|
|
|
try: |
|
import pybind11_tests |
|
except Exception: |
|
|
|
traceback.print_exc() |
|
raise |
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True) |
|
def use_multiprocessing_forkserver_on_linux(): |
|
if sys.platform != "linux": |
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
|
|
multiprocessing.set_start_method("forkserver") |
|
|
|
|
|
_long_marker = re.compile(r"([0-9])L") |
|
_hexadecimal = re.compile(r"0x[0-9a-fA-F]+") |
|
|
|
|
|
collect_ignore = [] |
|
|
|
|
|
def _strip_and_dedent(s): |
|
"""For triple-quote strings""" |
|
return textwrap.dedent(s.lstrip("\n").rstrip()) |
|
|
|
|
|
def _split_and_sort(s): |
|
"""For output which does not require specific line order""" |
|
return sorted(_strip_and_dedent(s).splitlines()) |
|
|
|
|
|
def _make_explanation(a, b): |
|
"""Explanation for a failed assert -- the a and b arguments are List[str]""" |
|
return ["--- actual / +++ expected"] + [ |
|
line.strip("\n") for line in difflib.ndiff(a, b) |
|
] |
|
|
|
|
|
class Output: |
|
"""Basic output post-processing and comparison""" |
|
|
|
def __init__(self, string): |
|
self.string = string |
|
self.explanation = [] |
|
|
|
def __str__(self): |
|
return self.string |
|
|
|
def __eq__(self, other): |
|
|
|
a = [ |
|
line |
|
for line in self.string.strip().splitlines() |
|
if not line.startswith("###") |
|
] |
|
b = _strip_and_dedent(other).splitlines() |
|
if a == b: |
|
return True |
|
self.explanation = _make_explanation(a, b) |
|
return False |
|
|
|
|
|
class Unordered(Output): |
|
"""Custom comparison for output without strict line ordering""" |
|
|
|
def __eq__(self, other): |
|
a = _split_and_sort(self.string) |
|
b = _split_and_sort(other) |
|
if a == b: |
|
return True |
|
self.explanation = _make_explanation(a, b) |
|
return False |
|
|
|
|
|
class Capture: |
|
def __init__(self, capfd): |
|
self.capfd = capfd |
|
self.out = "" |
|
self.err = "" |
|
|
|
def __enter__(self): |
|
self.capfd.readouterr() |
|
return self |
|
|
|
def __exit__(self, *args): |
|
self.out, self.err = self.capfd.readouterr() |
|
|
|
def __eq__(self, other): |
|
a = Output(self.out) |
|
b = other |
|
if a == b: |
|
return True |
|
self.explanation = a.explanation |
|
return False |
|
|
|
def __str__(self): |
|
return self.out |
|
|
|
def __contains__(self, item): |
|
return item in self.out |
|
|
|
@property |
|
def unordered(self): |
|
return Unordered(self.out) |
|
|
|
@property |
|
def stderr(self): |
|
return Output(self.err) |
|
|
|
|
|
@pytest.fixture() |
|
def capture(capsys): |
|
"""Extended `capsys` with context manager and custom equality operators""" |
|
return Capture(capsys) |
|
|
|
|
|
class SanitizedString: |
|
def __init__(self, sanitizer): |
|
self.sanitizer = sanitizer |
|
self.string = "" |
|
self.explanation = [] |
|
|
|
def __call__(self, thing): |
|
self.string = self.sanitizer(thing) |
|
return self |
|
|
|
def __eq__(self, other): |
|
a = self.string |
|
b = _strip_and_dedent(other) |
|
if a == b: |
|
return True |
|
self.explanation = _make_explanation(a.splitlines(), b.splitlines()) |
|
return False |
|
|
|
|
|
def _sanitize_general(s): |
|
s = s.strip() |
|
s = s.replace("pybind11_tests.", "m.") |
|
return _long_marker.sub(r"\1", s) |
|
|
|
|
|
def _sanitize_docstring(thing): |
|
s = thing.__doc__ |
|
return _sanitize_general(s) |
|
|
|
|
|
@pytest.fixture() |
|
def doc(): |
|
"""Sanitize docstrings and add custom failure explanation""" |
|
return SanitizedString(_sanitize_docstring) |
|
|
|
|
|
def _sanitize_message(thing): |
|
s = str(thing) |
|
s = _sanitize_general(s) |
|
return _hexadecimal.sub("0", s) |
|
|
|
|
|
@pytest.fixture() |
|
def msg(): |
|
"""Sanitize messages and add custom failure explanation""" |
|
return SanitizedString(_sanitize_message) |
|
|
|
|
|
def pytest_assertrepr_compare(op, left, right): |
|
"""Hook to insert custom failure explanation""" |
|
if hasattr(left, "explanation"): |
|
return left.explanation |
|
return None |
|
|
|
|
|
def gc_collect(): |
|
"""Run the garbage collector twice (needed when running |
|
reference counting tests with PyPy)""" |
|
gc.collect() |
|
gc.collect() |
|
|
|
|
|
def pytest_configure(): |
|
pytest.suppress = contextlib.suppress |
|
pytest.gc_collect = gc_collect |
|
|
|
|
|
def pytest_report_header(config): |
|
del config |
|
assert ( |
|
pybind11_tests.compiler_info is not None |
|
), "Please update pybind11_tests.cpp if this assert fails." |
|
return ( |
|
"C++ Info:" |
|
f" {pybind11_tests.compiler_info}" |
|
f" {pybind11_tests.cpp_std}" |
|
f" {pybind11_tests.PYBIND11_INTERNALS_ID}" |
|
f" PYBIND11_SIMPLE_GIL_MANAGEMENT={pybind11_tests.PYBIND11_SIMPLE_GIL_MANAGEMENT}" |
|
) |
|
|