diff --git a/minecraft/compat.py b/minecraft/compat.py index 9a06b6db..cf6c3d57 100644 --- a/minecraft/compat.py +++ b/minecraft/compat.py @@ -3,17 +3,10 @@ both Python2 and Python3 while using the same codebase. """ -# Raw input -> input shenangians -# example -# > from minecraft.compat import input -# > input("asd") +import six -# Hi, I'm pylint, and sometimes I act silly, at which point my programmer -# overlords need to correct me. - -# pylint: disable=undefined-variable,redefined-builtin,invalid-name -try: - input = raw_input -except NameError: - input = input -# pylint: enable=undefined-variable,redefined-builtin,invalid-name +# ### LONG ### +if six.PY3: + long = int +else: # pragma: no cover + long = long diff --git a/minecraft/exceptions.py b/minecraft/exceptions.py index 80f57cd9..e3d57f61 100644 --- a/minecraft/exceptions.py +++ b/minecraft/exceptions.py @@ -5,5 +5,19 @@ class YggdrasilError(Exception): """ - Base `Exception` for the Yggdrasil authentication service. + Base ``Exception`` for the Yggdrasil authentication service. + """ + + +class DeserializationError(Exception): + """ + ``Exception`` raised when something went wrong during the deserialization + process. + """ + + +class SerializationError(Exception): + """ + ``Exception`` raised when something went wrong during the serialization + process. """ diff --git a/minecraft/networking/datatypes.py b/minecraft/networking/datatypes.py new file mode 100644 index 00000000..9410ca6a --- /dev/null +++ b/minecraft/networking/datatypes.py @@ -0,0 +1,443 @@ +""" +Contains the datatypes used by the networking part of `pyminecraft`. +The types are described at http://wiki.vg/Protocol#Data_types + +These datatypes are used by the packet definitions. +""" + +__all__ = ["ENDIANNESS", + "Datatype", "NumberDatatype", "StringDatatype", + "Boolean", + "Byte", "UnsignedByte", + "Short", "UnsignedShort", + "Integer", "UnsignedInteger", + "Long", "UnsignedLong", + "LongLong", "UnsignedLongLong", + "Float", + "Double", + "VarInt", "VarLong", + "String"] + +from minecraft.exceptions import DeserializationError +from minecraft.compat import long +from io import BytesIO +import struct +import collections +import numbers + +ENDIANNESS = "!" # Network, big-endian + + +def raise_serialization_data(func): + """ + A decorator to be used on a ``Datatype``.serialize definition. + + Must be placed before a classmethod decorator. + """ + def wrapped(cls, data): + cls.raise_serialization_data(data) + + return func(cls, data) + + return wrapped + + +def raise_deserialization_data(func): + """ + A decorator to be used on a ``Datatype``.serialize definition. + + Must be placed before a classmethod decorator. + """ + def wrapped(cls, data): + cls.raise_deserialization_data(data) + + return func(cls, data) + + return wrapped + + +class Datatype(object): + """ + Base object for all `pyminecraft` networking datatypes. + + ``Datatype``.SIZE can be either a number, specifying an exact required size + of data to be deserialized, or it can be a tuple like this: + ``(MIN_SIZE, MAX_SIZE)`` + + + .. note:: + If ``ALLOWED_SERIALIZATION_TYPES`` is not empty, only the types found + in ``ALLOWED_SERIALIZATION_TYPES`` are allowed as serialization + ``data``. This does somewhat go against the Duck-typing principle. + + The same applies for ``ALLOWED_DESERIALIZATION_TYPES``. + + .. note:: + If ``DISALLOWED_SERIALIZATION_TYPES`` is not empty, only the types + found in ``DISALLOWED_SERIALIZATION_TYPES`` are allowed as + serialization ``data``. This does somewhat go against the + Duck-typing principle. + + ``DISALLOWED_SERIALIZATION_TYPES`` exists as a way to exclude certain + subclasses of a given type. + + The same applies for ``DISALLOWED_DESERIALIZATION_TYPES``. + """ + FORMAT = "" + SIZE = 0 + + ALLOWED_SERIALIZATION_TYPES = tuple() + ALLOWED_DESERIALIZATION_TYPES = tuple() + + DISALLOWED_SERIALIZATION_TYPES = tuple() + DISALLOWED_SERIALIZATION_TYPES = tuple() + + @classmethod + def read(cls, fileobject): + bin_data = fileobject.read(cls.SIZE) + return cls.deserialize(bin_data) + + @classmethod + @raise_deserialization_data + def deserialize(cls, data): + deserialized_data = struct.unpack(ENDIANNESS + cls.FORMAT, data)[0] + return deserialized_data + + @classmethod + def write(cls, fileobject, data): + return fileobject.write(cls.serialize(data)) + + @classmethod + @raise_serialization_data + def serialize(cls, data): + serialized_data = struct.pack(ENDIANNESS + cls.FORMAT, data) + return serialized_data + + @classmethod + def raise_serialization_data(cls, data): + """ + Raises an appropriate ``Exception`` if ``data`` is not valid. + + :return: ``None`` + :rtype: ``None`` + :raises: ``TypeError``, ``ValueError`` + """ + error_message = "'data's type ('{}') is not an allowed type." + error_message = error_message.format(type(data).__name__) + + if (cls.ALLOWED_SERIALIZATION_TYPES and + not any([isinstance(data, type_) for type_ + in cls.ALLOWED_SERIALIZATION_TYPES])): + + raise TypeError(error_message) + + for type_ in cls.DISALLOWED_SERIALIZATION_TYPES: + if isinstance(data, type_): + raise TypeError(error_message) + + cls._raise_serialization_value_error_data(data) + + return None + + @classmethod + def _raise_serialization_value_error_data(cls, data): + """ + Raises a ValueError if ``data`` is not valid. + + :return: ``None`` + :rtype: ``None`` + :raises: ``ValueError`` + """ + return None + + @classmethod + def raise_deserialization_data(cls, data): + """ + Raises an appropriate ``Exception`` if ``data`` is not valid. + + :return: ``None`` + :rtype: ``None`` + :raises: ``TypeError``, ``ValueError`` + """ + if (cls.ALLOWED_DESERIALIZATION_TYPES and + not any([isinstance(data, type_) for type_ + in cls.ALLOWED_DESERIALIZATION_TYPES])): + + err = "'data's type ('{}') is not an allowed type." + err = err.format(type(data).__name__) + + raise TypeError(err) + + if isinstance(cls.SIZE, numbers.Number): + if cls.SIZE != len(data): + err = "'data' must have a length of {}, not {}" + err = err.format(str(cls.SIZE), str(len(data))) + + raise ValueError(err) + + elif isinstance(cls.SIZE, collections.Sequence): + if not cls.SIZE[0] <= len(data) <= cls.SIZE[1]: + err = "'data' must have a length between {} and {}, not {}" + err = err.format(str(cls.SIZE[0]), str(cls.SIZE[1]), + str(len(data))) + + raise ValueError(err) + + else: + raise TypeError("'cls.SIZE' must be a number or a sequence.") + + return None + + +class NumberDatatype(Datatype): + """ + Base abstract class for all number-like minecraft networking datatypes. + + .. note:: + Numbers to be serialized must be between this classes + ``MIN_NUMBER_VALUE`` and ``MAX_NUMBER_VALUE``, or a ``ValueError`` will + be raised. + + If ``MIN_NUMBER_VALUE`` or ``MAX_NUMBER_VALUE`` are ``None`` + (as in the case of float), checking is left to the ``struct`` module. + """ + + MIN_NUMBER_VALUE = None + MAX_NUMBER_VALUE = None + + ALLOWED_SERIALIZATION_TYPES = (int, long) + DISALLOWED_SERIALIZATION_TYPES = (bool,) + + @classmethod + def _raise_serialization_value_error_data(cls, data): + if (cls.MIN_NUMBER_VALUE is not None + and cls.MAX_NUMBER_VALUE is not None): + + if not cls.MIN_NUMBER_VALUE <= data <= cls.MAX_NUMBER_VALUE: + err = "'data' must be an integer with value between {} and {}." + err = err.format(str(cls.MIN_NUMBER_VALUE), + str(cls.MAX_NUMBER_VALUE)) + + raise ValueError(err) + + return None + + +class StringDatatype(Datatype): + pass + + +class Boolean(Datatype): + FORMAT = "?" + SIZE = 1 + + ALLOWED_SERIALIZATION_TYPES = (bool,) + ALLOWED_DESERIALIZATION_TYPES = (collections.Sequence,) + + +class Byte(NumberDatatype): + FORMAT = "b" + SIZE = 1 + + MIN_NUMBER_VALUE = -128 + MAX_NUMBER_VALUE = 127 + + +class UnsignedByte(NumberDatatype): + FORMAT = "B" + SIZE = 1 + + MIN_NUMBER_VALUE = 0 + MAX_NUMBER_VALUE = 255 + + +class Short(NumberDatatype): + FORMAT = "h" + SIZE = 2 + + MIN_NUMBER_VALUE = -32768 + MAX_NUMBER_VALUE = 32767 + + +class UnsignedShort(NumberDatatype): + FORMAT = "H" + SIZE = 2 + + MIN_NUMBER_VALUE = 0 + MAX_NUMBER_VALUE = 65535 + + +class Integer(NumberDatatype): + FORMAT = "i" + SIZE = 4 + + MIN_NUMBER_VALUE = -2147483648 + MAX_NUMBER_VALUE = 2147483647 + + +class UnsignedInteger(NumberDatatype): + FORMAT = "I" + SIZE = 4 + + MIN_NUMBER_VALUE = 0 + MAX_NUMBER_VALUE = 4294967295 + + +class Long(NumberDatatype): + FORMAT = "l" + SIZE = 4 + + MIN_NUMBER_VALUE = -2147483648 + MAX_NUMBER_VALUE = 2147483647 + + +class UnsignedLong(NumberDatatype): + FORMAT = "L" + SIZE = 4 + + MIN_NUMBER_VALUE = 0 + MAX_NUMBER_VALUE = 4294967295 + + +class LongLong(NumberDatatype): + FORMAT = "q" + SIZE = 8 + + MIN_NUMBER_VALUE = -9223372036854775808 + MAX_NUMBER_VALUE = 9223372036854775807 + + +class UnsignedLongLong(NumberDatatype): + FORMAT = "Q" + SIZE = 8 + + MIN_NUMBER_VALUE = 0 + MAX_NUMBER_VALUE = 18446744073709551615 + + +class Float(NumberDatatype): + FORMAT = "f" + SIZE = 4 + + ALLOWED_SERIALIZATION_TYPES = (int, long, float) + DISALLOWED_SERIALIZATION_TYPES = (bool,) + + +class Double(Float): + FORMAT = "d" + SIZE = 8 + + +class VarInt(NumberDatatype): + # See: https://developers.google.com/protocol-buffers/docs/encoding#varints + # See: https://github.com/ammaraskar/pyCraft/blob/7e8df473520d57ca22fb57888681f51705128cdc/network/types.py#l123 # noqa + # See: https://github.com/google/protobuf/blob/0c59f2e6fc0a2cb8e8e3b4c7327f650e8586880a/python/google/protobuf/internal/decoder.py#l107 # noqa + # According to http://wiki.vg/Protocol#Data_types, + # MineCraftian VarInts can be at most 5 bytes. + + # Maximum integer value: size of serialized VarInt in bytes + SIZE_TABLE = { + 2**7: 1, + 2**14: 2, + 2**21: 3, + 2**28: 4, + 2**35: 5, + } + + # Largest element in SIZE_TABLE, assuming largest element is last. + MAX_SIZE = list(SIZE_TABLE.items())[-1][-1] + + SIZE = (1, MAX_SIZE) + + @classmethod + def read(cls, fileobject): + number = 0 # The decoded number + + i = 0 # Incrementor + while True: + if i > cls.MAX_SIZE: # Check if we have exceeded max-size + name_of_self = str(type(cls)) + e = "Data too large to be a {}".format(name_of_self) + raise DeserializationError(e) + + try: + byte = ord(fileobject.read(1)) # Read a byte as integer + except TypeError: + e = "Fileobject ran out of data. Socket closed?" + raise DeserializationError(e) + + number |= ((byte & 0x7f) << (i * 7)) + if not (byte & 0x80): + break + + i += 1 + return number + + @classmethod + @raise_deserialization_data + def deserialize(cls, data): + data_fileobject = BytesIO(bytes(data)) + return cls.read(data_fileobject) + + @classmethod + @raise_serialization_data + def serialize(cls, data): + if data > cls.SIZE_TABLE[-1][0]: + name_of_self = str(type(cls)) + e = "Number too big to serialize as {}".format(name_of_self) + raise ValueError(e) + + result = bytes() # Where we store the serialized number + + while True: + byte = data & 0x7f + data >>= 7 + + result += UnsignedByte.serialize(byte | (0x80 if data > 0 else 0)) + + if not data: + break + + return result + + +class VarLong(VarInt): + # According to http://wiki.vg/Protocol#Data_types, + # MineCraftian VarInts can be at most 10 bytes. + SIZE_TABLE = VarInt.SIZE_TABLE + SIZE_TABLE.update( + { + 2**42: 6, + 2**49: 7, + 2**56: 8, + 2**63: 9, + 2**70: 10, + } + ) + + MAX_SIZE = list(SIZE_TABLE.items())[-1][-1] + + SIZE = (1, MAX_SIZE) + + +class String(Datatype): + FORMAT = "utf-8" + + @classmethod + def read(cls, fileobject): + str_size = VarInt.read(fileobject) + string = fileobject.read(str_size).decode(cls.FORMAT) + + return string + + @classmethod + def deserialize(cls, data): + data_fileobject = BytesIO(bytes(data)) + return cls.read(data_fileobject) + + @classmethod + def serialize(cls, data): + data = data.encode(cls.FORMAT) + len_data = VarInt.serialize(len(data)) + + return len_data + data diff --git a/requirements.txt b/requirements.txt index d75395b4..470aeda6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ cryptography requests +six diff --git a/start.py b/start.py index 4cd6c1b0..4819a6af 100644 --- a/start.py +++ b/start.py @@ -6,7 +6,7 @@ from minecraft.exceptions import YggdrasilError from minecraft.networking.connection import Connection from minecraft.networking.packets import ChatMessagePacket, ChatPacket -from minecraft.compat import input +from six.moves import input def get_options(): diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 00000000..fc45894d --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,11 @@ +from minecraft import compat # noqa unused-import + +import unittest # noqa unused-import + + +class TestCompatLong(unittest.TestCase): + def test_import_long(self): + from minecraft.compat import long # noqa unused-import + + def test_long(self): + compat.long(45) diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py new file mode 100644 index 00000000..4a1f4909 --- /dev/null +++ b/tests/test_datatypes.py @@ -0,0 +1,331 @@ +from minecraft.networking.datatypes import * # noqa undefined-names + +import unittest + + +class BaseDatatypeTester(unittest.TestCase): + DATATYPE_CLS = Datatype # We use Datatype as a an example here. + + # VALID_VALUES should have the following format: + # [(DESERIALIZED_VALUE, SERIALIZED_VALUE), ...] + # + # So that DESERIALIZED_VALUE is SERIALIZED_VALUE when serialized + # and vice versa. + + VALID_VALUES = [] + + # INVALID_SERIALIZATION_VALUES should be a list of tuples + # containing the value and the expected exception. + INVALID_SERIALIZATION_VALUES = [] + + # INVALID_DESERIALIZATION_VALUES should be a list of tuples + # containing the value and the expected exception. + INVALID_DESERIALIZATION_VALUES = [] + + def dynamic_assert_equal(self, first, second): + """ + Overriden by floating point datatypes in order to handle + the floating point issue. + """ + return self.assertEqual(first, second) + + def test_init(self): + d = self.DATATYPE_CLS() # noqa + + def test_init_with_arg(self): + # We shouldn't accept any parameters. + with self.assertRaises(TypeError): + d = self.DATATYPE_CLS("This is a positional argument...") # noqa + + def test_valid_data_serialization_values(self): + for deserialized_val, serialized_val in self.VALID_VALUES: + self.dynamic_assert_equal( + self.DATATYPE_CLS.serialize(deserialized_val), + serialized_val) + + def test_valid_data_deserialization_values(self): + for deserialized_val, serialized_val in self.VALID_VALUES: + self.dynamic_assert_equal( + self.DATATYPE_CLS.deserialize(serialized_val), + deserialized_val) + + def test_invalid_data_serialization_values(self): + for value, exception in self.INVALID_SERIALIZATION_VALUES: + with self.assertRaises(exception): + self.DATATYPE_CLS.serialize(value) + + def test_invalid_data_deserialization_values(self): + for value, exception in self.INVALID_DESERIALIZATION_VALUES: + with self.assertRaises(exception): + self.DATATYPE_CLS.deserialize(value) + + +class BaseNumberDatatypeTester(BaseDatatypeTester): + BASE_NUMBER_INVALID_SERIALIZATION_VALUES = [ + ("", TypeError), + ("Test", TypeError), + (b"\x00", TypeError), + (b"\x80", TypeError), + (True, TypeError), + (False, TypeError) + ] + + def base_number_invalid_data_serialization_values(self): + values_to_test = BASE_INVALID_SERIALIZATION_VALUES + + if (cls.MIN_NUMBER_VALUE is not None + and cls.MAX_NUMBER_VALUE is not None): + + values_to_test.extend([ + (self.DATATYPE_CLS.MIN_NUMBER_VALUE - 1, ValueError), + (self.DATATYPE_CLS.MAX_NUMBER_VALUE + 1, ValueError) + ]) + + for value, exception in values_to_test: + with self.assertRaises(exception): + self.DATATYPE_CLS.serialize(value) + + +class BaseStringDatatypeTester(BaseDatatypeTester): + pass + + +BASE_INVALID_DESERIALIZATION_VALUES = [ + (-1, TypeError), + (0, TypeError), + (1, TypeError), + ("", ValueError), + (True, TypeError), + (False, TypeError) +] + + +class DatatypeTest(BaseDatatypeTester): + DATATYPE_CLS = Datatype + + +class NumberDatatypeTest(BaseNumberDatatypeTester): + DATATYPE_CLS = NumberDatatype + + +class StringDatatypeTest(BaseStringDatatypeTester): + DATATYPE_CLS = StringDatatype + + +class BooleanTest(BaseDatatypeTester): + DATATYPE_CLS = Boolean + + VALID_VALUES = [ + (True, b"\x01"), + (False, b"\x00") + ] + + INVALID_SERIALIZATION_VALUES = [ + ("\x00", TypeError), + ("\x01", TypeError), + ("\x02", TypeError), + (-1, TypeError), + (0, TypeError), + (1, TypeError), + ("", TypeError), + ("Test", TypeError) + ] + + # Use list(BASE_INVALID_DESERIALIZATION_VALUES) instead of + # just = BASE_INVALID_DESERIALIZATION_VALUES, cause we want a COPY + # of the list, NOT a reference (that we'll later extend!) + INVALID_DESERIALIZATION_VALUES = list(BASE_INVALID_DESERIALIZATION_VALUES) + INVALID_DESERIALIZATION_VALUES.extend([ + (b"\x00\x01", ValueError) + ]) + + +class ByteTest(BaseNumberDatatypeTester): + DATATYPE_CLS = Byte + + VALID_VALUES = [ + (-128, b"\x80"), + (-22, b"\xea"), + (0, b"\x00"), + (22, b"\x16"), + (127, b"\x7f") + ] + + INVALID_DESERIALIZATION_VALUES = list(BASE_INVALID_DESERIALIZATION_VALUES) + INVALID_DESERIALIZATION_VALUES.extend([ + (b"\x01\x20", ValueError), + ]) + + +class UnsignedByteTest(BaseNumberDatatypeTester): + DATATYPE_CLS = UnsignedByte + + VALID_VALUES = [ + (0, b"\x00"), + (127, b"\x7f"), + (255, b"\xff") + ] + + INVALID_DESERIALIZATION_VALUES = ByteTest.INVALID_DESERIALIZATION_VALUES + + +class ShortTest(BaseNumberDatatypeTester): + DATATYPE_CLS = Short + + VALID_VALUES = [ + (-32768, b"\x80\x00"), + (-10000, b"\xd8\xf0"), + (0, b"\x00\x00"), + (5000, b"\x13\x88"), + (32767, b"\x7f\xff") + ] + + INVALID_DESERIALIZATION_VALUES = list(BASE_INVALID_DESERIALIZATION_VALUES) + INVALID_DESERIALIZATION_VALUES.extend([ + (b"\xff", ValueError), + (b"\xff\x01\x6e", ValueError) + ]) + + +class UnsignedShortTest(BaseNumberDatatypeTester): + DATATYPE_CLS = UnsignedShort + + VALID_VALUES = [ + (0, b"\x00\x00"), + (10000, b"'\x10"), + (32767, b"\x7f\xff"), + (65535, b"\xff\xff") + ] + + INVALID_DESERIALIZATION_VALUES = ShortTest.INVALID_DESERIALIZATION_VALUES + + +class IntegerTest(BaseNumberDatatypeTester): + DATATYPE_CLS = Integer + + VALID_VALUES = [ + (-2147483648, b"\x80\x00\x00\x00"), + (-1000000, b"\xff\xf0\xbd\xc0"), + (0, b"\x00\x00\x00\x00"), + (10000000, b"\x00\x98\x96\x80"), + (2147483647, b"\x7f\xff\xff\xff") + ] + + INVALID_DESERIALIZATION_VALUES = list(BASE_INVALID_DESERIALIZATION_VALUES) + INVALID_DESERIALIZATION_VALUES.extend([ + (b"\xff", ValueError), + (b"\x00\x01", ValueError), + (b"\x76\x80\x80\x10\xff", ValueError) + ]) + + +class UnsignedIntegerTest(BaseNumberDatatypeTester): + DATATYPE_CLS = UnsignedInteger + + VALID_VALUES = [ + (0, b"\x00\x00\x00\x00"), + (10000000, b"\x00\x98\x96\x80"), + (2147483647, b"\x7f\xff\xff\xff"), + (4294967295, b"\xff\xff\xff\xff") + ] + + INVALID_DESERIALIZATION_VALUES = IntegerTest.INVALID_DESERIALIZATION_VALUES + + +class LongTest(IntegerTest): + DATATYPE_CLS = Long + + +class UnsignedLongTest(UnsignedInteger): + DATATYPE_CLS = UnsignedLong + + +class LongLongTest(BaseNumberDatatypeTester): + DATATYPE_CLS = LongLong + + VALID_VALUES = [ + (-9223372036854775808, b"\x80\x00\x00\x00\x00\x00\x00\x00"), + (-1000000, b"\xff\xff\xff\xff\xff\xf0\xbd\xc0"), + (0, b"\x00\x00\x00\x00\x00\x00\x00\x00"), + (10000000, b"\x00\x00\x00\x00\x00\x98\x96\x80"), + (9223372036854775807, b"\x7f\xff\xff\xff\xff\xff\xff\xff") + ] + + INVALID_DESERIALIZATION_VALUES = list(BASE_INVALID_DESERIALIZATION_VALUES) + INVALID_DESERIALIZATION_VALUES.extend([ + (b"\xff", ValueError), + (b"\x00\x01", ValueError), + (b"\x76\x80\x80\x10\xff", ValueError), + (b"\x55\x44\x33\x22\x11\x66\x77\x88\x99", ValueError) + ]) + + +class UnsignedLongLongTest(BaseNumberDatatypeTester): + DATATYPE_CLS = UnsignedLongLong + + VALID_VALUES = [ + (0, b"\x00\x00\x00\x00\x00\x00\x00\x00"), + (10000000, b"\x00\x00\x00\x00\x00\x98\x96\x80"), + (9223372036854775807, b"\x7f\xff\xff\xff\xff\xff\xff\xff"), + (18446744073709551615, b"\xff\xff\xff\xff\xff\xff\xff\xff") + ] + + INVALID_DESERIALIZATION_VALUES = \ + LongLongTest.INVALID_DESERIALIZATION_VALUES + + +class FloatTest(BaseNumberDatatypeTester): + DATATYPE_CLS = Float + + VALID_VALUES = [ + (-100.5467, b"\xc2\xc9\x17\xe9"), + (0.00000, b"\x00\x00\x00\x00"), + (5000.72, b"E\x9cE\xc3"), + (65.123565787856342347, b"B\x82?D"), + ] + + INVALID_DESERIALIZATION_VALUES = list(BASE_INVALID_DESERIALIZATION_VALUES) + INVALID_DESERIALIZATION_VALUES.extend([ + (b"\xff", ValueError), + (b"\x00\x01", ValueError), + (b"\x76\x80\x80\x10\xff", ValueError), + (b"\x55\x44\x33\x22\x11\x66\x77\x88\x99", ValueError) + ]) + + def dynamic_assert_equal(self, first, second): + return self.assertAlmostEqual(first, second, places=3) + + +class DoubleTest(FloatTest): + DATATYPE_CLS = Double + + VALID_VALUES = [ + (-10000560.86432, b"\xc1c\x13\x16\x1b\xa8\x82k"), + (-56.672345756870345754623, b"\xc0LV\x0fl\xfe\xaef"), + (0.00000, b"\x00\x00\x00\x00\x00\x00\x00\x00"), + (5000.72, b"@\xb3\x88\xb8Q\xeb\x85\x1f"), + (65.123565787856342347, b"@PG\xe8\x80zo\xd6"), + (5324342541.72123, b"A\xf3\xd5\xb0P\xdb\x8a(") + ] + + +class VarIntTest(BaseNumberDatatypeTester): + DATATYPE_CLS = VarInt + + INVALID_DESERIALIZATION_VALUES = BASE_INVALID_DESERIALIZATION_VALUES + +# def _bin(binstr): +# """ +# Accepts a pretty looking string of binary numbers and +# returns the binary number. + +# Parameters: +# binstr - a string with this format: `'1010 0010 0100'`. + +# Returns: +# Int +# """ +# binstr = binstr.replace(" ", "") # Remove all spaces. +# num = int("0b" + binstr, 2) + +# return num diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 4f5c989c..14eaa0aa 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,15 +1,30 @@ from minecraft.exceptions import YggdrasilError +from minecraft.exceptions import DeserializationError, SerializationError import unittest -class RaiseYggdrasilError(unittest.TestCase): - def test_raise_yggdrasil_error(self): - with self.assertRaises(YggdrasilError): - raise YggdrasilError +class BaseRaiseExceptionTest(unittest.TestCase): + EXCEPTION_TO_TEST = Exception - def test_raise_yggdrasil_error_message(self): - with self.assertRaises(YggdrasilError) as e: - raise YggdrasilError("Error!") + def test_raise_error(self): + with self.assertRaises(self.EXCEPTION_TO_TEST): + raise self.EXCEPTION_TO_TEST + + def test_raise_error_message(self): + with self.assertRaises(self.EXCEPTION_TO_TEST) as e: + raise self.EXCEPTION_TO_TEST("Error!") self.assertEqual(str(e.exception), "Error!") + + +class RaiseYggdrasilError(BaseRaiseExceptionTest): + EXCEPTION_TO_TEST = YggdrasilError + + +class RaiseDeserializationError(BaseRaiseExceptionTest): + EXCEPTION_TO_TEST = DeserializationError + + +class RaiseSerializationError(BaseRaiseExceptionTest): + EXCEPTION_TO_TEST = SerializationError diff --git a/tox.ini b/tox.ini index 4031a793..4d7fa84b 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ deps = nose requests cryptography + six [testenv:py27] deps =