240 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Python
		
	
	
		
		
			
		
	
	
			240 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Python
		
	
	
|  | """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, *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(): | ||
|  |     """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 | ||
|  |     pytest.suppress = suppress | ||
|  |     pytest.requires_numpy = skipif(not np, reason="numpy is not installed") | ||
|  |     pytest.requires_scipy = skipif(not np, reason="scipy is not installed") | ||
|  |     pytest.requires_eigen_and_numpy = skipif(not have_eigen or not np, | ||
|  |                                              reason="eigen and/or numpy are not installed") | ||
|  |     pytest.requires_eigen_and_scipy = skipif( | ||
|  |         not have_eigen or not scipy, reason="eigen and/or scipy are not installed") | ||
|  |     pytest.unsupported_on_pypy = skipif(pypy, reason="unsupported on PyPy") | ||
|  |     pytest.unsupported_on_py2 = skipif(sys.version_info.major < 3, | ||
|  |                                        reason="unsupported on Python 2.x") | ||
|  |     pytest.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() |