From 6cc5cca1fb0ae195548284bfe99239767caa235e Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 10 Apr 2018 22:29:44 +0000 Subject: [PATCH] Add DebugClient.host_local_debugger(). --- tests/helpers/debugclient.py | 154 ++++++++++++++++++++++++++------ tests/helpers/debugsession.py | 86 ++++++++++++++---- tests/system_tests/test_main.py | 3 +- 3 files changed, 196 insertions(+), 47 deletions(-) diff --git a/tests/helpers/debugclient.py b/tests/helpers/debugclient.py index da204172..d0641e66 100644 --- a/tests/helpers/debugclient.py +++ b/tests/helpers/debugclient.py @@ -1,49 +1,56 @@ +import threading +import warnings + from . import Closeable from .debugadapter import DebugAdapter from .debugsession import DebugSession -class DebugClient(Closeable): - """A high-level abstraction of a debug client (i.e. editor).""" +# TODO: Add a helper function to start a remote debugger for testing +# remote debugging? - def __init__(self, port=8888): - super(DebugClient, self).__init__() + +class _LifecycleClient(Closeable): + + def __init__(self, port=8888, breakpoints=None): + super(_LifecycleClient, self).__init__() self._port = port self._adapter = None self._session = None - # TODO: Support starting a remote debugger for testing - # remote debugging? + self._breakpoints = breakpoints - def start_debugger(self, argv): - if self._adapter is not None: - raise RuntimeError('debugger already running') - self._adapter = DebugAdapter.start(argv, port=self._port) + @property + def adapter(self): return self._adapter - def launch_script(self, filename, *argv, **kwargs): + @property + def session(self): + return self._session + + def start_debugging(self, launchcfg): + if self.closed: + raise RuntimeError('debug client closed') if self._adapter is not None: raise RuntimeError('debugger already running') assert self._session is None - argv = [ - filename, - ] + list(argv) - self._launch(argv, kwargs) - return self._adapter, self._session + raise NotImplementedError - def launch_module(self, module, *argv, **kwargs): - if self._adapter is not None: - raise RuntimeError('debugger already running') - assert self._session is None + def stop_debugging(self): + if self.closed: + raise RuntimeError('debug client closed') + if self._adapter is None: + raise RuntimeError('debugger not running') - argv = [ - '-m', module, - ] + list(argv) - self._launch(argv, kwargs) - return self._adapter, self._session + if self._session is not None: + self._detach() + self._adapter.close() + self._adapter = None def attach(self, **kwargs): + if self.closed: + raise RuntimeError('debug client closed') if self._adapter is None: raise RuntimeError('debugger not running') if self._session is not None: @@ -53,27 +60,118 @@ class DebugClient(Closeable): return self._session def detach(self): + if self.closed: + raise RuntimeError('debug client closed') if self._session is None: raise RuntimeError('not attached') + assert self._adapter is not None + if not self._session.is_client: + raise RuntimeError('detach not supported') + self._detach() # internal methods def _close(self): + if self._session is not None: + self._session.close() if self._adapter is not None: self._adapter.close() - def _launch(self, argv, kwargs): + def _launch(self, argv, script=None, wait_for_attach=None, + detachable=True, **kwargs): self._adapter = DebugAdapter.start( argv, + host='localhost' if detachable else None, port=self._port, + script=script, ) - self._attach(**kwargs) + if wait_for_attach: + wait_for_attach() + else: + self._attach(**kwargs) def _attach(self, **kwargs): addr = ('localhost', self._port) - self._session = DebugSession.create(addr, **kwargs) + self._session = DebugSession.create_client(addr, **kwargs) def _detach(self): self._session.close() self._session = None + + +class DebugClient(_LifecycleClient): + """A high-level abstraction of a debug client (i.e. editor).""" + + # TODO: Manage breakpoints, etc. + + +class EasyDebugClient(DebugClient): + + def start_detached(self, argv): + """Start an adapter in a background process.""" + if self.closed: + raise RuntimeError('debug client closed') + if self._adapter is not None: + raise RuntimeError('debugger already running') + assert self._session is None + + # TODO: Launch, handshake and detach? + self._adapter = DebugAdapter.start(argv, port=self._port) + return self._adapter + + def host_local_debugger(self, argv, script=None, **kwargs): + if self.closed: + raise RuntimeError('debug client closed') + if self._adapter is not None: + raise RuntimeError('debugger already running') + assert self._session is None + addr = ('localhost', self._port) + + def run(): + self._session = DebugSession.create_server(addr, **kwargs) + t = threading.Thread(target=run) + t.start() + + def wait(): + t.join(timeout=self._connecttimeout) + if t.is_alive(): + warnings.warn('timed out waiting for connection') + if self._session is None: + raise RuntimeError('unable to connect') + # Close the adapter when the session closes. + self._session.manage_adapter(self._adapter) + self._launch( + argv, + script=script, + wait_for_attach=wait, + detachable=False, + ) + + return self._adapter, self._session + + def launch_script(self, filename, *argv, **kwargs): + if self.closed: + raise RuntimeError('debug client closed') + if self._adapter is not None: + raise RuntimeError('debugger already running') + assert self._session is None + + argv = [ + filename, + ] + list(argv) + self._launch(argv, **kwargs) + return self._adapter, self._session + + def launch_module(self, module, *argv, **kwargs): + if self.closed: + raise RuntimeError('debug client closed') + if self._adapter is not None: + raise RuntimeError('debugger already running') + assert self._session is None + + argv = [ + '-m', module, + ] + list(argv) + self._launch(argv, **kwargs) + return self._adapter, self._session diff --git a/tests/helpers/debugsession.py b/tests/helpers/debugsession.py index 451766fb..c1545eee 100644 --- a/tests/helpers/debugsession.py +++ b/tests/helpers/debugsession.py @@ -12,7 +12,10 @@ from .message import ( raw_read_all as read_messages, raw_write_one as write_message ) -from .socket import create_client, close, recv_as_read, send_as_write +from .socket import ( + Connection, create_server, create_client, close, + recv_as_read, send_as_write, + timeout as socket_timeout) from .threading import get_locked_and_waiter from .vsc import parse_message @@ -25,22 +28,38 @@ class DebugSessionConnection(Closeable): TIMEOUT = 1.0 @classmethod - def create(cls, addr, timeout=TIMEOUT): + def create_client(cls, addr, **kwargs): + def connect(addr, timeout): + sock = create_client() + for _ in range(int(timeout * 10)): + try: + sock.connect(addr) + except OSError: + if cls.VERBOSE: + print('+', end='') + sys.stdout.flush() + time.sleep(0.1) + else: + break + else: + raise RuntimeError('could not connect') + return sock + return cls._create(connect, addr, **kwargs) + + @classmethod + def create_server(cls, addr, **kwargs): + def connect(addr, timeout): + server = create_server(addr) + with socket_timeout(server, timeout): + client = server.accept() + return Connection(client, server) + return cls._create(connect, addr, **kwargs) + + @classmethod + def _create(cls, connect, addr, timeout=None): if timeout is None: timeout = cls.TIMEOUT - sock = create_client() - for _ in range(int(timeout * 10)): - try: - sock.connect(addr) - except OSError: - if cls.VERBOSE: - print('+', end='') - sys.stdout.flush() - time.sleep(0.1) - else: - break - else: - raise RuntimeError('could not connect') + sock = connect(addr, timeout) if cls.VERBOSE: print('connected') self = cls(sock, ownsock=True) @@ -52,7 +71,14 @@ class DebugSessionConnection(Closeable): self._sock = sock self._ownsock = ownsock + @property + def is_client(self): + return self._server is None + def iter_messages(self): + if self.closed: + raise RuntimeError('connection closed') + def stop(): return self.closed read = recv_as_read(self._sock) @@ -62,6 +88,9 @@ class DebugSessionConnection(Closeable): yield parse_message(msg) def send(self, req): + if self.closed: + raise RuntimeError('connection closed') + def stop(): return self.closed write = send_as_write(self._sock) @@ -84,10 +113,17 @@ class DebugSession(Closeable): PORT = 8888 @classmethod - def create(cls, addr=None, **kwargs): + def create_client(cls, addr=None, **kwargs): if addr is None: addr = (cls.HOST, cls.PORT) - conn = DebugSessionConnection.create(addr) + conn = DebugSessionConnection.create_client(addr) + return cls(conn, owned=True, **kwargs) + + @classmethod + def create_server(cls, addr=None, **kwargs): + if addr is None: + addr = (cls.HOST, cls.PORT) + conn = DebugSessionConnection.create_server(addr) return cls(conn, owned=True, **kwargs) def __init__(self, conn, seq=1000, handlers=(), timeout=None, owned=False): @@ -107,11 +143,18 @@ class DebugSession(Closeable): self._listenerthread = threading.Thread(target=self._listen) self._listenerthread.start() + @property + def is_client(self): + return self._conn.is_client + @property def received(self): return list(self._received) def send_request(self, command, **args): + if self.closed: + raise RuntimeError('session closed') + wait = args.pop('wait', True) seq = self._seq self._seq += 1 @@ -129,10 +172,16 @@ class DebugSession(Closeable): return req def add_handler(self, handler, **kwargs): + if self.closed: + raise RuntimeError('session closed') + self._add_handler(handler, **kwargs) @contextlib.contextmanager def wait_for_event(self, event, **kwargs): + if self.closed: + raise RuntimeError('session closed') + def match(msg): return msg.type == 'event' and msg.event == event handlername = 'event {!r}'.format(event) @@ -141,6 +190,9 @@ class DebugSession(Closeable): @contextlib.contextmanager def wait_for_response(self, req, **kwargs): + if self.closed: + raise RuntimeError('session closed') + try: command, seq = req.command, req.seq except AttributeError: diff --git a/tests/system_tests/test_main.py b/tests/system_tests/test_main.py index edc5a75b..2bf41e01 100644 --- a/tests/system_tests/test_main.py +++ b/tests/system_tests/test_main.py @@ -1,7 +1,6 @@ import unittest -from tests.helpers.debugadapter import DebugAdapter -from tests.helpers.debugclient import DebugClient +from tests.helpers.debugclient import EasyDebugClient as DebugClient from tests.helpers.threading import get_locked_and_waiter from tests.helpers.vsc import parse_message from tests.helpers.workspace import Workspace, PathEntry