generated from mononoki/pyproj
151 lines
13 KiB
Markdown
151 lines
13 KiB
Markdown
# Клиент и тесты SIRMENT/core
|
||
|
||
Общий клиент `examples/client.py` используется для ручного запуска и настоящих
|
||
TCP-интеграционных тестов. Fixture загружает его по пути относительно
|
||
репозитория, без запуска CLI при импорте. Папка `src/.test` игнорируется Git и
|
||
не нужна для выполнения тестов из чистого клона.
|
||
|
||
## Установка и запуск
|
||
|
||
Нужен Python 3.11 и зависимости тестов:
|
||
|
||
```sh
|
||
python -m pip install -r req.txt
|
||
python -m pip install -r req-dev.txt
|
||
python -m pytest -q --cov=src --cov-branch --cov-report=term-missing --cov-report=xml
|
||
```
|
||
|
||
Для окружения sites на Windows, из корня репозитория:
|
||
|
||
```bat
|
||
D:\_Programs\anaconda\envs\sites\python.exe -m pip install -r req-dev.txt
|
||
D:\_Programs\anaconda\envs\sites\python.exe -m pytest -q --cov=src --cov-branch --cov-report=term-missing --cov-report=xml
|
||
tests\start_tests.bat
|
||
```
|
||
|
||
Батник переходит в корень репозитория независимо от текущей папки. Без аргументов
|
||
он запускает весь набор с покрытием; с аргументами передаёт их pytest:
|
||
|
||
```bat
|
||
tests\start_tests.bat tests/test_router.py -q
|
||
```
|
||
|
||
Отдельные группы:
|
||
|
||
```sh
|
||
python -m pytest tests/test_client_protocol.py -q
|
||
python -m pytest tests/test_session.py tests/test_session_manager.py tests/test_router.py -q
|
||
python -m pytest tests/test_transport_boundary.py -q
|
||
python -m pytest tests/test_entrypoint.py -q --cov=src --cov-branch
|
||
```
|
||
|
||
Новые регрессионные проверки намеренно не помечены `xfail` и не отключены.
|
||
С текущими ошибками реализации весь набор возвращает ненулевой код. После
|
||
исправления соответствующей ошибки тот же тест должен пройти без изменения
|
||
ожидаемого поведения. Результаты и пробелы проверки — в [REPORT.md](REPORT.md).
|
||
|
||
## Ручной клиент
|
||
|
||
После запуска сервера через `python src/main.py`:
|
||
|
||
```sh
|
||
python examples/client.py rover --host 127.0.0.1 --port 9000 --count 10 --interval 1
|
||
python examples/client.py rover --old-token 123 --old-upf 456
|
||
```
|
||
|
||
На Windows: `examples\start_client.bat rover --count 10 --interval 1`.
|
||
Клиент ждёт `token_ask`, отвечает `token_answer`, ждёт `handshake_ask`, отвечает
|
||
`handshake_answer` и подтверждает состояние `air` ответом на heartbeat.
|
||
Ручной запуск отправляет телеметрию с heartbeat между пакетами. Ожидания ответов
|
||
ограничены таймаутом. Сервер пока не сообщает результат восстановления отдельным
|
||
пакетом; клиент сообщает только факт отправки запроса.
|
||
|
||
## Что проверяется
|
||
|
||
| Файл | Сценарии |
|
||
| --- | --- |
|
||
| `test_client_protocol.py` | Порядок handshake клиента, буферизация данных до heartbeat, таймауты и ошибочные ответы |
|
||
| `test_client_run.py` | Настоящий TCP, метаданные портов, телеметрия, heartbeat, EOF, таймауты всех этапов, восстановление в обе стороны, отказ переноса и однократность |
|
||
| `test_session.py` | Дедлайны 60/5 секунд, собственный watcher, переходы протокола, retry handshake, последние данные, запрет новых отправок в stop/migrated, обработка данных во время фоновой отправки |
|
||
| `test_session_manager.py` | События, срок восстановления 60 секунд, истечение окна, повторный close после удаления, ошибка writer.wait_closed через настоящий Connection.close, безопасность старой очистки после реконекта |
|
||
| `test_router.py` | Исходящие/входящие связи, самоссылки, несвязанные узлы, удаление ссылок и данные отправителя |
|
||
| `test_transport_boundary.py` | Отключение/очистка плохих пакетов, лимит неверных команд, status True/False/Other, auth робота, произвольные JSON-данные без автокаста, heartbeat, TCP-фрейминг, watcher/EOF, фоновые сетевые ошибки |
|
||
| `test_entrypoint.py` | Настоящие main.py/TCP.run в отдельном процессе, handshake, heartbeat и завершение процесса |
|
||
| `test_source_validation.py` | Синтаксис src/examples и краткие docstring каждого теста |
|
||
|
||
Некорректные клиенты проверяются одновременно с исправным соединением:
|
||
закрытие одного транспорта не должно мешать heartbeat и телеметрии другого.
|
||
Локальная ошибка разбора пакета допускается, если транспорт закрывается,
|
||
запись и маршруты удаляются после окна восстановления, а другой клиент работает.
|
||
Ожидаемое исключение разрешается только для проверенного обработчика плохого
|
||
клиента; ошибки остальных задач продолжают проваливать тест.
|
||
|
||
`connection.payload.token` хранится в метаданных `auth` как токен робота.
|
||
Отсутствующий/null токен не мешает подключению; whitelist роутера пока не
|
||
реализован. Это поле отдельно от UID/upf восстановления в `token_answer`.
|
||
По согласованному протоколу отказ handshake означает только JSON `false`:
|
||
`0`, `null`, пустые строка/массив/объект означают успех. Тесты не требуют boolean.
|
||
Для status оставлены три представителя: True, False и Other = 0. Значение 0
|
||
отличает буквальный false от ложного значения Python без перебора всех типов.
|
||
|
||
Payload команд, читающих поля объекта, проверяется на безопасное отклонение,
|
||
если его нельзя разобрать. Содержимое heartbeat не используется, поэтому
|
||
необъектное значение разрешено. Для heartbeat без поля payload допустимы
|
||
обработка или локальное отключение с очисткой. Полезная нагрузка данных
|
||
сохраняется и маршрутизируется без преобразования JSON-значения в словарь.
|
||
|
||
## Метод проверки и завершение задач
|
||
|
||
Интеграционные тесты запускают настоящие TCP-обработчик, Session, SessionManager
|
||
и Router на локальном порту, выбранном ОС. Моки сервера не используются.
|
||
Маршруты задаются явно в тестах: правила будущих API/GUI не предполагаются.
|
||
Изолированные тесты менеджера используют наблюдатель вызовов роутера, а сам
|
||
Router проверяется отдельно.
|
||
|
||
В проверках таймаутов подменяются только монотонные часы и ожидания времени в
|
||
памяти. Тест освобождает выбранный `sleep` явно; реальные паузы в 5/60 секунд
|
||
не нужны. Основные методы сервера не подменяются. Ошибки транспорта вводятся
|
||
локально на одном реальном connection. Искусственная передача Python set в
|
||
сериализацию исключена: она не воспроизводит текущий путь данных из JSON.
|
||
|
||
Гонка watcher/TCP-finally проверяет одно закрытие и одну очистку и допускает
|
||
как ожидание очистки вызывающей корутиной, так и отдельную задачу очистки.
|
||
Ошибка закрытия воспроизводится на `writer.wait_closed` и проходит через
|
||
настоящий `Connection.close`; удаление записи и маршрутов должно состояться.
|
||
Уже существующий тест старой очистки после реконекта сохранён.
|
||
|
||
Фоновые сетевые исключения должны быть видны с исходным исключением; стандартное
|
||
логирование asyncio, включая `Task exception was never retrieved`, допускается.
|
||
Тест не требует немедленного disconnect от одной ошибки отдельной задачи:
|
||
закрытие проверяется через настоящий EOF. При разрыве во время отправки
|
||
допускаются завершение с сетевой ошибкой или отмена задачи; транспорт закрывается
|
||
один раз, другое соединение работает. Отмена уже запланированных отправок при
|
||
смене состояния не вводится как отдельное обязательное правило.
|
||
|
||
Фикстуры и контекст сервера завершают или отменяют созданные задачи, закрывают
|
||
клиентов и listener и проверяют исключения задач. Ожидания ограничены временем;
|
||
ошибка не превращается в зависший тест.
|
||
|
||
## Точка входа и покрытие дочернего процесса
|
||
|
||
В текущем `main.py` адрес/порт заданы напрямую, поэтому процессный тест запускает
|
||
`tests/entrypoint_runner.py`, который выполняет настоящий файл через `runpy`.
|
||
Обёртка перенаправляет только адрес и порт вызова `asyncio.start_server` на
|
||
`127.0.0.1:0` и вызывает оригинальную функцию. Код main.py, TCP.run, handshake,
|
||
сессий, маршрутизации и фоновых отправок не заменяется.
|
||
|
||
Процесс запускается из другой текущей папки. После проверки клиент закрывается,
|
||
а команда по stdin отменяет главную корутину. Это позволяет завершить процесс
|
||
штатно и сохранить покрытие, включая Windows. При зависании есть принудительное
|
||
завершение. Тест не доказывает настраиваемость порта самим production CLI:
|
||
такого интерфейса пока нет.
|
||
|
||
`pytest-cov` и `coverage` находятся в `req-dev.txt`. `.coveragerc` включает
|
||
покрытие ветвей и `patch = subprocess`, чтобы учитывать main.py/TCP.run из
|
||
дочернего процесса. Это [рекомендованный механизм pytest-cov 7](https://pytest-cov.readthedocs.io/en/latest/subprocess-support.html).
|
||
Порог 100% не установлен; ошибки поведения не скрываются высоким процентом.
|
||
`coverage.xml` и `.coverage*` — генерируемые, игнорируемые Git результаты.
|
||
|
||
`fixtures/client_run_expected.json` сохранён как исторический эталон старого
|
||
сценария. Нынешние тесты проверяют протокол и состояние сервера, а не этот лог.
|