新增pod_bridge吊舱桥接中间件(树莓派4B部署)
- pod_bridge: 协议G版编解码/地理位置引导律/PX4网关/串口链路/闭环主循环 - 含三项安全修复: position_time_ms拆分、表2-2锁定门控、run()异常护栏 - 测试92条全绿; 附树莓派4B(Ubuntu 24.04)部署注意事项与最小依赖清单
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""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 == ""
|
||||
Reference in New Issue
Block a user