123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- # -*- coding: utf-8 -*-
- """pytest configuration
- Extends output capture as needed by pybind11: ignore constructors, optional unordered lines.
- Adds docstring and exceptions message sanitizers: ignore Python 2 vs 3 differences.
- """
- import contextlib
- import difflib
- import gc
- import re
- import textwrap
- import pytest
- import env
- # Early diagnostic for failed imports
- import pybind11_tests # noqa: F401
- _unicode_marker = re.compile(r"u(\'[^\']*\')")
- _long_marker = re.compile(r"([0-9])L")
- _hexadecimal = re.compile(r"0x[0-9a-fA-F]+")
- # Avoid collecting Python3 only files
- collect_ignore = []
- if env.PY2:
- collect_ignore.append("test_async.py")
- 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(object):
- """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):
- # Ignore constructor/destructor output which is prefixed with "###"
- a = [
- line
- for line in self.string.strip().splitlines()
- if not line.startswith("###")
- ]
- b = _strip_and_dedent(other).splitlines()
- if a == b:
- return True
- else:
- 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
- else:
- self.explanation = _make_explanation(a, b)
- return False
- class Capture(object):
- 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
- else:
- 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(object):
- 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
- else:
- self.explanation = _make_explanation(a.splitlines(), b.splitlines())
- return False
- def _sanitize_general(s):
- s = s.strip()
- s = s.replace("pybind11_tests.", "m.")
- s = s.replace("unicode", "str")
- s = _long_marker.sub(r"\1", s)
- s = _unicode_marker.sub(r"\1", s)
- return s
- def _sanitize_docstring(thing):
- s = thing.__doc__
- s = _sanitize_general(s)
- return 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)
- s = _hexadecimal.sub("0", s)
- return s
- @pytest.fixture
- def msg():
- """Sanitize messages and add custom failure explanation"""
- return SanitizedString(_sanitize_message)
- # noinspection PyUnusedLocal
- def pytest_assertrepr_compare(op, left, right):
- """Hook to insert custom failure explanation"""
- if hasattr(left, "explanation"):
- return left.explanation
- @contextlib.contextmanager
- def suppress(exception):
- """Suppress the desired exception"""
- try:
- yield
- except exception:
- pass
- 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 = suppress
- pytest.gc_collect = gc_collect
|