Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions src/virtualship/cli/_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, VerticalScroll
from textual.dom import NoMatches
from textual.markup import escape
from textual.screen import ModalScreen, Screen
from textual.validation import Function, Integer
from textual.widgets import (
Expand Down Expand Up @@ -530,11 +531,25 @@ def _update_instrument_configs(self):
kwargs["sensors"] = (
sensors if sensors else _default_sensors(config_class)
)
setattr(
self.expedition.instruments_config,
instrument_name,
config_class(**kwargs),
)
try:
setattr(
self.expedition.instruments_config,
instrument_name,
config_class(**kwargs),
)
except (ValueError, Exception) as e:
# catch validation errors, e.g. drift_days >= cycle_days
if isinstance(e, ValueError):
title = info.get("title", instrument_name.replace("_", " ").title())
raise UserError(f"'{title}' configuration error: {e}") from None
elif ( # pydantic validation error
hasattr(e, "__class__")
and "ValidationError" in e.__class__.__name__
):
title = info.get("title", instrument_name.replace("_", " ").title())
raise UserError(f"'{title}' configuration error: {e}") from None
else:
raise

def _update_schedule(self):
for i, wp in enumerate(self.expedition.schedule.waypoints):
Expand Down Expand Up @@ -1150,7 +1165,9 @@ def save_pressed(self) -> None:

except Exception as e:
self.notify(
f"*** Error saving changes ***:\n\n{e}\n",
escape(
f"*** Error saving changes ***:\n\n{e}\n"
), # escape avoids issues with special characters being interpreted as markup
severity="error",
timeout=20,
)
Expand Down
9 changes: 9 additions & 0 deletions src/virtualship/models/expedition.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,15 @@ class ArgoFloatConfig(_InstrumentConfigMixin, pydantic.BaseModel):

model_config = pydantic.ConfigDict(populate_by_name=True)

@pydantic.model_validator(mode="after")
def _drift_days_less_than_cycle_days(self) -> ArgoFloatConfig:
"""Ensure drift_days is less than cycle_days."""
if self.drift_days >= self.cycle_days:
raise ValueError(
f"drift_days ({self.drift_days}) must be less than cycle_days ({self.cycle_days}). "
)
return self


@register_instrument_config(InstrumentType.ADCP)
class ADCPConfig(_InstrumentConfigMixin, pydantic.BaseModel):
Expand Down
29 changes: 29 additions & 0 deletions tests/instruments/test_argo_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,32 @@ def test_argo_config_unsupported_sensor_rejected():
stationkeeping_time_minutes=10,
sensors=[SensorConfig(sensor_type=SensorType.OXYGEN)],
)


def test_argo_config_drift_days_exceeds_cycle_days():
"""ArgoFloatConfig should reject drift_days >= cycle_days."""
base_kwargs = {
"min_depth_meter": 0.0,
"max_depth_meter": -2000,
"drift_depth_meter": -1000,
"vertical_speed_meter_per_second": -0.10,
"lifetime": timedelta(days=30),
"stationkeeping_time_minutes": 10,
}

# drift_days > cycle_days should raise validation error
with pytest.raises(
pydantic.ValidationError, match="drift_days .* must be less than cycle_days"
):
ArgoFloatConfig(**base_kwargs, cycle_days=10, drift_days=15)

# drift_days == cycle_days should also raise validation error
with pytest.raises(
pydantic.ValidationError, match="drift_days .* must be less than cycle_days"
):
ArgoFloatConfig(**base_kwargs, cycle_days=10, drift_days=10)

# check a valid configuration: drift_days < cycle_days
config = ArgoFloatConfig(**base_kwargs, cycle_days=10, drift_days=9)
assert config.drift_days == 9
assert config.cycle_days == 10
Loading