Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
leakcheck.py335 linesDownload Raw Back to tests
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 
codekingpro/portable-devtools · Team Ai