194 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			194 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Python
		
	
	
	
| 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))
 |