diff --git a/debugger_protocol/messages/_base.py b/debugger_protocol/messages/_base.py deleted file mode 100644 index 7efc3e3b..00000000 --- a/debugger_protocol/messages/_base.py +++ /dev/null @@ -1,22 +0,0 @@ -from debugger_protocol._base import Readonly, WithRepr - - -class Base(Readonly, WithRepr): - """Base class for message-related types.""" - - _INIT_ARGS = None - - @classmethod - def from_data(cls, **kwargs): - """Return an instance based on the given raw data.""" - return cls(**kwargs) - - def __init__(self): - self._validate() - - def _validate(self): - pass - - def as_data(self): - """Return serializable data for the instance.""" - return {} diff --git a/debugger_protocol/messages/message.py b/debugger_protocol/messages/message.py new file mode 100644 index 00000000..3908aa40 --- /dev/null +++ b/debugger_protocol/messages/message.py @@ -0,0 +1,366 @@ +from debugger_protocol._base import Readonly, WithRepr +from debugger_protocol.arg import param_from_datatype +from . import MESSAGE_TYPES, Message + +""" +From the schema: + +MESSAGE = [ + name + base + description + props: [PROPERTY + (properties: [PROPERTY])] +] + +PROPERTY = [ + name + type: choices (one or a list) + (enum/_enum) + description + required: True/False (default: False) +] + +inheritance: override properties of base +""" + + +class ProtocolMessage(Readonly, WithRepr, Message): + """Base class of requests, responses, and events.""" + + _reqid = 0 + TYPE = None + + @classmethod + def from_data(cls, type, seq, **kwargs): + """Return an instance based on the given raw data.""" + return cls(type=type, seq=seq, **kwargs) + + @classmethod + def _next_reqid(cls): + reqid = ProtocolMessage._reqid + ProtocolMessage._reqid += 1 + return reqid + + _NOT_SET = object() + + def __init__(self, seq=_NOT_SET, **kwargs): + type = kwargs.pop('type', self.TYPE) + if seq is self._NOT_SET: + seq = self._next_reqid() + self._bind_attrs( + type=type or None, + seq=int(seq) if seq or seq == 0 else None, + ) + self._validate() + + def _validate(self): + if self.type is None: + raise TypeError('missing type') + elif self.TYPE is not None and self.type != self.TYPE: + raise ValueError('type must be {!r}'.format(self.TYPE)) + elif self.type not in MESSAGE_TYPES: + raise ValueError('unsupported type {!r}'.format(self.type)) + + if self.seq is None: + raise TypeError('missing seq') + elif self.seq < 0: + msg = '"seq" must be a non-negative int, got {!r}' + raise ValueError(msg.format(self.seq)) + + def _init_args(self): + if self.TYPE is None: + yield ('type', self.type) + yield ('seq', self.seq) + + def as_data(self): + """Return serializable data for the instance.""" + data = { + 'type': self.type, + 'seq': self.seq, + } + return data + + +################################## + +class Request(ProtocolMessage): + """A client or server-initiated request.""" + + TYPE = 'request' + TYPE_KEY = 'command' + + COMMAND = None + ARGUMENTS = None + ARGUMENTS_REQUIRED = None + + @classmethod + def from_data(cls, type, seq, command, arguments=None): + """Return an instance based on the given raw data.""" + return super(Request, cls).from_data( + type, seq, + command=command, + arguments=arguments, + ) + + @classmethod + def _arguments_required(cls): + if cls.ARGUMENTS_REQUIRED is None: + return cls.ARGUMENTS is not None + return cls.ARGUMENTS_REQUIRED + + def __init__(self, arguments=None, **kwargs): + command = kwargs.pop('command', self.COMMAND) + args = None + if arguments is not None: + try: + arguments = dict(arguments) + except TypeError: + pass + if self.ARGUMENTS is not None: + param = param_from_datatype(self.ARGUMENTS) + args = param.bind(arguments) + if args is None: + raise TypeError('bad arguments {!r}'.format(arguments)) + arguments = args.coerce() + self._bind_attrs( + command=command or None, + arguments=arguments or None, + _args=args, + ) + super(Request, self).__init__(**kwargs) + + def _validate(self): + super(Request, self)._validate() + + if self.command is None: + raise TypeError('missing command') + elif self.COMMAND is not None and self.command != self.COMMAND: + raise ValueError('command must be {!r}'.format(self.COMMAND)) + + if self.arguments is None: + if self._arguments_required(): + raise TypeError('missing arguments') + else: + if self.ARGUMENTS is None: + raise TypeError('got unexpected arguments') + self._args.validate() + + def _init_args(self): + if self.COMMAND is None: + yield ('command', self.command) + if self.arguments is not None: + yield ('arguments', self.arguments) + yield ('seq', self.seq) + + def as_data(self): + """Return serializable data for the instance.""" + data = super(Request, self).as_data() + data.update({ + 'command': self.command, + }) + if self.arguments is not None: + data.update({ + 'arguments': self.arguments.as_data(), + }) + return data + + +class Response(ProtocolMessage): + """Response to a request.""" + + TYPE = 'response' + TYPE_KEY = 'command' + + COMMAND = None + BODY = None + ERROR_BODY = None + BODY_REQUIRED = None + ERROR_BODY_REQUIRED = None + + @classmethod + def from_data(cls, type, seq, request_seq, command, success, + body=None, message=None): + """Return an instance based on the given raw data.""" + return super(Response, cls).from_data( + type, seq, + request_seq=request_seq, + command=command, + success=success, + body=body, + message=message, + ) + + @classmethod + def _body_required(cls, success=True): + required = cls.BODY_REQUIRED if success else cls.ERROR_BODY_REQUIRED + if required is not None: + return required + bodyclass = cls.BODY if success else cls.ERROR_BODY + return bodyclass is not None + + def __init__(self, request_seq, body=None, message=None, success=True, + **kwargs): + command = kwargs.pop('command', self.COMMAND) + reqseq = request_seq + bodyarg = None + if body is not None: + try: + body = dict(body) + except TypeError: + pass + bodyclass = self.BODY if success else self.ERROR_BODY + if bodyclass is not None: + param = param_from_datatype(bodyclass) + bodyarg = param.bind(body) + if bodyarg is None: + raise TypeError('bad body type {!r}'.format(body)) + body = bodyarg.coerce() + self._bind_attrs( + command=command or None, + request_seq=int(reqseq) if reqseq or reqseq == 0 else None, + body=body or None, + _bodyarg=bodyarg, + message=message or None, + success=bool(success), + ) + super(Response, self).__init__(**kwargs) + + def _validate(self): + super(Response, self)._validate() + + if self.request_seq is None: + raise TypeError('missing request_seq') + elif self.request_seq < 0: + msg = 'request_seq must be a non-negative int, got {!r}' + raise ValueError(msg.format(self.request_seq)) + + if not self.command: + raise TypeError('missing command') + elif self.COMMAND is not None and self.command != self.COMMAND: + raise ValueError('command must be {!r}'.format(self.COMMAND)) + + if self.body is None: + if self._body_required(self.success): + raise TypeError('missing body') + elif self._bodyarg is None: + raise ValueError('got unexpected body') + else: + self._bodyarg.validate() + + if not self.success and not self.message: + raise TypeError('missing message') + + def _init_args(self): + if self.COMMAND is None: + yield ('command', self.command) + yield ('request_seq', self.request_seq) + yield ('success', self.success) + if not self.success: + yield ('message', self.message) + if self.body is not None: + yield ('body', self.body) + yield ('seq', self.seq) + + def as_data(self): + """Return serializable data for the instance.""" + data = super(Response, self).as_data() + data.update({ + 'request_seq': self.request_seq, + 'command': self.command, + 'success': self.success, + }) + if self.body is not None: + data.update({ + 'body': self.body.as_data(), + }) + if self.message is not None: + data.update({ + 'message': self.message, + }) + return data + + +################################## + +class Event(ProtocolMessage): + """Server-initiated event.""" + + TYPE = 'event' + TYPE_KEY = 'event' + + EVENT = None + BODY = None + BODY_REQUIRED = None + + @classmethod + def from_data(cls, type, seq, event, body=None): + """Return an instance based on the given raw data.""" + return super(Event, cls).from_data(type, seq, event=event, body=body) + + @classmethod + def _body_required(cls): + if cls.BODY_REQUIRED is None: + return cls.BODY is not None + return cls.BODY_REQUIRED + + def __init__(self, body=None, **kwargs): + event = kwargs.pop('event', self.EVENT) + bodyarg = None + if body is not None: + try: + body = dict(body) + except TypeError: + pass + if self.BODY is not None: + param = param_from_datatype(self.BODY) + bodyarg = param.bind(body) + if bodyarg is None: + raise TypeError('bad body type {!r}'.format(body)) + body = bodyarg.coerce() + + self._bind_attrs( + event=event or None, + body=body or None, + _bodyarg=bodyarg, + ) + super(Event, self).__init__(**kwargs) + + def _validate(self): + super(Event, self)._validate() + + if self.event is None: + raise TypeError('missing event') + if self.EVENT is not None and self.event != self.EVENT: + msg = 'event must be {!r}, got {!r}' + raise ValueError(msg.format(self.EVENT, self.event)) + + if self.body is None: + if self._body_required(): + raise TypeError('missing body') + elif self._bodyarg is None: + raise ValueError('got unexpected body') + else: + self._bodyarg.validate() + + def _init_args(self): + if self.EVENT is None: + yield ('event', self.event) + if self.body is not None: + yield ('body', self.body) + yield ('seq', self.seq) + + @property + def name(self): + return self.event + + def as_data(self): + """Return serializable data for the instance.""" + data = super(Event, self).as_data() + data.update({ + 'event': self.event, + }) + if self.body is not None: + data.update({ + 'body': self.body.as_data(), + }) + return data diff --git a/tests/debugger_protocol/messages/__init__.py b/tests/debugger_protocol/messages/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/debugger_protocol/messages/test_message.py b/tests/debugger_protocol/messages/test_message.py new file mode 100644 index 00000000..0171cde0 --- /dev/null +++ b/tests/debugger_protocol/messages/test_message.py @@ -0,0 +1,936 @@ +import unittest + +from debugger_protocol.arg import FieldsNamespace, Field +from debugger_protocol.messages import register +from debugger_protocol.messages.message import ( + ProtocolMessage, Request, Response, Event) + + +@register +class DummyRequest(object): + TYPE = 'request' + TYPE_KEY = 'command' + COMMAND = '...' + + +@register +class DummyResponse(object): + TYPE = 'response' + TYPE_KEY = 'command' + COMMAND = '...' + + +@register +class DummyEvent(object): + TYPE = 'event' + TYPE_KEY = 'event' + EVENT = '...' + + +class FakeMsg(ProtocolMessage): + + SEQ = 0 + + @classmethod + def _next_reqid(cls): + return cls.SEQ + + +class ProtocolMessageTests(unittest.TestCase): + + def test_from_data(self): + data = { + 'type': 'event', + 'seq': 10, + } + msg = ProtocolMessage.from_data(**data) + + self.assertEqual(msg.type, 'event') + self.assertEqual(msg.seq, 10) + + def test_defaults(self): # no args + class Spam(FakeMsg): + SEQ = 10 + TYPE = 'event' + + msg = Spam() + + self.assertEqual(msg.type, 'event') + self.assertEqual(msg.seq, 10) + + def test_all_args(self): + msg = ProtocolMessage(10, type='event') + + self.assertEqual(msg.type, 'event') + self.assertEqual(msg.seq, 10) + + def test_coercion_seq(self): + msg = ProtocolMessage('10', type='event') + + self.assertEqual(msg.seq, 10) + + def test_validation(self): + # type + + with self.assertRaises(TypeError): + ProtocolMessage(type=None) + with self.assertRaises(ValueError): + ProtocolMessage(type='spam') + + class Other(ProtocolMessage): + TYPE = 'spam' + + with self.assertRaises(ValueError): + Other(type='event') + + # seq + + with self.assertRaises(TypeError): + ProtocolMessage(None, type='event') + with self.assertRaises(ValueError): + ProtocolMessage(-1, type='event') + + def test_readonly(self): + msg = ProtocolMessage(10, type='event') + + with self.assertRaises(AttributeError): + msg.seq = 11 + with self.assertRaises(AttributeError): + msg.type = 'event' + with self.assertRaises(AttributeError): + msg.spam = object() + with self.assertRaises(AttributeError): + del msg.seq + + def test_repr(self): + msg = ProtocolMessage(10, type='event') + result = repr(msg) + + self.assertEqual(result, "ProtocolMessage(type='event', seq=10)") + + def test_repr_subclass(self): + class Eventish(ProtocolMessage): + TYPE = 'event' + + msg = Eventish(10) + result = repr(msg) + + self.assertEqual(result, 'Eventish(seq=10)') + + def test_as_data(self): + msg = ProtocolMessage(10, type='event') + data = msg.as_data() + + self.assertEqual(data, { + 'type': 'event', + 'seq': 10, + }) + + +class RequestTests(unittest.TestCase): + + def test_from_data_without_arguments(self): + data = { + 'type': 'request', + 'seq': 10, + 'command': 'spam', + } + msg = Request.from_data(**data) + + self.assertEqual(msg.type, 'request') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.command, 'spam') + self.assertIsNone(msg.arguments) + + def test_from_data_with_arguments(self): + class Spam(Request): + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('a'), + ] + + data = { + 'type': 'request', + 'seq': 10, + 'command': 'spam', + 'arguments': {'a': 'b'}, + } + #msg = Request.from_data(**data) + msg = Spam.from_data(**data) + + self.assertEqual(msg.type, 'request') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.command, 'spam') + self.assertEqual(msg.arguments, {'a': 'b'}) + + def test_defaults(self): + class Spam(Request, FakeMsg): + SEQ = 10 + COMMAND = 'spam' + + msg = Spam() + + self.assertEqual(msg.type, 'request') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.command, 'spam') + self.assertIsNone(msg.arguments) + + def test_all_args(self): + class Spam(Request): + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('a'), + ] + + args = {'a': 'b'} + msg = Spam(arguments=args, command='spam', seq=10) + + self.assertEqual(msg.type, 'request') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.command, 'spam') + self.assertEqual(msg.arguments, args) + + def test_no_arguments_not_required(self): + class Spam(Request): + COMMAND = 'spam' + ARGUMENTS = True + ARGUMENTS_REQUIRED = False + + msg = Spam() + + self.assertIsNone(msg.arguments) + + def test_no_args(self): + with self.assertRaises(TypeError): + Request() + + def test_coercion_arguments(self): + class Spam(Request): + COMMAND = 'spam' + class ARGUMENTS(FieldsNamespace): # noqa + FIELDS = [ + Field('a'), + ] + + args = [('a', 'b')] + msg = Spam(args) + + self.assertEqual(msg.arguments, {'a': 'b'}) + + with self.assertRaises(TypeError): + Spam(command='spam', arguments=11) + + def test_validation(self): + with self.assertRaises(TypeError): + Request() + + # command + + class Other1(Request): + COMMAND = 'eggs' + + with self.assertRaises(ValueError): + # command doesn't match + Other1(arguments=10, command='spam') + + # arguments + + with self.assertRaises(TypeError): + # unexpected arguments + Request(arguments=10, command='spam') + + class Other2(Request): + COMMAND = 'spam' + ARGUMENTS = int + + with self.assertRaises(ValueError): + # missing arguments (implicitly required) + Other2(command='eggs') + + class Other3(Request): + COMMAND = 'eggs' + ARGUMENTS = int + ARGUMENTS_REQUIRED = True + + with self.assertRaises(ValueError): + # missing arguments (explicitly required) + Other2(command='eggs') + + def test_repr_minimal(self): + msg = Request(command='spam', seq=10) + result = repr(msg) + + self.assertEqual(result, "Request(command='spam', seq=10)") + + def test_repr_full(self): + msg = Request(command='spam', seq=10) + result = repr(msg) + + self.assertEqual(result, "Request(command='spam', seq=10)") + + def test_repr_subclass_minimal(self): + class SpamRequest(Request): + COMMAND = 'spam' + + msg = SpamRequest(seq=10) + result = repr(msg) + + self.assertEqual(result, "SpamRequest(seq=10)") + + def test_repr_subclass_full(self): + class SpamRequest(Request): + COMMAND = 'spam' + class ARGUMENTS(FieldsNamespace): # noqa + FIELDS = [ + Field('a'), + ] + + msg = SpamRequest(arguments={'a': 'b'}, seq=10) + result = repr(msg) + + self.assertEqual(result, + "SpamRequest(arguments=ARGUMENTS(a='b'), seq=10)") + + def test_as_data_minimal(self): + msg = Request(command='spam', seq=10) + data = msg.as_data() + + self.assertEqual(data, { + 'type': 'request', + 'seq': 10, + 'command': 'spam', + }) + + def test_as_data_full(self): + class Spam(Request): + COMMAND = 'spam' + class ARGUMENTS(FieldsNamespace): # noqa + FIELDS = [ + Field('a'), + ] + + msg = Spam(arguments={'a': 'b'}, seq=10) + data = msg.as_data() + + self.assertEqual(data, { + 'type': 'request', + 'seq': 10, + 'command': 'spam', + 'arguments': {'a': 'b'}, + }) + + +class ResponseTests(unittest.TestCase): + + def test_from_data_without_body(self): + data = { + 'type': 'response', + 'seq': 10, + 'command': 'spam', + 'request_seq': 9, + 'success': True, + } + msg = Response.from_data(**data) + + self.assertEqual(msg.type, 'response') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.command, 'spam') + self.assertEqual(msg.request_seq, 9) + self.assertTrue(msg.success) + self.assertIsNone(msg.body) + self.assertIsNone(msg.message) + + def test_from_data_with_body(self): + class Spam(Response): + class BODY(FieldsNamespace): + FIELDS = [ + Field('a'), + ] + + data = { + 'type': 'response', + 'seq': 10, + 'command': 'spam', + 'request_seq': 9, + 'success': True, + 'body': {'a': 'b'}, + } + msg = Spam.from_data(**data) + + self.assertEqual(msg.type, 'response') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.command, 'spam') + self.assertEqual(msg.request_seq, 9) + self.assertTrue(msg.success) + self.assertEqual(msg.body, {'a': 'b'}) + self.assertIsNone(msg.message) + + def test_from_data_error_without_body(self): + data = { + 'type': 'response', + 'seq': 10, + 'command': 'spam', + 'request_seq': 9, + 'success': False, + 'message': 'oops!', + } + msg = Response.from_data(**data) + + self.assertEqual(msg.type, 'response') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.command, 'spam') + self.assertEqual(msg.request_seq, 9) + self.assertFalse(msg.success) + self.assertIsNone(msg.body) + self.assertEqual(msg.message, 'oops!') + + def test_from_data_error_with_body(self): + class Spam(Response): + class ERROR_BODY(FieldsNamespace): + FIELDS = [ + Field('a'), + ] + + data = { + 'type': 'response', + 'seq': 10, + 'command': 'spam', + 'request_seq': 9, + 'success': False, + 'message': 'oops!', + 'body': {'a': 'b'}, + } + msg = Spam.from_data(**data) + + self.assertEqual(msg.type, 'response') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.command, 'spam') + self.assertEqual(msg.request_seq, 9) + self.assertFalse(msg.success) + self.assertEqual(msg.body, {'a': 'b'}) + self.assertEqual(msg.message, 'oops!') + + def test_defaults(self): + class Spam(Response, FakeMsg): + SEQ = 10 + COMMAND = 'spam' + + msg = Spam('9') + + self.assertEqual(msg.type, 'response') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.request_seq, 9) + self.assertEqual(msg.command, 'spam') + self.assertTrue(msg.success) + self.assertIsNone(msg.body) + self.assertIsNone(msg.message) + + def test_all_args_not_error(self): + class Spam(Response): + class BODY(FieldsNamespace): + FIELDS = [ + Field('a'), + ] + + msg = Spam('9', command='spam', success=True, body={'a': 'b'}, + seq=10, type='response') + + self.assertEqual(msg.type, 'response') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.request_seq, 9) + self.assertEqual(msg.command, 'spam') + self.assertTrue(msg.success) + self.assertEqual(msg.body, {'a': 'b'}) + self.assertIsNone(msg.message) + + def test_all_args_error(self): + class Spam(Response): + COMMAND = 'spam' + class ERROR_BODY(FieldsNamespace): # noqa + FIELDS = [ + Field('a'), + ] + + msg = Spam('9', success=False, message='oops!', body={'a': 'b'}, + seq=10, type='response') + + self.assertEqual(msg.type, 'response') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.command, 'spam') + self.assertEqual(msg.request_seq, 9) + self.assertFalse(msg.success) + self.assertEqual(msg.body, Spam.ERROR_BODY(a='b')) + self.assertEqual(msg.message, 'oops!') + + def test_no_body_not_required(self): + class Spam(Response): + COMMAND = 'spam' + BODY = True + BODY_REQUIRED = False + + msg = Spam('9') + + self.assertIsNone(msg.body) + + def test_no_error_body_not_required(self): + class Spam(Response): + COMMAND = 'spam' + ERROR_BODY = True + ERROR_BODY_REQUIRED = False + + msg = Spam('9', success=False, message='oops!') + + self.assertIsNone(msg.body) + + def test_no_args(self): + with self.assertRaises(TypeError): + Response() + + def test_coercion_request_seq(self): + msg = Response('9', command='spam') + + self.assertEqual(msg.request_seq, 9) + + def test_coercion_success(self): + msg1 = Response(9, success=1, command='spam') + msg2 = Response(9, success=None, command='spam', message='oops!') + + self.assertIs(msg1.success, True) + self.assertIs(msg2.success, False) + + def test_coercion_body(self): + class Spam(Response): + COMMAND = 'spam' + class BODY(FieldsNamespace): # noqa + FIELDS = [ + Field('a'), + ] + + body = [('a', 'b')] + msg = Spam(9, body=body) + + self.assertEqual(msg.body, {'a': 'b'}) + + with self.assertRaises(TypeError): + Spam(9, command='spam', body=11) + + def test_coercion_error_body(self): + class Spam(Response): + COMMAND = 'spam' + class ERROR_BODY(FieldsNamespace): # noqa + FIELDS = [ + Field('a'), + ] + + body = [('a', 'b')] + msg = Spam(9, body=body, success=False, message='oops!') + + self.assertEqual(msg.body, {'a': 'b'}) + + with self.assertRaises(TypeError): + Spam(9, command='spam', success=False, message='oops!', body=11) + + def test_validation(self): + # request_seq + + with self.assertRaises(TypeError): + # missing + Response(None, command='spam') + with self.assertRaises(TypeError): + # missing + Response('', command='spam') + with self.assertRaises(TypeError): + # couldn't convert to int + Response(object(), command='spam') + with self.assertRaises(ValueError): + # not non-negative + Response(-1, command='spam') + + # command + + with self.assertRaises(TypeError): + # missing + Response(9, command=None) + with self.assertRaises(TypeError): + # missing + Response(9, command='') + + class Other1(Response): + COMMAND = 'eggs' + + with self.assertRaises(ValueError): + # does not match + Other1(9, command='spam') + + # body + + class Other2(Response): + class BODY(FieldsNamespace): + FIELDS = [ + Field('a'), + ] + + ERROR_BODY = BODY + + with self.assertRaises(ValueError): + # unexpected + Response(9, command='spam', body=11) + with self.assertRaises(TypeError): + # missing (implicitly required) + Other2(9, command='spam') + with self.assertRaises(TypeError): + # missing (explicitly required) + Other2.BODY_REQUIRED = True + Other2(9, command='spam') + with self.assertRaises(ValueError): + # unexpected (error) + Response(9, command='spam', body=11, success=False, message=':(') + with self.assertRaises(TypeError): + # missing (error) (implicitly required) + Other2(9, command='spam', success=False, message=':(') + with self.assertRaises(TypeError): + # missing (error) (explicitly required) + Other2.ERROR_BODY_REQUIRED = True + Other2(9, command='spam', success=False, message=':(') + + # message + + with self.assertRaises(TypeError): + # missing + Response(9, command='spam', success=False) + + def test_repr_minimal(self): + msg = Response(9, command='spam', seq=10) + result = repr(msg) + + self.assertEqual(result, + "Response(command='spam', request_seq=9, success=True, seq=10)") # noqa + + def test_repr_full(self): + msg = Response(9, command='spam', seq=10) + result = repr(msg) + + self.assertEqual(result, + "Response(command='spam', request_seq=9, success=True, seq=10)") # noqa + + def test_repr_error_minimal(self): + msg = Response(9, command='spam', success=False, message='oops!', + seq=10) + result = repr(msg) + + self.assertEqual(result, + "Response(command='spam', request_seq=9, success=False, message='oops!', seq=10)") # noqa + + def test_repr_error_full(self): + msg = Response(9, command='spam', success=False, message='oops!', + seq=10) + result = repr(msg) + + self.assertEqual(result, + "Response(command='spam', request_seq=9, success=False, message='oops!', seq=10)") # noqa + + def test_repr_subclass_minimal(self): + class SpamResponse(Response): + COMMAND = 'spam' + + msg = SpamResponse(9, seq=10) + result = repr(msg) + + self.assertEqual(result, + "SpamResponse(request_seq=9, success=True, seq=10)") + + def test_repr_subclass_full(self): + class SpamResponse(Response): + COMMAND = 'spam' + class BODY(FieldsNamespace): # noqa + FIELDS = [ + Field('a'), + ] + + msg = SpamResponse(9, body={'a': 'b'}, seq=10) + result = repr(msg) + + self.assertEqual(result, + "SpamResponse(request_seq=9, success=True, body=BODY(a='b'), seq=10)") # noqa + + def test_repr_subclass_error_minimal(self): + class SpamResponse(Response): + COMMAND = 'spam' + + msg = SpamResponse(9, success=False, message='oops!', seq=10) + result = repr(msg) + + self.assertEqual(result, + "SpamResponse(request_seq=9, success=False, message='oops!', seq=10)") # noqa + + def test_repr_subclass_error_full(self): + class SpamResponse(Response): + COMMAND = 'spam' + class ERROR_BODY(FieldsNamespace): # noqa + FIELDS = [ + Field('a'), + ] + + msg = SpamResponse(9, success=False, message='oops!', body={'a': 'b'}, + seq=10) + result = repr(msg) + + self.assertEqual(result, + "SpamResponse(request_seq=9, success=False, message='oops!', body=ERROR_BODY(a='b'), seq=10)") # noqa + + def test_as_data_minimal(self): + msg = Response(9, command='spam', seq=10) + data = msg.as_data() + + self.assertEqual(data, { + 'type': 'response', + 'seq': 10, + 'request_seq': 9, + 'command': 'spam', + 'success': True, + }) + + def test_as_data_full(self): + class Spam(Response): + COMMAND = 'spam' + class BODY(FieldsNamespace): # noqa + FIELDS = [ + Field('a'), + ] + + msg = Spam(9, body={'a': 'b'}, seq=10) + data = msg.as_data() + + self.assertEqual(data, { + 'type': 'response', + 'seq': 10, + 'request_seq': 9, + 'command': 'spam', + 'success': True, + 'body': {'a': 'b'}, + }) + + def test_as_data_error_minimal(self): + msg = Response(9, command='spam', success=False, message='oops!', + seq=10) + data = msg.as_data() + + self.assertEqual(data, { + 'type': 'response', + 'seq': 10, + 'request_seq': 9, + 'command': 'spam', + 'success': False, + 'message': 'oops!', + }) + + def test_as_data_error_full(self): + class Spam(Response): + COMMAND = 'spam' + class ERROR_BODY(FieldsNamespace): # noqa + FIELDS = [ + Field('a'), + ] + + msg = Spam(9, success=False, body={'a': 'b'}, message='oops!', seq=10) + data = msg.as_data() + + self.assertEqual(data, { + 'type': 'response', + 'seq': 10, + 'request_seq': 9, + 'command': 'spam', + 'success': False, + 'message': 'oops!', + 'body': {'a': 'b'}, + }) + + +class EventTests(unittest.TestCase): + + def test_from_data_without_body(self): + data = { + 'type': 'event', + 'seq': 10, + 'event': 'spam', + } + msg = Event.from_data(**data) + + self.assertEqual(msg.type, 'event') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.event, 'spam') + self.assertIsNone(msg.body) + + def test_from_data_with_body(self): + class Spam(Event): + class BODY(FieldsNamespace): + FIELDS = [ + Field('a'), + ] + + data = { + 'type': 'event', + 'seq': 10, + 'event': 'spam', + 'body': {'a': 'b'}, + } + msg = Spam.from_data(**data) + + self.assertEqual(msg.type, 'event') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.event, 'spam') + self.assertEqual(msg.body, {'a': 'b'}) + + def test_defaults(self): # no args + class Spam(Event, FakeMsg): + SEQ = 10 + EVENT = 'spam' + + msg = Spam() + + self.assertEqual(msg.type, 'event') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.event, 'spam') + self.assertIsNone(msg.body) + + def test_all_args(self): + class Spam(Event): + class BODY(FieldsNamespace): + FIELDS = [ + Field('a'), + ] + + msg = Spam(event='spam', body={'a': 'b'}, seq=10, type='event') + + self.assertEqual(msg.type, 'event') + self.assertEqual(msg.seq, 10) + self.assertEqual(msg.event, 'spam') + self.assertEqual(msg.body, {'a': 'b'}) + + def test_no_body_not_required(self): + class Spam(Event): + EVENT = 'spam' + BODY = True + BODY_REQUIRED = False + + msg = Spam() + + self.assertIsNone(msg.body) + + def test_no_args(self): + with self.assertRaises(TypeError): + Event() + + def test_coercion_body(self): + class Spam(Event): + EVENT = 'spam' + class BODY(FieldsNamespace): # noqa + FIELDS = [ + Field('a'), + ] + + body = [('a', 'b')] + msg = Spam(body=body) + + self.assertEqual(msg.body, {'a': 'b'}) + + with self.assertRaises(TypeError): + Spam(event='spam', body=11) + + def test_validation(self): + # event + + with self.assertRaises(TypeError): + # missing + Event(event=None) + with self.assertRaises(TypeError): + # missing + Event(event='') + + class Other1(Event): + EVENT = 'eggs' + + with self.assertRaises(ValueError): + # does not match + Other1(event='spam') + + # body + + class Other2(Event): + class BODY(FieldsNamespace): + FIELDS = [ + Field('a'), + ] + + with self.assertRaises(ValueError): + # unexpected + Event(event='spam', body=11) + with self.assertRaises(TypeError): + # missing (implicitly required) + Other2(9, command='spam') + with self.assertRaises(TypeError): + # missing (explicitly required) + Other2.BODY_REQUIRED = True + Other2(9, command='spam') + + def test_repr_minimal(self): + msg = Event(event='spam', seq=10) + result = repr(msg) + + self.assertEqual(result, "Event(event='spam', seq=10)") + + def test_repr_full(self): + msg = Event(event='spam', seq=10) + result = repr(msg) + + self.assertEqual(result, "Event(event='spam', seq=10)") + + def test_repr_subclass_minimal(self): + class SpamEvent(Event): + EVENT = 'spam' + + msg = SpamEvent(seq=10) + result = repr(msg) + + self.assertEqual(result, 'SpamEvent(seq=10)') + + def test_repr_subclass_full(self): + class SpamEvent(Event): + EVENT = 'spam' + class BODY(FieldsNamespace): # noqa + FIELDS = [ + Field('a'), + ] + + msg = SpamEvent(body={'a': 'b'}, seq=10) + result = repr(msg) + + self.assertEqual(result, "SpamEvent(body=BODY(a='b'), seq=10)") + + def test_as_data_minimal(self): + msg = Event(event='spam', seq=10) + data = msg.as_data() + + self.assertEqual(data, { + 'type': 'event', + 'seq': 10, + 'event': 'spam', + }) + + def test_as_data_full(self): + class Spam(Event): + EVENT = 'spam' + class BODY(FieldsNamespace): # noqa + FIELDS = [ + Field('a'), + ] + + msg = Spam(body={'a': 'b'}, seq=10) + data = msg.as_data() + + self.assertEqual(data, { + 'type': 'event', + 'seq': 10, + 'event': 'spam', + 'body': {'a': 'b'}, + })