"""Тесты разбора листов: группы, дни/пары/чётность, индексы, вспомогательные листы.""" 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_still_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["ауд. 402"]["available_days"] == ["четверг", "пятница"] assert info["ауд. 410"]["capacity"] == 30