"""Unit tests for Event Queue with Retry Logic.""" import time from datetime import datetime, timedelta from unittest.mock import Mock import pytest from core.event_queue import EventQueue, QueuedEvent class TestQueuedEvent: """Tests for QueuedEvent class.""" def test_should_retry_initial(self): """Test that new event should retry immediately.""" event = QueuedEvent( event_uid="test-uid", event_type="session_started", device_id="device-01", payload={}, created_at=datetime.utcnow(), ) assert event.should_retry(datetime.utcnow()) is True assert event.retry_count == 0 def test_should_retry_after_max(self): """Test that event won't retry after max retries.""" event = QueuedEvent( event_uid="test-uid", event_type="session_started", device_id="device-01", payload={}, created_at=datetime.utcnow(), retry_count=10, max_retries=10, ) assert event.should_retry(datetime.utcnow()) is False def test_should_retry_before_next_time(self): """Test that event won't retry before next_retry_time.""" event = QueuedEvent( event_uid="test-uid", event_type="session_started", device_id="device-01", payload={}, created_at=datetime.utcnow(), retry_count=1, next_retry_time=datetime.utcnow() + timedelta(seconds=10), ) assert event.should_retry(datetime.utcnow()) is False def test_should_retry_after_next_time(self): """Test that event will retry after next_retry_time.""" event = QueuedEvent( event_uid="test-uid", event_type="session_started", device_id="device-01", payload={}, created_at=datetime.utcnow(), retry_count=1, next_retry_time=datetime.utcnow() - timedelta(seconds=1), ) assert event.should_retry(datetime.utcnow()) is True def test_exponential_backoff(self): """Test exponential backoff calculation.""" event = QueuedEvent( event_uid="test-uid", event_type="session_started", device_id="device-01", payload={}, created_at=datetime.utcnow(), ) # First retry: 2^0 = 1s event.increment_retry() assert event.retry_count == 1 assert event.next_retry_time is not None delay1 = (event.next_retry_time - datetime.utcnow()).total_seconds() assert 1.9 <= delay1 <= 2.1 # 2^1 = 2s # Second retry: 2^2 = 4s event.increment_retry() assert event.retry_count == 2 assert event.next_retry_time is not None delay2 = (event.next_retry_time - datetime.utcnow()).total_seconds() assert 3.9 <= delay2 <= 4.1 # 2^2 = 4s # Third retry: 2^3 = 8s event.increment_retry() assert event.retry_count == 3 assert event.next_retry_time is not None delay3 = (event.next_retry_time - datetime.utcnow()).total_seconds() assert 7.9 <= delay3 <= 8.1 # 2^3 = 8s def test_max_backoff_cap(self): """Test that backoff is capped at 60s.""" event = QueuedEvent( event_uid="test-uid", event_type="session_started", device_id="device-01", payload={}, created_at=datetime.utcnow(), retry_count=10, # 2^10 = 1024s, but should be capped ) event.increment_retry() assert event.next_retry_time is not None delay = (event.next_retry_time - datetime.utcnow()).total_seconds() assert 59.9 <= delay <= 60.1 class TestEventQueue: """Tests for EventQueue class.""" def test_enqueue(self): """Test enqueuing events.""" send_mock = Mock(return_value=True) queue = EventQueue(send_callback=send_mock) event = { "event_type": "session_started", "device_id": "device-01", "session_id": "session-123", } uid = queue.enqueue(event) assert uid is not None assert queue.get_queue_size() == 1 assert queue.stats["pending_count"] == 1 def test_enqueue_preserves_uid(self): """Test that existing event_uid is preserved.""" send_mock = Mock(return_value=True) queue = EventQueue(send_callback=send_mock) event = { "event_uid": "my-custom-uid", "event_type": "session_started", "device_id": "device-01", } uid = queue.enqueue(event) assert uid == "my-custom-uid" def test_successful_send(self): """Test successful event sending.""" send_mock = Mock(return_value=True) queue = EventQueue(send_callback=send_mock) queue.start() event = {"event_type": "session_started", "device_id": "device-01"} queue.enqueue(event) # Wait for processing time.sleep(0.3) queue.stop() # Verify send was called assert send_mock.call_count == 1 assert queue.stats["sent_count"] == 1 assert queue.stats["pending_count"] == 0 def test_failed_send_with_retry(self): """Test that failed sends are retried.""" # Fail first 2 times, succeed on 3rd send_mock = Mock(side_effect=[False, False, True]) queue = EventQueue(send_callback=send_mock) queue.start() event = {"event_type": "session_started", "device_id": "device-01"} queue.enqueue(event) # Wait for initial send + 2 retries (initial + 2s + 4s = 7s) time.sleep(8) queue.stop() # Verify retries happened assert send_mock.call_count == 3 assert queue.stats["sent_count"] == 1 assert queue.stats["retry_count"] == 2 assert queue.stats["pending_count"] == 0 def test_max_retries_exceeded(self): """Test that events are marked failed after max retries.""" send_mock = Mock(return_value=False) # Always fail queue = EventQueue(send_callback=send_mock) queue.start() event = {"event_type": "session_started", "device_id": "device-01"} queue.enqueue(event) # Wait for first 5 retries (2s + 4s + 8s + 16s + 32s = 62s) # For faster tests, just verify retry behavior with 4 attempts time.sleep(32) # Enough for 4-5 retry attempts queue.stop() # Should have attempted at least 5 times assert send_mock.call_count >= 5 assert queue.stats["retry_count"] >= 4 def test_queue_statistics(self): """Test queue statistics tracking.""" send_mock = Mock(side_effect=[True, False, True]) queue = EventQueue(send_callback=send_mock) queue.start() # Enqueue 3 events for i in range(3): queue.enqueue({"event_type": "session_heartbeat", "device_id": f"device-{i:02d}"}) time.sleep(3) queue.stop() stats = queue.get_stats() assert stats["sent_count"] >= 2 # At least 2 succeeded assert stats["retry_count"] >= 1 # At least 1 retry def test_queue_stop_waits_for_completion(self): """Test that stop() waits for queue processing to finish.""" send_mock = Mock(return_value=True) queue = EventQueue(send_callback=send_mock) queue.start() # Enqueue event queue.enqueue({"event_type": "session_started", "device_id": "device-01"}) # Stop immediately queue.stop() # Verify event was processed before stop completed assert send_mock.call_count >= 0 # May or may not have processed assert not queue.running if __name__ == "__main__": pytest.main([__file__, "-v"])