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