using System.Net; using System.Net.Sockets; using Flyshot.Runtime.Fanuc.Protocol; namespace Flyshot.Core.Tests; /// /// 验证 FANUC TCP 10010 状态通道客户端的后台接收与缓存能力。 /// public sealed class FanucStateClientTests : IDisposable { private readonly TcpListener _listener; private readonly CancellationTokenSource _cts = new(); /// /// 在随机可用端口启动本地模拟控制器。 /// public FanucStateClientTests() { _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 FanucStateClient(); 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); } /// /// 验证后台循环能正确解析抓包样本状态帧。 /// [Fact] public async Task GetLatestFrame_ReceivesAndParsesCapturedStateFrame() { using var client = new FanucStateClient(); var capturedFrame = Convert.FromHexString( "646f7a0000005a000000004388a23243f1ed7f43e9de6bc265031ec2b33cc3c278e0153f8742f53c3f128dbc929529bc7861d63cb0184c3c1ca1a7000000000000000000000000000000020000000000000000000000017a6f64"); var handlerTask = RunStreamingControllerAsync(capturedFrame, _cts.Token); await client.ConnectAsync("127.0.0.1", Port, _cts.Token); // 给后台循环留出接收和解析的时间。 await Task.Delay(200, _cts.Token); var latest = client.GetLatestFrame(); Assert.NotNull(latest); Assert.Equal(0u, latest.MessageId); Assert.Equal(6, latest.Pose.Count); Assert.Equal(9, latest.JointOrExtensionValues.Count); Assert.Equal([2u, 0u, 0u, 1u], latest.TailWords); client.Disconnect(); await handlerTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token); } /// /// 验证状态客户端在连接前调用 GetLatestFrame 返回 null。 /// [Fact] public void GetLatestFrame_BeforeConnect_ReturnsNull() { using var client = new FanucStateClient(); Assert.Null(client.GetLatestFrame()); } /// /// 验证 Disconnect 后最新帧被清空。 /// [Fact] public async Task Disconnect_ClearsLatestFrame() { using var client = new FanucStateClient(); var capturedFrame = Convert.FromHexString( "646f7a0000005a000000004388a23243f1ed7f43e9de6bc265031ec2b33cc3c278e0153f8742f53c3f128dbc929529bc7861d63cb0184c3c1ca1a7000000000000000000000000000000020000000000000000000000017a6f64"); var handlerTask = RunStreamingControllerAsync(capturedFrame, _cts.Token); await client.ConnectAsync("127.0.0.1", Port, _cts.Token); await Task.Delay(200, _cts.Token); Assert.NotNull(client.GetLatestFrame()); client.Disconnect(); Assert.Null(client.GetLatestFrame()); await handlerTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token); } /// /// 启动模拟控制器,持续发送状态帧流。 /// private async Task RunStreamingControllerAsync(byte[] frames, CancellationToken cancellationToken) { using var controller = await _listener.AcceptTcpClientAsync(cancellationToken); await using var stream = controller.GetStream(); try { while (!cancellationToken.IsCancellationRequested) { await stream.WriteAsync(frames, cancellationToken); await Task.Delay(50, cancellationToken); } } catch (OperationCanceledException) { // 正常取消。 } catch (IOException) { // 客户端断开。 } } }