Files
FlyShotHost/tests/Flyshot.Server.IntegrationTests/LegacyHttpApiCompatibilityTests.cs
yunxiao.zhu a78e6761cb feat(fanuc): 添加协议编解码与状态页" -m "* 固化 10010 状态帧、10012 命令帧和 60015 J519 包编解码
* 扩展 ControllerClient 兼容层的执行参数和运行时编排
  * 新增 /status 页面与 /api/status/snapshot 状态快照接口
  * 补充 FANUC 协议、客户端和状态接口的最小验证测试
  * 更新 README、兼容要求和真机 Socket 通信实现计划
2026-04-24 21:26:25 +08:00

290 lines
13 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.Net;
using System.Net.Http.Json;
using System.Text.Json;
namespace Flyshot.Server.IntegrationTests;
/// <summary>
/// 锁定 flyshot-uaes-interface 现有 FastAPI 层的 HTTP 路径、参数绑定和返回 JSON 外形。
/// </summary>
public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory) : IClassFixture<FlyshotServerFactory>
{
/// <summary>
/// 验证根路径会返回旧 HTTP 服务使用的 Hello World JSON而不是跳转到健康检查页。
/// </summary>
[Fact]
public async Task Root_ReturnsLegacyHelloWorldPayload()
{
using var client = factory.CreateClient();
using var response = await client.GetAsync("/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
using var json = await ReadJsonAsync(response);
Assert.Equal("Hello World", json.RootElement.GetProperty("message").GetString());
}
/// <summary>
/// 验证初始化链路和机器人信息接口会保持旧 FastAPI 服务的路径与返回字段风格。
/// </summary>
[Fact]
public async Task InitEndpoints_ExposeLegacyRobotInfoAndSpeedRatioShape()
{
using var client = factory.CreateClient();
using (var connectServerResponse = await client.PostAsync("/connect_server/?server_ip=127.0.0.1&port=50001", content: null))
{
Assert.Equal(HttpStatusCode.OK, connectServerResponse.StatusCode);
using var connectServerJson = await ReadJsonAsync(connectServerResponse);
Assert.Equal("connected", connectServerJson.RootElement.GetProperty("status").GetString());
}
using (var setupResponse = await client.PostAsync("/setup_robot/?robot_name=FANUC_LR_Mate_200iD", content: null))
{
Assert.Equal(HttpStatusCode.OK, setupResponse.StatusCode);
using var setupJson = await ReadJsonAsync(setupResponse);
Assert.Equal("robot setup", setupJson.RootElement.GetProperty("status").GetString());
}
using (var isSetupResponse = await client.GetAsync("/is_setup/"))
{
Assert.Equal(HttpStatusCode.OK, isSetupResponse.StatusCode);
using var isSetupJson = await ReadJsonAsync(isSetupResponse);
Assert.True(isSetupJson.RootElement.GetProperty("is_setup").GetBoolean());
}
using (var activeControllerResponse = await client.PostAsync("/set_active_controller/?sim=true", content: null))
{
Assert.Equal(HttpStatusCode.OK, activeControllerResponse.StatusCode);
using var activeControllerJson = await ReadJsonAsync(activeControllerResponse);
Assert.Equal("active controller set", activeControllerJson.RootElement.GetProperty("status").GetString());
}
using (var connectRobotResponse = await client.PostAsync("/connect_robot/?ip=192.168.10.101", content: null))
{
Assert.Equal(HttpStatusCode.OK, connectRobotResponse.StatusCode);
using var connectRobotJson = await ReadJsonAsync(connectRobotResponse);
Assert.Equal("robot connected", connectRobotJson.RootElement.GetProperty("status").GetString());
}
using (var enableRobotResponse = await client.GetAsync("/enable_robot/"))
{
Assert.Equal(HttpStatusCode.OK, enableRobotResponse.StatusCode);
using var enableRobotJson = await ReadJsonAsync(enableRobotResponse);
Assert.True(enableRobotJson.RootElement.GetProperty("enable_robot").GetBoolean());
}
using (var robotInfoResponse = await client.GetAsync("/robot_info/"))
{
Assert.Equal(HttpStatusCode.OK, robotInfoResponse.StatusCode);
using var robotInfoJson = await ReadJsonAsync(robotInfoResponse);
var robotInfoRoot = robotInfoJson.RootElement;
Assert.Equal("FANUC_LR_Mate_200iD", robotInfoRoot.GetProperty("name").GetString());
Assert.Equal("flyshot-replacement-controller-client-compat/0.1.0", robotInfoRoot.GetProperty("server_version").GetString());
Assert.Equal(6, robotInfoRoot.GetProperty("dof").GetInt32());
Assert.Equal(1.0, robotInfoRoot.GetProperty("speed_ratio").GetDouble(), precision: 6);
}
using (var setSpeedRatioResponse = await client.PostAsJsonAsync("/set_speedRatio/", new { speed = 0.8 }))
{
Assert.Equal(HttpStatusCode.OK, setSpeedRatioResponse.StatusCode);
using var setSpeedRatioJson = await ReadJsonAsync(setSpeedRatioResponse);
Assert.Equal("set_speedRatio executed", setSpeedRatioJson.RootElement.GetProperty("message").GetString());
Assert.Equal(0, setSpeedRatioJson.RootElement.GetProperty("returnCode").GetInt32());
}
using var updatedRobotInfoResponse = await client.GetAsync("/robot_info/");
Assert.Equal(HttpStatusCode.OK, updatedRobotInfoResponse.StatusCode);
using var updatedRobotInfoJson = await ReadJsonAsync(updatedRobotInfoResponse);
Assert.Equal(0.8, updatedRobotInfoJson.RootElement.GetProperty("speed_ratio").GetDouble(), precision: 6);
}
/// <summary>
/// 验证 TCP、关节位置和位姿相关 HTTP 接口会保持旧服务的请求体与响应体结构。
/// </summary>
[Fact]
public async Task MotionStateEndpoints_RoundTripLegacyPayloadShapes()
{
using var client = factory.CreateClient();
await InitializeRobotAsync(client);
using (var setTcpResponse = await client.PostAsJsonAsync("/set_tcp/", new { x = 1.0, y = 2.0, z = 3.0 }))
{
Assert.Equal(HttpStatusCode.OK, setTcpResponse.StatusCode);
using var setTcpJson = await ReadJsonAsync(setTcpResponse);
Assert.Equal("TCP set", setTcpJson.RootElement.GetProperty("status").GetString());
}
using (var getTcpResponse = await client.GetAsync("/get_tcp/"))
{
Assert.Equal(HttpStatusCode.OK, getTcpResponse.StatusCode);
using var getTcpJson = await ReadJsonAsync(getTcpResponse);
var tcpValues = getTcpJson.RootElement.GetProperty("tcp").EnumerateArray().Select(static value => value.GetDouble()).ToArray();
Assert.Equal([1.0, 2.0, 3.0], tcpValues);
}
using (var moveJointResponse = await client.PostAsJsonAsync("/move_joint/", new { joints = new[] { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0 } }))
{
Assert.Equal(HttpStatusCode.OK, moveJointResponse.StatusCode);
using var moveJointJson = await ReadJsonAsync(moveJointResponse);
Assert.Equal("robot moved", moveJointJson.RootElement.GetProperty("status").GetString());
}
using (var getJointPositionResponse = await client.GetAsync("/get_joint_position/"))
{
Assert.Equal(HttpStatusCode.OK, getJointPositionResponse.StatusCode);
using var getJointPositionJson = await ReadJsonAsync(getJointPositionResponse);
var root = getJointPositionJson.RootElement;
Assert.True(root.GetProperty("success").GetBoolean());
var jointValues = root.GetProperty("points").EnumerateArray().Select(static value => value.GetDouble()).ToArray();
Assert.Equal([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], jointValues);
}
using var getPoseResponse = await client.GetAsync("/get_pose");
Assert.Equal(HttpStatusCode.OK, getPoseResponse.StatusCode);
using var getPoseJson = await ReadJsonAsync(getPoseResponse);
Assert.Equal(7, getPoseJson.RootElement.GetProperty("pose").GetArrayLength());
using (var executeTrajectoryResponse = await client.PostAsJsonAsync("/execute_trajectory/", new
{
method = "icsp",
save_traj = true,
waypoints = new[]
{
new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.1, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.2, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.3, 0.0, 0.0, 0.0, 0.0, 0.0 }
}
}))
{
Assert.Equal(HttpStatusCode.OK, executeTrajectoryResponse.StatusCode);
using var executeTrajectoryJson = await ReadJsonAsync(executeTrajectoryResponse);
Assert.Equal("trajectory executed", executeTrajectoryJson.RootElement.GetProperty("status").GetString());
}
}
/// <summary>
/// 验证飞拍 HTTP 接口可以按旧 API 层的路径和字段完成上传、列出、执行与删除。
/// </summary>
[Fact]
public async Task FlyshotEndpoints_RoundTripLegacyUploadExecuteAndDeleteFlow()
{
using var client = factory.CreateClient();
await InitializeRobotAsync(client);
var uploadPayload = new
{
addrs = new[]
{
new[] { 7, 8 },
new[] { 7, 8 },
Array.Empty<int>(),
Array.Empty<int>()
},
name = "demo-http-flyshot",
offset_values = new[] { 0.0, 1.0, 0.0, 0.0 },
shot_flags = new[] { false, true, false, false },
waypoints = new[]
{
new[] { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6 },
new[] { 0.2, 0.2, 0.3, 0.4, 0.5, 0.6 },
new[] { 0.3, 0.2, 0.3, 0.4, 0.5, 0.6 },
new[] { 0.4, 0.2, 0.3, 0.4, 0.5, 0.6 }
}
};
using (var uploadResponse = await client.PostAsJsonAsync("/upload_flyshot/", uploadPayload))
{
Assert.Equal(HttpStatusCode.OK, uploadResponse.StatusCode);
using var uploadJson = await ReadJsonAsync(uploadResponse);
Assert.Equal("FlyShot uploaded", uploadJson.RootElement.GetProperty("status").GetString());
}
using (var listResponse = await client.GetAsync("/list_flyShotTraj/"))
{
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
using var listJson = await ReadJsonAsync(listResponse);
var names = listJson.RootElement.GetProperty("flyshot_trajs").EnumerateArray().Select(static value => value.GetString()).ToArray();
Assert.Contains("demo-http-flyshot", names);
}
using (var validResponse = await client.PostAsJsonAsync("/is_flyShotTrajValid/", new
{
name = "demo-http-flyshot",
method = "icsp",
save_traj = false
}))
{
Assert.Equal(HttpStatusCode.OK, validResponse.StatusCode);
using var validJson = await ReadJsonAsync(validResponse);
Assert.True(validJson.RootElement.GetProperty("valid").GetBoolean());
Assert.True(validJson.RootElement.GetProperty("time").GetDouble() > 0.0);
}
using (var executeResponse = await client.PostAsJsonAsync("/execute_flyshot/", new
{
name = "demo-http-flyshot",
move_to_start = true,
method = "icsp",
save_traj = true,
use_cache = true
}))
{
Assert.Equal(HttpStatusCode.OK, executeResponse.StatusCode);
using var executeJson = await ReadJsonAsync(executeResponse);
var executeRoot = executeJson.RootElement;
Assert.Equal("FlyShot executed", executeRoot.GetProperty("status").GetString());
Assert.True(executeRoot.GetProperty("success").GetBoolean());
}
using (var saveInfoResponse = await client.PostAsJsonAsync("/save_traj_info/", new
{
name = "demo-http-flyshot",
method = "icsp"
}))
{
Assert.Equal(HttpStatusCode.OK, saveInfoResponse.StatusCode);
using var saveInfoJson = await ReadJsonAsync(saveInfoResponse);
Assert.True(saveInfoJson.RootElement.GetProperty("success").GetBoolean());
}
using (var deleteResponse = await client.PostAsJsonAsync("/delete_flyshot/", new { name = "demo-http-flyshot" }))
{
Assert.Equal(HttpStatusCode.OK, deleteResponse.StatusCode);
using var deleteJson = await ReadJsonAsync(deleteResponse);
Assert.Equal("FlyShot deleted", deleteJson.RootElement.GetProperty("status").GetString());
}
}
/// <summary>
/// 复用旧 API 层常见的初始化顺序,把当前宿主推进到可执行动作的最小状态。
/// </summary>
private static async Task InitializeRobotAsync(HttpClient client)
{
using var initResponse = await client.PostAsJsonAsync("/init_mpc_robt", new
{
server_ip = "127.0.0.1",
port = 50001,
robot_name = "FANUC_LR_Mate_200iD",
robot_ip = "192.168.10.101",
sim = true
});
Assert.Equal(HttpStatusCode.OK, initResponse.StatusCode);
using var initJson = await ReadJsonAsync(initResponse);
Assert.Equal("init_Success", initJson.RootElement.GetProperty("message").GetString());
Assert.Equal(0, initJson.RootElement.GetProperty("returnCode").GetInt32());
}
/// <summary>
/// 统一把 HTTP 响应体解析成 JsonDocument便于对旧接口的字段形状做精确断言。
/// </summary>
private static async Task<JsonDocument> ReadJsonAsync(HttpResponseMessage response)
{
await using var responseStream = await response.Content.ReadAsStreamAsync();
return await JsonDocument.ParseAsync(responseStream);
}
}