""" Tests for AutosaveManager """ import pytest import json import tempfile import shutil from pathlib import Path from datetime import datetime, timedelta from unittest.mock import Mock, patch, MagicMock from pyPhotoAlbum.autosave_manager import AutosaveManager class TestAutosaveManagerInit: """Tests for AutosaveManager initialization""" def test_init_creates_checkpoint_directory(self, tmp_path, monkeypatch): """Test that init creates the checkpoint directory""" checkpoint_dir = tmp_path / "checkpoints" monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() assert checkpoint_dir.exists() def test_init_with_existing_directory(self, tmp_path, monkeypatch): """Test init when checkpoint directory already exists""" checkpoint_dir = tmp_path / "checkpoints" checkpoint_dir.mkdir(parents=True) monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() assert checkpoint_dir.exists() class TestGetCheckpointPath: """Tests for _get_checkpoint_path method""" def test_get_checkpoint_path_basic(self, tmp_path, monkeypatch): """Test basic checkpoint path generation""" checkpoint_dir = tmp_path / "checkpoints" monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() path = manager._get_checkpoint_path("MyProject") assert path.parent == checkpoint_dir assert path.suffix == ".ppz" assert "checkpoint_MyProject_" in path.name def test_get_checkpoint_path_with_timestamp(self, tmp_path, monkeypatch): """Test checkpoint path with specific timestamp""" checkpoint_dir = tmp_path / "checkpoints" monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() timestamp = datetime(2024, 1, 15, 10, 30, 45) path = manager._get_checkpoint_path("TestProject", timestamp) assert "20240115_103045" in path.name def test_get_checkpoint_path_sanitizes_name(self, tmp_path, monkeypatch): """Test that special characters in project name are sanitized""" checkpoint_dir = tmp_path / "checkpoints" monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() path = manager._get_checkpoint_path("My Project!@#$%") # Should not contain special characters except - and _ name_without_ext = path.stem for char in name_without_ext: assert char.isalnum() or char in "-_", f"Invalid char: {char}" class TestCreateCheckpoint: """Tests for create_checkpoint method""" def test_create_checkpoint_success(self, tmp_path, monkeypatch): """Test successful checkpoint creation""" checkpoint_dir = tmp_path / "checkpoints" monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() # Mock save_to_zip - note the return value format with patch("pyPhotoAlbum.autosave_manager.save_to_zip") as mock_save: mock_save.return_value = (True, "Success") mock_project = Mock() mock_project.name = "TestProject" mock_project.file_path = "/path/to/project.ppz" success, message = manager.create_checkpoint(mock_project) assert success is True assert "Checkpoint created" in message mock_save.assert_called_once() def test_create_checkpoint_failure(self, tmp_path, monkeypatch): """Test checkpoint creation failure""" checkpoint_dir = tmp_path / "checkpoints" monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() with patch("pyPhotoAlbum.autosave_manager.save_to_zip") as mock_save: mock_save.return_value = (False, "Disk full") mock_project = Mock() mock_project.name = "TestProject" success, message = manager.create_checkpoint(mock_project) assert success is False assert "Checkpoint failed" in message def test_create_checkpoint_exception(self, tmp_path, monkeypatch): """Test checkpoint creation with exception""" checkpoint_dir = tmp_path / "checkpoints" monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() with patch("pyPhotoAlbum.autosave_manager.save_to_zip") as mock_save: mock_save.side_effect = Exception("IO Error") mock_project = Mock() mock_project.name = "TestProject" success, message = manager.create_checkpoint(mock_project) assert success is False assert "Checkpoint error" in message class TestSaveCheckpointMetadata: """Tests for _save_checkpoint_metadata method""" def test_save_metadata(self, tmp_path, monkeypatch): """Test saving checkpoint metadata""" checkpoint_dir = tmp_path / "checkpoints" checkpoint_dir.mkdir(parents=True) monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() mock_project = Mock() mock_project.name = "TestProject" mock_project.file_path = "/path/to/original.ppz" checkpoint_path = checkpoint_dir / "checkpoint_TestProject_20240115_103045.ppz" checkpoint_path.touch() manager._save_checkpoint_metadata(mock_project, checkpoint_path) metadata_path = checkpoint_path.with_suffix(".json") assert metadata_path.exists() with open(metadata_path, "r") as f: metadata = json.load(f) assert metadata["project_name"] == "TestProject" assert metadata["original_path"] == "/path/to/original.ppz" assert "timestamp" in metadata class TestListCheckpoints: """Tests for list_checkpoints method""" def test_list_checkpoints_empty(self, tmp_path, monkeypatch): """Test listing checkpoints when none exist""" checkpoint_dir = tmp_path / "checkpoints" monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() checkpoints = manager.list_checkpoints() assert checkpoints == [] def test_list_checkpoints_with_files(self, tmp_path, monkeypatch): """Test listing checkpoints with existing files""" checkpoint_dir = tmp_path / "checkpoints" checkpoint_dir.mkdir(parents=True) monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) # Create some checkpoint files cp1 = checkpoint_dir / "checkpoint_Project1_20240115_100000.ppz" cp2 = checkpoint_dir / "checkpoint_Project2_20240115_110000.ppz" cp1.touch() cp2.touch() # Create metadata for first checkpoint metadata1 = {"project_name": "Project1", "timestamp": "2024-01-15T10:00:00"} with open(cp1.with_suffix(".json"), "w") as f: json.dump(metadata1, f) manager = AutosaveManager() checkpoints = manager.list_checkpoints() assert len(checkpoints) == 2 def test_list_checkpoints_filter_by_project(self, tmp_path, monkeypatch): """Test listing checkpoints filtered by project name""" checkpoint_dir = tmp_path / "checkpoints" checkpoint_dir.mkdir(parents=True) monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) # Create checkpoint files with metadata cp1 = checkpoint_dir / "checkpoint_Project1_20240115_100000.ppz" cp2 = checkpoint_dir / "checkpoint_Project2_20240115_110000.ppz" cp1.touch() cp2.touch() metadata1 = {"project_name": "Project1", "timestamp": "2024-01-15T10:00:00"} metadata2 = {"project_name": "Project2", "timestamp": "2024-01-15T11:00:00"} with open(cp1.with_suffix(".json"), "w") as f: json.dump(metadata1, f) with open(cp2.with_suffix(".json"), "w") as f: json.dump(metadata2, f) manager = AutosaveManager() checkpoints = manager.list_checkpoints("Project1") assert len(checkpoints) == 1 assert checkpoints[0][1]["project_name"] == "Project1" def test_list_checkpoints_sorted_by_timestamp(self, tmp_path, monkeypatch): """Test that checkpoints are sorted by timestamp (newest first)""" checkpoint_dir = tmp_path / "checkpoints" checkpoint_dir.mkdir(parents=True) monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) # Create checkpoints with different timestamps cp1 = checkpoint_dir / "checkpoint_Project_20240115_080000.ppz" cp2 = checkpoint_dir / "checkpoint_Project_20240115_120000.ppz" cp3 = checkpoint_dir / "checkpoint_Project_20240115_100000.ppz" cp1.touch() cp2.touch() cp3.touch() for cp, hour in [(cp1, "08"), (cp2, "12"), (cp3, "10")]: metadata = {"project_name": "Project", "timestamp": f"2024-01-15T{hour}:00:00"} with open(cp.with_suffix(".json"), "w") as f: json.dump(metadata, f) manager = AutosaveManager() checkpoints = manager.list_checkpoints() # Should be sorted newest first: 12:00, 10:00, 08:00 assert "12:00:00" in checkpoints[0][1]["timestamp"] assert "10:00:00" in checkpoints[1][1]["timestamp"] assert "08:00:00" in checkpoints[2][1]["timestamp"] class TestLoadCheckpoint: """Tests for load_checkpoint method""" def test_load_checkpoint_success(self, tmp_path, monkeypatch): """Test successful checkpoint loading""" checkpoint_dir = tmp_path / "checkpoints" monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() with patch("pyPhotoAlbum.autosave_manager.load_from_zip") as mock_load: mock_project = Mock() mock_load.return_value = mock_project checkpoint_path = checkpoint_dir / "checkpoint_Test.ppz" success, result = manager.load_checkpoint(checkpoint_path) assert success is True assert result == mock_project def test_load_checkpoint_failure(self, tmp_path, monkeypatch): """Test checkpoint loading failure""" checkpoint_dir = tmp_path / "checkpoints" monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() with patch("pyPhotoAlbum.autosave_manager.load_from_zip") as mock_load: mock_load.side_effect = Exception("Corrupt file") checkpoint_path = checkpoint_dir / "checkpoint_Test.ppz" success, result = manager.load_checkpoint(checkpoint_path) assert success is False assert "Failed to load checkpoint" in result class TestDeleteCheckpoint: """Tests for delete_checkpoint method""" def test_delete_checkpoint_success(self, tmp_path, monkeypatch): """Test successful checkpoint deletion""" checkpoint_dir = tmp_path / "checkpoints" checkpoint_dir.mkdir(parents=True) monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) # Create checkpoint and metadata files cp = checkpoint_dir / "checkpoint_Test.ppz" cp.touch() metadata = cp.with_suffix(".json") metadata.touch() manager = AutosaveManager() result = manager.delete_checkpoint(cp) assert result is True assert not cp.exists() assert not metadata.exists() def test_delete_checkpoint_nonexistent(self, tmp_path, monkeypatch): """Test deleting nonexistent checkpoint""" checkpoint_dir = tmp_path / "checkpoints" checkpoint_dir.mkdir(parents=True) monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() cp = checkpoint_dir / "nonexistent.ppz" result = manager.delete_checkpoint(cp) assert result is True # Should succeed even if file doesn't exist class TestDeleteAllCheckpoints: """Tests for delete_all_checkpoints method""" def test_delete_all_checkpoints(self, tmp_path, monkeypatch): """Test deleting all checkpoints""" checkpoint_dir = tmp_path / "checkpoints" checkpoint_dir.mkdir(parents=True) monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) # Create multiple checkpoints for i in range(3): cp = checkpoint_dir / f"checkpoint_Project_{i}.ppz" cp.touch() metadata = {"project_name": "Project", "timestamp": f"2024-01-15T{i}:00:00"} with open(cp.with_suffix(".json"), "w") as f: json.dump(metadata, f) manager = AutosaveManager() manager.delete_all_checkpoints() remaining = list(checkpoint_dir.glob("checkpoint_*.ppz")) assert len(remaining) == 0 def test_delete_all_checkpoints_filtered(self, tmp_path, monkeypatch): """Test deleting all checkpoints for specific project""" checkpoint_dir = tmp_path / "checkpoints" checkpoint_dir.mkdir(parents=True) monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) # Create checkpoints for different projects for name in ["ProjectA", "ProjectB", "ProjectA"]: cp = checkpoint_dir / f"checkpoint_{name}_{datetime.now().strftime('%Y%m%d_%H%M%S%f')}.ppz" cp.touch() metadata = {"project_name": name, "timestamp": datetime.now().isoformat()} with open(cp.with_suffix(".json"), "w") as f: json.dump(metadata, f) manager = AutosaveManager() manager.delete_all_checkpoints("ProjectA") # Only ProjectB should remain remaining = list(checkpoint_dir.glob("checkpoint_*.ppz")) assert len(remaining) == 1 assert "ProjectB" in remaining[0].name class TestCleanupOldCheckpoints: """Tests for cleanup_old_checkpoints method""" def test_cleanup_old_checkpoints_by_age(self, tmp_path, monkeypatch): """Test cleanup of old checkpoints by age""" checkpoint_dir = tmp_path / "checkpoints" checkpoint_dir.mkdir(parents=True) monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) # Create old and new checkpoints old_time = datetime.now() - timedelta(hours=48) new_time = datetime.now() - timedelta(hours=1) old_cp = checkpoint_dir / "checkpoint_Project_old.ppz" new_cp = checkpoint_dir / "checkpoint_Project_new.ppz" old_cp.touch() new_cp.touch() old_metadata = {"project_name": "Project", "timestamp": old_time.isoformat()} new_metadata = {"project_name": "Project", "timestamp": new_time.isoformat()} with open(old_cp.with_suffix(".json"), "w") as f: json.dump(old_metadata, f) with open(new_cp.with_suffix(".json"), "w") as f: json.dump(new_metadata, f) manager = AutosaveManager() manager.cleanup_old_checkpoints(max_age_hours=24) # Only new checkpoint should remain remaining = list(checkpoint_dir.glob("checkpoint_*.ppz")) assert len(remaining) == 1 assert "new" in remaining[0].name def test_cleanup_old_checkpoints_by_count(self, tmp_path, monkeypatch): """Test cleanup of checkpoints by count""" checkpoint_dir = tmp_path / "checkpoints" checkpoint_dir.mkdir(parents=True) monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) # Create many recent checkpoints for i in range(5): timestamp = datetime.now() - timedelta(hours=i) cp = checkpoint_dir / f"checkpoint_Project_{i:02d}.ppz" cp.touch() metadata = {"project_name": "Project", "timestamp": timestamp.isoformat()} with open(cp.with_suffix(".json"), "w") as f: json.dump(metadata, f) manager = AutosaveManager() manager.cleanup_old_checkpoints(max_age_hours=24 * 7, max_count=3) # Should only keep 3 most recent remaining = list(checkpoint_dir.glob("checkpoint_*.ppz")) assert len(remaining) == 3 class TestHasCheckpoints: """Tests for has_checkpoints method""" def test_has_checkpoints_true(self, tmp_path, monkeypatch): """Test has_checkpoints returns True when checkpoints exist""" checkpoint_dir = tmp_path / "checkpoints" checkpoint_dir.mkdir(parents=True) monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) cp = checkpoint_dir / "checkpoint_Test.ppz" cp.touch() manager = AutosaveManager() assert manager.has_checkpoints() is True def test_has_checkpoints_false(self, tmp_path, monkeypatch): """Test has_checkpoints returns False when no checkpoints""" checkpoint_dir = tmp_path / "checkpoints" monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() assert manager.has_checkpoints() is False class TestGetLatestCheckpoint: """Tests for get_latest_checkpoint method""" def test_get_latest_checkpoint(self, tmp_path, monkeypatch): """Test getting the latest checkpoint""" checkpoint_dir = tmp_path / "checkpoints" checkpoint_dir.mkdir(parents=True) monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) # Create checkpoints with different timestamps for hour in [8, 10, 12]: cp = checkpoint_dir / f"checkpoint_Project_{hour:02d}.ppz" cp.touch() metadata = {"project_name": "Project", "timestamp": f"2024-01-15T{hour:02d}:00:00"} with open(cp.with_suffix(".json"), "w") as f: json.dump(metadata, f) manager = AutosaveManager() result = manager.get_latest_checkpoint() assert result is not None assert "12:00:00" in result[1]["timestamp"] def test_get_latest_checkpoint_none(self, tmp_path, monkeypatch): """Test getting latest checkpoint when none exist""" checkpoint_dir = tmp_path / "checkpoints" monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) manager = AutosaveManager() result = manager.get_latest_checkpoint() assert result is None def test_get_latest_checkpoint_filtered(self, tmp_path, monkeypatch): """Test getting latest checkpoint for specific project""" checkpoint_dir = tmp_path / "checkpoints" checkpoint_dir.mkdir(parents=True) monkeypatch.setattr(AutosaveManager, "CHECKPOINT_DIR", checkpoint_dir) # Create checkpoints for different projects for name, hour in [("ProjectA", 10), ("ProjectB", 12), ("ProjectA", 8)]: cp = checkpoint_dir / f"checkpoint_{name}_{hour:02d}.ppz" cp.touch() metadata = {"project_name": name, "timestamp": f"2024-01-15T{hour:02d}:00:00"} with open(cp.with_suffix(".json"), "w") as f: json.dump(metadata, f) manager = AutosaveManager() result = manager.get_latest_checkpoint("ProjectA") assert result is not None assert result[1]["project_name"] == "ProjectA" assert "10:00:00" in result[1]["timestamp"] # Latest for ProjectA