using System; using MultiWheelC.Fleet; using MyParking.Shared; namespace MultiWheelC.Tests { internal static class FleetLayoutCaptureTests { private const double Tolerance = 1e-9; public static void Run() { VerifySymmetricTailToTailLayout(); VerifyAsymmetricLayoutAndPoseReconstruction(); VerifyInputOrderDoesNotChangeResult(); VerifyEmptyInputIsRejected(); VerifyDuplicateVehicleIdIsRejected(); VerifyMissingLeaderIsRejected(); Console.WriteLine( "FleetLayoutCapture布局建立测试通过。共6个场景。"); } private static void VerifySymmetricTailToTailLayout() { var result = FleetLayoutCapture.Capture( new[] { new FleetMemberPose( 1, new Pose2D(-1.2, 0.0, 0.0)), new FleetMemberPose( 2, new Pose2D(1.2, 0.0, Math.PI)) }, leaderVehicleId: 1); AssertPose( result.FleetPoseInWorld, Pose2D.Identity, "对称双车中心"); AssertVehicleLayout( result.Layout, 1, new Pose2D(-1.2, 0.0, 0.0), "对称双车主车布局"); AssertVehicleLayout( result.Layout, 2, new Pose2D(1.2, 0.0, Math.PI), "对称双车从车布局"); } private static void VerifyAsymmetricLayoutAndPoseReconstruction() { var members = new[] { new FleetMemberPose( 3, new Pose2D(1.0, 1.0, 0.4)), new FleetMemberPose( 1, new Pose2D(4.0, 1.0, 0.4)), new FleetMemberPose( 2, new Pose2D(1.0, 4.0, -0.8)) }; var result = FleetLayoutCapture.Capture( members, leaderVehicleId: 1); AssertPose( result.FleetPoseInWorld, new Pose2D(2.0, 2.0, 0.4), "非对称三车中心"); for (var index = 0; index < members.Length; index++) { if (!result.Layout.TryGetVehicle( members[index].VehicleId, out var vehicleLayout)) { throw new InvalidOperationException( "非对称布局缺少成员车。" + members[index].VehicleId); } var reconstructedPoseInWorld = FrameTransform2D.Compose( result.FleetPoseInWorld, vehicleLayout.PoseInFleet); AssertPose( reconstructedPoseInWorld, members[index].PoseInWorld, "非对称布局世界位姿还原"); } } private static void VerifyInputOrderDoesNotChangeResult() { var first = FleetLayoutCapture.Capture( new[] { new FleetMemberPose( 1, new Pose2D(2.0, 3.0, 0.6)), new FleetMemberPose( 2, new Pose2D(4.0, 5.0, -1.0)) }, leaderVehicleId: 1); var second = FleetLayoutCapture.Capture( new[] { new FleetMemberPose( 2, new Pose2D(4.0, 5.0, -1.0)), new FleetMemberPose( 1, new Pose2D(2.0, 3.0, 0.6)) }, leaderVehicleId: 1); AssertPose( first.FleetPoseInWorld, second.FleetPoseInWorld, "输入顺序不变中心"); AssertSameLayout(first.Layout, second.Layout); } private static void VerifyEmptyInputIsRejected() { ExpectException( () => FleetLayoutCapture.Capture( Array.Empty(), leaderVehicleId: 1), "空成员集合"); } private static void VerifyDuplicateVehicleIdIsRejected() { ExpectException( () => FleetLayoutCapture.Capture( new[] { new FleetMemberPose( 1, Pose2D.Identity), new FleetMemberPose( 1, new Pose2D(1.0, 0.0, 0.0)) }, leaderVehicleId: 1), "重复车号"); } private static void VerifyMissingLeaderIsRejected() { ExpectException( () => FleetLayoutCapture.Capture( new[] { new FleetMemberPose( 2, Pose2D.Identity) }, leaderVehicleId: 1), "缺少主车"); } private static void AssertSameLayout( FleetLayout first, FleetLayout second) { if (first.VehicleCount != second.VehicleCount) { throw new InvalidOperationException( "输入顺序变化后成员数量发生变化。"); } for (var index = 0; index < first.Vehicles.Count; index++) { var vehicle = first.Vehicles[index]; AssertVehicleLayout( second, vehicle.VehicleId, vehicle.PoseInFleet, "输入顺序不变布局"); } } private static void AssertVehicleLayout( FleetLayout layout, int vehicleId, Pose2D expectedPoseInFleet, string scenario) { if (!layout.TryGetVehicle( vehicleId, out var vehicle)) { throw new InvalidOperationException( $"{scenario}缺少车辆{vehicleId}。"); } AssertPose( vehicle.PoseInFleet, expectedPoseInFleet, scenario); } private static void AssertPose( Pose2D actual, Pose2D expected, string scenario) { AssertNear( actual.XMeters, expected.XMeters, scenario + " X"); AssertNear( actual.YMeters, expected.YMeters, scenario + " Y"); AssertNear( AngleMath.NormalizeRadians( actual.YawRadians - expected.YawRadians), 0.0, scenario + " Yaw"); } private static void AssertNear( double actual, double expected, string valueName) { if (Math.Abs(actual - expected) > Tolerance) { throw new InvalidOperationException( $"{valueName}错误:" + $"actual={actual:F9}, expected={expected:F9}。"); } } private static void ExpectException( Action action, string scenario) where TException : Exception { try { action(); } catch (TException) { return; } throw new InvalidOperationException( $"{scenario}没有抛出{typeof(TException).Name}。"); } } }