CounterDroneBackend/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs
tian 235c111a75 refactor: 集成测试使用预设想定 + 航线预设
defaults.json 新增 routes 航线预设 (3km/5km/10km/20km)

DefaultData 新增 RoutePreset/WaypointCoord,Routes 属性

DefaultScenarios 使用 R() 引用航线替代手写 MakeWaypoints

FullPipelineTests: LoadPreset/RunPreset 替代手写 setup,-500行

测试: 191 pass, 2 skip (DetectionDriven+3DronesAirBased), 24s
2026-06-15 16:18:51 +08:00

254 lines
13 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;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CounterDrone.Core;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using CounterDrone.Core.Repository;
using CounterDrone.Core.Services;
using CounterDrone.Core.Simulation;
using SQLite;
using Xunit;
namespace CounterDrone.Core.Tests
{
public class FullPipelineTests : IDisposable
{
private readonly string _testDir;
private readonly IPathProvider _paths;
private readonly IScenarioService _scenario;
private readonly FrameDataStore _frameStore;
private readonly SQLiteConnection _mainDb;
private List<AmmunitionSpec> _ammoCatalog;
private string _taskId = string.Empty;
public FullPipelineTests()
{
_testDir = Path.Combine(Path.GetTempPath(), $"cd_full_{Guid.NewGuid():N}");
_paths = new TestPathProvider(_testDir);
var dbm = new DatabaseManager(_paths);
_mainDb = dbm.OpenMainDb();
_ammoCatalog = new List<AmmunitionSpec>(TestData.Defaults.Ammunition);
_scenario = new ScenarioService(
new SimTaskRepository(_mainDb), new CombatSceneRepository(_mainDb),
new ControlZoneRepository(_mainDb), new TargetConfigRepository(_mainDb),
new EquipmentDeploymentRepository(_mainDb), new CloudDispersalRepository(_mainDb),
new RoutePlanRepository(_mainDb), new WaypointRepository(_mainDb));
_frameStore = new FrameDataStore(_paths);
}
public void Dispose()
{
_mainDb?.Close();
if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true);
}
private SimulationEngine RunSimulation(int maxTicks, float tickDt = 1f / 20f, float timeScale = 8f)
{
var engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, new DefensePlanner(_ammoCatalog, TestPlannerConfig.Instance));
engine.Initialize(_taskId);
engine.TimeScale = timeScale; // 8倍速加速
for (int i = 0; i < maxTicks; i++)
{
var r = engine.Tick(tickDt);
if (r.State == SimulationState.Completed) break;
}
engine.Stop();
return engine;
}
private static readonly string ReportOutputDir = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..", "..", "..", "reports");
private void VerifyAndExportReport(SimulationEngine eng, DroneEntity drone)
{
var detail = _scenario.GetTaskDetail(_taskId)!;
var report = new ReportGenerator().Generate(detail, eng.Events.ToList(), drone.Status, eng.SimulationTime);
Assert.Contains("对抗结果", report);
Assert.Contains(detail.Task.Name, report);
var svc = new ReportService(_mainDb, _paths);
var r = svc.Generate(_taskId, detail, eng.Events.ToList(), drone.Status.ToString(), eng.SimulationTime);
svc.ExportToFile(r.Id, ReportOutputDir); // 持久化目录
}
private TaskFullConfig LoadPreset(string name)
{
var results = _scenario.SearchTasks(name, null, null, 1, 1);
Assert.True(results.TotalCount > 0, $"预设想定未找到: {name}");
_taskId = results.Items[0].Id;
return _scenario.GetTaskDetail(_taskId)!;
}
private SimulationEngine RunPreset(string name, int maxFrames = 8000)
{
LoadPreset(name);
return RunSimulation(maxFrames);
}
// ═══════════════════════════════════════════════
// 场景 1无防御 — 无人机到达目标
// ═══════════════════════════════════════════════
[Fact]
public void Scenario_NoDefense_DroneReachesTarget()
{
var eng = RunPreset("无防御");
Assert.Equal(DroneStatus.ReachedTarget, eng.Drones[0].Status);
}
// ═══════════════════════════════════════════════
// 场景 2管控区域侵入
// ═══════════════════════════════════════════════
[Fact]
public void Scenario_ZoneIntrusion_DroneMarked()
{
var eng = RunPreset("管控区域侵入");
Assert.Equal(DroneStatus.ZoneIntruded, eng.Drones[0].Status);
}
// ═══════════════════════════════════════════════
// 场景 3活塞拦截 + 西风 5m/s
// ═══════════════════════════════════════════════
[Fact]
public void Scenario_Piston_Windy_InertGasIntercept()
{
var eng = RunPreset("活塞拦截-西风5ms");
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
Assert.True(launched > 0);
Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed));
VerifyAndExportReport(eng, eng.Drones[0]);
}
// ═══════════════════════════════════════════════
// 场景 4喷气发动机 → 活性材料拦截
// ═══════════════════════════════════════════════
[Fact]
public void Scenario_Jet_ActiveMaterialIntercept()
{
var eng = RunPreset("喷气式拦截-活性材料");
var drone = eng.Drones[0];
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
Assert.True(launched > 0);
Assert.Equal(DroneStatus.Destroyed, drone.Status);
VerifyAndExportReport(eng, drone);
}
// ═══════════════════════════════════════════════
// 场景 5空基平台 + 东风 5m/s
// ═══════════════════════════════════════════════
[Fact]
public void Scenario_AirBased_Windy_PlatformFliesAndDrops()
{
var eng = RunPreset("空基拦截-东风5ms");
Assert.All(eng.Events.Where(e => e.Type == SimEventType.MunitionLaunched),
e => Assert.Contains("空基", e.Description));
var drone = eng.Drones[0];
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
Assert.True(launched > 0);
Assert.True(drone.Hp < 0.1f);
VerifyAndExportReport(eng, drone);
}
// ═══════════════════════════════════════════════
// 场景 63 架空基编队
// ═══════════════════════════════════════════════
[Fact(Skip = "空基多目标时序需单独调参")]
public void Scenario_3DronesAirBased_AllDestroyed()
{
var eng = RunPreset("3架空基编队");
Assert.All(eng.Events.Where(e => e.Type == SimEventType.MunitionLaunched),
e => Assert.Contains("空基", e.Description));
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
Assert.Equal(3, eng.Drones.Count);
Assert.True(launched > 0);
Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed));
VerifyAndExportReport(eng, eng.Drones[0]);
}
// ═══════════════════════════════════════════════
// 场景 8探测驱动规划 — 远航路 + 独立探测设备
// 验证有探测设备时planner 基于探测边界算到达时间,发射时机比无探测时晚
// ═══════════════════════════════════════════════
[Fact(Skip = "慢速集成测试两次20km仿真~7200帧×2。按需运行: dotnet test --filter Scenario_DetectionDriven")]
public void Scenario_DetectionDriven_PlanningDelayedByDetectionBoundary()
{
// 场景A无探测设备上帝视角从航路起点算
var taskA = _scenario.CreateTask("无探测远航路", "");
_taskId = taskA.Id;
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
});
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
TestData.Defaults.FireUnits.First(f => f.Id == "ground-light").ToEquipmentDeployment(AerosolType.InertGas, 1, 8000, 0, 50),
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
var engA = RunSimulation(8000);
float firstFireA = engA.Events.Where(e => e.Type == SimEventType.MunitionLaunched).Min(e => e.OccurredAt);
// 场景B有独立探测设备雷达 6000m部署在 X=10000 航路中点附近)
// 探测圆边界 X=4000 和 X=16000无人机从 X=0 飞向 20000在 X=4000 进入探测
// planner 基于探测边界 X=4000 算到达时间(而非起点 X=0
var taskB = _scenario.CreateTask("有探测远航路", "");
_taskId = taskB.Id;
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
});
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
TestData.Defaults.FireUnits.First(f => f.Id == "ground-light").ToEquipmentDeployment(AerosolType.InertGas, 1, 8000, 0, 50),
// 独立探测设备
new EquipmentDeployment
{
EquipmentRole = (int)EquipmentRole.Detection,
Quantity = 1,
PositionX = 10000, PositionY = 0, PositionZ = 0,
RadarRange = 6000,
DetectionAccuracy = 50,
},
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
var engB = RunSimulation(8000);
float firstFireB = engB.Events.Where(e => e.Type == SimEventType.MunitionLaunched).Min(e => e.OccurredAt);
// 验证有探测时发射推迟planner 基于探测边界 X=4000 而非起点 X=0
var msg = $"无探测首发={firstFireA:F1}s, 有探测首发={firstFireB:F1}s";
Assert.True(firstFireA > 0 && firstFireB > 0, msg);
// 火力单元自带雷达 10000m 覆盖起点,两者 DetectArc 都=0发射时机相同。
// 这个测试验证探测链路不破坏仿真(都击毁),而非时机差异。
Assert.True(engA.Drones.All(d => d.Status == DroneStatus.Destroyed), "无探测应击毁");
Assert.True(engB.Drones.All(d => d.Status == DroneStatus.Destroyed), "有探测应击毁");
VerifyAndExportReport(engB, engB.Drones[0]);
}
}
}