import unittest
+import unittest.mock
from test.support import (verbose, refcount_test, run_unittest,
strip_python_stderr, cpython_only, start_threads,
temp_dir, requires_type_collecting, TESTFN, unlink,
raise TypeError('requires _testcapi.with_tp_del')
return C
+try:
+ from _testcapi import ContainerNoGC
+except ImportError:
+ ContainerNoGC = None
+
### Support code
###############################################################################
gc.enable()
+ @unittest.skipIf(ContainerNoGC is None,
+ 'requires ContainerNoGC extension type')
+ def test_trash_weakref_clear(self):
+ # Test that trash weakrefs are properly cleared (bpo-38006).
+ #
+ # Structure we are creating:
+ #
+ # Z <- Y <- A--+--> WZ -> C
+ # ^ |
+ # +--+
+ # where:
+ # WZ is a weakref to Z with callback C
+ # Y doesn't implement tp_traverse
+ # A contains a reference to itself, Y and WZ
+ #
+ # A, Y, Z, WZ are all trash. The GC doesn't know that Z is trash
+ # because Y does not implement tp_traverse. To show the bug, WZ needs
+ # to live long enough so that Z is deallocated before it. Then, if
+ # gcmodule is buggy, when Z is being deallocated, C will run.
+ #
+ # To ensure WZ lives long enough, we put it in a second reference
+ # cycle. That trick only works due to the ordering of the GC prev/next
+ # linked lists. So, this test is a bit fragile.
+ #
+ # The bug reported in bpo-38006 is caused because the GC did not
+ # clear WZ before starting the process of calling tp_clear on the
+ # trash. Normally, handle_weakrefs() would find the weakref via Z and
+ # clear it. However, since the GC cannot find Z, WR is not cleared and
+ # it can execute during delete_garbage(). That can lead to disaster
+ # since the callback might tinker with objects that have already had
+ # tp_clear called on them (leaving them in possibly invalid states).
+
+ callback = unittest.mock.Mock()
+
+ class A:
+ __slots__ = ['a', 'y', 'wz']
+
+ class Z:
+ pass
+
+ # setup required object graph, as described above
+ a = A()
+ a.a = a
+ a.y = ContainerNoGC(Z())
+ a.wz = weakref.ref(a.y.value, callback)
+ # create second cycle to keep WZ alive longer
+ wr_cycle = [a.wz]
+ wr_cycle.append(wr_cycle)
+ # ensure trash unrelated to this test is gone
+ gc.collect()
+ gc.disable()
+ # release references and create trash
+ del a, wr_cycle
+ gc.collect()
+ # if called, it means there is a bug in the GC. The weakref should be
+ # cleared before Z dies.
+ callback.assert_not_called()
+ gc.enable()
+
+
class GCCallbackTests(unittest.TestCase):
def setUp(self):
# Save gc state and disable it.
"Class with static methods to test calling conventions"),
};
+/* ContainerNoGC -- a simple container without GC methods */
+
+typedef struct {
+ PyObject_HEAD
+ PyObject *value;
+} ContainerNoGCobject;
+
+static PyObject *
+ContainerNoGC_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
+{
+ PyObject *value;
+ char *names[] = {"value", NULL};
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", names, &value)) {
+ return NULL;
+ }
+ PyObject *self = type->tp_alloc(type, 0);
+ if (self == NULL) {
+ return NULL;
+ }
+ Py_INCREF(value);
+ ((ContainerNoGCobject *)self)->value = value;
+ return self;
+}
+
+static void
+ContainerNoGC_dealloc(ContainerNoGCobject *self)
+{
+ Py_DECREF(self->value);
+ Py_TYPE(self)->tp_free((PyObject *)self);
+}
+
+static PyMemberDef ContainerNoGC_members[] = {
+ {"value", T_OBJECT, offsetof(ContainerNoGCobject, value), READONLY,
+ PyDoc_STR("a container value for test purposes")},
+ {0}
+};
+
+static PyTypeObject ContainerNoGC_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "_testcapi.ContainerNoGC",
+ sizeof(ContainerNoGCobject),
+ .tp_dealloc = (destructor)ContainerNoGC_dealloc,
+ .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+ .tp_members = ContainerNoGC_members,
+ .tp_new = ContainerNoGC_new,
+};
+
static struct PyModuleDef _testcapimodule = {
PyModuleDef_HEAD_INIT,
Py_DECREF(subclass_with_finalizer_bases);
PyModule_AddObject(m, "HeapCTypeSubclassWithFinalizer", HeapCTypeSubclassWithFinalizer);
+ if (PyType_Ready(&ContainerNoGC_type) < 0) {
+ return NULL;
+ }
+ Py_INCREF(&ContainerNoGC_type);
+ if (PyModule_AddObject(m, "ContainerNoGC",
+ (PyObject *) &ContainerNoGC_type) < 0)
+ return NULL;
+
PyState_AddModule(m, &_testcapimodule);
return m;
}