pyPhotoAlbum/tests/test_project_serialization.py
Duncan Tourolle 46585228fd
Some checks failed
Lint / lint (push) Failing after 2m46s
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.9) (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
first commit
2025-10-21 22:02:49 +02:00

425 lines
16 KiB
Python

"""
Unit tests for project serialization (save/load to ZIP)
"""
import pytest
import os
import json
import zipfile
import tempfile
import shutil
from pathlib import Path
from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.page_layout import PageLayout
from pyPhotoAlbum.models import ImageData, TextBoxData
from pyPhotoAlbum.project_serializer import save_to_zip, load_from_zip, get_project_info
@pytest.fixture
def temp_dir():
"""Create a temporary directory for testing"""
temp_path = tempfile.mkdtemp()
yield temp_path
# Cleanup
if os.path.exists(temp_path):
shutil.rmtree(temp_path)
@pytest.fixture
def sample_project(temp_dir):
"""Create a sample project for testing"""
project = Project(name="Test Project", folder_path=os.path.join(temp_dir, "test_project"))
project.page_size_mm = (210, 297)
project.working_dpi = 300
project.export_dpi = 300
return project
@pytest.fixture
def sample_image(temp_dir):
"""Create a sample image file for testing"""
from PIL import Image
# Create a simple test image
img = Image.new('RGB', (100, 100), color='red')
image_path = os.path.join(temp_dir, "test_image.jpg")
img.save(image_path)
return image_path
class TestBasicSerialization:
"""Tests for basic save/load functionality"""
def test_save_empty_project(self, sample_project, temp_dir):
"""Test saving an empty project to ZIP"""
zip_path = os.path.join(temp_dir, "empty_project.ppz")
success, error = save_to_zip(sample_project, zip_path)
assert success is True
assert error is None
assert os.path.exists(zip_path)
assert zip_path.endswith('.ppz')
def test_save_adds_ppz_extension(self, sample_project, temp_dir):
"""Test that .ppz extension is added automatically"""
zip_path = os.path.join(temp_dir, "project")
success, error = save_to_zip(sample_project, zip_path)
assert success is True
expected_path = zip_path + '.ppz'
assert os.path.exists(expected_path)
def test_load_empty_project(self, sample_project, temp_dir):
"""Test loading an empty project from ZIP"""
zip_path = os.path.join(temp_dir, "empty_project.ppz")
save_to_zip(sample_project, zip_path)
loaded_project, error = load_from_zip(zip_path)
assert loaded_project is not None
assert error is None
assert loaded_project.name == "Test Project"
assert loaded_project.page_size_mm == (210, 297)
assert loaded_project.working_dpi == 300
assert len(loaded_project.pages) == 0
def test_load_nonexistent_file(self, temp_dir):
"""Test loading from a non-existent file"""
zip_path = os.path.join(temp_dir, "nonexistent.ppz")
loaded_project, error = load_from_zip(zip_path)
assert loaded_project is None
assert error is not None
assert "not found" in error.lower()
def test_save_project_with_pages(self, sample_project, temp_dir):
"""Test saving a project with multiple pages"""
# Add pages
for i in range(3):
layout = PageLayout()
page = Page(layout=layout, page_number=i+1)
sample_project.add_page(page)
zip_path = os.path.join(temp_dir, "project_with_pages.ppz")
success, error = save_to_zip(sample_project, zip_path)
assert success is True
assert os.path.exists(zip_path)
def test_load_project_with_pages(self, sample_project, temp_dir):
"""Test loading a project with multiple pages"""
# Add pages
for i in range(3):
layout = PageLayout()
page = Page(layout=layout, page_number=i+1)
sample_project.add_page(page)
# Save and load
zip_path = os.path.join(temp_dir, "project_with_pages.ppz")
save_to_zip(sample_project, zip_path)
loaded_project, error = load_from_zip(zip_path)
assert loaded_project is not None
assert len(loaded_project.pages) == 3
assert loaded_project.pages[0].page_number == 1
assert loaded_project.pages[2].page_number == 3
class TestZipStructure:
"""Tests for ZIP file structure and content"""
def test_zip_contains_project_json(self, sample_project, temp_dir):
"""Test that ZIP contains project.json"""
zip_path = os.path.join(temp_dir, "test.ppz")
save_to_zip(sample_project, zip_path)
with zipfile.ZipFile(zip_path, 'r') as zipf:
assert 'project.json' in zipf.namelist()
def test_project_json_is_valid(self, sample_project, temp_dir):
"""Test that project.json contains valid JSON"""
zip_path = os.path.join(temp_dir, "test.ppz")
save_to_zip(sample_project, zip_path)
with zipfile.ZipFile(zip_path, 'r') as zipf:
project_json = zipf.read('project.json').decode('utf-8')
data = json.loads(project_json)
assert 'name' in data
assert 'serialization_version' in data
assert data['name'] == "Test Project"
def test_version_in_serialized_data(self, sample_project, temp_dir):
"""Test that version information is included"""
zip_path = os.path.join(temp_dir, "test.ppz")
save_to_zip(sample_project, zip_path)
with zipfile.ZipFile(zip_path, 'r') as zipf:
project_json = zipf.read('project.json').decode('utf-8')
data = json.loads(project_json)
assert 'serialization_version' in data
assert data['serialization_version'] == "1.0"
class TestAssetManagement:
"""Tests for asset bundling and management"""
def test_save_project_with_image(self, sample_project, sample_image, temp_dir):
"""Test saving a project with an image"""
# Import image to project
imported_path = sample_project.asset_manager.import_asset(sample_image)
# Create page with image
layout = PageLayout()
img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100)
layout.add_element(img_data)
page = Page(layout=layout, page_number=1)
sample_project.add_page(page)
# Save
zip_path = os.path.join(temp_dir, "project_with_image.ppz")
success, error = save_to_zip(sample_project, zip_path)
assert success is True
assert os.path.exists(zip_path)
def test_assets_folder_in_zip(self, sample_project, sample_image, temp_dir):
"""Test that assets folder is included in ZIP"""
# Import image
imported_path = sample_project.asset_manager.import_asset(sample_image)
# Create page with image
layout = PageLayout()
img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100)
layout.add_element(img_data)
page = Page(layout=layout, page_number=1)
sample_project.add_page(page)
# Save
zip_path = os.path.join(temp_dir, "project_with_image.ppz")
save_to_zip(sample_project, zip_path)
# Check ZIP contents
with zipfile.ZipFile(zip_path, 'r') as zipf:
names = zipf.namelist()
# Should contain assets folder
asset_files = [n for n in names if n.startswith('assets/')]
assert len(asset_files) > 0
def test_load_project_with_image(self, sample_project, sample_image, temp_dir):
"""Test loading a project with images"""
# Import image
imported_path = sample_project.asset_manager.import_asset(sample_image)
# Create page with image
layout = PageLayout()
img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100)
layout.add_element(img_data)
page = Page(layout=layout, page_number=1)
sample_project.add_page(page)
# Save and load
zip_path = os.path.join(temp_dir, "project_with_image.ppz")
save_to_zip(sample_project, zip_path)
loaded_project, error = load_from_zip(zip_path)
assert loaded_project is not None
assert len(loaded_project.pages) == 1
assert len(loaded_project.pages[0].layout.elements) == 1
# Verify image element
img_element = loaded_project.pages[0].layout.elements[0]
assert isinstance(img_element, ImageData)
assert img_element.image_path != ""
def test_asset_reference_counts_preserved(self, sample_project, sample_image, temp_dir):
"""Test that asset reference counts are preserved"""
# Import image
imported_path = sample_project.asset_manager.import_asset(sample_image)
# Use image twice
layout1 = PageLayout()
img1 = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100)
layout1.add_element(img1)
page1 = Page(layout=layout1, page_number=1)
sample_project.add_page(page1)
layout2 = PageLayout()
img2 = ImageData(image_path=imported_path, x=20, y=20, width=100, height=100)
layout2.add_element(img2)
page2 = Page(layout=layout2, page_number=2)
sample_project.add_page(page2)
# Get relative path for reference count check
rel_path = os.path.relpath(imported_path, sample_project.folder_path)
original_ref_count = sample_project.asset_manager.get_reference_count(rel_path)
# Save and load
zip_path = os.path.join(temp_dir, "project_refs.ppz")
save_to_zip(sample_project, zip_path)
loaded_project, error = load_from_zip(zip_path)
assert loaded_project is not None
# Reference counts should be preserved
# Note: The actual reference counting behavior depends on deserialize implementation
class TestPortability:
"""Tests for project portability across different locations"""
def test_load_to_different_directory(self, sample_project, sample_image, temp_dir):
"""Test loading project to a different directory"""
# Import image and create page
imported_path = sample_project.asset_manager.import_asset(sample_image)
layout = PageLayout()
img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100)
layout.add_element(img_data)
page = Page(layout=layout, page_number=1)
sample_project.add_page(page)
# Save
zip_path = os.path.join(temp_dir, "portable_project.ppz")
save_to_zip(sample_project, zip_path)
# Load to a different location
new_location = os.path.join(temp_dir, "different_location")
loaded_project, error = load_from_zip(zip_path, extract_to=new_location)
assert loaded_project is not None
assert loaded_project.folder_path == new_location
assert os.path.exists(new_location)
# Verify assets were extracted
assets_folder = os.path.join(new_location, "assets")
assert os.path.exists(assets_folder)
def test_relative_paths_work_after_move(self, sample_project, sample_image, temp_dir):
"""Test that relative paths still work after loading to different location"""
# Import image
imported_path = sample_project.asset_manager.import_asset(sample_image)
layout = PageLayout()
img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100)
layout.add_element(img_data)
page = Page(layout=layout, page_number=1)
sample_project.add_page(page)
# Save
zip_path = os.path.join(temp_dir, "portable_project.ppz")
save_to_zip(sample_project, zip_path)
# Load to different location
new_location = os.path.join(temp_dir, "new_location")
loaded_project, error = load_from_zip(zip_path, extract_to=new_location)
# Verify image path is accessible from new location
img_element = loaded_project.pages[0].layout.elements[0]
image_path = img_element.image_path
# Image path should exist
# Note: May be absolute or relative depending on implementation
if not os.path.isabs(image_path):
full_path = os.path.join(loaded_project.folder_path, image_path)
assert os.path.exists(full_path)
else:
assert os.path.exists(image_path)
class TestProjectInfo:
"""Tests for get_project_info utility function"""
def test_get_project_info(self, sample_project, temp_dir):
"""Test getting project info without loading"""
# Add some pages
for i in range(5):
layout = PageLayout()
page = Page(layout=layout, page_number=i+1)
sample_project.add_page(page)
# Save
zip_path = os.path.join(temp_dir, "info_test.ppz")
save_to_zip(sample_project, zip_path)
# Get info
info = get_project_info(zip_path)
assert info is not None
assert info['name'] == "Test Project"
assert info['page_count'] == 5
assert info['version'] == "1.0"
assert info['working_dpi'] == 300
def test_get_info_invalid_zip(self, temp_dir):
"""Test getting info from invalid ZIP"""
zip_path = os.path.join(temp_dir, "invalid.ppz")
info = get_project_info(zip_path)
assert info is None
class TestEdgeCases:
"""Tests for edge cases and error handling"""
def test_save_to_invalid_path(self, sample_project):
"""Test saving to an invalid path"""
invalid_path = "/nonexistent/directory/project.ppz"
success, error = save_to_zip(sample_project, invalid_path)
assert success is False
assert error is not None
def test_load_corrupted_zip(self, temp_dir):
"""Test loading a corrupted ZIP file"""
# Create a fake corrupted file
corrupted_path = os.path.join(temp_dir, "corrupted.ppz")
with open(corrupted_path, 'w') as f:
f.write("This is not a ZIP file")
loaded_project, error = load_from_zip(corrupted_path)
assert loaded_project is None
assert error is not None
def test_load_zip_without_project_json(self, temp_dir):
"""Test loading a ZIP without project.json"""
zip_path = os.path.join(temp_dir, "no_json.ppz")
# Create ZIP without project.json
with zipfile.ZipFile(zip_path, 'w') as zipf:
zipf.writestr('dummy.txt', 'dummy content')
loaded_project, error = load_from_zip(zip_path)
assert loaded_project is None
assert error is not None
assert "project.json not found" in error
def test_project_with_text_elements(self, sample_project, temp_dir):
"""Test saving/loading project with text elements"""
# Create page with text
layout = PageLayout()
text = TextBoxData(
text_content="Hello World",
x=10, y=10, width=200, height=50
)
layout.add_element(text)
page = Page(layout=layout, page_number=1)
sample_project.add_page(page)
# Save and load
zip_path = os.path.join(temp_dir, "with_text.ppz")
save_to_zip(sample_project, zip_path)
loaded_project, error = load_from_zip(zip_path)
assert loaded_project is not None
assert len(loaded_project.pages) == 1
text_element = loaded_project.pages[0].layout.elements[0]
assert isinstance(text_element, TextBoxData)
assert text_element.text_content == "Hello World"