Files
FlyShotHost/tests/Flyshot.Server.IntegrationTests/LegacyHttpApiCompatibilityTests.cs
yunxiao.zhu 5193f537b0 ✨ feat(planning): 添加离线飞拍规划能力
* 固化离线飞拍轨迹加载、保存和规划结果。

* 真机初始化失败时保留模型加载后的离线规划能力。

* 更新状态接口、调试页和回归测试覆盖。
2026-06-24 11:40:07 +08:00

308 lines
14 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;
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>
/// 验证计划中明确移除的旧路由不会再出现在宿主中。
/// </summary>
/// <param name="method">请求方法。</param>
/// <param name="path">已移除路由。</param>
[Theory]
[InlineData("GET", "/")]
[InlineData("POST", "/connect_server/?server_ip=127.0.0.1&port=50001")]
[InlineData("GET", "/get_server_version/")]
[InlineData("GET", "/get_client_version/")]
[InlineData("POST", "/setup_robot_from_env/?env_file=test")]
[InlineData("POST", "/set_show_tcp/")]
[InlineData("GET", "/is_setup/")]
[InlineData("POST", "/set_active_controller/?sim=true")]
[InlineData("POST", "/init_mpc_robt")]
public async Task RemovedRoutes_ReturnNotFound(string method, string path)
{
using var client = factory.CreateClient();
using var request = new HttpRequestMessage(new HttpMethod(method), path);
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
/// <summary>
/// 验证已移除的旧根路径返回 404。
/// </summary>
[Fact]
public async Task Root_ReturnsNotFound()
{
using var client = factory.CreateClient();
using var response = await client.GetAsync("/");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
/// <summary>
/// 验证初始化链路和机器人信息接口会保持旧 FastAPI 服务的路径与返回字段风格。
/// </summary>
[Fact]
public async Task InitEndpoints_ExposeLegacyRobotInfoAndSpeedRatioShape()
{
using var client = factory.CreateClient();
await InitializeRobotAsync(client);
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 movePoseResponse = await client.PostAsJsonAsync("/move_pose/", new { x = 100.0, y = 200.0, z = 300.0, w = 1.0, p = 2.0, r = 3.0 }))
{
Assert.Equal(HttpStatusCode.OK, movePoseResponse.StatusCode);
using var movePoseJson = await ReadJsonAsync(movePoseResponse);
Assert.Equal("robot moved", movePoseJson.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>
/// 验证 MovePose 请求必须显式提供六个有限直角坐标字段,避免缺字段被模型绑定静默补 0。
/// </summary>
[Theory]
[InlineData("""{"x":100.0,"y":200.0,"z":300.0,"w":1.0,"p":2.0}""")]
[InlineData("null")]
[InlineData("""{"x":1e999,"y":200.0,"z":300.0,"w":1.0,"p":2.0,"r":3.0}""")]
[InlineData("""{"x":1000.1,"y":200.0,"z":300.0,"w":1.0,"p":2.0,"r":3.0}""")]
[InlineData("""{"x":100.0,"y":-1000.1,"z":300.0,"w":1.0,"p":2.0,"r":3.0}""")]
[InlineData("""{"x":100.0,"y":200.0,"z":-0.1,"w":1.0,"p":2.0,"r":3.0}""")]
[InlineData("""{"x":100.0,"y":200.0,"z":300.0,"w":-180.1,"p":2.0,"r":3.0}""")]
[InlineData("""{"x":100.0,"y":200.0,"z":300.0,"w":1.0,"p":90.1,"r":3.0}""")]
[InlineData("""{"x":100.0,"y":200.0,"z":300.0,"w":1.0,"p":2.0,"r":180.1}""")]
public async Task MovePose_InvalidPayload_ReturnsLegacyBadRequest(string payload)
{
using var client = factory.CreateClient();
await InitializeRobotAsync(client);
using var content = new StringContent(payload, Encoding.UTF8, "application/json");
using var response = await client.PostAsync("/move_pose/", content);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
using var json = await ReadJsonAsync(response);
Assert.Equal("MovePose failed", json.RootElement.GetProperty("detail").GetString());
}
/// <summary>
/// 验证飞拍 HTTP 接口可以按旧 API 层的路径和字段完成上传、列出、执行与删除。
/// </summary>
[Fact]
public async Task FlyshotEndpoints_RoundTripLegacyUploadExecuteAndDeleteFlow()
{
using var client = factory.CreateClient();
await InitializeRobotAsync(client);
using (var speedResponse = await client.PostAsJsonAsync("/set_speedRatio/", new { speed = 0.5 }))
{
Assert.Equal(HttpStatusCode.OK, speedResponse.StatusCode);
}
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.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.001, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.002, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.003, 0.0, 0.0, 0.0, 0.0, 0.0 }
}
};
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,
wait = 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("isValid").GetBoolean());
Assert.True(saveInfoJson.RootElement.GetProperty("finalSendPointCount").GetInt32() > 0);
}
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_robot", new
{
robot_name = "FANUC_LR_Mate_200iD",
robot_ip = "192.168.10.101"
});
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);
}
}