Files
MoviePilot/tests/test_architecture_event_facts.py
T

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))