montana/Русский/Тесты/test_presence_cache.py

538 lines
23 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""
test_presence_cache.py Unit tests для presence_cache.py
Montana Protocol
Тестирование кэша присутствия участников
"""
import sys
import os
import unittest
import time
import threading
from typing import Dict, Any
# Добавляем путь к модулю
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../экономика/банк_времени/код'))
from presence_cache import PresenceCache
class TestPresenceCacheBasic(unittest.TestCase):
"""Базовые тесты PresenceCache"""
def setUp(self):
"""Создаём новый кэш перед каждым тестом"""
self.cache = PresenceCache()
# ═══════════════════════════════════════════════════════════════════════
# GET/SET OPERATIONS
# ═══════════════════════════════════════════════════════════════════════
def test_set_and_get(self):
"""Установка и получение записи"""
data = {
"address": "user_123",
"presence_seconds": 100,
"t2_seconds": 100,
"last_activity": time.time(),
"is_active": True
}
self.cache.set("user_123", data)
result = self.cache.get("user_123")
self.assertIsNotNone(result)
self.assertEqual(result["address"], "user_123")
self.assertEqual(result["t2_seconds"], 100)
self.assertTrue(result["is_active"])
def test_get_nonexistent(self):
"""Получение несуществующей записи"""
result = self.cache.get("nonexistent")
self.assertIsNone(result)
def test_set_overwrite(self):
"""Перезапись существующей записи"""
data1 = {"t2_seconds": 100, "is_active": True}
data2 = {"t2_seconds": 200, "is_active": False}
self.cache.set("user_123", data1)
self.cache.set("user_123", data2)
result = self.cache.get("user_123")
self.assertEqual(result["t2_seconds"], 200)
self.assertFalse(result["is_active"])
# ═══════════════════════════════════════════════════════════════════════
# REMOVE OPERATIONS
# ═══════════════════════════════════════════════════════════════════════
def test_remove_existing(self):
"""Удаление существующей записи"""
self.cache.set("user_123", {"t2_seconds": 100})
self.assertIn("user_123", self.cache)
self.cache.remove("user_123")
self.assertNotIn("user_123", self.cache)
self.assertIsNone(self.cache.get("user_123"))
def test_remove_nonexistent(self):
"""Удаление несуществующей записи (не должно вызывать ошибку)"""
# Не должно выбросить исключение
self.cache.remove("nonexistent")
self.assertEqual(len(self.cache), 0)
def test_remove_from_empty_cache(self):
"""Удаление из пустого кэша"""
self.cache.remove("user_123")
self.assertEqual(len(self.cache), 0)
# ═══════════════════════════════════════════════════════════════════════
# LENGTH AND CONTAINS
# ═══════════════════════════════════════════════════════════════════════
def test_len_empty(self):
"""Длина пустого кэша"""
self.assertEqual(len(self.cache), 0)
def test_len_with_entries(self):
"""Длина кэша с записями"""
self.cache.set("user_1", {"t2_seconds": 100})
self.cache.set("user_2", {"t2_seconds": 200})
self.cache.set("user_3", {"t2_seconds": 300})
self.assertEqual(len(self.cache), 3)
def test_contains_true(self):
"""Проверка наличия существующего адреса"""
self.cache.set("user_123", {"t2_seconds": 100})
self.assertIn("user_123", self.cache)
self.assertTrue("user_123" in self.cache)
def test_contains_false(self):
"""Проверка наличия несуществующего адреса"""
self.assertNotIn("user_123", self.cache)
self.assertFalse("user_123" in self.cache)
# ═══════════════════════════════════════════════════════════════════════
# ALL ENTRIES
# ═══════════════════════════════════════════════════════════════════════
def test_all_empty(self):
"""Получение всех записей из пустого кэша"""
result = self.cache.all()
self.assertEqual(result, {})
self.assertIsInstance(result, dict)
def test_all_with_entries(self):
"""Получение всех записей"""
self.cache.set("user_1", {"t2_seconds": 100})
self.cache.set("user_2", {"t2_seconds": 200})
result = self.cache.all()
self.assertEqual(len(result), 2)
self.assertIn("user_1", result)
self.assertIn("user_2", result)
self.assertEqual(result["user_1"]["t2_seconds"], 100)
self.assertEqual(result["user_2"]["t2_seconds"], 200)
def test_all_returns_copy(self):
"""all() возвращает копию, не оригинал"""
self.cache.set("user_1", {"t2_seconds": 100})
result1 = self.cache.all()
result2 = self.cache.all()
# Должны быть разные объекты
self.assertIsNot(result1, result2)
# Изменение копии не должно влиять на кэш
result1["user_1"]["t2_seconds"] = 999
original = self.cache.get("user_1")
self.assertEqual(original["t2_seconds"], 100)
class TestPresenceCacheActiveCount(unittest.TestCase):
"""Тесты подсчёта активных участников"""
def setUp(self):
"""Создаём новый кэш перед каждым тестом"""
self.cache = PresenceCache()
# ═══════════════════════════════════════════════════════════════════════
# COUNT ACTIVE
# ═══════════════════════════════════════════════════════════════════════
def test_count_active_empty(self):
"""Подсчёт активных в пустом кэше"""
self.assertEqual(self.cache.count_active(), 0)
def test_count_active_all_active(self):
"""Все участники активны"""
self.cache.set("user_1", {"is_active": True})
self.cache.set("user_2", {"is_active": True})
self.cache.set("user_3", {"is_active": True})
self.assertEqual(self.cache.count_active(), 3)
def test_count_active_all_inactive(self):
"""Все участники неактивны"""
self.cache.set("user_1", {"is_active": False})
self.cache.set("user_2", {"is_active": False})
self.assertEqual(self.cache.count_active(), 0)
def test_count_active_mixed(self):
"""Смешанный случай: активные и неактивные"""
self.cache.set("user_1", {"is_active": True})
self.cache.set("user_2", {"is_active": False})
self.cache.set("user_3", {"is_active": True})
self.cache.set("user_4", {"is_active": False})
self.cache.set("user_5", {"is_active": True})
self.assertEqual(self.cache.count_active(), 3)
def test_count_active_missing_field(self):
"""Участник без поля is_active считается неактивным"""
self.cache.set("user_1", {"t2_seconds": 100}) # Нет is_active
self.cache.set("user_2", {"is_active": True})
self.assertEqual(self.cache.count_active(), 1)
class TestPresenceCacheTotalSeconds(unittest.TestCase):
"""Тесты подсчёта общих секунд"""
def setUp(self):
"""Создаём новый кэш перед каждым тестом"""
self.cache = PresenceCache()
# ═══════════════════════════════════════════════════════════════════════
# TOTAL SECONDS
# ═══════════════════════════════════════════════════════════════════════
def test_total_seconds_empty(self):
"""Общее количество секунд в пустом кэше"""
self.assertEqual(self.cache.total_seconds(), 0)
def test_total_seconds_single_user(self):
"""Один пользователь"""
self.cache.set("user_1", {"t2_seconds": 600})
self.assertEqual(self.cache.total_seconds(), 600)
def test_total_seconds_multiple_users(self):
"""Несколько пользователей"""
self.cache.set("user_1", {"t2_seconds": 100})
self.cache.set("user_2", {"t2_seconds": 200})
self.cache.set("user_3", {"t2_seconds": 300})
self.assertEqual(self.cache.total_seconds(), 600)
def test_total_seconds_missing_field(self):
"""Пользователь без t2_seconds считается как 0"""
self.cache.set("user_1", {"t2_seconds": 100})
self.cache.set("user_2", {}) # Нет t2_seconds
self.cache.set("user_3", {"t2_seconds": 200})
self.assertEqual(self.cache.total_seconds(), 300)
def test_total_seconds_large_numbers(self):
"""Большие числа (1000 пользователей по 600 секунд)"""
for i in range(1000):
self.cache.set(f"user_{i}", {"t2_seconds": 600})
self.assertEqual(self.cache.total_seconds(), 600_000)
class TestPresenceCacheClear(unittest.TestCase):
"""Тесты очистки кэша"""
def setUp(self):
"""Создаём новый кэш перед каждым тестом"""
self.cache = PresenceCache()
# ═══════════════════════════════════════════════════════════════════════
# CLEAR
# ═══════════════════════════════════════════════════════════════════════
def test_clear_empty(self):
"""Очистка пустого кэша"""
self.cache.clear()
self.assertEqual(len(self.cache), 0)
def test_clear_with_entries(self):
"""Очистка кэша с записями"""
self.cache.set("user_1", {"t2_seconds": 100})
self.cache.set("user_2", {"t2_seconds": 200})
self.cache.set("user_3", {"t2_seconds": 300})
self.assertEqual(len(self.cache), 3)
self.cache.clear()
self.assertEqual(len(self.cache), 0)
self.assertEqual(self.cache.count_active(), 0)
self.assertEqual(self.cache.total_seconds(), 0)
self.assertNotIn("user_1", self.cache)
def test_clear_idempotent(self):
"""Многократная очистка не вызывает ошибок"""
self.cache.set("user_1", {"t2_seconds": 100})
self.cache.clear()
self.cache.clear()
self.cache.clear()
self.assertEqual(len(self.cache), 0)
class TestPresenceCacheEdgeCases(unittest.TestCase):
"""Тесты граничных случаев"""
def setUp(self):
"""Создаём новый кэш перед каждым тестом"""
self.cache = PresenceCache()
# ═══════════════════════════════════════════════════════════════════════
# EDGE CASES
# ═══════════════════════════════════════════════════════════════════════
def test_empty_address(self):
"""Пустой адрес"""
self.cache.set("", {"t2_seconds": 100})
self.assertIn("", self.cache)
self.assertEqual(len(self.cache), 1)
result = self.cache.get("")
self.assertEqual(result["t2_seconds"], 100)
def test_unicode_address(self):
"""Unicode символы в адресе"""
address = "пользователь_123_蒙大拿_Ɉ"
self.cache.set(address, {"t2_seconds": 100})
self.assertIn(address, self.cache)
result = self.cache.get(address)
self.assertEqual(result["t2_seconds"], 100)
def test_zero_seconds(self):
"""Ноль секунд присутствия"""
self.cache.set("user_1", {"t2_seconds": 0})
self.assertEqual(self.cache.total_seconds(), 0)
self.assertEqual(len(self.cache), 1)
def test_negative_seconds(self):
"""Отрицательные секунды (некорректные данные)"""
self.cache.set("user_1", {"t2_seconds": -100})
# Кэш не валидирует данные, хранит как есть
self.assertEqual(self.cache.total_seconds(), -100)
def test_none_data(self):
"""None как данные"""
self.cache.set("user_1", None)
result = self.cache.get("user_1")
self.assertIsNone(result)
def test_empty_dict_data(self):
"""Пустой словарь как данные"""
self.cache.set("user_1", {})
result = self.cache.get("user_1")
self.assertEqual(result, {})
self.assertEqual(len(self.cache), 1)
class TestPresenceCacheThreadSafety(unittest.TestCase):
"""Тесты потокобезопасности"""
def setUp(self):
"""Создаём новый кэш перед каждым тестом"""
self.cache = PresenceCache()
# ═══════════════════════════════════════════════════════════════════════
# THREAD SAFETY
# ═══════════════════════════════════════════════════════════════════════
def test_concurrent_writes(self):
"""Одновременная запись из нескольких потоков"""
num_threads = 10
num_writes = 100
def write_worker(thread_id):
for i in range(num_writes):
address = f"user_{thread_id}_{i}"
self.cache.set(address, {"t2_seconds": i})
threads = []
for i in range(num_threads):
thread = threading.Thread(target=write_worker, args=(i,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
# Должно быть num_threads * num_writes записей
self.assertEqual(len(self.cache), num_threads * num_writes)
def test_concurrent_reads(self):
"""Одновременное чтение из нескольких потоков"""
# Подготовка данных
for i in range(100):
self.cache.set(f"user_{i}", {"t2_seconds": i})
results = []
def read_worker():
for i in range(100):
result = self.cache.get(f"user_{i}")
results.append(result)
threads = []
for i in range(10):
thread = threading.Thread(target=read_worker)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
# Все чтения должны быть успешными
self.assertEqual(len(results), 1000) # 10 потоков × 100 чтений
def test_concurrent_read_write(self):
"""Одновременное чтение и запись"""
num_iterations = 1000
def writer():
for i in range(num_iterations):
self.cache.set(f"user_{i}", {"t2_seconds": i})
def reader():
for i in range(num_iterations):
self.cache.get(f"user_{i}")
write_thread = threading.Thread(target=writer)
read_thread = threading.Thread(target=reader)
write_thread.start()
read_thread.start()
write_thread.join()
read_thread.join()
# Не должно быть race conditions или deadlocks
self.assertGreater(len(self.cache), 0)
def test_concurrent_clear(self):
"""Одновременная очистка из нескольких потоков"""
# Подготовка данных
for i in range(100):
self.cache.set(f"user_{i}", {"t2_seconds": i})
def clear_worker():
self.cache.clear()
threads = []
for i in range(5):
thread = threading.Thread(target=clear_worker)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
# В конце кэш должен быть пустым
self.assertEqual(len(self.cache), 0)
class TestPresenceCachePractical(unittest.TestCase):
"""Практические сценарии использования"""
def setUp(self):
"""Создаём новый кэш перед каждым тестом"""
self.cache = PresenceCache()
# ═══════════════════════════════════════════════════════════════════════
# PRACTICAL SCENARIOS
# ═══════════════════════════════════════════════════════════════════════
def test_typical_session(self):
"""Типичная сессия: добавление, обновление, удаление"""
# Пользователь заходит
self.cache.set("user_123", {
"address": "user_123",
"t2_seconds": 0,
"is_active": True,
"last_activity": time.time()
})
self.assertEqual(len(self.cache), 1)
self.assertEqual(self.cache.count_active(), 1)
# Обновляем активность
self.cache.set("user_123", {
"address": "user_123",
"t2_seconds": 600,
"is_active": True,
"last_activity": time.time()
})
self.assertEqual(self.cache.total_seconds(), 600)
# Пользователь уходит
self.cache.remove("user_123")
self.assertEqual(len(self.cache), 0)
self.assertEqual(self.cache.count_active(), 0)
def test_multiple_users_scenario(self):
"""Сценарий с несколькими пользователями"""
# 3 активных, 2 на паузе
self.cache.set("user_1", {"t2_seconds": 600, "is_active": True})
self.cache.set("user_2", {"t2_seconds": 300, "is_active": True})
self.cache.set("user_3", {"t2_seconds": 450, "is_active": True})
self.cache.set("user_4", {"t2_seconds": 200, "is_active": False})
self.cache.set("user_5", {"t2_seconds": 150, "is_active": False})
self.assertEqual(len(self.cache), 5)
self.assertEqual(self.cache.count_active(), 3)
self.assertEqual(self.cache.total_seconds(), 1700) # 600+300+450+200+150
def test_t2_reset_scenario(self):
"""Сценарий сброса τ₂ (каждые 10 минут)"""
# Начальное состояние
self.cache.set("user_1", {"t2_seconds": 600, "is_active": True})
self.cache.set("user_2", {"t2_seconds": 450, "is_active": True})
total_before = self.cache.total_seconds()
self.assertEqual(total_before, 1050)
# Сброс τ₂ — очищаем кэш
self.cache.clear()
self.assertEqual(len(self.cache), 0)
self.assertEqual(self.cache.total_seconds(), 0)
# Пользователи начинают новый τ₂
self.cache.set("user_1", {"t2_seconds": 0, "is_active": True})
self.cache.set("user_2", {"t2_seconds": 0, "is_active": True})
self.assertEqual(len(self.cache), 2)
self.assertEqual(self.cache.total_seconds(), 0)
# ═══════════════════════════════════════════════════════════════════════════
# RUN TESTS
# ═══════════════════════════════════════════════════════════════════════════
if __name__ == '__main__':
# Запускаем с verbose output
unittest.main(verbosity=2)