You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							239 lines
						
					
					
						
							6.4 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							239 lines
						
					
					
						
							6.4 KiB
						
					
					
				| """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 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"), | |
|         '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()
 |