127 lines
4.9 KiB
Python
127 lines
4.9 KiB
Python
"""Rc/Arc 指针身份一致性测试 Mixin。
|
||
|
||
验证:同一个 Rust Rc<T>/Arc<T> 无论通过哪条路径到达 Python,
|
||
始终返回相同的 PyObject(`a is b` 为 True)。
|
||
|
||
用法::
|
||
|
||
class TestMyLib(RcIdentityMixin, unittest.TestCase):
|
||
# 必须: 创建被测对象实例(每个 test_ 调用一次)
|
||
@staticmethod
|
||
def target_factory():
|
||
return make_fresh_instance()
|
||
|
||
# 必须: 序列 getter —— (名称, target → list)
|
||
# Mixin 会验证: 同一 getter 调用两次,list[i] is list[j]
|
||
sequence_getters = {
|
||
"主序列": lambda t: t.items,
|
||
"子序列": lambda t: t.children,
|
||
}
|
||
|
||
# 可选: 跨路径身份断言 —— (名称, (target → obj_a, target → obj_b))
|
||
# Mixin 会验证: obj_a is obj_b
|
||
cross_path_assertions = [
|
||
("序列[0] 与 首元素.父", lambda t: t.items[0], lambda t: t.items[0].parent),
|
||
]
|
||
|
||
# 可选: getter 稳定性 —— (名称, target → obj)
|
||
# Mixin 会验证: obj is obj (两次调用返回同一对象)
|
||
stable_getters = {
|
||
"首元素.属性": lambda t: t.items[0].attr,
|
||
}
|
||
|
||
# 可选: 序列长度检查的最小值(默认不检查,设为 >0 开启)
|
||
min_sequence_lengths = {
|
||
"主序列": 3,
|
||
"子序列": 2,
|
||
}
|
||
"""
|
||
|
||
import unittest
|
||
|
||
|
||
class RcIdentityMixin:
|
||
"""Rc/Arc 指针身份一致性测试 Mixin。
|
||
|
||
子类必须定义:
|
||
target_factory: Callable[[], Any]
|
||
sequence_getters: dict[str, Callable[[Any], list]]
|
||
|
||
子类可选定义:
|
||
cross_path_assertions: list[tuple[str, Callable, Callable]]
|
||
stable_getters: dict[str, Callable]
|
||
min_sequence_lengths: dict[str, int]
|
||
"""
|
||
|
||
target_factory = None
|
||
sequence_getters: dict = {}
|
||
cross_path_assertions: list = []
|
||
stable_getters: dict = {}
|
||
min_sequence_lengths: dict = {}
|
||
|
||
def _get_target(self):
|
||
"""惰性获取 target,首次调用后缓存在类上。避免 setUpClass MRO 冲突."""
|
||
cls = type(self)
|
||
# 每次测试重新创建——但这会太慢。用类级别缓存。
|
||
# 子类应在 setUpClass 中调用 self._get_target() 或自己设置 cls._cached_target。
|
||
if not hasattr(cls, "_cached_target"):
|
||
if cls.target_factory is None:
|
||
raise unittest.SkipTest(f"{cls.__name__} 未定义 target_factory")
|
||
cls._cached_target = cls.target_factory()
|
||
return cls._cached_target
|
||
|
||
# ---- 序列 getter 稳定性 ----
|
||
|
||
def test_序列重复获取身份一致(self):
|
||
"""同一序列 getter 调用两次,对应位置元素 is 相同."""
|
||
t = self._get_target()
|
||
for name, getter in self.sequence_getters.items():
|
||
seq1 = getter(t)
|
||
seq2 = getter(t)
|
||
self.assertEqual(len(seq1), len(seq2), f"{name}: 两次获取长度不同")
|
||
check_n = min(len(seq1), 10)
|
||
for i in range(check_n):
|
||
self.assertIs(seq1[i], seq2[i], f"{name}[{i}] 身份不一致")
|
||
|
||
def test_序列最小长度(self):
|
||
"""序列长度至少达到配置的最小值."""
|
||
t = self._get_target()
|
||
for name, getter in self.sequence_getters.items():
|
||
if name in self.min_sequence_lengths:
|
||
min_len = self.min_sequence_lengths[name]
|
||
actual = len(getter(t))
|
||
self.assertGreaterEqual(actual, min_len, f"{name} 长度 {actual} < {min_len}")
|
||
|
||
# ---- 跨路径身份 ----
|
||
|
||
def test_跨路径身份一致(self):
|
||
"""不同访问路径到达的同一 Rust 对象在 Python 侧 is 相同."""
|
||
t = self._get_target()
|
||
for i, (label, path_a, path_b) in enumerate(self.cross_path_assertions):
|
||
obj_a = path_a(t)
|
||
obj_b = path_b(t)
|
||
self.assertIsNotNone(obj_a, f"[{i}] {label}: path_a 返回 None")
|
||
self.assertIsNotNone(obj_b, f"[{i}] {label}: path_b 返回 None")
|
||
self.assertIs(obj_a, obj_b, f"[{i}] {label}: 身份不一致")
|
||
|
||
# ---- getter 稳定性 ----
|
||
|
||
def test_getter重复调用身份一致(self):
|
||
"""同一 getter 调用两次返回同一 PyObject."""
|
||
t = self._get_target()
|
||
for name, getter in self.stable_getters.items():
|
||
obj1 = getter(t)
|
||
obj2 = getter(t)
|
||
self.assertIs(obj1, obj2, f"{name}: 两次调用返回不同对象")
|
||
|
||
# ---- list.index 基于 is ----
|
||
|
||
def test_list_index_基于身份(self):
|
||
"""list.index(elem) 正常工作(依赖 __eq__ 基于 is 比较)."""
|
||
t = self._get_target()
|
||
for name, getter in self.sequence_getters.items():
|
||
seq = getter(t)
|
||
if len(seq) >= 2:
|
||
self.assertEqual(seq.index(seq[0]), 0, f"{name}: index(seq[0]) != 0")
|
||
self.assertEqual(seq.index(seq[-1]), len(seq) - 1, f"{name}: index(seq[-1]) != {len(seq) - 1}")
|