diff --git a/src/core/batch_rename_service.py b/src/core/batch_rename_service.py index 5ddcb3f..4fe18f5 100644 --- a/src/core/batch_rename_service.py +++ b/src/core/batch_rename_service.py @@ -236,9 +236,23 @@ def generate_preview( norm_target = os.path.normcase(os.path.abspath(item.new_path)) norm_src = os.path.normcase(os.path.abspath(item.original_path)) - # Case-Only Rename der eigenen Datei ist zulässig + # Case-Only Rename der eigenen Datei ist zulässig. os.path.normcase() + # allein reicht dafür nicht: normcase folgt der Pfad-STRING-Konvention + # des Betriebssystems (Windows -> lowercase, POSIX -> No-Op) und weiß + # nichts über das tatsächliche Dateisystem. Unter macOS ist die + # Pfadkonvention case-sensitiv (normcase verändert nichts), das dort + # standardmäßige APFS aber case-insensitiv -- "sample.txt" und + # "SAMPLE.txt" sind auf Dateisystemebene dieselbe Datei, obwohl ihre + # normcase-Strings sich unterscheiden. os.path.samefile() vergleicht + # dagegen tatsächliche Inode/Device-Identität und erkennt das korrekt. if norm_target == norm_src: continue + if os.path.exists(item.new_path) and os.path.exists(item.original_path): + try: + if os.path.samefile(item.new_path, item.original_path): + continue + except OSError: + pass # Ziel wird von einer anderen, nicht wegziehenden Batch-Datei blockiert if norm_target in blocked_original_paths: diff --git a/tests/test_preview_panel_hardening.py b/tests/test_preview_panel_hardening.py index 198fa4d..f9eb4e6 100644 --- a/tests/test_preview_panel_hardening.py +++ b/tests/test_preview_panel_hardening.py @@ -147,12 +147,22 @@ def test_metadata_handles_negative_or_invalid_timestamp_gracefully(self, tmp_pat class TestExcelPreviewExternalLaunch: def test_open_extern_handles_os_error_gracefully(self, tmp_path): + """_open_extern() waehlt den Aufruf je nach sys.platform (os.startfile + unter Windows, subprocess.Popen("open"/"xdg-open") sonst) -- os.startfile + existiert unter Linux/macOS gar nicht, patch() darauf wuerde dort mit + AttributeError abbrechen. Der passende Aufrufpfad fuer die aktuelle + Plattform wird stattdessen gemockt.""" from gui.preview.preview_panel import ExcelPreview widget = ExcelPreview() widget._path = str(tmp_path / "fake.xlsx") - with patch("os.startfile", side_effect=OSError("No application associated")): + if sys.platform == "win32": + target, kwargs = "os.startfile", {"side_effect": OSError("No application associated")} + else: + target, kwargs = "subprocess.Popen", {"side_effect": OSError("No application associated")} + + with patch(target, **kwargs): widget._open_extern() assert not widget.status_label.isHidden()