savetest-cli 0.3.0rc2__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.0rc2/savetest_cli.egg-info → savetest_cli-0.3.0rc3}/PKG-INFO +2 -2
- {savetest_cli-0.3.0rc2 → savetest_cli-0.3.0rc3}/README.md +1 -1
- {savetest_cli-0.3.0rc2 → savetest_cli-0.3.0rc3}/pyproject.toml +1 -1
- savetest_cli-0.3.0rc3/savetest_cli/__init__.py +1 -0
- {savetest_cli-0.3.0rc2 → savetest_cli-0.3.0rc3}/savetest_cli/cli.py +132 -134
- {savetest_cli-0.3.0rc2 → savetest_cli-0.3.0rc3/savetest_cli.egg-info}/PKG-INFO +2 -2
- savetest_cli-0.3.0rc2/savetest_cli/__init__.py +0 -1
- {savetest_cli-0.3.0rc2 → savetest_cli-0.3.0rc3}/MANIFEST.in +0 -0
- {savetest_cli-0.3.0rc2 → savetest_cli-0.3.0rc3}/savetest_cli/pwa-512x512.png +0 -0
- {savetest_cli-0.3.0rc2 → savetest_cli-0.3.0rc3}/savetest_cli.egg-info/SOURCES.txt +0 -0
- {savetest_cli-0.3.0rc2 → savetest_cli-0.3.0rc3}/savetest_cli.egg-info/dependency_links.txt +0 -0
- {savetest_cli-0.3.0rc2 → savetest_cli-0.3.0rc3}/savetest_cli.egg-info/entry_points.txt +0 -0
- {savetest_cli-0.3.0rc2 → savetest_cli-0.3.0rc3}/savetest_cli.egg-info/requires.txt +0 -0
- {savetest_cli-0.3.0rc2 → savetest_cli-0.3.0rc3}/savetest_cli.egg-info/top_level.txt +0 -0
- {savetest_cli-0.3.0rc2 → 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"
|
|
@@ -28,6 +28,7 @@ DEFAULT_SETTLE_SEC = 1.0
|
|
|
28
28
|
DEFAULT_ATTACHMENT_WAIT_SEC = 5.0
|
|
29
29
|
DEFAULT_WATCH_BATCH_WINDOW_SEC = 1.0
|
|
30
30
|
DEFAULT_WATCH_BATCH_MAX_WAIT_SEC = 5.0
|
|
31
|
+
DEFAULT_WATCH_HEARTBEAT_SEC = 15
|
|
31
32
|
TERMINAL_STATUSES = frozenset({"completed", "partial", "failed", "cancelled"})
|
|
32
33
|
_JUNK_NAMES = {".ds_store", "thumbs.db", ".project_id", ".gitkeep"}
|
|
33
34
|
|
|
@@ -826,26 +827,6 @@ def upload_files(
|
|
|
826
827
|
# watch — каталог в реальном времени
|
|
827
828
|
# ---------------------------------------------------------------------------
|
|
828
829
|
|
|
829
|
-
def _attachment_sources(data: Any) -> List[str]:
|
|
830
|
-
names: List[str] = []
|
|
831
|
-
|
|
832
|
-
def walk(node: Any) -> None:
|
|
833
|
-
if isinstance(node, dict):
|
|
834
|
-
source = node.get("source")
|
|
835
|
-
if isinstance(source, str) and source:
|
|
836
|
-
names.append(Path(source).name)
|
|
837
|
-
for value in node.values():
|
|
838
|
-
walk(value)
|
|
839
|
-
elif isinstance(node, list):
|
|
840
|
-
for value in node:
|
|
841
|
-
walk(value)
|
|
842
|
-
|
|
843
|
-
if isinstance(data, dict):
|
|
844
|
-
walk(data.get("attachments"))
|
|
845
|
-
walk(data.get("steps"))
|
|
846
|
-
return names
|
|
847
|
-
|
|
848
|
-
|
|
849
830
|
def _load_json_if_complete(path: Path) -> Optional[Any]:
|
|
850
831
|
try:
|
|
851
832
|
with open(path, "r", encoding="utf-8") as handle:
|
|
@@ -869,36 +850,6 @@ def _snapshot_raw_files(root: Path, include_attachments: bool) -> Dict[str, Tupl
|
|
|
869
850
|
return found
|
|
870
851
|
|
|
871
852
|
|
|
872
|
-
def _index_snapshot_names(
|
|
873
|
-
snapshot: Dict[str, Tuple[Path, int, int]],
|
|
874
|
-
) -> Dict[str, List[str]]:
|
|
875
|
-
"""Имя файла → пути. Сопоставление вложений не должно перебирать весь каталог."""
|
|
876
|
-
index: Dict[str, List[str]] = {}
|
|
877
|
-
for rel in snapshot:
|
|
878
|
-
index.setdefault(Path(rel).name, []).append(rel)
|
|
879
|
-
return index
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
def _resolve_named_file(
|
|
883
|
-
snapshot: Dict[str, Tuple[Path, int, int]],
|
|
884
|
-
result_rel: str,
|
|
885
|
-
source_name: str,
|
|
886
|
-
name_index: Optional[Dict[str, List[str]]] = None,
|
|
887
|
-
) -> Optional[str]:
|
|
888
|
-
name = Path(source_name).name
|
|
889
|
-
result_dir = str(Path(result_rel).parent)
|
|
890
|
-
if name_index is None:
|
|
891
|
-
matches = [rel for rel in snapshot if Path(rel).name == name]
|
|
892
|
-
else:
|
|
893
|
-
matches = name_index.get(name) or []
|
|
894
|
-
if not matches:
|
|
895
|
-
return None
|
|
896
|
-
for rel in matches:
|
|
897
|
-
if str(Path(rel).parent) == result_dir:
|
|
898
|
-
return rel
|
|
899
|
-
return matches[0]
|
|
900
|
-
|
|
901
|
-
|
|
902
853
|
def _pick_watch_batch(
|
|
903
854
|
snapshot: Dict[str, Tuple[Path, int, int]],
|
|
904
855
|
stable_since: Dict[str, Tuple[int, int, float]],
|
|
@@ -910,11 +861,8 @@ def _pick_watch_batch(
|
|
|
910
861
|
*,
|
|
911
862
|
flushing: bool,
|
|
912
863
|
) -> List[Tuple[str, Path]]:
|
|
913
|
-
"""Файлы, которые уже
|
|
914
|
-
|
|
915
|
-
Результат ждёт свои вложения не дольше attachment_wait_sec, чтобы не уехать
|
|
916
|
-
раньше скриншота. Пачка без *-result.json не отправляется: сервер её не примет.
|
|
917
|
-
"""
|
|
864
|
+
"""Файлы, которые уже дописаны. Результат и вложение уходят по отдельности."""
|
|
865
|
+
del include_attachments, attachment_wait_sec
|
|
918
866
|
ready: Dict[str, Path] = {}
|
|
919
867
|
for rel in list(stable_since):
|
|
920
868
|
if rel not in snapshot:
|
|
@@ -936,50 +884,7 @@ def _pick_watch_batch(
|
|
|
936
884
|
continue
|
|
937
885
|
ready[rel] = path
|
|
938
886
|
|
|
939
|
-
|
|
940
|
-
held = set()
|
|
941
|
-
if include_attachments:
|
|
942
|
-
for rel, path in list(ready.items()):
|
|
943
|
-
if not _is_result_file(path):
|
|
944
|
-
continue
|
|
945
|
-
document = _load_json_if_complete(path) or {}
|
|
946
|
-
missing = False
|
|
947
|
-
for source in _attachment_sources(document):
|
|
948
|
-
attachment_rel = _resolve_named_file(snapshot, rel, source, name_index)
|
|
949
|
-
if attachment_rel is None or attachment_rel not in ready:
|
|
950
|
-
missing = True
|
|
951
|
-
break
|
|
952
|
-
if not missing:
|
|
953
|
-
continue
|
|
954
|
-
ready_since = stable_since.get(rel, (0, 0, now))[2]
|
|
955
|
-
if not flushing and (now - ready_since) < settle_sec + attachment_wait_sec:
|
|
956
|
-
held.add(rel)
|
|
957
|
-
else:
|
|
958
|
-
click.echo(f"Результат {rel} отправляю без части вложений: они так и не появились.")
|
|
959
|
-
|
|
960
|
-
send_results = [
|
|
961
|
-
(rel, path) for rel, path in ready.items()
|
|
962
|
-
if _is_result_file(path) and rel not in held
|
|
963
|
-
]
|
|
964
|
-
if not send_results:
|
|
965
|
-
return []
|
|
966
|
-
|
|
967
|
-
needed = set()
|
|
968
|
-
for rel, path in send_results:
|
|
969
|
-
document = _load_json_if_complete(path) or {}
|
|
970
|
-
for source in _attachment_sources(document):
|
|
971
|
-
attachment_rel = _resolve_named_file(snapshot, rel, source, name_index)
|
|
972
|
-
if attachment_rel and attachment_rel in ready:
|
|
973
|
-
needed.add(attachment_rel)
|
|
974
|
-
|
|
975
|
-
selected = list(send_results)
|
|
976
|
-
for rel, path in ready.items():
|
|
977
|
-
if _is_result_file(path):
|
|
978
|
-
continue
|
|
979
|
-
if include_attachments and _is_attachment(path.name) and rel not in needed:
|
|
980
|
-
continue
|
|
981
|
-
selected.append((rel, path))
|
|
982
|
-
return selected
|
|
887
|
+
return [(rel, path) for rel, path in ready.items()]
|
|
983
888
|
|
|
984
889
|
|
|
985
890
|
def _watch_batch_due(
|
|
@@ -1018,28 +923,7 @@ def _chunk_watch_selection(
|
|
|
1018
923
|
max_count: int = DEFAULT_FILE_BATCH_COUNT,
|
|
1019
924
|
max_bytes: int = DEFAULT_FILE_BATCH_BYTES,
|
|
1020
925
|
) -> List[List[Tuple[str, Path]]]:
|
|
1021
|
-
"""Режет готовые файлы на
|
|
1022
|
-
results = [(rel, path) for rel, path in selected if _is_result_file(path)]
|
|
1023
|
-
extras = {rel: (rel, path) for rel, path in selected if not _is_result_file(path)}
|
|
1024
|
-
extras_by_name: Dict[str, List[str]] = {}
|
|
1025
|
-
for extra_rel in extras:
|
|
1026
|
-
extras_by_name.setdefault(Path(extra_rel).name, []).append(extra_rel)
|
|
1027
|
-
used = set()
|
|
1028
|
-
groups: List[List[Tuple[str, Path]]] = []
|
|
1029
|
-
for rel, path in results:
|
|
1030
|
-
group = [(rel, path)]
|
|
1031
|
-
document = _load_json_if_complete(path) or {}
|
|
1032
|
-
for source in _attachment_sources(document):
|
|
1033
|
-
name = Path(source).name
|
|
1034
|
-
for extra_rel in extras_by_name.get(name) or []:
|
|
1035
|
-
if extra_rel in used:
|
|
1036
|
-
continue
|
|
1037
|
-
group.append(extras[extra_rel])
|
|
1038
|
-
used.add(extra_rel)
|
|
1039
|
-
break
|
|
1040
|
-
groups.append(group)
|
|
1041
|
-
|
|
1042
|
-
loose = [extra for rel, extra in extras.items() if rel not in used]
|
|
926
|
+
"""Режет готовые файлы на пачки по числу и размеру, без разбора содержимого."""
|
|
1043
927
|
chunks: List[List[Tuple[str, Path]]] = []
|
|
1044
928
|
current: List[Tuple[str, Path]] = []
|
|
1045
929
|
current_bytes = 0
|
|
@@ -1051,19 +935,16 @@ def _chunk_watch_selection(
|
|
|
1051
935
|
current = []
|
|
1052
936
|
current_bytes = 0
|
|
1053
937
|
|
|
1054
|
-
for
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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):
|
|
1060
945
|
flush()
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
current_bytes += _selection_bytes(loose)
|
|
1064
|
-
loose = []
|
|
1065
|
-
current.extend(group)
|
|
1066
|
-
current_bytes += group_bytes
|
|
946
|
+
current.append(item)
|
|
947
|
+
current_bytes += size
|
|
1067
948
|
if len(current) >= max_count or current_bytes >= max_bytes:
|
|
1068
949
|
flush()
|
|
1069
950
|
flush()
|
|
@@ -1154,6 +1035,64 @@ def _fetch_task(host: str, project_id: str, task_id: str, token: str) -> Dict[st
|
|
|
1154
1035
|
return resp.json()
|
|
1155
1036
|
|
|
1156
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
|
+
|
|
1157
1096
|
@cli.command(
|
|
1158
1097
|
"watch",
|
|
1159
1098
|
context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
|
|
@@ -1166,7 +1105,7 @@ def _fetch_task(host: str, project_id: str, task_id: str, token: str) -> Dict[st
|
|
|
1166
1105
|
)
|
|
1167
1106
|
@click.option("--interval", default=DEFAULT_WATCH_INTERVAL_SEC, show_default=True, type=float, help="Как часто смотреть каталог (сек).")
|
|
1168
1107
|
@click.option("--settle", default=DEFAULT_SETTLE_SEC, show_default=True, type=float, help="Сколько секунд размер файла не должен меняться, прежде чем его забрать.")
|
|
1169
|
-
@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="Не используется: файл уходит, как только перестал меняться.")
|
|
1170
1109
|
@click.option("--batch-window", default=DEFAULT_WATCH_BATCH_WINDOW_SEC, show_default=True, type=float, help="Сколько секунд копить готовые файлы после последнего нового, прежде чем отправить пачку.")
|
|
1171
1110
|
@click.option("--batch-max-wait", default=DEFAULT_WATCH_BATCH_MAX_WAIT_SEC, show_default=True, type=float, help="Дольше этого готовые файлы не держатся, даже если новые всё ещё появляются. 0 — ждать только тишины.")
|
|
1172
1111
|
@click.option("--idle-timeout", default=0, show_default=True, type=int, help="Выйти, если столько секунд нет новых файлов и загрузок. 0 — не выходить по тишине.")
|
|
@@ -1270,6 +1209,21 @@ def watch(
|
|
|
1270
1209
|
click.echo(f"Также выйду после {idle_timeout} с тишины.")
|
|
1271
1210
|
|
|
1272
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)
|
|
1273
1227
|
try:
|
|
1274
1228
|
if command:
|
|
1275
1229
|
child["proc"] = subprocess.Popen(
|
|
@@ -1393,6 +1347,11 @@ def watch(
|
|
|
1393
1347
|
f"создано {status.get('results_created', 0)}, "
|
|
1394
1348
|
f"обработано {status.get('results_processed', 0)}"
|
|
1395
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
|
|
1396
1355
|
if state != "completed":
|
|
1397
1356
|
had_failure = True
|
|
1398
1357
|
for err in (status.get("errors") or [])[:3]:
|
|
@@ -1402,6 +1361,26 @@ def watch(
|
|
|
1402
1361
|
click.echo("Тест-ран не создан, новые пачки не отправляю.", err=True)
|
|
1403
1362
|
inflight = still_running
|
|
1404
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
|
+
|
|
1405
1384
|
if run_create_failed and not inflight:
|
|
1406
1385
|
click.echo("watch остановлен: первая загрузка не создала тест-ран.", err=True)
|
|
1407
1386
|
watch_code = 1
|
|
@@ -1426,6 +1405,25 @@ def watch(
|
|
|
1426
1405
|
time.sleep(interval)
|
|
1427
1406
|
finally:
|
|
1428
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)
|
|
1429
1427
|
|
|
1430
1428
|
test_status = 0
|
|
1431
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.0rc2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|