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,56 @@
// =============================================================================
// frame_transform_test.cpp —— RotateBodyToNed 单元测试
//
// 覆盖(物理追踪思路 §2.3,A4):
// 零航向恒等、朝东时机体前向 → 东向、垂直分量不变、模长守恒。
// =============================================================================
#include <cmath>
#include <gtest/gtest.h>
#include "visual_track_control/frame_transform.h"
namespace {
using drone::vtc::RotateBodyToNed;
constexpr double kPi = 3.14159265358979323846;
TEST(FrameTransformTest, ZeroYawIsIdentity) {
double x = 1.0, y = 0.5, z = -2.0;
RotateBodyToNed(x, y, z, 0.0);
EXPECT_NEAR(x, 1.0, 1e-12); // 北 = 前
EXPECT_NEAR(y, 0.5, 1e-12); // 东 = 右
EXPECT_NEAR(z, -2.0, 1e-12);
}
TEST(FrameTransformTest, YawEast90MapsForwardToEast) {
double x = 2.0, y = 0.0, z = 0.0;
RotateBodyToNed(x, y, z, kPi / 2.0); // 机头朝东
EXPECT_NEAR(x, 0.0, 1e-9); // 北向 ≈ 0
EXPECT_NEAR(y, 2.0, 1e-9); // 东向 = 前向速度
}
TEST(FrameTransformTest, RightIsEastWhenHeadingNorth) {
double x = 0.0, y = 1.0, z = 0.0;
RotateBodyToNed(x, y, z, 0.0);
EXPECT_NEAR(x, 0.0, 1e-12);
EXPECT_NEAR(y, 1.0, 1e-12);
}
TEST(FrameTransformTest, DownComponentUnchanged) {
double x = 1.0, y = 1.0, z = 3.5;
RotateBodyToNed(x, y, z, kPi / 3.0);
EXPECT_DOUBLE_EQ(z, 3.5);
}
TEST(FrameTransformTest, PreservesHorizontalNorm) {
// 旋转为正交变换:水平合速度不变(限幅约束不被破坏的前提)
double x = 3.0, y = -4.0, z = 1.0;
const double norm_before = std::hypot(x, y);
RotateBodyToNed(x, y, z, 0.7);
EXPECT_NEAR(std::hypot(x, y), norm_before, 1e-9);
}
} // namespace
@@ -0,0 +1,69 @@
// =============================================================================
// line_of_sight_test.cpp —— PixelToAngle 单元测试
//
// 覆盖(物理追踪思路 §4.1 / §5.1,C2):
// 零偏移、画面边缘偏移 = 半视场角、奇对称、单调性、
// 与线性近似的偏离(全程针孔模型)、构造期非法参数抛异常。
// =============================================================================
#include <cmath>
#include <gtest/gtest.h>
#include "visual_track_control/line_of_sight.h"
namespace {
using drone::vtc::PixelToAngle;
constexpr double kPi = 3.14159265358979323846;
constexpr double kRadToDeg = 180.0 / kPi;
TEST(PixelToAngleTest, ZeroOffsetGivesZeroAngle) {
PixelToAngle conv(1000.0, 90.0 * kPi / 180.0);
EXPECT_DOUBLE_EQ(conv.Convert(0.0), 0.0);
}
TEST(PixelToAngleTest, HalfSizeOffsetGivesHalfFov) {
// 针孔模型:偏移 = size/2 时 angle = atan(tan(θ/2)) = θ/2
PixelToAngle conv(1000.0, 90.0 * kPi / 180.0);
EXPECT_NEAR(conv.Convert(500.0), 45.0 * kPi / 180.0, 1e-12);
EXPECT_NEAR(conv.Convert(-500.0), -45.0 * kPi / 180.0, 1e-12);
}
TEST(PixelToAngleTest, OddSymmetry) {
PixelToAngle conv(1920.0, 60.0 * kPi / 180.0);
for (const double offset : {1.0, 100.0, 730.5}) {
EXPECT_NEAR(conv.Convert(-offset), -conv.Convert(offset), 1e-15);
}
}
TEST(PixelToAngleTest, MonotonicIncreasing) {
PixelToAngle conv(1080.0, 35.0 * kPi / 180.0);
double prev = conv.Convert(-600.0);
for (double x = -500.0; x <= 600.0; x += 100.0) {
const double cur = conv.Convert(x);
EXPECT_GT(cur, prev);
prev = cur;
}
}
TEST(PixelToAngleTest, DeviatesFromLinearApproximation) {
// C2:全程针孔模型。θ=90°、偏移比例 0.25 处线性近似误差约 4°,
// 针孔结果必须与之明显偏离(证明未走线性支路)。
PixelToAngle conv(1000.0, 90.0 * kPi / 180.0);
const double pinhole_deg = conv.Convert(250.0) * kRadToDeg; // atan(250·2·tan45°/1000)
const double linear_deg = 250.0 * (90.0 / 1000.0); // θ·offset/size = 22.5°
EXPECT_NEAR(pinhole_deg, std::atan(0.5) * kRadToDeg, 1e-9); // 26.565°
EXPECT_GT(std::fabs(pinhole_deg - linear_deg), 2.0);
}
TEST(PixelToAngleTest, ConstructorRejectsInvalidArgs) {
EXPECT_THROW(PixelToAngle(0.0, 1.0), std::invalid_argument);
EXPECT_THROW(PixelToAngle(-10.0, 1.0), std::invalid_argument);
EXPECT_THROW(PixelToAngle(1000.0, 0.0), std::invalid_argument);
EXPECT_THROW(PixelToAngle(1000.0, kPi), std::invalid_argument);
EXPECT_THROW(PixelToAngle(1000.0, kPi + 0.1), std::invalid_argument);
}
} // namespace
@@ -0,0 +1,181 @@
// =============================================================================
// mavlink_command_gateway_test.cpp —— MavlinkCommandGateway 单元测试
//
// 覆盖(物理追踪思路 §2.2、§4.2,A2 掩码三分支):
// ID 84 速度模式的 type_mask 选择(角速率非零 → 速率掩码;航向角非零 →
// 航向掩码;两者均零 → 速率掩码配零角速率,保持航向)、
// 速度/航向字段编码、刹车消息、坐标基准、传输失败传播。
//
// 构建约束:本文件**不得**包含 common/mavlink.h(MAVLINK_HELPER 默认空宏,
// 多 TU 包含会多重定义),故按 MAVLink v2 线格式手工解码:
// 帧头 10 字节(STX=0xFD, len, flags×2, seq, sys, comp, msgid×3 LE),
// 载荷按字段尺寸降序排列(ID 84:time,x,y,z,vx,vy,vz,afx,afy,afz,
// yaw,yaw_rate 均 4 字节在前,随后 u16 type_mask,再 target_system、
// target_component、coordinate_frame 三个 u8)。
// =============================================================================
#include <cstdint>
#include <cstring>
#include <vector>
#include <gtest/gtest.h>
#include "visual_track_control/i_command_transport.h"
#include "visual_track_control/mavlink_command_gateway.h"
#include "visual_track_control/vtc_types.h"
namespace {
using drone::vtc::ControlOutput;
using drone::vtc::ICommandTransport;
using drone::vtc::MavlinkCommandGateway;
// ---- MAVLink v2 帧布局常量(见文件头说明) ----
constexpr std::uint8_t kMavlinkV2Stx = 0xFD;
constexpr std::size_t kHeaderLen = 10;
constexpr std::size_t kMsgId84PayloadLen = 53;
// ---- ID 84 载荷字段偏移(相对载荷起点) ----
constexpr std::size_t kOffVx = 16;
constexpr std::size_t kOffVy = 20;
constexpr std::size_t kOffVz = 24;
constexpr std::size_t kOffYaw = 40;
constexpr std::size_t kOffYawRate = 44;
constexpr std::size_t kOffTypeMask = 48;
// u8 三字段线序(生成头 mavlink_msg_set_position_target_local_ned.h 为准)
constexpr std::size_t kOffTargetSystem = 50;
constexpr std::size_t kOffTargetComponent = 51;
constexpr std::size_t kOffCoordFrame = 52;
// ---- 预期掩码值(与实现中常量一致:置位 = 忽略) ----
// 基础 0x01C7(忽略位置与加速度)| 航向相关位
constexpr std::uint16_t kExpectMaskYaw = 0x01C7 | 0x0800; // 忽略 yaw_rate
constexpr std::uint16_t kExpectMaskYawRate = 0x01C7 | 0x0400; // 忽略 yaw
constexpr std::uint16_t kExpectMaskBrake = 0x01C7 | 0x0C00; // 两者均忽略
/// 捕获式传输:记录最后一次发送的字节流,可注入失败
class CaptureTransport : public ICommandTransport {
public:
bool Send(const std::uint8_t* data, std::size_t len) override {
++send_count_;
last_.assign(data, data + len);
return succeed_;
}
void SetSucceed(bool ok) { succeed_ = ok; }
int send_count() const { return send_count_; }
const std::vector<std::uint8_t>& last() const { return last_; }
private:
bool succeed_ = true;
int send_count_ = 0;
std::vector<std::uint8_t> last_;
};
float ReadFloat(const std::vector<std::uint8_t>& buf, std::size_t payload_off) {
float v;
std::memcpy(&v, buf.data() + kHeaderLen + payload_off, sizeof(v));
return v;
}
std::uint16_t ReadU16(const std::vector<std::uint8_t>& buf, std::size_t payload_off) {
return static_cast<std::uint16_t>(buf[kHeaderLen + payload_off]) |
(static_cast<std::uint16_t>(buf[kHeaderLen + payload_off + 1]) << 8);
}
void ExpectId84Header(const std::vector<std::uint8_t>& buf) {
ASSERT_GE(buf.size(), kHeaderLen + kMsgId84PayloadLen + 2); // +CRC
EXPECT_EQ(buf[0], kMavlinkV2Stx);
EXPECT_EQ(buf[1], kMsgId84PayloadLen);
// msgid 24 位小端(字节 7-9)= 84
EXPECT_EQ(buf[7], 84);
EXPECT_EQ(buf[8], 0);
EXPECT_EQ(buf[9], 0);
}
constexpr double kDegToRad = 3.14159265358979323846 / 180.0;
TEST(MavlinkGatewayTest, YawRateModeSelectsRateMask) {
CaptureTransport transport;
MavlinkCommandGateway gateway(transport, 1, 2, 3, 4);
ControlOutput out;
out.mode = drone::vtc::ControlMode::kVelocityHeading;
out.vx_mps = 1.5;
out.vy_mps = -0.5;
out.vz_mps = 0.25;
out.yaw_rate_dps = 5.0;
out.yaw_deg = 0.0;
ASSERT_TRUE(gateway.SendVelocityHeading(out, 12345));
const auto& buf = transport.last();
ExpectId84Header(buf);
EXPECT_EQ(ReadU16(buf, kOffTypeMask), kExpectMaskYawRate);
EXPECT_FLOAT_EQ(ReadFloat(buf, kOffVx), 1.5F);
EXPECT_FLOAT_EQ(ReadFloat(buf, kOffVy), -0.5F);
EXPECT_FLOAT_EQ(ReadFloat(buf, kOffVz), 0.25F);
EXPECT_FLOAT_EQ(ReadFloat(buf, kOffYawRate), static_cast<float>(5.0 * kDegToRad));
EXPECT_EQ(buf[kHeaderLen + kOffCoordFrame], 1); // MAV_FRAME_LOCAL_NED
EXPECT_EQ(buf[kHeaderLen + kOffTargetSystem], 3); // target_system
EXPECT_EQ(buf[kHeaderLen + kOffTargetComponent], 4); // target_component
}
TEST(MavlinkGatewayTest, YawOnlySelectsYawMask) {
CaptureTransport transport;
MavlinkCommandGateway gateway(transport, 1, 2, 3, 4);
ControlOutput out;
out.mode = drone::vtc::ControlMode::kVelocityHeading;
out.yaw_deg = 10.0;
out.yaw_rate_dps = 0.0;
ASSERT_TRUE(gateway.SendVelocityHeading(out, 1));
const auto& buf = transport.last();
ExpectId84Header(buf);
EXPECT_EQ(ReadU16(buf, kOffTypeMask), kExpectMaskYaw);
EXPECT_FLOAT_EQ(ReadFloat(buf, kOffYaw), static_cast<float>(10.0 * kDegToRad));
}
TEST(MavlinkGatewayTest, BothZeroKeepsHeadingViaRateMask) {
// A2 回归:旧实现在零角速率时落到航向掩码,等于误发绝对航向 0(正北)。
// 正确行为:速率掩码 + 零角速率 = 保持当前航向。
CaptureTransport transport;
MavlinkCommandGateway gateway(transport, 1, 2, 3, 4);
ControlOutput out;
out.mode = drone::vtc::ControlMode::kVelocityHeading;
out.vx_mps = 1.0;
ASSERT_TRUE(gateway.SendVelocityHeading(out, 1));
const auto& buf = transport.last();
ExpectId84Header(buf);
EXPECT_EQ(ReadU16(buf, kOffTypeMask), kExpectMaskYawRate);
EXPECT_FLOAT_EQ(ReadFloat(buf, kOffYawRate), 0.0F);
}
TEST(MavlinkGatewayTest, BrakeHoverSendsZeroVelocityAllIgnored) {
CaptureTransport transport;
MavlinkCommandGateway gateway(transport, 1, 2, 3, 4);
ASSERT_TRUE(gateway.SendBrakeHover(777));
const auto& buf = transport.last();
ExpectId84Header(buf);
EXPECT_EQ(ReadU16(buf, kOffTypeMask), kExpectMaskBrake);
EXPECT_FLOAT_EQ(ReadFloat(buf, kOffVx), 0.0F);
EXPECT_FLOAT_EQ(ReadFloat(buf, kOffVy), 0.0F);
EXPECT_FLOAT_EQ(ReadFloat(buf, kOffVz), 0.0F);
}
TEST(MavlinkGatewayTest, TransportFailurePropagates) {
CaptureTransport transport;
transport.SetSucceed(false);
MavlinkCommandGateway gateway(transport, 1, 2, 3, 4);
ControlOutput out;
out.mode = drone::vtc::ControlMode::kVelocityHeading;
EXPECT_FALSE(gateway.SendVelocityHeading(out, 1));
EXPECT_FALSE(gateway.SendBrakeHover(1));
EXPECT_EQ(transport.send_count(), 2);
}
} // namespace
@@ -0,0 +1,123 @@
// =============================================================================
// pid_controller_test.cpp —— PidController 单元测试
//
// 覆盖(物理追踪思路 §6.2 / §6.4):
// 位置式 P/I/D 三项、积分钳位、输出限幅、异常节拍保护、
// anti-windup 条件积分(C1 修复回归:饱和冻结 + 误差反号回卷)、
// ResetIntegral / Reset 行为。
// =============================================================================
#include <gtest/gtest.h>
#include "visual_track_control/pid_controller.h"
namespace {
using drone::vtc::PidController;
using drone::vtc::PidParams;
PidParams MakeParams(double kp, double ki, double kd, double integral_limit,
double output_limit, double filter_coef = 0.0) {
PidParams p;
p.kp = kp;
p.ki = ki;
p.kd = kd;
p.integral_limit = integral_limit;
p.derivative_filter_coef = filter_coef;
p.output_limit = output_limit;
return p;
}
TEST(PidControllerTest, ProportionalOnly) {
PidController pid(MakeParams(2.0, 0.0, 0.0, 0.0, 0.0));
EXPECT_DOUBLE_EQ(pid.Update(3.0, 0.1), 6.0);
EXPECT_DOUBLE_EQ(pid.Update(-1.5, 0.1), -3.0);
}
TEST(PidControllerTest, IntegralAccumulates) {
PidController pid(MakeParams(0.0, 1.0, 0.0, 0.0, 0.0));
// 两拍 error=2、dt=0.5:积分累计 1.0 → 2.0
EXPECT_DOUBLE_EQ(pid.Update(2.0, 0.5), 1.0);
EXPECT_DOUBLE_EQ(pid.Update(2.0, 0.5), 2.0);
}
TEST(PidControllerTest, IntegralClampedToLimit) {
PidController pid(MakeParams(0.0, 1.0, 0.0, /*integral_limit=*/1.0, 0.0));
for (int i = 0; i < 5; ++i) {
pid.Update(10.0, 1.0);
}
// 积分候选值被钳位在 1.0 → 输出恒为 1.0
EXPECT_DOUBLE_EQ(pid.LastOutput(), 1.0);
}
TEST(PidControllerTest, OutputClampedToLimit) {
PidController pid(MakeParams(10.0, 0.0, 0.0, 0.0, /*output_limit=*/3.0));
EXPECT_DOUBLE_EQ(pid.Update(1.0, 0.1), 3.0);
EXPECT_DOUBLE_EQ(pid.Update(-1.0, 0.1), -3.0);
}
TEST(PidControllerTest, AntiWindupFreezeWhileSaturatedSameDirection) {
// kp=0、ki=2、output_limit=5:error=+1 持续饱和时积分冻结,
// 输出恒钉在 +5(10 拍累计 10 拍也不会让积分继续增长)。
PidController pid(MakeParams(0.0, 2.0, 0.0, 0.0, 5.0));
for (int i = 0; i < 10; ++i) {
EXPECT_DOUBLE_EQ(pid.Update(10.0, 1.0), 5.0);
}
}
TEST(PidControllerTest, AntiWindupUnwindsAfterErrorFlips) {
// C1 回归:误差反号后积分必须立即回卷(旧判据"积分与输出同向"会
// 冻结回卷、输出一直钉在 +5)。
// kp=0、ki=2、limit=5:+10 饱和 3 拍、积分冻结在 0;
// error=-1 首拍 → 输出 2×(-1) = -2(立即回卷);
// 持续负误差使输出到达负饱和 -5(积分冻结在 -2,输出 = clamp(-6))。
PidController pid(MakeParams(0.0, 2.0, 0.0, 0.0, 5.0));
for (int i = 0; i < 3; ++i) {
EXPECT_DOUBLE_EQ(pid.Update(10.0, 1.0), 5.0);
}
EXPECT_DOUBLE_EQ(pid.Update(-1.0, 1.0), -2.0); // 首拍立即回卷
double out = -2.0;
for (int i = 1; i < 12; ++i) {
out = pid.Update(-1.0, 1.0);
}
EXPECT_NEAR(out, -5.0, 1e-9); // 旧判据此处仍为 +5.0
}
TEST(PidControllerTest, DerivativeFirstStepIsZero) {
PidController pid(MakeParams(0.0, 0.0, 1.0, 0.0, 0.0));
// 首拍无微分历史:微分项为 0
EXPECT_DOUBLE_EQ(pid.Update(5.0, 1.0), 0.0);
}
TEST(PidControllerTest, DerivativeWithLowPassFilter) {
PidController pid(MakeParams(0.0, 0.0, 1.0, 0.0, 0.0, /*filter=*/0.5));
pid.Update(0.0, 1.0); // 建立微分历史(导数 0)
// 第二拍原始导数 1.0,低通后 0.5×0 + 0.5×1.0 = 0.5
EXPECT_DOUBLE_EQ(pid.Update(1.0, 1.0), 0.5);
}
TEST(PidControllerTest, ZeroOrNegativeDtHoldsLastOutput) {
PidController pid(MakeParams(1.0, 0.0, 0.0, 0.0, 0.0));
EXPECT_DOUBLE_EQ(pid.Update(2.0, 0.1), 2.0);
EXPECT_DOUBLE_EQ(pid.Update(99.0, 0.0), 2.0);
EXPECT_DOUBLE_EQ(pid.Update(99.0, -1.0), 2.0);
}
TEST(PidControllerTest, ResetIntegralClearsOnlyIntegral) {
PidController pid(MakeParams(0.0, 1.0, 1.0, 0.0, 0.0));
pid.Update(2.0, 1.0); // 积分 = 2
pid.ResetIntegral();
// 积分清零、微分历史保留:输出应为 kd×(0−2)/1 = −2
EXPECT_DOUBLE_EQ(pid.Update(0.0, 1.0), -2.0);
}
TEST(PidControllerTest, ResetClearsEverything) {
PidController pid(MakeParams(1.0, 1.0, 1.0, 0.0, 0.0));
pid.Update(5.0, 1.0);
pid.Reset();
EXPECT_DOUBLE_EQ(pid.LastOutput(), 0.0);
// 全清后等同全新控制器:首拍无微分历史
EXPECT_DOUBLE_EQ(pid.Update(3.0, 1.0), 3.0 + 3.0); // kp·e + ki·∫e
}
} // namespace
@@ -0,0 +1,76 @@
// =============================================================================
// rate_limiter_test.cpp —— Clamp / SlewRateLimiter 单元测试
//
// 覆盖(物理追踪思路 §8、状态机设计 S6):
// 静态限幅、变化率限幅步进、首条指令从零渐变(S6)、
// 异常节拍保护、Reset 归零。
// =============================================================================
#include <gtest/gtest.h>
#include "visual_track_control/rate_limiter.h"
namespace {
using drone::vtc::Clamp;
using drone::vtc::SlewRateLimiter;
TEST(ClampTest, ClampsValue) {
EXPECT_DOUBLE_EQ(Clamp(5.0, -2.0, 2.0), 2.0);
EXPECT_DOUBLE_EQ(Clamp(-5.0, -2.0, 2.0), -2.0);
EXPECT_DOUBLE_EQ(Clamp(1.5, -2.0, 2.0), 1.5);
// 边界值:落在区间内
EXPECT_DOUBLE_EQ(Clamp(2.0, -2.0, 2.0), 2.0);
EXPECT_DOUBLE_EQ(Clamp(-2.0, -2.0, 2.0), -2.0);
}
TEST(SlewRateLimiterTest, FirstCommandRampsFromZero) {
// S6:初始输出为 0,首条大指令只能按变化率渐变
SlewRateLimiter lim(/*rate_limit=*/2.0);
EXPECT_DOUBLE_EQ(lim.Limit(10.0, 0.5), 1.0); // 0 + 2×0.5
EXPECT_DOUBLE_EQ(lim.Limit(10.0, 0.5), 2.0); // 继续渐变
EXPECT_DOUBLE_EQ(lim.Limit(10.0, 10.0), 10.0); // 步长足够 → 到达目标
}
TEST(SlewRateLimiterTest, TracksTargetWithinStep) {
SlewRateLimiter lim(10.0);
// 步长 10×1=10,目标 3 在步长内 → 直接到达
EXPECT_DOUBLE_EQ(lim.Limit(3.0, 1.0), 3.0);
EXPECT_DOUBLE_EQ(lim.Limit(3.0, 1.0), 3.0); // 已在目标:保持
}
TEST(SlewRateLimiterTest, NegativeDirection) {
SlewRateLimiter lim(2.0);
EXPECT_DOUBLE_EQ(lim.Limit(-10.0, 0.5), -1.0);
EXPECT_DOUBLE_EQ(lim.Limit(-10.0, 0.5), -2.0);
}
TEST(SlewRateLimiterTest, ZeroOrNegativeDtHolds) {
SlewRateLimiter lim(2.0);
lim.Limit(10.0, 1.0); // 输出 2.0
EXPECT_DOUBLE_EQ(lim.Limit(10.0, 0.0), 2.0);
EXPECT_DOUBLE_EQ(lim.Limit(10.0, -1.0), 2.0);
}
TEST(SlewRateLimiterTest, ZeroRateLimitFreezesOutput) {
SlewRateLimiter lim(0.0);
EXPECT_DOUBLE_EQ(lim.Limit(5.0, 1.0), 0.0);
}
TEST(SlewRateLimiterTest, ResetToValue) {
SlewRateLimiter lim(2.0);
lim.Limit(10.0, 1.0);
lim.Reset();
EXPECT_DOUBLE_EQ(lim.LastOutput(), 0.0);
lim.Reset(3.5);
EXPECT_DOUBLE_EQ(lim.LastOutput(), 3.5);
// 复位后从新值继续渐变
EXPECT_DOUBLE_EQ(lim.Limit(10.0, 1.0), 5.5); // 3.5 + 2×1
}
TEST(SlewRateLimiterTest, CustomInitialValue) {
SlewRateLimiter lim(1.0, /*initial=*/5.0);
EXPECT_DOUBLE_EQ(lim.Limit(10.0, 1.0), 6.0);
}
} // namespace
@@ -0,0 +1,41 @@
// =============================================================================
// staleness_checker_test.cpp —— StalenessChecker 单元测试
//
// 覆盖(物理追踪思路 §7.1):
// 从未取样判超龄、有效期内新鲜、超出有效期超龄、
// 边界恰好在有效期上(不算超龄)、时间戳不一致判超龄(C4)。
// =============================================================================
#include <gtest/gtest.h>
#include "visual_track_control/staleness_checker.h"
namespace {
using drone::vtc::StalenessChecker;
TEST(StalenessCheckerTest, NeverSampledIsStale) {
EXPECT_TRUE(StalenessChecker::IsStale(0, 1000, 200));
EXPECT_TRUE(StalenessChecker::IsStale(-5, 1000, 200));
}
TEST(StalenessCheckerTest, WithinValidityIsFresh) {
EXPECT_FALSE(StalenessChecker::IsStale(900, 1000, 200));
EXPECT_FALSE(StalenessChecker::IsStale(1000, 1000, 200)); // 刚取样
}
TEST(StalenessCheckerTest, BeyondValidityIsStale) {
EXPECT_TRUE(StalenessChecker::IsStale(799, 1000, 200)); // 201 > 200
}
TEST(StalenessCheckerTest, ExactlyAtBoundaryIsFresh) {
// 超龄判定为严格大于:恰好等于有效期仍算新鲜
EXPECT_FALSE(StalenessChecker::IsStale(800, 1000, 200));
}
TEST(StalenessCheckerTest, InconsistentTimestampIsStale) {
// C4:now < sample 时间戳不一致 → 年龄不可信 → 判超龄(宁降级不轻信)
EXPECT_TRUE(StalenessChecker::IsStale(2000, 1000, 200));
}
} // namespace
@@ -0,0 +1,349 @@
// =============================================================================
// tracking_control_law_test.cpp —— TrackingControlLaw 主控律单元测试
//
// 覆盖(物理追踪思路 §2.3 / §4 / §5 / §6 / §7 / §9):
// 超龄降级(姿态缺失/过期、视觉超龄、降级优先级)、判丢零速刹车、
// Coast 清积分与标志、速率式/位置式航向通道(含绝对航向合成与回绕,
// A2)、垂直统一速度支路(A1)、距离通道三种无距离降级动作、
// 首拍变化率限幅(S6)、机体系→NED 输出旋转(A4)。
// =============================================================================
#include <cmath>
#include <gtest/gtest.h>
#include "visual_track_control/tracking_control_law.h"
#include "visual_track_control/vtc_config.h"
#include "visual_track_control/vtc_types.h"
namespace {
using namespace drone::vtc;
constexpr double kPi = 3.14159265358979323846;
constexpr double kDegToRad = kPi / 180.0;
constexpr double kRadToDeg = 180.0 / kPi;
/// 测试基准配置:大限幅使限幅器近似透明,便于独立验证各通道数学。
/// 画面 1000×1000、视场角 90° → 针孔系数 2·tan(45°)/1000 = 0.002,
/// 偏移 100 像素 → 视线角 atan(0.2) ≈ 11.3099°。
TrackingConfig MakeConfig() {
TrackingConfig cfg;
cfg.control.frequency_hz = 20.0;
cfg.control.visual_stale_ms = 200;
cfg.control.radar_stale_ms = 500;
cfg.control.attitude_stale_ms = 300;
cfg.camera.image_width = 1000;
cfg.camera.image_height = 1000;
cfg.camera.fov_h_deg = 90.0;
cfg.camera.fov_v_deg = 90.0;
cfg.heading.mode = HeadingMode::kRate;
cfg.heading.gain = 1.0;
cfg.heading.yaw_rate_limit_dps = 30.0;
cfg.heading.yaw_slew_limit_dps2 = 100000.0;
cfg.vertical.vz_gain_mps_per_deg = 0.1;
cfg.vertical.vz_limit_mps = 2.0;
cfg.distance.d_exp_m = 10.0;
cfg.distance.kp = 0.5;
cfg.distance.ki = 0.0;
cfg.distance.kd = 0.0;
cfg.distance.integral_limit = 0.0;
cfg.distance.derivative_filter_coef = 0.0;
cfg.distance.approach_velocity_limit_mps = 3.0;
cfg.distance.retreat_velocity_limit_mps = 1.0;
cfg.distance.no_distance_action = NoDistanceAction::kHold;
cfg.distance.no_distance_approach_limit_mps = 0.5;
cfg.accel_limit.ax_mps2 = 1000.0;
cfg.accel_limit.ay_mps2 = 1000.0;
cfg.accel_limit.az_mps2 = 1000.0;
return cfg;
}
/// 新鲜快照:目标在画面中心,姿态/追踪/雷达均新鲜
ControlSnapshot MakeSnapshot(std::int64_t now_ms) {
ControlSnapshot s;
s.track.tracked = true;
s.track.is_predicted = false;
s.track.center = {500.0, 500.0};
s.track_time_ms = now_ms;
s.attitude_present = true;
s.attitude_time_ms = now_ms;
return s;
}
// ---- 超龄降级(§7.1 / §9.2) ----
TEST(TrackingControlLawTest, AttitudeMissingDegrades) {
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.attitude_present = false;
const auto out = law.Update(snap, 1000);
EXPECT_EQ(out.mode, ControlMode::kDegradedHold);
EXPECT_EQ(out.degraded_reason, DegradedReason::kAttitudeStale);
EXPECT_FALSE(out.valid);
}
TEST(TrackingControlLawTest, AttitudeStaleDegrades) {
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.attitude_time_ms = 600; // 400 ms 前 > 300 ms 阈值
const auto out = law.Update(snap, 1000);
EXPECT_EQ(out.mode, ControlMode::kDegradedHold);
EXPECT_EQ(out.degraded_reason, DegradedReason::kAttitudeStale);
}
TEST(TrackingControlLawTest, VisionStaleDegrades) {
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.track_time_ms = 700; // 300 ms 前 > 200 ms 阈值
const auto out = law.Update(snap, 1000);
EXPECT_EQ(out.mode, ControlMode::kDegradedHold);
EXPECT_EQ(out.degraded_reason, DegradedReason::kVisionStale);
}
TEST(TrackingControlLawTest, NeverSampledTrackDegrades) {
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.track_time_ms = 0; // 从未取样
const auto out = law.Update(snap, 1000);
EXPECT_EQ(out.mode, ControlMode::kDegradedHold);
EXPECT_EQ(out.degraded_reason, DegradedReason::kVisionStale);
}
TEST(TrackingControlLawTest, AttitudeStaleBeatsVisionStale) {
// 降级优先级:姿态校验在前(§9.2 姿态过期禁止一切空间转换)
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.attitude_present = false;
snap.track_time_ms = 0;
const auto out = law.Update(snap, 1000);
EXPECT_EQ(out.degraded_reason, DegradedReason::kAttitudeStale);
}
// ---- 判丢刹车(§6.4) ----
TEST(TrackingControlLawTest, LostTargetBrakes) {
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.track.tracked = false;
const auto out = law.Update(snap, 1000);
EXPECT_EQ(out.mode, ControlMode::kBrakeHover);
EXPECT_TRUE(out.valid);
EXPECT_DOUBLE_EQ(out.vx_mps, 0.0);
EXPECT_DOUBLE_EQ(out.vy_mps, 0.0);
EXPECT_DOUBLE_EQ(out.vz_mps, 0.0);
}
// ---- Coast(§6.4) ----
TEST(TrackingControlLawTest, CoastSetsFlagAndKeepsTracking) {
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.track.is_predicted = true;
const auto out = law.Update(snap, 1000);
EXPECT_EQ(out.mode, ControlMode::kVelocityHeading);
EXPECT_TRUE(out.coast_active);
EXPECT_TRUE(out.valid);
}
// ---- 水平航向通道(§4.2,A2) ----
TEST(TrackingControlLawTest, RateModeMapsBetaToYawRate) {
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.track.center = {600.0, 500.0}; // 目标偏右 100 像素
const auto out = law.Update(snap, 1000);
const double beta_deg = std::atan(0.2) * kRadToDeg; // ≈ 11.31°
EXPECT_NEAR(out.yaw_rate_dps, beta_deg, 1e-6); // gain=1,右转正
EXPECT_DOUBLE_EQ(out.yaw_deg, 0.0);
}
TEST(TrackingControlLawTest, RateModeClampsToYawRateLimit) {
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.track.center = {950.0, 500.0}; // 大偏移:β≈43.5° > 30°/s 上限
const auto out = law.Update(snap, 1000);
EXPECT_NEAR(out.yaw_rate_dps, 30.0, 1e-9);
}
TEST(TrackingControlLawTest, PositionModeComposesAbsoluteHeading) {
// A2:位置式输出为绝对 NED 航向 = 当前航向 + 限幅修正量
auto cfg = MakeConfig();
cfg.heading.mode = HeadingMode::kPosition;
TrackingControlLaw law(cfg);
auto snap = MakeSnapshot(1000);
snap.track.center = {600.0, 500.0};
snap.yaw_rad = 45.0 * kDegToRad;
const auto out = law.Update(snap, 1000);
const double beta_deg = std::atan(0.2) * kRadToDeg;
EXPECT_NEAR(out.yaw_deg, 45.0 + beta_deg, 1e-6);
EXPECT_DOUBLE_EQ(out.yaw_rate_dps, 0.0);
}
TEST(TrackingControlLawTest, PositionModeWrapsAround180) {
auto cfg = MakeConfig();
cfg.heading.mode = HeadingMode::kPosition;
TrackingControlLaw law(cfg);
auto snap = MakeSnapshot(1000);
snap.track.center = {600.0, 500.0};
snap.yaw_rad = 175.0 * kDegToRad;
const auto out = law.Update(snap, 1000);
const double beta_deg = std::atan(0.2) * kRadToDeg;
// 175 + 11.31 = 186.31 → 回绕到 ≈ -173.69
EXPECT_NEAR(out.yaw_deg, 175.0 + beta_deg - 360.0, 1e-6);
EXPECT_GT(out.yaw_deg, -180.0);
EXPECT_LT(out.yaw_deg, 180.0);
}
// ---- 垂直统一速度支路(§5,A1) ----
TEST(TrackingControlLawTest, VerticalOffsetDrivesVz) {
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.track.center = {500.0, 600.0}; // 目标偏低 100 像素 → α>0 → 下降
const auto out = law.Update(snap, 1000);
const double alpha_deg = std::atan(0.2) * kRadToDeg;
EXPECT_NEAR(out.vz_mps, 0.1 * alpha_deg, 1e-6);
EXPECT_EQ(out.mode, ControlMode::kVelocityHeading); // A1:不切姿态微调模式
}
TEST(TrackingControlLawTest, VerticalVelocityClamped) {
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.track.center = {500.0, 990.0}; // α≈44.5° × 0.1 = 4.45 > 2 上限
const auto out = law.Update(snap, 1000);
EXPECT_NEAR(out.vz_mps, 2.0, 1e-9);
}
// ---- 距离通道(§6.2 / §6.3) ----
TEST(TrackingControlLawTest, DistanceApproachWhenFar) {
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.radar_distance_m = 30.0; // Δd = +20 → kp 输出 10 → 限幅 3
snap.associated_to_target = true;
snap.radar_time_ms = 1000;
const auto out = law.Update(snap, 1000);
EXPECT_NEAR(out.vx_mps, 3.0, 1e-9);
}
TEST(TrackingControlLawTest, DistanceRetreatClampedWhenTooClose) {
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.radar_distance_m = 5.0; // Δd = -5 → kp 输出 -2.5 → 后退限幅 -1
snap.associated_to_target = true;
snap.radar_time_ms = 1000;
const auto out = law.Update(snap, 1000);
EXPECT_NEAR(out.vx_mps, -1.0, 1e-9);
}
TEST(TrackingControlLawTest, NoDistanceHoldZerosVx) {
TrackingControlLaw law(MakeConfig()); // 缺省 kHold
auto snap = MakeSnapshot(1000);
snap.associated_to_target = false;
const auto out = law.Update(snap, 1000);
EXPECT_EQ(out.mode, ControlMode::kVelocityHeading);
EXPECT_DOUBLE_EQ(out.vx_mps, 0.0);
}
TEST(TrackingControlLawTest, NoDistanceSlowApproach) {
auto cfg = MakeConfig();
cfg.distance.no_distance_action = NoDistanceAction::kSlowApproach;
TrackingControlLaw law(cfg);
auto snap = MakeSnapshot(1000);
snap.associated_to_target = false;
const auto out = law.Update(snap, 1000);
EXPECT_NEAR(out.vx_mps, 0.5, 1e-9);
}
TEST(TrackingControlLawTest, NoDistanceExitDegrades) {
auto cfg = MakeConfig();
cfg.distance.no_distance_action = NoDistanceAction::kExit;
TrackingControlLaw law(cfg);
auto snap = MakeSnapshot(1000);
snap.associated_to_target = false;
const auto out = law.Update(snap, 1000);
EXPECT_EQ(out.mode, ControlMode::kDegradedHold);
EXPECT_EQ(out.degraded_reason, DegradedReason::kNoDistanceExit);
EXPECT_FALSE(out.valid);
}
TEST(TrackingControlLawTest, StaleRadarFallsBackToNoDistance) {
TrackingControlLaw law(MakeConfig()); // kHold
auto snap = MakeSnapshot(1000);
snap.radar_distance_m = 30.0;
snap.associated_to_target = true;
snap.radar_time_ms = 400; // 600 ms 前 > 500 ms 阈值 → 距离不可用
const auto out = law.Update(snap, 1000);
EXPECT_EQ(out.mode, ControlMode::kVelocityHeading);
EXPECT_DOUBLE_EQ(out.vx_mps, 0.0);
}
// ---- 首拍/限幅(§8、S6) ----
TEST(TrackingControlLawTest, FirstTickVxSlewsFromZero) {
// S6:首条速度指令必须经变化率限幅,从 0 渐变而非阶跃
auto cfg = MakeConfig();
cfg.accel_limit.ax_mps2 = 2.0;
TrackingControlLaw law(cfg);
auto snap = MakeSnapshot(1000);
snap.radar_distance_m = 30.0; // 通道输出本应为 3.0
snap.associated_to_target = true;
snap.radar_time_ms = 1000;
const auto out = law.Update(snap, 1000); // 首拍:dt 按 1/20 Hz 兜底
EXPECT_NEAR(out.vx_mps, 2.0 * (1.0 / 20.0), 1e-9); // 0.1
}
// ---- 机体系 → NED 旋转(§2.3,A4) ----
TEST(TrackingControlLawTest, OutputRotatedToNedByYaw) {
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.yaw_rad = kPi / 2.0; // 机头朝东
snap.radar_distance_m = 30.0;
snap.associated_to_target = true;
snap.radar_time_ms = 1000;
const auto out = law.Update(snap, 1000);
// 机体前向 3.0 → 东向 3.0,北向 ≈ 0
EXPECT_NEAR(out.vx_mps, 0.0, 1e-9);
EXPECT_NEAR(out.vy_mps, 3.0, 1e-9);
}
TEST(TrackingControlLawTest, ZeroYawKeepsForwardAsNorth) {
TrackingControlLaw law(MakeConfig());
auto snap = MakeSnapshot(1000);
snap.yaw_rad = 0.0;
snap.radar_distance_m = 30.0;
snap.associated_to_target = true;
snap.radar_time_ms = 1000;
const auto out = law.Update(snap, 1000);
EXPECT_NEAR(out.vx_mps, 3.0, 1e-9);
EXPECT_NEAR(out.vy_mps, 0.0, 1e-9);
}
// ---- Reset(§6.4) ----
TEST(TrackingControlLawTest, ResetRestoresSlewFromZero) {
auto cfg = MakeConfig();
cfg.accel_limit.ax_mps2 = 2.0;
TrackingControlLaw law(cfg);
auto snap = MakeSnapshot(1000);
snap.radar_distance_m = 30.0;
snap.associated_to_target = true;
snap.radar_time_ms = 1000;
for (std::int64_t t = 1000; t < 3000; t += 50) {
snap.track_time_ms = t;
snap.radar_time_ms = t;
snap.attitude_time_ms = t;
law.Update(snap, t); // 若干拍后 vx 已爬升到限幅 3.0
}
law.Reset();
snap.track_time_ms = 3000;
snap.radar_time_ms = 3000;
snap.attitude_time_ms = 3000;
const auto out = law.Update(snap, 3000);
// Reset 后限幅器归零:又从 0.1 开始渐变
EXPECT_NEAR(out.vx_mps, 0.1, 1e-9);
}
} // namespace
@@ -0,0 +1,224 @@
// =============================================================================
// vtc_config_test.cpp —— TrackingConfig::LoadFromJson 单元测试
//
// 覆盖(物理追踪思路 §8、需求分析 NFR-007):
// 真实配置文件加载成功与取值正确(含 A1/A2 修改后的字段结构)、
// 缺字段 / 缺分组 / 非法枚举 / 非法数值 / 文件缺失均抛带字段名的异常。
//
// 依赖编译期宏:
// VTC_TEST_CONFIG —— config/track_control.json 绝对路径
// VTC_TEST_TMP_DIR —— 测试临时文件输出目录(构建目录)
// =============================================================================
#include <cstdio>
#include <fstream>
#include <stdexcept>
#include <string>
#include <gtest/gtest.h>
#include "visual_track_control/vtc_config.h"
namespace {
using drone::vtc::HeadingMode;
using drone::vtc::NoDistanceAction;
using drone::vtc::TrackingConfig;
/// 与真实配置同构的完整合法 JSON(用于派生各非法变体)
const char* kFullJson = R"({
"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
}
})";
/// 在 json 中把首个 from 替换为 to(未找到即测试失败)
void ReplaceOnce(std::string& json, const std::string& from, const std::string& to) {
const auto pos = json.find(from);
ASSERT_NE(pos, std::string::npos) << "未找到待替换片段: " << from;
json.replace(pos, from.size(), to);
}
/// 把 JSON 文本写入临时文件并返回路径(调用方用完后删除)
std::string WriteTempJson(const std::string& content, const char* name) {
const std::string path = std::string(VTC_TEST_TMP_DIR) + "/" + name;
std::ofstream ofs(path);
EXPECT_TRUE(static_cast<bool>(ofs)) << "无法写入临时配置: " << path;
ofs << content;
return path;
}
TEST(VtcConfigTest, LoadRealConfigSucceeds) {
const TrackingConfig cfg = TrackingConfig::LoadFromJson(VTC_TEST_CONFIG);
EXPECT_DOUBLE_EQ(cfg.control.frequency_hz, 20.0);
EXPECT_EQ(cfg.control.visual_stale_ms, 200);
EXPECT_EQ(cfg.camera.image_width, 1920);
EXPECT_EQ(cfg.camera.image_height, 1080);
EXPECT_DOUBLE_EQ(cfg.camera.fov_h_deg, 60.0);
EXPECT_EQ(cfg.heading.mode, HeadingMode::kRate); // A2:默认速率式
EXPECT_DOUBLE_EQ(cfg.heading.yaw_rate_limit_dps, 30.0);
EXPECT_DOUBLE_EQ(cfg.vertical.vz_gain_mps_per_deg, 0.1);
EXPECT_DOUBLE_EQ(cfg.vertical.vz_limit_mps, 2.0);
EXPECT_DOUBLE_EQ(cfg.distance.d_exp_m, 10.0);
EXPECT_EQ(cfg.distance.no_distance_action, NoDistanceAction::kHold);
EXPECT_DOUBLE_EQ(cfg.accel_limit.ax_mps2, 2.0);
}
TEST(VtcConfigTest, FullJsonRoundTripMatchesExpected) {
// 内嵌合法 JSON 与真实配置文件行为一致
const std::string path = WriteTempJson(kFullJson, "vtc_cfg_full.json");
const TrackingConfig cfg = TrackingConfig::LoadFromJson(path);
EXPECT_EQ(cfg.heading.mode, HeadingMode::kRate);
EXPECT_DOUBLE_EQ(cfg.distance.kp, 0.5);
std::remove(path.c_str());
}
TEST(VtcConfigTest, PositionModeAccepted) {
std::string json = kFullJson;
ReplaceOnce(json, "\"mode\": \"rate\"", "\"mode\": \"position\"");
const std::string path = WriteTempJson(json, "vtc_cfg_position.json");
const TrackingConfig cfg = TrackingConfig::LoadFromJson(path);
EXPECT_EQ(cfg.heading.mode, HeadingMode::kPosition);
std::remove(path.c_str());
}
TEST(VtcConfigTest, MissingFileThrowsWithPath) {
try {
TrackingConfig::LoadFromJson("/nonexistent/vtc_no_such_file.json");
FAIL() << "应当抛出异常";
} catch (const std::runtime_error& e) {
EXPECT_NE(std::string(e.what()).find("vtc_no_such_file.json"),
std::string::npos);
}
}
TEST(VtcConfigTest, MalformedJsonThrows) {
const std::string path = WriteTempJson("{ this is not json", "vtc_cfg_bad.json");
EXPECT_THROW(TrackingConfig::LoadFromJson(path), std::runtime_error);
std::remove(path.c_str());
}
TEST(VtcConfigTest, MissingFieldThrowsWithFieldName) {
std::string json = kFullJson;
ReplaceOnce(json, "\"gain\": 1.0,\n", "");
const std::string path = WriteTempJson(json, "vtc_cfg_missing.json");
try {
TrackingConfig::LoadFromJson(path);
FAIL() << "应当抛出异常";
} catch (const std::runtime_error& e) {
EXPECT_NE(std::string(e.what()).find("heading.gain"), std::string::npos);
}
std::remove(path.c_str());
}
TEST(VtcConfigTest, MissingSectionThrows) {
std::string json = kFullJson;
ReplaceOnce(json,
",\n \"accel_limit\": {\n"
" \"ax_mps2\": 2.0,\n"
" \"ay_mps2\": 2.0,\n"
" \"az_mps2\": 1.5\n"
" }",
"");
const std::string path = WriteTempJson(json, "vtc_cfg_nosection.json");
try {
TrackingConfig::LoadFromJson(path);
FAIL() << "应当抛出异常";
} catch (const std::runtime_error& e) {
EXPECT_NE(std::string(e.what()).find("accel_limit"), std::string::npos);
}
std::remove(path.c_str());
}
TEST(VtcConfigTest, InvalidHeadingModeThrows) {
std::string json = kFullJson;
ReplaceOnce(json, "\"mode\": \"rate\"", "\"mode\": \"magic\"");
const std::string path = WriteTempJson(json, "vtc_cfg_badmode.json");
try {
TrackingConfig::LoadFromJson(path);
FAIL() << "应当抛出异常";
} catch (const std::runtime_error& e) {
EXPECT_NE(std::string(e.what()).find("heading.mode"), std::string::npos);
}
std::remove(path.c_str());
}
TEST(VtcConfigTest, InvalidNoDistanceActionThrows) {
std::string json = kFullJson;
ReplaceOnce(json, "\"no_distance_action\": \"hold\"",
"\"no_distance_action\": \"zoom\"");
const std::string path = WriteTempJson(json, "vtc_cfg_badaction.json");
try {
TrackingConfig::LoadFromJson(path);
FAIL() << "应当抛出异常";
} catch (const std::runtime_error& e) {
EXPECT_NE(std::string(e.what()).find("no_distance_action"),
std::string::npos);
}
std::remove(path.c_str());
}
TEST(VtcConfigTest, NegativeFrequencyThrows) {
std::string json = kFullJson;
ReplaceOnce(json, "\"frequency_hz\": 20.0", "\"frequency_hz\": -1.0");
const std::string path = WriteTempJson(json, "vtc_cfg_negfreq.json");
try {
TrackingConfig::LoadFromJson(path);
FAIL() << "应当抛出异常";
} catch (const std::runtime_error& e) {
EXPECT_NE(std::string(e.what()).find("control.frequency_hz"),
std::string::npos);
}
std::remove(path.c_str());
}
TEST(VtcConfigTest, FovOutOfRangeThrows) {
std::string json = kFullJson;
ReplaceOnce(json, "\"fov_h_deg\": 60.0", "\"fov_h_deg\": 200.0");
const std::string path = WriteTempJson(json, "vtc_cfg_badfov.json");
try {
TrackingConfig::LoadFromJson(path);
FAIL() << "应当抛出异常";
} catch (const std::runtime_error& e) {
EXPECT_NE(std::string(e.what()).find("camera.fov_h_deg"), std::string::npos);
}
std::remove(path.c_str());
}
} // namespace