From 4ebb4d6550e434cf91003d7dedeb3b87e5491b77 Mon Sep 17 00:00:00 2001 From: "Josef M. Gallmetzer" <64498081+galjos@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:52:44 +0200 Subject: [PATCH] fix: reject start_file equal to restart_file on simple export Run plans already guarded restart collisions. Simple render/export now rejects the same case-insensitive clash so PQ cannot overwrite the starting structure. --- pqsetup/input_writer.py | 13 +++++++++++++ tests/test_presets_and_input.py | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/pqsetup/input_writer.py b/pqsetup/input_writer.py index 4276ed5..90f6fbc 100644 --- a/pqsetup/input_writer.py +++ b/pqsetup/input_writer.py @@ -294,6 +294,19 @@ def validate_setup( diagnostics.append( _error("input.restart_file", "Restart filename is too long.") ) + if ( + setup.start_file + and setup.start_file.casefold() == restart_filename(setup).casefold() + ): + diagnostics.append( + _error( + "input.restart_collision", + ( + "Start file and restart file must differ so PQ does not " + "overwrite the starting structure." + ), + ) + ) if len(setup.file_prefix) > 128: diagnostics.append(_error("input.file_prefix", "Run name is too long.")) if setup.ensemble != "OPT": diff --git a/tests/test_presets_and_input.py b/tests/test_presets_and_input.py index 248c90d..343d2d6 100644 --- a/tests/test_presets_and_input.py +++ b/tests/test_presets_and_input.py @@ -85,6 +85,8 @@ def test_mm_opt_with_md_ensemble_is_rejected() -> None: assert not result.valid assert "workflow.mm_opt_ensemble" in {item.code for item in result.diagnostics} + + def test_qm_rpmd_requires_bead_count() -> None: missing = render_input( SimulationSetup( @@ -123,6 +125,29 @@ def test_qm_rpmd_requires_bead_count() -> None: assert "rpmd_n_replica = 32;" in ok.input_text +def test_start_file_cannot_equal_restart_file() -> None: + result = render_input( + SimulationSetup(start_file="pq-run.rst", file_prefix="pq-run") + ) + + assert not result.valid + assert "input.restart_collision" in { + item.code for item in result.diagnostics + } + + casefold_result = render_input( + SimulationSetup( + start_file="Structure.rst", + restart_file="structure.rst", + file_prefix="run", + ) + ) + assert not casefold_result.valid + assert "input.restart_collision" in { + item.code for item in casefold_result.diagnostics + } + + def test_unreleased_and_unknown_runners_fail_without_probing( monkeypatch, ) -> None: