All checks were successful
Python CI / test (push) Successful in 1m20s
Lint / lint (push) Successful in 1m4s
Tests / test (3.11) (push) Successful in 1m27s
Tests / test (3.12) (push) Successful in 2m25s
Tests / test (3.13) (push) Successful in 2m52s
Tests / test (3.14) (push) Successful in 1m9s
433 lines
15 KiB
Python
433 lines
15 KiB
Python
"""
|
|
Comprehensive tests for project_serializer module
|
|
"""
|
|
|
|
import pytest
|
|
import os
|
|
import json
|
|
import zipfile
|
|
import tempfile
|
|
import shutil
|
|
from pathlib import Path
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
|
|
from pyPhotoAlbum.project_serializer import (
|
|
save_to_zip,
|
|
load_from_zip,
|
|
get_project_info,
|
|
_normalize_asset_paths,
|
|
_import_external_images,
|
|
SERIALIZATION_VERSION,
|
|
)
|
|
from pyPhotoAlbum.project import Project
|
|
from pyPhotoAlbum.models import ImageData
|
|
|
|
|
|
class TestSaveToZip:
|
|
"""Tests for save_to_zip function"""
|
|
|
|
def test_save_to_zip_basic(self, tmp_path):
|
|
"""Test basic project saving to zip"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
project = Project(name="TestProject", folder_path=str(project_folder))
|
|
|
|
zip_path = str(tmp_path / "test_project.ppz")
|
|
success, error = save_to_zip(project, zip_path)
|
|
|
|
assert success is True
|
|
assert error is None
|
|
assert os.path.exists(zip_path)
|
|
|
|
def test_save_to_zip_adds_extension(self, tmp_path):
|
|
"""Test that .ppz extension is added if missing"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
project = Project(name="TestProject", folder_path=str(project_folder))
|
|
|
|
zip_path = str(tmp_path / "test_project") # No extension
|
|
success, error = save_to_zip(project, zip_path)
|
|
|
|
assert success is True
|
|
assert os.path.exists(zip_path + ".ppz")
|
|
|
|
def test_save_to_zip_includes_project_json(self, tmp_path):
|
|
"""Test that saved zip contains project.json"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
project = Project(name="TestProject", folder_path=str(project_folder))
|
|
|
|
zip_path = str(tmp_path / "test_project.ppz")
|
|
save_to_zip(project, zip_path)
|
|
|
|
with zipfile.ZipFile(zip_path, "r") as zipf:
|
|
assert "project.json" in zipf.namelist()
|
|
|
|
project_data = json.loads(zipf.read("project.json"))
|
|
assert project_data["name"] == "TestProject"
|
|
assert "data_version" in project_data
|
|
|
|
def test_save_to_zip_includes_assets(self, tmp_path):
|
|
"""Test that saved zip includes asset files"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
# Create a dummy asset file
|
|
asset_file = assets_folder / "image.jpg"
|
|
asset_file.write_bytes(b"fake image data")
|
|
|
|
project = Project(name="TestProject", folder_path=str(project_folder))
|
|
|
|
zip_path = str(tmp_path / "test_project.ppz")
|
|
save_to_zip(project, zip_path)
|
|
|
|
with zipfile.ZipFile(zip_path, "r") as zipf:
|
|
assert "assets/image.jpg" in zipf.namelist()
|
|
|
|
def test_save_to_zip_handles_error(self, tmp_path):
|
|
"""Test error handling during save"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
project = Project(name="TestProject", folder_path=str(project_folder))
|
|
|
|
# Try to save to an invalid path
|
|
zip_path = "/nonexistent/directory/test.ppz"
|
|
success, error = save_to_zip(project, zip_path)
|
|
|
|
assert success is False
|
|
assert error is not None
|
|
assert "Error saving" in error
|
|
|
|
|
|
class TestLoadFromZip:
|
|
"""Tests for load_from_zip function"""
|
|
|
|
def test_load_from_zip_basic(self, tmp_path):
|
|
"""Test basic project loading from zip"""
|
|
# First create a valid project zip
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
project = Project(name="LoadTest", folder_path=str(project_folder))
|
|
zip_path = str(tmp_path / "test_project.ppz")
|
|
save_to_zip(project, zip_path)
|
|
|
|
# Now load it
|
|
extract_to = str(tmp_path / "extracted")
|
|
loaded_project = load_from_zip(zip_path, extract_to)
|
|
|
|
assert loaded_project.name == "LoadTest"
|
|
assert loaded_project.folder_path == extract_to
|
|
|
|
def test_load_from_zip_creates_temp_dir(self, tmp_path):
|
|
"""Test that loading creates a temp directory when none specified"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
project = Project(name="TempTest", folder_path=str(project_folder))
|
|
zip_path = str(tmp_path / "test_project.ppz")
|
|
save_to_zip(project, zip_path)
|
|
|
|
# Load without specifying extraction directory
|
|
loaded_project = load_from_zip(zip_path)
|
|
|
|
assert loaded_project.name == "TempTest"
|
|
assert loaded_project.folder_path is not None
|
|
assert os.path.exists(loaded_project.folder_path)
|
|
|
|
# Should have a _temp_dir attribute
|
|
assert hasattr(loaded_project, "_temp_dir")
|
|
|
|
def test_load_from_zip_file_not_found(self, tmp_path):
|
|
"""Test loading from nonexistent file"""
|
|
with pytest.raises(FileNotFoundError):
|
|
load_from_zip(str(tmp_path / "nonexistent.ppz"))
|
|
|
|
def test_load_from_zip_invalid_zip(self, tmp_path):
|
|
"""Test loading from invalid zip file"""
|
|
invalid_file = tmp_path / "invalid.ppz"
|
|
invalid_file.write_text("not a zip file")
|
|
|
|
with pytest.raises(Exception):
|
|
load_from_zip(str(invalid_file))
|
|
|
|
def test_load_from_zip_missing_project_json(self, tmp_path):
|
|
"""Test loading from zip without project.json"""
|
|
zip_path = tmp_path / "no_project.ppz"
|
|
|
|
# Create zip without project.json
|
|
with zipfile.ZipFile(str(zip_path), "w") as zipf:
|
|
zipf.writestr("other_file.txt", "some content")
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
load_from_zip(str(zip_path))
|
|
|
|
assert "project.json not found" in str(exc_info.value)
|
|
|
|
|
|
class TestGetProjectInfo:
|
|
"""Tests for get_project_info function"""
|
|
|
|
def test_get_project_info_basic(self, tmp_path):
|
|
"""Test getting project info from zip"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
project = Project(name="InfoTest", folder_path=str(project_folder))
|
|
zip_path = str(tmp_path / "test_project.ppz")
|
|
save_to_zip(project, zip_path)
|
|
|
|
info = get_project_info(zip_path)
|
|
|
|
assert info is not None
|
|
assert info["name"] == "InfoTest"
|
|
assert "version" in info
|
|
assert "page_count" in info
|
|
assert "page_size_mm" in info
|
|
assert "working_dpi" in info
|
|
|
|
def test_get_project_info_invalid_file(self, tmp_path):
|
|
"""Test getting info from invalid file"""
|
|
invalid_file = tmp_path / "invalid.ppz"
|
|
invalid_file.write_text("not a zip")
|
|
|
|
info = get_project_info(str(invalid_file))
|
|
|
|
assert info is None
|
|
|
|
def test_get_project_info_nonexistent_file(self, tmp_path):
|
|
"""Test getting info from nonexistent file"""
|
|
info = get_project_info(str(tmp_path / "nonexistent.ppz"))
|
|
|
|
assert info is None
|
|
|
|
|
|
class TestNormalizeAssetPaths:
|
|
"""Tests for _normalize_asset_paths function"""
|
|
|
|
def test_normalize_relative_path_unchanged(self, tmp_path):
|
|
"""Test that simple relative paths are unchanged"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
project = Project(name="Test", folder_path=str(project_folder))
|
|
|
|
# Add a page with an image that has a simple relative path
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
|
|
page_mock = Mock()
|
|
layout = PageLayout(width=210, height=297)
|
|
img = ImageData(image_path="assets/image.jpg")
|
|
layout.add_element(img)
|
|
page_mock.layout = layout
|
|
project.pages = [page_mock]
|
|
|
|
_normalize_asset_paths(project, str(project_folder))
|
|
|
|
# Path should be unchanged
|
|
assert img.image_path == "assets/image.jpg"
|
|
|
|
def test_normalize_absolute_path(self, tmp_path):
|
|
"""Test that absolute paths are normalized"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
project = Project(name="Test", folder_path=str(project_folder))
|
|
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
|
|
page_mock = Mock()
|
|
layout = PageLayout(width=210, height=297)
|
|
# Use a path that contains /assets/ pattern
|
|
abs_path = str(project_folder / "assets" / "image.jpg")
|
|
img = ImageData(image_path=abs_path)
|
|
layout.add_element(img)
|
|
page_mock.layout = layout
|
|
project.pages = [page_mock]
|
|
|
|
_normalize_asset_paths(project, str(project_folder))
|
|
|
|
# Path should be normalized to relative
|
|
assert img.image_path == "assets/image.jpg"
|
|
|
|
def test_normalize_legacy_path(self, tmp_path):
|
|
"""Test normalizing legacy project path format"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
project = Project(name="Test", folder_path=str(project_folder))
|
|
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
|
|
page_mock = Mock()
|
|
layout = PageLayout(width=210, height=297)
|
|
# Legacy path format
|
|
img = ImageData(image_path="./projects/old_project/assets/image.jpg")
|
|
layout.add_element(img)
|
|
page_mock.layout = layout
|
|
project.pages = [page_mock]
|
|
|
|
_normalize_asset_paths(project, str(project_folder))
|
|
|
|
# Should extract just the assets/filename part
|
|
assert img.image_path == "assets/image.jpg"
|
|
|
|
|
|
class TestImportExternalImages:
|
|
"""Tests for _import_external_images function"""
|
|
|
|
def test_import_external_images_no_external(self, tmp_path):
|
|
"""Test with no external images"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
project = Project(name="Test", folder_path=str(project_folder))
|
|
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
|
|
page_mock = Mock()
|
|
layout = PageLayout(width=210, height=297)
|
|
img = ImageData(image_path="assets/existing.jpg")
|
|
layout.add_element(img)
|
|
page_mock.layout = layout
|
|
project.pages = [page_mock]
|
|
|
|
# Should not raise and not change path
|
|
_import_external_images(project)
|
|
|
|
assert img.image_path == "assets/existing.jpg"
|
|
|
|
|
|
class TestRoundTrip:
|
|
"""Test save and load roundtrip"""
|
|
|
|
def test_roundtrip_basic(self, tmp_path):
|
|
"""Test saving and loading a project"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
original = Project(name="RoundTrip", folder_path=str(project_folder))
|
|
original.working_dpi = 150
|
|
|
|
zip_path = str(tmp_path / "roundtrip.ppz")
|
|
success, _ = save_to_zip(original, zip_path)
|
|
assert success
|
|
|
|
extract_to = str(tmp_path / "extracted")
|
|
loaded = load_from_zip(zip_path, extract_to)
|
|
|
|
assert loaded.name == original.name
|
|
assert loaded.working_dpi == original.working_dpi
|
|
|
|
def test_roundtrip_with_pages(self, tmp_path):
|
|
"""Test roundtrip with pages"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
original = Project(name="WithPages", folder_path=str(project_folder))
|
|
# Project starts with 1 page, add more using create_page
|
|
from pyPhotoAlbum.project import Page
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
|
|
page2 = Page(PageLayout(width=210, height=297))
|
|
page3 = Page(PageLayout(width=210, height=297))
|
|
original.add_page(page2)
|
|
original.add_page(page3)
|
|
|
|
zip_path = str(tmp_path / "pages.ppz")
|
|
save_to_zip(original, zip_path)
|
|
|
|
extract_to = str(tmp_path / "extracted")
|
|
loaded = load_from_zip(zip_path, extract_to)
|
|
|
|
# Pages are preserved (Project might not start with a default page)
|
|
assert len(loaded.pages) >= 2
|
|
|
|
def test_roundtrip_with_elements(self, tmp_path, temp_image_file):
|
|
"""Test roundtrip with elements on page"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
# Copy temp image to assets
|
|
shutil.copy(temp_image_file, assets_folder / "test.jpg")
|
|
|
|
original = Project(name="WithElements", folder_path=str(project_folder))
|
|
|
|
# Add element to first page (project starts with at least 1 page)
|
|
img = ImageData(image_path="assets/test.jpg", x=50, y=50, width=100, height=100)
|
|
# Check if there's a default page, add one if needed
|
|
if not original.pages:
|
|
from pyPhotoAlbum.project import Page
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
|
|
original.add_page(Page(PageLayout(width=210, height=297)))
|
|
original.pages[0].layout.add_element(img)
|
|
|
|
zip_path = str(tmp_path / "elements.ppz")
|
|
save_to_zip(original, zip_path)
|
|
|
|
extract_to = str(tmp_path / "extracted")
|
|
loaded = load_from_zip(zip_path, extract_to)
|
|
|
|
assert len(loaded.pages) >= 1
|
|
assert len(loaded.pages[0].layout.elements) >= 1
|
|
loaded_elem = loaded.pages[0].layout.elements[0]
|
|
assert loaded_elem.position == (50.0, 50.0)
|
|
assert loaded_elem.size == (100.0, 100.0)
|
|
|
|
|
|
class TestVersionCompatibility:
|
|
"""Tests for version handling"""
|
|
|
|
def test_version_included_in_save(self, tmp_path):
|
|
"""Test that version is included when saving"""
|
|
project_folder = tmp_path / "project"
|
|
project_folder.mkdir()
|
|
assets_folder = project_folder / "assets"
|
|
assets_folder.mkdir()
|
|
|
|
project = Project(name="Version", folder_path=str(project_folder))
|
|
zip_path = str(tmp_path / "version.ppz")
|
|
save_to_zip(project, zip_path)
|
|
|
|
with zipfile.ZipFile(zip_path, "r") as zipf:
|
|
data = json.loads(zipf.read("project.json"))
|
|
|
|
# Should have both legacy and new version fields
|
|
assert "serialization_version" in data
|
|
assert "data_version" in data
|