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.Reporting; 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 _ammoCatalog; private string _scenarioId = 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(TestData.Ammo); _scenario = new ScenarioService( new ScenarioRepository(_mainDb), new CombatSceneRepository(_mainDb), new ControlZoneRepository(_mainDb), new ScenarioDroneRepository(_mainDb), new ScenarioUnitRepository(_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(_scenarioId); 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.GetScenarioDetail(_scenarioId)!; var svc = new ReportService(_mainDb, _paths); var r = svc.Generate(_scenarioId, detail, eng.Events.ToList(), drone.Status.ToString(), eng.SimulationTime); Assert.Contains("对抗结果", r.Content); Assert.Contains(detail.Info.Name, r.Content); svc.ExportToFile(r.Id, ReportOutputDir, "md"); // 持久化目录 } private ScenarioConfig LoadPreset(string name) { var results = _scenario.SearchScenarios(name, null, null, 1, 1); Assert.True(results.TotalCount > 0, $"预设想定未找到: {name}"); _scenarioId = results.Items[0].Id; return _scenario.GetScenarioDetail(_scenarioId)!; } 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 detail = _scenario.GetScenarioDetail(_scenarioId)!; var reportData = new ReportGenerator().Generate(detail, eng.Events.ToList(), drone.Status, eng.SimulationTime, TestData.EmptyDroneSpecs, TestData.EmptyFuSpecs, TestData.EmptySensorSpecs); var report = new MarkdownRenderer().Render(reportData); Assert.True(drone.Hp < 0.1f, $"Hp={drone.Hp:F3} pos=({drone.PosX:F0},{drone.PosY:F0}) launched={eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched)}\n{report}"); VerifyAndExportReport(eng, drone); } // ═══════════════════════════════════════════════ // 场景 6:3 架空基编队 // ═══════════════════════════════════════════════ [Fact] public void Scenario_3DronesAirBased_AllDestroyed() { var eng = RunPreset("3架空基编队"); 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] public void Scenario_DetectionDriven_PlanningDelayedByDetectionBoundary() { // 场景A:无探测设备(上帝视角,从航路起点算) var scenarioA = _scenario.CreateScenario("无探测远航路", ""); _scenarioId = scenarioA.Id; _scenario.SaveScene(_scenarioId, new CombatScene { WindSpeed = 0, Visibility = 10000 }); _scenario.SaveScenarioDrone(_scenarioId, TestData.CreateScenarioDrone("default", 1)); _scenario.SaveRoute(_scenarioId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single }, new List { new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 }, new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 }, }); _scenario.SaveDeployment(_scenarioId, new List { TestData.All.FireUnits.First(f => f.Id == "ground-light").ToScenarioUnit(AerosolType.InertGas, 1, 8000, 0, 50), }); _scenario.AddDetection(_scenarioId, TestData.All.Sensors.First(s => s.Id == "radar-sr").ToScenarioUnit(8000, 0, 50)); _scenario.SaveCloudDispersal(_scenarioId, 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 scenarioB = _scenario.CreateScenario("有探测远航路", ""); _scenarioId = scenarioB.Id; _scenario.SaveScene(_scenarioId, new CombatScene { WindSpeed = 0, Visibility = 10000 }); _scenario.SaveScenarioDrone(_scenarioId, TestData.CreateScenarioDrone("default", 1)); _scenario.SaveRoute(_scenarioId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single }, new List { new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 }, new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 }, }); _scenario.SaveDeployment(_scenarioId, new List { TestData.All.FireUnits.First(f => f.Id == "ground-light").ToScenarioUnit(AerosolType.InertGas, 1, 8000, 0, 50), // 独立探测设备 TestData.CreateScenarioUnit(10000, 0, 0, 0, null), }); _scenario.SaveCloudDispersal(_scenarioId, 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]); } } }