Тесты и инфраструктура

- tests/: 104 теста — lessons (все форматы ячеек), xlsx (мержи/скрытые
  столбцы/красный текст/даты), parsing (группы/недели/индексы), API (TestClient,
  TTL/modified/stale-while-error, 404/400/502)
- xlsx_builder.py: генерация xlsx в памяти для тестов без openpyxl
- docker-compose: healthz-хелскчеки (не блокируют старт), SCHEDULE_LINK
- Makefile: target test
- requirements: закреплённые версии
- .gitignore: __pycache__/, .venv/ и т.д.
This commit is contained in:
2026-09-09 11:33:58 +03:00
parent 2febf9b4ff
commit 9c9875579c
14 changed files with 1122 additions and 18 deletions

173
tests/test_parsing.py Normal file
View File

@@ -0,0 +1,173 @@
"""Тесты разбора листов: группы, дни/пары/чётность, индексы, вспомогательные листы."""
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_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