using System;
|
using System.IO;
|
using System.Text;
|
using System.Threading;
|
using System.Threading.Tasks;
|
using CanSimulator;
|
using Xunit;
|
|
namespace Aging.MvpTests
|
{
|
/// <summary>
|
/// CAN 模拟器 TCP 线协议分帧编解码器测试。
|
/// 线格式:4 字节大端长度前缀 + UTF8 文本(与 BrokerIpc 同惯用法)。
|
/// </summary>
|
public class SimulatorFramingTests
|
{
|
[Fact]
|
public void Encode_RoundTrip_DecodesOriginalText()
|
{
|
string msg = "HELLO:CAN_BUS_1:0:HOST/PID";
|
byte[] frame = SimulatorFraming.Encode(msg);
|
|
Assert.Equal(4 + Encoding.UTF8.GetByteCount(msg), frame.Length);
|
|
var ms = new MemoryStream(frame);
|
byte[] all = ms.ToArray();
|
var got = SimulatorFraming.Decode(all, SimulatorFraming.HeaderSize, all.Length - SimulatorFraming.HeaderSize);
|
Assert.Equal(msg, got);
|
}
|
|
[Fact]
|
public void ReadFrameAsync_TwoFramesInOneBuffer_DecodesBothInOrder()
|
{
|
// 粘包:两个完整帧连续写入同一流
|
var ms = new MemoryStream();
|
byte[] f1 = SimulatorFraming.Encode("OK:TX:0x100:8");
|
ms.Write(f1, 0, f1.Length);
|
byte[] f2 = SimulatorFraming.Encode("EV:RECV:0x123:AA BB:12:00:00.111:2");
|
ms.Write(f2, 0, f2.Length);
|
ms.Position = 0;
|
|
string? r1 = SimulatorFraming.ReadFrameAsync(ms, CancellationToken.None).GetAwaiter().GetResult();
|
string? r2 = SimulatorFraming.ReadFrameAsync(ms, CancellationToken.None).GetAwaiter().GetResult();
|
|
Assert.Equal("OK:TX:0x100:8", r1);
|
Assert.Equal("EV:RECV:0x123:AA BB:12:00:00.111:2", r2);
|
}
|
|
[Fact]
|
public void ReadFrameAsync_FrameSplitAcrossTwoReads_ReassemblesFullFrame()
|
{
|
// 半包:帧体被拆成两次到达(用分段流模拟)
|
byte[] frame = SimulatorFraming.Encode("SEND:0x777:DE AD BE EF");
|
var chunked = new ChunkedStream(frame, chunk: 6); // 头部 6 字节后切断
|
|
string? got = SimulatorFraming.ReadFrameAsync(chunked, CancellationToken.None).GetAwaiter().GetResult();
|
Assert.Equal("SEND:0x777:DE AD BE EF", got);
|
}
|
|
[Fact]
|
public void ReadFrameAsync_LengthExceedsMax_Throws()
|
{
|
// 非法长度前缀:超过 MaxFrameBytes
|
byte[] bad = new byte[4];
|
bad[0] = 0x7F; bad[1] = 0xFF; bad[2] = 0xFF; bad[3] = 0xFF; // ~2GB
|
var ms = new MemoryStream(bad);
|
|
Assert.Throws<InvalidDataException>(() =>
|
SimulatorFraming.ReadFrameAsync(ms, CancellationToken.None).GetAwaiter().GetResult());
|
}
|
|
[Fact]
|
public void ReadFrameAsync_ZeroLengthPrefix_Throws()
|
{
|
byte[] bad = new byte[4]; // 长度 0
|
var ms = new MemoryStream(bad);
|
|
Assert.Throws<InvalidDataException>(() =>
|
SimulatorFraming.ReadFrameAsync(ms, CancellationToken.None).GetAwaiter().GetResult());
|
}
|
|
[Fact]
|
public void ReadFrameAsync_EmptyStream_ReturnsNull()
|
{
|
// 对端正常关闭:读不到任何字节时返回 null(断线感知契约)
|
var ms = new MemoryStream(new byte[0]);
|
string? got = SimulatorFraming.ReadFrameAsync(ms, CancellationToken.None).GetAwaiter().GetResult();
|
Assert.Null(got);
|
}
|
|
[Fact]
|
public void ReadFrameAsync_TruncatedBody_ReturnsNull()
|
{
|
// 头完整但体中途 EOF:返回 null 触发重连
|
byte[] frame = SimulatorFraming.Encode("QUERY:RECV_ALL");
|
var truncated = new byte[frame.Length - 3];
|
Array.Copy(frame, truncated, truncated.Length);
|
var ms = new MemoryStream(truncated);
|
|
string? got = SimulatorFraming.ReadFrameAsync(ms, CancellationToken.None).GetAwaiter().GetResult();
|
Assert.Null(got);
|
}
|
|
/// <summary>每次 Read 只从内部缓冲区取固定片段,模拟网络分片到达</summary>
|
private sealed class ChunkedStream : Stream
|
{
|
private readonly byte[] _data;
|
private readonly int _chunk;
|
private int _pos;
|
public ChunkedStream(byte[] data, int chunk) { _data = data; _chunk = chunk; }
|
public override bool CanRead => true;
|
public override bool CanSeek => false;
|
public override bool CanWrite => false;
|
public override long Length => _data.Length;
|
public override long Position { get => _pos; set => throw new NotSupportedException(); }
|
public override void Flush() { }
|
public override int Read(byte[] buffer, int offset, int count)
|
{
|
if (_pos >= _data.Length) return 0;
|
int n = Math.Min(Math.Min(count, _chunk), _data.Length - _pos);
|
Array.Copy(_data, _pos, buffer, offset, n);
|
_pos += n;
|
return n;
|
}
|
public override long Seek(long o, SeekOrigin s) => throw new NotSupportedException();
|
public override void SetLength(long v) => throw new NotSupportedException();
|
public override void Write(byte[] b, int o, int c) => throw new NotSupportedException();
|
}
|
}
|
}
|