✨ feat: 初始化飞拍替换方案仓库骨架
* 建立 .NET 8 解决方案及分层项目结构 * 添加 Flyshot.Core.Domain 领域模型(机器人、轨迹、运动学) * 添加 Flyshot.Core.Planning 规划层(ICSP、CubicSpline、采样器) * 添加 Flyshot.Core.Triggering 触发时序与 IO 时间轴 * 添加 Flyshot.Core.Config 配置兼容与 .robot 解析 * 添加 Flyshot.Server.Host 最小宿主及 /healthz 端点 * 补充单元测试与集成测试项目 * 添加 CLAUDE.md、AGENTS.md、README.md 项目规范
This commit is contained in:
207
tests/Flyshot.Core.Tests/DomainModelTests.cs
Normal file
207
tests/Flyshot.Core.Tests/DomainModelTests.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using System.Text.Json;
|
||||
using Flyshot.Core.Domain;
|
||||
|
||||
namespace Flyshot.Core.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the Task 2 domain contracts before planning and runtime code depend on them.
|
||||
/// </summary>
|
||||
public sealed class DomainModelTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensures robot profiles keep a stable copy of joint limits and reject invalid dimensions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RobotProfile_CopiesJointLimits_AndRejectsMismatchedDof()
|
||||
{
|
||||
var jointLimits = new[]
|
||||
{
|
||||
new JointLimit("J1", 7.85, 32.72, 272.7),
|
||||
new JointLimit("J2", 6.63, 27.63, 230.28)
|
||||
};
|
||||
|
||||
var profile = new RobotProfile(
|
||||
name: "LR_Mate_200iD_7L",
|
||||
modelPath: "Models/LR_Mate_200iD_7L.robot",
|
||||
degreesOfFreedom: 2,
|
||||
jointLimits: jointLimits,
|
||||
jointCouplings: Array.Empty<JointCoupling>(),
|
||||
servoPeriod: TimeSpan.FromMilliseconds(8),
|
||||
triggerPeriod: TimeSpan.FromMilliseconds(8));
|
||||
|
||||
Assert.Equal(2, profile.DegreesOfFreedom);
|
||||
Assert.NotSame(jointLimits, profile.JointLimits);
|
||||
|
||||
// The planner must not accept a profile whose DOF and limits disagree.
|
||||
Assert.Throws<ArgumentException>(() => new RobotProfile(
|
||||
name: "InvalidRobot",
|
||||
modelPath: "Models/Invalid.robot",
|
||||
degreesOfFreedom: 3,
|
||||
jointLimits: jointLimits,
|
||||
jointCouplings: Array.Empty<JointCoupling>(),
|
||||
servoPeriod: TimeSpan.FromMilliseconds(8),
|
||||
triggerPeriod: TimeSpan.FromMilliseconds(8)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures uploaded flyshot programs keep their shot metadata aligned with teach waypoints.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FlyshotProgram_RejectsMisalignedShotMetadata()
|
||||
{
|
||||
var waypoints = new[]
|
||||
{
|
||||
new JointWaypoint(new[] { 0.0, 1.0 }),
|
||||
new JointWaypoint(new[] { 2.0, 3.0 })
|
||||
};
|
||||
|
||||
var validProgram = new FlyshotProgram(
|
||||
name: "EOL10_EAU_0",
|
||||
waypoints: waypoints,
|
||||
shotFlags: new[] { true, false },
|
||||
offsetValues: new[] { 0, 1 },
|
||||
addressGroups: new[]
|
||||
{
|
||||
new IoAddressGroup(new[] { 100 }),
|
||||
new IoAddressGroup(Array.Empty<int>())
|
||||
});
|
||||
|
||||
Assert.Equal(1, validProgram.ShotWaypointCount);
|
||||
|
||||
// The gateway cannot recover from count mismatches after this point, so fail fast here.
|
||||
Assert.Throws<ArgumentException>(() => new FlyshotProgram(
|
||||
name: "BrokenProgram",
|
||||
waypoints: waypoints,
|
||||
shotFlags: new[] { true },
|
||||
offsetValues: new[] { 0, 1 },
|
||||
addressGroups: new[]
|
||||
{
|
||||
new IoAddressGroup(new[] { 100 }),
|
||||
new IoAddressGroup(Array.Empty<int>())
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures execution requests start from predictable defaults for compatibility paths.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TrajectoryRequest_UsesExpectedDefaults()
|
||||
{
|
||||
var request = new TrajectoryRequest(
|
||||
robot: CreateRobotProfile(),
|
||||
program: CreateProgram(),
|
||||
method: PlanningMethod.Icsp);
|
||||
|
||||
Assert.False(request.MoveToStart);
|
||||
Assert.False(request.SaveTrajectoryArtifacts);
|
||||
Assert.False(request.UseCache);
|
||||
Assert.Equal(PlanningMethod.Icsp, request.Method);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures runtime snapshots expose safe empty collections before the controller connects.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ControllerStateSnapshot_InitializesEmptyRuntimeCollections()
|
||||
{
|
||||
var snapshot = new ControllerStateSnapshot(
|
||||
capturedAt: DateTimeOffset.Parse("2026-04-23T10:00:00+08:00"),
|
||||
connectionState: "Disconnected",
|
||||
isEnabled: false,
|
||||
isInMotion: false,
|
||||
speedRatio: 100.0);
|
||||
|
||||
Assert.Empty(snapshot.JointPositions);
|
||||
Assert.Empty(snapshot.CartesianPose);
|
||||
Assert.Empty(snapshot.ActiveAlarms);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures JSON payloads keep stable enum and property names for downstream SDKs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DomainObjects_SerializeStableContract()
|
||||
{
|
||||
var result = new TrajectoryResult(
|
||||
programName: "EOL10_EAU_0",
|
||||
method: PlanningMethod.SelfAdaptIcsp,
|
||||
isValid: true,
|
||||
duration: TimeSpan.FromSeconds(1.25),
|
||||
shotEvents: new[]
|
||||
{
|
||||
new ShotEvent(
|
||||
waypointIndex: 0,
|
||||
triggerTime: 0.5,
|
||||
sampleIndex: 62,
|
||||
sampleTime: 0.496,
|
||||
addressGroup: new IoAddressGroup(new[] { 100 }))
|
||||
},
|
||||
triggerTimeline: new[]
|
||||
{
|
||||
new TrajectoryDoEvent(
|
||||
waypointIndex: 0,
|
||||
triggerTime: 0.5,
|
||||
offsetCycles: 0,
|
||||
holdCycles: 1,
|
||||
addressGroup: new IoAddressGroup(new[] { 100 }))
|
||||
},
|
||||
artifacts: new[]
|
||||
{
|
||||
new TrajectoryArtifact(
|
||||
kind: TrajectoryArtifactKind.JointDenseTrajectory,
|
||||
logicalName: "JointDetialTraj.txt",
|
||||
relativePath: "artifacts/JointDetialTraj.txt")
|
||||
},
|
||||
failureReason: null,
|
||||
usedCache: true,
|
||||
originalWaypointCount: 4,
|
||||
plannedWaypointCount: 5);
|
||||
|
||||
var json = JsonSerializer.Serialize(result);
|
||||
|
||||
Assert.Contains("\"programName\":\"EOL10_EAU_0\"", json);
|
||||
Assert.Contains("\"method\":\"SelfAdaptIcsp\"", json);
|
||||
Assert.Contains("\"kind\":\"JointDenseTrajectory\"", json);
|
||||
Assert.Contains("\"usedCache\":true", json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a representative robot profile for request-level domain tests.
|
||||
/// </summary>
|
||||
private static RobotProfile CreateRobotProfile()
|
||||
{
|
||||
return new RobotProfile(
|
||||
name: "LR_Mate_200iD_7L",
|
||||
modelPath: "Models/LR_Mate_200iD_7L.robot",
|
||||
degreesOfFreedom: 2,
|
||||
jointLimits: new[]
|
||||
{
|
||||
new JointLimit("J1", 7.85, 32.72, 272.7),
|
||||
new JointLimit("J2", 6.63, 27.63, 230.28)
|
||||
},
|
||||
jointCouplings: Array.Empty<JointCoupling>(),
|
||||
servoPeriod: TimeSpan.FromMilliseconds(8),
|
||||
triggerPeriod: TimeSpan.FromMilliseconds(8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a representative uploaded program for request-level domain tests.
|
||||
/// </summary>
|
||||
private static FlyshotProgram CreateProgram()
|
||||
{
|
||||
return new FlyshotProgram(
|
||||
name: "EOL10_EAU_0",
|
||||
waypoints: new[]
|
||||
{
|
||||
new JointWaypoint(new[] { 0.0, 1.0 }),
|
||||
new JointWaypoint(new[] { 2.0, 3.0 })
|
||||
},
|
||||
shotFlags: new[] { true, false },
|
||||
offsetValues: new[] { 0, 1 },
|
||||
addressGroups: new[]
|
||||
{
|
||||
new IoAddressGroup(new[] { 100 }),
|
||||
new IoAddressGroup(Array.Empty<int>())
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user