first commit

This commit is contained in:
2026-09-02 15:13:13 +08:00
commit cb00322a50
107 changed files with 10918 additions and 0 deletions
@@ -0,0 +1,171 @@
"""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)