"""config_test.py —— 桥接配置加载与校验单元测试。""" import json from pathlib import Path import pytest from pod_bridge.config import (BridgeConfig, GuidanceConfig, PodSerialConfig, load_from_json) _DEFAULT_CONFIG = Path(__file__).resolve().parents[2] / "config" / "pod_bridge.json" def _write(tmp_path, obj) -> str: p = tmp_path / "cfg.json" p.write_text(json.dumps(obj), encoding="utf-8") return str(p) def test_shipped_default_config_loads(): cfg = load_from_json(str(_DEFAULT_CONFIG)) assert cfg.pod_serial.baudrate == 115200 assert cfg.px4.connection_string == "udp://:14540" assert cfg.link.attitude_downlink_hz == pytest.approx(20.0) assert cfg.guidance.max_speed_mps == pytest.approx(5.0) assert cfg.guidance.stop_radius_m == pytest.approx(5.0) def test_empty_json_uses_all_defaults(tmp_path): cfg = load_from_json(_write(tmp_path, {})) default = BridgeConfig() assert cfg.pod_serial == default.pod_serial assert cfg.px4 == default.px4 assert cfg.link == default.link assert cfg.guidance == default.guidance def test_partial_override_keeps_other_defaults(tmp_path): cfg = load_from_json(_write(tmp_path, { "pod_serial": {"port": "COM3"}, "guidance": {"max_speed_mps": 8.0}, })) assert cfg.pod_serial.port == "COM3" assert cfg.pod_serial.baudrate == 115200 # 默认回退 assert cfg.guidance.max_speed_mps == 8.0 assert cfg.guidance.stop_radius_m == GuidanceConfig().stop_radius_m # 默认回退 def test_full_override_roundtrip(tmp_path): obj = { "pod_serial": {"port": "/dev/ttyUSB0", "baudrate": 115200}, "px4": {"connection_string": "serial:///dev/ttyS1:57600"}, "link": {"attitude_downlink_hz": 30.0, "telemetry_timeout_ms": 800}, "guidance": {"frequency_hz": 20.0, "stop_radius_m": 3.0, "max_speed_mps": 7.5, "speed_kp": 0.8, "yaw_kp": 1.5}, } cfg = load_from_json(_write(tmp_path, obj)) assert cfg.px4.connection_string == "serial:///dev/ttyS1:57600" assert cfg.link.attitude_downlink_hz == 30.0 assert cfg.link.telemetry_timeout_ms == 800 assert cfg.guidance.frequency_hz == 20.0 assert cfg.guidance.yaw_kp == 1.5 # --- 校验错误 --------------------------------------------------------------- def test_missing_file_raises(): with pytest.raises(ValueError, match="打开失败"): load_from_json(str(_DEFAULT_CONFIG.parent / "no_such_file.json")) def test_non_object_root_raises(tmp_path): p = tmp_path / "bad.json" p.write_text("[1, 2, 3]", encoding="utf-8") with pytest.raises(ValueError, match="根节点必须为对象"): load_from_json(str(p)) def test_malformed_json_raises(tmp_path): p = tmp_path / "bad.json" p.write_text("{not json", encoding="utf-8") with pytest.raises(ValueError, match="解析失败"): load_from_json(str(p)) def test_wrong_type_raises_with_field_name(tmp_path): with pytest.raises(ValueError, match="guidance.max_speed_mps"): load_from_json(_write(tmp_path, {"guidance": {"max_speed_mps": "fast"}})) def test_unknown_key_rejected(tmp_path): with pytest.raises(ValueError, match="未知字段"): load_from_json(_write(tmp_path, {"guidance": {"max_speed": 5.0}})) def test_empty_connection_string_raises(tmp_path): with pytest.raises(ValueError, match="px4.connection_string"): load_from_json(_write(tmp_path, {"px4": {"connection_string": ""}})) @pytest.mark.parametrize("obj,field", [ ({"link": {"attitude_downlink_hz": 100.0}}, "link.attitude_downlink_hz"), # >60 ({"link": {"attitude_downlink_hz": 0.5}}, "link.attitude_downlink_hz"), # <1 ({"guidance": {"max_speed_mps": -1.0}}, "guidance.max_speed_mps"), # 非正 ({"guidance": {"stop_radius_m": -2.0}}, "guidance.stop_radius_m"), # 负 ({"guidance": {"speed_derivative_filter_coef": 1.0}}, # 上界开 "guidance.speed_derivative_filter_coef"), ({"pod_serial": {"baudrate": 0}}, "pod_serial.baudrate"), # 非正 ]) def test_out_of_range_raises(tmp_path, obj, field): with pytest.raises(ValueError, match=field.replace(".", r"\.")): load_from_json(_write(tmp_path, obj)) def test_pod_serial_default_port_is_placeholder(): assert PodSerialConfig().port == ""