test_callback.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import math
  2. import textwrap
  3. import sys
  4. import pytest
  5. import threading
  6. import traceback
  7. import time
  8. import platform
  9. import numpy as np
  10. from numpy.testing import IS_PYPY
  11. from . import util
  12. class TestF77Callback(util.F2PyTest):
  13. sources = [util.getpath("tests", "src", "callback", "foo.f")]
  14. @pytest.mark.parametrize("name", "t,t2".split(","))
  15. @pytest.mark.slow
  16. def test_all(self, name):
  17. self.check_function(name)
  18. @pytest.mark.xfail(IS_PYPY,
  19. reason="PyPy cannot modify tp_doc after PyType_Ready")
  20. def test_docstring(self):
  21. expected = textwrap.dedent("""\
  22. a = t(fun,[fun_extra_args])
  23. Wrapper for ``t``.
  24. Parameters
  25. ----------
  26. fun : call-back function
  27. Other Parameters
  28. ----------------
  29. fun_extra_args : input tuple, optional
  30. Default: ()
  31. Returns
  32. -------
  33. a : int
  34. Notes
  35. -----
  36. Call-back functions::
  37. def fun(): return a
  38. Return objects:
  39. a : int
  40. """)
  41. assert self.module.t.__doc__ == expected
  42. def check_function(self, name):
  43. t = getattr(self.module, name)
  44. r = t(lambda: 4)
  45. assert r == 4
  46. r = t(lambda a: 5, fun_extra_args=(6, ))
  47. assert r == 5
  48. r = t(lambda a: a, fun_extra_args=(6, ))
  49. assert r == 6
  50. r = t(lambda a: 5 + a, fun_extra_args=(7, ))
  51. assert r == 12
  52. r = t(lambda a: math.degrees(a), fun_extra_args=(math.pi, ))
  53. assert r == 180
  54. r = t(math.degrees, fun_extra_args=(math.pi, ))
  55. assert r == 180
  56. r = t(self.module.func, fun_extra_args=(6, ))
  57. assert r == 17
  58. r = t(self.module.func0)
  59. assert r == 11
  60. r = t(self.module.func0._cpointer)
  61. assert r == 11
  62. class A:
  63. def __call__(self):
  64. return 7
  65. def mth(self):
  66. return 9
  67. a = A()
  68. r = t(a)
  69. assert r == 7
  70. r = t(a.mth)
  71. assert r == 9
  72. @pytest.mark.skipif(sys.platform == 'win32',
  73. reason='Fails with MinGW64 Gfortran (Issue #9673)')
  74. def test_string_callback(self):
  75. def callback(code):
  76. if code == "r":
  77. return 0
  78. else:
  79. return 1
  80. f = self.module.string_callback
  81. r = f(callback)
  82. assert r == 0
  83. @pytest.mark.skipif(sys.platform == 'win32',
  84. reason='Fails with MinGW64 Gfortran (Issue #9673)')
  85. def test_string_callback_array(self):
  86. # See gh-10027
  87. cu1 = np.zeros((1, ), "S8")
  88. cu2 = np.zeros((1, 8), "c")
  89. cu3 = np.array([""], "S8")
  90. def callback(cu, lencu):
  91. if cu.shape != (lencu,):
  92. return 1
  93. if cu.dtype != "S8":
  94. return 2
  95. if not np.all(cu == b""):
  96. return 3
  97. return 0
  98. f = self.module.string_callback_array
  99. for cu in [cu1, cu2, cu3]:
  100. res = f(callback, cu, cu.size)
  101. assert res == 0
  102. def test_threadsafety(self):
  103. # Segfaults if the callback handling is not threadsafe
  104. errors = []
  105. def cb():
  106. # Sleep here to make it more likely for another thread
  107. # to call their callback at the same time.
  108. time.sleep(1e-3)
  109. # Check reentrancy
  110. r = self.module.t(lambda: 123)
  111. assert r == 123
  112. return 42
  113. def runner(name):
  114. try:
  115. for j in range(50):
  116. r = self.module.t(cb)
  117. assert r == 42
  118. self.check_function(name)
  119. except Exception:
  120. errors.append(traceback.format_exc())
  121. threads = [
  122. threading.Thread(target=runner, args=(arg, ))
  123. for arg in ("t", "t2") for n in range(20)
  124. ]
  125. for t in threads:
  126. t.start()
  127. for t in threads:
  128. t.join()
  129. errors = "\n\n".join(errors)
  130. if errors:
  131. raise AssertionError(errors)
  132. def test_hidden_callback(self):
  133. try:
  134. self.module.hidden_callback(2)
  135. except Exception as msg:
  136. assert str(msg).startswith("Callback global_f not defined")
  137. try:
  138. self.module.hidden_callback2(2)
  139. except Exception as msg:
  140. assert str(msg).startswith("cb: Callback global_f not defined")
  141. self.module.global_f = lambda x: x + 1
  142. r = self.module.hidden_callback(2)
  143. assert r == 3
  144. self.module.global_f = lambda x: x + 2
  145. r = self.module.hidden_callback(2)
  146. assert r == 4
  147. del self.module.global_f
  148. try:
  149. self.module.hidden_callback(2)
  150. except Exception as msg:
  151. assert str(msg).startswith("Callback global_f not defined")
  152. self.module.global_f = lambda x=0: x + 3
  153. r = self.module.hidden_callback(2)
  154. assert r == 5
  155. # reproducer of gh18341
  156. r = self.module.hidden_callback2(2)
  157. assert r == 3
  158. class TestF77CallbackPythonTLS(TestF77Callback):
  159. """
  160. Callback tests using Python thread-local storage instead of
  161. compiler-provided
  162. """
  163. options = ["-DF2PY_USE_PYTHON_TLS"]
  164. class TestF90Callback(util.F2PyTest):
  165. sources = [util.getpath("tests", "src", "callback", "gh17797.f90")]
  166. @pytest.mark.slow
  167. def test_gh17797(self):
  168. def incr(x):
  169. return x + 123
  170. y = np.array([1, 2, 3], dtype=np.int64)
  171. r = self.module.gh17797(incr, y)
  172. assert r == 123 + 1 + 2 + 3
  173. class TestGH18335(util.F2PyTest):
  174. """The reproduction of the reported issue requires specific input that
  175. extensions may break the issue conditions, so the reproducer is
  176. implemented as a separate test class. Do not extend this test with
  177. other tests!
  178. """
  179. sources = [util.getpath("tests", "src", "callback", "gh18335.f90")]
  180. @pytest.mark.slow
  181. def test_gh18335(self):
  182. def foo(x):
  183. x[0] += 1
  184. r = self.module.gh18335(foo)
  185. assert r == 123 + 1
  186. class TestGH25211(util.F2PyTest):
  187. sources = [util.getpath("tests", "src", "callback", "gh25211.f"),
  188. util.getpath("tests", "src", "callback", "gh25211.pyf")]
  189. module_name = "callback2"
  190. def test_gh25211(self):
  191. def bar(x):
  192. return x*x
  193. res = self.module.foo(bar)
  194. assert res == 110
  195. @pytest.mark.slow
  196. @pytest.mark.xfail(condition=(platform.system().lower() == 'darwin'),
  197. run=False,
  198. reason="Callback aborts cause CI failures on macOS")
  199. class TestCBFortranCallstatement(util.F2PyTest):
  200. sources = [util.getpath("tests", "src", "callback", "gh26681.f90")]
  201. options = ['--lower']
  202. def test_callstatement_fortran(self):
  203. with pytest.raises(ValueError, match='helpme') as exc:
  204. self.module.mypy_abort = self.module.utils.my_abort
  205. self.module.utils.do_something('helpme')