diff --git a/debug_al.py b/debug_al.py index 02cfc88..0fc7f55 100644 --- a/debug_al.py +++ b/debug_al.py @@ -1,21 +1,22 @@ - import numpy as np from PIL import Image from faststack.imaging.editor import ImageEditor + def debug_run(): editor = ImageEditor() w, h = 200, 200 arr = np.zeros((h, w, 3), dtype=np.uint8) arr[:] = 200 arr[0, 0, 0] = 255 - - img = Image.fromarray(arr, 'RGB') + + img = Image.fromarray(arr, "RGB") editor.original_image = img editor._preview_image = img - + blacks, whites, p_low, p_high = editor.auto_levels(threshold_percent=0.1) print(f"RESULT: p_high={p_high}") + if __name__ == "__main__": debug_run() diff --git a/faststack/app.py b/faststack/app.py index 7beef43..051d2c0 100644 --- a/faststack/app.py +++ b/faststack/app.py @@ -41,7 +41,7 @@ QPoint, QCoreApplication, ) -from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox +from PySide6.QtWidgets import QApplication, QFileDialog from PySide6.QtQml import QQmlApplicationEngine from PIL import Image diff --git a/faststack/io/deletion.py b/faststack/io/deletion.py index 972eb4b..9ccb599 100644 --- a/faststack/io/deletion.py +++ b/faststack/io/deletion.py @@ -6,6 +6,17 @@ log = logging.getLogger(__name__) + +def _mkdir(path: Path) -> None: + """Helper for mocking Path.mkdir safely.""" + path.mkdir(parents=True, exist_ok=True) + + +def _unlink(path: Path) -> None: + """Helper for mocking Path.unlink safely.""" + path.unlink() + + def ensure_recycle_bin_dir(recycle_bin_dir: Path) -> bool: """Try to create the recycle bin directory. @@ -14,12 +25,13 @@ def ensure_recycle_bin_dir(recycle_bin_dir: Path) -> bool: False if creation failed (e.g., permission denied). """ try: - recycle_bin_dir.mkdir(parents=True, exist_ok=True) + _mkdir(recycle_bin_dir) return True except (PermissionError, OSError) as e: - log.error("Failed to create recycle bin directory: %s", e) + log.exception("Failed to create recycle bin directory: %s", e) return False + def confirm_permanent_delete(image_file, reason: str = "") -> bool: """Show a confirmation dialog for permanent deletion of a single image. @@ -34,10 +46,22 @@ def confirm_permanent_delete(image_file, reason: str = "") -> bool: raw_path = image_file.raw_pair # Build list of files that will be deleted - files_to_delete = [str(jpg_path.name)] + files_to_delete = [] + + # Handle primary JPG + if jpg_path: + files_to_delete.append(str(jpg_path.name)) + else: + log.warning("confirm_permanent_delete called with image_file.path=None") + + # Handle RAW pair if raw_path and raw_path.exists(): files_to_delete.append(str(raw_path.name)) + if not files_to_delete: + log.warning("No files to delete found for confirmation.") + return False + file_list = "\n".join(f" • {f}" for f in files_to_delete) msg_box = QMessageBox() @@ -53,9 +77,7 @@ def confirm_permanent_delete(image_file, reason: str = "") -> bool: f"The following files will be permanently deleted:\n{file_list}" ) - delete_btn = msg_box.addButton( - "Delete Permanently", QMessageBox.DestructiveRole - ) + delete_btn = msg_box.addButton("Delete Permanently", QMessageBox.DestructiveRole) cancel_btn = msg_box.addButton("Cancel", QMessageBox.RejectRole) msg_box.setDefaultButton(cancel_btn) @@ -63,6 +85,7 @@ def confirm_permanent_delete(image_file, reason: str = "") -> bool: return msg_box.clickedButton() == delete_btn + def confirm_batch_permanent_delete(images: list, reason: str = "") -> bool: """Show a confirmation dialog for permanent deletion of multiple images. @@ -117,6 +140,7 @@ def confirm_batch_permanent_delete(images: list, reason: str = "") -> bool: return msg_box.clickedButton() == delete_btn + def permanently_delete_image_files(image_file) -> bool: """Permanently delete an image and its RAW pair from disk. @@ -135,19 +159,19 @@ def permanently_delete_image_files(image_file) -> bool: # Delete JPG if jpg_path and jpg_path.exists(): try: - jpg_path.unlink() + _unlink(jpg_path) log.info("Permanently deleted: %s", jpg_path.name) deleted_any = True except OSError as e: - log.error("Failed to permanently delete %s: %s", jpg_path.name, e) + log.exception("Failed to permanently delete %s: %s", jpg_path.name, e) # Delete RAW if exists if raw_path and raw_path.exists(): try: - raw_path.unlink() + _unlink(raw_path) log.info("Permanently deleted: %s", raw_path.name) deleted_any = True except OSError as e: - log.error("Failed to permanently delete %s: %s", raw_path.name, e) + log.exception("Failed to permanently delete %s: %s", raw_path.name, e) return deleted_any diff --git a/faststack/io/watcher.py b/faststack/io/watcher.py index 204acb0..464914f 100644 --- a/faststack/io/watcher.py +++ b/faststack/io/watcher.py @@ -48,7 +48,7 @@ def __init__(self, directory: Path, callback): self.observer: Optional[Observer] = None # Initialize to None self.event_handler = ImageDirectoryEventHandler(callback) self.directory = directory - self.callback = callback # Store callback for new observer + self.callback = callback def start(self): """Starts watching the directory.""" diff --git a/faststack/tests/check_imports.py b/faststack/tests/check_imports.py index 3204e45..7e27361 100644 --- a/faststack/tests/check_imports.py +++ b/faststack/tests/check_imports.py @@ -1,5 +1,6 @@ import sys import os +import traceback # Add current directory to path sys.path.append(os.getcwd()) @@ -11,12 +12,10 @@ print("Success faststack.app") except ImportError as e: print(f"ImportError faststack.app: {e}") - import traceback traceback.print_exc() except Exception as e: print(f"Non-ImportError during import of faststack.app: {e}") - import traceback traceback.print_exc() @@ -27,11 +26,9 @@ print("Success test_raw_pipeline") except ImportError as e: print(f"ImportError test_raw_pipeline: {e}") - import traceback traceback.print_exc() except Exception as e: print(f"Non-ImportError during import of test_raw_pipeline: {e}") - import traceback traceback.print_exc() diff --git a/faststack/tests/test_config_setters.py b/faststack/tests/test_config_setters.py index 6bd969f..0f9a469 100644 --- a/faststack/tests/test_config_setters.py +++ b/faststack/tests/test_config_setters.py @@ -51,7 +51,7 @@ def decorator(func): sys.modules["PIL.Image"] = mock_pil.Image # Mock numpy -sys.modules["numpy"] = MagicMock() +#sys.modules["numpy"] = MagicMock() # Mock faststack.config mock_config_module = MagicMock() diff --git a/faststack/tests/test_permanent_delete.py b/faststack/tests/test_permanent_delete.py index ff4796c..85203bb 100644 --- a/faststack/tests/test_permanent_delete.py +++ b/faststack/tests/test_permanent_delete.py @@ -1,6 +1,5 @@ """Tests for permanent delete logic in faststack.io.deletion.""" -import pytest from pathlib import Path from unittest.mock import Mock, patch @@ -8,24 +7,27 @@ from faststack.io.deletion import ( ensure_recycle_bin_dir, confirm_permanent_delete, - permanently_delete_image_files + permanently_delete_image_files, ) + class MockImageFile: """Simple mock for ImageFile.""" - def __init__(self, jpg_path: Path, raw_path: Path = None): + + def __init__(self, jpg_path: Path | None, raw_path: Path | None = None): self.path = jpg_path self.raw_pair = raw_path self.is_video = False + class TestEnsureRecycleBinDir: def test_creation_success(self, tmp_path): """Should return True and create directory when successful.""" recycle_bin = tmp_path / "RecycleBin" assert not recycle_bin.exists() - + result = ensure_recycle_bin_dir(recycle_bin) - + assert result is True assert recycle_bin.exists() assert recycle_bin.is_dir() @@ -33,45 +35,60 @@ def test_creation_success(self, tmp_path): def test_creation_failure(self, tmp_path): """Should return False when creation raises PermissionError.""" recycle_bin = tmp_path / "RecycleBin" - - with patch.object(Path, "mkdir", side_effect=PermissionError("Mock perm error")): + + with patch( + "faststack.io.deletion._mkdir", + side_effect=PermissionError("Mock perm error"), + ): result = ensure_recycle_bin_dir(recycle_bin) assert result is False + class TestConfirmPermanentDelete: def test_confirm_yes(self): """Should return True when user accepts dialog.""" mock_img = MockImageFile(Path("test.jpg")) - + with patch("faststack.io.deletion.QMessageBox") as MockMSG: instance = MockMSG.return_value - instance.exec.return_value = 0 - + instance.exec.return_value = 0 + mock_delete_btn = Mock(name="DeleteButton") mock_cancel_btn = Mock(name="CancelButton") - + instance.addButton.side_effect = [mock_delete_btn, mock_cancel_btn] instance.clickedButton.return_value = mock_delete_btn - + result = confirm_permanent_delete(mock_img) assert result is True def test_confirm_no(self): """Should return False when user cancels.""" mock_img = MockImageFile(Path("test.jpg")) - + with patch("faststack.io.deletion.QMessageBox") as MockMSG: instance = MockMSG.return_value instance.exec.return_value = 0 - + mock_delete_btn = Mock(name="DeleteButton") mock_cancel_btn = Mock(name="CancelButton") - + instance.addButton.side_effect = [mock_delete_btn, mock_cancel_btn] instance.clickedButton.return_value = mock_cancel_btn - + + result = confirm_permanent_delete(mock_img) + assert result is False + + def test_confirm_handles_none_path(self): + """Should return False (and log warning) if image_file.path is None.""" + mock_img = MockImageFile(None) + + with patch("faststack.io.deletion.log") as mock_log: + # Should return False because files_to_delete will be empty result = confirm_permanent_delete(mock_img) assert result is False + assert mock_log.warning.call_count >= 1 + class TestPermanentlyDeleteImageFiles: def test_delete_success(self, tmp_path): @@ -80,11 +97,11 @@ def test_delete_success(self, tmp_path): raw = tmp_path / "img.orf" jpg.touch() raw.touch() - + img = MockImageFile(jpg, raw) - + result = permanently_delete_image_files(img) - + assert result is True assert not jpg.exists() assert not raw.exists() @@ -94,9 +111,9 @@ def test_delete_jpg_only(self, tmp_path): jpg = tmp_path / "img.jpg" jpg.touch() img = MockImageFile(jpg, None) - + result = permanently_delete_image_files(img) - + assert result is True assert not jpg.exists() @@ -104,9 +121,9 @@ def test_delete_handles_missing_files(self, tmp_path): """Should return False if files don't exist.""" jpg = tmp_path / "missing.jpg" img = MockImageFile(jpg, None) - + result = permanently_delete_image_files(img) - + assert result is False def test_delete_failure_logging(self, tmp_path): @@ -114,11 +131,12 @@ def test_delete_failure_logging(self, tmp_path): jpg = tmp_path / "protected.jpg" jpg.touch() img = MockImageFile(jpg, None) - - with patch.object(Path, "unlink", side_effect=OSError("Protected")): + + # Patch the localized helper instead of Path.unlink + with patch("faststack.io.deletion._unlink", side_effect=OSError("Protected")): with patch("faststack.io.deletion.log") as mock_log: result = permanently_delete_image_files(img) - + assert result is False assert jpg.exists() - mock_log.error.assert_called() + mock_log.exception.assert_called() diff --git a/faststack/ui/provider.py b/faststack/ui/provider.py index 0781e53..d0fc8b5 100644 --- a/faststack/ui/provider.py +++ b/faststack/ui/provider.py @@ -26,6 +26,7 @@ class ImageProvider(QQuickImageProvider): def __init__(self, app_controller): super().__init__(QQuickImageProvider.ImageType.Image) self.app_controller = app_controller + self._app_controller = app_controller # Backward compatibility alias self.placeholder = QImage(256, 256, QImage.Format.Format_RGB888) self.placeholder.fill(Qt.GlobalColor.darkGray) # Keepalive queue to prevent GC of buffers currently in use by QImage @@ -137,7 +138,6 @@ class UIState(QObject): metadataChanged = Signal() themeChanged = Signal() preloadingStateChanged = Signal() - preloadingStateChanged = Signal() preloadProgressChanged = Signal() # Recycle Bin Signals @@ -214,6 +214,7 @@ class UIState(QObject): def __init__(self, app_controller): super().__init__() self.app_controller = app_controller + self._app_controller = app_controller # Backward compatibility alias self._is_preloading = False self._preload_progress = 0 # 1 = light, 0 = dark (controller will overwrite this on startup) diff --git a/inspect_app.py b/inspect_app.py index 4622bfc..a3046bc 100644 --- a/inspect_app.py +++ b/inspect_app.py @@ -1,4 +1,3 @@ - from faststack.app import AppController import inspect @@ -6,7 +5,7 @@ print("Methods found:") found = False for name, _ in methods: - if 'auto_level' in name: + if "auto_level" in name: print(f" {name}") found = True diff --git a/repro_crash.py b/repro_crash.py index bb70c5f..e59f7a9 100644 --- a/repro_crash.py +++ b/repro_crash.py @@ -5,36 +5,40 @@ # Mock modules before importing editor # Note: These mocks remain in sys.modules for the test to use -sys.modules['cv2'] = MagicMock() -sys.modules['PIL'] = MagicMock() -sys.modules['PySide6.QtGui'] = MagicMock() +sys.modules["cv2"] = MagicMock() +sys.modules["PIL"] = MagicMock() +sys.modules["PySide6.QtGui"] = MagicMock() # Now import the class from faststack.imaging.editor import ImageEditor + class TestCrash(unittest.TestCase): def test_imread_none_crash(self): """ Simulate cv2.imread returning None and see if it crashes. """ editor = ImageEditor() - editor.original_image = MagicMock() # Pillow image mock - editor.original_image.convert.return_value = np.zeros((100, 100, 3), dtype=np.uint8) - + editor.original_image = MagicMock() # Pillow image mock + editor.original_image.convert.return_value = np.zeros( + (100, 100, 3), dtype=np.uint8 + ) + # Mock cv2.imread to return None - sys.modules['cv2'].imread.return_value = None - sys.modules['cv2'].IMREAD_UNCHANGED = -1 - - # Path must exist for the check at the start of load_image, + sys.modules["cv2"].imread.return_value = None + sys.modules["cv2"].IMREAD_UNCHANGED = -1 + + # Path must exist for the check at the start of load_image, # or we mock Path.exists - with patch('pathlib.Path.exists', return_value=True): - try: - print("Attempting to load image with mocks...") - success = editor.load_image("dummy_path.jpg") - print(f"Load result: {success}") - except Exception as e: - print(f"CRASHED: {e}") - raise e - -if __name__ == '__main__': + with patch("pathlib.Path.exists", return_value=True): + try: + print("Attempting to load image with mocks...") + success = editor.load_image("dummy_path.jpg") + print(f"Load result: {success}") + except Exception as e: + print(f"CRASHED: {e}") + raise e + + +if __name__ == "__main__": unittest.main() diff --git a/reproduce_bug.py b/reproduce_bug.py index a1e7546..f228544 100644 --- a/reproduce_bug.py +++ b/reproduce_bug.py @@ -1,10 +1,10 @@ - import os import time import shutil from pathlib import Path from faststack.io.indexer import find_images + def test_refresh_logic(): # Setup test dir test_dir = Path("./test_images_refresh") @@ -15,7 +15,7 @@ def test_refresh_logic(): # Create main image img_path = test_dir / "test.jpg" img_path.touch() - + # Set mtime to T0 t0 = time.time() - 100 os.utime(img_path, (t0, t0)) @@ -23,7 +23,7 @@ def test_refresh_logic(): # Initial Scan images = find_images(test_dir) print(f"Initial images: {[i.path.name for i in images]}") - + current_index = 0 original_path = images[current_index].path print(f"Current selection: {original_path.name} (Index {current_index})") @@ -32,36 +32,39 @@ def test_refresh_logic(): # 1. Create Backup (preserves mtime T0) backup_path = test_dir / "test-backup.jpg" shutil.copy2(img_path, backup_path) - + # 2. Save Main (update mtime to T1) t1 = time.time() - img_path.touch() # Updates mtime - + img_path.touch() # Updates mtime + # Refresh images = find_images(test_dir) - print(f"Refreshed images: {[i.path.name for i in images]}") + print(f"Refreshed images: {[i.path.name for i in images]}") # Expect: [test-backup.jpg, test.jpg] due to T0 < T1 - + # Selection Logic new_index = -1 for i, img_file in enumerate(images): if img_file.path == original_path: new_index = i break - + print(f"Old Index: {current_index}") print(f"New Index found: {new_index}") - + if new_index == -1: print("FAIL: Did not find original path in refreshed list.") # If we failed to find, current_index stays 0 # Index 0 is now 'test-backup.jpg' - print(f"Effective selection would remain index {current_index}: {images[current_index].path.name}") + print( + f"Effective selection would remain index {current_index}: {images[current_index].path.name}" + ) else: print(f"Selected: {images[new_index].path.name} (Index {new_index})") # Cleanup shutil.rmtree(test_dir) + if __name__ == "__main__": test_refresh_logic() diff --git a/reproduce_bug_case.py b/reproduce_bug_case.py index e4f280c..1e6d353 100644 --- a/reproduce_bug_case.py +++ b/reproduce_bug_case.py @@ -1,17 +1,18 @@ - from pathlib import Path + def test_path_equality(): p1 = Path("c:/code/faststack/test.jpg") p2 = Path("C:/code/faststack/test.jpg") - + print(f"p1: {p1}") print(f"p2: {p2}") print(f"p1 == p2: {p1 == p2}") - + p3 = Path("c:\\code\\faststack\\test.jpg") print(f"p3: {p3}") print(f"p1 == p3: {p1 == p3}") + if __name__ == "__main__": test_path_equality() diff --git a/reproduce_config_issue.py b/reproduce_config_issue.py index 7608c00..c403b72 100644 --- a/reproduce_config_issue.py +++ b/reproduce_config_issue.py @@ -1,6 +1,4 @@ - import sys -import os from pathlib import Path import configparser @@ -11,54 +9,56 @@ import faststack.logging_setup import faststack.config + def test_config_persistence(): print("Testing config persistence...") - + # Use a temporary file for testing test_config_dir = Path("c:/code/faststack/test_config_dir") test_config_dir.mkdir(exist_ok=True) - + # Monkeypatch get_app_data_dir to use local dir faststack.config.get_app_data_dir = lambda: test_config_dir - + # 1. Initialize config (should create defaults) app_config = faststack.config.AppConfig() print(f"Config path: {app_config.config_path}") - + # Verify default - initial_val = app_config.get('core', 'auto_level_threshold') + initial_val = app_config.get("core", "auto_level_threshold") print(f"Initial value: {initial_val}") if initial_val != "0.1": print("FAIL: Default value unexpected") - + # 2. Modify value new_val = "0.05" print(f"Setting value to: {new_val}") - app_config.set('core', 'auto_level_threshold', new_val) + app_config.set("core", "auto_level_threshold", new_val) app_config.save() - + # 3. Reload config from disk directly to verify file content raw_config = configparser.ConfigParser() raw_config.read(app_config.config_path) - file_val = raw_config.get('core', 'auto_level_threshold') + file_val = raw_config.get("core", "auto_level_threshold") print(f"Value in file: {file_val}") # 4. Re-initialize AppConfig (simulate app restart) # We must clear the global instance or create a new one to force reload # AppConfig.__init__ calls self.load() app_config_2 = faststack.config.AppConfig() - loaded_val = app_config_2.get('core', 'auto_level_threshold') + loaded_val = app_config_2.get("core", "auto_level_threshold") print(f"Loaded value: {loaded_val}") - + if loaded_val == new_val: print("SUCCESS: Value persisted correctly") else: print(f"FAIL: Value did not persist. Got {loaded_val}, expected {new_val}") - + # Clean up if (test_config_dir / "faststack.ini").exists(): (test_config_dir / "faststack.ini").unlink() test_config_dir.rmdir() + if __name__ == "__main__": test_config_persistence() diff --git a/reproduce_issue.py b/reproduce_issue.py index 9198551..8e9fdc1 100644 --- a/reproduce_issue.py +++ b/reproduce_issue.py @@ -1,28 +1,29 @@ -import os import pathlib -import sys + def reproduction_step(): base_dir = pathlib.Path("test_deletion_repro") base_dir.mkdir(exist_ok=True) - + recycle_bin = base_dir / "recycle_bin" recycle_bin.mkdir(exist_ok=True) - + file_name = "test_image.jpg" source_file = base_dir / file_name dest_file = recycle_bin / file_name - + # Clean up previous run - if source_file.exists(): source_file.unlink() - if dest_file.exists(): dest_file.unlink() - + if source_file.exists(): + source_file.unlink() + if dest_file.exists(): + dest_file.unlink() + # 1. Simulate state: File exists in BOTH source and recycle bin source_file.touch() dest_file.touch() - + print(f"Created {source_file} and {dest_file}") - + # 2. Try rename (Current Code) try: print("Attempting rename (should fail on Windows)...") @@ -32,11 +33,13 @@ def reproduction_step(): print("CAUGHT EXPECTED ERROR: FileExistsError during rename") except OSError as e: print(f"CAUGHT OTHER ERROR: {type(e).__name__}: {e}") - + # Reset for fix test - if not source_file.exists(): source_file.touch() - if not dest_file.exists(): dest_file.touch() - + if not source_file.exists(): + source_file.touch() + if not dest_file.exists(): + dest_file.touch() + # 3. Try replace (Proposed Fix) try: print("Attempting replace (should succeed)...") @@ -49,5 +52,6 @@ def reproduction_step(): except Exception as e: print(f"FAILED: Replace raised {type(e).__name__}: {e}") + if __name__ == "__main__": reproduction_step() diff --git a/reproduce_mmap_error.py b/reproduce_mmap_error.py index 7e03a32..307ae3f 100644 --- a/reproduce_mmap_error.py +++ b/reproduce_mmap_error.py @@ -1,8 +1,8 @@ - import mmap import os import tempfile + def reproduce(): with tempfile.NamedTemporaryFile(delete=False) as f: f.close() @@ -22,11 +22,12 @@ def reproduce(): print("VERIFIED: Reproduction successful.") else: print("VERIFIED: Reproduction successful (different message).") - + except Exception as e: print(f"Caught unexpected top level error: {e}") finally: os.unlink(path) + if __name__ == "__main__": reproduce() diff --git a/run_app.py b/run_app.py index e1cd53c..6c1da0f 100644 --- a/run_app.py +++ b/run_app.py @@ -1,5 +1,4 @@ import sys -import os from pathlib import Path # Add the directory containing the 'faststack' package to the Python path @@ -7,4 +6,5 @@ # Now, try to run the module import runpy -runpy.run_module('faststack.app', run_name='__main__', alter_sys=True) \ No newline at end of file + +runpy.run_module("faststack.app", run_name="__main__", alter_sys=True) diff --git a/scripts/smoke_verify.py b/scripts/smoke_verify.py index 924a699..bf06886 100644 --- a/scripts/smoke_verify.py +++ b/scripts/smoke_verify.py @@ -1,7 +1,6 @@ import sys import importlib.resources -import argparse -from pathlib import Path + def check_imports(): print("Checking imports...") @@ -11,16 +10,19 @@ def check_imports(): import faststack.io import faststack.imaging import faststack.app + print(" [OK] Imports successful") except ImportError as e: print(f" [FAIL] Import failed: {e}") return False return True + def check_cli(): print("Checking CLI entry point...") try: from faststack.app import cli + if not callable(cli): print(" [FAIL] faststack.app.cli is not callable") return False @@ -33,12 +35,13 @@ def check_cli(): return False return True + def check_assets(): print("Checking assets (QML files)...") try: # For Python 3.9+ standard library importlib.resources # We look for any .qml file in faststack package - qml_files = list(importlib.resources.files('faststack').rglob('*.qml')) + qml_files = list(importlib.resources.files("faststack").rglob("*.qml")) count = len(qml_files) if count > 0: print(f" [OK] Found {count} QML files") @@ -46,27 +49,31 @@ def check_assets(): print(f" - {p.name}") else: print(" [FAIL] No QML files found in package resources!") - print(" (Did you include package_data in pyproject.toml / MANIFEST.in?)") + print( + " (Did you include package_data in pyproject.toml / MANIFEST.in?)" + ) return False except Exception as e: print(f" [FAIL] Asset check failed: {e}") return False return True + def main(): print("=== FastStack Smoke Verification ===") print(f"Python: {sys.version}") - + if not check_imports(): sys.exit(1) - + if not check_cli(): sys.exit(1) - + if not check_assets(): sys.exit(1) - + print("\n[SUCCESS] faststack package seems healthy.") + if __name__ == "__main__": main() diff --git a/test_cachetools_api.py b/test_cachetools_api.py index 0a34e74..60565b2 100644 --- a/test_cachetools_api.py +++ b/test_cachetools_api.py @@ -1,4 +1,5 @@ """Quick test to check cachetools.LRUCache API.""" + from cachetools import LRUCache # Create a basic LRUCache @@ -9,8 +10,10 @@ print(f"maxsize value: {cache.maxsize}") # Check if we can access the internal attribute -if hasattr(cache, '_Cache__maxsize'): +if hasattr(cache, "_Cache__maxsize"): print(f"Internal _Cache__maxsize: {cache._Cache__maxsize}") # List all attributes -print(f"\nAll cache attributes: {[attr for attr in dir(cache) if not attr.startswith('_')]}") +print( + f"\nAll cache attributes: {[attr for attr in dir(cache) if not attr.startswith('_')]}" +) diff --git a/test_max_bytes.py b/test_max_bytes.py index 5a945de..53648fa 100644 --- a/test_max_bytes.py +++ b/test_max_bytes.py @@ -1,13 +1,16 @@ """Quick test to verify ByteLRUCache.max_bytes works correctly.""" + from faststack.imaging.cache import ByteLRUCache + class MockItem: def __init__(self, size: int): self._size = size - + def __sizeof__(self) -> int: return self._size + # Test 1: Initialize cache cache = ByteLRUCache(max_bytes=1000, size_of=lambda x: x.__sizeof__()) print(f"Initial max_bytes: {cache.max_bytes}") @@ -31,6 +34,8 @@ def __sizeof__(self) -> int: # "a" should have been evicted (LRU) assert "a" not in cache, "Item 'a' should have been evicted" assert "b" in cache or "c" in cache, "At least one of 'b' or 'c' should be in cache" -assert cache.currsize <= cache.max_bytes, f"Current size {cache.currsize} should be <= max_bytes {cache.max_bytes}" +assert cache.currsize <= cache.max_bytes, ( + f"Current size {cache.currsize} should be <= max_bytes {cache.max_bytes}" +) print("\n✓ All tests passed! ByteLRUCache.max_bytes works correctly.") diff --git a/tests/debug_import.py b/tests/debug_import.py index 847c7ce..3341486 100644 --- a/tests/debug_import.py +++ b/tests/debug_import.py @@ -1,23 +1,25 @@ -import sys import os +import sys import traceback # Add project root to path -# We are running from faststack/faststack, so root is .. -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -print(f"Sys Path: {sys.path[0]}") +# tests/ is at project_root/tests, so project_root is .. +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +sys.path.insert(0, PROJECT_ROOT) +print(f"Sys Path[0]: {sys.path[0]}") try: print("Attempting import faststack.app...") - import faststack.app - print("Import faststack.app success!") - + import faststack.app as app + print(f"Import faststack.app success! ({app.__file__})") + print("Attempting import AppController...") from faststack.app import AppController print("Import AppController success!") - + print("Attributes:") print(f"get_active_edit_path: {hasattr(AppController, 'get_active_edit_path')}") except Exception: print("Import FAILED:") traceback.print_exc() + raise # optional: makes CI fail loud diff --git a/tests/repro_exif_fix.py b/tests/repro_exif_fix.py index ee54cd3..a36f9a5 100644 --- a/tests/repro_exif_fix.py +++ b/tests/repro_exif_fix.py @@ -1,30 +1,32 @@ - from PIL import Image, ExifTags import io + def test_exif_sanitization(): # 1. Create a dummy image with EXIF orientation = 6 (Rotated 90 CW) - # We can't easily "create" raw EXIF bytes without saving, + # We can't easily "create" raw EXIF bytes without saving, # so we'll save a temp, change it, then load it. - - img = Image.new('RGB', (100, 100), color='red') + + img = Image.new("RGB", (100, 100), color="red") exif = img.getexif() - exif[ExifTags.Base.Orientation] = 6 # Simulate rotated - + exif[ExifTags.Base.Orientation] = 6 # Simulate rotated + buf = io.BytesIO() - img.save(buf, format='JPEG', exif=exif.tobytes()) + img.save(buf, format="JPEG", exif=exif.tobytes()) buf.seek(0) - + # 2. Load it back (this simulates self.original_image) original_image = Image.open(buf) - print(f"Original Orientation: {original_image.getexif().get(ExifTags.Base.Orientation)}") - + print( + f"Original Orientation: {original_image.getexif().get(ExifTags.Base.Orientation)}" + ) + # 3. Simulate processing (we have a new image to save, but want metadata from original) # In Editor code: existing logic takes original_image.info.get('exif') # Proposed logic: take original_image.getexif(), mod it, tobytes() - - new_img = Image.new('RGB', (100, 100), color='blue') # The "edited" image - + + new_img = Image.new("RGB", (100, 100), color="blue") # The "edited" image + # Proposed Fix Logic: exif_obj = original_image.getexif() if exif_obj: @@ -34,24 +36,25 @@ def test_exif_sanitization(): print("Successfully serialized modified EXIF.") except Exception as e: print(f"Failed to serialize: {e}") - exif_bytes = original_image.info.get('exif') # Fallback? + exif_bytes = original_image.info.get("exif") # Fallback? else: - exif_bytes = original_image.info.get('exif') + exif_bytes = original_image.info.get("exif") # Save out_buf = io.BytesIO() - new_img.save(out_buf, format='JPEG', exif=exif_bytes) + new_img.save(out_buf, format="JPEG", exif=exif_bytes) out_buf.seek(0) - + # 4. Verify result result_img = Image.open(out_buf) res_orientation = result_img.getexif().get(ExifTags.Base.Orientation) print(f"Result Orientation: {res_orientation}") - + if res_orientation == 1: print("PASS: Orientation sanitized.") else: print("FAIL: Orientation NOT sanitized.") + if __name__ == "__main__": test_exif_sanitization() diff --git a/tests/test_highlights_v2.py b/tests/test_highlights_v2.py index ffda9c3..f52091a 100644 --- a/tests/test_highlights_v2.py +++ b/tests/test_highlights_v2.py @@ -2,35 +2,35 @@ import numpy as np from faststack.imaging.editor import ImageEditor from faststack.imaging.math_utils import _apply_headroom_shoulder -from faststack.ui.provider import UIState -from faststack.app import AppController -from PySide6.QtCore import QObject, Signal +from PySide6.QtCore import QObject + class MockAppController(QObject): def __init__(self): super().__init__() self.image_editor = ImageEditor() - self.ui_state = None # Circular ref handle manually + self.ui_state = None # Circular ref handle manually + class TestHighlightsV2(unittest.TestCase): def test_shoulder_asymptote(self): - """Verify the new shoulder asymptotes to 1.0 + steepness.""" + """Verify the new shoulder asymptotes to 1.0 + max_overshoot.""" x = np.array([1.0, 2.0, 10.0, 100.0], dtype=np.float32) - steepness = 0.05 - out = _apply_headroom_shoulder(x, steepness=steepness) - + max_overshoot = 0.05 + out = _apply_headroom_shoulder(x, max_overshoot=max_overshoot) + # At 1.0, should be 1.0 self.assertAlmostEqual(out[0], 1.0, places=5) - - # Above 1.0, should be < 1.0 + steepness - self.assertTrue(np.all(out[1:] < 1.0 + steepness)) - + + # Above 1.0, should be < 1.0 + max_overshoot + self.assertTrue(np.all(out[1:] < 1.0 + max_overshoot)) + # Monotonicity self.assertTrue(out[1] > out[0]) self.assertTrue(out[2] > out[1]) - + # Asymptote check: at very large x, should be close to 1.05 - self.assertAlmostEqual(out[-1], 1.0 + steepness, delta=0.001) + self.assertAlmostEqual(out[-1], 1.0 + max_overshoot, delta=0.001) def test_analysis_decoupling(self): """Verify analysis runs before adjustments and is cached.""" @@ -39,30 +39,33 @@ def test_analysis_decoupling(self): linear = np.ones((100, 100, 3), dtype=np.float32) * 1.2 # sRGB mock indicating some clipping (e.g. 255) srgb = np.ones((100, 100, 3), dtype=np.uint8) * 255 - + # Run with highlights=-0.5 - editor._apply_highlights_shadows(linear, highlights=-0.5, shadows=0.0, srgb_u8=srgb) - + editor._apply_highlights_shadows( + linear, highlights=-0.5, shadows=0.0, srgb_u8=srgb + ) + # Check cache self.assertIsNotNone(editor._last_highlight_state) - self.assertGreater(editor._last_highlight_state['headroom_pct'], 0.9) - self.assertGreater(editor._last_highlight_state['clipped_pct'], 0.9) + self.assertGreater(editor._last_highlight_state["headroom_pct"], 0.9) + self.assertGreater(editor._last_highlight_state["clipped_pct"], 0.9) def test_robust_ceiling(self): """Verify headroom ceiling handles hot pixels.""" editor = ImageEditor() - linear = np.ones((100, 100, 3), dtype=np.float32) * 1.1 # Moderate headroom + linear = np.ones((100, 100, 3), dtype=np.float32) * 1.1 # Moderate headroom # Add a single hot pixel linear[50, 50, :] = 1000.0 - + # Use highlights recovery # This triggers the robust percentile logic out = editor._apply_highlights_shadows(linear, highlights=-1.0, shadows=0.0) - + # Check that we didn't explode or crash self.assertTrue(np.isfinite(out).all()) # The hot pixel should be compressed but not NaN - self.assertLess(out[50, 50, 0], 1000.0) + self.assertLess(out[50, 50, 0], 1000.0) + -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_prefetch_concurrency.py b/tests/test_prefetch_concurrency.py index f2fac87..6ee94d1 100644 --- a/tests/test_prefetch_concurrency.py +++ b/tests/test_prefetch_concurrency.py @@ -1,66 +1,74 @@ - import threading import time import pytest -import numpy as np from pathlib import Path -from concurrent.futures import Future from faststack.imaging.prefetch import Prefetcher -from faststack.models import ImageFile + # Mock objects to isolate Prefetcher logic class MockImageFile: def __init__(self, index): self.path = Path(f"/mock/image_{index}.jpg") + def mock_get_display_info(): return 1920, 1080, 1 + def mock_cache_put(key, value): pass + @pytest.fixture def prefetcher(): image_files = [MockImageFile(i) for i in range(100)] # Use a small radius to force more activity - p = Prefetcher(image_files, mock_cache_put, prefetch_radius=5, get_display_info=mock_get_display_info, debug=False) - + p = Prefetcher( + image_files, + mock_cache_put, + prefetch_radius=5, + get_display_info=mock_get_display_info, + debug=False, + ) + # Mock the internal decode method to avoid actual I/O and processing # We just return a dummy result after a tiny sleep def mock_decode_and_cache(*args, **kwargs): - time.sleep(0.0001) # fast sleep + time.sleep(0.0001) # fast sleep return Path("/mock/image_x.jpg"), 1 p._decode_and_cache = mock_decode_and_cache - + yield p p.shutdown() + def test_prefetch_concurrency(prefetcher): """ Stress test for race conditions in Prefetcher. Simulates concurrent navigation (update_prefetch), cancellation (cancel_all), and file list updates (set_image_files). """ - + # Configuration num_loops = 5000 num_threads = 4 - + # Shared state for error tracking errors = [] - + # Barrier to synchronize start barrier = threading.Barrier(num_threads) - + stop_event = threading.Event() def worker_update(): try: barrier.wait() for i in range(num_loops): - if stop_event.is_set(): break + if stop_event.is_set(): + break # Randomly jump around idx = i % 100 prefetcher.update_prefetch(idx, is_navigation=True, direction=1) @@ -72,8 +80,9 @@ def worker_cancel(): try: barrier.wait() for i in range(num_loops): - if stop_event.is_set(): break - if i % 10 == 0: # Cancel less frequently + if stop_event.is_set(): + break + if i % 10 == 0: # Cancel less frequently prefetcher.cancel_all() except Exception as e: errors.append(e) @@ -84,11 +93,12 @@ def worker_set_files(): barrier.wait() # Generate two lists to toggle between list1 = [MockImageFile(i) for i in range(100)] - list2 = [MockImageFile(i) for i in range(50)] # Different size - + list2 = [MockImageFile(i) for i in range(50)] # Different size + for i in range(num_loops): - if stop_event.is_set(): break - if i % 100 == 0: # Reload files occasionally + if stop_event.is_set(): + break + if i % 100 == 0: # Reload files occasionally new_list = list2 if i % 200 == 0 else list1 prefetcher.set_image_files(new_list) except Exception as e: @@ -98,9 +108,9 @@ def worker_set_files(): # Create threads threads = [ threading.Thread(target=worker_update), - threading.Thread(target=worker_update), # Two updaters + threading.Thread(target=worker_update), # Two updaters threading.Thread(target=worker_cancel), - threading.Thread(target=worker_set_files) + threading.Thread(target=worker_set_files), ] # Start threads @@ -120,10 +130,11 @@ def worker_set_files(): # Check that scheduled matches generation (basic check) for gen, scheduled_set in prefetcher._scheduled.items(): if gen > prefetcher.generation: - pytest.fail(f"Found scheduled set for future generation {gen} > {prefetcher.generation}") - + pytest.fail( + f"Found scheduled set for future generation {gen} > {prefetcher.generation}" + ) + # Check futures dict consistency # It's hard to assert exact size since threads stopped at random times, # but we can check if keys in futures are valid integers roughly assert isinstance(prefetcher.futures, dict) - diff --git a/tests/verify_manual.py b/tests/verify_manual.py index fcf49a1..18a1f7a 100644 --- a/tests/verify_manual.py +++ b/tests/verify_manual.py @@ -3,15 +3,17 @@ from pathlib import Path from unittest.mock import MagicMock, patch -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) try: from faststack.app import AppController + print("Imported AppController") except Exception as e: print(f"Failed to import AppController: {e}") sys.exit(1) + class DummyController: def __init__(self): self.current_edit_source_mode = "jpeg" @@ -20,32 +22,44 @@ def __init__(self): self.ui_state = MagicMock() self.ui_state.isHistogramVisible = False self.editSourceModeChanged = MagicMock() - + # Copy methods get_active_edit_path = AppController.get_active_edit_path is_valid_working_tif = AppController.is_valid_working_tif _set_current_index = AppController._set_current_index enable_raw_editing = AppController.enable_raw_editing - - def sync_ui_state(self): pass - def _reset_crop_settings(self): pass - def _do_prefetch(self, *args, **kwargs): pass - def update_histogram(self): pass - def load_image_for_editing(self): pass - def _develop_raw_backend(self): pass + + def sync_ui_state(self): + pass + + def _reset_crop_settings(self): + pass + + def _do_prefetch(self, *args, **kwargs): + pass + + def update_histogram(self): + pass + + def load_image_for_editing(self): + pass + + def _develop_raw_backend(self): + pass def log(msg): with open("verify_result.txt", "a") as f: f.write(msg + "\n") + def run_checks(): # Clear log with open("verify_result.txt", "w") as f: f.write("Starting Verification\n") controller = DummyController() - + # Setup data img_jpg = MagicMock() img_jpg.path = Path("test.jpg") # suffix is derived from Path, not assigned @@ -56,9 +70,9 @@ def run_checks(): img_raw = MagicMock() img_raw.path = Path("orphan.CR2") # suffix is derived from Path, not assigned img_raw.raw_pair = None - + controller.image_files = [img_jpg, img_raw] - + log("--- Test 1: Default Mode ---") controller.current_index = 0 path = controller.get_active_edit_path(0) @@ -73,23 +87,26 @@ def run_checks(): if controller.current_edit_source_mode == "raw": log("PASS: Mode switched") else: - log(f"FAIL: Mode not switched") + log("FAIL: Mode not switched") controller._develop_raw_backend.assert_called_once() log("PASS: Dev triggered") log("--- Test 3: Valid TIFF ---") img_jpg.has_working_tif = True - with patch.object(controller, 'is_valid_working_tif', return_value=True): + with patch.object(controller, "is_valid_working_tif", return_value=True): controller.load_image_for_editing = MagicMock() controller._develop_raw_backend = MagicMock() - controller.current_edit_source_mode = "jpeg" # Reset + controller.current_edit_source_mode = "jpeg" # Reset controller.enable_raw_editing() - - if controller.current_edit_source_mode == "raw" and controller.get_active_edit_path(0) == Path("test.tif"): - log("PASS: Mode raw, Returns TIFF") + + if ( + controller.current_edit_source_mode == "raw" + and controller.get_active_edit_path(0) == Path("test.tif") + ): + log("PASS: Mode raw, Returns TIFF") else: - log(f"FAIL: returns {controller.get_active_edit_path(0)}") - + log(f"FAIL: returns {controller.get_active_edit_path(0)}") + controller._develop_raw_backend.assert_not_called() log("PASS: No dev triggered") @@ -98,12 +115,13 @@ def run_checks(): # Note: Logic in app.py uses local import: from faststack.io.indexer import RAW_EXTENSIONS # Patching faststack.io.indexer.RAW_EXTENSIONS works if module is already loaded or loads fresh. # Since we imported AppController (which imports indexer), it is loaded. - with patch('faststack.io.indexer.RAW_EXTENSIONS', {'.CR2'}): + with patch("faststack.io.indexer.RAW_EXTENSIONS", {".CR2"}): # We also need to patch JPG_EXTENSIONS maybe? No, defaults are fine. controller._set_current_index(1) if controller.current_edit_source_mode == "raw": - log("PASS: Auto raw mode") + log("PASS: Auto raw mode") else: - log(f"FAIL: Mode is {controller.current_edit_source_mode}") - + log(f"FAIL: Mode is {controller.current_edit_source_mode}") + + run_checks() diff --git a/tests/verify_raw_mode.py b/tests/verify_raw_mode.py index 9d5a8ea..fa656f0 100644 --- a/tests/verify_raw_mode.py +++ b/tests/verify_raw_mode.py @@ -5,7 +5,7 @@ import os # Add project root to path -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) # Mock things we don't want to import fully or that need QObject # We need to test AppController methods, but AppController inherits QObject. @@ -20,6 +20,7 @@ # But methods are bound to 'self'. # We can create a dummy class that looks like AppController for these methods. + class DummyController: def __init__(self): self.current_edit_source_mode = "jpeg" @@ -32,6 +33,7 @@ def __init__(self): # Copy methods to test try: from faststack.app import AppController + get_active_edit_path = AppController.get_active_edit_path is_valid_working_tif = AppController.is_valid_working_tif _set_current_index = AppController._set_current_index @@ -39,6 +41,7 @@ def __init__(self): except Exception as e: print(f"CRITICAL ERROR importing AppController: {e}") import traceback + traceback.print_exc() # Define dummy placeholders to prevent AttributeError during test collection/execution get_active_edit_path = lambda *args: None @@ -46,40 +49,44 @@ def __init__(self): _set_current_index = lambda *args: None enable_raw_editing = lambda *args: None - def sync_ui_state(self): pass - + def _reset_crop_settings(self): pass - + def _do_prefetch(self, *args, **kwargs): pass - + def update_histogram(self): pass - + def load_image_for_editing(self): pass - + def _develop_raw_backend(self): pass + class TestRawMode(unittest.TestCase): def setUp(self): self.controller = DummyController() - + # Create mock image files self.img_jpg = MagicMock() - self.img_jpg.path = Path("test.jpg") # suffix is derived from Path, not assigned + self.img_jpg.path = Path( + "test.jpg" + ) # suffix is derived from Path, not assigned self.img_jpg.raw_pair = Path("test.CR2") self.img_jpg.working_tif_path = Path("test.tif") self.img_jpg.has_working_tif = False # Initially false self.img_raw_only = MagicMock() - self.img_raw_only.path = Path("orphan.CR2") # suffix is derived from Path, not assigned + self.img_raw_only.path = Path( + "orphan.CR2" + ) # suffix is derived from Path, not assigned self.img_raw_only.raw_pair = None - + self.controller.image_files = [self.img_jpg, self.img_raw_only] def test_default_mode(self): @@ -92,15 +99,15 @@ def test_default_mode(self): def test_switch_to_raw_with_development(self): """Test 2: Enabling RAW should switch mode and trigger develop if no TIFF.""" self.controller.current_index = 0 - + # Mock _develop_raw_backend self.controller._develop_raw_backend = MagicMock() - + self.controller.enable_raw_editing() - + self.assertEqual(self.controller.current_edit_source_mode, "raw") self.controller._develop_raw_backend.assert_called_once() - + # Path check: even if we switch mode, if TIFF is invalid, get_active_edit_path might return RAW path? # Logic says: if mode=raw, check valid TIFF, else return raw_pair. # So it should return the RAW file if TIFF not ready. @@ -113,7 +120,7 @@ def test_switch_to_raw_with_existing_tiff(self): self.img_jpg.has_working_tif = True # Mock is_valid_working_tif to return True - with patch.object(self.controller, 'is_valid_working_tif', return_value=True): + with patch.object(self.controller, "is_valid_working_tif", return_value=True): # Create mocks BEFORE calling enable_raw_editing self.controller.load_image_for_editing = MagicMock() self.controller._develop_raw_backend = MagicMock() @@ -134,18 +141,18 @@ def test_raw_only_case(self): """Test 4: Opening RAW-only files should force RAW mode.""" # Navigate to index 1 (RAW only) # Using _set_current_index logic - + # Need to mock the logic in _set_current_index... # Wait, I copied _set_current_index to DummyController. # But it requires `from faststack.io.indexer import RAW_EXTENSIONS`. # I need to mock that import or ensure it works. - - with patch('faststack.io.indexer.RAW_EXTENSIONS', {'.CR2', '.ARW'}): - self.controller._set_current_index(1) - + + with patch("faststack.io.indexer.RAW_EXTENSIONS", {".CR2", ".ARW"}): + self.controller._set_current_index(1) + self.assertEqual(self.controller.current_index, 1) self.assertEqual(self.controller.current_edit_source_mode, "raw") - + path = self.controller.get_active_edit_path(1) self.assertEqual(path, Path("orphan.CR2")) @@ -154,14 +161,15 @@ def test_navigation_resets_mode(self): # First set raw mode on index 0 self.controller.current_index = 0 self.controller.current_edit_source_mode = "raw" - + # Navigate to index 0 again via _set_current_index (like jumping or reloading) # Or pretend we have another image. Let's make index 0 a normal pair. - - with patch('faststack.io.indexer.RAW_EXTENSIONS', {'.CR2', '.ARW'}): + + with patch("faststack.io.indexer.RAW_EXTENSIONS", {".CR2", ".ARW"}): self.controller._set_current_index(0) - + self.assertEqual(self.controller.current_edit_source_mode, "jpeg") -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/verify_fix.py b/verify_fix.py index 1a81c4a..839cb1b 100644 --- a/verify_fix.py +++ b/verify_fix.py @@ -1,4 +1,3 @@ - import os import sys import logging @@ -21,50 +20,53 @@ # Assuming environment is set up. sys.exit(1) + # Verify the fix def verify(): # Setup with tempfile.NamedTemporaryFile(delete=False) as f: f.close() path = f.name - + print(f"Created empty file: {path}") - + try: # Create dummy ImageFile img_file = ImageFile(path=Path(path), name="empty.jpg", size=0, modified=0) - + def mock_cache_put(key, val): pass - + def mock_get_info(): return 100, 100, 1 - + # Instantiate Prefetcher # It creates a thread pool, so we should shut it down. prefetcher = Prefetcher([], mock_cache_put, 1, mock_get_info, debug=True) - + try: # Call _decode_and_cache # It checks self.generation (initially 0) against passed generation print("Calling _decode_and_cache...") result = prefetcher._decode_and_cache(img_file, 0, 0, 100, 100, 1) - + if result is None: print("SUCCESS: Returned None for empty file (graceful failure).") else: print(f"FAILURE: Returned {result}") finally: prefetcher.shutdown() - + except Exception as e: print(f"FAILED with exception: {e}") import traceback + traceback.print_exc() finally: if os.path.exists(path): os.unlink(path) + if __name__ == "__main__": # Configure logging to see the warning logging.basicConfig(level=logging.INFO) diff --git a/verify_fix_auto_levels.py b/verify_fix_auto_levels.py index 09476c3..9c59570 100644 --- a/verify_fix_auto_levels.py +++ b/verify_fix_auto_levels.py @@ -1,10 +1,10 @@ - import os import time import shutil from pathlib import Path from faststack.io.indexer import find_images + def verify_fix_logic(): # Setup test dir test_dir = Path("./verify_auto_levels") @@ -16,7 +16,7 @@ def verify_fix_logic(): img_name = "test_image.jpg" img_path = test_dir / img_name img_path.touch() - + # Set mtime to T0 t0 = time.time() - 100 os.utime(img_path, (t0, t0)) @@ -27,12 +27,12 @@ def verify_fix_logic(): current_index = 0 # User selects this selected_image = images[current_index] - - print(f"Initial: {[i.path.name for i in images]}") + + print(f"Initial: {[i.path.name for i in images]}") print(f"Selected: {selected_image.path.name} (Index {current_index})") # --- SIMULATE AUTO LEVELS --- - + # 1. Create Backup (preserves mtime T0) # The backup naming logic in create_backup_file is: filename-backup.jpg # Since 'test_image.jpg' -> 'test_image-backup.jpg' @@ -41,59 +41,65 @@ def verify_fix_logic(): shutil.copy2(img_path, backup_path) # Ensure backup has T0 os.utime(backup_path, (t0, t0)) - + # 2. Save Main (update mtime to T1) t1 = time.time() - img_path.touch() # Updates mtime - + img_path.touch() # Updates mtime + # --- SIMULATE APP REFRESH & SELECTION (The Fix Logic) --- - saved_path = img_path # The file we just saved to - + saved_path = img_path # The file we just saved to + # Refresh images = find_images(test_dir) print(f"Refreshed: {[i.path.name for i in images]}") - # Expected order: + # Expected order: # test_image-backup.jpg (T0) - # test_image.jpg (T1) + # test_image.jpg (T1) # So index 0 is backup, index 1 is edited - + # FIX LOGIC: new_index = -1 target_path = Path(saved_path).resolve() target_name = Path(saved_path).name - + for i, img_file in enumerate(images): # The app now uses .name matching if img_file.path.name == target_name: new_index = i break - - + # CHECK RESULTS if new_index == -1: print("FAIL: Count not find saved image in list.") exit(1) - + selected_in_ui = images[new_index] print(f"UI Selected: {selected_in_ui.path.name} (Index {new_index})") - + if selected_in_ui.path.name != img_name: - print(f"FAIL: Selected image {selected_in_ui.path.name} is NOT the edited image {img_name}") + print( + f"FAIL: Selected image {selected_in_ui.path.name} is NOT the edited image {img_name}" + ) exit(1) - + # Verify previous image is backup if new_index > 0: prev_image = images[new_index - 1] print(f"Previous Image (Left Arrow): {prev_image.path.name}") if prev_image.path.name != backup_name: - print(f"WARNING: Previous image is not the expected backup. Found: {prev_image.path.name}") + print( + f"WARNING: Previous image is not the expected backup. Found: {prev_image.path.name}" + ) else: - print("WARNING: No previous image found. Backup should be roughly before edited image.") - + print( + "WARNING: No previous image found. Backup should be roughly before edited image." + ) + print("SUCCESS: Fix verified.") # Cleanup shutil.rmtree(test_dir) + if __name__ == "__main__": verify_fix_logic() diff --git a/verify_fix_simple.py b/verify_fix_simple.py index 871ddb5..f430c93 100644 --- a/verify_fix_simple.py +++ b/verify_fix_simple.py @@ -1,16 +1,16 @@ - import mmap import os import tempfile + def verify(): # Setup with tempfile.NamedTemporaryFile(delete=False) as f: f.close() path = f.name - + print(f"Created empty file: {path}") - + try: # Verify the logic I added to prefetch.py # Logic: @@ -26,12 +26,13 @@ def verify(): with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mmapped: print("Mapped successfully") print("FAILURE: Should have skipped but didn't (or mmap worked unexpected)") - + except Exception as e: print(f"FAILED with exception: {e}") finally: if os.path.exists(path): os.unlink(path) + if __name__ == "__main__": verify()