feat: масштабное улучшение системы обработки ошибок и тестирования

Основные изменения:
- Добавлена иерархия исключений (17 классов) с кодами ошибок и контекстом
- Улучшена обработка ошибок: детальные сообщения с подсказками
- Добавлено 24 теста для экстремальных случаев (комбинаторика, циклы, async)
- Добавлено 23 теста для системы обработки ошибок
- Исправлен баг с optional-аргументами в renderer.py
- Обновлены импорты в тестах (src.breakshaft → breakshaft)

Документация:
- ERROR_DESIGN.md — проектирование системы ошибок
- COMMUTATIVITY_DESIGN.md — анализ проблемы некоммутативности (10 вариантов решений)

Файлы:
- src/breakshaft/exceptions.py (новый) — модуль исключений
- tests/test_error_handling.py (новый) — тесты ошибок
- tests/test_extreme_cases.py (новый) — экстремальные кейсы

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Qwen Code Assistant
2026-03-28 13:42:04 +00:00
parent 74d78b1957
commit ca605001b3
17 changed files with 3063 additions and 21 deletions

230
ERROR_DESIGN.md Normal file
View File

@@ -0,0 +1,230 @@
# Проектирование системы обработки ошибок для breakshaft
## Текущее состояние
Сейчас библиотека использует только `ValueError` и `TypeError` с краткими сообщениями. Это затрудняет отладку, особенно при использовании библиотеки как системы внедрения зависимостей.
### Существующие ошибки:
1. `ValueError: Function ... provided as injector, but return-type is not specified`
2. `ValueError: All callgraph subgraphs must be solved for callseq generation`
3. `ValueError: Unable to compute conversion graph on ...`
4. `ValueError: Unable to select conversion path`
5. `ValueError: Conversion path is not commutative`
6. `TypeError: Param ... must be type-annotated`
---
## Категории ошибок
### 1. Ошибки регистрации инжекторов (Injector Registration Errors)
| Код | Название | Описание |
|-----|----------|----------|
| `INJECTOR_001` | MissingReturnType | У функции-инжектора не указан тип возврата |
| `INJECTOR_002` | MissingParamType | У параметра инжектора не указан тип |
| `INJECTOR_003` | CircularDependency | Обнаружена циклическая зависимость при регистрации |
| `INJECTOR_004` | DuplicateInjector | Зарегистрировано несколько одинаковых инжекторов |
| `INJECTOR_005` | InvalidInjectorSignature | Некорректная сигнатура функции-инжектора |
### 2. Ошибки построения графа (Graph Construction Errors)
| Код | Название | Описание |
|-----|----------|----------|
| `GRAPH_001` | NoConversionPath | Невозможно построить путь преобразования между типами |
| `GRAPH_002` | AmbiguousPath | Найдено несколько путей преобразования (некоммутативность) |
| `GRAPH_003` | CycleDetected | Обнаружен цикл в графе преобразований |
| `GRAPH_004` | TypeMismatch | Тип аргумента не соответствует ожидаемому |
| `GRAPH_005` | MissingDependency | Зависимость не может быть удовлетворена |
### 3. Ошибки генерации кода (Code Generation Errors)
| Код | Название | Описание |
|-----|----------|----------|
| `CODEGEN_001` | TemplateRenderError | Ошибка при рендеринге Jinja2-шаблона |
| `CODEGEN_002` | InvalidGeneratedCode | Сгенерированный код некорректен |
| `CODEGEN_003` | NameCollision | Конфликт имён в сгенерированном коде |
### 4. Ошибки выполнения (Runtime Errors)
| Код | Название | Описание |
|-----|----------|----------|
| `RUNTIME_001` | InjectorCallFailed | Ошибка при вызове функции-инжектора |
| `RUNTIME_002` | ContextManagerError | Ошибка при входе/выходе из контекст-менеджера |
| `RUNTIME_003` | AsyncExecutionError | Ошибка при выполнении асинхронной операции |
### 5. Ошибки конфигурации (Configuration Errors)
| Код | Название | Описание |
|-----|----------|----------|
| `CONFIG_001` | InvalidOptions | Некорректные опции (force_commutative + allow_async и т.д.) |
| `CONFIG_002` | IncompatibleSettings | Несовместимые настройки |
---
## Иерархия исключений
```
BreakshaftError (базовое)
├── InjectorError
│ ├── MissingReturnType
│ ├── MissingParamType
│ ├── CircularDependency
│ ├── DuplicateInjector
│ └── InvalidInjectorSignature
├── GraphError
│ ├── NoConversionPath
│ ├── AmbiguousPath
│ ├── CycleDetected
│ ├── TypeMismatch
│ └── MissingDependency
├── CodegenError
│ ├── TemplateRenderError
│ ├── InvalidGeneratedCode
│ └── NameCollision
├── RuntimeError
│ ├── InjectorCallFailed
│ ├── ContextManagerError
│ └── AsyncExecutionError
└── ConfigurationError
├── InvalidOptions
└── IncompatibleSettings
```
---
## Детальное описание ошибок с примерами
### INJECTOR_001: MissingReturnType
```python
@repo.mark_injector()
def int_to_a(i: int): # Нет -> A
return A(i)
# Ошибка: INJECTOR_001: Function 'int_to_a' missing return type annotation
# Решение: Добавить аннотацию возврата: def int_to_a(i: int) -> A:
```
### INJECTOR_002: MissingParamType
```python
@repo.mark_injector()
def convert(value) -> A: # Нет типа у параметра
return A(value)
# Ошибка: INJECTOR_002: Parameter 'value' missing type annotation
# Решение: Добавить аннотацию: def convert(value: int) -> A:
```
### INJECTOR_003: CircularDependency
```python
@repo.mark_injector()
def a_to_b(a: A) -> B: ...
@repo.mark_injector()
def b_to_c(b: B) -> C: ...
@repo.mark_injector()
def c_to_a(c: C) -> A: ... # Замыкает цикл
# Ошибка: INJECTOR_003: Circular dependency detected: A -> B -> C -> A
# Решение: Разорвать цикл или использовать force_commutative=False
```
### GRAPH_001: NoConversionPath
```python
@repo.mark_injector()
def int_to_a(i: int) -> A: ...
def consumer(dep: B) -> str: ... # B нельзя получить из int
repo.get_conversion((int,), consumer)
# Ошибка: GRAPH_001: No conversion path from (int,) to consumer
# Доступные типы: {A}
# Требуемые типы: {B}
# Решение: Добавить инжектор для получения B
```
### GRAPH_002: AmbiguousPath
```python
@repo.mark_injector()
def int_to_a_direct(i: int) -> A:
return A(i)
@repo.mark_injector()
def int_to_b(i: int) -> B:
return B(float(i))
@repo.mark_injector()
def b_to_a(b: B) -> A:
return A(int(b.b))
def consumer(dep: A) -> int: ...
repo.get_conversion((int,), consumer, force_commutative=True)
# Два пути: int->A и int->B->A дают разные результаты
# Ошибка: GRAPH_002: Ambiguous conversion path (non-commutative graph)
# Путь 1: int -> A (прямой)
# Путь 2: int -> B -> A (через B)
# Решение: Использовать force_commutative=False или убрать один из путей
```
### GRAPH_004: TypeMismatch
```python
@repo.mark_injector()
def convert(s: str) -> A: ...
repo.get_conversion((int,), ...) # int нельзя преобразовать в str
# Ошибка: GRAPH_004: Type mismatch - expected str, got int
```
### CONFIG_001: InvalidOptions
```python
repo.get_conversion(..., allow_async=False, force_async=True)
# Ошибка: CONFIG_001: Invalid options - force_async=True but allow_async=False
# Решение: Установить allow_async=True или force_async=False
```
---
## Формат сообщений об ошибках
Каждое сообщение должно содержать:
1. **Код ошибки** (для поиска в документации)
2. **Краткое описание** (что произошло)
3. **Контекст** (где произошло, какие типы задействованы)
4. **Подсказку** (как исправить)
### Пример формата:
```
BreakshaftError [GRAPH_001]: No conversion path found
Cannot build conversion from source types to target function.
Context:
Source types: (int, float)
Target function: my_module.consumer
Required types: {A, B}
Available types: {A, C, D}
Missing types: {B}
Suggestions:
1. Add an injector that produces type 'B'
2. Check if type annotations are correct
3. Use force_commutative=False if multiple paths are expected
Documentation: https://breakshaft.readthedocs.io/errors/GRAPH_001
```
---
## План реализации
1. **Создать модуль `exceptions.py`** с иерархией исключений
2. **Добавить базовый класс `BreakshaftError`** с форматированием сообщений
3. **Заменить все `raise ValueError`** на специфичные исключения
4. **Добавить дополнительные проверки** (валидация на входе)
5. **Написать тесты** для всех типов ошибок
6. **Добавить документацию** по ошибкам