Files
HBlink4/tests/test_user_cache.py
cort 80d7392411 Add user routing cache with Last Heard dashboard
Features:
- User cache tracks radio IDs and last heard repeater for routing optimization
- 10-minute timeout (configurable via user_cache.timeout in config)
- Automatic cleanup every 60s to prevent memory growth
- Dashboard Last Heard display (top 10 users)
- View Full Cache modal showing up to 50 users
- Only displays talker alias when available (shows '-' otherwise)
- Periodic broadcast to dashboard every 10s

Logging improvements:
- Commented out per-packet validation debug logs
- Reduced dashboard logging (disabled access logs, WebSocket logs to DEBUG)
- All high-frequency debug logs marked with 'Per-packet logging' for easy re-enabling

Tests:
- Comprehensive test suite for user cache functionality
- All tests passing
2025-10-02 19:42:25 -05:00

194 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""
Test user cache functionality
"""
import pytest
from time import time, sleep
from hblink4.user_cache import UserCache, UserEntry
def test_user_cache_basic():
"""Test basic add and lookup"""
cache = UserCache(timeout_seconds=600)
# Add user
cache.update(
radio_id=3106000,
repeater_id=312345,
callsign="N0MJS",
slot=2,
talkgroup=3100
)
# Lookup user
entry = cache.lookup(3106000)
assert entry is not None
assert entry.radio_id == 3106000
assert entry.repeater_id == 312345
assert entry.callsign == "N0MJS"
assert entry.slot == 2
assert entry.talkgroup == 3100
def test_user_cache_update():
"""Test updating existing user"""
cache = UserCache(timeout_seconds=600)
# Add user
cache.update(3106000, 312345, "N0MJS", 2, 3100)
# Update with new repeater
cache.update(3106000, 312346, "N0MJS", 1, 3101, talker_alias="Cort")
# Verify update
entry = cache.lookup(3106000)
assert entry.repeater_id == 312346
assert entry.slot == 1
assert entry.talkgroup == 3101
assert entry.talker_alias == "Cort"
def test_user_cache_expiration():
"""Test that entries expire"""
cache = UserCache(timeout_seconds=1) # 1 second timeout
# Add user
cache.update(3106000, 312345, "N0MJS", 2, 3100)
# Should find it immediately
entry = cache.lookup(3106000)
assert entry is not None
# Wait for expiration
sleep(1.1)
# Should be expired now
entry = cache.lookup(3106000)
assert entry is None
def test_user_cache_cleanup():
"""Test cleanup removes expired entries"""
cache = UserCache(timeout_seconds=1)
# Add multiple users
for i in range(10):
cache.update(3106000 + i, 312345, f"USER{i}", 2, 3100)
# All should be present
assert len(cache._cache) == 10
# Wait for expiration
sleep(1.1)
# Cleanup should remove all
removed = cache.cleanup()
assert removed == 10
assert len(cache._cache) == 0
def test_user_cache_last_heard():
"""Test get_last_heard returns sorted list"""
cache = UserCache(timeout_seconds=600)
# Add users with slight delays
cache.update(3106001, 312345, "USER1", 2, 3100)
sleep(0.01)
cache.update(3106002, 312345, "USER2", 2, 3100)
sleep(0.01)
cache.update(3106003, 312345, "USER3", 2, 3100)
# Get last heard
last_heard = cache.get_last_heard(limit=10)
assert len(last_heard) == 3
# Should be sorted by most recent first
assert last_heard[0]['radio_id'] == 3106003
assert last_heard[1]['radio_id'] == 3106002
assert last_heard[2]['radio_id'] == 3106001
def test_user_cache_limit():
"""Test that last_heard respects limit"""
cache = UserCache(timeout_seconds=600)
# Add 100 users
for i in range(100):
cache.update(3106000 + i, 312345, f"USER{i}", 2, 3100)
# Request only 20
last_heard = cache.get_last_heard(limit=20)
assert len(last_heard) == 20
def test_user_cache_private_call_routing():
"""Test using cache for private call routing"""
cache = UserCache(timeout_seconds=600)
# User heard on repeater 312345
cache.update(3106000, 312345, "N0MJS", 2, 3100)
# Later, we want to route a private call to this user
repeater_id = cache.get_repeater_for_user(3106000)
assert repeater_id == 312345
# Unknown user should return None
repeater_id = cache.get_repeater_for_user(9999999)
assert repeater_id is None
def test_user_cache_stats():
"""Test cache statistics"""
cache = UserCache(timeout_seconds=1)
# Add users
for i in range(5):
cache.update(3106000 + i, 312345, f"USER{i}", 2, 3100)
stats = cache.get_stats()
assert stats['total_entries'] == 5
assert stats['valid_entries'] == 5
assert stats['expired_entries'] == 0
assert stats['timeout_seconds'] == 1
# Wait for expiration
sleep(1.1)
# Stats should show expired entries
stats = cache.get_stats()
assert stats['total_entries'] == 5
assert stats['valid_entries'] == 0
assert stats['expired_entries'] == 5
def test_user_cache_disabled():
"""Test that disabled cache can be checked"""
cache = None # Simulating disabled cache
# Code should handle None gracefully
if cache:
cache.update(3106000, 312345, "N0MJS", 2, 3100)
# No crash should occur
assert cache is None
def test_user_cache_clear():
"""Test clearing cache"""
cache = UserCache(timeout_seconds=600)
# Add users
for i in range(10):
cache.update(3106000 + i, 312345, f"USER{i}", 2, 3100)
assert len(cache._cache) == 10
# Clear
cache.clear()
assert len(cache._cache) == 0
if __name__ == '__main__':
pytest.main([__file__, '-v'])