From 559890c482f5973f06aafdfec9550fee5ba3c7f2 Mon Sep 17 00:00:00 2001
From: wenxiangchao <wenxiangchao@ihengxin.com.cn>
Date: 星期一, 09 十一月 2026 17:02:15 +0800
Subject: [PATCH] feat: CAN 模拟器 TCP 化 + 多设备并发调试 + 后台生命周期管理

---
 tests/Aging.MvpTests/SimulatorServerCoreTests.cs |  255 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 255 insertions(+), 0 deletions(-)

diff --git a/tests/Aging.MvpTests/SimulatorServerCoreTests.cs b/tests/Aging.MvpTests/SimulatorServerCoreTests.cs
new file mode 100644
index 0000000..e04a675
--- /dev/null
+++ b/tests/Aging.MvpTests/SimulatorServerCoreTests.cs
@@ -0,0 +1,255 @@
+using System;
+using System.IO;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+using CanSimulatorServer;
+using Xunit;
+
+namespace Aging.MvpTests
+{
+    /// <summary>
+    /// 妯℃嫙鍣ㄦ湇鍔$鏍稿績琛屼负娴嬭瘯锛欻ELLO 鐘舵�佹満銆丯ODE_BUSY 浠茶銆�
+    /// 璺ㄨ妭鐐硅矾鐢辨帹閫併�佽嚜鍔ㄥ搷搴斿洖鎺ㄣ�佸懡浠や覆鎵ц锛圓UTO_RESPONSE/STATUS锛夈��
+    /// 娴嬭瘯杩涚▼鍐呮墭绠� TcpSimulatorHost锛堥殢鏈虹鍙o級锛岃蛋鐪熷疄 TCP 鍥炵幆銆�
+    /// </summary>
+    public class SimulatorServerCoreTests : IDisposable
+    {
+        private readonly TcpSimulatorHost _host;
+        private readonly int _port;
+
+        public SimulatorServerCoreTests()
+        {
+            _host = new TcpSimulatorHost();
+            _host.StartAsync().GetAwaiter().GetResult();
+            _port = _host.Port;
+        }
+
+        public void Dispose() => _host.Dispose();
+
+        // ==================== 瀹㈡埛绔緟鍔� ====================
+
+        private sealed class FakeClient : IDisposable
+        {
+            public TcpClient Socket = null!;
+            public Stream Stream = null!;
+
+            public async Task ConnectAsync(int port)
+            {
+                Socket = new TcpClient();
+                await Socket.ConnectAsync("127.0.0.1", port);
+                Stream = Socket.GetStream();
+            }
+
+            public Task SendAsync(string msg)
+            {
+                byte[] f = CanSimulator.SimulatorFraming.Encode(msg);
+                return Stream.WriteAsync(f, 0, f.Length);
+            }
+            public Task<string> RecvAsync() => ReadFrameBlocking();
+
+            public async Task<string> HelloAsync(string bus, int node, string tag = "t")
+            {
+                await SendAsync($"HELLO:{bus}:{node}:{tag}");
+                return await ReadFrameBlocking();
+            }
+
+            private async Task<string> ReadFrameBlocking()
+            {
+                var s = await CanSimulator.SimulatorFraming.ReadFrameAsync(Stream, CancellationToken.None);
+                return s ?? "<EOF>";
+            }
+
+            public void Dispose() { try { Stream?.Dispose(); Socket?.Close(); } catch { } }
+        }
+
+        // ==================== HELLO 鐘舵�佹満 ====================
+
+        [Fact]
+        public void Hello_FirstClient_AcksOk()
+        {
+            using var c = new FakeClient();
+            c.ConnectAsync(_port).GetAwaiter().GetResult();
+            var ack = c.HelloAsync("CAN_BUS_1", 0).GetAwaiter().GetResult();
+            Assert.StartsWith("OK:HELLO", ack);
+        }
+
+        [Fact]
+        public void Query_BeforeHello_ReturnsNotHandshaked()
+        {
+            using var c = new FakeClient();
+            c.ConnectAsync(_port).GetAwaiter().GetResult();
+            c.SendAsync("QUERY:STATUS").GetAwaiter().GetResult();
+            var resp = c.RecvAsync().GetAwaiter().GetResult();
+            Assert.Equal("ERR:NOT_HANDSHAKED", resp);
+        }
+
+        [Fact]
+        public void UnknownCommand_ReturnsUnknownCmd_WithoutDroppingConnection()
+        {
+            using var c = new FakeClient();
+            c.ConnectAsync(_port).GetAwaiter().GetResult();
+            c.HelloAsync("CAN_BUS_1", 0).GetAwaiter().GetResult();
+
+            c.SendAsync("FOO:bar").GetAwaiter().GetResult();
+            Assert.Equal("ERR:UNKNOWN_CMD:FOO:bar", c.RecvAsync().GetAwaiter().GetResult());
+
+            // 杩炴帴淇濇寔鍙敤锛氬悗缁悎娉曞懡浠や粛鏈夊搷搴�
+            c.SendAsync("QUERY:STATUS").GetAwaiter().GetResult();
+            Assert.StartsWith("OK:", c.RecvAsync().GetAwaiter().GetResult());
+        }
+
+        // ==================== NODE_BUSY 浠茶 ====================
+
+        [Fact]
+        public void Hello_SameBusSameNode_SecondRejectedNodeBusy()
+        {
+            using var a = new FakeClient();
+            using var b = new FakeClient();
+            a.ConnectAsync(_port).GetAwaiter().GetResult();
+            b.ConnectAsync(_port).GetAwaiter().GetResult();
+
+            Assert.StartsWith("OK:HELLO", a.HelloAsync("CAN_BUS_1", 3).GetAwaiter().GetResult());
+            Assert.Equal("ERR:NODE_BUSY", b.HelloAsync("CAN_BUS_1", 3).GetAwaiter().GetResult());
+
+            // 涓嶅悓鎬荤嚎鍚岃妭鐐圭紪鍙蜂笉鍐茬獊
+            Assert.StartsWith("OK:HELLO", b.HelloAsync("CAN_BUS_2", 3).GetAwaiter().GetResult());
+        }
+
+        [Fact]
+        public void Hello_AfterDisconnect_SameNodeCanReattach()
+        {
+            var a = new FakeClient();
+            a.ConnectAsync(_port).GetAwaiter().GetResult();
+            a.HelloAsync("CAN_BUS_1", 7).GetAwaiter().GetResult();
+            a.Dispose(); // 妯℃嫙杩炴帴鏂紑
+
+            using var b = new FakeClient();
+            b.ConnectAsync(_port).GetAwaiter().GetResult();
+            // 鏈嶅姟绔劅鐭� EOF 闇�灏戣鏃堕棿锛岃疆璇㈤噸璇�
+            string ack = "";
+            for (int i = 0; i < 50 && !ack.StartsWith("OK"); i++)
+            {
+                Thread.Sleep(20);
+                b.SendAsync($"HELLO:CAN_BUS_1:7:retry{i}").GetAwaiter().GetResult();
+                ack = b.RecvAsync().GetAwaiter().GetResult();
+            }
+            Assert.StartsWith("OK:HELLO", ack);
+        }
+
+        // ==================== 璺敱涓庢帹閫� ====================
+
+        [Fact]
+        public void Send_FromOneNode_OtherNodeReceivesEvPush()
+        {
+            using var a = new FakeClient();
+            using var b = new FakeClient();
+            a.ConnectAsync(_port).GetAwaiter().GetResult();
+            b.ConnectAsync(_port).GetAwaiter().GetResult();
+            a.HelloAsync("CAN_BUS_1", 0).GetAwaiter().GetResult();
+            b.HelloAsync("CAN_BUS_1", 1).GetAwaiter().GetResult();
+
+            a.SendAsync("SEND:0x123:AA BB CC").GetAwaiter().GetResult();
+            var ack = a.RecvAsync().GetAwaiter().GetResult();
+            Assert.StartsWith("OK:TX", ack);
+
+            // 瀵圭鑺傜偣鏀跺埌鏈嶅姟绔富鍔ㄦ帹閫�
+            var push = b.RecvAsync().GetAwaiter().GetResult();
+            Assert.StartsWith("EV:RECV:0x123:AA BB CC", push);
+        }
+
+        [Fact]
+        public void Send_AutoResponseEnabled_RespondsToPeerAndSelf()
+        {
+            using var a = new FakeClient();
+            using var b = new FakeClient();
+            a.ConnectAsync(_port).GetAwaiter().GetResult();
+            b.ConnectAsync(_port).GetAwaiter().GetResult();
+            a.HelloAsync("CAN_BUS_1", 10).GetAwaiter().GetResult();
+            b.HelloAsync("CAN_BUS_1", 11).GetAwaiter().GetResult();
+
+            // b 寮�鍚嚜鍔ㄥ搷搴旓紙QUERY 鍐欏懡浠わ紝鍝嶅簲涓�琛岋級
+            b.SendAsync("QUERY:AUTO_RESPONSE:ON").GetAwaiter().GetResult();
+            Assert.StartsWith("OK:", b.RecvAsync().GetAwaiter().GetResult());
+
+            // a 鍙戝抚 鈫� b 璺敱鏀跺抚(鎺ㄩ��) + 鑷姩鍝嶅簲甯у洖 a
+            a.SendAsync("SEND:0x500:11 22").GetAwaiter().GetResult();
+            Assert.StartsWith("OK:TX", a.RecvAsync().GetAwaiter().GetResult());
+
+            // b 鑷繁鏀跺埌婧愯矾鐢辨帹閫�
+            Assert.StartsWith("EV:RECV:0x500:11 22", b.RecvAsync().GetAwaiter().GetResult());
+            // a 鏀跺埌鑷姩鍝嶅簲甯э紙鏉ユ簮=b 鑺傜偣 11锛�
+            var respPush = a.RecvAsync().GetAwaiter().GetResult();
+            Assert.StartsWith("EV:RECV:0x500:11 22", respPush);
+            Assert.EndsWith(":11", respPush); // srcNodeIndex=11
+        }
+
+        // ==================== 鍛戒护鎵ц ====================
+
+        [Fact]
+        public void Query_Status_AfterHello_ReturnsOkStatus()
+        {
+            using var a = new FakeClient();
+            a.ConnectAsync(_port).GetAwaiter().GetResult();
+            a.HelloAsync("CAN_BUS_1", 20).GetAwaiter().GetResult();
+
+            a.SendAsync("QUERY:STATUS").GetAwaiter().GetResult();
+            var resp = a.RecvAsync().GetAwaiter().GetResult();
+            Assert.StartsWith("OK:STATUS:", resp);
+            Assert.Contains("Node=20", resp); // 鑺傜偣浠ョ紪鍙峰鍧�
+        }
+
+        [Fact]
+        public void GarbageFrame_ClosesSession_ButHostKeepsListening()
+        {
+            // 闈炴硶闀垮害鍓嶇紑锛堝崗璁け姝ワ級锛氭湇鍔$鏂紑璇ヤ細璇濓紝浣嗗悗缁柊杩炴帴浠嶈兘 HELLO
+            int p = _port;
+            using (var raw = new TcpClient("127.0.0.1", p))
+            {
+                byte[] bad = { 0x00, 0x00, 0x00, 0x00 }; // 闀垮害 0
+                raw.GetStream().Write(bad, 0, 4);
+            }
+            Thread.Sleep(100);
+
+            using var c = new FakeClient();
+            c.ConnectAsync(_port).GetAwaiter().GetResult();
+            Assert.StartsWith("OK:HELLO", c.HelloAsync("CAN_BUS_9", 0).GetAwaiter().GetResult());
+        }
+
+        // ==================== 鎬荤嚎绾у簲绛斿櫒 ====================
+
+        [Fact]
+        public void BusEcho_Enabled_SenderReceivesOwnFrameAsResponder()
+        {
+            using var a = new FakeClient();
+            a.ConnectAsync(_port).GetAwaiter().GetResult();
+            a.HelloAsync("CAN_ECHO_ON", 0).GetAwaiter().GetResult();
+
+            a.SendAsync("QUERY:BUS_ECHO:ON").GetAwaiter().GetResult();
+            Assert.StartsWith("OK:", a.RecvAsync().GetAwaiter().GetResult());
+
+            // 鍗曡妭鐐瑰彂甯э紙鏍囧噯 ID锛夛細搴旂瓟鍣ㄥ簲鎶婂悓甯у洖鏄剧粰鍙戦�佽�咃紙鏃犻渶绗簩鑺傜偣锛�
+            a.SendAsync("SEND:0x111:AA BB").GetAwaiter().GetResult();
+            Assert.StartsWith("OK:TX", a.RecvAsync().GetAwaiter().GetResult());
+
+            var echo = a.RecvAsync().GetAwaiter().GetResult();
+            Assert.StartsWith("EV:RECV:0x111:AA BB", echo);
+            Assert.EndsWith(":RESPONDER", echo);
+        }
+
+        [Fact]
+        public void BusEcho_Disabled_NoEchoDelivered()
+        {
+            using var a = new FakeClient();
+            a.ConnectAsync(_port).GetAwaiter().GetResult();
+            a.HelloAsync("CAN_ECHO_OFF", 0).GetAwaiter().GetResult(); // 榛樿鍏�
+
+            a.SendAsync("SEND:0x912:11").GetAwaiter().GetResult();
+            Assert.StartsWith("OK:TX", a.RecvAsync().GetAwaiter().GetResult());
+
+            // 绱ф帴鐫�鐨勫搷搴斿簲涓烘煡璇㈡湰韬紝涓棿涓嶅簲鎻掑叆浠讳綍 EV:RECV 鍥炴樉
+            a.SendAsync("QUERY:BUS_ECHO").GetAwaiter().GetResult();
+            Assert.Equal("OK:BUS_ECHO:OFF", a.RecvAsync().GetAwaiter().GetResult());
+        }
+    }
+}

--
Gitblit v1.9.1