Files
schedule/tests/test_parsing.py
Vladislav Kopnin 34b975da69 Перенос в master всего, кроме админки: ТОП-группы, порядок групп, запуск без Docker
Админка осталась отдельной ветвью admin-panel; в master её нет (ни кода, ни
страниц, ни переменных окружения, ни тома настроек).

Что перенесено:
- parsing: имена групп вида «ИВТ-ТОП-11БО» распознаются, порядок групп = порядок
  столбцов таблицы (похожие заголовки с пробелами группой не считаются);
- main: GET /groups отдаёт группы в порядке столбцов; интервал обновления из
  SCHEDULE_INTERVAL_MINUTES (со старым SCHEDULE_CACHE_TTL как fallback);
- cache: по интервалу сверяем modified — если файл не менялся, не скачиваем;
  при загрузке считаем sha256 и пишем в лог и /status, изменился ли файл;
- source: запросы к Яндекс.Диску с no-cache и меткой времени;
- frontend: ответы API с Cache-Control: no-store, общий .env из корня проекта
  (и backend, и frontend), страницы/прокси админки нет;
- Dockerfile/compose/.env.example: без ADMIN_* и без тома настроек; в api-образ
  попадают все модули backend (проверяется тестом test_deploy);
- Makefile: make help/venv/run/run-api/run-front/test + docker-цели — «make run»
  поднимает всё локально без Docker;
- README: быстрый старт, полный список зависимостей, запуск локально и в Docker;
- deploy/: пример nginx и инструкция «второй стенд рядом с основным».

Тесты: 114 (удалены админские), все зелёные.
2026-10-06 15:15:44 +03:00

196 lines
7.6 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.

"""Тесты разбора листов: группы, дни/пары/чётность, индексы, вспомогательные листы."""
from datetime import date
import pytest
from fixtures import fixture_sheets, make_sheets
from schedule import parsing
from schedule.errors import ScheduleError
@pytest.fixture()
def sheets():
return make_sheets(fixture_sheets())
@pytest.fixture()
def data(sheets):
return parsing.parse_schedule(sheets)
def test_hidden_group_excluded(data):
assert set(data) == {"ИВТ-11БО", "ИТ-12БО"}
def test_group_names_with_top_and_sheet_order():
"""ТОП-группы распознаются, а порядок групп — как столбцы в таблице."""
sheets = fixture_sheets()
header = sheets[parsing.MAIN_SHEET].rows[0]
header[6] = "ИТ-ТОП-11БО\nсеместр до 11.01.27"
header[7] = "АА-11БО\nсеместр до 29.12.26"
data = parsing.parse_schedule(make_sheets(sheets))
assert list(data) == ["ИТ-ТОП-11БО", "АА-11БО"] # не по алфавиту, а по столбцам
assert data["ИТ-ТОП-11БО"]["days"]["понедельник"][1]["ч"]
def test_not_a_group_headers_ignored():
"""Похожие, но не групповые заголовки столбцами групп не становятся."""
sheets = fixture_sheets()
header = sheets[parsing.MAIN_SHEET].rows[0]
header[9] = "ИВТ 11БО" # пробел вместо дефиса — это не имя группы
data = parsing.parse_schedule(make_sheets(sheets))
assert set(data) == {"ИВТ-11БО", "ИТ-12БО"}
def test_semester(data):
assert data["ИВТ-11БО"]["semester_until"] == "11.01.27"
assert data["ИТ-12БО"]["semester_until"] == "29.12.26"
def test_day_period_parity(data):
mon = data["ИВТ-11БО"]["days"]["понедельник"]
p1 = mon[1]
assert p1["time"] == "9:00-10:35"
assert p1["ч"] is not None and p1["з"] is not None
assert "Математика" in p1["ч"]
assert "216" in p1["з"]
def test_empty_parity_slot_kept(data):
mon = data["ИТ-12БО"]["days"]["понедельник"]
assert mon[1]["з"] is None
# время на пустой слот подставлено
assert mon[1]["time"] == "9:00-10:35"
def test_lessons_preparsed(data):
mon = data["ИВТ-11БО"]["days"]["понедельник"]
lesson = mon[1]["lessons"]["ч"][0]
assert lesson["subject"] == "Математика"
assert lesson["teacher"] == "Смирнов А.В."
assert lesson["room"] == "ауд. 215"
assert lesson["note"] == "пара 14.09 не будет"
# пустое parity -> None
assert data["ИТ-12БО"]["days"]["понедельник"][1]["lessons"]["з"] is None
def test_marker_parsed(data):
mon_g1 = data["ИВТ-11БО"]["days"]["понедельник"]
mon_g2 = data["ИТ-12БО"]["days"]["понедельник"]
assert mon_g1[2]["lessons"]["ч"][0]["marker"] == "к.в. - 2"
assert mon_g2[2]["lessons"]["з"][0]["marker"] == "к.в."
def test_tuesday_only_ch(data):
tue = data["ИВТ-11БО"]["days"]["вторник"]
assert tue[1]["ч"] and "Биология" in tue[1]["ч"]
assert tue[1]["з"] is None
def test_missing_main_sheet(sheets):
del sheets[parsing.MAIN_SHEET]
with pytest.raises(ScheduleError):
parsing.parse_schedule(sheets)
def test_odd_day_block_warns(caplog):
sheets = make_sheets(fixture_sheets())
# уберём вторую строку понедельника (пара 1, з) -> нечётное число строк
rows = sheets[parsing.MAIN_SHEET].rows
# строки: 0 header, 1 с/по, 2..5 понедельник, 6..7 вторник
del rows[3]
with caplog.at_level("WARNING"):
result = parsing.parse_schedule(sheets)
assert any("сдвинуться" in r.message for r in caplog.records)
# разбор всё равно завершается
assert "ИВТ-11БО" in result
def test_week_ranges(sheets):
weeks = parsing.extract_week_ranges(sheets)
assert len(weeks) == 8
assert weeks[0] == {"parity": "ч", "start": date(2026, 8, 31), "end": date(2026, 9, 6)}
assert weeks[1]["parity"] == "з"
assert weeks[-2]["parity"] == "ч"
def test_current_parity():
sheets = make_sheets(fixture_sheets())
weeks = parsing.extract_week_ranges(sheets)
assert parsing.current_parity(weeks, date(2026, 9, 1)) == "ч"
assert parsing.current_parity(weeks, date(2026, 9, 9)) == "з"
assert parsing.current_parity(weeks, date(2030, 1, 1)) is None
def test_teacher_room_index(data):
teachers, rooms = parsing.build_teacher_room_index(data)
assert set(teachers) == {"Смирнов А.В.", "Иванов И.И.", "Петров П.П.", "Сидорова С.С."}
smirnov = teachers["Смирнов А.В."]["понедельник"]["1"]
assert len(smirnov["ч"]) == 1 and len(smirnov["з"]) == 1
assert smirnov["ч"][0]["subject"] == "Математика"
assert smirnov["ч"][0]["room"] == "ауд. 215"
assert smirnov["ч"][0]["groups"] == ["ИВТ-11БО"]
petrov_zn = teachers["Петров П.П."]["понедельник"]["2"]["з"]
assert petrov_zn[0]["marker"] == "к.в."
assert set(rooms) == {"ауд. 215", "ауд. 216", "ауд. 301", "ауд. 220", "ауд. 201"}
room_301 = rooms["ауд. 301"]["понедельник"]
assert room_301["1"]["ч"][0]["subject"] == "Физика"
assert room_301["1"]["ч"][0]["teacher"] == "Иванов И.И."
assert room_301["2"]["ч"][0]["groups"] == ["ИВТ-11БО"]
def test_day_times(data):
times = parsing.build_day_times(data)
assert times["понедельник"] == {"1": "9:00-10:35", "2": "10:45-12:20"}
assert times["вторник"] == {"1": "9:00-10:35"}
def test_group_schedule_shape(data):
out = parsing.build_group_schedule(data, "ИВТ-11БО")
assert out["group"] == "ИВТ-11БО"
assert out["semester_until"] == "11.01.27"
p1 = out["days"]["понедельник"]["1"]
assert p1["time"] == "9:00-10:35"
assert "raw" in p1["ч"] and "lessons" in p1["ч"]
assert p1["з"]["lessons"][0]["room"] == "ауд. 216"
assert parsing.build_group_schedule(data, "НЕТ-11БО") is None
def test_rooms_info(sheets):
info = parsing.parse_rooms_info(sheets)
assert info == {
"ауд. 215": {"room": "ауд. 215", "info": "комп.класс, 12 мест"},
"ауд. 301": {"room": "ауд. 301", "info": "интерактивная панель, 24 места"},
"ауд. 220": {"room": "ауд. 220", "info": "20 мест"},
}
def test_teachers_full(sheets):
full = parsing.parse_teachers_full(sheets)
assert full == {
"Смирнов А.В.": "Смирнов Александр Викторович",
"Иванов И.И.": "Иванов Иван Иванович",
}
def test_sort_rooms():
assert parsing.sort_rooms(["ауд. 10", "ауд. 9", "ауд. 100", "зал"]) == [
"ауд. 9", "ауд. 10", "ауд. 100", "зал",
]
def test_build_room_schedule(data):
_, rooms = parsing.build_teacher_room_index(data)
times = parsing.build_day_times(data)
out = parsing.build_room_schedule(rooms, "ауд. 301", times)
# дни с расписанием плюс дни, где есть время пар (даже без занятий)
assert set(out["days"]) == {"понедельник", "вторник"}
assert out["days"]["понедельник"]["1"]["ч"][0]["subject"] == "Физика"
assert parsing.build_room_schedule(rooms, "ауд. 999", times) is None