debugpy/tests/system_tests/test_main.py
Eric Snow 0d7e0d2656
Wait-on-exit before sending "disconnect" response. (#475)
(fixes gh-459)

We must wait-for-user before the "disconnect" response is sent. This PR fixes that.
2018-06-14 15:50:50 -06:00

524 lines
18 KiB
Python

import os
from textwrap import dedent
import unittest
import ptvsd
from ptvsd.socket import Address
from ptvsd.wrapper import INITIALIZE_RESPONSE # noqa
from tests.helpers.debugadapter import DebugAdapter
from tests.helpers.debugclient import EasyDebugClient as DebugClient
from tests.helpers.threading import get_locked_and_waiter
from tests.helpers.vsc import parse_message, VSCMessages
from tests.helpers.workspace import Workspace, PathEntry
def _strip_pydevd_output(out):
# TODO: Leave relevant lines from before the marker?
pre, sep, out = out.partition(
'pydev debugger: starting' + os.linesep + os.linesep)
return out if sep else pre
def lifecycle_handshake(session, command='launch', options=None):
with session.wait_for_event('initialized'):
req_initialize = session.send_request(
'initialize',
adapterID='spam',
)
req_command = session.send_request(command, **options or {})
# TODO: pre-set breakpoints
req_done = session.send_request('configurationDone')
return req_initialize, req_command, req_done
class TestsBase(object):
@property
def workspace(self):
try:
return self._workspace
except AttributeError:
self._workspace = Workspace()
self.addCleanup(self._workspace.cleanup)
return self._workspace
@property
def pathentry(self):
try:
return self._pathentry
except AttributeError:
self._pathentry = PathEntry()
self.addCleanup(self._pathentry.cleanup)
self._pathentry.install()
return self._pathentry
def write_script(self, name, content):
return self.workspace.write_python_script(name, content=content)
def write_debugger_script(self, filename, port, run_as):
cwd = os.getcwd()
kwargs = {
'filename': filename,
'port_num': port,
'debug_id': None,
'debug_options': None,
'run_as': run_as,
}
return self.write_script('debugger.py', """
import sys
sys.path.insert(0, {!r})
from ptvsd.debugger import debug
debug(
{filename!r},
{port_num!r},
{debug_id!r},
{debug_options!r},
{run_as!r},
)
""".format(cwd, **kwargs))
class CLITests(TestsBase, unittest.TestCase):
def test_script_args(self):
lockfile = self.workspace.lockfile()
donescript, lockwait = lockfile.wait_for_script()
filename = self.pathentry.write_module('spam', """
import sys
print(sys.argv)
sys.stdout.flush()
{}
import time
time.sleep(10000)
""".format(donescript.replace('\n', '\n ')))
with DebugClient() as editor:
adapter, session = editor.launch_script(
filename,
'--eggs',
)
lifecycle_handshake(session, 'launch')
lockwait(timeout=2.0)
out = adapter.output
self.assertIn(u"[{!r}, '--eggs']".format(filename),
out.decode('utf-8').strip().splitlines())
def test_run_to_completion(self):
filename = self.pathentry.write_module('spam', """
import sys
print('done')
sys.stdout.flush()
""")
with DebugClient() as editor:
adapter, session = editor.launch_script(
filename,
)
lifecycle_handshake(session, 'launch')
adapter.wait()
out = adapter.output.decode('utf-8')
rc = adapter.exitcode
self.assertIn('done', out.splitlines())
self.assertEqual(rc, 0)
def test_failure(self):
filename = self.pathentry.write_module('spam', """
import sys
sys.exit(42)
""")
with DebugClient() as editor:
adapter, session = editor.launch_script(
filename,
)
lifecycle_handshake(session, 'launch')
adapter.wait()
rc = adapter.exitcode
self.assertEqual(rc, 42)
class DebugTests(TestsBase, unittest.TestCase):
def test_script(self):
argv = []
filename = self.write_script('spam.py', """
import sys
print('done')
sys.stdout.flush()
""")
script = self.write_debugger_script(filename, 9876, run_as='script')
with DebugClient(port=9876) as editor:
adapter, session = editor.host_local_debugger(argv, script)
lifecycle_handshake(session, 'launch')
adapter.wait()
out = adapter.output.decode('utf-8')
rc = adapter.exitcode
self.assertIn('done', out.splitlines())
self.assertEqual(rc, 0)
# python -m ptvsd --server --port 1234 --file one.py
class LifecycleTests(TestsBase, unittest.TestCase):
@property
def messages(self):
try:
return self._messages
except AttributeError:
self._messages = VSCMessages()
return self._messages
def new_response(self, *args, **kwargs):
return self.messages.new_response(*args, **kwargs)
def new_event(self, *args, **kwargs):
return self.messages.new_event(*args, **kwargs)
def _wait_for_started(self):
lock, wait = get_locked_and_waiter()
# TODO: There's a race with the initial "output" event.
def handle_msg(msg):
if msg.type != 'event':
return False
if msg.event != 'output':
return False
lock.release()
return True
handlers = [
(handle_msg, "event 'output'"),
]
return handlers, (lambda: wait(reason="event 'output'"))
def assert_received(self, received, expected):
from tests.helpers.message import assert_messages_equal
received = [parse_message(msg) for msg in received]
expected = [parse_message(msg) for msg in expected]
assert_messages_equal(received, expected)
def new_version_event(self, received):
version = ptvsd.__version__
if received[0].body['data']['version'] != version:
version = '0+unknown'
return self.new_event(
'output',
category='telemetry',
output='ptvsd',
data={'version': version},
)
def test_pre_init(self):
filename = self.pathentry.write_module('spam', '')
handlers, wait_for_started = self._wait_for_started()
with DebugClient() as editor:
adapter, session = editor.launch_script(
filename,
handlers=handlers,
timeout=3.0,
)
wait_for_started()
out = adapter.output.decode('utf-8')
self.assert_received(session.received, [
self.new_version_event(session.received),
])
out = _strip_pydevd_output(out)
self.assertEqual(out, '')
def test_launch_ptvsd_client(self):
argv = []
lockfile = self.workspace.lockfile()
done, waitscript = lockfile.wait_in_script()
filename = self.write_script('spam.py', waitscript)
script = self.write_debugger_script(filename, 9876, run_as='script')
with DebugClient(port=9876) as editor:
adapter, session = editor.host_local_debugger(
argv,
script,
)
with session.wait_for_event('exited'):
with session.wait_for_event('thread'):
(req_initialize, req_launch, req_config
) = lifecycle_handshake(session, 'launch')
done()
adapter.wait()
# Skipping the 'thread exited' and 'terminated' messages which
# may appear randomly in the received list.
self.assert_received(session.received[:7], [
self.new_version_event(session.received),
self.new_response(req_initialize, **INITIALIZE_RESPONSE),
self.new_event('initialized'),
self.new_response(req_launch),
self.new_response(req_config),
self.new_event('process', **{
'isLocalProcess': True,
'systemProcessId': adapter.pid,
'startMethod': 'launch',
'name': filename,
}),
self.new_event('thread', reason='started', threadId=1),
])
def test_launch_ptvsd_server(self):
lockfile = self.workspace.lockfile()
done, waitscript = lockfile.wait_in_script()
filename = self.write_script('spam.py', waitscript)
with DebugClient() as editor:
adapter, session = editor.launch_script(
filename,
timeout=3.0,
)
with session.wait_for_event('thread'):
(req_initialize, req_launch, req_config
) = lifecycle_handshake(session, 'launch')
done()
adapter.wait()
self.maxDiff = None
self.assert_received(session.received[:7], [
self.new_version_event(session.received),
self.new_response(req_initialize, **INITIALIZE_RESPONSE),
self.new_event('initialized'),
self.new_response(req_launch),
self.new_response(req_config),
self.new_event('process', **{
'isLocalProcess': True,
'systemProcessId': adapter.pid,
'startMethod': 'launch',
'name': filename,
}),
self.new_event('thread', reason='started', threadId=1),
#self.new_event('thread', reason='exited', threadId=1),
#self.new_event('exited', exitCode=0),
#self.new_event('terminated'),
])
def test_attach_started_separately(self):
lockfile = self.workspace.lockfile()
done, waitscript = lockfile.wait_in_script()
filename = self.write_script('spam.py', waitscript)
addr = Address('localhost', 8888)
with DebugAdapter.start_for_attach(addr, filename) as adapter:
with DebugClient() as editor:
session = editor.attach_socket(addr, adapter)
with session.wait_for_event('thread'):
(req_initialize, req_launch, req_config
) = lifecycle_handshake(session, 'attach')
done()
adapter.wait()
self.assert_received(session.received[:7], [
self.new_version_event(session.received),
self.new_response(req_initialize, **INITIALIZE_RESPONSE),
self.new_event('initialized'),
self.new_response(req_launch),
self.new_response(req_config),
self.new_event('process', **{
'isLocalProcess': True,
'systemProcessId': adapter.pid,
'startMethod': 'attach',
'name': filename,
}),
self.new_event('thread', reason='started', threadId=1),
#self.new_event('thread', reason='exited', threadId=1),
#self.new_event('exited', exitCode=0),
#self.new_event('terminated'),
])
def test_attach_embedded(self):
lockfile = self.workspace.lockfile()
done, waitscript = lockfile.wait_in_script()
addr = Address('localhost', 8888)
script = dedent("""
from __future__ import print_function
import sys
sys.path.insert(0, {!r})
import ptvsd
ptvsd.enable_attach({}, redirect_output={})
%s
print('success!', end='')
""").format(os.getcwd(), tuple(addr), True)
filename = self.write_script('spam.py', script % waitscript)
with DebugAdapter.start_embedded(addr, filename) as adapter:
with DebugClient() as editor:
session = editor.attach_socket(addr, adapter)
(req_initialize, req_launch, req_config
) = lifecycle_handshake(session, 'attach')
done()
adapter.wait()
out = adapter.output.decode('utf-8')
self.assert_received(session.received, [
self.new_version_event(session.received),
self.new_response(req_initialize, **INITIALIZE_RESPONSE),
self.new_event('initialized'),
self.new_response(req_launch),
self.new_response(req_config),
self.new_event('process', **{
'isLocalProcess': True,
'systemProcessId': adapter.pid,
'startMethod': 'attach',
'name': filename,
}),
self.new_event('output', output='success!', category='stdout'),
self.new_event('exited', exitCode=0),
self.new_event('terminated'),
])
self.assertIn('success!', out)
def test_reattach(self):
lockfile1 = self.workspace.lockfile()
done1, waitscript1 = lockfile1.wait_in_script(timeout=5)
lockfile2 = self.workspace.lockfile()
done2, waitscript2 = lockfile2.wait_in_script(timeout=5)
filename = self.write_script('spam.py', waitscript1 + waitscript2)
addr = Address('localhost', 8888)
with DebugAdapter.start_for_attach(addr, filename) as adapter:
with DebugClient() as editor:
# Attach initially.
session1 = editor.attach_socket(addr, adapter)
with session1.wait_for_event('thread'):
reqs = lifecycle_handshake(session1, 'attach')
done1()
req_disconnect = session1.send_request('disconnect')
editor.detach(adapter)
# Re-attach
session2 = editor.attach_socket(addr, adapter)
(req_initialize, req_launch, req_config
) = lifecycle_handshake(session2, 'attach')
done2()
adapter.wait()
self.assert_received(session1.received, [
self.new_version_event(session1.received),
self.new_response(reqs[0], **INITIALIZE_RESPONSE),
self.new_event('initialized'),
self.new_response(reqs[1]),
self.new_response(reqs[2]),
self.new_event('process', **{
'isLocalProcess': True,
'systemProcessId': adapter.pid,
'startMethod': 'attach',
'name': filename,
}),
self.new_event('thread', reason='started', threadId=1),
self.new_response(req_disconnect),
])
self.messages.reset_all()
self.assert_received(session2.received, [
self.new_version_event(session2.received),
self.new_response(req_initialize, **INITIALIZE_RESPONSE),
self.new_event('initialized'),
self.new_response(req_launch),
self.new_response(req_config),
self.new_event('process', **{
'isLocalProcess': True,
'systemProcessId': adapter.pid,
'startMethod': 'attach',
'name': filename,
}),
self.new_event('exited', exitCode=0),
self.new_event('terminated'),
])
@unittest.skip('not implemented')
def test_attach_exit_during_session(self):
# TODO: Ensure we see the "terminated" and "exited" events.
raise NotImplementedError
@unittest.skip('re-attach needs fixing')
def test_attach_unknown(self):
lockfile = self.workspace.lockfile()
done, waitscript = lockfile.wait_in_script()
filename = self.write_script('spam.py', waitscript)
with DebugClient() as editor:
# Launch and detach.
# TODO: This is not an ideal way to spin up a process
# to which we can attach. However, ptvsd has no such
# capabilitity at present and attaching without ptvsd
# running isn't an option currently.
adapter, session = editor.launch_script(
filename,
)
lifecycle_handshake(session, 'launch')
editor.detach()
# Re-attach.
session = editor.attach()
(req_initialize, req_launch, req_config
) = lifecycle_handshake(session, 'attach')
done()
adapter.wait()
self.assert_received(session.received, [
self.new_version_event(session.received),
self.new_response(req_initialize, **INITIALIZE_RESPONSE),
self.new_event('initialized'),
self.new_response(req_launch),
self.new_response(req_config),
self.new_event('exited', exitCode=0),
self.new_event('terminated'),
])
def test_nodebug(self):
lockfile = self.workspace.lockfile()
done, waitscript = lockfile.wait_in_script()
filename = self.write_script('spam.py', dedent("""
print('+ before')
{}
print('+ after')
""").format(waitscript))
with DebugClient(port=9876) as editor:
adapter, session = editor.host_local_debugger(
argv=[
'--nodebug',
filename,
],
)
(req_initialize, req_launch, req_config
) = lifecycle_handshake(session, 'launch')
done()
adapter.wait()
self.assert_received(session.received[:11], [
self.new_version_event(session.received),
self.new_response(req_initialize, **INITIALIZE_RESPONSE),
self.new_event('initialized'),
self.new_response(req_launch),
self.new_event('output',
output='+ before',
category='stdout'),
self.new_event('output',
output='\n',
category='stdout'),
self.new_response(req_config),
self.new_event('output',
output='+ after',
category='stdout'),
self.new_event('output',
output='\n',
category='stdout'),
self.new_event('exited', exitCode=0),
self.new_event('terminated'),
])