conftest.py revision 12391:ceeca8b41e4b
14561Sgblack@eecs.umich.edu"""pytest configuration
24561Sgblack@eecs.umich.edu
34561Sgblack@eecs.umich.eduExtends output capture as needed by pybind11: ignore constructors, optional unordered lines.
44561Sgblack@eecs.umich.eduAdds docstring and exceptions message sanitizers: ignore Python 2 vs 3 differences.
54561Sgblack@eecs.umich.edu"""
64561Sgblack@eecs.umich.edu
74561Sgblack@eecs.umich.eduimport pytest
84561Sgblack@eecs.umich.eduimport textwrap
94561Sgblack@eecs.umich.eduimport difflib
104561Sgblack@eecs.umich.eduimport re
114561Sgblack@eecs.umich.eduimport sys
124561Sgblack@eecs.umich.eduimport contextlib
134561Sgblack@eecs.umich.eduimport platform
144561Sgblack@eecs.umich.eduimport gc
154561Sgblack@eecs.umich.edu
164561Sgblack@eecs.umich.edu_unicode_marker = re.compile(r'u(\'[^\']*\')')
174561Sgblack@eecs.umich.edu_long_marker = re.compile(r'([0-9])L')
184561Sgblack@eecs.umich.edu_hexadecimal = re.compile(r'0x[0-9a-fA-F]+')
194561Sgblack@eecs.umich.edu
204561Sgblack@eecs.umich.edu
214561Sgblack@eecs.umich.edudef _strip_and_dedent(s):
224561Sgblack@eecs.umich.edu    """For triple-quote strings"""
234561Sgblack@eecs.umich.edu    return textwrap.dedent(s.lstrip('\n').rstrip())
244561Sgblack@eecs.umich.edu
254561Sgblack@eecs.umich.edu
264561Sgblack@eecs.umich.edudef _split_and_sort(s):
274561Sgblack@eecs.umich.edu    """For output which does not require specific line order"""
284561Sgblack@eecs.umich.edu    return sorted(_strip_and_dedent(s).splitlines())
294561Sgblack@eecs.umich.edu
304561Sgblack@eecs.umich.edu
314561Sgblack@eecs.umich.edudef _make_explanation(a, b):
324561Sgblack@eecs.umich.edu    """Explanation for a failed assert -- the a and b arguments are List[str]"""
334561Sgblack@eecs.umich.edu    return ["--- actual / +++ expected"] + [line.strip('\n') for line in difflib.ndiff(a, b)]
344561Sgblack@eecs.umich.edu
354561Sgblack@eecs.umich.edu
364561Sgblack@eecs.umich.educlass Output(object):
374561Sgblack@eecs.umich.edu    """Basic output post-processing and comparison"""
384561Sgblack@eecs.umich.edu    def __init__(self, string):
394561Sgblack@eecs.umich.edu        self.string = string
404561Sgblack@eecs.umich.edu        self.explanation = []
414561Sgblack@eecs.umich.edu
424561Sgblack@eecs.umich.edu    def __str__(self):
434561Sgblack@eecs.umich.edu        return self.string
444561Sgblack@eecs.umich.edu
454561Sgblack@eecs.umich.edu    def __eq__(self, other):
464561Sgblack@eecs.umich.edu        # Ignore constructor/destructor output which is prefixed with "###"
474561Sgblack@eecs.umich.edu        a = [line for line in self.string.strip().splitlines() if not line.startswith("###")]
484561Sgblack@eecs.umich.edu        b = _strip_and_dedent(other).splitlines()
494561Sgblack@eecs.umich.edu        if a == b:
504561Sgblack@eecs.umich.edu            return True
514561Sgblack@eecs.umich.edu        else:
524561Sgblack@eecs.umich.edu            self.explanation = _make_explanation(a, b)
534561Sgblack@eecs.umich.edu            return False
544561Sgblack@eecs.umich.edu
554561Sgblack@eecs.umich.edu
564561Sgblack@eecs.umich.educlass Unordered(Output):
574561Sgblack@eecs.umich.edu    """Custom comparison for output without strict line ordering"""
584561Sgblack@eecs.umich.edu    def __eq__(self, other):
594561Sgblack@eecs.umich.edu        a = _split_and_sort(self.string)
604561Sgblack@eecs.umich.edu        b = _split_and_sort(other)
614561Sgblack@eecs.umich.edu        if a == b:
624587Sgblack@eecs.umich.edu            return True
634587Sgblack@eecs.umich.edu        else:
644587Sgblack@eecs.umich.edu            self.explanation = _make_explanation(a, b)
654587Sgblack@eecs.umich.edu            return False
664587Sgblack@eecs.umich.edu
674587Sgblack@eecs.umich.edu
684587Sgblack@eecs.umich.educlass Capture(object):
694587Sgblack@eecs.umich.edu    def __init__(self, capfd):
704561Sgblack@eecs.umich.edu        self.capfd = capfd
714561Sgblack@eecs.umich.edu        self.out = ""
724561Sgblack@eecs.umich.edu        self.err = ""
734561Sgblack@eecs.umich.edu
744561Sgblack@eecs.umich.edu    def __enter__(self):
754561Sgblack@eecs.umich.edu        self.capfd.readouterr()
764561Sgblack@eecs.umich.edu        return self
774561Sgblack@eecs.umich.edu
784561Sgblack@eecs.umich.edu    def __exit__(self, *_):
794561Sgblack@eecs.umich.edu        self.out, self.err = self.capfd.readouterr()
804587Sgblack@eecs.umich.edu
814587Sgblack@eecs.umich.edu    def __eq__(self, other):
824587Sgblack@eecs.umich.edu        a = Output(self.out)
834587Sgblack@eecs.umich.edu        b = other
844587Sgblack@eecs.umich.edu        if a == b:
854587Sgblack@eecs.umich.edu            return True
864587Sgblack@eecs.umich.edu        else:
874587Sgblack@eecs.umich.edu            self.explanation = a.explanation
884587Sgblack@eecs.umich.edu            return False
894587Sgblack@eecs.umich.edu
904587Sgblack@eecs.umich.edu    def __str__(self):
914587Sgblack@eecs.umich.edu        return self.out
924587Sgblack@eecs.umich.edu
934587Sgblack@eecs.umich.edu    def __contains__(self, item):
944587Sgblack@eecs.umich.edu        return item in self.out
954587Sgblack@eecs.umich.edu
964587Sgblack@eecs.umich.edu    @property
974587Sgblack@eecs.umich.edu    def unordered(self):
984587Sgblack@eecs.umich.edu        return Unordered(self.out)
994587Sgblack@eecs.umich.edu
1004587Sgblack@eecs.umich.edu    @property
1014587Sgblack@eecs.umich.edu    def stderr(self):
1024587Sgblack@eecs.umich.edu        return Output(self.err)
1034587Sgblack@eecs.umich.edu
1044587Sgblack@eecs.umich.edu
1054587Sgblack@eecs.umich.edu@pytest.fixture
1064587Sgblack@eecs.umich.edudef capture(capsys):
1074587Sgblack@eecs.umich.edu    """Extended `capsys` with context manager and custom equality operators"""
1084587Sgblack@eecs.umich.edu    return Capture(capsys)
1094587Sgblack@eecs.umich.edu
1104587Sgblack@eecs.umich.edu
1114587Sgblack@eecs.umich.educlass SanitizedString(object):
1124587Sgblack@eecs.umich.edu    def __init__(self, sanitizer):
1134587Sgblack@eecs.umich.edu        self.sanitizer = sanitizer
1144587Sgblack@eecs.umich.edu        self.string = ""
1154587Sgblack@eecs.umich.edu        self.explanation = []
1164587Sgblack@eecs.umich.edu
1174587Sgblack@eecs.umich.edu    def __call__(self, thing):
1184587Sgblack@eecs.umich.edu        self.string = self.sanitizer(thing)
1194587Sgblack@eecs.umich.edu        return self
1204587Sgblack@eecs.umich.edu
1214587Sgblack@eecs.umich.edu    def __eq__(self, other):
1224587Sgblack@eecs.umich.edu        a = self.string
1234587Sgblack@eecs.umich.edu        b = _strip_and_dedent(other)
1244587Sgblack@eecs.umich.edu        if a == b:
1254587Sgblack@eecs.umich.edu            return True
1264587Sgblack@eecs.umich.edu        else:
1274587Sgblack@eecs.umich.edu            self.explanation = _make_explanation(a.splitlines(), b.splitlines())
1284587Sgblack@eecs.umich.edu            return False
1294587Sgblack@eecs.umich.edu
1304587Sgblack@eecs.umich.edu
1314587Sgblack@eecs.umich.edudef _sanitize_general(s):
1324587Sgblack@eecs.umich.edu    s = s.strip()
1334587Sgblack@eecs.umich.edu    s = s.replace("pybind11_tests.", "m.")
1344587Sgblack@eecs.umich.edu    s = s.replace("unicode", "str")
1354587Sgblack@eecs.umich.edu    s = _long_marker.sub(r"\1", s)
1364587Sgblack@eecs.umich.edu    s = _unicode_marker.sub(r"\1", s)
1374587Sgblack@eecs.umich.edu    return s
1384587Sgblack@eecs.umich.edu
1394587Sgblack@eecs.umich.edu
1404587Sgblack@eecs.umich.edudef _sanitize_docstring(thing):
1414587Sgblack@eecs.umich.edu    s = thing.__doc__
1424587Sgblack@eecs.umich.edu    s = _sanitize_general(s)
1434587Sgblack@eecs.umich.edu    return s
1444587Sgblack@eecs.umich.edu
1454587Sgblack@eecs.umich.edu
1464587Sgblack@eecs.umich.edu@pytest.fixture
1474587Sgblack@eecs.umich.edudef doc():
1484587Sgblack@eecs.umich.edu    """Sanitize docstrings and add custom failure explanation"""
1494587Sgblack@eecs.umich.edu    return SanitizedString(_sanitize_docstring)
1504587Sgblack@eecs.umich.edu
1514587Sgblack@eecs.umich.edu
1524587Sgblack@eecs.umich.edudef _sanitize_message(thing):
1534587Sgblack@eecs.umich.edu    s = str(thing)
1544587Sgblack@eecs.umich.edu    s = _sanitize_general(s)
1554587Sgblack@eecs.umich.edu    s = _hexadecimal.sub("0", s)
1564587Sgblack@eecs.umich.edu    return s
1574587Sgblack@eecs.umich.edu
1584587Sgblack@eecs.umich.edu
1594587Sgblack@eecs.umich.edu@pytest.fixture
1604587Sgblack@eecs.umich.edudef msg():
1614587Sgblack@eecs.umich.edu    """Sanitize messages and add custom failure explanation"""
1624587Sgblack@eecs.umich.edu    return SanitizedString(_sanitize_message)
1634587Sgblack@eecs.umich.edu
1644587Sgblack@eecs.umich.edu
1654587Sgblack@eecs.umich.edu# noinspection PyUnusedLocal
1664587Sgblack@eecs.umich.edudef pytest_assertrepr_compare(op, left, right):
1674587Sgblack@eecs.umich.edu    """Hook to insert custom failure explanation"""
1684587Sgblack@eecs.umich.edu    if hasattr(left, 'explanation'):
1694587Sgblack@eecs.umich.edu        return left.explanation
1704587Sgblack@eecs.umich.edu
1714587Sgblack@eecs.umich.edu
1724587Sgblack@eecs.umich.edu@contextlib.contextmanager
1734587Sgblack@eecs.umich.edudef suppress(exception):
1744587Sgblack@eecs.umich.edu    """Suppress the desired exception"""
1754587Sgblack@eecs.umich.edu    try:
1764587Sgblack@eecs.umich.edu        yield
1774587Sgblack@eecs.umich.edu    except exception:
1784587Sgblack@eecs.umich.edu        pass
1794587Sgblack@eecs.umich.edu
1804587Sgblack@eecs.umich.edu
1814587Sgblack@eecs.umich.edudef gc_collect():
1824587Sgblack@eecs.umich.edu    ''' Run the garbage collector twice (needed when running
1834587Sgblack@eecs.umich.edu    reference counting tests with PyPy) '''
1844587Sgblack@eecs.umich.edu    gc.collect()
1854587Sgblack@eecs.umich.edu    gc.collect()
1864587Sgblack@eecs.umich.edu
1874587Sgblack@eecs.umich.edu
1884587Sgblack@eecs.umich.edudef pytest_namespace():
1894587Sgblack@eecs.umich.edu    """Add import suppression and test requirements to `pytest` namespace"""
1904587Sgblack@eecs.umich.edu    try:
1914587Sgblack@eecs.umich.edu        import numpy as np
1924587Sgblack@eecs.umich.edu    except ImportError:
1934587Sgblack@eecs.umich.edu        np = None
1944587Sgblack@eecs.umich.edu    try:
1954587Sgblack@eecs.umich.edu        import scipy
1964587Sgblack@eecs.umich.edu    except ImportError:
1974587Sgblack@eecs.umich.edu        scipy = None
1984587Sgblack@eecs.umich.edu    try:
1994587Sgblack@eecs.umich.edu        from pybind11_tests.eigen import have_eigen
2004587Sgblack@eecs.umich.edu    except ImportError:
2014587Sgblack@eecs.umich.edu        have_eigen = False
2024587Sgblack@eecs.umich.edu    pypy = platform.python_implementation() == "PyPy"
2034587Sgblack@eecs.umich.edu
2044587Sgblack@eecs.umich.edu    skipif = pytest.mark.skipif
2054587Sgblack@eecs.umich.edu    return {
2064587Sgblack@eecs.umich.edu        'suppress': suppress,
2074587Sgblack@eecs.umich.edu        'requires_numpy': skipif(not np, reason="numpy is not installed"),
2084587Sgblack@eecs.umich.edu        'requires_scipy': skipif(not np, reason="scipy is not installed"),
2094587Sgblack@eecs.umich.edu        'requires_eigen_and_numpy': skipif(not have_eigen or not np,
2104587Sgblack@eecs.umich.edu                                           reason="eigen and/or numpy are not installed"),
2114587Sgblack@eecs.umich.edu        'requires_eigen_and_scipy': skipif(not have_eigen or not scipy,
2124587Sgblack@eecs.umich.edu                                           reason="eigen and/or scipy are not installed"),
2134587Sgblack@eecs.umich.edu        'unsupported_on_pypy': skipif(pypy, reason="unsupported on PyPy"),
2144587Sgblack@eecs.umich.edu        'unsupported_on_py2': skipif(sys.version_info.major < 3,
2154587Sgblack@eecs.umich.edu                                     reason="unsupported on Python 2.x"),
2164587Sgblack@eecs.umich.edu        'gc_collect': gc_collect
2174587Sgblack@eecs.umich.edu    }
2184587Sgblack@eecs.umich.edu
2194587Sgblack@eecs.umich.edu
2204587Sgblack@eecs.umich.edudef _test_import_pybind11():
2214587Sgblack@eecs.umich.edu    """Early diagnostic for test module initialization errors
2224587Sgblack@eecs.umich.edu
2234587Sgblack@eecs.umich.edu    When there is an error during initialization, the first import will report the
2244587Sgblack@eecs.umich.edu    real error while all subsequent imports will report nonsense. This import test
2254587Sgblack@eecs.umich.edu    is done early (in the pytest configuration file, before any tests) in order to
2264587Sgblack@eecs.umich.edu    avoid the noise of having all tests fail with identical error messages.
2274587Sgblack@eecs.umich.edu
2284587Sgblack@eecs.umich.edu    Any possible exception is caught here and reported manually *without* the stack
2294587Sgblack@eecs.umich.edu    trace. This further reduces noise since the trace would only show pytest internals
2304587Sgblack@eecs.umich.edu    which are not useful for debugging pybind11 module issues.
2314587Sgblack@eecs.umich.edu    """
2324587Sgblack@eecs.umich.edu    # noinspection PyBroadException
2334587Sgblack@eecs.umich.edu    try:
2344587Sgblack@eecs.umich.edu        import pybind11_tests  # noqa: F401 imported but unused
2354587Sgblack@eecs.umich.edu    except Exception as e:
2364587Sgblack@eecs.umich.edu        print("Failed to import pybind11_tests from pytest:")
2374587Sgblack@eecs.umich.edu        print("  {}: {}".format(type(e).__name__, e))
2384587Sgblack@eecs.umich.edu        sys.exit(1)
2394587Sgblack@eecs.umich.edu
2404587Sgblack@eecs.umich.edu
2414587Sgblack@eecs.umich.edu_test_import_pybind11()
2424587Sgblack@eecs.umich.edu