425 lines
16 KiB
Python
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"
|