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