Improve test coverage wrt protocol versions; other fixes/improvements

Improvements to the test suite:
* List release version names and numbers in minecraft/__init__.py.
* Make some tests, which previously ran for *all* protocol versions,
  run only for release protocol versions (to improve test performance).
* Make some tests, which previously ran only for the latest protocol
  version, run for all release protocol versions (to improve coverage).
* Print each protocol version being tested to the debug log, to help
  identify sources of errors.
* Use the `nose-timer' plugin to show the run time of each test.

Fix errors revealed by increased test coverage:
* Fix: SoundEffectPacket.Pitch is not serialised correctly for early
  protocol versions.
* Fix: handleExceptionTest finishes later than necessary because
  the test overrode an exception handler used internally by
  `_FakeServerTest', causing the server thread to time out after 4s.
* Add support for multiple exception handlers in `Connection'
  (required for the above).

Improvements to data descriptors:
* Make syntax of property declarations more consistent/Pythonic.
* Factor the definition of several aliasing properties into the
  utility methods `attribute_alias' and `multi_attribute_alias',
  which construct suitable data descriptors.
* Define and use the named tuple `Direction' for (pitch, yaw) values.
This commit is contained in:
joodicator 2019-05-14 18:41:58 +02:00
parent 6ef868bc5b
commit 7b1567c352
12 changed files with 318 additions and 157 deletions

View File

@ -3,8 +3,11 @@ A modern, Python3-compatible, well-documented library for communicating
with a MineCraft server. with a MineCraft server.
""" """
# The version number of the most recent pyCraft release.
__version__ = "0.5.0" __version__ = "0.5.0"
# A dict mapping the name of each Minecraft version supported by pyCraft to
# the corresponding protocol version number.
SUPPORTED_MINECRAFT_VERSIONS = { SUPPORTED_MINECRAFT_VERSIONS = {
'1.8': 47, '1.8': 47,
'1.8.1': 47, '1.8.1': 47,
@ -175,5 +178,18 @@ SUPPORTED_MINECRAFT_VERSIONS = {
'1.14.1': 480, '1.14.1': 480,
} }
# Those Minecraft versions supported by pyCraft which are "release" versions,
# i.e. not development snapshots or pre-release versions.
RELEASE_MINECRAFT_VERSIONS = {
name: protocol for (name, protocol) in SUPPORTED_MINECRAFT_VERSIONS.items()
if __import__('re').match(r'\d+(\.\d+)+$', name)}
# The protocol versions of SUPPORTED_MINECRAFT_VERSIONS, without duplicates,
# in ascending numerical (and hence chronological) order.
SUPPORTED_PROTOCOL_VERSIONS = \ SUPPORTED_PROTOCOL_VERSIONS = \
sorted(set(SUPPORTED_MINECRAFT_VERSIONS.values())) sorted(set(SUPPORTED_MINECRAFT_VERSIONS.values()))
# The protocol versions of RELEASE_MINECRAFT_VERSIONS, without duplicates,
# in ascending numerical (and hence chronological) order.
RELEASE_PROTOCOL_VERSIONS = \
sorted(set(RELEASE_MINECRAFT_VERSIONS.values()))

View File

@ -82,22 +82,26 @@ class Connection(object):
version string or protocol version number, version string or protocol version number,
restricting the versions that the client may restricting the versions that the client may
use in connecting to the server. use in connecting to the server.
:param handle_exception: A function to be called when an exception :param handle_exception: The final exception handler. This is triggered
occurs in the client's networking thread, when an exception occurs in the networking
taking 2 arguments: the exception object 'e' thread that is not caught normally. After
as in 'except Exception as e', and a 3-tuple any other user-registered exception handlers
given by sys.exc_info(); or None for the are run, the final exception (which may be the
default behaviour of raising the exception original exception or one raised by another
from its original context; or False for no handler) is passed, regardless of whether or
action. In any case, the networking thread not it was caught by another handler, to the
will terminate, the exception will be final handler, which may be a function obeying
available via the 'exception' and 'exc_info' the protocol of 'register_exception_handler';
attributes of the 'Connection' instance. the value 'None', meaning that if the
exception was otherwise uncaught, it is
re-raised from the networking thread after
closing the connection; or the value 'False',
meaning that the exception is never re-raised.
:param handle_exit: A function to be called when a connection to a :param handle_exit: A function to be called when a connection to a
server terminates, not caused by an exception, server terminates, not caused by an exception,
and not with the intention to automatically and not with the intention to automatically
reconnect. Exceptions raised in this handler reconnect. Exceptions raised from this function
will be handled by handle_exception. will be handled by any matching exception handlers.
""" # NOQA """ # NOQA
# This lock is re-entrant because it may be acquired in a re-entrant # This lock is re-entrant because it may be acquired in a re-entrant
@ -110,6 +114,7 @@ class Connection(object):
self.early_packet_listeners = [] self.early_packet_listeners = []
self.outgoing_packet_listeners = [] self.outgoing_packet_listeners = []
self.early_outgoing_packet_listeners = [] self.early_outgoing_packet_listeners = []
self._exception_handlers = []
def proto_version(version): def proto_version(version):
if isinstance(version, str): if isinstance(version, str):
@ -191,11 +196,21 @@ class Connection(object):
""" """
Shorthand decorator to register a function as a packet listener. Shorthand decorator to register a function as a packet listener.
""" """
def _method_func(method): def listener_decorator(handler_func):
self.register_packet_listener(method, *packet_types, **kwds) self.register_packet_listener(handler_func, *packet_types, **kwds)
return method return handler_func
return _method_func return listener_decorator
def exception_handler(self, *exc_types, **kwds):
"""
Shorthand decorator to register a function as an exception handler.
"""
def exception_handler_decorator(handler_func):
self.register_exception_handler(handler_func, *exc_types, **kwds)
return handler_func
return exception_handler_decorator
def register_packet_listener(self, method, *packet_types, **kwds): def register_packet_listener(self, method, *packet_types, **kwds):
""" """
@ -229,6 +244,44 @@ class Connection(object):
else self.early_outgoing_packet_listeners else self.early_outgoing_packet_listeners
target.append(packets.PacketListener(method, *packet_types, **kwds)) target.append(packets.PacketListener(method, *packet_types, **kwds))
def register_exception_handler(self, handler_func, *exc_types, **kwds):
"""
Register a function to be called when an unhandled exception occurs
in the networking thread.
When multiple exception handlers are registered, they act like 'except'
clauses in a Python 'try' clause, with the earliest matching handler
catching the exception, and any later handlers catching any uncaught
exception raised from within an earlier handler.
Regardless of the presence or absence of matching handlers, any such
exception will cause the connection and the networking thread to
terminate, the final exception handler will be called (see the
'handle_exception' argument of the 'Connection' contructor), and the
original exception - or the last exception raised by a handler - will
be set as the 'exception' and 'exc_info' attributes of the
'Connection'.
:param handler_func: A function taking two arguments: the exception
object 'e' as in 'except Exception as e:', and the corresponding
3-tuple given by 'sys.exc_info()'. The return value of the function is
ignored, but any exception raised in it replaces the original
exception, and may be passed to later exception handlers.
:param exc_types: The types of exceptions that this handler shall
catch, as in 'except (exc_type_1, exc_type_2, ...) as e:'. If this is
empty, the handler will catch all exceptions.
:param early: If 'True', the exception handler is registered before
any existing exception handlers in the handling order.
"""
early = kwds.pop('early', False)
assert not kwds, 'Unexpected keyword arguments: %r' % (kwds,)
if early:
self._exception_handlers.insert(0, (handler_func, exc_types))
else:
self._exception_handlers.append((handler_func, exc_types))
def _pop_packet(self): def _pop_packet(self):
# Pops the topmost packet off the outgoing queue and writes it out # Pops the topmost packet off the outgoing queue and writes it out
# through the socket # through the socket
@ -400,25 +453,44 @@ class Connection(object):
self.write_packet(handshake) self.write_packet(handshake)
def _handle_exception(self, exc, exc_info): def _handle_exception(self, exc, exc_info):
handle_exception = self.handle_exception # Call the current PacketReactor's exception handler.
try: try:
if self.reactor.handle_exception(exc, exc_info): if self.reactor.handle_exception(exc, exc_info):
return return
if handle_exception not in (None, False): except Exception as new_exc:
self.handle_exception(exc, exc_info)
except BaseException as new_exc:
exc, exc_info = new_exc, sys.exc_info() exc, exc_info = new_exc, sys.exc_info()
# Call the user-registered exception handlers in order.
for handler, exc_types in self._exception_handlers:
if not exc_types or isinstance(exc, exc_types):
try:
handler(exc, exc_info)
caught = True
break
except Exception as new_exc:
exc, exc_info = new_exc, sys.exc_info()
else:
caught = False
# Call the user-specified final exception handler.
if self.handle_exception not in (None, False):
try:
self.handle_exception(exc, exc_info)
except Exception as new_exc:
exc, exc_info = new_exc, sys.exc_info()
# For backward compatibility, try to set the 'exc_info' attribute.
try: try:
exc.exc_info = exc_info # For backward compatibility. exc.exc_info = exc_info
except (TypeError, AttributeError): except (TypeError, AttributeError):
pass pass
# Record the exception and cleanly terminate the connection.
self.exception, self.exc_info = exc, exc_info self.exception, self.exc_info = exc, exc_info
with self._write_lock: self.disconnect(immediate=True)
if self.networking_thread and not self.networking_thread.interrupt:
self.disconnect(immediate=True) # If allowed by the final exception handler, re-raise the exception.
if handle_exception is None: if self.handle_exception is None and not caught:
raise_(*exc_info) raise_(*exc_info)
def _version_mismatch(self, server_protocol=None, server_version=None): def _version_mismatch(self, server_protocol=None, server_version=None):
@ -471,7 +543,7 @@ class NetworkingThread(threading.Thread):
self.connection.new_networking_thread = None self.connection.new_networking_thread = None
self._run() self._run()
self.connection._handle_exit() self.connection._handle_exit()
except BaseException as e: except Exception as e:
self.interrupt = True self.interrupt = True
self.connection._handle_exception(e, sys.exc_info()) self.connection._handle_exception(e, sys.exc_info())
finally: finally:

View File

@ -1,6 +1,7 @@
from minecraft.networking.packets import Packet from minecraft.networking.packets import Packet
from minecraft.networking.types import ( from minecraft.networking.types import (
VarInt, Integer, UnsignedByte, Position, Vector, MutableRecord VarInt, Integer, UnsignedByte, Position, Vector, MutableRecord,
attribute_alias, multi_attribute_alias,
) )
@ -20,19 +21,25 @@ class BlockChangePacket(Packet):
block_state_id = 0 block_state_id = 0
# For protocols < 347: an accessor for (block_state_id >> 4). # For protocols < 347: an accessor for (block_state_id >> 4).
@property
def blockId(self):
return self.block_state_id >> 4
@blockId.setter
def blockId(self, block_id): def blockId(self, block_id):
self.block_state_id = (self.block_state_id & 0xF) | (block_id << 4) self.block_state_id = (self.block_state_id & 0xF) | (block_id << 4)
blockId = property(lambda self: self.block_state_id >> 4, blockId)
# For protocols < 347: an accessor for (block_state_id & 0xF). # For protocols < 347: an accessor for (block_state_id & 0xF).
@property
def blockMeta(self):
return self.block_state_id & 0xF
@blockMeta.setter
def blockMeta(self, meta): def blockMeta(self, meta):
self.block_state_id = (self.block_state_id & ~0xF) | (meta & 0xF) self.block_state_id = (self.block_state_id & ~0xF) | (meta & 0xF)
blockMeta = property(lambda self: self.block_state_id & 0xF, blockMeta)
# This alias is retained for backward compatibility. # This alias is retained for backward compatibility.
def blockStateId(self, block_state_id): blockStateId = attribute_alias('block_state_id')
self.block_state_id = block_state_id
blockStateId = property(lambda self: self.block_state_id, blockStateId)
class MultiBlockChangePacket(Packet): class MultiBlockChangePacket(Packet):
@ -54,24 +61,28 @@ class MultiBlockChangePacket(Packet):
super(MultiBlockChangePacket.Record, self).__init__(**kwds) super(MultiBlockChangePacket.Record, self).__init__(**kwds)
# Access the 'x', 'y', 'z' fields as a Vector of ints. # Access the 'x', 'y', 'z' fields as a Vector of ints.
def position(self, position): position = multi_attribute_alias(Vector, 'x', 'y', 'z')
self.x, self.y, self.z = position
position = property(lambda r: Vector(r.x, r.y, r.z), position)
# For protocols < 347: an accessor for (block_state_id >> 4). # For protocols < 347: an accessor for (block_state_id >> 4).
@property
def blockId(self):
return self.block_state_id >> 4
@blockId.setter
def blockId(self, block_id): def blockId(self, block_id):
self.block_state_id = self.block_state_id & 0xF | block_id << 4 self.block_state_id = self.block_state_id & 0xF | block_id << 4
blockId = property(lambda r: r.block_state_id >> 4, blockId)
# For protocols < 347: an accessor for (block_state_id & 0xF). # For protocols < 347: an accessor for (block_state_id & 0xF).
@property
def blockMeta(self):
return self.block_state_id & 0xF
@blockMeta.setter
def blockMeta(self, meta): def blockMeta(self, meta):
self.block_state_id = self.block_state_id & ~0xF | meta & 0xF self.block_state_id = self.block_state_id & ~0xF | meta & 0xF
blockMeta = property(lambda r: r.block_state_id & 0xF, blockMeta)
# This alias is retained for backward compatibility. # This alias is retained for backward compatibility.
def blockStateId(self, block_state_id): blockStateId = attribute_alias('block_state_id')
self.block_state_id = block_state_id
blockStateId = property(lambda r: r.block_state_id, blockStateId)
def read(self, file_object): def read(self, file_object):
h_position = UnsignedByte.read(file_object) h_position = UnsignedByte.read(file_object)

View File

@ -1,4 +1,6 @@
from minecraft.networking.types import Vector, Float, Byte, Integer from minecraft.networking.types import (
Vector, Float, Byte, Integer, multi_attribute_alias,
)
from minecraft.networking.packets import Packet from minecraft.networking.packets import Packet
@ -19,23 +21,10 @@ class ExplosionPacket(Packet):
class Record(Vector): class Record(Vector):
__slots__ = () __slots__ = ()
@property position = multi_attribute_alias(Vector, 'x', 'y', 'z')
def position(self):
return Vector(self.x, self.y, self.x)
@position.setter player_motion = multi_attribute_alias(
def position(self, new_position): Vector, 'player_motion_x', 'player_motion_y', 'player_motion_z')
self.x, self.y, self.z = new_position
@property
def player_motion(self):
return Vector(self.player_motion_x, self.player_motion_y,
self.player_motion_z)
@player_motion.setter
def player_motion(self, new_player_motion):
self.player_motion_x, self.player_motion_y, self.player_motion_z \
= new_player_motion
def read(self, file_object): def read(self, file_object):
self.x = Float.read(file_object) self.x = Float.read(file_object)

View File

@ -1,7 +1,7 @@
from minecraft.networking.packets import Packet from minecraft.networking.packets import Packet
from minecraft.networking.types import ( from minecraft.networking.types import (
String, Boolean, UUID, VarInt String, Boolean, UUID, VarInt,
) )

View File

@ -68,7 +68,7 @@ class SoundEffectPacket(Packet):
if context.protocol_version >= 201: if context.protocol_version >= 201:
value = Float.read(file_object) value = Float.read(file_object)
else: else:
value = Byte.read(file_object) / 63.5 value = Byte.read(file_object)
if context.protocol_version < 204: if context.protocol_version < 204:
value /= 63.5 value /= 63.5
return value return value

View File

@ -3,7 +3,7 @@ from minecraft.networking.types.utility import descriptor
from minecraft.networking.types import ( from minecraft.networking.types import (
VarInt, UUID, Byte, Double, Integer, UnsignedByte, Short, Enum, Vector, VarInt, UUID, Byte, Double, Integer, UnsignedByte, Short, Enum, Vector,
PositionAndLook PositionAndLook, attribute_alias, multi_attribute_alias,
) )
@ -120,6 +120,7 @@ class SpawnObjectPacket(Packet):
else: else:
Byte.send(self.type_id, packet_buffer) Byte.send(self.type_id, packet_buffer)
# pylint: disable=no-member
xyz_type = Double if self.context.protocol_version >= 100 else Integer xyz_type = Double if self.context.protocol_version >= 100 else Integer
for coord in self.x, self.y, self.z: for coord in self.x, self.y, self.z:
xyz_type.send(coord, packet_buffer) xyz_type.send(coord, packet_buffer)
@ -147,28 +148,19 @@ class SpawnObjectPacket(Packet):
'in order to set the "type" property.') 'in order to set the "type" property.')
self.type_id = getattr(self.EntityType, type_name) self.type_id = getattr(self.EntityType, type_name)
# Access the fields 'x', 'y', 'z' as a Vector. @type.deleter
def position(self, position): def type(self):
self.x, self.y, self.z = position del self.type_id
position = property(lambda p: Vector(p.x, p.y, p.z), position)
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
# Access the fields 'x', 'y', 'z', 'yaw', 'pitch' as a PositionAndLook.
# NOTE: modifying the object retrieved from this property will not change # NOTE: modifying the object retrieved from this property will not change
# the packet; it can only be changed by attribute or property assignment. # the packet; it can only be changed by attribute or property assignment.
def position_and_look(self, position_and_look): position_and_look = multi_attribute_alias(
self.x, self.y, self.z = position_and_look.position PositionAndLook, x='x', y='y', z='z', yaw='yaw', pitch='pitch')
self.yaw, self.pitch = position_and_look.look
position_and_look = property(lambda p: PositionAndLook(
x=p.x, y=p.y, z=p.z, yaw=p.yaw, pitch=p.pitch),
position_and_look)
# Access the fields 'velocity_x', 'velocity_y', 'velocity_z' as a Vector. velocity = multi_attribute_alias(
def velocity(self, velocity): Vector, 'velocity_x', 'velocity_y', 'velocity_z')
self.velocity_x, self.velocity_y, self.velocity_z = velocity
velocity = property(lambda p: Vector(p.velocity_x, p.velocity_y,
p.velocity_z), velocity)
# This alias is retained for backward compatibility. # This alias is retained for backward compatibility.
def objectUUID(self, object_uuid): objectUUID = attribute_alias('object_uuid')
self.object_uuid = object_uuid
objectUUID = property(lambda self: self.object_uuid, objectUUID)

View File

@ -2,11 +2,14 @@
network representation. network representation.
""" """
from __future__ import division from __future__ import division
from collections import namedtuple from collections import namedtuple
from itertools import chain
__all__ = ( __all__ = (
'Vector', 'MutableRecord', 'PositionAndLook', 'descriptor', 'Vector', 'MutableRecord', 'PositionAndLook', 'descriptor',
'attribute_alias', 'multi_attribute_alias',
) )
@ -75,21 +78,49 @@ class MutableRecord(object):
return hash((type(self), values)) return hash((type(self), values))
class PositionAndLook(MutableRecord): def attribute_alias(name):
"""A mutable record containing 3 spatial position coordinates """An attribute descriptor that redirects access to a different attribute
and 2 rotational coordinates for a look direction. with a given name.
""" """
__slots__ = 'x', 'y', 'z', 'yaw', 'pitch' return property(fget=(lambda self: getattr(self, name)),
fset=(lambda self, value: setattr(self, name, value)),
fdel=(lambda self: delattr(self, name)))
# Access the fields 'x', 'y', 'z' as a Vector.
def position(self, position):
self.x, self.y, self.z = position
position = property(lambda self: Vector(self.x, self.y, self.z), position)
# Access the fields 'yaw', 'pitch' as a tuple. def multi_attribute_alias(container, *arg_names, **kwd_names):
def look(self, look): """A descriptor for an attribute whose value is a container of a given type
self.yaw, self.pitch = look with several fields, each of which is aliased to a different attribute
look = property(lambda self: (self.yaw, self.pitch), look) of the parent object.
The 'n'th name in 'arg_names' is the parent attribute that will be
aliased to the field of 'container' settable by the 'n'th positional
argument to its constructor, and accessible as its 'n'th iterable
element.
The name in 'kwd_names' mapped to by the key 'k' is the parent attribute
that will be aliased to the field of 'container' settable by the keyword
argument 'k' to its constructor, and accessible as its 'k' attribute.
"""
@property
def alias(self):
return container(
*(getattr(self, name) for name in arg_names),
**{kwd: getattr(self, name) for (kwd, name) in kwd_names.items()})
@alias.setter
def alias(self, values):
if arg_names:
for name, value in zip(arg_names, values):
setattr(self, name, value)
for kwd, name in kwd_names.items():
setattr(self, name, getattr(values, kwd))
@alias.deleter
def alias(self):
for name in chain(arg_names, kwd_names.values()):
delattr(self, name)
return alias
class descriptor(object): class descriptor(object):
@ -137,3 +168,17 @@ class descriptor(object):
def __delete__(self, instance): def __delete__(self, instance):
return self._fdel(self, instance) return self._fdel(self, instance)
Direction = namedtuple('Direction', ('yaw', 'pitch'))
class PositionAndLook(MutableRecord):
"""A mutable record containing 3 spatial position coordinates
and 2 rotational coordinates for a look direction.
"""
__slots__ = 'x', 'y', 'z', 'yaw', 'pitch'
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
look = multi_attribute_alias(Direction, 'yaw', 'pitch')

View File

@ -351,15 +351,14 @@ class HandleExceptionTest(ConnectTest):
def _start_client(self, client): def _start_client(self, client):
message = 'Min skoldpadda ar inte snabb, men den ar en skoldpadda.' message = 'Min skoldpadda ar inte snabb, men den ar en skoldpadda.'
@client.listener(clientbound.login.LoginSuccessPacket)
def handle_login_success(_packet): def handle_login_success(_packet):
raise Exception(message) raise Exception(message)
client.register_packet_listener(
handle_login_success, clientbound.login.LoginSuccessPacket)
def handle_exception(exc, exc_info): @client.exception_handler()
def handle_exception(exc, _exc_info):
assert isinstance(exc, Exception) and exc.args == (message,) assert isinstance(exc, Exception) and exc.args == (message,)
raise fake_server.FakeServerTestSuccess raise fake_server.FakeServerTestSuccess
client.handle_exception = handle_exception
client.connect() client.connect()

View File

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import unittest import unittest
import string import string
import logging
import struct
from zlib import decompress from zlib import decompress
from random import choice from random import choice
from minecraft import SUPPORTED_PROTOCOL_VERSIONS from minecraft import SUPPORTED_PROTOCOL_VERSIONS, RELEASE_PROTOCOL_VERSIONS
from minecraft.networking.connection import ConnectionContext from minecraft.networking.connection import ConnectionContext
from minecraft.networking.types import ( from minecraft.networking.types import (
VarInt, Enum, Vector, PositionAndLook VarInt, Enum, Vector, PositionAndLook
@ -14,6 +16,10 @@ from minecraft.networking.packets import (
clientbound clientbound
) )
TEST_VERSIONS = list(RELEASE_PROTOCOL_VERSIONS)
if SUPPORTED_PROTOCOL_VERSIONS[-1] not in TEST_VERSIONS:
TEST_VERSIONS.append(SUPPORTED_PROTOCOL_VERSIONS[-1])
class PacketBufferTest(unittest.TestCase): class PacketBufferTest(unittest.TestCase):
def test_basic_read_write(self): def test_basic_read_write(self):
@ -42,7 +48,8 @@ class PacketBufferTest(unittest.TestCase):
class PacketSerializationTest(unittest.TestCase): class PacketSerializationTest(unittest.TestCase):
def test_packet(self): def test_packet(self):
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS: for protocol_version in TEST_VERSIONS:
logging.debug('protocol_version = %r' % protocol_version)
context = ConnectionContext(protocol_version=protocol_version) context = ConnectionContext(protocol_version=protocol_version)
packet = serverbound.play.ChatPacket(context) packet = serverbound.play.ChatPacket(context)
@ -63,7 +70,8 @@ class PacketSerializationTest(unittest.TestCase):
self.assertEqual(packet.message, deserialized.message) self.assertEqual(packet.message, deserialized.message)
def test_compressed_packet(self): def test_compressed_packet(self):
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS: for protocol_version in TEST_VERSIONS:
logging.debug('protocol_version = %r' % protocol_version)
context = ConnectionContext(protocol_version=protocol_version) context = ConnectionContext(protocol_version=protocol_version)
msg = ''.join(choice(string.ascii_lowercase) for i in range(500)) msg = ''.join(choice(string.ascii_lowercase) for i in range(500))
@ -74,7 +82,8 @@ class PacketSerializationTest(unittest.TestCase):
self.write_read_packet(packet, -1) self.write_read_packet(packet, -1)
def write_read_packet(self, packet, compression_threshold): def write_read_packet(self, packet, compression_threshold):
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS: for protocol_version in TEST_VERSIONS:
logging.debug('protocol_version = %r' % protocol_version)
context = ConnectionContext(protocol_version=protocol_version) context = ConnectionContext(protocol_version=protocol_version)
packet_buffer = PacketBuffer() packet_buffer = PacketBuffer()
@ -108,7 +117,8 @@ class PacketListenerTest(unittest.TestCase):
def test_packet(chat_packet): def test_packet(chat_packet):
self.assertEqual(chat_packet.message, message) self.assertEqual(chat_packet.message, message)
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS: for protocol_version in TEST_VERSIONS:
logging.debug('protocol_version = %r' % protocol_version)
context = ConnectionContext(protocol_version=protocol_version) context = ConnectionContext(protocol_version=protocol_version)
listener = PacketListener(test_packet, serverbound.play.ChatPacket) listener = PacketListener(test_packet, serverbound.play.ChatPacket)
@ -145,13 +155,6 @@ class PacketEnumTest(unittest.TestCase):
class TestReadWritePackets(unittest.TestCase): class TestReadWritePackets(unittest.TestCase):
maxDiff = None maxDiff = None
def setUp(self):
self.context = ConnectionContext()
self.context.protocol_version = SUPPORTED_PROTOCOL_VERSIONS[-1]
def tearDown(self):
del self.context
def test_explosion_packet(self): def test_explosion_packet(self):
Record = clientbound.play.ExplosionPacket.Record Record = clientbound.play.ExplosionPacket.Record
packet = clientbound.play.ExplosionPacket( packet = clientbound.play.ExplosionPacket(
@ -159,6 +162,7 @@ class TestReadWritePackets(unittest.TestCase):
records=[Record(-14, -116, -5), Record(-77, 34, -36), records=[Record(-14, -116, -5), Record(-77, 34, -36),
Record(-35, -127, 95), Record(11, 113, -8)], Record(-35, -127, 95), Record(11, 113, -8)],
player_motion=Vector(4, 5, 0)) player_motion=Vector(4, 5, 0))
self._test_read_write_packet(packet) self._test_read_write_packet(packet)
def test_combat_event_packet(self): def test_combat_event_packet(self):
@ -187,57 +191,85 @@ class TestReadWritePackets(unittest.TestCase):
self._test_read_write_packet(packet) self._test_read_write_packet(packet)
def test_spawn_object_packet(self): def test_spawn_object_packet(self):
EntityType = clientbound.play.SpawnObjectPacket.field_enum( for protocol_version in TEST_VERSIONS:
'type_id', self.context) logging.debug('protocol_version = %r' % protocol_version)
context = ConnectionContext(protocol_version=protocol_version)
object_uuid = 'd9568851-85bc-4a10-8d6a-261d130626fa' EntityType = clientbound.play.SpawnObjectPacket.field_enum(
pos_look = PositionAndLook(x=68.0, y=38.0, z=76.0, yaw=16, pitch=23) 'type_id', context)
velocity = Vector(21, 55, 41)
entity_id, type_name, type_id = 49846, 'EGG', EntityType.EGG
packet = clientbound.play.SpawnObjectPacket( pos_look = PositionAndLook(
context=self.context, position=(Vector(68.0, 38.0, 76.0) if context.protocol_version
x=pos_look.x, y=pos_look.y, z=pos_look.z, >= 100 else Vector(68, 38, 76)),
yaw=pos_look.yaw, pitch=pos_look.pitch, yaw=16, pitch=23)
velocity_x=velocity.x, velocity_y=velocity.y, velocity = Vector(21, 55, 41)
velocity_z=velocity.z, object_uuid=object_uuid, entity_id, type_name, type_id = 49846, 'EGG', EntityType.EGG
entity_id=entity_id, type_id=type_id, data=1)
self.assertEqual(packet.position_and_look, pos_look)
self.assertEqual(packet.position, pos_look.position)
self.assertEqual(packet.velocity, velocity)
self.assertEqual(packet.objectUUID, object_uuid)
self.assertEqual(packet.type, type_name)
packet2 = clientbound.play.SpawnObjectPacket( packet = clientbound.play.SpawnObjectPacket(
context=self.context, position_and_look=pos_look, context=context,
velocity=velocity, type=type_name, x=pos_look.x, y=pos_look.y, z=pos_look.z,
object_uuid=object_uuid, entity_id=entity_id, data=1) yaw=pos_look.yaw, pitch=pos_look.pitch,
self.assertEqual(packet.__dict__, packet2.__dict__) velocity_x=velocity.x, velocity_y=velocity.y,
velocity_z=velocity.z,
entity_id=entity_id, type_id=type_id, data=1)
if context.protocol_version >= 49:
object_uuid = 'd9568851-85bc-4a10-8d6a-261d130626fa'
packet.object_uuid = object_uuid
self.assertEqual(packet.objectUUID, object_uuid)
self.assertEqual(packet.position_and_look, pos_look)
self.assertEqual(packet.position, pos_look.position)
self.assertEqual(packet.velocity, velocity)
self.assertEqual(packet.type, type_name)
packet2.position = pos_look.position packet2 = clientbound.play.SpawnObjectPacket(
self.assertEqual(packet.position, packet2.position) context=context, position_and_look=pos_look,
velocity=velocity, type=type_name,
entity_id=entity_id, data=1)
if context.protocol_version >= 49:
packet2.object_uuid = object_uuid
self.assertEqual(packet.__dict__, packet2.__dict__)
packet2.data = 0 packet2.position = pos_look.position
self._test_read_write_packet(packet) self.assertEqual(packet.position, packet2.position)
self._test_read_write_packet(packet2)
packet2.data = 0
if context.protocol_version < 49:
del packet2.velocity
self._test_read_write_packet(packet, context)
self._test_read_write_packet(packet2, context)
def test_sound_effect_packet(self): def test_sound_effect_packet(self):
SoundCategory = clientbound.play.SoundEffectPacket.SoundCategory for protocol_version in TEST_VERSIONS:
packet = clientbound.play.SoundEffectPacket( context = ConnectionContext(protocol_version=protocol_version)
sound_category=SoundCategory.NEUTRAL, sound_id=545,
effect_position=Vector(0.125, 300.0, 50.5), volume=0.75, pitch=1.5)
self._test_read_write_packet(packet)
def _test_read_write_packet(self, packet_in): packet = clientbound.play.SoundEffectPacket(
packet_in.context = self.context sound_id=545, effect_position=Vector(0.125, 300.0, 50.5),
packet_buffer = PacketBuffer() volume=0.75)
packet_in.write(packet_buffer) if context.protocol_version >= 201:
packet_buffer.reset_cursor() packet.pitch = struct.unpack('f', struct.pack('f', 1.5))[0]
VarInt.read(packet_buffer) else:
packet_id = VarInt.read(packet_buffer) packet.pitch = int(1.5 / 63.5) * 63.5
self.assertEqual(packet_id, packet_in.id) if context.protocol_version >= 95:
packet.sound_category = \
clientbound.play.SoundEffectPacket.SoundCategory.NEUTRAL
self._test_read_write_packet(packet, context)
packet_out = type(packet_in)(context=self.context) def _test_read_write_packet(self, packet_in, context=None):
packet_out.read(packet_buffer) if context is None:
self.assertIs(type(packet_in), type(packet_out)) for protocol_version in TEST_VERSIONS:
self.assertEqual(packet_in.__dict__, packet_out.__dict__) logging.debug('protocol_version = %r' % protocol_version)
context = ConnectionContext(protocol_version=protocol_version)
self._test_read_write_packet(packet_in, context)
else:
packet_in.context = context
packet_buffer = PacketBuffer()
packet_in.write(packet_buffer)
packet_buffer.reset_cursor()
VarInt.read(packet_buffer)
packet_id = VarInt.read(packet_buffer)
self.assertEqual(packet_id, packet_in.id)
packet_out = type(packet_in)(context=context)
packet_out.read(packet_buffer)
self.assertIs(type(packet_in), type(packet_out))
self.assertEqual(packet_in.__dict__, packet_out.__dict__)

View File

@ -9,9 +9,13 @@ from minecraft.networking.types import (
) )
from minecraft.networking.packets import PacketBuffer from minecraft.networking.packets import PacketBuffer
from minecraft.networking.connection import ConnectionContext from minecraft.networking.connection import ConnectionContext
from minecraft import SUPPORTED_PROTOCOL_VERSIONS from minecraft import SUPPORTED_PROTOCOL_VERSIONS, RELEASE_PROTOCOL_VERSIONS
TEST_VERSIONS = list(RELEASE_PROTOCOL_VERSIONS)
if SUPPORTED_PROTOCOL_VERSIONS[-1] not in TEST_VERSIONS:
TEST_VERSIONS.append(SUPPORTED_PROTOCOL_VERSIONS[-1])
TEST_DATA = { TEST_DATA = {
Boolean: [True, False], Boolean: [True, False],
UnsignedByte: [0, 125], UnsignedByte: [0, 125],
@ -36,7 +40,7 @@ TEST_DATA = {
class SerializationTest(unittest.TestCase): class SerializationTest(unittest.TestCase):
def test_serialization(self): def test_serialization(self):
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS: for protocol_version in TEST_VERSIONS:
context = ConnectionContext(protocol_version=protocol_version) context = ConnectionContext(protocol_version=protocol_version)
for data_type in Type.__subclasses__(): for data_type in Type.__subclasses__():

View File

@ -7,10 +7,11 @@
envlist = py27, py34, py35, py36, pypy, flake8, pylint-errors, pylint-full, verify-manifest envlist = py27, py34, py35, py36, pypy, flake8, pylint-errors, pylint-full, verify-manifest
[testenv] [testenv]
commands = nosetests commands = nosetests --with-timer
deps = deps =
nose nose
nose-timer
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
[testenv:cover] [testenv:cover]
@ -35,7 +36,7 @@ deps =
setenv = setenv =
PYCRAFT_RUN_INTERNET_TESTS=1 PYCRAFT_RUN_INTERNET_TESTS=1
commands = commands =
nosetests --with-xunit --with-xcoverage --cover-package=minecraft --cover-erase --cover-inclusive --cover-tests --cover-branches --cover-min-percentage=60 {[testenv]commands} --with-xunit --with-xcoverage --cover-package=minecraft --cover-erase --cover-inclusive --cover-tests --cover-branches --cover-min-percentage=60
deps = deps =
{[testenv:cover]deps} {[testenv:cover]deps}