codekingpro/portable-devtools
114k
1# Copyright (c) 2018 gevent community
2# Copyright (c) 2021 greenlet community
3#
4# This was originally part of gevent's test suite. The main author
5# (Jason Madden) vendored a copy of it into greenlet.
6#
7# Permission is hereby granted, free of charge, to any person obtaining a copy
8# of this software and associated documentation files (the "Software"), to deal
9# in the Software without restriction, including without limitation the rights
10# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11# copies of the Software, and to permit persons to whom the Software is
12# furnished to do so, subject to the following conditions:
13#
14# The above copyright notice and this permission notice shall be included in
15# all copies or substantial portions of the Software.
16#
17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23# THE SOFTWARE.
24import os
25import sys
26import gc
27
28from functools import wraps
29import unittest
30
31
32import objgraph
33
34# graphviz 0.18 (Nov 7 2021), available only on Python 3.6 and newer,
35# has added type hints (sigh). It wants to use ``typing.Literal`` for
36# some stuff, but that's only available on Python 3.9+. If that's not
37# found, it creates a ``unittest.mock.MagicMock`` object and annotates
38# with that. These are GC'able objects, and doing almost *anything*
39# with them results in an explosion of objects. For example, trying to
40# compare them for equality creates new objects. This causes our
41# leakchecks to fail, with reports like:
42#
43# greenlet.tests.leakcheck.LeakCheckError: refcount increased by [337, 1333, 343, 430, 530, 643, 769]
44# _Call 1820 +546
45# dict 4094 +76
46# MagicProxy 585 +73
47# tuple 2693 +66
48# _CallList 24 +3
49# weakref 1441 +1
50# function 5996 +1
51# type 736 +1
52# cell 592 +1
53# MagicMock 8 +1
54#
55# To avoid this, we *could* filter this type of object out early. In
56# principle it could leak, but we don't use mocks in greenlet, so it
57# doesn't leak from us. However, a further issue is that ``MagicMock``
58# objects have subobjects that are also GC'able, like ``_Call``, and
59# those create new mocks of their own too. So we'd have to filter them
60# as well, and they're not public. That's OK, we can workaround the
61# problem by being very careful to never compare by equality or other
62# user-defined operators, only using object identity or other builtin
63# functions.
64
65RUNNING_ON_GITHUB_ACTIONS = os.environ.get('GITHUB_ACTIONS')
66RUNNING_ON_TRAVIS = os.environ.get('TRAVIS') or RUNNING_ON_GITHUB_ACTIONS
67RUNNING_ON_APPVEYOR = os.environ.get('APPVEYOR')
68RUNNING_ON_CI = RUNNING_ON_TRAVIS or RUNNING_ON_APPVEYOR
69RUNNING_ON_MANYLINUX = os.environ.get('GREENLET_MANYLINUX')
70SKIP_LEAKCHECKS = RUNNING_ON_MANYLINUX or os.environ.get('GREENLET_SKIP_LEAKCHECKS')
71SKIP_FAILING_LEAKCHECKS = os.environ.get('GREENLET_SKIP_FAILING_LEAKCHECKS')
72ONLY_FAILING_LEAKCHECKS = os.environ.get('GREENLET_ONLY_FAILING_LEAKCHECKS')
73
74def ignores_leakcheck(func):
75 """
76 Ignore the given object during leakchecks.
77
78 Can be applied to a method, in which case the method will run, but
79 will not be subject to leak checks.
80
81 If applied to a class, the entire class will be skipped during leakchecks. This
82 is intended to be used for classes that are very slow and cause problems such as
83 test timeouts; typically it will be used for classes that are subclasses of a base
84 class and specify variants of behaviour (such as pool sizes).
85 """
86 func.ignore_leakcheck = True
87 return func
88
89def fails_leakcheck(func):
90 """
91 Mark that the function is known to leak.
92 """
93 func.fails_leakcheck = True
94 if SKIP_FAILING_LEAKCHECKS:
95 func = unittest.skip("Skipping known failures")(func)
96 return func
97
98class LeakCheckError(AssertionError):
99 pass
100
101if hasattr(sys, 'getobjects'):
102 # In a Python build with ``--with-trace-refs``, make objgraph
103 # trace *all* the objects, not just those that are tracked by the
104 # GC
105 class _MockGC(object):
106 def get_objects(self):
107 return sys.getobjects(0) # pylint:disable=no-member
108 def __getattr__(self, name):
109 return getattr(gc, name)
110 objgraph.gc = _MockGC()
111 fails_strict_leakcheck = fails_leakcheck
112else:
113 def fails_strict_leakcheck(func):
114 """
115 Decorator for a function that is known to fail when running
116 strict (``sys.getobjects()``) leakchecks.
117
118 This type of leakcheck finds all objects, even those, such as
119 strings, which are not tracked by the garbage collector.
120 """
121 return func
122
123class ignores_types_in_strict_leakcheck(object):
124 def __init__(self, types):
125 self.types = types
126 def __call__(self, func):
127 func.leakcheck_ignore_types = self.types
128 return func
129
130class _RefCountChecker(object):
131
132 # Some builtin things that we ignore
133 # XXX: Those things were ignored by gevent, but they're important here,
134 # presumably.
135 IGNORED_TYPES = () #(tuple, dict, types.FrameType, types.TracebackType)
136
137 # Names of types that should be ignored. Use this when we cannot
138 # or don't want to import the class directly.
139 IGNORED_TYPE_NAMES = (
140 # This appears in Python3.14 with the JIT enabled. It
141 # doesn't seem to be directly exposed to Python; the only way to get
142 # one is to cause code to get jitted and then look for all objects
143 # and find one with this name. But they multiply as code
144 # executes and gets jitted, in ways we don't want to rely on.
145 # So just ignore it.
146 'uop_executor',
147 )
148
149 def __init__(self, testcase, function):
150 self.testcase = testcase
151 self.function = function
152 self.deltas = []
153 self.peak_stats = {}
154 self.ignored_types = ()
155
156 # The very first time we are called, we have already been
157 # self.setUp() by the test runner, so we don't need to do it again.
158 self.needs_setUp = False
159
160 def _include_object_p(self, obj):
161 # pylint:disable=too-many-return-statements
162 #
163 # See the comment block at the top. We must be careful to
164 # avoid invoking user-defined operations.
165 if obj is self:
166 return False
167 kind = type(obj)
168 # ``self._include_object_p == obj`` returns NotImplemented
169 # for non-function objects, which causes the interpreter
170 # to try to reverse the order of arguments...which leads
171 # to the explosion of mock objects. We don't want that, so we implement
172 # the check manually.
173 if kind == type(self._include_object_p): # pylint: disable=unidiomatic-typecheck
174 try:
175 # pylint:disable=not-callable
176 exact_method_equals = self._include_object_p.__eq__(obj)
177 except AttributeError:
178 # Python 2.7 methods may only have __cmp__, and that raises a
179 # TypeError for non-method arguments
180 # pylint:disable=no-member
181 exact_method_equals = self._include_object_p.__cmp__(obj) == 0
182
183 if exact_method_equals is not NotImplemented and exact_method_equals:
184 return False
185
186 # Similarly, we need to check identity in our __dict__ to avoid mock explosions.
187 for x in self.__dict__.values():
188 if obj is x:
189 return False
190
191
192 if (
193 kind in self.ignored_types
194 or kind in self.IGNORED_TYPES
195 or kind.__name__ in self.IGNORED_TYPE_NAMES
196 ):
197 return False
198
199
200 return True
201
202 def _growth(self):
203 return objgraph.growth(limit=None, peak_stats=self.peak_stats,
204 filter=self._include_object_p)
205
206 def _report_diff(self, growth):
207 if not growth:
208 return "<Unable to calculate growth>"
209
210 lines = []
211 width = max(len(name) for name, _, _ in growth)
212 for name, count, delta in growth:
213 lines.append('%-*s%9d %+9d' % (width, name, count, delta))
214
215 diff = '\n'.join(lines)
216 return diff
217
218
219 def _run_test(self, args, kwargs):
220 gc_enabled = gc.isenabled()
221 gc.disable()
222
223 if self.needs_setUp:
224 self.testcase.setUp()
225 self.testcase.skipTearDown = False
226 try:
227 self.function(self.testcase, *args, **kwargs)
228 finally:
229 self.testcase.tearDown()
230 self.testcase.doCleanups()
231 self.testcase.skipTearDown = True
232 self.needs_setUp = True
233 if gc_enabled:
234 gc.enable()
235
236 def _growth_after(self):
237 # Grab post snapshot
238 # pylint:disable=no-member
239 if 'urlparse' in sys.modules:
240 sys.modules['urlparse'].clear_cache()
241 if 'urllib.parse' in sys.modules:
242 sys.modules['urllib.parse'].clear_cache()
243
244 return self._growth()
245
246 def _check_deltas(self, growth):
247 # Return false when we have decided there is no leak,
248 # true if we should keep looping, raises an assertion
249 # if we have decided there is a leak.
250
251 deltas = self.deltas
252 if not deltas:
253 # We haven't run yet, no data, keep looping
254 return True
255
256 if gc.garbage:
257 raise LeakCheckError("Generated uncollectable garbage %r" % (gc.garbage,))
258
259
260 # the following configurations are classified as "no leak"
261 # [0, 0]
262 # [x, 0, 0]
263 # [... a, b, c, d] where a+b+c+d = 0
264 #
265 # the following configurations are classified as "leak"
266 # [... z, z, z] where z > 0
267
268 if deltas[-2:] == [0, 0] and len(deltas) in (2, 3):
269 return False
270
271 if deltas[-3:] == [0, 0, 0]:
272 return False
273
274 if len(deltas) >= 4 and sum(deltas[-4:]) == 0:
275 return False
276
277 if len(deltas) >= 3 and deltas[-1] > 0 and deltas[-1] == deltas[-2] and deltas[-2] == deltas[-3]:
278 diff = self._report_diff(growth)
279 raise LeakCheckError('refcount increased by %r\n%s' % (deltas, diff))
280
281 # OK, we don't know for sure yet. Let's search for more
282 if sum(deltas[-3:]) <= 0 or sum(deltas[-4:]) <= 0 or deltas[-4:].count(0) >= 2:
283 # this is suspicious, so give a few more runs
284 limit = 11
285 else:
286 limit = 7
287 if len(deltas) >= limit:
288 raise LeakCheckError('refcount increased by %r\n%s'
289 % (deltas,
290 self._report_diff(growth)))
291
292 # We couldn't decide yet, keep going
293 return True
294
295 def __call__(self, args, kwargs):
296 for _ in range(3):
297 gc.collect()
298
299 expect_failure = getattr(self.function, 'fails_leakcheck', False)
300 if expect_failure:
301 self.testcase.expect_greenlet_leak = True
302 self.ignored_types = getattr(self.function, "leakcheck_ignore_types", ())
303
304 # Capture state before; the incremental will be
305 # updated by each call to _growth_after
306 growth = self._growth()
307
308 try:
309 while self._check_deltas(growth):
310 self._run_test(args, kwargs)
311
312 growth = self._growth_after()
313
314 self.deltas.append(sum((stat[2] for stat in growth)))
315 except LeakCheckError:
316 if not expect_failure:
317 raise
318 else:
319 if expect_failure:
320 raise LeakCheckError("Expected %s to leak but it did not." % (self.function,))
321
322def wrap_refcount(method):
323 if getattr(method, 'ignore_leakcheck', False) or SKIP_LEAKCHECKS:
324 return method
325
326 @wraps(method)
327 def wrapper(self, *args, **kwargs): # pylint:disable=too-many-branches
328 if getattr(self, 'ignore_leakcheck', False):
329 raise unittest.SkipTest("This class ignored during leakchecks")
330 if ONLY_FAILING_LEAKCHECKS and not getattr(method, 'fails_leakcheck', False):
331 raise unittest.SkipTest("Only running tests that fail leakchecks.")
332 return _RefCountChecker(self, method)(args, kwargs)
333
334 return wrapper
335 