Metadata-Version: 2.4
Name: doqa-pytest
Version: 0.1.0
Summary: DoQA adapter for pytest: reports autotests and results to DoQA or to Allure-compatible files
Author-email: DoQA team <support@doqa.app>
License: Apache-2.0
Project-URL: Homepage, https://doqa.app
Project-URL: Repository, https://github.com/doqa-app/doqa-python
Project-URL: Issues, https://github.com/doqa-app/doqa-python/issues
Keywords: doqa,pytest,testing,reporting,autotests
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: doqa-client==0.1.0
Requires-Dist: pytest>=7.0
Dynamic: license-file

# DoQA pytest Adapter - `doqa-pytest`

Адаптер отправляет результаты ваших pytest-тестов в DoQA: автотесты создаются/обновляются сами,
результаты приходят с шагами, фикстурами, параметрами, вложениями и ссылками. Работает в двух
режимах - **напрямую в API** DoQA или **файлами** (Allure-совместимые артефакты, без сети и без
токена в тестовом процессе). Ничего не ломает: если DoQA недоступен или не настроен, ваши тесты
проходят как обычно.

---

## Быстрый старт (2 минуты)

**Шаг 1.** Установите пакет:

```bash
pip install doqa-pytest
```

**Шаг 2.** Запустите тесты: `pytest`.

Плагин регистрируется автоматически через entry point `pytest11`. Без какой-либо конфигурации
адаптер уже пишет результаты в `./results/` - файлы Allure-совместимого формата, который
принимает конвейер загрузки DoQA (заодно они открываются обычным Allure Report). Загрузить их
в DoQA можно командой `doqactl upload` или CI-джобой. Разметка не обязательна: каждый тест
получает стабильный идентификатор автоматически.

**Шаг 3 (опционально).** Чтобы слать результаты сразу в DoQA - создайте `doqa.properties`
в рабочей директории запуска pytest (путь можно переопределить опцией `--doqa-config` или
переменной `DOQA_CONFIG`) или задайте те же ключи через env/опции pytest:

```properties
url=https://demo.doqa.app
token=<project token из настроек пространства>
spaceId=42
```

С этими тремя ключами адаптер переключается в API-режим: сам создаёт тест-ран и наполняет его
одним батчем в конце прогона (или в реальном времени - `importRealtime=true`).

> **Внимание:** токен в конфиге = каждый запуск тестов пишет в DoQA, включая локальные.
> Обычная схема: локально конфига нет (файлы никуда не отправляются), в CI ключи приходят из
> переменных окружения `DOQA_URL` / `DOQA_TOKEN` / `DOQA_SPACE_ID`.

---

## Как адаптер решает, куда слать (`reporting`)

| `reporting=` | Что происходит |
|---|---|
| `auto` *(по умолчанию)* | есть `url`+`token`+`spaceId` → API; нет → файлы **и WARNING в логе** с перечислением недостающих настроек |
| `api` | только API (без полного конфига - WARNING в лог и адаптер отключается) |
| `files` | только файлы Allure-совместимого формата в `resultsDir` (по умолчанию `results/`) |
| `off` | адаптер выключен полностью |

Файловый режим - это «путь CI-артефактов»: тестовому процессу не нужны ни сеть до DoQA, ни
секреты. Файлы забирает следующий шаг пайплайна:

```yaml
# .gitlab-ci.yml
test:
  script: pytest            # адаптер пишет results/
  artifacts:
    paths: [results/]

upload-to-doqa:
  needs: [test]
  script: doqactl upload --token "$DOQA_TOKEN" --space "$DOQA_SPACE_ID" results/
```

---

## Полная конфигурация

Источники (по возрастанию приоритета): файл `doqa.properties` → переменные окружения `DOQA_*` →
опции pytest `--doqa-*` и их ini-двойники `doqa_*` (опция командной строки сильнее ini-ключа).
Путь к файлу можно переопределить: `--doqa-config=…` / `DOQA_CONFIG`.

| Ключ (`doqa.properties`) | Env | Опция pytest | Что это | Дефолт |
|---|---|---|---|---|
| `reporting` | `DOQA_REPORTING` | `--doqa-reporting` | `api` / `files` / `auto` / `off` | `auto` |
| `resultsDir` | `DOQA_RESULTS_DIR` | `--doqa-results-dir` | каталог файлового режима | `results` |
| `url` | `DOQA_URL` | `--doqa-url` | адрес DoQA | - |
| `token` (алиас `privateToken`) | `DOQA_TOKEN` (`DOQA_PRIVATE_TOKEN`) | `--doqa-token` | project/personal token | - |
| `spaceId` (алиас `projectId`) | `DOQA_SPACE_ID` (`DOQA_PROJECT_ID`) | `--doqa-space-id` | id пространства | - |
| `configurationId` | `DOQA_CONFIGURATION_ID` | `--doqa-configuration-id` | конфигурация прогона (browser/OS/env) | - |
| `testRunId` | `DOQA_TEST_RUN_ID` | `--doqa-test-run-id` | существующий ран (нужен для mode 0 и 1) | - |
| `testRunName` | `DOQA_TEST_RUN_NAME` | `--doqa-test-run-name` | имя создаваемого рана (mode 2) | - |
| `adapterMode` | `DOQA_ADAPTER_MODE` | `--doqa-adapter-mode` | режим выбора рана: `0`/`selective`, `1`/`existing`, `2`/`new` - см. ниже | `2` |
| `importRealtime` | `DOQA_IMPORT_REALTIME` | `--doqa-import-realtime` | `true` = стрим результатов по мере прогона (пакет на каждый завершённый модуль, вместе с его teardown-фикстурами) | `false` (батч в конце) |
| `certValidation` | `DOQA_CERT_VALIDATION` | `--doqa-cert-validation` | `false` = доверять самоподписанным TLS (отключает и проверку hostname) | `true` |
| `proxy` | `DOQA_PROXY` | `--doqa-proxy` | HTTP-прокси, `host:port` | - |
| `environment` | `DOQA_ENVIRONMENT` | `--doqa-environment` | метка окружения прогона (матрица окружений DoQA) | - |
| `pipelineId` | `DOQA_PIPELINE_ID` | `--doqa-pipeline-id` | привязка рана к CI-пайплайну | авто: `CI_PIPELINE_ID` / `GITHUB_RUN_ID` |
| `ciRunId` | `DOQA_CI_RUN_ID` | `--doqa-ci-run-id` | id CI-запуска, инициированного из DoQA - приезжает в пайплайн сам и уезжает обратно с результатами | - |
| `branch` | `DOQA_BRANCH` | `--doqa-branch` | ветка прогона | авто: `CI_COMMIT_REF_NAME` / `GITHUB_REF_NAME` |
| `batchSize` | `DOQA_BATCH_SIZE` | `--doqa-batch-size` | максимум результатов в одном батч-запросе | `100` |
| `requestTimeoutMs` | `DOQA_REQUEST_TIMEOUT_MS` | `--doqa-request-timeout-ms` | таймаут HTTP-запроса | `30000` |
| `retries` | `DOQA_RETRIES` | `--doqa-retries` | попыток на запрос (ретраятся только безопасные повторы) | `3` |
| `retryBackoffMs` | `DOQA_RETRY_BACKOFF_MS` | `--doqa-retry-backoff-ms` | базовая пауза между попытками (экспоненциальная) | `500` |
| `maxTraceLength` | `DOQA_MAX_TRACE_LENGTH` | `--doqa-max-trace-length` | лимит длины stack trace в результате (символов) | `100000` |
| `maxMessageLength` | `DOQA_MAX_MESSAGE_LENGTH` | `--doqa-max-message-length` | лимит длины сообщений | `10000` |
| `maxParameterLength` | `DOQA_MAX_PARAMETER_LENGTH` | `--doqa-max-parameter-length` | лимит длины значений параметров | `2000` |

Ini-двойник каждой опции - тот же суффикс через подчёркивания: `doqa_url`, `doqa_space_id`,
`doqa_results_dir`, … (в `pytest.ini` / `[tool.pytest.ini_options]`). Явно ПУСТОЕ значение
опции (`--doqa-pipeline-id=`) очищает поле, унаследованное из нижнего слоя; пустые переменные
окружения игнорируются (CI-системы экспортируют их для незаданных настроек). Значение, целиком
состоящее из нераскрытой ссылки (`$DOQA_TOKEN` / `${DOQA_TOKEN}`), считается незаданным и
подсвечивается WARNING'ом - так выглядит несуществующая CI-переменная, дотёкшая до процесса
литералом. Файл настроек читается в UTF-8.

### Режимы запуска (API)

- **mode 2 / `new`** *(дефолт)* - адаптер сам создаёт тест-ран и шлёт всё в него. Для CI
  «просто прогони всё». Создание несёт идемпотентный ключ - повтор запроса при сетевом сбое
  никогда не оставит два рана.
- **mode 1 / `existing`** - шлёт всё в существующий ран `testRunId` (ран создали заранее - из
  UI или API). Если задан `testRunId`, а `adapterMode` не задан явно - адаптер сам работает в
  этом режиме (указанный ран никогда не игнорируется молча).
- **mode 0 / `selective`** - **селективный прогон**: адаптер спрашивает у DoQA, какие автотесты
  числятся в ране `testRunId`, и физически исполняет только их - остальные деселектятся ещё на
  сборе коллекции (честный `pytest_deselected`, экономия времени CI). Это тот режим, которым
  Run Player DoQA перезапускает выбранные тесты.

В mode 0 тесты исполняются **в порядке плана** DoQA (порядок набора запуска, задаётся в UI
drag-sort'ом) - автоматически, никакие orderer'ы включать не нужно; тесты вне плана стабильно
уходят в хвост. Шаблонные id с плейсхолдером (`login_{browser}`) матчатся по wildcard - в план
попадают все инвокации, чей раскрытый id в нём есть, а точный отбор происходит при отправке
результата. `pytest --collect-only` сервер не трогает и ран не создаёт. Если из выборки рана
не совпал ни один собранный тест, адаптер пишет WARNING со списком выбранных id - обычно это
значит, что каталог автотестов в DoQA отстал от кода и суточный прогон нужно перезалить.

---

## Разметка тестов (всё опционально)

```python
import doqa


@doqa.label("regression")                       # класс-уровень: наследуется всеми тестами
class TestLogin:

    @doqa.id("DOQA-42")                         # стабильный ключ автотеста (рекомендуем)
    @doqa.title("Успешный вход")
    @doqa.description("Проверяет happy-path входа по паролю")
    @doqa.display_name("Вход по паролю")        # имя автотеста (иначе - имя функции)
    @doqa.label("smoke")                        # объединится с класс-уровнем
    @doqa.label("owner", "qa-team")             # key:value-метка (owner/severity/epic/...)
    @doqa.tag("ui")
    @doqa.link.defect("https://tracker/BUG-77", title="флак на CI")
    @doqa.case_ids(1041)                        # привязка к ручным кейсам DoQA (N штук)
    @doqa.create_manual_case                    # завести связанный ручной кейс
    def test_login_happy_path(self):
        ...
```

Ссылки: универсальный `@doqa.link(url, type=…, title=…, description=…)` и типизированные
шорткаты `doqa.link.related` / `defect` / `requirement` / `blocked_by` / `repository`. Ещё два
декоратора управляют местом теста в дереве DoQA: `@doqa.namespace` (по умолчанию - dotted-путь
модуля) и `@doqa.class_name` (по умолчанию - класс или модуль). Все декораторы, кроме
`@doqa.id`, работают и на классах - тогда действуют на каждый тест класса; `@doqa.id` ставится
только на функции (id на классе схлопнул бы все его тесты в один автотест, такое применение
отклоняется с `TypeError`).

`@doqa.create_manual_case` - точечный opt-in на автоматическое создание ручного тест-кейса,
связанного с «сиротским» автотестом (тем, у которого нет привязки к кейсам). Работает и на
методе, и на классе, и из тела теста (`doqa.add_create_manual_case()`); источники складываются,
точечно выключить создание нельзя. Кейс заводится в момент приёма результата: ретроспективно,
для уже накопленных «сиротских» автотестов, ничего не создаётся.

Обычные no-arg марки pytest (`@pytest.mark.smoke`) автоматически попадают в `tags` автотеста -
двойная разметка не нужна (структурные марки вроде `skipif`/`parametrize` не в счёт).

### Каскад идентификации

Если `@doqa.id` нет, идентификатор ищется в таком порядке: `[DOQA-123]` или `@DOQA:123` в
display name → Allure id (`allure.id`, читается без зависимости от Allure - удобно при
миграции; атрибуция `ALLURE-<id>`) → детерминированный хэш полного имени теста
(модуль + класс + функция, без параметров инвокации). История автотеста стабильна в любом
случае; явный id делает её устойчивой ещё и к переименованиям.

### Параметризованные тесты

Аргументы каждой инвокации уезжают как именованные параметры результата:

```python
@pytest.mark.parametrize("browser", ["chrome", "firefox"])
def test_works_in(browser):
    ...                                         # parameters: [{name: "browser", value: "chrome"}]
```

По умолчанию все инвокации - один автотест. Хотите **отдельный автотест на каждую инвокацию** -
используйте плейсхолдер `{имя_аргумента}` в любом декораторе (`id`, `title`, `display_name`,
метки/теги/ссылки):

```python
@pytest.mark.parametrize("browser", ["chrome", "firefox"])
@doqa.id("LOGIN-IN-{browser}")                  # -> LOGIN-IN-chrome, LOGIN-IN-firefox
@doqa.title("Вход в {browser}")
def test_login_in(browser):
    ...
```

Нераспознанный плейсхолдер остаётся в тексте как есть; значения параметров усекаются по
`maxParameterLength` с маркером `… truncated (N chars)`.

---

## Шаги

`doqa.step` - одновременно контекст-менеджер и декоратор, вложенность без ограничений:

```python
with doqa.step("открыть страницу логина"):
    page.open()
    with doqa.step("ввести пароль", "форма из конфига"):   # второй аргумент - description
        page.type_password(password)


@doqa.step("авторизоваться под {user}")         # {param}-плейсхолдеры из аргументов функции
def authorize(user):
    ...


@doqa.step                                      # без текста - имя функции
def prepare_cart():
    ...
```

Упавший шаг закрывается с исходом ошибки (`failed` для assert и `pytest.fail`, `broken` для
прочих исключений) и её сообщением; исключение летит дальше как обычно. Работают только формы
`with doqa.step(...)` и `@doqa.step`: голый вызов `doqa.step("...")` без `with` шаг не создаёт.

## Runtime-API - из тела теста

```python
import doqa

doqa.add_parameter("env", "staging")            # значение уезжает типизированным, как есть
doqa.attach_file("artifacts/screenshot.png")    # файл к тесту или открытому шагу
doqa.attach(response_bytes, name="response.json",
            content_type="application/json")    # вложение из памяти, без temp-файла
doqa.attach(log_text, name="app.log")           # строка -> text/plain
doqa.add_link("https://tracker/TASK-5", type="requirement")
doqa.add_message("покупатель создан через фабрику")   # заметка к открытому шагу или тесту
doqa.add_case_ids(1042)
doqa.add_create_manual_case()
doqa.add_external_id("CART-DYN-1")              # пин id текущей инвокации (сильнее декоратора)
doqa.add_title("…"); doqa.add_description("…"); doqa.add_display_name("…")
doqa.add_labels("…"); doqa.add_tags("…")
doqa.add_label("severity", "critical")          # key:value-метка
```

Все вызовы безопасны: вне активного теста (или при `reporting=off`) они просто no-op.
Работают и внутри фикстур - шаги и вложения из фикстуры попадают в её setup/teardown-узел.

Шаги и вложения из потоков, которые тест порождает сам (свои executor'ы), нужно явно перенести
в контекст теста - контекст живёт в thread-local:

```python
ctx = doqa.capture_context()
executor.submit(lambda: doqa.run_with(ctx, lambda: worker_check()))
```

---

## Фикстуры = setup/teardown

Никаких флагов - фикстуры попадают в отчёт сами:

- **function-scoped** - блоки setup/teardown в каждом результате;
- **class- и module-scoped** - общие фикстуры у всех тестов своего класса/модуля (аналог
  `@BeforeAll`/`@AfterAll`; вложенный класс видит только свои);
- **session- и package-scoped** - у всех тестов прогона;
- финализаторы (`yield`-часть и `addfinalizer`) - блоки teardown;
- `doqa.step` / `doqa.attach*` внутри фикстуры вкладываются в её узел;
- служебные фикстуры самого pytest (`tmp_path`, `capsys`, …) в отчёт не попадают;
- имя узла - имя фикстуры, `@allure.title` на фикстуре подхватывается.

---

## Маппинг исходов

| Что случилось | Исход в DoQA |
|---|---|
| Тест прошёл | `passed` |
| Упала проверка (`AssertionError`, `pytest.fail`) | `failed` |
| Любое другое исключение (инфраструктура, таймаут) | `broken` |
| `pytest.skip` / `skipif` | `skipped` |
| `xfail` сработал (ожидаемое падение) | `skipped`, в сообщении `XFAIL <reason>` и реальная ошибка |
| `xfail` не сработал (XPASS) | `passed`, в сообщении `XPASS <reason>`; strict - `failed` |

`failed` vs `broken` - важное различие: кластеризация ошибок и flaky-аналитика DoQA
обрабатывают их по-разному. Падение teardown-фикстуры вливается в результат теста: сообщения
складываются, «инфраструктурный» `broken` перевешивает `failed` тела.

---

## Батч и realtime

По умолчанию результаты копятся и уезжают одним потоком чанков (по `batchSize`) в конце
прогона. `importRealtime=true` включает стрим: пакет отправляется на каждый завершённый модуль,
вместе с его teardown-фикстурами - прогресс рана в DoQA виден по ходу прогона. В обоих режимах
ошибка отправки логируется и не роняет тесты, а буфер добивается финальным флашем (в том числе
шатдаун-хуком процесса).

---

## Файловый режим

Полный аналог API-режима без сети: в `resultsDir` пишутся `<uuid>-result.json` на каждый тест,
`<uuid>-container.json` на фикстуры (только когда они есть), вложения и
`environment.properties` (когда задан ключ `environment`). Формат Allure-совместимый - файлы
принимает конвейер загрузки DoQA и открывает обычный Allure Report.

---

## pytest-xdist

Поддерживается из коробки: в mode 2 тест-ран создаёт контроллер - один раз - и передаёт его
воркерам через handshake xdist, воркеры репортят в него (иначе каждый процесс завёл бы свой
ран). В mode 0 и 1 ран и так зафиксирован конфигом - воркеры работают с ним напрямую. Порядок
плана в параллельном прогоне, естественно, не гарантируется.

---

## Миграция с allure-pytest

Разметка Allure подхватывается автоматически и без зависимости от Allure - по именам марок:

- `allure.id` - атрибуция `ALLURE-<id>` (участвует в каскаде идентификации);
- `@allure.epic` / `feature` / `story` / `owner` / `severity` - `key:value`-метки;
- `@allure.link` / `issue` / `tms` - типизированные ссылки;
- `@allure.description` - описание (если не задано через `doqa.description`);
- `@allure.title` - display name теста и имя узла фикстуры.

Разметка `doqa.*` всегда сильнее allure-совместимой. Файловый режим эмитит Allure-совместимые
результаты - существующий пайплайн загрузки продолжит работать.

---

## Траблшутинг

| Симптом | Причина и лечение |
|---|---|
| Результатов нигде нет | `reporting=api` без `url`/`token`/`spaceId` - смотрите WARNING в логе; либо `reporting=off`; либо плагин отключён `-p no:doqa` |
| Результаты в `results/`, а ждали в DoQA | это `auto` без API-конфига - в логе WARNING «no reporting configuration found (missing …)»; задайте `url`/`token`/`spaceId`. Если файловый режим выбран сознательно, поставьте `reporting=files` - предупреждение исчезнет |
| В логе «unexpanded variable reference» | до процесса дотёк литерал `$DOQA_TOKEN` - CI-переменная не существует или не экспортирована в джобу |
| Селективный прогон ничего не запустил | WARNING перечисляет выбранные id - каталог автотестов в DoQA отстал от кода, перезалейте полный прогон |
| Самоподписанный сертификат | `certValidation=false` (только для тестовых стендов!) |
| DoQA за прокси | `proxy=host:port` |
| Параметры/сообщения обрезаны `… truncated` | поднимите `maxParameterLength` / `maxMessageLength` / `maxTraceLength` |
| Локальные прогоны спамят раны в DoQA | уберите токен из локального конфига или поставьте локально `reporting=files` |
| Нужно разово выключить адаптер | `pytest -p no:doqa` |

Ошибка отправки **никогда не роняет прогон** - адаптер пишет WARNING и продолжает.

---

## Сборка адаптера из исходников

Адаптер живёт в монорепозитории `doqa-python` вместе со своим ядром `doqa-client` и собирается
парой editable-установок:

```bash
pip install -e ./doqa-client -e ./doqa-pytest
python -m pytest doqa-pytest/tests
```

## Лицензия

[Apache License 2.0](../LICENSE)
