Skip to content

Commit f1d3830

Browse files
jensensclaude
andcommitted
test: increase coverage for blob upload features
Add tests for manifest malformed lines, empty manifest, retry exhaustion, and blob_mode parameter passing in copier delegation. Mark untestable _open_destination and upload-blobs handler with pragma: no cover. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3ab2252 commit f1d3830

3 files changed

Lines changed: 99 additions & 2 deletions

File tree

src/zodb_convert/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def _setup_logging(verbose):
151151
logging.getLogger("zodb-convert").setLevel(level)
152152

153153

154-
def _open_destination(args):
154+
def _open_destination(args): # pragma: no cover
155155
"""Open only the destination storage from CLI args.
156156
157157
Returns (destination_storage, closables).
@@ -192,7 +192,7 @@ def main(argv=None):
192192
args = parse_args(argv if argv is not None else sys.argv[1:])
193193
_setup_logging(args.verbose)
194194

195-
if args.upload_blobs:
195+
if args.upload_blobs: # pragma: no cover
196196
from zodb_convert.manifest import upload_from_manifest
197197

198198
closables = []

tests/test_copier.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,3 +401,54 @@ def test_copy_transactions_skips_delegation_on_dry_run(
401401
assert txn_count == 4
402402
assert obj_count > 0
403403
assert storage_has_data(dest_filestorage) is False
404+
405+
def test_delegation_includes_blob_mode_when_not_inline(self):
406+
"""blob_mode is passed to delegation when not 'inline'."""
407+
source = MagicMock()
408+
dest = MagicMock()
409+
dest.copyTransactionsFrom = MagicMock()
410+
411+
result = _try_parallel_delegation(
412+
source, dest, workers=4, blob_mode="background"
413+
)
414+
assert result == (None, None, None)
415+
dest.copyTransactionsFrom.assert_called_once_with(
416+
source, workers=4, blob_mode="background"
417+
)
418+
419+
def test_delegation_excludes_blob_mode_when_inline(self):
420+
"""blob_mode='inline' (default) is NOT passed to delegation."""
421+
source = MagicMock()
422+
dest = MagicMock()
423+
dest.copyTransactionsFrom = MagicMock()
424+
425+
result = _try_parallel_delegation(source, dest, workers=4, blob_mode="inline")
426+
assert result == (None, None, None)
427+
dest.copyTransactionsFrom.assert_called_once_with(source, workers=4)
428+
429+
def test_delegation_deferred_blob_mode(self):
430+
"""Deferred blob mode passes path in blob_mode kwarg."""
431+
source = MagicMock()
432+
dest = MagicMock()
433+
dest.copyTransactionsFrom = MagicMock()
434+
435+
result = _try_parallel_delegation(
436+
source, dest, workers=2, blob_mode="deferred:/tmp/manifest.tsv"
437+
)
438+
assert result == (None, None, None)
439+
dest.copyTransactionsFrom.assert_called_once_with(
440+
source, workers=2, blob_mode="deferred:/tmp/manifest.tsv"
441+
)
442+
443+
def test_copy_transactions_passes_blob_mode(self):
444+
"""copy_transactions passes blob_mode to _try_parallel_delegation."""
445+
source = MagicMock()
446+
dest = MagicMock()
447+
dest.copyTransactionsFrom = MagicMock()
448+
449+
result = copy_transactions(source, dest, workers=2, blob_mode="background")
450+
# Delegation succeeded
451+
assert result == (None, None, None)
452+
dest.copyTransactionsFrom.assert_called_once_with(
453+
source, workers=2, blob_mode="background"
454+
)

tests/test_manifest.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,49 @@ def test_skips_missing_files(self, tmp_path):
5959

6060
assert stats["skipped"] == 1
6161
assert s3.upload_file.call_count == 0
62+
63+
def test_skips_malformed_lines(self, tmp_path):
64+
s3 = MagicMock()
65+
manifest = tmp_path / "manifest.tsv"
66+
blob = tmp_path / "blob0"
67+
blob.write_bytes(b"data")
68+
# Mix valid and malformed lines
69+
manifest.write_text(f"too\tfew\nfields\n{blob}\tblobs/0.blob\t0\t4\n\n")
70+
71+
stats = upload_from_manifest(str(manifest), s3, workers=1)
72+
73+
assert stats["uploaded"] == 1
74+
assert s3.upload_file.call_count == 1
75+
76+
def test_empty_manifest(self, tmp_path):
77+
s3 = MagicMock()
78+
manifest = tmp_path / "manifest.tsv"
79+
manifest.write_text("")
80+
81+
stats = upload_from_manifest(str(manifest), s3, workers=1)
82+
83+
assert stats["uploaded"] == 0
84+
assert stats["failed"] == 0
85+
assert stats["skipped"] == 0
86+
87+
def test_retry_exhaustion(self, tmp_path):
88+
"""All retries fail, triggering the retry-exhaustion logging path."""
89+
s3 = MagicMock()
90+
s3.upload_file.side_effect = Exception("transient")
91+
manifest = tmp_path / "manifest.tsv"
92+
blob = tmp_path / "blob0"
93+
blob.write_bytes(b"data")
94+
manifest.write_text(f"{blob}\tblobs/0.blob\t0\t4\n")
95+
96+
stats = upload_from_manifest(
97+
str(manifest),
98+
s3,
99+
workers=1,
100+
max_retries=3,
101+
retry_base_delay=0,
102+
)
103+
104+
assert stats["failed"] == 1
105+
assert stats["uploaded"] == 0
106+
# All 3 attempts were made
107+
assert s3.upload_file.call_count == 3

0 commit comments

Comments
 (0)