614 lines
25 KiB
Python
614 lines
25 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
test_breathing_sync.py — Unit tests для breathing_sync.py
|
|||
|
|
|
|||
|
|
Montana Protocol
|
|||
|
|
Тестирование механизма Breathing Synchronization (12-секундный цикл git sync)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import sys
|
|||
|
|
import os
|
|||
|
|
import unittest
|
|||
|
|
import asyncio
|
|||
|
|
from pathlib import Path
|
|||
|
|
from unittest.mock import Mock, patch, MagicMock, call
|
|||
|
|
from datetime import datetime, timezone
|
|||
|
|
|
|||
|
|
# Добавляем путь к модулю
|
|||
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../бот'))
|
|||
|
|
from breathing_sync import (
|
|||
|
|
BreathingConfig,
|
|||
|
|
BreathingSync,
|
|||
|
|
get_breathing_sync
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestBreathingConfig(unittest.TestCase):
|
|||
|
|
"""Тесты конфигурации Breathing Sync"""
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
# КОНСТАНТЫ
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
def test_sync_interval(self):
|
|||
|
|
"""Интервал синхронизации = 12 секунд"""
|
|||
|
|
self.assertEqual(BreathingConfig.SYNC_INTERVAL_SEC, 12)
|
|||
|
|
|
|||
|
|
def test_startup_delay(self):
|
|||
|
|
"""Задержка перед первой синхронизацией = 5 секунд"""
|
|||
|
|
self.assertEqual(BreathingConfig.STARTUP_DELAY_SEC, 5)
|
|||
|
|
|
|||
|
|
def test_remote_name(self):
|
|||
|
|
"""Git remote = origin"""
|
|||
|
|
self.assertEqual(BreathingConfig.REMOTE_NAME, "origin")
|
|||
|
|
|
|||
|
|
def test_branch_name(self):
|
|||
|
|
"""Git branch = main"""
|
|||
|
|
self.assertEqual(BreathingConfig.BRANCH_NAME, "main")
|
|||
|
|
|
|||
|
|
def test_git_timeout(self):
|
|||
|
|
"""Таймаут git операций = 30 секунд"""
|
|||
|
|
self.assertEqual(BreathingConfig.GIT_TIMEOUT_SEC, 30)
|
|||
|
|
|
|||
|
|
def test_sync_paths(self):
|
|||
|
|
"""Проверка списка синхронизируемых файлов"""
|
|||
|
|
self.assertIsInstance(BreathingConfig.SYNC_PATHS, list)
|
|||
|
|
self.assertIn("data/users.json", BreathingConfig.SYNC_PATHS)
|
|||
|
|
self.assertIn("node_crypto/nodes.json", BreathingConfig.SYNC_PATHS)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestBreathingSyncInit(unittest.TestCase):
|
|||
|
|
"""Тесты инициализации BreathingSync"""
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
# INITIALIZATION
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
def test_init_default_path(self):
|
|||
|
|
"""Инициализация с дефолтным путём"""
|
|||
|
|
sync = BreathingSync()
|
|||
|
|
|
|||
|
|
self.assertIsNotNone(sync.repo_path)
|
|||
|
|
self.assertIsInstance(sync.repo_path, Path)
|
|||
|
|
self.assertFalse(sync._running)
|
|||
|
|
|
|||
|
|
def test_init_custom_path(self):
|
|||
|
|
"""Инициализация с кастомным путём"""
|
|||
|
|
custom_path = Path("/tmp/montana_test")
|
|||
|
|
sync = BreathingSync(repo_path=custom_path)
|
|||
|
|
|
|||
|
|
self.assertEqual(sync.repo_path, custom_path)
|
|||
|
|
|
|||
|
|
def test_init_stats(self):
|
|||
|
|
"""Инициализация создаёт статистику"""
|
|||
|
|
sync = BreathingSync()
|
|||
|
|
|
|||
|
|
self.assertIsInstance(sync.stats, dict)
|
|||
|
|
self.assertEqual(sync.stats["total_inhales"], 0)
|
|||
|
|
self.assertEqual(sync.stats["total_exhales"], 0)
|
|||
|
|
self.assertEqual(sync.stats["failed_inhales"], 0)
|
|||
|
|
self.assertEqual(sync.stats["failed_exhales"], 0)
|
|||
|
|
self.assertIsNone(sync.stats["last_inhale"])
|
|||
|
|
self.assertIsNone(sync.stats["last_exhale"])
|
|||
|
|
self.assertIsNone(sync.stats["last_error"])
|
|||
|
|
|
|||
|
|
def test_init_stop_event(self):
|
|||
|
|
"""Инициализация создаёт stop event"""
|
|||
|
|
sync = BreathingSync()
|
|||
|
|
|
|||
|
|
self.assertIsInstance(sync._stop_event, asyncio.Event)
|
|||
|
|
self.assertFalse(sync._stop_event.is_set())
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestBreathingSyncGitCommand(unittest.TestCase):
|
|||
|
|
"""Тесты выполнения git команд"""
|
|||
|
|
|
|||
|
|
def setUp(self):
|
|||
|
|
"""Создаём BreathingSync перед каждым тестом"""
|
|||
|
|
self.sync = BreathingSync()
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
# GIT COMMAND EXECUTION
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
@patch('breathing_sync.subprocess.run')
|
|||
|
|
def test_run_git_command_success(self, mock_run):
|
|||
|
|
"""Успешное выполнение git команды"""
|
|||
|
|
mock_run.return_value = Mock(
|
|||
|
|
returncode=0,
|
|||
|
|
stdout="Already up to date.",
|
|||
|
|
stderr=""
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
result = self.sync._run_git_command(["pull"])
|
|||
|
|
|
|||
|
|
self.assertTrue(result["success"])
|
|||
|
|
self.assertEqual(result["returncode"], 0)
|
|||
|
|
self.assertEqual(result["stdout"], "Already up to date.")
|
|||
|
|
self.assertEqual(result["stderr"], "")
|
|||
|
|
|
|||
|
|
@patch('breathing_sync.subprocess.run')
|
|||
|
|
def test_run_git_command_failure(self, mock_run):
|
|||
|
|
"""Неудачное выполнение git команды"""
|
|||
|
|
mock_run.return_value = Mock(
|
|||
|
|
returncode=1,
|
|||
|
|
stdout="",
|
|||
|
|
stderr="fatal: not a git repository"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
result = self.sync._run_git_command(["status"])
|
|||
|
|
|
|||
|
|
self.assertFalse(result["success"])
|
|||
|
|
self.assertEqual(result["returncode"], 1)
|
|||
|
|
self.assertIn("not a git repository", result["stderr"])
|
|||
|
|
|
|||
|
|
@patch('breathing_sync.subprocess.run')
|
|||
|
|
def test_run_git_command_timeout(self, mock_run):
|
|||
|
|
"""Таймаут git команды"""
|
|||
|
|
from subprocess import TimeoutExpired
|
|||
|
|
mock_run.side_effect = TimeoutExpired("git", 30)
|
|||
|
|
|
|||
|
|
result = self.sync._run_git_command(["pull"], timeout=30)
|
|||
|
|
|
|||
|
|
self.assertFalse(result["success"])
|
|||
|
|
self.assertEqual(result["returncode"], -1)
|
|||
|
|
self.assertIn("Timeout", result["stderr"])
|
|||
|
|
|
|||
|
|
@patch('breathing_sync.subprocess.run')
|
|||
|
|
def test_run_git_command_exception(self, mock_run):
|
|||
|
|
"""Исключение при выполнении git команды"""
|
|||
|
|
mock_run.side_effect = Exception("Network error")
|
|||
|
|
|
|||
|
|
result = self.sync._run_git_command(["fetch"])
|
|||
|
|
|
|||
|
|
self.assertFalse(result["success"])
|
|||
|
|
self.assertEqual(result["returncode"], -1)
|
|||
|
|
self.assertIn("Network error", result["stderr"])
|
|||
|
|
|
|||
|
|
@patch('breathing_sync.subprocess.run')
|
|||
|
|
def test_run_git_command_uses_correct_path(self, mock_run):
|
|||
|
|
"""Git команда выполняется в правильной директории"""
|
|||
|
|
mock_run.return_value = Mock(returncode=0, stdout="", stderr="")
|
|||
|
|
|
|||
|
|
custom_path = Path("/custom/repo")
|
|||
|
|
sync = BreathingSync(repo_path=custom_path)
|
|||
|
|
sync._run_git_command(["status"])
|
|||
|
|
|
|||
|
|
# Проверяем что git вызван с правильным -C флагом
|
|||
|
|
call_args = mock_run.call_args[0][0]
|
|||
|
|
self.assertIn("-C", call_args)
|
|||
|
|
self.assertIn(str(custom_path), call_args)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestBreathingSyncInhale(unittest.TestCase):
|
|||
|
|
"""Тесты inhale (git pull)"""
|
|||
|
|
|
|||
|
|
def setUp(self):
|
|||
|
|
"""Создаём BreathingSync перед каждым тестом"""
|
|||
|
|
self.sync = BreathingSync()
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
# INHALE (GIT PULL)
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, '_run_git_command')
|
|||
|
|
def test_inhale_success(self, mock_git):
|
|||
|
|
"""Успешный inhale (получены изменения)"""
|
|||
|
|
mock_git.return_value = {
|
|||
|
|
"success": True,
|
|||
|
|
"stdout": "Updating abc123..def456",
|
|||
|
|
"stderr": "",
|
|||
|
|
"returncode": 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result = self.sync.inhale()
|
|||
|
|
|
|||
|
|
self.assertTrue(result)
|
|||
|
|
self.assertEqual(self.sync.stats["total_inhales"], 1)
|
|||
|
|
self.assertEqual(self.sync.stats["failed_inhales"], 0)
|
|||
|
|
self.assertIsNotNone(self.sync.stats["last_inhale"])
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, '_run_git_command')
|
|||
|
|
def test_inhale_already_up_to_date(self, mock_git):
|
|||
|
|
"""Inhale когда уже up to date"""
|
|||
|
|
mock_git.return_value = {
|
|||
|
|
"success": True,
|
|||
|
|
"stdout": "Already up to date.",
|
|||
|
|
"stderr": "",
|
|||
|
|
"returncode": 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result = self.sync.inhale()
|
|||
|
|
|
|||
|
|
self.assertTrue(result)
|
|||
|
|
self.assertEqual(self.sync.stats["total_inhales"], 1)
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, '_run_git_command')
|
|||
|
|
def test_inhale_failure(self, mock_git):
|
|||
|
|
"""Неудачный inhale"""
|
|||
|
|
mock_git.return_value = {
|
|||
|
|
"success": False,
|
|||
|
|
"stdout": "",
|
|||
|
|
"stderr": "error: cannot pull",
|
|||
|
|
"returncode": 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result = self.sync.inhale()
|
|||
|
|
|
|||
|
|
self.assertFalse(result)
|
|||
|
|
self.assertEqual(self.sync.stats["total_inhales"], 1)
|
|||
|
|
self.assertEqual(self.sync.stats["failed_inhales"], 1)
|
|||
|
|
self.assertEqual(self.sync.stats["last_error"], "error: cannot pull")
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, '_run_git_command')
|
|||
|
|
def test_inhale_uses_rebase(self, mock_git):
|
|||
|
|
"""Inhale использует --rebase"""
|
|||
|
|
mock_git.return_value = {
|
|||
|
|
"success": True,
|
|||
|
|
"stdout": "Already up to date.",
|
|||
|
|
"stderr": "",
|
|||
|
|
"returncode": 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
self.sync.inhale()
|
|||
|
|
|
|||
|
|
# Проверяем что вызван git pull с --rebase
|
|||
|
|
call_args = mock_git.call_args[0][0]
|
|||
|
|
self.assertIn("pull", call_args)
|
|||
|
|
self.assertIn("--rebase", call_args)
|
|||
|
|
self.assertIn("origin", call_args)
|
|||
|
|
self.assertIn("main", call_args)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestBreathingSyncExhale(unittest.TestCase):
|
|||
|
|
"""Тесты exhale (git push)"""
|
|||
|
|
|
|||
|
|
def setUp(self):
|
|||
|
|
"""Создаём BreathingSync перед каждым тестом"""
|
|||
|
|
self.sync = BreathingSync()
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
# EXHALE (GIT PUSH)
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, '_run_git_command')
|
|||
|
|
def test_exhale_no_changes(self, mock_git):
|
|||
|
|
"""Exhale когда нет изменений"""
|
|||
|
|
# git status --porcelain возвращает пусто (нет изменений)
|
|||
|
|
mock_git.return_value = {
|
|||
|
|
"success": True,
|
|||
|
|
"stdout": "", # Пусто = нет изменений
|
|||
|
|
"stderr": "",
|
|||
|
|
"returncode": 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result = self.sync.exhale()
|
|||
|
|
|
|||
|
|
# Должно быть успешно (нечего пушить)
|
|||
|
|
self.assertTrue(result)
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, '_run_git_command')
|
|||
|
|
@patch.object(Path, 'exists')
|
|||
|
|
def test_exhale_with_changes(self, mock_exists, mock_git):
|
|||
|
|
"""Exhale когда есть изменения"""
|
|||
|
|
mock_exists.return_value = True
|
|||
|
|
|
|||
|
|
# Последовательность git команд
|
|||
|
|
def git_side_effect(args):
|
|||
|
|
if "status" in args:
|
|||
|
|
return {"success": True, "stdout": "M data/users.json", "stderr": "", "returncode": 0}
|
|||
|
|
elif "add" in args:
|
|||
|
|
return {"success": True, "stdout": "", "stderr": "", "returncode": 0}
|
|||
|
|
elif "commit" in args:
|
|||
|
|
return {"success": True, "stdout": "[main abc123]", "stderr": "", "returncode": 0}
|
|||
|
|
elif "push" in args:
|
|||
|
|
return {"success": True, "stdout": "Everything up-to-date", "stderr": "", "returncode": 0}
|
|||
|
|
return {"success": True, "stdout": "", "stderr": "", "returncode": 0}
|
|||
|
|
|
|||
|
|
mock_git.side_effect = git_side_effect
|
|||
|
|
|
|||
|
|
result = self.sync.exhale()
|
|||
|
|
|
|||
|
|
self.assertTrue(result)
|
|||
|
|
self.assertEqual(self.sync.stats["total_exhales"], 1)
|
|||
|
|
self.assertEqual(self.sync.stats["failed_exhales"], 0)
|
|||
|
|
self.assertIsNotNone(self.sync.stats["last_exhale"])
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, '_run_git_command')
|
|||
|
|
@patch.object(Path, 'exists')
|
|||
|
|
def test_exhale_push_failure(self, mock_exists, mock_git):
|
|||
|
|
"""Exhale когда push не удался"""
|
|||
|
|
mock_exists.return_value = True
|
|||
|
|
|
|||
|
|
def git_side_effect(args):
|
|||
|
|
if "status" in args:
|
|||
|
|
return {"success": True, "stdout": "M data/users.json", "stderr": "", "returncode": 0}
|
|||
|
|
elif "add" in args:
|
|||
|
|
return {"success": True, "stdout": "", "stderr": "", "returncode": 0}
|
|||
|
|
elif "commit" in args:
|
|||
|
|
return {"success": True, "stdout": "[main abc123]", "stderr": "", "returncode": 0}
|
|||
|
|
elif "push" in args:
|
|||
|
|
return {"success": False, "stdout": "", "stderr": "error: failed to push", "returncode": 1}
|
|||
|
|
return {"success": True, "stdout": "", "stderr": "", "returncode": 0}
|
|||
|
|
|
|||
|
|
mock_git.side_effect = git_side_effect
|
|||
|
|
|
|||
|
|
result = self.sync.exhale()
|
|||
|
|
|
|||
|
|
self.assertFalse(result)
|
|||
|
|
self.assertEqual(self.sync.stats["failed_exhales"], 1)
|
|||
|
|
self.assertEqual(self.sync.stats["last_error"], "error: failed to push")
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, '_run_git_command')
|
|||
|
|
def test_exhale_nothing_to_commit(self, mock_git):
|
|||
|
|
"""Exhale когда нечего коммитить"""
|
|||
|
|
|
|||
|
|
def git_side_effect(args):
|
|||
|
|
if "status" in args:
|
|||
|
|
return {"success": True, "stdout": "M data/users.json", "stderr": "", "returncode": 0}
|
|||
|
|
elif "commit" in args:
|
|||
|
|
return {"success": False, "stdout": "", "stderr": "nothing to commit", "returncode": 1}
|
|||
|
|
return {"success": True, "stdout": "", "stderr": "", "returncode": 0}
|
|||
|
|
|
|||
|
|
mock_git.side_effect = git_side_effect
|
|||
|
|
|
|||
|
|
result = self.sync.exhale()
|
|||
|
|
|
|||
|
|
# Должно быть успешно (нечего коммитить — нормально)
|
|||
|
|
self.assertTrue(result)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestBreathingSyncBreathe(unittest.TestCase):
|
|||
|
|
"""Тесты breathe (полный цикл дыхания)"""
|
|||
|
|
|
|||
|
|
def setUp(self):
|
|||
|
|
"""Создаём BreathingSync перед каждым тестом"""
|
|||
|
|
self.sync = BreathingSync()
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
# BREATHE (ЦИКЛ)
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, 'exhale')
|
|||
|
|
@patch.object(BreathingSync, 'inhale')
|
|||
|
|
def test_breathe_both_success(self, mock_inhale, mock_exhale):
|
|||
|
|
"""Breathe когда оба успешны"""
|
|||
|
|
mock_inhale.return_value = True
|
|||
|
|
mock_exhale.return_value = True
|
|||
|
|
|
|||
|
|
result = self.sync.breathe()
|
|||
|
|
|
|||
|
|
self.assertTrue(result["inhale"])
|
|||
|
|
self.assertTrue(result["exhale"])
|
|||
|
|
mock_inhale.assert_called_once()
|
|||
|
|
mock_exhale.assert_called_once()
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, 'exhale')
|
|||
|
|
@patch.object(BreathingSync, 'inhale')
|
|||
|
|
def test_breathe_inhale_fails(self, mock_inhale, mock_exhale):
|
|||
|
|
"""Breathe когда inhale не удался"""
|
|||
|
|
mock_inhale.return_value = False
|
|||
|
|
mock_exhale.return_value = True
|
|||
|
|
|
|||
|
|
result = self.sync.breathe()
|
|||
|
|
|
|||
|
|
self.assertFalse(result["inhale"])
|
|||
|
|
self.assertTrue(result["exhale"])
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, 'exhale')
|
|||
|
|
@patch.object(BreathingSync, 'inhale')
|
|||
|
|
def test_breathe_exhale_fails(self, mock_inhale, mock_exhale):
|
|||
|
|
"""Breathe когда exhale не удался"""
|
|||
|
|
mock_inhale.return_value = True
|
|||
|
|
mock_exhale.return_value = False
|
|||
|
|
|
|||
|
|
result = self.sync.breathe()
|
|||
|
|
|
|||
|
|
self.assertTrue(result["inhale"])
|
|||
|
|
self.assertFalse(result["exhale"])
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, 'exhale')
|
|||
|
|
@patch.object(BreathingSync, 'inhale')
|
|||
|
|
def test_breathe_both_fail(self, mock_inhale, mock_exhale):
|
|||
|
|
"""Breathe когда оба не удались"""
|
|||
|
|
mock_inhale.return_value = False
|
|||
|
|
mock_exhale.return_value = False
|
|||
|
|
|
|||
|
|
result = self.sync.breathe()
|
|||
|
|
|
|||
|
|
self.assertFalse(result["inhale"])
|
|||
|
|
self.assertFalse(result["exhale"])
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, 'exhale')
|
|||
|
|
@patch.object(BreathingSync, 'inhale')
|
|||
|
|
def test_breathe_order(self, mock_inhale, mock_exhale):
|
|||
|
|
"""Breathe выполняется в правильном порядке: inhale → exhale"""
|
|||
|
|
mock_inhale.return_value = True
|
|||
|
|
mock_exhale.return_value = True
|
|||
|
|
|
|||
|
|
# Используем side_effect чтобы проверить порядок
|
|||
|
|
call_order = []
|
|||
|
|
mock_inhale.side_effect = lambda: call_order.append("inhale") or True
|
|||
|
|
mock_exhale.side_effect = lambda: call_order.append("exhale") or True
|
|||
|
|
|
|||
|
|
self.sync.breathe()
|
|||
|
|
|
|||
|
|
self.assertEqual(call_order, ["inhale", "exhale"])
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestBreathingSyncControl(unittest.TestCase):
|
|||
|
|
"""Тесты управления синхронизацией"""
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
# CONTROL
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
def test_stop(self):
|
|||
|
|
"""stop() останавливает синхронизацию"""
|
|||
|
|
sync = BreathingSync()
|
|||
|
|
|
|||
|
|
self.assertFalse(sync._stop_event.is_set())
|
|||
|
|
self.assertFalse(sync._running)
|
|||
|
|
|
|||
|
|
sync._running = True
|
|||
|
|
sync.stop()
|
|||
|
|
|
|||
|
|
self.assertTrue(sync._stop_event.is_set())
|
|||
|
|
self.assertFalse(sync._running)
|
|||
|
|
|
|||
|
|
def test_stop_idempotent(self):
|
|||
|
|
"""Множественный stop() безопасен"""
|
|||
|
|
sync = BreathingSync()
|
|||
|
|
|
|||
|
|
sync.stop()
|
|||
|
|
sync.stop()
|
|||
|
|
sync.stop()
|
|||
|
|
|
|||
|
|
self.assertTrue(sync._stop_event.is_set())
|
|||
|
|
|
|||
|
|
def test_get_stats(self):
|
|||
|
|
"""get_stats() возвращает статистику"""
|
|||
|
|
sync = BreathingSync()
|
|||
|
|
|
|||
|
|
stats = sync.get_stats()
|
|||
|
|
|
|||
|
|
self.assertIsInstance(stats, dict)
|
|||
|
|
self.assertIn("total_inhales", stats)
|
|||
|
|
self.assertIn("total_exhales", stats)
|
|||
|
|
self.assertIn("failed_inhales", stats)
|
|||
|
|
self.assertIn("failed_exhales", stats)
|
|||
|
|
self.assertIn("repo_path", stats)
|
|||
|
|
self.assertIn("interval_sec", stats)
|
|||
|
|
self.assertIn("is_running", stats)
|
|||
|
|
|
|||
|
|
def test_get_stats_values(self):
|
|||
|
|
"""get_stats() содержит правильные значения"""
|
|||
|
|
sync = BreathingSync()
|
|||
|
|
sync.stats["total_inhales"] = 10
|
|||
|
|
sync.stats["total_exhales"] = 8
|
|||
|
|
sync._running = True
|
|||
|
|
|
|||
|
|
stats = sync.get_stats()
|
|||
|
|
|
|||
|
|
self.assertEqual(stats["total_inhales"], 10)
|
|||
|
|
self.assertEqual(stats["total_exhales"], 8)
|
|||
|
|
self.assertTrue(stats["is_running"])
|
|||
|
|
self.assertEqual(stats["interval_sec"], 12)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestBreathingSyncLoop(unittest.TestCase):
|
|||
|
|
"""Тесты async цикла синхронизации"""
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
# ASYNC LOOP
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, 'breathe')
|
|||
|
|
async def test_run_breathing_loop_basic(self, mock_breathe):
|
|||
|
|
"""Базовый тест цикла"""
|
|||
|
|
mock_breathe.return_value = {"inhale": True, "exhale": True}
|
|||
|
|
|
|||
|
|
sync = BreathingSync()
|
|||
|
|
|
|||
|
|
# Запускаем цикл на 0.2 секунды
|
|||
|
|
task = asyncio.create_task(
|
|||
|
|
sync.run_breathing_loop(interval=0.1, only_when_master=False)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
await asyncio.sleep(0.3)
|
|||
|
|
sync.stop()
|
|||
|
|
await asyncio.sleep(0.1)
|
|||
|
|
task.cancel()
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
await task
|
|||
|
|
except asyncio.CancelledError:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# Должно быть хотя бы 1 вызов breathe
|
|||
|
|
self.assertGreater(mock_breathe.call_count, 0)
|
|||
|
|
|
|||
|
|
@patch.object(BreathingSync, 'breathe')
|
|||
|
|
async def test_run_breathing_loop_only_when_master(self, mock_breathe):
|
|||
|
|
"""Цикл работает только когда узел — мастер"""
|
|||
|
|
mock_breathe.return_value = {"inhale": True, "exhale": True}
|
|||
|
|
|
|||
|
|
sync = BreathingSync()
|
|||
|
|
is_master = False
|
|||
|
|
|
|||
|
|
def check_master():
|
|||
|
|
return is_master
|
|||
|
|
|
|||
|
|
# Запускаем цикл
|
|||
|
|
task = asyncio.create_task(
|
|||
|
|
sync.run_breathing_loop(
|
|||
|
|
interval=0.1,
|
|||
|
|
only_when_master=True,
|
|||
|
|
is_master_func=check_master
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Ждём немного (не мастер)
|
|||
|
|
await asyncio.sleep(0.3)
|
|||
|
|
call_count_not_master = mock_breathe.call_count
|
|||
|
|
|
|||
|
|
# Становимся мастером
|
|||
|
|
is_master = True
|
|||
|
|
await asyncio.sleep(0.3)
|
|||
|
|
call_count_as_master = mock_breathe.call_count
|
|||
|
|
|
|||
|
|
sync.stop()
|
|||
|
|
task.cancel()
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
await task
|
|||
|
|
except asyncio.CancelledError:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# Когда не мастер — breathe не вызывается (или очень редко)
|
|||
|
|
self.assertEqual(call_count_not_master, 0)
|
|||
|
|
|
|||
|
|
# Когда мастер — breathe вызывается
|
|||
|
|
self.assertGreater(call_count_as_master, 0)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestBreathingSyncSingleton(unittest.TestCase):
|
|||
|
|
"""Тесты singleton паттерна"""
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
# SINGLETON
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
def test_get_breathing_sync_returns_instance(self):
|
|||
|
|
"""get_breathing_sync() возвращает экземпляр"""
|
|||
|
|
sync = get_breathing_sync()
|
|||
|
|
|
|||
|
|
self.assertIsNotNone(sync)
|
|||
|
|
self.assertIsInstance(sync, BreathingSync)
|
|||
|
|
|
|||
|
|
def test_get_breathing_sync_singleton(self):
|
|||
|
|
"""get_breathing_sync() возвращает один и тот же экземпляр"""
|
|||
|
|
sync1 = get_breathing_sync()
|
|||
|
|
sync2 = get_breathing_sync()
|
|||
|
|
|
|||
|
|
self.assertIs(sync1, sync2)
|
|||
|
|
|
|||
|
|
def test_singleton_shared_state(self):
|
|||
|
|
"""Singleton делит состояние между вызовами"""
|
|||
|
|
sync1 = get_breathing_sync()
|
|||
|
|
sync1.stats["total_inhales"] = 42
|
|||
|
|
|
|||
|
|
sync2 = get_breathing_sync()
|
|||
|
|
|
|||
|
|
# Должен быть тот же объект с тем же состоянием
|
|||
|
|
self.assertEqual(sync2.stats["total_inhales"], 42)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|||
|
|
# RUN TESTS
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
# Запускаем с verbose output
|
|||
|
|
unittest.main(verbosity=2)
|