From 3af4b171c77873b06dfb080f20d14fbca0c880a0 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Sat, 10 Feb 2018 04:00:12 +0000 Subject: [PATCH 01/11] Factor out tests.helpers.protocol.Daemon. --- tests/helpers/protocol.py | 169 ++++++++++++++++++++++++++++++ tests/helpers/pydevd/_fake.py | 148 +++----------------------- tests/helpers/pydevd/_pydevd.py | 45 ++++---- tests/helpers/socket.py | 38 +++++++ tests/helpers/vsc/_fake.py | 180 +++++--------------------------- tests/helpers/vsc/_vsc.py | 42 +++----- 6 files changed, 282 insertions(+), 340 deletions(-) create mode 100644 tests/helpers/protocol.py create mode 100644 tests/helpers/socket.py diff --git a/tests/helpers/protocol.py b/tests/helpers/protocol.py new file mode 100644 index 000000000..50ee8f5ad --- /dev/null +++ b/tests/helpers/protocol.py @@ -0,0 +1,169 @@ +from collections import namedtuple +import threading + +from . import socket + + +class StreamFailure(Exception): + """Something went wrong while handling messages to/from a stream.""" + + def __init__(self, direction, msg, exception): + err = 'error while processing stream: {!r}'.format(exception) + super(StreamFailure, self).__init__(self, err) + self.direction = direction + self.msg = msg + self.exception = exception + + def __repr__(self): + return '{}(direction={!r}, msg={!r}, exception={!r})'.format( + type(self).__name__, + self.direction, + self.msg, + self.exception, + ) + + +class MessageProtocol(namedtuple('Protocol', 'parse encode iter')): + """A basic abstraction of a message protocol. + + parse(msg) - returns a message for the given data. + encode(msg) - returns the message, serialized to the line-format. + iter(stream, stop) - yield each message from the stream. "stop" + is a function called with no args which returns True if the + iterator should stop. + """ + + +class Started(object): + """A simple wrapper around a started message protocol daemon.""" + + def __init__(self, fake): + self.fake = fake + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + def send_message(self, msg): + return self.fake.send_response(msg) + + def close(self): + self.fake.close() + + +class Daemon(object): + """A testing double for a protocol daemon.""" + + STARTED = Started + + def __init__(self, connect, protocol, handler=None): + self._connect = connect + self._protocol = protocol + self._handler = handler + + self._closed = False + self._received = [] + self._failures = [] + + # These are set when we start. + self._host = None + self._port = None + self._sock = None + self._server = None + self._listener = None + + @property + def received(self): + """All the messages received thus far.""" + return list(self._received) + + @property + def failures(self): + """All send/recv failures thus far.""" + return self._failures + + def start(self, host, port): + """Start the fake daemon. + + This calls the earlier provided connect() function. + + A listener loop is started in another thread to handle incoming + messages from the socket. + """ + self._host = host or None + self._port = port + self._start() + return self.STARTED(self) + + def send_message(self, msg): + """Serialize msg to the line format and send it to the socket.""" + if self._closed: + raise EOFError('closed') + msg = self._protocol.parse(msg) + raw = self._protocol.encode(msg) + try: + self._send(raw) + except Exception as exc: + failure = StreamFailure('send', msg, exc) + self._failures.append(failure) + + def close(self): + """Clean up the daemon's resources (e.g. sockets, files, listener).""" + if self._closed: + return + + self._closed = True + self._close() + + def assert_received(self, case, expected): + """Ensure that the received messages match the expected ones.""" + received = [self._protocol.parse(msg) for msg in self._received] + expected = [self._protocol.parse(msg) for msg in expected] + case.assertEqual(received, expected) + + # internal methods + + def _start(self, host=None): + self._sock, self._server = self._connect( + host or self._host, + self._port, + ) + + # TODO: make it a daemon thread? + self._listener = threading.Thread(target=self._listen) + self._listener.start() + + def _listen(self): + with self._sock.makefile('rb') as sockfile: + for msg in self._protocol.iter(sockfile, lambda: self._closed): + if isinstance(msg, StreamFailure): + self._failures.append(msg) + else: + self._add_received(msg) + + def _add_received(self, msg): + self._received.append(msg) + + if self._handler is not None: + self._handler(msg, self.send_message) + + def _send(self, raw): + while raw: + sent = self._sock.send(raw) + raw = raw[sent:] + + def _close(self): + if self._sock is not None: + socket.close(self._sock) + self._sock = None + if self._server is not None: + socket.close(self._server) + self._server = None + if self._listener is not None: + self._listener.join(timeout=1) + # TODO: the listener isn't stopping! + #if self._listener.is_alive(): + # raise RuntimeError('timed out') + self._listener = None diff --git a/tests/helpers/pydevd/_fake.py b/tests/helpers/pydevd/_fake.py index 6e38b5d30..100e3e8fe 100644 --- a/tests/helpers/pydevd/_fake.py +++ b/tests/helpers/pydevd/_fake.py @@ -1,33 +1,23 @@ -import socket -import threading - from ptvsd.wrapper import start_server, start_client - -from ._pydevd import parse_message, iter_messages, StreamFailure +from ._pydevd import parse_message, encode_message, iter_messages +from tests.helpers import protocol -def socket_close(sock): - sock.shutdown(socket.SHUT_RDWR) - sock.close() +PROTOCOL = protocol.MessageProtocol( + parse=parse_message, + encode=encode_message, + iter=iter_messages, +) def _connect(host, port): if host is None: - return start_server(port) + return start_server(port), None else: - return start_client(host, port) - - -class _Started(object): + return start_client(host, port), None - def __init__(self, fake): - self.fake = fake - def __enter__(self): - return self - - def __exit__(self, *args): - self.close() +class Started(protocol.Started): def send_response(self, msg): return self.fake.send_response(msg) @@ -35,11 +25,8 @@ def send_response(self, msg): def send_event(self, msg): return self.fake.send_event(msg) - def close(self): - self.fake.close() - -class FakePyDevd(object): +class FakePyDevd(protocol.Daemon): """A testing double for PyDevd. Note that you have the option to provide a handler function. This @@ -66,118 +53,15 @@ class FakePyDevd(object): https://github.com/fabioz/PyDev.Debugger/blob/master/_pydevd_bundle/pydevd_comm.py """ # noqa - CONNECT = staticmethod(_connect) - - def __init__(self, handler=None, connect=None): - if connect is None: - connect = self.CONNECT - - self._handler = handler - self._connect = connect - - self._closed = False - self._received = [] - self._failures = [] - - # These are set when we start. - self._host = None - self._port = None - self._sock = None - self._listener = None - - @property - def received(self): - """All the messages received thus far.""" - return list(self._received) - - @property - def failures(self): - """All send/recv failures thus far.""" - return self._failures - - def start(self, host, port): - """Start the fake pydevd daemon. - - This calls the earlier provided connect() function. By default - this calls either start_server() or start_client() (depending on - the host) from ptvsd.wrapper. Thus the ptvsd message processor - is started and a PydevdSocket is used as the connection. - - A listener loop is started in another thread to handle incoming - messages from the socket (i.e. from ptvsd). - """ - self._host = host or None - self._port = port - self._sock = self._connect(self._host, self._port) - - # TODO: make daemon? - self._listener = threading.Thread(target=self._listen) - self._listener.start() + STARTED = Started - return _Started(self) + def __init__(self, handler=None): + super(FakePyDevd, self).__init__(_connect, PROTOCOL, handler) def send_response(self, msg): """Send a response message to the adapter (ptvsd).""" - return self._send_message(msg) + return self.send_message(msg) def send_event(self, msg): """Send an event message to the adapter (ptvsd).""" - return self._send_message(msg) - - def close(self): - """If started, close the socket and wait for the listener to finish.""" - if self._closed: - return - - self._closed = True - if self._sock is not None: - socket_close(self._sock) - self._sock = None - if self._listener is not None: - self._listener.join(timeout=1) - # TODO: the listener isn't stopping! - #if self._listener.is_alive(): - # raise RuntimeError('timed out') - self._listener = None - - def assert_received(self, case, expected): - """Ensure that the received messages match the expected ones.""" - received = [parse_message(msg) for msg in self._received] - expected = [parse_message(msg) for msg in expected] - case.assertEqual(received, expected) - - # internal methods - - def _listen(self): - with self._sock.makefile('rb') as sockfile: - for msg in iter_messages(sockfile, lambda: self._closed): - if isinstance(msg, StreamFailure): - self._failures.append(msg) - else: - self._add_received(msg) - - def _add_received(self, msg): - self._received.append(msg) - - if self._handler is not None: - self._handler(msg, self._send_message) - - def _send_message(self, msg): - """Serialize the message to the line format and send it to ptvsd. - - If the message is bytes or a string then it is send as-is. - """ - msg = parse_message(msg) - raw = msg.as_bytes() - if not raw.endswith(b'\n'): - raw += b'\n' - try: - self._send(raw) - except Exception as exc: - failure = StreamFailure('send', msg, exc) - self._failures.append(failure) - - def _send(self, raw): - while raw: - sent = self._sock.send(raw) - raw = raw[sent:] + return self.send_message(msg) diff --git a/tests/helpers/pydevd/_pydevd.py b/tests/helpers/pydevd/_pydevd.py index 691c6adac..c29a6bba4 100644 --- a/tests/helpers/pydevd/_pydevd.py +++ b/tests/helpers/pydevd/_pydevd.py @@ -1,25 +1,28 @@ from collections import namedtuple +from tests.helpers.protocol import StreamFailure + # TODO: Everything here belongs in a proper pydevd package. -class StreamFailure(Exception): - """Something went wrong while handling messages to/from a stream.""" +def parse_message(msg): + """Return a message object for the given "msg" data.""" + if type(msg) is bytes: + return RawMessage.from_bytes(msg) + elif isinstance(msg, str): + return RawMessage.from_bytes(msg) + elif type(msg) is RawMessage: + return msg + else: + raise NotImplementedError - def __init__(self, direction, msg, exception): - err = 'error while processing stream: {!r}'.format(exception) - super(StreamFailure, self).__init__(self, err) - self.direction = direction - self.msg = msg - self.exception = exception - def __repr__(self): - return '{}(direction={!r}, msg={!r}, exception={!r})'.format( - type(self).__name__, - self.direction, - self.msg, - self.exception, - ) +def encode_message(msg): + """Return the message, serialized to the line-format.""" + raw = msg.as_bytes() + if not raw.endswith(b'\n'): + raw += b'\n' + return raw def iter_messages(stream, stop=lambda: False): @@ -36,18 +39,6 @@ def iter_messages(stream, stop=lambda: False): yield StreamFailure('recv', None, exc) -def parse_message(msg): - """Return a message object for the given "msg" data.""" - if type(msg) is bytes: - return RawMessage.from_bytes(msg) - elif isinstance(msg, str): - return RawMessage.from_bytes(msg) - elif type(msg) is RawMessage: - return msg - else: - raise NotImplementedError - - class RawMessage(namedtuple('RawMessage', 'bytes')): """A pydevd message class that leaves the raw bytes unprocessed.""" diff --git a/tests/helpers/socket.py b/tests/helpers/socket.py new file mode 100644 index 000000000..298321543 --- /dev/null +++ b/tests/helpers/socket.py @@ -0,0 +1,38 @@ +from __future__ import absolute_import + +import socket + + +def connect(host, port): + """Return (client, server) after connecting. + + If host is None then it's a server, so it will wait for a connection + on localhost. Otherwise it will connect to the remote host. + """ + sock = socket.socket( + socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + ) + sock.setsockopt( + socket.SOL_SOCKET, + socket.SO_REUSEADDR, + 1, + ) + if host is None: + addr = ('127.0.0.1', port) + server = sock + server.bind(addr) + server.listen(1) + sock, _ = server.accept() + else: + addr = (host, port) + sock.connect(addr) + server = None + return sock, server + + +def close(sock): + """Shutdown and close the socket.""" + sock.shutdown(socket.SHUT_RDWR) + sock.close() diff --git a/tests/helpers/vsc/_fake.py b/tests/helpers/vsc/_fake.py index 3b9d5c605..3fc86b9ee 100644 --- a/tests/helpers/vsc/_fake.py +++ b/tests/helpers/vsc/_fake.py @@ -1,34 +1,23 @@ -import socket import threading -from ._vsc import StreamFailure, encode_message, iter_messages, parse_message -from ._vsc import RawMessage # noqa +from tests.helpers import protocol, socket +from ._vsc import encode_message, iter_messages, parse_message -def socket_close(sock): - sock.shutdown(socket.SHUT_RDWR) - sock.close() +PROTOCOL = protocol.MessageProtocol( + parse=parse_message, + encode=encode_message, + iter=iter_messages, +) -class _Started(object): - - def __init__(self, fake): - self.fake = fake - - def __enter__(self): - return self - - def __exit__(self, *args): - self.close() +class Started(protocol.Started): def send_request(self, msg): return self.fake.send_request(msg) - def close(self): - self.fake.close() - -class FakeVSC(object): +class FakeVSC(protocol.Daemon): """A testing double for a VSC debugger protocol client. This class facilitates sending VSC debugger protocol messages over @@ -58,52 +47,33 @@ class FakeVSC(object): """ # noqa def __init__(self, start_adapter, handler=None): - def start_adapter(host, port, start_adapter=start_adapter): - self._adapter = start_adapter(host, port) - - self._start_adapter = start_adapter - self._handler = handler + super(FakeVSC, self).__init__(socket.connect, PROTOCOL, handler) - self._closed = False - self._received = [] - self._failures = [] + def start_adapter(host, port, _start_adapter=start_adapter): + self._adapter = _start_adapter(host, port) - # These are set when we start. - self._host = None - self._port = None + self._start_adapter = start_adapter self._adapter = None - self._sock = None - self._server = None - self._listener = None - - @property - def addr(self): - host, port = self._host, self._port - if host is None: - host = '127.0.0.1' - return (host, port) - - @property - def received(self): - """All the messages received thus far.""" - return list(self._received) - - @property - def failures(self): - """All send/recv failures thus far.""" - return self._failures def start(self, host, port): """Start the fake and the adapter.""" - if self._closed or self._adapter is not None: + if self._adapter is not None: raise RuntimeError('already started') + return super(FakeVSC, self).start(host, port) + + def send_request(self, req): + """Send the given Request object.""" + return self.send_message(req) - if not host: + # internal methods + + def _start(self, host=None): + start_adapter = (lambda: self._start_adapter(self._host, self._port)) + if not self._host: # The adapter is the server so start it first. - t = threading.Thread( - target=lambda: self._start_adapter(host, port)) + t = threading.Thread(target=start_adapter) t.start() - self._start('127.0.0.1', port) + super(FakeVSC, self)._start('127.0.0.1') t.join(timeout=1) if t.is_alive(): raise RuntimeError('timed out') @@ -111,107 +81,15 @@ def start(self, host, port): # The adapter is the client so start it last. # TODO: For now don't use this. raise NotImplementedError - t = threading.Thread( - target=lambda: self._start(host, port)) + t = threading.Thread(target=super(FakeVSC, self)._start) t.start() - self._start_adapter(host, port) + start_adapter() t.join(timeout=1) if t.is_alive(): raise RuntimeError('timed out') - return _Started(self) - - def send_request(self, req): - """Send the given Request object.""" - if self._closed: - raise EOFError('closed') - req = parse_message(req) - raw = encode_message(req) - try: - self._send(raw) - except Exception as exc: - failure = ('send', req, exc) - self._failures.append(failure) - - def close(self): - """Close the fake's resources (e.g. socket, adapter).""" - if self._closed: - return - - self._closed = True - self._close() - - def assert_received(self, case, expected): - """Ensure that the received messages match the expected ones.""" - received = [parse_message(msg) for msg in self._received] - expected = [parse_message(msg) for msg in expected] - case.assertEqual(received, expected) - - # internal methods - - def _start(self, host, port): - self._host = host - self._port = port - self._connect() - - # TODO: make daemon? - self._listener = threading.Thread(target=self._listen) - self._listener.start() - - def _connect(self): - sock = socket.socket( - socket.AF_INET, - socket.SOCK_STREAM, - socket.IPPROTO_TCP, - ) - sock.setsockopt( - socket.SOL_SOCKET, - socket.SO_REUSEADDR, - 1, - ) - if self._host is None: - server = sock - server.bind(self.addr) - server.listen(1) - sock, _ = server.accept() - else: - sock.connect(self.addr) - server = None - self._server = server - self._sock = sock - - def _listen(self): - with self._sock.makefile('rb') as sockfile: - for msg in iter_messages(sockfile, lambda: self._closed): - if isinstance(msg, StreamFailure): - self._failures.append(msg) - else: - self._add_received(msg) - - def _add_received(self, msg): - self._received.append(msg) - - if self._handler is not None: - self._handler(msg, self.send_request) - - def _send(self, raw): - while raw: - sent = self._sock.send(raw) - raw = raw[sent:] - def _close(self): if self._adapter is not None: self._adapter.close() self._adapter = None - if self._sock is not None: - socket_close(self._sock) - self._sock = None - if self._server is not None: - socket_close(self._server) - self._server = None - if self._listener is not None: - self._listener.join(timeout=1) - # TODO: the listener isn't stopping! - #if self._listener.is_alive(): - # raise RuntimeError('timed out') - self._listener = None + super(FakeVSC, self)._close() diff --git a/tests/helpers/vsc/_vsc.py b/tests/helpers/vsc/_vsc.py index 5db4080b1..9b214a747 100644 --- a/tests/helpers/vsc/_vsc.py +++ b/tests/helpers/vsc/_vsc.py @@ -2,27 +2,22 @@ import json from debugger_protocol.messages import wireformat +from tests.helpers.protocol import StreamFailure # TODO: Use more of the code from debugger_protocol. -class StreamFailure(Exception): - """Something went wrong while handling messages to/from a stream.""" - - def __init__(self, direction, msg, exception): - err = 'error while processing stream: {!r}'.format(exception) - super(StreamFailure, self).__init__(self, err) - self.direction = direction - self.msg = msg - self.exception = exception - - def __repr__(self): - return '{}(direction={!r}, msg={!r}, exception={!r})'.format( - type(self).__name__, - self.direction, - self.msg, - self.exception, - ) +def parse_message(msg): + """Return a message object for the given "msg" data.""" + if type(msg) is str: + data = json.loads(msg) + elif isinstance(msg, bytes): + data = json.loads(msg.decode('utf-8')) + elif type(msg) is RawMessage: + return msg + else: + data = msg + return RawMessage.from_data(**data) def encode_message(msg): @@ -42,19 +37,6 @@ def iter_messages(stream, stop=lambda: False): yield StreamFailure('recv', None, exc) -def parse_message(msg): - """Return a message object for the given "msg" data.""" - if type(msg) is str: - data = json.loads(msg) - elif isinstance(msg, bytes): - data = json.loads(msg.decode('utf-8')) - elif type(msg) is RawMessage: - return msg - else: - data = msg - return RawMessage.from_data(**data) - - class RawMessage(namedtuple('RawMessage', 'data')): """A wrapper around a line-formatted debugger protocol message.""" From 8b2c585f50fe0ceff3dba5622e0e1501dae8d360 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 12 Feb 2018 22:50:48 +0000 Subject: [PATCH 02/11] Add test helper methods. --- tests/helpers/protocol.py | 69 ++++++++++++++++++++++----- tests/helpers/pydevd/_fake.py | 62 ++++++++++++++++++++++-- tests/helpers/pydevd/_pydevd.py | 69 +++++++++++++++++++++++++++ tests/helpers/vsc/_fake.py | 40 ++++++++++++++++ tests/ptvsd/highlevel/__init__.py | 74 +++++++++++++++++++++++++++++ tests/ptvsd/highlevel/test_basic.py | 48 +++++-------------- 6 files changed, 311 insertions(+), 51 deletions(-) diff --git a/tests/helpers/protocol.py b/tests/helpers/protocol.py index 50ee8f5ad..a61ed736e 100644 --- a/tests/helpers/protocol.py +++ b/tests/helpers/protocol.py @@ -58,15 +58,22 @@ class Daemon(object): STARTED = Started - def __init__(self, connect, protocol, handler=None): + @classmethod + def validate_message(cls, msg): + """Ensure the message is legitimate.""" + # By default check nothing. + + def __init__(self, connect, protocol, handler): self._connect = connect self._protocol = protocol - self._handler = handler self._closed = False self._received = [] self._failures = [] + self._handlers = [] + self._default_handler = handler + # These are set when we start. self._host = None self._port = None @@ -101,13 +108,8 @@ def send_message(self, msg): """Serialize msg to the line format and send it to the socket.""" if self._closed: raise EOFError('closed') - msg = self._protocol.parse(msg) - raw = self._protocol.encode(msg) - try: - self._send(raw) - except Exception as exc: - failure = StreamFailure('send', msg, exc) - self._failures.append(failure) + self._validate_message(msg) + self._send_message(msg) def close(self): """Clean up the daemon's resources (e.g. sockets, files, listener).""" @@ -117,6 +119,23 @@ def close(self): self._closed = True self._close() + def add_handler(self, handler, oneoff=True): + """Add the given handler to the list of possible handlers.""" + entry = (handler, 1 if oneoff else None) + self._handlers.append(entry) + return handler + + def reset(self, force=False): + """Clear the recorded messages.""" + if self._failures: + raise RuntimeError('have failures ({!r})'.format(self._failures)) + if self._handlers: + if force: + self._handlers = [] + else: + raise RuntimeError('have pending handlers') + self._received = [] + def assert_received(self, case, expected): """Ensure that the received messages match the expected ones.""" received = [self._protocol.parse(msg) for msg in self._received] @@ -145,9 +164,35 @@ def _listen(self): def _add_received(self, msg): self._received.append(msg) - - if self._handler is not None: - self._handler(msg, self.send_message) + self._handle_message(msg) + + def _handle_message(self, msg): + for i, entry in enumerate(list(self._handlers)): + handle_message, remaining = entry + handled = handle_message(msg, self._send_message) + if handled or handled is None: + if remaining is not None: + if remaining == 1: + self._handlers.pop(i) + else: + self._handlers[i] = (handle_message, remaining-1) + return handled + else: + if self._default_handler is not None: + return self._default_handler(msg, self._send_message) + return False + + def _validate_message(self, msg): + return + + def _send_message(self, msg): + msg = self._protocol.parse(msg) + raw = self._protocol.encode(msg) + try: + self._send(raw) + except Exception as exc: + failure = StreamFailure('send', msg, exc) + self._failures.append(failure) def _send(self, raw): while raw: diff --git a/tests/helpers/pydevd/_fake.py b/tests/helpers/pydevd/_fake.py index 100e3e8fe..4ea13a97d 100644 --- a/tests/helpers/pydevd/_fake.py +++ b/tests/helpers/pydevd/_fake.py @@ -1,5 +1,9 @@ +from _pydevd_bundle.pydevd_comm import ( + CMD_VERSION, +) + from ptvsd.wrapper import start_server, start_client -from ._pydevd import parse_message, encode_message, iter_messages +from ._pydevd import parse_message, encode_message, iter_messages, Message from tests.helpers import protocol @@ -55,13 +59,65 @@ class FakePyDevd(protocol.Daemon): STARTED = Started + VERSION = '1.1.1' + + @classmethod + def validate_message(cls, msg): + """Ensure the message is legitimate.""" + # TODO: Check the message. + + @classmethod + def handle_request(cls, req, send_message, handler=None): + """The default message handler.""" + if handler is not None: + handler(req, send_message) + + resp = cls._get_response(cls, req) + if resp is not None: + send_message(resp) + + @classmethod + def _get_response(cls, req): + try: + cmdid, seq, _ = req + except (IndexError, ValueError): + req = req.msg + cmdid, seq, _ = req + + if cmdid == CMD_VERSION: + return Message(CMD_VERSION, seq, cls.VERSION) + else: + return None + def __init__(self, handler=None): - super(FakePyDevd, self).__init__(_connect, PROTOCOL, handler) + super(FakePyDevd, self).__init__( + _connect, + PROTOCOL, + (lambda msg, send: self.handle_request(msg, send, handler)), + ) def send_response(self, msg): """Send a response message to the adapter (ptvsd).""" - return self.send_message(msg) + # XXX Ensure it's a response? + return self._send_message(msg) def send_event(self, msg): """Send an event message to the adapter (ptvsd).""" + # XXX Ensure it's a request? return self.send_message(msg) + + def add_pending_response(self, cmdid, text): + """Add a response for a request.""" + def handle_request(req, send_message, respid=cmdid): + try: + cmdid, seq, _ = req + except (IndexError, ValueError): + req = req.msg + cmdid, seq, _ = req + if cmdid != respid: + return False + resp = Message(cmdid, seq, text) + send_message(resp) + return True + + self.add_handler(handle_request) diff --git a/tests/helpers/pydevd/_pydevd.py b/tests/helpers/pydevd/_pydevd.py index c29a6bba4..0612d261b 100644 --- a/tests/helpers/pydevd/_pydevd.py +++ b/tests/helpers/pydevd/_pydevd.py @@ -1,4 +1,8 @@ from collections import namedtuple +try: + from urllib.parse import quote, unquote +except ImportError: + from urllib import quote, unquote from tests.helpers.protocol import StreamFailure @@ -13,7 +17,10 @@ def parse_message(msg): return RawMessage.from_bytes(msg) elif type(msg) is RawMessage: return msg + elif type(msg) is Message: + return msg else: + print(msg) raise NotImplementedError @@ -56,6 +63,68 @@ def __new__(cls, raw): self = super(RawMessage, cls).__new__(cls, raw) return self + @property + def msg(self): + try: + return self._msg + except AttributeError: + self._msg = Message.from_bytes(self.bytes) + return self._msg + def as_bytes(self): """Return the line-formatted bytes corresponding to the message.""" return self.bytes + + +class Message(namedtuple('Message', 'cmdid seq payload')): + """A de-seralized PyDevd message.""" + + @classmethod + def from_bytes(cls, raw): + """Return a RawMessage corresponding to the given raw message.""" + raw = RawMessage.from_bytes(raw) + parts = raw.bytes.split(b'\t', 2) + return cls(*parts) + + @classmethod + def parse_payload(cls, payload): + """Return the de-serialized payload.""" + if isinstance(payload, bytes): + payload = payload.decode('utf-8') + if isinstance(payload, str): + text = unquote(payload) + return cls._parse_payload_text(text) + elif hasattr(payload, 'as_text'): + return payload + else: + raise ValueError('unsupported payload {!r}'.format(payload)) + + @classmethod + def _parse_payload_text(cls, text): + # TODO: convert to the appropriate payload type. + return text + + def __new__(cls, cmdid, seq, payload): + cmdid = int(cmdid) if cmdid or cmdid == 0 else None + seq = int(seq) if seq or seq == 0 else None + payload = cls.parse_payload(payload) + self = super(Message, cls).__new__(cls, cmdid, seq, payload) + return self + + def __init__(self, *args, **kwargs): + if self.cmdid is None: + raise TypeError('missing cmdid') + if self.seq is None: + raise TypeError('missing seq') + + def as_bytes(self): + """Return the line-formatted bytes corresponding to the message.""" + try: + payload_as_text = self.payload.as_text + except AttributeError: + text = self.payload + else: + text = payload_as_text() + payload = quote(text) + data = '{}\t{}\t{}'.format(self.cmdid, self.seq, payload) + return data.encode('utf-8') diff --git a/tests/helpers/vsc/_fake.py b/tests/helpers/vsc/_fake.py index 3fc86b9ee..d66b3d96c 100644 --- a/tests/helpers/vsc/_fake.py +++ b/tests/helpers/vsc/_fake.py @@ -1,3 +1,4 @@ +import contextlib import threading from tests.helpers import protocol, socket @@ -65,6 +66,28 @@ def send_request(self, req): """Send the given Request object.""" return self.send_message(req) + def wait_for_response(self, req, **kwargs): + reqseq = req['seq'] + command = req['command'] + + def match(msg): + msg = msg.data + if msg['type'] != 'response' or msg['request_seq'] != reqseq: + return False + assert(msg['command'] == command) + return True + + return self._wait_for_message(match, req, **kwargs) + + def wait_for_event(self, event, **kwargs): + def match(msg): + msg = msg.data + if msg['type'] != 'event' or msg['event'] != event: + return False + return True + + return self._wait_for_message(match, req=None, **kwargs) + # internal methods def _start(self, host=None): @@ -93,3 +116,20 @@ def _close(self): self._adapter.close() self._adapter = None super(FakeVSC, self)._close() + + @contextlib.contextmanager + def _wait_for_message(self, match, req=None, timeout=1): + lock = threading.Lock() + lock.acquire() + + def handle_message(msg, _): + if match(msg): + lock.release() + else: + return False + self.add_handler(handle_message) + + yield req + + lock.acquire(timeout=timeout) # Wait for the message to match. + lock.release() diff --git a/tests/ptvsd/highlevel/__init__.py b/tests/ptvsd/highlevel/__init__.py index 678df08cb..565f1213c 100644 --- a/tests/ptvsd/highlevel/__init__.py +++ b/tests/ptvsd/highlevel/__init__.py @@ -1,6 +1,10 @@ import platform import unittest +from _pydevd_bundle.pydevd_comm import ( + CMD_VERSION, +) + from tests.helpers.pydevd import FakePyDevd from tests.helpers.vsc import FakeVSC @@ -9,11 +13,81 @@ class HighlevelTestCase(unittest.TestCase): + """The base class for high-level ptvsd tests.""" + + MIN_INITIALIZE_ARGS = { + 'adapterID': 'spam', + } + + def next_vsc_seq(self): + try: + seq = self._seq + except AttributeError: + seq = 0 + self._seq = seq + 1 + return seq def new_fake(self, pydevd=None, handler=None): + """Return a new fake VSC that may be used in tests.""" if pydevd is None: pydevd = FakePyDevd() vsc = FakeVSC(pydevd.start, handler) self.addCleanup(vsc.close) return vsc, pydevd + + def attach(self, vsc, pydevd, **kwargs): + """Initialize the debugger protocol and then attach.""" + self._handshake(vsc, pydevd, 'attach', **kwargs) + + def launch(self, vsc, pydevd, **kwargs): + """Initialize the debugger protocol and then launch.""" + self._handshake(vsc, pydevd, 'launch', **kwargs) + + def _handshake(self, vsc, pydevd, command, reset=True, **kwargs): + initargs = dict( + kwargs.pop('initargs', None) or {}, + disconnect=kwargs.pop('disconnect', True), + ) + with vsc.wait_for_event('initialized'): + self._initialize(vsc, pydevd, **initargs) + req = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': command, + 'arguments': kwargs, + } + with vsc.wait_for_response(req): + vsc.send_request(req) + if reset: + vsc.reset() + pydevd.reset() + + def _initialize(self, vsc, pydevd, disconnect=True, **reqargs): + """ + See https://code.visualstudio.com/docs/extensionAPI/api-debugging#_the-vs-code-debug-protocol-in-a-nutshell + """ # noqa + pydevd.add_pending_response(CMD_VERSION, pydevd.VERSION) + req = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'initialize', + 'arguments': dict(self.MIN_INITIALIZE_ARGS, **reqargs), + } + with vsc.wait_for_response(req): + vsc.send_request(req) + + if disconnect: + self.addCleanup(self.disconnect) + + def disconnect(self, vsc, **reqargs): + req = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'disconnect', + 'arguments': reqargs, + } + with vsc.wait_for_response(req): + vsc.send_request(req) + # TODO: wait for an exit event? +# vsc.close() diff --git a/tests/ptvsd/highlevel/test_basic.py b/tests/ptvsd/highlevel/test_basic.py index 10aa2c4b9..74ca883f9 100644 --- a/tests/ptvsd/highlevel/test_basic.py +++ b/tests/ptvsd/highlevel/test_basic.py @@ -1,11 +1,9 @@ -import threading import unittest from _pydevd_bundle.pydevd_comm import ( CMD_VERSION, ) -from tests.helpers.pydevd import FakePyDevd from . import OS_ID, HighlevelTestCase @@ -15,7 +13,10 @@ class LivecycleTests(HighlevelTestCase): See https://code.visualstudio.com/docs/extensionAPI/api-debugging#_the-vs-code-debug-protocol-in-a-nutshell """ # noqa - def test_startup(self): + def test_attach(self): + raise NotImplementedError + + def test_launch(self): raise NotImplementedError def test_shutdown(self): @@ -25,45 +26,20 @@ def test_shutdown(self): class MessageTests(HighlevelTestCase): def test_initialize(self): - self.pseq = -1 - plock = threading.Lock() - plock.acquire() - - def handle_pydevd(msg, _): - try: - seq = msg.bytes.split(b'\t')[1] - except IndexError: - return - self.pseq = int(seq.decode('utf-8')) - plock.release() - pydevd = FakePyDevd(handle_pydevd) - - self.num_left_vsc = 2 - vlock = threading.Lock() - vlock.acquire() + vsc, pydevd = self.new_fake() - def handle_vsp(msg, _): - if self.num_left_vsc == 0: - return - self.num_left_vsc -= 1 - if self.num_left_vsc == 0: - vlock.release() - vsc, _ = self.new_fake(pydevd, handle_vsp) with vsc.start(None, 8888): - vsc.send_request({ + pydevd.add_pending_response(CMD_VERSION, pydevd.VERSION) + req = { 'type': 'request', - 'seq': 42, + 'seq': self.next_vsc_seq(), 'command': 'initialize', 'arguments': { 'adapterID': 'spam', }, - }) - plock.acquire(timeout=1) - pydevd.send_response( - '{}\t{}\t'.format(CMD_VERSION, self.pseq)) - plock.release() - vlock.acquire(timeout=1) # wait for 2 messages to come back - vlock.release() + } + with vsc.wait_for_response(req): + vsc.send_request(req) self.maxDiff = None self.assertFalse(pydevd.failures) @@ -72,7 +48,7 @@ def handle_vsp(msg, _): { 'type': 'response', 'seq': 0, - 'request_seq': 42, + 'request_seq': req['seq'], 'command': 'initialize', 'success': True, 'message': '', From a1cfda159a918c4e73141b7b48689a392dbf552c Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 12 Feb 2018 23:06:44 +0000 Subject: [PATCH 03/11] Add lifecycle tests. --- tests/ptvsd/highlevel/test_basic.py | 199 ++++++++++++++++++++++++++-- 1 file changed, 191 insertions(+), 8 deletions(-) diff --git a/tests/ptvsd/highlevel/test_basic.py b/tests/ptvsd/highlevel/test_basic.py index 74ca883f9..1852b96c0 100644 --- a/tests/ptvsd/highlevel/test_basic.py +++ b/tests/ptvsd/highlevel/test_basic.py @@ -1,5 +1,3 @@ -import unittest - from _pydevd_bundle.pydevd_comm import ( CMD_VERSION, ) @@ -7,20 +5,206 @@ from . import OS_ID, HighlevelTestCase -@unittest.skip('finish!') class LivecycleTests(HighlevelTestCase): """ See https://code.visualstudio.com/docs/extensionAPI/api-debugging#_the-vs-code-debug-protocol-in-a-nutshell """ # noqa def test_attach(self): - raise NotImplementedError + vsc, pydevd = self.new_fake() + + with vsc.start(None, 8888): + with vsc.wait_for_event('initialized'): + # initialize + pydevd.add_pending_response(CMD_VERSION, pydevd.VERSION) + req_initialize = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'initialize', + 'arguments': { + 'adapterID': 'spam', + }, + } + with vsc.wait_for_response(req_initialize): + vsc.send_request(req_initialize) + + # attach + req_attach = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'attach', + 'arguments': {}, + } + with vsc.wait_for_response(req_attach): + vsc.send_request(req_attach) + + # end + req_disconnect = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'disconnect', + 'arguments': {}, + } + with vsc.wait_for_response(req_disconnect): + vsc.send_request(req_disconnect) + + self.assertFalse(pydevd.failures) + self.assertFalse(vsc.failures) + vsc.assert_received(self, [ + { + 'type': 'response', + 'seq': 0, + 'request_seq': req_initialize['seq'], + 'command': 'initialize', + 'success': True, + 'message': '', + 'body': dict( + supportsExceptionInfoRequest=True, + supportsConfigurationDoneRequest=True, + supportsConditionalBreakpoints=True, + supportsSetVariable=True, + supportsExceptionOptions=True, + exceptionBreakpointFilters=[ + { + 'filter': 'raised', + 'label': 'Raised Exceptions', + 'default': 'true' + }, + { + 'filter': 'uncaught', + 'label': 'Uncaught Exceptions', + 'default': 'true' + }, + ], + ), + }, + { + 'type': 'event', + 'seq': 1, + 'event': 'initialized', + 'body': {}, + }, + { + 'type': 'response', + 'seq': 2, + 'request_seq': req_attach['seq'], + 'command': 'attach', + 'success': True, + 'message': '', + 'body': {}, + }, + { + 'type': 'response', + 'seq': 3, + 'request_seq': req_disconnect['seq'], + 'command': 'disconnect', + 'success': True, + 'message': '', + 'body': {}, + }, + ]) + seq = 1000000000 + pydevd.assert_received(self, [ + '{}\t{}\t1.1\t{}\tID'.format(CMD_VERSION, seq, OS_ID), + ]) def test_launch(self): - raise NotImplementedError + vsc, pydevd = self.new_fake() + + with vsc.start(None, 8888): + with vsc.wait_for_event('initialized'): + # initialize + pydevd.add_pending_response(CMD_VERSION, pydevd.VERSION) + req_initialize = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'initialize', + 'arguments': { + 'adapterID': 'spam', + }, + } + with vsc.wait_for_response(req_initialize): + vsc.send_request(req_initialize) + + # launch + req_launch = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'launch', + 'arguments': {}, + } + with vsc.wait_for_response(req_launch): + vsc.send_request(req_launch) + + # end + req_disconnect = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'disconnect', + 'arguments': {}, + } + with vsc.wait_for_response(req_disconnect): + vsc.send_request(req_disconnect) - def test_shutdown(self): - raise NotImplementedError + self.assertFalse(pydevd.failures) + self.assertFalse(vsc.failures) + vsc.assert_received(self, [ + { + 'type': 'response', + 'seq': 0, + 'request_seq': req_initialize['seq'], + 'command': 'initialize', + 'success': True, + 'message': '', + 'body': dict( + supportsExceptionInfoRequest=True, + supportsConfigurationDoneRequest=True, + supportsConditionalBreakpoints=True, + supportsSetVariable=True, + supportsExceptionOptions=True, + exceptionBreakpointFilters=[ + { + 'filter': 'raised', + 'label': 'Raised Exceptions', + 'default': 'true' + }, + { + 'filter': 'uncaught', + 'label': 'Uncaught Exceptions', + 'default': 'true' + }, + ], + ), + }, + { + 'type': 'event', + 'seq': 1, + 'event': 'initialized', + 'body': {}, + }, + { + 'type': 'response', + 'seq': 2, + 'request_seq': req_launch['seq'], + 'command': 'launch', + 'success': True, + 'message': '', + 'body': {}, + }, + { + 'type': 'response', + 'seq': 3, + 'request_seq': req_disconnect['seq'], + 'command': 'disconnect', + 'success': True, + 'message': '', + 'body': {}, + }, + ]) + seq = 1000000000 + pydevd.assert_received(self, [ + '{}\t{}\t1.1\t{}\tID'.format(CMD_VERSION, seq, OS_ID), + ]) class MessageTests(HighlevelTestCase): @@ -41,7 +225,6 @@ def test_initialize(self): with vsc.wait_for_response(req): vsc.send_request(req) - self.maxDiff = None self.assertFalse(pydevd.failures) self.assertFalse(vsc.failures) vsc.assert_received(self, [ From b0e3c786f61161e771792145e74b1b79a354bd0c Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 12 Feb 2018 23:24:01 +0000 Subject: [PATCH 04/11] Compare by message part. --- tests/helpers/pydevd/_pydevd.py | 9 +++++---- tests/ptvsd/highlevel/test_basic.py | 9 ++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/helpers/pydevd/_pydevd.py b/tests/helpers/pydevd/_pydevd.py index 0612d261b..68a6cfd05 100644 --- a/tests/helpers/pydevd/_pydevd.py +++ b/tests/helpers/pydevd/_pydevd.py @@ -12,15 +12,16 @@ def parse_message(msg): """Return a message object for the given "msg" data.""" if type(msg) is bytes: - return RawMessage.from_bytes(msg) + return Message.from_bytes(msg) elif isinstance(msg, str): - return RawMessage.from_bytes(msg) + return Message.from_bytes(msg) elif type(msg) is RawMessage: - return msg + return msg.msg elif type(msg) is Message: return msg + elif isinstance(msg, tuple): + return Message(*msg) else: - print(msg) raise NotImplementedError diff --git a/tests/ptvsd/highlevel/test_basic.py b/tests/ptvsd/highlevel/test_basic.py index 1852b96c0..2964b9f2e 100644 --- a/tests/ptvsd/highlevel/test_basic.py +++ b/tests/ptvsd/highlevel/test_basic.py @@ -104,8 +104,9 @@ def test_attach(self): }, ]) seq = 1000000000 + text = '\t'.join(['1.1', OS_ID, 'ID']) pydevd.assert_received(self, [ - '{}\t{}\t1.1\t{}\tID'.format(CMD_VERSION, seq, OS_ID), + (CMD_VERSION, seq, text), ]) def test_launch(self): @@ -202,8 +203,9 @@ def test_launch(self): }, ]) seq = 1000000000 + text = '\t'.join(['1.1', OS_ID, 'ID']) pydevd.assert_received(self, [ - '{}\t{}\t1.1\t{}\tID'.format(CMD_VERSION, seq, OS_ID), + (CMD_VERSION, seq, text), ]) @@ -263,6 +265,7 @@ def test_initialize(self): }, ]) seq = 1000000000 + text = '\t'.join(['1.1', OS_ID, 'ID']) pydevd.assert_received(self, [ - '{}\t{}\t1.1\t{}\tID'.format(CMD_VERSION, seq, OS_ID), + (CMD_VERSION, seq, text), ]) From 6ae0264e3500b128b301fe09ead0dcdaa1ae0e6e Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 12 Feb 2018 23:24:44 +0000 Subject: [PATCH 05/11] Disconnect when done. --- tests/ptvsd/highlevel/__init__.py | 2 +- tests/ptvsd/highlevel/test_basic.py | 26 +++++++++++++++----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/ptvsd/highlevel/__init__.py b/tests/ptvsd/highlevel/__init__.py index 565f1213c..40de28c57 100644 --- a/tests/ptvsd/highlevel/__init__.py +++ b/tests/ptvsd/highlevel/__init__.py @@ -78,7 +78,7 @@ def _initialize(self, vsc, pydevd, disconnect=True, **reqargs): vsc.send_request(req) if disconnect: - self.addCleanup(self.disconnect) + self.addCleanup(lambda: self.disconnect(vsc)) def disconnect(self, vsc, **reqargs): req = { diff --git a/tests/ptvsd/highlevel/test_basic.py b/tests/ptvsd/highlevel/test_basic.py index 2964b9f2e..4eb625df3 100644 --- a/tests/ptvsd/highlevel/test_basic.py +++ b/tests/ptvsd/highlevel/test_basic.py @@ -215,17 +215,21 @@ def test_initialize(self): vsc, pydevd = self.new_fake() with vsc.start(None, 8888): - pydevd.add_pending_response(CMD_VERSION, pydevd.VERSION) - req = { - 'type': 'request', - 'seq': self.next_vsc_seq(), - 'command': 'initialize', - 'arguments': { - 'adapterID': 'spam', - }, - } - with vsc.wait_for_response(req): - vsc.send_request(req) + try: + pydevd.add_pending_response(CMD_VERSION, pydevd.VERSION) + req = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'initialize', + 'arguments': { + 'adapterID': 'spam', + }, + } + with vsc.wait_for_response(req): + vsc.send_request(req) + finally: + self.disconnect(vsc) + vsc._received.pop(-1) self.assertFalse(pydevd.failures) self.assertFalse(vsc.failures) From e2e1cd9b1f806e49eb3d55dafd66fe586c66e701 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 13 Feb 2018 00:22:26 +0000 Subject: [PATCH 06/11] Add config handling. --- tests/helpers/pydevd/_fake.py | 2 +- tests/ptvsd/highlevel/__init__.py | 38 +++++++++++- tests/ptvsd/highlevel/test_basic.py | 89 ++++++++++++++++++++++++++--- 3 files changed, 120 insertions(+), 9 deletions(-) diff --git a/tests/helpers/pydevd/_fake.py b/tests/helpers/pydevd/_fake.py index 4ea13a97d..4e7d9bb30 100644 --- a/tests/helpers/pydevd/_fake.py +++ b/tests/helpers/pydevd/_fake.py @@ -72,7 +72,7 @@ def handle_request(cls, req, send_message, handler=None): if handler is not None: handler(req, send_message) - resp = cls._get_response(cls, req) + resp = cls._get_response(req) if resp is not None: send_message(resp) diff --git a/tests/ptvsd/highlevel/__init__.py b/tests/ptvsd/highlevel/__init__.py index 40de28c57..65dd86984 100644 --- a/tests/ptvsd/highlevel/__init__.py +++ b/tests/ptvsd/highlevel/__init__.py @@ -44,7 +44,9 @@ def launch(self, vsc, pydevd, **kwargs): """Initialize the debugger protocol and then launch.""" self._handshake(vsc, pydevd, 'launch', **kwargs) - def _handshake(self, vsc, pydevd, command, reset=True, **kwargs): + def _handshake(self, vsc, pydevd, command, + breakpoints=None, excbreakpoints=None, + reset=True, **kwargs): initargs = dict( kwargs.pop('initargs', None) or {}, disconnect=kwargs.pop('disconnect', True), @@ -59,6 +61,36 @@ def _handshake(self, vsc, pydevd, command, reset=True, **kwargs): } with vsc.wait_for_response(req): vsc.send_request(req) + + # Handle breakpoints + if breakpoints: + args = self._parse_breakpoints(breakpoints) + req = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'setBreakpoints', + 'arguments': self._parse_breakpoints(breakpoints), + } + with vsc.wait_for_response(req): + vsc.send_request(req) + if excbreakpoints: + req = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'setExceptionBreakpoints', + 'arguments': self._parse_breakpoints(excbreakpoints), + } + with vsc.wait_for_response(req): + vsc.send_request(req) + req = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'configurationDone', + 'arguments': {} + } + with vsc.wait_for_response(req): + vsc.send_request(req) + if reset: vsc.reset() pydevd.reset() @@ -80,6 +112,10 @@ def _initialize(self, vsc, pydevd, disconnect=True, **reqargs): if disconnect: self.addCleanup(lambda: self.disconnect(vsc)) + def _parse_breakpoints(self, breakpoints): + raise NotImplementedError + #for bp in breakpoints or (): + def disconnect(self, vsc, **reqargs): req = { 'type': 'request', diff --git a/tests/ptvsd/highlevel/test_basic.py b/tests/ptvsd/highlevel/test_basic.py index 4eb625df3..46cea0490 100644 --- a/tests/ptvsd/highlevel/test_basic.py +++ b/tests/ptvsd/highlevel/test_basic.py @@ -1,5 +1,6 @@ from _pydevd_bundle.pydevd_comm import ( CMD_VERSION, + CMD_RUN, ) from . import OS_ID, HighlevelTestCase @@ -38,6 +39,16 @@ def test_attach(self): with vsc.wait_for_response(req_attach): vsc.send_request(req_attach) + # configuration + req_config = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'configurationDone', + 'arguments': {} + } + with vsc.wait_for_response(req_config): + vsc.send_request(req_config) + # end req_disconnect = { 'type': 'request', @@ -96,6 +107,15 @@ def test_attach(self): { 'type': 'response', 'seq': 3, + 'request_seq': req_config['seq'], + 'command': 'configurationDone', + 'success': True, + 'message': '', + 'body': {}, + }, + { + 'type': 'response', + 'seq': 4, 'request_seq': req_disconnect['seq'], 'command': 'disconnect', 'success': True, @@ -103,10 +123,14 @@ def test_attach(self): 'body': {}, }, ]) - seq = 1000000000 - text = '\t'.join(['1.1', OS_ID, 'ID']) pydevd.assert_received(self, [ - (CMD_VERSION, seq, text), + # (cmdid, seq, text) + ( + CMD_VERSION, + 1000000000, + '\t'.join(['1.1', OS_ID, 'ID']), + ), + (CMD_RUN, 1000000001, ''), ]) def test_launch(self): @@ -137,6 +161,16 @@ def test_launch(self): with vsc.wait_for_response(req_launch): vsc.send_request(req_launch) + # configuration + req_config = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'configurationDone', + 'arguments': {} + } + with vsc.wait_for_response(req_config): + vsc.send_request(req_config) + # end req_disconnect = { 'type': 'request', @@ -195,6 +229,15 @@ def test_launch(self): { 'type': 'response', 'seq': 3, + 'request_seq': req_config['seq'], + 'command': 'configurationDone', + 'success': True, + 'message': '', + 'body': {}, + }, + { + 'type': 'response', + 'seq': 4, 'request_seq': req_disconnect['seq'], 'command': 'disconnect', 'success': True, @@ -202,14 +245,46 @@ def test_launch(self): 'body': {}, }, ]) - seq = 1000000000 - text = '\t'.join(['1.1', OS_ID, 'ID']) pydevd.assert_received(self, [ - (CMD_VERSION, seq, text), + # (cmdid, seq, text) + ( + CMD_VERSION, + 1000000000, + '\t'.join(['1.1', OS_ID, 'ID']), + ), + (CMD_RUN, 1000000001, ''), ]) -class MessageTests(HighlevelTestCase): +class RequestTests(HighlevelTestCase): + """ + lifecycle (in order): + + initialize + attach + launch + setBreakpoints + setExceptionBreakpoints + configurationDone + disconnect + + normal operation: + + threads + stackTrace + scopes + variables + setVariable + evaluate + pause + continue + next + stepIn + stepOut + setBreakpoints + setExceptionBreakpoints + exceptionInfo + """ def test_initialize(self): vsc, pydevd = self.new_fake() From 00791b7952e16c75acda5d66665208e407ad09f6 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 13 Feb 2018 00:27:04 +0000 Subject: [PATCH 07/11] Disconnect sooner. --- tests/ptvsd/highlevel/__init__.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/ptvsd/highlevel/__init__.py b/tests/ptvsd/highlevel/__init__.py index 65dd86984..c5eb10337 100644 --- a/tests/ptvsd/highlevel/__init__.py +++ b/tests/ptvsd/highlevel/__init__.py @@ -1,3 +1,4 @@ +import contextlib import platform import unittest @@ -36,6 +37,15 @@ def new_fake(self, pydevd=None, handler=None): return vsc, pydevd + @contextlib.contextmanager + def launched(self, vsc, pydevd, port=8888, **kwargs): + with vsc.start(None, port): + try: + self.launch(vsc, pydevd, **kwargs) + finally: + self.disconnect(vsc) + vsc._received.pop(-1) + def attach(self, vsc, pydevd, **kwargs): """Initialize the debugger protocol and then attach.""" self._handshake(vsc, pydevd, 'attach', **kwargs) @@ -64,7 +74,6 @@ def _handshake(self, vsc, pydevd, command, # Handle breakpoints if breakpoints: - args = self._parse_breakpoints(breakpoints) req = { 'type': 'request', 'seq': self.next_vsc_seq(), @@ -95,7 +104,7 @@ def _handshake(self, vsc, pydevd, command, vsc.reset() pydevd.reset() - def _initialize(self, vsc, pydevd, disconnect=True, **reqargs): + def _initialize(self, vsc, pydevd, **reqargs): """ See https://code.visualstudio.com/docs/extensionAPI/api-debugging#_the-vs-code-debug-protocol-in-a-nutshell """ # noqa @@ -109,9 +118,6 @@ def _initialize(self, vsc, pydevd, disconnect=True, **reqargs): with vsc.wait_for_response(req): vsc.send_request(req) - if disconnect: - self.addCleanup(lambda: self.disconnect(vsc)) - def _parse_breakpoints(self, breakpoints): raise NotImplementedError #for bp in breakpoints or (): From b1e6522ea6dc3f59fb77870ca9a12a25502ebac4 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 13 Feb 2018 00:27:45 +0000 Subject: [PATCH 08/11] Track capabilities. --- tests/helpers/vsc/_fake.py | 6 ++++-- tests/ptvsd/highlevel/__init__.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/helpers/vsc/_fake.py b/tests/helpers/vsc/_fake.py index d66b3d96c..81405ce66 100644 --- a/tests/helpers/vsc/_fake.py +++ b/tests/helpers/vsc/_fake.py @@ -118,13 +118,15 @@ def _close(self): super(FakeVSC, self)._close() @contextlib.contextmanager - def _wait_for_message(self, match, req=None, timeout=1): + def _wait_for_message(self, match, req=None, handler=None, timeout=1): lock = threading.Lock() lock.acquire() - def handle_message(msg, _): + def handle_message(msg, send_message): if match(msg): lock.release() + if handler is not None: + handler(msg, send_message) else: return False self.add_handler(handle_message) diff --git a/tests/ptvsd/highlevel/__init__.py b/tests/ptvsd/highlevel/__init__.py index c5eb10337..81075bb25 100644 --- a/tests/ptvsd/highlevel/__init__.py +++ b/tests/ptvsd/highlevel/__init__.py @@ -108,6 +108,8 @@ def _initialize(self, vsc, pydevd, **reqargs): """ See https://code.visualstudio.com/docs/extensionAPI/api-debugging#_the-vs-code-debug-protocol-in-a-nutshell """ # noqa + def handle_response(resp, _): + self._capabilities = resp.data['body'] pydevd.add_pending_response(CMD_VERSION, pydevd.VERSION) req = { 'type': 'request', @@ -115,7 +117,7 @@ def _initialize(self, vsc, pydevd, **reqargs): 'command': 'initialize', 'arguments': dict(self.MIN_INITIALIZE_ARGS, **reqargs), } - with vsc.wait_for_response(req): + with vsc.wait_for_response(req, handler=handle_response): vsc.send_request(req) def _parse_breakpoints(self, breakpoints): From 09e05b1ee10d0cf3d34b637a1082957054252239 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 13 Feb 2018 00:41:03 +0000 Subject: [PATCH 09/11] Send a CMD_RUN response. --- tests/ptvsd/highlevel/__init__.py | 2 ++ tests/ptvsd/highlevel/test_basic.py | 33 ++++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/tests/ptvsd/highlevel/__init__.py b/tests/ptvsd/highlevel/__init__.py index 81075bb25..69ae6f0bd 100644 --- a/tests/ptvsd/highlevel/__init__.py +++ b/tests/ptvsd/highlevel/__init__.py @@ -4,6 +4,7 @@ from _pydevd_bundle.pydevd_comm import ( CMD_VERSION, + CMD_RUN, ) from tests.helpers.pydevd import FakePyDevd @@ -91,6 +92,7 @@ def _handshake(self, vsc, pydevd, command, } with vsc.wait_for_response(req): vsc.send_request(req) + pydevd.add_pending_response(CMD_RUN, '') req = { 'type': 'request', 'seq': self.next_vsc_seq(), diff --git a/tests/ptvsd/highlevel/test_basic.py b/tests/ptvsd/highlevel/test_basic.py index 46cea0490..4719108ab 100644 --- a/tests/ptvsd/highlevel/test_basic.py +++ b/tests/ptvsd/highlevel/test_basic.py @@ -1,3 +1,6 @@ +import os +import sys + from _pydevd_bundle.pydevd_comm import ( CMD_VERSION, CMD_RUN, @@ -6,7 +9,7 @@ from . import OS_ID, HighlevelTestCase -class LivecycleTests(HighlevelTestCase): +class LifecycleTests(HighlevelTestCase): """ See https://code.visualstudio.com/docs/extensionAPI/api-debugging#_the-vs-code-debug-protocol-in-a-nutshell """ # noqa @@ -40,6 +43,7 @@ def test_attach(self): vsc.send_request(req_attach) # configuration + pydevd.add_pending_response(CMD_RUN, '') req_config = { 'type': 'request', 'seq': self.next_vsc_seq(), @@ -114,8 +118,19 @@ def test_attach(self): 'body': {}, }, { - 'type': 'response', + 'type': 'event', 'seq': 4, + 'event': 'process', + 'body': { + 'name': sys.argv[0], + 'systemProcessId': os.getpid(), + 'isLocalProcess': True, + 'startMethod': 'attach', + }, + }, + { + 'type': 'response', + 'seq': 5, 'request_seq': req_disconnect['seq'], 'command': 'disconnect', 'success': True, @@ -162,6 +177,7 @@ def test_launch(self): vsc.send_request(req_launch) # configuration + pydevd.add_pending_response(CMD_RUN, '') req_config = { 'type': 'request', 'seq': self.next_vsc_seq(), @@ -236,8 +252,19 @@ def test_launch(self): 'body': {}, }, { - 'type': 'response', + 'type': 'event', 'seq': 4, + 'event': 'process', + 'body': { + 'name': sys.argv[0], + 'systemProcessId': os.getpid(), + 'isLocalProcess': True, + 'startMethod': 'launch', + }, + }, + { + 'type': 'response', + 'seq': 5, 'request_seq': req_disconnect['seq'], 'command': 'disconnect', 'success': True, From 06092ca639523f1d747a676f88c91e3689923b86 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 13 Feb 2018 00:58:09 +0000 Subject: [PATCH 10/11] Add a test for "threads". --- tests/ptvsd/highlevel/__init__.py | 3 +- tests/ptvsd/highlevel/test_basic.py | 46 ++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/tests/ptvsd/highlevel/__init__.py b/tests/ptvsd/highlevel/__init__.py index 69ae6f0bd..42e96272f 100644 --- a/tests/ptvsd/highlevel/__init__.py +++ b/tests/ptvsd/highlevel/__init__.py @@ -3,8 +3,8 @@ import unittest from _pydevd_bundle.pydevd_comm import ( - CMD_VERSION, CMD_RUN, + CMD_VERSION, ) from tests.helpers.pydevd import FakePyDevd @@ -43,6 +43,7 @@ def launched(self, vsc, pydevd, port=8888, **kwargs): with vsc.start(None, port): try: self.launch(vsc, pydevd, **kwargs) + yield finally: self.disconnect(vsc) vsc._received.pop(-1) diff --git a/tests/ptvsd/highlevel/test_basic.py b/tests/ptvsd/highlevel/test_basic.py index 4719108ab..a5e7ff41e 100644 --- a/tests/ptvsd/highlevel/test_basic.py +++ b/tests/ptvsd/highlevel/test_basic.py @@ -2,8 +2,9 @@ import sys from _pydevd_bundle.pydevd_comm import ( - CMD_VERSION, + CMD_LIST_THREADS, CMD_RUN, + CMD_VERSION, ) from . import OS_ID, HighlevelTestCase @@ -375,3 +376,46 @@ def test_initialize(self): pydevd.assert_received(self, [ (CMD_VERSION, seq, text), ]) + + def test_threads(self): + vsc, pydevd = self.new_fake() + with self.launched(vsc, pydevd): + pydevd.add_pending_response(CMD_LIST_THREADS, """ + + + + + + """.strip().replace('\n', '')) + req = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'threads', + 'arguments': {}, + } + with vsc.wait_for_response(req): + vsc.send_request(req) + + self.maxDiff = None + self.assertFalse(pydevd.failures) + self.assertFalse(vsc.failures) + vsc.assert_received(self, [ + { + 'type': 'response', + 'seq': 5, + 'request_seq': req['seq'], + 'command': 'threads', + 'success': True, + 'message': '', + 'body': { + 'threads': [ + {'id': 1, 'name': 'spam'}, + {'id': 3, 'name': ''}, + ], + }, + }, + ]) + pydevd.assert_received(self, [ + # (cmdid, seq, text) + (CMD_LIST_THREADS, 1000000002, ''), + ]) From c0234ec8b0ef86669a2aa402640348a9172d0980 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 13 Feb 2018 01:03:30 +0000 Subject: [PATCH 11/11] Split up test_basic.py. --- .../{test_basic.py => test_lifecycle.py} | 138 ----------------- tests/ptvsd/highlevel/test_messages.py | 143 ++++++++++++++++++ 2 files changed, 143 insertions(+), 138 deletions(-) rename tests/ptvsd/highlevel/{test_basic.py => test_lifecycle.py} (69%) create mode 100644 tests/ptvsd/highlevel/test_messages.py diff --git a/tests/ptvsd/highlevel/test_basic.py b/tests/ptvsd/highlevel/test_lifecycle.py similarity index 69% rename from tests/ptvsd/highlevel/test_basic.py rename to tests/ptvsd/highlevel/test_lifecycle.py index a5e7ff41e..2c75b3be8 100644 --- a/tests/ptvsd/highlevel/test_basic.py +++ b/tests/ptvsd/highlevel/test_lifecycle.py @@ -2,7 +2,6 @@ import sys from _pydevd_bundle.pydevd_comm import ( - CMD_LIST_THREADS, CMD_RUN, CMD_VERSION, ) @@ -282,140 +281,3 @@ def test_launch(self): ), (CMD_RUN, 1000000001, ''), ]) - - -class RequestTests(HighlevelTestCase): - """ - lifecycle (in order): - - initialize - attach - launch - setBreakpoints - setExceptionBreakpoints - configurationDone - disconnect - - normal operation: - - threads - stackTrace - scopes - variables - setVariable - evaluate - pause - continue - next - stepIn - stepOut - setBreakpoints - setExceptionBreakpoints - exceptionInfo - """ - - def test_initialize(self): - vsc, pydevd = self.new_fake() - - with vsc.start(None, 8888): - try: - pydevd.add_pending_response(CMD_VERSION, pydevd.VERSION) - req = { - 'type': 'request', - 'seq': self.next_vsc_seq(), - 'command': 'initialize', - 'arguments': { - 'adapterID': 'spam', - }, - } - with vsc.wait_for_response(req): - vsc.send_request(req) - finally: - self.disconnect(vsc) - vsc._received.pop(-1) - - self.assertFalse(pydevd.failures) - self.assertFalse(vsc.failures) - vsc.assert_received(self, [ - { - 'type': 'response', - 'seq': 0, - 'request_seq': req['seq'], - 'command': 'initialize', - 'success': True, - 'message': '', - 'body': dict( - supportsExceptionInfoRequest=True, - supportsConfigurationDoneRequest=True, - supportsConditionalBreakpoints=True, - supportsSetVariable=True, - supportsExceptionOptions=True, - exceptionBreakpointFilters=[ - { - 'filter': 'raised', - 'label': 'Raised Exceptions', - 'default': 'true' - }, - { - 'filter': 'uncaught', - 'label': 'Uncaught Exceptions', - 'default': 'true' - }, - ], - ), - }, - { - 'type': 'event', - 'seq': 1, - 'event': 'initialized', - 'body': {}, - }, - ]) - seq = 1000000000 - text = '\t'.join(['1.1', OS_ID, 'ID']) - pydevd.assert_received(self, [ - (CMD_VERSION, seq, text), - ]) - - def test_threads(self): - vsc, pydevd = self.new_fake() - with self.launched(vsc, pydevd): - pydevd.add_pending_response(CMD_LIST_THREADS, """ - - - - - - """.strip().replace('\n', '')) - req = { - 'type': 'request', - 'seq': self.next_vsc_seq(), - 'command': 'threads', - 'arguments': {}, - } - with vsc.wait_for_response(req): - vsc.send_request(req) - - self.maxDiff = None - self.assertFalse(pydevd.failures) - self.assertFalse(vsc.failures) - vsc.assert_received(self, [ - { - 'type': 'response', - 'seq': 5, - 'request_seq': req['seq'], - 'command': 'threads', - 'success': True, - 'message': '', - 'body': { - 'threads': [ - {'id': 1, 'name': 'spam'}, - {'id': 3, 'name': ''}, - ], - }, - }, - ]) - pydevd.assert_received(self, [ - # (cmdid, seq, text) - (CMD_LIST_THREADS, 1000000002, ''), - ]) diff --git a/tests/ptvsd/highlevel/test_messages.py b/tests/ptvsd/highlevel/test_messages.py new file mode 100644 index 000000000..421bb1743 --- /dev/null +++ b/tests/ptvsd/highlevel/test_messages.py @@ -0,0 +1,143 @@ +from _pydevd_bundle.pydevd_comm import ( + CMD_LIST_THREADS, + CMD_VERSION, +) + +from . import OS_ID, HighlevelTestCase + + +class RequestTests(HighlevelTestCase): + """ + lifecycle (in order): + + initialize + attach + launch + setBreakpoints + setExceptionBreakpoints + configurationDone + disconnect + + normal operation: + + threads + stackTrace + scopes + variables + setVariable + evaluate + pause + continue + next + stepIn + stepOut + setBreakpoints + setExceptionBreakpoints + exceptionInfo + """ + + def test_initialize(self): + vsc, pydevd = self.new_fake() + + with vsc.start(None, 8888): + try: + pydevd.add_pending_response(CMD_VERSION, pydevd.VERSION) + req = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'initialize', + 'arguments': { + 'adapterID': 'spam', + }, + } + with vsc.wait_for_response(req): + vsc.send_request(req) + finally: + self.disconnect(vsc) + vsc._received.pop(-1) + + self.assertFalse(pydevd.failures) + self.assertFalse(vsc.failures) + vsc.assert_received(self, [ + { + 'type': 'response', + 'seq': 0, + 'request_seq': req['seq'], + 'command': 'initialize', + 'success': True, + 'message': '', + 'body': dict( + supportsExceptionInfoRequest=True, + supportsConfigurationDoneRequest=True, + supportsConditionalBreakpoints=True, + supportsSetVariable=True, + supportsExceptionOptions=True, + exceptionBreakpointFilters=[ + { + 'filter': 'raised', + 'label': 'Raised Exceptions', + 'default': 'true' + }, + { + 'filter': 'uncaught', + 'label': 'Uncaught Exceptions', + 'default': 'true' + }, + ], + ), + }, + { + 'type': 'event', + 'seq': 1, + 'event': 'initialized', + 'body': {}, + }, + ]) + seq = 1000000000 + text = '\t'.join(['1.1', OS_ID, 'ID']) + pydevd.assert_received(self, [ + (CMD_VERSION, seq, text), + ]) + + def test_threads(self): + vsc, pydevd = self.new_fake() + with self.launched(vsc, pydevd): + pydevd.add_pending_response(CMD_LIST_THREADS, """ + + + + + + """.strip().replace('\n', '')) + req = { + 'type': 'request', + 'seq': self.next_vsc_seq(), + 'command': 'threads', + 'arguments': {}, + } + with vsc.wait_for_response(req): + vsc.send_request(req) + + self.maxDiff = None + self.assertFalse(pydevd.failures) + self.assertFalse(vsc.failures) + vsc.assert_received(self, [ + { + 'type': 'response', + 'seq': 5, + 'request_seq': req['seq'], + 'command': 'threads', + 'success': True, + 'message': '', + 'body': { + 'threads': [ + {'id': 1, 'name': 'spam'}, + {'id': 3, 'name': ''}, + ], + }, + }, + ]) + pydevd.assert_received(self, [ + # (cmdid, seq, text) + (CMD_LIST_THREADS, 1000000002, ''), + ])