Задачи Sprint 1 (Critical Fixes): - [1.1] Fallback в пустом embed (!morning) — добавлена проверка has_real_data и fallback сообщение при отключении внешних API - [1.2] Обработка ошибок токена/сети в bot.run() — raise_exception=True + логирование LoginFailure, HTTPException, Exception - [1.3] Рефакторинг парсинга погоды — вынесено в format_weather_data_for_console(), убран дублирующий код из 2 файлов Изменения: • utils/morning_runner.py — добавлена проверка has_real_data после формирования description_lines • bot.py — обработчики исключений для запуска бота с детализированным логированием и пользовательскими сообщениями • utils/pogoda.py — новая функция format_weather_data_for_console() для центрального форматирования погодных данных • console_commands/pogoda.py — замена 15 строк дублирующейся логики на вызов единой функции (24→8 строк) • console_commands/morning.py — аналогичные изменения для команды morning (19→13 строк с погодой) • tests/test_morning_runner.py — +2 теста для fallback сценариев empty embed и only weather data • tests/test_bot.py — новый файл с 2 тестами на проверку кода обработки ошибок • tests/test_pogoda.py — +6 тестов для format_weather_data_for_console() Статистика тестирования: • Общее количество тестов: 200 (было 190) • Новые тесты: 10 • Все тесты проходят успешно Примечания: - Сообщения без восклицательных знаков согласно preferencem
311 lines
13 KiB
Python
311 lines
13 KiB
Python
import pytest
|
||
from utils.pogoda import translate_weather, pressure_to_mmhg, wmo_to_russian, format_weather_data_for_console
|
||
|
||
|
||
class TestFormatWeatherDataForConsole:
|
||
"""Тесты функции format_weather_data_for_console()."""
|
||
|
||
def test_format_valid_data(self):
|
||
"""Полные данные должны быть отформатированы корректно."""
|
||
data = {
|
||
"current_condition": [{
|
||
"temp_C": "25",
|
||
"FeelsLikeC": "26",
|
||
"weatherDesc": [{"value": "Clear"}],
|
||
"humidity": "45",
|
||
"windspeedKmph": "10",
|
||
"pressure": "1013",
|
||
}]
|
||
}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
|
||
assert isinstance(result, list), "Результат должен быть списком строк"
|
||
assert len(result) == 5, "Должно быть 5 полей погоды"
|
||
assert "Температура: 25°C (ощущается как 26°C)" in result[0]
|
||
assert "Описание: Ясно" in result[1]
|
||
assert "Влажность: 45%" in result[2]
|
||
assert "Ветер: 2.8 м/с" in result[3] # 10 / 3.6 = 2.777... ≈ 2.8
|
||
assert "Давление: 759.8 мм рт. ст." in result[4]
|
||
|
||
def test_format_empty_data(self):
|
||
"""Пустые данные должны возвращать None."""
|
||
data = {
|
||
"current_condition": [{}]
|
||
}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
|
||
assert result is None, "Пустые данные должны возвращать None"
|
||
|
||
def test_format_missing_current_condition(self):
|
||
"""Отсутствие current_condition должно вернуть None."""
|
||
data = {}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
|
||
assert result is None, "Отсутствие current_condition должно вернуть None"
|
||
|
||
def test_format_with_dashes(self):
|
||
"""Неизвестные значения должны отображаться как '—'."""
|
||
data = {
|
||
"current_condition": [{
|
||
"temp_C": "—",
|
||
"FeelsLikeC": "—",
|
||
"weatherDesc": [{"value": "—"}],
|
||
"humidity": "—",
|
||
"windspeedKmph": "—",
|
||
"pressure": "—",
|
||
}]
|
||
}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
|
||
assert isinstance(result, list), "Результат должен быть списком строк"
|
||
assert "Температура: —°C (ощущается как —°C)" in result[0]
|
||
assert "Описание: —" in result[1]
|
||
assert "Влажность: —%" in result[2]
|
||
assert "Ветер: — м/с" in result[3]
|
||
assert "Давление: — мм рт. ст." in result[4]
|
||
|
||
def test_format_wind_conversion(self):
|
||
"""Проверка конвертации ветра из км/ч в м/с."""
|
||
data = {
|
||
"current_condition": [{
|
||
"temp_C": "20",
|
||
"FeelsLikeC": "19",
|
||
"weatherDesc": [{"value": "Cloudy"}],
|
||
"humidity": "60",
|
||
"windspeedKmph": "36",
|
||
"pressure": "1000",
|
||
}]
|
||
}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
# 36 / 3.6 = 10.0
|
||
assert "Ветер: 10.0 м/с" in result[3]
|
||
|
||
def test_format_negative_temperature(self):
|
||
"""Отрицательная температура должна отображаться корректно."""
|
||
data = {
|
||
"current_condition": [{
|
||
"temp_C": "-5",
|
||
"FeelsLikeC": "-10",
|
||
"weatherDesc": [{"value": "Snow"}],
|
||
"humidity": "80",
|
||
"windspeedKmph": "20",
|
||
"pressure": "980",
|
||
}]
|
||
}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
|
||
assert isinstance(result, list), "Результат должен быть списком строк"
|
||
assert "Температура: -5°C (ощущается как -10°C)" in result[0]
|
||
|
||
|
||
class TestTranslateWeather:
|
||
|
||
@pytest.mark.parametrize(
|
||
"english, expected",
|
||
[
|
||
("Clear", "Ясно"),
|
||
("Sunny", "Ясно"),
|
||
("Partly cloudy", "Переменная облачность"),
|
||
("Cloudy", "Облачно"),
|
||
("Overcast", "Пасмурно"),
|
||
("Fog", "Туман"),
|
||
("Foggy", "Туманно"),
|
||
("Mist", "Туман"),
|
||
("Haze", "Дымка"),
|
||
("Light rain", "Небольшой дождь"),
|
||
("Moderate rain", "Умеренный дождь"),
|
||
("Heavy rain", "Сильный дождь"),
|
||
("Moderate or heavy rain at times", "Сильный дождь"), # "Heavy rain" совпадает раньше в mapping dict (key in text)
|
||
("Heavy rain at times", "Сильный дождь"),
|
||
("Light snow", "Небольшой снег"),
|
||
("Moderate snow", "Умеренный снег"),
|
||
("Heavy snow", "Сильный снег"),
|
||
("Blowing snow", "Метель"),
|
||
("Light freezing rain", "Лёгкий ледяной дождь"),
|
||
("Heavy freezing rain", "Сильный ледяной дождь"),
|
||
("Moderate or heavy freezing rain", "Сильный ледяной дождь"),
|
||
("Light sleet", "Light sleet"),
|
||
("Moderate or heavy sleet", "Moderate or heavy sleet"),
|
||
("Thundery outbreaks in nearby", "Гроза вблизи"),
|
||
("Patchy rain nearby", "Местами дождь"),
|
||
("Patchy snow nearby", "Местами снег"),
|
||
("Patchy sleet nearby", "Местами слякоть"),
|
||
("Patchy light drizzle", "Местами лёгкая морось"),
|
||
("Moderate or heavy snow in area", "Снег"),
|
||
("Moderate or heavy rain in area", "Дождь"),
|
||
],
|
||
)
|
||
def test_translate_known(self, english, expected):
|
||
"""Известные переводы должны возвращать ожидаемый результат."""
|
||
assert translate_weather(english) == expected
|
||
|
||
@pytest.mark.parametrize(
|
||
"input_value, expected",
|
||
[
|
||
("", "—"),
|
||
(None, "—"),
|
||
(" ", " "), # пробелы не считаются пустыми
|
||
],
|
||
)
|
||
def test_translate_empty(self, input_value, expected):
|
||
"""Пустой или None ввод должен возвращать '—'."""
|
||
assert translate_weather(input_value) == expected
|
||
|
||
def test_translate_unknown_returns_original(self):
|
||
"""Неизвестный перевод должен возвращать оригинальный текст."""
|
||
unknown_text = "Unknown weather condition XYZ"
|
||
assert translate_weather(unknown_text) == unknown_text
|
||
|
||
def test_translate_partial_match(self):
|
||
"""Частичное совпадение ключа в тексте должно сработать."""
|
||
# "Moderate or heavy rain in area" должно найтись в "Light Moderate or heavy rain in area"
|
||
text_with_prefix = "Light Moderate or heavy rain in area"
|
||
assert translate_weather(text_with_prefix) == "Дождь"
|
||
|
||
def test_translate_longer_key_priority(self):
|
||
"""translate_weather ищет key in text, порядок dict важен.
|
||
"Heavy rain" стоит раньше "Moderate or heavy rain at times" в mapping,
|
||
и "heavy rain" in "moderate or heavy rain at times" = True.
|
||
Поэтому совпадёт первым и вернёт "Сильный дождь"."""
|
||
text = "Moderate or heavy rain at times"
|
||
assert translate_weather(text) == "Сильный дождь"
|
||
|
||
def test_translate_case_insensitive(self):
|
||
"""Перевод должен быть регистронезависимым."""
|
||
assert translate_weather("CLEAR") == "Ясно"
|
||
assert translate_weather("partly cloudy") == "Переменная облачность"
|
||
assert translate_weather("HEAVY RAIN") == "Сильный дождь"
|
||
|
||
def test_translate_with_whitespace(self):
|
||
"""Текст с пробелами по краям должен корректно переводиться."""
|
||
assert translate_weather(" Clear ") == "Ясно"
|
||
|
||
|
||
class TestPressureToMMHG:
|
||
"""Тесты функции pressure_to_mmhg() — конвертация давления из мб в мм рт. ст."""
|
||
|
||
@pytest.mark.parametrize(
|
||
"mb, expected",
|
||
[
|
||
(1013, 759.8),
|
||
(1000, 750.1),
|
||
(980, 735.1),
|
||
(1030, 772.6),
|
||
# (0, "—"), # 0 — falsy, возвращается '—' (баг)
|
||
],
|
||
)
|
||
def test_pressure_valid(self, mb, expected):
|
||
"""Валидные числовые значения должны конвертироваться корректно."""
|
||
assert pressure_to_mmhg(mb) == expected
|
||
|
||
@pytest.mark.parametrize(
|
||
"mb, expected",
|
||
[
|
||
("1013", 759.8),
|
||
("1000", 750.1),
|
||
("980", 735.1),
|
||
],
|
||
)
|
||
def test_pressure_string(self, mb, expected):
|
||
"""Строка-число должна конвертироваться корректно."""
|
||
assert pressure_to_mmhg(mb) == expected
|
||
|
||
@pytest.mark.parametrize(
|
||
"input_value, expected",
|
||
[
|
||
("—", "—"),
|
||
(None, "—"),
|
||
("", "—"),
|
||
],
|
||
)
|
||
def test_pressure_invalid(self, input_value, expected):
|
||
"""Невалидные значения должны возвращать '—'."""
|
||
assert pressure_to_mmhg(input_value) == expected
|
||
|
||
def test_pressure_non_numeric_string(self):
|
||
"""Невалидная строка должна возвращать '—'."""
|
||
assert pressure_to_mmhg("abc") == "—"
|
||
|
||
def test_pressure_zero(self):
|
||
"""Нулевое значение — falsy, возвращается '—' (известный баг)."""
|
||
assert pressure_to_mmhg(0) == "—"
|
||
|
||
def test_pressure_negative(self):
|
||
"""Отрицательное значение должно конвертироваться."""
|
||
assert pressure_to_mmhg(-100) == -75.0
|
||
|
||
def test_pressure_float_string(self):
|
||
"""Строка с десятичной точкой должна конвертироваться."""
|
||
assert pressure_to_mmhg("1013.25") == 760.0
|
||
|
||
def test_pressure_very_large(self):
|
||
"""Очень большое значение должно работать."""
|
||
assert pressure_to_mmhg(999999) == 750061.2
|
||
|
||
|
||
class TestWmoToRussian:
|
||
"""Тесты функции wmo_to_russian() — перевод WMO кодов погоды."""
|
||
|
||
@pytest.mark.parametrize(
|
||
"code, expected",
|
||
[
|
||
(0, "Ясно"),
|
||
(1, "Ясно"),
|
||
(2, "Переменная облачность"),
|
||
(3, "Пасмурно"),
|
||
(45, "Туман"),
|
||
(48, "Туман"),
|
||
(51, "Лёгкая морось"),
|
||
(53, "Морось"),
|
||
(55, "Сильная морось"),
|
||
(56, "Ледяная морось"),
|
||
(57, "Сильная ледяная морось"),
|
||
(61, "Небольшой дождь"),
|
||
(63, "Дождь"),
|
||
(65, "Сильный дождь"),
|
||
(66, "Ледяной дождь"),
|
||
(67, "Сильный ледяной дождь"),
|
||
(71, "Небольшой снег"),
|
||
(73, "Снег"),
|
||
(75, "Сильный снег"),
|
||
(77, "Снежная крупа"),
|
||
(80, "Небольшой ливень"),
|
||
(81, "Ливень"),
|
||
(82, "Сильный ливень"),
|
||
(85, "Снежный ливень"),
|
||
(86, "Сильный снежный ливень"),
|
||
(95, "Гроза"),
|
||
(96, "Гроза с градом"),
|
||
(99, "Сильная гроза с градом"),
|
||
],
|
||
)
|
||
def test_wmo_known(self, code, expected):
|
||
"""Известные WMO коды должны возвращать ожидаемый перевод."""
|
||
assert wmo_to_russian(code) == expected
|
||
|
||
def test_wmo_unknown(self):
|
||
"""Неизвестный код должен возвращать 'Неизвестно'."""
|
||
assert wmo_to_russian(999) == "Неизвестно"
|
||
|
||
def test_wmo_negative_code(self):
|
||
"""Отрицательный код должен возвращать 'Неизвестно'."""
|
||
assert wmo_to_russian(-1) == "Неизвестно"
|
||
|
||
def test_wmo_none(self):
|
||
"""None должен возвращать 'Неизвестно'."""
|
||
assert wmo_to_russian(None) == "Неизвестно"
|
||
|
||
def test_wmo_large_code(self):
|
||
"""Очень большой код должен возвращать 'Неизвестно'."""
|
||
assert wmo_to_russian(9999) == "Неизвестно"
|
||
|
||
def test_wmo_float_code(self):
|
||
"""Дробный код — не найдётся в mapping."""
|
||
assert wmo_to_russian(1.5) == "Неизвестно"
|