259 lines
8.0 KiB
Python
259 lines
8.0 KiB
Python
"""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'])
|