import pytest from lingkong_motor_service.config import ConfigError, Settings def test_defaults_match_connected_ms3008_installation() -> None: settings = Settings() assert settings.can_channel == 0 assert settings.can_device == 0 assert settings.can_bitrate == 1_000_000 assert settings.motor_ids == (1, 2) assert settings.open_control_raw == 400 assert settings.close_control_raw == 400 assert settings.broadcast_frequency_hz == 10 assert settings.derate_after_s == 5 assert settings.derated_control_raw == 100 assert settings.temperature_limit_c == 80 assert settings.temperature_poll_interval_s == 0.5 assert settings.http_host == "0.0.0.0" assert settings.api_token is None @pytest.mark.parametrize("field", ["open_control_raw", "close_control_raw"]) @pytest.mark.parametrize("value", [851, 0, True, 1.5]) def test_rejects_invalid_open_loop_magnitude(field: str, value: object) -> None: with pytest.raises(ConfigError, match="1..850"): Settings(**{field: value}).validate() @pytest.mark.parametrize("motor_ids", [(), (1, 1), (0,), (3,), (5,), (True,)]) def test_motor_ids_are_nonempty_unique_and_in_broadcast_range( motor_ids: tuple[int, ...], ) -> None: with pytest.raises(ConfigError, match="MOTOR_IDS"): Settings(motor_ids=motor_ids).validate() def test_environment_values_are_parsed(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("MOTOR_CAN_CHANNEL", "1") monkeypatch.setenv("MOTOR_CAN_DEVICE", "2") monkeypatch.setenv("MOTOR_IDS", "2, 1") monkeypatch.setenv("MOTOR_OPEN_CONTROL_RAW", "123") monkeypatch.setenv("MOTOR_CLOSE_CONTROL_RAW", "234") monkeypatch.setenv("MOTOR_DERATE_AFTER_S", "6") monkeypatch.setenv("MOTOR_DERATED_CONTROL_RAW", "90") monkeypatch.setenv("MOTOR_TEMPERATURE_LIMIT_C", "75") monkeypatch.setenv("MOTOR_TEMPERATURE_POLL_INTERVAL_S", "0.25") settings = Settings.from_env() assert settings.can_channel == 1 assert settings.can_device == 2 assert settings.motor_ids == (2, 1) assert settings.open_control_raw == 123 assert settings.close_control_raw == 234 assert settings.derate_after_s == 6 assert settings.derated_control_raw == 90 assert settings.temperature_limit_c == 75 assert settings.temperature_poll_interval_s == 0.25 @pytest.mark.parametrize("channel", [-1, 2, "CAN1", True]) def test_canalystii_channel_is_physical_zero_or_one(channel: object) -> None: with pytest.raises(ConfigError, match="panel CAN1"): Settings(can_channel=channel).validate() @pytest.mark.parametrize("device", [-1, True, "0"]) def test_canalystii_device_index_is_non_negative_integer(device: object) -> None: with pytest.raises(ConfigError, match="MOTOR_CAN_DEVICE"): Settings(can_device=device).validate() def test_broadcast_frequency_is_loaded_from_environment( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("MOTOR_BROADCAST_FREQUENCY_HZ", "250") assert Settings.from_env().broadcast_frequency_hz == 250 @pytest.mark.parametrize("frequency", [False, float("nan"), 0.0, 1001.0]) def test_broadcast_frequency_must_be_finite_and_bounded(frequency: float) -> None: with pytest.raises(ConfigError, match="MOTOR_BROADCAST_FREQUENCY_HZ"): Settings(broadcast_frequency_hz=frequency).validate() def test_external_http_binding_does_not_require_api_token() -> None: Settings(http_host="0.0.0.0", api_token=None).validate() @pytest.mark.parametrize( ("field", "value", "message"), [ ("derate_after_s", 0, "MOTOR_DERATE_AFTER_S"), ("derated_control_raw", 851, "MOTOR_DERATED_CONTROL_RAW"), ("temperature_limit_c", 128, "MOTOR_TEMPERATURE_LIMIT_C"), ("temperature_poll_interval_s", 0, "MOTOR_TEMPERATURE_POLL_INTERVAL_S"), ], ) def test_power_and_temperature_settings_are_bounded( field: str, value: object, message: str ) -> None: with pytest.raises(ConfigError, match=message): Settings(**{field: value}).validate() def test_derated_value_cannot_increase_the_configured_output() -> None: with pytest.raises(ConfigError, match="must not exceed"): Settings(open_control_raw=50, derated_control_raw=100).validate()