conftest.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. # -*- coding: utf-8 -*-
  2. """pytest configuration
  3. Extends output capture as needed by pybind11: ignore constructors, optional unordered lines.
  4. Adds docstring and exceptions message sanitizers: ignore Python 2 vs 3 differences.
  5. """
  6. import contextlib
  7. import difflib
  8. import gc
  9. import re
  10. import textwrap
  11. import pytest
  12. import env
  13. # Early diagnostic for failed imports
  14. import pybind11_tests # noqa: F401
  15. _unicode_marker = re.compile(r"u(\'[^\']*\')")
  16. _long_marker = re.compile(r"([0-9])L")
  17. _hexadecimal = re.compile(r"0x[0-9a-fA-F]+")
  18. # Avoid collecting Python3 only files
  19. collect_ignore = []
  20. if env.PY2:
  21. collect_ignore.append("test_async.py")
  22. def _strip_and_dedent(s):
  23. """For triple-quote strings"""
  24. return textwrap.dedent(s.lstrip("\n").rstrip())
  25. def _split_and_sort(s):
  26. """For output which does not require specific line order"""
  27. return sorted(_strip_and_dedent(s).splitlines())
  28. def _make_explanation(a, b):
  29. """Explanation for a failed assert -- the a and b arguments are List[str]"""
  30. return ["--- actual / +++ expected"] + [
  31. line.strip("\n") for line in difflib.ndiff(a, b)
  32. ]
  33. class Output(object):
  34. """Basic output post-processing and comparison"""
  35. def __init__(self, string):
  36. self.string = string
  37. self.explanation = []
  38. def __str__(self):
  39. return self.string
  40. def __eq__(self, other):
  41. # Ignore constructor/destructor output which is prefixed with "###"
  42. a = [
  43. line
  44. for line in self.string.strip().splitlines()
  45. if not line.startswith("###")
  46. ]
  47. b = _strip_and_dedent(other).splitlines()
  48. if a == b:
  49. return True
  50. else:
  51. self.explanation = _make_explanation(a, b)
  52. return False
  53. class Unordered(Output):
  54. """Custom comparison for output without strict line ordering"""
  55. def __eq__(self, other):
  56. a = _split_and_sort(self.string)
  57. b = _split_and_sort(other)
  58. if a == b:
  59. return True
  60. else:
  61. self.explanation = _make_explanation(a, b)
  62. return False
  63. class Capture(object):
  64. def __init__(self, capfd):
  65. self.capfd = capfd
  66. self.out = ""
  67. self.err = ""
  68. def __enter__(self):
  69. self.capfd.readouterr()
  70. return self
  71. def __exit__(self, *args):
  72. self.out, self.err = self.capfd.readouterr()
  73. def __eq__(self, other):
  74. a = Output(self.out)
  75. b = other
  76. if a == b:
  77. return True
  78. else:
  79. self.explanation = a.explanation
  80. return False
  81. def __str__(self):
  82. return self.out
  83. def __contains__(self, item):
  84. return item in self.out
  85. @property
  86. def unordered(self):
  87. return Unordered(self.out)
  88. @property
  89. def stderr(self):
  90. return Output(self.err)
  91. @pytest.fixture
  92. def capture(capsys):
  93. """Extended `capsys` with context manager and custom equality operators"""
  94. return Capture(capsys)
  95. class SanitizedString(object):
  96. def __init__(self, sanitizer):
  97. self.sanitizer = sanitizer
  98. self.string = ""
  99. self.explanation = []
  100. def __call__(self, thing):
  101. self.string = self.sanitizer(thing)
  102. return self
  103. def __eq__(self, other):
  104. a = self.string
  105. b = _strip_and_dedent(other)
  106. if a == b:
  107. return True
  108. else:
  109. self.explanation = _make_explanation(a.splitlines(), b.splitlines())
  110. return False
  111. def _sanitize_general(s):
  112. s = s.strip()
  113. s = s.replace("pybind11_tests.", "m.")
  114. s = s.replace("unicode", "str")
  115. s = _long_marker.sub(r"\1", s)
  116. s = _unicode_marker.sub(r"\1", s)
  117. return s
  118. def _sanitize_docstring(thing):
  119. s = thing.__doc__
  120. s = _sanitize_general(s)
  121. return s
  122. @pytest.fixture
  123. def doc():
  124. """Sanitize docstrings and add custom failure explanation"""
  125. return SanitizedString(_sanitize_docstring)
  126. def _sanitize_message(thing):
  127. s = str(thing)
  128. s = _sanitize_general(s)
  129. s = _hexadecimal.sub("0", s)
  130. return s
  131. @pytest.fixture
  132. def msg():
  133. """Sanitize messages and add custom failure explanation"""
  134. return SanitizedString(_sanitize_message)
  135. # noinspection PyUnusedLocal
  136. def pytest_assertrepr_compare(op, left, right):
  137. """Hook to insert custom failure explanation"""
  138. if hasattr(left, "explanation"):
  139. return left.explanation
  140. @contextlib.contextmanager
  141. def suppress(exception):
  142. """Suppress the desired exception"""
  143. try:
  144. yield
  145. except exception:
  146. pass
  147. def gc_collect():
  148. """Run the garbage collector twice (needed when running
  149. reference counting tests with PyPy)"""
  150. gc.collect()
  151. gc.collect()
  152. def pytest_configure():
  153. pytest.suppress = suppress
  154. pytest.gc_collect = gc_collect