__init__.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests for greenlet.
  4. """
  5. import os
  6. import sys
  7. import unittest
  8. from gc import collect
  9. from gc import get_objects
  10. from threading import active_count as active_thread_count
  11. from time import sleep
  12. from time import time
  13. import psutil
  14. from greenlet import greenlet as RawGreenlet
  15. from greenlet import getcurrent
  16. from greenlet._greenlet import get_pending_cleanup_count
  17. from greenlet._greenlet import get_total_main_greenlets
  18. from . import leakcheck
  19. PY312 = sys.version_info[:2] >= (3, 12)
  20. PY313 = sys.version_info[:2] >= (3, 13)
  21. WIN = sys.platform.startswith("win")
  22. RUNNING_ON_GITHUB_ACTIONS = os.environ.get('GITHUB_ACTIONS')
  23. RUNNING_ON_TRAVIS = os.environ.get('TRAVIS') or RUNNING_ON_GITHUB_ACTIONS
  24. RUNNING_ON_APPVEYOR = os.environ.get('APPVEYOR')
  25. RUNNING_ON_CI = RUNNING_ON_TRAVIS or RUNNING_ON_APPVEYOR
  26. RUNNING_ON_MANYLINUX = os.environ.get('GREENLET_MANYLINUX')
  27. class TestCaseMetaClass(type):
  28. # wrap each test method with
  29. # a) leak checks
  30. def __new__(cls, classname, bases, classDict):
  31. # pylint and pep8 fight over what this should be called (mcs or cls).
  32. # pylint gets it right, but we can't scope disable pep8, so we go with
  33. # its convention.
  34. # pylint: disable=bad-mcs-classmethod-argument
  35. check_totalrefcount = True
  36. # Python 3: must copy, we mutate the classDict. Interestingly enough,
  37. # it doesn't actually error out, but under 3.6 we wind up wrapping
  38. # and re-wrapping the same items over and over and over.
  39. for key, value in list(classDict.items()):
  40. if key.startswith('test') and callable(value):
  41. classDict.pop(key)
  42. if check_totalrefcount:
  43. value = leakcheck.wrap_refcount(value)
  44. classDict[key] = value
  45. return type.__new__(cls, classname, bases, classDict)
  46. class TestCase(TestCaseMetaClass(
  47. "NewBase",
  48. (unittest.TestCase,),
  49. {})):
  50. cleanup_attempt_sleep_duration = 0.001
  51. cleanup_max_sleep_seconds = 1
  52. def wait_for_pending_cleanups(self,
  53. initial_active_threads=None,
  54. initial_main_greenlets=None):
  55. initial_active_threads = initial_active_threads or self.threads_before_test
  56. initial_main_greenlets = initial_main_greenlets or self.main_greenlets_before_test
  57. sleep_time = self.cleanup_attempt_sleep_duration
  58. # NOTE: This is racy! A Python-level thread object may be dead
  59. # and gone, but the C thread may not yet have fired its
  60. # destructors and added to the queue. There's no particular
  61. # way to know that's about to happen. We try to watch the
  62. # Python threads to make sure they, at least, have gone away.
  63. # Counting the main greenlets, which we can easily do deterministically,
  64. # also helps.
  65. # Always sleep at least once to let other threads run
  66. sleep(sleep_time)
  67. quit_after = time() + self.cleanup_max_sleep_seconds
  68. # TODO: We could add an API that calls us back when a particular main greenlet is deleted?
  69. # It would have to drop the GIL
  70. while (
  71. get_pending_cleanup_count()
  72. or active_thread_count() > initial_active_threads
  73. or (not self.expect_greenlet_leak
  74. and get_total_main_greenlets() > initial_main_greenlets)):
  75. sleep(sleep_time)
  76. if time() > quit_after:
  77. print("Time limit exceeded.")
  78. print("Threads: Waiting for only", initial_active_threads,
  79. "-->", active_thread_count())
  80. print("MGlets : Waiting for only", initial_main_greenlets,
  81. "-->", get_total_main_greenlets())
  82. break
  83. collect()
  84. def count_objects(self, kind=list, exact_kind=True):
  85. # pylint:disable=unidiomatic-typecheck
  86. # Collect the garbage.
  87. for _ in range(3):
  88. collect()
  89. if exact_kind:
  90. return sum(
  91. 1
  92. for x in get_objects()
  93. if type(x) is kind
  94. )
  95. # instances
  96. return sum(
  97. 1
  98. for x in get_objects()
  99. if isinstance(x, kind)
  100. )
  101. greenlets_before_test = 0
  102. threads_before_test = 0
  103. main_greenlets_before_test = 0
  104. expect_greenlet_leak = False
  105. def count_greenlets(self):
  106. """
  107. Find all the greenlets and subclasses tracked by the GC.
  108. """
  109. return self.count_objects(RawGreenlet, False)
  110. def setUp(self):
  111. # Ensure the main greenlet exists, otherwise the first test
  112. # gets a false positive leak
  113. super().setUp()
  114. getcurrent()
  115. self.threads_before_test = active_thread_count()
  116. self.main_greenlets_before_test = get_total_main_greenlets()
  117. self.wait_for_pending_cleanups(self.threads_before_test, self.main_greenlets_before_test)
  118. self.greenlets_before_test = self.count_greenlets()
  119. def tearDown(self):
  120. if getattr(self, 'skipTearDown', False):
  121. return
  122. self.wait_for_pending_cleanups(self.threads_before_test, self.main_greenlets_before_test)
  123. super().tearDown()
  124. def get_expected_returncodes_for_aborted_process(self):
  125. import signal
  126. # The child should be aborted in an unusual way. On POSIX
  127. # platforms, this is done with abort() and signal.SIGABRT,
  128. # which is reflected in a negative return value; however, on
  129. # Windows, even though we observe the child print "Fatal
  130. # Python error: Aborted" and in older versions of the C
  131. # runtime "This application has requested the Runtime to
  132. # terminate it in an unusual way," it always has an exit code
  133. # of 3. This is interesting because 3 is the error code for
  134. # ERROR_PATH_NOT_FOUND; BUT: the C runtime abort() function
  135. # also uses this code.
  136. #
  137. # If we link to the static C library on Windows, the error
  138. # code changes to '0xc0000409' (hex(3221226505)), which
  139. # apparently is STATUS_STACK_BUFFER_OVERRUN; but "What this
  140. # means is that nowadays when you get a
  141. # STATUS_STACK_BUFFER_OVERRUN, it doesn’t actually mean that
  142. # there is a stack buffer overrun. It just means that the
  143. # application decided to terminate itself with great haste."
  144. #
  145. #
  146. # On windows, we've also seen '0xc0000005' (hex(3221225477)).
  147. # That's "Access Violation"
  148. #
  149. # See
  150. # https://devblogs.microsoft.com/oldnewthing/20110519-00/?p=10623
  151. # and
  152. # https://docs.microsoft.com/en-us/previous-versions/k089yyh0(v=vs.140)?redirectedfrom=MSDN
  153. # and
  154. # https://devblogs.microsoft.com/oldnewthing/20190108-00/?p=100655
  155. expected_exit = (
  156. -signal.SIGABRT,
  157. # But beginning on Python 3.11, the faulthandler
  158. # that prints the C backtraces sometimes segfaults after
  159. # reporting the exception but before printing the stack.
  160. # This has only been seen on linux/gcc.
  161. -signal.SIGSEGV,
  162. ) if not WIN else (
  163. 3,
  164. 0xc0000409,
  165. 0xc0000005,
  166. )
  167. return expected_exit
  168. def get_process_uss(self):
  169. """
  170. Return the current process's USS in bytes.
  171. uss is available on Linux, macOS, Windows. Also known as
  172. "Unique Set Size", this is the memory which is unique to a
  173. process and which would be freed if the process was terminated
  174. right now.
  175. If this is not supported by ``psutil``, this raises the
  176. :exc:`unittest.SkipTest` exception.
  177. """
  178. try:
  179. return psutil.Process().memory_full_info().uss
  180. except AttributeError as e:
  181. raise unittest.SkipTest("uss not supported") from e
  182. def run_script(self, script_name, show_output=True):
  183. import subprocess
  184. script = os.path.join(
  185. os.path.dirname(__file__),
  186. script_name,
  187. )
  188. try:
  189. return subprocess.check_output([sys.executable, script],
  190. encoding='utf-8',
  191. stderr=subprocess.STDOUT)
  192. except subprocess.CalledProcessError as ex:
  193. if show_output:
  194. print('-----')
  195. print('Failed to run script', script)
  196. print('~~~~~')
  197. print(ex.output)
  198. print('------')
  199. raise
  200. def assertScriptRaises(self, script_name, exitcodes=None):
  201. import subprocess
  202. with self.assertRaises(subprocess.CalledProcessError) as exc:
  203. output = self.run_script(script_name, show_output=False)
  204. __traceback_info__ = output
  205. # We're going to fail the assertion if we get here, at least
  206. # preserve the output in the traceback.
  207. if exitcodes is None:
  208. exitcodes = self.get_expected_returncodes_for_aborted_process()
  209. self.assertIn(exc.exception.returncode, exitcodes)
  210. return exc.exception