pyPhotoAlbum/tests/test_async_loading_mixin.py
Duncan Tourolle f6ed11b0bc
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
black formatting
2025-11-27 23:07:16 +01:00

636 lines
20 KiB
Python

"""
Tests for async_loading mixin module
"""
import pytest
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch, PropertyMock
class TestAsyncLoadingMixinInit:
"""Tests for AsyncLoadingMixin initialization"""
def test_init_async_loading_creates_cache(self):
"""Test that _init_async_loading creates image cache"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
with (
patch("pyPhotoAlbum.mixins.async_loading.ImageCache") as mock_cache,
patch("pyPhotoAlbum.mixins.async_loading.AsyncImageLoader") as mock_loader,
patch("pyPhotoAlbum.mixins.async_loading.AsyncPDFGenerator") as mock_pdf,
):
mock_loader_instance = Mock()
mock_loader.return_value = mock_loader_instance
mock_pdf_instance = Mock()
mock_pdf.return_value = mock_pdf_instance
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj._init_async_loading()
mock_cache.assert_called_once_with(max_memory_mb=512)
assert hasattr(obj, "image_cache")
def test_init_async_loading_creates_image_loader(self):
"""Test that _init_async_loading creates async image loader"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
with (
patch("pyPhotoAlbum.mixins.async_loading.ImageCache") as mock_cache,
patch("pyPhotoAlbum.mixins.async_loading.AsyncImageLoader") as mock_loader,
patch("pyPhotoAlbum.mixins.async_loading.AsyncPDFGenerator") as mock_pdf,
):
mock_loader_instance = Mock()
mock_loader.return_value = mock_loader_instance
mock_pdf_instance = Mock()
mock_pdf.return_value = mock_pdf_instance
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj._init_async_loading()
mock_loader.assert_called_once()
assert hasattr(obj, "async_image_loader")
mock_loader_instance.start.assert_called_once()
def test_init_async_loading_creates_pdf_generator(self):
"""Test that _init_async_loading creates async PDF generator"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
with (
patch("pyPhotoAlbum.mixins.async_loading.ImageCache") as mock_cache,
patch("pyPhotoAlbum.mixins.async_loading.AsyncImageLoader") as mock_loader,
patch("pyPhotoAlbum.mixins.async_loading.AsyncPDFGenerator") as mock_pdf,
):
mock_loader_instance = Mock()
mock_loader.return_value = mock_loader_instance
mock_pdf_instance = Mock()
mock_pdf.return_value = mock_pdf_instance
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj._init_async_loading()
mock_pdf.assert_called_once()
assert hasattr(obj, "async_pdf_generator")
mock_pdf_instance.start.assert_called_once()
class TestAsyncLoadingMixinCleanup:
"""Tests for AsyncLoadingMixin cleanup"""
def test_cleanup_stops_image_loader(self):
"""Test that _cleanup_async_loading stops image loader"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj.async_image_loader = Mock()
obj.async_pdf_generator = Mock()
obj.image_cache = Mock()
obj._cleanup_async_loading()
obj.async_image_loader.stop.assert_called_once()
def test_cleanup_stops_pdf_generator(self):
"""Test that _cleanup_async_loading stops PDF generator"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj.async_image_loader = Mock()
obj.async_pdf_generator = Mock()
obj.image_cache = Mock()
obj._cleanup_async_loading()
obj.async_pdf_generator.stop.assert_called_once()
def test_cleanup_clears_cache(self):
"""Test that _cleanup_async_loading clears image cache"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj.async_image_loader = Mock()
obj.async_pdf_generator = Mock()
obj.image_cache = Mock()
obj._cleanup_async_loading()
obj.image_cache.clear.assert_called_once()
def test_cleanup_handles_missing_components(self):
"""Test that _cleanup_async_loading handles missing components gracefully"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
# Don't set any async components
# Should not raise
obj._cleanup_async_loading()
class TestOnImageLoaded:
"""Tests for _on_image_loaded callback"""
def test_on_image_loaded_calls_element_callback(self):
"""Test that _on_image_loaded calls element's callback"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
def update(self):
pass
obj = TestClass()
mock_image = Mock()
mock_user_data = Mock()
mock_user_data._on_async_image_loaded = Mock()
obj._on_image_loaded(Path("/test/image.jpg"), mock_image, mock_user_data)
mock_user_data._on_async_image_loaded.assert_called_once_with(mock_image)
def test_on_image_loaded_triggers_update(self):
"""Test that _on_image_loaded triggers widget update"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
def __init__(self):
self.update_called = False
def update(self):
self.update_called = True
obj = TestClass()
obj._on_image_loaded(Path("/test/image.jpg"), Mock(), None)
assert obj.update_called
def test_on_image_loaded_handles_none_user_data(self):
"""Test that _on_image_loaded handles None user_data"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
def update(self):
pass
obj = TestClass()
# Should not raise
obj._on_image_loaded(Path("/test/image.jpg"), Mock(), None)
class TestOnImageLoadFailed:
"""Tests for _on_image_load_failed callback"""
def test_on_image_load_failed_calls_element_callback(self):
"""Test that _on_image_load_failed calls element's callback"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
mock_user_data = Mock()
mock_user_data._on_async_image_load_failed = Mock()
obj._on_image_load_failed(Path("/test/image.jpg"), "Error message", mock_user_data)
mock_user_data._on_async_image_load_failed.assert_called_once_with("Error message")
def test_on_image_load_failed_handles_none_user_data(self):
"""Test that _on_image_load_failed handles None user_data"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
# Should not raise
obj._on_image_load_failed(Path("/test/image.jpg"), "Error", None)
class TestOnPdfProgress:
"""Tests for _on_pdf_progress callback"""
def test_on_pdf_progress_updates_dialog(self):
"""Test that _on_pdf_progress updates progress dialog"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj._pdf_progress_dialog = Mock()
obj._on_pdf_progress(5, 10, "Processing page 5")
obj._pdf_progress_dialog.setValue.assert_called_once_with(5)
obj._pdf_progress_dialog.setLabelText.assert_called_once_with("Processing page 5")
def test_on_pdf_progress_handles_no_dialog(self):
"""Test that _on_pdf_progress handles missing dialog"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
# No _pdf_progress_dialog attribute
# Should not raise
obj._on_pdf_progress(5, 10, "Processing")
class TestOnPdfComplete:
"""Tests for _on_pdf_complete callback"""
def test_on_pdf_complete_closes_dialog(self):
"""Test that _on_pdf_complete closes progress dialog"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
def window(self):
return Mock(spec=[])
obj = TestClass()
mock_dialog = Mock()
obj._pdf_progress_dialog = mock_dialog
obj._on_pdf_complete(True, [])
mock_dialog.close.assert_called_once()
assert obj._pdf_progress_dialog is None
def test_on_pdf_complete_shows_success_status(self):
"""Test that _on_pdf_complete shows success status"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
mock_main_window = Mock()
class TestClass(AsyncLoadingMixin):
def window(self):
return mock_main_window
obj = TestClass()
obj._on_pdf_complete(True, [])
mock_main_window.show_status.assert_called_once()
call_args = mock_main_window.show_status.call_args[0]
assert "successfully" in call_args[0]
def test_on_pdf_complete_shows_warnings(self):
"""Test that _on_pdf_complete shows warning count"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
mock_main_window = Mock()
class TestClass(AsyncLoadingMixin):
def window(self):
return mock_main_window
obj = TestClass()
obj._on_pdf_complete(True, ["warning1", "warning2"])
mock_main_window.show_status.assert_called_once()
call_args = mock_main_window.show_status.call_args[0]
assert "2 warnings" in call_args[0]
def test_on_pdf_complete_shows_failure_status(self):
"""Test that _on_pdf_complete shows failure status"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
mock_main_window = Mock()
class TestClass(AsyncLoadingMixin):
def window(self):
return mock_main_window
obj = TestClass()
obj._on_pdf_complete(False, [])
mock_main_window.show_status.assert_called_once()
call_args = mock_main_window.show_status.call_args[0]
assert "failed" in call_args[0]
class TestOnPdfFailed:
"""Tests for _on_pdf_failed callback"""
def test_on_pdf_failed_closes_dialog(self):
"""Test that _on_pdf_failed closes progress dialog"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
def window(self):
return Mock(spec=[])
obj = TestClass()
mock_dialog = Mock()
obj._pdf_progress_dialog = mock_dialog
obj._on_pdf_failed("Error occurred")
mock_dialog.close.assert_called_once()
assert obj._pdf_progress_dialog is None
def test_on_pdf_failed_shows_error_status(self):
"""Test that _on_pdf_failed shows error status"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
mock_main_window = Mock()
class TestClass(AsyncLoadingMixin):
def window(self):
return mock_main_window
obj = TestClass()
obj._on_pdf_failed("Something went wrong")
mock_main_window.show_status.assert_called_once()
call_args = mock_main_window.show_status.call_args[0]
assert "failed" in call_args[0]
assert "Something went wrong" in call_args[0]
class TestRequestImageLoad:
"""Tests for request_image_load method"""
def test_request_image_load_no_loader(self):
"""Test request_image_load when loader not initialized"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
mock_image_data = Mock()
# Should not raise
obj.request_image_load(mock_image_data)
def test_request_image_load_empty_path(self):
"""Test request_image_load with empty image path"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj.async_image_loader = Mock()
mock_image_data = Mock()
mock_image_data.image_path = ""
obj.request_image_load(mock_image_data)
obj.async_image_loader.request_load.assert_not_called()
def test_request_image_load_non_assets_path_skipped(self):
"""Test request_image_load skips paths outside assets folder"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj.async_image_loader = Mock()
mock_image_data = Mock()
mock_image_data.image_path = "/absolute/path/image.jpg"
obj.request_image_load(mock_image_data)
obj.async_image_loader.request_load.assert_not_called()
def test_request_image_load_path_not_resolved(self):
"""Test request_image_load when path resolution fails"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj.async_image_loader = Mock()
mock_image_data = Mock()
mock_image_data.image_path = "assets/missing.jpg"
mock_image_data.resolve_image_path.return_value = None
obj.request_image_load(mock_image_data)
obj.async_image_loader.request_load.assert_not_called()
def test_request_image_load_success(self, tmp_path):
"""Test successful request_image_load"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin, LoadPriority
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj.async_image_loader = Mock()
# Create actual file
asset_path = tmp_path / "assets" / "photo.jpg"
asset_path.parent.mkdir(parents=True)
asset_path.write_text("test")
mock_image_data = Mock()
mock_image_data.image_path = "assets/photo.jpg"
mock_image_data.resolve_image_path.return_value = str(asset_path)
obj.request_image_load(mock_image_data, priority=LoadPriority.HIGH)
obj.async_image_loader.request_load.assert_called_once()
call_kwargs = obj.async_image_loader.request_load.call_args[1]
assert call_kwargs["priority"] == LoadPriority.HIGH
assert call_kwargs["user_data"] == mock_image_data
class TestExportPdfAsync:
"""Tests for export_pdf_async method"""
def test_export_pdf_async_no_generator(self):
"""Test export_pdf_async when generator not initialized"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
mock_project = Mock()
result = obj.export_pdf_async(mock_project, "/output.pdf")
assert result is False
def test_export_pdf_async_creates_progress_dialog(self, qtbot):
"""Test export_pdf_async creates progress dialog"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
from PyQt6.QtWidgets import QWidget
class TestWidget(QWidget, AsyncLoadingMixin):
pass
widget = TestWidget()
qtbot.addWidget(widget)
widget.async_pdf_generator = Mock()
widget.async_pdf_generator.export_pdf.return_value = True
mock_project = Mock()
mock_project.pages = [Mock(is_cover=False, is_double_spread=False)]
widget.export_pdf_async(mock_project, "/output.pdf")
assert hasattr(widget, "_pdf_progress_dialog")
assert widget._pdf_progress_dialog is not None
def test_export_pdf_async_calls_generator(self, qtbot):
"""Test export_pdf_async calls the PDF generator"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
from PyQt6.QtWidgets import QWidget
class TestWidget(QWidget, AsyncLoadingMixin):
pass
widget = TestWidget()
qtbot.addWidget(widget)
widget.async_pdf_generator = Mock()
widget.async_pdf_generator.export_pdf.return_value = True
mock_project = Mock()
mock_project.pages = []
result = widget.export_pdf_async(mock_project, "/output.pdf", export_dpi=150)
widget.async_pdf_generator.export_pdf.assert_called_once_with(mock_project, "/output.pdf", 150)
assert result is True
class TestOnPdfCancel:
"""Tests for _on_pdf_cancel callback"""
def test_on_pdf_cancel_cancels_export(self):
"""Test that _on_pdf_cancel cancels the export"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj.async_pdf_generator = Mock()
obj._on_pdf_cancel()
obj.async_pdf_generator.cancel_export.assert_called_once()
def test_on_pdf_cancel_handles_no_generator(self):
"""Test that _on_pdf_cancel handles missing generator"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
# No async_pdf_generator
# Should not raise
obj._on_pdf_cancel()
class TestGetAsyncStats:
"""Tests for get_async_stats method"""
def test_get_async_stats_empty(self):
"""Test get_async_stats with no components initialized"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
stats = obj.get_async_stats()
assert stats == {}
def test_get_async_stats_with_loader(self):
"""Test get_async_stats includes loader stats"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj.async_image_loader = Mock()
obj.async_image_loader.get_stats.return_value = {"loaded": 10}
stats = obj.get_async_stats()
assert "image_loader" in stats
assert stats["image_loader"]["loaded"] == 10
def test_get_async_stats_with_pdf_generator(self):
"""Test get_async_stats includes PDF generator stats"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj.async_pdf_generator = Mock()
obj.async_pdf_generator.get_stats.return_value = {"exports": 5}
stats = obj.get_async_stats()
assert "pdf_generator" in stats
assert stats["pdf_generator"]["exports"] == 5
def test_get_async_stats_with_all_components(self):
"""Test get_async_stats includes all component stats"""
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
class TestClass(AsyncLoadingMixin):
pass
obj = TestClass()
obj.async_image_loader = Mock()
obj.async_image_loader.get_stats.return_value = {"loaded": 10}
obj.async_pdf_generator = Mock()
obj.async_pdf_generator.get_stats.return_value = {"exports": 5}
stats = obj.get_async_stats()
assert "image_loader" in stats
assert "pdf_generator" in stats