Files
MoShiro/backend/tests/MoShiro.TestSupport/ChunkFactory.cs
T
justme ad5efc62ff agent: unify listening and dialing with directed mutual measurements
Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry.

Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
2026-10-21 11:55:36 +08:00

138 lines
6.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json;
using Fleet.Core;
using MoShiro.Protocol;
namespace MoShiro.TestSupport;
/// <summary>每一个回显包的设定:上行或下行丢失,以及两个方向的单向时延(微秒)。</summary>
public readonly record struct EchoPacket(bool LostUp, bool LostDown, long OwdUpUs, long OwdDownUs, long ProcessUs = 50);
/// <summary>合成原始块:用于测试、模拟器和离线工具。块头里的时钟锚点保证 拨号方与监听方时钟相差 <see cref="DialerOffsetUs"/>。</summary>
public static class ChunkFactory
{
/// <summary>合成块默认属于哪个拨号侧 Agent(测试夹具在后端创建一个真实的拨号侧 Agent 之后把它的 id 写在这里)。</summary>
public static string DefaultDialer { get; set; } = "agt_dialer_test";
public const long AgentMonoAnchorUs = 1_000_000_000;
public const long DialerOffsetUs = 5_000_000;
public static RawChunkHeader Header(NodeId agent, string kind, long startUtcMs, int durationMs = 10_000, IReadOnlyDictionary<string, JsonElement>? parameters = null)
{
KindSpec spec = Kinds.Find(kind)!;
ParamResolution resolved = ParamResolver.Resolve(spec.Params, parameters);
return new RawChunkHeader
{
ChunkId = new ChunkId(Guid.CreateVersion7()),
Kind = kind,
AgentId = agent,
DialerId = new NodeId(DefaultDialer),
Params = new Dictionary<string, JsonElement>(resolved.Values!.Values),
Port = 7443,
StartUtcMs = startUtcMs,
DurationMs = durationMs,
Clock = new ChunkClock { ListenerMonoUs = AgentMonoAnchorUs, UtcMs = startUtcMs, DialerOffsetUs = DialerOffsetUs },
};
}
/// <summary>拨号方时钟下,距离块起点 <paramref name="offsetMs"/> 毫秒的时刻(微秒)。</summary>
public static long DialerUs(long offsetMs) => (offsetMs * 1000) + AgentMonoAnchorUs - DialerOffsetUs;
public static long AgentUs(long offsetMs) => (offsetMs * 1000) + AgentMonoAnchorUs;
public static RawChunk Echo(
NodeId agent,
string kind,
long startUtcMs,
int count,
int intervalMs,
Func<int, EchoPacket> packet,
int durationMs = 10_000,
IReadOnlyDictionary<string, JsonElement>? parameters = null,
IEnumerable<RawStream>? extraStreams = null)
{
var msg = new StreamBuilder(Kinds.Find(kind)!.FindStream("msg")!);
for (int i = 0; i < count; i++)
{
EchoPacket p = packet(i);
long t1 = DialerUs((long)i * intervalMs);
long t2 = t1 + DialerOffsetUs + p.OwdUpUs;
long t3 = t2 + p.ProcessUs;
long t4 = t3 - DialerOffsetUs + p.OwdDownUs;
msg.Add(i, 64, t1, p.LostUp ? null : t2, p.LostUp ? null : t3, p.LostUp || p.LostDown ? null : t4);
}
var streams = new List<RawStream> { msg.Build() };
if (extraStreams is not null)
{
streams.AddRange(extraStreams);
}
return new RawChunk(Header(agent, kind, startUtcMs, durationMs, parameters), streams);
}
/// <summary>10 Hz 的 udp_echo,没有丢包,RTT = 2 × owdMs。</summary>
public static RawChunk UdpEchoClean(NodeId agent, long startUtcMs, int count = 100, double owdMs = 20) =>
Echo(agent, "udp_echo", startUtcMs, count, 100, _ => new EchoPacket(false, false, (long)(owdMs * 1000), (long)(owdMs * 1000)));
public static RawStream TcpInfo(IEnumerable<(long TsUs, byte Side, uint SrttUs, uint RetransTotal, uint SegsOut)> samples)
{
var builder = new StreamBuilder(Kinds.TcpEcho.FindStream("tcp_info")!);
foreach ((long ts, byte side, uint srtt, uint retrans, uint segs) in samples)
{
builder.Add(ts, side, srtt, srtt / 4, 10, retrans, segs, segs * 100L, null);
}
return builder.Build();
}
public static RawChunk Connect(NodeId agent, long startUtcMs, IEnumerable<(long TcpUs, long? TlsUs, ConnectOutcomeCode Outcome)> attempts)
{
var builder = new StreamBuilder(Kinds.Connect.FindStream("conn")!);
int i = 0;
foreach ((long tcp, long? tls, ConnectOutcomeCode outcome) in attempts)
{
builder.Add(i, DialerUs(i * 5000L), outcome == ConnectOutcomeCode.Timeout ? null : tcp, tls, (long)outcome);
i++;
}
return new RawChunk(Header(agent, "connect", startUtcMs), [builder.Build()]);
}
/// <summary>udp_train:Agent 以固定间隔发包,Dialer 记录收到的时刻;<paramref name="lossEvery"/> 为 N 时每 N 个包丢一个。</summary>
public static RawChunk Train(NodeId agent, long startUtcMs, double mbps, int phase, int seconds = 10, int lossEvery = 0, long owdUs = 20_000)
{
const int size = 1250;
int packets = (int)(mbps * 1_000_000 / 8 / size * seconds);
double intervalUs = seconds * 1_000_000.0 / packets;
var pkt = new StreamBuilder(Kinds.UdpTrain.FindStream("pkt")!);
for (int i = 0; i < packets; i++)
{
long sendAgent = AgentMonoAnchorUs + (long)(i * intervalUs);
bool lost = lossEvery > 0 && i % lossEvery == 0;
long recvDialer = sendAgent + owdUs - DialerOffsetUs;
pkt.Add(i, size, sendAgent, lost ? null : recvDialer);
}
var parameters = new Dictionary<string, JsonElement>
{
["mbps"] = JsonSerializer.SerializeToElement(mbps, FactoryJson.Default.Double),
["duration_s"] = JsonSerializer.SerializeToElement((double)seconds, FactoryJson.Default.Double),
};
RawChunkHeader header = Header(agent, "udp_train", startUtcMs, seconds * 1000, parameters);
header.Phase = phase;
header.TestId = new TestId("tst_test");
return new RawChunk(header, [pkt.Build(), new StreamBuilder(Kinds.UdpTrain.FindStream("step")!).Build()]);
}
}
public enum ConnectOutcomeCode
{
Ok = 0,
Timeout = 1,
Reset = 2,
TlsAlert = 3,
}
[System.Text.Json.Serialization.JsonSerializable(typeof(double))]
internal sealed partial class FactoryJson : System.Text.Json.Serialization.JsonSerializerContext;