123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- """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 pytest
- import textwrap
- import difflib
- import re
- import sys
- import contextlib
- import platform
- import gc
- _unicode_marker = re.compile(r'u(\'[^\']*\')')
- _long_marker = re.compile(r'([0-9])L')
- _hexadecimal = re.compile(r'0x[0-9a-fA-F]+')
- 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, *_):
- 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_namespace():
- """Add import suppression and test requirements to `pytest` namespace"""
- try:
- import numpy as np
- except ImportError:
- np = None
- try:
- import scipy
- except ImportError:
- scipy = None
- try:
- from pybind11_tests.eigen import have_eigen
- except ImportError:
- have_eigen = False
- pypy = platform.python_implementation() == "PyPy"
- skipif = pytest.mark.skipif
- return {
- 'suppress': suppress,
- 'requires_numpy': skipif(not np, reason="numpy is not installed"),
- 'requires_scipy': skipif(not np, reason="scipy is not installed"),
- 'requires_eigen_and_numpy': skipif(not have_eigen or not np,
- reason="eigen and/or numpy are not installed"),
- 'requires_eigen_and_scipy': skipif(not have_eigen or not scipy,
- reason="eigen and/or scipy are not installed"),
- 'unsupported_on_pypy': skipif(pypy, reason="unsupported on PyPy"),
- 'unsupported_on_py2': skipif(sys.version_info.major < 3,
- reason="unsupported on Python 2.x"),
- 'gc_collect': gc_collect
- }
- def _test_import_pybind11():
- """Early diagnostic for test module initialization errors
- When there is an error during initialization, the first import will report the
- real error while all subsequent imports will report nonsense. This import test
- is done early (in the pytest configuration file, before any tests) in order to
- avoid the noise of having all tests fail with identical error messages.
- Any possible exception is caught here and reported manually *without* the stack
- trace. This further reduces noise since the trace would only show pytest internals
- which are not useful for debugging pybind11 module issues.
- """
- # noinspection PyBroadException
- try:
- import pybind11_tests # noqa: F401 imported but unused
- except Exception as e:
- print("Failed to import pybind11_tests from pytest:")
- print(" {}: {}".format(type(e).__name__, e))
- sys.exit(1)
- _test_import_pybind11()
|