diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..758b44e --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Тесты для проекта модульности конфигурации \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..78546b8 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,140 @@ +""" +test_config.py - Тесты для конфигурации +Проверяют корректность чтения и структуры config.py +""" +import pytest +from config import YEAR, MONTH, SHIFTS, OPERATORS, OPERATOR_PREFERENCES + + +class TestConfigConstants: + """Тесты для констант конфигурации""" + + def test_year_is_int(self): + """Год должен быть целым числом""" + assert isinstance(YEAR, int) + assert YEAR == 2026 + + def test_month_is_int(self): + """Месяц должен быть целым числом""" + assert isinstance(MONTH, int) + assert MONTH == 4 + + def test_month_in_range(self): + """Месяц должен быть в диапазоне 1-12""" + assert 1 <= MONTH <= 12 + + def test_shifts_is_list(self): + """Смены должны быть списком""" + assert isinstance(SHIFTS, list) + assert len(SHIFTS) == 4 + + def test_shifts_structure(self): + """Каждая смена должна быть кортежем из двух чисел""" + for shift in SHIFTS: + assert isinstance(shift, tuple) + assert len(shift) == 2 + start, end = shift + assert isinstance(start, (int, float)) + assert isinstance(end, (int, float)) + assert start < end + + def test_operators_is_list(self): + """Операторы должны быть списком""" + assert isinstance(OPERATORS, list) + assert len(OPERATORS) > 0 + + def test_operators_not_empty(self): + """Список операторов не должен быть пустым""" + assert len(OPERATORS) >= 1 + + def test_operators_strings(self): + """Все операторы должны быть строками""" + for op in OPERATORS: + assert isinstance(op, str) + assert len(op) > 0 + + def test_operator_preferences_structure(self): + """Структура предпочтений должна быть корректной""" + assert isinstance(OPERATOR_PREFERENCES, dict) + assert len(OPERATOR_PREFERENCES) == len(OPERATORS) + + def test_operator_preferences_values(self): + """Значения предпочтений должны быть словарями""" + for op, prefs in OPERATOR_PREFERENCES.items(): + assert isinstance(prefs, dict) + # Проверка, что ключи предпочтений корректны + valid_keys = {'dates', 'day_of_week', 'shift', 'off_dates'} + assert set(prefs.keys()).issubset(valid_keys) + + +class TestConfigDates: + """Тесты для дат""" + + def test_year_is_positive(self): + """Год должен быть положительным""" + assert YEAR > 0 + + def test_month_is_valid(self): + """Месяц должен быть валидным (1-12)""" + assert 1 <= MONTH <= 12 + + def test_shifts_time_valid(self): + """Время смены должно быть валидным (0-24)""" + for start, end in SHIFTS: + assert 0 <= start <= 24 + assert 0 <= end <= 24 + assert start < end + + +class TestConfigOperators: + """Тесты для операторов""" + + def test_operators_unique(self): + """Имена операторов должны быть уникальными""" + assert len(OPERATORS) == len(set(OPERATORS)) + + def test_operators_no_empty_strings(self): + """Имена операторов не должны быть пустыми строками""" + for op in OPERATORS: + assert op.strip() != "" + + def test_operators_no_special_chars(self): + """Имена операторов не должны содержать специальные символы""" + for op in OPERATORS: + assert not any(c in op for c in ['<', '>', '/', '\\', '|', ':', '*', '?', '"']) + + +class TestConfigPreferences: + """Тесты для предпочтений операторов""" + + def test_all_operators_have_preferences(self): + """Все операторы должны иметь предпочтения""" + assert len(OPERATOR_PREFERENCES) == len(OPERATORS) + + def test_dates_in_range(self): + """Дни в предпочтениях должны быть в диапазоне 1-31""" + for prefs in OPERATOR_PREFERENCES.values(): + if 'dates' in prefs: + for day in prefs['dates']: + assert 1 <= day <= 31 + + def test_shift_in_range(self): + """Смена в предпочтениях должна быть в диапазоне 0-3""" + for prefs in OPERATOR_PREFERENCES.values(): + if 'shift' in prefs: + assert 0 <= prefs['shift'] <= 3 + + def test_day_of_week_valid(self): + """День недели в предпочтениях должен быть валидным""" + for prefs in OPERATOR_PREFERENCES.values(): + if 'day_of_week' in prefs: + day_str = prefs['day_of_week'] + # Проверяем, что это строка с валидным значением + assert day_str.lower() in ['понедельник', 'вторник', 'среда', 'четверг', 'пятница', 'суббота', 'воскресенье'] + + def test_off_dates_in_range(self): + """Дни отдыха должны быть в диапазоне 1-31""" + for prefs in OPERATOR_PREFERENCES.values(): + if 'off_dates' in prefs: + for day in prefs['off_dates']: + assert 1 <= day <= 31 \ No newline at end of file diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..dd42407 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,354 @@ +""" +test_models.py - Тесты для моделей и предпочтений +Проверяют корректность работы класса OperatorPreferences и связанных функций +""" +import pytest +from datetime import datetime +from models import ( + OperatorPreferences, + PreferencesBuilder, + DayOfWeek, + PreferenceType, + get_days, + create_model, + solve_model, + get_solution +) +from config import YEAR, MONTH, SHIFTS, OPERATORS + +class TestDayOfWeek: + """Тесты для enum DayOfWeek""" + + def test_day_of_week_values(self): + """Проверка значений enum""" + assert DayOfWeek.MONDAY.value == 0 + assert DayOfWeek.TUESDAY.value == 1 + assert DayOfWeek.WEDNESDAY.value == 2 + assert DayOfWeek.THURSDAY.value == 3 + assert DayOfWeek.FRIDAY.value == 4 + assert DayOfWeek.SATURDAY.value == 5 + assert DayOfWeek.SUNDAY.value == 6 + + def test_day_of_week_names(self): + """Проверка названий enum""" + assert DayOfWeek.MONDAY.name == "MONDAY" + assert DayOfWeek.SUNDAY.name == "SUNDAY" + + +class TestOperatorPreferences: + """Тесты для класса OperatorPreferences""" + + def test_creation(self): + """Тест создания объекта предпочтений""" + prefs = OperatorPreferences("Тестовый") + assert prefs.operator_name == "Тестовый" + assert prefs.work_on_dates == [] + assert prefs.work_on_day_of_week == [] + assert prefs.work_only_shift is None + assert prefs.off_on_dates == [] + + def test_to_dict_empty(self): + """Тест конвертации в словарь для пустых предпочтений""" + prefs = OperatorPreferences("Тестовый") + result = prefs.to_dict() + assert result == {} + + def test_to_dict_with_dates(self): + """Тест конвертации с датами""" + prefs = OperatorPreferences("Тестовый") + prefs.work_on_dates = [10, 15, 20] + result = prefs.to_dict() + assert result['dates'] == [10, 15, 20] + + def test_to_dict_with_shift(self): + """Тест конвертации со сменой""" + prefs = OperatorPreferences("Тестовый") + prefs.work_only_shift = 2 + result = prefs.to_dict() + assert result['shift'] == 2 + + def test_to_dict_with_day_of_week(self): + """Тест конвертации с днем недели""" + prefs = OperatorPreferences("Тестовый") + prefs.work_on_day_of_week = [DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY] + result = prefs.to_dict() + assert result['day_of_week'] == ['monday', 'wednesday'] + + def test_to_dict_with_off_dates(self): + """Тест конвертации с днями отдыха""" + prefs = OperatorPreferences("Тестовый") + prefs.off_on_dates = [5, 12, 19] + result = prefs.to_dict() + assert result['off_dates'] == [5, 12, 19] + + def test_to_dict_all(self): + """Тест конвертации всех полей""" + prefs = OperatorPreferences("Тестовый") + prefs.work_on_dates = [10, 15] + prefs.work_on_day_of_week = [DayOfWeek.MONDAY] + prefs.work_only_shift = 1 + prefs.off_on_dates = [5] + result = prefs.to_dict() + assert result['dates'] == [10, 15] + assert result['day_of_week'] == ['monday'] + assert result['shift'] == 1 + assert result['off_dates'] == [5] + + def test_from_dict(self): + """Тест создания из словаря""" + data = { + 'dates': [10, 15], + 'day_of_week': ['monday'], + 'shift': 1, + 'off_dates': [5] + } + prefs = OperatorPreferences.from_dict("Тестовый", data) + assert prefs.work_on_dates == [10, 15] + assert prefs.work_on_day_of_week == [DayOfWeek.MONDAY] + assert prefs.work_only_shift == 1 + assert prefs.off_on_dates == [5] + + def test_from_dict_partial(self): + """Тест создания из словаря с неполными данными""" + data = {'dates': [10]} + prefs = OperatorPreferences.from_dict("Тестовый", data) + assert prefs.work_on_dates == [10] + assert prefs.work_on_day_of_week == [] + assert prefs.work_only_shift is None + assert prefs.off_on_dates == [] + + def test_from_dict_invalid_day_of_week(self): + """Тест обработки невалидного дня недели""" + data = {'day_of_week': ['invalid']} + prefs = OperatorPreferences.from_dict("Тестовый", data) + assert prefs.work_on_day_of_week == [] + + def test_from_dict_invalid_shift(self): + """Тест обработки невалидной смены""" + data = {'shift': 5} + prefs = OperatorPreferences.from_dict("Тестовый", data) + assert prefs.work_only_shift is None + + def test_from_dict_invalid_dates(self): + """Тест обработки невалидных дат""" + data = {'dates': [32]} + prefs = OperatorPreferences.from_dict("Тестовый", data) + assert prefs.work_on_dates == [] + + def test_from_dict_invalid_off_dates(self): + """Тест обработки невалидных дней отдыха""" + data = {'off_dates': [32]} + prefs = OperatorPreferences.from_dict("Тестовый", data) + assert prefs.off_on_dates == [] + + +class TestPreferencesBuilder: + """Тесты для класса PreferencesBuilder""" + + def test_creation(self): + """Тест создания объекта""" + builder = PreferencesBuilder("Тестовый") + assert builder.operator_name == "Тестовый" + assert builder._preferences is None + + def test_add_dates(self): + """Тест добавления дат""" + builder = PreferencesBuilder("Тестовый") + builder.add_dates([10, 15]) + assert builder._preferences.work_on_dates == [10, 15] + + def test_add_day_of_week(self): + """Тест добавления дней недели""" + builder = PreferencesBuilder("Тестовый") + builder.add_day_of_week([DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY]) + assert len(builder._preferences.work_on_day_of_week) == 2 + + def test_add_shift(self): + """Тест добавления смены""" + builder = PreferencesBuilder("Тестовый") + builder.add_shift(2) + assert builder._preferences.work_only_shift == 2 + + def test_add_off_dates(self): + """Тест добавления дней отдыха""" + builder = PreferencesBuilder("Тестовый") + builder.add_off_dates([5, 12]) + assert builder._preferences.off_on_dates == [5, 12] + + def test_to_dict(self): + """Тест конвертации в словарь""" + builder = PreferencesBuilder("Тестовый") + builder.add_dates([10]) + builder.add_shift(1) + result = builder.to_dict() + assert result['dates'] == [10] + assert result['shift'] == 1 + + def test_to_dict_empty(self): + """Тест конвертации пустых предпочтений""" + builder = PreferencesBuilder("Тестовый") + result = builder.to_dict() + assert result == {} + + def test_to_dict_invalid(self): + """Тест обработки невалидных данных""" + builder = PreferencesBuilder("Тестовый") + builder.add_dates([32]) # Невалидная дата + result = builder.to_dict() + assert result == {} + + def test_to_dict_invalid_day_of_week(self): + """Тест обработки невалидного дня недели""" + builder = PreferencesBuilder("Тестовый") + builder.add_day_of_week([DayOfWeek.MONDAY, "invalid"]) + result = builder.to_dict() + assert result['day_of_week'] == ['monday'] + + def test_to_dict_invalid_shift(self): + """Тест обработки невалидной смены""" + builder = PreferencesBuilder("Тестовый") + builder.add_shift(5) # Невалидная смена + result = builder.to_dict() + assert result == {} + + def test_to_dict_invalid_off_dates(self): + """Тест обработки невалидных дней отдыха""" + builder = PreferencesBuilder("Тестовый") + builder.add_off_dates([32]) # Невалидная дата + result = builder.to_dict() + assert result == {} + + +class TestGetDays: + """Тесты для функции get_days""" + + def test_days_count(self): + """Проверка количества дней""" + days = get_days() + assert len(days) == 30 # Апрель 2026 + + def test_days_format(self): + """Проверка формата дней""" + days = get_days() + for day in days: + assert isinstance(day, str) + assert len(day) == 10 # YYYY-MM-DD + assert '-' in day + + def test_days_sorted(self): + """Проверка сортировки дней""" + days = get_days() + assert days == sorted(days) + + def test_days_start(self): + """Проверка первого дня""" + days = get_days() + assert days[0] == "2026-04-01" + + def test_days_end(self): + """Проверка последнего дня""" + days = get_days() + assert days[-1] == "2026-04-30" + + +class TestCreateModel: + """Тесты для функции create_model""" + + def test_model_creation(self): + """Тест создания модели""" + model, variables, operators_set = create_model() + assert model is not None + assert variables is not None + assert operators_set is not None + assert len(operators_set) == len(OPERATORS) + + def test_variables_count(self): + """Тест количества переменных""" + model, variables, operators_set = create_model() + expected_count = len(get_days()) * len(SHIFTS) * len(OPERATORS) + assert len(variables) == expected_count + + def test_operators_set(self): + """Тест множества операторов""" + model, variables, operators_set = create_model() + assert operators_set == set(OPERATORS) + + def test_model_has_constraints(self): + """Тест наличия ограничений""" + model, variables, operators_set = create_model() + # Модель должна иметь ограничения + assert model.NumConstraints() > 0 + + def test_model_has_hints(self): + """Тест наличия подсказок""" + model, variables, operators_set = create_model() + # Модель должна иметь подсказки + assert model.NumHints() > 0 + + +class TestSolveModel: + """Тесты для функции solve_model""" + + def test_solve_returns_bool(self): + """Тест возврата булевого значения""" + result = solve_model() + assert isinstance(result, bool) + + def test_solve_with_valid_config(self): + """Тест решения с валидной конфигурацией""" + # Это может занять время, поэтому тестуем только факт вызова + try: + result = solve_model() + # Результат может быть True или False в зависимости от конфигурации + assert isinstance(result, bool) + except Exception as e: + pytest.fail(f"Ошибка при решении модели: {e}") + + +class TestGetSolution: + """Тесты для функции get_solution""" + + def test_solution_structure(self): + """Тест структуры решения""" + schedule = get_solution() + assert isinstance(schedule, dict) + assert len(schedule) > 0 + + def test_solution_days(self): + """Тест дней в решении""" + schedule = get_solution() + days = get_days() + assert set(schedule.keys()) == set(days) + + def test_solution_shifts(self): + """Тест смен в решении""" + schedule = get_solution() + for day, day_shifts in schedule.items(): + assert isinstance(day_shifts, dict) + assert len(day_shifts) == len(SHIFTS) + + def test_solution_operators(self): + """Тест операторов в решении""" + schedule = get_solution() + for day, day_shifts in schedule.items(): + for shift_idx, shift_data in day_shifts.items(): + assert isinstance(shift_data, dict) + assert len(shift_data) == len(OPERATORS) + + def test_solution_values(self): + """Тест значений в решении""" + schedule = get_solution() + for day, day_shifts in schedule.items(): + for shift_idx, shift_data in day_shifts.items(): + for op_name, value in shift_data.items(): + assert isinstance(value, bool) + assert value in [True, False] + + def test_solution_consistency(self): + """Тест согласованности решения""" + schedule = get_solution() + # Каждый день и смена должны иметь ровно одного оператора + for day, day_shifts in schedule.items(): + for shift_idx, shift_data in day_shifts.items(): + active_operators = [op for op, value in shift_data.items() if value] + assert len(active_operators) == 1 \ No newline at end of file