discordBot/tests/test_morning_runner.py
deadzilla 8ee5ed669f Исправление планировщика morning: tasks.loop → asyncio.sleep
- Scheduler больше не опрашивает каждую секунду (tasks.loop мог пропустить
  момент срабатывания из-за неточности тайминга). Теперь спит ровно до
  целевого времени через asyncio.sleep, затем запускает morning и повторяет.
- Добавлено логирование ожидания и срабатывания расписания.
- Перенес инициализацию планировщика из on_ready в on_guild_available —
  гарантирует, что кэш гильдии загружен до запуска Scheduler.
- Обновлены тесты под новый Scheduler (asyncio.create_task вместо tasks.loop).
- README.md: исправлены пути admin.py → console_commands/admin.py для docker exec.
- docker-compose.yml: добавлены переменные LOG_LEVEL и rate-limit конфиги.
- console_commands/__init__.py: переименованы help → console_help,
  reload → reload_cogs (избежал конфликта с built-in и уточнил имена).
2026-06-16 22:10:40 +05:00

193 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Тесты для utils/morning_runner.py — Scheduler и run_morning."""
import asyncio
from datetime import date, datetime
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
import discord
import pytest
from utils.morning_runner import Scheduler, run_morning
class TestSchedulerInit:
"""Тесты инициализации Scheduler."""
def test_init_sets_morning_time(self):
"""Инициализация должна устанавливать время."""
bot = AsyncMock()
with patch("asyncio.create_task") as mock_task:
scheduler = Scheduler(bot, "08:30")
assert scheduler.morning_time == "08:30"
def test_init_default_morning_time(self):
"""Инициализация с дефолтным временем."""
bot = AsyncMock()
with patch("asyncio.create_task") as mock_task:
scheduler = Scheduler(bot)
assert scheduler.morning_time == "07:00"
def test_init_creates_task(self):
"""Инициализация должна создавать asyncio.Task."""
bot = AsyncMock()
with patch("asyncio.create_task") as mock_task:
scheduler = Scheduler(bot)
mock_task.assert_called_once()
assert scheduler._task is not None
class TestSchedulerCalculateNextRun:
"""Тесты расчёта следующего запуска."""
def test_next_run_today_before_time(self):
"""Если сейчас раньше времени — вернуть сегодня."""
bot = AsyncMock()
with patch("asyncio.create_task"):
scheduler = Scheduler(bot, "14:00")
now = datetime(2026, 5, 29, 10, 0, 0)
next_run = scheduler._calculate_next_run(now)
assert next_run == datetime(2026, 5, 29, 14, 0, 0)
def test_next_run_tomorrow_after_time(self):
"""Если сейчас позже времени — вернуть завтра."""
bot = AsyncMock()
with patch("asyncio.create_task"):
scheduler = Scheduler(bot, "14:00")
now = datetime(2026, 5, 29, 15, 0, 0)
next_run = scheduler._calculate_next_run(now)
assert next_run == datetime(2026, 5, 30, 14, 0, 0)
def test_next_run_exact_time(self):
"""Если сейчас ровно время — вернуть завтра."""
bot = AsyncMock()
with patch("asyncio.create_task"):
scheduler = Scheduler(bot, "14:00")
now = datetime(2026, 5, 29, 14, 0, 0)
next_run = scheduler._calculate_next_run(now)
assert next_run == datetime(2026, 5, 30, 14, 0, 0)
class TestSchedulerStartStop:
"""Тесты запуска/остановки планировщика."""
def test_start_starts_task(self):
"""start() должен запустить task."""
bot = AsyncMock()
with patch("asyncio.create_task"):
scheduler = Scheduler(bot)
assert scheduler._running is True
def test_stop_stops_task(self):
"""stop() должен остановить task."""
bot = AsyncMock()
with patch("asyncio.create_task"):
scheduler = Scheduler(bot)
scheduler.stop()
assert scheduler._running is False
def test_double_start_no_duplicate(self):
"""Повторный start не должен создавать второй task."""
bot = AsyncMock()
with patch("asyncio.create_task") as mock_task:
scheduler = Scheduler(bot)
scheduler.start() # второй вызов
# create_task вызван только при инициализации
assert mock_task.call_count == 1
class TestRunMorning:
"""Тесты run_morning."""
@pytest.mark.asyncio
async def test_run_morning_sends_embed(self):
"""run_morning должен отправлять embed в канал."""
bot = AsyncMock()
channel = AsyncMock()
channel.name = "test-channel"
channel.guild.me = MagicMock()
channel.permissions_for.return_value.send_messages = True
weather_data = {
"current_condition": [
{"temp_C": "20", "FeelsLikeC": "22", "weatherDesc": [{"value": "Clear"}], "humidity": "50", "windspeedKmph": "10", "pressure": "1013"}
]
}
articles = [{"title": "Test", "link": "http://test.com", "pub_date": "Mon, 01 Jan 2026 00:00:00 GMT", "creator": "", "tags": []}]
posts = [{"title": "Test", "link": "http://test.com", "pub_date": "Mon, 01 Jan 2026 00:00:00 GMT", "creator": "", "tags": []}]
with patch("utils.morning_runner.fetch_weather", new=AsyncMock(return_value=weather_data)), \
patch("utils.morning_runner.fetch_rss", new=AsyncMock(side_effect=[articles, posts])), \
patch("utils.morning_runner.fetch_cat", new=AsyncMock(return_value="http://cat.jpg")), \
patch("utils.morning_runner.discord.Embed") as mock_embed:
await run_morning(bot, channel)
channel.send.assert_called_once()
call_args = channel.send.call_args[1]
assert "embed" in call_args
assert call_args["embed"] is not None
class TestRunMorningWithFallback:
"""Тесты fallback в пустом embed."""
@pytest.mark.asyncio
async def test_run_morning_empty_embed_fallback(self):
"""run_morning должен добавлять fallback сообщение при пустых данных."""
bot = AsyncMock()
channel = AsyncMock()
channel.name = "test-channel"
channel.guild.me = MagicMock()
channel.permissions_for.return_value.send_messages = True
# Все API возвращают None/пусто
with patch("utils.morning_runner.fetch_weather", new=AsyncMock(return_value=None)), \
patch("utils.morning_runner.fetch_rss", new=AsyncMock(return_value=None)), \
patch("utils.morning_runner.fetch_cat", new=AsyncMock(return_value=None)), \
patch("utils.morning_runner.discord.Embed") as mock_embed_class:
embed_mock = AsyncMock()
mock_embed_class.return_value = embed_mock
await run_morning(bot, channel)
# Убедимся, что send был вызван
channel.send.assert_called_once()
call_args = channel.send.call_args[1]
assert "embed" in call_args
# Проверяем, что description содержит fallback сообщение
embed_description = call_args["embed"].description
assert "Не удалось получить данные из внешних источников" in embed_description
@pytest.mark.asyncio
async def test_run_morning_only_weather_data(self):
"""run_morning должен корректно обрабатывать только погоду без новостей."""
bot = AsyncMock()
channel = AsyncMock()
channel.name = "test-channel"
channel.guild.me = MagicMock()
channel.permissions_for.return_value.send_messages = True
weather_data = {
"current_condition": [
{"temp_C": "20", "FeelsLikeC": "22", "weatherDesc": [{"value": "Clear"}], "humidity": "50", "windspeedKmph": "10", "pressure": "1013"}
]
}
with patch("utils.morning_runner.fetch_weather", new=AsyncMock(return_value=weather_data)), \
patch("utils.morning_runner.fetch_rss", new=AsyncMock(side_effect=[None, None])), \
patch("utils.morning_runner.fetch_cat", new=AsyncMock(return_value=None)):
await run_morning(bot, channel)
channel.send.assert_called_once()
call_args = channel.send.call_args[1]
assert "embed" in call_args
# Проверяем, что в embed есть только погода и нет fallback сообщения
embed_description = call_args["embed"].description
assert "Погода в Магнитогорске" in embed_description
assert "Не удалось получить данные из внешних источников" not in embed_description