Files
crewAI/lib/crewai/tests/events/test_depends.py
Greyson LaLonde 53b239c6df feat: improve event bus thread safety and async support
Add thread-safe, async-compatible event bus with read–write locking and
handler dependency ordering. Remove blinker dependency and implement
direct dispatch. Improve type safety, error handling, and deterministic
event synchronization.

Refactor tests to auto-wait for async handlers, ensure clean teardown,
and add comprehensive concurrency coverage. Replace thread-local state
in AgentEvaluator with instance-based locking for correct cross-thread
access. Enhance tracing reliability and event finalization.
2025-10-14 13:28:58 -04:00

287 lines
9.1 KiB
Python

"""Tests for FastAPI-style dependency injection in event handlers."""
import asyncio
import pytest
from crewai.events import Depends, crewai_event_bus
from crewai.events.base_events import BaseEvent
class DependsTestEvent(BaseEvent):
"""Test event for dependency tests."""
value: int = 0
type: str = "test_event"
@pytest.mark.asyncio
async def test_basic_dependency():
"""Test that handler with dependency runs after its dependency."""
execution_order = []
with crewai_event_bus.scoped_handlers():
@crewai_event_bus.on(DependsTestEvent)
def setup(source, event: DependsTestEvent):
execution_order.append("setup")
@crewai_event_bus.on(DependsTestEvent, Depends(setup))
def process(source, event: DependsTestEvent):
execution_order.append("process")
event = DependsTestEvent(value=1)
future = crewai_event_bus.emit("test_source", event)
if future:
await asyncio.wrap_future(future)
assert execution_order == ["setup", "process"]
@pytest.mark.asyncio
async def test_multiple_dependencies():
"""Test handler with multiple dependencies."""
execution_order = []
with crewai_event_bus.scoped_handlers():
@crewai_event_bus.on(DependsTestEvent)
def setup_a(source, event: DependsTestEvent):
execution_order.append("setup_a")
@crewai_event_bus.on(DependsTestEvent)
def setup_b(source, event: DependsTestEvent):
execution_order.append("setup_b")
@crewai_event_bus.on(
DependsTestEvent, depends_on=[Depends(setup_a), Depends(setup_b)]
)
def process(source, event: DependsTestEvent):
execution_order.append("process")
event = DependsTestEvent(value=1)
future = crewai_event_bus.emit("test_source", event)
if future:
await asyncio.wrap_future(future)
# setup_a and setup_b can run in any order (same level)
assert "process" in execution_order
assert execution_order.index("process") > execution_order.index("setup_a")
assert execution_order.index("process") > execution_order.index("setup_b")
@pytest.mark.asyncio
async def test_chain_of_dependencies():
"""Test chain of dependencies (A -> B -> C)."""
execution_order = []
with crewai_event_bus.scoped_handlers():
@crewai_event_bus.on(DependsTestEvent)
def handler_a(source, event: DependsTestEvent):
execution_order.append("handler_a")
@crewai_event_bus.on(DependsTestEvent, depends_on=Depends(handler_a))
def handler_b(source, event: DependsTestEvent):
execution_order.append("handler_b")
@crewai_event_bus.on(DependsTestEvent, depends_on=Depends(handler_b))
def handler_c(source, event: DependsTestEvent):
execution_order.append("handler_c")
event = DependsTestEvent(value=1)
future = crewai_event_bus.emit("test_source", event)
if future:
await asyncio.wrap_future(future)
assert execution_order == ["handler_a", "handler_b", "handler_c"]
@pytest.mark.asyncio
async def test_async_handler_with_dependency():
"""Test async handler with dependency on sync handler."""
execution_order = []
with crewai_event_bus.scoped_handlers():
@crewai_event_bus.on(DependsTestEvent)
def sync_setup(source, event: DependsTestEvent):
execution_order.append("sync_setup")
@crewai_event_bus.on(DependsTestEvent, depends_on=Depends(sync_setup))
async def async_process(source, event: DependsTestEvent):
await asyncio.sleep(0.01)
execution_order.append("async_process")
event = DependsTestEvent(value=1)
future = crewai_event_bus.emit("test_source", event)
if future:
await asyncio.wrap_future(future)
assert execution_order == ["sync_setup", "async_process"]
@pytest.mark.asyncio
async def test_mixed_handlers_with_dependencies():
"""Test mix of sync and async handlers with dependencies."""
execution_order = []
with crewai_event_bus.scoped_handlers():
@crewai_event_bus.on(DependsTestEvent)
def setup(source, event: DependsTestEvent):
execution_order.append("setup")
@crewai_event_bus.on(DependsTestEvent, depends_on=Depends(setup))
def sync_process(source, event: DependsTestEvent):
execution_order.append("sync_process")
@crewai_event_bus.on(DependsTestEvent, depends_on=Depends(setup))
async def async_process(source, event: DependsTestEvent):
await asyncio.sleep(0.01)
execution_order.append("async_process")
@crewai_event_bus.on(
DependsTestEvent, depends_on=[Depends(sync_process), Depends(async_process)]
)
def finalize(source, event: DependsTestEvent):
execution_order.append("finalize")
event = DependsTestEvent(value=1)
future = crewai_event_bus.emit("test_source", event)
if future:
await asyncio.wrap_future(future)
# Verify execution order
assert execution_order[0] == "setup"
assert "finalize" in execution_order
assert execution_order.index("finalize") > execution_order.index("sync_process")
assert execution_order.index("finalize") > execution_order.index("async_process")
@pytest.mark.asyncio
async def test_independent_handlers_run_concurrently():
"""Test that handlers without dependencies can run concurrently."""
execution_order = []
with crewai_event_bus.scoped_handlers():
@crewai_event_bus.on(DependsTestEvent)
async def handler_a(source, event: DependsTestEvent):
await asyncio.sleep(0.01)
execution_order.append("handler_a")
@crewai_event_bus.on(DependsTestEvent)
async def handler_b(source, event: DependsTestEvent):
await asyncio.sleep(0.01)
execution_order.append("handler_b")
event = DependsTestEvent(value=1)
future = crewai_event_bus.emit("test_source", event)
if future:
await asyncio.wrap_future(future)
# Both handlers should have executed
assert len(execution_order) == 2
assert "handler_a" in execution_order
assert "handler_b" in execution_order
@pytest.mark.asyncio
async def test_circular_dependency_detection():
"""Test that circular dependencies are detected and raise an error."""
from crewai.events.handler_graph import CircularDependencyError, build_execution_plan
# Create circular dependency: handler_a -> handler_b -> handler_c -> handler_a
def handler_a(source, event: DependsTestEvent):
pass
def handler_b(source, event: DependsTestEvent):
pass
def handler_c(source, event: DependsTestEvent):
pass
# Build a dependency graph with a cycle
handlers = [handler_a, handler_b, handler_c]
dependencies = {
handler_a: [Depends(handler_b)],
handler_b: [Depends(handler_c)],
handler_c: [Depends(handler_a)], # Creates the cycle
}
# Should raise CircularDependencyError about circular dependency
with pytest.raises(CircularDependencyError, match="Circular dependency"):
build_execution_plan(handlers, dependencies)
@pytest.mark.asyncio
async def test_handler_without_dependency_runs_normally():
"""Test that handlers without dependencies still work as before."""
execution_order = []
with crewai_event_bus.scoped_handlers():
@crewai_event_bus.on(DependsTestEvent)
def simple_handler(source, event: DependsTestEvent):
execution_order.append("simple_handler")
event = DependsTestEvent(value=1)
future = crewai_event_bus.emit("test_source", event)
if future:
await asyncio.wrap_future(future)
assert execution_order == ["simple_handler"]
@pytest.mark.asyncio
async def test_depends_equality():
"""Test Depends equality and hashing."""
def handler_a(source, event):
pass
def handler_b(source, event):
pass
dep_a1 = Depends(handler_a)
dep_a2 = Depends(handler_a)
dep_b = Depends(handler_b)
# Same handler should be equal
assert dep_a1 == dep_a2
assert hash(dep_a1) == hash(dep_a2)
# Different handlers should not be equal
assert dep_a1 != dep_b
assert hash(dep_a1) != hash(dep_b)
@pytest.mark.asyncio
async def test_aemit_ignores_dependencies():
"""Test that aemit only processes async handlers (no dependency support yet)."""
execution_order = []
with crewai_event_bus.scoped_handlers():
@crewai_event_bus.on(DependsTestEvent)
def sync_handler(source, event: DependsTestEvent):
execution_order.append("sync_handler")
@crewai_event_bus.on(DependsTestEvent)
async def async_handler(source, event: DependsTestEvent):
execution_order.append("async_handler")
event = DependsTestEvent(value=1)
await crewai_event_bus.aemit("test_source", event)
# Only async handler should execute
assert execution_order == ["async_handler"]