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); } /// /// 验证在连接前调用命令会抛出 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; } } }