""" Tests for the 3MF archive copy path. Regression guards for #1032 where large 3MF files were silently truncated during archiving on Raspberry Pi OS / armv7l, leaving the archive row in place but the on-disk file no longer a valid ZIP. """ import io import logging import os import zipfile from pathlib import Path import pytest from backend.app.services.archive import ThreeMFParser, _copy_and_fsync def _make_3mf(path: Path, payload_size: int = 0) -> None: """Write a minimal valid 3MF (ZIP) file with an optional large payload.""" with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf: zf.writestr("Metadata/slice_info.config", "") if payload_size: # Uncompressible payload forces real bytes on disk. zf.writestr("blob.bin", os.urandom(payload_size)) class TestCopyAndFsync: def test_copies_small_file_byte_for_byte(self, tmp_path: Path) -> None: src = tmp_path / "src.bin" dst = tmp_path / "dst.bin" src.write_bytes(b"hello world") _copy_and_fsync(src, dst) assert dst.read_bytes() == b"hello world" def test_copies_large_file_byte_for_byte(self, tmp_path: Path) -> None: """Spans multiple 1 MiB chunks to exercise the copy loop.""" src = tmp_path / "src.bin" dst = tmp_path / "dst.bin" payload = os.urandom(5 * 1024 * 1024 + 123) # 5 MiB + change src.write_bytes(payload) _copy_and_fsync(src, dst) assert dst.stat().st_size == len(payload) assert dst.read_bytes() == payload def test_preserves_mtime_via_copystat(self, tmp_path: Path) -> None: src = tmp_path / "src.bin" dst = tmp_path / "dst.bin" src.write_bytes(b"x") os.utime(src, (1_700_000_000, 1_700_000_000)) _copy_and_fsync(src, dst) assert int(dst.stat().st_mtime) == 1_700_000_000 def test_overwrites_existing_destination(self, tmp_path: Path) -> None: src = tmp_path / "src.bin" dst = tmp_path / "dst.bin" src.write_bytes(b"new") dst.write_bytes(b"old old old") _copy_and_fsync(src, dst) assert dst.read_bytes() == b"new" def test_produces_valid_zip_on_3mf(self, tmp_path: Path) -> None: """The whole point of #1032: copy of a valid 3MF stays a valid ZIP.""" src = tmp_path / "src.3mf" dst = tmp_path / "dst.3mf" _make_3mf(src, payload_size=2 * 1024 * 1024) # 2 MiB, multi-chunk assert zipfile.is_zipfile(src) _copy_and_fsync(src, dst) assert zipfile.is_zipfile(dst) class TestThreeMFParserErrorVisibility: def test_parse_logs_warning_on_corrupted_zip( self, tmp_path: Path, caplog: pytest.LogCaptureFixture, ) -> None: """Silent `except Exception: pass` was how #1032 escaped detection; parse() must now surface the failure at WARNING.""" corrupted = tmp_path / "bad.3mf" corrupted.write_bytes(b"not a zip") with caplog.at_level(logging.WARNING, logger="backend.app.services.archive"): result = ThreeMFParser(corrupted).parse() assert result == {} assert any("failed to parse" in rec.message and str(corrupted) in rec.message for rec in caplog.records), ( "Expected a WARNING mentioning the failed parse and file path" ) def test_parse_returns_partial_metadata_without_raising( self, tmp_path: Path, ) -> None: """A valid-but-minimal 3MF must still parse without raising.""" p = tmp_path / "ok.3mf" with zipfile.ZipFile(p, "w") as zf: zf.writestr("Metadata/slice_info.config", "") result = ThreeMFParser(p).parse() # No assertions about which keys are present — just that it didn't blow up. assert isinstance(result, dict) class TestZipFileSentinel: """Sanity check the sentinel the archive pipeline relies on.""" def test_is_zipfile_on_truncated_zip_returns_false(self, tmp_path: Path) -> None: """Truncating a valid ZIP mid-stream must flip is_zipfile() to False. This is the exact post-condition archive_print now trusts.""" src = tmp_path / "src.3mf" _make_3mf(src, payload_size=1024 * 1024) full = src.read_bytes() assert zipfile.is_zipfile(io.BytesIO(full)) truncated = tmp_path / "truncated.3mf" # Strip the trailing end-of-central-directory record — exactly what a # short sendfile return would leave behind. truncated.write_bytes(full[: len(full) // 2]) assert not zipfile.is_zipfile(truncated)