CounterDroneBackend/test/unit/CounterDrone.Core.Tests/SimulationEngineTests.cs
tian 3b8fb4ee86 refactor: code cleanup - warnings, dead code, naming
Warnings (9 to 0): events declared nullable, _cache nullable, IDamageModel param nullable.

Dead code removed: DefaultFormations class + FormationTemplate; AlgorithmTypes legacy types (ThreatProfile/DefenseRecommendation/DefenseSolution/MultiGroupRecommendation/RecommendedPlatform/RecommendedDetection); DroneEntity CurrentWaypointIndex/FormationOffsetX/Y/ApplyFormationOffset/wind params; Vector3 operators+Length+DistanceTo; CloudExpansionModel Phase2Duration/TimeToDensity; Kinematics EstimatedShellTime/GaussianConcentration; InterceptCandidate write-only fields (CoverageDuration/FlightTime/ShellFlightTime); ParticleParams EmitRate/ColorHex.

Naming: DefaultDefensePlanner -> DefensePlanner (only impl of IDefensePlanner); inCloudTime -> inCloudDistance (returns meters not seconds); DamageAssessment class summary fixed; wind comments clarified (drone wind removed vs cloud wind active).

Tests: 191 pass, 0 warnings.
2026-06-14 15:52:05 +08:00

192 lines
7.7 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 Xunit;
namespace CounterDrone.Core.Tests
{
/// <summary>仿真引擎基础单元测试(不含集成场景,集成场景见 FullPipelineTests</summary>
public class SimulationEngineTests : IDisposable
{
private readonly string _testDir;
private readonly SimulationEngine _engine;
private readonly IScenarioService _scenarioService;
private readonly SQLite.SQLiteConnection _mainDb;
private string _taskId = string.Empty;
public SimulationEngineTests()
{
_testDir = Path.Combine(Path.GetTempPath(), $"cd_test_{Guid.NewGuid():N}");
var paths = new TestPathProvider(_testDir);
var dbManager = new DatabaseManager(paths);
_mainDb = dbManager.OpenMainDb();
foreach (var a in DefaultAmmunition.GetAll()) _mainDb.Insert(a);
_scenarioService = 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));
_engine = new SimulationEngine(_scenarioService, new FrameDataStore(paths),
new DamageModelRouter(), paths, new DefensePlanner(DefaultAmmunition.GetAll(), TestPlannerConfig.Instance));
}
public void Dispose()
{
_engine.Stop();
_mainDb?.Close();
if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true);
}
private void SetupScenario()
{
var task = _scenarioService.CreateTask("Test", "");
_taskId = task.Id;
_scenarioService.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenarioService.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
TargetType = (int)TargetType.Piston,
Quantity = 1,
PowerType = (int)PowerType.Piston,
TypicalSpeed = 60,
});
_scenarioService.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
new EquipmentDeployment
{
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
PlatformType = (int)PlatformType.GroundBased,
Quantity = 1, PositionX = 0, PositionY = 0, PositionZ = 0,
AerosolType = (int)AerosolType.InertGas, MunitionCount = 1, Cooldown = 5,
},
});
_scenarioService.SaveCloudDispersal(_taskId, new CloudDispersal
{
AerosolType = (int)AerosolType.InertGas,
DisperseHeight = 300,
Duration = 60,
RecommendedTiming = 10,
PositionX = 1000,
PositionY = 300,
PositionZ = 0,
PositionMode = (int)PositionMode.AlgorithmRecommended,
});
_scenarioService.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 },
new Waypoint { PosX = 10000, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 },
});
}
[Fact]
public void Initialize_LoadsEntities()
{
SetupScenario();
_engine.Initialize(_taskId);
Assert.Single(_engine.Drones);
Assert.Equal(SimulationState.Running, _engine.State);
}
[Fact]
public void Tick_DroneMoves()
{
SetupScenario();
_engine.Initialize(_taskId);
_engine.Tick(1f / 20f);
Assert.True(_engine.Drones[0].PosX > 0);
}
[Fact]
public void PauseAndResume()
{
SetupScenario();
_engine.Initialize(_taskId);
_engine.Pause();
Assert.Equal(SimulationState.Paused, _engine.State);
var result = _engine.Tick(1f);
Assert.Equal(SimulationState.Paused, result.State);
_engine.Resume();
Assert.Equal(SimulationState.Running, _engine.State);
}
[Fact]
public void Stop_ClosesGracefully()
{
SetupScenario();
_engine.Initialize(_taskId);
_engine.Tick(1f / 20f);
_engine.Stop();
Assert.Equal(SimulationState.Stopped, _engine.State);
}
[Fact]
public void FrameData_IsWritten()
{
SetupScenario();
_engine.Initialize(_taskId);
for (int i = 0; i < 10; i++) _engine.Tick(1f / 20f);
var framePath = Path.Combine(_testDir, "frames", $"{_taskId}.db");
Assert.True(File.Exists(framePath));
}
[Fact]
public void AirBased_LaunchesWithCorrectDescription()
{
var task = _scenarioService.CreateTask("t", "");
_scenarioService.SaveScene(task.Id, new CombatScene());
_scenarioService.SaveTarget(task.Id, new TargetConfig { GroupId = "d", Quantity = 1, TypicalSpeed = 60, TypicalAltitude = 300 });
_scenarioService.SaveDeployment(task.Id, new List<EquipmentDeployment>
{
new() { EquipmentRole = (int)EquipmentRole.LaunchPlatform, PlatformType = (int)PlatformType.AirBased,
Quantity = 1, PositionX = 0, PositionY = 2000, PositionZ = 0,
CruiseSpeed = 55, ReleaseAltitude = 1500, AerosolType = (int)AerosolType.InertGas, MunitionCount = 1 },
});
_scenarioService.SaveCloudDispersal(task.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 300 });
_scenarioService.SaveRoute(task.Id, "d", new RoutePlan(), new() { new() { PosX = 0, PosY = 300, PosZ = 0, Speed = 60 }, new() { PosX = 5000, PosY = 300, PosZ = 0, Speed = 60 } });
_engine.SetFireSchedule(new List<FireEvent> { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 2500, TargetY = 300, TargetZ = 0 } });
_engine.Initialize(task.Id);
_engine.TimeScale = 10f;
SimEvent launch = null;
for (int i = 0; i < 5000; i++)
{
_engine.Tick(1f / 20f);
launch = _engine.Events.FirstOrDefault(e => e.Type == SimEventType.MunitionLaunched);
if (launch != null) break;
if (_engine.State == SimulationState.Completed) break;
}
Assert.NotNull(launch);
Assert.Contains("空基", launch.Description);
}
[Fact]
public void GroundBased_LaunchesWithCorrectDescription()
{
SetupScenario();
_engine.SetFireSchedule(new List<FireEvent> { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 1000, TargetY = 300, TargetZ = 0, MuzzleVelocity = 800 } });
_engine.Initialize(_taskId);
SimEvent launch = null;
for (int i = 0; i < 50; i++) { _engine.Tick(1f / 20f); launch = _engine.Events.FirstOrDefault(e => e.Type == SimEventType.MunitionLaunched); if (launch != null) break; }
Assert.NotNull(launch);
Assert.Contains("计划发射", launch.Description);
}
}
}