""" Smoke test for the async event bus. """ import asyncio import pytest from app.core.bus import EventBus from app.domain.events import TradeEvent, HeartbeatEvent, EventType class MockConsumer: """Test consumer that collects events.""" def __init__(self): self.events: list = [] async def handle(self, event): self.events.append(event) class FailingConsumer: """Consumer that always raises.""" async def handle(self, event): raise ValueError("I always fail") @pytest.mark.asyncio async def test_bus_fanout(): """Events are delivered to all consumers.""" bus = EventBus() c1 = MockConsumer() c2 = MockConsumer() bus.add_consumer(c1) bus.add_consumer(c2) await bus.start() event = TradeEvent( provider="test", symbol="BTCUSDT", price=42000.0, size=1.5, ) await bus.publish(event) # Give worker time to process await asyncio.sleep(0.1) await bus.stop() assert len(c1.events) == 1 assert len(c2.events) == 1 assert c1.events[0].symbol == "BTCUSDT" assert c2.events[0].price == 42000.0 @pytest.mark.asyncio async def test_bus_failing_consumer_doesnt_block(): """A failing consumer doesn't prevent others from receiving events.""" bus = EventBus() good = MockConsumer() bad = FailingConsumer() bus.add_consumer(bad) bus.add_consumer(good) await bus.start() await bus.publish(HeartbeatEvent(provider="test")) await asyncio.sleep(0.1) await bus.stop() assert len(good.events) == 1 assert good.events[0].event_type == EventType.HEARTBEAT @pytest.mark.asyncio async def test_bus_multiple_events(): """Multiple events are delivered in order.""" bus = EventBus() consumer = MockConsumer() bus.add_consumer(consumer) await bus.start() for i in range(10): await bus.publish( TradeEvent( provider="test", symbol=f"SYM{i}", price=float(i), size=1.0, ) ) await asyncio.sleep(0.2) await bus.stop() assert len(consumer.events) == 10 symbols = [e.symbol for e in consumer.events] assert symbols == [f"SYM{i}" for i in range(10)] @pytest.mark.asyncio async def test_bus_queue_overflow(): """Bus handles queue overflow without crashing.""" bus = EventBus(queue_size=3) consumer = MockConsumer() bus.add_consumer(consumer) # Don't start worker — queue will fill up for i in range(10): await bus.publish( HeartbeatEvent(provider="test") ) # Should not raise await bus.start() await asyncio.sleep(0.1) await bus.stop() # Some events were dropped, but consumer got the ones that fit assert len(consumer.events) >= 1 @pytest.mark.asyncio async def test_bus_backpressure_drops_quotes_before_trades(): """Under backpressure, quotes are dropped but trades survive.""" from app.domain.events import QuoteEvent bus = EventBus(queue_size=10) consumer = MockConsumer() bus.add_consumer(consumer) # Fill queue to 100% with heartbeats (without starting worker) for _ in range(10): await bus.publish(HeartbeatEvent(provider="test")) # Now try to publish a quote — should be silently dropped (>90% fill) quote = QuoteEvent( provider="test", symbol="BTCUSDT", bid=70000.0, ask=70001.0, bid_size=1.0, ask_size=1.0, ) await bus.publish(quote) # Start worker, drain existing events await bus.start() await asyncio.sleep(0.1) await bus.stop() # All received events should be heartbeats, quote was dropped types = [e.event_type for e in consumer.events] # The queue was full so older events get replaced; quote should NOT be there assert EventType.TRADE not in types # no trades published # Verify no quotes survived (they are low-priority under pressure) # Note: with queue_size=10 and 10 heartbeats, queue was 100% full # Quote at fill=100% with priority=1 gets dropped @pytest.mark.asyncio async def test_bus_heartbeat_monitor_emits_on_silence(): """Heartbeat monitor fires when a provider goes silent.""" bus = EventBus(queue_size=100, heartbeat_interval=0.3) consumer = MockConsumer() bus.add_consumer(consumer) bus.register_provider("test_provider") await bus.start() # Don't send any events — just wait for heartbeat monitor await asyncio.sleep(0.8) await bus.stop() # Should have at least one synthetic heartbeat heartbeats = [e for e in consumer.events if e.event_type == EventType.HEARTBEAT] assert len(heartbeats) >= 1 assert heartbeats[0].provider == "test_provider" @pytest.mark.asyncio async def test_bus_fill_percent(): """fill_percent property works correctly.""" bus = EventBus(queue_size=100) assert bus.fill_percent == 0.0 for _ in range(50): await bus.publish(HeartbeatEvent(provider="test")) assert 0.49 <= bus.fill_percent <= 0.51