mirror of
https://github.com/ammaraskar/pyCraft.git
synced 2024-11-22 02:08:56 +01:00
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:
parent
6ef868bc5b
commit
7b1567c352
@ -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()))
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from minecraft.networking.packets import Packet
|
||||
|
||||
from minecraft.networking.types import (
|
||||
String, Boolean, UUID, VarInt
|
||||
String, Boolean, UUID, VarInt,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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__)
|
||||
|
@ -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__():
|
||||
|
5
tox.ini
5
tox.ini
@ -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}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user