namespace AcDream.Core.Net.Tests.Transport;
internal enum LinkDirection
{
ClientToServer,
ServerToClient,
}
///
/// Deterministic datagram fault injector between two endpoints. Pure data
/// structure — no sockets, no threads, no wall clock. Callers push each
/// datagram through and deliver whatever comes back,
/// in order.
///
///
/// Fault evaluation order per datagram (first match wins):
/// scheduled per-index drops () → one-shot drop budget
/// () → persistent predicates () →
/// seeded random loss (). A surviving datagram is
/// then subject to an armed : the next survivor is held
/// back and delivered immediately AFTER the survivor that follows it
/// (adjacent swap). Datagram bytes are copied on entry, so callers may pass
/// stack-allocated spans.
///
///
internal sealed class LossyLink
{
private sealed class DirectionState
{
public int TransmitIndex;
public int PendingDropCount;
public readonly HashSet DropIndices = new();
public readonly List> DropPredicates = new();
public Random? LossRandom;
public double LossProbability;
public bool ReorderArmed;
public byte[]? Held;
public int Dropped;
public int Delivered;
}
private readonly DirectionState _clientToServer = new();
private readonly DirectionState _serverToClient = new();
private DirectionState State(LinkDirection direction) =>
direction == LinkDirection.ClientToServer ? _clientToServer : _serverToClient;
/// Drop the next datagrams in .
public void DropNext(LinkDirection direction, int count = 1)
{
ArgumentOutOfRangeException.ThrowIfNegative(count);
State(direction).PendingDropCount += count;
}
/// Drop the datagram with the given per-direction transmit index (0-based).
public void DropAt(LinkDirection direction, int transmitIndex) =>
State(direction).DropIndices.Add(transmitIndex);
///
/// Drop every datagram matching (persistent;
/// receives the per-direction transmit index and the datagram bytes).
///
public void Drop(LinkDirection direction, Func predicate) =>
State(direction).DropPredicates.Add(predicate);
///
/// Swap the next two surviving datagrams: the next survivor is held and
/// released right after the survivor that follows it.
///
public void Reorder(LinkDirection direction) =>
State(direction).ReorderArmed = true;
///
/// Enable seeded random loss: each surviving datagram is dropped with
/// using Random(seed) — fully
/// deterministic for a given seed + transmit sequence.
///
public void RandomLoss(LinkDirection direction, double probability, int seed)
{
ArgumentOutOfRangeException.ThrowIfNegative(probability);
ArgumentOutOfRangeException.ThrowIfGreaterThan(probability, 1.0);
DirectionState state = State(direction);
state.LossProbability = probability;
state.LossRandom = new Random(seed);
}
public int TransmitCount(LinkDirection direction) => State(direction).TransmitIndex;
public int DroppedCount(LinkDirection direction) => State(direction).Dropped;
public int DeliveredCount(LinkDirection direction) => State(direction).Delivered;
///
/// Push one datagram through the link. Returns the datagrams to deliver
/// now, in order (0, 1, or 2 entries — 2 when a held reordered datagram
/// is released).
///
public IReadOnlyList Transmit(LinkDirection direction, ReadOnlySpan datagram)
{
DirectionState state = State(direction);
int index = state.TransmitIndex++;
byte[] copy = datagram.ToArray();
bool drop = state.DropIndices.Remove(index);
if (!drop && state.PendingDropCount > 0)
{
state.PendingDropCount--;
drop = true;
}
if (!drop)
{
foreach (Func predicate in state.DropPredicates)
{
if (predicate(index, copy))
{
drop = true;
break;
}
}
}
if (!drop
&& state.LossRandom is not null
&& state.LossRandom.NextDouble() < state.LossProbability)
{
drop = true;
}
if (drop)
{
state.Dropped++;
return Array.Empty();
}
if (state.ReorderArmed)
{
state.ReorderArmed = false;
state.Held = copy;
return Array.Empty();
}
if (state.Held is not null)
{
byte[] held = state.Held;
state.Held = null;
state.Delivered += 2;
return new[] { copy, held };
}
state.Delivered++;
return new[] { copy };
}
///
/// Force-release a datagram held by that nothing
/// followed (it would otherwise be stuck forever). Returns the held
/// datagram or an empty list.
///
public IReadOnlyList DrainHeld(LinkDirection direction)
{
DirectionState state = State(direction);
if (state.Held is null)
return Array.Empty();
byte[] held = state.Held;
state.Held = null;
state.Delivered++;
return new[] { held };
}
}