mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/jxxghp/MoviePilot.git
synced 2026-09-20 08:03:34 +08:00
1162 lines
32 KiB
Python
1162 lines
32 KiB
Python
"""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))
|