using System.Net;
using System.Net.Sockets;
using Flyshot.Runtime.Fanuc.Protocol;
namespace Flyshot.Core.Tests;
///
/// 验证 FANUC TCP 10012 命令客户端的帧收发与响应解析。
///
public sealed class FanucCommandClientTests : IDisposable
{
private readonly TcpListener _listener;
private readonly CancellationTokenSource _cts = new();
///
/// 在随机可用端口启动本地模拟控制器。
///
public FanucCommandClientTests()
{
_listener = new TcpListener(IPAddress.Loopback, 0);
_listener.Start();
}
///
/// 获取分配给本地模拟控制器的端口。
///
private int Port => ((IPEndPoint)_listener.LocalEndpoint).Port;
///
/// 清理模拟控制器和取消源。
///
public void Dispose()
{
_cts.Cancel();
_listener.Stop();
_cts.Dispose();
}
///
/// 验证命令客户端可以连接本地模拟控制器。
///
[Fact]
public async Task ConnectAsync_ConnectsToLocalListener()
{
using var client = new FanucCommandClient();
var acceptTask = _listener.AcceptTcpClientAsync();
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
Assert.True(client.IsConnected);
// 确保模拟侧也完成握手
await acceptTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token);
}
///
/// 验证 StopProgram 命令帧与抓包样本一致,并能解析成功响应。
///
[Fact]
public async Task StopProgramAsync_SendsCorrectFrameAndParsesSuccess()
{
using var client = new FanucCommandClient();
var handlerTask = RunSingleResponseControllerAsync(
Convert.FromHexString("646f7a0000001a0000210300000008525642555354534d7a6f64"),
Convert.FromHexString("646f7a0000001200002103000000007a6f64"),
_cts.Token);
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
var response = await client.StopProgramAsync("RVBUSTSM", _cts.Token);
Assert.True(response.IsSuccess);
Assert.Equal(FanucCommandMessageIds.StopProgram, response.MessageId);
await handlerTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token);
}
///
/// 验证 ResetRobot 空命令帧能正确发送并解析结果响应。
///
[Fact]
public async Task ResetRobotAsync_SendsEmptyCommandAndParsesResponse()
{
using var client = new FanucCommandClient();
var expectedFrame = FanucCommandProtocol.PackEmptyCommand(FanucCommandMessageIds.ResetRobot);
var responseFrame = Convert.FromHexString("646f7a0000001200002100000000007a6f64");
var handlerTask = RunSingleResponseControllerAsync(expectedFrame, responseFrame, _cts.Token);
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
var response = await client.ResetRobotAsync(_cts.Token);
Assert.True(response.IsSuccess);
Assert.Equal(FanucCommandMessageIds.ResetRobot, response.MessageId);
await handlerTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token);
}
///
/// 验证 GetProgramStatus 命令帧能正确发送并解析程序状态响应。
///
[Fact]
public async Task GetProgramStatusAsync_SendsFrameAndParsesStatusResponse()
{
using var client = new FanucCommandClient();
var expectedFrame = FanucCommandProtocol.PackProgramCommand(FanucCommandMessageIds.GetProgramStatus, "RVBUSTSM");
var responseFrame = Convert.FromHexString("646f7a000000160000200300000000000000017a6f64");
var handlerTask = RunSingleResponseControllerAsync(expectedFrame, responseFrame, _cts.Token);
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
var response = await client.GetProgramStatusAsync("RVBUSTSM", _cts.Token);
Assert.True(response.IsSuccess);
Assert.Equal(FanucCommandMessageIds.GetProgramStatus, response.MessageId);
Assert.Equal(1u, response.ProgramStatus);
await handlerTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token);
}
///
/// 验证 StartProgram 命令帧能正确发送并解析成功响应。
///
[Fact]
public async Task StartProgramAsync_SendsCorrectFrameAndParsesSuccess()
{
using var client = new FanucCommandClient();
var expectedFrame = FanucCommandProtocol.PackProgramCommand(FanucCommandMessageIds.StartProgram, "RVBUSTSM");
var responseFrame = Convert.FromHexString("646f7a0000001200002102000000007a6f64");
var handlerTask = RunSingleResponseControllerAsync(expectedFrame, responseFrame, _cts.Token);
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
var response = await client.StartProgramAsync("RVBUSTSM", _cts.Token);
Assert.True(response.IsSuccess);
Assert.Equal(FanucCommandMessageIds.StartProgram, response.MessageId);
await handlerTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token);
}
///
/// 验证命令响应 result_code 非零时,客户端会抛出可诊断异常而不是让上层误判成功。
///
[Fact]
public async Task StopProgramAsync_NonZeroResultCode_ThrowsDiagnosticException()
{
using var client = new FanucCommandClient();
var handlerTask = RunSingleResponseControllerAsync(
FanucCommandProtocol.PackProgramCommand(FanucCommandMessageIds.StopProgram, "RVBUSTSM"),
Convert.FromHexString("646f7a00000012000021030000002a7a6f64"),
_cts.Token);
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
var exception = await Assert.ThrowsAsync(
() => client.StopProgramAsync("RVBUSTSM", _cts.Token));
Assert.Contains("0x2103", exception.Message);
Assert.Contains("42", exception.Message);
await handlerTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token);
}
///
/// 验证在连接前调用命令会抛出 InvalidOperationException。
///
[Fact]
public async Task SendProgramCommandAsync_BeforeConnect_Throws()
{
using var client = new FanucCommandClient();
await Assert.ThrowsAsync(
() => client.StopProgramAsync("RVBUSTSM", _cts.Token));
}
///
/// 启动模拟控制器,接收一条请求帧并比对期望内容,然后返回预设响应。
///
private async Task RunSingleResponseControllerAsync(
byte[] expectedFrame,
byte[] responseFrame,
CancellationToken cancellationToken)
{
using var controller = await _listener.AcceptTcpClientAsync(cancellationToken);
await using var stream = controller.GetStream();
var buffer = new byte[expectedFrame.Length];
await ReadExactAsync(stream, buffer, cancellationToken);
Assert.Equal(expectedFrame, buffer);
await stream.WriteAsync(responseFrame, cancellationToken);
}
///
/// 从流中精确读取指定长度的字节。
///
private static async Task ReadExactAsync(NetworkStream stream, byte[] buffer, CancellationToken cancellationToken)
{
var totalRead = 0;
while (totalRead < buffer.Length)
{
var read = await stream.ReadAsync(buffer.AsMemory(totalRead), cancellationToken);
if (read == 0)
{
throw new IOException("模拟控制器读取到 EOF。");
}
totalRead += read;
}
}
}