| import os |
| import re |
| import sys |
| import warnings |
| from inspect import isabstract |
| from test import support |
| from test.support import os_helper |
| from test.libregrtest.utils import clear_caches |
| |
| try: |
| from _abc import _get_dump |
| except ImportError: |
| import weakref |
| |
| def _get_dump(cls): |
| # Reimplement _get_dump() for pure-Python implementation of |
| # the abc module (Lib/_py_abc.py) |
| registry_weakrefs = set(weakref.ref(obj) for obj in cls._abc_registry) |
| return (registry_weakrefs, cls._abc_cache, |
| cls._abc_negative_cache, cls._abc_negative_cache_version) |
| |
| |
| def dash_R(ns, test_name, test_func): |
| """Run a test multiple times, looking for reference leaks. |
| |
| Returns: |
| False if the test didn't leak references; True if we detected refleaks. |
| """ |
| # This code is hackish and inelegant, but it seems to do the job. |
| import copyreg |
| import collections.abc |
| |
| if not hasattr(sys, 'gettotalrefcount'): |
| raise Exception("Tracking reference leaks requires a debug build " |
| "of Python") |
| |
| # Avoid false positives due to various caches |
| # filling slowly with random data: |
| warm_caches() |
| |
| # Save current values for dash_R_cleanup() to restore. |
| fs = warnings.filters[:] |
| ps = copyreg.dispatch_table.copy() |
| pic = sys.path_importer_cache.copy() |
| try: |
| import zipimport |
| except ImportError: |
| zdc = None # Run unmodified on platforms without zipimport support |
| else: |
| zdc = zipimport._zip_directory_cache.copy() |
| abcs = {} |
| for abc in [getattr(collections.abc, a) for a in collections.abc.__all__]: |
| if not isabstract(abc): |
| continue |
| for obj in abc.__subclasses__() + [abc]: |
| abcs[obj] = _get_dump(obj)[0] |
| |
| # bpo-31217: Integer pool to get a single integer object for the same |
| # value. The pool is used to prevent false alarm when checking for memory |
| # block leaks. Fill the pool with values in -1000..1000 which are the most |
| # common (reference, memory block, file descriptor) differences. |
| int_pool = {value: value for value in range(-1000, 1000)} |
| def get_pooled_int(value): |
| return int_pool.setdefault(value, value) |
| |
| nwarmup, ntracked, fname = ns.huntrleaks |
| fname = os.path.join(os_helper.SAVEDCWD, fname) |
| repcount = nwarmup + ntracked |
| |
| # Pre-allocate to ensure that the loop doesn't allocate anything new |
| rep_range = list(range(repcount)) |
| rc_deltas = [0] * repcount |
| alloc_deltas = [0] * repcount |
| fd_deltas = [0] * repcount |
| getallocatedblocks = sys.getallocatedblocks |
| gettotalrefcount = sys.gettotalrefcount |
| fd_count = os_helper.fd_count |
| |
| # initialize variables to make pyflakes quiet |
| rc_before = alloc_before = fd_before = 0 |
| |
| if not ns.quiet: |
| print("beginning", repcount, "repetitions", file=sys.stderr) |
| print(("1234567890"*(repcount//10 + 1))[:repcount], file=sys.stderr, |
| flush=True) |
| |
| dash_R_cleanup(fs, ps, pic, zdc, abcs) |
| |
| for i in rep_range: |
| test_func() |
| dash_R_cleanup(fs, ps, pic, zdc, abcs) |
| |
| # dash_R_cleanup() ends with collecting cyclic trash: |
| # read memory statistics immediately after. |
| alloc_after = getallocatedblocks() |
| rc_after = gettotalrefcount() |
| fd_after = fd_count() |
| |
| if not ns.quiet: |
| print('.', end='', file=sys.stderr, flush=True) |
| |
| rc_deltas[i] = get_pooled_int(rc_after - rc_before) |
| alloc_deltas[i] = get_pooled_int(alloc_after - alloc_before) |
| fd_deltas[i] = get_pooled_int(fd_after - fd_before) |
| |
| alloc_before = alloc_after |
| rc_before = rc_after |
| fd_before = fd_after |
| |
| if not ns.quiet: |
| print(file=sys.stderr) |
| |
| # These checkers return False on success, True on failure |
| def check_rc_deltas(deltas): |
| # Checker for reference counters and memory blocks. |
| # |
| # bpo-30776: Try to ignore false positives: |
| # |
| # [3, 0, 0] |
| # [0, 1, 0] |
| # [8, -8, 1] |
| # |
| # Expected leaks: |
| # |
| # [5, 5, 6] |
| # [10, 1, 1] |
| return all(delta >= 1 for delta in deltas) |
| |
| def check_fd_deltas(deltas): |
| return any(deltas) |
| |
| failed = False |
| for deltas, item_name, checker in [ |
| (rc_deltas, 'references', check_rc_deltas), |
| (alloc_deltas, 'memory blocks', check_rc_deltas), |
| (fd_deltas, 'file descriptors', check_fd_deltas) |
| ]: |
| # ignore warmup runs |
| deltas = deltas[nwarmup:] |
| if checker(deltas): |
| msg = '%s leaked %s %s, sum=%s' % ( |
| test_name, deltas, item_name, sum(deltas)) |
| print(msg, file=sys.stderr, flush=True) |
| with open(fname, "a") as refrep: |
| print(msg, file=refrep) |
| refrep.flush() |
| failed = True |
| return failed |
| |
| |
| def dash_R_cleanup(fs, ps, pic, zdc, abcs): |
| import copyreg |
| import collections.abc |
| |
| # Restore some original values. |
| warnings.filters[:] = fs |
| copyreg.dispatch_table.clear() |
| copyreg.dispatch_table.update(ps) |
| sys.path_importer_cache.clear() |
| sys.path_importer_cache.update(pic) |
| try: |
| import zipimport |
| except ImportError: |
| pass # Run unmodified on platforms without zipimport support |
| else: |
| zipimport._zip_directory_cache.clear() |
| zipimport._zip_directory_cache.update(zdc) |
| |
| # clear type cache |
| sys._clear_type_cache() |
| |
| # Clear ABC registries, restoring previously saved ABC registries. |
| abs_classes = [getattr(collections.abc, a) for a in collections.abc.__all__] |
| abs_classes = filter(isabstract, abs_classes) |
| for abc in abs_classes: |
| for obj in abc.__subclasses__() + [abc]: |
| for ref in abcs.get(obj, set()): |
| if ref() is not None: |
| obj.register(ref()) |
| obj._abc_caches_clear() |
| |
| clear_caches() |
| |
| |
| def warm_caches(): |
| # char cache |
| s = bytes(range(256)) |
| for i in range(256): |
| s[i:i+1] |
| # unicode cache |
| [chr(i) for i in range(256)] |
| # int cache |
| list(range(-5, 257)) |