File: test_potentially_slicing_weak_ptr.py

package info (click to toggle)
pybind11 3.0.1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 4,448 kB
  • sloc: cpp: 27,239; python: 13,512; ansic: 4,244; makefile: 204; sh: 36
file content (174 lines) | stat: -rw-r--r-- 5,590 bytes parent folder | download
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# Copyright (c) 2025 The pybind Community.
# All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.

# This module tests the interaction of pybind11's shared_ptr and smart_holder
# mechanisms with trampoline object lifetime management and inheritance slicing.
#
# The following combinations are covered:
#
# - Holder type: std::shared_ptr (class_ holder) vs.
#                py::smart_holder
# - Conversion function: obj.cast<std::shared_ptr<T>>() vs.
#                        py::potentially_slicing_weak_ptr<T>(obj)
# - Python object type: C++ base class vs.
#                       Python-derived trampoline class
#
# The tests verify
#
# - that casting or passing Python objects into functions returns usable
#   std::shared_ptr<T> instances.
# - that inheritance slicing occurs as expected in controlled cases
#   (issue #1333).
# - that surprising weak_ptr behavior (issue #5623) can be reproduced when
#   smart_holder is used.
# - that the trampoline object remains alive in all situations
#   (no use-after-free) as long as the C++ shared_ptr exists.
#
# Where applicable, trampoline state is introspected to confirm whether the
# C++ object retains knowledge of the Python override or has fallen back to
# the base implementation.

from __future__ import annotations

import gc
import weakref

import pytest

import env
import pybind11_tests.potentially_slicing_weak_ptr as m


class PyDrvdSH(m.VirtBaseSH):
    def get_code(self):
        return 200


class PyDrvdSP(m.VirtBaseSP):
    def get_code(self):
        return 200


VIRT_BASE_TYPES = {
    "SH": {100: m.VirtBaseSH, 200: PyDrvdSH},
    "SP": {100: m.VirtBaseSP, 200: PyDrvdSP},
}

RTRN_FUNCS = {
    "SH": {
        "oc": m.SH_rtrn_obj_cast_shared_ptr,
        "ps": m.SH_rtrn_potentially_slicing_shared_ptr,
    },
    "SP": {
        "oc": m.SP_rtrn_obj_cast_shared_ptr,
        "ps": m.SP_rtrn_potentially_slicing_shared_ptr,
    },
}

SP_OWNER_TYPES = {
    "SH": m.SH_SpOwner,
    "SP": m.SP_SpOwner,
}

WP_OWNER_TYPES = {
    "SH": m.SH_WpOwner,
    "SP": m.SP_WpOwner,
}

GC_IS_RELIABLE = not (env.PYPY or env.GRAALPY)


@pytest.mark.parametrize("expected_code", [100, 200])
@pytest.mark.parametrize("rtrn_kind", ["oc", "ps"])
@pytest.mark.parametrize("holder_kind", ["SH", "SP"])
def test_rtrn_obj_cast_shared_ptr(holder_kind, rtrn_kind, expected_code):
    obj = VIRT_BASE_TYPES[holder_kind][expected_code]()
    ptr = RTRN_FUNCS[holder_kind][rtrn_kind](obj)
    assert ptr.get_code() == expected_code
    objref = weakref.ref(obj)
    del obj
    gc.collect()
    assert ptr.get_code() == expected_code  # the ptr Python object keeps obj alive
    assert objref() is not None
    del ptr
    gc.collect()
    if GC_IS_RELIABLE:
        assert objref() is None


@pytest.mark.parametrize("expected_code", [100, 200])
@pytest.mark.parametrize("holder_kind", ["SH", "SP"])
def test_with_sp_owner(holder_kind, expected_code):
    spo = SP_OWNER_TYPES[holder_kind]()
    assert spo.get_code() == -888
    assert spo.get_trampoline_state() == "sp nullptr"

    obj = VIRT_BASE_TYPES[holder_kind][expected_code]()
    assert obj.get_code() == expected_code

    spo.set_sp(obj)
    assert spo.get_code() == expected_code
    expected_trampoline_state = (
        "dynamic_cast failed" if expected_code == 100 else "trampoline alive"
    )
    assert spo.get_trampoline_state() == expected_trampoline_state

    del obj
    gc.collect()
    if holder_kind == "SH":
        assert spo.get_code() == expected_code
    elif GC_IS_RELIABLE:
        assert (
            spo.get_code() == 100
        )  # see issue #1333 (inheritance slicing) and PR #5624
    assert spo.get_trampoline_state() == expected_trampoline_state


@pytest.mark.parametrize("expected_code", [100, 200])
@pytest.mark.parametrize("set_meth", ["set_wp", "set_wp_potentially_slicing"])
@pytest.mark.parametrize("holder_kind", ["SH", "SP"])
def test_with_wp_owner(holder_kind, set_meth, expected_code):
    wpo = WP_OWNER_TYPES[holder_kind]()
    assert wpo.get_code() == -999
    assert wpo.get_trampoline_state() == "sp nullptr"

    obj = VIRT_BASE_TYPES[holder_kind][expected_code]()
    assert obj.get_code() == expected_code

    getattr(wpo, set_meth)(obj)
    if (
        holder_kind == "SP"
        or expected_code == 100
        or set_meth == "set_wp_potentially_slicing"
    ):
        assert wpo.get_code() == expected_code
    else:
        assert wpo.get_code() == -999  # see issue #5623 (weak_ptr expired) and PR #5624
    if expected_code == 100:
        expected_trampoline_state = "dynamic_cast failed"
    elif holder_kind == "SH" and set_meth == "set_wp":
        expected_trampoline_state = "sp nullptr"
    else:
        expected_trampoline_state = "trampoline alive"
    assert wpo.get_trampoline_state() == expected_trampoline_state

    del obj
    gc.collect()
    if GC_IS_RELIABLE:
        assert wpo.get_code() == -999


def test_potentially_slicing_weak_ptr_not_convertible_error():
    with pytest.raises(Exception) as excinfo:
        m.SH_rtrn_potentially_slicing_shared_ptr("")
    assert str(excinfo.value) == (
        '"str" object is not convertible to std::weak_ptr<T>'
        " (with T = pybind11_tests::potentially_slicing_weak_ptr::VirtBase<0>)"
    )
    with pytest.raises(Exception) as excinfo:
        m.SP_rtrn_potentially_slicing_shared_ptr([])
    assert str(excinfo.value) == (
        '"list" object is not convertible to std::weak_ptr<T>'
        " (with T = pybind11_tests::potentially_slicing_weak_ptr::VirtBase<1>)"
    )