import json import os from pathlib import Path from unittest.mock import Mock import pytest import src.presets.storage_system as storage_system class MockPresetItem: """Simple container for preset collection entries.""" def __init__(self): self.name = "" self.source = "" class MockPresetCollection: """Minimal stand-in for Blender's CollectionProperty.""" def __init__(self): self._items = [] self.cleared = False def clear(self): self.cleared = True self._items.clear() def add(self): item = MockPresetItem() self._items.append(item) return item def remove(self, index): self._items.pop(index) def __iter__(self): return iter(self._items) def __len__(self): return len(self._items) class MockProps: """Mock text texture properties containing preset collection.""" def __init__(self): self.presets = MockPresetCollection() self.save_with_blend_file = False self.new_preset_name = "" self.active_preset_index = 0 self.preset_search_query = "" self.last_loaded_preset = "" class MockScene(dict): """Dictionary-like scene object supporting custom properties.""" def __init__(self, props=None): super().__init__() self.text_texture_props = props def __bool__(self): # Scene should be truthy even when no custom properties are set return True @pytest.fixture def make_scene(mock_bpy_fixture): """Factory for assigning a mock scene to bpy.context.""" def _make_scene(props=None): scene = MockScene(props) mock_bpy_fixture.context.scene = scene return scene return _make_scene def test_get_persistent_preset_path_creates_directory(tmp_path, mock_bpy_fixture): config_dir = tmp_path / "config" config_dir.mkdir() mock_bpy_fixture.utils.user_resource.return_value = str(config_dir) path = storage_system.get_persistent_preset_path() expected = config_dir / "text_texture_generator" / "presets" assert Path(path) == expected assert expected.is_dir() def test_save_preset_unified_persistent_writes_file(tmp_path, monkeypatch): persistent_dir = tmp_path / "persistent" persistent_dir.mkdir() monkeypatch.setattr(storage_system, "get_persistent_preset_path", lambda: str(persistent_dir)) preset_data = {"text": "hello", "font_size": 24} result = storage_system.save_preset_unified(preset_data, "SamplePreset", save_to_blend_file=False) saved_path = persistent_dir / "SamplePreset.json" assert result is True assert saved_path.exists() with saved_path.open("r") as handle: assert json.load(handle) == preset_data def test_save_preset_unified_blend_delegates(monkeypatch): mock_save = Mock(return_value=True) monkeypatch.setattr(storage_system, "save_blend_file_preset", mock_save) payload = {"text": "blend storage"} result = storage_system.save_preset_unified(payload, "BlendPreset", save_to_blend_file=True) assert result is True mock_save.assert_called_once_with("BlendPreset", payload) def test_get_blend_file_presets_initializes_empty_collection(make_scene): scene = make_scene() presets = storage_system.get_blend_file_presets() assert presets == {} assert scene["text_texture_presets"] == "{}" def test_save_blend_file_preset_stores_json_in_scene(make_scene): scene = make_scene() preset_content = {"text": "stored", "font_size": 18} result = storage_system.save_blend_file_preset("StoredPreset", preset_content) assert result is True stored = json.loads(scene["text_texture_presets"]) assert stored["StoredPreset"] == preset_content roundtrip = storage_system.get_blend_file_presets() assert roundtrip["StoredPreset"] == preset_content def test_delete_blend_file_preset_removes_entry(make_scene): scene = make_scene() storage_system.save_blend_file_preset("TempPreset", {"value": 1}) removed = storage_system.delete_blend_file_preset("TempPreset") assert removed is True assert json.loads(scene["text_texture_presets"]) == {} removed_again = storage_system.delete_blend_file_preset("TempPreset") assert removed_again is False def test_get_unified_preset_list_prioritizes_blend_presets(tmp_path, monkeypatch): persistent_dir = tmp_path / "persistent" persistent_dir.mkdir() monkeypatch.setattr(storage_system, "get_persistent_preset_path", lambda: str(persistent_dir)) with (persistent_dir / "alpha.json").open("w") as handle: json.dump({"source": "persistent"}, handle) with (persistent_dir / "shared.json").open("w") as handle: json.dump({"source": "persistent-shared"}, handle) monkeypatch.setattr( storage_system, "get_blend_file_presets", lambda: { "shared": {"source": "blend"}, "blend_only": {"source": "blend-only"}, }, ) presets = storage_system.get_unified_preset_list() assert presets["alpha"][1] == "PERSISTENT_FILE" assert presets["alpha"][0]["source"] == "persistent" assert presets["shared"][1] == "BLEND_FILE" assert presets["shared"][0]["source"] == "blend" assert presets["blend_only"][1] == "BLEND_FILE" def test_migrate_from_legacy_system_migrates_files_and_creates_backup(tmp_path, monkeypatch): legacy_dir = tmp_path / "legacy" persistent_dir = tmp_path / "persistent" legacy_dir.mkdir() persistent_dir.mkdir() legacy_payload = {"example": True} with (legacy_dir / "legacy_preset.json").open("w") as handle: json.dump(legacy_payload, handle) with (persistent_dir / "existing.json").open("w") as handle: json.dump({"existing": True}, handle) monkeypatch.setattr(storage_system, "get_preset_path", lambda: str(legacy_dir)) monkeypatch.setattr(storage_system, "get_persistent_preset_path", lambda: str(persistent_dir)) result = storage_system.migrate_from_legacy_system() migrated_file = persistent_dir / "legacy_preset.json" backup_file = persistent_dir / "legacy_backup" / "legacy_preset.json" marker_file = persistent_dir / ".migration_complete" assert result is True assert migrated_file.exists() assert json.loads(migrated_file.read_text()) == legacy_payload assert backup_file.exists() assert marker_file.exists() def test_initialize_presets_populates_props_collection(make_scene, monkeypatch): props = MockProps() # Seed with existing entry to ensure clear() is invoked props.presets.add().name = "stale" migrate_mock = Mock(return_value=True) monkeypatch.setattr(storage_system, "migrate_from_legacy_system", migrate_mock) monkeypatch.setattr( storage_system, "get_unified_preset_list", lambda: { "BlendPreset": ({"value": "blend"}, "BLEND_FILE"), "PersistentPreset": ({"value": "persistent"}, "PERSISTENT_FILE"), }, ) scene = make_scene(props) storage_system.initialize_presets() assert migrate_mock.called assert props.presets.cleared is True assert len(props.presets) == 2 entries = {(item.name, item.source) for item in props.presets} assert ("BlendPreset", "BLEND_FILE") in entries assert ("PersistentPreset", "PERSISTENT_FILE") in entries assert "text_texture_presets" not in scene # ensure scene custom data untouched def test_ensure_presets_available_refreshes_when_empty(make_scene, monkeypatch): props = MockProps() make_scene(props) refresh_mock = Mock(return_value=True) monkeypatch.setattr(storage_system, "refresh_presets_sync", refresh_mock) result = storage_system.ensure_presets_available() assert result is True refresh_mock.assert_called_once() def test_ensure_presets_available_returns_true_when_presets_loaded(make_scene, monkeypatch): props = MockProps() props.presets.add().name = "cached" make_scene(props) refresh_mock = Mock(return_value=True) monkeypatch.setattr(storage_system, "refresh_presets_sync", refresh_mock) result = storage_system.ensure_presets_available() assert result is True refresh_mock.assert_not_called() def test_refresh_presets_sync_returns_false_without_scene(mock_bpy_fixture): mock_bpy_fixture.context.scene = None assert storage_system.refresh_presets_sync() is False