savetest-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- savetest_cli-0.1.0/PKG-INFO +128 -0
- savetest_cli-0.1.0/README.md +118 -0
- savetest_cli-0.1.0/pyproject.toml +22 -0
- savetest_cli-0.1.0/savetest_cli/__init__.py +1 -0
- savetest_cli-0.1.0/savetest_cli/cli.py +401 -0
- savetest_cli-0.1.0/savetest_cli.egg-info/PKG-INFO +128 -0
- savetest_cli-0.1.0/savetest_cli.egg-info/SOURCES.txt +10 -0
- savetest_cli-0.1.0/savetest_cli.egg-info/dependency_links.txt +1 -0
- savetest_cli-0.1.0/savetest_cli.egg-info/entry_points.txt +2 -0
- savetest_cli-0.1.0/savetest_cli.egg-info/requires.txt +2 -0
- savetest_cli-0.1.0/savetest_cli.egg-info/top_level.txt +1 -0
- savetest_cli-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: savetest-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for SaveTest: zip Allure results and upload them to SaveTest
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: click>=8.0
|
|
9
|
+
Requires-Dist: requests>=2.28
|
|
10
|
+
|
|
11
|
+
# savetest-cli
|
|
12
|
+
|
|
13
|
+
CLI-утилита для упаковки Allure-результатов в ZIP и загрузки их в [SaveTest](https://save-test.ru).
|
|
14
|
+
|
|
15
|
+
## Установка
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install savetest-cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Команды
|
|
22
|
+
|
|
23
|
+
### `savetest zip_report <path>`
|
|
24
|
+
|
|
25
|
+
Упаковывает директорию с Allure-результатами в ZIP-архив.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
savetest zip_report ./AllureReport
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Параметр | По умолчанию | Описание |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `path` | — | Путь к директории с Allure-результатами |
|
|
34
|
+
| `--output`, `-o` | `allure-results.zip` | Путь к выходному ZIP-файлу |
|
|
35
|
+
| `--include-attachments` | `false` | Включить вложения (картинки, видео, `*-attachment.json`) |
|
|
36
|
+
| `--no-include-attachments` | — | Исключить вложения (быстрее, меньше объём) |
|
|
37
|
+
|
|
38
|
+
**Примеры:**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Упаковать без вложений (быстрее)
|
|
42
|
+
savetest zip_report ./AllureReport
|
|
43
|
+
|
|
44
|
+
# Указать выходной файл
|
|
45
|
+
savetest zip_report ./AllureReport --output my-report.zip
|
|
46
|
+
|
|
47
|
+
# Включить вложения
|
|
48
|
+
savetest zip_report ./AllureReport --include-attachments
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### `savetest upload`
|
|
54
|
+
|
|
55
|
+
Загружает ZIP-архив в SaveTest по частям (чанками).
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
savetest upload --token sat_xxx --project-id 40
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Параметр | Env-переменная | По умолчанию | Описание |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `--zip-file`, `-z` | — | `allure-results.zip` | Путь к ZIP-файлу |
|
|
64
|
+
| `--host` | `SAVETEST_HOST` | `https://save-test.ptnl.moscow` | Хост SaveTest |
|
|
65
|
+
| `--token`, `-t` | `SAVETEST_TOKEN` | — | API-токен (**обязательно**) |
|
|
66
|
+
| `--project-id`, `-p` | `SAVETEST_PROJECT_ID` | — | ID проекта (**обязательно**) |
|
|
67
|
+
| `--test-run-id` | `SAVETEST_TEST_RUN_ID` | — | ID существующего тест-рана (иначе создаётся новый) |
|
|
68
|
+
| `--test-run-name` | `SAVETEST_TEST_RUN_NAME` | — | Имя нового тест-рана |
|
|
69
|
+
| `--branch`, `-b` | `SAVETEST_BRANCH` | — | Ветка Git |
|
|
70
|
+
| `--include-attachments` | `SAVETEST_INCLUDE_ATTACHMENTS` | `false` | Сообщить серверу, что ZIP содержит вложения |
|
|
71
|
+
| `--chunk-size` | — | `50` | Размер чанка при загрузке (МБ) |
|
|
72
|
+
| `--wait` / `--no-wait` | — | `--wait` | Дождаться завершения импорта и создания тест-рана |
|
|
73
|
+
| `--poll-interval` | — | `1.5` | Интервал опроса статуса (сек) |
|
|
74
|
+
| `--timeout` | — | `3600` | Таймаут ожидания обработки (сек) |
|
|
75
|
+
|
|
76
|
+
**Примеры:**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Минимальный вызов (токен через переменную окружения)
|
|
80
|
+
export SAVETEST_TOKEN=sat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
81
|
+
savetest upload --project-id 40
|
|
82
|
+
|
|
83
|
+
# Создать новый тест-ран с именем
|
|
84
|
+
savetest upload \
|
|
85
|
+
--token sat_xxx \
|
|
86
|
+
--project-id 40 \
|
|
87
|
+
--test-run-name "Smoke DEV #42" \
|
|
88
|
+
--branch dev
|
|
89
|
+
|
|
90
|
+
# Загрузить в существующий тест-ран
|
|
91
|
+
savetest upload \
|
|
92
|
+
--token sat_xxx \
|
|
93
|
+
--project-id 40 \
|
|
94
|
+
--test-run-id 7
|
|
95
|
+
|
|
96
|
+
# Загрузить с вложениями из другого файла
|
|
97
|
+
savetest upload \
|
|
98
|
+
--token sat_xxx \
|
|
99
|
+
--project-id 40 \
|
|
100
|
+
--zip-file my-report.zip \
|
|
101
|
+
--include-attachments
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Типичный сценарий
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# 1. Упаковать результаты (без вложений)
|
|
110
|
+
savetest zip_report ./AllureReport
|
|
111
|
+
|
|
112
|
+
# 2. Загрузить в SaveTest (CLI дождётся импорта и выведет ссылку на тест-ран)
|
|
113
|
+
export SAVETEST_TOKEN=sat_xxx
|
|
114
|
+
savetest upload --project-id 40 --branch dev --test-run-name "CI build #123"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
По умолчанию `upload` ждёт полной обработки. Чтобы вернуться сразу после приёма чанков: `--no-wait`.
|
|
118
|
+
|
|
119
|
+
## Соответствие параметрам Jenkins Pipeline
|
|
120
|
+
|
|
121
|
+
| Jenkins-параметр | CLI-опция |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `savetest_host` | `--host` |
|
|
124
|
+
| `savetest_project_id` | `--project-id` |
|
|
125
|
+
| `savetest_test_run_id` | `--test-run-id` |
|
|
126
|
+
| `savetest_include_attachments` | `--include-attachments` / `--no-include-attachments` |
|
|
127
|
+
| `branch` | `--branch` |
|
|
128
|
+
| токен (захардкожен) | `--token` / `SAVETEST_TOKEN` |
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# savetest-cli
|
|
2
|
+
|
|
3
|
+
CLI-утилита для упаковки Allure-результатов в ZIP и загрузки их в [SaveTest](https://save-test.ru).
|
|
4
|
+
|
|
5
|
+
## Установка
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install savetest-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Команды
|
|
12
|
+
|
|
13
|
+
### `savetest zip_report <path>`
|
|
14
|
+
|
|
15
|
+
Упаковывает директорию с Allure-результатами в ZIP-архив.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
savetest zip_report ./AllureReport
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| Параметр | По умолчанию | Описание |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `path` | — | Путь к директории с Allure-результатами |
|
|
24
|
+
| `--output`, `-o` | `allure-results.zip` | Путь к выходному ZIP-файлу |
|
|
25
|
+
| `--include-attachments` | `false` | Включить вложения (картинки, видео, `*-attachment.json`) |
|
|
26
|
+
| `--no-include-attachments` | — | Исключить вложения (быстрее, меньше объём) |
|
|
27
|
+
|
|
28
|
+
**Примеры:**
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Упаковать без вложений (быстрее)
|
|
32
|
+
savetest zip_report ./AllureReport
|
|
33
|
+
|
|
34
|
+
# Указать выходной файл
|
|
35
|
+
savetest zip_report ./AllureReport --output my-report.zip
|
|
36
|
+
|
|
37
|
+
# Включить вложения
|
|
38
|
+
savetest zip_report ./AllureReport --include-attachments
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
### `savetest upload`
|
|
44
|
+
|
|
45
|
+
Загружает ZIP-архив в SaveTest по частям (чанками).
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
savetest upload --token sat_xxx --project-id 40
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| Параметр | Env-переменная | По умолчанию | Описание |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| `--zip-file`, `-z` | — | `allure-results.zip` | Путь к ZIP-файлу |
|
|
54
|
+
| `--host` | `SAVETEST_HOST` | `https://save-test.ptnl.moscow` | Хост SaveTest |
|
|
55
|
+
| `--token`, `-t` | `SAVETEST_TOKEN` | — | API-токен (**обязательно**) |
|
|
56
|
+
| `--project-id`, `-p` | `SAVETEST_PROJECT_ID` | — | ID проекта (**обязательно**) |
|
|
57
|
+
| `--test-run-id` | `SAVETEST_TEST_RUN_ID` | — | ID существующего тест-рана (иначе создаётся новый) |
|
|
58
|
+
| `--test-run-name` | `SAVETEST_TEST_RUN_NAME` | — | Имя нового тест-рана |
|
|
59
|
+
| `--branch`, `-b` | `SAVETEST_BRANCH` | — | Ветка Git |
|
|
60
|
+
| `--include-attachments` | `SAVETEST_INCLUDE_ATTACHMENTS` | `false` | Сообщить серверу, что ZIP содержит вложения |
|
|
61
|
+
| `--chunk-size` | — | `50` | Размер чанка при загрузке (МБ) |
|
|
62
|
+
| `--wait` / `--no-wait` | — | `--wait` | Дождаться завершения импорта и создания тест-рана |
|
|
63
|
+
| `--poll-interval` | — | `1.5` | Интервал опроса статуса (сек) |
|
|
64
|
+
| `--timeout` | — | `3600` | Таймаут ожидания обработки (сек) |
|
|
65
|
+
|
|
66
|
+
**Примеры:**
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Минимальный вызов (токен через переменную окружения)
|
|
70
|
+
export SAVETEST_TOKEN=sat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
71
|
+
savetest upload --project-id 40
|
|
72
|
+
|
|
73
|
+
# Создать новый тест-ран с именем
|
|
74
|
+
savetest upload \
|
|
75
|
+
--token sat_xxx \
|
|
76
|
+
--project-id 40 \
|
|
77
|
+
--test-run-name "Smoke DEV #42" \
|
|
78
|
+
--branch dev
|
|
79
|
+
|
|
80
|
+
# Загрузить в существующий тест-ран
|
|
81
|
+
savetest upload \
|
|
82
|
+
--token sat_xxx \
|
|
83
|
+
--project-id 40 \
|
|
84
|
+
--test-run-id 7
|
|
85
|
+
|
|
86
|
+
# Загрузить с вложениями из другого файла
|
|
87
|
+
savetest upload \
|
|
88
|
+
--token sat_xxx \
|
|
89
|
+
--project-id 40 \
|
|
90
|
+
--zip-file my-report.zip \
|
|
91
|
+
--include-attachments
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Типичный сценарий
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# 1. Упаковать результаты (без вложений)
|
|
100
|
+
savetest zip_report ./AllureReport
|
|
101
|
+
|
|
102
|
+
# 2. Загрузить в SaveTest (CLI дождётся импорта и выведет ссылку на тест-ран)
|
|
103
|
+
export SAVETEST_TOKEN=sat_xxx
|
|
104
|
+
savetest upload --project-id 40 --branch dev --test-run-name "CI build #123"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
По умолчанию `upload` ждёт полной обработки. Чтобы вернуться сразу после приёма чанков: `--no-wait`.
|
|
108
|
+
|
|
109
|
+
## Соответствие параметрам Jenkins Pipeline
|
|
110
|
+
|
|
111
|
+
| Jenkins-параметр | CLI-опция |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `savetest_host` | `--host` |
|
|
114
|
+
| `savetest_project_id` | `--project-id` |
|
|
115
|
+
| `savetest_test_run_id` | `--test-run-id` |
|
|
116
|
+
| `savetest_include_attachments` | `--include-attachments` / `--no-include-attachments` |
|
|
117
|
+
| `branch` | `--branch` |
|
|
118
|
+
| токен (захардкожен) | `--token` / `SAVETEST_TOKEN` |
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "savetest-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CLI for SaveTest: zip Allure results and upload them to SaveTest"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
dependencies = [
|
|
13
|
+
"click>=8.0",
|
|
14
|
+
"requests>=2.28",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
savetest = "savetest_cli.cli:cli"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["."]
|
|
22
|
+
include = ["savetest_cli*"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import math
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import zipfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
ATTACHMENT_EXTENSIONS = {
|
|
13
|
+
".png", ".jpg", ".jpeg", ".gif", ".svg", ".mp4", ".webm",
|
|
14
|
+
}
|
|
15
|
+
ATTACHMENT_SUFFIXES = {"-attachment.json"}
|
|
16
|
+
|
|
17
|
+
DEFAULT_HOST = "https://save-test.ptnl.moscow"
|
|
18
|
+
DEFAULT_CHUNK_SIZE_MB = 50
|
|
19
|
+
DEFAULT_POLL_INTERVAL_SEC = 1.5
|
|
20
|
+
DEFAULT_WAIT_TIMEOUT_SEC = 3600
|
|
21
|
+
TERMINAL_STATUSES = frozenset({"completed", "partial", "failed", "cancelled"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Helpers
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
def _is_attachment(name: str) -> bool:
|
|
29
|
+
p = Path(name)
|
|
30
|
+
if p.suffix.lower() in ATTACHMENT_EXTENSIONS:
|
|
31
|
+
return True
|
|
32
|
+
for s in ATTACHMENT_SUFFIXES:
|
|
33
|
+
if p.name.endswith(s):
|
|
34
|
+
return True
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _sizeof_fmt(num: int) -> str:
|
|
39
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
40
|
+
if num < 1024:
|
|
41
|
+
return f"{num:.1f} {unit}"
|
|
42
|
+
num /= 1024
|
|
43
|
+
return f"{num:.1f} TB"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _poll_import_task(
|
|
47
|
+
host: str,
|
|
48
|
+
project_id: str,
|
|
49
|
+
task_id: str,
|
|
50
|
+
headers: Dict[str, str],
|
|
51
|
+
poll_interval: float,
|
|
52
|
+
timeout_sec: int,
|
|
53
|
+
) -> dict[str, Any]:
|
|
54
|
+
"""Ожидает завершения фонового импорта (как UI SaveTest)."""
|
|
55
|
+
url = f"{host}/api/v1/projects/{project_id}/allure-results/tasks/{task_id}"
|
|
56
|
+
deadline = time.monotonic() + timeout_sec
|
|
57
|
+
poll_errors = 0
|
|
58
|
+
max_poll_errors = 10
|
|
59
|
+
last_progress = -1
|
|
60
|
+
|
|
61
|
+
click.echo(f"\nОжидание обработки (task_id: {task_id})...")
|
|
62
|
+
|
|
63
|
+
while time.monotonic() < deadline:
|
|
64
|
+
try:
|
|
65
|
+
resp = requests.get(url, headers=headers, timeout=60)
|
|
66
|
+
_check_response(resp, "task status")
|
|
67
|
+
status = resp.json()
|
|
68
|
+
poll_errors = 0
|
|
69
|
+
except SystemExit:
|
|
70
|
+
raise
|
|
71
|
+
except requests.RequestException as exc:
|
|
72
|
+
poll_errors += 1
|
|
73
|
+
if poll_errors >= max_poll_errors:
|
|
74
|
+
click.echo(
|
|
75
|
+
f"\nНе удалось получить статус задачи после {max_poll_errors} попыток: {exc}",
|
|
76
|
+
err=True,
|
|
77
|
+
)
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
time.sleep(poll_interval)
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
state = status.get("status", "")
|
|
83
|
+
progress = status.get("progress") or 0
|
|
84
|
+
|
|
85
|
+
if progress != last_progress:
|
|
86
|
+
click.echo(f" [{state}] прогресс: {progress}%")
|
|
87
|
+
last_progress = progress
|
|
88
|
+
|
|
89
|
+
if state in TERMINAL_STATUSES:
|
|
90
|
+
return status
|
|
91
|
+
|
|
92
|
+
time.sleep(poll_interval)
|
|
93
|
+
|
|
94
|
+
click.echo(f"\nТаймаут ожидания ({timeout_sec} с). Задача ещё выполняется: {task_id}", err=True)
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _print_import_result(host: str, project_id: str, status: dict[str, Any]) -> None:
|
|
99
|
+
state = status.get("status", "")
|
|
100
|
+
test_run_id = status.get("test_run_id")
|
|
101
|
+
test_run_name = status.get("test_run_name")
|
|
102
|
+
|
|
103
|
+
click.echo("\n--- Итог импорта ---")
|
|
104
|
+
click.echo(f"Статус: {state}")
|
|
105
|
+
if test_run_id is not None:
|
|
106
|
+
click.echo(f"Тест-ран ID: {test_run_id}")
|
|
107
|
+
click.echo(f"Тест-ран: {test_run_name or '—'}")
|
|
108
|
+
click.echo(f"Ссылка: {host}/projects/{project_id}/test-runs/{test_run_id}")
|
|
109
|
+
click.echo(f"Обработано: {status.get('results_processed', 0)}")
|
|
110
|
+
click.echo(f"Создано: {status.get('results_created', 0)}")
|
|
111
|
+
click.echo(f"Сопоставлено: {status.get('test_cases_matched', 0)}")
|
|
112
|
+
click.echo(f"Пропущено: {status.get('skipped', 0)}")
|
|
113
|
+
|
|
114
|
+
errors = status.get("errors") or []
|
|
115
|
+
if errors:
|
|
116
|
+
click.echo("Ошибки:")
|
|
117
|
+
for err in errors:
|
|
118
|
+
click.echo(f" - {err}")
|
|
119
|
+
|
|
120
|
+
if state == "completed":
|
|
121
|
+
click.echo("\nИмпорт завершён успешно.")
|
|
122
|
+
elif state == "partial":
|
|
123
|
+
click.echo("\nИмпорт завершён с ошибками (часть результатов создана).", err=True)
|
|
124
|
+
sys.exit(1)
|
|
125
|
+
elif state == "cancelled":
|
|
126
|
+
click.echo("\nИмпорт отменён.", err=True)
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
else:
|
|
129
|
+
click.echo("\nИмпорт не удался.", err=True)
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# CLI root
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
@click.group()
|
|
138
|
+
@click.version_option(package_name="savetest-cli")
|
|
139
|
+
def cli() -> None:
|
|
140
|
+
"""SaveTest CLI — упаковка и загрузка Allure-результатов."""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# zip_report
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
@cli.command("zip_report")
|
|
148
|
+
@click.argument("path", type=click.Path(exists=True, file_okay=False, resolve_path=True))
|
|
149
|
+
@click.option(
|
|
150
|
+
"--output", "-o",
|
|
151
|
+
default="allure-results.zip",
|
|
152
|
+
show_default=True,
|
|
153
|
+
help="Путь к выходному ZIP-файлу.",
|
|
154
|
+
)
|
|
155
|
+
@click.option(
|
|
156
|
+
"--include-attachments/--no-include-attachments",
|
|
157
|
+
default=False,
|
|
158
|
+
show_default=True,
|
|
159
|
+
help="Включать вложения (картинки, видео, *-attachment.json).",
|
|
160
|
+
)
|
|
161
|
+
def zip_report(path: str, output: str, include_attachments: bool) -> None:
|
|
162
|
+
"""Упаковать директорию PATH с Allure-результатами в ZIP.
|
|
163
|
+
|
|
164
|
+
\b
|
|
165
|
+
Примеры:
|
|
166
|
+
savetest zip_report ./AllureReport
|
|
167
|
+
savetest zip_report ./AllureReport --output report.zip
|
|
168
|
+
savetest zip_report ./AllureReport --include-attachments
|
|
169
|
+
"""
|
|
170
|
+
src = Path(path)
|
|
171
|
+
out = Path(output)
|
|
172
|
+
|
|
173
|
+
if out.exists():
|
|
174
|
+
out.unlink()
|
|
175
|
+
|
|
176
|
+
skipped = 0
|
|
177
|
+
added = 0
|
|
178
|
+
|
|
179
|
+
click.echo(f"Упаковка: {src} → {out}")
|
|
180
|
+
|
|
181
|
+
with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED, allowZip64=True) as zf:
|
|
182
|
+
for file_path in sorted(src.rglob("*")):
|
|
183
|
+
if not file_path.is_file():
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
rel = file_path.relative_to(src.parent)
|
|
187
|
+
|
|
188
|
+
if not include_attachments and _is_attachment(file_path.name):
|
|
189
|
+
skipped += 1
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
zf.write(file_path, rel)
|
|
193
|
+
added += 1
|
|
194
|
+
|
|
195
|
+
size = _sizeof_fmt(out.stat().st_size)
|
|
196
|
+
click.echo(f"Готово: добавлено {added} файлов, пропущено {skipped} вложений. Размер: {size}")
|
|
197
|
+
click.echo(f"Файл: {out.resolve()}")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
# upload
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
@cli.command("upload")
|
|
205
|
+
@click.option(
|
|
206
|
+
"--zip-file", "-z",
|
|
207
|
+
default="allure-results.zip",
|
|
208
|
+
show_default=True,
|
|
209
|
+
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
|
210
|
+
help="Путь к ZIP-файлу с Allure-результатами.",
|
|
211
|
+
)
|
|
212
|
+
@click.option(
|
|
213
|
+
"--host",
|
|
214
|
+
default=DEFAULT_HOST,
|
|
215
|
+
show_default=True,
|
|
216
|
+
envvar="SAVETEST_HOST",
|
|
217
|
+
help="Хост SaveTest (или переменная окружения SAVETEST_HOST).",
|
|
218
|
+
)
|
|
219
|
+
@click.option(
|
|
220
|
+
"--token", "-t",
|
|
221
|
+
required=True,
|
|
222
|
+
envvar="SAVETEST_TOKEN",
|
|
223
|
+
help="API-токен SaveTest (или переменная окружения SAVETEST_TOKEN).",
|
|
224
|
+
)
|
|
225
|
+
@click.option(
|
|
226
|
+
"--project-id", "-p",
|
|
227
|
+
required=True,
|
|
228
|
+
envvar="SAVETEST_PROJECT_ID",
|
|
229
|
+
help="ID проекта в SaveTest (или переменная окружения SAVETEST_PROJECT_ID).",
|
|
230
|
+
)
|
|
231
|
+
@click.option(
|
|
232
|
+
"--test-run-id",
|
|
233
|
+
default="",
|
|
234
|
+
envvar="SAVETEST_TEST_RUN_ID",
|
|
235
|
+
help="ID существующего тест-рана (если не задан — создаётся новый).",
|
|
236
|
+
)
|
|
237
|
+
@click.option(
|
|
238
|
+
"--test-run-name",
|
|
239
|
+
default="",
|
|
240
|
+
envvar="SAVETEST_TEST_RUN_NAME",
|
|
241
|
+
help="Имя нового тест-рана (используется, если --test-run-id не задан).",
|
|
242
|
+
)
|
|
243
|
+
@click.option(
|
|
244
|
+
"--branch", "-b",
|
|
245
|
+
default="",
|
|
246
|
+
envvar="SAVETEST_BRANCH",
|
|
247
|
+
help="Ветка Git для тест-рана.",
|
|
248
|
+
)
|
|
249
|
+
@click.option(
|
|
250
|
+
"--include-attachments/--no-include-attachments",
|
|
251
|
+
default=False,
|
|
252
|
+
show_default=True,
|
|
253
|
+
envvar="SAVETEST_INCLUDE_ATTACHMENTS",
|
|
254
|
+
help="Сообщить серверу, что ZIP содержит вложения.",
|
|
255
|
+
)
|
|
256
|
+
@click.option(
|
|
257
|
+
"--chunk-size",
|
|
258
|
+
default=DEFAULT_CHUNK_SIZE_MB,
|
|
259
|
+
show_default=True,
|
|
260
|
+
type=int,
|
|
261
|
+
help="Размер чанка при загрузке (МБ).",
|
|
262
|
+
)
|
|
263
|
+
@click.option(
|
|
264
|
+
"--wait/--no-wait",
|
|
265
|
+
default=True,
|
|
266
|
+
show_default=True,
|
|
267
|
+
help="Дождаться полной обработки импорта на сервере.",
|
|
268
|
+
)
|
|
269
|
+
@click.option(
|
|
270
|
+
"--poll-interval",
|
|
271
|
+
default=DEFAULT_POLL_INTERVAL_SEC,
|
|
272
|
+
show_default=True,
|
|
273
|
+
type=float,
|
|
274
|
+
help="Интервал опроса статуса задачи (сек).",
|
|
275
|
+
)
|
|
276
|
+
@click.option(
|
|
277
|
+
"--timeout",
|
|
278
|
+
default=DEFAULT_WAIT_TIMEOUT_SEC,
|
|
279
|
+
show_default=True,
|
|
280
|
+
type=int,
|
|
281
|
+
help="Максимальное время ожидания обработки (сек).",
|
|
282
|
+
)
|
|
283
|
+
def upload(
|
|
284
|
+
zip_file: str,
|
|
285
|
+
host: str,
|
|
286
|
+
token: str,
|
|
287
|
+
project_id: str,
|
|
288
|
+
test_run_id: str,
|
|
289
|
+
test_run_name: str,
|
|
290
|
+
branch: str,
|
|
291
|
+
include_attachments: bool,
|
|
292
|
+
chunk_size: int,
|
|
293
|
+
wait: bool,
|
|
294
|
+
poll_interval: float,
|
|
295
|
+
timeout: int,
|
|
296
|
+
) -> None:
|
|
297
|
+
"""Загрузить ZIP с Allure-результатами в SaveTest и дождаться импорта.
|
|
298
|
+
|
|
299
|
+
\b
|
|
300
|
+
После загрузки чанков CLI опрашивает статус задачи, пока не создастся
|
|
301
|
+
тест-ран и не завершится импорт (по умолчанию --wait).
|
|
302
|
+
|
|
303
|
+
\b
|
|
304
|
+
Токен можно передать через переменную окружения SAVETEST_TOKEN.
|
|
305
|
+
|
|
306
|
+
\b
|
|
307
|
+
Примеры:
|
|
308
|
+
savetest upload --token sat_xxx --project-id 40
|
|
309
|
+
savetest upload --token sat_xxx --project-id 40 --test-run-id 7
|
|
310
|
+
savetest upload --token sat_xxx --project-id 40 --test-run-name "Smoke DEV" --branch dev
|
|
311
|
+
SAVETEST_TOKEN=sat_xxx savetest upload --project-id 40
|
|
312
|
+
"""
|
|
313
|
+
host = host.rstrip("/")
|
|
314
|
+
chunk_bytes = chunk_size * 1024 * 1024
|
|
315
|
+
zip_path = Path(zip_file)
|
|
316
|
+
zip_size = zip_path.stat().st_size
|
|
317
|
+
total_chunks = math.ceil(zip_size / chunk_bytes)
|
|
318
|
+
|
|
319
|
+
click.echo(f"Файл: {zip_path} ({_sizeof_fmt(zip_size)})")
|
|
320
|
+
click.echo(f"Хост: {host}")
|
|
321
|
+
click.echo(f"Проект: {project_id}")
|
|
322
|
+
click.echo(f"Чанков: {total_chunks} × {chunk_size} МБ")
|
|
323
|
+
|
|
324
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
325
|
+
|
|
326
|
+
# --- INIT ---
|
|
327
|
+
init_data: dict = {
|
|
328
|
+
"filename": "allure-results.zip",
|
|
329
|
+
"total_chunks": str(total_chunks),
|
|
330
|
+
"chunk_size": str(chunk_bytes),
|
|
331
|
+
"git_branch": branch,
|
|
332
|
+
"include_attachments": "true" if include_attachments else "false",
|
|
333
|
+
}
|
|
334
|
+
if test_run_id:
|
|
335
|
+
init_data["test_run_id"] = test_run_id
|
|
336
|
+
elif test_run_name:
|
|
337
|
+
init_data["test_run_name"] = test_run_name
|
|
338
|
+
|
|
339
|
+
click.echo("\nИнициализация загрузки...")
|
|
340
|
+
resp = requests.post(
|
|
341
|
+
f"{host}/api/v1/projects/{project_id}/allure-results/upload/init",
|
|
342
|
+
headers=headers,
|
|
343
|
+
data=init_data,
|
|
344
|
+
timeout=60,
|
|
345
|
+
)
|
|
346
|
+
_check_response(resp, "init")
|
|
347
|
+
|
|
348
|
+
upload_id = resp.json()["upload_id"]
|
|
349
|
+
click.echo(f"Upload ID: {upload_id}")
|
|
350
|
+
|
|
351
|
+
# --- CHUNKS ---
|
|
352
|
+
click.echo("\nЗагрузка чанков...")
|
|
353
|
+
with open(zip_path, "rb") as fh:
|
|
354
|
+
for i in range(total_chunks):
|
|
355
|
+
chunk_data = fh.read(chunk_bytes)
|
|
356
|
+
resp = requests.put(
|
|
357
|
+
f"{host}/api/v1/projects/{project_id}/allure-results/upload/{upload_id}/chunk",
|
|
358
|
+
headers=headers,
|
|
359
|
+
data={"chunk_index": str(i)},
|
|
360
|
+
files={"file": (f"part_{i:06d}", chunk_data, "application/octet-stream")},
|
|
361
|
+
timeout=120,
|
|
362
|
+
)
|
|
363
|
+
_check_response(resp, f"chunk {i}")
|
|
364
|
+
click.echo(f" [{i + 1}/{total_chunks}] ✓ {_sizeof_fmt(len(chunk_data))}")
|
|
365
|
+
|
|
366
|
+
# --- COMPLETE ---
|
|
367
|
+
click.echo("\nЗавершение загрузки...")
|
|
368
|
+
resp = requests.post(
|
|
369
|
+
f"{host}/api/v1/projects/{project_id}/allure-results/upload/{upload_id}/complete",
|
|
370
|
+
headers=headers,
|
|
371
|
+
timeout=60,
|
|
372
|
+
)
|
|
373
|
+
_check_response(resp, "complete")
|
|
374
|
+
|
|
375
|
+
result = resp.json()
|
|
376
|
+
task_id = result.get("task_id")
|
|
377
|
+
if not task_id:
|
|
378
|
+
click.echo("Чанки приняты, но task_id не получен:", err=True)
|
|
379
|
+
click.echo(json.dumps(result, ensure_ascii=False, indent=2))
|
|
380
|
+
sys.exit(1)
|
|
381
|
+
|
|
382
|
+
click.echo(f"Чанки загружены. Задача импорта: {task_id}")
|
|
383
|
+
|
|
384
|
+
if not wait:
|
|
385
|
+
click.echo(json.dumps(result, ensure_ascii=False, indent=2))
|
|
386
|
+
click.echo("\nИмпорт выполняется в фоне. Используйте --wait или проверьте задачу в UI.")
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
final = _poll_import_task(
|
|
390
|
+
host, project_id, task_id, headers, poll_interval, timeout
|
|
391
|
+
)
|
|
392
|
+
_print_import_result(host, project_id, final)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _check_response(resp: "requests.Response", step: str) -> None:
|
|
396
|
+
if not resp.ok:
|
|
397
|
+
click.echo(
|
|
398
|
+
f"\nОшибка на шаге '{step}': HTTP {resp.status_code}\n{resp.text}",
|
|
399
|
+
err=True,
|
|
400
|
+
)
|
|
401
|
+
sys.exit(1)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: savetest-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for SaveTest: zip Allure results and upload them to SaveTest
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: click>=8.0
|
|
9
|
+
Requires-Dist: requests>=2.28
|
|
10
|
+
|
|
11
|
+
# savetest-cli
|
|
12
|
+
|
|
13
|
+
CLI-утилита для упаковки Allure-результатов в ZIP и загрузки их в [SaveTest](https://save-test.ru).
|
|
14
|
+
|
|
15
|
+
## Установка
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install savetest-cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Команды
|
|
22
|
+
|
|
23
|
+
### `savetest zip_report <path>`
|
|
24
|
+
|
|
25
|
+
Упаковывает директорию с Allure-результатами в ZIP-архив.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
savetest zip_report ./AllureReport
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Параметр | По умолчанию | Описание |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `path` | — | Путь к директории с Allure-результатами |
|
|
34
|
+
| `--output`, `-o` | `allure-results.zip` | Путь к выходному ZIP-файлу |
|
|
35
|
+
| `--include-attachments` | `false` | Включить вложения (картинки, видео, `*-attachment.json`) |
|
|
36
|
+
| `--no-include-attachments` | — | Исключить вложения (быстрее, меньше объём) |
|
|
37
|
+
|
|
38
|
+
**Примеры:**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Упаковать без вложений (быстрее)
|
|
42
|
+
savetest zip_report ./AllureReport
|
|
43
|
+
|
|
44
|
+
# Указать выходной файл
|
|
45
|
+
savetest zip_report ./AllureReport --output my-report.zip
|
|
46
|
+
|
|
47
|
+
# Включить вложения
|
|
48
|
+
savetest zip_report ./AllureReport --include-attachments
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### `savetest upload`
|
|
54
|
+
|
|
55
|
+
Загружает ZIP-архив в SaveTest по частям (чанками).
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
savetest upload --token sat_xxx --project-id 40
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Параметр | Env-переменная | По умолчанию | Описание |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `--zip-file`, `-z` | — | `allure-results.zip` | Путь к ZIP-файлу |
|
|
64
|
+
| `--host` | `SAVETEST_HOST` | `https://save-test.ptnl.moscow` | Хост SaveTest |
|
|
65
|
+
| `--token`, `-t` | `SAVETEST_TOKEN` | — | API-токен (**обязательно**) |
|
|
66
|
+
| `--project-id`, `-p` | `SAVETEST_PROJECT_ID` | — | ID проекта (**обязательно**) |
|
|
67
|
+
| `--test-run-id` | `SAVETEST_TEST_RUN_ID` | — | ID существующего тест-рана (иначе создаётся новый) |
|
|
68
|
+
| `--test-run-name` | `SAVETEST_TEST_RUN_NAME` | — | Имя нового тест-рана |
|
|
69
|
+
| `--branch`, `-b` | `SAVETEST_BRANCH` | — | Ветка Git |
|
|
70
|
+
| `--include-attachments` | `SAVETEST_INCLUDE_ATTACHMENTS` | `false` | Сообщить серверу, что ZIP содержит вложения |
|
|
71
|
+
| `--chunk-size` | — | `50` | Размер чанка при загрузке (МБ) |
|
|
72
|
+
| `--wait` / `--no-wait` | — | `--wait` | Дождаться завершения импорта и создания тест-рана |
|
|
73
|
+
| `--poll-interval` | — | `1.5` | Интервал опроса статуса (сек) |
|
|
74
|
+
| `--timeout` | — | `3600` | Таймаут ожидания обработки (сек) |
|
|
75
|
+
|
|
76
|
+
**Примеры:**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Минимальный вызов (токен через переменную окружения)
|
|
80
|
+
export SAVETEST_TOKEN=sat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
81
|
+
savetest upload --project-id 40
|
|
82
|
+
|
|
83
|
+
# Создать новый тест-ран с именем
|
|
84
|
+
savetest upload \
|
|
85
|
+
--token sat_xxx \
|
|
86
|
+
--project-id 40 \
|
|
87
|
+
--test-run-name "Smoke DEV #42" \
|
|
88
|
+
--branch dev
|
|
89
|
+
|
|
90
|
+
# Загрузить в существующий тест-ран
|
|
91
|
+
savetest upload \
|
|
92
|
+
--token sat_xxx \
|
|
93
|
+
--project-id 40 \
|
|
94
|
+
--test-run-id 7
|
|
95
|
+
|
|
96
|
+
# Загрузить с вложениями из другого файла
|
|
97
|
+
savetest upload \
|
|
98
|
+
--token sat_xxx \
|
|
99
|
+
--project-id 40 \
|
|
100
|
+
--zip-file my-report.zip \
|
|
101
|
+
--include-attachments
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Типичный сценарий
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# 1. Упаковать результаты (без вложений)
|
|
110
|
+
savetest zip_report ./AllureReport
|
|
111
|
+
|
|
112
|
+
# 2. Загрузить в SaveTest (CLI дождётся импорта и выведет ссылку на тест-ран)
|
|
113
|
+
export SAVETEST_TOKEN=sat_xxx
|
|
114
|
+
savetest upload --project-id 40 --branch dev --test-run-name "CI build #123"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
По умолчанию `upload` ждёт полной обработки. Чтобы вернуться сразу после приёма чанков: `--no-wait`.
|
|
118
|
+
|
|
119
|
+
## Соответствие параметрам Jenkins Pipeline
|
|
120
|
+
|
|
121
|
+
| Jenkins-параметр | CLI-опция |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `savetest_host` | `--host` |
|
|
124
|
+
| `savetest_project_id` | `--project-id` |
|
|
125
|
+
| `savetest_test_run_id` | `--test-run-id` |
|
|
126
|
+
| `savetest_include_attachments` | `--include-attachments` / `--no-include-attachments` |
|
|
127
|
+
| `branch` | `--branch` |
|
|
128
|
+
| токен (захардкожен) | `--token` / `SAVETEST_TOKEN` |
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
savetest_cli/__init__.py
|
|
4
|
+
savetest_cli/cli.py
|
|
5
|
+
savetest_cli.egg-info/PKG-INFO
|
|
6
|
+
savetest_cli.egg-info/SOURCES.txt
|
|
7
|
+
savetest_cli.egg-info/dependency_links.txt
|
|
8
|
+
savetest_cli.egg-info/entry_points.txt
|
|
9
|
+
savetest_cli.egg-info/requires.txt
|
|
10
|
+
savetest_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
savetest_cli
|