SaveTest — ДокументацияSaveTest — Документация Сайт
Руководство пользователя
Руководство администратора
Установка и разработка
Руководство пользователя
Руководство администратора
Установка и разработка
  • Установка и разработка

    • Установка и разработка
    • Быстрый старт
    • Установка
    • Обновление
    • Домен и SSL — Nginx (опционально)
    • Разработка плагинов парсеров
Сайт

Разработка плагинов парсеров

Плагинная система позволяет добавлять поддержку новых языков и форматов тестовых файлов без изменения кода ядра SaveTest. Каждый язык или формат обрабатывается отдельным сервисом с единым HTTP-контрактом.

Для начала работы рекомендуется изучить готовые плагины из комплекта поставки в репозитории savelinkQA/savetest-plugins: parser-plugins/python-parser и parser-plugins/gherkin-parser — они служат рабочими примерами и содержат base_parser.py, который нужно взять за основу.

Назначение и устройство

Каждый плагин — отдельный микросервис на FastAPI с REST API для разбора файлов. Бэкенд SaveTest вызывает плагины по URL, объявленным в переменных окружения (PLUGIN_URLS), и при старте считывает метаданные через GET /config.

Поставляемые плагины (пример состава)

  • python-parser
    • Язык/формат: Python .py
    • Фреймворки: pytest, unittest
  • gherkin-parser
    • Язык/формат: Gherkin .feature
    • Фреймворки: Cucumber, Behave

Состав набора может расширяться; структура каталога в комплекте разработчика плагинов: parser-plugins/<имя-плагина>/.

Режимы парсинга

POST /parse — один файл (legacy)

Один HTTP-запрос — один файл. Используется для обратной совместимости, если новый режим недоступен.

POST /parse_repo — рекомендуемый режим

Один запрос обрабатывает весь репозиторий или переданный список путей к файлам.

Преимущества:

  • Меньше HTTP-запросов (условно один на язык вместо запроса на каждый файл)
  • Возможность внутреннего кэша на объём репозитория (удобно при общих конфигах)
  • Дедупликация case_id в рамках одного языка на стороне плагина

Бэкенд автоматически выбирает /parse_repo, если endpoint указан в plugin.json. Если parse_repo нет, используется пофайловый режим.

Формат плагина

Ожидаемая структура каталога:

plugin-name/
├── plugin.json          # Конфигурация плагина
├── Dockerfile           # Docker-образ плагина
├── requirements.txt     # Зависимости Python
├── base_parser.py       # Базовые классы парсинга
├── src/
│   └── parser.py        # FastAPI и логика парсера
└── README.md            # Описание плагина (для команды)

Файл plugin.json

Обязательный файл с метаданными:

{
  "name": "plugin-name",
  "version": "1.1.0",
  "display_name": "Display Name",
  "description": "Description of what this parser does",
  "language": "language-identifier",
  "supported_extensions": [".ext"],
  "file_patterns": ["**/*_test.ext"],
  "api_version": "1.1",
  "endpoints": {
    "parse": "/parse",
    "parse_repo": "/parse_repo",
    "can_parse": "/can_parse",
    "health": "/health",
    "config": "/config"
  },
  "config": {
    "port": 8000,
    "timeout": 30,
    "repo_timeout": 300
  }
}

Поля в config:

ПолеПо умолчаниюОписание
timeout30Таймаут (сек) для /parse — один файл
repo_timeout300Таймаут (сек) для /parse_repo — весь репозиторий

Обязательные endpoints API

POST /parse

Парсит один файл.

Тело запроса:

{
  "file_path": "/path/to/test/file.py",
  "repo_path": "/path/to/repository"
}

repo_path опционален; нужен для корректного suite_name и порядка файлов.

Ответ:

{
  "success": true,
  "metadata": [
    {
      "tms": "case-guid",
      "file_path": "/path/to/file",
      "suite_id": "suite-guid",
      "suite_name": "Suite Name",
      "title": "Test title",
      "description": "Test description",
      "severity": "normal",
      "priority": "normal",
      "tags": [],
      "links": [],
      "steps": [],
      "iterations": [],
      "custom_fields": []
    }
  ],
  "error": null
}

POST /parse_repo

Парсит репозиторий за один вызов.

Тело запроса:

{
  "repo_path": "/path/to/repository",
  "file_paths": ["/path/to/file1.py", "/path/to/file2.py"]
}

file_paths опционален: если не передан, плагин сам обходит репозиторий по паттернам из plugin.json.

Ответ:

{
  "success": true,
  "metadata": [
    {
      "tms": "case-guid",
      "file_path": "/path/to/file",
      "suite_id": "suite-guid",
      "suite_name": "Suite Name",
      "title": "Test title",
      "steps": [],
      "custom_fields": []
    }
  ],
  "errors": [],
  "files_processed": 42,
  "files_failed": 0
}

Обработка ошибок в /parse_repo:

  • Ошибка в одном файле → строка в errors, остальные файлы обрабатываются
  • Дубликат case_id между файлами → в errors, второй экземпляр не попадает в metadata
  • Дубликат case_id внутри файла → файл считается ошибочным целиком
  • success: false, если files_failed > 0 или есть межфайловые дубликаты case_id

POST /can_parse

Запрос:

{
  "file_path": "/path/to/file.py"
}

Ответ:

{
  "can_parse": true
}

GET /health

Ответ:

{
  "status": "ok",
  "plugin_name": "plugin-name",
  "version": "1.1.0"
}

GET /config

Возвращает содержимое plugin.json. Бэкенд использует это для автоматической регистрации плагина при старте.

Проверки дубликатов

УровеньГдеЧто
Внутри файлаПлагин (_validate_and_process_metadata)Дубликаты case_id в одном файле
Между файлами, один языкПлагин (check_cross_file_duplicates в /parse_repo)Дубликаты case_id между файлами
Между языкамиЭкстрактор (_extract_via_parse_repo)Дубликаты при слиянии результатов плагинов
Suite / каталогиЭкстрактор (_validate_extracted_metadata)suite_id в разных директориях

Пошаговая разработка нового плагина

1. Структура каталогов

mkdir -p parser-plugins/my-parser/src
cd parser-plugins/my-parser

2. Файл base_parser.py

Скопируйте из готового плагина — parser-plugins/python-parser/base_parser.py. В нём определены базовые классы BaseParser, TestMetadata и метод check_cross_file_duplicates, который используется в /parse_repo.

3. plugin.json

Скопируйте шаблон с другого плагина и адаптируйте. В endpoints обязательно укажите "parse_repo": "/parse_repo".

4. Реализация в src/parser.py

Класс парсера наследуется от BaseParser и реализует:

  • can_parse(file_path: Path) -> bool
  • parse_file(file_path: Path) -> List[TestMetadata]

Подключите маршруты /parse и /parse_repo. Пример каркаса для /parse_repo:

from base_parser import BaseParser, TestMetadata, ParserError, ParserValidationError

class ParseRepoRequest(BaseModel):
    repo_path: str
    file_paths: Optional[List[str]] = None

class ParseRepoResponse(BaseModel):
    success: bool
    metadata: List[Dict[str, Any]]
    errors: List[str] = []
    files_processed: int = 0
    files_failed: int = 0

@app.post("/parse_repo", response_model=ParseRepoResponse)
async def parse_repo(request: ParseRepoRequest):
    repo_path = Path(request.repo_path)
    if not repo_path.exists():
        return ParseRepoResponse(success=False, metadata=[],
                                 errors=[f"repo_path не найден: {repo_path}"])

    if request.file_paths is not None:
        file_paths = [Path(fp) for fp in request.file_paths]
    else:
        # Обход репозитория по вашим паттернам
        file_paths = [...]

    all_raw, errors = [], []
    files_processed = files_failed = 0

    for file_path in file_paths:
        try:
            all_raw.extend(parser.parse_file(file_path))
            files_processed += 1
        except (ParserError, ParserValidationError) as e:
            errors.append(f"{file_path}: {e}")
            files_failed += 1

    clean, dup_errors = parser.check_cross_file_duplicates(all_raw)
    errors.extend(dup_errors)

    return ParseRepoResponse(
        success=files_failed == 0 and not dup_errors,
        metadata=[m.to_dict() for m in clean],
        errors=errors,
        files_processed=files_processed,
        files_failed=files_failed,
    )

5. Dockerfile

FROM python:3.11-slim

WORKDIR /app

COPY base_parser.py /app/
COPY plugin.json /app/
COPY src/ /app/src/
COPY requirements.txt /app/

RUN pip install --no-cache-dir -r requirements.txt

CMD ["uvicorn", "src.parser:app", "--host", "0.0.0.0", "--port", "8000"]

6. requirements.txt

Минимум:

fastapi==0.104.1
uvicorn==0.24.0
pydantic==2.5.0

7. Сервис в docker-compose.yml

my-parser-plugin:
  build:
    context: ./parser-plugins/my-parser
    dockerfile: Dockerfile
  container_name: my-parser-plugin
  restart: unless-stopped
  volumes:
    - git_repos:/app/git_repos:ro
  networks:
    - savetest-network
  environment:
    - PLUGIN_NAME=my-parser
    - LOG_LEVEL=INFO
  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
    interval: 10s
    timeout: 5s
    retries: 3
    start_period: 10s

Имена сети и тома (savetest-network, git_repos) должны совпадать с остальным стеком SaveTest.

8. Регистрация на бэкенде

Ручная правка кода бэкенда не нужна: при старте подтягивается GET /config у плагина. Добавьте URL вашего сервиса в PLUGIN_URLS (список через запятую) или в PLUGIN_1_URL, PLUGIN_2_URL, … у сервиса backend — формат и значения по умолчанию описаны в Параметры окружения.

После изменений пересоберите образ плагина и перезапустите стек:

docker compose up -d --build

Убедитесь, что плагин зарегистрировался: в интерфейсе SaveTest при создании проекта в режиме «Код автотестов» должен появиться новый парсер в списке доступных языков.

9. Документация плагина

В README.md плагина имеет смысл описать:

  • Поддерживаемые фреймворки и версии
  • Декораторы, аннотации, соглашения об именовании
  • Примеры тестовых файлов
  • Особенности парсинга и ограничения

Локальный запуск плагина вне Docker

Если процесс плагина слушает на хосте, а backend в контейнере, в PLUGIN_URLS укажите адрес, доступный из контейнера: например host.docker.internal (Docker Desktop) или IP хоста в вашей сети. Убедитесь, что firewall и bind-адрес сервиса плагина позволяют такое подключение.

Назад
Домен и SSL — Nginx (опционально)