feat: приоритизация инжекторов (Этапы 1-2)
Реализована система приоритизации инжекторов:
Этап 1 - Базовая модель приоритета (float):
- Добавлено поле priority: float в ConversionPoint
- mark_injector(priority=10.5) для установки приоритета
- Интеграция в graph_walker для выбора пути по приоритету
- Aggregate priority для многошаговых путей
Этап 2 - Относительные приоритеты:
- more_than(target) - приоритет выше чем у target
- less_than(target) - приоритет ниже чем у target
- PriorityResolver для разрешения графа зависимостей
- Топологическая сортировка для вычисления приоритетов
- Обнаружение циклов в приоритетах (CircularDependency)
Файлы:
- priority_types.py - классы MoreThan, LessThan, more_than(), less_than()
- priority_resolver.py - PriorityResolver, CycleDetectedError
- test_priority_stage1.py - 21 тест базовых приоритетов
- test_priority_stage2.py - 18 тестов относительных приоритетов
Пример использования:
@repo.mark_injector(priority=10.0)
def int_to_a_v1(i: int) -> A: ...
@repo.mark_injector(priority=more_than(int_to_a_v1))
def int_to_a_v2(i: int) -> A: ...
@repo.mark_injector(priority=less_than(int_to_a_v2))
def int_to_a_v3(i: int) -> A: ...
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
426
tests/test_priority_stage1.py
Normal file
426
tests/test_priority_stage1.py
Normal file
@@ -0,0 +1,426 @@
|
||||
"""
|
||||
Тесты приоритизации инжекторов - Этап 1: Базовая модель приоритета (float).
|
||||
|
||||
Проверка:
|
||||
- Сохранение приоритета в ConversionPoint
|
||||
- Передача приоритета через mark_injector(priority=...)
|
||||
- Выбор пути с наивысшим приоритетом
|
||||
- Детерминизм выбора при одинаковых приоритетах
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
from breakshaft import ConvRepo
|
||||
from breakshaft.models import ConversionPoint
|
||||
|
||||
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class B:
|
||||
b: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
c: str
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Юнит-тесты: ConversionPoint с приоритетом
|
||||
# =============================================================================
|
||||
|
||||
class TestConversionPointPriority:
|
||||
"""Тесты хранения приоритета в ConversionPoint."""
|
||||
|
||||
def test_default_priority_is_zero(self):
|
||||
"""Приоритет по умолчанию равен 0.0."""
|
||||
def func(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = ConversionPoint.from_fn(func)
|
||||
assert len(cps) > 0
|
||||
for cp in cps:
|
||||
assert cp.priority == 0.0
|
||||
|
||||
def test_priority_preserved_in_copy_with(self):
|
||||
"""copy_with сохраняет приоритет."""
|
||||
def func(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = ConversionPoint.from_fn(func)
|
||||
cp = cps[0]
|
||||
|
||||
# Создаём копию с изменённым injects
|
||||
cp_copy = cp.copy_with(injects=B)
|
||||
|
||||
# Приоритет должен сохраниться
|
||||
assert cp_copy.priority == cp.priority
|
||||
|
||||
def test_priority_can_be_set_via_copy_with(self):
|
||||
"""copy_with может изменять приоритет."""
|
||||
def func(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = ConversionPoint.from_fn(func)
|
||||
cp = cps[0]
|
||||
|
||||
# Изменяем приоритет
|
||||
cp_copy = cp.copy_with(priority=10.5)
|
||||
|
||||
assert cp_copy.priority == 10.5
|
||||
assert cp.priority == 0.0 # Оригинал не изменился
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Юнит-тесты: mark_injector с приоритетом
|
||||
# =============================================================================
|
||||
|
||||
class TestMarkInjectorPriority:
|
||||
"""Тесты декоратора mark_injector с приоритетом."""
|
||||
|
||||
def test_mark_injector_default_priority(self):
|
||||
"""mark_injector без priority использует 0.0."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector()
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == 0.0
|
||||
|
||||
def test_mark_injector_with_priority(self):
|
||||
"""mark_injector(priority=X) устанавливает приоритет."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=10.5)
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == 10.5
|
||||
|
||||
def test_mark_injector_negative_priority(self):
|
||||
"""mark_injector поддерживает отрицательные приоритеты."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=-5.0)
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == -5.0
|
||||
|
||||
def test_mark_injector_priority_with_rettype(self):
|
||||
"""mark_injector(priority=..., rettype=...) работает корректно."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=7.5, rettype=A)
|
||||
def int_to_a(i: int):
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == 7.5
|
||||
assert cps[0].injects == A
|
||||
|
||||
def test_mark_injector_priority_with_type_remap(self):
|
||||
"""mark_injector(priority=..., type_remap=...) работает корректно."""
|
||||
repo = ConvRepo()
|
||||
|
||||
type NewA = A
|
||||
type_remap = {'i': int, 'return': NewA}
|
||||
|
||||
@repo.mark_injector(priority=3.0, type_remap=type_remap)
|
||||
def int_to_a(i: int) -> NewA:
|
||||
return NewA(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == 3.0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Интеграционные тесты: Выбор пути по приоритету
|
||||
# =============================================================================
|
||||
|
||||
class TestPriorityPathSelection:
|
||||
"""Тесты выбора пути преобразования по приоритету."""
|
||||
|
||||
def test_higher_priority_path_selected(self):
|
||||
"""Путь с более высоким приоритетом выбирается."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=1.0)
|
||||
def int_to_a_low(i: int) -> A:
|
||||
return A(i * 10) # Низкий приоритет: A(420)
|
||||
|
||||
@repo.mark_injector(priority=10.0)
|
||||
def int_to_a_high(i: int) -> A:
|
||||
return A(i + 100) # Высокий приоритет: A(142)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
# force_commutative=False позволяет выбрать любой путь
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Должен выбрать путь с высоким приоритетом (42 + 100 = 142)
|
||||
assert result == 142
|
||||
|
||||
def test_lower_priority_path_selected_when_higher_not_available(self):
|
||||
"""Путь с низким приоритетом выбирается если высокий недоступен."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=1.0)
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
fn = repo.get_conversion((int,), consumer)
|
||||
result = fn(42)
|
||||
|
||||
assert result == 420
|
||||
|
||||
def test_equal_priorities_use_fallback(self):
|
||||
"""При одинаковых приоритетах используется fallback (имя функции)."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=5.0)
|
||||
def aaa_converter(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
@repo.mark_injector(priority=5.0)
|
||||
def zzz_converter(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
# При одинаковых приоритетах выбор детерминирован (по имени функции)
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Должен выбрать один из путей (детерминировано)
|
||||
assert result in [420, 142]
|
||||
|
||||
def test_priority_with_multiple_steps(self):
|
||||
"""Приоритеты работают в многошаговых преобразованиях."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=1.0)
|
||||
def int_to_b_low(i: int) -> B:
|
||||
return B(float(i) * 10)
|
||||
|
||||
@repo.mark_injector(priority=10.0)
|
||||
def int_to_b_high(i: int) -> B:
|
||||
return B(float(i) + 100)
|
||||
|
||||
@repo.mark_injector()
|
||||
def b_to_a(b: B) -> A:
|
||||
return A(int(b.b))
|
||||
|
||||
def consumer(dep: B) -> float:
|
||||
return dep.b
|
||||
|
||||
# Тестируем выбор int->B (первый шаг)
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Должен выбрать int->B(high): (42 + 100) = 142
|
||||
assert result == 142.0
|
||||
|
||||
def test_priority_deterministic_selection(self):
|
||||
"""Приоритеты обеспечивают детерминированный выбор."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=1.0)
|
||||
def int_to_a_v1(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
@repo.mark_injector(priority=10.0)
|
||||
def int_to_a_v2(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
# Запускаем много раз - результат должен быть одинаковым
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
results = [fn(42) for _ in range(10)]
|
||||
|
||||
# Все результаты должны быть одинаковыми (детерминизм)
|
||||
assert len(set(results)) == 1
|
||||
assert results[0] == 142 # Высокий приоритет
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Интеграционные тесты: add_injector с приоритетом
|
||||
# =============================================================================
|
||||
|
||||
class TestAddInjectorPriority:
|
||||
"""Тесты функции add_injector с приоритетом."""
|
||||
|
||||
def test_add_injector_with_priority(self):
|
||||
"""add_injector(priority=...) устанавливает приоритет."""
|
||||
repo = ConvRepo()
|
||||
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
repo.add_injector(int_to_a, priority=5.5)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == 5.5
|
||||
|
||||
def test_add_injector_default_priority(self):
|
||||
"""add_injector без priority использует 0.0."""
|
||||
repo = ConvRepo()
|
||||
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
repo.add_injector(int_to_a)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == 0.0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Тесты: Приоритеты с Union-типами
|
||||
# =============================================================================
|
||||
|
||||
class TestPriorityWithUnionTypes:
|
||||
"""Тесты приоритетов с Union-типами."""
|
||||
|
||||
def test_priority_with_union_return_type(self):
|
||||
"""Приоритет применяется ко всем вариантам Union return type."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=7.0)
|
||||
def int_to_a_or_b(i: int) -> A | B:
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
# Для Union создаётся несколько ConversionPoint
|
||||
assert len(cps) > 0
|
||||
# Все должны иметь одинаковый приоритет
|
||||
for cp in cps:
|
||||
assert cp.priority == 7.0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Тесты: Приоритеты в конвейерах (pipelines)
|
||||
# =============================================================================
|
||||
|
||||
class TestPriorityInPipelines:
|
||||
"""Тесты приоритетов в конвейерах преобразований."""
|
||||
|
||||
def test_pipeline_respects_priorities(self):
|
||||
"""Конвейер уважает приоритеты инжекторов."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=1.0)
|
||||
def int_to_a_low(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
@repo.mark_injector(priority=10.0)
|
||||
def int_to_a_high(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
@repo.mark_injector()
|
||||
def a_to_b(a: A) -> B:
|
||||
return B(float(a.a))
|
||||
|
||||
def consumer1(dep: A) -> B: # Потребляет A напрямую
|
||||
return a_to_b(dep)
|
||||
|
||||
def consumer2(dep: B) -> float:
|
||||
return dep.b
|
||||
|
||||
pipeline = repo.create_pipeline(
|
||||
(int,),
|
||||
[consumer1, consumer2],
|
||||
force_commutative=False
|
||||
)
|
||||
|
||||
result = pipeline(42)
|
||||
|
||||
# Должен выбрать путь с высоким приоритетом: (42 + 100) = 142
|
||||
assert result == 142.0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Тесты: Краевые случаи
|
||||
# =============================================================================
|
||||
|
||||
class TestPriorityEdgeCases:
|
||||
"""Тесты краевых случаев приоритетов."""
|
||||
|
||||
def test_very_large_priority(self):
|
||||
"""Очень большой приоритет работает корректно."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=1e10)
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == 1e10
|
||||
|
||||
def test_very_small_negative_priority(self):
|
||||
"""Очень маленький отрицательный приоритет работает корректно."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=-1e10)
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == -1e10
|
||||
|
||||
def test_float_priority_precision(self):
|
||||
"""Дробные приоритеты сохраняют точность."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=3.14159)
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert abs(cps[0].priority - 3.14159) < 1e-10
|
||||
|
||||
def test_zero_priority_same_as_default(self):
|
||||
"""Приоритет 0.0 эквивалентен приориту по умолчанию."""
|
||||
repo1 = ConvRepo()
|
||||
repo2 = ConvRepo()
|
||||
|
||||
@repo1.mark_injector()
|
||||
def int_to_a1(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
@repo2.mark_injector(priority=0.0)
|
||||
def int_to_a2(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps1 = list(repo1.convertor_set)
|
||||
cps2 = list(repo2.convertor_set)
|
||||
|
||||
assert cps1[0].priority == cps2[0].priority
|
||||
447
tests/test_priority_stage2.py
Normal file
447
tests/test_priority_stage2.py
Normal file
@@ -0,0 +1,447 @@
|
||||
"""
|
||||
Тесты приоритизации инжекторов - Этап 2: Относительные приоритеты.
|
||||
|
||||
Проверка:
|
||||
- more_than() создаёт ограничение приоритета
|
||||
- less_than() создаёт ограничение приоритета
|
||||
- Разрешение графа относительных приоритетов
|
||||
- Обнаружение циклов в приоритетах
|
||||
- Транзитивность приоритетов (A > B > C ⇒ A > C)
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
from breakshaft import ConvRepo, more_than, less_than, CircularDependency
|
||||
from breakshaft.priority_types import MoreThan, LessThan
|
||||
from breakshaft.priority_resolver import PriorityResolver, CycleDetectedError
|
||||
from breakshaft.models import ConversionPoint
|
||||
|
||||
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class B:
|
||||
b: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
c: str
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Юнит-тесты: RelativePriority классы
|
||||
# =============================================================================
|
||||
|
||||
class TestRelativePriorityClasses:
|
||||
"""Тесты классов относительных приоритетов."""
|
||||
|
||||
def test_more_than_creation(self):
|
||||
"""more_than() создаёт MoreThan объект."""
|
||||
def func(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
rel = more_than(func)
|
||||
|
||||
assert isinstance(rel, MoreThan)
|
||||
assert rel.target is func
|
||||
|
||||
def test_less_than_creation(self):
|
||||
"""less_than() создаёт LessThan объект."""
|
||||
def func(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
rel = less_than(func)
|
||||
|
||||
assert isinstance(rel, LessThan)
|
||||
assert rel.target is func
|
||||
|
||||
def test_more_than_frozen(self):
|
||||
"""MoreThan неизменяемый (frozen dataclass)."""
|
||||
def func(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
rel = more_than(func)
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
rel.target = None # type: ignore
|
||||
|
||||
def test_less_than_frozen(self):
|
||||
"""LessThan неизменяемый (frozen dataclass)."""
|
||||
def func(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
rel = less_than(func)
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
rel.target = None # type: ignore
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Юнит-тесты: PriorityResolver
|
||||
# =============================================================================
|
||||
|
||||
class TestPriorityResolver:
|
||||
"""Тесты разрешителя приоритетов."""
|
||||
|
||||
def test_simple_more_than(self):
|
||||
"""Простое more_than ограничение."""
|
||||
def func1(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
def func2(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
cp1 = ConversionPoint.from_fn(func1)[0]
|
||||
cp2 = ConversionPoint.from_fn(func2)[0]
|
||||
|
||||
cp1 = cp1.copy_with(priority=more_than(func2))
|
||||
|
||||
resolver = PriorityResolver()
|
||||
resolver.add_injector(cp1)
|
||||
resolver.add_injector(cp2)
|
||||
resolver.add_constraint(cp1, cp2, direction=1)
|
||||
|
||||
priorities = resolver.resolve()
|
||||
|
||||
assert priorities[cp1] > priorities[cp2]
|
||||
|
||||
def test_simple_less_than(self):
|
||||
"""Простое less_than ограничение."""
|
||||
def func1(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
def func2(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
cp1 = ConversionPoint.from_fn(func1)[0]
|
||||
cp2 = ConversionPoint.from_fn(func2)[0]
|
||||
|
||||
cp1 = cp1.copy_with(priority=less_than(func2))
|
||||
|
||||
resolver = PriorityResolver()
|
||||
resolver.add_injector(cp1)
|
||||
resolver.add_injector(cp2)
|
||||
resolver.add_constraint(cp1, cp2, direction=-1)
|
||||
|
||||
priorities = resolver.resolve()
|
||||
|
||||
assert priorities[cp1] < priorities[cp2]
|
||||
|
||||
def test_transitive_priorities(self):
|
||||
"""Транзитивность приоритетов: A > B > C ⇒ A > C."""
|
||||
def func_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
def func_b(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
def func_c(i: int) -> A:
|
||||
return A(i * 100)
|
||||
|
||||
cp_a = ConversionPoint.from_fn(func_a)[0]
|
||||
cp_b = ConversionPoint.from_fn(func_b)[0]
|
||||
cp_c = ConversionPoint.from_fn(func_c)[0]
|
||||
|
||||
resolver = PriorityResolver()
|
||||
resolver.add_injector(cp_a)
|
||||
resolver.add_injector(cp_b)
|
||||
resolver.add_injector(cp_c)
|
||||
|
||||
# A > B, B > C
|
||||
resolver.add_constraint(cp_a, cp_b, direction=1)
|
||||
resolver.add_constraint(cp_b, cp_c, direction=1)
|
||||
|
||||
priorities = resolver.resolve()
|
||||
|
||||
assert priorities[cp_a] > priorities[cp_b]
|
||||
assert priorities[cp_b] > priorities[cp_c]
|
||||
assert priorities[cp_a] > priorities[cp_c] # Транзитивность
|
||||
|
||||
def test_cycle_detection(self):
|
||||
"""Обнаружение цикла: A > B > C > A."""
|
||||
def func_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
def func_b(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
def func_c(i: int) -> A:
|
||||
return A(i * 100)
|
||||
|
||||
cp_a = ConversionPoint.from_fn(func_a)[0]
|
||||
cp_b = ConversionPoint.from_fn(func_b)[0]
|
||||
cp_c = ConversionPoint.from_fn(func_c)[0]
|
||||
|
||||
resolver = PriorityResolver()
|
||||
resolver.add_injector(cp_a)
|
||||
resolver.add_injector(cp_b)
|
||||
resolver.add_injector(cp_c)
|
||||
|
||||
# A > B, B > C, C > A (цикл!)
|
||||
resolver.add_constraint(cp_a, cp_b, direction=1)
|
||||
resolver.add_constraint(cp_b, cp_c, direction=1)
|
||||
resolver.add_constraint(cp_c, cp_a, direction=1)
|
||||
|
||||
with pytest.raises(CycleDetectedError):
|
||||
resolver.resolve()
|
||||
|
||||
def test_multiple_constraints_same_injector(self):
|
||||
"""Несколько ограничений для одного инжектора."""
|
||||
def func_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
def func_b(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
def func_c(i: int) -> A:
|
||||
return A(i * 100)
|
||||
|
||||
cp_a = ConversionPoint.from_fn(func_a)[0]
|
||||
cp_b = ConversionPoint.from_fn(func_b)[0]
|
||||
cp_c = ConversionPoint.from_fn(func_c)[0]
|
||||
|
||||
resolver = PriorityResolver()
|
||||
resolver.add_injector(cp_a)
|
||||
resolver.add_injector(cp_b)
|
||||
resolver.add_injector(cp_c)
|
||||
|
||||
# A > B, A > C
|
||||
resolver.add_constraint(cp_a, cp_b, direction=1)
|
||||
resolver.add_constraint(cp_a, cp_c, direction=1)
|
||||
|
||||
priorities = resolver.resolve()
|
||||
|
||||
assert priorities[cp_a] > priorities[cp_b]
|
||||
assert priorities[cp_a] > priorities[cp_c]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Интеграционные тесты: Относительные приоритеты в ConvRepo
|
||||
# =============================================================================
|
||||
|
||||
class TestRelativePrioritiesInRepo:
|
||||
"""Тесты относительных приоритетов в репозитории."""
|
||||
|
||||
def test_more_than_in_mark_injector(self):
|
||||
"""more_than в mark_injector."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector()
|
||||
def int_to_a_base(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
@repo.mark_injector(priority=more_than(int_to_a_base))
|
||||
def int_to_a_preferred(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Должен выбрать путь с высоким приоритетом (42 + 100 = 142)
|
||||
assert result == 142
|
||||
|
||||
def test_less_than_in_mark_injector(self):
|
||||
"""less_than в mark_injector."""
|
||||
repo = ConvRepo()
|
||||
|
||||
# Сначала определяем функцию которая будет "выше"
|
||||
@repo.mark_injector(priority=10.0)
|
||||
def int_to_a_preferred(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
# Потом функцию с less_than
|
||||
@repo.mark_injector(priority=less_than(int_to_a_preferred))
|
||||
def int_to_a_low(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Должен выбрать путь с высоким приоритетом (42 + 100 = 142)
|
||||
assert result == 142
|
||||
|
||||
def test_chain_more_than(self):
|
||||
"""Цепочка more_than: A > B > C."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector()
|
||||
def int_to_a_c(i: int) -> A:
|
||||
return A(i * 100) # Низкий приоритет
|
||||
|
||||
@repo.mark_injector(priority=more_than(int_to_a_c))
|
||||
def int_to_a_b(i: int) -> A:
|
||||
return A(i * 10) # Средний приоритет
|
||||
|
||||
@repo.mark_injector(priority=more_than(int_to_a_b))
|
||||
def int_to_a_a(i: int) -> A:
|
||||
return A(i + 100) # Высокий приоритет
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Должен выбрать путь с высоким приоритетом (42 + 100 = 142)
|
||||
assert result == 142
|
||||
|
||||
def test_circular_dependency_raises(self):
|
||||
"""Циклическая зависимость вызывает CircularDependency."""
|
||||
repo = ConvRepo()
|
||||
|
||||
# Определяем функции сначала
|
||||
@repo.mark_injector()
|
||||
def int_to_a_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
@repo.mark_injector()
|
||||
def int_to_a_b(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
@repo.mark_injector()
|
||||
def int_to_a_c(i: int) -> A:
|
||||
return A(i * 100)
|
||||
|
||||
# Теперь добавляем циклические зависимости
|
||||
repo.add_injector(int_to_a_a, priority=more_than(int_to_a_b))
|
||||
repo.add_injector(int_to_a_b, priority=more_than(int_to_a_c))
|
||||
repo.add_injector(int_to_a_c, priority=more_than(int_to_a_a))
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
# Цикл: A > B > C > A
|
||||
with pytest.raises(CircularDependency):
|
||||
repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Тесты: Смешанные абсолютные и относительные приоритеты
|
||||
# =============================================================================
|
||||
|
||||
class TestMixedPriorities:
|
||||
"""Тесты смешанных абсолютных и относительных приоритетов."""
|
||||
|
||||
def test_absolute_and_relative(self):
|
||||
"""Смешение абсолютных и относительных приоритетов."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=5.0)
|
||||
def int_to_a_base(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
@repo.mark_injector(priority=more_than(int_to_a_base))
|
||||
def int_to_a_high(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# more_than должен дать приоритет выше чем 5.0
|
||||
assert result == 142
|
||||
|
||||
def test_relative_with_absolute_fallback(self):
|
||||
"""Относительный приоритет с абсолютным fallback."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=10.0)
|
||||
def int_to_a_high(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
@repo.mark_injector(priority=less_than(int_to_a_high))
|
||||
def int_to_a_low(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Должен выбрать путь с высоким приоритетом
|
||||
assert result == 142
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Тесты: Краевые случаи
|
||||
# =============================================================================
|
||||
|
||||
class TestRelativePriorityEdgeCases:
|
||||
"""Тесты краевых случаев относительных приоритетов."""
|
||||
|
||||
def test_self_reference_raises(self):
|
||||
"""Ссылка на себя вызывает цикл."""
|
||||
repo = ConvRepo()
|
||||
|
||||
# Определяем функцию сначала
|
||||
@repo.mark_injector()
|
||||
def int_to_a_self(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
# Теперь добавляем self-reference
|
||||
repo.add_injector(int_to_a_self, priority=more_than(int_to_a_self))
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
with pytest.raises((CircularDependency, CycleDetectedError)):
|
||||
repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
|
||||
def test_non_existent_target_ignored(self):
|
||||
"""Несуществующая цель игнорируется."""
|
||||
repo = ConvRepo()
|
||||
|
||||
def non_existent(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
@repo.mark_injector(priority=more_than(non_existent))
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
# Должно работать, non_existent не зарегистрирован
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
assert result == 42
|
||||
|
||||
def test_multiple_more_than_same_target(self):
|
||||
"""Несколько more_than на одну цель."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector()
|
||||
def int_to_a_base(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
@repo.mark_injector(priority=more_than(int_to_a_base))
|
||||
def int_to_a_v1(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
@repo.mark_injector(priority=more_than(int_to_a_base))
|
||||
def int_to_a_v2(i: int) -> A:
|
||||
return A(i + 200)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Оба v1 и v2 имеют приоритет выше base, выбирается один из них
|
||||
assert result in [142, 242]
|
||||
Reference in New Issue
Block a user