mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-03 06:27:27 +08:00
977 lines
27 KiB
Python
977 lines
27 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)
|
|
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"]) == 4
|
|
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", "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"
|
|
assert facts["producers"][0]["events"] == ["EventType.Alpha"]
|
|
|
|
|
|
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:
|
|
"""统一事实覆盖当前 97 个 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) == 97
|
|
assert sum(not fact["dynamic"] and not fact["invalid"] for fact in producers) == 96
|
|
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) == 98
|
|
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))
|