pytest.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. """py.test hacks to support XFAIL/XPASS"""
  2. import sys
  3. import re
  4. import functools
  5. import os
  6. import contextlib
  7. import warnings
  8. import inspect
  9. import pathlib
  10. from typing import Any, Callable
  11. from sympy.utilities.exceptions import SymPyDeprecationWarning
  12. # Imported here for backwards compatibility. Note: do not import this from
  13. # here in library code (importing sympy.pytest in library code will break the
  14. # pytest integration).
  15. from sympy.utilities.exceptions import ignore_warnings # noqa:F401
  16. ON_TRAVIS = os.getenv('TRAVIS_BUILD_NUMBER', None)
  17. try:
  18. import pytest
  19. USE_PYTEST = getattr(sys, '_running_pytest', False)
  20. except ImportError:
  21. USE_PYTEST = False
  22. raises: Callable[[Any, Any], Any]
  23. XFAIL: Callable[[Any], Any]
  24. skip: Callable[[Any], Any]
  25. SKIP: Callable[[Any], Any]
  26. slow: Callable[[Any], Any]
  27. nocache_fail: Callable[[Any], Any]
  28. if USE_PYTEST:
  29. raises = pytest.raises
  30. skip = pytest.skip
  31. XFAIL = pytest.mark.xfail
  32. SKIP = pytest.mark.skip
  33. slow = pytest.mark.slow
  34. nocache_fail = pytest.mark.nocache_fail
  35. from _pytest.outcomes import Failed
  36. else:
  37. # Not using pytest so define the things that would have been imported from
  38. # there.
  39. # _pytest._code.code.ExceptionInfo
  40. class ExceptionInfo:
  41. def __init__(self, value):
  42. self.value = value
  43. def __repr__(self):
  44. return "<ExceptionInfo {!r}>".format(self.value)
  45. def raises(expectedException, code=None):
  46. """
  47. Tests that ``code`` raises the exception ``expectedException``.
  48. ``code`` may be a callable, such as a lambda expression or function
  49. name.
  50. If ``code`` is not given or None, ``raises`` will return a context
  51. manager for use in ``with`` statements; the code to execute then
  52. comes from the scope of the ``with``.
  53. ``raises()`` does nothing if the callable raises the expected exception,
  54. otherwise it raises an AssertionError.
  55. Examples
  56. ========
  57. >>> from sympy.testing.pytest import raises
  58. >>> raises(ZeroDivisionError, lambda: 1/0)
  59. <ExceptionInfo ZeroDivisionError(...)>
  60. >>> raises(ZeroDivisionError, lambda: 1/2)
  61. Traceback (most recent call last):
  62. ...
  63. Failed: DID NOT RAISE
  64. >>> with raises(ZeroDivisionError):
  65. ... n = 1/0
  66. >>> with raises(ZeroDivisionError):
  67. ... n = 1/2
  68. Traceback (most recent call last):
  69. ...
  70. Failed: DID NOT RAISE
  71. Note that you cannot test multiple statements via
  72. ``with raises``:
  73. >>> with raises(ZeroDivisionError):
  74. ... n = 1/0 # will execute and raise, aborting the ``with``
  75. ... n = 9999/0 # never executed
  76. This is just what ``with`` is supposed to do: abort the
  77. contained statement sequence at the first exception and let
  78. the context manager deal with the exception.
  79. To test multiple statements, you'll need a separate ``with``
  80. for each:
  81. >>> with raises(ZeroDivisionError):
  82. ... n = 1/0 # will execute and raise
  83. >>> with raises(ZeroDivisionError):
  84. ... n = 9999/0 # will also execute and raise
  85. """
  86. if code is None:
  87. return RaisesContext(expectedException)
  88. elif callable(code):
  89. try:
  90. code()
  91. except expectedException as e:
  92. return ExceptionInfo(e)
  93. raise Failed("DID NOT RAISE")
  94. elif isinstance(code, str):
  95. raise TypeError(
  96. '\'raises(xxx, "code")\' has been phased out; '
  97. 'change \'raises(xxx, "expression")\' '
  98. 'to \'raises(xxx, lambda: expression)\', '
  99. '\'raises(xxx, "statement")\' '
  100. 'to \'with raises(xxx): statement\'')
  101. else:
  102. raise TypeError(
  103. 'raises() expects a callable for the 2nd argument.')
  104. class RaisesContext:
  105. def __init__(self, expectedException):
  106. self.expectedException = expectedException
  107. def __enter__(self):
  108. return None
  109. def __exit__(self, exc_type, exc_value, traceback):
  110. if exc_type is None:
  111. raise Failed("DID NOT RAISE")
  112. return issubclass(exc_type, self.expectedException)
  113. class XFail(Exception):
  114. pass
  115. class XPass(Exception):
  116. pass
  117. class Skipped(Exception):
  118. pass
  119. class Failed(Exception): # type: ignore
  120. pass
  121. def XFAIL(func):
  122. def wrapper():
  123. try:
  124. func()
  125. except Exception as e:
  126. message = str(e)
  127. if message != "Timeout":
  128. raise XFail(func.__name__)
  129. else:
  130. raise Skipped("Timeout")
  131. raise XPass(func.__name__)
  132. wrapper = functools.update_wrapper(wrapper, func)
  133. return wrapper
  134. def skip(str):
  135. raise Skipped(str)
  136. def SKIP(reason):
  137. """Similar to ``skip()``, but this is a decorator. """
  138. def wrapper(func):
  139. def func_wrapper():
  140. raise Skipped(reason)
  141. func_wrapper = functools.update_wrapper(func_wrapper, func)
  142. return func_wrapper
  143. return wrapper
  144. def slow(func):
  145. func._slow = True
  146. def func_wrapper():
  147. func()
  148. func_wrapper = functools.update_wrapper(func_wrapper, func)
  149. func_wrapper.__wrapped__ = func
  150. return func_wrapper
  151. def nocache_fail(func):
  152. "Dummy decorator for marking tests that fail when cache is disabled"
  153. return func
  154. @contextlib.contextmanager
  155. def warns(warningcls, *, match='', test_stacklevel=True):
  156. '''
  157. Like raises but tests that warnings are emitted.
  158. >>> from sympy.testing.pytest import warns
  159. >>> import warnings
  160. >>> with warns(UserWarning):
  161. ... warnings.warn('deprecated', UserWarning, stacklevel=2)
  162. >>> with warns(UserWarning):
  163. ... pass
  164. Traceback (most recent call last):
  165. ...
  166. Failed: DID NOT WARN. No warnings of type UserWarning\
  167. was emitted. The list of emitted warnings is: [].
  168. ``test_stacklevel`` makes it check that the ``stacklevel`` parameter to
  169. ``warn()`` is set so that the warning shows the user line of code (the
  170. code under the warns() context manager). Set this to False if this is
  171. ambiguous or if the context manager does not test the direct user code
  172. that emits the warning.
  173. If the warning is a ``SymPyDeprecationWarning``, this additionally tests
  174. that the ``active_deprecations_target`` is a real target in the
  175. ``active-deprecations.md`` file.
  176. '''
  177. # Absorbs all warnings in warnrec
  178. with warnings.catch_warnings(record=True) as warnrec:
  179. # Any warning other than the one we are looking for is an error
  180. warnings.simplefilter("error")
  181. warnings.filterwarnings("always", category=warningcls)
  182. # Now run the test
  183. yield warnrec
  184. # Raise if expected warning not found
  185. if not any(issubclass(w.category, warningcls) for w in warnrec):
  186. msg = ('Failed: DID NOT WARN.'
  187. ' No warnings of type %s was emitted.'
  188. ' The list of emitted warnings is: %s.'
  189. ) % (warningcls, [w.message for w in warnrec])
  190. raise Failed(msg)
  191. # We don't include the match in the filter above because it would then
  192. # fall to the error filter, so we instead manually check that it matches
  193. # here
  194. for w in warnrec:
  195. # Should always be true due to the filters above
  196. assert issubclass(w.category, warningcls)
  197. if not re.compile(match, re.I).match(str(w.message)):
  198. raise Failed(f"Failed: WRONG MESSAGE. A warning with of the correct category ({warningcls.__name__}) was issued, but it did not match the given match regex ({match!r})")
  199. if test_stacklevel:
  200. for f in inspect.stack():
  201. thisfile = f.filename
  202. file = os.path.split(thisfile)[1]
  203. if file.startswith('test_'):
  204. break
  205. elif file == 'doctest.py':
  206. # skip the stacklevel testing in the doctests of this
  207. # function
  208. return
  209. else:
  210. raise RuntimeError("Could not find the file for the given warning to test the stacklevel")
  211. for w in warnrec:
  212. if w.filename != thisfile:
  213. msg = f'''\
  214. Failed: Warning has the wrong stacklevel. The warning stacklevel needs to be
  215. set so that the line of code shown in the warning message is user code that
  216. calls the deprecated code (the current stacklevel is showing code from
  217. {w.filename} (line {w.lineno}), expected {thisfile})'''.replace('\n', ' ')
  218. raise Failed(msg)
  219. if warningcls == SymPyDeprecationWarning:
  220. this_file = pathlib.Path(__file__)
  221. active_deprecations_file = (this_file.parent.parent.parent / 'doc' /
  222. 'src' / 'explanation' /
  223. 'active-deprecations.md')
  224. if not active_deprecations_file.exists():
  225. # We can only test that the active_deprecations_target works if we are
  226. # in the git repo.
  227. return
  228. targets = []
  229. for w in warnrec:
  230. targets.append(w.message.active_deprecations_target)
  231. with open(active_deprecations_file) as f:
  232. text = f.read()
  233. for target in targets:
  234. if f'({target})=' not in text:
  235. raise Failed(f"The active deprecations target {target!r} does not appear to be a valid target in the active-deprecations.md file ({active_deprecations_file}).")
  236. def _both_exp_pow(func):
  237. """
  238. Decorator used to run the test twice: the first time `e^x` is represented
  239. as ``Pow(E, x)``, the second time as ``exp(x)`` (exponential object is not
  240. a power).
  241. This is a temporary trick helping to manage the elimination of the class
  242. ``exp`` in favor of a replacement by ``Pow(E, ...)``.
  243. """
  244. from sympy.core.parameters import _exp_is_pow
  245. def func_wrap():
  246. with _exp_is_pow(True):
  247. func()
  248. with _exp_is_pow(False):
  249. func()
  250. wrapper = functools.update_wrapper(func_wrap, func)
  251. return wrapper
  252. @contextlib.contextmanager
  253. def warns_deprecated_sympy():
  254. '''
  255. Shorthand for ``warns(SymPyDeprecationWarning)``
  256. This is the recommended way to test that ``SymPyDeprecationWarning`` is
  257. emitted for deprecated features in SymPy. To test for other warnings use
  258. ``warns``. To suppress warnings without asserting that they are emitted
  259. use ``ignore_warnings``.
  260. .. note::
  261. ``warns_deprecated_sympy()`` is only intended for internal use in the
  262. SymPy test suite to test that a deprecation warning triggers properly.
  263. All other code in the SymPy codebase, including documentation examples,
  264. should not use deprecated behavior.
  265. If you are a user of SymPy and you want to disable
  266. SymPyDeprecationWarnings, use ``warnings`` filters (see
  267. :ref:`silencing-sympy-deprecation-warnings`).
  268. >>> from sympy.testing.pytest import warns_deprecated_sympy
  269. >>> from sympy.utilities.exceptions import sympy_deprecation_warning
  270. >>> with warns_deprecated_sympy():
  271. ... sympy_deprecation_warning("Don't use",
  272. ... deprecated_since_version="1.0",
  273. ... active_deprecations_target="active-deprecations")
  274. >>> with warns_deprecated_sympy():
  275. ... pass
  276. Traceback (most recent call last):
  277. ...
  278. Failed: DID NOT WARN. No warnings of type \
  279. SymPyDeprecationWarning was emitted. The list of emitted warnings is: [].
  280. .. note::
  281. Sometimes the stacklevel test will fail because the same warning is
  282. emitted multiple times. In this case, you can use
  283. :func:`sympy.utilities.exceptions.ignore_warnings` in the code to
  284. prevent the ``SymPyDeprecationWarning`` from being emitted again
  285. recursively. In rare cases it is impossible to have a consistent
  286. ``stacklevel`` for deprecation warnings because different ways of
  287. calling a function will produce different call stacks.. In those cases,
  288. use ``warns(SymPyDeprecationWarning)`` instead.
  289. See Also
  290. ========
  291. sympy.utilities.exceptions.SymPyDeprecationWarning
  292. sympy.utilities.exceptions.sympy_deprecation_warning
  293. sympy.utilities.decorator.deprecated
  294. '''
  295. with warns(SymPyDeprecationWarning):
  296. yield