172 lines
5.7 KiB
Python
172 lines
5.7 KiB
Python
"""vtc_config_test.py —— TrackingConfig::load_from_json 单元测试。
|
|
|
|
移植自 reference-c++/visual_track_control/tests/vtc_config_test.cpp。
|
|
覆盖(物理追踪思路 §8、需求分析 NFR-007):真实配置文件加载成功与取值
|
|
正确(含 A1/A2 修改后的字段结构)、缺字段 / 缺分组 / 非法枚举 / 非法数值 /
|
|
文件缺失均抛带字段名的 ValueError。
|
|
|
|
C++ 版用字符串替换构造非法变体;Python 版改为 dict 变异(更稳健),
|
|
临时文件经 pytest 的 tmp_path 提供。
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from visual_track_control.config import (HeadingMode, NoDistanceAction,
|
|
load_from_json)
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
REAL_CONFIG = _REPO_ROOT / "reference-c++" / "visual_track_control" / "config" / "track_control.json"
|
|
|
|
|
|
def full_config() -> dict:
|
|
"""与真实配置同构的完整合法配置(用于派生各非法变体)。"""
|
|
return {
|
|
"control": {
|
|
"frequency_hz": 20.0,
|
|
"visual_stale_ms": 200,
|
|
"radar_stale_ms": 500,
|
|
"attitude_stale_ms": 300,
|
|
},
|
|
"camera": {
|
|
"image_width": 1920,
|
|
"image_height": 1080,
|
|
"fov_h_deg": 60.0,
|
|
"fov_v_deg": 35.0,
|
|
},
|
|
"heading": {
|
|
"mode": "rate",
|
|
"gain": 1.0,
|
|
"yaw_rate_limit_dps": 30.0,
|
|
"yaw_slew_limit_dps2": 60.0,
|
|
},
|
|
"vertical": {
|
|
"vz_gain_mps_per_deg": 0.1,
|
|
"vz_limit_mps": 2.0,
|
|
},
|
|
"distance": {
|
|
"d_exp_m": 10.0,
|
|
"kp": 0.5,
|
|
"ki": 0.05,
|
|
"kd": 0.1,
|
|
"integral_limit": 2.0,
|
|
"derivative_filter_coef": 0.2,
|
|
"approach_velocity_limit_mps": 3.0,
|
|
"retreat_velocity_limit_mps": 1.0,
|
|
"no_distance_action": "hold",
|
|
"no_distance_approach_limit_mps": 0.5,
|
|
},
|
|
"accel_limit": {
|
|
"ax_mps2": 2.0,
|
|
"ay_mps2": 2.0,
|
|
"az_mps2": 1.5,
|
|
},
|
|
}
|
|
|
|
|
|
def write_temp_json(tmp_path: Path, config: dict, name: str) -> str:
|
|
path = tmp_path / name
|
|
path.write_text(json.dumps(config), encoding="utf-8")
|
|
return str(path)
|
|
|
|
|
|
def test_load_real_config_succeeds():
|
|
cfg = load_from_json(str(REAL_CONFIG))
|
|
assert cfg.control.frequency_hz == 20.0
|
|
assert cfg.control.visual_stale_ms == 200
|
|
assert cfg.camera.image_width == 1920
|
|
assert cfg.camera.image_height == 1080
|
|
assert cfg.camera.fov_h_deg == 60.0
|
|
assert cfg.heading.mode is HeadingMode.RATE # A2:默认速率式
|
|
assert cfg.heading.yaw_rate_limit_dps == 30.0
|
|
assert cfg.vertical.vz_gain_mps_per_deg == 0.1
|
|
assert cfg.vertical.vz_limit_mps == 2.0
|
|
assert cfg.distance.d_exp_m == 10.0
|
|
assert cfg.distance.no_distance_action is NoDistanceAction.HOLD
|
|
assert cfg.accel_limit.ax_mps2 == 2.0
|
|
|
|
|
|
def test_full_json_round_trip_matches_expected(tmp_path):
|
|
# 内嵌合法配置与真实配置文件行为一致
|
|
path = write_temp_json(tmp_path, full_config(), "vtc_cfg_full.json")
|
|
cfg = load_from_json(path)
|
|
assert cfg.heading.mode is HeadingMode.RATE
|
|
assert cfg.distance.kp == 0.5
|
|
|
|
|
|
def test_position_mode_accepted(tmp_path):
|
|
config = full_config()
|
|
config["heading"]["mode"] = "position"
|
|
path = write_temp_json(tmp_path, config, "vtc_cfg_position.json")
|
|
cfg = load_from_json(path)
|
|
assert cfg.heading.mode is HeadingMode.POSITION
|
|
|
|
|
|
def test_missing_file_throws_with_path():
|
|
with pytest.raises(ValueError) as exc_info:
|
|
load_from_json("/nonexistent/vtc_no_such_file.json")
|
|
assert "vtc_no_such_file.json" in str(exc_info.value)
|
|
|
|
|
|
def test_malformed_json_throws(tmp_path):
|
|
path = tmp_path / "vtc_cfg_bad.json"
|
|
path.write_text("{ this is not json", encoding="utf-8")
|
|
with pytest.raises(ValueError):
|
|
load_from_json(str(path))
|
|
|
|
|
|
def test_missing_field_throws_with_field_name(tmp_path):
|
|
config = full_config()
|
|
del config["heading"]["gain"]
|
|
path = write_temp_json(tmp_path, config, "vtc_cfg_missing.json")
|
|
with pytest.raises(ValueError) as exc_info:
|
|
load_from_json(path)
|
|
assert "heading.gain" in str(exc_info.value)
|
|
|
|
|
|
def test_missing_section_throws(tmp_path):
|
|
config = full_config()
|
|
del config["accel_limit"]
|
|
path = write_temp_json(tmp_path, config, "vtc_cfg_nosection.json")
|
|
with pytest.raises(ValueError) as exc_info:
|
|
load_from_json(path)
|
|
assert "accel_limit" in str(exc_info.value)
|
|
|
|
|
|
def test_invalid_heading_mode_throws(tmp_path):
|
|
config = full_config()
|
|
config["heading"]["mode"] = "magic"
|
|
path = write_temp_json(tmp_path, config, "vtc_cfg_badmode.json")
|
|
with pytest.raises(ValueError) as exc_info:
|
|
load_from_json(path)
|
|
assert "heading.mode" in str(exc_info.value)
|
|
|
|
|
|
def test_invalid_no_distance_action_throws(tmp_path):
|
|
config = full_config()
|
|
config["distance"]["no_distance_action"] = "zoom"
|
|
path = write_temp_json(tmp_path, config, "vtc_cfg_badaction.json")
|
|
with pytest.raises(ValueError) as exc_info:
|
|
load_from_json(path)
|
|
assert "no_distance_action" in str(exc_info.value)
|
|
|
|
|
|
def test_negative_frequency_throws(tmp_path):
|
|
config = full_config()
|
|
config["control"]["frequency_hz"] = -1.0
|
|
path = write_temp_json(tmp_path, config, "vtc_cfg_negfreq.json")
|
|
with pytest.raises(ValueError) as exc_info:
|
|
load_from_json(path)
|
|
assert "control.frequency_hz" in str(exc_info.value)
|
|
|
|
|
|
def test_fov_out_of_range_throws(tmp_path):
|
|
config = full_config()
|
|
config["camera"]["fov_h_deg"] = 200.0
|
|
path = write_temp_json(tmp_path, config, "vtc_cfg_badfov.json")
|
|
with pytest.raises(ValueError) as exc_info:
|
|
load_from_json(path)
|
|
assert "camera.fov_h_deg" in str(exc_info.value)
|