Правило: если строка или столбец в таблице скрыт, его данные не попадают в расписание — раньше скрытые столбцы пропускались только при поиске групп. - xlsx: у листа появился набор скрытых строк (Worksheet.hidden_rows); - расписание, недели: скрытые строки пропускаются; - аудитории и справочник ФИО: скрытые столбцы не читаются вовсе (убрана двухпроходная логика «сначала видимые, потом скрытые»); - тесты: билдер умеет скрытые строки, добавлены проверки «скрытое не парсится». Проверено на таблице: группы и всё расписание совпали байт в байт (скрытых строк нет), из аудиторий ушли только те, что лежат в скрытых столбцах (402, 405, 415), справочник ФИО стал короче на 2 скрытые записи, у 223 осталось описание из видимого столбца (20 мест, интерактивная панель). Тесты: 171.
358 lines
16 KiB
Python
358 lines
16 KiB
Python
"""Тесты разбора листов: группы, дни/пары/чётность, индексы, вспомогательные листы."""
|
||
|
||
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_room_index_detached_room():
|
||
"""Аудитория, оторванная пустой строкой, попадает в индекс с предметом и преподавателем."""
|
||
sheets = fixture_sheets()
|
||
sheets[parsing.MAIN_SHEET].rows[6][7] = "Химия\nПетров П.П.\n\n ауд. 201"
|
||
data = parsing.parse_schedule(make_sheets(sheets))
|
||
_, rooms = parsing.build_teacher_room_index(data)
|
||
entry = rooms["ауд. 201"]["вторник"]["1"]["ч"][0]
|
||
assert entry["subject"] == "Химия"
|
||
assert entry["teacher"] == "Петров П.П."
|
||
assert entry["groups"] == ["ИТ-12БО"]
|
||
|
||
|
||
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 set(info) == {"ауд. 215", "ауд. 301", "ауд. 220"}
|
||
assert info["ауд. 215"]["info"] == "комп.класс, 12 мест"
|
||
assert info["ауд. 215"]["type"] == "комп.класс"
|
||
assert info["ауд. 215"]["capacity"] == 12
|
||
assert info["ауд. 215"]["equipment"] == ["ПК"]
|
||
|
||
assert info["ауд. 301"]["info"] == "интерактивная панель, 24 места"
|
||
assert info["ауд. 301"]["capacity"] == 24
|
||
assert info["ауд. 301"]["equipment"] == ["интерактивная панель"]
|
||
|
||
assert info["ауд. 220"]["info"] == "20 мест"
|
||
assert info["ауд. 220"]["type"] == ""
|
||
assert info["ауд. 220"]["capacity"] == 20
|
||
assert info["ауд. 220"]["equipment"] == []
|
||
|
||
|
||
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
|
||
|
||
|
||
def test_room_attributes_from_descriptions():
|
||
"""Атрибуты аудиторий разбираются из описаний вида «тип; оснащение; N мест»."""
|
||
from xlsx_builder import SheetSpec
|
||
|
||
descriptions = {
|
||
"201": "комп.класс МО и СТ; без проектора; маркер; 12 мест",
|
||
"203": "аудитория для практических; без проектора; меловая доска; 44 места",
|
||
"210": "комп.класс; проектор; маркер; 28 мест",
|
||
"305": "кабинет английского; телевизор; маркер; 24 места",
|
||
"402": "402 четверг пятница",
|
||
"405": "пк",
|
||
"410": "",
|
||
}
|
||
spec = SheetSpec(rows=[
|
||
[None] * 6 + list(descriptions.keys()),
|
||
[None] * 6 + list(descriptions.values()),
|
||
])
|
||
info = parsing.parse_rooms_info(make_sheets({parsing.ROOMS_SHEET: spec}))
|
||
|
||
assert info["ауд. 201"]["type"] == "комп.класс"
|
||
assert info["ауд. 201"]["capacity"] == 12
|
||
# «без проектора» — проектора нет, хотя слово в тексте есть
|
||
assert info["ауд. 201"]["equipment"] == ["ПК", "маркер"]
|
||
assert info["ауд. 201"]["available_days"] is None
|
||
|
||
assert info["ауд. 203"]["type"] == "аудитория для практических"
|
||
assert info["ауд. 203"]["equipment"] == ["меловая доска"]
|
||
assert info["ауд. 203"]["capacity"] == 44
|
||
|
||
assert info["ауд. 210"]["equipment"] == ["ПК", "проектор", "маркер"]
|
||
|
||
assert info["ауд. 305"]["type"] == "кабинет английского"
|
||
assert info["ауд. 305"]["equipment"] == ["телевизор", "маркер"]
|
||
|
||
assert info["ауд. 402"]["available_days"] == ["четверг", "пятница"]
|
||
assert info["ауд. 402"]["capacity"] is None
|
||
assert info["ауд. 402"]["type"] == ""
|
||
|
||
assert info["ауд. 405"]["equipment"] == ["ПК"]
|
||
assert info["ауд. 410"]["info"] == ""
|
||
assert info["ауд. 410"]["type"] == ""
|
||
|
||
|
||
def test_room_payload_shape():
|
||
payload = parsing.room_payload("ауд. 215", {
|
||
"info": "лекционная; проектор; 96 мест",
|
||
"type": "лекционная",
|
||
"type_group": "лекционная",
|
||
"capacity": 96,
|
||
"equipment": ["проектор"],
|
||
"available_days": None,
|
||
})
|
||
assert payload == {
|
||
"room": "ауд. 215",
|
||
"info": "лекционная; проектор; 96 мест",
|
||
"type": "лекционная",
|
||
"type_group": "лекционная",
|
||
"capacity": 96,
|
||
"equipment": ["проектор"],
|
||
"available_days": None,
|
||
}
|
||
|
||
|
||
def test_room_type_group_merges_similar():
|
||
"""Близкие формулировки типов сводятся к общему типу для фильтра."""
|
||
assert parsing.room_type_group("комп.класс") == "комп.класс"
|
||
assert parsing.room_type_group("комп.класс Linux") == "комп.класс"
|
||
assert parsing.room_type_group("компьютерный класс") == "комп.класс"
|
||
assert parsing.room_type_group("английский") == "кабинет английского"
|
||
assert parsing.room_type_group("кабинет английского") == "кабинет английского"
|
||
assert parsing.room_type_group("ауд. каф. ДА") == "аудитория кафедры"
|
||
assert parsing.room_type_group("лекционная") == "лекционная"
|
||
assert parsing.room_type_group("лекционно-практическая") == "лекционно-практическая"
|
||
assert parsing.room_type_group("аудитория для практических") == "аудитория для практических"
|
||
assert parsing.room_type_group("") == ""
|
||
|
||
|
||
def test_lowercase_except_abbreviations():
|
||
"""Обозначения — в нижнем регистре, аббревиатуры остаются заглавными."""
|
||
assert parsing.room_type("комп.класс Linux; 10 мест") == "комп.класс linux"
|
||
assert parsing.room_type("Кабинет Английского") == "кабинет английского"
|
||
assert parsing.room_equipment("ПК, Проектор, интернет МО") == ["ПК", "проектор"]
|
||
assert parsing.lower_except_abbr("МО и СТ; без проектора") == "МО и СТ; без проектора"
|
||
|
||
|
||
def test_room_visible_column_wins_over_hidden_duplicate():
|
||
"""Скрытый столбец-дубль (у 223) не парсится вовсе — берём видимый."""
|
||
from xlsx_builder import SheetSpec
|
||
|
||
spec = SheetSpec(
|
||
rows=[
|
||
[None] * 6 + ["223", "223"],
|
||
[None] * 6 + [
|
||
"комп.класс Linux/Windows; интерактивная панель; маркер; 20 мест",
|
||
"компьютерный класс для МО и СТ; 24 места",
|
||
],
|
||
],
|
||
hidden_cols=[(7, 7)],
|
||
)
|
||
info = parsing.parse_rooms_info(make_sheets({parsing.ROOMS_SHEET: spec}))
|
||
room = info["ауд. 223"]
|
||
assert room["capacity"] == 20
|
||
assert "интерактивная панель" in room["equipment"]
|
||
assert room["type"] == "комп.класс linux/windows"
|
||
|
||
|
||
def test_room_only_in_hidden_column_not_parsed():
|
||
"""Скрытое не парсим: аудитория со скрытым столбцом описания не получает."""
|
||
from xlsx_builder import SheetSpec
|
||
|
||
spec = SheetSpec(
|
||
rows=[
|
||
[None] * 6 + ["402", "410"],
|
||
[None] * 6 + ["402 четверг пятница", "проектор; 30 мест"],
|
||
],
|
||
hidden_cols=[(6, 7)],
|
||
)
|
||
info = parsing.parse_rooms_info(make_sheets({parsing.ROOMS_SHEET: spec}))
|
||
assert info == {}
|
||
|
||
|
||
def test_hidden_rows_not_parsed(monkeypatch):
|
||
"""Скрытые строки листа в расписание не попадают."""
|
||
from fixtures import fixture_sheets
|
||
from xlsx_builder import SheetSpec, build_workbook
|
||
|
||
sheets = fixture_sheets()
|
||
main = sheets[parsing.MAIN_SHEET]
|
||
# строки: 0 заголовок, 1 с/по, 2..5 понедельник, 6..7 вторник — скроем строку с первым вторником
|
||
spec = SheetSpec(
|
||
rows=main.rows,
|
||
merges=main.merges,
|
||
hidden_cols=main.hidden_cols,
|
||
hidden_rows=[6],
|
||
)
|
||
data = parsing.parse_schedule(make_sheets({parsing.MAIN_SHEET: spec}))
|
||
tuesday = data["ИВТ-11БО"]["days"].get("вторник", {})
|
||
assert not tuesday, "строка вторника скрыта — занятий быть не должно"
|
||
assert data["ИВТ-11БО"]["days"]["понедельник"][1]["ч"] # понедельник на месте
|