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