feat: добавлены консольные команды logs, reload, trigger morning
- console_commands/logs.py — чтение последних строк лога (tail -20) - console_commands/reload.py — горячая перезагрузка всех cogs - console_commands/trigger_morning.py — ручной запуск morning-дайджеста - logs доступна через admin.py (docker exec) и интерактивный терминал - reload и trigger morning доступны через интерактивный терминал - сохранён bot._scheduler для доступа к планировщику - обновлены __init__.py, admin.py, README.md, ISSUES.md - добавлены тесты: test_console_logs.py (4), test_console_reload.py (2), test_console_trigger_morning.py (3) - итого 243 теста
This commit is contained in:
parent
5c438b4c48
commit
c37baafacc
@ -16,9 +16,7 @@
|
|||||||
|
|
||||||
### Средний приоритет
|
### Средний приоритет
|
||||||
|
|
||||||
- [ ] **`reload`** — горячая перезагрузка cogs без остановки бота
|
- [
|
||||||
- [ ] **`logs`** — последние строки лога (tail -20)
|
|
||||||
- [ ] **`trigger morning`** — ручной запуск morning-дайджеста в канал
|
|
||||||
|
|
||||||
### Низкий приоритет
|
### Низкий приоритет
|
||||||
|
|
||||||
|
|||||||
16
README.md
16
README.md
@ -54,6 +54,9 @@ python bot.py
|
|||||||
| 5 | `help` | Показать список всех команд |
|
| 5 | `help` | Показать список всех команд |
|
||||||
| 6 | `status` | Статус бота: пинг и uptime |
|
| 6 | `status` | Статус бота: пинг и uptime |
|
||||||
| 7 | `stats` | Количество серверов, каналов, пользователей |
|
| 7 | `stats` | Количество серверов, каналов, пользователей |
|
||||||
|
| 8 | `logs` | Последние строки лога (tail -20) |
|
||||||
|
| 9 | `reload` | Горячая перезагрузка всех cogs |
|
||||||
|
| 10 | `trigger morning` | Ручной запуск morning-дайджеста |
|
||||||
| 0 | `stop` | Остановка бота |
|
| 0 | `stop` | Остановка бота |
|
||||||
|
|
||||||
> Номера команд генерируются автоматически из `ALL_CONSOLE_COMMANDS` в порядке определения в `console_commands/__init__.py`.
|
> Номера команд генерируются автоматически из `ALL_CONSOLE_COMMANDS` в порядке определения в `console_commands/__init__.py`.
|
||||||
@ -82,6 +85,9 @@ console_commands/ # Консольные команды
|
|||||||
help.py # help — список всех команд
|
help.py # help — список всех команд
|
||||||
status.py # status — статус бота в терминале
|
status.py # status — статус бота в терминале
|
||||||
stats.py # stats — статистика серверов в терминале
|
stats.py # stats — статистика серверов в терминале
|
||||||
|
logs.py # logs — последние строки лога (tail)
|
||||||
|
reload.py # reload — горячая перезагрузка cogs
|
||||||
|
trigger_morning.py # trigger morning — ручной запуск morning-дайджеста
|
||||||
utils/ # Утилиты (API-клиенты, конвертации)
|
utils/ # Утилиты (API-клиенты, конвертации)
|
||||||
__init__.py # __all__ — публичный API утилит
|
__init__.py # __all__ — публичный API утилит
|
||||||
pogoda.py # fetch_weather(), fetch_open_meteo(), wmo_to_russian(), translate_weather(), pressure_to_mmhg(), format_weather_data_for_console(), format_weather_for_embed()
|
pogoda.py # fetch_weather(), fetch_open_meteo(), wmo_to_russian(), translate_weather(), pressure_to_mmhg(), format_weather_data_for_console(), format_weather_for_embed()
|
||||||
@ -148,8 +154,11 @@ python -m pytest tests/ -v
|
|||||||
| `test_admin.py` | `admin.py` — CLI для docker exec | 5 |
|
| `test_admin.py` | `admin.py` — CLI для docker exec | 5 |
|
||||||
| `test_commands_status.py` | команда `!status` (embed, uptime) | 6 |
|
| `test_commands_status.py` | команда `!status` (embed, uptime) | 6 |
|
||||||
| `test_commands_stats.py` | команда `!stats` (серверы, каналы) | 4 |
|
| `test_commands_stats.py` | команда `!stats` (серверы, каналы) | 4 |
|
||||||
|
| `test_console_logs.py` | команда `logs` (чтение лога) | 4 |
|
||||||
|
| `test_console_reload.py` | команда `reload` (перезагрузка cogs) | 2 |
|
||||||
|
| `test_console_trigger_morning.py` | команда `trigger morning` (запуск дайджеста) | 3 |
|
||||||
|
|
||||||
**Итого: 233 теста.**
|
**Итого: 243 теста.**
|
||||||
|
|
||||||
## Запуск в Docker
|
## Запуск в Docker
|
||||||
|
|
||||||
@ -181,10 +190,13 @@ docker exec discord-bot python admin.py pogoda
|
|||||||
docker exec discord-bot python admin.py news
|
docker exec discord-bot python admin.py news
|
||||||
docker exec discord-bot python admin.py cat
|
docker exec discord-bot python admin.py cat
|
||||||
docker exec discord-bot python admin.py morning
|
docker exec discord-bot python admin.py morning
|
||||||
|
docker exec discord-bot python admin.py logs # последние строки лога
|
||||||
docker exec discord-bot python admin.py help
|
docker exec discord-bot python admin.py help
|
||||||
docker stop discord-bot # остановка бота
|
docker stop discord-bot # остановка бота
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Команды `reload` и `trigger morning` доступны только через интерактивный терминал бота (требуют запущенного экземпляра бота).
|
||||||
|
|
||||||
Результат выводится в stdout терминала. Команды используют те же `utils`, что и Discord-команды.
|
Результат выводится в stdout терминала. Команды используют те же `utils`, что и Discord-команды.
|
||||||
|
|
||||||
## API и внешние сервисы
|
## API и внешние сервисы
|
||||||
|
|||||||
1
bot.py
1
bot.py
@ -55,6 +55,7 @@ class BotRunner:
|
|||||||
# Запуск планировщика
|
# Запуск планировщика
|
||||||
morning_time = os.getenv("MORNING_TIME", "07:00")
|
morning_time = os.getenv("MORNING_TIME", "07:00")
|
||||||
self.scheduler = Scheduler(self.bot, morning_time)
|
self.scheduler = Scheduler(self.bot, morning_time)
|
||||||
|
self.bot._scheduler = self.scheduler
|
||||||
logger.info(f" Планировщик запущен (время: {morning_time})")
|
logger.info(f" Планировщик запущен (время: {morning_time})")
|
||||||
|
|
||||||
self.bot_ready.set()
|
self.bot_ready.set()
|
||||||
|
|||||||
@ -6,6 +6,9 @@ from .morning import morning
|
|||||||
from .help import help
|
from .help import help
|
||||||
from .status import status
|
from .status import status
|
||||||
from .stats import stats
|
from .stats import stats
|
||||||
|
from .logs import logs
|
||||||
|
from .reload import reload
|
||||||
|
from .trigger_morning import trigger_morning
|
||||||
|
|
||||||
ALL_CONSOLE_COMMANDS = {
|
ALL_CONSOLE_COMMANDS = {
|
||||||
"stop": stop,
|
"stop": stop,
|
||||||
@ -16,4 +19,7 @@ ALL_CONSOLE_COMMANDS = {
|
|||||||
"help": help,
|
"help": help,
|
||||||
"status": status,
|
"status": status,
|
||||||
"stats": stats,
|
"stats": stats,
|
||||||
|
"logs": logs,
|
||||||
|
"reload": reload,
|
||||||
|
"trigger morning": trigger_morning,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,23 +11,56 @@ import sys
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
|
async def run_logs():
|
||||||
|
"""Вывести последние строки лога."""
|
||||||
|
from console_commands.logs import LOG_FILE, DEFAULT_LINES
|
||||||
|
|
||||||
|
lines = DEFAULT_LINES
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
try:
|
||||||
|
lines = int(sys.argv[2])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
log_file = LOG_FILE
|
||||||
|
if not log_file.exists():
|
||||||
|
print(f"Файл лога не найден: {log_file}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(log_file, "r", encoding="utf-8") as f:
|
||||||
|
all_lines = f.readlines()
|
||||||
|
tail = all_lines[-lines:] if len(all_lines) > lines else all_lines
|
||||||
|
print(f"\nПоследние {len(tail)} строк {log_file}:")
|
||||||
|
print("-" * 40)
|
||||||
|
print("".join(tail), end="")
|
||||||
|
print("-" * 40)
|
||||||
|
except OSError as e:
|
||||||
|
print(f"Ошибка чтения лога: {e}")
|
||||||
|
|
||||||
|
|
||||||
def print_help():
|
def print_help():
|
||||||
"""Показать список команд."""
|
"""Показать список команд."""
|
||||||
print("\nАдмин-команды:")
|
print("\nАдмин-команды:")
|
||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
commands = [
|
commands = [
|
||||||
("pogoda", "Прогноз погоды в Магнитогорске"),
|
("pogoda", "Прогноз погоды в Магнитогорске"),
|
||||||
("news", "Топ-5 статей и новостей AI с Habr"),
|
("news", "Топ-5 статей и новостей AI с Habr"),
|
||||||
("cat", "URL случайного котика"),
|
("cat", "URL случайного котика"),
|
||||||
("morning", "Утренний дайджест: погода + новости + котик"),
|
("morning", "Утренний дайджест: погода + новости + котик"),
|
||||||
("help", "Показать этот список"),
|
("logs", "Последние строки лога (tail -20)"),
|
||||||
|
("help", "Показать этот список"),
|
||||||
]
|
]
|
||||||
for name, desc in commands:
|
for name, desc in commands:
|
||||||
print(f" {name:<12} — {desc}")
|
print(f" {name:<12} — {desc}")
|
||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
print("\n status — через Discord: !status")
|
print("\n status — через Discord: !status")
|
||||||
print(" stats — через Discord: !stats")
|
print(" stats — через Discord: !stats")
|
||||||
print(" stop — docker stop discord-bot\n")
|
print(" reload — через терминал бота (интерактив)")
|
||||||
|
print(" trigger morning — через терминал бота (интерактив)")
|
||||||
|
print(" stop — docker stop discord-bot\n")
|
||||||
|
|
||||||
|
|
||||||
async def run_pogoda():
|
async def run_pogoda():
|
||||||
@ -171,6 +204,7 @@ COMMANDS = {
|
|||||||
"news": run_news,
|
"news": run_news,
|
||||||
"cat": run_cat,
|
"cat": run_cat,
|
||||||
"morning": run_morning,
|
"morning": run_morning,
|
||||||
|
"logs": run_logs,
|
||||||
"help": lambda: print_help(),
|
"help": lambda: print_help(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
console_commands/logs.py
Normal file
28
console_commands/logs.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""Просмотр последних строк лога."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
LOG_FILE = Path("logs/bot.log")
|
||||||
|
DEFAULT_LINES = 20
|
||||||
|
|
||||||
|
|
||||||
|
def logs(stop_event, bot, lines: int = DEFAULT_LINES):
|
||||||
|
"""Показать последние строки лога (tail -N)."""
|
||||||
|
if stop_event.is_set():
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not LOG_FILE.exists():
|
||||||
|
print(f"Файл лога не найден: {LOG_FILE}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
||||||
|
all_lines = f.readlines()
|
||||||
|
tail = all_lines[-lines:] if len(all_lines) > lines else all_lines
|
||||||
|
|
||||||
|
print(f"\nПоследние {len(tail)} строк {LOG_FILE}:")
|
||||||
|
print("-" * 40)
|
||||||
|
print("".join(tail), end="")
|
||||||
|
print("-" * 40)
|
||||||
|
except OSError as e:
|
||||||
|
print(f"Ошибка чтения лога: {e}")
|
||||||
23
console_commands/reload.py
Normal file
23
console_commands/reload.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"""Горячая перезагрузка cogs без остановки бота."""
|
||||||
|
|
||||||
|
from commands import ALL_COMMANDS
|
||||||
|
|
||||||
|
|
||||||
|
def reload(stop_event, bot):
|
||||||
|
"""Перезагрузить все cogs бота."""
|
||||||
|
if stop_event.is_set():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Удалить все текущие cogs
|
||||||
|
cog_names = list(bot.cogs.keys())
|
||||||
|
for cog_name in cog_names:
|
||||||
|
bot.remove_cog(cog_name)
|
||||||
|
|
||||||
|
# Пересоздать и добавить все cogs
|
||||||
|
reloaded = []
|
||||||
|
for cog_class in ALL_COMMANDS:
|
||||||
|
cog = cog_class()
|
||||||
|
bot.add_cog(cog)
|
||||||
|
reloaded.append(cog.__class__.__name__)
|
||||||
|
|
||||||
|
print(f"\nПерезагружено cogs: {', '.join(reloaded)}")
|
||||||
22
console_commands/trigger_morning.py
Normal file
22
console_commands/trigger_morning.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"""Ручной запуск morning-дайджеста в канал."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
|
def trigger_morning(stop_event, bot):
|
||||||
|
"""Ручной запуск morning-дайджеста через scheduler."""
|
||||||
|
if stop_event.is_set():
|
||||||
|
return None
|
||||||
|
|
||||||
|
scheduler = getattr(bot, "_scheduler", None)
|
||||||
|
if scheduler is None:
|
||||||
|
print("Планировщик не запущен. Утренний дайджест недоступен.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _run():
|
||||||
|
await scheduler._run_morning()
|
||||||
|
|
||||||
|
# Запустить async-метод в event loop бота
|
||||||
|
future = asyncio.run_coroutine_threadsafe(_run(), bot.loop)
|
||||||
|
future.result()
|
||||||
|
print("\nУтренний дайджест запущен вручную.")
|
||||||
@ -55,11 +55,11 @@ class TestAdminImport:
|
|||||||
|
|
||||||
assert hasattr(admin, "main")
|
assert hasattr(admin, "main")
|
||||||
assert hasattr(admin, "COMMANDS")
|
assert hasattr(admin, "COMMANDS")
|
||||||
assert len(admin.COMMANDS) == 5
|
assert len(admin.COMMANDS) == 6
|
||||||
|
|
||||||
def test_commands_mapping(self):
|
def test_commands_mapping(self):
|
||||||
"""Все команды зарегистрированы в маппинге."""
|
"""Все команды зарегистрированы в маппинге."""
|
||||||
from console_commands.admin import COMMANDS
|
from console_commands.admin import COMMANDS
|
||||||
|
|
||||||
expected = {"pogoda", "news", "cat", "morning", "help"}
|
expected = {"pogoda", "news", "cat", "morning", "logs", "help"}
|
||||||
assert set(COMMANDS.keys()) == expected
|
assert set(COMMANDS.keys()) == expected
|
||||||
|
|||||||
87
tests/test_console_logs.py
Normal file
87
tests/test_console_logs.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
"""Тесты для console_commands/logs.py."""
|
||||||
|
import importlib
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
class TestLogsCommand:
|
||||||
|
"""Тесты консольной команды logs."""
|
||||||
|
|
||||||
|
def _get_module(self):
|
||||||
|
"""Импортировать модуль logs."""
|
||||||
|
return importlib.import_module("console_commands.logs")
|
||||||
|
|
||||||
|
def _get_logs_func(self):
|
||||||
|
"""Импортировать функцию logs."""
|
||||||
|
from console_commands.logs import logs
|
||||||
|
return logs
|
||||||
|
|
||||||
|
def test_logs_no_file(self, capfd):
|
||||||
|
"""Выводит сообщение если файл лога не существует."""
|
||||||
|
stop_event = MagicMock()
|
||||||
|
stop_event.is_set.return_value = False
|
||||||
|
bot = MagicMock()
|
||||||
|
|
||||||
|
mod = self._get_module()
|
||||||
|
original = mod.LOG_FILE
|
||||||
|
try:
|
||||||
|
mod.LOG_FILE = Path("/nonexistent/bot.log")
|
||||||
|
self._get_logs_func()(stop_event, bot)
|
||||||
|
finally:
|
||||||
|
mod.LOG_FILE = original
|
||||||
|
|
||||||
|
captured = capfd.readouterr()
|
||||||
|
assert "не найден" in captured.out
|
||||||
|
|
||||||
|
def test_logs_reads_last_lines(self, capfd, tmp_path):
|
||||||
|
"""Выводит последние N строк лога."""
|
||||||
|
log_file = tmp_path / "bot.log"
|
||||||
|
lines = [f"Line {i}\n" for i in range(30)]
|
||||||
|
log_file.write_text("".join(lines))
|
||||||
|
|
||||||
|
stop_event = MagicMock()
|
||||||
|
stop_event.is_set.return_value = False
|
||||||
|
bot = MagicMock()
|
||||||
|
|
||||||
|
mod = self._get_module()
|
||||||
|
original = mod.LOG_FILE
|
||||||
|
try:
|
||||||
|
mod.LOG_FILE = log_file
|
||||||
|
self._get_logs_func()(stop_event, bot, lines=5)
|
||||||
|
finally:
|
||||||
|
mod.LOG_FILE = original
|
||||||
|
|
||||||
|
captured = capfd.readouterr()
|
||||||
|
assert "Line 25" in captured.out
|
||||||
|
assert "Line 29" in captured.out
|
||||||
|
assert "Line 1" not in captured.out
|
||||||
|
|
||||||
|
def test_logs_fewer_lines_than_requested(self, capfd, tmp_path):
|
||||||
|
"""Если строк меньше N, выводит все."""
|
||||||
|
log_file = tmp_path / "bot.log"
|
||||||
|
log_file.write_text("Line 1\nLine 2\n")
|
||||||
|
|
||||||
|
stop_event = MagicMock()
|
||||||
|
stop_event.is_set.return_value = False
|
||||||
|
bot = MagicMock()
|
||||||
|
|
||||||
|
mod = self._get_module()
|
||||||
|
original = mod.LOG_FILE
|
||||||
|
try:
|
||||||
|
mod.LOG_FILE = log_file
|
||||||
|
self._get_logs_func()(stop_event, bot, lines=10)
|
||||||
|
finally:
|
||||||
|
mod.LOG_FILE = original
|
||||||
|
|
||||||
|
captured = capfd.readouterr()
|
||||||
|
assert "Line 1" in captured.out
|
||||||
|
assert "Line 2" in captured.out
|
||||||
|
|
||||||
|
def test_logs_stop_event(self):
|
||||||
|
"""Не выполняется если stop_event установлен."""
|
||||||
|
stop_event = MagicMock()
|
||||||
|
stop_event.is_set.return_value = True
|
||||||
|
bot = MagicMock()
|
||||||
|
|
||||||
|
result = self._get_logs_func()(stop_event, bot)
|
||||||
|
assert result is None
|
||||||
54
tests/test_console_reload.py
Normal file
54
tests/test_console_reload.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"""Тесты для console_commands/reload.py."""
|
||||||
|
import importlib
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
class TestReloadCommand:
|
||||||
|
"""Тесты консольной команды reload."""
|
||||||
|
|
||||||
|
def _get_module(self):
|
||||||
|
"""Импортировать модуль reload."""
|
||||||
|
return importlib.reload(importlib.import_module("console_commands.reload"))
|
||||||
|
|
||||||
|
def test_reload_removes_and_adds_cogs(self, capfd):
|
||||||
|
"""Удаляет старые cogs и добавляет новые."""
|
||||||
|
stop_event = MagicMock()
|
||||||
|
stop_event.is_set.return_value = False
|
||||||
|
|
||||||
|
bot = MagicMock()
|
||||||
|
bot.cogs = {"TestCog1": MagicMock(), "TestCog2": MagicMock()}
|
||||||
|
|
||||||
|
cog_class1 = MagicMock()
|
||||||
|
cog_class1.__name__ = "TestCog1"
|
||||||
|
cog_instance1 = MagicMock()
|
||||||
|
cog_instance1.__class__.__name__ = "TestCog1"
|
||||||
|
cog_class1.return_value = cog_instance1
|
||||||
|
|
||||||
|
cog_class2 = MagicMock()
|
||||||
|
cog_class2.__name__ = "TestCog2"
|
||||||
|
cog_instance2 = MagicMock()
|
||||||
|
cog_instance2.__class__.__name__ = "TestCog2"
|
||||||
|
cog_class2.return_value = cog_instance2
|
||||||
|
|
||||||
|
mod = self._get_module()
|
||||||
|
original = mod.ALL_COMMANDS
|
||||||
|
try:
|
||||||
|
mod.ALL_COMMANDS = [cog_class1, cog_class2]
|
||||||
|
mod.reload(stop_event, bot)
|
||||||
|
finally:
|
||||||
|
mod.ALL_COMMANDS = original
|
||||||
|
|
||||||
|
assert bot.remove_cog.call_count == 2
|
||||||
|
assert bot.add_cog.call_count == 2
|
||||||
|
captured = capfd.readouterr()
|
||||||
|
assert "Перезагружено" in captured.out
|
||||||
|
|
||||||
|
def test_reload_stop_event(self):
|
||||||
|
"""Не выполняется если stop_event установлен."""
|
||||||
|
stop_event = MagicMock()
|
||||||
|
stop_event.is_set.return_value = True
|
||||||
|
bot = MagicMock()
|
||||||
|
|
||||||
|
mod = self._get_module()
|
||||||
|
result = mod.reload(stop_event, bot)
|
||||||
|
assert result is None
|
||||||
53
tests/test_console_trigger_morning.py
Normal file
53
tests/test_console_trigger_morning.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"""Тесты для console_commands/trigger_morning.py."""
|
||||||
|
import importlib
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
|
||||||
|
class TestTriggerMorningCommand:
|
||||||
|
"""Тесты консольной команды trigger morning."""
|
||||||
|
|
||||||
|
def test_trigger_morning_no_scheduler(self, capfd):
|
||||||
|
"""Выводит сообщение если scheduler не найден."""
|
||||||
|
stop_event = MagicMock()
|
||||||
|
stop_event.is_set.return_value = False
|
||||||
|
bot = MagicMock()
|
||||||
|
# Убедимся что _scheduler не установлен через getattr
|
||||||
|
type(bot)._scheduler = None
|
||||||
|
|
||||||
|
mod = importlib.import_module("console_commands.trigger_morning")
|
||||||
|
mod.trigger_morning(stop_event, bot)
|
||||||
|
|
||||||
|
captured = capfd.readouterr()
|
||||||
|
assert "не запущен" in captured.out
|
||||||
|
|
||||||
|
def test_trigger_morning_stop_event(self):
|
||||||
|
"""Не выполняется если stop_event установлен."""
|
||||||
|
stop_event = MagicMock()
|
||||||
|
stop_event.is_set.return_value = True
|
||||||
|
bot = MagicMock()
|
||||||
|
|
||||||
|
mod = importlib.import_module("console_commands.trigger_morning")
|
||||||
|
result = mod.trigger_morning(stop_event, bot)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_trigger_morning_calls_scheduler(self, capfd):
|
||||||
|
"""Вызывает scheduler._run_morning()."""
|
||||||
|
stop_event = MagicMock()
|
||||||
|
stop_event.is_set.return_value = False
|
||||||
|
|
||||||
|
scheduler = MagicMock()
|
||||||
|
bot = MagicMock()
|
||||||
|
bot._scheduler = scheduler
|
||||||
|
|
||||||
|
mock_future = MagicMock()
|
||||||
|
mock_future.result.return_value = None
|
||||||
|
|
||||||
|
mod = importlib.import_module("console_commands.trigger_morning")
|
||||||
|
with patch.object(
|
||||||
|
mod, "asyncio",
|
||||||
|
MagicMock(run_coroutine_threadsafe=MagicMock(return_value=mock_future))
|
||||||
|
):
|
||||||
|
mod.trigger_morning(stop_event, bot)
|
||||||
|
|
||||||
|
captured = capfd.readouterr()
|
||||||
|
assert "запущен вручную" in captured.out
|
||||||
Loading…
x
Reference in New Issue
Block a user