diff --git a/gui_v6/processing_runner.py b/gui_v6/processing_runner.py index 7a5c6b8..1c34f85 100644 --- a/gui_v6/processing_runner.py +++ b/gui_v6/processing_runner.py @@ -99,6 +99,10 @@ def discover_documents(input_path, extensions: Optional[Sequence[str]] = None) - return [] +class OutputNotWritableError(RuntimeError): + """Le dossier de sortie n'est pas inscriptible (échec amont, message clair).""" + + @dataclass class DocResult: """Détail anonymisé d'un document traité (pour la télémétrie d'usage). @@ -195,6 +199,19 @@ class ProcessingRunner: log("Aucun document supporté détecté.") return summary + # Sonde amont : on vérifie une seule fois que le dossier de sortie est + # inscriptible AVANT la boucle, pour un échec clair et unique (P1-6) + # plutôt qu'une erreur cryptique répétée à chaque document. + try: + out_root.mkdir(parents=True, exist_ok=True) + probe = out_root / ".anon_write_test" + probe.write_text("", encoding="utf-8") + probe.unlink() + except Exception as exc: + raise OutputNotWritableError( + f"Dossier de sortie non inscriptible : {out_root} ({exc})" + ) from exc + for index, doc in enumerate(docs, start=1): if stop_event is not None and stop_event.is_set(): summary.stopped = True diff --git a/tests/unit/test_gui_v6_processing_runner.py b/tests/unit/test_gui_v6_processing_runner.py index e78820d..1a3565a 100644 --- a/tests/unit/test_gui_v6_processing_runner.py +++ b/tests/unit/test_gui_v6_processing_runner.py @@ -197,6 +197,23 @@ def test_progress_callbacks(tmp_path): assert (2, 2) in events # progression finale atteinte +def test_run_fails_fast_when_output_not_writable(tmp_path, monkeypatch): + from gui_v6.processing_runner import ProcessingRunner, OutputNotWritableError + src = tmp_path / "in" + src.mkdir() + (src / "a.txt").write_text("x", encoding="utf-8") + out = tmp_path / "ro" + out.mkdir() + + def boom(*a, **k): + raise PermissionError("read-only") + + monkeypatch.setattr("gui_v6.processing_runner.Path.mkdir", boom) + runner = ProcessingRunner(process_fn=lambda d, o: {}) + with pytest.raises(OutputNotWritableError): + runner.run(src, out) + + def test_no_double_run(tmp_path): _touch(tmp_path / "a.pdf") started = threading.Event()