1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
|
# mode: run
cimport cython
import sys
# The tests here are to do with a deterministic order of destructors which
# isn't reliable for PyPy. Therefore, on PyPy we treat the test as
# "compiles and doesn't crash"
IS_PYPY = hasattr(sys, 'pypy_version_info')
# Count number of times an object was deallocated twice. This should remain 0.
cdef int double_deallocations = 0
def assert_no_double_deallocations():
if IS_PYPY:
return
global double_deallocations
err = double_deallocations
double_deallocations = 0
assert not err
# Compute x = f(f(f(...(None)...))) nested n times and throw away the result.
# The real test happens when exiting this function: then a big recursive
# deallocation of x happens. We are testing two things in the tests below:
# that Python does not crash and that no double deallocation happens.
# See also https://github.com/python/cpython/pull/11841
def recursion_test(f, int n=2**20):
x = None
cdef int i
for i in range(n):
x = f(x)
@cython.trashcan(True)
cdef class Recurse:
"""
>>> recursion_test(Recurse)
>>> assert_no_double_deallocations()
"""
cdef public attr
cdef int deallocated
def __cinit__(self, x):
self.attr = x
def __dealloc__(self):
# Check that we're not being deallocated twice
global double_deallocations
double_deallocations += self.deallocated
self.deallocated = 1
cdef class RecurseSub(Recurse):
"""
>>> recursion_test(RecurseSub)
>>> assert_no_double_deallocations()
"""
cdef int subdeallocated
def __dealloc__(self):
# Check that we're not being deallocated twice
global double_deallocations
double_deallocations += self.subdeallocated
self.subdeallocated = 1
@cython.freelist(4)
@cython.trashcan(True)
cdef class RecurseFreelist:
"""
>>> recursion_test(RecurseFreelist)
>>> recursion_test(RecurseFreelist, 1000)
>>> assert_no_double_deallocations()
"""
cdef public attr
cdef int deallocated
def __cinit__(self, x):
self.attr = x
def __dealloc__(self):
# Check that we're not being deallocated twice
global double_deallocations
double_deallocations += self.deallocated
self.deallocated = 1
# Subclass of list => uses trashcan by default
# As long as https://github.com/python/cpython/pull/11841 is not fixed,
# this does lead to double deallocations, so we skip that check.
cdef class RecurseList(list):
"""
>>> RecurseList(42)
[42]
>>> recursion_test(RecurseList)
"""
def __init__(self, x):
super().__init__((x,))
# Some tests where the trashcan is NOT used. When the trashcan is not used
# in a big recursive deallocation, the __dealloc__s of the base classes are
# only run after the __dealloc__s of the subclasses.
# We use this to detect trashcan usage.
cdef int base_deallocated = 0
cdef int trashcan_used = 0
def assert_no_trashcan_used():
if IS_PYPY:
return
global base_deallocated, trashcan_used
err = trashcan_used
trashcan_used = base_deallocated = 0
assert not err
cdef class Base:
def __dealloc__(self):
global base_deallocated
base_deallocated = 1
# Trashcan disabled by default
cdef class Sub1(Base):
"""
>>> recursion_test(Sub1, 100)
>>> assert_no_trashcan_used()
"""
cdef public attr
def __cinit__(self, x):
self.attr = x
def __dealloc__(self):
global base_deallocated, trashcan_used
trashcan_used += base_deallocated
@cython.trashcan(True)
cdef class Middle(Base):
cdef public foo
# Trashcan disabled explicitly
@cython.trashcan(False)
cdef class Sub2(Middle):
"""
>>> recursion_test(Sub2, 1000)
>>> assert_no_trashcan_used()
"""
cdef public attr
def __cinit__(self, x):
self.attr = x
def __dealloc__(self):
global base_deallocated, trashcan_used
trashcan_used += base_deallocated
|