CounterDroneBackend/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs
tian 537671ab3a DroneProfile→ScenarioDrone / EquipmentDeployment→ScenarioUnit 重命名
- 模型表: ScenarioDrone(原DroneProfile) / ScenarioUnit(原EquipmentDeployment)
- ScenarioConfig.Equipment→Units
- 数据库旧表清理 + scenariosVersion 4→5
- 238 测试全过
2026-06-18 16:36:25 +08:00

202 lines
9.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using CounterDrone.Core.Simulation;
using Xunit;
namespace CounterDrone.Core.Tests
{
public class DroneEntityTests
{
private ScenarioDrone CreateConfig(float speed = 120, float altitude = 300)
{
return new ScenarioDrone
{
DroneType = (int)DroneType.FixedWing,
PowerType = (int)PowerType.Piston,
TypicalSpeed = speed,
TypicalAltitude = altitude,
Wingspan = 2.5,
Quantity = 1,
};
}
private List<Waypoint> CreateRoute(float fromX, float toX, float alt, float speed)
{
return new List<Waypoint>
{
new() { PosX = fromX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed },
new() { PosX = toX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed },
};
}
[Fact]
public void InitialPosition_IsFirstWaypoint()
{
var drone = new DroneEntity("d1", "g1", CreateConfig(),
CreateRoute(100, 500, 300, 120), 0, 50, 0, 0, FormationMode.Single);
Assert.Equal(100, drone.PosX, 1);
Assert.Equal(300, drone.PosY, 1);
}
[Fact]
public void Update_MovesTowardTarget()
{
var drone = new DroneEntity("d1", "g1", CreateConfig(),
CreateRoute(0, 1000, 300, 120), 0, 50, 0, 0, FormationMode.Single);
float prevX = drone.PosX;
drone.Update(1f);
Assert.True(drone.PosX > prevX);
}
[Fact]
public void Update_ReachesTarget_StatusChanges()
{
var drone = new DroneEntity("d1", "g1", CreateConfig(360),
CreateRoute(0, 100, 300, 360), 0, 50, 0, 0, FormationMode.Single);
drone.Update(2f);
Assert.Equal(DroneStatus.ReachedTarget, drone.Status);
}
[Fact]
public void Damage_ReducesHp()
{
var drone = new DroneEntity("d1", "g1", CreateConfig(),
CreateRoute(0, 100, 300, 60), 0, 50, 0, 0, FormationMode.Single);
drone.ApplyDamage(0.4f);
Assert.Equal(0.6f, drone.Hp, 0.01f);
}
[Fact]
public void Damage_ExceedingHp_Destroys()
{
var drone = new DroneEntity("d1", "g1", CreateConfig(),
CreateRoute(0, 100, 300, 60), 0, 50, 0, 0, FormationMode.Single);
drone.ApplyDamage(1.5f);
Assert.True(drone.Hp <= 0);
Assert.Equal(DroneStatus.Destroyed, drone.Status);
}
[Fact]
public void Formation_Lateral_ZOffsetsDiffer()
{
var route = CreateRoute(0, 100, 300, 60);
var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 50, 0, 0, FormationMode.Formation);
var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 50, 0, 0, FormationMode.Formation);
var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 2, 50, 0, 0, FormationMode.Formation);
// 起始Z不同
Assert.NotEqual(d0.PosZ, d1.PosZ);
Assert.NotEqual(d1.PosZ, d2.PosZ);
// 航点Z不同偏移持久化到航点
Assert.NotEqual(d0.Route[0].PosZ, d1.Route[0].PosZ);
Assert.NotEqual(d0.Route[1].PosZ, d1.Route[1].PosZ);
}
[Fact]
public void Formation_Longitudinal_XOffsetsDiffer()
{
var route = CreateRoute(0, 100, 300, 60);
var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 0, 0, 100, FormationMode.Formation);
var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 0, 0, 1, 100, FormationMode.Formation);
Assert.NotEqual(d0.PosX, d1.PosX);
}
[Fact]
public void Formation_Mixed_2x2()
{
var route = CreateRoute(0, 100, 300, 60);
var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 50, 0, 100, FormationMode.Formation);
var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 50, 0, 100, FormationMode.Formation);
var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 0, 50, 1, 100, FormationMode.Formation);
var d3 = new DroneEntity("d3", "g1", CreateConfig(), route, 1, 50, 1, 100, FormationMode.Formation);
Assert.NotEqual(d0.PosZ, d1.PosZ); // lateral
Assert.NotEqual(d0.PosX, d2.PosX); // longitudinal
}
[Fact]
public void Swarm_ProducesRandomOffsets()
{
var route = CreateRoute(0, 100, 300, 60);
var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 30, 0, 0, FormationMode.Swarm);
var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 30, 0, 0, FormationMode.Swarm);
Assert.True(d0.PosX != d1.PosX || d0.PosZ != d1.PosZ);
}
// ═══════════════════════════════════════
// RouteGeometry 驱动的多 waypoint 折线运动
// 验证无人机沿 L 形航路X 段→Z 段)正确移动并最终到达终点
// ═══════════════════════════════════════
[Fact]
public void MultiWaypoint_LShape_MovesAlongBothSegments()
{
// L 形航路:(0,0) → (100,0) → (100,100),总长 200m速度 60km/h≈16.67m/s
var route = new List<Waypoint>
{
new() { PosX = 0, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 },
new() { PosX = 100, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 },
new() { PosX = 100, PosY = 300, PosZ = 100, Altitude = 300, Speed = 60 },
};
var drone = new DroneEntity("d1", "g1", CreateConfig(speed: 60), route,
0, 0, 0, 0, FormationMode.Single);
// 推进到第一段中点(弧长 50应在 X=50, Z=0
drone.Update(50f / (60f / 3.6f));
Assert.Equal(50f, drone.PosX, 1);
Assert.Equal(0f, drone.PosZ, 1);
Assert.Equal(DroneStatus.Flying, drone.Status);
// 推进到拐点(弧长 100应在 X=100, Z=0
drone.Update(50f / (60f / 3.6f));
Assert.Equal(100f, drone.PosX, 1);
Assert.Equal(0f, drone.PosZ, 1);
Assert.Equal(DroneStatus.Flying, drone.Status);
// 推进到第二段中点(弧长 150应在 X=100, Z=50
drone.Update(50f / (60f / 3.6f));
Assert.Equal(100f, drone.PosX, 1);
Assert.Equal(50f, drone.PosZ, 1);
Assert.Equal(DroneStatus.Flying, drone.Status);
// 推进到终点(弧长 200应在 X=100, Z=100状态 ReachedTarget
drone.Update(50f / (60f / 3.6f));
Assert.Equal(100f, drone.PosX, 1);
Assert.Equal(100f, drone.PosZ, 1);
Assert.Equal(DroneStatus.ReachedTarget, drone.Status);
}
// ═══════════════════════════════════════
// 弧长缓存一致性:多段航路上非整段边界处的位置必须与 RouteGeometry.PositionAt 精确一致
// 防止 Update() 内段长缓存引入的数值偏差
// ═══════════════════════════════════════
[Fact]
public void MultiWaypoint_SubSegmentPositions_MatchGeometry()
{
// 不规则三段折线段长各异50, 120, 80避免整段边界凑巧对齐
var route = new List<Waypoint>
{
new() { PosX = 0, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 },
new() { PosX = 50, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 }, // 段1: 50m
new() { PosX = 50, PosY = 300, PosZ = 120, Altitude = 300, Speed = 60 }, // 段2: 120m
new() { PosX = 130, PosY = 300, PosZ = 120, Altitude = 300, Speed = 60 }, // 段3: 80m
};
const float speed = 60f;
float speedMs = speed / 3.6f;
// 取若干非整段边界的弧长采样点,验证每一步位置 = RouteGeometry.PositionAt
float[] sampleArcs = { 17f, 50f, 73f, 170f, 200f, 249f };
foreach (var arc in sampleArcs)
{
var drone = new DroneEntity("d", "g", CreateConfig(speed), route,
0, 0, 0, 0, FormationMode.Single);
drone.Update(arc / speedMs);
var (ex, ey, ez) = RouteGeometry.PositionAt(route, arc);
Assert.True(System.Math.Abs(drone.PosX - ex) < 0.5f, $"arc={arc}: X {drone.PosX} vs {ex}");
Assert.True(System.Math.Abs(drone.PosY - ey) < 0.5f, $"arc={arc}: Y {drone.PosY} vs {ey}");
Assert.True(System.Math.Abs(drone.PosZ - ez) < 0.5f, $"arc={arc}: Z {drone.PosZ} vs {ez}");
}
}
}
}