CounterDroneBackend/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs
tian b3dec77e92 fix: DispersionModelTests 共享状态污染导致随机失败
Dissipates_AfterMaxDuration 直接用 Ammo() 返回的共享引用并修改 MaxDuration=2,后续 Phase2/Phase3 测试拿到被污染的数据

改为独立 new AmmunitionSpec 避免修改共享对象
2026-06-15 19:11:44 +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 TargetConfig CreateConfig(float speed = 120, float altitude = 300)
{
return new TargetConfig
{
TargetType = (int)TargetType.Piston,
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}");
}
}
}
}