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 }; } }