odoo_mqtt/iot_bridge/tests/unit/test_event_queue.py
2026-02-10 20:00:27 +01:00

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'])