savetest-cli 0.3.0rc1__tar.gz → 0.3.0rc3__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.3.0rc1/savetest_cli.egg-info → savetest_cli-0.3.0rc3}/PKG-INFO +2 -2
- {savetest_cli-0.3.0rc1 → savetest_cli-0.3.0rc3}/README.md +1 -1
- {savetest_cli-0.3.0rc1 → savetest_cli-0.3.0rc3}/pyproject.toml +1 -1
- savetest_cli-0.3.0rc3/savetest_cli/__init__.py +1 -0
- {savetest_cli-0.3.0rc1 → savetest_cli-0.3.0rc3}/savetest_cli/cli.py +145 -120
- {savetest_cli-0.3.0rc1 → savetest_cli-0.3.0rc3/savetest_cli.egg-info}/PKG-INFO +2 -2
- savetest_cli-0.3.0rc1/savetest_cli/__init__.py +0 -1
- {savetest_cli-0.3.0rc1 → savetest_cli-0.3.0rc3}/MANIFEST.in +0 -0
- {savetest_cli-0.3.0rc1 → savetest_cli-0.3.0rc3}/savetest_cli/pwa-512x512.png +0 -0
- {savetest_cli-0.3.0rc1 → savetest_cli-0.3.0rc3}/savetest_cli.egg-info/SOURCES.txt +0 -0
- {savetest_cli-0.3.0rc1 → savetest_cli-0.3.0rc3}/savetest_cli.egg-info/dependency_links.txt +0 -0
- {savetest_cli-0.3.0rc1 → savetest_cli-0.3.0rc3}/savetest_cli.egg-info/entry_points.txt +0 -0
- {savetest_cli-0.3.0rc1 → savetest_cli-0.3.0rc3}/savetest_cli.egg-info/requires.txt +0 -0
- {savetest_cli-0.3.0rc1 → savetest_cli-0.3.0rc3}/savetest_cli.egg-info/top_level.txt +0 -0
- {savetest_cli-0.3.0rc1 → savetest_cli-0.3.0rc3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: savetest-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.0rc3
|
|
4
4
|
Summary: CLI for SaveTest: upload Allure results as a ZIP, as raw files, or by watching a directory
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -220,7 +220,7 @@ savetest watch ./allure-results \
|
|
|
220
220
|
- Если тесты и загрузка — разные джобы, отдайте каталог `allure-results` артефактом тоже с `when: always`.
|
|
221
221
|
- `--wait` оставляйте включённым: джоба должна завершиться только когда импорт дошёл до конца.
|
|
222
222
|
- Вложения включайте только если они реально нужны. По умолчанию CLI их не шлёт.
|
|
223
|
-
- `0.3.
|
|
223
|
+
- `0.3.0rc3` — предварительная версия для SaveTest 1.5.0: `pip install savetest-cli==0.3.0rc3`. Обычный `pip install savetest-cli` остаётся на последней стабильной версии.
|
|
224
224
|
|
|
225
225
|
### Результаты по мере прогона
|
|
226
226
|
|
|
@@ -210,7 +210,7 @@ savetest watch ./allure-results \
|
|
|
210
210
|
- Если тесты и загрузка — разные джобы, отдайте каталог `allure-results` артефактом тоже с `when: always`.
|
|
211
211
|
- `--wait` оставляйте включённым: джоба должна завершиться только когда импорт дошёл до конца.
|
|
212
212
|
- Вложения включайте только если они реально нужны. По умолчанию CLI их не шлёт.
|
|
213
|
-
- `0.3.
|
|
213
|
+
- `0.3.0rc3` — предварительная версия для SaveTest 1.5.0: `pip install savetest-cli==0.3.0rc3`. Обычный `pip install savetest-cli` остаётся на последней стабильной версии.
|
|
214
214
|
|
|
215
215
|
### Результаты по мере прогона
|
|
216
216
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "savetest-cli"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.0rc3"
|
|
8
8
|
description = "CLI for SaveTest: upload Allure results as a ZIP, as raw files, or by watching a directory"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0rc3"
|
|
@@ -22,11 +22,13 @@ DEFAULT_POLL_INTERVAL_SEC = 1.5
|
|
|
22
22
|
DEFAULT_WAIT_TIMEOUT_SEC = 3600
|
|
23
23
|
DEFAULT_FILE_BATCH_COUNT = 20
|
|
24
24
|
DEFAULT_FILE_BATCH_BYTES = 32 * 1024 * 1024
|
|
25
|
+
DEFAULT_WATCH_BATCH_COUNT = 200
|
|
25
26
|
DEFAULT_WATCH_INTERVAL_SEC = 0.5
|
|
26
27
|
DEFAULT_SETTLE_SEC = 1.0
|
|
27
28
|
DEFAULT_ATTACHMENT_WAIT_SEC = 5.0
|
|
28
29
|
DEFAULT_WATCH_BATCH_WINDOW_SEC = 1.0
|
|
29
30
|
DEFAULT_WATCH_BATCH_MAX_WAIT_SEC = 5.0
|
|
31
|
+
DEFAULT_WATCH_HEARTBEAT_SEC = 15
|
|
30
32
|
TERMINAL_STATUSES = frozenset({"completed", "partial", "failed", "cancelled"})
|
|
31
33
|
_JUNK_NAMES = {".ds_store", "thumbs.db", ".project_id", ".gitkeep"}
|
|
32
34
|
|
|
@@ -825,26 +827,6 @@ def upload_files(
|
|
|
825
827
|
# watch — каталог в реальном времени
|
|
826
828
|
# ---------------------------------------------------------------------------
|
|
827
829
|
|
|
828
|
-
def _attachment_sources(data: Any) -> List[str]:
|
|
829
|
-
names: List[str] = []
|
|
830
|
-
|
|
831
|
-
def walk(node: Any) -> None:
|
|
832
|
-
if isinstance(node, dict):
|
|
833
|
-
source = node.get("source")
|
|
834
|
-
if isinstance(source, str) and source:
|
|
835
|
-
names.append(Path(source).name)
|
|
836
|
-
for value in node.values():
|
|
837
|
-
walk(value)
|
|
838
|
-
elif isinstance(node, list):
|
|
839
|
-
for value in node:
|
|
840
|
-
walk(value)
|
|
841
|
-
|
|
842
|
-
if isinstance(data, dict):
|
|
843
|
-
walk(data.get("attachments"))
|
|
844
|
-
walk(data.get("steps"))
|
|
845
|
-
return names
|
|
846
|
-
|
|
847
|
-
|
|
848
830
|
def _load_json_if_complete(path: Path) -> Optional[Any]:
|
|
849
831
|
try:
|
|
850
832
|
with open(path, "r", encoding="utf-8") as handle:
|
|
@@ -868,22 +850,6 @@ def _snapshot_raw_files(root: Path, include_attachments: bool) -> Dict[str, Tupl
|
|
|
868
850
|
return found
|
|
869
851
|
|
|
870
852
|
|
|
871
|
-
def _resolve_named_file(
|
|
872
|
-
snapshot: Dict[str, Tuple[Path, int, int]],
|
|
873
|
-
result_rel: str,
|
|
874
|
-
source_name: str,
|
|
875
|
-
) -> Optional[str]:
|
|
876
|
-
name = Path(source_name).name
|
|
877
|
-
result_dir = str(Path(result_rel).parent)
|
|
878
|
-
matches = [rel for rel in snapshot if Path(rel).name == name]
|
|
879
|
-
if not matches:
|
|
880
|
-
return None
|
|
881
|
-
for rel in matches:
|
|
882
|
-
if str(Path(rel).parent) == result_dir:
|
|
883
|
-
return rel
|
|
884
|
-
return matches[0]
|
|
885
|
-
|
|
886
|
-
|
|
887
853
|
def _pick_watch_batch(
|
|
888
854
|
snapshot: Dict[str, Tuple[Path, int, int]],
|
|
889
855
|
stable_since: Dict[str, Tuple[int, int, float]],
|
|
@@ -895,11 +861,8 @@ def _pick_watch_batch(
|
|
|
895
861
|
*,
|
|
896
862
|
flushing: bool,
|
|
897
863
|
) -> List[Tuple[str, Path]]:
|
|
898
|
-
"""Файлы, которые уже
|
|
899
|
-
|
|
900
|
-
Результат ждёт свои вложения не дольше attachment_wait_sec, чтобы не уехать
|
|
901
|
-
раньше скриншота. Пачка без *-result.json не отправляется: сервер её не примет.
|
|
902
|
-
"""
|
|
864
|
+
"""Файлы, которые уже дописаны. Результат и вложение уходят по отдельности."""
|
|
865
|
+
del include_attachments, attachment_wait_sec
|
|
903
866
|
ready: Dict[str, Path] = {}
|
|
904
867
|
for rel in list(stable_since):
|
|
905
868
|
if rel not in snapshot:
|
|
@@ -921,49 +884,7 @@ def _pick_watch_batch(
|
|
|
921
884
|
continue
|
|
922
885
|
ready[rel] = path
|
|
923
886
|
|
|
924
|
-
|
|
925
|
-
if include_attachments:
|
|
926
|
-
for rel, path in list(ready.items()):
|
|
927
|
-
if not _is_result_file(path):
|
|
928
|
-
continue
|
|
929
|
-
document = _load_json_if_complete(path) or {}
|
|
930
|
-
missing = False
|
|
931
|
-
for source in _attachment_sources(document):
|
|
932
|
-
attachment_rel = _resolve_named_file(snapshot, rel, source)
|
|
933
|
-
if attachment_rel is None or attachment_rel not in ready:
|
|
934
|
-
missing = True
|
|
935
|
-
break
|
|
936
|
-
if not missing:
|
|
937
|
-
continue
|
|
938
|
-
ready_since = stable_since.get(rel, (0, 0, now))[2]
|
|
939
|
-
if not flushing and (now - ready_since) < settle_sec + attachment_wait_sec:
|
|
940
|
-
held.add(rel)
|
|
941
|
-
else:
|
|
942
|
-
click.echo(f"Результат {rel} отправляю без части вложений: они так и не появились.")
|
|
943
|
-
|
|
944
|
-
send_results = [
|
|
945
|
-
(rel, path) for rel, path in ready.items()
|
|
946
|
-
if _is_result_file(path) and rel not in held
|
|
947
|
-
]
|
|
948
|
-
if not send_results:
|
|
949
|
-
return []
|
|
950
|
-
|
|
951
|
-
needed = set()
|
|
952
|
-
for rel, path in send_results:
|
|
953
|
-
document = _load_json_if_complete(path) or {}
|
|
954
|
-
for source in _attachment_sources(document):
|
|
955
|
-
attachment_rel = _resolve_named_file(snapshot, rel, source)
|
|
956
|
-
if attachment_rel and attachment_rel in ready:
|
|
957
|
-
needed.add(attachment_rel)
|
|
958
|
-
|
|
959
|
-
selected = list(send_results)
|
|
960
|
-
for rel, path in ready.items():
|
|
961
|
-
if _is_result_file(path):
|
|
962
|
-
continue
|
|
963
|
-
if include_attachments and _is_attachment(path.name) and rel not in needed:
|
|
964
|
-
continue
|
|
965
|
-
selected.append((rel, path))
|
|
966
|
-
return selected
|
|
887
|
+
return [(rel, path) for rel, path in ready.items()]
|
|
967
888
|
|
|
968
889
|
|
|
969
890
|
def _watch_batch_due(
|
|
@@ -1002,25 +923,7 @@ def _chunk_watch_selection(
|
|
|
1002
923
|
max_count: int = DEFAULT_FILE_BATCH_COUNT,
|
|
1003
924
|
max_bytes: int = DEFAULT_FILE_BATCH_BYTES,
|
|
1004
925
|
) -> List[List[Tuple[str, Path]]]:
|
|
1005
|
-
"""Режет готовые файлы на
|
|
1006
|
-
results = [(rel, path) for rel, path in selected if _is_result_file(path)]
|
|
1007
|
-
extras = {rel: (rel, path) for rel, path in selected if not _is_result_file(path)}
|
|
1008
|
-
used = set()
|
|
1009
|
-
groups: List[List[Tuple[str, Path]]] = []
|
|
1010
|
-
for rel, path in results:
|
|
1011
|
-
group = [(rel, path)]
|
|
1012
|
-
document = _load_json_if_complete(path) or {}
|
|
1013
|
-
for source in _attachment_sources(document):
|
|
1014
|
-
name = Path(source).name
|
|
1015
|
-
for extra_rel, extra in extras.items():
|
|
1016
|
-
if extra_rel in used or Path(extra_rel).name != name:
|
|
1017
|
-
continue
|
|
1018
|
-
group.append(extra)
|
|
1019
|
-
used.add(extra_rel)
|
|
1020
|
-
break
|
|
1021
|
-
groups.append(group)
|
|
1022
|
-
|
|
1023
|
-
loose = [extra for rel, extra in extras.items() if rel not in used]
|
|
926
|
+
"""Режет готовые файлы на пачки по числу и размеру, без разбора содержимого."""
|
|
1024
927
|
chunks: List[List[Tuple[str, Path]]] = []
|
|
1025
928
|
current: List[Tuple[str, Path]] = []
|
|
1026
929
|
current_bytes = 0
|
|
@@ -1032,19 +935,16 @@ def _chunk_watch_selection(
|
|
|
1032
935
|
current = []
|
|
1033
936
|
current_bytes = 0
|
|
1034
937
|
|
|
1035
|
-
for
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
938
|
+
for item in selected:
|
|
939
|
+
_rel, path = item
|
|
940
|
+
try:
|
|
941
|
+
size = path.stat().st_size
|
|
942
|
+
except OSError:
|
|
943
|
+
size = 0
|
|
944
|
+
if current and (len(current) >= max_count or current_bytes + size > max_bytes):
|
|
1041
945
|
flush()
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
current_bytes += _selection_bytes(loose)
|
|
1045
|
-
loose = []
|
|
1046
|
-
current.extend(group)
|
|
1047
|
-
current_bytes += group_bytes
|
|
946
|
+
current.append(item)
|
|
947
|
+
current_bytes += size
|
|
1048
948
|
if len(current) >= max_count or current_bytes >= max_bytes:
|
|
1049
949
|
flush()
|
|
1050
950
|
flush()
|
|
@@ -1135,6 +1035,64 @@ def _fetch_task(host: str, project_id: str, task_id: str, token: str) -> Dict[st
|
|
|
1135
1035
|
return resp.json()
|
|
1136
1036
|
|
|
1137
1037
|
|
|
1038
|
+
def _watch_session_url(host: str, project_id: str, session_id: Optional[str] = None, action: str = "") -> str:
|
|
1039
|
+
url = f"{host}/api/v1/projects/{project_id}/allure-results/watch/sessions"
|
|
1040
|
+
if session_id:
|
|
1041
|
+
url = f"{url}/{session_id}"
|
|
1042
|
+
if action:
|
|
1043
|
+
url = f"{url}/{action}"
|
|
1044
|
+
return url
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def _post_watch_session(
|
|
1048
|
+
host: str,
|
|
1049
|
+
project_id: str,
|
|
1050
|
+
token: str,
|
|
1051
|
+
payload: Dict[str, Any],
|
|
1052
|
+
session_id: Optional[str] = None,
|
|
1053
|
+
action: str = "",
|
|
1054
|
+
) -> Optional[Dict[str, Any]]:
|
|
1055
|
+
"""Сигнал платформе. Ошибка здесь не останавливает тесты."""
|
|
1056
|
+
try:
|
|
1057
|
+
resp = requests.post(
|
|
1058
|
+
_watch_session_url(host, project_id, session_id, action),
|
|
1059
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
1060
|
+
json=payload,
|
|
1061
|
+
timeout=20,
|
|
1062
|
+
)
|
|
1063
|
+
except requests.RequestException:
|
|
1064
|
+
return None
|
|
1065
|
+
if resp.status_code >= 400:
|
|
1066
|
+
return None
|
|
1067
|
+
try:
|
|
1068
|
+
body = resp.json()
|
|
1069
|
+
except ValueError:
|
|
1070
|
+
return None
|
|
1071
|
+
return body if isinstance(body, dict) else None
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def _watch_session_payload(
|
|
1075
|
+
*,
|
|
1076
|
+
test_run_id: Optional[str],
|
|
1077
|
+
test_run_name: Optional[str],
|
|
1078
|
+
stats: Dict[str, int],
|
|
1079
|
+
status: Optional[str] = None,
|
|
1080
|
+
) -> Dict[str, Any]:
|
|
1081
|
+
payload: Dict[str, Any] = {
|
|
1082
|
+
"results_processed": stats["processed"],
|
|
1083
|
+
"results_created": stats["created"],
|
|
1084
|
+
"test_cases_matched": stats["matched"],
|
|
1085
|
+
"skipped": stats["skipped"],
|
|
1086
|
+
}
|
|
1087
|
+
if test_run_name:
|
|
1088
|
+
payload["test_run_name"] = test_run_name
|
|
1089
|
+
if test_run_id and str(test_run_id).isdigit():
|
|
1090
|
+
payload["test_run_id"] = int(test_run_id)
|
|
1091
|
+
if status:
|
|
1092
|
+
payload["status"] = status
|
|
1093
|
+
return payload
|
|
1094
|
+
|
|
1095
|
+
|
|
1138
1096
|
@cli.command(
|
|
1139
1097
|
"watch",
|
|
1140
1098
|
context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
|
|
@@ -1147,7 +1105,7 @@ def _fetch_task(host: str, project_id: str, task_id: str, token: str) -> Dict[st
|
|
|
1147
1105
|
)
|
|
1148
1106
|
@click.option("--interval", default=DEFAULT_WATCH_INTERVAL_SEC, show_default=True, type=float, help="Как часто смотреть каталог (сек).")
|
|
1149
1107
|
@click.option("--settle", default=DEFAULT_SETTLE_SEC, show_default=True, type=float, help="Сколько секунд размер файла не должен меняться, прежде чем его забрать.")
|
|
1150
|
-
@click.option("--attachment-wait", default=DEFAULT_ATTACHMENT_WAIT_SEC, show_default=True, type=float, help="
|
|
1108
|
+
@click.option("--attachment-wait", default=DEFAULT_ATTACHMENT_WAIT_SEC, show_default=True, type=float, help="Не используется: файл уходит, как только перестал меняться.")
|
|
1151
1109
|
@click.option("--batch-window", default=DEFAULT_WATCH_BATCH_WINDOW_SEC, show_default=True, type=float, help="Сколько секунд копить готовые файлы после последнего нового, прежде чем отправить пачку.")
|
|
1152
1110
|
@click.option("--batch-max-wait", default=DEFAULT_WATCH_BATCH_MAX_WAIT_SEC, show_default=True, type=float, help="Дольше этого готовые файлы не держатся, даже если новые всё ещё появляются. 0 — ждать только тишины.")
|
|
1153
1111
|
@click.option("--idle-timeout", default=0, show_default=True, type=int, help="Выйти, если столько секунд нет новых файлов и загрузок. 0 — не выходить по тишине.")
|
|
@@ -1193,8 +1151,8 @@ def watch(
|
|
|
1193
1151
|
|
|
1194
1152
|
Готовые файлы не уходят по одному. Если они появляются подряд, watch держит их
|
|
1195
1153
|
--batch-window секунд после последнего нового и отправляет вместе. Дольше
|
|
1196
|
-
--batch-max-wait секунд пачка не копится. Предел одной отправки —
|
|
1197
|
-
или 32
|
|
1154
|
+
--batch-max-wait секунд пачка не копится. Предел одной отправки — 200 файлов
|
|
1155
|
+
или 32 МБ.
|
|
1198
1156
|
"""
|
|
1199
1157
|
command = [arg for arg in ctx.args if arg != "--"]
|
|
1200
1158
|
host = host.rstrip("/")
|
|
@@ -1209,6 +1167,7 @@ def watch(
|
|
|
1209
1167
|
run_create_failed = False
|
|
1210
1168
|
sent_batches = 0
|
|
1211
1169
|
drain_until = 0.0
|
|
1170
|
+
announced_flush = False
|
|
1212
1171
|
hold_opened = 0.0
|
|
1213
1172
|
hold_quiet = 0.0
|
|
1214
1173
|
hold_key: Optional[Tuple[str, ...]] = None
|
|
@@ -1250,6 +1209,21 @@ def watch(
|
|
|
1250
1209
|
click.echo(f"Также выйду после {idle_timeout} с тишины.")
|
|
1251
1210
|
|
|
1252
1211
|
watch_code = 0
|
|
1212
|
+
session_id = None
|
|
1213
|
+
session_stats = {"processed": 0, "created": 0, "matched": 0, "skipped": 0}
|
|
1214
|
+
last_heartbeat = 0.0
|
|
1215
|
+
session_dirty = False
|
|
1216
|
+
open_payload: Dict[str, Any] = {}
|
|
1217
|
+
if test_run_name:
|
|
1218
|
+
open_payload["test_run_name"] = test_run_name
|
|
1219
|
+
if test_run_id and str(test_run_id).isdigit():
|
|
1220
|
+
open_payload["test_run_id"] = int(test_run_id)
|
|
1221
|
+
opened = _post_watch_session(host, project_id, token, open_payload)
|
|
1222
|
+
if opened and opened.get("task_id"):
|
|
1223
|
+
session_id = str(opened["task_id"])
|
|
1224
|
+
click.echo(f"Платформа показывает загрузку: {session_id}")
|
|
1225
|
+
else:
|
|
1226
|
+
click.echo("Карточка загрузки на платформе не открылась. Файлы всё равно будут отправлены.", err=True)
|
|
1253
1227
|
try:
|
|
1254
1228
|
if command:
|
|
1255
1229
|
child["proc"] = subprocess.Popen(
|
|
@@ -1267,6 +1241,10 @@ def watch(
|
|
|
1267
1241
|
flushing = stop["requested"] or timed_out
|
|
1268
1242
|
|
|
1269
1243
|
snapshot = _snapshot_raw_files(root, include_attachments) if root.is_dir() else {}
|
|
1244
|
+
if flushing and not announced_flush:
|
|
1245
|
+
pending = sum(1 for rel in snapshot if rel not in uploaded)
|
|
1246
|
+
click.echo(f"После остановки осталось файлов: {pending}.")
|
|
1247
|
+
announced_flush = True
|
|
1270
1248
|
if any(rel not in uploaded for rel in snapshot):
|
|
1271
1249
|
last_activity = now
|
|
1272
1250
|
|
|
@@ -1300,7 +1278,7 @@ def watch(
|
|
|
1300
1278
|
hold_key = ready_key
|
|
1301
1279
|
|
|
1302
1280
|
batch_full = bool(batch) and (
|
|
1303
|
-
len(batch) >=
|
|
1281
|
+
len(batch) >= DEFAULT_WATCH_BATCH_COUNT
|
|
1304
1282
|
or _selection_bytes(batch) >= DEFAULT_FILE_BATCH_BYTES
|
|
1305
1283
|
)
|
|
1306
1284
|
send_now = bool(batch) and _watch_batch_due(
|
|
@@ -1313,7 +1291,10 @@ def watch(
|
|
|
1313
1291
|
flushing=flushing,
|
|
1314
1292
|
)
|
|
1315
1293
|
if send_now and not run_create_failed and (known_run_id or not inflight):
|
|
1316
|
-
chunks = _chunk_watch_selection(
|
|
1294
|
+
chunks = _chunk_watch_selection(
|
|
1295
|
+
batch,
|
|
1296
|
+
max_count=DEFAULT_WATCH_BATCH_COUNT,
|
|
1297
|
+
)
|
|
1317
1298
|
if not known_run_id:
|
|
1318
1299
|
chunks = chunks[:1]
|
|
1319
1300
|
for chunk in chunks:
|
|
@@ -1366,6 +1347,11 @@ def watch(
|
|
|
1366
1347
|
f"создано {status.get('results_created', 0)}, "
|
|
1367
1348
|
f"обработано {status.get('results_processed', 0)}"
|
|
1368
1349
|
)
|
|
1350
|
+
session_stats["processed"] += int(status.get("results_processed") or 0)
|
|
1351
|
+
session_stats["created"] += int(status.get("results_created") or 0)
|
|
1352
|
+
session_stats["matched"] += int(status.get("test_cases_matched") or 0)
|
|
1353
|
+
session_stats["skipped"] += int(status.get("skipped") or 0)
|
|
1354
|
+
session_dirty = True
|
|
1369
1355
|
if state != "completed":
|
|
1370
1356
|
had_failure = True
|
|
1371
1357
|
for err in (status.get("errors") or [])[:3]:
|
|
@@ -1375,6 +1361,26 @@ def watch(
|
|
|
1375
1361
|
click.echo("Тест-ран не создан, новые пачки не отправляю.", err=True)
|
|
1376
1362
|
inflight = still_running
|
|
1377
1363
|
|
|
1364
|
+
if session_id and (
|
|
1365
|
+
now - last_heartbeat >= DEFAULT_WATCH_HEARTBEAT_SEC
|
|
1366
|
+
or (session_dirty and now - last_heartbeat >= 2)
|
|
1367
|
+
):
|
|
1368
|
+
sent = _post_watch_session(
|
|
1369
|
+
host,
|
|
1370
|
+
project_id,
|
|
1371
|
+
token,
|
|
1372
|
+
_watch_session_payload(
|
|
1373
|
+
test_run_id=known_run_id,
|
|
1374
|
+
test_run_name=test_run_name,
|
|
1375
|
+
stats=session_stats,
|
|
1376
|
+
),
|
|
1377
|
+
session_id=session_id,
|
|
1378
|
+
action="heartbeat",
|
|
1379
|
+
)
|
|
1380
|
+
last_heartbeat = now
|
|
1381
|
+
if sent is not None:
|
|
1382
|
+
session_dirty = False
|
|
1383
|
+
|
|
1378
1384
|
if run_create_failed and not inflight:
|
|
1379
1385
|
click.echo("watch остановлен: первая загрузка не создала тест-ран.", err=True)
|
|
1380
1386
|
watch_code = 1
|
|
@@ -1399,6 +1405,25 @@ def watch(
|
|
|
1399
1405
|
time.sleep(interval)
|
|
1400
1406
|
finally:
|
|
1401
1407
|
_stop_child()
|
|
1408
|
+
if session_id:
|
|
1409
|
+
finish_status = "failed" if had_failure or run_create_failed else "completed"
|
|
1410
|
+
closed = _post_watch_session(
|
|
1411
|
+
host,
|
|
1412
|
+
project_id,
|
|
1413
|
+
token,
|
|
1414
|
+
_watch_session_payload(
|
|
1415
|
+
test_run_id=known_run_id,
|
|
1416
|
+
test_run_name=test_run_name,
|
|
1417
|
+
stats=session_stats,
|
|
1418
|
+
status=finish_status,
|
|
1419
|
+
),
|
|
1420
|
+
session_id=session_id,
|
|
1421
|
+
action="complete",
|
|
1422
|
+
)
|
|
1423
|
+
if closed is not None:
|
|
1424
|
+
click.echo("Платформа закрыла карточку загрузки.")
|
|
1425
|
+
else:
|
|
1426
|
+
click.echo("Не удалось закрыть карточку загрузки на платформе.", err=True)
|
|
1402
1427
|
|
|
1403
1428
|
test_status = 0
|
|
1404
1429
|
if child["proc"] is not None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: savetest-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.0rc3
|
|
4
4
|
Summary: CLI for SaveTest: upload Allure results as a ZIP, as raw files, or by watching a directory
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -220,7 +220,7 @@ savetest watch ./allure-results \
|
|
|
220
220
|
- Если тесты и загрузка — разные джобы, отдайте каталог `allure-results` артефактом тоже с `when: always`.
|
|
221
221
|
- `--wait` оставляйте включённым: джоба должна завершиться только когда импорт дошёл до конца.
|
|
222
222
|
- Вложения включайте только если они реально нужны. По умолчанию CLI их не шлёт.
|
|
223
|
-
- `0.3.
|
|
223
|
+
- `0.3.0rc3` — предварительная версия для SaveTest 1.5.0: `pip install savetest-cli==0.3.0rc3`. Обычный `pip install savetest-cli` остаётся на последней стабильной версии.
|
|
224
224
|
|
|
225
225
|
### Результаты по мере прогона
|
|
226
226
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.0rc1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|