From 5efb265ef5164a952fc7f39a44ea15a3d7a0b72f Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 14:50:54 +0100 Subject: [PATCH 01/18] wip: claude tests for recipes --- tests/test_recipes.py | 199 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 tests/test_recipes.py diff --git a/tests/test_recipes.py b/tests/test_recipes.py new file mode 100644 index 0000000..bb1f55f --- /dev/null +++ b/tests/test_recipes.py @@ -0,0 +1,199 @@ +import json +import pytest +import BioSimSpace as bss + +from meze import ( + MezeRecipe, + ColdMezeRecipe, + HotMezeRecipe, + AlchemicalMezeRecipe, +) + + +# --------------------------------------------------------------------------- +# MezeRecipe.validate_model +# --------------------------------------------------------------------------- + +def test_model_none_passthrough(): + assert MezeRecipe(model=None).model is None + + +def test_model_coerced_to_int(): + assert MezeRecipe(model="0").model == 0 + + +def test_model_invalid_raises(): + with pytest.raises(ValueError, match="Cannot covert"): + MezeRecipe(model="abc") + + +# --------------------------------------------------------------------------- +# MezeRecipe.validate_temperature / validate_pressure / validate_cutoff_distance +# --------------------------------------------------------------------------- + +def test_temperature_coerced_to_bss_type(): + recipe = MezeRecipe(temperature=300.0) + assert isinstance(recipe.temperature, bss.Types.Temperature) + + +def test_temperature_bss_type_passthrough(): + temperature = bss.Types.Temperature(310.0, "kelvin") + recipe = MezeRecipe(temperature=temperature) + assert recipe.temperature is temperature + + +def test_temperature_negative_raises(): + with pytest.raises(ValueError, match="greater than or equal to 0"): + MezeRecipe(temperature=-1.0) + + +def test_pressure_coerced_to_bss_type(): + recipe = MezeRecipe(pressure=1.0) + assert isinstance(recipe.pressure, bss.Types.Pressure) + + +def test_pressure_negative_raises(): + with pytest.raises(ValueError, match="greater than or equal to 0"): + MezeRecipe(pressure=-1.0) + + +def test_nb_cutoff_coerced_to_bss_type(): + recipe = MezeRecipe(nb_cutoff=12.0) + assert isinstance(recipe.nb_cutoff, bss.Types.Length) + + +def test_nb_cutoff_negative_raises(): + with pytest.raises(ValueError, match="greater than or equal to 0"): + MezeRecipe(nb_cutoff=-1.0) + + +# --------------------------------------------------------------------------- +# ColdMezeRecipe.validate_time / validate_temperature_range +# --------------------------------------------------------------------------- + +def test_cold_recipe_dt_and_runtime_coerced_to_picoseconds(): + recipe = ColdMezeRecipe(dt=0.001, runtime=100.0) + assert isinstance(recipe.dt, bss.Types.Time) + assert isinstance(recipe.runtime, bss.Types.Time) + assert recipe.runtime.picoseconds().value() == pytest.approx(100.0) + + +def test_cold_recipe_negative_runtime_raises(): + with pytest.raises(ValueError, match="greater than or equal to 0"): + ColdMezeRecipe(runtime=-1.0) + + +def test_cold_recipe_temperature_range_coerced(): + recipe = ColdMezeRecipe(start_temperature=100.0, end_temperature=300.0) + assert isinstance(recipe.start_temperature, bss.Types.Temperature) + assert isinstance(recipe.end_temperature, bss.Types.Temperature) + + +def test_cold_recipe_negative_start_temperature_raises(): + with pytest.raises(ValueError, match="greater than or equal to 0"): + ColdMezeRecipe(start_temperature=-1.0) + + +# --------------------------------------------------------------------------- +# HotMezeRecipe.validate_time / validate_timestep +# --------------------------------------------------------------------------- + +def test_hot_recipe_runtime_coerced_to_nanoseconds(): + recipe = HotMezeRecipe(runtime=100.0) + assert isinstance(recipe.runtime, bss.Types.Time) + assert recipe.runtime.nanoseconds().value() == pytest.approx(100.0) + + +def test_hot_recipe_negative_runtime_raises(): + with pytest.raises(ValueError, match="greater than or equal to 0"): + HotMezeRecipe(runtime=-1.0) + + +def test_hot_recipe_dt_coerced_to_picoseconds(): + recipe = HotMezeRecipe(dt=0.002) + assert isinstance(recipe.dt, bss.Types.Time) + assert recipe.dt.picoseconds().value() == pytest.approx(0.002) + + +def test_hot_recipe_negative_dt_raises(): + with pytest.raises(ValueError, match="greater than or equal to 0"): + HotMezeRecipe(dt=-0.002) + + +def test_cold_and_hot_runtime_use_different_units(): + # Same field name, same numeric input, different units: Cold is + # picoseconds, Hot is nanoseconds (1000x). Passing the same runtime + # to the wrong recipe class silently gives the wrong simulated time. + cold = ColdMezeRecipe(runtime=1.0) + hot = HotMezeRecipe(runtime=1.0) + cold_in_ns = cold.runtime.nanoseconds().value() + hot_in_ns = hot.runtime.nanoseconds().value() + assert hot_in_ns == pytest.approx(cold_in_ns * 1000) + + +# --------------------------------------------------------------------------- +# AlchemicalMezeRecipe.validate_sampling_time / validate_picosecond_times +# --------------------------------------------------------------------------- + +def test_alchemical_recipe_sampling_time_coerced_to_nanoseconds(): + recipe = AlchemicalMezeRecipe(sampling_time=4.0) + assert isinstance(recipe.sampling_time, bss.Types.Time) + assert recipe.sampling_time.nanoseconds().value() == pytest.approx(4.0) + + +def test_alchemical_recipe_zero_sampling_time_raises(): + # Unlike temperature/pressure/nb_cutoff/dt (which reject only < 0, + # so 0 is accepted), sampling_time specifically rejects <= 0. + with pytest.raises(ValueError, match="greater than 0"): + AlchemicalMezeRecipe(sampling_time=0) + + +def test_alchemical_recipe_negative_sampling_time_raises(): + with pytest.raises(ValueError, match="greater than 0"): + AlchemicalMezeRecipe(sampling_time=-4.0) + + +def test_zero_is_valid_for_other_bounded_fields(): + # Documents the asymmetry with sampling_time above: these fields use + # a strict "< 0 raises" check, so exactly 0 is accepted. + recipe = MezeRecipe(pressure=0.0, temperature=0.0, nb_cutoff=0.0) + assert recipe.pressure.atm().value() == pytest.approx(0.0) + assert recipe.temperature.kelvin().value() == pytest.approx(0.0) + assert recipe.nb_cutoff.angstroms().value() == pytest.approx(0.0) + + +def test_alchemical_recipe_dt_coerced_to_picoseconds(): + recipe = AlchemicalMezeRecipe(dt=0.002) + assert isinstance(recipe.dt, bss.Types.Time) + + +def test_alchemical_recipe_negative_dt_raises(): + with pytest.raises(ValueError, match="greater than or equal to 0"): + AlchemicalMezeRecipe(dt=-0.002) + + +# --------------------------------------------------------------------------- +# MezeRecipe.__getitem__ / __setitem__ / to_json +# --------------------------------------------------------------------------- + +def test_recipe_getitem(): + recipe = MezeRecipe(group_name="vim2") + assert recipe["group_name"] == "vim2" + + +def test_recipe_setitem(): + recipe = MezeRecipe() + recipe["group_name"] = "kpc2" + assert recipe.group_name == "kpc2" + + +def test_recipe_to_json_round_trip(tmp_path): + recipe = MezeRecipe(group_name="vim2", metal="ZN") + out_file = tmp_path / "recipe.json" + + recipe.to_json(str(out_file)) + + with open(out_file) as f: + data = json.load(f) + assert data["group_name"] == "vim2" + assert data["metal"] == "ZN" From ec457cbeb185be52b0034d26f782034310a56c5b Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 15:06:10 +0100 Subject: [PATCH 02/18] fix: check for the correct error from a model recipe --- tests/test_recipes.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index bb1f55f..e08b5ef 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -1,7 +1,7 @@ import json import pytest import BioSimSpace as bss - +from pydantic import ValidationError from meze import ( MezeRecipe, ColdMezeRecipe, @@ -10,10 +10,6 @@ ) -# --------------------------------------------------------------------------- -# MezeRecipe.validate_model -# --------------------------------------------------------------------------- - def test_model_none_passthrough(): assert MezeRecipe(model=None).model is None @@ -23,14 +19,10 @@ def test_model_coerced_to_int(): def test_model_invalid_raises(): - with pytest.raises(ValueError, match="Cannot covert"): + with pytest.raises(ValidationError, match="a valid integer"): MezeRecipe(model="abc") -# --------------------------------------------------------------------------- -# MezeRecipe.validate_temperature / validate_pressure / validate_cutoff_distance -# --------------------------------------------------------------------------- - def test_temperature_coerced_to_bss_type(): recipe = MezeRecipe(temperature=300.0) assert isinstance(recipe.temperature, bss.Types.Temperature) From 808b4ade88cd1c0b7da66e4312857c6f468bf381 Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 15:33:13 +0100 Subject: [PATCH 03/18] refactor: fix BSS typing: --- meze/sofra.py | 96 +++++++++++++++++++++++++-------------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/meze/sofra.py b/meze/sofra.py index d3f4803..0c469f4 100644 --- a/meze/sofra.py +++ b/meze/sofra.py @@ -34,11 +34,11 @@ from MDAnalysis.core.groups import Residue as mdaResidue import BioSimSpace as bss from BioSimSpace._SireWrappers import System as bssSystem -from BioSimSpace.Types._time import Time as bssTime -from BioSimSpace.Types._temperature import Temperature as bssTemperature -from BioSimSpace.Types._pressure import Pressure as bssPressure if TYPE_CHECKING: from BioSimSpace.Protocol._protocol import Protocol as bssProtocol + from BioSimSpace.Types._time import Time as bssTime + from BioSimSpace.Types._temperature import Temperature as bssTemperature + from BioSimSpace.Types._pressure import Pressure as bssPressure from .utils import ( _residue_restraint_mask, _write_distance_restraints, @@ -232,10 +232,10 @@ class ColdMezeRecipe(MezeRecipe): 2, ge=1, le=2, description="Type of barostat, 1: Berendsen, 2: MC" ) - runtime: Union[float, bssTime] = Field( + runtime: Union[float, bss.Types.Time] = Field( 100.0, description="Simulation time in picoseconds" ) - dt: Union[float, bssTime] = Field( + dt: Union[float, bss.Types.Time] = Field( 0.001, description="Integrator timestep, in picoseconds" ) start_temperature: float = Field( @@ -277,10 +277,10 @@ def validate_temperature_range(cls, value): class HotMezeRecipe(MezeRecipe): """Meze workflow recipe for production runs """ - runtime: Union[float, bssTime] = Field( + runtime: Union[float, bss.Types.Time] = Field( 100.0, description="Simulation time in nanoseconds" ) - dt: Union[float, bssTime] = Field( + dt: Union[float, bss.Types.Time] = Field( 0.002, description="Integrator timestep, in picoseconds" ) @@ -316,7 +316,7 @@ class AlchemicalMezeRecipe(MezeRecipe): n_lambdas: int = Field( 16, ge=3, description="Number of lambda windows" ) - sampling_time: Union[float, bssTime] = Field( + sampling_time: Union[float, bss.Types.Time] = Field( 4.0, description="Runtime for each lambda window in ns." ) restart_interval: int = Field( @@ -355,12 +355,12 @@ class AlchemicalMezeRecipe(MezeRecipe): @field_validator("sampling_time", mode="after") @classmethod def validate_sampling_time(cls, value): - if isinstance(value, bssTime): + if isinstance(value, bss.Types.Time): return value value = float(value) if value <= 0: raise ValueError("sampling_time must be greater than 0 ns") - return bssTime(value, "nanoseconds") + return bss.Types.Time(value, "nanoseconds") @field_validator("dt", mode="after") @classmethod @@ -2610,12 +2610,12 @@ def run( barostat: Optional[int] = None, n_sd_cycles: Optional[int] = None, nb_cutoff: Optional[float] = None, - timestep: Optional[Union[float, bssTime]] = None, - runtime: Optional[Union[float, bssTime]] = None, - temperature: Optional[Union[float, bssTemperature]] = None, - start_temperature: Optional[Union[float, bssTemperature]] = 300, - end_temperature: Optional[Union[float, bssTemperature]] = 300, - pressure: Optional[Union[float, bssPressure]] = None, + timestep: Optional[Union[float, "bssTime"]] = None, + runtime: Optional[Union[float, "bssTime"]] = None, + temperature: Optional[Union[float, "bssTemperature"]] = None, + start_temperature: Optional[Union[float, "bssTemperature"]] = 300, + end_temperature: Optional[Union[float, "bssTemperature"]] = 300, + pressure: Optional[Union[float, "bssPressure"]] = None, is_gpu: Optional[bool] = True, engine_executable: Optional["str"] = None, additional_positional_restraints: Optional[dict[str, Any]] = None, @@ -2768,11 +2768,11 @@ def heat( ] = None, restart: Optional[bool] = False, restraint_weight: Optional[float] = None, - timestep: Optional[Union[float, bssTemperature]] = None, - runtime: Optional[Union[float, bssTime]] = None, - temperature: Optional[Union[float, bssTemperature]] = None, - start_temperature: Optional[Union[float, bssTemperature]] = 300, - end_temperature: Optional[Union[float, bssTemperature]] = 300, + timestep: Optional[Union[float, "bssTemperature"]] = None, + runtime: Optional[Union[float, "bssTime"]] = None, + temperature: Optional[Union[float, "bssTemperature"]] = None, + start_temperature: Optional[Union[float, "bssTemperature"]] = 300, + end_temperature: Optional[Union[float, "bssTemperature"]] = 300, process_name: Optional[str] = "nvt", is_gpu: Optional[bool] = True, engine_executable: Optional[str] = None, @@ -2811,10 +2811,10 @@ def pressurise( ] = None, restart: Optional[bool] = False, restraint_weight: Optional[float] = None, - timestep: Optional[Union[float, bssTemperature]] = None, - runtime: Optional[Union[float, bssTime]] = None, - temperature: Optional[Union[float, bssTemperature]] = 300, - pressure: Optional[Union[float, bssPressure]] = 1.0, + timestep: Optional[Union[float, "bssTemperature"]] = None, + runtime: Optional[Union[float, "bssTime"]] = None, + temperature: Optional[Union[float, "bssTemperature"]] = 300, + pressure: Optional[Union[float, "bssPressure"]] = 1.0, process_name: Optional[str] = "npt", is_gpu: Optional[bool] = True, engine_executable: Optional[str] = None, @@ -2930,10 +2930,10 @@ def run( system: Optional[bssSystem] = None, process_name: Optional[str] = "meze-run", nb_cutoff: Optional[float] = None, - timestep: Optional[Union[float, bssTime]] = None, - runtime: Optional[Union[float, bssTime]] = None, - temperature: Optional[Union[float, bssTemperature]] = 300, - pressure: Optional[Union[float, bssPressure]] = 1, + timestep: Optional[Union[float, "bssTime"]] = None, + runtime: Optional[Union[float, "bssTime"]] = None, + temperature: Optional[Union[float, "bssTemperature"]] = 300, + pressure: Optional[Union[float, "bssPressure"]] = 1, engine_executable: Optional[str] = None, write_frequency: Optional[int] = 100000, distance_write_frequency: Optional[int] = 10000, @@ -3413,12 +3413,12 @@ def run( barostat: Optional[int] = None, n_sd_cycles: Optional[int] = None, nb_cutoff: Optional[float] = None, - timestep: Optional[Union[float, bssTime]] = None, - runtime: Optional[Union[float, bssTime]] = None, - temperature: Optional[Union[float, bssTemperature]] = None, - start_temperature: Optional[Union[float, bssTemperature]] = 300, - end_temperature: Optional[Union[float, bssTemperature]] = 300, - pressure: Optional[Union[float, bssPressure]] = None, + timestep: Optional[Union[float, "bssTime"]] = None, + runtime: Optional[Union[float, "bssTime"]] = None, + temperature: Optional[Union[float, "bssTemperature"]] = None, + start_temperature: Optional[Union[float, "bssTemperature"]] = 300, + end_temperature: Optional[Union[float, "bssTemperature"]] = 300, + pressure: Optional[Union[float, "bssPressure"]] = None, engine_executable: Optional[str] = None, qm_theory: Optional[str] = "DFTB3", metal_resids_for_distance_restraints: Optional[ @@ -3577,11 +3577,11 @@ def heat( system: Optional[bssSystem] = None, workdir: Optional[str] = None, restart: Optional[bool] = False, - timestep: Optional[Union[float, bssTemperature]] = 0.001, - runtime: Optional[Union[float, bssTime]] = None, - temperature: Optional[Union[float, bssTemperature]] = None, - start_temperature: Optional[Union[float, bssTemperature]] = 300, - end_temperature: Optional[Union[float, bssTemperature]] = 300, + timestep: Optional[Union[float, "bssTemperature"]] = 0.001, + runtime: Optional[Union[float, "bssTime"]] = None, + temperature: Optional[Union[float, "bssTemperature"]] = None, + start_temperature: Optional[Union[float, "bssTemperature"]] = 300, + end_temperature: Optional[Union[float, "bssTemperature"]] = 300, process_name: Optional[str] = "qm-nvt", engine_executable: Optional[str] = None, qm_theory: Optional[str] = "DFTB3", @@ -3620,10 +3620,10 @@ def pressurise( system: Optional[bssSystem] = None, workdir: Optional[str] = None, restart: Optional[bool] = False, - timestep: Optional[Union[float, bssTemperature]] = 0.001, - runtime: Optional[Union[float, bssTime]] = None, - temperature: Optional[Union[float, bssTemperature]] = 300, - pressure: Optional[Union[float, bssPressure]] = 1.0, + timestep: Optional[Union[float, "bssTemperature"]] = 0.001, + runtime: Optional[Union[float, "bssTime"]] = None, + temperature: Optional[Union[float, "bssTemperature"]] = 300, + pressure: Optional[Union[float, "bssPressure"]] = 1.0, process_name: Optional[str] = "qm-npt", engine_executable: Optional[str] = None, qm_theory: Optional[str] = "DFTB3", @@ -3718,10 +3718,10 @@ def run( process_name: Optional[str] = "qm-meze-run", ensemble: Optional[Literal["nvt", "npt"]] = "nvt", nb_cutoff: Optional[float] = None, - timestep: Optional[Union[float, bssTime]] = 0.001, - runtime: Optional[Union[float, bssTime]] = None, - temperature: Optional[Union[float, bssTemperature]] = 300, - pressure: Optional[Union[float, bssPressure]] = None, + timestep: Optional[Union[float, "bssTime"]] = 0.001, + runtime: Optional[Union[float, "bssTime"]] = None, + temperature: Optional[Union[float, "bssTemperature"]] = 300, + pressure: Optional[Union[float, "bssPressure"]] = None, engine_executable: Optional[str] = None, write_frequency: Optional[int] = 500, qm_theory: Optional[str] = "DFTB3", From 1acc7d1e76e29ee4038cd98052eb900f14e3137b Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 15:48:37 +0100 Subject: [PATCH 04/18] fix: make dt/sampling time > 0 --- meze/sofra.py | 10 +++++----- tests/test_recipes.py | 23 ++++------------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/meze/sofra.py b/meze/sofra.py index 0c469f4..21fb9ea 100644 --- a/meze/sofra.py +++ b/meze/sofra.py @@ -255,7 +255,7 @@ class ColdMezeRecipe(MezeRecipe): def validate_time(cls, value): if isinstance(value, bss.Types.Time): return value - if value < 0: + if value <= 0: raise ValueError( "dt, time must be greater than or equal to 0 picoseconds" ) @@ -289,9 +289,9 @@ class HotMezeRecipe(MezeRecipe): def validate_time(cls, value): if isinstance(value, bss.Types.Time): return value - if value < 0: + if value <= 0: raise ValueError( - "dt must be greater than or equal to 0 nanoseconds" + "dt must be greater than 0 nanoseconds" ) return bss.Types.Time(value, "nanoseconds") @@ -300,9 +300,9 @@ def validate_time(cls, value): def validate_timestep(cls, value): if isinstance(value, bss.Types.Time): return value - if value < 0: + if value <= 0: raise ValueError( - "dt must be greater than or equal to 0 picoseconds" + "dt must be greater than 0 picoseconds" ) return bss.Types.Time(value, "picoseconds") diff --git a/tests/test_recipes.py b/tests/test_recipes.py index e08b5ef..b98b5b4 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -59,10 +59,6 @@ def test_nb_cutoff_negative_raises(): MezeRecipe(nb_cutoff=-1.0) -# --------------------------------------------------------------------------- -# ColdMezeRecipe.validate_time / validate_temperature_range -# --------------------------------------------------------------------------- - def test_cold_recipe_dt_and_runtime_coerced_to_picoseconds(): recipe = ColdMezeRecipe(dt=0.001, runtime=100.0) assert isinstance(recipe.dt, bss.Types.Time) @@ -86,10 +82,6 @@ def test_cold_recipe_negative_start_temperature_raises(): ColdMezeRecipe(start_temperature=-1.0) -# --------------------------------------------------------------------------- -# HotMezeRecipe.validate_time / validate_timestep -# --------------------------------------------------------------------------- - def test_hot_recipe_runtime_coerced_to_nanoseconds(): recipe = HotMezeRecipe(runtime=100.0) assert isinstance(recipe.runtime, bss.Types.Time) @@ -97,8 +89,8 @@ def test_hot_recipe_runtime_coerced_to_nanoseconds(): def test_hot_recipe_negative_runtime_raises(): - with pytest.raises(ValueError, match="greater than or equal to 0"): - HotMezeRecipe(runtime=-1.0) + with pytest.raises(ValueError, match="greater than 0"): + HotMezeRecipe(runtime=0) def test_hot_recipe_dt_coerced_to_picoseconds(): @@ -108,14 +100,11 @@ def test_hot_recipe_dt_coerced_to_picoseconds(): def test_hot_recipe_negative_dt_raises(): - with pytest.raises(ValueError, match="greater than or equal to 0"): - HotMezeRecipe(dt=-0.002) + with pytest.raises(ValueError, match="greater than 0"): + HotMezeRecipe(dt=0) def test_cold_and_hot_runtime_use_different_units(): - # Same field name, same numeric input, different units: Cold is - # picoseconds, Hot is nanoseconds (1000x). Passing the same runtime - # to the wrong recipe class silently gives the wrong simulated time. cold = ColdMezeRecipe(runtime=1.0) hot = HotMezeRecipe(runtime=1.0) cold_in_ns = cold.runtime.nanoseconds().value() @@ -123,10 +112,6 @@ def test_cold_and_hot_runtime_use_different_units(): assert hot_in_ns == pytest.approx(cold_in_ns * 1000) -# --------------------------------------------------------------------------- -# AlchemicalMezeRecipe.validate_sampling_time / validate_picosecond_times -# --------------------------------------------------------------------------- - def test_alchemical_recipe_sampling_time_coerced_to_nanoseconds(): recipe = AlchemicalMezeRecipe(sampling_time=4.0) assert isinstance(recipe.sampling_time, bss.Types.Time) From 2bb20f331f7d04206a52f519589988eabef86252 Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 15:49:35 +0100 Subject: [PATCH 05/18] refactor: remove placeholder --- tests/test_placeholder.py | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 tests/test_placeholder.py diff --git a/tests/test_placeholder.py b/tests/test_placeholder.py deleted file mode 100644 index 51ac826..0000000 --- a/tests/test_placeholder.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_import(): - import meze From e00c515088b85d0165edd3427f72271df2927c29 Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 15:50:50 +0100 Subject: [PATCH 06/18] fix: alchemical dt > 0 --- meze/sofra.py | 2 +- tests/test_recipes.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/meze/sofra.py b/meze/sofra.py index 21fb9ea..3feda4d 100644 --- a/meze/sofra.py +++ b/meze/sofra.py @@ -367,7 +367,7 @@ def validate_sampling_time(cls, value): def validate_picosecond_times(cls, value): if isinstance(value, bss.Types.Time): return value - if value < 0: + if value <= 0: raise ValueError( "dt must be greater than or equal to 0 picoseconds" ) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index b98b5b4..5c9487a 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -119,8 +119,6 @@ def test_alchemical_recipe_sampling_time_coerced_to_nanoseconds(): def test_alchemical_recipe_zero_sampling_time_raises(): - # Unlike temperature/pressure/nb_cutoff/dt (which reject only < 0, - # so 0 is accepted), sampling_time specifically rejects <= 0. with pytest.raises(ValueError, match="greater than 0"): AlchemicalMezeRecipe(sampling_time=0) @@ -131,8 +129,6 @@ def test_alchemical_recipe_negative_sampling_time_raises(): def test_zero_is_valid_for_other_bounded_fields(): - # Documents the asymmetry with sampling_time above: these fields use - # a strict "< 0 raises" check, so exactly 0 is accepted. recipe = MezeRecipe(pressure=0.0, temperature=0.0, nb_cutoff=0.0) assert recipe.pressure.atm().value() == pytest.approx(0.0) assert recipe.temperature.kelvin().value() == pytest.approx(0.0) From 9e6e4815162fc119fcb1e6e0843ac569d7a5d0c7 Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 15:56:37 +0100 Subject: [PATCH 07/18] fix: add dt/sampling time = 0 tests --- tests/test_recipes.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 5c9487a..9fdf79d 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -67,10 +67,20 @@ def test_cold_recipe_dt_and_runtime_coerced_to_picoseconds(): def test_cold_recipe_negative_runtime_raises(): - with pytest.raises(ValueError, match="greater than or equal to 0"): + with pytest.raises(ValueError, match="greater than 0"): ColdMezeRecipe(runtime=-1.0) +def test_cold_recipe_zero_runtime_raises(): + with pytest.raises(ValueError, match="greater than 0"): + ColdMezeRecipe(runtime=0) + + +def test_hot_recipe_zero_dt_raises(): + with pytest.raises(ValueError, match="greater than 0"): + ColdMezeRecipe(dt=0) + + def test_cold_recipe_temperature_range_coerced(): recipe = ColdMezeRecipe(start_temperature=100.0, end_temperature=300.0) assert isinstance(recipe.start_temperature, bss.Types.Temperature) @@ -89,6 +99,11 @@ def test_hot_recipe_runtime_coerced_to_nanoseconds(): def test_hot_recipe_negative_runtime_raises(): + with pytest.raises(ValueError, match="greater than 0"): + HotMezeRecipe(runtime=-1.0) + + +def test_hot_recipe_zero_runtime_raises(): with pytest.raises(ValueError, match="greater than 0"): HotMezeRecipe(runtime=0) @@ -100,6 +115,11 @@ def test_hot_recipe_dt_coerced_to_picoseconds(): def test_hot_recipe_negative_dt_raises(): + with pytest.raises(ValueError, match="greater than 0"): + HotMezeRecipe(dt=-1.0) + + +def test_hot_recipe_cold_dt_raises(): with pytest.raises(ValueError, match="greater than 0"): HotMezeRecipe(dt=0) @@ -141,13 +161,14 @@ def test_alchemical_recipe_dt_coerced_to_picoseconds(): def test_alchemical_recipe_negative_dt_raises(): - with pytest.raises(ValueError, match="greater than or equal to 0"): + with pytest.raises(ValueError, match="greater than 0"): AlchemicalMezeRecipe(dt=-0.002) -# --------------------------------------------------------------------------- -# MezeRecipe.__getitem__ / __setitem__ / to_json -# --------------------------------------------------------------------------- +def test_alchemical_recipe_zero_dt_raises(): + with pytest.raises(ValueError, match="greater than 0"): + AlchemicalMezeRecipe(dt=0) + def test_recipe_getitem(): recipe = MezeRecipe(group_name="vim2") From cc205e2ab5ce501309fc0ec54029ed8d9dbe3d2f Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 16:09:41 +0100 Subject: [PATCH 08/18] test: add simple test for default recipe values --- meze/sofra.py | 6 ++---- tests/test_recipes.py | 42 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/meze/sofra.py b/meze/sofra.py index 3feda4d..4ecf8ec 100644 --- a/meze/sofra.py +++ b/meze/sofra.py @@ -106,13 +106,11 @@ class MezeRecipe(BaseModel): "g16", description="Gaussian version" ) memory: float = Field( - 12000, description="Memory for Gaussian calculations in MB" + 12_000, description="Memory for Gaussian calculations in MB" ) - nprocshared: int = Field( 8, description="Number of processors for Gaussian calculations" ) - only_optimise_hydrogens: bool = Field( True, description="Only optimise hydrogen atoms" ) @@ -206,7 +204,7 @@ def __setitem__(self, key: str, value): def to_json(self, file: str): with open(file, "w") as ofile: - ofile.write(self.model_dump_json(indent=2)) + ofile.write(self.model_dump_json(indent=2, fallback=str)) class ColdMezeRecipe(MezeRecipe): diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 9fdf79d..c3ed778 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -2,6 +2,7 @@ import pytest import BioSimSpace as bss from pydantic import ValidationError +import os from meze import ( MezeRecipe, ColdMezeRecipe, @@ -10,10 +11,6 @@ ) -def test_model_none_passthrough(): - assert MezeRecipe(model=None).model is None - - def test_model_coerced_to_int(): assert MezeRecipe(model="0").model == 0 @@ -191,3 +188,40 @@ def test_recipe_to_json_round_trip(tmp_path): data = json.load(f) assert data["group_name"] == "vim2" assert data["metal"] == "ZN" + + +def test_default_meze_recipe(tmp_path): + os.chdir(tmp_path) + recipe = MezeRecipe() + assert recipe.workdir == tmp_path + assert recipe.metal == "ZN" + assert recipe.metal_charge == 2 + assert recipe.coordination_cut_off == 2.8 + assert recipe.path_to_engine is None + assert recipe.model is None + assert recipe.gaussian_version == "g16" + assert recipe.memory == 12_000 + assert recipe.nprocshared == 8 + assert recipe.only_optimise_hydrogens is True + assert recipe.protein_forcefield == "ff14SB" + assert recipe.ligand_forcefield == "gaff2" + assert recipe.water_model == "tip3p" + assert recipe.box_shape == "octahedral" + assert recipe.box_edges == 10.0 + assert recipe.solvent_closeness == 0.75 + assert recipe.n_repeats == 3 + assert recipe.temperature._value == 300.0 + assert recipe.pressure._value == 1.0 + assert recipe.nb_cutoff == 12.0 + + +def test_cold_recipe_inherits_coerced_temperature_and_pressure(): + recipe = ColdMezeRecipe() + assert isinstance(recipe.temperature, bss.Types.Temperature) + assert isinstance(recipe.pressure, bss.Types.Pressure) + + +def test_hot_recipe_inherits_coerced_temperature_and_pressure(): + recipe = HotMezeRecipe() + assert isinstance(recipe.temperature, bss.Types.Temperature) + assert isinstance(recipe.pressure, bss.Types.Pressure) From 0a091bb0fbbd47241ad0197ea70bf5a23fb57060 Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 16:22:12 +0100 Subject: [PATCH 09/18] fix: error messages on meze recipe --- meze/sofra.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meze/sofra.py b/meze/sofra.py index 4ecf8ec..2fdc69b 100644 --- a/meze/sofra.py +++ b/meze/sofra.py @@ -255,7 +255,7 @@ def validate_time(cls, value): return value if value <= 0: raise ValueError( - "dt, time must be greater than or equal to 0 picoseconds" + "dt, time must be greater than 0 picoseconds" ) return bss.Types.Time(value, "picoseconds") @@ -367,7 +367,7 @@ def validate_picosecond_times(cls, value): return value if value <= 0: raise ValueError( - "dt must be greater than or equal to 0 picoseconds" + "dt must be greater than 0 picoseconds" ) return bss.Types.Time(value, "picoseconds") From 1a1153bdc91dade84ded82e3fd0c2cc7967cec7a Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 16:26:45 +0100 Subject: [PATCH 10/18] ci: update max line length --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1dd18b5..b8bb262 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,7 @@ jobs: run: flake8 meze/ --count --select=E9,F63,F7,F82 --show-source --statistics - name: Lint (style/complexity report only) - run: flake8 meze/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + run: flake8 meze/ --count --exit-zero --max-complexity=10 --max-line-length=79 --statistics test: runs-on: ubuntu-latest From d5f285c91d206d90c79eef5b84d521ba2118e26a Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 16:27:05 +0100 Subject: [PATCH 11/18] fix: access nb_cutoff value --- tests/test_recipes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index c3ed778..cce17cd 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -193,7 +193,7 @@ def test_recipe_to_json_round_trip(tmp_path): def test_default_meze_recipe(tmp_path): os.chdir(tmp_path) recipe = MezeRecipe() - assert recipe.workdir == tmp_path + assert recipe.workdir == str(tmp_path) assert recipe.metal == "ZN" assert recipe.metal_charge == 2 assert recipe.coordination_cut_off == 2.8 @@ -212,7 +212,7 @@ def test_default_meze_recipe(tmp_path): assert recipe.n_repeats == 3 assert recipe.temperature._value == 300.0 assert recipe.pressure._value == 1.0 - assert recipe.nb_cutoff == 12.0 + assert recipe.nb_cutoff._value == 12.0 def test_cold_recipe_inherits_coerced_temperature_and_pressure(): From d30a1cbfea3ceb8c4ff6a666e4e7f436ec46105b Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 16:53:44 +0100 Subject: [PATCH 12/18] fix: add lomap parsing back in --- meze/sofra.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meze/sofra.py b/meze/sofra.py index 2fdc69b..7bfea8c 100644 --- a/meze/sofra.py +++ b/meze/sofra.py @@ -4336,7 +4336,7 @@ def set_ligand_network( log.info("Lomap finished succesfully. Parsing outputs.") self.transformations, self.lomap_scores, network_file = ( - lomap_directory, f"{self.group_name}_score_with_connection.txt" + self._parse_lomap_output(scores_file, lomap_directory) ) self.save_network_file(network_file) From 3d0ba91c68af8c0d56736d520b877910126892ca Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 17:18:37 +0100 Subject: [PATCH 13/18] test: validate lomap output parsing --- tests/test_network.py | 88 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/test_network.py diff --git a/tests/test_network.py b/tests/test_network.py new file mode 100644 index 0000000..d81f464 --- /dev/null +++ b/tests/test_network.py @@ -0,0 +1,88 @@ +from meze import Sofra +import pytest + + +def test_sofra_parse_lomap_output(tmp_path): + sofra = Sofra( + mezes={}, + sofra_file="dummy.json", + sofra_contents={"a": "b"} + ) + lomap_file = tmp_path / "lomap.txt" + lomap_file.write_text( + "Index_1 ,Index_2 ,Filename_1 ,Filename_2 " + " ,Str_sim ,Eff_sim ,Loose_sim ,Connect \n" + "0 ,1 ,ligand_11.sdf ,ligand_12.sdf " + " ,0.74082 ,0.74082 ,0.74082 ,Yes ," + "0:19,1:0,2:1,3:2,4:3,5:4,6:5,7:6,8:7,9:8,10:9,11:10,12:11,13:12,14:13" + ",15:14,16:15,17:16,18:17,19:18,20:25,21:26,22:24,23:27,24:28,25:29," + "26:30,27:31,28:21,29:32,30:33" + ) + transf, scores, file = sofra._parse_lomap_output( + lomap_file, + tmp_path + ) + assert transf == [("ligand_11", "ligand_12")] + assert scores == [0.74082] + assert file == str(tmp_path / f"{sofra.group_name}_lomap_network.csv") + assert (tmp_path / file).read_text() == ( + "Name_1,Name_2,Score\n" + "ligand_11,ligand_12,0.74082\n" + ) + + +def test_sofra_parse_lomap_output_raises(tmp_path): + sofra = Sofra( + mezes={}, + sofra_file="dummy.json", + sofra_contents={"a": "b"} + ) + lomap_file = tmp_path / "lomap.txt" + lomap_file.write_text( + "Index_1 ,Index_2 ,Filename_1 ,Filename_2 " + " ,Str_sim ,Eff_sim ,Loose_sim ,Connect \n" + "0 ,1 ,ligand_11.sdf ,ligand_12.sdf " + " ,0.74082 ,0.74082 ,0.74082 ,No ," + "0:19,1:0,2:1,3:2,4:3,5:4,6:5,7:6,8:7,9:8,10:9,11:10,12:11,13:12,14:13" + ",15:14,16:15,17:16,18:17,19:18,20:25,21:26,22:24,23:27,24:28,25:29," + "26:30,27:31,28:21,29:32,30:33" + ) + with pytest.raises(RuntimeError): + sofra._parse_lomap_output( + lomap_file, + tmp_path + ) + + +def test_sofra_parse_lomap_output_mixed_rows(tmp_path): + sofra = Sofra( + mezes={}, + sofra_file="dummy.json", + sofra_contents={"a": "b"} + ) + lomap_file = tmp_path / "lomap.txt" + lomap_file.write_text( + "Index_1 ,Index_2 ,Filename_1 ,Filename_2 " + " ,Str_sim ,Eff_sim ,Loose_sim ,Connect \n" + "0 ,1 ,ligand_11.sdf ,ligand_12.sdf " + " ,0.74082 ,0.74082 ,0.74082 ,Yes ," + "0:19,1:0,2:1,3:2,4:3,5:4,6:5,7:6,8:7,9:8,10:9,11:10,12:11,13:12,14:13" + ",15:14,16:15,17:16,18:17,19:18,20:25,21:26,22:24,23:27,24:28,25:29," + "26:30,27:31,28:21,29:32,30:33" + "2 ,3 ,ligand_11.sdf ,ligand_12.sdf " + " ,0.74082 ,0.74082 ,0.74082 ,No ," + "0:19,1:0,2:1,3:2,4:3,5:4,6:5,7:6,8:7,9:8,10:9,11:10,12:11,13:12,14:13" + ",15:14,16:15,17:16,18:17,19:18,20:25,21:26,22:24,23:27,24:28,25:29," + "26:30,27:31,28:21,29:32,30:33" + ) + transf, scores, file = sofra._parse_lomap_output( + lomap_file, + tmp_path + ) + assert transf == [("ligand_11", "ligand_12")] + assert scores == [0.74082] + assert file == str(tmp_path / f"{sofra.group_name}_lomap_network.csv") + assert (tmp_path / file).read_text() == ( + "Name_1,Name_2,Score\n" + "ligand_11,ligand_12,0.74082\n" + ) From 8e919eeff9aa277d7a338b1a9dccc4306c83dcfc Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 17:24:24 +0100 Subject: [PATCH 14/18] test: claude-written ligand static methods tests --- tests/test_ligand.py | 80 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/test_ligand.py diff --git a/tests/test_ligand.py b/tests/test_ligand.py new file mode 100644 index 0000000..830f684 --- /dev/null +++ b/tests/test_ligand.py @@ -0,0 +1,80 @@ +import pytest + +from meze import Ligand + + +# --------------------------------------------------------------------------- +# Ligand._validate_file +# --------------------------------------------------------------------------- + +def test_validate_file_str_wrapped_in_list(): + assert Ligand._validate_file("ligand_11.pdb") == ["ligand_11.pdb"] + + +def test_validate_file_list_passthrough(): + files = ["ligand_11.pdb", "ligand_11.mol2"] + assert Ligand._validate_file(files) == files + + +def test_validate_file_too_many_raises(): + with pytest.raises(ValueError, match="Too many values"): + Ligand._validate_file(["a.pdb", "b.pdb", "c.pdb"]) + + +def test_validate_file_wrong_type_raises(): + with pytest.raises(TypeError, match="Expected str or list"): + Ligand._validate_file(123) + + +# --------------------------------------------------------------------------- +# Ligand._check_files_exist +# --------------------------------------------------------------------------- + +def test_check_files_exist_passes_for_real_files(tmp_path): + file_1 = tmp_path / "a.pdb" + file_2 = tmp_path / "b.pdb" + file_1.write_text("dummy") + file_2.write_text("dummy") + + # no exception raised + Ligand._check_files_exist([str(file_1), str(file_2)]) + + +def test_check_files_exist_missing_file_raises(): + with pytest.raises(FileNotFoundError, match="Ligand file not found"): + Ligand._check_files_exist(["/nonexistent/ligand.pdb"]) + + +# --------------------------------------------------------------------------- +# Ligand._validate_charge +# --------------------------------------------------------------------------- + +def test_validate_charge_float_passthrough(): + assert Ligand._validate_charge(1.0) == 1.0 + + +def test_validate_charge_int_coerced_to_float(): + charge = Ligand._validate_charge(1) + assert charge == 1.0 + assert isinstance(charge, float) + + +def test_validate_charge_string_coerced_to_float(): + charge = Ligand._validate_charge("-1") + assert charge == -1.0 + assert isinstance(charge, float) + + +def test_validate_charge_invalid_raises(): + with pytest.raises(TypeError, match="must be an integer or float"): + Ligand._validate_charge("abc") + + +# --------------------------------------------------------------------------- +# Ligand._infer_ligand_name +# --------------------------------------------------------------------------- + +def test_infer_ligand_name_uses_file_stem(): + with pytest.warns(UserWarning, match="inferring from file name"): + name = Ligand._infer_ligand_name(["some/dir/ligand_11.pdb"]) + assert name == "ligand_11" From 9b48854f8a979c518cbab025050daabe068c6ca4 Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 17:25:34 +0100 Subject: [PATCH 15/18] refactor: rename claude's funcitons --- tests/test_ligand.py | 7 +------ tests/test_recipes.py | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/test_ligand.py b/tests/test_ligand.py index 830f684..1fc9c9f 100644 --- a/tests/test_ligand.py +++ b/tests/test_ligand.py @@ -1,17 +1,12 @@ import pytest - from meze import Ligand -# --------------------------------------------------------------------------- -# Ligand._validate_file -# --------------------------------------------------------------------------- - def test_validate_file_str_wrapped_in_list(): assert Ligand._validate_file("ligand_11.pdb") == ["ligand_11.pdb"] -def test_validate_file_list_passthrough(): +def test_validate_file_list(): files = ["ligand_11.pdb", "ligand_11.mol2"] assert Ligand._validate_file(files) == files diff --git a/tests/test_recipes.py b/tests/test_recipes.py index cce17cd..858de86 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -25,7 +25,7 @@ def test_temperature_coerced_to_bss_type(): assert isinstance(recipe.temperature, bss.Types.Temperature) -def test_temperature_bss_type_passthrough(): +def test_temperature_bss_type(): temperature = bss.Types.Temperature(310.0, "kelvin") recipe = MezeRecipe(temperature=temperature) assert recipe.temperature is temperature From 28b7b05b725abdf74918a7b94c97d1af4bd8e361 Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 17:29:22 +0100 Subject: [PATCH 16/18] fix: raise errors via log --- meze/ligand.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/meze/ligand.py b/meze/ligand.py index e8bfd11..280fef7 100644 --- a/meze/ligand.py +++ b/meze/ligand.py @@ -61,15 +61,16 @@ def _validate_file(file): return [file] elif isinstance(file, list): if len(file) > 2: - raise ValueError( + message = ( f"Too many values for 'file': {file}." - f"Expected a 'str' or a list of at most 2 input files." + "Expected a 'str' or a list of at most 2 input files." ) + log.error(message) + raise ValueError(message) return file - - raise TypeError( - f"Expected str or list[str], got {type(file)}" - ) + message = f"Expected str or list[str], got {type(file)}" + log.error(message) + raise TypeError(message) @staticmethod def _check_files_exist(files): @@ -84,10 +85,12 @@ def _validate_charge(charge): try: return float(charge) except (TypeError, ValueError): - raise TypeError( - f"Ligand charge must be an integer or float " + message = ( + "Ligand charge must be an integer or float " f"(got {charge} of type {type(charge)})." ) + log.error(message) + raise TypeError(message) @staticmethod def _infer_ligand_name(file): @@ -124,7 +127,10 @@ def parameterise(self, directory = os.getcwd() if len(self.file) > 1: - raise UserWarning(f"Expected one ligand file but got {self.file}") + warnings.warn( + f"Expected one ligand file but got {self.file}", + UserWarning + ) else: file = self.file[0] From 492f5534eee6b673a1f2768dd343f3559e73e39e Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 17:30:57 +0100 Subject: [PATCH 17/18] refactor: fix style --- tests/test_ligand.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/tests/test_ligand.py b/tests/test_ligand.py index 1fc9c9f..8dda3d2 100644 --- a/tests/test_ligand.py +++ b/tests/test_ligand.py @@ -21,17 +21,12 @@ def test_validate_file_wrong_type_raises(): Ligand._validate_file(123) -# --------------------------------------------------------------------------- -# Ligand._check_files_exist -# --------------------------------------------------------------------------- - def test_check_files_exist_passes_for_real_files(tmp_path): file_1 = tmp_path / "a.pdb" file_2 = tmp_path / "b.pdb" file_1.write_text("dummy") file_2.write_text("dummy") - # no exception raised Ligand._check_files_exist([str(file_1), str(file_2)]) @@ -40,11 +35,7 @@ def test_check_files_exist_missing_file_raises(): Ligand._check_files_exist(["/nonexistent/ligand.pdb"]) -# --------------------------------------------------------------------------- -# Ligand._validate_charge -# --------------------------------------------------------------------------- - -def test_validate_charge_float_passthrough(): +def test_validate_charge_float(): assert Ligand._validate_charge(1.0) == 1.0 @@ -65,10 +56,6 @@ def test_validate_charge_invalid_raises(): Ligand._validate_charge("abc") -# --------------------------------------------------------------------------- -# Ligand._infer_ligand_name -# --------------------------------------------------------------------------- - def test_infer_ligand_name_uses_file_stem(): with pytest.warns(UserWarning, match="inferring from file name"): name = Ligand._infer_ligand_name(["some/dir/ligand_11.pdb"]) From 9eab395f56b3dd73c4a842956b739fa9537c57ea Mon Sep 17 00:00:00 2001 From: "jasmin.guven@hotmail.com" Date: Mon, 10 Aug 2026 17:34:24 +0100 Subject: [PATCH 18/18] test: claude-written tests for helpers --- tests/test_helpers.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/test_helpers.py diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..0b6c67c --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,24 @@ +from unittest.mock import patch +import pytest +from meze.helpers import _check_ambertools + + +def test_check_ambertools_all_found_on_path(): + with patch("meze.helpers.shutil.which", return_value="/usr/bin/tool"): + _check_ambertools() + + +def test_check_ambertools_found_via_amberhome(): + with patch("meze.helpers.shutil.which", return_value=None), \ + patch.dict("os.environ", {"AMBERHOME": "/opt/amber"}, clear=True), \ + patch("meze.helpers.os.path.exists", return_value=True): + _check_ambertools() + + +def test_check_ambertools_missing_raises(): + with patch("meze.helpers.shutil.which", return_value=None), \ + patch.dict("os.environ", {}, clear=True): + with pytest.raises( + RuntimeError, match="AmberTools installation required" + ): + _check_ambertools()