mirror of
https://github.com/n0mjs710/HBlink4.git
synced 2026-11-08 04:19:26 +00:00
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
194 lines
4.8 KiB
Python
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'])
|