diff --git a/debugger_protocol/messages/__init__.py b/debugger_protocol/messages/__init__.py new file mode 100644 index 000000000..a6dfa91dd --- /dev/null +++ b/debugger_protocol/messages/__init__.py @@ -0,0 +1,67 @@ + + +MESSAGE_TYPES = {} +MESSAGE_TYPE_KEYS = {} + + +def register(cls, msgtype=None, typekey=None, key=None): + """Add the message class to the registry. + + The class is also fixed up if necessary. + """ + if not isinstance(cls, type): + raise RuntimeError('may not be used as a decorator factory.') + + if msgtype is None: + if cls.TYPE is None: + raise RuntimeError('class missing TYPE') + msgtype = cls.TYPE + if typekey is None: + if cls.TYPE_KEY is None: + raise RuntimeError('class missing TYPE_KEY') + typekey = cls.TYPE_KEY + if key is None: + key = getattr(cls, typekey, + getattr(cls, typekey.upper(), None)) + if not key: + raise RuntimeError('missing type key attribute') + + try: + registered = MESSAGE_TYPES[msgtype] + except KeyError: + registered = MESSAGE_TYPES[msgtype] = {} + MESSAGE_TYPE_KEYS[msgtype] = typekey + else: + if typekey != MESSAGE_TYPE_KEYS[msgtype]: + msg = 'mismatch on TYPE_KEY ({!r} != {!r})' + raise RuntimeError( + msg.format(typekey, MESSAGE_TYPE_KEYS[msgtype])) + + if key in registered: + raise RuntimeError('{}:{} already registered'.format(msgtype, key)) + registered[key] = cls + + # XXX init args + + return cls + + +class Message(object): + """The API for register-able message types.""" + + TYPE = None + TYPE_KEY = None + + @classmethod + def from_data(cls, **kwargs): + """Return an instance based on the given raw data.""" + raise NotImplementedError + + def as_data(self): + """Return serializable data for the instance.""" + raise NotImplementedError + + +# Force registration. +from .requests import * # noqa +from .events import * # noqa diff --git a/debugger_protocol/messages/_base.py b/debugger_protocol/messages/_base.py deleted file mode 100644 index 7efc3e3bb..000000000 --- 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/_io.py b/debugger_protocol/messages/_io.py new file mode 100644 index 000000000..c58778efc --- /dev/null +++ b/debugger_protocol/messages/_io.py @@ -0,0 +1,49 @@ +import json + +from . import MESSAGE_TYPES, MESSAGE_TYPE_KEYS + + +def read(stream): + """Return an instance based on the given bytes.""" + headers = {} + for line in stream: + if line == b'\r\n': + break + assert(line.endswith(b'\r\n')) + line = line[:-2].decode('ascii') + try: + name, value = line.split(': ', 1) + except ValueError: + raise RuntimeError('invalid header line: {}'.format(line)) + headers[name] = value + + size = int(headers['Content-Length']) + body = stream.read(size) + + data = json.loads(body.decode('utf-8')) + + msgtype = data['type'] + typekey = MESSAGE_TYPE_KEYS[msgtype] + key = data[typekey] + cls = MESSAGE_TYPES[msgtype][key] + + return cls.from_data(**data) + + +def as_bytes(msg): + """Return the raw bytes for the message.""" + headers, body = _as_http_data(msg) + headers = '\r\n'.join('{}: {}'.format(name, value) + for name, value in headers.items()) + return headers.encode('ascii') + b'\r\n\r\n' + body.encode('utf-8') + + +def _as_http_data(msg): + payload = msg.as_data() + body = json.dumps(payload) + + headers = { + 'Content-Length': len(body), + 'Content-Type': 'application/json', + } + return headers, body diff --git a/debugger_protocol/messages/_requests.py b/debugger_protocol/messages/_requests.py new file mode 100644 index 000000000..c696528b2 --- /dev/null +++ b/debugger_protocol/messages/_requests.py @@ -0,0 +1,331 @@ +from debugger_protocol.arg import FieldsNamespace, Field, Enum +from .shared import Checksum, Source + + +class Message(FieldsNamespace): + """A structured message object. + + Used to return errors from requests. + """ + FIELDS = [ + Field('id', int), + Field('format'), + Field.START_OPTIONAL, + Field('variables', {str: str}), + Field('sendTelemetry', bool), + Field('showUser', bool), + Field('url'), + Field('urlLabel'), + ] + + +class ExceptionBreakpointsFilter(FieldsNamespace): + """ + An ExceptionBreakpointsFilter is shown in the UI as an option for + configuring how exceptions are dealt with. + """ + + FIELDS = [ + Field('filter'), + Field('label'), + Field.START_OPTIONAL, + Field('default', bool), + ] + + +class ColumnDescriptor(FieldsNamespace): + """ + A ColumnDescriptor specifies what module attribute to show in a + column of the ModulesView, how to format it, and what the column's + label should be. It is only used if the underlying UI actually + supports this level of customization. + """ + + TYPES = {"string", "number", "boolean", "unixTimestampUTC"} + FIELDS = [ + Field('attributeName'), + Field('label'), + Field.START_OPTIONAL, + Field('format'), + Field('type'), + Field('width', int), + ] + + +class Capabilities(FieldsNamespace): + """Information about the capabilities of a debug adapter.""" + + FIELDS = [ + Field.START_OPTIONAL, + Field('supportsConfigurationDoneRequest', bool), + Field('supportsFunctionBreakpoints', bool), + Field('supportsConditionalBreakpoints', bool), + Field('supportsHitConditionalBreakpoints', bool), + Field('supportsEvaluateForHovers', bool), + Field('exceptionBreakpointFilters', [ExceptionBreakpointsFilter]), + Field('supportsStepBack', bool), + Field('supportsSetVariable', bool), + Field('supportsRestartFrame', bool), + Field('supportsGotoTargetsRequest', bool), + Field('supportsStepInTargetsRequest', bool), + Field('supportsCompletionsRequest', bool), + Field('supportsModulesRequest', bool), + Field('additionalModuleColumns', [ColumnDescriptor]), + Field('supportedChecksumAlgorithms', [Enum(str, Checksum.ALGORITHMS)]), + Field('supportsRestartRequest', bool), + Field('supportsExceptionOptions', bool), + Field('supportsValueFormattingOptions', bool), + Field('supportsExceptionInfoRequest', bool), + Field('supportTerminateDebuggee', bool), + Field('supportsDelayedStackTraceLoading', bool), + Field('supportsLoadedSourcesRequest', bool), + ] + + +class ModulesViewDescriptor(FieldsNamespace): + """ + The ModulesViewDescriptor is the container for all declarative + configuration options of a ModuleView. For now it only specifies + the columns to be shown in the modules view. + """ + + FIELDS = [ + Field('columns', [ColumnDescriptor]), + ] + + +class Thread(FieldsNamespace): + """A thread.""" + + FIELDS = [ + Field('id', int), + Field('name'), + ] + + +class StackFrame(FieldsNamespace): + """A Stackframe contains the source location.""" + + PRESENTATION_HINTS = {"normal", "label", "subtle"} + FIELDS = [ + Field('id', int), + Field('name'), + Field('source', Source, optional=True), + Field('line', int), + Field('column', int), + Field.START_OPTIONAL, + Field('endLine', int), + Field('endColumn', int), + Field("moduleId", {int, str}), + Field('presentationHint'), + ] + + +class Scope(FieldsNamespace): + """ + A Scope is a named container for variables. Optionally a scope + can map to a source or a range within a source. + """ + + FIELDS = [ + Field('name'), + Field('variablesReference', int), + Field('namedVariables', int, optional=True), + Field('indexedVariables', int, optional=True), + Field('expensive', bool), + Field.START_OPTIONAL, + Field('source', Source), + Field('line', int), + Field('column', int), + Field('endLine', int), + Field('endColumn', int), + ] + + +class VariablePresentationHint(FieldsNamespace): + """ + Optional properties of a variable that can be used to determine + how to render the variable in the UI. + """ + + KINDS = {"property", "method", "class", "data", "event", "baseClass", + "innerClass", "interface", "mostDerivedClass", "virtual"} + ATTRIBUTES = {"static", "constant", "readOnly", "rawString", + "hasObjectId", "canHaveObjectId", "hasSideEffects"} + VISIBILITIES = {"public", "private", "protected", "internal", "final"} + FIELDS = [ + Field.START_OPTIONAL, + Field('kind', enum=KINDS), + Field('attributes', [Enum(str, ATTRIBUTES)]), + Field('visibility', enum=VISIBILITIES), + ] + + +class Variable(FieldsNamespace): + """A Variable is a name/value pair. + + Optionally a variable can have a 'type' that is shown if space + permits or when hovering over the variable's name. An optional + 'kind' is used to render additional properties of the variable, + e.g. different icons can be used to indicate that a variable is + public or private. If the value is structured (has children), a + handle is provided to retrieve the children with the + VariablesRequest. If the number of named or indexed children is + large, the numbers should be returned via the optional + 'namedVariables' and 'indexedVariables' attributes. The client can + use this optional information to present the children in a paged UI + and fetch them in chunks. + """ + + FIELDS = [ + Field('name'), + Field('value'), + Field.START_OPTIONAL, + Field('type'), + Field('presentationHint', VariablePresentationHint), + Field('evaluateName'), + Field('variablesReference', int, optional=False), + Field('namedVariables', int), + Field('indexedVariables', int), + ] + + +class SourceBreakpoint(FieldsNamespace): + """Properties of a breakpoint passed to the setBreakpoints request.""" + + FIELDS = [ + Field('line', int), + Field.START_OPTIONAL, + Field('column', int), + Field('condition'), + Field('hitCondition'), + ] + + +class FunctionBreakpoint(FieldsNamespace): + """ + Properties of a breakpoint passed to the setFunctionBreakpoints request. + """ + + FIELDS = [ + Field('name'), + Field.START_OPTIONAL, + Field('condition'), + Field('hitCondition'), + ] + + +class StepInTarget(FieldsNamespace): + """ + A StepInTarget can be used in the 'stepIn' request and determines + into which single target the stepIn request should step. + """ + + FIELDS = [ + Field('id', int), + Field('label'), + ] + + +class GotoTarget(FieldsNamespace): + """ + A GotoTarget describes a code location that can be used as a target + in the 'goto' request. The possible goto targets can be determined + via the 'gotoTargets' request. + """ + + FIELDS = [ + Field('id', int), + Field('label'), + Field('line', int), + Field.START_OPTIONAL, + Field('column', int), + Field('endLine', int), + Field('endColumn', int), + ] + + +class CompletionItem(FieldsNamespace): + """ + CompletionItems are the suggestions returned from the CompletionsRequest. + """ + + TYPES = {"method", "function", "constructor", "field", "variable", + "class", "interface", "module", "property", "unit", "value", + "enum", "keyword", "snippet", "text", "color", "file", + "reference", "customcolor"} + FIELDS = [ + Field('label'), + Field.START_OPTIONAL, + Field('text'), + Field('type'), + Field('start', int), + Field('length', int), + ] + + +class ValueFormat(FieldsNamespace): + """Provides formatting information for a value.""" + + FIELDS = [ + Field.START_OPTIONAL, + Field('hex', bool), + ] + + +class StackFrameFormat(ValueFormat): + """Provides formatting information for a stack frame.""" + + FIELDS = ValueFormat.FIELDS + [ + Field('parameters', bool), + Field('parameterTypes', bool), + Field('parameterNames', bool), + Field('parameterValues', bool), + Field('line', bool), + Field('module', bool), + Field('includeAll', bool), + ] + + +class ExceptionPathSegment(FieldsNamespace): + """ + An ExceptionPathSegment represents a segment in a path that is used + to match leafs or nodes in a tree of exceptions. If a segment + consists of more than one name, it matches the names provided if + 'negate' is false or missing or it matches anything except the names + provided if 'negate' is true. + """ + + FIELDS = [ + Field('negate', bool, optional=True), + Field('names', [str]), + ] + + +ExceptionBreakMode = Enum(str, + {"never", "always", "unhandled", "userUnhandled"}) + + +class ExceptionOptions(FieldsNamespace): + """ + An ExceptionOptions assigns configuration options to a set of exceptions. + """ + + FIELDS = [ + Field('path', [ExceptionPathSegment], optional=True), + Field('breakMode', ExceptionBreakMode), + ] + + +class ExceptionDetails(FieldsNamespace): + """Detailed information about an exception that has occurred.""" + + FIELDS = [ + Field.START_OPTIONAL, + Field('message'), + Field('typeName'), + Field('fullTypeName'), + Field('evaluateName'), + Field('stackTrace'), + Field('innerException', ['']), + ] diff --git a/debugger_protocol/messages/events.py b/debugger_protocol/messages/events.py new file mode 100644 index 000000000..d3c98ea9a --- /dev/null +++ b/debugger_protocol/messages/events.py @@ -0,0 +1,235 @@ +from debugger_protocol.arg import ANY, FieldsNamespace, Field +from . import register +from .shared import Breakpoint, Module, Source +from .message import Event + + +@register +class InitializedEvent(Event): + """"Event message for 'initialized' event type. + + This event indicates that the debug adapter is ready to accept + configuration requests (e.g. SetBreakpointsRequest, + SetExceptionBreakpointsRequest). A debug adapter is expected to + send this event when it is ready to accept configuration requests + (but not before the InitializeRequest has finished). + + The sequence of events/requests is as follows: + - adapters sends InitializedEvent (after the InitializeRequest + has returned) + - frontend sends zero or more SetBreakpointsRequest + - frontend sends one SetFunctionBreakpointsRequest + - frontend sends a SetExceptionBreakpointsRequest if one or more + exceptionBreakpointFilters have been defined (or if + supportsConfigurationDoneRequest is not defined or false) + - frontend sends other future configuration requests + - frontend sends one ConfigurationDoneRequest to indicate the end + of the configuration + """ + + EVENT = 'initialized' + + +@register +class StoppedEvent(Event): + """Event message for 'stopped' event type. + + The event indicates that the execution of the debuggee has stopped + due to some condition. This can be caused by a break point + previously set, a stepping action has completed, by executing a + debugger statement etc. + """ + + EVENT = 'stopped' + + class BODY(FieldsNamespace): + REASONS = {'step', 'breakpoint', 'exception', 'pause', 'entry'} + FIELDS = [ + Field('reason', enum=REASONS), + Field.START_OPTIONAL, + Field('description'), + Field('threadId', int), + Field('text'), + Field('allThreadsStopped', bool), + ] + + +@register +class ContinuedEvent(Event): + """Event message for 'continued' event type. + + The event indicates that the execution of the debuggee has + continued. + + Please note: a debug adapter is not expected to send this event + in response to a request that implies that execution continues, + e.g. 'launch' or 'continue'. It is only necessary to send a + ContinuedEvent if there was no previous request that implied this. + """ + + EVENT = 'continued' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('threadId', int), + Field.START_OPTIONAL, + Field('allThreadsContinued', bool), + ] + + +@register +class ExitedEvent(Event): + """Event message for 'exited' event type. + + The event indicates that the debuggee has exited. + """ + + EVENT = 'exited' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('exitCode', int), + ] + + +@register +class TerminatedEvent(Event): + """Event message for 'terminated' event types. + + The event indicates that debugging of the debuggee has terminated. + """ + + EVENT = 'terminated' + + class BODY(FieldsNamespace): + FIELDS = [ + Field.START_OPTIONAL, + Field('restart', ANY), + ] + + +@register +class ThreadEvent(Event): + """Event message for 'thread' event type. + + The event indicates that a thread has started or exited. + """ + + EVENT = 'thread' + + class BODY(FieldsNamespace): + REASONS = {'started', 'exited'} + FIELDS = [ + Field('threadId', int), + Field('reason', enum=REASONS), + ] + + +@register +class OutputEvent(Event): + """Event message for 'output' event type. + + The event indicates that the target has produced some output. + """ + + EVENT = 'output' + + class BODY(FieldsNamespace): + CATEGORIES = {'console', 'stdout', 'stderr', 'telemetry'} + FIELDS = [ + Field('output'), + Field.START_OPTIONAL, + Field('category', enum=CATEGORIES), + Field('variablesReference', int), # "number" + Field('source'), + Field('line', int), + Field('column', int), + Field('data', ANY), + ] + + +@register +class BreakpointEvent(Event): + """Event message for 'breakpoint' event type. + + The event indicates that some information about a breakpoint + has changed. + """ + + EVENT = 'breakpoint' + + class BODY(FieldsNamespace): + REASONS = {'changed', 'new', 'removed'} + FIELDS = [ + Field('breakpoint', Breakpoint), + Field('reason', enum=REASONS), + ] + + +@register +class ModuleEvent(Event): + """Event message for 'module' event type. + + The event indicates that some information about a module + has changed. + """ + + EVENT = 'module' + + class BODY(FieldsNamespace): + REASONS = {'new', 'changed', 'removed'} + FIELDS = [ + Field('module', Module), + Field('reason', enum=REASONS), + ] + + +@register +class LoadedSourceEvent(Event): + """Event message for 'loadedSource' event type. + + The event indicates that some source has been added, changed, or + removed from the set of all loaded sources. + """ + + EVENT = 'loadedSource' + + class BODY(FieldsNamespace): + REASONS = {'new', 'changed', 'removed'} + FIELDS = [ + Field('source', Source), + Field('reason', enum=REASONS), + ] + + +@register +class ProcessEvent(Event): + """Event message for 'process' event type. + + The event indicates that the debugger has begun debugging a new + process. Either one that it has launched, or one that it has + attached to. + """ + + EVENT = 'process' + + class BODY(FieldsNamespace): + START_METHODS = {'launch', 'attach', 'attachForSuspendedLaunch'} + FIELDS = [ + Field('name'), + Field.START_OPTIONAL, + Field('systemProcessId', int), + Field('isLocalProcess', bool), + Field('startMethod', enum=START_METHODS), + ] + + +# Clean up the implicit __all__. +del register +del Event +del FieldsNamespace +del Field +del ANY +del Breakpoint +del Module +del Source diff --git a/debugger_protocol/messages/message.py b/debugger_protocol/messages/message.py new file mode 100644 index 000000000..3908aa404 --- /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/debugger_protocol/messages/requests.py b/debugger_protocol/messages/requests.py new file mode 100644 index 000000000..61429f550 --- /dev/null +++ b/debugger_protocol/messages/requests.py @@ -0,0 +1,1049 @@ +from debugger_protocol.arg import FieldsNamespace, Field +from . import register +from . import _requests as datatypes +from . import shared +from .message import Request, Response + + +@register +class ErrorResponse(Response): + """An error response (for any unsuccessful request). + + On error (whenever 'success' is false) the body can provide more + details. + """ + + COMMAND = 'error' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('error', datatypes.Message), + ] + + +################################## + +@register +class RunInTerminalRequest(Request): + """runInTerminal request. + + With this request a debug adapter can run a command in a terminal. + """ + + COMMAND = 'runInTerminal' + + class ARGUMENTS(FieldsNamespace): + KINDS = {'integrated', 'external'} + FIELDS = [ + Field('kind', enum=KINDS, optional=True), + Field('title', optional=True), + Field('cwd'), + Field('args', [str]), + Field.START_OPTIONAL, + Field('env', {str: {str, None}}), + ] + + +@register +class RunInTerminalResponse(Response): + COMMAND = 'runInTerminal' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('processId', int), # number + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class InitializeRequest(Request): + COMMAND = 'initialize' + + class ARGUMENTS(FieldsNamespace): + PATH_FORMATS = {'path', 'uri'} + FIELDS = [ + Field('clientID', optional=True), + Field('adapterID'), + Field.START_OPTIONAL, + Field('locale'), + Field('linesStartAt1', bool), + Field('columnsStartAt1', bool), + Field('pathFormat', enum=PATH_FORMATS), + Field('supportsVariableType', bool), + Field('supportsVariablePaging', bool), + Field('supportsRunInTerminalRequest', bool), + ] + + +@register +class InitializeResponse(Response): + COMMAND = 'initialize' + + BODY = datatypes.Capabilities + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class ConfigurationDoneRequest(Request): + """configurationDone request. + + The client of the debug protocol must send this request at the end + of the sequence of configuration requests (which was started by + the InitializedEvent). + """ + + COMMAND = 'configurationDone' + + ARGUMENTS_REQUIRED = False + + +@register +class ConfigurationDoneResponse(Response): + COMMAND = 'configurationDone' + + # This is just an acknowledgement. + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class LaunchRequest(Request): + COMMAND = 'launch' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('noDebug', bool, optional=True, default=False), + ] + + +@register +class LaunchResponse(Response): + COMMAND = 'launch' + + # This is just an acknowledgement. + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class AttachRequest(Request): + COMMAND = 'attach' + + ARGUMENTS_REQUIRED = False + #class ARGUMENTS(FieldsNamespace): + # FIELDS = [] + + +@register +class AttachResponse(Response): + COMMAND = 'attach' + + # This is just an acknowledgement. + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class RestartRequest(Request): + """restart request. + + Restarts a debug session. If the capability 'supportsRestartRequest' + is missing or has the value false, the client will implement + 'restart' by terminating the debug adapter first and then launching + it anew. A debug adapter can override this default behaviour by + implementing a restart request and setting the capability + 'supportsRestartRequest' to true. + """ + + COMMAND = 'restart' + + ARGUMENTS_REQUIRED = False + + +@register +class RestartResponse(Response): + COMMAND = 'restart' + + # This is just an acknowledgement. + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class DisconnectRequest(Request): + """disconnect request. + + terminateDebuggee: Indicates whether the debuggee should be + terminated when the debugger is disconnected. If unspecified, + the debug adapter is free to do whatever it thinks is best. A + client can only rely on this attribute being properly honored if + a debug adapter returns true for the 'supportTerminateDebuggee' + capability. + """ + + COMMAND = 'disconnect' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('terminateDebuggee', bool, optional=True), + ] + + +@register +class DisconnectResponse(Response): + COMMAND = 'disconnect' + + # This is just an acknowledgement. + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class SetBreakpointsRequest(Request): + """setBreakpoints request. + + Sets multiple breakpoints for a single source and clears all + previous breakpoints in that source. To clear all breakpoint for + a source, specify an empty array. When a breakpoint is hit, a + StoppedEvent (event type 'breakpoint') is generated. + """ + + COMMAND = 'setBreakpoints' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('source', shared.Source), + Field.START_OPTIONAL, + Field('breakpoints', [datatypes.SourceBreakpoint]), + Field('lines', [int]), + Field('sourceModified', bool), + ] + + +@register +class SetBreakpointsResponse(Response): + COMMAND = 'setBreakpoints' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('breakpoints', [shared.Breakpoint]), + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class SetFunctionBreakpointsRequest(Request): + """setFunctionBreakpoints request. + + Sets multiple function breakpoints and clears all previous function + breakpoints. To clear all function breakpoint, specify an empty + array. When a function breakpoint is hit, a StoppedEvent (event + type 'function breakpoint') is generated. + """ + + COMMAND = 'setFunctionBreakpoints' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('breakpoints', [datatypes.FunctionBreakpoint]), + ] + + +@register +class SetFunctionBreakpointsResponse(Response): + COMMAND = 'setFunctionBreakpoints' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('breakpoints', [shared.Breakpoint]), + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class SetExceptionBreakpointsRequest(Request): + """setExceptionBreakpoints request. + + The request configures the debuggers response to thrown exceptions. + If an exception is configured to break, a StoppedEvent is fired + (event type 'exception'). + """ + + COMMAND = 'setExceptionBreakpoints' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('filters', [str]), + Field.START_OPTIONAL, + Field('exceptionOptions', [datatypes.ExceptionOptions]), + ] + + +@register +class SetExceptionBreakpointsResponse(Response): + COMMAND = 'setExceptionBreakpoints' + + # This is just an acknowledgement. + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class ContinueRequest(Request): + """continue request. + + The request starts the debuggee to run again. + + threadId: Continue execution for the specified thread (if possible). + If the backend cannot continue on a single thread but will + continue on all threads, it should set the allThreadsContinued + attribute in the response to true. + """ + + COMMAND = 'continue' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('threadId', int), + ] + + +@register +class ContinueResponse(Response): + COMMAND = 'continue' + + class BODY(FieldsNamespace): + FIELDS = [ + Field.START_OPTIONAL, + Field('allThreadsContinued', bool), + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class NextRequest(Request): + """next request. + + The request starts the debuggee to run again for one step. The + debug adapter first sends the NextResponse and then a StoppedEvent + (event type 'step') after the step has completed. + """ + + COMMAND = 'next' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('threadId', int), + ] + + +@register +class NextResponse(Response): + COMMAND = 'next' + + # This is just an acknowledgement. + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class StepInRequest(Request): + """stepIn request. + + The request starts the debuggee to step into a function/method if + possible. If it cannot step into a target, 'stepIn' behaves like + 'next'. The debug adapter first sends the StepInResponse and then + a StoppedEvent (event type 'step') after the step has completed. + If there are multiple function/method calls (or other targets) on + the source line, the optional argument 'targetId' can be used to + control into which target the 'stepIn' should occur. The list of + possible targets for a given source line can be retrieved via the + 'stepInTargets' request. + """ + + COMMAND = 'stepIn' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('threadId', int), + Field('targetId', int), + ] + + +@register +class StepInResponse(Response): + COMMAND = 'stepIn' + + # This is just an acknowledgement. + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class StepOutRequest(Request): + """stepOut request. + + The request starts the debuggee to run again for one step. The + debug adapter first sends the StepOutResponse and then a + StoppedEvent (event type 'step') after the step has completed. + """ + + COMMAND = 'stepOut' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('threadId', int), + ] + + +@register +class StepOutResponse(Response): + COMMAND = 'stepOut' + + # This is just an acknowledgement. + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class StepBackRequest(Request): + """stepBack request. + + The request starts the debuggee to run one step backwards. The + debug adapter first sends the StepBackResponse and then a + StoppedEvent (event type 'step') after the step has completed. + Clients should only call this request if the capability + supportsStepBack is true. + """ + + COMMAND = 'stepBack' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('threadId', int), + ] + + +@register +class StepBackResponse(Response): + COMMAND = 'stepBack' + + # This is just an acknowledgement. + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class ReverseContinueRequest(Request): + """reverseContinue request. + + The request starts the debuggee to run backward. Clients should + only call this request if the capability supportsStepBack is true. + """ + + COMMAND = 'reverseContinue' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('threadId', int), + ] + + +@register +class ReverseContinueResponse(Response): + COMMAND = 'reverseContinue' + + # This is just an acknowledgement. + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class RestartFrameRequest(Request): + """restartFrame request. + + The request restarts execution of the specified stackframe. The + debug adapter first sends the RestartFrameResponse and then a + StoppedEvent (event type 'restart') after the restart has + completed. + """ + + COMMAND = 'restartFrame' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('frameId', int), + ] + + +@register +class RestartFrameResponse(Response): + COMMAND = 'restartFrame' + + # This is just an acknowledgement. + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class GotoRequest(Request): + """goto request. + + The request sets the location where the debuggee will continue to + run. This makes it possible to skip the execution of code or to + executed code again. The code between the current location and the + goto target is not executed but skipped. The debug adapter first + sends the GotoResponse and then a StoppedEvent (event type 'goto'). + """ + + COMMAND = 'goto' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('threadId', int), + Field('targetId', int), + ] + + +@register +class GotoResponse(Response): + COMMAND = 'goto' + + # This is just an acknowledgement. + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class PauseRequest(Request): + """pause request. + + The request suspenses the debuggee. The debug adapter first sends + the PauseResponse and then a StoppedEvent (event type 'pause') + after the thread has been paused successfully. + """ + + COMMAND = 'pause' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('threadId', int), + ] + + +@register +class PauseResponse(Response): + COMMAND = 'pause' + + # This is just an acknowledgement. + BODY_REQUIRED = False + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class StackTraceRequest(Request): + """stackTrace request. + + The request returns a stacktrace from the current execution state. + """ + + COMMAND = 'stackTrace' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('threadId', int), + Field.START_OPTIONAL, + Field('startFrame', int), + Field('levels', int), + Field('format', datatypes.StackFrameFormat), + ] + + +@register +class StackTraceResponse(Response): + COMMAND = 'stackTrace' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('stackFrames', [datatypes.StackFrame]), + Field.START_OPTIONAL, + Field('totalFrames', int), + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class ScopesRequest(Request): + """scopes request. + + The request returns the variable scopes for a given stackframe ID. + """ + + COMMAND = 'scopes' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('frameId', int), + ] + + +@register +class ScopesResponse(Response): + COMMAND = 'scopes' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('scopes', [datatypes.Scope]), + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class VariablesRequest(Request): + """variables request. + + Retrieves all child variables for the given variable reference. An + optional filter can be used to limit the fetched children to either + named or indexed children. + """ + + COMMAND = 'variables' + + class ARGUMENTS(FieldsNamespace): + FILTERS = {'indexed', 'named'} + FIELDS = [ + Field('variablesReference', int), + Field.START_OPTIONAL, + Field('filter', enum=FILTERS), + Field('start', int), + Field('count', int), + Field('format', datatypes.ValueFormat), + ] + + +@register +class VariablesResponse(Response): + COMMAND = 'variables' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('variables', [datatypes.Variable]), + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class SetVariableRequest(Request): + """setVariable request. + + Set the variable with the given name in the variable container + to a new value. + """ + + COMMAND = 'setVariable' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('variablesReference', int), + Field('name'), + Field('value'), + Field.START_OPTIONAL, + Field('format', datatypes.ValueFormat), + ] + + +@register +class SetVariableResponse(Response): + """ + """ + + COMMAND = 'setVariable' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('value'), + Field.START_OPTIONAL, + Field('type'), + Field('variablesReference', int), # number + Field('namedVariables', int), # number + Field('indexedVariables', int), # number + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class SourceRequest(Request): + """source request. + + The request retrieves the source code for a given source reference. + """ + + COMMAND = 'source' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('source', shared.Source, optional=True), + Field('sourceReference', int), + ] + + +@register +class SourceResponse(Response): + COMMAND = 'source' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('content'), + Field.START_OPTIONAL, + Field('mimeType'), + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class ThreadsRequest(Request): + """threads request. + + The request retrieves a list of all threads. + """ + + COMMAND = 'threads' + + ARGUMENTS_REQUIRED = False + + +@register +class ThreadsResponse(Response): + COMMAND = 'threads' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('threads', [datatypes.Thread]), + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class ModulesRequest(Request): + """modules request. + + Modules can be retrieved from the debug adapter with the + ModulesRequest which can either return all modules or a range of + modules to support paging. + """ + + COMMAND = 'modules' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field.START_OPTIONAL, + Field('startModule', int, default=0), + Field('moduleCount', int), + ] + + +@register +class ModulesResponse(Response): + COMMAND = 'modules' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('modules', [shared.Module]), + Field.START_OPTIONAL, + Field('totalModules', int), + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class LoadedSourcesRequest(Request): + """loadedSources request. + + Retrieves the set of all sources currently loaded by the debugged + process. + """ + + COMMAND = 'loadedSources' + + ARGUMENTS_REQUIRED = False + #class ARGUMENTS(FieldsNamespace): + # FIELDS = [] + + +@register +class LoadedSourcesResponse(Response): + COMMAND = 'loadedSources' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('sources', [shared.Source]), + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class EvaluateRequest(Request): + """evaluate request. + + Evaluates the given expression in the context of the top most stack + frame. The expression has access to any variables and arguments + that are in scope. + """ + + COMMAND = 'evaluate' + + class ARGUMENTS(FieldsNamespace): + CONTEXTS = {'watch', 'repl', 'hover'} + FIELDS = [ + Field('expression'), + Field.START_OPTIONAL, + Field('frameId', int), + Field('context', enum=CONTEXTS), + Field('format', datatypes.ValueFormat), + ] + + +@register +class EvaluateResponse(Response): + COMMAND = 'evaluate' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('result'), + Field.START_OPTIONAL, + Field('type'), + Field('presentationHint', datatypes.VariablePresentationHint), + Field('variablesReference', int, optional=False), # number + Field('namedVariables', int), # number + Field('indexedVariables', int), # number + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class StepInTargetsRequest(Request): + """stepInTargets request. + + This request retrieves the possible stepIn targets for the specified + stack frame. These targets can be used in the 'stepIn' request. + The StepInTargets may only be called if the + 'supportsStepInTargetsRequest' capability exists and is true. + """ + + COMMAND = 'stepInTargets' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('frameId', int), + ] + + +@register +class StepInTargetsResponse(Response): + COMMAND = 'stepInTargets' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('targets', [datatypes.StepInTarget]), + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class GotoTargetsRequest(Request): + """gotoTargets request. + + This request retrieves the possible goto targets for the specified + source location. These targets can be used in the 'goto' request. + The GotoTargets request may only be called if the + 'supportsGotoTargetsRequest' capability exists and is true. + """ + + COMMAND = 'gotoTargets' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('source', shared.Source), + Field('line', int), + Field.START_OPTIONAL, + Field('column', int), + ] + + +@register +class GotoTargetsResponse(Response): + COMMAND = 'gotoTargets' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('targets', [datatypes.GotoTarget]), + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class CompletionsRequest(Request): + """completions request. + + Returns a list of possible completions for a given caret position + and text. The CompletionsRequest may only be called if the + 'supportsCompletionsRequest' capability exists and is true. + """ + + COMMAND = 'completions' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('frameId', int, optional=True), + Field('text'), + Field('column', int), + Field.START_OPTIONAL, + Field('line', int), + ] + + +@register +class CompletionsResponse(Response): + COMMAND = 'completions' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('targets', [datatypes.CompletionItem]), + ] + + ERROR_BODY = ErrorResponse.BODY + + +################################## + +@register +class ExceptionInfoRequest(Request): + """exceptionInfo request. + + Retrieves the details of the exception that caused the StoppedEvent + to be raised. + """ + + COMMAND = 'exceptionInfo' + + class ARGUMENTS(FieldsNamespace): + FIELDS = [ + Field('threadId', int), + ] + + +@register +class ExceptionInfoResponse(Response): + COMMAND = 'exceptionInfo' + + class BODY(FieldsNamespace): + FIELDS = [ + Field('exceptionId'), + Field('description', optional=True), + Field('breakMode', datatypes.ExceptionBreakMode), + Field.START_OPTIONAL, + Field('details', datatypes.ExceptionDetails), + ] + + ERROR_BODY = ErrorResponse.BODY + + +# Clean up the implicit __all__. +del register +del Request +del Response +del FieldsNamespace +del Field +del datatypes +del shared diff --git a/debugger_protocol/messages/shared.py b/debugger_protocol/messages/shared.py new file mode 100644 index 000000000..5331b2ea2 --- /dev/null +++ b/debugger_protocol/messages/shared.py @@ -0,0 +1,85 @@ +from debugger_protocol.arg import ANY, FieldsNamespace, Field + + +class Checksum(FieldsNamespace): + """The checksum of an item calculated by the specified algorithm.""" + + ALGORITHMS = {'MD5', 'SHA1', 'SHA256', 'timestamp'} + + FIELDS = [ + Field('algorithm', enum=ALGORITHMS), + Field('checksum'), + ] + + +class Source(FieldsNamespace): + """A Source is a descriptor for source code. + + It is returned from the debug adapter as part of a StackFrame + and it is used by clients when specifying breakpoints. + """ + + HINTS = {'normal', 'emphasize', 'deemphasize'} + + FIELDS = [ + Field.START_OPTIONAL, + Field('name'), + Field('path'), + Field('sourceReference', int), # number + Field('presentationHint', enum=HINTS), + Field('origin'), + Field('sources', ['']), + Field('adapterData', ANY), + Field('checksums', [Checksum]), + ] + + +class Breakpoint(FieldsNamespace): + """Information about a Breakpoint. + + The breakpoint comes from setBreakpoints or setFunctionBreakpoints. + """ + + FIELDS = [ + Field('id', int, optional=True), + Field('verified', bool), + Field.START_OPTIONAL, + Field('message'), + Field('source', Source), + Field('line', int), + Field('column', int), + Field('endLine', int), + Field('endColumn', int), + ] + + +class Module(FieldsNamespace): + """A Module object represents a row in the modules view. + + Two attributes are mandatory: an id identifies a module in the + modules view and is used in a ModuleEvent for identifying a module + for adding, updating or deleting. The name is used to minimally + render the module in the UI. + + Additional attributes can be added to the module. They will show up + in the module View if they have a corresponding ColumnDescriptor. + + To avoid an unnecessary proliferation of additional attributes with + similar semantics but different names we recommend to re-use + attributes from the 'recommended' list below first, and only + introduce new attributes if nothing appropriate could be found. + """ + + FIELDS = [ + Field('id', {int, str}), + Field('name'), + Field.START_OPTIONAL, + Field('path'), + Field('isOptimized', bool), + Field('isUserCode', bool), + Field('version'), + Field('symbolStatus'), + Field('symbolFilePath'), + Field('dateTimeStamp'), + Field('addressRange'), + ] diff --git a/tests/debugger_protocol/messages/__init__.py b/tests/debugger_protocol/messages/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/debugger_protocol/messages/test_events.py b/tests/debugger_protocol/messages/test_events.py new file mode 100644 index 000000000..9c623132f --- /dev/null +++ b/tests/debugger_protocol/messages/test_events.py @@ -0,0 +1,356 @@ +import unittest + +from debugger_protocol.messages import events + + +class StringLike: + + def __init__(self, value): + self.value = value + + def __str__(self): + return self.value + + +class EventsTests(unittest.TestCase): + + def test_implicit___all__(self): + names = set(name + for name in vars(events) + if not name.startswith('__')) + + self.assertEqual(names, { + 'InitializedEvent', + 'StoppedEvent', + 'ContinuedEvent', + 'ExitedEvent', + 'TerminatedEvent', + 'ThreadEvent', + 'OutputEvent', + 'BreakpointEvent', + 'ModuleEvent', + 'LoadedSourceEvent', + 'ProcessEvent', + }) + + +class TestBase: + + NAME = None + EVENT = None + BODY = None + BODY_MIN = None + + def test_event_full(self): + event = self.EVENT(self.BODY, seq=9) + + self.assertEqual(event.event, self.NAME) + self.assertEqual(event.body, self.BODY) + + def test_event_minimal(self): + event = self.EVENT(self.BODY_MIN, seq=9) + + self.assertEqual(event.body, self.BODY_MIN) + + def test_event_empty_body(self): + if self.BODY_MIN: + with self.assertRaises(TypeError): + self.EVENT({}, seq=9) + + def test_from_data(self): + event = self.EVENT.from_data( + type='event', + seq=9, + event=self.NAME, + body=self.BODY, + ) + + self.assertEqual(event.body, self.BODY) + + def test_as_data(self): + event = self.EVENT(self.BODY, seq=9) + data = event.as_data() + + self.assertEqual(data, { + 'type': 'event', + 'seq': 9, + 'event': self.NAME, + 'body': self.BODY, + }) + + +class InitializedEventTests(unittest.TestCase): + + def test_event(self): + event = events.InitializedEvent(seq=9) + + self.assertEqual(event.event, 'initialized') + + +class StoppedEventTests(TestBase, unittest.TestCase): + + NAME = 'stopped' + EVENT = events.StoppedEvent + BODY = { + 'reason': 'step', + 'description': 'descr', + 'threadId': 10, + 'text': '...', + 'allThreadsStopped': False, + } + BODY_MIN = { + 'reason': 'step', + } + + def test_reasons(self): + for reason in events.StoppedEvent.BODY.REASONS: + with self.subTest(reason): + body = { + 'reason': reason, + } + event = events.StoppedEvent(body, seq=9) + + self.assertEqual(event.body.reason, reason) + + +class ContinuedEventTests(TestBase, unittest.TestCase): + + NAME = 'continued' + EVENT = events.ContinuedEvent + BODY = { + 'threadId': 10, + 'allThreadsContinued': True, + } + BODY_MIN = { + 'threadId': 10, + } + + +class ExitedEventTests(TestBase, unittest.TestCase): + + NAME = 'exited' + EVENT = events.ExitedEvent + BODY = { + 'exitCode': 0, + } + BODY_MIN = BODY + + +class TerminatedEventTests(TestBase, unittest.TestCase): + + NAME = 'terminated' + EVENT = events.TerminatedEvent + BODY = { + 'restart': True, + } + BODY_MIN = {} + + +class ThreadEventTests(TestBase, unittest.TestCase): + + NAME = 'thread' + EVENT = events.ThreadEvent + BODY = { + 'threadId': 10, + 'reason': 'exited', + } + BODY_MIN = BODY + + def test_reasons(self): + for reason in self.EVENT.BODY.REASONS: + with self.subTest(reason): + body = { + 'threadId': 10, + 'reason': reason, + } + event = self.EVENT(body, seq=9) + + self.assertEqual(event.body.reason, reason) + + +class OutputEventTests(TestBase, unittest.TestCase): + + NAME = 'output' + EVENT = events.OutputEvent + BODY = { + 'output': '...', + 'category': 'stdout', + 'variablesReference': 10, + 'source': '...', + 'line': 11, + 'column': 12, + 'data': None, + } + BODY_MIN = { + 'output': '...', + } + + def test_categories(self): + for category in self.EVENT.BODY.CATEGORIES: + with self.subTest(category): + body = dict(self.BODY, **{ + 'category': category, + }) + event = self.EVENT(body, seq=9) + + self.assertEqual(event.body.category, category) + + +class BreakpointEventTests(TestBase, unittest.TestCase): + + NAME = 'breakpoint' + EVENT = events.BreakpointEvent + BODY = { + 'breakpoint': { + 'id': 10, + 'verified': True, + 'message': '...', + 'source': { + 'name': '...', + 'path': '...', + 'sourceReference': 15, + 'presentationHint': 'normal', + 'origin': '...', + 'sources': [ + {'name': '...'}, + ], + 'adapterData': None, + 'checksums': [ + {'algorithm': 'MD5', 'checksum': '...'}, + ], + }, + 'line': 11, + 'column': 12, + 'endLine': 11, + 'endColumn': 12, + }, + 'reason': 'new', + } + BODY_MIN = { + 'breakpoint': { + 'id': 10, + 'verified': True, + }, + 'reason': 'new', + } + + def test_reasons(self): + for reason in self.EVENT.BODY.REASONS: + with self.subTest(reason): + body = dict(self.BODY, **{ + 'reason': reason, + }) + event = self.EVENT(body, seq=9) + + self.assertEqual(event.body.reason, reason) + + +class ModuleEventTests(TestBase, unittest.TestCase): + + NAME = 'module' + EVENT = events.ModuleEvent + BODY = { + 'module': { + 'id': 10, + 'name': '...', + 'path': '...', + 'isOptimized': False, + 'isUserCode': True, + 'version': '...', + 'symbolStatus': '...', + 'symbolFilePath': '...', + 'dateTimeStamp': '...', + 'addressRange': '...', + }, + 'reason': 'new', + } + BODY_MIN = { + 'module': { + 'id': 10, + 'name': '...', + }, + 'reason': 'new', + } + + def test_reasons(self): + for reason in self.EVENT.BODY.REASONS: + with self.subTest(reason): + body = dict(self.BODY, **{ + 'reason': reason, + }) + event = self.EVENT(body, seq=9) + + self.assertEqual(event.body.reason, reason) + + +class LoadedSourceEventTests(TestBase, unittest.TestCase): + + NAME = 'loadedSource' + EVENT = events.LoadedSourceEvent + BODY = { + 'source': { + 'name': '...', + 'path': '...', + 'sourceReference': 15, + 'presentationHint': 'normal', + 'origin': '...', + 'sources': [ + {'name': '...'}, + ], + 'adapterData': None, + 'checksums': [ + {'algorithm': 'MD5', 'checksum': '...'}, + ], + }, + 'reason': 'new', + } + BODY_MIN = { + 'source': {}, + 'reason': 'new', + } + + def test_reasons(self): + for reason in self.EVENT.BODY.REASONS: + with self.subTest(reason): + body = dict(self.BODY, **{ + 'reason': reason, + }) + event = self.EVENT(body, seq=9) + + self.assertEqual(event.body.reason, reason) + + def test_hints(self): + for hint in self.EVENT.BODY.FIELDS[0].datatype.HINTS: + with self.subTest(hint): + body = dict(self.BODY) + body['source'].update(**{ + 'presentationHint': hint, + }) + event = self.EVENT(body, seq=9) + + self.assertEqual(event.body.source.presentationHint, hint) + + +class ProcessEventTests(TestBase, unittest.TestCase): + + NAME = 'process' + EVENT = events.ProcessEvent + BODY = { + 'name': '...', + 'systemProcessId': 10, + 'isLocalProcess': True, + 'startMethod': 'launch', + } + BODY_MIN = { + 'name': '...', + } + + def test_start_methods(self): + for method in self.EVENT.BODY.START_METHODS: + with self.subTest(method): + body = dict(self.BODY, **{ + 'startMethod': method, + }) + event = self.EVENT(body, seq=9) + + self.assertEqual(event.body.startMethod, method) diff --git a/tests/debugger_protocol/messages/test_message.py b/tests/debugger_protocol/messages/test_message.py new file mode 100644 index 000000000..0171cde07 --- /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'}, + }) diff --git a/tests/debugger_protocol/messages/test_requests.py b/tests/debugger_protocol/messages/test_requests.py new file mode 100644 index 000000000..87a87386e --- /dev/null +++ b/tests/debugger_protocol/messages/test_requests.py @@ -0,0 +1,126 @@ +import unittest + +from debugger_protocol.messages import requests + + +class RequestsTests(unittest.TestCase): + + def test_implicit___all__(self): + names = set(name + for name in vars(requests) + if not name.startswith('__')) + + self.assertEqual(names, { + 'ErrorResponse', + 'RunInTerminalRequest', + 'RunInTerminalResponse', + 'InitializeRequest', + 'InitializeResponse', + 'ConfigurationDoneRequest', + 'ConfigurationDoneResponse', + 'LaunchRequest', + 'LaunchResponse', + 'AttachRequest', + 'AttachResponse', + 'RestartRequest', + 'RestartResponse', + 'DisconnectRequest', + 'DisconnectResponse', + 'SetBreakpointsRequest', + 'SetBreakpointsResponse', + 'SetFunctionBreakpointsRequest', + 'SetFunctionBreakpointsResponse', + 'SetExceptionBreakpointsRequest', + 'SetExceptionBreakpointsResponse', + 'ContinueRequest', + 'ContinueResponse', + 'NextRequest', + 'NextResponse', + 'StepInRequest', + 'StepInResponse', + 'StepOutRequest', + 'StepOutResponse', + 'StepBackRequest', + 'StepBackResponse', + 'ReverseContinueRequest', + 'ReverseContinueResponse', + 'RestartFrameRequest', + 'RestartFrameResponse', + 'GotoRequest', + 'GotoResponse', + 'PauseRequest', + 'PauseResponse', + 'StackTraceRequest', + 'StackTraceResponse', + 'ScopesRequest', + 'ScopesResponse', + 'VariablesRequest', + 'VariablesResponse', + 'SetVariableRequest', + 'SetVariableResponse', + 'SourceRequest', + 'SourceResponse', + 'ThreadsRequest', + 'ThreadsResponse', + 'ModulesRequest', + 'ModulesResponse', + 'LoadedSourcesRequest', + 'LoadedSourcesResponse', + 'EvaluateRequest', + 'EvaluateResponse', + 'StepInTargetsRequest', + 'StepInTargetsResponse', + 'GotoTargetsRequest', + 'GotoTargetsResponse', + 'CompletionsRequest', + 'CompletionsResponse', + 'ExceptionInfoRequest', + 'ExceptionInfoResponse', + }) + + +# TODO: Add tests for every request/response type. + +#class TestBase: +# +# NAME = None +# EVENT = None +# BODY = None +# BODY_MIN = None +# +# def test_event_full(self): +# event = self.EVENT(self.BODY, seq=9) +# +# self.assertEqual(event.event, self.NAME) +# self.assertEqual(event.body, self.BODY) +# +# def test_event_minimal(self): +# event = self.EVENT(self.BODY_MIN, seq=9) +# +# self.assertEqual(event.body, self.BODY_MIN) +# +# def test_event_empty_body(self): +# if self.BODY_MIN: +# with self.assertRaises(TypeError): +# self.EVENT({}, seq=9) +# +# def test_from_data(self): +# event = self.EVENT.from_data( +# type='event', +# seq=9, +# event=self.NAME, +# body=self.BODY, +# ) +# +# self.assertEqual(event.body, self.BODY) +# +# def test_as_data(self): +# event = self.EVENT(self.BODY, seq=9) +# data = event.as_data() +# +# self.assertEqual(data, { +# 'type': 'event', +# 'seq': 9, +# 'event': self.NAME, +# 'body': self.BODY, +# })