conftest.py revision 11986
111986Sandreas.sandberg@arm.com"""pytest configuration
211986Sandreas.sandberg@arm.com
311986Sandreas.sandberg@arm.comExtends output capture as needed by pybind11: ignore constructors, optional unordered lines.
411986Sandreas.sandberg@arm.comAdds docstring and exceptions message sanitizers: ignore Python 2 vs 3 differences.
511986Sandreas.sandberg@arm.com"""
611986Sandreas.sandberg@arm.com
711986Sandreas.sandberg@arm.comimport pytest
811986Sandreas.sandberg@arm.comimport textwrap
911986Sandreas.sandberg@arm.comimport difflib
1011986Sandreas.sandberg@arm.comimport re
1111986Sandreas.sandberg@arm.comimport sys
1211986Sandreas.sandberg@arm.comimport contextlib
1311986Sandreas.sandberg@arm.com
1411986Sandreas.sandberg@arm.com_unicode_marker = re.compile(r'u(\'[^\']*\')')
1511986Sandreas.sandberg@arm.com_long_marker = re.compile(r'([0-9])L')
1611986Sandreas.sandberg@arm.com_hexadecimal = re.compile(r'0x[0-9a-fA-F]+')
1711986Sandreas.sandberg@arm.com
1811986Sandreas.sandberg@arm.com
1911986Sandreas.sandberg@arm.comdef _strip_and_dedent(s):
2011986Sandreas.sandberg@arm.com    """For triple-quote strings"""
2111986Sandreas.sandberg@arm.com    return textwrap.dedent(s.lstrip('\n').rstrip())
2211986Sandreas.sandberg@arm.com
2311986Sandreas.sandberg@arm.com
2411986Sandreas.sandberg@arm.comdef _split_and_sort(s):
2511986Sandreas.sandberg@arm.com    """For output which does not require specific line order"""
2611986Sandreas.sandberg@arm.com    return sorted(_strip_and_dedent(s).splitlines())
2711986Sandreas.sandberg@arm.com
2811986Sandreas.sandberg@arm.com
2911986Sandreas.sandberg@arm.comdef _make_explanation(a, b):
3011986Sandreas.sandberg@arm.com    """Explanation for a failed assert -- the a and b arguments are List[str]"""
3111986Sandreas.sandberg@arm.com    return ["--- actual / +++ expected"] + [line.strip('\n') for line in difflib.ndiff(a, b)]
3211986Sandreas.sandberg@arm.com
3311986Sandreas.sandberg@arm.com
3411986Sandreas.sandberg@arm.comclass Output(object):
3511986Sandreas.sandberg@arm.com    """Basic output post-processing and comparison"""
3611986Sandreas.sandberg@arm.com    def __init__(self, string):
3711986Sandreas.sandberg@arm.com        self.string = string
3811986Sandreas.sandberg@arm.com        self.explanation = []
3911986Sandreas.sandberg@arm.com
4011986Sandreas.sandberg@arm.com    def __str__(self):
4111986Sandreas.sandberg@arm.com        return self.string
4211986Sandreas.sandberg@arm.com
4311986Sandreas.sandberg@arm.com    def __eq__(self, other):
4411986Sandreas.sandberg@arm.com        # Ignore constructor/destructor output which is prefixed with "###"
4511986Sandreas.sandberg@arm.com        a = [line for line in self.string.strip().splitlines() if not line.startswith("###")]
4611986Sandreas.sandberg@arm.com        b = _strip_and_dedent(other).splitlines()
4711986Sandreas.sandberg@arm.com        if a == b:
4811986Sandreas.sandberg@arm.com            return True
4911986Sandreas.sandberg@arm.com        else:
5011986Sandreas.sandberg@arm.com            self.explanation = _make_explanation(a, b)
5111986Sandreas.sandberg@arm.com            return False
5211986Sandreas.sandberg@arm.com
5311986Sandreas.sandberg@arm.com
5411986Sandreas.sandberg@arm.comclass Unordered(Output):
5511986Sandreas.sandberg@arm.com    """Custom comparison for output without strict line ordering"""
5611986Sandreas.sandberg@arm.com    def __eq__(self, other):
5711986Sandreas.sandberg@arm.com        a = _split_and_sort(self.string)
5811986Sandreas.sandberg@arm.com        b = _split_and_sort(other)
5911986Sandreas.sandberg@arm.com        if a == b:
6011986Sandreas.sandberg@arm.com            return True
6111986Sandreas.sandberg@arm.com        else:
6211986Sandreas.sandberg@arm.com            self.explanation = _make_explanation(a, b)
6311986Sandreas.sandberg@arm.com            return False
6411986Sandreas.sandberg@arm.com
6511986Sandreas.sandberg@arm.com
6611986Sandreas.sandberg@arm.comclass Capture(object):
6711986Sandreas.sandberg@arm.com    def __init__(self, capfd):
6811986Sandreas.sandberg@arm.com        self.capfd = capfd
6911986Sandreas.sandberg@arm.com        self.out = ""
7011986Sandreas.sandberg@arm.com        self.err = ""
7111986Sandreas.sandberg@arm.com
7211986Sandreas.sandberg@arm.com    def __enter__(self):
7311986Sandreas.sandberg@arm.com        self.capfd.readouterr()
7411986Sandreas.sandberg@arm.com        return self
7511986Sandreas.sandberg@arm.com
7611986Sandreas.sandberg@arm.com    def __exit__(self, *_):
7711986Sandreas.sandberg@arm.com        self.out, self.err = self.capfd.readouterr()
7811986Sandreas.sandberg@arm.com
7911986Sandreas.sandberg@arm.com    def __eq__(self, other):
8011986Sandreas.sandberg@arm.com        a = Output(self.out)
8111986Sandreas.sandberg@arm.com        b = other
8211986Sandreas.sandberg@arm.com        if a == b:
8311986Sandreas.sandberg@arm.com            return True
8411986Sandreas.sandberg@arm.com        else:
8511986Sandreas.sandberg@arm.com            self.explanation = a.explanation
8611986Sandreas.sandberg@arm.com            return False
8711986Sandreas.sandberg@arm.com
8811986Sandreas.sandberg@arm.com    def __str__(self):
8911986Sandreas.sandberg@arm.com        return self.out
9011986Sandreas.sandberg@arm.com
9111986Sandreas.sandberg@arm.com    def __contains__(self, item):
9211986Sandreas.sandberg@arm.com        return item in self.out
9311986Sandreas.sandberg@arm.com
9411986Sandreas.sandberg@arm.com    @property
9511986Sandreas.sandberg@arm.com    def unordered(self):
9611986Sandreas.sandberg@arm.com        return Unordered(self.out)
9711986Sandreas.sandberg@arm.com
9811986Sandreas.sandberg@arm.com    @property
9911986Sandreas.sandberg@arm.com    def stderr(self):
10011986Sandreas.sandberg@arm.com        return Output(self.err)
10111986Sandreas.sandberg@arm.com
10211986Sandreas.sandberg@arm.com
10311986Sandreas.sandberg@arm.com@pytest.fixture
10411986Sandreas.sandberg@arm.comdef capture(capfd):
10511986Sandreas.sandberg@arm.com    """Extended `capfd` with context manager and custom equality operators"""
10611986Sandreas.sandberg@arm.com    return Capture(capfd)
10711986Sandreas.sandberg@arm.com
10811986Sandreas.sandberg@arm.com
10911986Sandreas.sandberg@arm.comclass SanitizedString(object):
11011986Sandreas.sandberg@arm.com    def __init__(self, sanitizer):
11111986Sandreas.sandberg@arm.com        self.sanitizer = sanitizer
11211986Sandreas.sandberg@arm.com        self.string = ""
11311986Sandreas.sandberg@arm.com        self.explanation = []
11411986Sandreas.sandberg@arm.com
11511986Sandreas.sandberg@arm.com    def __call__(self, thing):
11611986Sandreas.sandberg@arm.com        self.string = self.sanitizer(thing)
11711986Sandreas.sandberg@arm.com        return self
11811986Sandreas.sandberg@arm.com
11911986Sandreas.sandberg@arm.com    def __eq__(self, other):
12011986Sandreas.sandberg@arm.com        a = self.string
12111986Sandreas.sandberg@arm.com        b = _strip_and_dedent(other)
12211986Sandreas.sandberg@arm.com        if a == b:
12311986Sandreas.sandberg@arm.com            return True
12411986Sandreas.sandberg@arm.com        else:
12511986Sandreas.sandberg@arm.com            self.explanation = _make_explanation(a.splitlines(), b.splitlines())
12611986Sandreas.sandberg@arm.com            return False
12711986Sandreas.sandberg@arm.com
12811986Sandreas.sandberg@arm.com
12911986Sandreas.sandberg@arm.comdef _sanitize_general(s):
13011986Sandreas.sandberg@arm.com    s = s.strip()
13111986Sandreas.sandberg@arm.com    s = s.replace("pybind11_tests.", "m.")
13211986Sandreas.sandberg@arm.com    s = s.replace("unicode", "str")
13311986Sandreas.sandberg@arm.com    s = _long_marker.sub(r"\1", s)
13411986Sandreas.sandberg@arm.com    s = _unicode_marker.sub(r"\1", s)
13511986Sandreas.sandberg@arm.com    return s
13611986Sandreas.sandberg@arm.com
13711986Sandreas.sandberg@arm.com
13811986Sandreas.sandberg@arm.comdef _sanitize_docstring(thing):
13911986Sandreas.sandberg@arm.com    s = thing.__doc__
14011986Sandreas.sandberg@arm.com    s = _sanitize_general(s)
14111986Sandreas.sandberg@arm.com    return s
14211986Sandreas.sandberg@arm.com
14311986Sandreas.sandberg@arm.com
14411986Sandreas.sandberg@arm.com@pytest.fixture
14511986Sandreas.sandberg@arm.comdef doc():
14611986Sandreas.sandberg@arm.com    """Sanitize docstrings and add custom failure explanation"""
14711986Sandreas.sandberg@arm.com    return SanitizedString(_sanitize_docstring)
14811986Sandreas.sandberg@arm.com
14911986Sandreas.sandberg@arm.com
15011986Sandreas.sandberg@arm.comdef _sanitize_message(thing):
15111986Sandreas.sandberg@arm.com    s = str(thing)
15211986Sandreas.sandberg@arm.com    s = _sanitize_general(s)
15311986Sandreas.sandberg@arm.com    s = _hexadecimal.sub("0", s)
15411986Sandreas.sandberg@arm.com    return s
15511986Sandreas.sandberg@arm.com
15611986Sandreas.sandberg@arm.com
15711986Sandreas.sandberg@arm.com@pytest.fixture
15811986Sandreas.sandberg@arm.comdef msg():
15911986Sandreas.sandberg@arm.com    """Sanitize messages and add custom failure explanation"""
16011986Sandreas.sandberg@arm.com    return SanitizedString(_sanitize_message)
16111986Sandreas.sandberg@arm.com
16211986Sandreas.sandberg@arm.com
16311986Sandreas.sandberg@arm.com# noinspection PyUnusedLocal
16411986Sandreas.sandberg@arm.comdef pytest_assertrepr_compare(op, left, right):
16511986Sandreas.sandberg@arm.com    """Hook to insert custom failure explanation"""
16611986Sandreas.sandberg@arm.com    if hasattr(left, 'explanation'):
16711986Sandreas.sandberg@arm.com        return left.explanation
16811986Sandreas.sandberg@arm.com
16911986Sandreas.sandberg@arm.com
17011986Sandreas.sandberg@arm.com@contextlib.contextmanager
17111986Sandreas.sandberg@arm.comdef suppress(exception):
17211986Sandreas.sandberg@arm.com    """Suppress the desired exception"""
17311986Sandreas.sandberg@arm.com    try:
17411986Sandreas.sandberg@arm.com        yield
17511986Sandreas.sandberg@arm.com    except exception:
17611986Sandreas.sandberg@arm.com        pass
17711986Sandreas.sandberg@arm.com
17811986Sandreas.sandberg@arm.com
17911986Sandreas.sandberg@arm.comdef pytest_namespace():
18011986Sandreas.sandberg@arm.com    """Add import suppression and test requirements to `pytest` namespace"""
18111986Sandreas.sandberg@arm.com    try:
18211986Sandreas.sandberg@arm.com        import numpy as np
18311986Sandreas.sandberg@arm.com    except ImportError:
18411986Sandreas.sandberg@arm.com        np = None
18511986Sandreas.sandberg@arm.com    try:
18611986Sandreas.sandberg@arm.com        import scipy
18711986Sandreas.sandberg@arm.com    except ImportError:
18811986Sandreas.sandberg@arm.com        scipy = None
18911986Sandreas.sandberg@arm.com    try:
19011986Sandreas.sandberg@arm.com        from pybind11_tests import have_eigen
19111986Sandreas.sandberg@arm.com    except ImportError:
19211986Sandreas.sandberg@arm.com        have_eigen = False
19311986Sandreas.sandberg@arm.com
19411986Sandreas.sandberg@arm.com    skipif = pytest.mark.skipif
19511986Sandreas.sandberg@arm.com    return {
19611986Sandreas.sandberg@arm.com        'suppress': suppress,
19711986Sandreas.sandberg@arm.com        'requires_numpy': skipif(not np, reason="numpy is not installed"),
19811986Sandreas.sandberg@arm.com        'requires_scipy': skipif(not np, reason="scipy is not installed"),
19911986Sandreas.sandberg@arm.com        'requires_eigen_and_numpy': skipif(not have_eigen or not np,
20011986Sandreas.sandberg@arm.com                                           reason="eigen and/or numpy are not installed"),
20111986Sandreas.sandberg@arm.com        'requires_eigen_and_scipy': skipif(not have_eigen or not scipy,
20211986Sandreas.sandberg@arm.com                                           reason="eigen and/or scipy are not installed"),
20311986Sandreas.sandberg@arm.com    }
20411986Sandreas.sandberg@arm.com
20511986Sandreas.sandberg@arm.com
20611986Sandreas.sandberg@arm.comdef _test_import_pybind11():
20711986Sandreas.sandberg@arm.com    """Early diagnostic for test module initialization errors
20811986Sandreas.sandberg@arm.com
20911986Sandreas.sandberg@arm.com    When there is an error during initialization, the first import will report the
21011986Sandreas.sandberg@arm.com    real error while all subsequent imports will report nonsense. This import test
21111986Sandreas.sandberg@arm.com    is done early (in the pytest configuration file, before any tests) in order to
21211986Sandreas.sandberg@arm.com    avoid the noise of having all tests fail with identical error messages.
21311986Sandreas.sandberg@arm.com
21411986Sandreas.sandberg@arm.com    Any possible exception is caught here and reported manually *without* the stack
21511986Sandreas.sandberg@arm.com    trace. This further reduces noise since the trace would only show pytest internals
21611986Sandreas.sandberg@arm.com    which are not useful for debugging pybind11 module issues.
21711986Sandreas.sandberg@arm.com    """
21811986Sandreas.sandberg@arm.com    # noinspection PyBroadException
21911986Sandreas.sandberg@arm.com    try:
22011986Sandreas.sandberg@arm.com        import pybind11_tests  # noqa: F401 imported but unused
22111986Sandreas.sandberg@arm.com    except Exception as e:
22211986Sandreas.sandberg@arm.com        print("Failed to import pybind11_tests from pytest:")
22311986Sandreas.sandberg@arm.com        print("  {}: {}".format(type(e).__name__, e))
22411986Sandreas.sandberg@arm.com        sys.exit(1)
22511986Sandreas.sandberg@arm.com
22611986Sandreas.sandberg@arm.com
22711986Sandreas.sandberg@arm.com_test_import_pybind11()
228