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

- 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

349
tests/test_lessons.py Normal file
View File

@@ -0,0 +1,349 @@
"""Тесты разбора ячеек занятий: каждый найденный в таблице формат."""
import pytest
from schedule.lessons import Lesson, parse_lesson_cell
R = "\u27EA" # ⟨
Cc = "\u27EB" # ⟩
def lessons(text):
return [l.to_dict() for l in parse_lesson_cell(text)]
def one(text):
ls = parse_lesson_cell(text)
assert len(ls) == 1, f"ожидалося 1 занятие, got: {ls}"
return ls[0].to_dict()
# --- базовые формы -------------------------------------------------------------
def test_simple_three_lines():
assert one("Математика\nСмирнов А.В.\nауд. 215") == {
"marker": "", "subject": "Математика", "teacher": "Смирнов А.В.",
"room": "ауд. 215", "note": "",
}
def test_two_lines_no_room():
assert one("ТВИМС\nКоролев Г.К") == {
"marker": "", "subject": "ТВИМС", "teacher": "Королев Г.К", "room": "", "note": "",
}
def test_single_word_subject():
assert one("ДАСП") == {
"marker": "", "subject": "ДАСП", "teacher": "", "room": "", "note": "",
}
def test_acronym_subject_not_org():
for subject in ("МАИС", "БЖД", "ООП", "ЯПМТ", "ТВиМС", "ФТД"):
result = one(f"{subject}\nГладков А.Н.\nауд. 207")
assert result["subject"] == subject, subject
assert result["teacher"] == "Гладков А.Н."
def test_multiple_teachers_slash():
result = one("Иностранный язык\nМастакова Н.К./Умнова И.В./Чвягина Т.В.\nауд. 305 / 207")
assert result["teacher"] == "Мастакова Н.К./Умнова И.В./Чвягина Т.В."
assert result["room"] == "ауд. 305 / 207"
def test_subject_lines_join():
result = one("Методы компьютерной лингвистики\nв ИИ - 2\nПарамонов И.В.\nауд. 231")
assert result["subject"] == "Методы компьютерной лингвистики в ИИ - 2"
assert result["teacher"] == "Парамонов И.В."
assert result["room"] == "ауд. 231"
# --- маркеры -------------------------------------------------------------------
@pytest.mark.parametrize(
"raw, expected",
[
("к.в. - 2\nФизика\nИванов И.И.\nауд. 301", "к.в. - 2"),
("к.в.-7\nСети Петри\nБашкин В.А.\nауд. 224", "к.в. - 7"),
("к.в.\nХимия\nПетров П.П.\nауд. 201", "к.в."),
("к.в. - 1 \nОсновы тестирования\nОкопный Марк Олегович\nонлайн", "к.в. - 1"),
("к.в.-2 Распределенные технологии\nКорсаков С.В.\nауд. 201", "к.в. - 2"),
],
)
def test_markers(raw, expected):
assert one(raw)["marker"] == expected
def test_marker_alone_line_then_blank():
ls = lessons("к.в. \n\nОсновы веб-технологий\nЛавровский В.А.\nауд. 210")
assert len(ls) == 1
assert ls[0]["marker"] == "к.в."
assert ls[0]["subject"] == "Основы веб-технологий"
def test_marker_inherited():
ls = lessons("к.в. - 3\nA\nБелов А.А.\nауд. 1\n\nB\nБелов А.А.\nауд. 2")
assert [l["marker"] for l in ls] == ["к.в. - 3", "к.в. - 3"]
# --- аудитории и локации ---------------------------------------------------------
def test_room_normalization():
result = one("Математика\nСмирнов А.В.\nауд312")
assert result["room"] == "ауд. 312"
def test_room_with_building():
result = one("Основы права (л)\nКолпазанова Е.В.\nюридический корпус, 3 этаж ауд. 22\nначало в 16:05")
assert result["subject"] == "Основы права (л)"
assert result["teacher"] == "Колпазанова Е.В."
assert result["room"] == "юридический корпус, 3 этаж ауд. 22"
assert result["note"] == "начало в 16:05"
def test_room_with_time_after():
result = one("Бизнес-аналитика\nЕлин А.С.\nонлайн\nс 17:00 до 20:00\nс 8.09.26")
assert result["room"] == "онлайн"
assert "с 17:00 до 20:00" in result["note"]
assert "с 8.09.26" in result["note"]
def test_room_online_with_time_inside():
result = one("Облачные сервисы\nБегунов А.В.\nонлайн (начало в 14:30)")
assert result["room"] == "онлайн"
assert result["note"] == "начало в 14:30"
def test_room_url():
result = one("Физкультура (онлайн)\nhttps://next.uniyar.ac.ru/course/view.php?id=407")
assert result["subject"] == "Физкультура (онлайн)"
assert result["room"].startswith("https://next.uniyar.ac.ru")
assert result["teacher"] == ""
def test_location_instead_of_room():
result = one("Прикладная физическая культура\nТкач О.С.\nпервый этаж, восточное крыло")
assert result["room"] == "первый этаж, восточное крыло"
assert result["note"] == ""
def test_room_paren_note():
result = one("Базы данных\nПолетаев А.Ю.\nауд. 223 (возможно и 221)")
assert result["room"] == "ауд. 223"
assert result["note"] == "возможно и 221"
def test_room_paren_alternative_kept():
result = one("Сетевые технологии Huawei\nКорсаков С.В.\nауд.201 (221)")
assert result["room"] == "ауд. 201 (221)"
assert result["note"] == ""
# --- преподаватели ---------------------------------------------------------------
def test_full_name_teacher():
result = one("Основы тестирования\nОкопный Марк Олегович (БИФИТ)\nонлайн\nначало в 12:00")
assert result["teacher"] == "Окопный Марк Олегович (БИФИТ)"
assert result["room"] == "онлайн"
assert result["note"] == "начало в 12:00"
def test_full_name_with_company():
result = one("Проектный практикум\nЛабеев Игорь Владимирович (Корус консалтинг)\nначало занятия в 18:30 онлайн\nссылка в эл. университете")
assert result["teacher"] == "Лабеев Игорь Владимирович (Корус консалтинг)"
assert result["room"] == "онлайн"
assert "начало занятия в 18:30" in result["note"]
assert "ссылка в эл. университете" in result["note"]
def test_teacher_with_substitute():
result = one("Параллельное программирование\nМещеряков И.А. (Федулов Д.Д.) (начало пары в 14:05)\nауд.213")
assert result["teacher"] == "Мещеряков И.А. (Федулов Д.Д.)"
assert result["room"] == "ауд. 213"
assert result["note"] == "начало пары в 14:05"
def test_teacher_lecture_marker_kept():
result = one("Теория автоматов\nСмирнов А.В. (л)\nауд.312")
assert result["teacher"] == "Смирнов А.В. (л)"
def test_org_as_teacher():
result = one("Управление ИТ-проектами\nПравоТех\nауд. 309")
assert result["teacher"] == "ПравоТех"
assert result["subject"] == "Управление ИТ-проектами"
def test_single_word_org_in_teacher_position():
result = one("Оценка качества ПО\nХинт")
assert result["teacher"] == "Хинт"
assert result["subject"] == "Оценка качества ПО"
def test_org_as_location():
ls = lessons(
"Сети Петри\nБашкин В.А.\nауд. 224\n\nСовременные инструменты промышленной разработки\nКоновал О.В.\nНПО Криста"
)
second = ls[1]
assert second["teacher"] == "Коновал О.В."
assert second["room"] == "НПО Криста"
def test_teacher_and_room_on_one_line():
result = one("Логические контроллеры\nКузьмин Е.В.ауд. 301 (начало в 12:30)")
assert result["teacher"] == "Кузьмин Е.В."
assert result["room"] == "ауд. 301"
assert result["note"] == "начало в 12:30"
def test_initials_without_space():
result = one("Методы сжатия КрасновМ.В.\nауд.231")
assert result["subject"] == "Методы сжатия"
assert result["teacher"] == "КрасновМ.В."
assert result["room"] == "ауд. 231"
def test_inline_subject_teacher_room():
result = one("Сети Петри Башкин В.А. ауд. 224")
assert result["subject"] == "Сети Петри"
assert result["teacher"] == "Башкин В.А."
assert result["room"] == "ауд. 224"
# --- заметки ---------------------------------------------------------------------
def test_red_note():
assert one(f"Математика\nСмирнов А.В.\nауд. 215\n{R}пара 14.09 не будет{Cc}")["note"] == "пара 14.09 не будет"
def test_red_note_on_room_line():
result = one(f"Русский язык\nШаманова М.В.\nауд.312 {R}с 7.09 по 20.09 пары не будет{Cc}")
assert result["room"] == "ауд. 312"
assert result["note"] == "с 7.09 по 20.09 пары не будет"
def test_red_note_multiline_with_embedded_lesson():
raw = (
"Теория автоматов\nСмирнов А.В. (л)\nауд.312\n"
f"{R}занятия начнутся с 21.09\nИностранный язык\nБугрова Е.И.\n"
f"ауд. 312 у ИТ-22БО 7.09 и 14.09 с 12:30{Cc}"
)
result = one(raw)
assert result["subject"] == "Теория автоматов"
assert result["teacher"] == "Смирнов А.В. (л)"
assert result["room"] == "ауд.312" or result["room"] == "ауд. 312"
assert "занятия начнутся с 21.09" in result["note"]
assert "Бугрова Е.И." in result["note"]
def test_red_note_between_teacher_and_room():
raw = f"Математика\nСмирнов А.В.\n{R}(пара 8.09 не будет)\n{Cc}ауд. 203"
result = one(raw)
assert result["teacher"] == "Смирнов А.В."
assert result["room"] == "ауд. 203"
assert result["note"] == "пара 8.09 не будет"
def test_only_red_text_gives_lesson_with_note():
result = one(f"{R}нет занятий{Cc}")
assert result["note"] == "нет занятий"
def test_date_note_not_room():
result = one("Практика в НПО Криста\nс 9.09.2026")
assert result["subject"] == "Практика в НПО Криста"
assert result["room"] == ""
assert result["note"] == "с 9.09.2026"
# --- несколько занятий в одной ячейке ---------------------------------------------
def test_two_lessons_blank_line():
ls = lessons("Сети Петри\nБашкин В.А.\nауд. 224\n\nСовременные инструменты\nКоновал О.В.\nауд. 301")
assert [l["subject"] for l in ls] == ["Сети Петри", "Современные инструменты"]
assert [l["room"] for l in ls] == ["ауд. 224", "ауд. 301"]
def test_two_lessons_room_subject_border():
ls = lessons("Логические контроллеры\nКузьмин Е.В.ауд. 301 (начало в 12:30)\nЧисленные методы\nГлызин С.Д. ауд. 312")
assert [l["subject"] for l in ls] == ["Логические контроллеры", "Численные методы"]
assert [l["teacher"] for l in ls] == ["Кузьмин Е.В.", "Глызин С.Д."]
assert [l["room"] for l in ls] == ["ауд. 301", "ауд. 312"]
def test_group_tag_after_room_is_note():
result = one("Алгебра и геометрия\nСедов А.Г.\nауд.219\nИТ-11")
assert result["room"] == "ауд. 219"
assert result["note"] == "ИТ-11"
def test_lesson_with_red_between_lessons():
ls = lessons(
"Введение в промышленную разработку\nПарамонов И.В.\n"
f"ауд. 220 {R}2.09 пара отменяется{Cc}\n\n"
"Язык UML и CASE-системы\nЛарина Ю.А.\nауд.210"
)
assert ls[0]["note"] == "2.09 пара отменяется"
assert ls[0]["room"] == "ауд. 220"
assert ls[1]["subject"] == "Язык UML и CASE-системы"
# --- пустые и экзотические ----------------------------------------------------------
def test_empty_cell():
assert parse_lesson_cell("") == []
assert parse_lesson_cell(" \n ") == []
def test_cell_with_underscores_subject():
lesson = one("Промышленное программирование\nПарамонов И.В.\nауд. 220")
assert lesson["subject"] == "Промышленное программирование"
def test_lesson_subject_only():
result = one(f"{R}не будет{Cc}")
assert result["subject"] == ""
assert result["note"] == "не будет"
def test_unclosed_paren_in_table():
# в таблице не закрыли скобку вокруг красной заметки
result = one("Основы веб-технологий\nЛавровский В.А. (" + R + "предмет начнется с 8.09)" + Cc + "\nауд. 210")
assert result["teacher"] == "Лавровский В.А."
assert result["room"] == "ауд. 210"
assert result["note"] == "предмет начнется с 8.09"
def test_inline_lesson_after_room_no_blank_line():
raw = (
"к.в. - 2\nАлгоритмы и структуры данных Волченков С.Г. "
+ R + "(14.09 по 30.09 пары нет) " + Cc
+ "ауд.219 \nМетоды сжатия КрасновМ.В. ауд.231"
)
ls = lessons(raw)
assert len(ls) == 2
assert ls[0]["room"] == "ауд. 219"
assert "14.09 по 30.09 пары нет" in ls[0]["note"]
assert ls[1]["subject"] == "Методы сжатия"
assert ls[1]["teacher"] == "КрасновМ.В."
assert ls[1]["room"] == "ауд. 231"
def test_plain_time_paren_line():
result = one("Параллельное программирование (пр)\nМещеряков И.А.\nауд. 213\n(начало пары в 12:30)")
assert result["note"] == "начало пары в 12:30"
def test_full_name_inline_after_subject():
result = one("Основы тестирования веб-приложений Денис Верховцев (Сигма)\nонлайн")
assert result["subject"] == "Основы тестирования веб-приложений"
assert result["teacher"] == "Денис Верховцев (Сигма)"
assert result["room"] == "онлайн"