Metadata-Version: 2.4
Name: doqa-pytest
Version: 0.1.4
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,pytest-bdd,gherkin,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.4
Requires-Dist: pytest>=7.0
Dynamic: license-file

# Плагин pytest: `doqa-pytest`

Плагин передаёт результаты тестов pytest в DoQA: через API DoQA или через файлы Allure-совместимого
формата, которые загружаются в DoQA отдельным шагом. Для каждого теста DoQA получает исход, шаги,
фикстуры, параметры, вложения и ссылки. По этим данным DoQA создаёт и обновляет автотесты.

Ошибки отправки не останавливают тесты и не меняют их результат: плагин пишет WARNING в лог и
продолжает работу.

Требования: Python 3.9 или новее, pytest 7.0 или новее. Сценарии pytest-bdd 6.1–8.x плагин
поддерживает без отдельной настройки, см. [pytest-bdd](#pytest-bdd).

---

## Быстрый старт

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

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

**Шаг 2.** Запустите тесты командой `pytest`.

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

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

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

Когда заданы `url`, `token` и `spaceId`, плагин создаёт прогон в DoQA и отправляет в него
результаты: по умолчанию одним пакетом в конце прогона, с `importRealtime=true` после завершения
каждого тестового модуля.

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

---

## Способ отправки (`reporting`)

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

В файловом режиме тестовому процессу не нужны ни доступ к 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 отклонил запрос или не ответил (401 или 403 из-за токена, 422 при отсутствии
активной CI-привязки, сетевая ошибка), плагин не отключается, а записывает результаты всего
прогона в `resultsDir` и пишет в лог WARNING с причиной и подсказкой. Если DoQA отклонил пакет
результатов посреди прогона (обрыв связи, отозванный токен), в файлы записывается только этот
пакет, остальные плагин продолжает отправлять через API.

Рядом с результатами плагин записывает `doqa-reporting.properties` (`sink=api|files`, `runId`,
`delivered`, `fallbackResults`). По этому файлу джоба загрузки отличает ситуацию «файлов нет,
потому что всё отправлено через API» от ситуации «плагин не отработал» и загружает то, что
осталось на диске.

---

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

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

| Ключ (`doqa.properties`) | Переменная окружения | Опция 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` | токен проекта или персональный токен | нет |
| `spaceId` (алиас `projectId`) | `DOQA_SPACE_ID` (`DOQA_PROJECT_ID`) | `--doqa-space-id` | id пространства | нет |
| `configurationId` | `DOQA_CONFIGURATION_ID` | `--doqa-configuration-id` | конфигурация прогона (браузер, ОС, окружение) | нет |
| `testRunId` | `DOQA_TEST_RUN_ID` | `--doqa-test-run-id` | существующий прогон (нужен для режимов 0 и 1) | нет |
| `testRunName` | `DOQA_TEST_RUN_NAME` | `--doqa-test-run-name` | имя создаваемого прогона (режим 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-сертификат и имя хоста (для самоподписанных сертификатов) | `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; 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.

### Режимы прогона

**Режим 2 (`new`, по умолчанию).** Плагин создаёт прогон и отправляет в него все результаты.
Подходит для CI, где выполняются все тесты. Запрос на создание содержит идемпотентный ключ, поэтому
повтор запроса после сетевой ошибки не создаёт второй прогон.

**Режим 1 (`existing`).** Плагин отправляет все результаты в существующий прогон `testRunId`,
созданный заранее в интерфейсе DoQA или через API. Если `testRunId` задан, а `adapterMode` нет,
плагин работает в этом режиме, чтобы указанный прогон не был проигнорирован.

**Режим 0 (`selective`).** Плагин запрашивает у DoQA список автотестов прогона `testRunId` и
выполняет только их. Остальные тесты плагин исключает на этапе сбора через стандартный хук
`pytest_deselected`, поэтому на них не тратится время CI. Этот режим DoQA включает, когда запускает
выбранные автотесты в CI: он передаёт в пайплайн `DOQA_TEST_RUN_ID` и `DOQA_ADAPTER_MODE=0`.

В режиме 0 плагин выполняет тесты **в порядке плана** DoQA без дополнительной настройки. Это
порядок набора запуска, который задают в интерфейсе DoQA перетаскиванием. Тесты, которых нет в
плане, выполняются в конце в исходном порядке. Идентификаторы с плейсхолдером (`login_{browser}`)
при отборе сравниваются по маске: в прогон попадают все инвокации, раскрытый идентификатор которых
есть в плане, а точная проверка выполняется при отправке результата.

`pytest --collect-only` к DoQA не обращается и прогон не создаёт. Если ни один собранный тест не
совпал со списком прогона, плагин пишет WARNING со списком выбранных идентификаторов. Обычно это
значит, что каталог автотестов в 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` (по умолчанию путь модуля через
точки) и `@doqa.class_name` (по умолчанию класс или модуль). Все декораторы, кроме `@doqa.id`,
работают и на классах и тогда действуют на каждый тест класса. `@doqa.id` ставится только на
функции: id на классе объединил бы все его тесты в один автотест, поэтому такое использование
вызывает `TypeError`.

`@doqa.create_manual_case` запрашивает создание ручного тест-кейса для автотеста, у которого нет
привязки к кейсам. Декоратор работает на методе и на классе, тот же запрос можно сделать из тела
теста вызовом `doqa.add_create_manual_case()`. Источники дополняют друг друга, выключить создание
для отдельного теста нельзя. Кейс создаётся, когда DoQA получает результат; для автотестов, которые
уже есть в DoQA без кейсов, кейсы задним числом не создаются.

Обычные метки pytest без аргументов (`@pytest.mark.smoke`) автоматически попадают в теги автотеста,
дублировать их не нужно. Служебные метки вроде `skipif` и `parametrize` в теги не попадают.

### Идентификатор автотеста

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

DoQA принимает идентификатор длиной до 255 символов. Более длинный идентификатор плагин не отправляет: он пишет
WARNING и берёт идентификатор из следующего источника. Имя автотеста длиннее 255 символов и
заголовок шага длиннее 500 символов обрезаются, иначе DoQA отклонил бы весь пакет.

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

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

```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` вызовы ничего не делают. Внутри фикстур они тоже
работают: шаги и вложения из фикстуры попадают в её узел setup или teardown.

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

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

---

## Фикстуры

Фикстуры попадают в отчёт без дополнительной настройки:

- **фикстуры с scope function**: блоки setup и teardown каждого результата;
- **фикстуры с scope class и module**: общие фикстуры всех тестов своего класса или модуля
  (аналог `@BeforeAll`/`@AfterAll`); вложенный класс видит только свои;
- **фикстуры с scope session и package**: фикстуры всех тестов прогона;
- финализаторы (часть после `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` |

DoQA по-разному обрабатывает `failed` и `broken` при кластеризации ошибок и анализе нестабильных
тестов. Если упала 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

В режиме 2 прогон в DoQA создаёт процесс-контроллер xdist, один раз. Он передаёт прогон workers
через механизм xdist, и workers отправляют результаты в этот прогон. Без этого каждый процесс
создал бы свой прогон. В режимах 0 и 1 прогон задан настройками, и workers работают с ним напрямую.
Порядок плана при параллельном запуске не гарантируется.

---

## pytest-bdd

Сценарии [pytest-bdd](https://pypi.org/project/pytest-bdd/) 6.1–8.x являются обычными тестами pytest, поэтому
для них ничего настраивать не нужно: достаточно установить `doqa-pytest` рядом с `pytest-bdd`. Всё
остальное из этого README действует и для сценариев: режимы прогона, выборочный запуск, порядок по
плану, realtime, файловый режим, pytest-xdist, pytest-rerunfailures, фасад `doqa` в коде шагов.

### Что получает DoQA

- **Автотест**: сценарий. Scenario Outline даёт один автотест, каждая строка Examples даёт
  отдельный результат с параметрами по колонкам (`user = eve`, `password = p1`).
- **Имя**: имя сценария. В каталоге у Outline остаётся шаблон (`Вход под <user>`), у результата
  строки подставлены значения (`Вход под eve`). `@doqa.display_name` и `@allure.title` над
  `@scenario` важнее имени сценария.
- **Шаги**: шаги сценария с ключевым словом из файла (`Given корзина пуста`, `Дано …`), включая
  шаги Background. Упавший шаг получает свой исход и сообщение, шаги после него передаются как
  `skipped`. Шаг без определения получает `broken` с сообщением «Нет определения шага». В описании
  автотеста у Outline шаги остаются шаблоном с `<…>`.
- **DocString и DataTable**: текстовое вложение шага.
- **Вложенные шаги**: `doqa.step`, `doqa.attach` и фикстуры, которые шаг запрашивает, попадают
  внутрь своего шага Gherkin. Внутренние фикстуры pytest-bdd в отчёт не попадают.
- **Теги** сценария, Rule и Feature становятся тегами автотеста, кроме тегов DoQA (ниже).

### Теги DoQA

В `.feature` нельзя повесить декоратор, поэтому разметку DoQA задают тегами:

| Тег | Что делает | Аналог в коде |
|---|---|---|
| `@doqa.id:<значение>` | идентификатор автотеста, дословно; `{колонка}` подставляется из строки Examples | `@doqa.id` |
| `@DOQA-<n>` или `@DOQA:<n>` | идентификатор `DOQA-<n>`; то же даёт `[DOQA-n]` в имени сценария | `[DOQA-n]` в имени |
| `@allure.id:<n>` | идентификатор `ALLURE-<n>` и связь с ручным кейсом `<n>` | `allure.id` |
| `@doqa.case:<n>` | привязка ручного кейса `<n>`; для нескольких кейсов тег повторяют | `@doqa.case_ids` |

```gherkin
@doqa.case:1041
Feature: Корзина

  @doqa.id:CART-BUY-{product} @doqa.case:1042
  Scenario Outline: Покупка <product>
    When пользователь покупает "<product>"
    Then в корзине "<product>"

    Examples:
      | product |
      | яблоко  |
      | груша   |
```

Здесь каждая строка Examples становится отдельным автотестом (`CART-BUY-яблоко`, `CART-BUY-груша`), и оба
присылают кейсы 1041 и 1042. В DoQA у ручного кейса один автотест, поэтому кейс, который пришёл от
нескольких автотестов, остаётся за тем, чей результат DoQA принял последним. Тег `@doqa.case:` на
Feature или на Outline с `{колонка}` удобен, когда в фиче один сценарий; иначе ставьте разные кейсы
на уровне сценария.

Правила:

- Источники идентификатора проверяются сверху вниз, как в таблице. Внутри одного источника
  побеждает ближайший уровень: сценарий, затем Rule (pytest-bdd 8), затем Feature. Два тега одного
  источника на одном уровне дают WARNING, берётся первый в файле. Идентификатор на Feature даёт всем
  её сценариям один ключ и предупреждение о дубле.
- `@doqa.case:` складывается со всех уровней и с `@doqa.case_ids`. Нечисловое значение даёт WARNING,
  тег игнорируется.
- Декоратор `@doqa.id` над `@scenario` важнее тега `@doqa.id:`.
- Вместо `:` можно писать `=` (`@allure.id=5`), регистр префикса `doqa.` не важен.
- Теги DoQA не становятся маркерами pytest: регистрировать их для `--strict-markers` не нужно, в теги
  автотеста они не попадают, а `pytest -m` по ним не отбирает. Остальные теги по-прежнему становятся
  маркерами и уходят в теги автотеста в алфавитном порядке.
- При выборочном запуске (режим 0) сценарии отбираются по тем же тегам на этапе сбора, в том числе
  строки Outline по `{колонка}` в `@doqa.id:`.

### Ограничения

- **Теги DoQA на блоке Examples не поддерживаются.** pytest-bdd 8 вешает теги Examples маркерами в
  обход плагина, и с `--strict-markers` падает сбор всего модуля; pytest-bdd 6 и 7 такие теги
  теряют. Плагин игнорирует тег DoQA на Examples с WARNING. Вместо него используйте `{колонка}` в
  теге сценария.
- **Ключ без тегов остаётся прежним:** хэш от модуля и имени функции теста (`pytest:…`), как у
  обычного теста pytest. Имя сценария и файл фичи в ключ не входят. Функцию, которую создаёт
  `scenarios()`, pytest-bdd называет по имени сценария, поэтому переименование сценария меняет
  ключ. Если `scenarios()` подключает в один модуль одноимённые сценарии разных фич, pytest-bdd
  добавляет суффиксы `_1`, `_2` в порядке обхода фич. Таким сценариям задайте `@doqa.id:`.
- **Имена существующих автотестов не меняются.** DoQA обновляет имя автотеста, только если оно
  заканчивается именем функции теста. Автотесты, созданные версиями до 0.1.4, сохранят имя вида
  `test_login_outline[eve-p1]`; новые сразу получают имя сценария.
- **Переход с 0.1.3.** У сценариев, где уже стоят теги `@doqa.id:`, `@DOQA-<n>`, `@DOQA:<n>`,
  `@allure.id:` или метка `[DOQA-n]` в имени, ключ меняется на заданный тегом. Эти теги и
  `@doqa.case:` уходят из тегов автотеста и перестают быть маркерами pytest. Параметр
  `_pytest_bdd_example` заменён параметрами по колонкам.
- pytest-bdd 6.x проверяется только с pytest 7.

---

## Переход с allure-pytest

Плагин читает разметку Allure без зависимости от Allure, по именам меток:

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

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

---

## Устранение неполадок

| Симптом | Причина и решение |
|---|---|
| Результатов нет ни в DoQA, ни в файлах | задан `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`, и предупреждение пропадёт |
| DoQA недоступен или отклонил токен, в `results/` появились файлы | плагин перешёл в файловый режим (в логе WARNING «could not establish the test run» или «results chunk failed»). Загрузите `results/` джобой загрузки, как в файловом режиме |
| В логе «unexpanded variable reference» | в процесс попала строка `$DOQA_TOKEN` без подстановки: переменная CI не существует или не передана в джобу |
| Выборочный прогон не выполнил ни одного теста | WARNING перечисляет выбранные идентификаторы: каталог автотестов в DoQA не совпадает с кодом. Отправьте в DoQA результаты полного прогона |
| Самоподписанный сертификат | `certValidation=false` (только для тестовых стендов) |
| DoQA доступен через прокси | `proxy=host:port` |
| Параметры или сообщения обрезаны с `… truncated` | увеличьте `maxParameterLength`, `maxMessageLength` или `maxTraceLength` |
| Локальные запуски создают прогоны в DoQA | уберите токен из локальной конфигурации или задайте локально `reporting=files` |
| Нужно выключить плагин для одного запуска | `pytest -p no:doqa` |

---

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

Плагин находится в монорепозитории `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)
