Files
MoviePilot/tests/test_architecture_event_facts.py

1162 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Unified Event producer and consumer AST collector tests."""
from collections import defaultdict
from pathlib import Path
from scripts.architecture.event_facts import (
collect_event_facts,
fingerprint_event_fact,
)
EVENT_MEMBERS = {
"EventType": ("Alpha", "Beta", "Gamma"),
"ChainEventType": ("Delta", "Epsilon"),
}
def _collect_facts(
tmp_path: Path,
source: str,
*,
module_name: str = "app.sample",
) -> dict[str, list[dict]]:
"""从单个临时宿主模块收集统一 Event 事实。"""
path = tmp_path / f"{module_name.replace('.', '_')}.py"
path.write_text(source, encoding="utf-8")
return collect_event_facts({module_name: path}, EVENT_MEMBERS)
def _collect(
tmp_path: Path,
source: str,
*,
module_name: str = "app.sample",
) -> tuple[dict[str, list[dict]], list[dict]]:
"""从单个临时宿主模块收集 Event consumer。"""
path = tmp_path / f"{module_name.replace('.', '_')}.py"
path.write_text(source, encoding="utf-8")
facts = collect_event_facts({module_name: path}, EVENT_MEMBERS)
static: dict[str, list[dict]] = defaultdict(list)
dynamic: list[dict] = []
for fact in facts["consumers"]:
location = {
"caller": fact["caller"],
"line": fact["line"],
"handler": fact["handler"],
"identity": "|".join(
(
fact["registration_kind"],
fact["handler"],
fact["priority"],
)
),
"priority": fact["priority"],
"registration_kind": fact["registration_kind"],
}
for event in fact["events"]:
static[event].append(dict(location))
if fact["dynamic"]:
dynamic.append(dict(location))
return dict(static), dynamic
def _stable_locations(locations: list[dict]) -> list[dict]:
"""移除仅用于诊断的行号,比较稳定 consumer identity。"""
return [{key: value for key, value in item.items() if key != "line"} for item in locations]
def test_collect_event_consumers_resolves_canonical_instances_and_aliases(
tmp_path: Path,
) -> None:
"""Canonical singleton、构造器、模块和简单赋值别名都应可证明。"""
static, dynamic = _collect(
tmp_path,
"""
from app.runtime.events import EventManager as Bus, eventmanager as global_bus
from app.schemas.types import EventType as Events, ChainEventType as ChainEvents
import app.runtime.events as runtime_events
import app.schemas.types as schema_types
alias = global_bus
constructed = Bus()
existing = Bus.get_existing_instance()
def listener(event):
pass
@alias.register(Events.Alpha, priority=3)
def decorated(event):
pass
@global_bus.register(etype=ChainEvents.Epsilon)
def keyword_decorated(event):
pass
constructed.add_event_listener(
event_type=ChainEvents.Delta,
handler=listener,
priority=7,
)
runtime_events.eventmanager.add_event_listener(
schema_types.EventType.Beta,
listener,
)
existing.add_event_listener(Events.Gamma, listener, 11)
""",
)
assert dynamic == []
assert _stable_locations(static["EventType.Alpha"]) == [
{
"caller": "app.sample",
"handler": "decorated",
"identity": "decorator|decorated|3",
"priority": "3",
"registration_kind": "decorator",
}
]
assert _stable_locations(static["EventType.Beta"]) == [
{
"caller": "app.sample",
"handler": "listener",
"identity": "listener|listener|<default>",
"priority": "<default>",
"registration_kind": "listener",
}
]
assert static["EventType.Gamma"][0]["priority"] == "11"
assert static["ChainEventType.Delta"][0]["priority"] == "7"
assert static["ChainEventType.Epsilon"][0]["handler"] == "keyword_decorated"
def test_collect_event_consumers_only_counts_applied_register_decorators(
tmp_path: Path,
) -> None:
"""register 只有装饰声明或立即应用到 handler 时才形成 consumer。"""
static, dynamic = _collect(
tmp_path,
"""
from app.runtime.events import eventmanager
from app.schemas.types import EventType
unused = eventmanager.register(EventType.Alpha)
used = eventmanager.register(EventType.Beta, priority=4)
@used
def via_alias(event):
pass
def immediate(event):
pass
eventmanager.register(EventType.Gamma, priority=8)(immediate)
""",
)
assert dynamic == []
assert "EventType.Alpha" not in static
assert static["EventType.Beta"][0]["handler"] == "via_alias"
assert static["EventType.Beta"][0]["registration_kind"] == "decorator"
assert static["EventType.Gamma"][0]["handler"] == "immediate"
assert static["EventType.Gamma"][0]["registration_kind"] == "decorator"
def test_collect_event_consumers_expands_enum_and_partial_dynamic_lists(
tmp_path: Path,
) -> None:
"""register 的 enum/list 静态成员和未知成员必须分别保留。"""
static, dynamic = _collect(
tmp_path,
"""
from app.runtime.events import eventmanager
from app.schemas.types import EventType, ChainEventType
runtime_event = EventType("runtime")
selected = [EventType.Alpha, runtime_event, ChainEventType.Delta]
@eventmanager.register(EventType)
def all_broadcast(event):
pass
@eventmanager.register(selected, priority=5)
def selected_events(event):
pass
""",
)
assert set(static) == {
"ChainEventType.Delta",
"EventType.Alpha",
"EventType.Beta",
"EventType.Gamma",
}
assert [item["handler"] for item in static["EventType.Alpha"]] == [
"all_broadcast",
"selected_events",
]
assert _stable_locations(dynamic) == [
{
"caller": "app.sample",
"handler": "selected_events",
"identity": "decorator|selected_events|5",
"priority": "5",
"registration_kind": "decorator",
}
]
def test_collect_event_consumers_ignores_unknown_receivers_and_internal_delegation(
tmp_path: Path,
) -> None:
"""同名 API 和 EventManager 实现内部转调都不是独立 consumer 声明。"""
static, dynamic = _collect(
tmp_path,
"""
from app.schemas.types import EventType
registry.register(EventType.Alpha)
registry.add_event_listener(EventType.Beta, handler)
class EventManager:
def register(self, event):
self.add_event_listener(event, handler)
""",
module_name="app.runtime.events",
)
assert static == {}
assert dynamic == []
def test_collect_event_consumers_respects_shadowing_and_rebinding(
tmp_path: Path,
) -> None:
"""参数遮蔽、局部重绑和模块最终重绑不得伪造 manager provenance。"""
static, dynamic = _collect(
tmp_path,
"""
from app.runtime.events import eventmanager as bus
from app.schemas.types import EventType
@bus.register(EventType.Alpha)
def before_rebind(event):
pass
def listener(event):
pass
def shadow(bus):
bus.add_event_listener(EventType.Beta, listener)
def local_alias():
local = original_bus
local.add_event_listener(EventType.Beta, listener)
local = object()
local.add_event_listener(EventType.Gamma, listener)
def final_global_binding():
bus.add_event_listener(EventType.Gamma, listener)
original_bus = bus
bus = object()
""",
)
assert dynamic == []
assert set(static) == {"EventType.Alpha", "EventType.Beta"}
assert static["EventType.Alpha"][0]["handler"] == "before_rebind"
assert static["EventType.Beta"][0]["handler"] == "listener"
def test_collect_event_consumers_handles_type_checking_runtime_else(
tmp_path: Path,
) -> None:
"""TYPE_CHECKING body 只用于类型else 才是运行期 provenance。"""
static, dynamic = _collect(
tmp_path,
"""
from typing import TYPE_CHECKING
from app.schemas.types import EventType
if TYPE_CHECKING:
from app.runtime.events import eventmanager as type_bus
else:
from app.runtime.events import eventmanager as runtime_bus
def listener(event):
pass
type_bus.add_event_listener(EventType.Alpha, listener)
runtime_bus.add_event_listener(EventType.Beta, listener)
""",
)
assert dynamic == []
assert set(static) == {"EventType.Beta"}
def test_collect_event_consumers_requires_canonical_type_checking_provenance(
tmp_path: Path,
) -> None:
"""只有未遮蔽的 typing.TYPE_CHECKING 才能裁剪运行期分支。"""
static, dynamic = _collect(
tmp_path,
"""
from typing import TYPE_CHECKING as TC
from app.runtime.events import eventmanager
from app.schemas.types import EventType
def type_only(event):
pass
def runtime_only(event):
pass
def shadowed_only(event):
pass
def rebound_only(event):
pass
if TC:
eventmanager.add_event_listener(EventType.Alpha, type_only)
else:
eventmanager.add_event_listener(EventType.Beta, runtime_only)
def shadowed(TC):
if TC:
eventmanager.add_event_listener(EventType.Gamma, shadowed_only)
TYPE_CHECKING = True
if TYPE_CHECKING:
eventmanager.add_event_listener(EventType.Alpha, rebound_only)
""",
)
assert dynamic == []
assert set(static) == {
"EventType.Alpha",
"EventType.Beta",
"EventType.Gamma",
}
assert [item["handler"] for item in static["EventType.Alpha"]] == ["rebound_only"]
assert static["EventType.Beta"][0]["handler"] == "runtime_only"
assert static["EventType.Gamma"][0]["handler"] == "shadowed_only"
def test_collect_event_consumers_respects_compound_scope_targets(
tmp_path: Path,
) -> None:
"""循环、异常和推导式目标必须遮蔽同名 EventManager alias。"""
static, dynamic = _collect(
tmp_path,
"""
from app.runtime.events import eventmanager as bus
from app.schemas.types import EventType
def listener(event):
pass
def scoped(values):
local = bus
for local in values:
local.add_event_listener(EventType.Alpha, listener)
local.add_event_listener(EventType.Beta, listener)
try:
raise RuntimeError
except RuntimeError as bus:
bus.add_event_listener(EventType.Alpha, listener)
[bus.add_event_listener(EventType.Beta, listener) for bus in values]
bus.add_event_listener(EventType.Gamma, listener)
""",
)
assert dynamic == []
assert set(static) == {"EventType.Gamma"}
def test_collect_event_consumers_merges_try_exception_entry_states(
tmp_path: Path,
) -> None:
"""异常 handler 必须从 try 内所有可能抛出点的合并状态开始。"""
static, dynamic = _collect(
tmp_path,
"""
from app.runtime.events import eventmanager
from app.schemas.types import EventType
class Other:
def add_event_listener(self, *args):
pass
def listener(event):
pass
bus = eventmanager
try:
bus = Other()
raise RuntimeError
except RuntimeError:
bus.add_event_listener(EventType.Alpha, listener)
bus = eventmanager
try:
raise RuntimeError
except RuntimeError:
bus.add_event_listener(EventType.Beta, listener)
""",
)
assert dynamic == []
assert set(static) == {"EventType.Beta"}
def test_collect_event_consumers_models_while_with_match_and_delete_bindings(
tmp_path: Path,
) -> None:
"""复合语句目标和 del 必须遵循 Python 的绑定及路径语义。"""
static, dynamic = _collect(
tmp_path,
"""
from contextlib import nullcontext
from app.runtime.events import eventmanager as bus
from app.schemas.types import EventType
global_bus = bus
class Other:
def add_event_listener(self, *args):
pass
def listener(event):
pass
def scoped(context):
bus = Other()
while False:
bus = global_bus
bus.add_event_listener(EventType.Alpha, listener)
bus = global_bus
with nullcontext(Other()) as bus:
bus.add_event_listener(EventType.Alpha, listener)
bus.add_event_listener(EventType.Alpha, listener)
bus = global_bus
match Other():
case bus:
bus.add_event_listener(EventType.Alpha, listener)
bus = global_bus
while context:
bus.add_event_listener(EventType.Beta, listener)
break
with nullcontext():
bus.add_event_listener(EventType.Gamma, listener)
async def async_scoped(context):
bus = global_bus
async with context as bus:
bus.add_event_listener(EventType.Alpha, listener)
def deleted_name_is_local():
global_bus.add_event_listener(EventType.Beta, listener)
bus.add_event_listener(EventType.Alpha, listener)
del bus
""",
)
assert dynamic == []
assert set(static) == {"EventType.Beta", "EventType.Gamma"}
assert len(static["EventType.Beta"]) == 2
def test_collect_event_consumers_writes_walrus_to_containing_scope(
tmp_path: Path,
) -> None:
"""推导式海象绑定属于包含函数,普通外层 alias 仍可读取。"""
static, dynamic = _collect(
tmp_path,
"""
from app.runtime.events import eventmanager as bus
from app.schemas.types import EventType
global_bus = bus
def listener(event):
pass
def rebound(values):
[(bus := object()) for value in values]
bus.add_event_listener(EventType.Alpha, listener)
def retained(values):
bus = global_bus
[bus.add_event_listener(EventType.Beta, listener) for value in values]
def eager_iterable():
[value for value in ((bus := global_bus),)]
bus.add_event_listener(EventType.Gamma, listener)
""",
)
assert dynamic == []
assert set(static) == {"EventType.Beta", "EventType.Gamma"}
def test_collect_event_consumers_uses_late_bound_function_closures(
tmp_path: Path,
) -> None:
"""嵌套函数和 lambda 必须读取包含函数的最终 cell provenance。"""
static, dynamic = _collect(
tmp_path,
"""
from app.runtime.events import eventmanager as global_bus
from app.schemas.types import EventType
def listener(event):
pass
def rebound():
bus = global_bus
def nested():
bus.add_event_listener(EventType.Alpha, listener)
callback = lambda: bus.add_event_listener(EventType.Alpha, listener)
bus = object()
return nested, callback
def retained():
bus = global_bus
def nested():
bus.add_event_listener(EventType.Beta, listener)
callback = lambda: bus.add_event_listener(EventType.Gamma, listener)
return nested, callback
""",
)
assert dynamic == []
assert set(static) == {"EventType.Beta", "EventType.Gamma"}
def test_collect_event_consumers_validates_decorator_factory_application(
tmp_path: Path,
) -> None:
"""保存后的 factory 仅在合法单参数应用时形成 consumer。"""
static, dynamic = _collect(
tmp_path,
"""
from app.runtime.events import eventmanager
from app.schemas.types import EventType
def positional(event):
pass
def keyword(event):
pass
factory = eventmanager.register(EventType.Beta, priority=4)
factory(positional)
eventmanager.register(EventType.Gamma)(f=keyword)
unused = eventmanager.register(EventType.Alpha)
unused()
unused(handler=positional)
unused(positional, keyword)
unused(positional, f=keyword)
""",
)
assert dynamic == []
assert set(static) == {"EventType.Beta", "EventType.Gamma"}
assert static["EventType.Beta"][0]["handler"] == "positional"
assert static["EventType.Gamma"][0]["handler"] == "keyword"
def test_collect_event_consumers_validates_real_event_manager_signatures(
tmp_path: Path,
) -> None:
"""两种注册 API 只接受各自真实且完整的参数绑定。"""
static, dynamic = _collect(
tmp_path,
"""
from app.runtime.events import eventmanager
from app.schemas.types import EventType
def listener(event):
pass
eventmanager.add_event_listener(
event_type=EventType.Alpha,
handler=listener,
priority=3,
)
@eventmanager.register(etype=EventType.Beta, priority=4)
def decorated(event):
pass
eventmanager.add_event_listener(etype=EventType.Gamma, handler=listener)
eventmanager.add_event_listener(EventType.Gamma)
eventmanager.add_event_listener(
EventType.Gamma,
listener,
handler=listener,
)
eventmanager.add_event_listener(EventType.Gamma, listener, unknown=True)
@eventmanager.register(event_type=EventType.Gamma)
def wrong_keyword(event):
pass
@eventmanager.register()
def missing_event(event):
pass
""",
)
assert dynamic == []
assert set(static) == {"EventType.Alpha", "EventType.Beta"}
assert static["EventType.Alpha"][0]["priority"] == "3"
assert static["EventType.Beta"][0]["priority"] == "4"
def test_collect_event_consumers_keeps_only_proven_dynamic_registrations(
tmp_path: Path,
) -> None:
"""只有 receiver 已证明而事件值未知时才进入 dynamic facts。"""
static, dynamic = _collect(
tmp_path,
"""
from app.runtime.events import eventmanager
from app.schemas.types import EventType
def register_runtime(name, handler):
event_type = EventType(name)
eventmanager.add_event_listener(event_type=event_type, handler=handler)
unrelated.add_event_listener(event_type, handler)
unrelated.register(event_type)(handler)
""",
)
assert static == {}
assert len(dynamic) == 1
assert dynamic[0]["caller"] == "app.sample"
assert dynamic[0]["handler"] == "handler"
assert dynamic[0]["registration_kind"] == "listener"
def test_collect_event_consumers_excludes_plugin_modules(tmp_path: Path) -> None:
"""即使调用方误传插件模块collector 也不得读取其注册事实。"""
host_path = tmp_path / "host.py"
plugin_path = tmp_path / "plugin.py"
source = """
from app.runtime.events import eventmanager
from app.schemas.types import EventType
@eventmanager.register(EventType.Alpha)
def handler(event):
pass
"""
host_path.write_text(source, encoding="utf-8")
plugin_path.write_text(source.replace("Alpha", "Beta"), encoding="utf-8")
facts = collect_event_facts(
{
"app.host": host_path,
"app.plugins.sample": plugin_path,
},
EVENT_MEMBERS,
)
static: dict[str, list[dict]] = defaultdict(list)
dynamic: list[dict] = []
for fact in facts["consumers"]:
for event in fact["events"]:
static[event].append(fact)
if fact["dynamic"]:
dynamic.append(fact)
assert dynamic == []
assert set(static) == {"EventType.Alpha"}
def test_collect_event_facts_resolves_producer_receivers_and_bound_aliases(
tmp_path: Path,
) -> None:
"""Producer 仅接受 canonical receiver并保留方法与 qualname。"""
facts = _collect_facts(
tmp_path,
"""
from app.runtime.events import EventManager, eventmanager
from app.schemas.types import ChainEventType, EventType
import app.runtime.events as runtime_events
import app.schemas.types as schema_types
receiver = eventmanager
emit = receiver.send_event
emit(EventType.Alpha)
strict_emit = receiver.send_event_strict
strict_emit(EventType.Beta)
EventManager().send_event(EventType.Gamma)
EventManager.get_existing_instance().send_event(ChainEventType.Delta)
async def publish():
await runtime_events.eventmanager.async_send_event(
etype=schema_types.EventType.Beta,
)
""",
)
assert facts["consumers"] == []
assert len(facts["producers"]) == 5
assert {
(
fact["qualname"],
fact["method"],
fact["receiver_kind"],
tuple(fact["events"]),
)
for fact in facts["producers"]
} == {
("<module>", "send_event", "canonical_singleton", ("EventType.Alpha",)),
(
"<module>",
"send_event_strict",
"canonical_singleton",
("EventType.Beta",),
),
("<module>", "send_event", "constructed_manager", ("EventType.Gamma",)),
("<module>", "send_event", "existing_manager", ("ChainEventType.Delta",)),
("publish", "async_send_event", "canonical_singleton", ("EventType.Beta",)),
}
producer_keys = {
"caller",
"line",
"qualname",
"method",
"receiver_kind",
"events",
"dynamic",
"invalid",
"fingerprint",
}
assert all(set(fact) == producer_keys for fact in facts["producers"])
assert all(fact["fingerprint"] == fingerprint_event_fact(fact) for fact in facts["producers"])
def test_collect_event_facts_scopes_injected_ports_to_owning_classes(
tmp_path: Path,
) -> None:
"""构造注入端口可沿继承使用,但不得证明无关类的同名字段。"""
facts = _collect_facts(
tmp_path,
"""
from typing import Protocol
from app.runtime.events import EventManager as CanonicalEventManager
from app.schemas.types import EventType
class SampleEventPublisher(Protocol):
def send_event(self, etype, data=None, priority=10): ...
class PublisherMixin:
def emit(self):
receiver = self.publisher
bound = receiver.send_event
bound(EventType.Alpha)
class GoodPublisher(PublisherMixin):
def __init__(self, publisher: SampleEventPublisher):
self.publisher = publisher
class SiblingPublisher(PublisherMixin):
def emit_sibling(self):
self.publisher.send_event(EventType.Beta)
class BadPublisher:
def emit(self):
self.publisher.send_event(EventType.Beta)
def free_function(publisher: SampleEventPublisher):
publisher.send_event(EventType.Beta)
class GoodManager:
def __init__(self, manager: CanonicalEventManager):
self.manager = manager
def emit(self):
self.manager.async_send_event(EventType.Gamma)
class EventManager:
pass
class FakeManagerOwner:
def __init__(self, manager: EventManager):
self.manager = manager
def emit(self):
self.manager.send_event(EventType.Beta)
""",
)
assert facts["consumers"] == []
assert [fact["events"] for fact in facts["producers"]] == [
["EventType.Gamma"],
["EventType.Alpha"],
]
assert {fact["receiver_kind"] for fact in facts["producers"]} == {
"injected_event_manager",
"injected_event_publisher",
}
def test_collect_event_facts_separates_static_dynamic_and_invalid_producers(
tmp_path: Path,
) -> None:
"""有限 IfExp 展开,运行期值与伪 enum 成员分别标记。"""
facts = _collect_facts(
tmp_path,
"""
from app.runtime.events import eventmanager
from app.schemas.types import EventType
selected = EventType.Alpha if enabled else EventType.Beta
eventmanager.send_event(selected)
eventmanager.send_event(EventType(name))
eventmanager.send_event(EventType.Missing)
unknown.send_event(EventType.Gamma)
eventmanager.send_event()
eventmanager.send_event(event_type=EventType.Alpha)
eventmanager.send_event(EventType.Alpha, etype=EventType.Beta)
eventmanager.send_event(EventType.Alpha, unknown=True)
""",
)
producers = facts["producers"]
assert len(producers) == 3
assert [(fact["events"], fact["dynamic"], fact["invalid"]) for fact in producers] == [
(["EventType.Alpha", "EventType.Beta"], False, False),
([], True, False),
([], False, True),
]
def test_collect_event_facts_accepts_only_event_manager_internal_producers(
tmp_path: Path,
) -> None:
"""EventManager 内部 self 可生产事件,但注册委托不重复计 consumer。"""
facts = _collect_facts(
tmp_path,
"""
from app.schemas.types import EventType
class EventManager:
def __init__(self):
self.send_event(EventType.Alpha)
def register(self, handler):
self.add_event_listener(EventType.Beta, handler)
""",
module_name="app.runtime.events",
)
assert facts["consumers"] == []
assert len(facts["producers"]) == 1
assert facts["producers"][0]["receiver_kind"] == "event_manager_self"
def test_collect_event_facts_tracks_chain_base_context_event_manager(
tmp_path: Path,
) -> None:
"""ChainBase 迁入 canonical base 模块后仍应识别上下文注入的事件管理器。"""
facts = _collect_facts(
tmp_path,
"""
from app.schemas.types import EventType
class ChainBase:
def __init__(self, context):
self.eventmanager = context.event_manager
def publish(self):
self.eventmanager.send_event(EventType.Alpha)
""",
module_name="app.chain.base",
)
assert len(facts["producers"]) == 1
assert facts["producers"][0]["events"] == ["EventType.Alpha"]
assert facts["producers"][0]["receiver_kind"] == "injected_event_manager"
assert facts["producers"][0]["events"] == ["EventType.Alpha"]
def test_collect_event_facts_tracks_relative_imported_chain_owner(
tmp_path: Path,
) -> None:
"""相对导入的 Chain owner 也应继承 facade 组合得到的事件端口。"""
base_path = tmp_path / "base.py"
owner_path = tmp_path / "owner.py"
facade_path = tmp_path / "facade.py"
base_path.write_text(
"""
class ChainBase:
def __init__(self, context):
self.eventmanager = context.event_manager
""",
encoding="utf-8",
)
owner_path.write_text(
"""
from app.schemas.types import EventType
class SettlementOwner:
def publish(self):
self.eventmanager.send_event(EventType.Alpha)
""",
encoding="utf-8",
)
facade_path.write_text(
"""
from app.chain.base import ChainBase
from .owner import SettlementOwner
class TransferChain(SettlementOwner, ChainBase):
pass
""",
encoding="utf-8",
)
facts = collect_event_facts(
{
"app.chain.base": base_path,
"app.chain.transfer.owner": owner_path,
"app.chain.transfer.facade": facade_path,
},
EVENT_MEMBERS,
)
assert len(facts["producers"]) == 1
producer = facts["producers"][0]
assert producer["caller"] == "app.chain.transfer.owner"
assert producer["qualname"] == "SettlementOwner.publish"
assert producer["events"] == ["EventType.Alpha"]
assert producer["receiver_kind"] == "injected_event_manager"
def test_collect_event_facts_tracks_runtime_type_checking_base_alias(
tmp_path: Path,
) -> None:
"""Owner 经 TYPE_CHECKING 的运行期基类别名仍应继承事件端口。"""
base_path = tmp_path / "base.py"
contract_path = tmp_path / "contract.py"
owner_path = tmp_path / "owner.py"
base_path.write_text(
"""
class ChainBase:
def __init__(self, context):
self.eventmanager = context.event_manager
""",
encoding="utf-8",
)
contract_path.write_text(
"""
from typing import TYPE_CHECKING
from app.chain.base import ChainBase
_RuntimeBase = ChainBase
if TYPE_CHECKING:
class _OwnerBase(_RuntimeBase):
pass
else:
_OwnerBase = _RuntimeBase
""",
encoding="utf-8",
)
owner_path.write_text(
"""
from app.schemas.types import EventType
from .contract import _OwnerBase
class SettlementOwner(_OwnerBase):
def publish(self):
self.eventmanager.send_event(EventType.Alpha)
""",
encoding="utf-8",
)
facts = collect_event_facts(
{
"app.chain.base": base_path,
"app.chain.sample.contract": contract_path,
"app.chain.sample.owner": owner_path,
},
EVENT_MEMBERS,
)
assert len(facts["producers"]) == 1
producer = facts["producers"][0]
assert producer["caller"] == "app.chain.sample.owner"
assert producer["qualname"] == "SettlementOwner.publish"
assert producer["events"] == ["EventType.Alpha"]
assert producer["receiver_kind"] == "injected_event_manager"
def test_collect_event_facts_rejects_shadowed_type_checking_base_alias(
tmp_path: Path,
) -> None:
"""被重绑定的 TYPE_CHECKING 分支不能作为静态继承证据。"""
base_path = tmp_path / "base.py"
contract_path = tmp_path / "contract.py"
owner_path = tmp_path / "owner.py"
base_path.write_text(
"""
class ChainBase:
def __init__(self, context):
self.eventmanager = context.event_manager
""",
encoding="utf-8",
)
contract_path.write_text(
"""
from typing import TYPE_CHECKING
from app.chain.base import ChainBase
TYPE_CHECKING = True
if TYPE_CHECKING:
_OwnerBase = ChainBase
else:
_OwnerBase = object
""",
encoding="utf-8",
)
owner_path.write_text(
"""
from app.schemas.types import EventType
from .contract import _OwnerBase
class SettlementOwner(_OwnerBase):
def publish(self):
self.eventmanager.send_event(EventType.Alpha)
""",
encoding="utf-8",
)
facts = collect_event_facts(
{
"app.chain.base": base_path,
"app.chain.sample.contract": contract_path,
"app.chain.sample.owner": owner_path,
},
EVENT_MEMBERS,
)
assert facts["producers"] == []
def test_collect_event_facts_consumer_schema_and_fingerprint_are_stable(
tmp_path: Path,
) -> None:
"""Consumer 使用完整字段名,摘要排除 line 且覆盖全部治理字段。"""
source = """
from app.runtime.events import eventmanager
from app.schemas.types import EventType
@eventmanager.register(EventType.Alpha, priority=3)
def handler(event):
pass
"""
fact = _collect_facts(tmp_path, source)["consumers"][0]
assert set(fact) == {
"caller",
"line",
"qualname",
"method",
"receiver_kind",
"events",
"dynamic",
"invalid",
"fingerprint",
"handler",
"priority",
"registration_kind",
}
assert fact["registration_kind"] == "decorator"
moved = dict(fact, line=fact["line"] + 100, fingerprint="stale")
assert fingerprint_event_fact(moved) == fact["fingerprint"]
for key, value in {
"caller": "app.changed",
"qualname": "Changed.handler",
"method": "add_event_listener",
"receiver_kind": "constructed_manager",
"events": ["EventType.Beta"],
"dynamic": True,
"invalid": True,
"handler": "other",
"priority": "4",
"registration_kind": "listener",
}.items():
changed = dict(fact, **{key: value})
assert fingerprint_event_fact(changed) != fact["fingerprint"]
def test_collect_event_facts_excludes_all_plugin_facts(tmp_path: Path) -> None:
"""误传插件路径时 producer 与 consumer 都不得进入宿主事实。"""
host_path = tmp_path / "host_facts.py"
plugin_path = tmp_path / "plugin_facts.py"
source = """
from app.runtime.events import eventmanager
from app.schemas.types import EventType
eventmanager.send_event(EventType.Alpha)
@eventmanager.register(EventType.Beta)
def handler(event):
pass
"""
host_path.write_text(source, encoding="utf-8")
plugin_path.write_text(source.replace("Alpha", "Gamma"), encoding="utf-8")
facts = collect_event_facts(
{
"app.host": host_path,
"app.plugins.sample": plugin_path,
},
EVENT_MEMBERS,
)
assert len(facts["producers"]) == 1
assert facts["producers"][0]["caller"] == "app.host"
assert len(facts["consumers"]) == 1
assert facts["consumers"][0]["caller"] == "app.host"
def test_collect_event_facts_matches_current_host_inventory() -> None:
"""统一事实覆盖当前 86 个 producer 调用与 17 个 consumer 调用。"""
from scripts.architecture.baseline import (
_event_enum_members,
discover_modules,
)
facts = collect_event_facts(
discover_modules(),
{enum_name: _event_enum_members(enum_name) for enum_name in ("EventType", "ChainEventType")},
)
producers = facts["producers"]
consumers = facts["consumers"]
assert len(producers) == 86
assert sum(not fact["dynamic"] and not fact["invalid"] for fact in producers) == 85
assert sum(fact["dynamic"] for fact in producers) == 1
assert sum(fact["invalid"] for fact in producers) == 0
assert sum(len(fact["events"]) for fact in producers) == 87
assert len(consumers) == 17
assert sum(not fact["dynamic"] and not fact["invalid"] for fact in consumers) == 16
assert sum(fact["dynamic"] for fact in consumers) == 1
assert sum(fact["invalid"] for fact in consumers) == 0
assert sum(len(fact["events"]) for fact in consumers) == 16
consumer_fingerprints = [fact["fingerprint"] for fact in consumers]
assert len(consumer_fingerprints) == len(set(consumer_fingerprints))