diff --git a/src/CounterDrone.Core/Simulation/FrameDataStore.cs b/src/CounterDrone.Core/Simulation/FrameDataStore.cs index 1c87c1a..53a8737 100644 --- a/src/CounterDrone.Core/Simulation/FrameDataStore.cs +++ b/src/CounterDrone.Core/Simulation/FrameDataStore.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using CounterDrone.Core.Models; @@ -8,6 +9,8 @@ namespace CounterDrone.Core.Simulation public class FrameDataStore { private readonly IPathProvider _paths; + /// 帧数据保留天数,默认 180 天(6 个月) + public int RetentionDays { get; set; } = 180; public FrameDataStore(IPathProvider paths) { @@ -58,5 +61,28 @@ namespace CounterDrone.Core.Simulation var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db"); if (File.Exists(dbPath)) File.Delete(dbPath); } + + /// 清理过期的帧数据库文件 + /// 被删除的文件数量 + public int CleanupExpired() + { + var dir = _paths.GetFramesDir(); + if (!Directory.Exists(dir)) return 0; + + var cutoff = DateTime.Now.AddDays(-RetentionDays); + var deleted = 0; + + foreach (var file in Directory.GetFiles(dir, "*.db")) + { + var creationTime = File.GetCreationTime(file); + if (creationTime < cutoff) + { + File.Delete(file); + deleted++; + } + } + + return deleted; + } } } diff --git a/test/reports/喷气拦截_20260611_083136.md b/test/reports/喷气拦截_20260611_083136.md new file mode 100644 index 0000000..1d24d50 --- /dev/null +++ b/test/reports/喷气拦截_20260611_083136.md @@ -0,0 +1,47 @@ +# 仿真评估报告 + +**任务名称**:喷气拦截 +**任务编号**:SIM-20260611-004 +**仿真耗时**:106.4 秒 + +## 一、仿真前 — 我方配置 + +| 配置项 | 值 | +|--------|-----| +| 目标数量 | 1 | +| 发射平台 | 6 台 | +| 探测设备 | 1 台 | +| 气溶胶类型 | ActiveMaterial | + +## 二、仿真中 — 关键事件时序 + +| 时间(s) | 事件 | 描述 | +|---------|------|------| +| 92.2 | MunitionLaunched | 计划发射 mun_1 | +| 92.2 | MunitionLaunched | 计划发射 mun_2 | +| 92.2 | MunitionLaunched | 计划发射 mun_3 | +| 92.2 | MunitionLaunched | 计划发射 mun_4 | +| 92.2 | MunitionLaunched | 计划发射 mun_5 | +| 92.2 | MunitionLaunched | 计划发射 mun_6 | +| 101.8 | CloudGenerated | 云团生成 | +| 101.8 | CloudGenerated | 云团生成 | +| 101.8 | CloudGenerated | 云团生成 | +| 101.8 | CloudGenerated | 云团生成 | +| 101.8 | CloudGenerated | 云团生成 | +| 101.8 | CloudGenerated | 云团生成 | +| 106.4 | DroneDestroyed | 无人机 drone_1 被摧毁 | +| 106.4 | SimulationEnd | | + +## 三、仿真后 — 数据统计 + +| 统计项 | 值 | +|--------|-----| +| 无人机总数 | 1 | +| 被摧毁 | 1 | +| 到达目标 | 0 | +| 侵入管控区 | 0 | +| 弹药发射次数 | 6 | +| 云团生成次数 | 6 | + +**对抗结果**:成功拦截 + diff --git a/test/reports/喷气拦截_20260611_083213.md b/test/reports/喷气拦截_20260611_083213.md new file mode 100644 index 0000000..e2f757d --- /dev/null +++ b/test/reports/喷气拦截_20260611_083213.md @@ -0,0 +1,47 @@ +# 仿真评估报告 + +**任务名称**:喷气拦截 +**任务编号**:SIM-20260611-003 +**仿真耗时**:106.4 秒 + +## 一、仿真前 — 我方配置 + +| 配置项 | 值 | +|--------|-----| +| 目标数量 | 1 | +| 发射平台 | 6 台 | +| 探测设备 | 1 台 | +| 气溶胶类型 | ActiveMaterial | + +## 二、仿真中 — 关键事件时序 + +| 时间(s) | 事件 | 描述 | +|---------|------|------| +| 92.2 | MunitionLaunched | 计划发射 mun_1 | +| 92.2 | MunitionLaunched | 计划发射 mun_2 | +| 92.2 | MunitionLaunched | 计划发射 mun_3 | +| 92.2 | MunitionLaunched | 计划发射 mun_4 | +| 92.2 | MunitionLaunched | 计划发射 mun_5 | +| 92.2 | MunitionLaunched | 计划发射 mun_6 | +| 101.8 | CloudGenerated | 云团生成 | +| 101.8 | CloudGenerated | 云团生成 | +| 101.8 | CloudGenerated | 云团生成 | +| 101.8 | CloudGenerated | 云团生成 | +| 101.8 | CloudGenerated | 云团生成 | +| 101.8 | CloudGenerated | 云团生成 | +| 106.4 | DroneDestroyed | 无人机 drone_1 被摧毁 | +| 106.4 | SimulationEnd | | + +## 三、仿真后 — 数据统计 + +| 统计项 | 值 | +|--------|-----| +| 无人机总数 | 1 | +| 被摧毁 | 1 | +| 到达目标 | 0 | +| 侵入管控区 | 0 | +| 弹药发射次数 | 6 | +| 云团生成次数 | 6 | + +**对抗结果**:成功拦截 + diff --git a/test/reports/活塞拦截_20260611_083140.md b/test/reports/活塞拦截_20260611_083140.md new file mode 100644 index 0000000..7bfa144 --- /dev/null +++ b/test/reports/活塞拦截_20260611_083140.md @@ -0,0 +1,46 @@ +# 仿真评估报告 + +**任务名称**:活塞拦截 +**任务编号**:SIM-20260611-041 +**仿真耗时**:185.6 秒 + +## 一、仿真前 — 我方配置 + +| 配置项 | 值 | +|--------|-----| +| 目标数量 | 1 | +| 发射平台 | 5 台 | +| 探测设备 | 1 台 | +| 气溶胶类型 | InertGas | + +## 二、仿真中 — 关键事件时序 + +| 时间(s) | 事件 | 描述 | +|---------|------|------| +| 167.4 | MunitionLaunched | 计划发射 mun_1 | +| 167.4 | MunitionLaunched | 计划发射 mun_2 | +| 167.4 | MunitionLaunched | 计划发射 mun_3 | +| 167.4 | MunitionLaunched | 计划发射 mun_4 | +| 167.4 | MunitionLaunched | 计划发射 mun_5 | +| 177.0 | CloudGenerated | 云团生成 | +| 177.0 | CloudGenerated | 云团生成 | +| 177.0 | CloudGenerated | 云团生成 | +| 177.0 | CloudGenerated | 云团生成 | +| 177.0 | CloudGenerated | 云团生成 | +| 185.6 | DroneDestroyed | 无人机 drone_1 被摧毁 | +| 185.6 | DroneDestroyed | 无人机 drone_1 被摧毁 | +| 185.6 | SimulationEnd | | + +## 三、仿真后 — 数据统计 + +| 统计项 | 值 | +|--------|-----| +| 无人机总数 | 1 | +| 被摧毁 | 2 | +| 到达目标 | 0 | +| 侵入管控区 | 0 | +| 弹药发射次数 | 5 | +| 云团生成次数 | 5 | + +**对抗结果**:成功拦截 + diff --git a/test/reports/活塞拦截_20260611_083216.md b/test/reports/活塞拦截_20260611_083216.md new file mode 100644 index 0000000..7bfa144 --- /dev/null +++ b/test/reports/活塞拦截_20260611_083216.md @@ -0,0 +1,46 @@ +# 仿真评估报告 + +**任务名称**:活塞拦截 +**任务编号**:SIM-20260611-041 +**仿真耗时**:185.6 秒 + +## 一、仿真前 — 我方配置 + +| 配置项 | 值 | +|--------|-----| +| 目标数量 | 1 | +| 发射平台 | 5 台 | +| 探测设备 | 1 台 | +| 气溶胶类型 | InertGas | + +## 二、仿真中 — 关键事件时序 + +| 时间(s) | 事件 | 描述 | +|---------|------|------| +| 167.4 | MunitionLaunched | 计划发射 mun_1 | +| 167.4 | MunitionLaunched | 计划发射 mun_2 | +| 167.4 | MunitionLaunched | 计划发射 mun_3 | +| 167.4 | MunitionLaunched | 计划发射 mun_4 | +| 167.4 | MunitionLaunched | 计划发射 mun_5 | +| 177.0 | CloudGenerated | 云团生成 | +| 177.0 | CloudGenerated | 云团生成 | +| 177.0 | CloudGenerated | 云团生成 | +| 177.0 | CloudGenerated | 云团生成 | +| 177.0 | CloudGenerated | 云团生成 | +| 185.6 | DroneDestroyed | 无人机 drone_1 被摧毁 | +| 185.6 | DroneDestroyed | 无人机 drone_1 被摧毁 | +| 185.6 | SimulationEnd | | + +## 三、仿真后 — 数据统计 + +| 统计项 | 值 | +|--------|-----| +| 无人机总数 | 1 | +| 被摧毁 | 2 | +| 到达目标 | 0 | +| 侵入管控区 | 0 | +| 弹药发射次数 | 5 | +| 云团生成次数 | 5 | + +**对抗结果**:成功拦截 + diff --git a/test/unit/CounterDrone.Core.Tests/EdgeCaseTests.cs b/test/unit/CounterDrone.Core.Tests/EdgeCaseTests.cs new file mode 100644 index 0000000..9f586bb --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/EdgeCaseTests.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.IO; +using CounterDrone.Core; +using CounterDrone.Core.Algorithms; +using CounterDrone.Core.Models; +using CounterDrone.Core.Repository; +using CounterDrone.Core.Services; +using CounterDrone.Core.Simulation; +using Xunit; + +namespace CounterDrone.Core.Tests +{ + /// 边界场景测试 + public class EdgeCaseTests : IDisposable + { + private readonly string _testDir; + private readonly IScenarioService _scenario; + private readonly SQLite.SQLiteConnection _db; + private string _taskId = string.Empty; + + public EdgeCaseTests() + { + _testDir = Path.Combine(Path.GetTempPath(), $"cd_edge_{Guid.NewGuid():N}"); + var paths = new TestPathProvider(_testDir); + var dbm = new DatabaseManager(paths); + _db = dbm.OpenMainDb(); + + _db.Insert(new AmmunitionSpec + { + AerosolType = (int)AerosolType.InertGas, InitialRadius = 50, + CoreDensity = 1.0, DispersionRateBase = 5, MaxRadius = 500, + MaxDuration = 120, EffectiveConcentration = 0.05, + }); + + _scenario = new ScenarioService( + new SimTaskRepository(_db), new CombatSceneRepository(_db), + new ControlZoneRepository(_db), new TargetConfigRepository(_db), + new EquipmentDeploymentRepository(_db), new CloudDispersalRepository(_db), + new RoutePlanRepository(_db), new WaypointRepository(_db)); + } + + public void Dispose() + { + _db?.Close(); + if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true); + } + + [Fact] + public void EmptyDeployment_EngineRunsWithoutCrash() + { + var task = _scenario.CreateTask("空部署", ""); + _taskId = task.Id; + + _scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 }); + _scenario.SaveTarget(_taskId, new TargetConfig + { + TargetType = (int)TargetType.Piston, Quantity = 1, + TypicalSpeed = 100, TypicalAltitude = 300, + }); + _scenario.SaveDeployment(_taskId, new List()); + _scenario.SaveCloudDispersal(_taskId, new CloudDispersal()); + _scenario.SaveRoute(_taskId, new RoutePlan { FormationMode = (int)FormationMode.Single }, + new List + { + new Waypoint { PosX = 0, PosY = 300, PosZ = 0 }, + new Waypoint { PosX = 1000, PosY = 300, PosZ = 0 }, + }); + + var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)), + new DamageModelRouter(), new TestPathProvider(_testDir)); + engine.Initialize(_taskId); + engine.TimeScale = 4f; + + for (int i = 0; i < 500; i++) + { + var r = engine.Tick(1f / 20f); + if (r.State == SimulationState.Completed) break; + } + engine.Stop(); + + Assert.Equal(DroneStatus.ReachedTarget, engine.Drones[0].Status); + } + + [Fact] + public void ExtremeWind_DroneStillFlies() + { + var task = _scenario.CreateTask("飓风", ""); + _taskId = task.Id; + + _scenario.SaveScene(_taskId, new CombatScene + { + WindSpeed = 30, // 最大风速 + WindDirection = (int)WindDirection.E, + }); + _scenario.SaveTarget(_taskId, new TargetConfig + { + TargetType = (int)TargetType.Piston, Quantity = 1, + TypicalSpeed = 600, // 高速对抗强风 + }); + _scenario.SaveDeployment(_taskId, new List()); + _scenario.SaveCloudDispersal(_taskId, new CloudDispersal()); + _scenario.SaveRoute(_taskId, new RoutePlan { FormationMode = (int)FormationMode.Single }, + new List + { + new Waypoint { PosX = 0, PosY = 300, PosZ = 0 }, + new Waypoint { PosX = 5000, PosY = 300, PosZ = 0 }, + }); + + var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)), + new DamageModelRouter(), new TestPathProvider(_testDir)); + engine.Initialize(_taskId); + engine.TimeScale = 4f; + + for (int i = 0; i < 500; i++) + { + var r = engine.Tick(1f / 20f); + if (r.State == SimulationState.Completed) break; + } + engine.Stop(); + + // 强风下应偏移到东方(+X) + Assert.True(engine.Drones[0].PosX > 0); + } + + [Fact] + public void LongRoute_CompletesWithoutError() + { + var task = _scenario.CreateTask("超长航路", ""); + _taskId = task.Id; + + _scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 }); + _scenario.SaveTarget(_taskId, new TargetConfig + { + TargetType = (int)TargetType.HighSpeed, Quantity = 1, + TypicalSpeed = 500, + }); + _scenario.SaveDeployment(_taskId, new List()); + _scenario.SaveCloudDispersal(_taskId, new CloudDispersal()); + + // 500 km/h, 100km 航程 + _scenario.SaveRoute(_taskId, new RoutePlan { FormationMode = (int)FormationMode.Single }, + new List + { + new Waypoint { PosX = 0, PosY = 500, PosZ = 0 }, + new Waypoint { PosX = 100000, PosY = 500, PosZ = 0 }, + }); + + var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)), + new DamageModelRouter(), new TestPathProvider(_testDir)); + engine.Initialize(_taskId); + engine.TimeScale = 4f; + + for (int i = 0; i < 5000; i++) + { + var r = engine.Tick(1f / 20f); + if (r.State == SimulationState.Completed) break; + } + engine.Stop(); + + Assert.Equal(DroneStatus.ReachedTarget, engine.Drones[0].Status); + } + } +} diff --git a/test/unit/CounterDrone.Core.Tests/FrameDataStoreTests.cs b/test/unit/CounterDrone.Core.Tests/FrameDataStoreTests.cs new file mode 100644 index 0000000..7dc0b55 --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/FrameDataStoreTests.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using CounterDrone.Core; +using CounterDrone.Core.Simulation; +using Xunit; + +namespace CounterDrone.Core.Tests +{ + public class FrameDataStoreTests : IDisposable + { + private readonly string _testDir; + private readonly FrameDataStore _store; + + public FrameDataStoreTests() + { + _testDir = Path.Combine(Path.GetTempPath(), $"cd_frames_{Guid.NewGuid():N}"); + var paths = new TestPathProvider(_testDir); + _store = new FrameDataStore(paths); + } + + public void Dispose() + { + if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true); + } + + [Fact] + public void CleanupExpired_DeletesOldFiles() + { + // 设置为 0 天保留,所有文件都是"过期"的 + _store.RetentionDays = 0; + + // 创建一些假帧数据库 + var db1 = _store.CreateOrOpen("task_old_1"); + db1.Close(); + var db2 = _store.CreateOrOpen("task_old_2"); + db2.Close(); + + var deleted = _store.CleanupExpired(); + Assert.Equal(2, deleted); + + // 文件应该被删除 + Assert.False(File.Exists(Path.Combine(_testDir, "frames", "task_old_1.db"))); + Assert.False(File.Exists(Path.Combine(_testDir, "frames", "task_old_2.db"))); + } + + [Fact] + public void CleanupExpired_KeepsRecentFiles() + { + _store.RetentionDays = 365; // 保留 1 年 + + var db = _store.CreateOrOpen("task_new"); + db.Close(); + + // 手动修改创建时间为"最近" + var filePath = Path.Combine(_testDir, "frames", "task_new.db"); + File.SetCreationTime(filePath, DateTime.Now); + + var deleted = _store.CleanupExpired(); + Assert.Equal(0, deleted); + + // 文件应该保留 + Assert.True(File.Exists(filePath)); + } + + [Fact] + public void CleanupExpired_EmptyDir_ReturnsZero() + { + var deleted = _store.CleanupExpired(); + Assert.Equal(0, deleted); + } + } +}