76 lines
4.9 KiB
Markdown
76 lines
4.9 KiB
Markdown
# Тесты contracts-flask
|
||
|
||
Набор pytest-тестов под актуальный код сервиса (`site/`).
|
||
Покрытие: юнит (чистая логика), интеграционные (SQLite), HTTP-эндпоинты (Flask test client), безопасность.
|
||
|
||
## Запуск
|
||
|
||
```bash
|
||
cd contracts-flask
|
||
pytest tests/ -q
|
||
```
|
||
|
||
pytest установлен в venv: `/home/naeel/nubes/contracts/.venv/bin/python -m pytest tests/ -q`.
|
||
|
||
## Файлы
|
||
|
||
| Файл | Что тестирует |
|
||
|---|---|
|
||
| `conftest.py` | site/ в `sys.path`; фикстура `db` — изолированная БД (временный `DB_PATH` + свежая схема); защита реальной `/tmp/contracts.db` от пересоздания |
|
||
| `test_metrics.py` | `services/metrics.py`: `normalize_date`, `check_arithmetic` (sum == price×qty), `_to_decimal`, `ClassifyMetrics` |
|
||
| `test_grouping.py` | `services/grouping.py`: `normalize_number`, `group_documents` (группировка contract+supplement, unresolved), `apply_groups` (mock db) |
|
||
| `test_llm_client.py` | `services/llm_client.py`: `FakeLLMClient` (точное/частичное совпадение, default, история вызовов), `HttpxLLMClient` |
|
||
| `test_llm.py` | `services/llm.py`: `call_llm` — парсинг plain JSON и markdown-обёрток (```json) |
|
||
| `test_classify.py` | `services/classify.py`: garbage-фильтры по имени/заголовку, `_safe_json_parse` (7 edge-case: trailing comma, chatter, пусто), `_smart_extract` |
|
||
| `test_parse.py` | `services/parse.py`: `parse_file` (text/docx/pdf, ошибки), `_parse_text` |
|
||
| `test_connection.py` | `db/connection.py`: `_pg_to_sqlite` (все замены %s/::jsonb/BOOLEAN/ILIKE/TRUE), `_extract_table` |
|
||
| `test_spec_events.py` | `db/spec_events.py`: `apply_ops` (ADD/UPDATE/DELETE/UNRESOLVED/unknown), `clear_current`, `reset`, `_hash`, `get_next_seq` |
|
||
| `test_spec_current.py` | `db/spec_current.py`: `list_by_contract` (порядок), `get_elements_json` |
|
||
| `test_process_pipeline.py` | `services/process.py`: `run_pipeline` с Fake LLM — **full_replace не дублирует строки**, **трансляция target_id→target_hash**, `_elements_to_text` |
|
||
| `test_routes.py` | HTTP-эндпоинты (Flask test client): `/health`, `/api/spec-current`, `/api/groups` |
|
||
|
||
## Ключевые проверки
|
||
|
||
- `test_full_replace_no_duplicates` — два `full_replace` подряд → в `spec_current` 3 строки, а не 6; история `spec_events` (6 событий) сохранена.
|
||
- `test_update_applies` — `target_id: "r1"` транслируется в `target_hash` → UPDATE применяется (status `applied`), а не уходит в UNRESOLVED.
|
||
|
||
## Изоляция
|
||
|
||
- Каждый тест, работающий с БД, получает свою копию SQLite через фикстуру `db`
|
||
(monkeypatch `DB_PATH` → `tmp_path`, `init_db()`).
|
||
- LLM-вызовы в пайплайне подменяются через `monkeypatch.setattr("services.llm.call_llm", ...)` — сеть не используется.
|
||
|
||
## Нагрузочные тесты (`tests/load/`, маркер `load`)
|
||
|
||
Долгие стресс-тесты, реально нагружают SQLite/пайплайн. Запуск отдельно:
|
||
|
||
```bash
|
||
pytest -m load -q # только нагрузочные
|
||
pytest -m "not load" -q # быстрые юнит-тесты без нагрузочных
|
||
```
|
||
|
||
| Файл | Что нагружает |
|
||
|---|---|
|
||
| `load/test_load_pipeline.py` | массовый `run_pipeline`: N контрактов × M допников (Fake LLM) — проверка, что `full_replace` не дублирует строки в масштабе |
|
||
| `load/test_load_apply_ops.py` | массовые `apply_ops`: N ADD → N UPDATE → N DELETE, целостность `spec_events` |
|
||
| `load/test_load_concurrency.py` | конкурентная запись в SQLite (WAL + `busy_timeout`), проверка отсутствия потери данных |
|
||
| `load/stress_http.py` | standalone HTTP-стресс: `PUT` на ВМ → `POST /api/upload_refs` (реальный VM-путь) + `/health` |
|
||
|
||
Масштаб через переменные окружения (дефолты — большие):
|
||
|
||
```bash
|
||
LOAD_CONTRACTS=100 LOAD_SUPPS=20 LOAD_SERVICES=10 \
|
||
LOAD_OPS=5000 LOAD_THREADS=4 LOAD_PER=200 \
|
||
pytest -m load -q
|
||
|
||
Примечание по конкурентной записи: стабильный уровень `LOAD_THREADS=4`
|
||
(= `MAX_WORKERS` приложения). `>= 8` — нестабильно: SQLite с
|
||
`busy_timeout=5000` даёт `database is locked` (см. `History/2026-08-26-load-sqlite-locked.md`).
|
||
```
|
||
|
||
HTTP-стресс против локального/прод сервиса:
|
||
|
||
```bash
|
||
python tests/load/stress_http.py --url http://127.0.0.1:5000 --n 1000 --threads 32 --size 100000
|
||
```
|