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.
"""
# The version number of the most recent pyCraft release.
__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 = {
'1.8': 47,
'1.8.1': 47,
@ -175,5 +178,18 @@ SUPPORTED_MINECRAFT_VERSIONS = {
'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 = \
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,
restricting the versions that the client may
use in connecting to the server.
:param handle_exception: A function to be called when an exception
occurs in the client's networking thread,
taking 2 arguments: the exception object 'e'
as in 'except Exception as e', and a 3-tuple
given by sys.exc_info(); or None for the
default behaviour of raising the exception
from its original context; or False for no
action. In any case, the networking thread
will terminate, the exception will be
available via the 'exception' and 'exc_info'
attributes of the 'Connection' instance.
:param handle_exception: The final exception handler. This is triggered
when an exception occurs in the networking
thread that is not caught normally. After
any other user-registered exception handlers
are run, the final exception (which may be the
original exception or one raised by another
handler) is passed, regardless of whether or
not it was caught by another handler, to the
final handler, which may be a function obeying
the protocol of 'register_exception_handler';
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
server terminates, not caused by an exception,
and not with the intention to automatically
reconnect. Exceptions raised in this handler
will be handled by handle_exception.
reconnect. Exceptions raised from this function
will be handled by any matching exception handlers.
""" # NOQA
# 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.outgoing_packet_listeners = []
self.early_outgoing_packet_listeners = []
self._exception_handlers = []
def proto_version(version):
if isinstance(version, str):
@ -191,11 +196,21 @@ class Connection(object):
"""
Shorthand decorator to register a function as a packet listener.
"""
def _method_func(method):
self.register_packet_listener(method, *packet_types, **kwds)
return method
def listener_decorator(handler_func):
self.register_packet_listener(handler_func, *packet_types, **kwds)
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):
"""
@ -229,6 +244,44 @@ class Connection(object):
else self.early_outgoing_packet_listeners
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):
# Pops the topmost packet off the outgoing queue and writes it out
# through the socket
@ -400,25 +453,44 @@ class Connection(object):
self.write_packet(handshake)
def _handle_exception(self, exc, exc_info):
handle_exception = self.handle_exception
# Call the current PacketReactor's exception handler.
try:
if self.reactor.handle_exception(exc, exc_info):
return
if handle_exception not in (None, False):
self.handle_exception(exc, exc_info)
except BaseException as new_exc:
except Exception as new_exc:
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:
exc.exc_info = exc_info # For backward compatibility.
exc.exc_info = exc_info
except (TypeError, AttributeError):
pass
# Record the exception and cleanly terminate the connection.
self.exception, self.exc_info = exc, exc_info
with self._write_lock:
if self.networking_thread and not self.networking_thread.interrupt:
self.disconnect(immediate=True)
if handle_exception is None:
self.disconnect(immediate=True)
# If allowed by the final exception handler, re-raise the exception.
if self.handle_exception is None and not caught:
raise_(*exc_info)
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._run()
self.connection._handle_exit()
except BaseException as e:
except Exception as e:
self.interrupt = True
self.connection._handle_exception(e, sys.exc_info())
finally:

View File

@ -1,6 +1,7 @@
from minecraft.networking.packets import Packet
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
# 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):
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).
@property
def blockMeta(self):
return self.block_state_id & 0xF
@blockMeta.setter
def blockMeta(self, meta):
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.
def blockStateId(self, block_state_id):
self.block_state_id = block_state_id
blockStateId = property(lambda self: self.block_state_id, blockStateId)
blockStateId = attribute_alias('block_state_id')
class MultiBlockChangePacket(Packet):
@ -54,24 +61,28 @@ class MultiBlockChangePacket(Packet):
super(MultiBlockChangePacket.Record, self).__init__(**kwds)
# Access the 'x', 'y', 'z' fields as a Vector of ints.
def position(self, position):
self.x, self.y, self.z = position
position = property(lambda r: Vector(r.x, r.y, r.z), position)
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
# 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):
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).
@property
def blockMeta(self):
return self.block_state_id & 0xF
@blockMeta.setter
def blockMeta(self, meta):
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.
def blockStateId(self, block_state_id):
self.block_state_id = block_state_id
blockStateId = property(lambda r: r.block_state_id, blockStateId)
blockStateId = attribute_alias('block_state_id')
def read(self, 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
@ -19,23 +21,10 @@ class ExplosionPacket(Packet):
class Record(Vector):
__slots__ = ()
@property
def position(self):
return Vector(self.x, self.y, self.x)
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
@position.setter
def position(self, new_position):
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
player_motion = multi_attribute_alias(
Vector, 'player_motion_x', 'player_motion_y', 'player_motion_z')
def read(self, file_object):
self.x = Float.read(file_object)

View File

@ -1,7 +1,7 @@
from minecraft.networking.packets import Packet
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:
value = Float.read(file_object)
else:
value = Byte.read(file_object) / 63.5
value = Byte.read(file_object)
if context.protocol_version < 204:
value /= 63.5
return value

View File

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

View File

@ -2,11 +2,14 @@
network representation.
"""
from __future__ import division
from collections import namedtuple
from itertools import chain
__all__ = (
'Vector', 'MutableRecord', 'PositionAndLook', 'descriptor',
'attribute_alias', 'multi_attribute_alias',
)
@ -75,21 +78,49 @@ class MutableRecord(object):
return hash((type(self), values))
class PositionAndLook(MutableRecord):
"""A mutable record containing 3 spatial position coordinates
and 2 rotational coordinates for a look direction.
def attribute_alias(name):
"""An attribute descriptor that redirects access to a different attribute
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 look(self, look):
self.yaw, self.pitch = look
look = property(lambda self: (self.yaw, self.pitch), look)
def multi_attribute_alias(container, *arg_names, **kwd_names):
"""A descriptor for an attribute whose value is a container of a given type
with several fields, each of which is aliased to a different attribute
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):
@ -137,3 +168,17 @@ class descriptor(object):
def __delete__(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):
message = 'Min skoldpadda ar inte snabb, men den ar en skoldpadda.'
@client.listener(clientbound.login.LoginSuccessPacket)
def handle_login_success(_packet):
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,)
raise fake_server.FakeServerTestSuccess
client.handle_exception = handle_exception
client.connect()

View File

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*-
import unittest
import string
import logging
import struct
from zlib import decompress
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.types import (
VarInt, Enum, Vector, PositionAndLook
@ -14,6 +16,10 @@ from minecraft.networking.packets import (
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):
def test_basic_read_write(self):
@ -42,7 +48,8 @@ class PacketBufferTest(unittest.TestCase):
class PacketSerializationTest(unittest.TestCase):
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)
packet = serverbound.play.ChatPacket(context)
@ -63,7 +70,8 @@ class PacketSerializationTest(unittest.TestCase):
self.assertEqual(packet.message, deserialized.message)
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)
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)
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)
packet_buffer = PacketBuffer()
@ -108,7 +117,8 @@ class PacketListenerTest(unittest.TestCase):
def test_packet(chat_packet):
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)
listener = PacketListener(test_packet, serverbound.play.ChatPacket)
@ -145,13 +155,6 @@ class PacketEnumTest(unittest.TestCase):
class TestReadWritePackets(unittest.TestCase):
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):
Record = clientbound.play.ExplosionPacket.Record
packet = clientbound.play.ExplosionPacket(
@ -159,6 +162,7 @@ class TestReadWritePackets(unittest.TestCase):
records=[Record(-14, -116, -5), Record(-77, 34, -36),
Record(-35, -127, 95), Record(11, 113, -8)],
player_motion=Vector(4, 5, 0))
self._test_read_write_packet(packet)
def test_combat_event_packet(self):
@ -187,57 +191,85 @@ class TestReadWritePackets(unittest.TestCase):
self._test_read_write_packet(packet)
def test_spawn_object_packet(self):
EntityType = clientbound.play.SpawnObjectPacket.field_enum(
'type_id', self.context)
for protocol_version in TEST_VERSIONS:
logging.debug('protocol_version = %r' % protocol_version)
context = ConnectionContext(protocol_version=protocol_version)
object_uuid = 'd9568851-85bc-4a10-8d6a-261d130626fa'
pos_look = PositionAndLook(x=68.0, y=38.0, z=76.0, yaw=16, pitch=23)
velocity = Vector(21, 55, 41)
entity_id, type_name, type_id = 49846, 'EGG', EntityType.EGG
EntityType = clientbound.play.SpawnObjectPacket.field_enum(
'type_id', context)
packet = clientbound.play.SpawnObjectPacket(
context=self.context,
x=pos_look.x, y=pos_look.y, z=pos_look.z,
yaw=pos_look.yaw, pitch=pos_look.pitch,
velocity_x=velocity.x, velocity_y=velocity.y,
velocity_z=velocity.z, object_uuid=object_uuid,
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)
pos_look = PositionAndLook(
position=(Vector(68.0, 38.0, 76.0) if context.protocol_version
>= 100 else Vector(68, 38, 76)),
yaw=16, pitch=23)
velocity = Vector(21, 55, 41)
entity_id, type_name, type_id = 49846, 'EGG', EntityType.EGG
packet2 = clientbound.play.SpawnObjectPacket(
context=self.context, position_and_look=pos_look,
velocity=velocity, type=type_name,
object_uuid=object_uuid, entity_id=entity_id, data=1)
self.assertEqual(packet.__dict__, packet2.__dict__)
packet = clientbound.play.SpawnObjectPacket(
context=context,
x=pos_look.x, y=pos_look.y, z=pos_look.z,
yaw=pos_look.yaw, pitch=pos_look.pitch,
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
self.assertEqual(packet.position, packet2.position)
packet2 = clientbound.play.SpawnObjectPacket(
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
self._test_read_write_packet(packet)
self._test_read_write_packet(packet2)
packet2.position = pos_look.position
self.assertEqual(packet.position, packet2.position)
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):
SoundCategory = clientbound.play.SoundEffectPacket.SoundCategory
packet = clientbound.play.SoundEffectPacket(
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)
for protocol_version in TEST_VERSIONS:
context = ConnectionContext(protocol_version=protocol_version)
def _test_read_write_packet(self, packet_in):
packet_in.context = self.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 = clientbound.play.SoundEffectPacket(
sound_id=545, effect_position=Vector(0.125, 300.0, 50.5),
volume=0.75)
if context.protocol_version >= 201:
packet.pitch = struct.unpack('f', struct.pack('f', 1.5))[0]
else:
packet.pitch = int(1.5 / 63.5) * 63.5
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)
packet_out.read(packet_buffer)
self.assertIs(type(packet_in), type(packet_out))
self.assertEqual(packet_in.__dict__, packet_out.__dict__)
def _test_read_write_packet(self, packet_in, context=None):
if context is None:
for protocol_version in TEST_VERSIONS:
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.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 = {
Boolean: [True, False],
UnsignedByte: [0, 125],
@ -36,7 +40,7 @@ TEST_DATA = {
class SerializationTest(unittest.TestCase):
def test_serialization(self):
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS:
for protocol_version in TEST_VERSIONS:
context = ConnectionContext(protocol_version=protocol_version)
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
[testenv]
commands = nosetests
commands = nosetests --with-timer
deps =
nose
nose-timer
-r{toxinidir}/requirements.txt
[testenv:cover]
@ -35,7 +36,7 @@ deps =
setenv =
PYCRAFT_RUN_INTERNET_TESTS=1
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 =
{[testenv:cover]deps}