def test_symmetry(self): s = Sqn(1) greater = 0 lower = 0 for i in range(1, Sqn._max_sequence + 1): if Sqn(i) > s: greater += 1 elif Sqn(i) < s: lower += 1 assert greater > 0 and greater == lower
class Header(Comparable): """Create a PyGaSe package header. # Arguments sequence (int): package sequence number ack (int): sequence number of the last received package ack_bitfield (str): A 32 character string representing the 32 sequence numbers prior to the last one received, with the first character corresponding the packge directly preceding it and so forth. '1' means that package has been received, '0' means it hasn't. # Attributes sequence (int): see corresponding constructor argument ack (int): see corresponding constructor argument ack_bitfield (str): see corresponding constructor argument --- Sequence numbers: A sequence of 0 means no packages have been sent or received. After 65535 sequence numbers wrap around to 1, so they can be stored in 2 bytes. """ def __init__(self, sequence: int, ack: int, ack_bitfield: str): self.sequence = Sqn(sequence) self.ack = Sqn(ack) self.ack_bitfield = ack_bitfield def to_bytearray(self) -> bytearray: """Return 12 bytes representing the header.""" result = bytearray(PROTOCOL_ID) result.extend(self.sequence.to_sqn_bytes()) result.extend(self.ack.to_sqn_bytes()) result.extend(int(self.ack_bitfield, 2).to_bytes(4, "big")) return result def destructure(self) -> tuple: """Return the tuple `(sequence, ack, ack_bitfield)`.""" return (self.sequence, self.ack, self.ack_bitfield) @classmethod def deconstruct_datagram(cls, datagram: bytes) -> tuple: """Return a tuple containing the header and the rest of the datagram. # Arguments datagram (bytes): serialized PyGaSe package to deconstruct # Returns tuple: `(header, payload)` with `payload` being a bytestring of the rest of the datagram """ if datagram[:4] != PROTOCOL_ID: raise ProtocolIDMismatchError sequence = Sqn.from_sqn_bytes(datagram[4:6]) ack = Sqn.from_sqn_bytes(datagram[6:8]) ack_bitfield = bin(int.from_bytes(datagram[8:12], "big"))[2:].zfill(32) payload = datagram[12:] return (cls(sequence, ack, ack_bitfield), payload)
def test_recv_duplicate_package_out_of_sequence(self): connection = Connection(("host", 1234), None) connection.remote_sequence = Sqn(1000) connection.ack_bitfield = "1" * 32 with pytest.raises(DuplicateSequenceError): curio.run( connection._recv, Package(Header(sequence=990, ack=500, ack_bitfield="1" * 32)))
def test_recv_second_package(self): connection = Connection(("host", 1234), None) connection.remote_sequence = Sqn(1) connection.ack_bitfield = "0" * 32 curio.run(connection._recv, Package(Header(sequence=2, ack=1, ack_bitfield="0" * 32))) assert connection.remote_sequence == 2 assert connection.ack_bitfield == "1" + "0" * 31
def deconstruct_datagram(cls, datagram: bytes) -> tuple: """Return a tuple containing the header and the rest of the datagram. # Arguments datagram (bytes): serialized PyGaSe package to deconstruct # Returns tuple: `(header, payload)` with `payload` being a bytestring of the rest of the datagram """ if datagram[:4] != PROTOCOL_ID: raise ProtocolIDMismatchError sequence = Sqn.from_sqn_bytes(datagram[4:6]) ack = Sqn.from_sqn_bytes(datagram[6:8]) ack_bitfield = bin(int.from_bytes(datagram[8:12], "big"))[2:].zfill(32) payload = datagram[12:] return (cls(sequence, ack, ack_bitfield), payload)
def from_datagram(cls, datagram: bytes) -> "ClientPackage": """Override #Package.from_datagram to include `time_order`.""" header, payload = Header.deconstruct_datagram(datagram) time_order = Sqn.from_sqn_bytes(payload[:2]) payload = payload[2:] events = cls._read_out_event_block(payload) result = cls(header, time_order, events) result._datagram = datagram # pylint: disable=protected-access return result
def __init__(self, remote_address: tuple, event_handler, event_wire=None): logger.debug( f"Creating connection instance for remote address {remote_address}." ) self.remote_address = remote_address self.event_handler = event_handler self.event_wire = event_wire self.local_sequence = Sqn(0) self.remote_sequence = Sqn(0) self.ack_bitfield = "0" * 32 self.latency = 0.0 self.status = ConnectionStatus.get("Disconnected") self.quality = "good" # this is used for congestion avoidance self._package_interval = self._package_intervals["good"] self._outgoing_event_queue = curio.UniversalQueue() self._incoming_event_queue = curio.UniversalQueue() self._pending_acks: dict = {} self._event_callback_sequence = Sqn(0) self._events_with_callbacks: dict = {} self._event_callbacks: dict = {} self._last_recv = time.time()
def test_recv_three_packages_arrive_out_of_sequence(self): connection = Connection(("host", 1234), None) connection.remote_sequence = Sqn(100) connection.ack_bitfield = "0110" + "1" * 28 curio.run( connection._recv, Package(Header(sequence=101, ack=100, ack_bitfield="1" * 32))) assert connection.remote_sequence == 101 assert connection.ack_bitfield == "10110" + "1" * 27 curio.run(connection._recv, Package(Header(sequence=99, ack=100, ack_bitfield="1" * 32))) assert connection.remote_sequence == 101 assert connection.ack_bitfield == "11110" + "1" * 27 curio.run(connection._recv, Package(Header(sequence=96, ack=101, ack_bitfield="1" * 32))) assert connection.remote_sequence == 101 assert connection.ack_bitfield == "1" * 32
class ClientPackage(Package): """Subclass of #Package for packages sent by PyGaSe clients. # Arguments time_order (int): the clients last known time order of the game state # Attributes time_order (int): see corresponding constructor argument """ def __init__(self, header: Header, time_order: int, events: list = None): super().__init__(header, events) self.time_order = Sqn(time_order) def to_datagram(self) -> bytes: """Override `Package.to_datagram` to include `time_order`.""" if self._datagram is not None: return self._datagram datagram = self.header.to_bytearray() # The header makes up the first 12 bytes of the package datagram.extend(self.time_order.to_sqn_bytes()) datagram.extend(self._create_event_block()) datagram = datagram if len(datagram) > self._max_size: raise OverflowError("Package exceeds the maximum size of " + str(self._max_size) + " bytes.") self._datagram = bytes(datagram) return self._datagram @classmethod def from_datagram(cls, datagram: bytes) -> "ClientPackage": """Override #Package.from_datagram to include `time_order`.""" header, payload = Header.deconstruct_datagram(datagram) time_order = Sqn.from_sqn_bytes(payload[:2]) payload = payload[2:] events = cls._read_out_event_block(payload) result = cls(header, time_order, events) result._datagram = datagram # pylint: disable=protected-access return result
def test_send_package(self): send_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) recv_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) recv_socket.bind(("localhost", 0)) connection = Connection(recv_socket.getsockname(), None) assert connection.local_sequence == 0 curio.run(connection._send_next_package, send_socket) data = curio.run(recv_socket.recv, Package._max_size) package = Package.from_datagram(data) assert package.header.sequence == 1 and connection.local_sequence == 1 assert package.header.ack == 0 and package.header.ack_bitfield == "0" * 32 curio.run(connection._send_next_package, send_socket) data = curio.run(recv_socket.recv, Package._max_size) package = Package.from_datagram(data) assert package.header.sequence == 2 and connection.local_sequence == 2 assert package.header.ack == 0 and package.header.ack_bitfield == "0" * 32 connection.local_sequence = Sqn.get_max_sequence() curio.run(connection._send_next_package, send_socket) data = curio.run(recv_socket.recv, Package._max_size) package = Package.from_datagram(data) assert package.header.sequence == 1 and connection.local_sequence == 1 assert package.header.ack == 0 and package.header.ack_bitfield == "0" * 32 curio.run(send_socket.close) curio.run(recv_socket.close)
def test_initialize_with_overflowing_value(self): for i in range(Sqn._max_sequence + 1, 2 * Sqn._max_sequence): with pytest.raises(ValueError) as error: Sqn(i) assert str(error.value) == "value exceeds maximum sequence number"
def test_add_within_sequence_with_ints(self): s = Sqn(0) for i in range(1, Sqn._max_sequence + 1): s += 1 assert s == i assert s.__class__ == Sqn
def __init__(self, time_order: int, **kwargs): self.__dict__ = kwargs self.time_order = Sqn(time_order)
def test_sequence_wrap_around_with_int(self): s = Sqn(Sqn._max_sequence) s += 1 assert s == 1
def test_initialize_negative_values(self): for i in range(-1, -(Sqn._max_sequence + 2), -1): with pytest.raises(ValueError) as error: Sqn(i) assert str(error.value) == "sequence numbers must not be negative"
def test_small_difference_around_sequence_edge(self): s1 = Sqn(2) s2 = Sqn(Sqn._max_sequence - 2) assert s2 - s1 == -4 assert s1 - s2 == 4 assert s1 > s2 and s2 < s1
def test_small_difference_within_sequence(self): s1 = Sqn(2) s2 = Sqn(5) assert s2 - s1 == 3 assert s1 - s2 == -3 assert s2 > s1 and s1 < s2
def test_add_negative_int_with_larger_norm(self): s = Sqn(2) with pytest.raises(ValueError) as error: s += -3 assert str(error.value) == "sequence numbers must not be negative"
def test_add_negative_int(self): s = Sqn(2) s += -1 assert s == 1 and s.__class__ == Sqn
def __init__(self, header: Header, time_order: int, events: list = None): super().__init__(header, events) self.time_order = Sqn(time_order)
def __init__(self, time_order: int = 0, game_status: int = GameStatus.get("Paused"), **kwargs): self.__dict__ = kwargs self.game_status = game_status self.time_order = Sqn(time_order)
def __init__(self, sequence: int, ack: int, ack_bitfield: str): self.sequence = Sqn(sequence) self.ack = Sqn(ack) self.ack_bitfield = ack_bitfield
def test_sequence_wrap_around(self): s = Sqn(Sqn._max_sequence) s += Sqn(1) assert s == 1
def test_bytes(self): for i in range(2 * Sqn._bytesize): b = Sqn(i).to_sqn_bytes() assert len(b) == Sqn._bytesize assert b.__class__ == bytes assert Sqn.from_sqn_bytes(b) == Sqn(i)
def test_initialize_valid_values(self): for i in range(Sqn._max_sequence + 1): s = Sqn(i) assert s == i
def from_bytes(cls, bytepack: bytes) -> "GameStateUpdate": """Extend `Sendable.from_bytes` to make sure time_order is of type `Sqn`.""" update = super().from_bytes(bytepack) update.time_order = Sqn(update.time_order) # pylint: disable=no-member return update
def test_large_distance(self): assert Sqn(50000) - Sqn(20000) == 30000 assert Sqn(Sqn._max_sequence - 100) - Sqn(20000) == -20100