debugpy/tests/helpers/debugclient.py
Karthik Nadig 5f777818aa
Tests for break on start, wait for normal and abnormal exit (#952)
* Add variables and evaluate tests

* Add sort order and evaluate tests

* Ensure the last output is seen

* Add stop on entry tests, normal and abnormal exit tests

* Fix tests for 2.7

* Fix minor linting issue
2018-10-24 12:37:51 -07:00

270 lines
8 KiB
Python

from __future__ import absolute_import
import os
import traceback
import warnings
from ptvsd.socket import Address
from ptvsd._util import new_hidden_thread, Closeable, ClosedError
from .debugadapter import DebugAdapter, wait_for_socket_server
from .debugsession import DebugSession
# TODO: Add a helper function to start a remote debugger for testing
# remote debugging?
class _LifecycleClient(Closeable):
SESSION = DebugSession
def __init__(
self,
addr=None,
port=8888,
breakpoints=None,
connecttimeout=1.0,
):
super(_LifecycleClient, self).__init__()
self._addr = Address.from_raw(addr, defaultport=port)
self._connecttimeout = connecttimeout
self._adapter = None
self._session = None
self._breakpoints = breakpoints
@property
def adapter(self):
return self._adapter
@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
raise NotImplementedError
def stop_debugging(self):
if self.closed:
raise RuntimeError('debug client closed')
if self._adapter is None:
raise RuntimeError('debugger not running')
if self._session is not None:
self._detach()
try:
self._adapter.close()
except ClosedError:
pass
self._adapter = None
def attach_pid(self, pid, **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:
raise RuntimeError('already attached')
raise NotImplementedError
def attach_socket(self, addr=None, adapter=None, **kwargs):
if self.closed:
raise RuntimeError('debug client closed')
if adapter is None:
adapter = self._adapter
elif self._adapter is not None:
raise RuntimeError('already using managed adapter')
if adapter is None:
raise RuntimeError('debugger not running')
if self._session is not None:
raise RuntimeError('already attached')
if addr is None:
addr = adapter.address
self._attach(addr, **kwargs)
return self._session
def detach(self, adapter=None):
if self.closed:
raise RuntimeError('debug client closed')
if self._session is None:
raise RuntimeError('not attached')
if adapter is None:
adapter = self._adapter
assert 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:
try:
self._session.close()
except ClosedError:
pass
if self._adapter is not None:
try:
self._adapter.close()
except ClosedError:
pass
def _launch(self,
argv,
script=None,
wait_for_connect=None,
detachable=True,
env=None,
cwd=None,
**kwargs):
if script is not None:
def start(*args, **kwargs):
return DebugAdapter.start_wrapper_script(
script, *args, **kwargs)
else:
start = DebugAdapter.start
new_addr = Address.as_server if detachable else Address.as_client
addr = new_addr(None, self._addr.port)
self._adapter = start(argv, addr=addr, env=env, cwd=cwd)
if wait_for_connect:
wait_for_connect()
else:
try:
wait_for_socket_server(addr)
except Exception:
# If we fail to connect, print out the adapter output.
self._adapter.VERBOSE = True
raise
self._attach(addr, **kwargs)
def _attach(self, addr, **kwargs):
if addr is None:
addr = self._addr
assert addr.host == 'localhost'
self._session = self.SESSION.create_client(addr, **kwargs)
def _detach(self):
session = self._session
if session is None:
return
self._session = None
try:
session.close()
except ClosedError:
pass
class DebugClient(_LifecycleClient):
"""A high-level abstraction of a debug client (i.e. editor)."""
# TODO: Manage breakpoints, etc.
# TODO: Add debugger methods here (e.g. "pause").
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,
env=None,
cwd=None,
**kwargs): # noqa
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._addr.port)
self._run_server_ex = None
def run():
try:
self._session = self.SESSION.create_server(addr, **kwargs)
except Exception:
self._run_server_ex = traceback.format_exc()
t = new_hidden_thread(
target=run,
name='test.client',
)
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:
message = 'unable to connect after {} secs'.format( # noqa
self._connecttimeout)
if self._run_server_ex is None:
raise Exception(message)
else:
message = message + os.linesep + self._run_server_ex # noqa
raise Exception(message)
# The adapter will close when the connection does.
self._launch(
argv,
script=script,
wait_for_connect=wait,
detachable=False,
env=env,
cwd=cwd)
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)
if kwargs.pop('nodebug', False):
argv.insert(0, '--nodebug')
if kwargs.pop('wait', True):
argv.insert(0, '--wait')
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)
if kwargs.pop('nodebug', False):
argv.insert(0, '--nodebug')
self._launch(argv, **kwargs)
return self._adapter, self._session