Skip to content

Commit 1d26f6a

Browse files
committed
feat(push): extend 502/504 failure logging with upload summary and file list; include gpkg diff size and change count via safe helper; add real-diff test; no change to success path
1 parent 195c012 commit 1d26f6a

File tree

2 files changed

+159
-4
lines changed

2 files changed

+159
-4
lines changed

mergin/client_push.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -268,14 +268,31 @@ def push_project_finalize(job):
268268
resp = job.mc.post("/v1/project/push/finish/%s" % job.transaction_id)
269269
job.server_resp = json.load(resp)
270270
except ClientError as err:
271-
# Log additional metadata on server error 502 or 504
272-
if hasattr(err, "http_error") and err.http_error in (502, 504):
271+
# Log additional metadata on server error 502 or 504 (extended logging only)
272+
http_code = getattr(err, "http_error", None)
273+
if http_code in (502, 504):
273274
job.mp.log.error(
274-
f"Push failed with HTTP error {err.http_error}. "
275+
f"Push failed with HTTP error {http_code}. "
275276
f"Upload details: {len(job.upload_queue_items)} file chunks, total size {job.total_size} bytes."
276277
)
278+
job.mp.log.error("Files:")
279+
for f in job.changes.get("added", []) + job.changes.get("updated", []):
280+
path = f.get("path", "<unknown>")
281+
size = f.get("size", "?")
282+
if "diff" in f:
283+
diff_info = f.get("diff", {})
284+
diff_size = diff_info.get("size", "?")
285+
# best-effort: number of geodiff changes (if available)
286+
changes_cnt = _geodiff_changes_count(job.mp, diff_info.get("path"))
287+
if changes_cnt is None:
288+
job.mp.log.error(f" - {path}, size={size}, diff_size={diff_size}, changes=n/a")
289+
else:
290+
job.mp.log.error(f" - {path}, size={size}, diff_size={diff_size}, changes={changes_cnt}")
291+
else:
292+
job.mp.log.error(f" - {path}, size={size}")
293+
277294
# server returns various error messages with filename or something generic
278-
# it would be better if it returned list of failed files (and reasons) whenever possible
295+
# it would be better if it returned list of failed files (and reasons) whenever possible
279296
job.mp.log.error("--- push finish failed! " + str(err))
280297

281298
# if push finish fails, the transaction is not killed, so we
@@ -303,6 +320,36 @@ def push_project_finalize(job):
303320
job.mp.log.info("--- push finished - new project version " + job.server_resp["version"])
304321

305322

323+
def _geodiff_changes_count(mp, diff_rel_path):
324+
"""
325+
Best-effort: return number of changes in the .gpkg diff (int) or None.
326+
Never raises – diagnostics/logging must not fail.
327+
"""
328+
if not diff_rel_path:
329+
return None
330+
try:
331+
diff_abs = mp.fpath_meta(diff_rel_path)
332+
except Exception:
333+
return None
334+
335+
try:
336+
from pygeodiff import GeoDiff
337+
return GeoDiff().changes_count(diff_abs)
338+
except Exception:
339+
pass
340+
341+
try:
342+
import geodiff # optional fallback
343+
if hasattr(geodiff, "changes_count"):
344+
try:
345+
return geodiff.changes_count(diff_abs)
346+
except Exception:
347+
return None
348+
except Exception:
349+
return None
350+
351+
return None
352+
306353
def push_project_cancel(job):
307354
"""
308355
To be called (from main thread) to cancel a job that has uploads in progress.

mergin/test/test_push_logging.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import logging
2+
import tempfile
3+
from pathlib import Path
4+
from types import SimpleNamespace
5+
6+
import pytest
7+
8+
9+
@pytest.mark.parametrize("status_code", [502, 504])
10+
def test_push_finalize_logs_on_5xx_real_diff(caplog, status_code):
11+
# geodiff is required – if missing, skip the test gracefully
12+
pytest.importorskip("pygeodiff")
13+
from pygeodiff import GeoDiff
14+
15+
from mergin.client_push import push_project_finalize
16+
from mergin.common import ClientError
17+
18+
# --- exact paths according to your repo structure ---
19+
data_dir = Path(__file__).resolve().parent / "test_data"
20+
base = data_dir / "base.gpkg"
21+
modified = data_dir / "inserted_1_A.gpkg"
22+
23+
# if something is missing, print the directory contents and FAIL
24+
if not base.exists() or not modified.exists():
25+
print("[DEBUG] data_dir:", data_dir)
26+
print("[DEBUG] data_dir.exists():", data_dir.exists())
27+
if data_dir.exists():
28+
print("[DEBUG] contents:", [p.name for p in sorted(data_dir.iterdir())])
29+
pytest.fail(f"Expected files are not available: base={base.exists()} inserted_1_A={modified.exists()}")
30+
31+
# --- create a real .diff (changeset) ---
32+
with tempfile.TemporaryDirectory(prefix="geodiff-") as tmpdir:
33+
tmpdir = Path(tmpdir)
34+
diff_path = tmpdir / "base_to_inserted_1_A.diff"
35+
36+
gd = GeoDiff()
37+
gd.create_changeset(str(base), str(modified), str(diff_path))
38+
diff_size = diff_path.stat().st_size
39+
file_size = modified.stat().st_size
40+
41+
# --- logger captured by caplog ---
42+
logger = logging.getLogger("mergin_test")
43+
logger.setLevel(logging.DEBUG)
44+
logger.propagate = True
45+
46+
# --- fake mc.post: /finish -> 5xx, /cancel -> ok ---
47+
def fake_post(url, *args, **kwargs):
48+
if "/finish/" in url:
49+
err = ClientError("Gateway error")
50+
setattr(err, "http_error", status_code)
51+
raise err
52+
if "/cancel/" in url:
53+
return SimpleNamespace(msg="cancelled")
54+
return SimpleNamespace(msg="ok")
55+
56+
# --- fake future/executor (no exceptions) ---
57+
fake_future = SimpleNamespace(done=lambda: True, exception=lambda: None, running=lambda: False)
58+
fake_executor = SimpleNamespace(shutdown=lambda wait=True: None)
59+
60+
# mp.fpath_meta should resolve the relative diff path to an absolute path
61+
def fpath_meta(rel):
62+
return str(diff_path) if rel == diff_path.name else rel
63+
64+
# --- minimal job – only what finalize() uses ---
65+
job = SimpleNamespace(
66+
executor=fake_executor,
67+
futures=[fake_future],
68+
transferred_size=1234,
69+
total_size=1234,
70+
upload_queue_items=[1],
71+
transaction_id="tx-1",
72+
mp=SimpleNamespace(
73+
log=logger,
74+
update_metadata=lambda *a, **k: None,
75+
apply_push_changes=lambda *a, **k: None,
76+
fpath_meta=fpath_meta,
77+
),
78+
mc=SimpleNamespace(post=fake_post),
79+
changes={
80+
"added": [],
81+
"updated": [{
82+
"path": modified.name,
83+
"size": file_size,
84+
"diff": {"path": diff_path.name, "size": diff_size},
85+
"chunks": [1],
86+
}],
87+
"removed": [],
88+
},
89+
tmp_dir=SimpleNamespace(cleanup=lambda: None),
90+
)
91+
92+
# capture ERROR-level logs from "mergin_test"
93+
caplog.set_level(logging.ERROR, logger="mergin_test")
94+
95+
# --- call production logic (expecting 5xx) ---
96+
with pytest.raises(ClientError):
97+
push_project_finalize(job)
98+
99+
text = caplog.text
100+
101+
assert f"Push failed with HTTP error {status_code}" in text
102+
assert "Upload details:" in text
103+
assert "Files:" in text
104+
assert modified.name in text
105+
assert f"size={file_size}" in text
106+
assert f"diff_size={diff_size}" in text
107+
# helper may compute changes, or log changes=n/a – accept both
108+
assert ("changes=" in text) or ("changes=n/a" in text)

0 commit comments

Comments
 (0)