"""Losses and T3 expirations are counted per association (see sctp_stats.py). Replayed against aiortc's own RTCSctpTransport: five chunks sent and the first one lost, so three acknowledgements in a row report it missing and the sender enters fast recovery, once. Then T3 expires, once. """ import asyncio from aiortc.rtcsctptransport import RTCSctpTransport, SackChunk from meshbay_node.transport.webrtc import sctp_stats class _Dtls: state = "connected" def _register_data_receiver(self, *a): pass class _Pc: def __init__(self, sctp): self.sctp = sctp def _sack(cumulative_tsn: int, gaps: list) -> SackChunk: sack = SackChunk() sack.cumulative_tsn = cumulative_tsn % 2**32 sack.gaps = gaps sack.duplicates = [] sack.advertised_rwnd = 1 << 20 return sack async def _replay() -> RTCSctpTransport: s = RTCSctpTransport(_Dtls()) s._association_state = s.State.ESTABLISHED async def sent(*_a, **_k): pass s._send_chunk = sent for name in ("_t3_start", "_t3_restart", "_t3_cancel"): setattr(s, name, lambda: None) s._cwnd = 100_000 for _ in range(5): await s._send(1, 51, b"x" * 1000) first = s._sent_queue[0].tsn for last_seen in (2, 3, 4): await s._receive_sack_chunk(_sack(first - 1, [(2, last_seen + 1)])) # Still recovering from the same loss: one episode, not a second. await s._receive_sack_chunk(_sack(first - 1, [(2, 5)])) s._t3_expired() return s def test_a_loss_and_a_t3_expiry_are_each_counted_once(): s = asyncio.run(_replay()) assert s._mb_losses == 1 assert s._mb_t3 == 1 assert sctp_stats.describe(_Pc(s)).endswith("loss=1 t3=1") def test_an_association_that_lost_nothing_says_so(): async def fresh() -> RTCSctpTransport: return RTCSctpTransport(_Dtls()) s = asyncio.run(fresh()) assert sctp_stats.describe(_Pc(s)).endswith("rtt=? loss=0 t3=0") def test_a_session_without_an_association_adds_nothing(): assert sctp_stats.describe(None) == ""